RT-Thread 嵌入式开发学习日志
本笔记记录了从 STM32 裸机开发到 RT-Thread RTOS 的完整学习过程,涵盖外设驱动、内存管理、线程调度、IPC 通信等核心知识点。
一、内存管理——理解一切的基础
在正式学习 RTOS 之前,必须先搞清楚程序运行时内存是怎么分布的。这不是可选项,而是必修课。
很多人学 RTOS 时遇到奇怪的崩溃、数据错乱、野指针,追根溯源几乎都是内存问题。只有真正理解了内存的布局和生命周期,才能看懂线程栈是怎么分配的、邮箱为什么不能传局部变量、全局变量和堆变量有什么本质区别。
1.1 STM32 的存储器结构
STM32 有两类存储器,职责完全不同:
| 存储器 | 类型 | 用途 | 掉电 |
|---|---|---|---|
| Flash | 非易失性 | 存放程序代码、常量、初始值 | 保留 |
| RAM | 易失性 | 程序运行时的工作内存 | 丢失 |
Flash 是只读的运行载体,RAM 才是程序真正"活动"的地方。理解内存管理,本质上就是理解 RAM 是怎么被划分和使用的。
1.2 RAM 的完整划分
一个 C 程序运行时,RAM 被划分为以下几个区域:
RAM 地址空间(以 STM32F4 为例,RAM 起始地址 0x20000000)
┌──────────────────────┐ 0x20000000(低地址)
│ .data 段 │ 已初始化的全局/静态变量
│ int a = 10; │ (初始值从 Flash 复制过来)
├──────────────────────┤
│ .bss 段 │ 未初始化的全局/静态变量
│ int b; │ (启动时由启动文件自动清零)
├──────────────────────┤
│ │
│ 堆 (Heap) │ ↑ 向高地址增长
│ malloc/rt_malloc │ 动态分配,需手动释放
│ │
│ ~~~~~ │ (空闲区域,堆和栈在此相向增长)
│ │
│ 栈 (Stack) │ ↓ 向低地址增长
│ 局部变量、返回地址 │ 函数调用时自动压栈/出栈
│ │
└──────────────────────┘ 0x20000000 + RAM_SIZE(高地址)
各区域详解:
.data 段
- 存放所有已赋初值的全局变量和静态变量
- 初始值保存在 Flash 里,芯片上电后由启动文件(
startup_stm32xxx.s)自动从 Flash 复制到 RAM - 例:
int count = 100;(全局)→ 放在.data段
.bss 段
- 存放所有未赋初值的全局变量和静态变量
- 不占用 Flash 空间(只记录大小),启动时由启动文件自动清零
- 例:
int count;(全局,未赋值)→ 放在.bss段,启动后值为 0
堆(Heap)
- 程序运行时通过
malloc/rt_malloc动态申请的内存 - 从低地址向高地址增长
- 必须手动释放,否则造成内存泄漏
- 裸机中堆的大小由链接脚本(
.ld文件)中的_Min_Heap_Size决定
栈(Stack)
- 函数调用时自动分配,存放局部变量、函数参数、返回地址、寄存器现场
- 从高地址向低地址增长
- 函数返回时自动回收,无需手动管理
- 裸机中栈的大小由链接脚本中的
_Min_Stack_Size决定(通常 512B ~ 4KB)
1.3 裸机 vs RTOS:内存管理的本质区别
这是引入 RTOS 后最需要重新理解的部分。
裸机的内存模型
裸机程序只有一个执行流(main + 中断),内存结构非常简单:
裸机 RAM 布局
┌──────────────┐
│ .data/.bss │ 全局变量(整个程序生命周期)
├──────────────┤
│ 堆 │ malloc(一般裸机很少用)
├──────────────┤
│ 栈 │ 整个程序共用一个栈
│ │ main → func_a → func_b → ...
└──────────────┘
- 只有一个栈:所有函数调用共用这一个栈,深度嵌套调用会导致栈溢出
- 堆几乎不用:裸机开发中
malloc用得很少,担心碎片化和泄漏 - 中断使用同一个栈:中断发生时直接在当前栈上压栈,需要预留足够空间
RTOS 的内存模型
引入 RTOS 后,内存结构发生了根本性变化:
RTOS RAM 布局(以 RT-Thread 为例)
┌──────────────────────────┐
│ .data / .bss │ 全局变量(同裸机)
├──────────────────────────┤
│ │
│ RT-Thread 内存堆 │ 由 RT-Thread 统一管理
│ ┌────────────────────┐ │
│ │ 线程1 的栈空间 │ │ ← rt_thread_create 时从堆里划出
│ ├────────────────────┤ │
│ │ 线程2 的栈空间 │ │ ← 每个线程有自己独立的栈
│ ├────────────────────┤ │
│ │ 线程3 的栈空间 │ │
│ ├────────────────────┤ │
│ │ 线程控制块(TCB) │ │ ← 线程的"身份证"也在堆里
│ ├────────────────────┤ │
│ │ 信号量/互斥锁对象 │ │ ← IPC 对象也从堆里分配
│ ├────────────────────┤ │
│ │ 消息队列缓冲区 │ │
│ └────────────────────┘ │
│ │
└──────────────────────────┘
核心区别对比:
| 维度 | 裸机 | RTOS |
|---|---|---|
| 栈的数量 | 1 个(全局共用) | N 个(每个线程独立一个) |
| 栈的分配方式 | 链接脚本静态分配 | 从 OS 堆动态分配 |
| 堆的管理者 | C 库(malloc) |
RT-Thread(rt_malloc) |
| 内存隔离 | 无隔离,全局共享 | 线程栈相互隔离 |
| 动态对象 | 几乎没有 | 线程、信号量、队列等都是动态对象 |
| 内存风险 | 栈溢出覆盖全局变量 | 线程栈溢出只影响本线程(相对隔离) |
关键理解:RTOS 的"堆中之栈"
这是最容易混淆的概念。在 RTOS 里:
// 创建线程时,RT-Thread 在内部做了这些事:
rt_thread_t tid = rt_thread_create(
"task1", // 线程名
entry, // 入口函数
RT_NULL, // 参数
1024, // ← 这 1024 字节的"栈",是从 RT-Thread 的堆里 rt_malloc 出来的!
10, // 优先级
10 // 时间片
);
- 这 1024 字节在物理上是堆内存(
rt_malloc分配) - 但在逻辑上是这个线程的栈(线程运行时 SP 寄存器指向这里)
- 线程被删除时,这块内存会被
rt_free归还给堆
所以说:RTOS 的线程栈,本质上是堆里划出来的一块区域,被当作栈来使用。
1.4 变量的生命周期与存储位置
理解了内存布局,就能准确判断每种变量的生命周期:
| 变量类型 | 存储位置 | 生命周期 | 示例 |
|---|---|---|---|
| 全局变量(有初值) | .data 段 |
整个程序运行期间 | int g = 1;(文件作用域) |
| 全局变量(无初值) | .bss 段 |
整个程序运行期间 | int g;(文件作用域) |
| 静态局部变量 | .data/.bss 段 |
整个程序运行期间 | static int s = 0;(函数内) |
| 局部变量 | 栈 | 函数执行期间 | int x = 5;(函数内) |
| 动态分配 | 堆 | malloc 到 free 之间 |
int *p = malloc(4); |
静态局部变量的特殊性:
void counter(void) {
static int cnt = 0; // 存在 .data 段,不在栈上!
cnt++; // 每次调用都保留上次的值
rt_kprintf("%d\n", cnt);
}
// 调用3次输出:1, 2, 3
// 如果是普通局部变量,每次都是 0
1.5 裸机 vs RTOS 的堆管理差异
裸机的堆(C 标准库 malloc)
裸机使用 C 标准库的 malloc/free,底层由 newlib 或 microlib 实现:
// 裸机动态分配
uint8_t *buf = (uint8_t *)malloc(64);
if (buf == NULL) { /* 分配失败处理 */ }
// 使用...
free(buf);
裸机堆的问题:
- 不是线程安全的(裸机没有线程,但中断里调用 malloc 会出问题)
- 长时间运行后容易产生内存碎片
- 堆大小固定,由链接脚本决定,超出直接返回 NULL
RTOS 的堆(RT-Thread 内存管理)
RT-Thread 接管了堆的管理,提供多种内存管理算法:
// RT-Thread 动态分配
void *ptr = rt_malloc(64);
if (ptr == RT_NULL) { /* 分配失败处理 */ }
// 使用...
rt_free(ptr);
// 分配并清零(类似 calloc)
void *ptr2 = rt_calloc(4, sizeof(int)); // 分配 4 个 int 并清零
// 重新调整大小(类似 realloc)
ptr = rt_realloc(ptr, 128);
RT-Thread 提供三种内存管理算法(可在 menuconfig 中选择):
| 算法 | 适用场景 | 特点 |
|---|---|---|
| 小内存管理(small mem) | RAM < 2MB 的小型系统 | 简单高效,有碎片 |
| slab 内存管理 | 频繁分配固定大小对象 | 碎片少,速度快 |
| memheap 管理 | 多块不连续内存 | 可管理多个内存区域 |
RT-Thread 堆的优势:
- 线程安全:内部有互斥保护,多线程同时
rt_malloc不会出问题 - 可调试:可通过
free命令查看内存使用情况 - 统一管理:线程栈、IPC 对象、用户数据都从同一个堆里分配
// 在 FinSH 控制台输入 free 命令,可以看到:
msh >free
total memory: 98304 ← 总堆大小
used memory : 12480 ← 已使用
maximum allocated memory: 13056
1.6 内存管理不当引发的典型 Bug
学习过程中亲身踩过的坑,直接说明了理解内存的重要性。
Bug 1:邮箱传递局部变量地址(悬空指针)
// ❌ 错误写法:发送局部变量的地址
void thread1_entry(void *param) {
struct sensor_data data = {25, 60, 3}; // 局部变量,在线程1的栈上
rt_mb_send(mb, (rt_uint32_t)&data); // 发送的是栈地址
// 函数继续执行,data 所在的栈帧随时可能被覆盖!
}
void thread2_entry(void *param) {
struct sensor_data *p;
rt_mb_recv(mb, (rt_uint32_t*)&p, RT_WAITING_FOREVER);
// ❌ 此时 p 指向的内存已经是 thread1 栈上的垃圾数据
rt_kprintf("温度:%d\n", p->temperature); // 读到随机值,甚至崩溃
}
根本原因:局部变量在线程的栈上,线程函数继续执行后栈帧会被后续调用覆盖,地址虽然还在,但内容已经不可信。
// ✅ 正确写法1:使用全局变量(生命周期贯穿整个程序)
static struct sensor_data global_data;
global_data.temperature = 25;
rt_mb_send(mb, (rt_uint32_t)&global_data);
// ✅ 正确写法2:使用消息队列(内部 memcpy,数据安全)
rt_mq_send(mq, &data, sizeof(data)); // 数据被拷贝进队列,局部变量也安全
Bug 2:邮箱接收类型错误(栈破坏)
// ❌ 错误写法:用 char 接收邮箱数据
char received; // char 只有 1 字节!
rt_mb_recv(mb, (rt_uint32_t*)&received, RT_WAITING_FOREVER);
// 邮箱固定传 4 字节,写入 char 变量会越界覆盖栈上相邻的数据!
// ✅ 正确写法:必须用 rt_uint32_t 接收
rt_uint32_t received;
rt_mb_recv(mb, &received, RT_WAITING_FOREVER);
根本原因:邮箱的每封"信"固定是 4 字节(rt_uint32_t),如果接收变量不够大,rt_mb_recv 写入时会越界,破坏栈上相邻的数据,导致难以排查的随机崩溃。
Bug 3:线程栈溢出
// ❌ 栈空间给太小,函数调用层次深时溢出
rt_thread_create("task", entry, RT_NULL, 256, 10, 10);
// ^^^ 256 字节往往不够
// entry 函数里调用了 OLED_ShowStr → OLED_Write_data → rt_i2c_transfer → ...
// 每层调用都要压栈,256 字节很快就用完了,溢出后覆盖相邻线程的栈或堆数据
排查方法:RT-Thread 提供栈使用率检测:
// 在 FinSH 里输入 list_thread 可以看到每个线程的栈使用情况
msh >list_thread
thread pri status sp stack size max used left tick error
-------- --- ------- ---------- ---------- ------ ---------- ---
task1 10 suspend 0x000001a4 0x00000400 42% 0x00000009 000
// ^^^
// 栈使用率 42%,说明还有余量
// 如果接近 100% 就要增大栈空间
1.7 内存管理总结:从裸机到 RTOS 的思维转变
理解了以上内容,就能建立起一个完整的内存视角:
裸机思维:
"我有一块 RAM,全局变量放前面,栈在后面,偶尔 malloc 一下"
RTOS 思维:
"我有一块 RAM,OS 把它统一管理成一个大堆
每个线程从堆里借一块当自己的栈
IPC 对象(信号量/队列)也从堆里分配
我需要时刻清楚:这个变量在哪里?它的生命周期到什么时候?"
一句话记住核心原则:
在 RTOS 里,凡是需要跨线程共享的数据,要么放全局变量(
.data/.bss),要么用消息队列(OS 帮你拷贝),绝对不能传局部变量的地址。
有了这个内存基础,后面学习线程调度、IPC 通信时遇到的很多"玄学问题"都能找到根因。接下来先从裸机阶段的踩坑开始。
二、STM32 裸机阶段——踩坑与修复
2.1 ADC 配置 Bug:未初始化结构体成员
现象:ADC 读出的值明显偏大,寄存器 CFGR1 = 0x0000202F,说明 ADC 处于 10 位左对齐模式,而不是预期的 12 位右对齐。
根因分析:
// ❌ 错误写法
ADC_InitTypeDef ADC_InitStructure;
// 没有调用 ADC_StructInit,结构体成员是栈上的垃圾值
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
// ADC_ExternalTrigConv 没有赋值,是随机值,被 OR 进了寄存器!
ADC_Init(ADC1, &ADC_InitStructure);
修复方案:
// ✅ 正确写法:先调用 StructInit 清零,再按需赋值
ADC_InitTypeDef ADC_InitStructure;
ADC_StructInit(&ADC_InitStructure); // 关键!将所有成员初始化为默认值
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_ScanDirection = ADC_ScanDirection_Upward;
ADC_Init(ADC1, &ADC_InitStructure);
教训:C 语言中局部变量不会自动清零,结构体初始化必须显式处理。
三、I2C 协议与软件 I2C 驱动
3.1 I2C 协议基础
- 两线制:SCL(时钟线)+ SDA(数据线),支持多主多从
- 起始/停止条件:SCL 高电平时 SDA 下降沿为 START,上升沿为 STOP
- 应答机制:每传输 8 位后,接收方拉低 SDA 表示 ACK
3.2 软件 I2C 驱动设计(低耦合宏定义方案)
通过宏定义隔离硬件,驱动代码与具体引脚解耦:
// my_i2c_soft.h
#define IIC_SCL_PORT GPIOB
#define IIC_SCL_PIN GPIO_Pin_6
#define IIC_SDA_PORT GPIOB
#define IIC_SDA_PIN GPIO_Pin_7
#define IIC_GPIO_RCC_ENABLE() RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE)
#define IIC_SCL(x) do{ (x) ? GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN) \
: GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); }while(0)
#define IIC_SDA(x) do{ (x) ? GPIO_SetBits(IIC_SDA_PORT, IIC_SDA_PIN) \
: GPIO_ResetBits(IIC_SDA_PORT, IIC_SDA_PIN); }while(0)
#define IIC_READ_SDA() GPIO_ReadInputDataBit(IIC_SDA_PORT, IIC_SDA_PIN)
#define IIC_DELAY() do{ volatile int i = 10; while(i--); }while(0)
3.3 SSD1306 OLED 驱动要点
- Page Addressing Mode:128×64 屏幕分为 8 页,每页 8 行,每列 1 字节(垂直方向 bit0 在上)
- GDDRAM:显示数据存储在芯片内部的图形显示 RAM 中
- I2C 地址:
0x78(含读写位的 8 位地址)=0x3C(7 位纯净地址)
// HAL 库写法
void OLED_Write_cmd(uint8_t cmd) {
HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 0x100);
}
// OLED_Printf 封装
void OLED_Printf(uint8_t x, uint8_t y, const char *format, ...) {
char buffer[64];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
OLED_ShowStr(x, y, buffer, 16);
}
四、SPI 协议与 NRF24L01
4.1 SPI 四种模式
| 模式 | CPOL | CPHA | 空闲时钟 | 采样边沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 上升沿 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿 |
| Mode 2 | 1 | 0 | 高电平 | 下降沿 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿 |
4.2 软件 SPI 字节交换(Mode 0)
SPI 的本质是"交换"——主机发一位,同时收一位:
unsigned char SPI_Swapbyte(unsigned char bytesend) {
unsigned char i;
for (i = 0; i < 8; i++) {
if (bytesend & 0x80) SPI_W_MOSI(1);
else SPI_W_MOSI(0);
bytesend <<= 1;
SPI_W_SCK(1); // 上升沿:从机采样 MOSI
if (SPI_R_MISO()) bytesend |= 0x01; // 同时主机采样 MISO
SPI_W_SCK(0);
}
return bytesend;
}
五、RT-Thread RTOS 核心概念
5.1 线程调度
优先级调度 + 时间片轮转:
- RT-Thread 支持 0~31 共 32 个优先级,数字越小优先级越高
- 相同优先级的线程按时间片轮转
- 就绪列表(Ready List)本质是一个按优先级组织的链表数组
- 位图调度算法:用一个 32 位整数的每一位代表对应优先级是否有就绪线程,通过
__CLZ(计前导零)指令 O(1) 找到最高优先级
5.2 链表结构
RT-Thread 内核大量使用侵入式双向循环链表:
// 节点嵌入在数据结构内部(侵入式)
struct rt_thread {
// ...
rt_list_t list; // 链表节点就在线程控制块里
// ...
};
优点:无需额外内存分配,通过 rt_container_of 宏从节点指针反推出整个结构体指针。
5.3 线程同步
互斥锁(Mutex)
- 有所有权:只有加锁的线程才能解锁
- 防优先级反转:持有锁的低优先级线程会被临时提升优先级
- 适用场景:保护共享资源(全局变量、外设)
rt_mutex_t mutex = rt_mutex_create("mtx", RT_IPC_FLAG_PRIO);
// 线程 A
rt_mutex_take(mutex, RT_WAITING_FOREVER);
// ... 操作共享资源 ...
rt_mutex_release(mutex);
信号量(Semaphore)
- 无所有权:任意线程都可以 release
- 计数型:可以累积多次 release
- 适用场景:事件通知、资源计数
rt_sem_t sem = rt_sem_create("sem", 0, RT_IPC_FLAG_PRIO);
// 生产者线程
rt_sem_release(sem);
// 消费者线程
rt_sem_take(sem, RT_WAITING_FOREVER);
5.4 线程通信
邮箱(Mailbox)
- 每次只能传递 4 字节(通常是指针)
- 不拷贝数据,传的是地址
- 注意:绝对不能发送局部变量的地址(悬空指针问题,见第一章)
rt_mailbox_t mb = rt_mb_create("mb", 4, RT_IPC_FLAG_PRIO);
// 发送方(必须用全局变量)
static struct sensor_data global_data = {20, 30, 5};
rt_mb_send(mb, (rt_uint32_t)&global_data);
// 接收方
struct sensor_data *p;
rt_mb_recv(mb, (rt_uint32_t*)&p, RT_WAITING_FOREVER);
rt_kprintf("温度:%d\n", p->temperature);
消息队列(Message Queue)
- 通过
memcpy拷贝数据,发送方数据可以是局部变量 - 更安全,但有内存拷贝开销
// 创建:消息大小 = sizeof(struct sensor_data),队列深度 = 4
rt_mq_t mq = rt_mq_create("mq", sizeof(struct sensor_data), 4, RT_IPC_FLAG_PRIO);
// 发送(数据被拷贝进队列,局部变量也安全)
struct sensor_data data = {25, 60, 3};
rt_mq_send(mq, &data, sizeof(data));
// 接收
struct sensor_data recv;
rt_mq_recv(mq, &recv, sizeof(recv), RT_WAITING_FOREVER);
邮箱 vs 消息队列对比:
| 特性 | 邮箱 | 消息队列 |
|---|---|---|
| 传输内容 | 4 字节(指针) | 任意大小数据 |
| 数据拷贝 | 不拷贝 | memcpy 拷贝 |
| 安全性 | 需注意悬空指针 | 更安全 |
| 性能 | 更快 | 有拷贝开销 |
5.5 信号(Signal)
RT-Thread 的信号类似 Linux 的软件中断,可以打断线程的正常执行流:
void thread1_signal_handler(int sig) {
rt_kprintf("收到信号 %d\n", sig);
}
static void thread1_entry(void *parameter) {
rt_signal_install(SIGUSR1, thread1_signal_handler);
rt_signal_unmask(SIGUSR1);
// 正常运行...
}
// 在其他地方发送信号
rt_thread_kill(tid1, SIGUSR1);
六、RT-Thread 设备框架
6.1 BSP 与自动初始化机制
RT-Thread 的 BSP(板级支持包)通过 INIT_DEVICE_EXPORT 宏实现自动初始化:
// drv_i2c.c 底层驱动(系统自动调用,无需手动 Init)
INIT_DEVICE_EXPORT(rt_hw_i2c_init);
进入 main() 之前,系统已经自动完成了所有硬件初始化,不需要也不应该再调用 MX_I2C1_Init() 等 CubeMX 生成的函数。
6.2 用 RTT I/O 框架重构 OLED 驱动
核心思想:应用层只调用 OS 提供的标准 API,与底层硬件完全解耦。
关键坑:I2C 地址位数差异
| 库 | 地址格式 | 示例 |
|---|---|---|
| HAL 库 | 8 位(含读写位) | 0x78 |
| RT-Thread | 7 位(纯净地址) | 0x3C(= 0x78 >> 1) |
#include <rtdevice.h>
#define OLED_I2C_BUS_NAME "i2c1"
#define OLED_ADDR_7BIT 0x3C // ⚠️ 不是 0x78!
static struct rt_i2c_bus_device *i2c_bus = RT_NULL;
void OLED_Write_cmd(uint8_t cmd) {
uint8_t buf[2] = {0x00, cmd};
struct rt_i2c_msg msg;
msg.addr = OLED_ADDR_7BIT;
msg.flags = RT_I2C_WR;
msg.buf = buf;
msg.len = 2;
rt_i2c_transfer(i2c_bus, &msg, 1);
}
int OLED_Init(void) {
i2c_bus = (struct rt_i2c_bus_device *)rt_device_find(OLED_I2C_BUS_NAME);
if (i2c_bus == RT_NULL) {
rt_kprintf("Fatal: can't find %s!\n", OLED_I2C_BUS_NAME);
return -1;
}
rt_thread_mdelay(100);
// ... 发送初始化命令序列 ...
return 0;
}
6.3 软件 I2C 配置(board.h)
// drivers/board.h 中,在官方预留的 #ifdef 块内填写引脚
#define BSP_USING_I2C1
#ifdef BSP_USING_I2C1
#define BSP_I2C1_SCL_PIN GET_PIN(B, 6)
#define BSP_I2C1_SDA_PIN GET_PIN(B, 7)
#endif
为什么用软件 I2C? STM32 早期硬件 I2C 存在硅缺陷,RT-Thread 官方 BSP 默认主推软件模拟 I2C,稳定性更高。对于 OLED 这类低速外设,软件 I2C 完全够用。
6.4 UART 设备框架
rt_device_t uart2_dev = rt_device_find("uart2");
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
config.baud_rate = BAUD_RATE_9600;
config.bufsz = 128;
rt_device_control(uart2_dev, RT_DEVICE_CTRL_CONFIG, &config);
rt_device_open(uart2_dev, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX);
七、学习路线回顾
裸机开发
└─ ADC 踩坑(未初始化结构体)→ 养成 StructInit 习惯
└─ 软件 I2C → 理解协议时序
└─ OLED 驱动 → 理解显存映射
└─ SPI + NRF24L01 → 理解全双工通信
引入 RTOS
└─ 内存布局 → 理解栈/堆/全局变量的本质
└─ 线程调度 → 优先级、时间片、位图算法
└─ 链表结构 → 侵入式双向循环链表
└─ 线程同步 → mutex(有所有权)vs semaphore(无所有权)
└─ 线程通信 → mailbox(传指针)vs mq(拷贝数据)
└─ 信号机制 → 软件中断
架构升级
└─ 抛弃 HAL 库 → 拥抱 RTT 设备框架
└─ 自动初始化机制 → 无需手动 Init
└─ I2C 地址坑(0x78 vs 0x3C)
└─ 编译孤岛问题 → 宏定义必须放在公共头文件
└─ 防御性编程 → rt_device_find 返回值检查
最后更新:2026-02-26