设计思想
嵌入式系统中,运行时的内存分配是设计缺陷
在嵌入式系统中,运行时的内存分配应被视为设计缺陷。系统的所有资源应尽可能在构造或初始化阶段就完成分配和配置。这不仅有助于提高系统的可预测性和稳定性,也更容易进行内存使用分析和资源规划。
需要注意的是,这一原则并不意味着禁止使用动态内存分配(如 malloc
、new
),而是强调动态内存分配的时间点必须在系统的初始化阶段完成。在系统进入主循环或开始任务调度之后,不应再进行新的内存分配操作。否则将引入:
- 未知的延迟(如碎片整理等)
- 难以追踪的内存泄漏或越界错误
- 潜在的内存分配失败
换句话说,“动态分配可以用,但必须在静态时机使用”。在这种前提下,你甚至可以用一个只出不进的栈来实现内存分配,并非常方便的监视内存使用情况。
一切回调/中断都必须是无阻塞的
回调是跨上下文的执行点,无论来自中断、调度器还是线程之间的异步触发,其职责都应当极度克制。回调的设计目的不是“处理”,而是“转交”。
在 LibXR 中,所有回调仅允许执行以下两类操作:
- 通知线程:设置状态、释放信号量、发布事件;
- 推送数据:写入无锁队列、缓存结构等预分配数据区。
除此之外的行为一律禁止:
- 阻塞等待(如信号量、锁、睡眠)
- 业务逻辑处理或调用上层接口
- 动态内存分配
- 调用任何不确定时延的函数
上下文(thread/isr)必须在回调中显式传递
- 所有回调统一传入
bool in_isr
参数 - 操作系统封装 API 明确分为
Api()
/ApiFromCallback(in_isr)
- 用户无需判断当前上下文,只需将
in_isr
向上传递 - 保证所有回调、任务切换、信号交互在 ISR / Thread 两种模式下都能安全运行
- 永远不需要猜当前是不是在中断运行
任何 I/O 操作都必须绑定确定的完成行为
I/O 操作的重点不是“发出请求”,而是如何得知它完成了,以及之后如何处理结果。
LibXR 通过 Operation
类型对 I/O 行为进行建模,在操作发起时就绑定好完成后的响应机制:
行为模式 | 类型 | 描述 |
---|---|---|
回调 | CALLBACK | 操作完成后自动触发回调函数 |
阻塞 | BLOCK | 当前任务等待,直到操作完成或超时 |
轮询 | POLLING | 用户主动检查操作是否完成 |
忽略 | NONE | 发起即忘,不等待任何反馈 |
WriteOperation op_cb(callback); // 异步回调
WriteOperation op_block(sem, 100); // 阻塞等待
WriteOperation op_poll(status); // 轮询状态
WriteOperation op_none; // 忽略结果
uart1.Write("Hello, world!", op_block);
一个没有绑定完成行为的操作,是不可控的。
接口中不应出现任何平台相关类型
使用者不应在任何接口中看到平台相关的结构体或依赖。例如:
- STM32 的
UART_HandleTypeDef*
- ESP-IDF 的
uart_port_t
- Linux 的
termios
- FreeRTOS 的
TaskHandle_t
、SemaphoreHandle_t
这类类型只能存在于平台实现内部,绝不应出现在接口声明、参数或返回值中。
接口必须保持平台无关性,以确保:
- 不同平台间可替换
- 行为可测试、可模拟
- 未使用平台代码可裁剪
若平台类型出现在接口中,则该接口不能复用、不可维护,且会导致上层逻辑依赖具体实现。