下面是我写的代码,你分析一下什么原因#include "adc.h" #include "./SYSTEM/delay/delay.h" #include <math.h> ADC_HandleTypeDef g_adc_handle; /* ADC句柄 */ ///* ADC初始化函数,本函数支持ADC1/ADC2任意通道, 但是不支持ADC3 */ //void adc_init(void) //{ // g_adc_handle.Instance = ADC_ADCX; /* 选择哪个ADC */ // g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 数据对齐方式:右对齐 */ // g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; /* 非扫描模式,仅用到一个通道 */ // g_adc_handle.Init.ContinuousConvMode = DISABLE; /* 关闭连续转换模式 */ // g_adc_handle.Init.NbrOfConversion = 1; /* 赋值范围是1~16,本实验用到1个规则通道序列 */ // g_adc_handle.Init.DiscontinuousConvMode = DISABLE; /* 禁止规则通道组间断模式 */ // g_adc_handle.Init.NbrOfDiscConversion = 0; /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */ // g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 触发转换方式:软件触发 */ // HAL_ADC_Init(&g_adc_handle); /* 初始化 */ // HAL_ADCEx_Calibration_Start(&g_adc_handle); /* 校准ADC */ //} ///* ADC底层驱动,引脚配置,时钟使能 */ //void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc) //{ // if(hadc->Instance == ADC_ADCX) // { // GPIO_InitTypeDef gpio_init_struct; // RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; // // ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx时钟 */ // ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 开启GPIO时钟 */ // /* 设置ADC时钟 */ // adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; /* ADC外设时钟 */ // adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6; /* 分频因子6时钟为72M/6=12MHz */ // HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 设置ADC时钟 */ // /* 设置AD采集通道对应IO引脚工作模式 */ // gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN; /* ADC通道IO引脚 */ // gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟 */ // HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct); // } //} ///** // * @brief 设置ADC通道采样时间 // * @param adcx : adc句柄指针,ADC_HandleTypeDef // * @param ch : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_17 // * @param stime: 采样时间 0~7, 对应关系为: // * @arg ADC_SAMPLETIME_1CYCLE_5, 1.5个ADC时钟周期 ADC_SAMPLETIME_7CYCLES_5, 7.5个ADC时钟周期 // * @arg ADC_SAMPLETIME_13CYCLES_5, 13.5个ADC时钟周期 ADC_SAMPLETIME_28CYCLES_5, 28.5个ADC时钟周期 // * @arg ADC_SAMPLETIME_41CYCLES_5, 41.5个ADC时钟周期 ADC_SAMPLETIME_55CYCLES_5, 55.5个ADC时钟周期 // * @arg ADC_SAMPLETIME_71CYCLES_5, 71.5个ADC时钟周期 ADC_SAMPLETIME_239CYCLES_5, 239.5个ADC时钟周期 // * @param rank: 多通道采集时需要设置的采集编号, // 假设你定义channle1的rank=1,channle2 的rank=2, // 那么对应你在DMA缓存空间的变量数组AdcDMA[0] 就i是channle1的转换结果,AdcDMA[1]就是通道2的转换结果。 // 单通道DMA设置为 ADC_REGULAR_RANK_1 // * @arg 编号1~16:ADC_REGULAR_RANK_1~ADC_REGULAR_RANK_16 // * @retval 无 // */ //void adc_channel_set(ADC_HandleTypeDef *adc_handle, uint32_t ch, uint32_t rank, uint32_t stime) //{ // ADC_ChannelConfTypeDef adc_ch_conf; // // adc_ch_conf.Channel = ch; /* 通道 */ // adc_ch_conf.Rank = rank; /* 序列 */ // adc_ch_conf.SamplingTime = stime; /* 采样时间 */ // HAL_ADC_ConfigChannel(adc_handle, &adc_ch_conf); /* 通道配置 */ //} ///** // * @brief 获得ADC转换后的结果 // * @param ch: 通道值 0~17,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_17 // * @retval 无 // */ //uint32_t adc_get_result(uint32_t ch) //{ // adc_channel_set(&g_adc_handle , ch, ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5); /* 设置通道,序列和采样时间 */ // HAL_ADC_Start(&g_adc_handle); /* 开启ADC */ // HAL_ADC_PollForConversion(&g_adc_handle, 20); /* 轮询转换 */ // return (uint16_t)HAL_ADC_GetValue(&g_adc_handle); /* 返回最近一次ADC1规则组的转换结果 */ //} ///** // * @brief 获取通道ch的转换值,取times次,然后平均 // * @param ch : 通道号, 0~17 // * @param times : 获取次数 // * @retval 通道ch的times次转换结果平均值 // */ //uint32_t adc_get_result_average(uint32_t ch, uint8_t times) //{ // uint32_t temp_val = 0; // uint8_t t; // for (t = 0; t < times; t++) /* 获取times次数据 */ // { // temp_val += adc_get_result(ch); // delay_ms(5); // } // return temp_val / times; /* 返回平均值 */ //} /*************************单通道ADC采集(DMA读取)实验和多通道ADC采集(DMA读取)实验公用代码*******************************/ DMA_HandleTypeDef g_dma_adc_handle = {0}; // 定义要搬运ADC数据的DMA句柄 ADC_HandleTypeDef g_adc_dma_handle = {0}; // 定义ADC(DMA读取)句柄 uint8_t g_adc_dma_sta = 0; // DMA传输状态标志, 0,未完成; 1, 已完成 DMA_HandleTypeDef g_dma_nch_adc_handle = {0}; /* 定义要搬运ADC多通道数据的DMA句柄 */ ADC_HandleTypeDef g_adc_nch_dma_handle = {0}; /* 定义ADC(多通道DMA读取)句柄 */ /** * @brief ADC N通道(6通道) DMA读取 初始化函数 * @note 本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置 * 另外,由于本函数用到了6个通道, 宏定义会比较多内容, 因此,本函数就不采用宏定义的方式来修改通道了, * 直接在本函数里面修改, 这里我们默认使用PA0~PA5这6个通道. * * 注意: 本函数还是使用 ADC_ADCX(默认=ADC1) 和 ADC_ADCX_DMACx( DMA1_Channel1 ) 及其相关定义 * 不要乱修改adc.h里面的这两部分内容, 必须在理解原理的基础上进行修改, 否则可能导致无法正常使用. * * @param mar : 存储器地址 * @retval 无 */ void adc_nch_dma_init(uint32_t mar) { GPIO_InitTypeDef gpio_init_struct; RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; ADC_ChannelConfTypeDef adc_ch_conf = {0}; ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx时钟 */ __HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟 */ if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7) /* 大于DMA1_Channel7, 则为DMA2的通道了 */ { __HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */ } else { __HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */ } /* 设置ADC时钟 */ adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; /* ADC外设时钟 */ adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6; /* 分频因子6时钟为72M/6=12MHz */ HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 设置ADC时钟 */ /* 设置ADC1通道0~5对应的IO口模拟输入 AD采集引脚模式设置,模拟输入 PA1对应 ADC1_IN1 PA2对应 ADC1_IN2 PA3对应 ADC1_IN3 */ gpio_init_struct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3; /* GPIOA1-3 */ gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟 */ HAL_GPIO_Init(GPIOA, &gpio_init_struct); /* 初始化DMA */ g_dma_nch_adc_handle.Instance = ADC_ADCX_DMACx; /* 设置DMA通道 */ g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; /* 从外设到存储器模式 */ g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */ g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */ g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; /* 外设数据长度:16位 */ g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; /* 存储器数据长度:16位 */ g_dma_nch_adc_handle.Init.Mode = DMA_NORMAL; /* 外设流控模式 */ g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */ HAL_DMA_Init(&g_dma_nch_adc_handle); __HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_nch_adc_handle); /* 将DMA与adc联系起来 */ /* 初始化ADC */ g_adc_nch_dma_handle.Instance = ADC_ADCX; /* 选择哪个ADC */ g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 数据对齐方式:右对齐 */ g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE; /* 使能扫描模式 */ g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE; /* 使能连续转换 */ g_adc_nch_dma_handle.Init.NbrOfConversion = 3; /* 赋值范围是1~16,本实验用到3个规则通道序列 */ g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE; /* 禁止规则通道组间断模式 */ g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0; /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */ g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */ HAL_ADC_Init(&g_adc_nch_dma_handle); /* 初始化 */ HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle); /* 校准ADC */ /* 配置ADC通道 */ adc_ch_conf.Channel = ADC_CHANNEL_1; /* 配置使用的ADC通道 */ adc_ch_conf.Rank = ADC_REGULAR_RANK_1; /* 采样序列里的第1个 */ adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5 ; /* 采样时间,设置最大采样周期:239.5个ADC周期 */ HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); /* 通道配置 */ adc_ch_conf.Channel = ADC_CHANNEL_2; /* 配置使用的ADC通道 */ adc_ch_conf.Rank = ADC_REGULAR_RANK_2; /* 采样序列里的第2个 */ adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5 ; /* 采样时间,设置最大采样周期:239.5个ADC周期 */ HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); /* 配置ADC通道 */ adc_ch_conf.Channel = ADC_CHANNEL_3; /* 配置使用的ADC通道 */ adc_ch_conf.Rank = ADC_REGULAR_RANK_3; /* 采样序列里的第3个 */ adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5 ; /* 采样时间,设置最大采样周期:239.5个ADC周期 */ HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); /* 配置ADC通道 */ /* 配置DMA数据流请求中断优先级 */ HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3); HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn); HAL_DMA_Start_IT(&g_dma_nch_adc_handle, (uint32_t)&ADC1->DR, mar, 0); /* 启动DMA,并开启中断 */ HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0); /* 开启ADC,通过DMA传输结果 */ } /*************************单通道ADC采集(DMA读取)实验和多通道ADC采集(DMA读取)实验公用代码*******************************/ /** * @brief 使能一次ADC DMA传输 * @note 该函数用寄存器来操作,防止用HAL库操作对其他参数有修改,也为了兼容性 * @param ndtr: DMA传输的次数 * @retval 无 */ void adc_dma_enable(uint16_t cndtr) { ADC_ADCX->CR2 &= ~(1 << 0); /* 先关闭ADC */ ADC_ADCX_DMACx->CCR &= ~(1 << 0); /* 关闭DMA传输 */ while (ADC_ADCX_DMACx->CCR & (1 << 0)); /* 确保DMA可以被设置 */ ADC_ADCX_DMACx->CNDTR = cndtr; /* DMA传输数据量 */ ADC_ADCX_DMACx->CCR |= 1 << 0; /* 开启DMA传输 */ ADC_ADCX->CR2 |= 1 << 0; /* 重新启动ADC */ ADC_ADCX->CR2 |= 1 << 22; /* 启动规则转换通道 */ } /** * @brief ADC DMA采集中断服务函数 * @param 无 * @retval 无 */ void ADC_ADCX_DMACx_IRQHandler(void) { if (ADC_ADCX_DMACx_IS_TC()) { g_adc_dma_sta = 1; /* 标记DMA传输完成 */ ADC_ADCX_DMACx_CLR_TC(); /* 清除DMA1 数据流7 传输完成中断 */ } },main.c代码#include "./SYSTEM/sys/sys.h" #include "./SYSTEM/usart/usart.h" #include "./SYSTEM/delay/delay.h" #include "./BSP/LED/led.h" #include "./BSP/DMA/dma.h" #include "./BSP/KEY/key.h" #include "./BSP/ADC/adc.h" #include "OLED.h" // 重新排序全局变量定义,确保阈值变量在ADC缓冲区之前 volatile float MQ7_ALARM_THRESHOLD = 7.0; // 添加volatile防止优化 volatile float MQ8_ALARM_THRESHOLD = 20.0; volatile float MP2_ALARM_THRESHOLD = 15.0; // 添加全局变量存储基线值 float baseline_values[3] = {0}; extern const uint8_t HZ_zhong[]; // 添加内存保护屏障 #define ADC_DMA_BUF_SIZE (3 * 50) /* ADC DMA缓冲区大小 */ uint32_t __attribute__((aligned(4))) memory_barrier[4]; // 内存屏障 static uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA缓冲区 */ extern uint8_t g_adc_dma_sta; /* DMA传输完成标志 */ // 汉字点阵数据 const uint8_t HZ_guo[] = { 0x00,0xFE,0xFE,0x12,0x92,0x92,0x92,0xF2, 0xF2,0x92,0x92,0x92,0x12,0xFE,0xFE,0x00, 0x00,0xFF,0xFF,0x48,0x48,0x48,0x48,0x4F, 0x4F,0x4A,0x4E,0x4C,0x48,0xFF,0xFF,0x00 }; const uint8_t HZ_nan[] = { 0x04,0xE4,0xE4,0x24, 0x64,0xE4,0xA4,0x3F, 0x3F,0xA4,0xE4,0x64, 0x24,0xE4,0xE4,0x04, 0x00,0xFF,0xFF,0x08, 0x09,0x09,0x09,0x7F, 0x7F,0x09,0x09,0x49, 0xC8,0xFF,0x7F,0x00,/*"南",0*/ }; const uint8_t HZ_fang[] = { 0x08,0x08,0x08,0x08, 0x08,0xF8,0xF9,0x8F, 0x8E,0x88,0x88,0x88, 0x88,0x08,0x08,0x08, 0x00,0x80,0xC0,0x60, 0x38,0x1F,0x07,0x00, 0x40,0xC0,0xC0,0x7F, 0x3F,0x00,0x00,0x00,/*"方",0*/ }; const uint8_t HZ_dian[] = { 0x00,0x00,0xF8,0xF8, 0x88,0x88,0x88,0xFF, 0xFF,0x88,0x88,0x88, 0xF8,0xF8,0x00,0x00, 0x00,0x00,0x1F,0x1F, 0x08,0x08,0x08,0x7F, 0xFF,0x88,0x88,0x88, 0x9F,0x9F,0xF0,0xF0,/*"电",0*/ }; const uint8_t HZ_wang[] = { 0x00,0xFE,0xFE,0x22, 0x62,0xC2,0xF2,0x72, 0x22,0x62,0xC2,0xF2, 0x72,0xFE,0xFE,0x00, 0x00,0xFF,0xFF,0x18, 0x0E,0x07,0x0F,0x1E, 0x18,0x0E,0x07,0x4F, 0xCE,0xFF,0x7F,0x00,/*"网",0*/ }; ////////////////////////////////////// 按键处理优化 /////////////////////////////////// int selected_sensor = 0; // 当前选择的传感器 // 按键状态结构体 typedef struct { uint32_t last_time; // 上次检测时间 uint8_t last_state; // 上次状态 uint8_t pressed; // 按下标志 } KeyState; KeyState xz_key = {0, 1, 0}; KeyState key0_key = {0, 1, 0}; KeyState key1_key = {0, 1, 0}; // 更新阈值函数(带范围限制) void update_threshold(float delta) { switch (selected_sensor) { case 0: MQ7_ALARM_THRESHOLD += delta; if (MQ7_ALARM_THRESHOLD < 0) MQ7_ALARM_THRESHOLD = 0; if (MQ7_ALARM_THRESHOLD > 100) MQ7_ALARM_THRESHOLD = 100; break; case 1: MQ8_ALARM_THRESHOLD += delta; if (MQ8_ALARM_THRESHOLD < 0) MQ8_ALARM_THRESHOLD = 0; if (MQ8_ALARM_THRESHOLD > 100) MQ8_ALARM_THRESHOLD = 100; break; case 2: MP2_ALARM_THRESHOLD += delta; if (MP2_ALARM_THRESHOLD < 0) MP2_ALARM_THRESHOLD = 0; if (MP2_ALARM_THRESHOLD > 100) MP2_ALARM_THRESHOLD = 100; break; } } // 更新按键状态 void update_key_state(KeyState *key, GPIO_PinState current_state) { uint32_t now = HAL_GetTick(); uint8_t current_pin_state = (current_state == GPIO_PIN_SET) ? 1 : 0; // 检测下降沿(按键按下) if (key->last_state == 1 && current_pin_state == 0) { if ((now - key->last_time) > 20) { // 20ms消抖 key->pressed = 1; } } // 更新状态 key->last_state = current_pin_state; key->last_time = now; } // 处理按键逻辑 void process_keys(void) { // 更新按键状态 update_key_state(&xz_key, HAL_GPIO_ReadPin(XZ_GPIO_PORT, XZ_GPIO_PIN)); update_key_state(&key0_key, HAL_GPIO_ReadPin(KEY0_GPIO_PORT, KEY0_GPIO_PIN)); update_key_state(&key1_key, HAL_GPIO_ReadPin(KEY1_GPIO_PORT, KEY1_GPIO_PIN)); // 处理XZ按键(选择传感器) if (xz_key.pressed) { selected_sensor = (selected_sensor + 1) % 3; xz_key.pressed = 0; } // 处理KEY0按键(增加阈值) if (key0_key.pressed) { update_threshold(1.0f); key0_key.pressed = 0; } // 处理KEY1按键(减少阈值) if (key1_key.pressed) { update_threshold(-1.0f); key1_key.pressed = 0; } } /////////////////////////////////////////////////////////////////////////////////// // 显示电压值(清除残留字符) void display_voltage_value(uint8_t x, uint8_t y, float voltage) { char buffer[10]; // 清除旧内容(5位宽度,包括小数点和两位小数) OLED_ShowString(x, y, " ", 8); // 显示新值 snprintf(buffer, sizeof(buffer), "%.0f", voltage); OLED_ShowString(x, y, buffer, 8); } // 显示阈值值(清除残留字符) void display_threshold_value(uint8_t x, uint8_t y, float threshold) { char buffer[10]; // 清除旧内容(4位宽度,包括小数点和一位小数) OLED_ShowString(x, y, " ", 8); // 显示新值 snprintf(buffer, sizeof(buffer), "%.1f", threshold); OLED_ShowString(x, y, buffer, 8); } int main(void) { // float mq7_concentration, mq8_concentration, mp2_concentration; HAL_Init(); sys_stm32_clock_init(RCC_PLL_MUL9); delay_init(72); usart_init(115200); led_init(); key_init(); OLED_Init(); OLED_Clear(); // 开机动画 OLED_ShowChinese(2, 16, HZ_zhong); OLED_ShowChinese(21, 16, HZ_guo); OLED_ShowChinese(40, 16, HZ_nan); OLED_ShowChinese(59, 16, HZ_fang); OLED_ShowChinese(78, 16, HZ_dian); OLED_ShowChinese(97, 16, HZ_wang); OLED_UpData(); delay_ms(1000); OLED_Clear(); // 添加变量用于存储每个通道的ADC值和电压 uint16_t adc_value[3] = {0}; float voltage[3] = {0.0f}; // 初始化ADC DMA adc_nch_dma_init((uint32_t)g_adc_dma_buf); adc_dma_enable(ADC_DMA_BUF_SIZE); // 显示初始静态文本 OLED_ShowString(1, 1, "CO:", 8); OLED_ShowString(1, 16, "CO2:", 8); OLED_ShowString(1, 32, "CH4:", 8); // 初始显示阈值(使用清除残留字符的函数) display_threshold_value(90, 1, MQ7_ALARM_THRESHOLD); display_threshold_value(90, 16, MQ8_ALARM_THRESHOLD); display_threshold_value(90, 32, MP2_ALARM_THRESHOLD); // 初始选择标记 selected_sensor = 0; OLED_ShowString(80, 1, "*", 8); // 初始选择第一个传感器 OLED_UpData(); delay_ms(100); while (1) { // 处理按键 process_keys(); // 更新选择标记 - 先清除所有标记 OLED_ShowString(80, 1, " ", 8); OLED_ShowString(80, 16, " ", 8); OLED_ShowString(80, 32, " ", 8); // 设置当前选中标记 switch (selected_sensor) { case 0: OLED_ShowString(80, 1, "*", 8); break; case 1: OLED_ShowString(80, 16, "*", 8); break; case 2: OLED_ShowString(80, 32, "*", 8); break; } if (g_adc_dma_sta == 1) { // 处理DMA缓冲区数据 for (uint8_t j = 0; j < 3; j++) { uint32_t sum = 0; // 计算该通道的平均值 for (uint16_t i = 0; i < (ADC_DMA_BUF_SIZE / 3); i++) { sum += g_adc_dma_buf[(3 * i) + j]; } // 存储平均值 adc_value[j] = sum / (ADC_DMA_BUF_SIZE / 3); switch(j) { case 0: display_voltage_value(35, 1, adc_value[0] ); break; case 1: display_voltage_value(35, 16, adc_value[1] ); break; case 2: display_voltage_value(35, 32, adc_value[2] ); break; } } display_threshold_value(90, 1, MQ7_ALARM_THRESHOLD); display_threshold_value(90, 16, MQ8_ALARM_THRESHOLD); display_threshold_value(90, 32, MP2_ALARM_THRESHOLD); g_adc_dma_sta = 0; adc_dma_enable(ADC_DMA_BUF_SIZE); } LED0_TOGGLE(); OLED_UpData(); delay_ms(10); } },用的是STM32F103的M48Z-M3的单片机开发板,出现了使用多通道ADC采集时给ADC采集引脚接上气体传感器后其数值变化浮动很大的情况,并且气体传感器在检测到对应气体时依旧我行我素地在那波动,而不是检测到气体后数值升高
时间: 2025-07-21 18:05:03 浏览: 16
<think>问题分析:
根据描述,使用多通道ADC采集气体传感器时,数值波动很大,且对气体不敏感。可能的原因包括:
1. 电源噪声:气体传感器通常需要稳定的电源,电源噪声可能导致ADC读数波动。
2. 参考电压不稳定:ADC的参考电压波动会导致所有通道的读数波动。
3. 传感器输出信号不稳定:气体传感器输出可能受环境温度、湿度等影响。
4. 软件滤波不足:ADC采样值没有进行足够的滤波处理。
5. 硬件设计问题:传感器与ADC之间的电路设计可能引入噪声,如缺少滤波电容。
6. 采样时间不足:ADC采样时间太短,导致采样值不准确。
7. 通道间干扰:多通道采集时,通道切换可能导致相邻通道干扰。
针对代码的分析:
1. 代码中使用了DMA多通道采集,配置了3个通道(PA1, PA2, PA3),采样时间设置为ADC_SAMPLETIME_239CYCLES_5(239.5个周期),这个时间对于一般应用是足够的。
2. 在DMA传输完成后,对每个通道的数据进行了平均(平均次数为ADC_DMA_BUF_SIZE/3,即50次)。这种平均滤波应该能够有效抑制噪声。
3. 但是,如果噪声幅度较大,或者有周期性干扰,简单的平均可能不够。
改进建议:
1. 检查硬件:确保传感器供电稳定,在传感器输出端和地之间加一个滤波电容(如0.1uF~10uF)。同时,检查ADC参考电压是否稳定,可以在VREF引脚加电容。
2. 增加软件滤波:除了平均滤波,可以尝试使用滑动平均滤波、中值滤波等更有效的滤波算法。
3. 增加采样时间:虽然当前采样时间已经较长,但如果传感器输出阻抗较大,可能需要更长的采样时间。但STM32F103的ADC最大采样时间已经是239.5周期,无法再增加。
4. 检查通道切换顺序:如果多个通道的输入阻抗相差很大,切换通道时可能会产生串扰。可以尝试在通道切换之间增加延迟,但硬件上已经固定,软件无法调整顺序(HAL库配置的顺序)。
5. 检查传感器输出范围:确保传感器输出电压在ADC的采集范围内,并且有足够的动态范围。
6. 校准传感器:有些气体传感器需要预热,并且需要校准(如先记录无气体时的基准值,然后根据变化量计算浓度)。
代码修改建议:
1. 增加中值滤波:在平均之前,先对每个通道的50个采样值进行中值滤波,或者使用滑动平均滤波。
2. 增加基准值校准:在初始化时,记录一段时间的ADC值作为基准(即无气体时的值),然后后续的测量值减去基准值,再计算浓度。
由于代码较长,这里给出一个修改思路:
步骤1:定义基准值数组,并在启动时采集基准值(比如在程序开始后,等待10秒,让传感器稳定,然后采集基准值)。
步骤2:修改数据处理部分,使用基准值进行校准。
步骤3:增加中值滤波函数。
示例代码修改:
在全局变量中增加:
#define SAMPLE_COUNT 50 // 每个通道的采样次数
uint16_t baseline[3] = {0}; // 存储三个通道的基准值
在main函数中,初始化ADC后,先采集基准值(注意:此时应该没有气体):
// 等待一段时间,让传感器预热
delay_ms(10000); // 10秒
// 采集基准值
for (int i=0; i<3; i++) {
baseline[i] = 0;
}
// 多采集几次求平均
for (int times=0; times<10; times++) {
g_adc_dma_sta = 0;
adc_dma_enable(ADC_DMA_BUF_SIZE);
while(g_adc_dma_sta == 0); // 等待一次DMA完成
for (uint8_t j=0; j<3; j++) {
uint32_t sum = 0;
for (uint16_t i=0; i<SAMPLE_COUNT; i++) {
sum += g_adc_dma_buf[3*i+j];
}
baseline[j] += sum / SAMPLE_COUNT;
}
}
for (int j=0; j<3; j++) {
baseline[j] /= 10;
}
步骤4:在每次采集数据处理时,减去基准值:
adc_value[j] = (sum / SAMPLE_COUNT) - baseline[j]; // 注意:这里可能为负数,需要根据情况处理
步骤5:增加中值滤波:由于已经进行了多次平均,中值滤波可以放在每个通道的50个数据上,但这样会消耗较多内存和时间。另一种方法是使用滑动平均滤波(队列)。
考虑到资源,这里建议使用移动平均滤波(每次DMA传输50个点,我们直接使用平均已经是一种滤波)。如果波动仍然大,可以增加DMA传输的数据量(即增加SAMPLE_COUNT)。
步骤6:检查是否因为DMA传输的数据量太大导致处理时间过长?目前是50个点,3个通道,共150个点,处理起来应该很快。
另外,注意在显示电压时,使用的是原始ADC值,没有转换成电压。如果波动是ADC值的波动,那么转换成电压后,波动范围是多少?如果波动范围在几十个mV以内,可能是正常的。
如果上述方法还不能解决问题,可能需要检查硬件。
由于问题描述中波动很大,还有一个可能的原因是地线噪声。确保模拟地和数字地的连接合理,传感器的地和ADC的地是同一个地。
最后,修改后的主循环数据处理部分:
我们使用基准值校准,并且为了进一步平滑,我们可以对连续多次的基准值校准后的结果再进行平均(比如记录最近5次的校准值,然后求平均)。
这里给出一个简单的修改示例,在原来基础上增加基准校准和二次平均滤波(移动平均):
定义:
#define FILTER_SIZE 5
uint16_t filter_buf[3][FILTER_SIZE] = {0}; // 三个通道,每个通道FILTER_SIZE个历史数据
uint8_t filter_index = 0;
在初始化基准值后,在循环中:
if (g_adc_dma_sta == 1)
{
// 处理DMA缓冲区数据
for (uint8_t j = 0; j < 3; j++)
{
uint32_t sum = 0;
// 计算该通道的平均值
for (uint16_t i = 0; i < (ADC_DMA_BUF_SIZE / 3); i++)
{
sum += g_adc_dma_buf[(3 * i) + j];
}
// 存储平均值,并减去基准值
uint16_t raw_value = sum / (ADC_DMA_BUF_SIZE / 3);
uint16_t cal_value = raw_value - baseline[j]; // 校准后的值
// 存入历史数据
filter_buf[j][filter_index] = cal_value;
// 对历史数据求平均
uint32_t filter_sum = 0;
for (int k=0; k<FILTER_SIZE; k++) {
filter_sum += filter_buf[j][k];
}
adc_value[j] = filter_sum / FILTER_SIZE;
// 显示
... // 这里显示adc_value[j]
}
filter_index = (filter_index + 1) % FILTER_SIZE;
... // 其他
}
注意:这样会消耗3*5=15个字的额外内存,并且需要初始化filter_buf为0。
但是,由于基准值是在无气体时采集的,当有气体时,传感器输出增加,减去基准值后得到的是气体浓度的相对值。这样,即使传感器有漂移,也能在一定程度上抵消。
另外,如果波动是由电源引起的,那么基准值也会波动,所以这种方法可能不能完全消除电源波动的影响。更好的方法是使用一个稳定的参考电压,或者使用差分输入。
由于问题复杂,建议逐步调试。
最后,关于气体传感器不响应的问题,请确保:
1. 传感器已经预热(通常需要24小时以上才能稳定,但最初几分钟也可能有输出)。
2. 测试方法正确,例如用打火机气体测试甲烷传感器,应该靠近传感器。
如果以上都尝试了还是不行,可能需要检查传感器是否损坏。
由于代码修改较多,这里不提供完整代码,只提供修改思路。请根据实际情况调整。
注意:在初始化采集基准值时,要确保没有目标气体存在。
相关问题:
1. 如何减少ADC采集时的电源噪声干扰?
2. 在STM32中,如何提高ADC采集的精度?
3. 气体传感器常用的数据处理方法有哪些?
4. 多通道ADC采集时,如何避免通道间的串扰?</think>### 问题分析:ADC数值波动大且气体检测不灵敏
根据您的描述和代码分析,主要存在两个问题:
1. **ADC数值波动大**:即使接上气体传感器,数值仍在剧烈波动
2. **气体检测不灵敏**:传感器对气体变化没有明显响应
### 根本原因分析:
1. **ADC参考电压不稳定**
- 开发板使用3.3V作为VREF,但气体传感器工作时电流变化可能导致电压波动
- 缺少参考电压滤波电容(0.1μF+10μF)
2. **传感器接口设计问题**
- 气体传感器需要稳定的负载电阻(通常10K-100KΩ)
- 传感器输出端缺少RC滤波电路
- 传感器预热时间不足(MQ系列需24-48小时老化)
3. **软件处理缺陷**
- 未进行传感器校准和基线处理
- 采样算法对噪声抑制不足
- 未考虑传感器响应特性(对数响应)
### 解决方案:
#### 1. 硬件改进
```c
// 在传感器输出端添加RC滤波(硬件修改)
// 示例电路:
// 传感器OUT → 10KΩ电阻 → 10μF电容 → GND
// ADC引脚接在电阻和电容之间
```
#### 2. 软件优化(修改main.c)
```c
// 新增基线校准函数
void calibrate_baseline() {
OLED_ShowString(1, 48, "Calibrating...", 8);
OLED_UpData();
for(int i = 0; i < 3; i++) {
float sum = 0;
for(int j = 0; j < 10; j++) {
g_adc_dma_sta = 0;
adc_dma_enable(ADC_DMA_BUF_SIZE);
while(!g_adc_dma_sta); // 等待DMA完成
// 计算该通道的平均值
for (uint16_t k = 0; k < (ADC_DMA_BUF_SIZE / 3); k++) {
sum += g_adc_dma_buf[(3 * k) + i];
}
delay_ms(100);
}
baseline_values[i] = sum / (ADC_DMA_BUF_SIZE / 3 * 10);
}
OLED_ShowString(1, 48, " ", 8); // 清除提示
}
// 改进的滤波算法(移动平均+中值滤波)
float filtered_value(uint16_t *buf, int channel) {
#define FILTER_SIZE 5
static uint16_t history[3][FILTER_SIZE] = {0};
static uint8_t idx[3] = {0};
// 更新历史数据
history[channel][idx[channel]] = buf[channel];
idx[channel] = (idx[channel] + 1) % FILTER_SIZE;
// 中值滤波
uint16_t temp[FILTER_SIZE];
memcpy(temp, history[channel], sizeof(temp));
// 冒泡排序
for(int i = 0; i < FILTER_SIZE-1; i++) {
for(int j = i+1; j < FILTER_SIZE; j++) {
if(temp[j] < temp[i]) {
uint16_t swap = temp[i];
temp[i] = temp[j];
temp[j] = swap;
}
}
}
// 取中值
return temp[FILTER_SIZE/2];
}
// 气体浓度计算函数(考虑对数响应)
float calculate_concentration(float adc_value, int sensor_type) {
// ADC值转电压 (0-3.3V)
float voltage = adc_value * 3.3 / 4095.0;
// 减去基线值
float baseline_voltage = baseline_values[sensor_type] * 3.3 / 4095.0;
float delta_v = voltage - baseline_voltage;
if(delta_v < 0) delta_v = 0;
// MQ传感器特性: Rs/R0 = a * (浓度)^b
// 简化计算(实际需要根据数据手册校准)
const float a[3] = {0.65, 0.35, 0.45}; // MQ7, MQ8, MP2.5的校准参数
const float b[3] = {-0.25, -0.42, -0.30};
// 使用对数转换
float concentration = pow(10, (log10(delta_v + 0.001) - log10(a[sensor_type])) / b[sensor_type]);
return concentration;
}
```
#### 3. 主循环修改
```c
int main(void) {
// ...初始化代码不变...
// 添加预热提示
OLED_ShowString(1, 48, "Warming up...", 8);
OLED_UpData();
delay_ms(180000); // 预热3分钟(MQ系列需要)
OLED_ShowString(1, 48, " ", 8);
calibrate_baseline(); // 基线校准
while(1) {
process_keys();
// ...清除标记代码不变...
if (g_adc_dma_sta == 1) {
for (uint8_t j = 0; j < 3; j++) {
// 使用改进滤波
float filtered = filtered_value(g_adc_dma_buf, j);
// 计算浓度
float concentration = calculate_concentration(filtered, j);
// 显示浓度值
char conc_buf[10];
snprintf(conc_buf, sizeof(conc_buf), "%.1f", concentration);
OLED_ShowString(35, 1 + j*16, " ", 8);
OLED_ShowString(35, 1 + j*16, conc_buf, 8);
// 报警检测(添加迟滞)
static float last_conc[3] = {0};
float threshold = (j==0) ? MQ7_ALARM_THRESHOLD :
(j==1) ? MQ8_ALARM_THRESHOLD : MP2_ALARM_THRESHOLD;
if(concentration > threshold && fabs(concentration - last_conc[j]) > threshold*0.1) {
// 触发报警(示例:LED闪烁)
LED1_ON();
delay_ms(100);
LED1_OFF();
}
last_conc[j] = concentration;
}
// ...阈值显示代码不变...
g_adc_dma_sta = 0;
adc_dma_enable(ADC_DMA_BUF_SIZE);
}
// ...其他代码不变...
}
}
```
### 关键改进说明:
1. **传感器预热处理**
- MQ系列传感器需要长时间预热(首次使用需24-48小时)
- 添加3分钟预热提示,实际部署应延长预热时间
2. **基线校准**
- 上电时采集环境空气作为基准值
- 后续测量值减去基线值,消除环境干扰
3. **复合滤波算法**
- 移动平均:平滑短期波动
- 中值滤波:消除突发干扰
- 迟滞比较:防止阈值附近抖动
4. **对数转换**
- 气体传感器通常有对数响应特性
- 使用`log10()`和`pow(10)`进行线性化处理
5. **硬件改进建议**
- 在传感器输出端添加RC低通滤波(10KΩ+10μF)
- VREF引脚添加10μF钽电容并联0.1μF陶瓷电容
- 传感器供电使用独立的LDO(非MCU同一电源)
###
阅读全文
相关推荐















