ARTICLE DETAIL

资讯详情

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

深挖mbed OS源码:HAL、RTOS与驱动架构全解析

深挖mbed OS源码:HAL、RTOS与驱动架构全解析 1. 先从源码目录说起mbed OS 到底在解决什么问题做了几年嵌入式开发的人大概率都有过这种经历上半年用 STM32 写了一套业务逻辑下半年项目换成了 NXP 或者 Nordic 的芯片然后发现所有外设驱动、RTOS 封装、底层初始化代码全得推倒重来。明明核心业务逻辑没变却要为每一颗芯片重写一遍基础设施。mbed OS 想解决的就是这个问题。它不是一颗具体的芯片也不是某个厂商的库而是一套建立在 Cortex-M 生态之上的开源嵌入式操作系统把 HAL、RTOS、驱动框架、测试工具全部串成了一条完整的开发链路。这篇博文我会直接从源码层面拆开 mbed OS 的骨架把 HAL 层如何抽象硬件、RTOS 层如何封装线程与同步原语、驱动层如何组织回调机制、测试体系如何保证跨平台质量这四块内容讲透。适合正在使用 mbed OS 做产品开发、或者想从源码角度理解嵌入式操作系统设计思路的工程师。即使你现在用的是 FreeRTOS 或者裸机 HAL 库理解了 mbed OS 的分层方式之后再回头看自己项目里的代码组织也会有不少启发。先给一个整体印象。从 GitHub 拉下 mbed-os 仓库之后你会在根目录看到几个核心文件夹platform、drivers、hal、rtos、connectivity、events、storage、targets。这八个目录基本就是 mbed OS 的全部家当。很多人第一次看到这个目录结构会懵为什么 platform 和 drivers 要分开hal 和 targets 又是什么关系connectivity 为什么要单独占一层这些问题背后其实是 mbed OS 的核心设计哲学用分层把芯片相关和芯片无关彻底切开让应用代码只依赖稳定 API不依赖具体硬件。2. HAL 层源码解剖从 PinName 到外设驱动的距离2.1 PinName 与引脚映射的底层机制HAL 层是 mbed OS 里最有意思的部分。它不叫外设驱动库而是叫硬件抽象层名字本身就说明了定位不是把某个外设封装成好用的接口而是定义一套不管底下是什么芯片都能用同一套方式操作硬件的标准接口。这套标准接口的核心是一个叫PinName的枚举类型。每个 mbed 支持的平台上都会有一个PinNames.h文件里面用枚举把芯片所有可用引脚定义成带语义的名称。比如 STM32F103C8T6 平台你会看到PA_0、PB_5、PC_13这样的枚举值每个枚举背后映射到芯片的具体端口和引脚号。但 PinName 并不只是一个简单的编号它和一组名为PinMap的表配合完成了从引脚名字到外设实例的桥接。这里插入一段我实际看源码时的理解。mbed 的 HAL 在初始化一个串口或者 I2C 外设时并不像我们手写 STM32 HAL 库那样手动去设置GPIO_InitStruct.Pin GPIO_PIN_5然后调用HAL_GPIO_Init。它做的是传入一个 PinName比如PA_9然后在对应的PinMap表里去查找这个引脚能不能复用成UART_TX。如果能就把引脚复用配置、外设寄存器基地址、中断号全部一次搞定。这种设计的好处是用户根本不需要知道PA_9 对应 USART1_TX 还是 USART2_TX只需要告诉 mbed 我要用 PA_9 做 TX剩下的交给查表逻辑。PinMap 的核心结构体长这样typedef struct { PinName pin; int peripheral; PinFunction function; } PinMap;peripheral字段存放外设实例编号比如UART_1、I2C_2function字段表示这个引脚在当前外设下承担的功能比如SERIAL_TX或SERIAL_RX。当你调用Serial构造函数并传入 TX 和 RX 引脚时mbed 会在内部调用pin_function函数遍历 PinMap 表找到匹配项后完成引脚复用和外设时钟使能。2.2 以 GPIO 为例拆解 HAL 接口的实现套路理解了 PinMap 之后再看 HAL 层任何外设的实现都会很顺。拿最基础的 GPIO 举例。mbed OS 的 HAL 层会定义一套统一的 GPIO 操作函数不管你用的是 ST、NXP 还是瑞萨的芯片应用层调用的都是同一组 API。GPIO HAL 的核心对象是gpio_t结构体源码里大概是这个形态typedef struct gpio_s gpio_t; struct gpio_s { PinName pin; PinDirection direction; int mask; uint32_t mode; };看到这里你可能会想这也没把寄存器和端口号存进去啊操作的时候怎么找到对应的 GPIO 寄存器答案是每个平台的实现文件里gpio_t的字段会不同。gpio_s这个结构体在 targets 目录下的具体实现里会额外添加端口基地址等芯片相关字段。HAL 层暴露给上层的 API 是一组固定函数gpio_init、gpio_mode、gpio_dir、gpio_write、gpio_read。每个函数在 targets 里有对应平台的实现。以gpio_init为例它会做三件事把传入的 PinName 映射到具体的端口和引脚号、使能对应 GPIO 端口的时钟、把引脚初始化为输入模式并应用上拉或下拉配置。整个过程被封装在函数内部用户只看到我初始化了一个引脚它叫 PA_5。这里值得注意的一个细节是mbed 的gpio_t中有一个mask字段。这个字段在 ARM 平台上用于快速位带操作在 Cortex-M 处理器上GPIO 输出寄存器往往支持 BSRR 这类原子写寄存器mbed 将多个引脚操作映射到同一个寄存器时用 mask 标记当前操作的是哪一位从而避免读-改-写带来的竞争问题。这也是 mbed 在实时性上做得比较细致的地方。2.3 串口、定时器等复用型外设的 HAL 设计要点GPIO 只是最简单的数字外设真正体现 HAL 层设计功力的是串口、SPI、I2C 这类复用型外设。以串口为例HAL 层会提供serial_t结构体和serial_init、serial_putc、serial_getc、serial_irq_handler等函数。serial_init的实现过程会比较长根据传入的 TX 和 RX 引脚查找 PinMap 表确定使用哪个 UART 外设实例然后配置引脚复用、使能 UART 时钟、设置波特率、配置数据位和停止位最后使能接收中断。这一整套初始化的顺序很重要如果先使能 UART 外设再配置引脚某些芯片上会出现引脚电平抖动导致的误触发。HAL 层串口实现另一个值得学习的地方是serial_irq_handler的机制。这个函数接收一个函数指针和一个参数指针在中断触发时调用。mbed 的做法是典型的 C 语言回调桥接static void uart_irq_handler(uint32_t id, SerialIrq event) { serial_t *obj (serial_t *)id; if (obj-handler) { obj-handler(obj-handler_arg, event); } }这里把 HAL 层的中断回调转接到了上层 user 层的Serial类上。C 语言函数指针 参数指针的组合是嵌入式里最常见的回调承载方式mbed OS 整个事件框架也建立在这个基础之上。写到这里必须提醒一点在使用 mbed 的 HAL 时尽量不要在中断回调里做耗时操作。很多刚上手 mbed 的人把printf放在串口中断回调里然后发现系统卡死或者输出乱码。原因是 mbed 的printf最终会经过文件系统抽象层这个过程可能会触发调度器操作而在中断上下文里操作调度器是禁区。真要在中断里传数据正确做法是只设置一个标志位或者往消息队列里放一条消息把实际的业务处理放到线程上下文。3. RTOS 层源码解析从 CMSIS-RTOS 到 C 封装3.1 底层内核究竟是谁mbed OS 的 RTOS 层底层并不是自己造轮子的内核而是基于 CMSIS-RTOS 2 标准接口实现的。CMSIS-RTOS 是 ARM 定义的一套 RTOS 标准 API任何 RTOS 内核只要实现了这套接口上层的应用代码就可以无缝迁移。mbed OS 默认使用的是 RTX 5 作为底层内核也就是 Keil RTX5。RTX 5 是一个免版税、源码开放的实时操作系统内核专门为 Cortex-M 处理器优化支持基于位带的精确中断延迟、零中断延迟特性同时内部任务切换做了非常精细的优化。mbed OS 在 RTX 5 之上加了一层 C 封装这就是我们平时在应用里直接使用的rtos::Thread、rtos::Mutex、rtos::Semaphore等类。这种标准接口 具体内核 面向对象封装的三层结构是 mbed OS RTOS 层最核心的设计思路。CMSIS-RTOS 2 定义了osThreadNew、osMutexNew、osMessageQueuePut这些 C 函数RTX 5 实现了这些函数mbed 的 rtos 目录下再用 C 类把这些函数包装成更容易使用的接口。3.2 线程创建的背后发生了什么以rtos::Thread为例。我们通常这样创建一个线程rtos::Thread thread; thread.start(callback(some_function));这行代码背后发生了很多事。Thread的构造函数里会先分配一块线程控制块TCB并设置默认的栈大小、优先级等参数。默认栈大小在rtos/include/rtos/Thread.h里有定义通常是 4KB 左右具体数值取决于平台。start方法内部调用的是osThreadNew。这个函数会做几件事检查 TCB 是否已初始化、设置线程优先级和时间片、把线程添加到就绪队列。关键点在于osThreadNew并不立即运行线程它会先创建一个初始就绪状态的任务实际运行要等到调度器切换到它。mbed 的线程包装有几个容易踩的坑我在实际项目里踩过两次。第一个坑是Thread对象不能随便定在局部变量里然后start。因为线程运行期间对象本身和它的上下文必须一直有效。如果你在一个函数里创建了一个局部Thread函数返回后对象被销毁但线程还在跑这时候访问已经析构的对象就会导致未定义行为。正确做法是把Thread定义成成员变量、全局变量或静态变量。第二个坑是线程栈大小问题。mbed 默认的 4KB 栈对于简单的循环任务通常够用但如果你在中断里调用printf或者使用了较深的递归函数很容易栈溢出。RTX 5 带有栈溢出检测功能一旦溢出会触发osRtxErrorNotify默认行为是进入mbed_die也就是系统死机。在开发阶段建议打开栈水位监控方法是在mbed_app.json里配置平台相关的堆栈统计选项然后周期性调用thread.stack_usage()查看水位。3.3 Mutex、Semaphore、Queue 的封装思路RTOS 层的另一大块是同步与通信原语。rtos::Mutex是对osMutexNew的封装内部使用 CMSIS-RTOS 的互斥锁机制支持递归加锁和优先级继承。mbed 的Mutex类有几个细节做得比较好构造函数里直接创建内核对象析构函数里释放lock方法带超时参数trylock提供非阻塞尝试。rtos::Semaphore的封装类似底层是计数信号量。这里有一个使用心得信号量的初始计数值非常关键mbed 中Semaphore构造参数的含义是可用资源数量。经常有人创建rtos::Semaphore(1)当作互斥锁用这在简单场景下没问题但如果有中断需要释放信号量建议用Semaphore而不是Mutex因为信号量可以在中断上下文安全释放而 Mutex 的释放操作不应该在中断里调用。rtos::Queue和rtos::MemoryPool是 mbed 提供的高层消息传递机制。Queue 底层是一个环形缓冲区支持任意类型的元素内部使用 RTX 的osMessageQueue。我实际用的感受是Queue 的 API 设计比裸 RTOS 的队列接口友好得多它支持 C 对象直接入队不需要手动管理内存拷贝。来看一个典型的生产者-消费者模型这段代码可以从 mbed 官方例程里看到影子rtos::Queueint, 10 queue; rtos::Thread producer; rtos::Thread consumer; void producer_task() { int value 0; while (true) { queue.put(value); value; ThisThread::sleep_for(100ms); } } void consumer_task() { while (true) { int *received; queue.get(received); printf(received: %d\n, *received); } }这个例子里queue.put和queue.get是阻塞调用当队列满或空时线程会进入等待状态。mbed 的Queue内部通过osMessageQueue的等待机制实现不会浪费 CPU 资源忙等。3.4 中断与线程的交互事件标志的作用中断和线程之间的数据交互除了队列之外事件标志EventFlags也是常用手段。rtos::EventFlags封装了 CMSIS-RTOS 的事件标志 API。它和信号量的区别在于信号量是计数累加的而事件标志是对一组二进制位做逻辑判断更适合等待多个条件组合的场景。有一次我做多传感器采集三个传感器完成中断分别设置三个事件位主线程等待所有位都置位后再统一处理数据。如果用信号量逻辑会非常别扭用EventFlags就一行代码event_flags.wait_all(SENSOR1_FLAG | SENSOR2_FLAG | SENSOR3_FLAG);这个 API 内部的实现是线程进入阻塞状态RTX 内核维护一个等待事件标志的线程链表当中断里调用event_flags.set(bit)时内核检查等待线程的条件是否满足满足则把线程唤醒。整个过程不依赖轮询CPU 利用率高延迟也可控。在选择用队列还是事件标志时我的经验法则是如果只是通知某个事件发生了用事件标志如果需要传递具体数据用队列。两者可以混合使用比如中断里设置事件位线程被唤醒后从队列里取数据这种模式在很多 mbed OS 的 BLE 例程里能看到。4. 设备驱动体系从裸机驱动到事件驱动框架4.1 drivers 目录下的类设计思路mbed OS 的drivers目录存放的是芯片无关的外设驱动类DigitalOut、DigitalIn、AnalogIn、PwmOut、I2C、SPI、Serial、CAN等等。这一层直接面向应用开发者是使用频率最高的 API 层。和 HAL 层不同drivers 层的类大量使用 C 特性。以DigitalOut为例class DigitalOut { public: DigitalOut(PinName pin); void write(int value); int read(); DigitalOut operator (int value); operator int(); };这个类内部持有gpio_t对象构造函数调用gpio_init并设置为输出模式。析构函数会把引脚恢复成高阻输入态避免程序退出后引脚状态不确定。operator重载让led 1这种赋值语法成为可能这也是 mbed 代码看起来比较简洁的原因之一。drivers 层另一个重要的设计是回调机制。传统的 STM32 HAL 库中外设中断触发后用户要在HAL_UART_RxCpltCallback这类固定的回调函数里写逻辑。mbed OS 的做法更灵活构造函数或者配置函数里传入一个Callback对象中断触发时调用这个 Callback。Callback 可以绑定普通函数、成员函数、Lambda 表达式极大地提高了代码复用性。比如串口接收中断的使用方式Serial serial(PA_2, PA_3); serial.attach(callback(this, MyClass::on_rx), Serial::RxIrq);这个 attach 最终会把回调注册到 HAL 层的中断处理函数中建立起从硬件中断到用户代码的完整链路。这种设计在嵌入式 OS 里并不算新颖但 mbed 把事件回调做得比较纯粹没有引入太多额外的抽象调试起来很直接。4.2 手写一个传感器驱动的完整流程理解了 drivers 层的设计模式后写一个自己的传感器驱动就顺理成章了。热词里提到 DHT11这个传感器正好适合演示 mbed 驱动的写法因为它用的是单总线协议需要精确的时序控制很适合展示如何在 mbed 里处理时间敏感的操作。DHT11 的数据线只需要一个引脚需要既能输出又能输入。mbed 的DigitalInOut正好支持这种模式。驱动的基本思路是拉低总线至少 18ms 发起开始信号然后释放总线等待传感器拉低响应信号再读取 40 位数据。这里有一个 mbed 特有的注意事项DHT11 的时序要求微秒级精度而 mbed 的wait_us函数在 RTOS 环境下并不保证精确。当系统中有多个线程运行时wait_us可能被线程调度打断导致时序偏移DHT11 读取失败。我在实际开发中常用的解决办法是在读取时序期间暂时屏蔽调度器或者把 DHT11 读取放到一个独占了 CPU 的高优先级线程里。如果是裸机环境直接用wait_us就能满足 DHT11 的时序要求但在 mbed OS 的 RTOS 环境下强烈建议用CriticalSectionLock或者在读取期间禁用调度器否则大概率读到错误数据。#include mbed.h DigitalInOut dht11(PA_4); bool dht11_read(float *humidity, float *temperature) { uint8_t data[5] {0}; CriticalSectionLock::enable(); // 发送开始信号 dht11.output(); dht11.write(0); wait_us(20000); dht11.write(1); wait_us(40); dht11.input(); // 等待响应 uint32_t timeout 0; while (dht11.read() 1) { if (timeout 100) { CriticalSectionLock::disable(); return false; } } // ... 后续读取40位数据 CriticalSectionLock::disable(); *humidity data[0] data[1] / 10.0f; *temperature data[2] data[3] / 10.0f; return true; }这个例子里CriticalSectionLock的用法可以保证临界区内代码不会被线程调度打断代价是这段时间内系统无法响应其他线程。因此临界区代码要尽量短DHT11 的整个时序读取过程大约 5ms在大多数应用场景下可以接受。但如果是硬实时要求很高的系统建议改用 DMA 或者定时器输入捕获来读取时序不要用这种阻塞式方法。4.3 I2C/SPI 总线的驱动封装解读与 GPIO 这类单引脚操作不同I2C 和 SPI 这类总线外设的驱动更复杂。mbed 的I2C类是最常用的。它的核心方法包括write、read、start、stop等底层通过 HAL 层的i2c_系列函数操作硬件外设。mbed 的 I2C 驱动有一个很有意思的细节它内部维护了总线频率、地址长度、是否使用 DMA 等状态frequency方法可以动态改变总线速率。如果你做的项目里同时挂了多个 I2C 设备一个设备要求 100kHz另一个要求 400kHz可以在切换设备时动态调用frequency调整。这个操作在硬件层面会重新计算时钟寄存器分频值比裸机去改寄存器方便很多。SPI 驱动也类似mbed 的SPI类支持主从模式切换、位序控制、时钟极性和相位配置。有一个问题很多人在 mbed 上遇到过SPI 的波特率最终值跟请求值不一致。原因是芯片的 SPI 时钟分频器是有固定档位的不能任意设置。mbed 在初始化时会选择最接近但不高于请求值的分频系数所以实际波特率可能只有标称的 90%。如果你的项目对 SPI 时序有严格需求建议读一下目标平台的SPIObject源码看看分频计算逻辑不要盲目假设设置 10MHz 就一定是 10MHz。5. 测试体系一块板子怎么保证换平台不出事5.1 Greentea 测试框架到底怎么跑mbed OS 自带一套比较完整的测试生态其中最核心的是 Greentea 测试框架。Greentea 是一个 Python 实现的测试调度工具它负责把测试固件烧录到开发板上、通过串口与板卡通信、汇总测试结果。开发者在 mbed 中写的测试用例会被编译成一个独立的测试固件板卡运行后在串口上输出特定的打印信息Greentea 的 Python 脚本解析这些信息判断测试是否通过。Greentea 的测试用例注册方式比较特别。你会在测试源码里看到类似这样的宏static control_t test_case_1() { TEST_ASSERT_TRUE(some_condition); return CaseNext; } Case cases[] { Case(test case 1, test_case_1), };这段代码会被特殊处理。Case结构体后面的字符串是测试用例名称Greentea 会解析编译产物的符号表把这些测试名称收集起来与板卡串口上报的测试名称做匹配。前期调试测试用例时常见的问题是测试名称不匹配导致 Greentea 报错 Test case not found。这个坑通常是测试源码更新了但编译缓存没有刷新清理重建一般能解决。跑 Greentea 测试的关键命令是mbed test -t GCC_ARM -m DISCO_L475VG_IOT01A这条命令会编译所有测试用例并尝试烧录运行。如果只是编译不烧录可以加--compile参数如果板卡连接了多个串口可能需要用--serial-port指定正确的串口。5.2 单元测试与硬件在环测试的分工Greentea 偏向硬件在环测试即真实板卡上运行、通过串口上报结果。另一套测试方式是基于 Unity 的单元测试运行在主机上不依赖硬件。mbed 的测试目录里UNITTESTS文件夹存放这类纯软件测试。这两套测试的分工非常清晰单元测试验证纯逻辑比如协议解析、状态机、算法Greentea 验证硬件相关功能比如 GPIO 翻转、串口收发、I2C 读写外设芯片。你写的驱动代码如果和硬件耦合不深的部分抽出来做单元测试能显著提高调试效率。典型的做法是传感器驱动里的数据帧校验逻辑抽成纯函数放在 UNITTESTS 里跑不必每次改一行代码都烧录板卡验证。测试体系还有一个值得说的点mbed OS 的测试代码里大量使用了MBED_TEST_ASSERT之类的宏。这些宏除了打印断言信息外还会上报给 Greentea做统计。在编写自己的驱动测试时建议按 mbed 规范的断言风格来写这样结果输出更容易和 Greentea 对接。5.3 实际跑测试时踩过的效率陷阱硬件在环测试最大的痛点是时间。一个测试用例从编译到烧录再到跑完通常需要几秒甚至十几秒。如果一次改了很多代码每轮验证都要等效率很低。我个人的经验是先跑编译编译能过再烧板先跑最小用例集确认核心功能没问题再跑全量测试。Greentea 支持指定单个测试文件来跑可以使用mbed test -t GCC_ARM -m YOUR_BOARD --tests network这种形式的过滤。另外Greentea 的测试输出可以加-v参数查看详细日志排查问题时这个输出很重要因为它会显示板卡串口输出的完整原始数据。调试中断相关的测试时有一个常见的坑mbed OS 的测试固件默认开启了看门狗或者低功耗模式如果测试用例执行时间过长可能触发看门狗复位导致 Greentea 认为板卡没有响应而报超时。遇到这种情况检查mbed_app.json里低功耗相关的配置把看门狗关掉再跑测试。6. 开发中的高频故障与排查经验6.1 编译与链接阶段的典型问题mbed OS 的编译体系基于 Mbed CLI 2 或 CMake对新手来说最常遇到的是编译选项不匹配的问题。比如你用了某个外设类但它只在特定目标平台上支持编译时就会报 undefined reference。这个错误信息比较隐蔽它不会直接说这个平台不支持而是报找不到某个函数的符号。排查思路是去targets.json里查当前目标平台的设备列表看看你用的外设是否在该平台的device_has列表里。比如 RTC 功能不是所有平台都支持用之前先确认一下。另一个常见问题是浮点运算相关的链接错误。Cortex-M4 和 M7 平台支持硬件浮点单元mbed 默认使用硬浮点 ABI但如果你在工程里手动加入了一个编译选项不对的第三方库就可能报uses VFP register arguments, target does not的错误。解决办法是确认所有参与链接的编译单元都使用一致的-mfloat-abi选项。6.2 运行时崩溃的定位思路mbed OS 提供一个运行时错误处理机制当系统检测到致命错误时会调用mbed_error_printf输出错误信息然后停在mbed_error函数里。调试这种崩溃第一步是看串口输出的错误码。mbed 错误码有一套编码规则包含错误类型、错误模块、错误值。比如MBED_ERROR_CODE_ASSERT_FAILED配合MBED_ERROR_MODULE_PLATFORM提示你平台层有断言失败。定位断言失败的一个实用技巧是使用MBED_ASSERT宏。在关键逻辑处加上自己的断言一旦条件不满足输出会包含文件和行号。这样能快速缩小问题范围。我在调试 I2C 总线时经常在写入操作前后加断言检查总线忙状态能立刻发现总线锁死的问题。内存问题也是崩溃高发区。mbed OS 使用动态内存分配如果没有合理配置堆大小运行一段时间后可能出现out of memory错误。排查时可以用mbed_stats_heap_get读取堆的使用情况看看剩余量。对于长期运行的设备建议对堆和栈的使用设定监控任务定期采集数据防止内存碎片导致的不稳定。6.3 与 STM32 HAL 库混用的注意点很多开发者拿到 mbed OS 的工程后习惯性地想把以前 STM32 HAL 库的代码直接搬进去用。这里有一个大坑mbed OS 和 STM32 HAL 库对同一份硬件资源的管理方式完全不同混用可能导致外设状态不一致。最典型的冲突是时钟配置。mbed OS 启动时会通过平台代码配置系统时钟如果你在应用初始化时又调用了HAL_RCC_ClockConfig可能会覆盖 mbed 的配置导致串口波特率、定时器周期全部偏离预期。我的建议是如果要在 mbed 里复用 STM32 HAL 库代码先确认它操作的外设不会被 mbed 的驱动同时使用。比如你单独用 HAL 库操作一个 TIM 定时器而 mbed 的 ticker 也用了另一个定时器两边互不干扰这种情况下混用是可行的但如果两边碰了同一个外设就等着出 bug 吧。另一个注意点是中断优先级。mbed OS 的 RTX 内核依赖特定的中断优先级分组配置如果你手动修改了 NVIC 优先级分组RTX 的调度可能异常。实际遇到的案例里有人为了某个高优先级外设中断把整个系统的中断优先级分组从 4 位抢占优先级改成了 3 位结果系统频繁死机。排查了很久才发现是优先级分组被改导致 RTX 的内部机制失效。如果确实需要某个中断拥有超高优先级建议在 mbed 的中断优先级策略框架内调整而不是直接动 NVIC 分组。7. 一点个人的体会从裸机开发切换到一个完整的 RTOS 平台最大的感受不是 API 好不好用而是思维方式的变化。写裸机代码的时候心里装的是寄存器、中断标志、轮询循环写 mbed OS 应用的时候心里装的是线程、队列、事件、回调。这种抽象让我们能把更多精力放在业务逻辑上但也要求我们对底层机制有足够深入的理解否则出了问题会特别被动。我始终觉得学习 mbed OS 最好的方式不是只看文档而是把源码真正打开读一遍。从一个简单的 GPIO 操作追到 HAL 层从一次线程创建追到 RTX 内核的调度逻辑从一次串口中断追到事件回调链路的完整路径。走完这几条链路之后你不仅理解了 mbed OS 的设计智慧也会对整个嵌入式系统的架构有全新的认识。
返回列表