1. 课程导入
TinyML 是指在资源极度受限的微控制器(MCU)或极低功耗处理器上部署机器学习模型的技术与实践。与云端或边缘服务器上的 AI 不同,TinyML 设备通常只有几十 KB 到几 MB 的 RAM,Flash 存储以 MB 计,CPU 主频在几十到几百 MHz 之间,且往往依赖电池供电。在这样的约束下,将传感器数据采集、信号预处理、模型推理和业务逻辑全部揉杂在一个 main() 函数或单一文件中,或许能在 Demo 阶段跑通,但一旦进入产品化、量产维护或功能迭代阶段,就会迅速演变为一场灾难。本章作为整个课程的开篇,将系统阐述 TinyML 工程分层的必要性、核心目标,并给出贯穿后续所有章节的整体架构蓝图。
上图展示了本课程所倡导的 TinyML 工程分层架构。从底层的硬件抽象到顶层的业务逻辑,每一层都有明确的职责边界和通信契约。这种分层并非为了追求理论上的整洁,而是直接源于 TinyML 产品开发中反复出现的痛点:传感器型号更换导致上层代码大面积修改、模型更新后前处理参数不匹配、业务逻辑与推理过程耦合导致无法单独测试、以及多人协作时代码冲突不断。接下来,我们将深入剖析分层的必要性、目标,并逐一解读各层的职责与交互方式。
一、为什么 TinyML 必须分层:从三个真实痛点说起
在传统嵌入式开发中,工程师习惯将代码按功能模块划分,但在 TinyML 项目中,由于数据流贯穿传感器→预处理→模型→决策,开发者往往倾向于将整个流程写成一个顺序执行的循环。这种“直通式”写法在原型验证阶段效率极高,但进入产品化阶段后,以下三个问题会迅速暴露:
- 硬件耦合导致“换传感器即重写”:某智能家居项目使用 BMI160 加速度计完成跌倒检测,当供应链原因需要替换为 LSM6DSO 时,由于数据读取代码、量程转换、滤波参数全部散落在业务逻辑中,工程师不得不逐行排查所有涉及加速度数据的代码,耗时两周才完成替换,且引入了新的噪声问题。
- 模型迭代与前后处理脱节:团队将模型从浮点版本量化为 INT8 后,发现推理结果异常。排查发现,前处理中的归一化参数仍按浮点模型计算,而量化模型要求输入范围是 [-128, 127]。由于前处理与推理引擎没有清晰的接口契约,这类问题极难定位。
- 测试与验证成本高:当业务逻辑(如告警阈值判断)与推理代码直接嵌套时,无法单独验证“模型输出正确但业务策略有误”的场景。每次测试都必须运行完整的数据采集和推理流程,且难以模拟边界条件。
这些痛点的根源在于:TinyML 系统本质上是一个数据驱动的信号处理与决策系统,其各个阶段具有完全不同的变化频率和验证方法。传感器驱动跟随硬件变化,前处理跟随信号特征和模型输入要求变化,推理引擎跟随模型格式和算子支持变化,业务逻辑则跟随产品需求变化。如果不进行分层,这些不同频率的变化会相互干扰,导致“牵一发而动全身”。
二、分层架构的核心目标与设计原则
TinyML 工程分层并非简单地把代码放进不同文件夹,而是建立一套稳定的接口契约和单向依赖关系。其核心目标可归纳为以下四点:
| 目标 | 说明 | 衡量指标 |
|---|---|---|
| 可替换性 | 更换传感器、MCU 或模型时,受影响范围被限制在单一层内 | 替换传感器时,业务层代码改动量为 0 行 |
| 可测试性 | 每一层都可以独立进行单元测试或模拟测试 | 前处理层可在 PC 端用 Python 验证输出 |
| 可维护性 | 新工程师能快速定位问题所在层,而非通读全部代码 | Bug 定位时间缩短 50% 以上 |
| 可演进性 | 支持模型在线升级、传感器动态配置、业务策略远程调整 | OTA 升级包仅包含变更层对应的二进制 |
为了实现上述目标,分层设计需要遵循三条基本原则:
- 单向依赖:上层可以调用下层接口,但下层绝不能反向依赖上层。例如,驱动层不知道业务逻辑的存在,推理引擎也不关心数据是来自真实传感器还是测试向量。
- 数据契约先行:层与层之间传递的数据结构必须提前定义并固化。例如,驱动层输出的是经过校准的物理量(如加速度单位为 m/s²),而非原始寄存器值;前处理层输出的是模型输入张量的标准格式(如 INT8 数组 + 缩放因子)。
- 配置与逻辑分离:各层的行为参数(如采样率、滤波截止频率、告警阈值)应通过配置结构体或配置文件注入,而非硬编码在层内部。这样可以在不修改代码的情况下调整行为。
三、各层职责详解与交互流程
下面我们逐一剖析上图中五个层次的具体职责、典型实现方式以及它们之间的交互契约。
1. 硬件抽象层(HAL)
HAL 是直接面对 MCU 寄存器和外设的一层。它的存在意义在于:让上层驱动代码与具体的 MCU 型号解耦。例如,STM32 和 ESP32 的 GPIO 操作方式完全不同,但通过 HAL 封装后,驱动层可以调用统一的 hal_gpio_write(pin, level) 接口。HAL 通常包含:
- 时钟使能与复位控制
- GPIO 读写与中断注册
- UART/I2C/SPI 总线的底层收发
- 定时器与 PWM 输出
- 低功耗模式切换(sleep/deep-sleep)
在实际项目中,HAL 可以由芯片厂商的 SDK(如 STM32Cube HAL、ESP-IDF)直接提供,也可以基于 CMSIS 自行封装。关键是要保证 HAL 的接口风格统一,且不包含任何业务语义。
2. 驱动层(Driver)
驱动层是传感器、执行器、通信模块等具体器件的“翻译官”。它负责将 HAL 提供的原始寄存器读写能力,转换为具有明确物理意义的数据结构。以加速度计为例,驱动层的典型接口为:
typedef struct {
float x; // 单位: m/s²
float y;
float z;
uint32_t timestamp_ms; // 采样时刻
} accel_sample_t;
// 初始化传感器,返回 0 表示成功
int accel_init(const accel_config_t *cfg);
// 读取一帧数据,阻塞或非阻塞
int accel_read(accel_sample_t *sample);
驱动层需要处理的工作包括:配置传感器量程和采样率、执行自检、进行温度补偿、处理 I2C 通信错误重试等。驱动层输出的数据必须是“干净”的物理量,而不是原始寄存器值——这一约定可以避免上层重复进行单位换算和位操作。
3. 信号前处理层(Pre-processing)
前处理层是 TinyML 系统中容易被低估但至关重要的一环。它的输入是驱动层提供的原始采样序列,输出是模型可以直接消费的特征张量。常见的前处理操作包括:
- 滤波:去除工频干扰(50Hz/60Hz)、高频噪声或基线漂移
- 窗口化:将连续数据流切分为固定长度的帧(如 256 点、1 秒窗口)
- 特征提取:计算时域统计量(均值、方差、峰值)、频域特征(FFT 幅度谱)、或专用特征(MFCC 用于语音)
- 归一化/标准化:将数据缩放到模型期望的输入范围(如 [-1, 1] 或 [0, 255])
前处理层必须与模型训练时的数据预处理流程保持一致。例如,如果在训练时使用了 100Hz 低通滤波,那么部署时的前处理也必须使用相同的滤波参数。这一层通常需要支持在 PC 端用 Python 进行离线验证,因此建议将核心算法实现为纯 C 函数,便于交叉编译和测试。
4. 推理引擎层(Inference Engine)
推理引擎层负责加载模型、管理张量内存、执行算子计算。它对外提供统一的推理接口,隐藏了模型格式(TFLite、ONNX 转换后的 C 数组)、量化方案(INT8、FLOAT16)和算子实现细节。典型的接口设计如下:
typedef struct {
const uint8_t *model_data;
size_t model_size;
int tensor_arena_size; // 推理工作区大小
} inference_config_t;
typedef struct {
int8_t *input; // 输入张量指针
int8_t *output; // 输出张量指针
size_t input_len;
size_t output_len;
} inference_result_t;
int inference_init(const inference_config_t *cfg);
int inference_run(const int8_t *input, size_t input_len,
int8_t *output, size_t output_len);
推理引擎层需要处理的关键问题包括:内存池管理(避免动态分配)、算子内核优化(利用 CMSIS-DSP 或硬件加速指令)、以及异常处理(如模型校验失败、推理超时)。这一层通常由 TFLite-Micro 或类似框架提供,但工程上需要对其进行封装,以便未来更换推理框架时不影响上层代码。
5. 业务逻辑层(Application)
业务逻辑层是产品功能的直接体现,它决定了“拿到推理结果后做什么”。例如:
- 如果模型输出是“跌倒”概率 > 0.8,则触发蜂鸣器并发送 BLE 告警
- 如果连续 10 次推理结果均为“静止”,则进入低功耗模式
- 根据推理结果调整采样频率(动态功耗管理)
业务逻辑层应该完全感知不到传感器型号、模型内部结构或前处理算法的细节。它只面对两个东西:前处理层输出的特征张量和推理引擎输出的结果张量。这种隔离使得产品经理调整业务策略时,只需要修改这一层的代码,而无需触碰底层任何模块。
四、案例拆解:一个工业振动监测设备的重构过程
为了更直观地展示分层带来的收益,我们来看一个实际的工业案例。某工厂部署了一批旋转机械振动监测设备,用于预测性维护。设备使用 ADXL345 加速度计采集振动信号,通过 TinyML 模型判断设备状态(正常/异常/严重)。
初始状态(未分层):所有代码写在一个 main.c 文件中,约 2000 行。数据采集、滤波、FFT、模型推理、告警逻辑全部顺序执行。设备工作基本正常,但存在以下问题:
- 更换为 ADXL355(更高精度)时,需要修改 17 处代码,包括 I2C 地址、量程设置、数据格式转换
- 模型升级后,发现前处理中的归一化参数未同步更新,导致推理准确率下降 15%
- 无法在实验室单独测试告警逻辑,必须连接真实传感器并等待振动事件发生
重构为分层架构后:团队按照本章所述的五层模型进行重构,耗时 3 天完成代码迁移。重构后的代码结构如下:
project/
├── hal/ # 硬件抽象层
│ ├── hal_gpio.c
│ ├── hal_i2c.c
│ └── hal_uart.c
├── drivers/ # 驱动层
│ ├── adxl345.c
│ └── sht30.c
├── preprocessing/ # 前处理层
│ ├── filter.c
│ ├── fft.c
│ └── normalize.c
├── inference/ # 推理引擎层
│ ├── tflite_micro_wrapper.c
│ └── model_data.c
└── app/ # 业务逻辑层
├── main.c
└── alarm_policy.c
关键改进与结果:
| 改进项 | 重构前 | 重构后 |
|---|---|---|
| 更换传感器耗时 | 2 周(涉及 17 处修改) | 2 小时(仅修改驱动层,新增 adxl355.c) |
| 模型升级验证 | 需人工检查所有参数 | 前处理层有独立单元测试,自动验证输出范围 |
| 告警逻辑测试 | 需真实振动台 | 可注入模拟推理结果,在 PC 上完成测试 |
| 代码可读性 | 2000 行单文件 | 每层 200-400 行,职责清晰 |
复盘经验:重构过程中最大的阻力来自“觉得没必要”的心理——因为设备已经能工作了。但重构完成后,团队发现后续的功能迭代(如增加温度监测、调整告警阈值)变得异常轻松。这个案例说明,分层架构的收益不是立竿见影的,而是在项目生命周期中持续释放的。
五、常见误区与总结复盘
在指导多个 TinyML 项目落地后,我们发现工程师在分层实践中容易陷入以下误区:
- 误区一:分层 = 多写文件。分层的关键是接口契约和依赖方向,而不是文件数量。如果两个文件互相包含头文件,即使分属不同文件夹,也是耦合的。
- 误区二:过度设计。对于只有 1-2 个传感器、模型固定不变的超简单项目,强行五层分离反而增加维护成本。建议从三层(驱动+处理、推理、应用)起步,随着复杂度增加再细化。
- 误区三:忽略数据契约的版本管理。当模型升级导致输入张量维度变化时,前处理层和推理层的接口必须同步更新。建议在接口头文件中加入版本宏,并在初始化时进行兼容性检查。
- 误区四:性能损失不可接受。合理的分层设计几乎不增加运行时开销——函数调用在编译时会被内联优化,数据传递通过指针完成。真正的性能瓶颈在于算法本身,而非分层结构。