ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入 mbed OS:HAL 层、RTOS 内核与驱动架构全解析

深入 mbed OS:HAL 层、RTOS 内核与驱动架构全解析 1. 拿到 mbed OS 源码以后我建议你先别急着编译很多伙伴拿到 mbed OS 的源码仓库第一反应是找main.cpp然后试图从入口一路往下读。这个方向本身没错但 mbed OS 和你在 STM32CubeMX 里生成的裸机工程完全是两种物种。它不是一个从 main 开始顺序执行的程序而是一个运行在 Cortex-M 内核上的小型分布式操作系统。你在 ARM 官方文档里看到的 mbed OS 定位是面向物联网边缘节点的安全连接平台但我更愿意把它看作一套HAL RTOS 安全通信栈 驱动框架的集合体。我用一个实际例子说明问题。你在 GitHub 上 clone 下 mbed-os 仓库后第一眼看到的是大量目录hal/、rtos/、drivers/、platform/、connectivity/、targets/、features/、tools/每个目录下还有嵌套的子目录。很多人到这里就慌了——几千个文件从哪里下手我的建议是先把targets/目录忘掉。targets/是各芯片厂商比如 ST、NXP、Nordic移植代码的聚集地里面大部分是针对某个具体芯片的底层寄存器操作对于想理解协议栈的人来说这是一个迷宫。你真正应该先看的是这四块目录作用关注理由hal/硬件抽象层 API 定义了解 mbed OS 如何统一 GPIO/UART/SPI/I2C/PWM/ADC 等外设接口rtos/基于 CMSIS-RTOS2 的线程封装了解 mbed OS 的线程、信号量、队列、事件标志如何工作drivers/面向用户的 C 驱动类学习继承-组合设计模式在嵌入式驱动中的应用platform/平台基础模块了解回调机制、低频时钟、看门狗等基础服务把这些目录读明白你再看其他部分就会轻松很多。因为 connectivity、security、storage 这些高阶模块最终都要调用 HAL 和 RTOS 提供的底层服务。底层服务的地基不看清上层就是空中楼阁。另外提醒一下mbed OS 版本演进后目录结构有过调整我下面讲的以 mbed OS 5.x 和 6.x 的通用结构为主个别小差异不影响整体理解。拿到源码后建议先切到一个 release 分支再开始读master 分支可能处于开发状态不好跟踪。2. HAL 层mbed OS 真正藏在地基里的设计2.1 HAL 到底是什么和 STM32 HAL 库的差别这里必须先做一个概念澄清因为 H A L 在嵌入式圈子里是个被用烂的词。你在 STM32 项目里用的 STM32 HAL 库严格来说是外设驱动库它把寄存器操作封装成了函数比如HAL_GPIO_WritePin()、HAL_UART_Transmit()。而 mbed OS 的 HAL是一组接口规范它规定每个平台必须实现哪些底层函数至于这些函数内部用寄存器方式还是用厂商 SDK 方式实现mbed OS 不管。打个比方STM32 HAL 库是某个家具厂生产的椅子而 mbed OS HAL 是一把椅子的设计图纸。只要图纸上要求的尺寸、承重达标你用木头、金属还是塑料做是工厂自己的事。mbed OS 的 HAL API 分布在hal/目录下的头文件中常见的接口组有gpio_api.hgpio_init()、gpio_write()、gpio_read()serial_api.hserial_init()、serial_putc()、serial_getc()spi_api.hspi_init()、spi_master_write()、spi_slave_write()i2c_api.hi2c_init()、i2c_write()、i2c_read()pwmout_api.hpwmout_init()、pwmout_period()、pwmout_write()analogin_api.hanalogin_init()、analogin_read_u16()每个 API 前缀后面都接具体功能命名非常规整。重要的是这些 API 的实现在哪在targets/目录下。拿 STM32 系列来说实现在targets/TARGET_STM32/下对应的PeripheralPins和mbed_xxx文件里。2.2 从头文件到硬件寄存器HAL 的完整调用链我以 GPIO 为例给你拆一条完整调用链这样你对 mbed OS HAL 的运转机制就有体感了。第一个环节是面向用户的 C 类位于drivers/目录下的DigitalOut.hclass DigitalOut { public: DigitalOut(PinName pin) : gpio() { gpio_init(gpio, pin, PIN_OUTPUT); } void write(int value) { gpio_write(gpio, value); } int read() { return gpio_read(gpio); } DigitalOut operator (int value) { write(value); return *this; } operator int() { return read(); } private: gpio_t gpio; };这个类本身几乎没有逻辑它只是保存了一个gpio_t结构体然后转发调用gpio_init()、gpio_write()、gpio_read()。这里的gpio_t是什么在 Cortex-M 平台上它包含一个指向寄存器基地址的指针和引脚号typedef struct gpio_s gpio_t; struct gpio_s { uint32_t mask; PinName pin; PinFunction function; void *reg_data; };第二步是gpio_api.c的实现。在targets/TARGET_STM32/TARGET_STM32F4/gpio_irq_api.c这类文件里你会看到void gpio_write(gpio_t *obj, int value) { if (value 0) { if (obj-reg_data ! NULL) { GPIO_TypeDef *port (GPIO_TypeDef *)obj-reg_data; port-BSRR (uint32_t)(obj-mask 16); } } else { if (obj-reg_data ! NULL) { GPIO_TypeDef *port (GPIO_TypeDef *)obj-reg_data; port-BSRR obj-mask; } } }看到了吗在这个层次就已经是寄存器操作了。用过 STM32 标准外设库的伙伴应该非常眼熟BSRR寄存器低 16 位用于置位高 16 位用于复位写 1 生效写 0 无操作——这是 ST 全系 GPIO 的统一玩法。第三步是关键gpio_t里的mask和reg_data是怎么来的这就要看mbed_sdk_init()和引脚映射机制了。每个平台的PeripheralPins.c文件里维护了一张巨大的引脚功能映射表记录哪个引脚可以复用为哪个外设的哪个通道。当你调用DigitalOut(pin)时mbed OS 会去这张表里寻找该引脚然后配置 GPIO 模式设置reg_data指向该引脚所在的 GPIO 端口基地址同时根据引脚序号计算出mask值。所以 HAL 层总结起来就是三件事定义统一的、平台无关的 API 头文件每个 target 提供 API 的底层实现内部直接操作寄存器维护引脚与外设功能之间的映射关系。2.3 在 mbed Studio 里实测 GPIO HAL API如果你在 mbed Studio 里建了一个新工程可以直接在main.cpp里写这么一段测一下 HAL API 是否正常#include mbed.h DigitalOut led(LED1); int main() { while (true) { led 1; ThisThread::sleep_for(500ms); led 0; ThisThread::sleep_for(500ms); } }这段代码虽然简单但背后完成了DigitalOut构造时调用gpio_init()通过引脚映射找到 LED1 对应的 GPIO 端口和 pin把寄存器的MODER配置成输出模式然后led 1触发gpio_write()去写BSRR寄存器。如果你用示波器量 LED1 引脚就能看到 500ms 周期的方波。很多人在这里会问LED1又是什么它在PinNames.h中定义是平台相关的枚举值。你切到不同的开发板LED1映射的引脚会不一样但用户代码不需要改动——这就是 HAL 的威力。你写的DigitalOut led(LED1)可以在 ST 的板子上跑也可以在 NXP、Nordic 的板子上跑只要板子定义了这个枚举。实用小提示如果你要驱动自己的 GPIO别直接传裸数字引脚号比如DigitalOut mypin(PB_0)这类宏定义同样在PinNames.h里要优先使用它们保持代码可移植。3. RTOS 内核mbed OS 的线程机制不只是会 new Thread3.1 CMSIS-RTOS2 与 mbed RTOS 的父子关系mbed OS 的 RTOS 部分很多人误以为它是 ARM 自己从零写的一套内核。实际上mbed OS 的线程调度底层是CMSIS-RTOS2 规范是 ARM 制定的 RTOS 标准 API而 mbedrtos/目录是用 C 对 CMSIS-RTOS2 做的封装。标准库里有两个实现可以做后盾对于没有 MMU 的 Cortex-M 系列默认用的是RTX5——这是 ARM 官方为 Cortex-M 写的 RTOS 内核百分之百是 CMSIS-RTOS2 的实现参考。此外 mbed OS 也能通过配置选择其他内核不过你实际用的时候大多数情况就是 RTX5。所以你在rtos/里看到的Thread、Mutex、Semaphore、Queue、EventFlags这些类最终都会调 CMSIS-RTOS2 的osThreadNew()、osMutexAcquire()、osSemaphoreAcquire()这些 C 接口。我知道有伙伴会问既然这么麻烦为什么 ARM 不直接提供 C 接口给用户用原因很简单CMSIS-RTOS2 是 CMSIS 标准的一部分它要保证任何芯片厂商、任何工具链都能使用C API 是最小公倍数。而 C 封装是给写应用的人提供便利的。如果你用纯 C 写 mbed OS 应用也可以直接调 CMSIS-RTOS2 API效果没有区别。这套分层让内核层面的实现可以被替换而不影响上层应用这是嵌入式 OS 设计中非常重要的架构原则。3.2 Thread 的核心栈、优先级、状态切换来看一个最简单的多线程例子#include mbed.h Thread thread1; Thread thread2; DigitalOut led1(LED1); DigitalOut led2(LED2); void task_1() { while (true) { led1 !led1; ThisThread::sleep_for(200ms); } } void task_2() { while (true) { led2 !led2; ThisThread::sleep_for(150ms); } } int main() { thread1.start(task_1); thread2.start(task_2); while (true) { ThisThread::sleep_for(1000ms); } }这里有两个关键点值得展开。第一是线程栈。Thread类默认分配的栈多大查看源码你会发现mbed OS 6 里Thread类有一个默认栈大小OS_STACK_SIZE通常是 4096 字节4KB。如果你的任务函数里有比较大的局部数组、递归调用这个默认值可能不够导致溢出。mbed OS 的调试手段之一是在mbed_app.json里开启platform.stack-overflow-detection{ target_overrides: { *: { platform.stack-overflow-detection: true } } }开启后如果栈溢出系统会调用error()并进入死循环或打印错误信息这可以帮助你在开发阶段及早发现问题。这是一个很多人容易踩的坑任务函数里放了一个 2KB 的数组跑着跑着就 HardFault根本查不出原因其实只是栈分配小了。第二点更关键优先级与调度方式。RTX5 默认是抢占式时间片轮转调度线程优先级从 1 到 7osPriorityLow到osPriorityRealtime同优先级线程按时间片轮流执行。每个 Thread 创建时如果不指定优先级默认是osPriorityNormal。这意味着你的两个 LED 任务会被调度器分时执行——当一个任务调用sleep_for()时它会主动让出 CPU另一个任务才能运行。你可以想象 RTOS 内核里有一个就绪队列就绪的任务按优先级排队每次上下文切换时保存当前任务的寄存器状态到它自己的栈然后加载下一个任务的上下文。想调整个任务的优先级thread1.set_priority(osPriorityHigh);或者创建时指定Thread thread1(osPriorityHigh, 4096);第二个参数是栈大小字节数。实践中一个常见问题是High优先级的任务如果while(1)里没有sleep_for()或等待锁它会一直霸占 CPU所有Normal优先级任务全部饿死。这是初学者最容易犯的错。在写无线协议栈时我曾经犯过这样的错一个轮询任务优先级设太高结果系统连按键响应都不见了。所以优先级设计的原则是只有真正紧急的任务才提高优先级而且每个循环必须包含阻塞点sleep、等待信号量、等待队列。3.3 线程间通信信号量与消息队列的适用边界我在网络上看到很多 RTOS 面试题里问信号量和互斥锁的区别其实这是有实战意义的。我用 mbed OS 的口径给你梳理清楚。**Mutex互斥锁**解决的是互斥访问问题。比如两个线程都要通过同一个 I2C 总线去读传感器数据如果不加锁两次传输的字节可能会交叉串扰。用 Mutex 保护Mutex i2c_mutex; void read_sensor_a() { for (int i 0; i 100; i) { i2c_mutex.lock(); // 执行 I2C 读取传感器 A 的操作 i2c_mutex.unlock(); ThisThread::sleep_for(10ms); } } void read_sensor_b() { while (true) { i2c_mutex.lock(); // 执行 I2C 读取传感器 B 的操作 i2c_mutex.unlock(); ThisThread::sleep_for(10ms); } }Mutex 必须由持有它的线程解锁不能跨线程释放。RTX5 还支持优先级继承能一定程度上防止优先级反转。**Semaphore信号量**解决的是资源计数和事件通知问题。比如一个线程负责从串口接收数据另一个线程负责处理数据。串口接收线程每收到一帧数据就sem.release()处理线程阻塞在sem.acquire()上一旦有数据就继续跑Semaphore frame_sem; uint8_t frame_buffer[64]; void uart_rx_thread() { while (true) { // 等待并接收一帧数据 uint32_t n serial_receive(frame_buffer, sizeof(frame_buffer)); if (n 0) { frame_sem.release(); } } } void process_thread() { while (true) { frame_sem.acquire(); // 处理 frame_buffer 中的数据 process_frame(frame_buffer); } }信号量的初始计数值可以设置Semaphore(2)表示允许最多同时有两个资源被占用。你看信号量和互斥锁的使用场景完全不同一个管有多少资源可用一个管谁有权限进入临界区。新手经常混用最典型的错误是拿信号量当锁用——比如用Semaphore(1)保护共享变量——逻辑上可以跑但信号量没有优先级继承机制高优先级任务可能被低优先级任务阻塞很长时间这是实时系统的大忌。所以锁就是锁信号量就是信号量别互相替代。**Queue消息队列**适合传递数据本身而不是只发通知。mbed OS 的QueueT, N是模板类典型用法typedef struct { uint8_t sensor_id; float value; } sensor_data_t; Queuesensor_data_t, 16 data_queue; void producer_thread() { sensor_data_t data {1, 25.6f}; bool ok data_queue.try_put(data); if (ok) { // 成功入队 } else { // 队列满丢弃或处理错误 } } void consumer_thread() { sensor_data_t *data; bool ok data_queue.try_get(data); if (ok) { process_data(data); // 注意要释放这个指针 // 如果 Queue 是用内存池管理的需要调用 data_queue.free(data); } }这里有一个常见的坑try_put里传的是指针不是值的拷贝。如果你传局部变量的地址出了函数作用域这个地址就失效了——所以 mbed OS 的 Queue 实现内部是拷贝内容的但try_get取出的是内部缓冲区中的指针你需要free()回去。如果不 free队列的内存在连续多次存取后会耗尽表现为程序跑一段时间后突然队列操作失败。我见过很多人在社区里问这个问题其实只要理解Queue 管理的是内置缓冲区是手动分配堆还是静态池就能想明白。3.4 事件标志EventFlags轻量级的同步手段如果你只需要等待某一个位变高这种单纯同步用信号量和队列反而麻烦。mbed OS 的EventFlags是最好的选择EventFlags flags; #define FLAG_BUTTON_PRESSED (1 0) #define FLAG_TIMER_EXPIRED (1 1) void button_thread() { while (true) { if (button_read()) { flags.set(FLAG_BUTTON_PRESSED); } ThisThread::sleep_for(5ms); } } void main_thread() { while (true) { uint32_t ret flags.wait_any(FLAG_BUTTON_PRESSED | FLAG_TIMER_EXPIRED); if (ret FLAG_BUTTON_PRESSED) { handle_button(); } } }wait_any可以一次等多个标志返回实际被置位的位掩码。相比信号量事件标志支持的是多对多关系——多个线程可以等待同一个标志或者一个线程等多个标志。而信号量只能做到一个信号量对应一个计数本质上是事件计数不是位掩码。在使用小内存嵌入式平台时事件标志是效率最高的同步原语因为它只需要一个 32 位整数。4. 驱动体系从引脚号到用户类的整个链路4.1 驱动框架的分层设计mbed OS 的驱动体系我总结为三个层次底层HAL API前面已经讲透中层平台目标板级支持targets/下每个 MCU 的寄存器配置和引脚映射上层drivers/里的 C 驱动类DigitalOut、SPI、I2C、USBSerial等看一个典型驱动类的完整定义方式以drivers/SPI.h为例它的核心逻辑大致如下class SPI { public: SPI(PinName mosi, PinName miso, PinName sclk, PinName ssel NC); void format(int bits, int mode 0); void frequency(int hz 1000000); int write(int value); int transfer(const uint8_t *tx_buffer, int tx_length, uint8_t *rx_buffer, int rx_length); private: spi_t _spi; };构造函数接收引脚号内部调用 HAL API 的spi_init()。write()则是单字节全双工读写。format()和frequency()分别设置数据位宽、SPI 模式和时钟频率最终也是映射到spi_format()、spi_frequency()。这些 C 类最大的优势是资源和配置都在构造阶段搞定。你构造一个SPI对象时底层外设就被初始化了不需要手动调用begin()之类的初始函数。对象析构时对应外设被释放如果你的板子支持 sleep 模式它还会参与功耗管理。这个设计模式在嵌入式 C 里非常推荐把配置和业务分离把资源生命周期绑定到对象生命周期。4.2 为什么要特别留意 NULL 引脚NC在drivers/驱动类里很多构造函数支持一个特殊值NCNot Connected未连接。典型例子是 SPI 的片选引脚ssel NC以及 UART 的流控引脚。这背后有实际意义比如你用某 SPI 设备时不需要硬件片选而是用 GPIO 软件控制那么传入NC就能让底层的spi_init()跳过片选引脚的配置。我在实际项目中发现很多人在使用SPI把ssel随便指到一个不用的引脚又不接硬件结果 SPI 波形始终不对。因为如果你给了一个有效引脚号spi_init()会把它配置为外设的硬件片选输出它会随着每次传输自动翻转——如果你的设备不认这个片选通信自然失败。所以不需要硬件片选传入NC需要硬件片选传入正确的引脚想要软件控制片选传入NC另外用DigitalOut控制真正接硬件的片选引脚。类似地I2C 使用frequency()、write()/read()时如果不注意从机地址的 7/8 位格式也很容易踩坑。7 位地址和 8 位地址的区别在于是否带读写位。mbed OS 的I2C::write(int address, const char *data, int length)中address是 8 位地址还是 7 位查源码你会发现它内部会拼接读写位所以你在外面应该传 7 位地址。如果你习惯性地把 8 位地址直接传进去地址就会左移一位设备从 0x50 变成 0xA0自然 match 不上。这类问题是所有从 STM32 裸机转 mbed 的人都会经历一遍的。4.3 外部驱动的写法继承与组合的选择假设你要驱动一个外部传感器比如 BME280 温湿度气压传感器。你在 mbed 社区里可能会下载到别人写好的库也可以自己写。自己写的时候注意驱动类应该组合I2C或SPI对象而不是继承它。class BME280 { public: BME280(PinName sda, PinName scl) : _i2c(sda, scl), _addr(0x76 1) { } bool init() { // 发送初始化命令 char cmd[2]; cmd[0] 0xE0; // soft reset register cmd[1] 0xB6; _i2c.write(_addr, cmd, 2); wait_us(1000); return read_id() 0x60; } float read_temperature() { // 读寄存器计算温度 } private: I2C _i2c; char _addr; };这个设计把I2C作为成员变量持有构造 BME280 的时候传入引脚号。这种组合模式非常贴合实际BME280 需要 I2C 传输而这个 I2C 总线可能还挂载着其他设备你不可能让一个传感器类独占一条 I2C 总线。在 mbed OS 中应把 I2C 对象放进更上层的管理器层次或者在每个操作前加锁让同一总线上的多个设备通过 Mutex 串行访问。还有一个最佳实践你的驱动类初始化方法一定要返回bool或错误码不要 void。传感器通信失败是常态——设备没焊好、地址错、总线冲突——如果初始化不返回状态你根本不知道失败发生在哪。这个习惯我从写 mbed 驱动开始一直保持到现在帮助排除的问题不计其数。4.4 工具链注意ARM Compiler 版本与 mbed OS 的兼容在相关热搜词里我看到很多人搜arm compiler 5.06u7 下载这其实和 mbed OS 的历史渊源很深。mbed OS 5.x 时代默认工具链是 ARM Compiler 5AC5很多人用 Keil 开发 mbed 工程就必须装对应的 AC5 版本。由于 AC5 已经停止更新ARM 官方把 mbed OS 6 的默认工具链切换到了 AC6 和 GCC_ARM。如果你拿到一个 mbed OS 5.x 的经典工程却使用 AC6 去编译大概率会遇到语法不兼容、内建函数提示错误等问题。反过来在 mbed Studio 上把 Mbed OS 6 的导出工程拿到 Keil 里用 AC5 编译也会被一堆 C11 特性卡住。所以这里建议mbed OS 5.x 老工程优先用 AC5 或 GCC_ARM 5.4mbed OS 6.x 新工程优先用 AC6 或 GCC_ARM 最新稳定版如果必须用旧工程迁移到新工具链重点关注编译器的--c11或-stdc11选项是否开启、pack结构体的对齐方式是否变化。mbed 官方也提供mbed compile -t ARM -m ...这种命令行编译方式它会自动探测系统里已安装的 ARM Compiler 版本。当你同一台机器装了 AC5 和 AC6 时是通过MBED_ARM_COMPILER_PATH环境变量来指定具体路径的这个环境变量设置不对编译时会报找不到编译器的错误。所以如果你在 Windows 上装 mbed CLI建议先把不同工具链路径理清楚避免版本打架。5. 测试体系mbed OS 的自动化测试是怎么组织的5.1 单元测试框架mbed OS 自带的 Greentea 测试工具mbed OS 的测试体系常常被初学者忽略但其实它的设计非常值得学习。它不仅用于 mbed OS 自身的回归测试也能给你自己的模块写自动化测试。mbed OS 测试分为两层热词里有核电rtos测试其实那是工业级软件测试的专用概念和 mbed OS 测试体系不是一回事单元测试在主机上编译运行不需要真实硬件用 C 测试框架跑纯逻辑模块。硬件测试在真实板子上跑通过串口与主机通信主机端的greentea工具解析测试输出来判断 pass/fail。greentea是 mbed OS 官方推荐的硬件测试工具。工作流程如下板子通过 USB 串口连接到主机主机执行mbed test -m board -t toolchain --greentea测试程序编译烧录到板子运行时通过串口输出特定格式的测试结果greentea解析串口信息报告测试结果。5.2 一个硬件测试用例的实际写法在 mbed OS 工程里你通常会有一个tests/目录。如果你建立的是一个 mbed 测试工程源码目录结构类似my-test/ ├── mbed-os/ ├── main.cpp └── mbed_app.jsonmain.cpp 里用 mbed 的测试宏写用例#include mbed.h #include greentea-client/test_env.h #include unity/unity.h #include utest/utest.h using namespace utest; DigitalOut led(LED1); // 测试用例1验证 GPIO 输出翻转 void test_gpio_toggle() { led 1; wait_us(100); TEST_ASSERT_EQUAL(1, led.read()); led 0; wait_us(100); TEST_ASSERT_EQUAL(0, led.read()); } // 测试用例2验证定时器 void test_timer() { Timer t; t.start(); wait_us(1000); t.stop(); TEST_ASSERT_INT32_WITHIN(200, 1000, t.read_us()); } utest::v1::status_t test_setup(const size_t number_of_cases) { GREENTEA_SETUP(10, default_auto); return verbose_test_setup_handler(number_of_cases); } Case cases[] { Case(GPIO toggle, test_gpio_toggle), Case(Timer accuracy, test_timer), }; Specification specification(test_setup, cases); int main() { return !Harness::run(specification); }这段代码有四个关键知识点GREENTEA_SETUP(timeout, default_auto)告诉 greentea 测试超时时间以及是否自动上报结果。如果写default_autogreentea 会自动从串口读取测试结果如果要手动确认结果可以换成一个自定义的流程名。TEST_ASSERT_*系列宏来自 mbed OS 集成的 Unity 测试框架断言失败会记录错误信息并继续跑后续测试当然你也可以配置为中断。Case(名称, 函数指针)是一个测试用例的注册方式utest 框架按顺序执行这些 Case。main()返回!Harness::run(specification)这是 utest 的标准入口。如果所有测试通过Harness::run返回 truemain 返回 0shell 层面也就是成功。你运行mbed test后greentea 会显示类似这样的结果test case: GPIO toggle passed test case: Timer accuracy passed Test cases: 2 passed, 0 failed这个体系可以很好地和 CI/CD 结合。如果你想在 Jenkins/GitLab CI 里跑 mbed 硬件测试让板子常驻连接在测试服务器上每次代码提交后自动烧录并跑测试——这是很有价值的质量防线。5.3 开发阶段的调试与测试资源配置再说一个实际工程中很容易忽视的问题mbed 的资源分配。当你的程序跑在拥有丰富外设的板子上时默认的 UART、定时器等资源可能和 wire 日志端口冲突。mbed OS 的调试串口默认绑定的引脚在目标板的mbed_config.h里可以通过platform.stdio-baud-rate和platform.stdio-flush-at-exit配置。在mbed_app.json里可以覆盖{ target_overrides: { NUCLEO_F429ZI: { platform.stdio-baud-rate: 115200, platform.stdio-flush-at-exit: true, platform.stdio-buffered-serial: true } } }有一个容易忽略的坑platform.stdio-buffered-serial如果从 false 改为 true那么printf的输出会延迟到缓冲区满或主动 flush。很多人的板子跑着跑着突然不打印了可能就是 buffered serial 导致的假象。调试阶段我建议保持platform.stdio-buffered-serial为 false确保 printf 实时输出。而发布版本为了节省 CPU 时间可以开启 buffered。另外一个高频问题UART 波特率改了没生效。mbed OS 里printf使用的调试串口波特率默认是 9600。如果你是第一次接触某个板子按默认 9600 连接串口可能没有任何输出因为很多开发板出厂 bootloader 或示例程序使用的是 115200。在 mbed_app.json 里改一下即可{ target_overrides: { *: { platform.stdio-baud-rate: 115200 } } }这个配置项全局生效改完保存然后重新编译烧录串口输出正常。说这一句是因为我见过太多新手的第一个 mbed 程序跑不起来其实就是串口波特率不匹配。5.4 mbed OS 6 引入的greentea 与 pytest 混合测试mbed OS 6 开始官方测试框架也在演进。greentea 本身负责硬件通信和结果收集而更高层的测试驱动可以用 Python 的 pytest 编写通过 mbed CLI 的--pytest选项运行。这意味着你可以用 Python 访问板子串口设置测试步骤而板子端只需要提供对应的固件命令响应。举个例子你可以这样设计一个系统级测试主机端 pytest 脚本通过串口发送ATRST等待板子重启板子端固件启动后输出READYpytest 脚本等待READY后发指令读取传感器比对结果。这种主机编排 设备响应的测试模式非常适合做系统集成测试和验收测试。它的价值在于测试用例写在 Python 里可读性高、可扩展性强而且不需要在 C 代码里反复调整测试逻辑。这在 mbed OS 5 时代是不太方便实现的虽然套件也很成熟但你可以控制细节更少。6. 综合实战从 HAL 到 RTOS 再到测试体系的落地串联6.1 一个常见的完整工程结构接下来我把前面讲到的东西串成一个典型工程。假设要实现功能两个传感器数据采集 周期性上报 串口命令控制。工程结构可能是my-iot-project/ ├── mbed-os/ # mbed OS 源码 ├── main.cpp # 主入口 ├── sensors/ │ ├── BME280.h │ ├── BME280.cpp │ └── SensorManager.h # 传感器总线管理 ├── mbed_app.json # 平台配置 ├── mbed_settings.py # 工具链设置 └── tests/ └── test_sensor_manager.cpp这个结构把驱动层BME280和业务层SensorManager分离同时保留测试目录是很清晰的模块化思路。6.2 核心代码逻辑参考main.cpp 可能长这样#include mbed.h #include BME280.h #include SensorManager.h // 模块级对象 I2C i2c_bus(PB_9, PB_8); SensorManager sensor_mgr(i2c_bus); // 线程 Thread sensor_thread(osPriorityNormal, 1024); Thread report_thread(osPriorityLow, 1024); // 队列传感器数据送给报告线程 Queuesensor_data_t, 8 data_queue; void sensor_loop() { sensor_data_t data; while (true) { if (sensor_mgr.read_all(data) MBED_SUCCESS) { data_queue.try_put(data); } ThisThread::sleep_for(500ms); } } void report_loop() { sensor_data_t *data; while (true) { if (data_queue.try_get(data)) { // 打印或者通过通信模块上报 printf(temp%.2f humi%.2f\n,>
返回列表