前言: 在上一篇中,我们通过隐藏结构体定义,实现了数据的绝对安全。但代价是必须使用 Motor_Create() 来动态申请内存。
痛点明确:
-
内存碎片:长期运行的系统不敢用
heap。 -
野指针风险:如果用户传了一个错误的地址
(Motor_Handle)0x20001234,系统会直接 HardFault。 -
Use-After-Free:如果一个对象被销毁了,用户还拿着旧句柄去操作,会发生不可预知的错误。
今天,我们把这些问题一次性解决。
一、 拒绝 Malloc:静态对象池技术
既然不能动态分配,那我们就 预先分配。 我们在驱动的内部(.c 文件里),直接定义一个全局的静态数组。这个数组就是我们的“私有池子”。
1.1 改造驱动实现 (Inside .c)
我们不再 include <stdlib.h>,而是自己管理内存。
// motor_driver.c
#include "motor_driver.h"
// 系统最大支持 4 个电机 (在编译时确定内存占用)
#define MAX_MOTORS 4
// 真正的结构体定义(依然对外隐藏)
struct Motor_t {
uint8_t is_allocated; // 【关键】标记该槽位是否被占用
uint8_t current_speed;
GPIO_TypeDef *port;
};
// 【核心技术】:静态内存池
// 这块内存在编译时就占用了 BSS 段,完全不需要 malloc
// static 关键字保证了外部无法直接访问这个数组
static struct Motor_t motor_pool[MAX_MOTORS];
// 创建句柄
Motor_Handle Motor_Create(void) {
for (int i = 0; i < MAX_MOTORS; i++) {
// 遍历池子,找一个没用的空位
if (motor_pool[i].is_allocated == 0) {
motor_pool[i].is_allocated = 1; // 标记占用
// 这里我们先暂时返回指针,下一节会优化它
return (Motor_Handle)&motor_pool[i];
}
}
return NULL; // 池子满了
}
// 销毁句柄
void Motor_Destroy(Motor_Handle h) {
if (h) {
struct Motor_t* p = (struct Motor_t*)h;
p->is_allocated = 0; // 只是标记为空,内存并不释放
}
}
效果: 用户依然只能拿到 Motor_Handle(不透明指针),依然不知道结构体大小。但在底层,我们完全避开了 malloc/free,实现了 零碎片、确定性 (Deterministic) 的内存管理。
二、 拒绝 HardFault:索引即句柄
上面的代码虽然解决了内存问题,但它返回的还是一个 指针。 如果用户恶作剧,传了一个 (Motor_Handle)0xFFFFFFFF 进来,你的驱动去访问 h->is_allocated,CPU 依然会炸。
为了极致的稳健性,我们需要转换思维: 句柄,本质上就是一个“凭证”。谁说凭证一定要是内存地址?它可以是数组下标。
2.1 修改头文件:句柄是整数
// motor_driver.h
// 【大变革】句柄不再是指针,而是一个简单的 ID
// 0, 1, 2, 3 是有效 ID,255 (0xFF) 代表无效
typedef uint8_t Motor_Handle;
#define MOTOR_INVALID_HANDLE 0xFF
Motor_Handle Motor_Create(void);
// 返回值改为 int (Status Code),不再是 void
int Motor_SetSpeed(Motor_Handle h, uint8_t speed);
2.2 修改源文件:数组越界检查
// motor_driver.c
static struct Motor_t motor_pool[MAX_MOTORS];
Motor_Handle Motor_Create(void) {
for (int i = 0; i < MAX_MOTORS; i++) {
if (motor_pool[i].is_allocated == 0) {
motor_pool[i].is_allocated = 1;
// 【关键】:返回数组下标(索引),而不是地址
return (Motor_Handle)i;
}
}
return MOTOR_INVALID_HANDLE;
}
int Motor_SetSpeed(Motor_Handle h, uint8_t speed) {
// 【极致安全检查】
// 1. 检查是否越界:防止用户传个 100 进来
// 这种检查是指针做不到的(指针无法判断是否属于本模块)
if (h >= MAX_MOTORS) {
return ERROR_INVALID_HANDLE;
}
// 2. 检查对象是否存活:防止用户操作一个已经 Destroy 的电机
if (motor_pool[h].is_allocated == 0) {
return ERROR_OBJECT_CLOSED;
}
// 3. 安全操作
motor_pool[h].current_speed = speed;
return SUCCESS;
}
对比一下安全性:
-
指针句柄:传错了地址 -> 访问非法内存 -> HardFault (系统死机)。
-
索引句柄:传错了 ID ->
if (id >= MAX)拦截 -> 返回错误码 (系统继续运行)。
对于医疗器械、航空航天等不允许死机的领域,索引句柄是唯一的选择。
三、 进阶技巧:防篡改校验 (Magic Number)
有些极其严格的系统(比如文件系统句柄),还会担心一种 “借尸还魂” 的情况:
-
任务 A 申请了 ID=1 的电机。
-
任务 A 把它
Destroy了。 -
系统把 ID=1 分配给了 任务 B 的“水泵”。
-
任务 A 有个 Bug,它不知道 ID=1 已经销毁了,继续用旧句柄去设置速度。
-
结果:任务 A 意外控制了 任务 B 的水泵!(因为 ID 一样)。
为了解决这个问题,我们可以在结构体里加一个 Magic Number (或 Version)。
-
原理:
-
结构体里加个
uint8_t version。每次分配时,version++。 -
句柄变成
uint16_t:高 8 位存 version,低 8 位存 index。
-
-
校验:
-
调用 API 时,先拆解句柄。
-
检查
handle.version == pool[index].version。 -
如果不相等,说明这个 ID 已经被“转世投胎”给新对象了,原来的句柄彻底失效。
-
(注:这在 Windows 的 HANDLE 管理机制中有类似应用)
四、 总结与思考
至此,我们的 “句柄三部曲” 彻底完结。我们经历了一次从菜鸟到架构师的思维升华:
| 阶段 | 形式 | 优点 | 缺点 | 适用场景 |
| V1.0 | 全局变量 | 简单 | 无法复用 | SysTick, Log |
| V2.0 | 结构体指针 | 可复用 | 封装性差 | 团队内部小模块 |
| V3.0 | 不透明指针 | 封装好 | 依赖 Malloc | 通用 PC 软件 / SDK |
| V4.0 | 索引句柄 | 极度安全 | 无 Malloc | 高可靠嵌入式系统 |
最后的思考:FreeRTOS 的选择
有人问:既然索引句柄这么好,为什么 FreeRTOS 的 TaskHandle_t 还是指针?
FreeRTOS 的 TaskHandle_t 本质是一个 void*,指向 TCB。 这是因为 FreeRTOS 追求 极致的效率。
-
指针访问:
MOV R0, [R1](一步到位)。 -
索引访问:
MOV R0, Base + Index * Size(需要计算偏移)。
在每秒发生几千次任务切换的内核里,为了省那几条指令,FreeRTOS 选择了指针,牺牲了一点点安全性(如果你瞎传句柄,内核真会挂)。 但在你的 应用层驱动 里,安全性通常比那一纳秒的性能更重要。
没有最好的架构,只有最适合场景的架构。 希望这一系列文章,能让你手中的 C 语言,不再仅仅是过程的堆砌,而是充满设计美感的系统。
/*******************************************
* Description:
* 本文为作者《嵌入式开发基础与工程实践》系列文之一。
* 关注我即可订阅后续内容更新,采用异步推送机制。
* 转发本文可视为广播分发,有助于信息传播至更多节点。
*******************************************/
5065

被折叠的 条评论
为什么被折叠?



