1. 系统概述与开发环境搭建
烟雾报警系统是智能家居和工业安全领域的基础设施,而基于STM32的方案因其性价比高、开发资源丰富成为首选。这次我们要实现的系统核心是STM32F103单片机,配合MQ-2传感器(仿真中用滑动变阻器模拟)、LCD1602显示屏和蜂鸣器报警电路。整个开发流程涉及Proteus仿真和Keil编程,对初学者特别友好。
开发环境准备其实比你想象的简单。我用的Proteus 8.9有个隐藏技巧——安装后一定要勾选STM32库支持,否则找不到F103型号。Keil MDK5的配置更要注意,安装完STM32CubeMX后,记得通过包安装器添加F1系列支持包。有次我忘了这步,调试时各种报错,折腾了半天才发现问题。
硬件连线示意图:
- STM32F103C8T6核心板
- ADC1通道1接滑动变阻器(模拟MQ-2)
- PA0-PA2接三个按键(设置/加/减)
- PB0-PB7接LCD1602数据线
- PA8接蜂鸣器驱动三极管基极
提示:Proteus里搜索"POT-HG"可以找到更精确的滑动变阻器模型,比默认的电位器仿真效果更好。
2. Proteus仿真搭建技巧
仿真电路搭建是验证设计的关键步骤。这里有个实用技巧:在Proteus中放置STM32芯片后,先右键设置晶振频率为8MHz(与代码中HSI配置一致)。我见过有人直接用默认值,结果串口通信全乱套。
ADC采集部分的仿真有特殊处理:
- 放置滑动变阻器并设置为10KΩ
- 连接变阻器中间引脚到PA1(ADC1通道1)
- 添加电压探针方便观察输入变化
LCD1602的驱动电路要注意:
- 对比度调节端必须接1KΩ电阻到地
- 使能信号E的上升沿时间要大于450ns
- 数据线建议加上4.7KΩ上拉电阻
蜂鸣器驱动是新手容易踩坑的地方。仿真中我用的是BUZZER元件,但实际项目要用电磁式蜂鸣器。关键点在于:
// 驱动电路原理 BEEP_Pin --[1KΩ]--> NPN三极管基极 三极管集电极接蜂鸣器负极 蜂鸣器正极接5V电源3. HAL库编程详解
3.1 ADC采集配置
CubeMX配置ADC时有个小技巧:把采样时间设为239.5周期(对应ADC_SAMPLETIME_239CYCLES_5),这样能获得更稳定的读数。我在项目中发现,默认的1.5周期采样容易受干扰。
ADC初始化代码:
void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); }实际采集时采用轮询方式更可靠:
float Get_Smoke_Value(void) { uint32_t raw_value; float voltage; HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); raw_value = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); voltage = (float)raw_value * 3.3f / 4095.0f; return voltage * 100.0f; // 转换为ppm模拟值 }3.2 LCD1602驱动优化
标准4位模式驱动LCD1602时,我优化了写命令函数:
void LCD_WriteCmd(uint8_t cmd) { HAL_GPIO_WritePin(LCD_RS_GPIO_Port, LCD_RS_Pin, GPIO_PIN_RESET); LCD_Write4Bits(cmd>>4); // 先高4位 LCD_Write4Bits(cmd&0x0F); // 后低4位 HAL_Delay(1); // 实测需要至少37us延时 }显示字符串时采用DMA+GPIO可以大幅降低CPU占用:
void LCD_ShowString(uint8_t x, uint8_t y, char *str) { LCD_SetCursor(x, y); while(*str) { LCD_WriteData(*str++); // 可用HAL_DMA_Start替代延时 for(volatile int i=0; i<100; i++); } }3.3 按键中断处理
CubeMX配置外部中断时,建议将按键对应的GPIO设置为下拉输入模式,中断触发边沿选择上升沿。我遇到过按键抖动导致多次触发的问题,后来在回调函数中加入20ms防抖处理:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { static uint32_t last_tick = 0; if(HAL_GetTick() - last_tick < 20) return; if(GPIO_Pin == KEY_SET_Pin) { set_mode = !set_mode; LCD_ShowString(0,1, set_mode ? "SET:ON " : "SET:OFF"); } // ...其他按键处理 last_tick = HAL_GetTick(); }4. 系统调试与优化
4.1 报警阈值设置
阈值调整逻辑我做了人性化改进:在设置模式下,短按加减键调整步长为10ppm,长按则步长变为50ppm。实现原理是利用定时器中断检测按键持续时间:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint8_t key2_cnt = 0, key3_cnt = 0; if(HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin)) { if(++key2_cnt > 5) { // 长按0.5秒 threshold += 50; key2_cnt = 0; } } else if(key2_cnt > 0) { threshold += 10; // 短按 key2_cnt = 0; } // 同理处理KEY_DOWN }4.2 报警逻辑优化
原始设计是检测到超标立即报警,我增加了持续检测机制——只有浓度连续3次超过阈值才触发报警,避免误报:
uint8_t alarm_check(float current_val) { static uint8_t count = 0; if(current_val > threshold) { if(++count >= 3) { count = 3; return 1; } } else { count = 0; } return 0; }4.3 串口调试技巧
在usart.c中添加printf重定向,可以方便地查看系统状态:
#include <stdio.h> int __io_putchar(int ch) { HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 10); return ch; } // 使用时直接调用 printf("当前浓度:%.1fppm 阈值:%dppm\n", smoke_value, threshold);5. 完整代码架构解析
整个工程采用模块化设计,主要文件结构如下:
/Drivers /STM32F1xx_HAL_Driver // HAL库文件 /CMSIS // 内核支持文件 /Inc main.h lcd1602.h adc.h ... /Src main.c // 主循环和初始化 lcd1602.c // LCD驱动 adc.c // 采集处理 ...主程序流程采用事件驱动架构:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_TIM3_Init(); MX_USART1_UART_Init(); LCD_Init(); HAL_TIM_Base_Start_IT(&htim3); // 启动定时器 while(1) { float smoke = Get_Smoke_Value(); Display_Values(smoke); if(alarm_check(smoke)) { HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_SET); } HAL_Delay(300); } }在调试这个系统时,我发现ADC读数偶尔会跳变,后来在电源引脚加了0.1μF去耦电容后问题解决。另一个常见问题是LCD显示乱码,通常是初始化时序不对,解决方法是在LCD_Init()函数后加500ms延时。