嵌入式SDK架构设计经验-高级进阶心得

今天来分享下工作这些年以来设计嵌入式SDK的一些经验。

相信大家在日常工作过程中,都或多或少接触过不同的SDK,像STM32Cube、MCUXpresso、ESP-IDF、Zephyr、nRF Connect SDK这些都是业内非常知名的SDK设计。

我自己这几年工作中,也和团队搭建了好几个产品相关的SDK平台,今天来聊聊我对SDK设计的理解。

记住设计思路

SDK 不是把驱动代码打个包,而是把配置、构建、调试、交付和长期维护的秩序一起设计出来。

在嵌入式、单片机领域,SDK的形态其实前后是经历了四次变化的,从寄存器的开发包,到外设抽象的库方式,再到系统级组件化SDK。我们可以来看看每个阶段的设计思路:

十多年前的SDK设计

十多年前,我使用的 MCU 的 SDK,其实非常简单直接。

一个启动文件,一个链接脚本,一堆寄存器头文件,再给几个外设例程。用户把工程导进 Keil 或 IAR,GPIO 点个灯,UART 打个 hello world,基本就算入门。

这个阶段的SDK设计目标很明确:让用户离硬件近一点,把寄存器怎么配讲清楚就行。

这种方式也不是不好。你做小项目、做单板验证、做课程实验,非常方便。问题出在项目变多以后。

芯片型号一多,开发板一多,客户一多,麻烦就来了。

比如同一个 UART 模块,A 客户裸机用,B 客户 FreeRTOS 用,C 客户要接 RT-Thread。你一开始为了方便,把xSemaphoreTake()写进驱动里,后来客户说我不用 FreeRTOS,你就尴尬了。这个时候你就需要重新添加 OSAL,也就是操作系统抽象层,这时候就需要彻底重构了,改动N多文件才行。

还有板级配置。

不同板卡有不同的外设、不同芯片型号有不同的配置信息,早期大家喜欢把这些东西写到board.happ_config.h、示例 README,甚至直接写进某个 C 文件。项目开始时配置非常快,但是两三年后,里面的内容越来越臃肿,配置非常多,这时候就会非常乱。

外设抽象SDK设计

后来 STM32Cube、MCUXpresso SDK 这类厂商 SDK 慢慢成熟起来,SDK 不再只是寄存器资料包,而是变成一套芯片软件产品。

你会看到 HAL、LL、BSP、Middleware、Examples 这些东西。

HAL 给大多数用户一个好用的外设接口,LL 给需要性能和细粒度控制的人留更多的驱动方式,BSP 负责开发板上的 LED、按键、外部 Flash、传感器,中间件再放入 USB、文件系统、TCP/IP、图形库、RTOS 适配。

这种设计最大的好处是上手快。

很多工程师第一次做 STM32,就是靠 CubeMX 把时钟、GPIO、串口先配起来。先跑通,再回头看时钟树、外设寄存器、中断优先级。这个路径对入门非常友好。

但你真要把 SDK 当平台维护,传统厂商 SDK 的历史包袱也会冒出来。

目录按芯片系列设计,例程按板卡设计,IDE 工程一大堆,组件依赖藏在工程文件里。一个工程怎么同时支持多个平台,如何一键配置不同外设,如果在工程中快速剪裁,如果支持不同的构建方式,虽然厂商也在尽可能的补这部分的配置,但是目前来看,体验还是比较差。

Zephyr 和 ESP-IDF 给了两个很好的参照

这几年绕不开两个路线,一个是 Zephyr,一个是 ESP-IDF。

Zephyr 的思路借鉴了Linux框架。它不是把 SDK 当某个芯片的外设包,而是把内核、驱动模型、设备树、Kconfig、构建系统、板级描述、测试框架都放进一个 RTOS 平台里。

设备树用来描述硬件拓扑和板级连接,Kconfig 管功能开关和裁剪,驱动通过统一 device model 注册,west/manifest 管多仓库和版本。

这套架构,你可能第一次看起来感觉繁琐、麻烦。

像传统 MCU 工程师第一次看到 devicetree,经常会有一种感觉:我不就是配个 UART 吗,怎么多出这么多文件?

但等你的产品线复杂起来,比如 BLE、Matter、Thread、MCUboot、安全启动、OTA、多核通信都要管,Zephyr 这种结构的价值就出来了。它牺牲了一点入门爽感,换来多板、多芯片、多协议长期演进的秩序。

