ARTICLE DETAIL

资讯详情

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

STM32环境监测系统全开源:从原理图到代码的完整复现实战

STM32环境监测系统全开源:从原理图到代码的完整复现实战 我把整套工程开源了硬件原理图含立创EDA工程、STM32的Keil工程源码、Proteus仿真文件全部打包放出。这篇文章会把设计思路、代码结构、仿真搭建和实物调试的坑一次性讲透让你拿到文件后能直接复现而不是对着原理图干瞪眼。先说清楚这套系统做什么基于STM32F103C8T6最小系统板外接DHT11温湿度传感器、MQ-2烟雾浓度传感器、光敏电阻模块实时采集环境数据并显示在LCD1602上。四项环境指标里的温湿度和烟雾浓度一旦超过预设阈值蜂鸣器自动报警同时板载LED闪烁提示。三个按键用来调整报警阈值掉电不丢失。整套系统核心逻辑跑在定时器中断和状态机上代码结构清晰非常适合作毕设原型、智能家居入门项目或者给刚学完STM32外设想练综合项目的人。1. 我从一个“翻车”开始聊为什么坚持做这块板子去年帮一个师弟改毕设他拿某宝买来的“STM32毕业设计全家桶”方案主控是F103ZET6正点原子板子外挂一堆模块杜邦线飞得跟蜘蛛网似的。最离谱的是他想测厨房烟雾浓度MQ-2模块就放在板子旁边蜂鸣器一响他第一时间不是去看数据而是先找是哪根线松了。那套东西本质上就是外设模块的罗列谈不上系统设计。后来我把这套家用环境监测重新捋了一遍从传感器选型到电源树再到代码调度全部重构定下几个硬指标单块小PCB完成所有功能不依赖开发板不飞线。任何模块坏掉系统其余部分照样工作不能因为DHT11卡死导致蜂鸣器失效。报警阈值必须能通过按键现场修改不能每次改参数都重新烧程序。原理图、代码、仿真三件套齐全能先在电脑上验证逻辑再动烙铁。1.1 传感器选型该省的地方省该花的钱不能省很多初学者一上来就想用SHT30、BME280这种高精度数字传感器觉得DHT11精度差丢人。我反而坚持用DHT11做温湿度采集理由很实在这套系统监测的是“家里舒不舒服、安不安全”不是做计量校准。湿度精度±5%RH、温度精度±2℃对判断“要不要开加湿器”“厨房是不是水汽太大”完全够用。更关键的是DHT11是单总线协议市面教程资料多到泛滥学它的时序解析远比I2C读SHT30更能锻炼底层能力。MQ-2这个模块争议更大很多人说它需要预热、需要校准、输出不准确我承认这些毛病都存在。但注意家庭环境监测的本质是“趋势判断和阈值告警”不是精确测PPM浓度。MQ-2对液化气、丙烷、氢气的灵敏度高响应时间在10秒以内放在厨房测燃气泄漏完全胜任。我真正看重的是它的模拟电压输出——STM32的ADC正好派上用场这比纯数字接口的传感器更能讲清楚“模拟量采集→量化→标定”这条完整的信号链。传感器接口精度/量程成本适合场景DHT11单总线温度±2℃湿度±5%RH约3元室内舒适度判断学习单总线时序SHT30I2C温度±0.3℃湿度±2%RH约10元对精度有要求且愿意写I2C协议DHT22单总线温度±0.5℃湿度±2%RH约8元想要DHT11的简单又想稍微准一点MQ-2模拟量300-10000ppm可燃气体约5元燃气/烟雾趋势监测MQ-135模拟量空气质量约6元监测甲醛、氨气等空气污染物光照部分我选了最朴素的光敏电阻模块——就是LM393比较器小板上面有个可调电阻光照变强时电压输出跳变。有些方案会用BH1750数字光照传感器但对一个“白天亮、晚上暗”的判断逻辑来说杀鸡用牛刀了。光敏模块锦上添花的作用是晚上自动打开报警指示灯。如果你愿意多花两块钱把光敏电阻换成贴片光敏二极管焊在PCB上还能省掉一个模块的钱。1.2 为什么是STM32F103C8T6而不是Arduino或者51这个问题的答案直接决定了整套工程的代码风格。我知道用Arduino写这个项目两小时就能调完但Arduino的高度封装会把DHT11的时序、ADC的采样保持、定时器中断这些核心机理全部藏起来。51单片机倒是对底层友好但主频12MHz、没有硬件除法器跑起浮点运算和稍复杂的按键菜单就开始吃力了。STM32F103C8T6在这类项目里是性价比最平衡的选择主频72MHz处理DHT11时序和LCD刷新绰绰有余。2个12位ADC采样光敏和MQ-2不用外挂ADC芯片。3个通用定时器加1个高级定时器做多路PWM和精确延时都方便。20KB RAM、64KB Flash塞下RTOS都够了裸机状态机更宽裕。最关键的是市面上所有资料都围绕F103遇到问题一搜一堆解决方案。封装上选LQFP48的C8T6而非F103ZET6理由是四层板小尺寸C8T6只需两层板就能布通打样成本低。C8T6的引脚更紧凑对新手焊接也友好一些。实话说这些资源用在这个项目上有富余但富余的算力正好支撑后面我说的模块化代码设计——你不必为了省资源把代码写到没法维护。2. 原理图设计的几个关键取舍不是把线连通就完事这套系统的原理图初看很简单主控加传感器加显示加报警但细节全藏在你看不见的地方。我在设计原理图时踩过几个跟头这里把核心节点梳理出来。2.1 IO资源分配先把每个引脚的“第二功能”逼出来画原理图前最忌直接拉线要把所有外设占用的引脚在Excel或者纸上列清楚检查有没有和调试口、启动配置冲突。功能模块STM32引脚说明DHT11数据线PA0开漏输出兼输入外部上拉4.7kΩMQ-2模拟输出PA1ADC1_IN1采样烟雾浓度光敏模块输出PA2ADC1_IN2或者作为数字量输入LCD1602数据线PB0-PB3四线模式只占4个数据脚LCD1602 RSPB10寄存器选择LCD1602 ENPB11使能信号蜂鸣器驱动PA8TIM1_CH1 PWM输出可以驱动不同响度报警指示灯PA9普通推挽输出按键1/2/3PB12/PB13/PB14内部上拉输入按下接地预留串口PA9/PA10注意和指示灯引脚冲突时二选一这里有个很容易踩的坑PA9和PA10同时是USART1的收发引脚。如果你既想用串口打印调试信息又想点灯就会打架。我最终把指示灯挪到PA8配合TIM1做PWM呼吸灯效果PA9/PA10留给串口作为调试口。这就是画原理图之前必须做的“功能复用仲裁”工作。2.2 电源系统从USB取电到3.3V稳压的讲究整套系统用USB线供电——手机充电头或者电脑USB口都能带起来。5V进入板子后分成两路一路直接给LCD1602背光和MQ-2加热丝供电另一路经过AMS1117-3.3稳压给STM32、DHT11和光敏模块供电。为什么MQ-2要单独吃5V因为MQ-2内部那个加热电阻需要150mA左右的电流把敏感层加热到工作温度AMS1117的压差和散热扛不住这种持续负载。电源部分的去耦电容设计要严格按照芯片手册来AMS1117输入端放一个100μF电解电容加一个0.1μF陶瓷电容做高频去耦稳定输入电压。输出端放一个22μF钽电容加上0.1μF陶瓷电容确保3.3V纹波够小。STM32每个VDD引脚旁边放一个100nF陶瓷电容且要尽可能靠近引脚走线要先过电容再进芯片。VDDA引脚单独接一个1μF电容到地这个直接影响ADC采样精度偷懒不得。我第一次改版的时候省了VDDA的1μF电容结果ADC采样出来的值波动有几十个LSB光敏值跳得像随机数。后来老老实实补上电容瞬间安静了。原理图上已经帮你摆好了这些电容你打样时千万别删除。2.3 传感器采样电路里的电阻分压与滤波设计MQ-2模块的标准接法是模拟输出直接进ADC引脚但如果直接把模块插到板子上模块自带的电位器已经把输出调到了0-5V范围。STM32的ADC输入范围是0-3.3V直接怼5V会烧引脚。所以我用了一个电阻分压网络把MQ-2输出衰减到0-3.3V。具体计算是假设MQ-2输出最大值4V分压后要变成3.3V那么R1/(R1R2) 3.3/4 0.825。选R110kΩ、R22.2kΩ分压比约0.82刚好满足。这里注意别用精确的3.3/50.66去算因为MQ-2的模拟输出不会真到5V按4V设计余量更合适。分压之后我加了一级RC低通滤波R1kΩC100nF截止频率约1.6kHz。MQ-2这个传感器的响应带宽本身只有几赫兹真正有用的信号都在低频段滤掉高频噪声后ADC采样数据稳定得多。没有这级滤波你在LCD上看到的烟雾值末尾两位会一直抖。光敏模块是数字量输出方案就简单了直接进GPIO。但如果你买的是不带LM393的裸光敏电阻需要用10kΩ电阻做分压光敏电阻和10kΩ电阻串联中间节点接ADC引脚。光照强时光敏电阻阻值降到几kΩ节点电压升高光照弱时阻值升到几百kΩ节点电压趋近于0。这个电路在原理图上也有备用接法你可以自己切换。2.4 报警电路为什么蜂鸣器用三极管而不是直接接GPIO蜂鸣器分为有源和无源两种无源蜂鸣器需要PWM方波驱动才有声音有源蜂鸣器内部有振荡电路给高电平就响。这套系统里我用的是5V有源蜂鸣器但这又带来一个问题——STM32的GPIO只能输出3.3V高电平直接驱动5V蜂鸣器电流不够。所以蜂鸣器驱动电路用了一个S8050三极管做开关管。具体接法是蜂鸣器正极接5V负极接三极管集电极三极管发射极接地基极通过一个1kΩ电阻接到STM32的PA8引脚。当PA8输出高电平时三极管导通蜂鸣器回路通电开始响。这个1kΩ基极电阻是限流电阻防止基极电流过大烧坏IO口计算方式是(3.3V-0.7V)/1kΩ基极电流约2.6mA足够让S8050饱和导通。基极一定要串这个电阻直接接会拉低IO电压甚至烧引脚。指示灯也类似思路但LED电流很小直接串一个330Ω限流电阻就行。330Ω怎么算出来的LED正向压降按2V算3.3V-2V1.3V落在电阻上要限流到4mA左右电阻值就是1.3V/4mA约325Ω取标称值330Ω。2.5 按键电路的上拉设计与去抖硬件方案三个按键如果像开发板那样外接上拉电阻当然没问题但STM32内部自带上拉电阻原理图上我直接用内部上拉GPIO配置为输入上拉模式按键一端接IO另一端接地。按键按下时IO读到低电平松开时上拉电阻保证IO稳定在高电平。内部的这个上拉电阻一般在30-50kΩ之间这对按键检测够了但如果你的按键引线很长超过10cm建议还是外部加一个10kΩ上拉防止工频干扰让IO误触发。原理图上预留了外部上拉电阻的位置做实物时可以按需焊接。硬件去抖我没额外加电容而是在软件里做了20ms的延时消抖——这个后面代码部分细说。3. 这套系统的主控代码是怎么把“同时干很多事”变成现实的单片机编程最容易犯的毛病是while(1)里一堆delay平铺直叙读DHT11延时500ms再刷新LCD延时20ms再读烟雾ADC延时50ms只要中间任何一个延时卡住整个系统就“假死”了。按键按下没反应、蜂鸣器该响的时候不响都源于这种落后的时间管理方式。这套代码改用了“时间片轮询”加“状态机”的架构系统内部分了三层底层驱动层负责操作具体硬件中间业务逻辑层处理状态转换最上层的App层关心“当前该干什么”。这套架构往下能跑裸机往上以后想接RTOS代码迁移成本极低。3.1 定时器产生的“心跳”1毫秒时间基准我用TIM2来做系统心跳配置72MHz时钟的预分频器72分频得到1MHz计数频率自动重装载值设999就得到了1kHz的更新中断——也就是1毫秒的基准节拍。整个系统所有需要延时的场景都不再阻塞CPU而是看这个节拍计数器走到哪了。TIM2的中断服务函数里放了几个全局的节拍变量volatile uint32_t uwTick 0; // 系统运行毫秒计数 volatile uint8_t dht11_tick 0; // DHT11读取周期标志 volatile uint16_t sensor_tick 0; // ADC轮询周期计数 volatile uint16_t lcd_tick 0; // LCD刷新周期计数 1s volatile uint16_t key_tick 0; // 按键扫描周期计数 20ms主循环的执行逻辑变成这样int main(void) { HAL_Init(); SystemClock_Config(); GPIO_Init(); TIM2_Start(); DHT11_Init(); LCD1602_Init(); Buzzer_Init(); Key_Init(); ADC_Init(); Menu_Init(); while (1) { if (dht11_tick || key_tick || sensor_tick || lcd_tick) { if (dht11_tick) { dht11_tick 0; DHT11_Poll(); // 非阻塞方式启动读取 } if (sensor_tick) { sensor_tick 0; Adc_Poll(); // 采集烟雾和光照 } if (key_tick) { key_tick 0; Key_Poll(); // 20ms扫描一次按键 } if (lcd_tick) { lcd_tick 0; Lcd_Poll(); // 周期性刷新显示 } Menu_Process(); // 状态机处理用户界面逻辑 Alert_Process(); // 报警逻辑判断 } } }这里有几个读者注意到的妙点我在系统心跳里分别设置了DHT11每2秒采样一次不是500msDHT11数据手册要求的读取间隔本来就大于1秒、按键每20毫秒检测一次、ADC每200毫秒采样一次取平均、LCD每500毫秒刷新一次显示。每个任务按照自己的节奏运行互不阻塞。DHT11如果因为时序原因卡了最多就是几毫秒的毛刺不会让整个系统崩溃。定时器初始化代码里最核心的就是计算预分频系数和重装载值static void TIM2_Init(void) { TIM_HandleTypeDef htim2; __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; // 72MHz/72 1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 1000 - 1; // 1MHz/1000 1kHz中断 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim2); HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); HAL_TIM_Base_Start_IT(htim2); }72MHz除以72等于1MHz也就是计数器每微秒计一次数计数器从0计到999后溢出触发中断就是每1000微秒也就是1毫秒触发一次。这个计算逻辑建议背下来往后做任何STM32定时器项目都绕不开。3.2 状态机支撑的“菜单”按键设置阈值就是这样实现的按键设置报警阈值听起来简单——按一下加一长按快加就行。但如果不规划状态机代码写起来非常乱你如何在“显示温度”和“设置湿度上限”之间切换如何在“设置温度上限”状态下让按温度加键只加温度不加别的我把系统的界面和逻辑分成了几个状态typedef enum { STATE_MAIN_DISPLAY 0, // 主界面轮流显示温湿度/烟雾 STATE_SET_TEMP_HIGH, // 设置温度上限 STATE_SET_HUMI_HIGH, // 设置湿度上限 STATE_SET_SMOKE_HIGH, // 设置烟雾报警阈值 } MenuState_t;在Menu_Process()里判断当前状态再根据按键事件切换到对应处理函数。比如在“设置温度上限”状态下每按一次UP键温度阈值加1℃并立即存储到Flash按DOWN键减1℃按OK键退出当前设置并回到主显示界面。这个架构有个直观的好处——以后想加一个“温度下限”报警只需要增加一个枚举成员再在菜单循环里补一个状态转移分支不需要惊天动地改整个逻辑。菜单里最容易被忽略的是存储在Flash里的阈值参数掉电了也要保留上次设置。STM32内部Flash的最后一个扇区0x0800FC00之后正好可以放参数。我写了一个Erase_Write_Flash()函数来操作内部Flash——写入前要先擦除扇区因为Flash只能把1写成0而擦除能让所有位变回1。void Flash_SaveParams(void) { uint32_t addr 0x0800FC00; HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress addr; erase.NbPages 1; uint32_t PageError 0; HAL_FLASHEx_Erase(erase, PageError); // 按顺序写入校准后的阈值参数 uint32_t temp (uint32_t)g_envParams.tempHigh; HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, temp); uint32_t humi (uint32_t)g_envParams.humiHigh; HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr 4, humi); uint32_t smoke (uint32_t)g_envParams.smokeHigh; HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr 8, smoke); HAL_FLASH_Lock(); }3.3 串口调试写代码的时候这条“生命线”至关重要这套工程我把USART1通过PA9/PA10引出来了板上留了一个4针的调试排针。系统运行过程中的温湿度值、ADC原始值、状态机跳到哪个状态全部可以打上串口用串口助手查看。建议你调试实物时先别急着看LCD打开串口调试助手每500ms打印一次当前采集的数据。哪个传感器值不对一眼就能定位不用反复猜。void Debug_Print(void) { printf(Temp:%d.%d Humi:%d.%d SmokeADC:%d SmokePct:%d\r\n, (int)g_dht11.temp_int, (int)g_dht11.temp_dec, (int)g_dht11.humi_int, (int)g_dht11.humi_dec, g_mq2.adc_value, g_mq2.percent); }4. DHT11时序代码的底层细节就是那些网上讲不清楚的位数DHT11是这套系统里唯一一个需要你手写时序的地方。它的通信协议是单总线总共只有一根数据线同时做供电外的双向通信。主机先拉低总线18ms以上发起开始信号然后释放总线让上拉电阻把总线拉高。接下来DHT11响应先拉低80μs再拉高80μs然后开始传输40bit数据。40bit数据包含8bit湿度整数部分、8bit湿度小数部分、8bit温度整数部分、8bit温度小数部分、8bit校验和。前四组数据加起来如果等于校验和这帧数据才算有效。数据位本身以不同的高电平持续时间区分0和154μs高电平是0而124μs高电平是1。读数据的核心代码就是精确测量引脚保持高电平的时间。实际代码我使用HAL库的定时器延时函数完成时序转换但重点在于GPIO模式的切换——读取DHT11数据时引脚要设为输入模式发送起始信号时要切到输出模式不能一直保持输出。void DHT11_Start(void) { // 把PA0设置为输出模式 GPIO_InitTypeDef gpio; gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); delay_us(20000); // 拉低至少18ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 等10-40us后切输入模式 delay_us(30); gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_NOPULL; // 外部已有4.7k上拉 HAL_GPIO_Init(GPIOA, gpio); } uint8_t DHT11_ReadByte(void) { uint8_t value 0; for (int i 0; i 8; i) { while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET); // 等待低电平结束 delay_us(50); // 高电平后延时50us if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { value | (uint8_t)(0x80 i); // 高电平持续超过50us说明是1 // 剩余的约40us高电平要等信号自己结束 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET); } } return value; }读取位数据的逻辑核心是如果数据位是0高电平只有约54μs你延时50μs后再读引脚此时高电平已经结束读到的应该是低电平如果数据位是1高电平约124μs延时50μs后依然读得到高电平。判断引脚状态的那个瞬间就把0和1区分开了。这个50μs的延时参数非常关键。设短了会把0误判成1还没到判断时刻低电平已经退场不算主要会错位判断设长了会把1误判成0高电平已经把124μs走完了。我实测下来在所有STM32F103型号上用delay_us(50)都稳定但如果你换成别的型号单片机需要微调这个参数。工程实现时我做了个HAL_GetTick()超时保护——读取循环里如果超过200ms还没等到低电平开始的那一位直接判定传感器无响应并返回错误码这样DHT11坏了或者线松了不会卡死主程序。这个超时机制建议你不要删掉。DHT11每次读取间隔要大于1秒因为传感器内部的测温频率实际只有约0.5Hz。工程里我把DHT11的采样周期设成2秒保证每次读取前总线处于空闲状态。如果你读出来的数据一直是0或者湿度非常离谱优先检查上拉电阻焊接和读时序延时参数这两块扛了七成故障原因。5. 完整ADC采集链路从模拟电压到“读得懂的百分比”烟雾和光照的采集都是走ADC。STM32F103的ADC是12位的也就是采样的数字范围是0到4095。直接显示这个原始值用户根本看不懂我写了一层换算逻辑把它变成“0到100”的百分比值。ADC初始化按常规来但有几个细节容易出错一是ADC的采样时间要设置得长一些因为传感器输出的是高阻信号采样时间太短电容充不满会导致读数偏小二是连续采样多次取平均我一次采样16次去掉最大最小值剩下的取平均抖动最少。校准思路是这样的烟雾传感器在清洁空气中的输出值应该是最低的记作baseline。实测我发现MQ-2在不通电预热10分钟后输出约0.5V对应ADC原始值约620。随着烟雾浓度上升输出电压接近2V以上ADC值超过1800。我用动态范围的方式做归一化#define SMOKE_BASELINE (uint16_t)600 #define SMOKE_MAX_ADC (uint16_t)2600 uint8_t Smoke_GetPercent(void) { uint16_t adc Get_AdcAverage(); if (adc SMOKE_BASELINE) return 0; if (adc SMOKE_MAX_ADC) return 100; return (uint8_t)(((uint32_t)(adc - SMOKE_BASELINE) * 100) / (SMOKE_MAX_ADC - SMOKE_BASELINE)); }百分比阈值默认设在40也就是说ADC值超过1400就开始报警。这样“0到100百分比”直观易懂不会看到一长串ADC原始值发怵。工程里报警判断用的是这个百分数而不是ADC原始值——即使换了一块传感器只要重新标定baseline和max整套报警逻辑不需要动。电源电压波动对ADC的影响不能忽略。AMS1117的3.3V输出虽然稳压了但如果电池供电会有压降。我在PCB上把VDDA通过一个磁珠和一个1μF电容单独隔离尽可能让ADC基准电压稳定。[注意]如果你想追求更加精准的绝对电压值可以增加一个基准源芯片如REF3033但家用监测场景真没必要。6. 用Proteus把整套逻辑先跑通能验证什么、不能验证什么这套工程附带的Proteus仿真文件是基于Proteus 8.9及以上版本创建的。仿真文件里选用的STM32F103C8T6模型、DHT11模型、LM016L液晶模型这几个模型在Proteus库里全部内置不需要额外装元件库。打开就能跑前提是你的Proteus装了STM32相关库并配置了芯片的固件文件。仿真工程里我在RCC配置中把晶振频率设成8MHz让代码里SystemClock_Config初始化后的实际主频是72MHz。这里几个常见的仿真问题说一下仿真里双击STM32芯片必须把Program File指向编译出来的hex文件否则芯片不会跑任何代码。KEIL里要勾选Output选项卡的Create HEX File才能生成hex。DHT11模型双击可以设置温度和湿度值你用鼠标拖动滑条就能模拟环境变化这个很方便。MQ-2在Proteus里没有现成模型我是用可调电阻连接到ADC引脚模拟烟雾浓度。旋转电位器改变电压看ADC采集百分比是否跟着变。这在逻辑验证层面已足够。LCD1602用LM016L模型替代这个模型四线模式兼容性很好总线上注意LCD的VO对比度引脚接一个电位器到地。仿真文件能验证代码逻辑层面的问题按键状态机流转是否正确、菜单字段有没有写反、LCD显示刷新有没有乱码、报警阈值是否正常触发这些在改实物之前就能定位掉。但它验证不了真实世界的问题DHT11真实时序的微妙偏差、MQ-2传感器预热的漫长等待、5V电源线压降、LCD1602的对比度调节。这些必须上实物才能调清楚。如果你手头还没有实物完全可以通过仿真先把这套系统的代码逻辑全部过一遍。我强烈建议你把仿真当作第一道测试关卡代码先过仿真再烧实物效率高很多。等代码逻辑验证熟了再开始焊接PCB到那时出问题的只会是硬件层面而不会是状态机逻辑的Bug。7. 实物调试的“血泪清单”照着这个步骤三天内搞定我把自己调试实物时遇到的坑整理成了一份清单。按这个顺序排查你基本不会出现坐在那满头问号的情况。7.1 电源上电前先测短路第一次上电是最紧张的。我用万用表蜂鸣档直接测3.3V和GND之间的阻值如果接近0说明有短路立刻断电检查。正常情况应该看到几百欧到上千欧的阻值板上有电阻电容充电过程确认无短路后再上USB供电。上电后手摸一下AMS1117、STM32芯片表面温度微温正常烫手就有问题。7.2 供电顺序先点亮主控再挂传感器第一次调试别把所有传感器都焊到板上我习惯第一步只焊最小系统加调试串口烧一个简单的LED闪烁程序确认主控能跑第二步焊LCD1602点亮显示内容第三步焊DHT11第四步再焊MQ-2。每次增加一个模块出问题了排查范围就小一轮。如果一次全焊上去不工作你真的很难判断是哪颗电容虚焊还是哪个引脚连锡。7.3 LCD显示乱码的常见原因LCD1602用四线模式时数据位D4-D7要接在同一个GPIO端口的连续引脚上工程里用了PB0-PB3RS和EN也不能乱接。如果屏幕显示整行方块大概率初始化时序不对或者对比度没调好。开发板上LCD的VO脚很多直连电位器我板子上也放了这电位器的位置。调整电位器让屏幕出现“淡淡的黑影”这个状态就是对比度刚好的状态比肉眼可见字符更重要。7.4 DHT11数据异常的排查路径如果串口打印出来的湿度是255或者温度是不正常的负值先用逻辑分析仪或者示波器抓PA0引脚波形看看起始信号后传感器有没有拉低80μs响应。没有响应就检查4.7kΩ上拉电阻是否焊接正常有响应但数据全是FF检查50μs的延时参数是否跑偏有些编译器优化等级设置不一样会改变延时循环开启优化后延时变短参数需要重新标定。我的工程代码里用了HAL库的微秒延时函数而不是裸循环就是为了避免编译器优化把延时吃掉。7.5 蜂鸣器经常不响或者异常响蜂鸣器驱动电路如果三极管型号不对或者基极电阻虚焊会表现为有源蜂鸣器直接静音。用万用表测量三极管基极-发射极电压如果烧程序给高电平后电压接近0.7V说明驱动正常问题大概率在蜂鸣器本身供电。还有一种容易被忽略的情况PA8默认复用TIM1_CH1如果你只是要数字高低电平驱动蜂鸣器记得在GPIO初始化时把引脚配置成推挽输出而不是复用功能否则初始化后引脚被定时器外设占用普通HAL_GPIO_WritePin控制无效。7.6 阈值写好Flash但还是乱恢复默认值这大概率不是因为Flash写入失败而是你每次烧录程序后做了全片擦除把Flash里存的阈值参数一并擦掉了。建议调试阶段先把“读取Flash失败就写入默认值”这段逻辑加上保证第一次烧录后能正常使用后期每次改代码重新烧录不上电时不要全片擦除就不影响参数。8. 代码架构的一个精妙点如何做到“传感器坏了系统不死”这是整套代码最值得揣摩的地方。DHT11单总线协议有个天然的缺点——如果传感器没有物理响应读时序的代码可能卡在while循环里出不来。我在DHT11读取函数的每一次循环等待上都加了超时判断uint32_t timeout HAL_GetTick(); while (HAL_GPIO_ReadPin(...) GPIO_PIN_RESET) { if (HAL_GetTick() - timeout 100) { DHT11_Reset(); return NULL_VALUE; } }在Alert_Process报警逻辑里判断所有传感器数据当前是否有效如果DHT11连续3次返回NULL就只在LCD上报“传感器未连接”但不会触发报警鸣叫。这样避免了一个灵异场景厨房燃气泄漏的同时DHT11恰好坏了系统如果卡死在DHT11读时序烟雾报警就永远没机会执行了。为了让每个模块的耦合度降到最低我用结构体把所有传感器状态封装起来typedef struct { int8_t temp_int; uint8_t temp_dec; uint8_t humi_int; uint8_t humi_dec; uint8_t is_valid; } DHT11_Data_t; typedef struct { uint16_t adc_value; uint8_t percent; uint8_t is_alert; } MQ2_Data_t; typedef struct { int8_t tempHigh; uint8_t humiHigh; uint8_t smokeHigh; } EnvParams_t;代码的每个模块只管自己的数据更新。报警逻辑只读这些结构体和阈值不关心数据是来自DHT11还是后续你要换成SHT30。以后想升级传感器只需要封装一个同样是输出DHT11_Data_t的新驱动函数底层实现随便改上层逻辑一行都不用动。我实际在后续版本里把DHT11换成DHT22就只改了驱动层——这验证了这种分层设计的价值。9. 整套开源文件的组织方式拿到手之后从哪里看起开源包里的文件分成五个目录01_Datasheet/所有关键元器件的数据手册包括STM32F103C8T6、DHT11、MQ-2、LCD1602、AMS1117的PDF。02_Schematic/立创EDA格式的原理图工程和Gerber文件直接就能投板。03_Firmware/完整Keil MDK工程打开就能编译。如果你用STM32CubeIDECore/Src下的源码也可以直接粘进新工程依赖的HAL库文件在官方固件包里都有。04_Simulation/Proteus仿真工程双击就能跑。05_Doc/这份项目的完整说明文档和调试手册。拿到资料后的第一步建议不是去开Keil疯狂看代码而是打开05_Doc里的设计文档从系统框图开始理解模块划分然后对照原理图看硬件设计最后才看代码。如果直接陷进某段DHT11时序代码里很容易迷失在底层细节里忽略整棵树的架构。我自己做嵌入式这些年最大的体会是一个“完整”的开源项目难的不是某个技术点而是“传感器采集、用户交互、数据展示、报警决策、参数存储”这五个环节都能可靠地协调工作。单看DHT11时序、单看ADC采样、单看菜单状态机网上都有大量单独教程但把它们像齿轮一样啮合到一起才是做真实产品的原型阶段的日常。本项目的代码你完全可以改造成自家的应用。比如把LCD1602换成OLED屏把蜂鸣器换成继电器控制排风扇把烟雾报警逻辑改成“浓度超过阈值自动打开窗户电机”——那已经不是在做课程作业而是在做产品原型了。过程中如果卡在某个环节欢迎给我留言交流。
返回列表