STM32F103C8T6酒精检测系统:ADC采集与Flash掉电保护实战

发布时间:2026/7/18 1:49:13
STM32F103C8T6酒精检测系统:ADC采集与Flash掉电保护实战
在实际嵌入式系统开发中酒精浓度检测与报警系统是一个典型的传感器应用项目它综合了模拟信号采集、数据处理、阈值判断、人机交互和系统保护等多个技术点。基于 STM32F103C8T6 这款性价比极高的 ARM Cortex-M3 内核单片机配合 MQ-3 酒精传感器、LCD1602 液晶显示屏以及掉电保护机制可以构建一个稳定可靠的酒驾预警装置。本文将完整介绍如何从零搭建这样一个系统包括硬件选型、电路连接、软件编程、阈值保存与恢复、声光报警触发逻辑以及常见问题排查方法。1. 系统整体设计与核心组件选型酒精浓度检测报警器的核心功能是实时采集环境中的酒精浓度值与预设的安全阈值比较当浓度超标时触发声光报警并能在系统断电后保留阈值设置。整个系统的可靠性取决于硬件选型的合理性和软件逻辑的严谨性。1.1 STM32F103C8T6 主控芯片特性分析STM32F103C8T6 属于 STM32F1 系列中的中等容量产品具有 64KB Flash、20KB RAM最高工作频率 72MHz。对于本项目来说其资源完全足够ADC 模块MQ-3 传感器输出模拟信号需要 12 位 ADC 进行采集。STM32F103C8T6 有 2 个 ADC10 个通道足够使用。GPIO 数量需要驱动 LCD1602至少 6 个 IO、LED 和蜂鸣器各 1 个 IO、按键输入设置阈值用至少 1 个芯片的 37 个 IO 口完全满足需求。Flash 存储内部 Flash 可用于保存阈值参数实现掉电保护。STM32F103C8T6 的 64KB Flash 中最后几页通常用于存储用户数据。定时器用于产生 PWM 控制蜂鸣器音调以及实现软件延时替代简单的delay()函数。在实际项目中如果考虑未来功能扩展如增加无线通信、数据记录等STM32F103C8T6 也留有足够的资源余量。1.2 MQ-3 酒精传感器工作原理与使用要点MQ-3 是一种半导体气敏传感器其对酒精的灵敏度较高也能检测苯、烷类气体。使用时需要注意加热电压通常需要 5V 电压给加热丝供电使传感器工作在最佳温度。负载电阻传感器输出信号需要串联一个负载电阻通常 4.7kΩ-10kΩ到地从电阻两端取电压信号。预热时间首次上电需要预热 1-2 分钟读数才会稳定。环境干扰对温度、湿度敏感不同环境需要校准。实际应用中最好有温湿度补偿。MQ-3 的输出电压与酒精浓度呈正相关但并非线性关系。通常需要在实际使用环境中用标准酒精浓度样本进行校准建立电压-浓度对应关系。1.3 LCD1602 液晶屏接口选择与驱动方式LCD1602 是字符型液晶模块每行显示 16 个字符共 2 行。与 STM32 的连接方式有 8 位和 4 位两种模式8 位模式需要 8 条数据线 3 条控制线RS、RW、E共 11 个 IO。4 位模式只使用高 4 位数据线 3 条控制线共 7 个 IO节省 IO 资源但编程稍复杂。对于 STM32F103C8T6IO 资源充足但为了布线简洁通常选择 4 位模式。需要注意的是4 位模式下每次传输一个字节要分两次先高4位后低4位。2. 硬件电路设计与连接正确的硬件连接是项目成功的基础。下面给出核心部件的连接方案和原理说明。2.1 STM32 最小系统电路STM32F103C8T6 需要最基本的工作电路// 电源部分 3.3V稳压供电STM32工作电压为2.0-3.6V典型3.3V 滤波电容VDD、VDDA引脚都需要100nF和4.7uF电容滤波 // 时钟电路 8MHz外部晶振HSE输入配合22pF负载电容 32.768kHz外部晶振可选用于RTC本项目非必需 // 复位电路 NRST引脚接10k上拉电阻和100nF电容到地实现上电复位和手动复位 // 调试接口 SWD接口SWDIO、SWCLK连接调试器用于程序下载和调试实际制作时如果使用现成的 STM32F103C8T6 最小系统板这些电路已经集成只需关注外设连接。2.2 MQ-3 传感器接口电路MQ-3 与 STM32 的正确连接方式MQ-3传感器模块 VCC - 5V加热电压 GND - GND AOUT - STM32的ADC引脚如PA0串联1kΩ电阻限流 DOUT - 可接STM32的普通IO用于简单阈值报警本项目主要用ADC 分压电路 STM32的ADC输入电压范围0-3.3V如果MQ-3输出可能超过3.3V需要电阻分压 建议方案MQ-3 AOUT → 10kΩ → ADC引脚 ↓ 10kΩ → GNDMQ-3 模块的 AOUT 输出电压通常在 0-5V而 STM32 的 ADC 只能接受 0-3.3V 输入所以分压电路是必要的。如果使用现成的 MQ-3 模块有些已经内置了分压电路需要确认输出范围。2.3 LCD1602 连接方案4 位模式下的 LCD1602 连接表LCD1602 引脚STM32 引脚功能说明VSSGND电源地VDD5V电源正V010kΩ电位器中端对比度调节RSPB0寄存器选择0-指令1-数据RWGND始终写模式接地EPB1使能信号D4PB2数据位4D5PB3数据位5D6PB4数据位6D7PB5数据位7A5V串联220Ω电阻背光正极KGND背光负极注意LCD1602 工作电压是 5V而 STM32 GPIO 是 3.3V 电平。但由于 3.3V 已经高于 LCD1602 的高电平阈值通常 2.2V可以直接驱动不需要电平转换。2.4 声光报警电路设计声光报警需要 LED 和蜂鸣器// LED报警电路 STM32 IO如PC13 → 220Ω限流电阻 → LED正极 → LED负极 → GND // 蜂鸣器驱动电路 有源蜂鸣器STM32 IO如PA8 → 1kΩ电阻 → NPN三极管基极 三极管集电极 → 蜂鸣器正极 → 5V 蜂鸣器负极 → 三极管发射极 → GND 无源蜂鸣器需要PWM驱动连接方式相同但需要通过定时器产生不同频率方波有源蜂鸣器内部有振荡电路给电就响频率固定无源蜂鸣器需要外部提供振荡信号可以控制音调。本项目建议使用有源蜂鸣器控制简单。3. 软件开发环境准备与工程配置STM32 开发有多种工具链选择这里以最常用的 Keil MDK-ARM 为例。3.1 Keil MDK-ARM 安装与芯片支持包安装步骤下载并安装 Keil MDK-ARM建议版本 5.30 以上安装 STM32F1 系列 Device Family Pack打开 Keil点击 Pack Installer图标像盒子搜索 STM32F1安装 Keil::STM32F1xx_DFP配置调试工具ST-LinkOptions for Target → Debug → 选择 ST-Link DebuggerSettings → Port 选择 SWDMax Clock 可设为 4MHz3.2 新建工程与基本配置在 Keil 中新建工程Project → New μVision Project选择存储路径选择芯片STMicroelectronics → STM32F103C8选择运行环境Manage Run-Time EnvironmentCMSIS → CORE必选Device → Startup必选如果使用 HAL 库选择 STM32Cube Framework → Classic关键配置Options for Target// Target选项卡 Xtal (MHz): 8.0 // 外部晶振频率 Use MicroLIB: 勾选 // 减小代码体积优化printf // C/C选项卡 Define: STM32F103x6,USE_STDPERIPH_DRIVER // 根据使用的库定义 Include Paths: 添加库文件头文件路径 // Debug选项卡 Use: ST-Link Debugger Settings: SWD, 4MHz, Reset and Run勾选3.3 工程文件结构规划合理的文件结构便于维护Project/ ├── CMSIS/ // 内核相关文件系统自动添加 ├── STM32F10x_StdPeriph_Driver/ // 标准外设库 ├── User/ │ ├── main.c │ ├── stm32f10x_it.c // 中断服务函数 │ ├── lcd1602.c/h // LCD驱动 │ ├── mq3.c/h // 传感器驱动 │ ├── eeprom_emul.c/h // Flash模拟EEPROM │ └── system.c/h // 系统初始化 ├── Libraries/ // 第三方库 └── Output/ // 编译输出4. 核心功能模块代码实现下面分段实现各功能模块重点解释关键代码和设计思路。4.1 ADC 采集 MQ-3 传感器数据STM32 的 ADC 采集需要配置时钟、通道、采样时间等参数// mq3.h #ifndef __MQ3_H #define __MQ3_H #include stm32f10x.h #define MQ3_ADC_Channel ADC_Channel_0 // PA0对应ADC通道0 #define MQ3_ADCx ADC1 void MQ3_Init(void); uint16_t MQ3_GetADCValue(void); float MQ3_GetVoltage(void); float MQ3_GetConcentration(void); #endif// mq3.c #include mq3.h #include stm32f10x_adc.h #include stm32f10x_gpio.h #include stm32f10x_rcc.h void MQ3_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; // 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); // 配置PA0为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // ADC模式配置 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; // 独立模式 ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; // 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel 1; // 1个转换通道 ADC_Init(ADC1, ADC_InitStructure); // 设置通道0的采样时间 ADC_RegularChannelConfig(ADC1, MQ3_ADC_Channel, 1, ADC_SampleTime_239Cycles5); // 开启ADC并校准 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 开始转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); } uint16_t MQ3_GetADCValue(void) { while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成 return ADC_GetConversionValue(ADC1); } float MQ3_GetVoltage(void) { uint16_t adc_value MQ3_GetADCValue(); return (adc_value * 3.3f) / 4096; // 12位ADC参考电压3.3V } float MQ3_GetConcentration(void) { float voltage MQ3_GetVoltage(); // 需要根据实际校准曲线转换电压为浓度值 // 简单线性关系浓度(mg/L) (电压 - 基线电压) * 系数 // 实际项目需要多点校准 float base_voltage 0.8f; // 洁净空气中的基准电压 float factor 0.5f; // 校准系数 if(voltage base_voltage) return 0.0f; else return (voltage - base_voltage) * factor; }ADC 采集的关键点采样时间要足够长确保电容充电完成。MQ-3 输出阻抗较高建议使用较长的采样时间如 239.5 周期。连续转换模式下第一次转换结果可能不准可以丢弃前几次结果。实际浓度计算需要根据传感器特性曲线校准最好使用标准酒精样本进行多点校准。4.2 LCD1602 显示驱动实现LCD1602 的 4 位模式驱动需要实现基本的指令和数据发送函数// lcd1602.h #ifndef __LCD1602_H #define __LCD1602_H #include stm32f10x.h #include delay.h // 需要实现微秒延时函数 // 引脚定义 #define LCD_RS_PIN GPIO_Pin_0 #define LCD_RS_PORT GPIOB #define LCD_E_PIN GPIO_Pin_1 #define LCD_E_PORT GPIOB #define LCD_D4_PIN GPIO_Pin_2 #define LCD_D4_PORT GPIOB #define LCD_D5_PIN GPIO_Pin_3 #define LCD_D5_PORT GPIOB #define LCD_D6_PIN GPIO_Pin_4 #define LCD_D6_PORT GPIOB #define LCD_D7_PIN GPIO_Pin_5 #define LCD_D7_PORT GPIOB void LCD_Init(void); void LCD_Clear(void); void LCD_SetCursor(uint8_t row, uint8_t col); void LCD_WriteString(char *str); void LCD_WriteNumber(uint16_t num); #endif// lcd1602.c #include lcd1602.h // 写命令函数内部使用 static void LCD_WriteCmd(uint8_t cmd) { GPIO_ResetBits(LCD_RS_PORT, LCD_RS_PIN); // RS0写命令 // 写高4位 if(cmd 0x80) GPIO_SetBits(LCD_D7_PORT, LCD_D7_PIN); else GPIO_ResetBits(LCD_D7_PORT, LCD_D7_PIN); if(cmd 0x40) GPIO_SetBits(LCD_D6_PORT, LCD_D6_PIN); else GPIO_ResetBits(LCD_D6_PORT, LCD_D6_PIN); if(cmd 0x20) GPIO_SetBits(LCD_D5_PORT, LCD_D5_PIN); else GPIO_ResetBits(LCD_D5_PORT, LCD_D5_PIN); if(cmd 0x10) GPIO_SetBits(LCD_D4_PORT, LCD_D4_PIN); else GPIO_ResetBits(LCD_D4_PORT, LCD_D4_PIN); GPIO_SetBits(LCD_E_PORT, LCD_E_PIN); // E1 Delay_us(10); // 延时10us GPIO_ResetBits(LCD_E_PORT, LCD_E_PIN); // E0 Delay_us(10); // 写低4位 if(cmd 0x08) GPIO_SetBits(LCD_D7_PORT, LCD_D7_PIN); else GPIO_ResetBits(LCD_D7_PORT, LCD_D7_PIN); if(cmd 0x04) GPIO_SetBits(LCD_D6_PORT, LCD_D6_PIN); else GPIO_ResetBits(LCD_D6_PORT, LCD_D6_PIN); if(cmd 0x02) GPIO_SetBits(LCD_D5_PORT, LCD_D5_PIN); else GPIO_ResetBits(LCD_D5_PORT, LCD_D5_PIN); if(cmd 0x01) GPIO_SetBits(LCD_D4_PORT, LCD_D4_PIN); else GPIO_ResetBits(LCD_D4_PORT, LCD_D4_PIN); GPIO_SetBits(LCD_E_PORT, LCD_E_PIN); Delay_us(10); GPIO_ResetBits(LCD_E_PORT, LCD_E_PIN); Delay_us(100); // 命令执行需要更长时间 } // 写数据函数内部使用 static void LCD_WriteData(uint8_t data) { GPIO_SetBits(LCD_RS_PORT, LCD_RS_PIN); // RS1写数据 // 写高4位与写命令相同 if(data 0x80) GPIO_SetBits(LCD_D7_PORT, LCD_D7_PIN); else GPIO_ResetBits(LCD_D7_PORT, LCD_D7_PIN); if(data 0x40) GPIO_SetBits(LCD_D6_PORT, LCD_D6_PIN); else GPIO_ResetBits(LCD_D6_PORT, LCD_D6_PIN); if(data 0x20) GPIO_SetBits(LCD_D5_PORT, LCD_D5_PIN); else GPIO_ResetBits(LCD_D5_PORT, LCD_D5_PIN); if(data 0x10) GPIO_SetBits(LCD_D4_PORT, LCD_D4_PIN); else GPIO_ResetBits(LCD_D4_PORT, LCD_D4_PIN); GPIO_SetBits(LCD_E_PORT, LCD_E_PIN); Delay_us(10); GPIO_ResetBits(LCD_E_PORT, LCD_E_PIN); Delay_us(10); // 写低4位 if(data 0x08) GPIO_SetBits(LCD_D7_PORT, LCD_D7_PIN); else GPIO_ResetBits(LCD_D7_PORT, LCD_D7_PIN); if(data 0x04) GPIO_SetBits(LCD_D6_PORT, LCD_D6_PIN); else GPIO_ResetBits(LCD_D6_PORT, LCD_D6_PIN); if(data 0x02) GPIO_SetBits(LCD_D5_PORT, LCD_D5_PIN); else GPIO_ResetBits(LCD_D5_PORT, LCD_D5_PIN); if(data 0x01) GPIO_SetBits(LCD_D4_PORT, LCD_D4_PIN); else GPIO_ResetBits(LCD_D4_PORT, LCD_D4_PIN); GPIO_SetBits(LCD_E_PORT, LCD_E_PIN); Delay_us(10); GPIO_ResetBits(LCD_E_PORT, LCD_E_PIN); Delay_us(100); } void LCD_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 开启GPIOB时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 配置所有LCD引脚为推挽输出 GPIO_InitStructure.GPIO_Pin LCD_RS_PIN | LCD_E_PIN | LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // LCD初始化序列 Delay_ms(15); // 上电延时 LCD_WriteCmd(0x33); // 初始化序列开始 Delay_ms(5); LCD_WriteCmd(0x32); // 设置为4位模式 Delay_ms(5); LCD_WriteCmd(0x28); // 2行显示5x8点阵 Delay_ms(5); LCD_WriteCmd(0x0C); // 显示开光标关闪烁关 Delay_ms(5); LCD_WriteCmd(0x06); // 文字不动地址自动1 Delay_ms(5); LCD_WriteCmd(0x01); // 清屏 Delay_ms(5); } void LCD_Clear(void) { LCD_WriteCmd(0x01); Delay_ms(2); } void LCD_SetCursor(uint8_t row, uint8_t col) { uint8_t address; if(row 0) address 0x80 col; // 第一行起始地址0x80 else address 0xC0 col; // 第二行起始地址0xC0 LCD_WriteCmd(address); } void LCD_WriteString(char *str) { while(*str) { LCD_WriteData(*str); } } void LCD_WriteNumber(uint16_t num) { char buffer[6]; uint8_t i 0; if(num 0) { LCD_WriteData(0); return; } while(num 0) { buffer[i] (num % 10) 0; num / 10; } while(i 0) { LCD_WriteData(buffer[--i]); } }LCD1602 驱动的关键注意事项初始化序列必须严格按照时序要求特别是上电后的延时。每次写命令或数据后要有足够的延时确保 LCD 控制器完成处理。4 位模式下先传输高 4 位再传输低 4 位。显示内容超过 16 字符时需要处理换行或滚动。4.3 Flash 模拟 EEPROM 实现阈值掉电保护STM32F103C8T6 没有真正的 EEPROM但可以用内部 Flash 模拟。需要注意 Flash 的擦写寿命约 1 万次// eeprom_emul.h #ifndef __EEPROM_EMUL_H #define __EEPROM_EMUL_H #include stm32f10x.h #define EEPROM_START_ADDRESS ((uint32_t)0x0800FC00) // 最后一页起始地址 #define EEPROM_SIZE 1024 // 1KB空间 #define THRESHOLD_ADDRESS (EEPROM_START_ADDRESS 0) // 阈值存储地址 uint16_t EEPROM_ReadThreshold(void); void EEPROM_WriteThreshold(uint16_t threshold); #endif// eeprom_emul.c #include eeprom_emul.h // Flash解锁 static void FLASH_Unlock(void) { if((FLASH-CR FLASH_CR_LOCK) ! RESET) { FLASH-KEYR FLASH_KEY1; FLASH-KEYR FLASH_KEY2; } } // Flash锁定 static void FLASH_Lock(void) { FLASH-CR | FLASH_CR_LOCK; } // 擦除指定页 static FLASH_Status FLASH_ErasePage(uint32_t Page_Address) { FLASH_Status status FLASH_COMPLETE; FLASH-CR | FLASH_CR_PER; // 页擦除使能 FLASH-AR Page_Address; // 设置擦除地址 FLASH-CR | FLASH_CR_STRT; // 开始擦除 while((FLASH-SR FLASH_SR_BSY) FLASH_SR_BSY); // 等待擦除完成 if(FLASH-SR FLASH_SR_EOP) { FLASH-SR FLASH_SR_EOP; // 清除EOP标志 } status (FLASH-SR (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)); FLASH-CR ~FLASH_CR_PER; // 关闭页擦除 return status; } uint16_t EEPROM_ReadThreshold(void) { uint16_t *pThreshold (uint16_t*)THRESHOLD_ADDRESS; // 检查是否已写入有效数据非0xFFFF if(*pThreshold ! 0xFFFF) return *pThreshold; else return 200; // 默认阈值200mg/L } void EEPROM_WriteThreshold(uint16_t threshold) { uint16_t current_threshold EEPROM_ReadThreshold(); // 如果新阈值与当前相同不需要写入 if(current_threshold threshold) return; FLASH_Unlock(); // 检查目标地址是否为空0xFFFF if(*(__IO uint16_t*)THRESHOLD_ADDRESS ! 0xFFFF) { // 需要先擦除整页 FLASH_ErasePage(EEPROM_START_ADDRESS); } // 写入新阈值 FLASH-CR | FLASH_CR_PG; // 编程使能 *(__IO uint16_t*)THRESHOLD_ADDRESS threshold; while((FLASH-SR FLASH_SR_BSY) FLASH_SR_BSY); // 等待写入完成 if(FLASH-SR FLASH_SR_EOP) { FLASH-SR FLASH_SR_EOP; // 清除EOP标志 } FLASH-CR ~FLASH_CR_PG; // 关闭编程 FLASH_Lock(); }Flash 模拟 EEPROM 的关键安全考虑Flash 写入前必须先擦除擦除单位是整页1KB。尽量减少写入次数避免频繁擦写导致 Flash 损坏。写入前检查数据是否变化避免不必要的写入操作。重要数据可以存储多份副本增加可靠性。4.4 主程序逻辑与报警控制主程序需要协调各个模块实现完整的检测报警功能// main.c #include stm32f10x.h #include lcd1602.h #include mq3.h #include eeprom_emul.h #include delay.h // 报警阈值单位mg/L #define ALARM_THRESHOLD_DEFAULT 200 // GPIO定义 #define LED_PIN GPIO_Pin_13 #define LED_PORT GPIOC #define BUZZER_PIN GPIO_Pin_8 #define BUZZER_PORT GPIOA // 全局变量 uint16_t g_alarm_threshold ALARM_THRESHOLD_DEFAULT; uint8_t g_alarm_enabled 1; // 声光报警控制 void Alarm_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA, ENABLE); // LED配置 GPIO_InitStructure.GPIO_Pin LED_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(LED_PORT, GPIO_InitStructure); // 蜂鸣器配置 GPIO_InitStructure.GPIO_Pin BUZZER_PIN; GPIO_Init(BUZZER_PORT, GPIO_InitStructure); // 初始状态关闭 GPIO_SetBits(LED_PORT, LED_PIN); // LED灭低电平有效 GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN); // 蜂鸣器关闭 } void Alarm_On(void) { if(g_alarm_enabled) { GPIO_ResetBits(LED_PORT, LED_PIN); // LED亮 GPIO_SetBits(BUZZER_PORT, BUZZER_PIN); // 蜂鸣器响 } } void Alarm_Off(void) { GPIO_SetBits(LED_PORT, LED_PIN); // LED灭 GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN); // 蜂鸣器关闭 } // 按键处理简单查询方式 void Key_Process(void) { // 假设按键接在PA1用于切换报警使能/禁用 static uint8_t key_state 1; static uint8_t last_key_state 1; uint8_t current_key_state GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); // 检测下降沿按键按下 if(last_key_state 1 current_key_state 0) { Delay_ms(20); // 消抖 if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 0) { g_alarm_enabled !g_alarm_enabled; } } last_key_state current_key_state; } // 显示更新 void Display_Update(float concentration) { char buffer[17]; LCD_Clear(); LCD_SetCursor(0, 0); LCD_WriteString(Alcohol: ); LCD_WriteNumber((uint16_t)concentration); LCD_WriteString( mg/L); LCD_SetCursor(1, 0); if(concentration g_alarm_threshold) { LCD_WriteString(ALARM! ); if(g_alarm_enabled) LCD_WriteString(ON ); else LCD_WriteString(OFF); } else { LCD_WriteString(Normal Th:); LCD_WriteNumber(g_alarm_threshold); } } int main(void) { float alcohol_concentration; // 系统初始化 Delay_Init(); // 延时函数初始化 LCD_Init(); // LCD初始化 MQ3_Init(); // 传感器初始化 Alarm_Init(); // 报警器初始化 // 从Flash读取保存的阈值 g_alarm_threshold EEPROM_ReadThreshold(); // 显示启动信息 LCD_SetCursor(0, 0); LCD_WriteString(Alcohol Detector); LCD_SetCursor(1, 0); LCD_WriteString(Initializing...); Delay_ms(2000); while(1) { // 读取酒精浓度 alcohol_concentration MQ3_GetConcentration(); // 按键处理 Key_Process(); // 报警判断 if(alcohol_concentration g_alarm_threshold) { Alarm_On(); } else { Alarm_Off(); } // 更新显示 Display_Update(alcohol_concentration); // 延时500ms降低采样频率 Delay_ms(500); } }主程序的设计要点采用非阻塞方式处理按键避免影响实时性。显示更新频率适中既要保证信息及时又要避免闪烁。报警判断要有适当的滞后或滤波防止误报。关键参数如阈值在修改后及时保存到 Flash。5. 系统调试与常见问题排查实际制作过程中会遇到各种问题下面列出典型问题及解决方法。5.1 硬件连接问题排查表问题现象可能原因检查方法解决方案LCD1602 无显示电源接反或电压不对测量VDD-VSS间电压确保5V供电V0电位器调节对比度LCD1602 显示乱码初始化序列错误检查初始化代码时序增加命令间延时确认4/8位模式设置MQ-3 读数始终为0传感器未预热或接线错误测量AOUT引脚电压等待1-2分钟预热检查分压电路MQ-3 读数跳动大电源噪声或ADC配置问题测量电源纹波检查采样时间增加电源滤波电容延长ADC采样时间蜂鸣器不响驱动电路问题测量三极管基极电压检查限流电阻和三极管极性Flash 写入失败未解锁或地址错误检查Flash状态寄存器确保先解锁再操作地址要在Flash范围内5.2 软件调试技巧ADC 采集值不稳定// 解决方法软件滤波 #define SAMPLE_COUNT 10 // 采样次数 uint16_t MQ3_GetFilteredADCValue(void) { uint32_t sum 0; for(uint8_t i 0; i SAMPLE_COUNT; i) { sum MQ3_GetADCValue(); Delay_ms(10); } return sum / SAMPLE_COUNT; }LCD 显示字符错位检查光标自动增量设置0x06 指令确认每行显示字符不超过16个清屏后等待足够时间1.6msFlash 写入后读取值不正确写入前必须确保地址已擦除全0xFF写入半字16位要对齐到偶数地址写入后检查FLASH_SR寄存器错误标志5.3 校准与精度提升MQ-3 传感器需要校准才能获得准确浓度值基线校准在洁净空气中记录ADC值作为基准。多点校准使用已知浓度的酒精样本建立电压