第二章 嵌入式系统基础:微控制器(MCU)架构、通信协议与传感器采集

好,咱们正式开始啃嵌入式系统这块硬骨头。说实话,很多搞TinyML的朋友最后翻车,不是模型没写好,而是连MCU的基本脾气都没摸透。我当年第一次把模型部署到STM32上,板子直接死机,查了三天才发现是GPIO配置冲突了。嗯,这种坑,咱们今天一次性讲清楚。

2.1 微控制器(MCU)架构:一颗芯片就是一个微型计算机

MCU说白了就是一颗集成了CPU、内存、外设接口的芯片。你想想看,它跟电脑CPU最大的区别是什么?电脑CPU只管算,内存、硬盘、显卡都得外挂。而MCU呢?RAM、Flash、定时器、ADC全塞在一个封装里,上电就能跑。

我个人习惯把MCU架构拆成三块来看:

  • 内核(Core):负责执行指令。ARM Cortex-M系列是TinyML的绝对主力,M0主打低功耗,M4/M7带FPU和DSP指令,跑神经网络推理会快很多。
  • 存储器(Memory):Flash存程序,RAM存运行时数据。注意,TinyML模型通常放在Flash里,推理时再加载到RAM。
  • 外设(Peripherals):GPIO、UART、I2C、SPI、ADC、定时器……这些才是MCU跟物理世界打交道的触手。

核心要点:选型时别光看主频,要看Flash和RAM够不够装你的模型。我见过有人用8KB RAM的MCU硬塞一个50KB的模型,结果可想而知。

2.2 GPIO:最基础也最容易出坑的接口

GPIO(通用输入输出引脚)是MCU最基础的通信方式。你可以把它想象成一根数字信号线,要么输出高电平(3.3V或5V),要么输出低电平(0V)。

我在项目中遇到过最典型的坑:GPIO输出模式配置错误。比如你要驱动一个LED,结果把引脚配成了输入模式,灯死活不亮。更隐蔽的是,有些MCU的GPIO默认是浮空输入,不接上拉电阻的话,电平会乱跳。

避坑指南:我曾经在批量生产时发现10%的板子按键失灵,查到最后是GPIO内部上拉电阻没使能。记住:按键输入一定要配置内部上拉或下拉,别指望外部电阻——省那几分钱,后面维修成本翻十倍。

GPIO的典型应用场景:

  • 控制LED、继电器、蜂鸣器
  • 读取按键、开关状态
  • 模拟PWM(通过定时器翻转电平)
  • 作为软件模拟I2C/SPI的引脚

2.3 UART:最古老的串行通信,调试必备

UART(通用异步收发器)是嵌入式开发者的「眼睛」。你想想看,MCU跑起来后,你怎么知道它内部发生了什么?靠UART打印日志啊!

UART通信只需要两根线:TX(发送)和RX(接收)。双方约定好波特率(比如115200),就能收发数据。注意,UART是异步的,没有时钟线,所以波特率必须一致,否则收到的全是乱码。

我个人习惯在调试阶段把UART波特率设高一点(921600),这样打印大量数据时不会卡顿。但量产时建议降到115200,抗干扰能力更强。

小技巧:用UART调试时,记得在关键函数入口和出口各打一条日志。我曾经靠这个方法,定位到一个只在特定时序下触发的bug——那个bug让我加班到凌晨三点。

2.4 I2C:两线制总线,传感器的最爱

I2C(Inter-Integrated Circuit)用两根线(SCL时钟线、SDA数据线)挂载多个设备。每个设备有唯一地址,主机通过地址寻址。你想想看,一个MCU可以同时挂温度传感器、加速度计、气压计,只需要两根线,是不是很爽?

I2C的典型速率:标准模式100kHz,快速模式400kHz,高速模式3.4MHz。对于TinyML应用,传感器数据量通常不大,100kHz就够用了。

我在项目中遇到过I2C通信失败的经典场景:

  • 上拉电阻没接或阻值不对(通常4.7kΩ)
  • 地址冲突(两个传感器用了相同地址)
  • 时钟延展导致超时(从机拉低SCL不放)

经验之谈:调试I2C时,逻辑分析仪是你的好朋友。别靠猜,直接抓波形看。我见过有人花了两天查代码,最后发现是焊盘虚焊——波形一看就明白了。

2.5 SPI:高速全双工,适合大数据量传输

SPI(串行外设接口)比I2C快得多,因为它有四根线:SCLK(时钟)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、CS(片选)。全双工通信,意味着可以同时收发数据。

SPI的速率可以跑到几十MHz,适合传输图像、音频等大数据。TinyML中,如果你用外部Flash存模型,或者用摄像头采集图像,SPI是首选。

SPI有四种模式(CPOL和CPHA的组合),这个特别容易搞混。我建议你记住一个原则:看从机数据手册,它说用模式0你就用模式0,别自作聪明。

避坑指南:我曾经在SPI通信时发现数据总是错位,查了半天发现是CS片选信号没处理好。记住:每次通信前先拉低CS,通信完再拉高,中间不要有其他操作干扰。

2.6 传感器数据采集基础:从物理量到数字信号

传感器采集的本质是什么?把物理量(温度、加速度、气压)转换成电信号,再通过ADC(模数转换器)变成数字量。MCU内部通常集成了ADC,分辨率从8位到16位不等。

采集流程大致如下:

  1. 传感器上电并稳定(通常需要几毫秒到几百毫秒)
  2. 通过I2C/SPI读取传感器寄存器
  3. 将原始数据转换成物理量(比如ADC值转成温度)
  4. 数据预处理(滤波、归一化)后送入模型推理

我个人习惯在采集数据前先做一次「自检」:读取传感器的ID寄存器,确认通信正常。这一步能避免很多低级错误。

实用技巧:传感器数据通常有噪声,别直接喂给模型。我常用的方法是滑动平均滤波:取最近N个采样值的平均值。N取4或8,计算量小,效果也不错。

2.7 实战:用I2C读取温度传感器

咱们来点实际的。假设你手头有一个I2C接口的温度传感器(比如SHT30),怎么用MCU读取数据?

// 伪代码示例:I2C读取SHT30温度
#include "i2c.h"

#define SHT30_ADDR 0x44  // 7位地址

void read_temperature(float *temp) {
    uint8_t cmd[2] = {0x2C, 0x06};  // 单次测量命令
    uint8_t buf[6] = {0};
    
    // 发送测量命令
    i2c_write(SHT30_ADDR, cmd, 2);
    delay_ms(20);  // 等待测量完成
    
    // 读取6字节数据
    i2c_read(SHT30_ADDR, buf, 6);
    
    // 计算温度:原始值 / 65535 * 175 - 45
    uint16_t raw = (buf[0] << 8) | buf[1];
    *temp = (raw / 65535.0f) * 175.0f - 45.0f;
}

这段代码看起来简单,但有几个细节要注意:

  • I2C地址是7位还是8位?很多库函数会自动左移一位,别重复移位
  • 等待时间要足够,但别太长。SHT30的测量时间约15ms,我设20ms留余量
  • 原始数据转物理量的公式,每个传感器都不一样,一定要看数据手册

总结一下:嵌入式系统基础是TinyML的根基。GPIO、UART、I2C、SPI这四个协议,你至少得熟练掌握两个(I2C和SPI最常用)。传感器采集看似简单,但时序、噪声、转换公式这些细节,往往决定了你的模型能不能在真实环境中跑起来。

下一章咱们开始讲「数据预处理与特征工程」,到时候我会分享一个我踩过的坑:采集了10000条数据,结果因为采样率不一致,模型准确率直接掉到50%。嗯,到时候细聊。