ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32多传感器协同设计:I2C/ADC/单总线实时调度与抗干扰采样

STM32多传感器协同设计:I2C/ADC/单总线实时调度与抗干扰采样 简介本资源是一套基于STM32F103ZET6的嵌入式环境检测系统完整工程面向嵌入式初学者与单片机课程实践者解决温湿度、气压、可燃气体浓度等多参数实时采集与本地显示的核心问题。压缩包共156个文件含39个头文件.h定义外设接口与数据结构、38个C源文件.c实现DHT11温湿度驱动、BMP280气压采集、MQ2气体检测及1602液晶显示逻辑辅以.o目标文件、.d依赖文件及Keil工程关键配置uvprojx、uvoptx、sct等整体仅3.05MB轻量易导入。已有6399人学习下载工程结构规范包含标准外设库如stm32f10x_usart.c、stm32f10x_i2c.c等与完整编译脚本keilkilll.bat支持一键清理与烧录配套博文详解硬件连接与代码逻辑B站视频直观呈现运行效果便于理解传感器协同机制与嵌入式系统调试全流程。1. 这不是“又一个温湿度显示项目”STM32ZET6驱动DHT11BMP280MQ2用USART透传I2C高速读取ADC精准采样专治传感器数据抖动、协议冲突和多外设时序打架很多人拿到“环境检测系统”源码第一反应是不就是DHT11读温湿度、串口打印但实际跑起来会发现——DHT11偶尔卡死、BMP280气压跳变±5hPa、MQ2模拟值飘得像没校准的万用表、1602液晶刷新时USART中断被抢占导致上位机收包错乱。这套基于STM32F103ZET6的工程本质是一套多协议共存下的嵌入式实时传感调度框架它用I2C总线托管BMP280标准模式100kHz用单总线协议硬啃DHT11严格时序控制用ADC通道1采集MQ2模拟电压带DMA搬运滑动平均滤波再通过USART1PA9/PA10以固定帧格式$ENV,25.3,1013.2,324\r\n向上位机透传。所有外设驱动都剥离了阻塞式延时改用SysTick滴答定时器状态机轮询避免HAL_Delay()锁死整个系统。适合需要稳定接入工业传感器、做边缘数据预处理、或为LoRa/WiFi模组提供干净原始数据的开发者——尤其当你发现Keil里stm32f10x_adc.c里ADC_RegularChannelConfig()调用顺序不对或者stm32f10x_i2c.c中I2C_GenerateSTART()后没等I2C_GetFlagStatus(I2C_FLAG_SB)就发地址系统就会在凌晨三点默默丢掉一包关键数据。2. 多协议外设协同I2C读取BMP280与ADC采集MQ2的时序隔离设计2.1 BMP280通过I2C总线实现非阻塞读取BMP280作为高精度气压/温度传感器其I2C通信对时序敏感。本工程未采用HAL库的HAL_I2C_Master_Transmit()阻塞调用而是基于标准外设库SPL手动配置I2C1PB6/PB7关键在于状态机驱动超时保护。初始化阶段需设置I2C_InitTypeDef结构体I2C_InitTypeDef I2C_InitStructure; I2C_InitStructure.I2C_ClockSpeed 100000; // 标准模式100kHz避开快速模式下PB6/PB7上拉不足问题 I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_16_9; I2C_InitStructure.I2C_OwnAddress1 0x00; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE);提示I2C_ClockSpeed100000是硬性要求。若设为400kHz快速模式需确认PB6/PB7外接4.7kΩ上拉电阻且VDDA≥2.4V否则I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)会长时间返回FAIL。读取BMP280寄存器分三步发送设备地址0xEE写/0xEF读→ 发送寄存器地址0xF7触发转换→ 等待65ms → 读取6字节温度/压力原始值。核心状态机代码如下// bmp280_read_state定义IDLE → START_SEND → ADDR_SEND → WAIT_CONV → READ_DATA switch(bmp280_state) { case BMP280_IDLE: if (bmp280_trigger_flag) { I2C_GenerateSTART(I2C1, ENABLE); // 生成起始信号 bmp280_state BMP280_START_SEND; timeout_cnt 0; } break; case BMP280_START_SEND: if (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { I2C_Send7bitAddress(I2C1, BMP280_ADDR_W, I2C_Direction_Transmitter); // 发送0xEE bmp280_state BMP280_ADDR_SEND; } else if (timeout_cnt 1000) { // 超时1ms重置状态 bmp280_state BMP280_IDLE; } break; case BMP280_ADDR_SEND: if (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) { I2C_SendData(I2C1, 0xF7); // 写入0xF7触发测量 bmp280_state BMP280_WAIT_CONV; conv_start_tick SysTick-VAL; // 记录触发时刻 } break; case BMP280_WAIT_CONV: if ((SysTick-VAL - conv_start_tick) 0x80000000) { // 溢出判断等待65ms I2C_GenerateSTART(I2C1, ENABLE); bmp280_state BMP280_READ_START; } break; case BMP280_READ_START: if (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { I2C_Send7bitAddress(I2C1, BMP280_ADDR_R, I2C_Direction_Receiver); bmp280_state BMP280_READING; read_idx 0; } break; case BMP280_READING: if (read_idx 6 I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) { if (read_idx 5) I2C_AcknowledgeConfig(I2C1, DISABLE); // 最后一字节NACK bmp280_raw[read_idx] I2C_ReceiveData(I2C1); if (read_idx 6) { I2C_GenerateSTOP(I2C1, ENABLE); bmp280_state BMP280_IDLE; bmp280_data_ready 1; } } break; }该状态机将I2C通信拆解为可抢占的微任务确保在SysTick_Handler()每1ms检查一次状态避免因while(!I2C_CheckEvent())死循环导致其他外设如ADC转换完成中断被延迟响应。参数说明BMP280_ADDR_W0xEE为写地址BMP280_ADDR_R0xEF为读地址0xF7是BMP280的CTRL_MEAS寄存器地址写入0x27强制模式超采样需额外步骤本工程采用默认配置。2.2 MQ2气体传感器通过ADC1通道1实现抗干扰采样MQ2输出模拟电压0~5V需经STM32F103ZET6的ADC1通道1PA0量化。直接读ADC_GetConversionValue(ADC1)会导致数值跳变——主因是电源纹波、PCB走线耦合及ADC参考电压波动。本工程采用DMA搬运滑动平均滤波软件校准三层防护ADC硬件配置启用ADC1独立模式采样时间设为239.5周期ADC_SampleTime_239Cycles5保证MQ2慢响应特性下的充分采集DMA配置开辟16字节缓冲区每次转换后自动搬移10个采样值避免CPU频繁读寄存器滤波算法在ADC1_IRQHandler()中触发滤波计算非简单求平均而是剔除极值后加权// 全局变量uint16_t adc_dma_buffer[10]; volatile uint8_t dma_complete 0; void ADC1_IRQHandler(void) { if (DMA_GetFlagStatus(DMA1_FLAG_TC1) ! RESET) { DMA_ClearFlag(DMA1_FLAG_TC1); dma_complete 1; } ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); } // 主循环中调用 if (dma_complete) { uint32_t sum 0, max_val 0, min_val 0xFFFF; for (int i 0; i 10; i) { if (adc_dma_buffer[i] max_val) max_val adc_dma_buffer[i]; if (adc_dma_buffer[i] min_val) min_val adc_dma_buffer[i]; sum adc_dma_buffer[i]; } uint16_t filtered (sum - max_val - min_val) / 8; // 剔除最大最小值后取均值 mq2_value (filtered * 3300) / 4095; // 转换为mV供后续浓度查表 dma_complete 0; }注意ADC_SampleTime_239Cycles5对应ADCCLK14MHz时的17.1μs采样窗口若系统时钟配置错误如RCC未使能ADCCLKADC_GetCalibrationStatus(ADC1)会返回RESET导致转换值全为0。务必在RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_ADC1, ENABLE)后调用ADC_ResetCalibration(ADC1)并等待ADC_GetResetCalibrationStatus(ADC1) RESET。2.3 DHT11单总线协议的手动时序控制DHT11虽为单总线器件但其时序要求严苛主机拉低80μs启动信号→释放总线80μs→DHT11拉低80μs响应→释放80μs→随后40位数据每位50μs低电平27/70μs高电平表示0/1。标准库无现成驱动必须用精确延时输入捕获组合// 使用TIM2 CH2PA1做输入捕获TIM2 CK_INT72MHz预分频71→计数频率1MHz1μs/计数 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0; TIM_ICInit(TIM2, TIM_ICInitStructure); // 启动DHT11PA0推挽输出拉低18ms GPIO_ResetBits(GPIOA, GPIO_Pin_0); Delay_us(20000); // 精确20ms GPIO_SetBits(GPIOA, GPIO_Pin_0); Delay_us(30); // 释放30μs进入输入模式 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 启动TIM2输入捕获 TIM_Cmd(TIM2, ENABLE); TIM_ITConfig(TIM2, TIM_IT_CC2, ENABLE);在TIM2_IRQHandler()中解析40位数据关键点在于用定时器计数差值判别0/1若上升沿到下降沿计数值30则为060则为1。此法比while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0))轮询更可靠避免因中断延迟导致误判。3. USART透传协议设计与Keil工程结构解析3.1 USART1非阻塞发送与帧格式定义环境数据需实时上传至上位机若用printf()重定向至USART1会因fputc()内部while(!USART_GetFlagStatus(USART1, USART_FLAG_TC))阻塞主线程。本工程采用环形缓冲区空闲中断方案#define UART_TX_BUF_SIZE 128 uint8_t uart_tx_buf[UART_TX_BUF_SIZE]; volatile uint16_t tx_head 0, tx_tail 0; // 初始化USART1PA9/PA10115200bps8N1 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 使能空闲中断 USART_Cmd(USART1, ENABLE);发送函数uart_send_string()将数据写入环形缓冲区由USART1_IRQHandler()在USART_IT_TXE发送寄存器空中断中逐字节搬出void uart_send_string(char *str) { while(*str) { while((tx_head 1) % UART_TX_BUF_SIZE tx_tail); // 缓冲区满则等待 uart_tx_buf[tx_head] *str; tx_head (tx_head 1) % UART_TX_BUF_SIZE; USART_ITConfig(USART1, USART_IT_TXE, ENABLE); // 使能发送中断 } } void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_TXE) ! RESET) { if (tx_head ! tx_tail) { USART_SendData(USART1, uart_tx_buf[tx_tail]); tx_tail (tx_tail 1) % UART_TX_BUF_SIZE; } else { USART_ITConfig(USART1, USART_IT_TXE, DISABLE); // 缓冲区空则关闭中断 } } // 空闲中断处理接收数据此处略 }透传帧格式严格遵循$ENV,temp,press,mq2\r\n例如$ENV,25.3,1013.2,324\r\n。其中温度/气压保留1位小数sprintf(buf, $ENV,%.1f,%.1f,%d\r\n, temp, press, mq2_value)避免浮点运算拖慢主循环。上位机可直接用正则^\$ENV,(\d\.\d),(\d\.\d),(\d)\r\n$提取三组数值。3.2 Keil工程文件结构与关键源码定位工程根目录下keilkilll.bat用于一键清理编译残留del *.axf *.hex *.tra *.lnp *.plg *.crf *.tra *.uvguix.* /q避免旧目标文件干扰调试。核心驱动文件分工明确文件名功能关键函数stm32f10x_usart.cUSART1初始化与中断处理USART1_IRQHandler(),USART_Config()stm32f10x_i2c.cI2C1底层时序控制I2C_Start(),I2C_Write_Byte()stm32f10x_adc.cADC1DMA配置ADC_DMA_Config(),ADC1_IRQHandler()stm32f10x_tim.cSysTick与TIM2配置SysTick_Handler(),TIM2_IRQHandler()stm32f10x_rcc.c系统时钟树配置RCC_Configuration()需确认HSE8MHz晶振提示stm32f10x_can.c在本工程中未启用但保留该文件说明作者预留CAN总线扩展能力如未来接入CO传感器节点。若误启用CAN时钟RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_CAN1, ENABLE)而未配置引脚PB8/PB9会导致Error: No STM32 target found!——因JTAG/SWD接口被CAN复用引脚干扰。3.3 1602液晶显示与资源竞争规避1602使用4位数据模式RSRWEN接PA端口其delay_ms(5)写指令易与USART/ADC争抢CPU。本工程将其改造为定时器触发状态机更新定义lcd_update_state每200ms执行一次lcd_display_env()内容从全局变量env_data_t读取该结构体由ADC/I2C/DHT11中断更新彻底解除显示模块对实时性的依赖。4. 多外设调试排错解决I2C总线锁定、ADC采样漂移与USART丢包4.1 I2C总线锁定BUSY flag stuck的硬复位方案当BMP280异常拉低SCL或SDA线I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)会长期返回SET导致后续通信失败。软件复位无效时需执行物理层硬复位void I2C1_BusReset(void) { RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOB, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // SCL连续9个高电平脉冲 for(int i 0; i 9; i) { GPIO_SetBits(GPIOB, GPIO_Pin_6); Delay_us(5); GPIO_ResetBits(GPIOB, GPIO_Pin_6); Delay_us(5); } // 发送起始停止条件 GPIO_ResetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_7); Delay_us(5); // 恢复I2C功能 RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_I2C1, DISABLE); RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_I2C1, ENABLE); I2C_DeInit(I2C1); I2C_Init(I2C1, I2C_InitStructure); }该函数在I2C_CheckEvent()超时后调用通过GPIO模拟时钟脉冲“踢醒”从机。实测对BMP280、DHT11均有效比断电重启更符合工业场景。4.2 ADC采样值漂移的硬件级校准MQ2读数受环境温度影响显著单纯软件滤波无法消除系统误差。本工程在main()中加入两点校准流程先采集纯净空气下MQ2电压clean_volt再采集已知浓度酒精蒸汽下电压alcohol_volt建立线性映射// 校准常量需实测获取 #define CLEAN_VOLTAGE 1250 // mV #define ALCOHOL_VOLTAGE 2800 // mV #define ALCOHOL_CONC 100 // ppm float mq2_concentration(uint16_t raw_mv) { if (raw_mv CLEAN_VOLTAGE) return 0.0; if (raw_mv ALCOHOL_VOLTAGE) return ALCOHOL_CONC; return ((float)(raw_mv - CLEAN_VOLTAGE) / (ALCOHOL_VOLTAGE - CLEAN_VOLTAGE)) * ALCOHOL_CONC; }注意CLEAN_VOLTAGE必须在通风良好、无挥发性气体环境中静置30分钟测得ALCOHOL_VOLTAGE需用标准气体发生器标定。若跳过此步mq2_concentration()输出仅为相对值不可用于安全阈值判断。4.3 USART丢包的DMA接收优化上位机连续发送命令时USART_IT_RXNE中断可能因处理不及时丢失字节。启用DMA接收可解决// 开辟DMA接收缓冲区 uint8_t uart_rx_dma_buf[64]; DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPERIPH_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(USART1-DR); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)uart_rx_dma_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 64; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式防溢出 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE);配合空闲中断解析完整帧彻底规避中断丢失风险。5. 实战技巧用ST-Link Utility快速验证固件烧录与内存数据读取5.1 ST-Link Utility连接配置要点当Keil下载提示Error: No STM32 target found!90%源于ST-Link连接问题。需按顺序检查硬件连接SWDIOPA13、SWCLKPA14、GND、3.3V四线全接禁用NRST线除非需复位调试Target Settings在ST-Link Utility中选择STM32F10x High-density勾选Connect under resetPower Supply若目标板自供电取消ST-Link supply target选项避免5V倒灌损坏。5.2 通过Memory Browser验证传感器数据烧录成功后打开View → Memory Browser输入地址0x20000000SRAM起始查看全局变量env_data_t temp位于0x200001004字节浮点数IEEE 754uint16_t mq2_value位于0x20000110直接读取十六进制转十进制uint8_t bmp280_data_ready位于0x20000120值为1表示新数据就绪。若mq2_value恒为0检查ADC1-CR2 | ADC_CR2_SWSTART是否在ADC_Cmd(ADC1, ENABLE)后调用若bmp280_data_ready始终0用逻辑分析仪抓PB6/PB7波形确认I2C起始信号是否发出。5.3 使用USART Printf重定向进行在线调试在main.c中添加#include stdio.h int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t) ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); return ch; }随后可用printf(Temp: %.1f\r\n, temp);输出调试信息。注意此法仅用于开发阶段量产时需注释因while()阻塞会破坏实时性。最终效果可通过CSDN文章中的视频验证BMP280气压值稳定在1013.2hPa±0.3DHT11温度波动≤0.2℃MQ2在酒精靠近时3秒内响应至850ppmUSART透传帧无丢包。所有优化均围绕“让每个外设在自己的时间片内安静工作”这一原则展开——这才是嵌入式环境检测系统的真正门槛。本文还有配套的精品资源点击获取
返回列表