尧图精选

mbed OS源码架构解析:HAL、RTX5、驱动与硬件在环测试

🕒 发布时间:2026/9/9 6:06:58 📁 来源:尧图网络
最早接触 Arm mbed OS是我从传统 STM32 HAL 库往带 RTOS 的 IoT 节点迁移的时候。第一反应就是源码目录怎么这么乱它不像大家习惯的“标准外设库 用户 Src/Inc”结构而是把 HAL、RTOS、驱动和测试体系拆成好几个独立模块再用平台配置强行组合在一起。当时我花了整整一个周末把这些目录的关系捋清楚之后才发现这套架构其实比很多商用 RTOS 都更有参考价值。这篇文章我想按源码目录的脉络把 mbed OS 里最容易劝退新人的四个部分拆开讲一遍HAL 层如何隐藏芯片寄存器差异、RTOS 用什么内核和线程模型、驱动层怎么在 HAL 之上做二次抽象、测试体系又是如何做到硬件在环自动化回归。如果你正准备从裸机转 RTOS、给团队做嵌入式 OS 选型或者只是好奇一个“面向物联网”的操作系统源码到底怎么组织这篇应该能给你省下不少时间。1. 先把整张图理清mbed OS 分层架构到底在解决什么问题1.1 为什么会有 mbed OS裸机开发的天花板先说痛点。很多人是从 STM32 的 HAL 库或者标准库开始写代码的这类开发方式的典型流程是初始化时钟、配置 GPIO、轮询或中断处理数据、最后在while(1)里堆一个超级大的状态机。前几个外设还好一旦业务里同时出现按键扫描、传感器采集、无线通信、数据上报你会明显感到代码开始失控——不是不能跑是每次加需求都提心吊胆。mbed OS 的目标就是把这个“裸机 外设库”的开发模式往上推一层。它不是一个单纯的外设驱动集合而是一个带实时内核、网络协议栈、安全组件和设备管理框架的操作系统。你写的应用只需要面向rtos::Thread和DigitalOut这类抽象接口底层芯片换了应用代码基本不需要动。这套思路最直接的收益是你在 NUCLEO_F429ZI 上调通的逻辑换成 Nordic nRF52840-DK 时不需要把 GPIO 和定时器代码全部重写一遍。听起来像广告但 mbed OS 的源码组织方式确实是在为这个目标服务的。1.2 分层总览与源码目录的对应关系在 mbed-os 仓库根目录下代码不是按“芯片型号”堆的而是按职责模块划分。我整理了一个我后来一直当索引用的表格目录职责典型内容rtos/实时内核与 C 封装Thread、Mutex、Semaphore、EventFlags、Queuehal/硬件抽象层接口定义gpio_api.h、i2c_api.h、spi_api.h、serial_api.hplatform/基础工具与公共设施Callback、NonCopyable、Span、CircularBuffer、mbed_assertdrivers/面向用户的外设 C 类DigitalIn、DigitalOut、I2C、SPI、PwmOut、BufferedSerialtargets/各芯片厂商的底层实现TARGET_STM32、TARGET_NORDIC、TARGET_NXP等features/网络、BLE、安全等功能netsocket/、ble/、crypto/tools/构建、测试、配置工具mbed_toolchain/、build_api.py、test_api.py这套分层的关键细节是HAL 目录里的头文件是公共接口实际函数实现全在targets/下。比如hal/gpio_api.h声明了gpio_init、gpio_write这些函数但真正操作 STM32 寄存器的代码在targets/TARGET_STM/TARGET_STM32F4/.../gpio_api.c里。mbed OS 在构建时会通过mbed-os/targets/targets.json里的设备描述来选择启用哪些驱动。这块 JSON 会声明当前芯片是否支持I2C、SPI、PWM、ANALOGIN等能力也就是device_has数组。如果你的板子没有 DAC那么 HAL 层中与 DAC 相关的代码就不会被编译进去用户代码如果强行调用也会在编译期报错。这种做法比“把所有外设代码全编进去运行时才发现不支持”要干净得多。知道了这个分层再看 mbed 文档里经常出现的 “HAL” 就不会蒙圈了它不是某个厂商的库函数名而是 mbed OS 对硬件能力的一组契约。上层驱动依赖这组契约编写下层厂商负责用寄存器把它们填上。2. HAL 层拆解硬件差异被接口“吸收”的地方2.1 HAL 并不等于“库函数包”它是一份硬件契约很多嵌入式开发者听到 HAL 的第一反应是“哦就是芯片厂商写好的外设驱动库”。在 mbed OS 语境下这个理解差得很远。ST 的 HAL 库、Nordic 的 nrfx这些都是具体的芯片 SDK而 mbed OS 的 HAL 是一层对板级能力的统一描述它只有函数签名和数据类型没有寄存器操作。举个例子mbed OS 里代表一个 GPIO 对象的类型是gpio_t。不同平台下这个结构体里面放的成员完全不同在 STM32 上可能是GPIO_TypeDef *reg加uint32_t pin在 NXP 的某些芯片上可能是PORT_Type *port加位掩码。但 HAL 接口gpio_init_out(gpio_t *obj, PinName pin)在所有平台上长得一模一样。上层驱动根本不关心gpio_t里面存了什么。我一开始也犯过傻直接在应用层想访问gpio_t内部的寄存器字段结果发现不同类型的芯片编译不过。后来才明白mbed OS 刻意让你不要去关心gpio_t里面的内容因为那会破坏硬件抽象的意义。2.2 从 GPIO 到 DMAHAL API 的设计骨架mbed OS 的 HAL 接口风格偏底层看起来很像 C 语言 SDK。它按外设类型拆成若干xxx_api.h文件每个文件提供初始化、读写、中断控制三个维度的 API。拿 I2C 举例HAL 层接口大体长这样void i2c_init(i2c_t *obj, PinName sda, PinName scl); int i2c_start(i2c_t *obj); int i2c_stop(i2c_t *obj); int i2c_read(i2c_t *obj, int address, char *data, int length, int stop); int i2c_write(i2c_t *obj, int address, const char *data, int length, int stop);UART 的对应接口是serial_init、serial_putc、serial_getcPWM 是pwmout_init、pwmout_period、pwmout_pulsewidth_us。这种接口设计思路类似于 POSIX 对文件操作的抽象把“能做的操作”定义清楚实现的细节由厂商去填充。HAL 层还定义了很多能力宏比如DEVICE_I2C、DEVICE_SPI、DEVICE_ANALOGIN。这些宏在编译时由 target 配置决定。写驱动时如果担心兼容性标准做法是先用宏包一层#if DEVICE_I2C mbed::I2C i2c(PB_7, PB_9); #endif这种方式在多平台源码里很常见。很多开源 mbed 组件为了保证在低配单片机上也能编过都会用DEVICE_XXX做条件编译。2.3 一个外设调用的完整链路从I2C i2c(...)到寄存器如果你只写应用通常直接使用drivers/目录下的 C 类例如I2C i2c(PB_7, PB_9); char addr 0xA0; i2c.write(addr, reg, 1);这行代码背后的调用链是怎样的我实际跟过源码大体是I2C类的构造函数会把PinNamePB_7这种枚举值传给 HAL 层的i2c_init。i2c_init内部首先调用pinmap_pinout(pin, PinMap_I2C_SDA)这会在PeripheralPins.c里查一张引脚映射表找到这个引脚对应的 I2C 外设号和复用功能配置。拿到外设号后再调芯片 SDK 的函数去使能时钟、配置 GPIO 复用寄存器。后续i2c_write就通过操作 I2C 外设的数据寄存器、状态寄存器完成起始信号、发送数据、停止信号。这个过程里最值得学习的是**引脚映射表PinMap**的做法。传统 ST 开发中你自己查数据手册确认哪个引脚能复用成 I2C1_SCL然后手动配置 GPIOmbed OS 把这些信息集中放到了PeripheralPins.c。更换板子时你只需要查这块板子支持哪个PinName接在哪个外设上然后改构造参数即可。如果你做过的项目经常换引脚应该能体会这种“查表代替翻手册”有多爽。代价是你不能任意指定引脚只能在板级支持的映射里选这其实是合理约束。2.4 HAL 层改动带来的坑不要直接面向芯片 SDK 编程我在实际开发里最主要的经验教训是同一份代码里尽量避免混用 mbed HAL 和芯片厂商 HAL。不是说不能而是混用之后代码的可移植性会瞬间消失。比如你为了用一个 ST 特有的低功耗模式直接在 mbed 工程里 include 了stm32f4xx_hal_pwr.h然后调用HAL_PWR_EnterSTOPMode。短期内没问题但哪天你想把应用迁移到 nRF52840 上这段代码就必须全部删掉重写。mbed 的平台配置给你提供了 API 外的统一入口应该优先使用platform/中的DeepSleepLock这类抽象而不是直接调芯片库。那“直接用 ST HAL 写一切”岂不是更顺手如果项目以后不打算换芯片确实可以但 mbed OS 本身不鼓励这种用法。它的价值就在于把底层封装起来让业务逻辑脱离芯片型号。3. RTOS 内核RTX5、CMSIS-RTOS2 与 mbed 线程模型3.1 为什么选择 RTX5 而不是自己维护内核mbed OS 的 RTOS 模块不是完全从零造的一个内核而是基于 ARM 官方 CMSIS-RTOS2 规范封装出来的。CMSIS-RTOS2 定义了一套统一的 RTOS API任何内核只要实现这套 API就能被 mbed OS 的rtos/模块使用。mbed OS 默认使用的实现是 Keil RTX5。选 RTX5 的好处很实在它是 ARM 自家维护的内核对 Cortex-M 内核的调度器、PendSV 异常、SysTick 配置做了深度优化同时 CMSIS-RTOS2 又是开放标准以后即使不用 RTX5换了其他兼容内核应用程序基本不用改。mbed OS 在 RTX5 之上包了一层 C 接口。我们平时写代码直接 includembed.h或者rtos.h用的是rtos::Thread这类类而不是 CMSIS 的osThreadNew函数。这种封装把动态内存、错误处理等细节隐藏掉了对应用开发者更友好。我个人觉得 mbed OS 这层 C 封装是做得比较克制的。它没有为了面向对象而过度设计封出来的类基本是一对一映射 CMSIS-RTOS2 的概念比如Thread、Mutex、Semaphore、EventFlags、Queue、Mail。你如果之前懂一点 CMSIS-RTOS2看 mbed 的线程文档几乎不用重新学。3.2 线程、事件标志、消息队列日常用得最多的组件实际写 mbed OS 应用时真正高频使用的 RTOS 组件并不算多。这里按使用频率大概排个序RTOS 组件典型用途底层对应Thread创建独立任务循环osThreadId_tosThreadNewThisThread::sleep_for非阻塞延时osDelayEventFlags中断里通知线程osEventFlagsSetMutex保护共享外设/总线osMutexAcquireQueueT, N线程间数据传递osMessageQueuePutSemaphore资源计数osSemaphoreAcquire先看一个最简单的线程例子#include mbed.h DigitalOut led(LED1); void led_thread() { while (true) { led !led; ThisThread::sleep_for(500ms); } } int main() { Thread thread; thread.start(led_thread); while (true) { ThisThread::sleep_for(1s); } }Thread一旦start就会在就绪队列里参与调度。sleep_for会让出 CPU让低优先级线程也能运行。如果你在裸机里写过一个delay_ms阻塞主循环对比一下就知道差别有多大。再举个例子。假设你有一个按键接在外部中断引脚PA_0上你希望按键触发后让采集线程立刻做一次传感器采样。最直观的写法是在中断回调里置一个标志位主循环轮询mbed OS 里更推荐的方式是事件标志InterruptIn button(PA_0); EventFlags flags; #define BUTTON_EVENT (1 0) void on_button_fall() { flags.set(BUTTON_EVENT); } void worker() { while (true) { flags.wait_any(BUTTON_EVENT); // 采样传感器、发送数据等不在中断上下文里做 } }这样做的好处非常明显中断回调只负责“set 一个位”真正的业务逻辑全部移到线程上下文中避免了在中断里调用可能阻塞的函数。这也是 RTOS 项目里“中断处理要短”的最佳实践。3.3 线程栈大小、优先级与调度最容易忽略的配置mbed OS 的Thread默认栈大小通常取决于OS_STACK_SIZE在很多 target 上不是很大。如果线程里用了printf、浮点、或者较深的函数调用栈很容易爆。我踩过最惨的一次是这样的一个采集线程里调用了printf输出调试日志然后在一个大结构体数组里做均值滤波程序运行几分钟后就 HardFault。排查到最后才发现是线程栈设小了局部变量把栈挤爆了。从那以后我所有线程都不再用默认栈大小而是明确传参Thread t(osPriorityNormal, 2048); // 2KB 栈如果线程里有浮点运算和打印我一般直接开 4096 甚至 8192。内存够用就不要在这些地方抠否则排查崩溃问题的时间远大于省下的那几 KB RAM。关于优先级建议从简大部分任务设为osPriorityNormal少数实时性要求高的设为osPriorityAboveNormal。mbed OS 属于抢占式调度高优先级线程就绪后低优先级线程会被立刻打断。如果两个线程都做长时间忙等低优先级线程可能长期得不到运行这在实时系统里叫优先级饥饿。3.4 从裸机 while(1) 迁移到多线程的思考方式很多刚接触 mbed OS 的人会把“线程”理解成裸机里多个 while 循环拼在一起。比如原来是“按键扫描 loop 显示 loop 数据上报 loop”的状态机迁移后直接每个 loop 开一个线程。能跑但这不是最优解。线程化真正的价值在于把“由事件驱动的异步处理”自然表达出来。一个按键检测线程等待事件标志、一个传感器线程做周期采样、一个通信线程等待消息队列线程之间通过消息传递而不共享全局变量。这种模型比裸机状态机嵌套要容易维护得多。当然线程多了也有代价全局变量访问要加锁、调试难度增加、栈内存消耗明显。我的建议是不要为了用线程而用线程优先让“传感器采样”“通信处理”“UI 显示”这些天然独立的业务各自成一个线程其余简单逻辑继续用定时器回调即可。4. 驱动层再看一层外设驱动、传感器驱动与设备抽象4.1 从 HAL 到“能用”驱动层还差什么HAL 已经帮你把 UART、I2C、SPI、GPIO 这些“总线能力”封装好了但你要做的产品不是用一条 I2C 总线就完事而是要驱动一个具体的传感器、屏幕或执行器。比如你拿到了 DHT11 温湿度传感器它虽然通过一根 GPIO 线通信但需要按严格的时序读写数据位、解析湿度整数、小数部分还要处理校验和。这些业务逻辑不属于 HAL 的范畴应该放到驱动层。mbed OS 源码里有一个components/目录专门放官方和合作伙伴维护的设备驱动组件比如各种传感器、Wi-Fi 模块、扩展板驱动。这些驱动组件通常依赖drivers/下的 C 类比如I2C、SPI、DigitalOut。用户项目里也可以写自己的驱动类官方没有强制你一定要放哪。4.2 写一个可复用的传感器驱动以 I2C OLED、DHT11、MT6701 为例驱动层写的代码最忌讳一上来就写在main.cpp里。我习惯把每个传感器封装成一个独立的 C 类构造函数接收总线对象指针对外暴露非常简单的读取接口。比如一个 I2C 接口的 OLEDSSD1306 控制芯片驱动基本骨架是class SSD1306 { public: SSD1306(I2C *i2c, PinName reset_pin); bool init(); void clear(); void put_text(const char *text); private: void write_cmd(uint8_t cmd); void write_data(const uint8_t *buffer, size_t size); I2C *_i2c; DigitalOut _reset; };构造函数里保存总线指针而不是直接创建I2C对象是为了让总线可以被多个设备共享。OLED 和磁编码器挂在同一条 I2C 上时如果每个驱动构造函数都创建一个I2C实例很容易导致总线状态混乱。DHT11 这类单总线时序传感器则更麻烦。它要求主机先把引脚拉低 18ms再释放然后读取传感器返回的高低电平脉宽分别代表 bit0 和 bit1。这个时序是微秒级的用InterruptIn很难处理而普通ThisThread::sleep_for最小粒度经常不够。在实际项目里我通常用wait_us 关中断的方式读取或者在允许的芯片上用硬件定时器捕获。这种代码不适合在多个线程里同时调用驱动内部一定要用一个锁保护起来class DHT11 { public: bool read(float humidity, float temperature); private: Mutex _mutex; DigitalInOut _pin; }; bool DHT11::read(float humidity, float temperature) { _mutex.lock(); // 拉低、释放、采样脉宽、解析数据、校验 _mutex.unlock(); }有一个项目里我用 MT6701 磁编码器测角度它是 I2C/SPI 接口输出的。真正让我头疼的不是读原始角度值而是输出噪声和偶尔的跳变。最后在驱动层里加了均值滤波和连续跳变检测保证每次读出来的角度在一个稳定的区间内平滑变化。这类逻辑完全应该在驱动层处理而不是在业务代码里到处 if。4.3 官方组件与第三方库怎样把生态用起来mbed 的组件生态目前不像 Arduino 那么庞大但有价值的驱动库基本集中在mbed-os/components、mbed-os-examples以及第三方仓库。使用第三方库时我的经验是先确认它对应的是哪个 mbed OS 大版本。mbed OS 5 和 mbed OS 6 之间有不少 API 变动网上很多老代码还停留在 OS 5直接拿到 OS 6 上编译会报错。如果你自己写驱动并想开源建议按 mbed 的风格提供下面这些内容构造函数参数是总线对象和引脚不要用全局I2C实例。提供bool init()、bool read()这类显式初始化与读取接口。数据和寄存器访问保持清晰方便后续加锁或异步化。用#if DEVICE_XXX做能力宏保护保证缺少外设的 target 也能编译。4.4 驱动层是否安全与裸机 HAL 直接操作的区别mbed OS 驱动层还有一个隐性好处是安全性。比如DigitalOut会检查引脚是否已经被其他设备占用SPI总线类在部分实现里会避免同一条总线被两个 SPI 对象重复初始化。这一点看似不起眼但在多线程环境下能救你一命。早年我在 STM32 上用裸机 HAL 写代码时曾因为两个模块各自初始化了同一个 SPI 外设导致一个模块改完配置后另一个模块的传输全部乱掉。用 mbed 驱动类后因为总线资源和外设资源由对象管理至少能在设计层面减少“跨模块乱动寄存器”的情况。5. 测试体系Greentea、utest 与硬件在环5.1 为什么要做硬件在环测试嵌入式圈子里有一个很常见的现象代码在开发板上跑得好好的一集成硬件就出问题。传感器数据不对、引脚冲突、极端时序下偶发失效这些靠“编译通过”根本发现不了。mbed OS 为此专门搭了一套硬件在环Hardware-in-the-Loop测试体系关键是 Greentea 和 utest。Greentea 是运行在主机端Python的测试调度工具。它会编译测试固件烧录到目标板通过串口和板上的测试程序通信自动判断测试通过还是失败。utest 是板端测试框架提供了Case、TestSuite这样的组织方式用来把一个个测试用例注册起来。底层断言用的是 Unity 测试框架。这套流程对开发者的价值在于你把板子接上电脑敲一条命令测试固件自动烧录运行结果自动汇总而不需要人为盯着串口判断“数据对不对”。对做驱动库或者平台适配的团队来说这种能力尤其重要。5.2 在板端写一个能自动跑的驱动测试用例假设你写了一个SSD1306OLED 驱动想判断屏幕是否能正常显示“Hello”。最笨的方法是下载固件后肉眼看屏幕但这不能自动化。mbed 测试体系的做法是让测试程序主动检测变量然后用断言汇报。一个最简驱动的 utest 用例长这样#include utest/utest.h #include unity/unity.h #include greentea-client/test_env.h using namespace utest; void test_ssd1306_init() { I2C i2c(PB_7, PB_9); SSD1306 oled(i2c, PA_8); bool ok oled.init(); TEST_ASSERT_TRUE(ok); } utest::v1::status_t test_setup(const Case *const source, const size_t count_of_cases) { GREENTEA_SETUP(10, default_auto); return greentea_test_setup_handler(source, count_of_cases); } Case cases[] { Case(SSD1306 init, test_ssd1306_init), }; Specification specification(test_setup, cases); int main() { return !Harness::run(specification); }这里面的核心是断言和回调协议。板端测试跑完会通过串口拍一个结果给 GreenteaGreentea 再把这个结果汇总输出。如果你在自研测试平台完全可以把这套思路抄过来主机通过串口下发指令板端执行用例后上报 PASS/FAIL。5.3 测试中最容易翻车的三个细节第一串口波特率不一致。Greentea 默认的串口通信波特率很多板子是 9600但你的应用可能已经改成了 115200。如果两个不一致测试程序根本收不到主机指令整个流程卡死在超时。检查mbed_app.json里的platform.stdio-baud-rate是否和 Greentea 配置匹配。第二测试用例之间不要有隐含顺序。比如测试 A 修改了全局状态测试 B 依赖这个状态一旦 Greentea 打乱用例顺序B 就会莫名失败。正确做法是每个用例尽量独立彼此之间的数据交换通过参数传入而不是靠全局变量。第三不要在测试中依赖肉眼观察。很多开发者在测试里写了“请观察 LED 是否闪烁”然后手动确认。这类步骤没法自动化也很难回归。正确的做法是让被测代码把结果通过串口或测试框架断言出来比如 LED 状态最终映射成一个布尔变量输出给 Greentea。5.4 测试体系与源码目录的关系mbed OS 仓库里每个模块都有对应的测试工程比如hal/tests/下有很多 HAL 层的自测用例。这部分代码在源码分析时常常被忽略但反而是学习 mbed 设计思想的好材料。读一下这些测试用例你会知道官方认为每个 HAL 接口的边界行为是什么比如 UART 在 FIFO 满时返回什么、GPIO 中断是否会触发两次。如果你做的是平台移植把硬件在环测试跑通是很有价值的一项工作。它远胜过“我能点灯所以我移植成功”的开发自测。6. 从零搭一个最小 mbed OS 工程工具链、编译与调试6.1 工具链与工程形态的选择现在开始实际的工程搭建。mbed OS 支持多种工具链最常用的是 Arm CompilerARMCC/AC5、AC6和 GCC Arm。具体用哪个主要看你手里的工程生态。老一点的 mbed 工程很多使用 Arm Compiler 5.06 update 7它兼容旧代码和旧启动文件但已经停止更新新开发不建议再选。Arm Compiler 6 基于 Clang标准支持更好但个别老外设库代码可能编译不通过。GCC Arm 是开源首选没有 license 限制配合arm-none-eabi-gcc和cmake也很流行。工程形态上我推荐优先用 Mbed Studio 或者 mbed-cli。Mbed Studio 适合图形界面用户导入 mbed-os 仓库后点一下编译就能跑mbed-cli 更适合服务器、CI、或者习惯命令行的用户。由于 mbed-cli 官方维护已进入维护模式我平时在 CI 里更倾向直接用 CMake 脚本控制 mbed-os 构建但如果只是本地学习mbed-cli 仍然简洁。6.2 快速创建一个可编译的最小工程先用 mbed-cli 创建工程mbed new mbed-demo --create-only cd mbed-demo mbed deploymbed new会帮你生成一个带mbed-os子模块和默认配置的目录mbed deploy拉取子模块依赖。如果你网络不好也可以直接 clone mbed-os 仓库然后在自己的工程根目录写一个mbed_app.json。一个典型的最小main.cpp大概长这样#include mbed.h BufferedSerial pc(USBTX, USBRX, 115200); DigitalOut led(LED1); int main() { printf(mbed OS demo start\r\n); while (true) { led !led; printf(led toggled\r\n); ThisThread::sleep_for(500ms); } }然后在mbed_app.json里覆盖串口波特率和需要的系统配置{ target_overrides: { *: { platform.stdio-baud-rate: 115200, platform.stdio-buffered-serial: true } } }编译并烧录一条命令mbed compile -m NUCLEO_F429ZI -t GCC_ARM -f-f参数会尝试把生成的固件拷贝到板载 DAPLink 磁盘中省去手动拖拽。如果是独立调试器比如 J-Link需要自己用烧录软件把生成的.bin烧进去。6.3 踩过的坑帮助快速定位问题的速查表我最后放一张自己积累的 mbed OS 排错速查表至少帮我节省过几十个小时问题现象可能原因解决方式printf完全没有输出串口引脚不对或波特率不匹配检查开发板的USBTX/USBRX定义与platform.stdio-baud-rate编译报Cannot open source file mbed.h没有把 mbed-os 仓库加入 include 路径或工程结构不对用mbed deploy拉依赖检查编译命令的 target 参数运行一段时间后 HardFault线程栈溢出加大线程栈并开启栈溢出检测MBED_STACK_STATS_ENABLED中断里调用printf后系统卡死在中断上下文做了阻塞/重操作中断里只置EventFlags业务逻辑移到线程代码逻辑正确但换板子后引脚失效PinName 映射和板卡不一致查目标板头文件中的引脚定义改成实际连接的引脚同时使用原生 HAL 和 mbed 时报资源冲突两套代码互相操作同一个外设确认外设管理权分割尽量只用一套 APII2C 传感器偶发通信失败同一个I2C对象被多个线程同时使用无保护给共享总线加Mutex或把通信封装到单一驱动类中其中最值得多说一句的是栈溢出。mbed OS 默认开启了部分栈检测但要看具体 target。如果怀疑栈溢出可以在mbed_app.json打开下面的配置{ macros: [MBED_STACK_STATS_ENABLED1] }然后在程序里调用mbed_stats_stack_get_each来查看各线程栈使用峰值。用数据确定栈大小比拍脑袋分配要可靠得多。调试串口输出还有一个细节BufferedSerial是有内部缓冲的。你在中断上下文里写数据时不会立刻发送只有缓冲区满或者刷新时才会排空。如果调试时发现日志滞后试试直接把printf换成pc.write并且在发送后调用“刷新”机制。6.4 一个小技巧在运行时识别当前 target 和编译信息调试多板工程时我经常需要确认当前跑的固件到底编译的是哪个板级 target。这时候可以不依赖烧录器而是在 main 函数一开始打印编译宏#ifdef MBED_MAJOR_VERSION printf(mbed-os major: %d\r\n, MBED_MAJOR_VERSION); #endif #ifdef TARGET_NAME printf(target: %s\r\n, TARGET_NAME); #endif #ifdef TOOLCHAIN_NAME printf(toolchain: %s\r\n, TOOLCHAIN_NAME); #endif这样每次烧录后串口弹出的第一行就能明确告诉你固件是什么时候、用哪个工具链、为哪块板子编译的。排查“我烧错固件了”这类低级问题特别管用。如果你也是从 STM32 HAL 库这条路转过来学 mbed OS我个人的感受是不要一上来就钻到每个外设的寄存器实现里而是先按HAL - RTOS - drivers - components这条依赖链把代码从下往上读一遍。mbed OS 的架构不一定是你产品的最优解但它的分层方式尤其是“接口定义与具体实现彻底分家”和“测试框架与硬件在环挂钩”这两件事放在任何中型嵌入式项目里都值得借鉴。后面我做自研 RTOS 项目的时很多模块划分思路其实都受了 mbed OS 源码布局的影响。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →