mbed OS源码架构解析:从HAL到RTOS的驱动与移植实战

发布时间:2026/9/4 13:31:32
mbed OS源码架构解析:从HAL到RTOS的驱动与移植实战 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 和 platformhal 依赖 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 OSHAL 层就是你最需要花心思的地方。不夸张地说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_writeI2C类则持有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 与设备端测试代码通信收集测试结果。htrunGreentea 底层的宿主-目标通信工具负责在 PC 和板卡之间传输测试帧。ICTT编译器和测试工具链的集成层偏构建检查。这个测试体系和传统的“本地跑一个 main 函数”完全不同。mbed OS 的设备端代码会包含一小段测试协议栈板子跑完一条用例后通过串口回报给 PCPC 端再判断通过还是失败最后汇总成报告。这也解释了为什么 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 --parallel1mbedgt就是 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 源码的同行有点帮助。