FreeRTOS 第三课:C 语言扩展 学习笔记

发布时间:2026/11/5 3:40:05
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 返回 → 弹出 a

2. 调用栈(Call Stack)

函数调用时,栈上依次保存:

内容说明
返回地址函数结束后回到哪里
函数参数传入的实参(或寄存器保存)
局部变量函数内部定义的变量
保存的寄存器调用前需要保留的 CPU 寄存器
  • 每调用一层函数,栈就"长高"一层(往下压);
  • 递归太深或局部变量太大 →栈溢出(Stack Overflow)。

3. FreeRTOS 的任务栈

每个 FreeRTOS 任务都有一块独立的任务栈:

对比项裸机"主栈"FreeRTOS 任务栈
数量只有 1 个每个任务 1 个
分配方式启动文件预留xTaskCreate 时从堆分配
大小Minimum Stack SizexTaskCreate 的 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_tBaseType_t 的无符号版本,非负数对 STM32F1 单片机为 uint32_t
TickType_t xLastWakeTime;/* tick 计数 */BaseType_t xReturn;/* pdPASS / pdFAIL */UBaseType_t uxPriority;/* 优先级,非负数 */

2. 变量的命名规则:前缀 + 实际名字

前缀类型
vvoid
cchar(int8_t)
sshort(int16_t)
llong(int32_t)
xBaseType_t、TickType_t,或其它未知数据类型
eenum(枚举类型)
p指针
uunsigned(无符号数)

例子:

TickType_t xTicksToWait;/* x = TickType_t */UBaseType_t uxPriority;/* u + x = unsigned BaseType_t */charcChar;/* c = char */void*pvParameters;/* p + v = void 指针 */

3. 函数的命名规则:返回类型 + 模块名 + 实际名字

函数名 =返回类型前缀 + 所属模块名 + 动作名:

函数拆解含义
vTaskDelayv(void)+ Task(模块)+ Delay(动作)任务延时,无返回值
xTaskCreatex(BaseType_t)+ Task + Create创建任务,返回成功/失败
xQueueSendx + Queue + Send发送到队列,返回结果
vSemaphoreGivev + 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)
函数命名返回类型前缀 + 模块名 + 动作名
宏命名全大写 + 下划线

记忆口诀:

  1. 作用域管空间,生命周期管时间。
  2. 程序住 FLASH,变量住 SRAM;.data 存有值、.bss 存清零、heap 向上、stack 向下。
  3. 堆要手动 free,栈自动生长;RTOS 里每任务一个栈。
  4. x 是 BaseType_t,u 是无符号,p 是指针,v 是 void;函数名=类型前缀+模块+动作。

本文为 FreeRTOS 第三课学习笔记,如有错误欢迎评论区指正。