4. 传感器数据采集基础:GPIO与ADC配置实战,I2C/SPI总线驱动温湿度传感器(SHT30),DMA传输与双缓冲机制
各位同学,欢迎来到第四章。这一章我们真正开始“动手摸传感器”。
我个人习惯把传感器采集比作嵌入式系统的“五官”。数据不准,后面做再花哨的推理也是白搭。今天咱们就围绕一个实战场景——用STM32读取SHT30温湿度传感器,把GPIO、ADC、I2C/SPI、DMA和双缓冲这几个核心知识点串起来。
4.1 GPIO与ADC:从数字到模拟的“第一公里”
先聊GPIO。很多人觉得GPIO就是点灯,其实不然。在传感器采集里,GPIO承担着“使能”、“中断触发”和“协议时序”三个角色。
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,结果在高温高湿环境下数据跳变,找了好久才发现是通信干扰。
4.3 DMA传输:把CPU从“搬运工”解放出来
为什么要用DMA?你想想看,如果每次ADC转换完或者I2C接收完数据,CPU都要去搬数据,那CPU大部分时间都在做“苦力”。DMA就是硬件级的“数据搬运工”,它不占用CPU内核。
配置DMA其实不复杂,核心就三步:
- 选择DMA流和通道——查参考手册,看哪个DMA流对应你的外设请求
- 配置传输方向、地址、数据宽度——外设到内存,还是内存到外设
- 使能循环模式或单次模式——连续采集用循环,单次触发用单次
下面是一个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
}
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.6 实战要点总结
- GPIO配置:I2C用开漏输出+上拉,SPI用推挽输出,ADC输入用模拟模式
- SHT30驱动:I2C地址0x44/0x45,命令0x2C06启动测量,读取6字节含CRC
- DMA配置:外设地址不递增,内存地址递增,循环模式用于连续采集
- 双缓冲:软件切换更灵活,硬件DBM更高效,根据项目实时性要求选择