FreeRTOS 第三课:C 语言扩展 学习笔记
FreeRTOS 第三课:C 语言扩展 学习笔记
嵌入式学习笔记 · FreeRTOS 第 3 课
今天学习了 C 语言扩展:变量的作用域和生命周期、变量在 SRAM 里的分布、堆和栈的概念、FreeRTOS 的任务栈,以及 FreeRTOS 的代码规范,整理成文方便复习。
文章目录
- FreeRTOS 第三课:C 语言扩展 学习笔记
- 一、变量的作用域和生命周期
- 1. C 语言的 5 类变量
- 2. 完整代码例子
- 二、变量在 SRAM 里的分布
- 1. 单片机的 FLASH 和 SRAM
- 2. 变量在 SRAM 的分布
- 3. Minimum Heap Size 和 Minimum Stack Size
- 三、堆的概念
- 四、栈
- 1. 栈的概念
- 2. 调用栈(Call Stack)
- 3. FreeRTOS 的任务栈
- 五、FreeRTOS 的代码规范
- 1. 三个常用数据类型
- 2. 变量的命名规则:前缀 + 实际名字
- 3. 函数的命名规则:返回类型 + 模块名 + 实际名字
- 4. 宏定义的命名规则
- 六、小结速查表
一、变量的作用域和生命周期
写 C 语言/嵌入式代码,经常要回答两个问题:
- 这个变量在哪里能用?—— 这就是作用域(Scope)
- 这个变量什么时候创建、什么时候销毁?—— 这就是生命周期(Lifetime)
| 概念 | 回答的问题 | 通俗说法 |
|---|---|---|
| 作用域 | 变量在哪些代码里可见 | “这个变量管到哪一行” |
| 生命周期 | 变量从生到死的过程 | “这个变量活多久” |
记忆:作用域是"空间范围",生命周期是"时间范围"。
1. C 语言的 5 类变量
| 变量类型 | 生命周期 | 作用域 | 典型用途 |
|---|---|---|---|
| 全局变量 | 程序开始 → 程序结束 | 项目当中的所有文件 | 需要跨文件共享的场合 |
| 局部变量 | 进入函数/代码块 → 退出函数/代码块 | 定义它的函数/代码块 | 函数内部存储临时数据 |
| 静态全局变量 | 程序开始 → 程序结束 | 定义它的 .c 文件 | 封装模块的内部数据 |
| 静态局部变量 | 首次进入函数 → 程序结束 | 定义它的函数/代码块 | 函数内需要保持的数据 |
| 堆变量 | 从 malloc() → 到 free() | 无固定作用域(靠指针访问) | 长度不定的数据 |
注意:堆变量没有固定作用域——它在堆上分配后,靠指针在任意代码中访问,指针传到哪就能在哪用。
2. 完整代码例子
下面这个例子把 5 类变量全部用上:用户输入数字 n,程序分配一个 n 个元素的 int 数组,依次计算 1²、2²、3²……填入数组并打印。
文件 1:my_file.h
intGetNextSquare();/* 获取下一个平方数 */文件 2:my_file.c
/* 静态全局变量,仅展示用 */staticintstaticGlobalVar1=100;/* @作用: 输入一个数, 计算它的平方 */staticintSquare(intinput)/* input:函数参数 */{returninput*input;}/* @作用: 获取下一个平方数 */intGetNextSquare(){/* 静态局部变量,存储当前底数的值 */staticintnum=1;/* 计算 num 的平方 */intret;ret=Square(num);/* 递增底数的值 */num++;/* 返回结果 */returnret;}文件 3:main.c
#include<stdio.h>#include<stdlib.h>#include"my_file.h"intglobalVar1=36;/* 全局变量1, 仅展示用 */intglobalVar2[100];/* 全局变量2, 仅展示用 */intmain(intargc,char*argv[]){intn;/* 局部变量 *//* #1. 要求用户输入数字n, n表示要产生的平方数的个数 */scanf("%d",&n);/* #2. 使用malloc()分配一个长度为n的int型数组 */int*pBuffer=(int*)malloc(sizeof(int)*n);/* 堆变量 *//* #3. 逐个计算平方数, 并填写到数组中 */for(inti=0;i<n;i++){pBuffer[i]=GetNextSquare();}/* #4. 打印结果到控制台 */for(inti=0;i<n;i++){printf("%d, ",pBuffer[i]);}printf("\n");return0;}运行结果(输入5):
5 1, 4, 9, 16, 25,- GetNextSquare() 每调用一次,静态局部变量 num 自增 1,返回 num²;
- 如果 num 是普通局部变量,每次调用都会从 1 重新开始,结果会变成 5 个 1。
代码里各变量对照:
| 代码位置 | 变量 | 类型 |
|---|---|---|
| main.c 顶部 | globalVar1 / globalVar2 | 全局变量 |
| main() 内 | n | 局部变量 |
| main() 内 | pBuffer | 堆变量 |
| my_file.c 顶部 | staticGlobalVar1 | 静态全局变量 |
| Square() 参数 | input | 函数参数(类似局部变量) |
| GetNextSquare() 内 | num | 静态局部变量 |
| GetNextSquare() 内 | ret | 局部变量 |
二、变量在 SRAM 里的分布
1. 单片机的 FLASH 和 SRAM
| 对比项 | FLASH(Flash 存储器) | SRAM(静态随机存储器) |
|---|---|---|
| 存什么 | 程序代码、常量(const) | 变量(全局/局部/堆栈) |
| 掉电后 | 数据不丢失 | 数据丢失 |
| 特点 | 容量大、慢、只读(运行中) | 容量小、快、可读可写 |
| 地址 | 0x08000000 起 | 0x20000000 起 |
记忆:程序住 FLASH,变量住 SRAM。
2. 变量在 SRAM 的分布
SRAM 从上到下分为几段,编译器把不同类型的变量放到不同段里:
┌───────────────────────────────┐ 高地址 │ .stack 段 │ 栈:局部变量、函数参数等 │ (向下增长) │ (不包括静态局部变量) ├───────────────────────────────┤ │ 未分配区域 │ heap 和 stack 之间可扩展的空间 ├───────────────────────────────┤ │ .heap 段 │ 堆:malloc() 分配的内存 │ (向上增长) │ ├───────────────────────────────┤ │ .bss 段 │ 无初值或零初值的全局变量/静态变量 ├───────────────────────────────┤ │ .data 段 │ 有初值的全局变量/静态变量 └───────────────────────────────┘ 低地址| 段 | 存放内容 | 说明 |
|---|---|---|
| .data 段 | 有初值的全局变量或静态变量 | 程序启动时从 FLASH 拷贝初值到 SRAM |
| .bss 段 | 无初值或者零初值的全局变量或静态变量 | 启动时清零 |
| .heap 段 | 堆:malloc() 分配的内存 | 从低地址向上增长 |
| .stack 段 | 栈:局部变量、参数等(不包括静态局部变量) | 从高地址向下增长 |
| 未分配区域 | heap 与 stack 之间的剩余空间 | 两边按需"抢地盘" |
要点:静态局部变量不放在栈里,它和全局变量一样放在 .data/.bss 段,所以才能"跨调用保持值"。
3. Minimum Heap Size 和 Minimum Stack Size
在 Keil 工程里(Options → Target),有两个和 SRAM 分配相关的设置:
| 配置 | 作用 | 对应段 |
|---|---|---|
| Minimum Heap Size | 给 C 库 malloc/free 预留的最小堆大小 | .heap 段 |
| Minimum Stack Size | 给启动代码预留的最小栈大小 | .stack 段 |
- 它们写在启动文件(startup_xxx.s)里:
Heap_Size EQU 0x00000200 Stack_Size EQU 0x00000400- 这是裸机/启动阶段的堆栈大小;FreeRTOS 起来之后,任务栈由 FreeRTOS 自己从 heap_x 分配,互不干扰。
三、堆的概念
**堆(Heap)**是程序运行期间动态分配内存的区域:
| 特点 | 说明 |
|---|---|
| 分配方式 | malloc() / calloc() 动态申请 |
| 释放方式 | free() 手动释放 |
| 大小 | 运行时才决定(长度不定的数据) |
| 管理 | 由 C 库/内存管理器维护 |
| 风险 | 不 free 就内存泄漏;多次分配释放会产生碎片 |
int*p=(int*)malloc(sizeof(int)*100);/* 申请 */if(p!=NULL){/* 使用 */}free(p);/* 释放,必须成对 */p=NULL;在 FreeRTOS 里,任务栈、队列等内核对象的内存由 FreeRTOS 自己的堆(heap_1 ~ heap_5)管理,和 C 库的 malloc 是两套体系(第二课讲过)。
四、栈
1. 栈的概念
**栈(Stack)**是一块"后进先出(LIFO)"的内存区域:
- 压栈(Push):数据从栈顶压入;
- 出栈(Pop):数据从栈顶弹出;
- 栈顶指针 SP 由 CPU 自动维护;
- 在 ARM 上栈从高地址向低地址增长。
voidfuncA(void){inta=1;funcB();}voidfuncB(void){intb=2;}调用过程:
funcA 开始 → 压入 a=1 funcA 调用 funcB → 压入返回地址 funcB 开始 → 压入 b=2 funcB 返回 → 弹出 b、返回地址 funcA 返回 → 弹出 a2. 调用栈(Call Stack)
函数调用时,栈上依次保存:
| 内容 | 说明 |
|---|---|
| 返回地址 | 函数结束后回到哪里 |
| 函数参数 | 传入的实参(或寄存器保存) |
| 局部变量 | 函数内部定义的变量 |
| 保存的寄存器 | 调用前需要保留的 CPU 寄存器 |
- 每调用一层函数,栈就"长高"一层(往下压);
- 递归太深或局部变量太大 →栈溢出(Stack Overflow)。
3. FreeRTOS 的任务栈
每个 FreeRTOS 任务都有一块独立的任务栈:
| 对比项 | 裸机"主栈" | FreeRTOS 任务栈 |
|---|---|---|
| 数量 | 只有 1 个 | 每个任务 1 个 |
| 分配方式 | 启动文件预留 | xTaskCreate 时从堆分配 |
| 大小 | Minimum Stack Size | xTaskCreate 的 uxStackDepth 参数(单位:字) |
| 内容 | 主函数调用链 | 任务的局部变量 + 任务切换时的上下文(寄存器) |
/* uxStackDepth = 128,表示 128 字 = 512 字节的任务栈 */xTaskCreate(Task_LED,"LED",128,NULL,1,NULL);- 任务切换时,FreeRTOS 把当前任务的寄存器压入它自己的栈,再从下一个任务的栈里恢复,互不干扰;
- 任务栈太小 → 栈溢出 → 程序跑飞;可以开
configCHECK_FOR_STACK_OVERFLOW检测。
记忆:裸机只有一个大栈,RTOS 每个任务一个小栈。
五、FreeRTOS 的代码规范
FreeRTOS 源码有一套统一的命名规范,看懂前缀就能猜出类型和作用。
1. 三个常用数据类型
| 类型 | 作用 | 说明 |
|---|---|---|
| TickType_t | 记录系统节拍的数据类型 | 具体类型取决于 configTICK_TYPE_WIDTH_IN_BITS(16/32 位) |
| BaseType_t | 基本数据类型,表示成功/失败、是/否 | 当前处理器架构下最有效率的类型(STM32 = int32_t) |
| UBaseType_t | BaseType_t 的无符号版本,非负数 | 对 STM32F1 单片机为 uint32_t |
TickType_t xLastWakeTime;/* tick 计数 */BaseType_t xReturn;/* pdPASS / pdFAIL */UBaseType_t uxPriority;/* 优先级,非负数 */2. 变量的命名规则:前缀 + 实际名字
| 前缀 | 类型 |
|---|---|
| v | void |
| c | char(int8_t) |
| s | short(int16_t) |
| l | long(int32_t) |
| x | BaseType_t、TickType_t,或其它未知数据类型 |
| e | enum(枚举类型) |
| p | 指针 |
| u | unsigned(无符号数) |
例子:
TickType_t xTicksToWait;/* x = TickType_t */UBaseType_t uxPriority;/* u + x = unsigned BaseType_t */charcChar;/* c = char */void*pvParameters;/* p + v = void 指针 */3. 函数的命名规则:返回类型 + 模块名 + 实际名字
函数名 =返回类型前缀 + 所属模块名 + 动作名:
| 函数 | 拆解 | 含义 |
|---|---|---|
| vTaskDelay | v(void)+ Task(模块)+ Delay(动作) | 任务延时,无返回值 |
| xTaskCreate | x(BaseType_t)+ Task + Create | 创建任务,返回成功/失败 |
| xQueueSend | x + Queue + Send | 发送到队列,返回结果 |
| vSemaphoreGive | v + Semaphore + Give | 释放信号量 |
补充:以prv开头的函数(如 prvIdleTask)表示模块内部私有函数(static)。
4. 宏定义的命名规则
宏定义全部大写 + 下划线分隔:
| 分类 | 例子 |
|---|---|
| 配置宏 | configUSE_PREEMPTION、configTICK_RATE_HZ |
| 返回值宏 | pdTRUE、pdFALSE、pdPASS、pdFAIL |
| 错误宏 | errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY |
| 移植宏 | portMAX_DELAY、portYIELD、portTICK_PERIOD_MS |
#defineconfigUSE_PREEMPTION1#definepdTRUE(1)#defineportMAX_DELAY(0xFFFFFFFFUL)六、小结速查表
| 问题 | 答案 |
|---|---|
| 作用域 | 变量"在哪能用"(空间范围) |
| 生命周期 | 变量"活多久"(时间范围) |
| 5 类变量 | 全局、局部、静态全局、静态局部、堆变量 |
| 堆变量作用域 | 无固定作用域,靠指针访问 |
| 程序代码/常量放哪 | FLASH |
| 变量放哪 | SRAM |
| .data 段 | 有初值的全局/静态变量 |
| .bss 段 | 无初值/零初值的全局/静态变量 |
| 堆 | malloc/free,向上增长 |
| 栈 | 局部变量/参数,向下增长,LIFO |
| FreeRTOS 任务栈 | 每任务一个,xTaskCreate 时从堆分配 |
| TickType_t | 记录系统节拍(16/32 位) |
| BaseType_t / UBaseType_t | 有符号/无符号基本类型(STM32 为 int32_t/uint32_t) |
| 变量命名 | 类型前缀 + 名字(v/c/s/l/x/e/p/u) |
| 函数命名 | 返回类型前缀 + 模块名 + 动作名 |
| 宏命名 | 全大写 + 下划线 |
记忆口诀:
- 作用域管空间,生命周期管时间。
- 程序住 FLASH,变量住 SRAM;.data 存有值、.bss 存清零、heap 向上、stack 向下。
- 堆要手动 free,栈自动生长;RTOS 里每任务一个栈。
- x 是 BaseType_t,u 是无符号,p 是指针,v 是 void;函数名=类型前缀+模块+动作。
本文为 FreeRTOS 第三课学习笔记,如有错误欢迎评论区指正。