4. 传感器数据采集基础:GPIO与ADC配置实战,I2C/SPI总线驱动温湿度传感器(SHT30),DMA传输与双缓冲机制

各位同学,欢迎来到第四章。这一章我们真正开始“动手摸传感器”。

我个人习惯把传感器采集比作嵌入式系统的“五官”。数据不准,后面做再花哨的推理也是白搭。今天咱们就围绕一个实战场景——用STM32读取SHT30温湿度传感器,把GPIO、ADC、I2C/SPI、DMA和双缓冲这几个核心知识点串起来。

4.1 GPIO与ADC:从数字到模拟的“第一公里”

先聊GPIO。很多人觉得GPIO就是点灯,其实不然。在传感器采集里,GPIO承担着“使能”、“中断触发”和“协议时序”三个角色。

关键认知: GPIO的推挽输出与开漏输出,在I2C总线中直接决定了通信成败。开漏输出配合上拉电阻,才能实现“线与”功能。

ADC呢?说白了就是把电压值变成数字。STM32的ADC分辨率通常是12位,也就是0~4095。我遇到过不少新手,直接拿ADC原始值当物理量用,结果数据完全对不上。

举个例子,你测一个0~3.3V的模拟信号,ADC读回来是2048。那实际电压是多少?

V = (ADC_Value / 4096) * 3.3V
  = (2048 / 4096) * 3.3
  = 1.65V

嗯,这里要注意:STM32的参考电压Vref+默认接VDDA,如果你板子上VDDA不是精确的3.3V,那就要用内部参考电压通道做校准。我曾经在一个项目中忽略了这点,采集的电池电压偏差了0.2V,排查了整整两天。

4.2 I2C/SPI总线驱动SHT30:实战选型与配置

SHT30是Sensirion家的数字温湿度传感器,精度不错,价格也亲民。它支持I2C和SPI两种接口。我个人更推荐I2C,因为连线少,两根线就够了。

先看I2C的初始化流程:

// I2C1初始化(以STM32F4为例)
void I2C1_Init(void) {
    // 1. 使能时钟
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;

    // 2. 配置PB6(SCL), PB7(SDA)为开漏输出
    GPIOB->MODER |= (2 << 12) | (2 << 14);  // 复用功能
    GPIOB->OTYPER |= (1 << 6) | (1 << 7);   // 开漏输出
    GPIOB->AFR[0] |= (4 << 24) | (4 << 28); // AF4

    // 3. 配置I2C时序(400kHz快速模式)
    I2C1->TIMINGR = 0x00201D2B;  // 根据时钟频率计算
    I2C1->CR1 |= I2C_CR1_PE;     // 使能外设
}

SHT30的I2C地址是0x44(ADDR引脚接GND)或0x45(接VDD)。发送测量命令也很简单:

// 发送单次测量命令(高重复性)
uint8_t cmd[2] = {0x2C, 0x06};
I2C1_Write(SHT30_ADDR, cmd, 2);

读取数据时,SHT30会返回6个字节:温度高8位、温度低8位、CRC、湿度高8位、湿度低8位、CRC。CRC校验不能省,我见过有人跳过CRC,结果在高温高湿环境下数据跳变,找了好久才发现是通信干扰。

小技巧: SHT30的CRC多项式是0x31,初始值0xFF。写一个CRC校验函数,每次读取后都做验证,能过滤掉90%以上的通信异常。

4.3 DMA传输:把CPU从“搬运工”解放出来

为什么要用DMA?你想想看,如果每次ADC转换完或者I2C接收完数据,CPU都要去搬数据,那CPU大部分时间都在做“苦力”。DMA就是硬件级的“数据搬运工”,它不占用CPU内核。

配置DMA其实不复杂,核心就三步:

  1. 选择DMA流和通道——查参考手册,看哪个DMA流对应你的外设请求
  2. 配置传输方向、地址、数据宽度——外设到内存,还是内存到外设
  3. 使能循环模式或单次模式——连续采集用循环,单次触发用单次

下面是一个ADC用DMA传输的配置片段:

// ADC1 + DMA2 Stream0 配置
void ADC1_DMA_Init(void) {
    // 使能DMA2时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;

    // 配置DMA2 Stream0
    DMA2_Stream0->PAR = (uint32_t)&(ADC1->DR);  // 外设地址
    DMA2_Stream0->M0AR = (uint32_t)adc_buffer;   // 内存地址
    DMA2_Stream0->NDTR = 100;                     // 传输100次
    DMA2_Stream0->CR |= (3 << 25);               // 数据宽度16位
    DMA2_Stream0->CR |= DMA_SxCR_CIRC;            // 循环模式
    DMA2_Stream0->CR |= DMA_SxCR_EN;              // 使能DMA
}
避坑指南: 我曾经在配置DMA时忘记设置外设地址递增,结果每次传输都从同一个地址读数据,采集到的数据全是重复的。记住:外设到内存,外设地址不递增,内存地址递增。

4.4 双缓冲机制:让数据采集“永不停歇”

单缓冲的问题很明显:CPU在处理数据时,DMA不能往同一个缓冲区写数据,否则会覆盖。这就造成了“处理时间”和“采集时间”的冲突。

双缓冲怎么解决?两个缓冲区交替使用。DMA往Buffer A写的时候,CPU处理Buffer B的数据;下一轮交换。

实现方式有两种:

方式 原理 适用场景
软件切换 DMA传输完成中断中,手动切换目标地址 任意DMA外设,灵活性高
硬件双缓冲 STM32部分DMA支持自动双缓冲(DBM位) F4/H7系列,减少中断开销

我个人习惯用软件切换,因为可控性更强。核心逻辑如下:

volatile uint8_t active_buffer = 0;
uint16_t buffer_a[100];
uint16_t buffer_b[100];

void DMA2_Stream0_IRQHandler(void) {
    if (DMA2->LISR & DMA_LISR_TCIF0) {
        DMA2->LIFCR |= DMA_LIFCR_CTCIF0;  // 清除标志

        // 切换缓冲区
        if (active_buffer == 0) {
            DMA2_Stream0->M0AR = (uint32_t)buffer_b;
            active_buffer = 1;
            // 处理 buffer_a 中的数据
            process_sensor_data(buffer_a, 100);
        } else {
            DMA2_Stream0->M0AR = (uint32_t)buffer_a;
            active_buffer = 0;
            // 处理 buffer_b 中的数据
            process_sensor_data(buffer_b, 100);
        }
    }
}
核心收益: 双缓冲让数据采集和处理完全流水线化。理论上,只要处理时间小于采集时间,系统就能“零丢包”连续运行。

4.5 本章知识体系总览

下面这张图把本章的核心知识点串在了一起,方便你对照理解:

第4章 传感器数据采集知识体系 SHT30 温湿度传感器 I2C 接口 SPI 接口 GPIO/ADC DMA 数据传输(外设→内存) Buffer A(采集) Buffer B(处理) 数据解析 + 滤波 + 归一化 流水线采集 → 零丢包 → 高性能推理

4.6 实战要点总结

  • GPIO配置:I2C用开漏输出+上拉,SPI用推挽输出,ADC输入用模拟模式
  • SHT30驱动:I2C地址0x44/0x45,命令0x2C06启动测量,读取6字节含CRC
  • DMA配置:外设地址不递增,内存地址递增,循环模式用于连续采集
  • 双缓冲:软件切换更灵活,硬件DBM更高效,根据项目实时性要求选择
我的建议: 刚开始做传感器采集时,先用轮询方式把数据读通,确认传感器和通信都没问题,再上DMA和双缓冲。一步到位反而容易出问题,排查起来更头疼。