
一个偶然的机会我接了个充电桩运维改造的项目发现充电桩厂家对“环境安全监测”这块普遍做得比较粗——很多老桩只装了过流保护温度、烟雾、可燃气体这些隐患全靠运维巡检靠肉眼判断。后来我把这套基于STM32的环境安全监测方案整理成开源项目配套代码、原理图和Proteus仿真放到社区后不少同行问细节。今天干脆把整个项目从设计思路到代码实现再到仿真调试完整梳理一遍代码和原理图都能在开源仓库里直接拿到。适合做充电桩、配电柜、储能柜环境监测的朋友直接抄作业也适合刚入门STM32的开发者拿来当一个完整项目练手。1. 项目定位与整体设计思路1.1 充电桩环境监测到底在监测什么大多数人第一反应是“充电桩不是有BMS和充电保护吗还需要额外监测什么环境”实际上充电桩内部环境远比想象中复杂。我拆过好几个品牌的交流桩和直流桩常见的隐患有这么几类高温大功率充电时充电模块、线缆接头、继电器触点温升明显尤其是接线端子接触不良时局部温度能到70到80度这在封闭的桩体里非常危险。凝露与高湿充电桩常年户外使用早晚温差大密封圈老化后水汽进入轻则绝缘下降重则短路打火。烟雾与可燃气体线缆绝缘层过热冒烟、外部火源接近、可燃气体泄漏都是充电桩起火的重要诱因。异常倾倒与破坏有些桩安装在路边被车辆碰撞后虽然还能工作但内部结构可能已经受损。这些隐患用充电桩自身的充电管理芯片是“看不到”的必须靠独立的环境监测子系统来兜底。所以这套系统的核心职责非常清晰实时采集充电桩内部环境的温度、湿度、烟雾可燃气体浓度配合门禁和倾倒检测一旦发现异常立即本地声光报警并通过继电器切断充电回路或启动排风扇。1.2 系统架构与技术选型分析整体架构采用“单MCU 多传感器 本地决策”的经典工控思路不做云平台不引入复杂RTOS所有逻辑都跑在裸机状态机上。为什么这么选首先考虑成本。充电桩本来就是低毛利硬件环境监测作为附加功能如果主控直接上一个带Wi-Fi和以太网的高端芯片单板成本至少多出一二十元很多厂商不会买单。STM32F103C8T6虽然发布很多年了但72MHz主频、64KB Flash、20KB RAM的配置跑这类传感器采集和逻辑判断绰绰有余零售价也就十块出头量大还能更低。其次考虑可靠性。环境安全监测属于“常年无感、出事救命”的功能越简单越不容易坏。裸机状态机没有任务调度开销也不会因为某个传感器任务卡死导致整个系统瘫痪。我用定时器做1秒周期的软件调度每个传感器采集函数控制在几十毫秒内完成实时性完全够用。外围器件的选型也遵循“够用就好”的原则温湿度用DHT11烟雾可燃气体用MQ-2报警输出用有源蜂鸣器和继电器显示用0.96寸I2C OLED。这套组合成本控制在三十元以内即便加上电源模块和PCB打样费用整个原型板成本不到五十元。提示如果你手头已经有STM32最小系统板比如常见的蓝板完全可以跳过PCB设计用杜邦线直接搭传感器验证逻辑后再画集成板开发周期能压缩一半以上。2. 硬件设计与原理图拆解2.1 核心主控与最小系统设计原理图的核心自然是STM32F103C8T6的最小系统。这部分很多人照着参考设计画就行但我还是想强调几个容易踩坑的细节。晶振电路8MHz无源晶振配合22pF负载电容这个没太多争议。关键是在晶振下方不要铺地否则会影响起振稳定性。PCB上晶振尽量靠近MCU的OSC_IN和OSC_OUT引脚走线不要过长。另外别忘了给晶振两端并联一个1MΩ反馈电阻有些参考设计会省掉实际在低电压或干扰环境下容易出现起振失败。复位电路NRST引脚接一个10kΩ上拉电阻到3.3V再并联一个0.1μF电容到地组成经典的RC复位电路。这个组合能保证上电时低电平脉冲足够宽MCU可靠复位。如果现场干扰较强我建议在复位引脚再加一个100nF的滤波电容效果更稳定。BOOT配置BOOT0和BOOT1都通过10kΩ电阻下拉到地从主Flash启动。这里要特别提醒BOOT0引脚在STM32F103上不能悬空必须明确接地或接固定电平否则量产时偶尔会出现上电不进用户程序的问题。我调试时曾在BOOT0上只留了一个焊盘没连结果芯片一直在ISP模式下空转HEX怎么烧录都不跑排查了很久才发现是BOOT0悬空导致。供电部分如果系统直接从充电桩内部的12V或5V取电板上建议加一级降压电路稳压到5V再通过AMS1117-3.3转3.3V给MCU和传感器。AMS1117的输入输出各预留一个10μF电解电容和一个0.1μF陶瓷电容布局上要靠近芯片本体防止纹波过大影响ADC采样稳定性。调试接口SWD只要4根线SWDIO、SWCLK、GND、3.3V比JTAG省IO。BOOT0下拉后理论上用ST-Link就能直接读芯片。我在原理图里还会留一个可选的NRST复位引脚方便在目标板死机时用ST-Link Utility里的connect under reset模式恢复。2.2 传感器与信号链路设计这一节是原理图的重点因为不同传感器对电路的要求差异很大。DHT11温湿度传感器是单总线协议DATA引脚需要外接一个4.7kΩ上拉电阻到3.3V。如果你像我一样把数据线拉得比较长比如超过20cm建议上拉电阻改为2.2kΩ并且靠近MCU引脚端再接一个100Ω串联电阻能明显提高抗干扰能力。更讲究一点的做法是在DHT11的VCC和GND之间加一个100nF去耦电容这个电容能让传感器在读取瞬间的电流波动不影响总线信号。MQ-2烟雾可燃气体传感器要注意的点比较多。它内部有一个加热丝工作电流在150mA左右供电不能用MCU的3.3V引脚直接带最好单独用5V供电且电源走线要粗。MQ-2的AO引脚输出的是一个0到5V的模拟电压对应气体浓度变化这个电压不能直接进STM32的ADC引脚因为STM32的ADC输入范围是0到3.3V超过会烧坏引脚。我常用的方案是10kΩ和4.7kΩ电阻分压分压比大约是0.32把5V上限映射到1.6V左右留出安全余量。也可以用可调电位器做手动校准在原理图上留一个可调位置。继电器驱动电路比较关键MCU引脚驱动能力有限不能直接驱动继电器线圈。我用的方案是NPN三极管S8050加续流二极管1N4148。三极管基极通过1kΩ电阻接PA2发射极接地集电极接继电器线圈线圈另一端接5V同时反向并联1N4148。没有这个续流二极管的话继电器断开瞬间线圈产生的反向电动势很容易打坏三极管甚至MCU引脚这是新手最容易忽略的坑。有源蜂鸣器接法类似也是三极管驱动。注意有源蜂鸣器是直流驱动给它高电平就响无源蜂鸣器需要PWM驱动这里选有源就是为了简化逻辑。OLED显示屏走I2CSDA接PB7SCL接PB6加上拉电阻4.7kΩ。I2C地址默认是0x78不同厂商的OLED模块地址可能不同代码里加个扫描函数确认一下最稳。2.3 电源与通信接口设计要点充电桩内部环境有强电、有逆变器电磁干扰比普通电子产品严重得多。电源设计上如果直接从充电桩的辅助电源取电输入端一定要加TVS管和共模电感。TVS管选SMBJ15A或类似型号放在电源入口最前端防止雷击浪涌。共模电感我常用几百微亨到几毫亨的规格主要滤除高频骚扰。通信接口方面这套系统比较“轻”不强制联网。如果想接入充电桩主控或后台建议预留一个UART串口PA9/PA10通过MAX3485转成RS485。RS485在工控和充电桩行业非常常用抗干扰能力强。MAX3485的A和B之间需要加120Ω终端电阻距离短的情况下加一个就够了。RO、DI、RE、DE这四个信号脚最好用TVS管做保护我用的是SMBJ6.0CA焊在连接器附近。如果你是做小批量样机验证不打算加RS485串口直接用CH340模块转USB调试也可以。但在原理图上保留串口和RS485的焊盘位置后续量产直接贴电容电阻和通信芯片就能升级改版成本为零。3. 核心代码实现与逻辑拆解3.1 代码工程结构与初始化流程整个工程用Keil MDK开发芯片型号选择STM32F103C8编译器用AC5或AC6都行。我习惯把代码按功能模块拆文件方便维护和复用。工程目录大致是这样的User/ main.c stm32f1xx_it.c BSP/ bsp_clock.c bsp_gpio.c bsp_adc.c bsp_tim.c bsp_i2c.c DEVICES/ dht11.c mq2.c oled.c relay.c buzzer.c key.c APP/ app_monitor.c app_alert.c DOC/main里做的事情很纯粹先初始化时钟和外设然后进入一个while(1)大循环配合SysTick定时器做任务调度。SysTick每1毫秒中断一次用一个全局计数变量累加主循环里根据计数值判断是否到达某个任务的执行周期。传感器采集任务我设置的周期是1秒OLED刷新周期是500毫秒继电器和蜂鸣器的状态刷新是200毫秒。这样分配的好处是重负载的传感器读取不会拖累报警响应——出异常时最慢200毫秒内继电器就能动作对充电桩这类场景完全够用。int main(void) { SystemClock_Init(); GPIO_Init(); ADC_Init(); TIM_Init(); I2C_Init(); OLED_Init(); DHT11_Init(); MQ2_Init(); OLED_Clear(); OLED_ShowString(0, 0, Charger Monitor); while (1) { if (g_1s_flag) { g_1s_flag 0; Monitor_ReadSensors(); } if (g_500ms_flag) { g_500ms_flag 0; OLED_ShowData(); } if (g_200ms_flag) { g_200ms_flag 0; Monitor_CheckThreshold(); Alert_Update(); } } }代码逻辑其实就三层采集、显示、判断。判断逻辑每200毫秒跑一次读取最近一次的传感器结果和阈值做比较决定继电器是否需要动作。这种时间片轮询的方式对实时性要求不高的环境监测类项目是最稳妥的也好调试。3.2 传感器数据采集与处理逻辑DHT11的时序是这类传感器里最麻烦的一部分。单总线协议要求主机先输出至少18ms的低电平启动信号然后释放总线等待DHT11应答接着按40比特的数据流读取。每一位的0和1是靠高电平持续时间区分的26到28微秒表示070微秒表示1。我直接说踩坑经验。很多人第一次调DHT11读不到数据绝大多数原因是延时不够精确。在72MHz主频下用软件延时循环很容易被编译器优化掉所以延时函数必须用volatile变量或者用SysTick来做微秒级延时。还有一个坑是DHT11读取完毕后不能马上再次读取建议至少间隔1秒否则传感器不响应。我在代码里专门做了读取间隔保护就算主循环误调用也能保证每次读取间隔不小于800毫秒。读取到的40位数据格式是8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。校验和等于前四个字节之和的低8位如果校验不通过丢弃本次数据保留上一次的有效值。这个校验逻辑千万不能省单总线协议没有帧同步机制误帧率其实不低。MQ-2的ADC读取相对简单。用ADC1的通道1采集PA1引脚的模拟电压。需要注意MQ-2上电后有预热期加热丝需要几十秒才能稳定这段时间输出会漂移代码里可以加一个上电初始化后的预热延迟或者干脆在显示界面上标记“预热中”。更科学的做法是连续采集10次ADC值去掉最大值和最小值后取平均能有效滤掉抖动。ADC的配置我直接用STM32标准库ADC1工作在连续转换模式采样时间为239.5个周期转换结束后DMA搬运数据到内存。DMA的好处是主循环不用等待转换完成数据更新由硬件自动完成。void MQ2_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; DMA_InitTypeDef dma; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); gpio.GPIO_Pin GPIO_Pin_1; gpio.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, gpio); dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)g_adc_buf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize 10; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, dma); DMA_Cmd(DMA1_Channel1, ENABLE); adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode ENABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }注意这段代码是标准的采集初始化实际工程里别忘了把DMA1的时钟也打开否则DMA寄存器配置后会写不进去。这类问题用调试器看寄存器状态很容易发现但在裸板环境下会表现为ADC值永远不变容易让人误以为是传感器坏了。3.3 安全阈值判断与报警联动机制阈值判断是整个系统的决策引擎。我把它单独放到app_monitor.c里维护一个全局结构体里面保存当前温度、湿度、气体浓度和各个报警标志。判断逻辑采用分级报警不搞一刀切。温度默认阈值我设置为55摄氏度湿度70%气体浓度按ADC值超3000作为预警。实际项目里你要根据安装环境的背景值标定比如充电桩在南方夏季午后暴晒桩体内温度可能本来就到45度这时阈值设在50度就更合理。#define TEMP_ALARM_HIGH 55 #define HUMI_ALARM_HIGH 70 #define GAS_ALARM_VALUE 3000 typedef struct { uint8_t temp_int; uint8_t humi_int; uint16_t gas_adc; uint8_t temp_alarm; uint8_t humi_alarm; uint8_t gas_alarm; uint8_t global_alarm; } Monitor_Data_t;报警联动机制分两级一级预警温度超过设定值但未到危险值或湿度偏高或气体浓度轻微超标。此时蜂鸣器以慢节奏鸣叫提示OLED显示告警信息但继电器不动作给运维人员留出处理窗口。二级紧急温度超过65度或气体浓度严重超标或烟雾报警触发。此时蜂鸣器连续急促鸣叫继电器吸合切断充电回路同时如果系统带排风扇启动排风。切断后需要人工复位按键才能恢复避免反复上电对充电模块造成二次伤害。这段逻辑用状态机实现各报警等级之间支持降级。比如气体浓度从严重超标回落到正常范围超过30秒后可以自动解除紧急状态但继电器恢复仍需要人工确认防止误恢复。为什么要这样设计因为充电桩一旦因环境异常被切断如果没有人工确认就自动恢复万一是线缆燃烧这类真实故障复电会带来更大的安全风险。按键设置部分我用两个按键K1用于切换设置项K2用于调节阈值。长按K1进入设置模式短按切换温度、湿度、气体阈值项长按K2保存退出。OLED在设置模式下显示当前阈值方便现场调试。这块逻辑虽然不复杂但写的时候要注意按键消抖我采用20毫秒软件消抖加上状态检测来处理长按和短按的区别。4. 仿真验证与调试实录4.1 Proteus仿真搭建过程配套的Proteus仿真工程主要在开发阶段使用因为真实充电桩环境不是随时都能复现高温、烟雾异常。Proteus 8.9以上版本对STM32F103C8T6的支持已经很完善了可以直接画原理图挂载HEX文件仿真。仿真工程搭建的步骤我整理一下。先在元器件库里选STM32F103C8T6再选DHT11、MQ-2、LM016L用液晶屏代替OLED因为Proteus的I2C OLED模型不好用、BUTTON、RESISTOR、LED、BUZZER、RELAY等。需要说明的是Proteus自带库里并没有真实的DHT11模型我使用的是社区做的DHT11模型搜索“DHT11”能找到。MQ-2同样没有原厂模型通常用可调电位器替代模拟量输出具体做法是拖一个POT-HG型号的电位器通过调节滑动端电压来模拟气体浓度变化。接完线后双击STM32芯片在Program File里加载工程输出的.hex文件设置晶振频率为8MHz点击运行。如果一切正常液晶屏会显示温湿度和气体浓度调整DHT11模型参数或电位器观察继电器和蜂鸣器是否按阈值动作。这套仿真最大的价值是调试报警联动逻辑。我在开发中就用它在电脑上反复模拟温度超限、气体超标的场景把状态机的bug基本上全部过滤掉了省下大量反复烧录真机的次数。仿真做得越充分真机调试时遇到的就只剩硬件层面的问题。4.2 常见仿真问题与排查仿真阶段遇到最多的问题就是“仿真运行了但STM32不执行代码”。绝大多数原因是没有正确加载HEX文件或者芯片型号选错。注意STM32F103C8和STM32F103C8T6在Proteus里是同一个模型但加载HEX时路径不能有中文否则编译器或仿真器会找不到文件。另一个高发问题是DHT11时序在仿真的表现和真实芯片差异较大。Proteus的模型对时序的模拟有一定容差在真实硬件上能跑的代码可能在仿真里读不到数据或反之。我试过几次把DHT11读取间隔从1秒调整到1.2秒仿真就稳定了但在真机上两种间隔都能正常工作。遇到这种问题不要纠结仿真主要验证逻辑流程传感器数值是否精确不是重点。还有一种情况是继电器模型吸合后LED不亮。检查一下继电器线圈的驱动电压是否匹配Proteus的继电器模型默认有不同的线圈电压参数选5V的模型就不会有这个问题。还有续流二极管在仿真里可以不画但真机上必须画这个在仿真中容易忽略掉。为了方便快速排查我把常见问题整理成了下面的表现象原因解决办法仿真运行后STM32不执行未加载HEX或路径含中文芯片属性中加载.hex路径改英文DHT11读数不稳定或为0仿真模型时序容差差异把读取间隔调整到1.2秒以上继电器不动作线圈电压模型不匹配选择5V线圈的继电器模型液晶屏无显示引脚连接错误或对比度未调检查LM016L各引脚调整对比度电位器ADC值一直不变MQ-2替代模型输出电压固定调节电位器滑动端位置注意仿真通过不等于硬件一定没问题。仿真最大的盲区是电源完整性、电磁干扰和传感器真实时序这些只能在真实板子上验证。5. 从开源项目到实际部署的进阶思考5.1 从原型到量产的改动点如果你在开源项目基础上要做成产品有四个地方需要重点考虑。第一MCU选型。如果你量产后发现成本压力大可以考虑把MCU换成更低成本的STM32F030系列或者国产替代型号。因为这套系统的算法和逻辑并不依赖F103的特殊外设把ADC、定时器、I2C这几个外设驱动平迁过去工作量不大。第二传感器选型。DHT11精度在±2℃和±5%RH以内对充电桩环境监测够了但如果客户要求更高的测量精度建议换SHT30精度能到±0.3℃和±2%RH。SHT30是标准I2C接口代码改动量也不大把它封装成和DHT11一样的读取接口即可。第三PCB布局。原型板上导线飞线无所谓量产板一定要把模拟地和数字地分开电源走线加粗传感器尽量远离继电器和大电流线缆。我见过一个板子因为MQ-2离继电器太近继电器吸合瞬间的磁场在ADC上灌入尖峰气体浓度值莫名跳变重新布局后才解决。第四阈值参数化。量产时建议把温度、湿度、气体阈值做成参数存储到Flash里默认用出厂值现场用上位机或按键配置。充电桩不同地区的环境差异很大固定阈值会让误报率居高不下这一点在项目中后期尤其重要。5.2 下一步扩展方向建议基础版做完后扩展方向按优先级排列的话依次是这几个。通信上云是很多人第一反应要做的。如果只是单个桩RS485本地监控就够用如果想做集群管理可以加一个4G模块或走充电桩已有的以太网口把监测数据上报到云端平台。STM32F103C8的RAM只有20KB跑复杂协议栈比较吃力建议不要直接在F103上跑MQTT而是通过串口接一个4G透传模块由通信模块处理协议封装MCU只负责数据打包。第二个方向是增加漏电流检测。充电桩的漏电流保护我见过不少是装在主回路里的但环境监测模块如果本身能检测充电桩外壳漏电安全性会进一步提升。用零序电流互感器加运放放大再进ADC采样算法上算有效值或峰值这套扩展电路也不复杂。第三个方向是维护预测。记录传感器数据和报警历史到外部Flash运维人员到场后通过蓝牙或USB读取分析桩体的健康状况。比如温度屡次接近阈值说明通风系统可能老化提前安排维护。这块很实用但在基础版本里先不做因为会增加很多存储和交互逻辑。我个人在实际部署中还发现一个有趣的问题。充电桩在北方冬天清晨环境温度可能到零下MQ-2传感器的加热丝在低温下启动电流更大电源设计如果余量不足会出现启动失败。这个坑我在一个北方项目里踩到过后来在电源入口加了一个更大容量的电容才解决。如果你在寒区做部署建议提前做低温启动测试。这套系统做完之后给我的最大体会是环境监测类项目本身技术难度不高难的是对应用场景的理解和对细节的把控。比如阈值到底设置多少传感器放在桩体哪个位置才能采到有代表性的温度这些在代码和原理图里都看不到但恰恰是项目能否真正落地、是否会被客户认可的关键。开源出去的代码是一个起点真正值钱的是我们把这些现场问题一点点踩平的过程。如果你也在做类似的环境监测项目建议先用这套原型跑起来把数据采出来再根据实际环境去调整阈值和布局这样踩坑的成本会低很多。