ESP-IDF 则是另一种设计思路。

它没有先追求“我要通用到所有 MCU”,而是围绕 Espressif 自家芯片,把 FreeRTOS、Wi-Fi、BLE、网络协议栈、NVS、分区表、OTA、安全启动、日志、事件循环、组件构建系统、idf.py工具都打包到工作流中。

你用 ESP-IDF 做项目,会明显感觉到它交付的不是单个驱动,而是一套产品开发工作流。

这个设计很值得我们学习。

相信很多有三五年工作经验的同学在自己工作中也设计过一些大大小小的SDK或者工程架构,但是很多同学设计时,都是基于怎么交付“代码”进行设计的,没有围绕怎么交付“做项目”这个点去做,

例如:构建怎么跑,组件怎么加,配置怎么改,日志怎么看,固件怎么打包,OTA 包怎么生成,版本怎么查,出了问题怎么抓现场信息。

如何设计一个产品的SDK?

如果让我设计一个新的嵌入式产品SDK,那我们可以参考下方思路:

首先,做好分层处理。

  1. 1.比如最底下这层,放 CMSIS、启动文件、中断向量、链接脚本、寄存器定义。这层尽量别掺业务逻辑。
  1. 2.再往上是 SoC 支持,处理时钟、复位、电源域、pinmux、芯片勘误。这里要非常克制,不要把板子上的传感器初始化塞进来。
  1. 3.然后才是 drivers,比如 sensor、display、flash、radio 这类可实例化设备。它们应该依赖抽象过的 I2C/SPI/GPIO,而不是某个芯片的寄存器配置。
  1. 4.再往上是 middleware,USB、TCP/IP、文件系统、BLE、crypto、GUI 这些。
  1. 5.services 这一层经常被忽略,但我觉得很重要的。日志、事件、配置存储、OTA、shell、diagnostics,这些东西不是具体业务,但每个产品都会用。

我比较喜欢这样的目录:

sdk/ manifests/ tools/ docs/ scripts/ components/ cmsis/ soc/ ll/ hal/ osal/ drivers/ middleware/ services/ boards/ examples/ tests/ templates/

这种设计的好处是soc不能越界到板级器件,hal不能偷偷依赖 FreeRTOS,drivers不能直接摸寄存器,examples不能只是摆设,tools不能变成一堆没人敢删的脚本。

然后,配置脚本保持一致

SDK 维护到后期,最折磨人的不是某个驱动 bug,而是配置混乱。我倾向于把配置分成三类。

芯片能力放soc.yaml,比如有几个 UART、几个 SPI、DMA 通道、中断号、时钟源、电源域。板级连接放board.yaml,比如 LED 极性、按键引脚、晶振频率、外部 Flash 型号、传感器中断脚。功能裁剪放sdkconfig,比如是否启用日志、USB、文件系统、BLE、OTA。

你可以用 YAML、JSON、TOML,也可以更进一步用 Devicetree。格式不是最重要的,最重要的是做好模块分类,让每类配置都放置到对应文件中。

接着,别让 IDE 当唯一入口

很多同学设计SDK时会默认把 IDE 当设计的唯一入口。

因为用户喜欢图形界面,FAE 演示也方便。Keil 点一下,IAR 点一下,CubeIDE 点一下,工程就跑起来了,看着挺方便。但你做平台 SDK,命令行构建必须优先支持。IDE 可以有,但它应该是前端,不应该是唯一入口。

原因很简单:CI 不会帮你点 IDE,后期单元测试、自动化构建甚至接入AI编程,命令行构建才更灵活。

尽可能把构建、烧录、固件打包、签名、OTA 包生成规范起来,走一个统一入口。至于这个入口叫sdk.pyidf.pywest都无所谓。

这部分就是我对嵌入式SDK设计的一些理解。

另外,如果你感觉看理论还是差点意思,你也可以看看我录制的这套Linux项目课程,里面的SDK设计也花了我很多心思,这个SDK基于CMake自动化构建,支持X86、ARM平台,集成交叉编译工具链,PC模拟器,支持组件化设计,模块化编程,资源与业务分离,前后端分离,动态页面加载。

复制链接或点击下方阅读原文:

https://www.bilibili.com/cheese/play/ss147530510

通过这类项目,带你了解一个企业级的产品软件架构设计,学习高级工程师开发思路,后期无论是在企业中开发项目、面试谈论架构设计、都能帮助你从容应对,凭借扎实架构能力冲击更高薪资。