mbed OS 源码我翻过不止三遍。第一遍是为把一个传感器驱动从 HAL 层穿透到寄存器,结果被上层抽象的封装绕了不少弯路;第二遍调 RTOS 下的串口中断,才发现整个事件推进链路不看源码根本定位不了问题;第三遍把 HAL、RTOS、驱动和测试体系完整串起来之后,才真正敢说自己看懂了这套架构。这篇就当是我把三遍笔记整理后的公开版,围绕 mbed OS 源码架构里的 HAL、RTOS、驱动、测试这四条主线展开,适合三类人:准备在全志、NXP、ST 等不同芯片之间做通用固件层的工程师,刚入门 RTOS 想看看真实产品级代码怎么组织的同学,以及打算给新 MCU 移植 mbed OS 的开发者。
1. 先从整体看:mbed OS 的分层逻辑
1.1 源码树里到底藏了哪些关键模块
mbed OS 的源码目录虽然看着多,但我们做嵌入式开发真正要盯住的就那么几个。我自己习惯把仓库根目录下面的模块按“硬件相关”和“软件抽象”两条线去分:
- hal:硬件抽象层,定义了串口、GPIO、I2C、SPI、定时器、PWM、ADC、DAC 等外设的统一 C 接口。这是整个架构的地基。
- targets:各家芯片的实现代码。ST、NXP、Nordic、Renesas 等厂商的移植都在这个目录里。每个芯片下面又有 TARGET_xxx 子目录,里面是 startup 文件、链接脚本、外设驱动实现。
- rtos:基于 CMSIS-RTOS2 封装的线程、信号量、消息队列、事件标志等。它把内核接口固定下来,底层可以接 RTX5,也可以接 FreeRTOS。
- drivers:面向应用层的 C++ 驱动库,像 DigitalOut、I2C 这类类封装,内部调用 hal 里的 C 函数。
- platform:平台公共部分,包括 CriticalSection、Timer、NonCopyable、回调机制等基础工具,被 drivers 和用户代码共用。
- features:可选功能组件,比如 BLE、NFC、LoRa、SecureStore 这些。
- tools、tests:构建脚本和测试框架,后面讲测试体系时会展开。
从依赖关系上看,drivers 依赖 hal 和 platform,hal 依赖 targets,而用户代码可以直接停在 drivers 层,也可以下探到 hal 层。整个 mbed OS 源码架构强调的“分层”,本质是让上层代码和具体芯片解耦。
1.2 为什么要把“硬件访问”单独抽成一层
很多 MCU 工程师最初理解 HAL 时容易有一个误区:以为 HAL 就是把寄存器操作封装成函数。对 mbed OS 来说,HAL 的意义远不止函数封装,更重要的是统一接口约束。
比如你要操作一个 I2C 外设,在裸机开发里你是直接去翻芯片手册填寄存器,但在 mbed OS 里,无论是 NXP 的 LPC 系列还是 ST 的 F1 系列,上层调用的都是i2c_write、i2c_read这一组接口,底层实现由各家的 targets 代码去完成。这样做的好处很直接:你的驱动代码不用为每个芯片重新实现一遍,只要遵循同样的调用约定,拿到什么板子上都能编译。
代价当然也有。抽象层必然带来一点运行时开销,比如函数指针、对象管理的间接跳转,对实时性要求极其苛刻的场景会不划算。但绝大多数产品项目里,这点开销换来的跨芯片可迁移性和团队协作效率,是很划算的。
我还想强调一点:mbed OS 的 HAL 是 C 接口而不是 C++ 接口。这一点不是随便定的。C 接口对编译器更友好、链接符号更简单、跨语言绑定也更方便,所以你在 hal 目录里看到的是serial_api.h、gpio_api.h这种文件,而不是Serial.h这种 C++ 头文件。C++ 的封装是 drivers 层的事。
2. HAL 层:整个架构的地基
2.1 HAL 在源码树中的位置和约定
HAL 层的头文件路径是hal/和platform/,而实现分散在targets/TARGET_厂商/TARGET_型号/下面。以 STM32F103 为例,你会看到类似这样的路径:
targets/TARGET_STM/TARGET_STM32F1/TARGET_NUCLEO_F103RB/往里翻,有设备初始化逻辑、时钟配置、启动文件、链接脚本,还有一张非常关键的PinNames.h。这张表定义了板子上每个引脚能映射到哪个外设,实际上是一张“引脚功能映射表”。
HAL API 的命名规则非常统一:外设_动词,比如gpio_write、serial_getc、i2c_start、spi_master_write。这个风格从早期 mbed 一直延续到现在,熟悉之后查代码很舒服,不用猜函数名。
每个 HAL API 的头文件里会有两个部分:
- 通用接口声明:所有芯片都必须实现,比如
gpio_init。 - 可选扩展或辅助函数:特定芯片可以额外提供,但不能作为上层依赖。
如果你要给一个新 MCU 移植 mbed OS,HAL 层就是你最需要花心思的地方。不夸张地说,mbed OS 能不能跑起来,取决于 HAL 接口是否完整落实;后续 RTOS 和驱动能不能正常工作,也建立在这层是正确的。
2.2 PinNames 与引脚映射机制
mbed OS 里操作引脚不是直接写“PA0”这种字符串,而是先定义引脚别名。打开PinNames.h,你会看到类似:
typedef enum { PA_0 = 0x00, PA_1 = 0x01, ... NC = (int)0xFFFFFFFF } PinName;但这只是第一步,真正有意思的是“功能复用”的映射方式。mbed OS 的引脚可以叠加外设功能,比如 I2C 的 SCL 可能同时被定义成I2C_SCL,而引脚本身还是PB_6。驱动配置时用pinmap_pinout这类函数把功能标志和引脚号组合起来,查表找到对应的复用寄存器配置。
这意味着驱动层永远不需要知道某个芯片的复用控制寄存器究竟是 AFIO 还是 GPIOx_AFRL,它只需要告诉 HAL 层“我要在这个引脚上启用 I2C 功能”,剩下的交给 targets 里那张表。这个设计的直接好处是:你的业务代码在不同开发板之间迁移时,只要板级 PinNames 配置对了,代码几乎不用改。
2.3 串讲一个 HAL 调用链:串口启动
为了把 HAL 的工作方式说透,我拿串口举个例子。你在应用层最常见的是:
mbed::UnbufferedSerial serial(USBTX, USBRX, 115200);这个构造函数从drivers/UnbufferedSerial.h开始,往下会调用serial_init这个 HAL 函数:
void serial_init(serial_t *obj, PinName tx, PinName rx);在 STM32 的 targets 实现里,这个函数最终会做几件事:
- 根据传入的 PinName 解析出 UART 外设编号(USART1、USART2...)。
- 使能对应 GPIO 时钟和 UART 时钟。
- 根据引脚功能映射表,把 TX、RX 脚配置成复用推挽/浮空输入。
- 调用底层
uart_init配置波特率、数据位、停止位、校验位。 - 注册中断向量、启用收发。
从调用链可以看出来,mbed OS 把“外设逻辑”和“引脚物理连接”分离得比较干净。如果你在板子上换了一个引脚,只需要修改 PinNames 映射,而不是重写串口初始化逻辑。这也是为什么很多工程师喜欢用 mbed OS 做原型验证,改板子很快。
2.4 HAL 移植前必须搞清的资源归属
做 HAL 适配时,我经常见到新手把注意力全放在 API 实现上,忽略了系统的资源归属问题。mbed OS 不是给你一个 API 就完事,它还会管理中断优先级、DMA、时钟树。最典型的坑有两个:
- 时钟初始化:很多 MCU 的 HAL 实现里,
SystemInit是在 C 运行时初始化阶段就执行的,如果你的驱动在mbed_main或者静态对象构造阶段就启动了外设,但目标芯片的时钟还没切到高速外部晶振,那初期测出来的波特率或定时器周期就会很奇怪。 - 中断向量:mbed OS 依靠 CMSIS 的向量表统一接管中断。如果你在新板卡上直接照抄某个 SDK 的中断服务函数名,可能和 mbed OS 的弱定义冲突,导致中断被屏蔽或指向错误的处理函数。
所以我在移植前会先画一张资源清单:哪些外设由 HAL 驱动,哪些时钟源要默认开启,哪些中断归 RTOS 管理,哪些 DMA 通道保留给用户。这张清单看起来费时间,但越早画清楚,后面排错越省事。
3. RTOS 层:调度、同步与中断协作
3.1 内核抽象是“双层”的
mbed OS 的 RTOS 不是一个从零写的内核,而是基于 CMSIS-RTOS2 标准的封装。具体到源码里,rtos/目录提供的是Thread、Mutex、Semaphore、EventFlags、MessageQueue这些 C++ 类,而它们底层调用的是cmsis_os2.h里的 C API,比如osThreadNew、osMutexAcquire、osEventFlagsSet。
为什么搞两个层次?这和大公司做软件平台是同样的思路:接口统一、实现可替换。mbed OS 默认可以和 ARM 的 RTX5 配合,也可以切到 FreeRTOS 作为内核,只要实现 CMSIS-RTOS2 规定的接口,上层代码完全不用变。
但这个双层结构也带来一个排查难点:当你看到EventFlags的某次wait超时返回时,问题可能出在 mbed 封装层,也可能出在内核调度,甚至可能出在中断里没有正确调用osEventFlagsSet。所以一定要学会在两层之间来回定位,只看一头很容易误判。
3.2 线程和事件标志的经典组合
我写 RTOS 应用特别爱用事件标志,因为它比裸奔标记位更安全,又比信号量更适合表达“多条件汇聚”。在 mbed OS 里事件标志 API 是EventFlags,一个线程等待多个事件位,另一个上下文(中断或线程)置位唤醒。比如这样一个场景:按键按下发一个事件,串口收到一帧发一个事件,LED 线程同时等待这两个事件分别去做不同的事。
#include "mbed.h" EventFlags flags; void key_isr() { flags.set(0x01); // 按键事件 } int main() { InterruptIn key(BUTTON1); key.fall(&key_isr); while (true) { uint32_t f = flags.wait_any(0x03); if (f & 0x01) { // 处理按键事件 } } }注意EventFlags::set可以在中断上下文调用,因为底层对应的是 CMSIS-RTOS2 的osEventFlagsSet,内核实现会保证这条路径可被中断调用。这个特性非常关键,否则你就要把事件缓存到并发队列里,再让线程去查,代码会更绕。
如果你在写裸机程序时习惯用一个全局volatile标志位,到 RTOS 里我建议立刻改成EventFlags或消息队列。原因很简单:volatile只保证编译器不优化读,但不保证多线程或中断和主循环之间的可见性,更不提供阻塞等待能力。
3.3 同步原语怎么选:一个实用对照
mbed OS 里同步原语不少,但大部分需求其实集中在几个常用点上。我整理了一张简易对照表,摘自自己写代码时的选择逻辑:
| 场景 | 推荐原语 | 说明 |
|---|---|---|
| 一个线程等一个或多个事件 | EventFlags | 支持多事件位“或”等待 |
| 多份数据按顺序传递 | MessageQueue / Mail | 带缓冲,避免丢失 |
| 保护共享资源(比如外设) | Mutex | 可嵌套,防优先级反转 |
| 控制并发数量或计数 | Semaphore | 更灵活,但要小心超发 |
| 线程延迟到超时再执行 | ThisThread::sleep_for | 别用阻塞延时库函数 |
实际项目里最容易出问题的是“用锁颗粒度太大”或“锁顺序不一致”。比如两个线程都需要访问同一个外设,又需要互相通信,一旦一个持锁等消息、一个持锁等释放,就很容易死锁。我的习惯是:锁的范围尽量只覆盖外设寄存器操作,不要覆盖漫长的业务逻辑;多线程之间优先用消息队列而不是共享状态。
3.4 中断上下文里哪些函数不能调
这是新人掉坑最密集的区域。mbed OS 的 RTOS 虽然封装很友好,但底层仍然是严格区分线程上下文和中断上下文的。在中断服务函数里,很多函数不能随便调用,因为它们可能依赖线程调度、内存动态分配或锁机制。
可以安全调用的典型函数包括:
osEventFlagsSet系列,给线程发事件。osMessageQueuePut系列,往队列塞数据。- 部分 HAL 函数,比如 GPIO 读写、串口非阻塞发送的底层接口。
不建议调用的函数包括:
ThisThread::sleep_for线程睡眠。Mutex的lock,因为中断里拿锁可能让系统直接崩溃。- 含有 malloc、new、printf 重定向到阻塞串口的操作,这类调用轻则性能劣化,重则死锁。
我自己排查这类问题最快的定位法,是打开内核调试信息,看看死锁是发生在内存分配函数还是在调度器临界区代码里。配合 RTOS 的栈高水位检测,基本能锁定是哪段 ISR 干了不该干的事。
3.5 RTOS 调试经验:先关优化再查时序
mbed OS 默认的编译优化等级不低,某次我在优化开起来之后,线程间一个明显的事件丢失问题怎么也复现不了,把编译器优化降到-O0就能稳定复现。后来才发现是一个未初始化变量在优化下碰巧被清成了零,掩盖了真问题。
所以遇到 RTOS 下诡异的现象,我第一件事不是改代码,而是先关编译器优化、打开调试日志、加打印点位,把线程时序还原出来。很多时候问题不在内核,而在你自己代码里某个外设或状态变量被多个线程同时改,却没有加锁保护。
4. 驱动体系:从 C++ 外壳到设备注册
4.1 驱动与 HAL 的相处方式
mbed OS 的驱动层drivers/是 C++ 类库,外部开发者平时接触的DigitalOut、I2C、SPI、InterruptIn都在这一层。这些类自身不直接操寄存器,而是调用 HAL 层 C 函数。这样分层有一点值得注意:驱动类负责生命周期管理、锁、回调,HAL 函数负责具体硬件动作。
比如DigitalOut构造时,它的构造函数会调用gpio_init和gpio_dir,而write方法调到gpio_write;I2C类则持有i2c_t句柄对象,内部操作都围绕这个句柄展开。如果你在调试驱动时跳进一个类方法里发现最终落到一个 C 函数,不要惊讶,这正是 mbed OS 源码架构的正常路径。
还有一点:mbed OS 驱动类大量使用NonCopyable禁止拷贝。原因是外设句柄和中断回调涉及独享资源,拷贝对象会导致两个实例同时操作同一个外设,出现不可预期行为。想传递这种对象时,优先用指针或引用。
4.2 常见驱动类是怎么组织的
以InterruptIn为例,它内部会注册一个回调,底层通过gpio_irq_init把引脚中断挂到系统上。rise和fall这两个接口填的就是你定义的回调函数,后续内部中断发生后,mbed 会调用这个回调,再转发到用户函数。回调函数可以用函数指针、函数对象或mbed::callback任意形式,我习惯在嵌入式场景里直接用静态函数加对象指针的方式,少引入 C++ 新特性,避免栈和代码体积膨胀。
再看 I2C 类,它的接口包括start、stop、write、read,这些方法对应 HAL 的i2c_start、i2c_stop、i2c_write、i2c_read。I2C 本身是个有状态协议,所以类内部维护了总线状态。如果你在自己的驱动里要用中断方式读取 I2C,需要认真处理回调在中断上下文被触发的情况,尽量避免在中断里做耗时解析。
4.3 中断下半部与 EventQueue
裸机时代我们习惯在定时器中断或 GPIO 中断里直接处理业务,但到 RTOS 里,中断里的任务越短越好,耗时的协议解析、滤波、状态机更新都应该放到线程上下文。mbed OS 提供了EventQueue,它的思路很简单:把中断里需要做的耗时操作打包成一个事件,投递到队列里,然后由一个专门的线程按顺序执行。
#include "mbed.h" EventQueue queue(32 * EVENTS_EVENT_SIZE); Thread eventThread; void on_irq() { queue.call([]{ // 这里是线程上下文,可以放心解析数据 printf("handle in thread\n"); }); } int main() { eventThread.start(callback(&queue, &EventQueue::dispatch_forever)); InterruptIn key(BUTTON1); key.fall(&on_irq); // 主线程继续做别的事 }这个模式特别适合传感器和通信协议栈,很多产品里的状态机引擎就是用 EventQueue 驱动的。值得注意的是EventQueue本身有内存需求,EVENTS_EVENT_SIZE要按事件对象大小估算,不太够就调大,太小则会出现投递失败或运行时断言。
4.4 三个驱动实战案例的温度
这里写三个在 mbed OS 上做过的驱动案例,覆盖 GPIO、I2C、单总线三类典型外设,也对应大家在嵌入式论坛里经常搜的几个场景。
DHT11 温湿度传感器
DHT11 是单总线协议,mbed OS 没有专门驱动类,我在自己的驱动里直接操作DigitalInOut,就是既当输出又当输入。关键点是时序要求:主机先拉低至少 18ms 启动,然后释放总线,再从高电平开始读取 40bit 数据。RTOS 环境下读这种时序严苛的传感器,必须关掉调度器抢占或使用高优先级线程,否则一个线程切换就会导致采样失败。我实际在 mbed OS 里用CriticalSectionLock包住读取过程,实测能稳定工作,但耗时接近 1ms 的场景要谨慎评估对系统影响。
OLED 屏
OLED 通常走 I2C 或 SPI。用 mbed OS 的I2C类写起来很快,但要注意屏幕的 I2C 地址通常由硬件跳线决定,确认后不要写错。另一个坑是 I2C 总线速率,有的 OLED 模块在 400kHz 下不稳定,我一般先降到 100kHz 把底层读写验证通过,再往上提。驱动里不要频繁开关总线,一次连续发送整块显存数据会比多次小数据包稳定得多。
MT6701 磁编码器
MT6701 用模拟 I2C 读取时,线性区输出是需要滤波和校准的。简单处理是直接读原始角度值,但实际电机轴上会有安装偏心、磁场畸变,不做滤波很难直接用于闭环控制。我在 mbed 上实现了滑动平均滤波和一阶低通滤波,同时加了一个简单的上电校准流程:让电机转一圈,采集最大最小值,再映射到 0~360 度。这样在位置闭环里的表现会好很多。
这些驱动如果放到裸机上写其实也不复杂,关键是 mbed OS 的多线程环境会引入“另一个线程同时在访问同一个 I2C 总线”的问题。所以我给每个共享总线的驱动都配了一个 Mutex,并且在驱动类内部加锁,避免由业务代码到处漏锁。
5. 测试体系:mbed 怎么保证这么多板子都能用
5.1 测试金字塔和工具栈
mbed OS 源码架构里测试体系非常庞大,这也是它和很多 MCU SDK 最大的区别。一套完整的 mbed OS 测试链路包含几个工具:
- mbed-ls:检测电脑上连接的 mbed 开发板,列出设备端口和目标 ID。相当于“设备发现”。
- Greentea:跑硬件测试的主框架。它负责把测试程序编译、烧录到板子,然后通过串口或 USB 与设备端测试代码通信,收集测试结果。
- htrun:Greentea 底层的宿主-目标通信工具,负责在 PC 和板卡之间传输测试帧。
- ICTT:编译器和测试工具链的集成层,偏构建检查。
这个测试体系和传统的“本地跑一个 main 函数”完全不同。mbed OS 的设备端代码会包含一小段测试协议栈,板子跑完一条用例后通过串口回报给 PC,PC 端再判断通过还是失败,最后汇总成报告。这也解释了为什么 mbed OS 的tests目录里会大量出现TEST_宏和MBED_TEST之类的声明。
5.2 本地跑一次硬件在环测试的完整流程
我以自己给一块自定义板子跑 HAL 串口测试为例。首先确保 mbed CLI 或 mbed-tools 能识别到设备,然后切到测试目录,命令大致是:
mbed-tools compile -m MY_TARGET -t GCC_ARM mbedgt -m MY_TARGET --parallel=1mbedgt就是 Greentea 的命令行入口。它会自动发现要用哪个测试程序、烧录到哪块板子、跑哪些用例。设备端的测试代码会通过串口上报日志和测试结果,PC 端拿到结果后输出类似:
test case: "UART serial test" PASSED如果硬件有问题,会直接看到 FAILED 或超时。这种“回环测试”最常用的用例是把 TX 和 RX 用一根杜邦线短接,然后设备端发送一组数据,从 RX 收回来并比对,如果一致,串口通路基本没问题。
5.3 哪些测试适合放进 CI
不是所有测试都要硬件。mbed OS 把测试做了分层,我自己的做法是:
- 第一层:编译测试(ICTT 或 mbed-tools 直接编译),检查代码能否对上不同编译器和配置,没有硬件的 CI 也可以先跑。
- 第二层:单元测试,用一套 mock 或纯逻辑测试,覆盖协议解析、校验算法、状态机这类不依赖硬件的部分。
- 第三层:硬件在环测试,每天合并主干时跑一遍关键外设用例,保证板卡资源和 HAL 实现不被改坏。
- 第四层:整机系统测试,包括低功耗、长时间压力、掉电加密,一般放 nightly。
CI 里我通常会配置矩阵:不同的编译器、不同的优化等级、多条 target。这样能在合入前就发现架构层的不兼容问题,而不是等打板了再返工。
5.4 不稳定外设的测试技巧
传感器类外设天然有噪声和漂移,测试用例如果直接把采样值和期望值做严格比较,很容易偶发失败,导致大家开始忽略红灯。我的习惯是给这类测试加“容许范围”和“重试次数”。比如压力传感器上电后先预热 200ms,连续采样 5 次,取中间值,再和阈值比较。测试报告里也要输出原始值,否则现场定位不了是传感器硬件坏了还是算法 bug。
另外一个技巧是用“宿主端脚本”辅助测试。mbed OS 的 host test 机制允许你在 PC 端跑 Python 脚本,动态操控设备。比如做一个自动化按键时序测试,PC 端先往设备发指令,设备端收到后输出一串日志,脚本再比较日志内容是否和预期一致。这在协议栈测试里特别好用,能覆盖很多手工点按钮才能触发的边界场景。
6. 从源码到新板:一次真实的移植经历
6.1 移植前要准备的三件事
给一块新 MCU 移植 mbed OS,不是把 SDK 拷贝过来就完事。我每次开始移植前必做三件事:
- 拿到目标芯片的参考手册和寄存器说明,特别是时钟树、GPIO 复用表、中断向量表。没有这些,HAL 的功能实现就是瞎填。
- 找到同系列芯片在 mbed OS 里的已有移植代码作为模板,比如要移植的是某颗 STM32G4,就先看 STM32F1/F3 的 targets 代码怎么写的,尽量复用。
- 确认编译工具链。mbed OS 通常用 ARM Compiler 或 GCC_ARM,新板卡的启动文件需要和编译器配合,别混用。
6.2 targets.json 该填什么
mbed OS 使用targets.json描述每个芯片和开发板的元信息。路径一般在targets/targets.json里对应的MY_TARGET节点。关键字段包括:
core:内核类型,比如 Cortex-M4、Cortex-M33。supported_toolchains:支持的编译器列表。inherits:继承的自定义目标。macros:编译宏,比如要定义MY_CHIP。device_has:该目标具备的功能,比如SERIAL、I2C、SPI。
有一次我漏了device_has里的某个功能,结果 mbed OS 在编译驱动时直接没编译相关代码,应用层调用接口也不能顺利链接。这些元信息看起来不起眼,但影响的是整个驱动树的裁剪。
6.3 HAL 适配的最小集
委托我已经在移植中反复确认过,最小可运行集其实不大:
- GPIO 相关:
gpio_init、gpio_dir、gpio_write、gpio_read,至少保证能点亮 LED、读按键。 - 串口相关:
serial_init、serial_putc、serial_getc、serial_irq,保证能打通 printf。 - 定时器相关:
timer_init、timer_read、timer_read_us,用来支撑 RTOS tick 或延迟。
很多外设可以先不移植,先把“能打印、能延时、能闪灯、能调 RTOS”跑通,再去逐项补齐 I2C、SPI、PWM、ADC。
但要注意,RTOS 的 tick 默认依赖系统定时器,如果新板卡没有提供us_ticker或lp_ticker的实现,调度器起不来。us_ticker是 mbed OS 里的微妙级计时器,它会作为内核调度的时间基准。我第一次移植时忽略了它,结果线程调度完全乱套,查了好几个小时才定位到是 tick 源没配。
6.4 移植中我踩过的坑
- 编译选项不一致:新板卡用的链接脚本里有一段是给 ARM Compiler 的分散加载语法,换成 GCC 后没有对等处理,导致 RAM 初始化失败。务必检查 startup 文件和链接脚本支持当前工具链。
- 未初始化数据段:跑 RTOS 后,线程栈和系统堆经常放在
.bss,如果链接脚本里没有正确留出空间,系统会在首次进入调度器后瞬间崩。 - SysTick 冲突:如果 HAL 层用自己的 SysTick 做 delay,但 RTOS 也用 SysTick 做 tick,两者会抢占同一个硬件定时器。mbed OS 里应该由
us_ticker和内核协调分配,不要在驱动里私自开 SysTick 中断。 - mbed-tools 的缓存:改完
targets.json之后如果没刷新构建缓存,会一直用旧的配置,表现就是明明改对了还报老错误。我的习惯是先 clean 再 build。
6.5 让新板跑起来的第一支灯
把基础配置做完后,我会先点亮一支 LED 来验证整个工具链和启动流程。做法是写一个极其精简的程序:
#include "mbed.h" DigitalOut led(LED1); int main() { while (true) { led = !led; ThisThread::sleep_for(200ms); } }这根 LED 能闪起来,说明几件事情同时成立:启动文件正确、时钟初始化正常、链接脚本的路由正确、GPIO HAL 实现无误。等这一步跑通了,再上 RTOS 线程和事件,最后再开始做复杂的驱动。
7. 最后说点我的老实话
如果只让我总结一条 mbed OS 源码架构的最深刻体会,那就是:它的分层不是为了漂亮,而是为了让“平台层”和“业务层”能够在真实产品里解耦。HAL 的 C 接口统一了硬件差异,RTOS 的 CMSIS-RTOS2 抽象统一了调度方式,驱动层用 C++ 封装统一了外设操作,测试体系又从外围保证每次改动不会让历史功能悄悄坏掉。四者环环相扣。
我个人的习惯是每移植一个外设驱动,都会先写一个独立于业务之外的 self-test 程序,把它放进测试框架里跑。因为驱动如果只靠主程序的特定逻辑去验证,很难覆盖边界状态和时序问题。而一旦把它变成可重复的测试用例,以后无论是升级 mbed OS 版本还是换芯片型号,都能立刻知道哪些驱动仍然可靠。
另外建议一点:阅读源码不要在 IDE 里看单个文件,要把 HAL 调用链从头跟到尾,从你的业务代码一路跟到寄存器的最后一步。一开始会觉得路径很长,但跟几次之后,你会对整个系统的数据流和资源管理形成肌肉记忆。
这篇就写到这里,希望对正在啃 mbed OS 源码的同行有点帮助。