我接触mbed OS的时间不算早,第一次认真翻它的源码是在一个多传感器网关项目上。当时要用Cortex-M4的板子同时跑蓝牙、几个数字传感器和一个简易的本地决策逻辑,裸机轮询已经撑不住,但手头几个厂商的SDK写法又完全不一样,一个外设初始化搞出好几种风格,移植起来相当痛苦。后来发现mbed OS把HAL、RTOS、驱动和测试体系整个串成了一条清晰的链路,这才算真正搞清楚一套物联网操作系统应该怎么组织代码。
如果你也想把这套架构吃透,或者正在纠结“要不要在项目里用mbed OS”,这篇内容应该能帮上忙。我会从源码目录结构讲起,拆HAL层如何屏蔽硬件差异、RTX 5内核怎么调度线程、驱动框架如何封装外设,再聊到Greentea测试体系和交叉编译工具链。整个过程不追求面面俱到,重点放在“源码为什么这么组织”和“真上板子会踩什么坑”这两件事上。
1. mbed OS整体架构与源码目录拆解
1.1 先搞清楚mbed OS到底是个什么东西
很多人第一次看到mbed OS,容易把它跟Arduino、RT-Thread、FreeRTOS混在一起。其实它更偏一个“物联网场景的操作系统全家桶”,不是单纯的RTOS,也不只是硬件抽象库。它由Arm官方推动,主要跑在Cortex-M系列的MCU上,源码开源在GitHub的ARMmbed/mbed-os仓库里。
mbed OS解决的痛点是嵌入式开发里最老生常谈的“可移植性”。你换一块MCU,外设寄存器不同、时钟树不同、中断向量不同,厂商SDK一般只管自家芯片,你的业务代码想换个平台就得跟着大改。mbed OS的做法是把这一切统一成一套API:操作GPIO就是DigitalOut、读ADC就是AnalogIn、发串口就是Serial,上层代码不用管底层是NXP还是ST还是Nordic。
1.2 源码根目录就是一张地图
我第一次把mbed-os仓库clone下来,看一眼目录直接被吓住,文件数量实在太多了。不要慌,真正核心的目录也就那么几个:
| 目录 | 作用 | 阅读优先级 |
|---|---|---|
| hal/ | 硬件抽象层,定义统一的外设API头文件,比如spi_api.h、gpio_api.h | 高 |
| rtos/ | RTOS的C++封装,包装了RTX 5内核,提供Thread、Semaphore、Queue等 | 高 |
| drivers/ | 设备驱动类,比如DigitalOut、I2C、SPI、PwmOut,大部分业务代码只跟这层打交道 | 高 |
| platform/ | 平台底层,包括sleep、critical section、非易失存储等 | 中 |
| targets/ | 各家芯片的平台代码,真正的寄存器操作都在这里,按厂商分目录 | 高(看具体芯片时) |
| features/ | 各种通信协议栈和组件,比如BLE、LoRa、WiFi、NFC | 按需 |
| events/ | 事件队列EventQueue,mbed OS异步编程的核心 | 高 |
| tests/ | 测试代码,包括单元测试和Greentea测试用例 | 中 |
| tools/ | 构建脚本、测试脚本,mbed CLI的基础 | 低 |
这其实是一套分层的思路:业务代码 -> drivers或平台库 -> HAL API -> targets平台实现。往下看源码的时候,心里装着这条链路,就不会迷路。
1.3 这套分层的好处到底在哪
刚开始我觉得mbed OS的目录有点“重”,一个blinky都要带一堆依赖。但真在多个平台间移植过项目之后,才明白这种分层是在用“结构复杂度”换“维护省心”。
举一个实际例子:用mbed OS开发时,你的业务代码只需要处理DigitalOut、Thread、EventQueue这类对象。当产品改版换了主控芯片,同样的代码重新编译一遍,换一下targets平台对应的目标型号,很大概率就能跑起来。如果某个外设在新平台上有特殊处理,只需要改HAL层对应的几个实现文件,业务层完全不用动。
对照一下Linux内核,其实思路是类似的:驱动层面向硬件,内核抽象层面对驱动,系统调用面对应用。mbed OS把这套思想搬到了单片机世界,只不过规模和复杂程度小了几个量级,但骨架是通的。这也是为什么很多人建议学mbed OS源码时,不要只满足于API调用,要顺着调用链往targets里钻,看寄存器是怎么被压到统一接口底下的。
2. HAL层到底抽象了什么,又没抽象什么
2.1 HAL在mbed OS里的准确边界
HAL的全称是Hardware Abstraction Layer,mbed OS的hal目录里放的都是函数声明和头文件,真正的实现散落到targets目录下各家厂商的代码里。这种设计与Linux内核的include/linux和driver/关系很像:接口统一由上层定义,实现由各家平台自己完成。
具体看几个典型的HAL接口:
// hal/spi_api.h void spi_init(spi_t *obj, PinName mosi, PinName miso, PinName sclk, PinName ssel); void spi_format(spi_t *obj, int bits, int mode, int slave); void spi_frequency(spi_t *obj, int hz); int spi_master_write(spi_t *obj, int value);上层drivers里的SPI类,最终就是调用这些API,drivers只负责把对象和状态管理封装好,真正的寄存器时光交错、时钟分频这些细节全部在targets平台代码里。
这套设计的核心价值是统一。不同厂商的SPI初始化方式差别很大,有的时钟极性和相位编号还不一样,但因为HAL接口固定了,上层感知不到这些差异。
2.2 一条调用链追到底,才能理解栈结构
我建议你亲手追一条调用链。以点灯为例,业务代码写的是:
DigitalOut led(LED1); led = 1;点击去看到的就是drivers/DigitalOut.cpp,里面封装了一个mbed::DigitalOut类,构造函数最终调用hal层的gpio_init和gpio_write。再往targets里找,比如K64F平台的gpio_api.c,里面会写:
void gpio_init(gpio_t *obj, PinName pin) { // 查PinMap表,获取PORT和PIN号 // 使能时钟 // 配置GPIO模块寄存器 }这个过程就是把“引脚号”翻译成“寄存器地址加位操作”的过程。也就是说,你业务代码里的LED1,最终会变成某个PORT的第几根引脚,再配上方向寄存器和数据寄存器的值。
所以一些搜热词里常见的“hal库驱动dht11”、“hal库驱动oled代码”,本质上就是在这套抽象上写驱动。你写的驱动面向的是mbed OS的HAL API,而不是某一款芯片的寄存器,这样换平台时驱动基本不用改。
2.3 引脚复用表PinMap,HAL的隐形关键
顺着上面K64F的gpio_init继续挖,会发现mbed OS里到处是PinMap表。这个表定义了“芯片引脚号 -> 外设功能 -> 寄存器配置”的映射关系,本质上是一张静态查找表。
// targets/TARGET_Freescale/TARGET_K64F/PeripheralPins.c static const PinMap PinMap_SPI_SCLK[] = { {PTC5, SPI_0, ALT1}, {PTD1, SPI_0, ALT2}, {PTE2, SPI_1, ALT2}, {NC, NC, 0} };每当调用spi_init,HAL实现会通过pinmap_find_peripheral这类函数在这张表里查,找当前引脚对应的SPI外设编号和复用功能(ALT几)。找不到就直接报错或返回NC。
这个机制尤其容易出问题,比如同一根引脚同时被PinMap表和GPIO初始化用了,结果引脚被占用或者复用冲突。排查这类问题不要只看代码逻辑,先检查PinMap表是否符合芯片手册的引脚功能。早期我在I2C上栽过跟头,代码看起来没错,但SDA和SCL两根线正好被PinMap提到了不同外设上,最后只能换引脚解决。
2.4 中断回调机制:HAL层怎么处理异步事件
除了常规外设操作,HAL层还负责中断。以GPIO中断为例,hal/gpio_irq_api.h里定义了:
void gpio_irq_init(gpio_irq_t *obj, PinName pin, gpio_irq_handler handler, uint32_t id); void gpio_irq_set(gpio_irq_t *obj, gpio_irq_event event, uint32_t enable);上层drivers里的InterruptIn类会注册一个handler,并把自己的this指针作为id传下去。底层中断触发时,HAL实现会把外设中断标志清除,然后通过这个handler和id回调到具体的C++对象。
这种设计在嵌入式里非常常见,相当于给底层中断回调做了一个上下文桥接。由于HAL内部拿到的是id(一般是对象指针),需要非常小心对象的生命周期,如果InterruptIn对象被销毁了但中断仍然触发,就会产生悬垂指针。我在项目里会统一保证中断对象的生命周期覆盖整个使用期,绝不在局部函数里创建中断对象。
3. RTOS内核:RTX 5在mbed OS里的正确打开方式
3.1 RTX 5与CMSIS-RTOS2,还有那层C++外壳
mbed OS的RTOS核心不是自己从零发明的,它用的是Keil RTX 5内核,并且遵循CMSIS-RTOS2标准接口。CMSIS-RTOS2是Arm Cortex-M软件接口标准里定义的一组RTOS API,RTX 5是具体实现。mbed OS又在这个C接口外面包了一层C++类,就是我们日常用的rtos目录下的东西。
| mbed OS C++类 | CMSIS-RTOS2底层API | 功能 |
|---|---|---|
| rtos::Thread | osThreadNew | 创建和管理线程 |
| rtos::Mutex | osMutexNew | 互斥锁,保护临界资源 |
| rtos::Semaphore | osSemaphoreNew | 信号量,任务同步 |
| rtos::EventFlags | osEventFlagsNew | 事件标志,多事件等待 |
| rtos::Queue | osMessageQueueNew | 消息队列,线程间传递数据 |
| rtos::Mail | 对应osMessageQueue + 内存池 | 封装了一块共享内存的消息队列 |
平时写业务代码基本用C++类就够了,但只要底层一出问题,比如线程创建失败、信号量超时,就得去查CMSIS-RTOS2的返回值和RTX 5的配置。
3.2 线程优先级与调度策略,别一上来就乱设优先级
RTX 5是一个抢占式实时内核,默认支持时间片轮转调度。每个线程都有优先级,mbed OS里常用的优先级包括:
osPriorityIdle(最低)osPriorityLowosPriorityNormalosPriorityHighosPriorityRealtime(接近最高,仅供实时任务)
线程创建时如果不指定栈大小,会使用默认配置。这个默认值在mbed_app.json或mbed-os源码的rtos/include/mbed_rtos_conf.h相关位置定义。注意一点,不要盲目调高所有线程优先级,这会导致低优先级线程饿死。实际项目中我通常会遵循“中断服务程序之外只有两到三个优先级”的原则,把实时控制放Realtime,把通信和业务逻辑放Normal,把后台统计和日志放BelowNormal,这样调度压力小,也容易排查问题。
线程函数的签名是这样的:
#include "rtos/Thread.h" void led_thread() { while (true) { led = !led; ThisThread::sleep_for(500ms); } } int main() { rtos::Thread thread; thread.start(led_thread); // 主线程继续做别的事 }Thead对象还可以指定栈大小和优先级:
rtos::Thread thread(osPriorityNormal, 4096, nullptr, "led_thread");3.3 线程同步的典型错误:在中断里调用阻塞API
嵌入式开发里最典型的问题之一,就是在中断服务程序里调用信号量获取或延迟函数。mbed OS和RTX 5也有对应的限制,中断上下文里只能调用带_ISR后缀或明确支持中断的API,比如Semaphore::release是支持从ISR调用的,但获取信号量绝对不能在中断里阻塞等待。
我踩过的另一个坑是ISR里调用了printf,导致系统直接卡死。UART在中断上下文里阻塞输出,很快把整个RTOS打断,原因就是printf内部锁定了互斥量,而该互斥量可能已被当前被中断的任务持有,产生了死锁。mbed OS里禁止ISR与普通线程共享同一个锁,要么用无锁队列、要么用EventQueue把中断事件“搬”到线程上下文处理。
3.4 EventQueue:异步编程的主力,比裸队列好用
mbed OS的events/EventQueue组件可以理解为一个“线程安全的事件调度器”,它允许你把函数和参数封装成一个事件,然后放入队列,在指定上下文中按顺序或延时执行。这个设计非常适合处理中断下半部分、按键消抖、定时采样等场景。
典型的用法是:
#include "events/EventQueue.h" #include "rtos/Thread.h" static events::EventQueue queue(32 * EVENTS_EVENT_SIZE); void sensor_read() { float value = sensor.read(); printf("sensor: %.2f\r\n", value); // 每100ms再调度一次自己 queue.call_in(100ms, sensor_read); } int main() { rtos::Thread t; t.start(callback(&queue, &events::EventQueue::dispatch_forever)); queue.call_in(100ms, sensor_read); }这个模式比一个while循环里轮询要清晰得多。事件队列内部由RTOS消息队列驱动,EventQueue可以跨线程调用,也可以在ISR中通过queue.call_from_isr来投递事件。这样中断里只做“记录哪个事件发生了”,具体耗时操作全部回到线程上下文去执行,避免了中断处理得太久。
EventQueue在实际项目中还有一个隐藏好处:它可以控制所有周期任务集中在单独线程执行,你只需要关心这个线程的优先级,而不必给每个定时器任务都开线程,内存占用和调度开销都会减少。
4. 驱动框架与设备管理,怎么理解drivers层
4.1 drivers目录不是Linux设备驱动,但思想是通的
注意一点,mbed OS的drivers目录跟Linux里的设备驱动不是一个概念。mbed OS的drivers是一组C++类,它们封装了具体外设,提供面向业务的对象化接口。Linux设备驱动强调设备模型、device/driver匹配、sysfs、devicetree,而mbed OS强调的是“几行代码快速操作一种外设”。
| 常见驱动类 | 外设 | 典型用法 |
|---|---|---|
| DigitalOut / DigitalIn | GPIO输出/输入 | led、按键、继电器控制 |
| AnalogIn | ADC | 电位器、电压采样 |
| PwmOut | PWM输出 | 舵机、LED调光 |
| Serial / BufferedSerial | UART | 串口通信、调试日志 |
| SPI / I2C | 总线通信 | 传感器、OLED、EEPROM |
| CAN | CAN总线 | 车载/工业现场 |
这些类通常薄薄一层,比如DigitalOut就是把HAL的gpio函数包起来,构造时调用gpio_init,写值时调用gpio_write。虽然简单,但带给业务层的提升是巨大的,你不需要记忆寄存器地址,也不需要关心时钟使能。
4.2 自己写一个驱动,接入mbed OS的思路
实践中很多人想扩展mbed OS的驱动,比如驱动一颗DHT11或者一颗OLED屏。写法上不复杂,只需要把外设操作封装成一个类即可。
以DHT11为例(老生常谈,但很经典):
class DHT11 { public: DHT11(PinName pin) : _pin(pin) {} bool read() { // 1. 用DigitalInOut配置引脚 // 2. 主机发起起始信号 // 3. 读取50us电平变化,解析40bit数据 // 4. 校验 return _humidity != 0; } float get_humidity() { return _humidity; } float get_temperature() { return _temperature; } private: DigitalInOut _pin; float _humidity; float _temperature; };如果你要追求更好的扩展性,可以让这个类继承某个抽象接口,统一放到一个components或drivers子目录里,但mbed OS本身对这类自定义驱动没有强制规范。我的经验是:保证构造函数初始化完硬件、类不持有全局静态状态、方法里不要阻塞太久,这三点做到,驱动就足够健壮了。
如果你要给这个驱动加测试,那就轮到下一章的测试体系登场。
4.3 设备“注册”机制与Linux字符设备框架的区别
很多学过Linux驱动的人会找mbed OS里的设备号、register_chrdev、删掉节点这类东西,结果找不到。原因是mbed OS的设备管理极其轻量:每个驱动类就是一个C++对象,你定义一个对象它就“注册”了一个设备。没有设备树、没有auto-probe、没有/sys目录。
相比之下,Linux的字符设备驱动框架强调用户态通过open/read/ioctl访问设备,mbed OS更接近“裸机面向对象封装”的路线。这两者不矛盾,如果你在mbed OS上实现的是一个大系统,完全可以借鉴Linux思想:定义统一的设备访问接口、做注册表、支持运行时查找,但大部分单片机项目不需要这么重。
5. 交叉编译、工具链选型和构建系统
5.1 Arm Compiler 6,还是 GCC Arm Embedded?
mbed OS官方支持的编译工具链主要有这几种:
| 工具链 | 命令行标识 | 特点 | 适用场景 |
|---|---|---|---|
| Arm Compiler 6 | AC6 | Arm官方,Clang前端 + Arm优化器,生成代码效率高 | 量产项目、商业开发 |
| Arm Compiler 5 | AC5 | 老牌armcc,兼容老工程 | 老项目迁移、特定保守场景 |
| GCC Arm Embedded | GCC_ARM | 免费开源,社区广泛,内存溢出检测更成熟 | 学习、开源项目、实验 |
Arm Compiler 5.06u7这类历史版本现在仍有存量项目在用,但新项目建议直接上Arm Compiler 6,不仅优化效果好,而且mbed OS新版本对于GCC和AC6的适配更完善。GCC的好处是完全免费,而且我周围不少人用GCC配合OpenOCD和VSCode做调试,流程很顺手。如果只是学习,优先GCC_ARM,别在工具链上花太多精力。
5.2 mbed CLI的那些事
mbed CLI是mbed OS最常用的构建入口。先说一个最直观的例子,编译一个目标板程序:
mbed compile -t GCC_ARM -m K64F这里的-t指定工具链,-m指定目标平台。命令执行时,mbed会先解析所有源码依赖,再把编译链接过程跑完,最后生成bin文件,直接拖进DAPLink虚拟U盘就能烧录。
如果你新建了一个程序,要用到mbed OS:
mbed new my_project cd my_project mbed deploymbed new会创建空工程,mbed deploy会把mbed-os仓库和lib文件夹里的库下载下来。这里很容易遇到的问题就是你改完mbed-os里的文件再执行一次mbed deploy,改动会被恢复,所以我建议如果你要修改源码,用git管理自己的分支,或者明确区分“自己的驱动”和“mbed OS库”。
5.3 交叉编译的本质与链接坑
交叉编译,本质上是在x86主机上生成ARM架构的机器码。过程看起来只是调用arm-none-eabi-gcc,但背后藏了几个容易踩的坑:
- 浮点调用约定:带FPU的Cortex-M4/M7要指定
-mfloat-abi=hard,Cortex-M0/M3则用软浮点,mbed OS会根据target自动调整,但你单独编译一个库时容易乱。 - 启动文件和链接脚本:mbed OS使用自己的一套启动文件和链接脚本,不要替换成裸机的。
- 系统调用和C库:由于没有操作系统,printf最终会需要一个
_write或_sys_write钩子,mbed OS提供了对应的重定向函数,但如果你用自己的C库,要确保这些符号连接正确。
我在一次项目中倒腾过升级包,遇到的就是编译工具链版本不一致导致二进制不兼容的问题。从那以后我都坚持“同一套工具链完成所有功能模块编译”,绝不混合GCC和AC6产物。
5.4 从在线编译器到本地CLI的迁移
很早之前mbed有在线编译器,直接网页编译,现在官方已经不再主推,本地cli才是正确做法。本地环境搭建建议用官方提供的mbed CLI安装脚本或pip安装,常见问题就是Python版本不兼容。
mbed CLI 2(基于CMake)是新一代思路,与mbed CLI 1不同,适合有一定CMake基础的人。实际上做产品级开发,我反而建议理解mbed CLI背后的CMake逻辑,这样CI/自动化编译脚本就好写了。
6. 测试体系:从单元测试到Greentea硬件在环
6.1 mbed OS测试分几层
mbed OS在tets目录里提供了相对完整的测试体系,不是简单的printf对比输出。大致分三层:
- 单元测试(UNITTESTS):不依赖硬件,在主机上用宿主测试框架跑逻辑函数。
- 功能测试(Greentea):目标板上跑固件,主机通过串口跟板子交互,判断测试是否通过。
- 系统测试/集成测试:多个模块组合,在目标板上跑复杂场景,用于产品验证。
对于驱动开发者来说,Greentea是最常用的一层。它解决了“板子上的程序怎么自动验证”这个问题:目标板上烧录一个带有测试固件的bin,测试固件里注册一组测试用例;上位机Greentea工具通过DAPLink串口跟板子通信,板子打印结果,上位机解析判断PASS还是FAIL。
6.2 Greentea怎么用
假设你想跑mbed os自带的某个测试用例,先进入测试目录:
mbed test -t GCC_ARM -m K64F --greentea这条命令会扫描当前工程里的测试用例,编译并烧录到目标板,然后自动开始跑。串口协议是mbed OS约定的,你不需要手动处理UART,Greentea工具会自动识别。
自己在工程里添加Greentea测试用例也很简单,在TESTS或features/tests之类目录下创建测试目录即可。编译系统会自动把特定命名形式的目录标记为测试套件。
6.3 utest的写法,以驱动测试为例
mbed OS的Greentea测试用例通常使用utest框架来组织。先给一个简化的测试用例代码:
#include "utest/utest.h" #include "unity/unity.h" #include "greentea-client/test_env.h" using namespace utest; void test_led_on() { DigitalOut led(LED1); led = 1; TEST_ASSERT_EQUAL(1, led.read()); } utest::v1::status_t greentea_setup(const size_t number_of_cases) { GREENTEA_SETUP(10, "default_auto"); return greentea_test_setup_handler(number_of_cases); } Case cases[] = { Case("LED ON", test_led_on), }; Specification specification(greentea_setup, cases); int main() { return !Harness::run(specification); }这里用到了utest和unity两个组件。utest负责组织用例和报告结果,unity提供断言宏。Greentea通过串口把结果上报给上位机。你在主机上执行mbed test,就能看到半自动化的测试汇总。
这个框架的启发意义在于:做嵌入式开发时,硬件测试不能停留在“自己点两下看现象”。凡是可重复、可自动化的验证都应该固化成测试用例,不然后期回归测试会非常痛苦。
6.4 常见测试场景与热词回归
搜索热度里有很多“rtos测试”、“核儿rtos测试”、“hal库测试”之类字眼。刚上手的人容易把RTOS测试想成大工程。实则常见的就是几类:
- 基础RTOS功能测试:创建线程、信号量释放等待、消息队列收发、优先级抢占,用持续时间、返回值和断言判断成败。
- HAL外设测试:GPIO电平翻转测试、UART回环测试、SPI读写Flash测试、I2C扫描总线上的设备地址。
- 驱动集成测试:比如DHT11读取温湿度,连续采集多次,验证数据落在合理范围。
- 中断响应测试:测量中断到某个线程唤醒的时间间隔,判断实时性是否达标。
Greentea对测试的好处是自动收集结果,你可以把大量测试固件跑一遍,一次拿到一整份报告。在量产前的那段时间,回归效率提升非常明显。
7. 踩坑实录、源码阅读路线与一些收尾经验
7.1 从零读mbed OS源码的推荐顺序
如果是新手,我建议不要一上来就啃内核调度算法,而是顺着“业务 -> drivers -> HAL -> targets”这条链往下走。
第一步,把blinky程序编译并烧录到板子上,感受整个流程。第二步,改成用DigitalOut点灯,然后单步或加打印追进去,看DigitalOut构造时做了什么。第三步,换成带中断和线程的程序,比如按键触发线程打印,感受RTOS上下文变化。第四步,打开EventQueue,把一个周期采样任务用事件队列实现,理解异步编程。第五步,再回头去读RTX 5的信号量和消息队列实现。
这条路走下来,你会对mbed OS的架构有整体认识。不建议先去读targets里的startup文件或链接脚本,那是细节,不是主线。
7.2 三个我亲身踩过的坑
第一个坑:引脚冲突。外设初始化调用失败,程序直接assert。排查半天,最后发现是同一个引脚被两个HAL初始化了,一个是I2C一个是GPIO。mbed os不是每次都能检查引脚复用冲突,所以定义引脚前必须翻PinMap表。
第二个坑:线程栈溢出。默认栈大小在复杂任务里不够,特别是你用了一些递归或者大结构体变量。现象很迷惑,可能运行十分钟后才崩,也可能是某个函数调用后突然HardFault。解决办法有两个,一是直接给线程指定足够大的栈,二是在调试配置里开启栈溢出检测。用GCC编译时还可以在链接脚本里增加栈保护区功能。
第三个坑:工具链差异导致的兼容问题。AC5编译的库跟AC6编译的程序混用,经常出现ABI兼容问题。实际切工具链时必须整体重新编译,特别是mbed-os这种大仓库,混用的后果很隐蔽,跑起来偶尔崩溃,查错非常讨厌。我后来统一了构建脚本和CI,才彻底解决。
7.3 现在的mbed OS还值得学吗
这是很多人会问的问题,尤其是看到mbed OS 6.x已经停止更新的消息。我的看法是:作为项目选型,新项目如果追求长期维护,可以考虑其他方案,但作为源码学习的对象,mbed OS的价值依然很高。
因为它把一个物联网操作系统的“标准洗牌”摆在了你面前:HAL该怎么抽象、RTOS怎么和框架结合、驱动类的接口怎么设计、测试怎么自动化、配置化怎么组织。这些架构经验即使在FreeRTOS、RT-Thread、Zephyr或者各家厂商的SDK里,也依然适用。
如果有时间,我建议除了追代码,还应该把数据手册翻一翻,因为mbed OS的HAL隐藏了很多芯片细节,想排查硬件问题,最终还得回到参考手册。源码和手册对照着读,嵌入式水平提升会非常快。
最后分享一个小技巧:读mbed OS源码时,善用“被动调试”的手段。与其在那干看,不如在业务代码里故意制造一个外设错误,然后利用RTOS的assert和错误回调把调用栈打出来,再顺着栈往回查源码,你会在最短时间内搞懂关键路径。这一点我屡试不爽,省掉了很多啃代码的时间。