ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入解析 mbed OS:从 HAL 层到 RTX5 内核与驱动实战

深入解析 mbed OS:从 HAL 层到 RTX5 内核与驱动实战 说实话我第一次接触 mbed OS 是在一块 Nucleo 开发板上。当时项目急着要做原型没时间从头折腾寄存器听说 mbed OS 能快速点灯、能跑 RTOS拎过来就是一顿用。结果灯亮了RTOS 也跑起来了但源码里到底发生了什么我一直似懂非懂。直到后来我把 HAL、内核、驱动、测试整个链路啃了一遍再回头看那些上层 API才发现这个系统的设计比我想象中干净得多。这篇文章不是 ARM 官方文档的复述而是我从实际源码阅读和项目使用里沉淀下来的梳理。我会带着你从 HAL 层往下钻看 PinName 如何把引脚和寄存器联系起来再进 RTOS 层聊 RTX5 调度器和事件驱动内核接着以磁编码器 MT6701 这种真实外设驱动为例子把从读取到滤波校准的完整路径串起来最后讲 mbed OS 那套值得借鉴的测试体系。不管你是从 STM32 HAL 库转过来的还是第一次接触 RTOS这篇文章的粒度应该都能让你看得下去。1. 先搞懂 mbed OS 的整体设计它到底在解什么题1.1 事件驱动这不是拿来唬人的名词很多人第一次听到事件驱动这个词是在嵌入式操作系统或者 GUI 框架的文档里感觉很高大上。其实放到 mbed OS 的场景里它解决的是一个非常实际的问题Cortex-M 芯片的 RAM、Flash、功耗预算都有限物联网设备又经常要处理不同来源的中断和低速外设如果每个外设都靠 CPU 轮询功耗和实时性都扛不住。mbed OS 的内核基于 RTX5设计上提倡事件驱动模型。这句话什么意思意思是你不要在一个 while(1) 里反复读传感器、等数据、再去处理和显示而是把任务拆成什么时候发生了什么事件比如 I2C 数据就绪、这个事件该由谁来响应、响应完了之后还要不要等下一个事件。事件本质上是消息谁发消息、谁收消息由内核来调度和投递。这样 CPU 在没有事件的时候可以睡觉有事件的时候醒来处理完马上睡低功耗才能谈得上。RTX5 内核源码在 mbed OS 里对应 rtos/ 目录下的 Thread、EventQueue、Semaphore 这些上层封装。要理解整个 mbed OS第一步不是去抠某个 API 实现而是先在脑子里建立一个分层模型最下面是芯片厂商提供的低层寄存器操作中间是 mbed 自己定义好的 HAL 接口再往上是 C 封装的高层驱动 API最上面才是你的业务代码。后面所有章节都在跟这个分层模型打交道。1.2 源码目录就是一张架构图我平时读源码的习惯是先看目录结构再猜设计者想干什么。mbed OS 的顶层目录长得非常像它的架构宣言下面是我最常翻的几个目录目录作用典型内容hal/硬件抽象层 C 接口gpio_api.h、i2c_api.h、serial_api.hplatform/平台相关的基础设施和引脚定义PinNames.h、PeripheralNames.h、mbed_error.hdrivers/面向用户的高层 C 驱动DigitalOut.h、I2C.h、SPI.h、BufferedSerial.hrtos/基于 RTX5 的 RTOS C 封装Thread.h、EventQueue.h、Mutex.h、Semaphore.htargets/各芯片的目标实现targets/TARGET_STM/TARGET_STM32F1/...testing/测试框架和工具utest、greentea 相关脚本tools/构建、烧录、打包工具mbed-tools 相关源码如果你曾经用过 STM32 HAL 库你会发现 mbed OS 里面也有一个hal目录但两者的含义并不完全一样。STM32 HAL 是 ST 公司对自家芯片寄存器的封装角度是把所有功能都暴露给开发者你来调。mbed OS 的 hal 目录则是 ARM 定义的一层 C 语言抽象接口目标是无论是谁家的 Cortex-M 芯片上层都只调同一套函数。真正的寄存器操作代码被藏在 targets/ 里不同芯片厂商自己实现编译时通过 TARGET_ 宏来选择和剪裁。这也是很多从裸机开发转过来的朋友最容易懵的地方在 STM32 裸机工程里你 include 的是 stm32f1xx_hal_gpio.h用的是 HAL_GPIO_WritePin到了 mbed 工程里你 include 的是 mbed.h用的是 DigitalOut。两个库都能点灯但设计哲学完全不同。后面我会专门用一节来对比这个差异。1.3 HAL在不同语境下根本不是同一个东西我在一些嵌入式社区里看到不少开发者在讨论hal库时吵起来一查发现有人说的是 STM32 HAL 库有人说的是 mbed OS HAL还有人说的是 Linux 内核的 Hardware Abstraction Layer。它们的共同点是都想把硬件差异封装起来但抽象级别和设计目标差别很大。语境HAL 指什么抽象粒度的特点STM32 HAL 库ST 官方寄存器封装面向单片机外设API 细到寄存器例如 HAL_UART_Transmitmbed OS HALARM 定义的 C 接口层面向板级能力抽象例如 serial_api.h 只要求你能收发字节Android HAL向上屏蔽内核驱动的接口面向系统服务例如 Camera HAL、Sensor HALLinux 用户态硬件相关驱动库复杂多样很多时候指某个具体设备框架下的用户态部分理解这一点很重要因为后面读 mbed OS 源码时如果你带着 STM32 HAL 的印象去找某个寄存器的操作函数往往找不到。它把那些函数挪到了 targets/ 目录下而且为了跨平台甚至故意弱化了寄存器操作的直接可见性。你要看的是它定义好的接口约定而不是某个具体的寄存器位。2. 深入 HAL 层PinName 映射和那些 C API 的中介作用2.1 为什么一句 DigitalOut led(LED1) 就能控制板载 LED很多初学者用 mbed 开发时会发出灵魂一问LED1 到底是从哪里来的我在哪定义它的其实 LED1 这类名字来自板级支持包里的 PinNames.h。不同板卡在 targets/ 目录下有各自的 PinNames.h里面用枚举定义了板级可见的引脚名称。比如某一块 STM32 目标板PinNames.h 里会有一个枚举把 LED1 映射到某个物理引脚编号大致长这样typedef enum { LED1 PA_5, LED2 PA_6, ... // 以及一系列 MCU 引脚 PA_0 0x00, PA_1 0x01, ... } PinName;看到没PA_5 本身又被定义成了一个枚举值。每次往上层传一个 PinName本质上传递的是一个整数编号这个编号背后藏着端口号、引脚号、甚至复用功能信息。当你在代码里写 DigitalOut led(LED1) 时DigitalOut 构造函数最终会调用 hal/gpio_api.h 里声明的 gpio_init把枚举值翻译成 GPIO 端口的时钟、CRL/CRH 寄存器或 MODER 寄存器配置。这个设计最大的好处是让上层代码看不到任何一组寄存器位的名字。你换一块板子只要板级 PinNames.h 里 LED1 的映射变了上层代码一行都不用动。代价是你要想手动调寄存器会觉得它像黑盒。实际上它就是为了让你不调。2.2 引脚复用和外设冲突一个很隐蔽的坑HAL 层要处理的另一件大事是引脚复用。Cortex-M 芯片的引脚往往有多种功能比如同一个引脚既能当 I2C 的 SCL也能当 SPI 的 SCLK还能当普通 GPIO。mbed OS 的做法是在 targets 目录下的 PeripheralPins.c 文件里维护一张大表告诉系统每个引脚可以映射成哪些外设功能。你的代码里一旦写了 I2C i2c(PB_8, PB_9)构造函数就会去查这张表确认 PB_8 和 PB_9 是不是真的支持 I2C 功能同时检查这两个引脚是否已经被别的对象占用。如果两个外设对象不小心用了同一个引脚系统会在启动时报错。这类问题在 STM32 HAL 裸机开发里通常要等到运行时你才发现外设读不到数据而 mbed 把冲突检测提前到了初始化阶段这一点对排查问题帮助很大。不过这里有个使用心得别太依赖这种检测。它能够捕获明显的同一引脚重复使用但引脚复用表本身是芯片厂商维护的不同系列之间可能存在遗漏或错误。我遇到过一块小众板卡的 PeripheralPins.c 里少了一组 SPI 引脚的映射导致我明明接对了初始化却不成功。最后的解法是去查芯片手册自己往表里补或者换个引脚用。遇到这类问题别一头扎进应用代码先怀疑表。2.3 从 C API 到 C 封装mbed HAL 的经典调用链接着我们追踪一条具体的调用链。HAL 层以 C 函数形式定义了底层接口比如 GPIO 的初始化函数是void gpio_init(gpio_t *obj, PinName pin); void gpio_write(gpio_t *obj, int value);drivers/DigitalOut.h 里的 C 类内部持有 gpio_t 结构体构造函数里调 gpio_init赋值操作符重载里调 gpio_write。I2C 也是一样I2C.h 内部持有 i2c_t 结构体重I2C::read 最终调 hal/i2c_api.h 里的 i2c_read。这种底层 C 接口 上层 C 封装的结构在嵌入式 C 里面非常经典。为什么不用纯 C 直接到底因为 C 接口便于芯片厂商按自己的习惯实现也方便将来绑定其它高级语言。为什么上层又要包一层 C因为用户写起来舒服RAII、重载、模板这些特性能让代码表达更自然。如果你看惯了 STM32 HAL 那种函数满天飞的形式再看 mbed 这种风格一开始会有点不适应但习惯之后你会发现代码的抽象层次非常清晰。3. RTOS 层源码解析RTX5 到底是怎么管理任务的3.1 Thread 的创建和调度策略从 osThreadNew 说起mbed OS 的 RTOS 层以 RTX5 为内核对外提供 CMSIS-RTOS v2 风格的 API。用户代码里常见的 Thread 类底层调用的其实是 osThreadNew。创建线程时你需要指定一个栈大小。很多新手会随手填一个 1024 或者 2048然后祈祷不溢出。实际上RTX5 的线程栈大小是按字节算的默认值在不同平台不一样但这个值跟你的任务里有没有使用 printf、有没有浮点运算、有没有比较深的函数调用链直接相关。我的经验是任务里跑 printf 的栈至少给 2048 字节往上纯逻辑控制的任务 1024 字节通常够但如果你在中断里又有额外的嵌套调用就得再留富余。RTX5 提供了栈溢出检测机制在异常处理或者调试时能够通过 osRtxErrorNotify 回调把信息吐出来开发阶段最好打开。线程调度方面RTX5 支持抢占式调度、时间片调度和协作式调度。默认行为是抢占式加时间片也就是多个同优先级线程轮流运行。正因为有时间片机制你的 while(1) 里即使不写 osDelay也不会永远把别的同优先级任务饿死。但请注意高优先级线程在低优先级线程加锁的情况下不断运行依然可能形成优先级反转或者死锁这是后面要说的重点。3.2 EventQueuembed OS 里最值得先学会的组件如果你只打算记一个 RTOS 知识点我建议你记 EventQueue。它是 mbed OS 事件驱动模型的核心工具尤其适合用来做中断延后处理也就是所谓的 deferred interrupt。很多外设驱动在中断服务函数里不应该做耗时操作正确做法是把任务丢给 EventQueue 去排队执行。EventQueue 的本质是一个线程安全的队列加一个事件调度的循环你可以把它理解成RTOS 世界里的消息循环。下面的代码展示了它的典型用法#include mbed.h EventQueue queue(32 * EVENTS_EVENT_SIZE); Thread eventThread; void post_from_isr() { // 中断里只负责把事件压入队列 queue.call(handle_ready); } void handle_ready() { // 实际的数据处理在普通线程上下文中做 printf(handle in thread\r\n); } int main() { eventThread.start(callback(queue, EventQueue::dispatch_forever)); // 模拟一个外部中断触发 post_from_isr(); ThisThread::sleep_for(100ms); }每个事件在放进队列时会分配一个 EVENTS_EVENT_SIZE 大小的控制块队列总大小在构造时指定。如果你的事件参数很多或者回调对象很复杂32 个可能不够用需要观察运行时的返回状态。EventQueue 的使用诀窍是在中断服务函数里只调用 queue.call把耗时的一切放到 dispatch 循环所在的普通线程中执行这样既保护了中断响应时间又不容易触发 RTOS 中断上下文限制。3.3 锁、信号量与 RTOS 面试高频考点RTX5 提供了 Mutex、Semaphore、MessageQueue、EventFlags 等组件。在实际面试或者项目评审里大家最关心的是怎么选型以及怎么避免坑。组件核心语义典型场景容易踩的坑Mutex互斥锁有所有权概念保护共享资源比如外设寄存器中断里不能用忘记释放会死锁Semaphore计数信号量无所有权概念任务间同步比如接收完一批数据后通知处理任务容易和 Mutex 混用释放次数比获取多也没人拦MessageQueue带类型的消息队列任务间传递数据块比如传感器数据队列满时 push 失败需要做丢弃策略EventFlags事件标志组等待多个条件的组合需要小心位被多个线程互相清零面试里还有一个高频问题为什么中断里不能调用 Mutex 的 lock 方法因为 lock 可能会导致当前线程阻塞而中断上下文本身不属于任何线程不能阻塞也不应该发生线程切换。mbed 的许多带 FromISR 后缀或 call 类接口才是专为中断设计的机制。优先级反转也是经典考点。RTX5 的 Mutex 实现了优先级继承机制能够在检测到低优先级线程持有锁而高优先级线程等待时临时提升低优先级线程的优先级。这能缓解大部分反转问题但不能完全消灭所有实时性灾难设计任务优先级时仍要尽量让持锁的任务短小精悍。4. 驱动体系从官方驱动到自研外设驱动再到滤波与校准4.1 官方驱动的分层和社区生态mbed OS 的 drivers/ 目录里默认提供了大量外设驱动类DigitalIn、DigitalOut、I2C、SPI、PwmOut、AnalogIn、BufferedSerial以及一些通信协议栈驱动。这些驱动只解决MCU 外设本身的问题比如 I2C 驱动负责发起始位、发地址、收发字节它不关心总线上挂的是温度传感器还是磁编码器。真正面向具体器件的驱动比如某款温湿度传感器、某块 OLED 屏幕、某颗电机驱动芯片通常在 mbed 社区的 os.mbed.com 组件库或者第三方 GitHub 仓库里。你写业务的时候经常能搜到现成的库比如 TB6612 电机驱动、SSD1306 OLED、DHT11、INA226 电流监测芯片这些都有不少人维护。不过社区库质量参差不齐。我的建议是传感器和电机这种外设驱动能自己写就自己写实在没时间也要抄完后重写关键读写部分。因为这些库很多时候是特定板卡上调好的直接拿到你的板子上引脚定义、时序参数、滤波逻辑都可能不匹配。驱动这东西只有你自己理清了时序图和寄存器表出了问题才可能快速定位。4.2 MT6701 磁编码器实战模拟 IIC 读取和滤波校准这里我用一个具体例子把上面的概念串起来。MT6701 是一款磁编码器芯片可以通过 I2C 输出 14 位的绝对角度值。它支持 I2C、SPI、UVW/ABZ 等多种接口I2C 模式下一帧读取角度数据非常简洁。假设你用的是 STM32F103 这类不带硬件 I2C 或者硬件 I2C 用起来不太顺手的芯片你可能要写软件模拟 I2C。在 mbed 里我一般直接用 DigitalInOut 来模拟 I2C 时序或者干脆找一个现成的软件 I2C 类。热词里提到的用 STM32F103C8T6 HAL 库模拟 IIC 读取 MT6701 磁编码器的滤波与校准实战在 mbed 上同样适用原理一致。读取 MT6701 角度寄存器一次典型的事务是#include mbed.h #define MT6701_ADDR (0x06 1) // 7-bit 地址是 0x06左移一位用于 8-bit 总线 I2C i2c(PB_9, PB_8); float read_mt6701_angle() { uint8_t reg_addr 0x00; // 角度数据起始寄存器 uint8_t data[2] {0}; i2c.write(MT6701_ADDR, reg_addr, 1); i2c.read(MT6701_ADDR, data, 2); // MT6701 的 14 位角度高字节 低字节的低 2 位 uint16_t raw ((uint16_t)data[0] 8) | data[1]; uint16_t angle_raw raw 2; // 0 ~ 16383 对应 0 ~ 360 度 float angle (float)angle_raw * 360.0f / 16384.0f; return angle; }这里的 i2c.write 和 i2c.read 每次都会产生 Start 和 Stop 条件第一次 write 指定寄存器地址第二次 read 读取两个字节。实际接线时记得给 SCL、SDA 接上拉电阻一般 4.7k 到 10k 都行否则总线时序会不稳定。不过直接读回来的角度值放到真实的电机或云台控制里你会发现噪声大、跳变多。这就要上滤波和校准了。滤波主要解决随机噪声校准主要解决系统误差。我常用的组合是一阶低通滤波加偏移校准。一阶低通滤波很简单代码大概长这样float filtered_angle 0.0f; float alpha 0.3f; // 越大越跟手越小越平滑 float update_angle(float new_angle) { filtered_angle alpha * new_angle (1.0f - alpha) * filtered_angle; return filtered_angle; }但要说清楚角度数据有个特殊问题它从 359 度跳到 0 度时是连续过渡如果直接把这个跳变放进滤波公式会出现一圈一个假抖动。常见的处理方法是先把角度差折算到 -180 到 180 度区间再滤波最后再绕回 0~360 度。这个细节不做的话低通滤波会在过零处产生明显的滞后毛刺。校准方面磁编码器安装时常常会有安装偏心导致角度输出不是严格线性。如果你的精度要求高可以在整圈匀布取若干点用实测值和参考角做查表插值如果只是矫正固定的零点偏移跑一圈取平均就能找到初始偏差。更聪明的做法是用一个旋转平台记录整圈误差再做傅里叶拟合把一次谐波误差扣掉。总之先滤波再校准最后再做角度控制这套数据路径对很多位置传感器都通用。4.3 调试外设驱动时我始终不离手的三样东西调 I2C 这类总线靠眼睛看代码很难发现时序问题。我调试驱动时必定会用逻辑分析仪抓波形看一眼 Start 条件、地址方向位、ACK/NACK、数据位是不是符合预期。碰到一次通信偶尔失败多半是上拉电阻不合适或者速率设置太高把 I2C 频率从 400kHz 降到 100kHz 能解决一大半问题。第二样是 printf 串口输出。很多人写 mbed 代码时直接用 printf却发现没输出第一反应是板子坏了。其实你需要检查串口引脚定义、波特率、以及是否有重定向代码。mbed 6.x 里 BufferedSerial 的默认波特率是 9600如果你上位机用的是 115200就会看到乱码。这个问题我见到无数人踩过。第三样是开发板上的故障指示灯。mbed OS 有错误报告机制很多硬件异常会触发系统进入错误状态并点灯或输出错误码。比如栈溢出、断言失败、内存分配失败都可能点亮故障相关的 LED。配上调试器看 trap 信息定位速度会快很多。5. 测试体系mbed OS 是怎么保证改了不挂的5.1 Greentea 和 UTest一个负责跑一个负责断言mbed OS 的测试体系在嵌入式圈子里算是很有特点的。它把测试分成两个角色一部分测试代码跑在开发板上target 端另一部分跑在 PC 上host 端通过串口连接两者并完成同步和结果上报。这套系统的核心工具叫 Greentea它主要负责发现设备、构建测试固件、跑测试、收集结果。target 端使用的测试框架叫 utest本质上是一个 C 实现的微测试框架里面集成了对 Unity 断言库的封装。举个例子如果我给 MT6701 驱动写一个测试用例可能是这样的#include utest/utest.h #include unity/unity.h #include mt6701.h using namespace utest::v1; static MT6701 encoder(PB_9, PB_8); control_t test_read_angle_range() { float angle encoder.read_angle(); TEST_ASSERT_TRUE(angle 0.0f angle 360.0f); return CaseNext; } 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(read angle range, test_read_angle_range), }; Specification specification(greentea_setup, cases); int main() { return !Harness::run(specification); }这个测试编译好之后烧进板子Greentea 会通过串口与板卡的测试程序握手板卡跑完一个用例就把结果打印到串口host 端收集后再发起下一个用例。整个过程不需要人工干预非常适合 CI 自动化。在 mbed-os 仓库里几乎每个 HAL 接口都有对应的测试用例你在编译官方工程时可以看到大量类似测试。5.2 怎么在自己的项目里用上这一套测试即使你不做 CI我也建议在项目里引入 uTest 写几个核心模块的测试尤其是协议解析、角度滤波、PID 计算这种纯逻辑代码。它们不依赖硬件就能验证大半正确性剩下的再靠板级验证。跑测试的典型命令是这样前提是装了 mbed-toolsmbed-tools compile -m NUCLEO_F103RB -t GCC_ARM --profile develop mbed-tools test -m NUCLEO_F103RB -t GCC_ARM --profile develop --tests tests-mt6701第一个命令编译固件第二个命令触发 Greentea 测试流程。刚开始用的时候最常见的坑是串口被别的终端软件占用导致 Greentea 无法与板卡通信右上角图标一直转圈。解决办法是关掉其它串口上位机或者给 Greentea 明确指定 COM 口。还有一个小技巧测试用例里不要只测正常情况要多写几个边界用例。比如角度值 0、16383、接近 360 度的换算过零跳变的滤波输出I2C 通信失败时返回的错误码。这些边界点往往是实际跑起来后才暴露问题的重灾区。5.3 测试体系的启发驱动开发也应该测试先行我在看 mbed OS 的测试代码时有个很深的感触它不是在写完代码之后补测试而是先定接口再写测试再实现功能。HAL 接口的测试在芯片移植阶段就在跑芯片厂商移植完 HAL 后直接跑同一条测试用例这是保证不同芯片行为一致最重要的手段。这对个人开发者的启发是如果下一次你打算写一个驱动先花半小时把接口签名和预期行为写清楚再写一个模拟输出的测试用例最后才去填实现。这个习惯会让你后期调试的耐心成本降低一半以上。就算你在裸机上开发这套思路同样成立。6. 这些年我在 mbed OS 开发上踩过的坑和避坑指南6.1 mbed OS 和传统 HAL 库混用的冲突有些朋友接手项目时板子上既有 mbed OS 代码又参考了 STM32 HAL 库初始化外设的方式两套时钟初始化、NVIC 配置混在一起跑起来各种奇怪问题。本质原因是 mbed OS 在进来之前已经初始化了时钟、中断向量和部分外设你再手动用 HAL 库初始化一遍很容易把状态改乱。我建议进了 mbed OS就别再用 STM32 HAL 库直接初始化那些已经被 mbed 管理的外设真需要底层能力走 mbed 的 HAL 接口或者用 MbedPinMap 这类机制补充。实在绕不开也一定要确认 mbed 没有初始化过那个外设并且搞清楚时钟树是谁在管理。6.2 中断上下文里的红线RTOS 开发里中断服务函数是雷区。mbed OS 里在中断里调用 printf、malloc、Mutex::lock、ThisThread::sleep_for 这类接口轻则触发断言重则挂机。正确做法是中断里只置标志位、推 EventQueue、或者调用带 FromISR 后缀的机制把耗时和阻塞逻辑挪到线程上下文。这里有个排查技巧如果系统在随机位置卡住先在中断服务函数里做减法把疑似有问题的调用注释掉再试。6.3 串口输出只收一次的问题热词里有个现象非常典型STM32 HAL 库串口中断接收只收一次在 mbed 环境里也有人遇到类似情况。排查思路要分两层一是硬件层确认 RX 引脚外设配置、波特率、DMA 模式是否正常二是软件层看你使用的接收 API 是阻塞式还是非阻塞式。mbed 6.x 的 UnbufferedSerial 和 BufferedSerial 行为有差异如果你使用 UnbufferedSerial::read 且没有持续读数据就会停留在硬件缓冲或直接丢失。遇到只收一次的情况先在上位机看数据有没有发出来再用逻辑分析仪看波形最后再怀疑代码。6.4 建议整理成一张速查表现象可能原因检查顺序点灯失败引脚映射错误、引脚冲突看 PinNames.h、看 PeripheralPins.c、量波形I2C 偶尔通信失败上拉电阻不对、速率太高逻辑分析仪抓波形、降到 100kHz 再试printf 无输出串口引脚/波特率/重定向检查 Serial 引脚、波特率、终端占用RTOS 任务卡死死锁、栈溢出、中断里阻塞查锁顺序、开栈溢出检测、中断函数瘦身编译过但上电即挂多套初始化冲突、错误配置查 mbed_app.json、禁用部分初始化逐步二分这张表是我自己调试时反复用到的套路。很多时候嵌入式的问题不是原理复杂而是排查顺序不对。先看硬件波形再看系统配置最后再看业务逻辑能帮你少走很多弯路。6.5 mbed OS 的现状和我的使用心态ARM 官方已经宣布 mbed OS 将进入维护阶段但这不妨碍我们从中学习。它的 HAL 抽象思路、事件驱动内核设计、测试驱动的开发流程在今天依然影响着一大批嵌入式操作系统和开发框架。我在自己写的代码里依然保留了很多从 mbed OS 里学到的习惯接口分层清晰、中断延后处理、测试覆盖核心逻辑、配置与外设解耦。所以如果你是从这里开始入门 RTOS 或者物联网开发不要怕这个项目是不是老了。重要的是它的架构思想是成熟的。拿它练手、做产品原型、做学习项目都完全够用等你真的需要长期维护和生产级能力再去评估其它方案也不迟。就我自己而言每次翻 mbed OS 的源码总能发现一些值得借鉴的小设计比如引脚映射表的结构、HAL 接口的命名风格、测试用例的组织方式。把它的源码当成一本开放的嵌入式架构案例集阅读价值远超能跑 demo这个层面。你如果也打算深入啃一啃建议带着一个问题进去比如我想搞清楚 DigitalOut 到寄存器到底经历了什么沿着一条调用链往下追比从头翻书的效率高得多。读完之后你会发现看其它嵌入式平台的源码时也多了许多观察的角度。
返回列表