如果你最近去过地下车库或者商业综合体停车场,大概率会看到一排排新装的充电桩。恰恰是这种没人值守、相对封闭、又潮湿的环境,最容易积累安全风险:充电枪线缆过热、电池热失控前冒出的烟、暴雨过后从排水口倒灌进来的积水,每一样都可能从小问题变成大事故。我前后用STM32F103C8T6做了两版充电桩环境安全监测系统,现在整理成开源项目,配全套代码、原理图和Proteus仿真文件。整套系统做的事情很简单:实时采集充电桩周边的温度、湿度和烟雾浓度,分级给出预警和报警,同时驱动蜂鸣器、LED和继电器——继电器可以联动排风扇,或者直接切断充电回路。适合三类人看:一是想学STM32、需要一个真实小项目的学生,二是想给自家充电桩加一道保险的DIY玩家,三是做充电桩配套设备、需要快速出一版参考设计的嵌入式工程师。
1. 整体设计思路:为什么充电桩要配环境监测
1.1 充电桩不是“插上就能充”那么简单
很多人觉得充电桩就是一个大号充电器,里面是成熟的BMS和充电模块,不需要额外监测。实际去过现场的人不会这么想。公共充电桩多数装在户外雨棚、地下车库、路边配电箱旁边,环境差异极大。最常见的安全隐患有三个。
第一是线缆和枪头过热。电动车充电电流动辄几十安培,枪头里面的端子如果接触不良,接触电阻一上去,局部温度会迅速超过80℃,外壳都烫手。第二是热失控前的烟雾。锂电池热失控不是瞬间发生,早期一定会冒烟,如果烟雾能被提前探测到,哪怕只早一分钟,都有机会切断充电、疏散人员。第三是潮湿和积水。地下车库在梅雨季湿度经常到85%以上,配电箱和枪座容易结露,绝缘性能下降,如果车位地势再低一点,暴雨积水还可能直接淹没设备。
所以这套系统的核心思路不是去接管充电桩的充电逻辑,而是做“旁路哨兵”:独立供电、独立传感、独立报警,环境参数异常时给出声光告警,再通过继电器对外做一级联动。哪怕充电桩本身的保护设计失效,这套系统也能兜底。
1.2 监测参数怎么选、阈值怎么定
我在最初设计时考虑过很多传感器:火焰传感器、水浸传感器、振动传感器、CO传感器都有接触,但最终基础版本只保留三个参数:温度、湿度、烟雾浓度。
温度是充电桩过热最直接的体现。环境温度阈值建议预警45℃、报警60℃。要注意这个阈值是“环境温度”而不是设备表面温度,传感器要避免紧贴大功率发热器件,否则会频繁误报。
湿度主要防绝缘下降。预警80%RH、报警90%RH,这两个值参考了大多数低压电气设备的运行环境要求。湿度超了不一定立刻出故障,但是长期高湿会导致凝露、爬电、金属件锈蚀,所以超过90%并且持续一段时间就要报警。
烟雾浓度用MQ-2气体传感器,它同时对油烟、液化气、酒精和烟雾有响应,用来做“早期火灾”探测非常合适。报警阈值我建议用标定后的原始AD值,不要直接用百分比。因为MQ-2模块一致性一般,不同模块在同浓度下的输出电压可能有差异。后面软件部分我会详细讲标定流程。
1.3 为什么选STM32F103C8T6,而不是51、Arduino或者ESP32
老实说,这种监测系统用51单片机也能做,但体验会差很多;用Arduino开发最快,但不适合深入学习,也不适合后面接复杂协议栈。我做了一个简单的对比。
| 方案 | 价格 | 外设资源 | 调试手段 | 仿真支持 | 学习价值 |
|---|---|---|---|---|---|
| STC89C52 | 6元左右 | AD、定时器都有但资源紧张 | 串口/SWD困难 | Proteus支持好 | 偏基础 |
| Arduino UNO | 20-60元 | 库丰富、上手快 | 串口方便 | 一般 | 对硬件细节理解浅 |
| STM32F103C8T6 | 8-15元 | 2路12位ADC、3路USART、I2C/SPI/TIM齐全 | SWD+串口很成熟 | Proteus/Wokwi均支持 | 体系完整,适合入手 |
| ESP32 | 15-30元 | 外设多、带WiFi/蓝牙 | JTAG/USB/串口 | 部分在线支持 | 侧重联网应用 |
选择STM32F103C8T6最关键的原因是3.3V逻辑电平和丰富的ADC资源。OLED、DHT11都是3.3V器件,和STM32天然匹配。12位ADC读MQ-2模拟量,精度足够;4个定时器做延时和调度也绰绰有余。另外这块芯片的参考资料多到爆炸,不管是CubeMX配置还是HAL库问题,遇到坑基本都能搜到答案。
如果未来要加WiFi远程上报,方案上也留了扩展口,加一块ESP8266-01S就行,不会推翻整版设计。
2. 硬件与原理图拆解:从选型到画板的关键细节
2.1 系统框架与引脚分配
整套系统的数据流是这样的:DHT11通过单总线把温湿度送给STM32,MQ-2模块的AO引脚输出电压,经过分压和滤波后进入ADC,STM32在OLED上刷新显示,同时按状态机驱动LED、蜂鸣器和继电器,UART1把实时数据打印到调试串口。
引脚分配表格我直接放出来,开源原理图里的网络标号和代码里的宏定义是完全对应的:
| 功能模块 | 接口类型 | STM32引脚 | 说明 |
|---|---|---|---|
| DHT11温湿度 | 单总线 | PB12 | 数据线,板载4.7k上拉 |
| MQ-2烟雾AO | 模拟量 | PA1 | ADC1_IN1,分压后输入 |
| OLED SSD1306 | I2C | PB6=SCL、PB7=SDA | 地址0x3C或0x3D |
| 有源蜂鸣器 | GPIO输出 | PB0 | S8050三极管驱动 |
| 绿色LED | GPIO输出 | PB1 | 正常运行指示 |
| 黄色LED | GPIO输出 | PB10 | 预警状态指示 |
| 红色LED | GPIO输出 | PB11 | 报警状态指示 |
| 继电器控制 | GPIO输出 | PA8 | 低电平触发继电器模块 |
| 调试串口UART1 | USART | PA9=TX、PA10=RX | 115200或9600 |
| SWD仿真调试 | SWD | PA13=SWDIO、PA14=SWCLK | 4Pin插针引出 |
这里有个细节:PB6和PB7是I2C1的硬件引脚,用作I2C是标准用法;PB10和PB11是I2C2的引脚,被我拿来当普通GPIO驱动LED,完全没问题。DHT11放在PB12是为了避开UART引脚,这样以后如果要接ESP8266,UART2的PA2、PA3可以直接用,不会打架。
2.2 电源、传感器和驱动电路
电源部分我建议采用“5V总线+3.3V总线”的双轨结构。如果是从充电桩取电,220V进线先接一个隔离AC-DC模块,输出5V/3W,例如HLK-PM01这类模块,尺寸小、带隔离,安全性比直接线性降压高很多。5V给MQ-2的加热回路和继电器模块供电,再经过一个AMS1117-3.3给STM32、OLED、DHT11供电。
不要小看MQ-2的功耗,它的加热丝工作电流大约150mA,如果直接从一个AMS1117-3.3后面取电,会拖垮3.3V电压。所以MQ-2模块必须挂在5V总线上,这点很重要。
MQ-2模块的AO输出在5V供电下会接近0~5V,而STM32的ADC输入范围是0~3.3V,直接接会烧引脚。这一点很多人第一次都不注意。正确做法是加一个分压网络:AO出来接20k对地,再串联10k到PA1,这样最高大约3.3V;同时在PA1处并联一个104电容做低通滤波,滤掉传感器输出上的高频抖动。仿真阶段如果你的MQ-2模块AO就是0~3.3V输出,那这一段画不画分压都行,但实物一定不能省。
蜂鸣器我用的是5V有源蜂鸣器,通过S8050三极管驱动:PA8接1k电阻到三极管基极,发射极接地,集电极接蜂鸣器负极,蜂鸣器正极接5V。GPIO输出高电平,蜂鸣器响。LED都串330Ω限流电阻。继电器用一个1路5V低电平触发的光耦隔离模块,IN接PA8,继电器触点不要直接带220V负载,而是去控制交流接触器线圈,接触器再切断充电回路或启动排风扇,这样强弱电彻底隔离,安全上才说得过去。
2.3 画原理图时容易漏掉的细节
开源原理图我是用KiCad画的,如果你偏好嘉立创EDA也可以直接迁移,元件封装都不复杂。有几个细节是我打样之后才补上的,现在都写在原理图里。
BOOT0必须加10k下拉电阻。很多最小系统板把BOOT0直接接地也能跑,但自制PCB上悬空是不行的,容易受干扰导致启动模式异常。NRST上拉10k到3.3V,并对地并100nF。8MHz晶振两个负载电容用22pF。STM32的每个VDD引脚旁边都要放104去耦,最好再加一个10uF钽电容做储能,ADC采样瞬间电流很大,电源纹波会直接影响AD值。
另外建议把SWD调试口做成4Pin插针,VCC、SWDIO、SWCLK、GND四根线,平时接ST-LINK下载调试都靠它。如果空间允许,再引一个PA9/PA10的调试串口。有了串口,你在现场调阈值、看实时数据会省太多事。
3. 软件实现:STM32驱动、状态机与报警逻辑
3.1 工程搭建与代码结构
软件基于STM32CubeMX生成HAL库工程,用Keil MDK5编译。我推荐这么搭而不是用标准外设库,是因为HAL库的ADC、I2C、UART配置在CubeMX里勾选就能生成,省去大量初始化代码,而且网上基于HAL库的问题案例非常多。如果你习惯用VS Code加EIDE插件和arm-none-eabi-gcc编译,理论上一套代码也能编译通过,但本文默认按Keil流程走。
工程目录刻意做了分层,不要一气呵成写在main.c里:
ChargingPile_Monitor/ ├── Core/ # 启动文件、中断、时钟配置 ├── Drivers/ # STM32F1xx_HAL_Driver ├── BSP/ # 板级驱动 │ ├── bsp_dht11.c/h # DHT11单总线驱动 │ ├── bsp_mq2.c/h # MQ-2 ADC采集与滤波 │ ├── bsp_oled.c/h # SSD1306驱动 │ └── bsp_alarm.c/h # LED、蜂鸣器、继电器控制 ├── APP/ # 应用层 │ ├── app_monitor.c/h # 采样调度、报警状态机 │ └── app_ui.c/h # 屏幕页面刷新 └── MDK-ARM/这样分层的好处是:BSP层负责和寄存器、引脚打交道,APP层只管业务逻辑。以后换传感器或者换屏幕,只改BSP,不动APP;如果你拿去参加毕业设计,这个结构也符合软件工程的评审要求。
3.2 DHT11单总线驱动:时序是最容易翻车的地方
DHT11用的是单总线协议,一根线既做控制又做数据回传。时序上分四步:主机拉低至少18ms发起开始信号;拉高20~40us后释放;DHT11响应,把总线拉低80us再拉高80us;随后输出40位数据,高位在前。每一位数据都是50us低电平开头,随后高电平持续26~28us表示“0”,持续70us表示“1”。判断逻辑就是等50us低电平结束后,延时至位的中间再读电平。
上电延时和响应检测必须分开写。很多人在“等待响应”阶段直接死循环,一旦传感器没接好,程序就卡死在那。我在开源代码里加了超时保护。
uint8_t DHT11_ReadData(uint8_t *humid, uint8_t *temp) { uint8_t data[5] = {0}; uint16_t time_out = 0; DHT11_Start(); // 拉低18ms发起 if (!DHT11_CheckResponse()) return 1; for (int i = 0; i < 40; i++) { time_out = 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET) { if (++time_out > 1000) return 2; // 低电平超时保护 } delay_us(40); // 延时至位中间 uint8_t bit = (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) ? 1 : 0; data[i / 8] = (data[i / 8] << 1) | bit; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { if (++time_out > 4000) return 2; } } if (data[4] != (data[0] + data[1] + data[2] + data[3])) return 3; // 校验和 *humid = data[0]; *temp = data[2]; return 0; }注意一个坑:HAL库自带的HAL_Delay只能到毫秒级,而DHT11读位需要微秒级延时。我用的方案是基于DWT数据观察点的微秒延时,不占用定时器,精度也够。
static void delay_us_init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } void delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t cycles = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < cycles); }还有一点,读DHT11期间最好先把中断关掉,否则定时器中断或串口中断插进来,时序就乱了。实测下来,中断一多,校验和就频繁失败。代码里在DHT11_Start前后加__disable_irq()和__enable_irq(),但要注意别把SysTick中断关了,那样HAL_Delay会失效。
3.3 MQ-2模拟量采集:ADC滤波与标定
MQ-2的AO接到PA1之后,软件上就是循环读取ADC。用HAL库最简单的查询模式就行,不用开DMA,因为采样频率不高。
uint16_t MQ2_ReadRaw(void) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); uint16_t raw = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); return raw; }直接读原始值一定很跳,因为MQ-2内部是半导体气敏元件,输出本身就有噪声。我用了“10次采样去掉最大最小再取平均”的软件滤波,实测滤波后波动可以控制在±20个AD值以内。
uint16_t MQ2_GetFiltered(void) { uint16_t buf[10]; for (int i = 0; i < 10; i++) { buf[i] = MQ2_ReadRaw(); HAL_Delay(5); } // 简单选择排序,去掉最大值和最小值 for (int i = 0; i < 9; i++) { for (int j = i + 1; j < 10; j++) { if (buf[j] < buf[i]) { uint16_t tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; } } } uint32_t sum = 0; for (int i = 1; i < 9; i++) sum += buf[i]; return (uint16_t)(sum / 8); }阈值标定是很多人忽略的一环。MQ-2在干净空气中的输出电压并不是0,而且受温湿度影响有漂移。正确的标定流程是:上电后先让模块预热3~5分钟,连续采集50次取平均,得到“干净空气基准值”base_adc。然后用打火机气体或者点燃的香靠近传感器,记录一个“典型报警值”smoke_adc。实际报警阈值取base_adc + (smoke_adc - base_adc) * 0.7比较合理,留出余量。
我在项目里把这三个阈值都做成了宏,放在bsp_mq2.h里,实装时只需要改三个宏然后重新编译。
3.4 OLED显示与UI布局
OLED用的是SSD1306驱动芯片,0.96寸,128x64分辨率,I2C接口。软件驱动在开源库里已经写好,底层支持硬件I2C和软件模拟I2C两种方式。我建议用硬件I2C,PB6/PB7上拉了4.7k电阻,HAL库配置为I2C1,速率100kHz。
UI布局我按“一行数据、一行状态”来排,四行内容如下:
T:25.3C H:62% SMOKE: 45% STATE:NORMAL TH:1.2V/2.0VOLED显示浮点数有个技巧,直接用sprintf打印%.1f在嵌入式环境里占资源,而且有些编译器默认不启用浮点打印。我都是拆开打印:sprintf(buf, "T:%d.%dC", (int)temp, (int)(temp * 10) % 10),用整数运算代替浮点格式化,速度和Flash占用都友好很多。
UI刷新频率不要太高,200ms一次就够了。OLED是I2C慢速设备,如果每50ms刷一次,屏幕会闪,还会占用大量CPU。我在app_monitor.c里用一个uwTick计数做分时调度:每200ms刷屏、每500ms采一次传感器、每1000ms打印一次串口日志,互不干扰。
3.5 分级报警状态机
报警逻辑如果写成“一旦超阈值就响”是很容易误报的,尤其MQ-2偶尔会冒尖峰。我采用了一个带消抖计数的三态状态机:NORMAL、WARNING、ALARM。
逻辑核心是:温度、湿度、烟雾任一参数达到预警线时进入WARNING;达到报警线并持续10个采样周期(每秒1次,就是10秒)才进入ALARM,触发蜂鸣器和继电器;状态一旦进入ALARM,不会立刻回到NORMAL,需要连续20秒恢复正常读数才会自动解除,防止噪声导致继电器反复吸合。
typedef enum {STATE_NORMAL, STATE_WARNING, STATE_ALARM} AlarmState; AlarmState state = STATE_NORMAL; uint8_t alarm_cnt = 0; uint8_t recover_cnt = 0; void Alarm_Update(float temp, float hum, uint16_t smoke_raw) { uint8_t over_alarm = (temp >= TEMP_ALARM) || (hum >= HUM_ALARM) || (smoke_raw >= SMOKE_ALARM); uint8_t over_warn = (temp >= TEMP_WARN) || (hum >= HUM_WARN) || (smoke_raw >= SMOKE_WARN); if (over_alarm) { if (alarm_cnt < 10) alarm_cnt++; if (alarm_cnt >= 10) state = STATE_ALARM; recover_cnt = 0; } else if (state == STATE_ALARM) { // 报警解除需要连续20秒正常 if (++recover_cnt >= 20) { state = STATE_NORMAL; alarm_cnt = 0; recover_cnt = 0; } } else if (over_warn) { alarm_cnt = 0; state = STATE_WARNING; } else { alarm_cnt = 0; state = STATE_NORMAL; } // 根据state驱动外设 switch (state) { case STATE_NORMAL: // 绿灯,蜂鸣器关,继电器不动作 break; case STATE_WARNING: // 黄灯,蜂鸣器每隔2s短响 break; case STATE_ALARM: // 红灯,蜂鸣器快速响,继电器吸合 break; } }蜂鸣器在WARNING状态用“响200ms、停1800ms”的节奏,ALARM状态变成“响200ms、停200ms”,听起来急促很多,现场人员能明显区分。继电器低电平触发,进入ALARM时HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET)吸合,退出后释放。
3.6 串口调试与WiFi扩展
UART1我在CubeMX里配置成115200,8N1,重定向printf到串口方便调试。每秒钟打印一行:
[T=25.3C H=62% S=045%] STATE:NORMAL仿真阶段这行数据直接接到Proteus的Virtual Terminal,验证逻辑非常直观。如果你要加WiFi远程上报,把ESP8266-01S接到UART2(PA2、PA3),波特率设成115200,代码里每秒往UART2丢一行JSON:
sprintf(json, "{\"t\":%d.%d,\"h\":%d,\"s\":%d}\r\n", (int)temp, (int)(temp*10)%10, (int)hum, smoke_raw); HAL_UART_Transmit(&huart2, (uint8_t*)json, strlen(json), 100);ESP8266出厂默认是AT固件,用AT指令连上家里WiFi和MQTT服务器就能上报。这部分我在开源代码里留了app_mqtt.c的骨架,你自己填服务器地址和Topic就行。
4. Proteus仿真搭建:先跑通逻辑再打板
4.1 仿真能解决什么,不能解决什么
仿真最大的价值是把“传感器数据→状态判断→外设动作”这条逻辑链路完整跑一遍。你不需要焊板子,不需要买元件,就能验证代码里的阈值判断、消抖逻辑、OLED画面有没有错位、串口输出是否正常。对于学生党来说,Proteus仿真截图还可以直接放进课程设计报告里,比干巴巴的文字说明强太多了。
但仿真的局限也很明显。它能模拟逻辑电平,但模拟不了真实传感器的物理特性,比如MQ-2预热漂移、DHT11的供电毛刺、模拟I2C受外部干扰导致的误码。另外Proteus里的DHT11模型响应是“瞬间”的,而真实DHT11每次读取间隔要1秒以上,仿真里你连续读也读得出来,实物上就会失败。所以我的建议是:先用仿真验证逻辑,再用实物验证手感,两者不是替代关系。
4.2 Proteus建仿真工程的步骤
我用的是Proteus 8 Professional版本,以下步骤在这个版本上实测可行。
第一步,新建工程,选择“New Project”,芯片型号搜索“STM32F103C8T6”,在微处理器类别里能直接找到。如果没有,可能是你的库版本太旧,需要用ST的Proteus库升级包。
第二步,放置外围元件。DHT11直接搜索“DHT11”能出来模型;OLED的话,如果你的Proteus版本支持SSD1306模型,直接搜“SSD1306”或“OLED_SSD1306”,版本较老或没装第三方库的话,建议先用LCD1602模型代替,或者干脆用Virtual Terminal查看串口数据,因为仿真阶段我们更关心的是采样值是否进入预期范围。MQ-2用滑动变阻器POT-HG代替,一端接3.3V,一端接地,滑动端接PA1,转动变阻器就能模拟烟雾浓度从低到高的过程。
第三步,连线。注意ST芯片模型上VDD接3.3V,VSS接地,NRST通过10k电阻接3.3V,BOOT0接GND。如果你的模型有VREF+和VREF-引脚,VREF+接3.3V,VREF-接地,否则ADC读取全是0。OLED的SCL接PB6,SDA接PB7,两个引脚各加一个4.7k上拉到3.3V。
第四步,加载HEX文件。双击STM32芯片,在“Program File”一栏选择Keil编译出来的hex文件,位置一般在工程目录下的MDK-ARM/xxx/xxx.hex。Processor Clock Frequency这个参数非常关键,后面单独说。
第五步,放置Virtual Terminal,RX接STM32的PA9,TX接PA10,波特率设成和代码里UART1一致,运行就能看到串口日志。
第六步,运行仿真。常态下绿色LED亮;转动变阻器让PA1电压上升,烟雾值超过预警阈值后黄色LED亮,继续旋转超过报警阈值,红色LED亮、蜂鸣器响、继电器模型吸合。
4.3 时钟频率设置是仿真的最大坑
在Proteus里跑STM32仿真,十有八九会遇到HAL_Delay不准、串口乱码、OLED刷新过快或过慢的问题,根因都是时钟频率对不上。
CubeMX默认生成代码是外部8MHz晶振,PLL倍频到72MHz。但在Proteus的STM32模型里,芯片主频是由模型属性“Processor Clock Frequency”直接决定的,跟原理图上画不画晶振关系不大。如果模型属性设成8MHz,而代码里SystemCoreClock是72MHz,SysTick的延时计算就会快9倍,HAL_Delay(1000)实际只有111ms。
我推荐的稳定组合是:仿真阶段代码里使用内部HSI时钟并关闭PLL,让主频稳定在8MHz,同时把Proteus里的Processor Clock Frequency也设成8MHz。这样SysTick和串口波特率全都能对齐。实际打板时再改回外部晶振72MHz,CubeMX里重新配置一下RCC就行。这个切换我在开源项目里用宏PROTEUS_SIM区分,仿真和实物共用一份代码。
4.4 仿真效果与实物差异
仿真跑通后,我的验证顺序是:先看正常状态显示,再人为让变阻器滑到高位触发预警和报警,观察LED和蜂鸣器是否按状态机切换,再用Virtual Terminal确认串口日志和屏幕数据一致。这些都通过后,我会继续修改TEMP_WARN和SMOKE_ALARM等阈值宏,重新编译加载hex,确认改阈值没有引入逻辑回归。
实物上要注意的差异点有几个:DHT11上电后前1秒读出来的值往往是0,所以我在代码里加了一个“上电丢弃第一次采样”的处理;MQ-2模块刚上电的几分钟内基线会一直漂,表现为AD值缓慢下降,这是正常现象,等稳定后再标定相关阈值;OLED在真实硬件上如果I2C地址不对会一直黑屏,记得先用I2C扫描程序确认地址是0x3C还是0x3D。
5. 常见问题排查实录:从编译到实装的坑
5.1 ST-Link连接失败:no target found这类问题
这是STM32新手遇到最多的报错,完整信息通常是“Error: no STM32 target found! If your product embeds Debug Authentication, please ...”之类。出现这个问题的原因分散在硬件、固件和软件三层。
先把物理连接检查一遍:SWDIO接到PA13,SWCLK接到PA14,GND必须共地,目标板要有独立供电。BOOT0要确认是低电平,如果BOOT0悬空或者被拉高,芯片会进入系统存储器模式而不会正常启动Flash中的程序。很多自制板子就是这里翻车。
如果连接硬件都没问题,用STM32 ST-LINK Utility或STM32CubeProgrammer打开软件,点连接,软件会尝试读芯片ID。如果读不到ID,试试“Connect Under Reset”模式,也就是在复位期间建立连接。这个方法能救回很多“死锁”的芯片。克隆版ST-LINK在Win10/Win11下还经常遇到驱动问题,用Zadig把驱动换成WinUSB再试。
另一个可能性是调试接口被代码复用了。如果你在CubeMX里不小心把PA13/PA14配成了普通GPIO,程序一运行SWD口就失效了。这属于“烧录一次后再也连不上”的典型案例,处理办法是进入Bootloader模式(BOOT0拉高),用串口ISP把Flash擦掉,再改代码。
5.2 Keil编译与下载问题
Keil5装的时候如果既要玩51又要玩STM32,记得选“C51”和“MDK”两个组件,然后分别和谐,别共用破解文件。工程第一次编译报core_cm3.h找不到,通常是Keil的Device Pack没装,在Pack Installer里搜“STM32F1xx”安装即可。
HEX文件没生成也是常见问题。默认情况下Keil只在输出目录生成axf文件,要在Options for Target → Output里勾选“Create HEX File”,编译后才能在Objects或Listings目录找到hex。仿真加载的就是这个文件。
5.3 DHT11读数异常或校验失败
DHT11最典型的现象就是串口打印出来全是255或者温度湿度一直不变。先检查供电:DHT11支持3.3V到5.5V,3.3V下也能工作,但信号线上拉电阻一定要有。如果用的是裸传感器而不是模块,必须自己加4.7k上拉,否则读出来全乱。
第二个原因是读数间隔太短。DHT11每次采样间隔要大于1秒,代码里如果每200ms读一次,传感器来不及刷新,返回的永远是上一次数据。我给DHT11_ReadData加了个调用限制,两次读取间隔小于1秒直接返回上一次缓存。
第三个原因是中断打断时序。读取过程中如果被UART中断插入,时延可能偏掉几十微秒,位就判错了。解决方案在3.2已经说过,读取期间关中断。点亮OLED之后还会出现一个问题:OLED的I2C中断优先级如果比DHT11读取高,也会造成干扰,建议把DHT11读取期间的全局中断关闭。
5.4 OLED不显示、花屏或者显示一半
先分清是硬件问题还是驱动问题。用逻辑分析仪抓I2C时序最直接,没有的话可以先跑一个I2C扫描程序,看能不能扫到0x3C或0x3D。OLED模块背面的地址电阻决定地址,大多数是0x3C,但有些模块默认0x3D。我在Proteus仿真里也碰到过模型默认地址0x3D而代码写0x3C导致黑屏的情况,一度以为驱动写错了。
花屏通常和电源纹波有关。OLED驱动芯片内部电荷泵在升压时对电源要求较高,如果3.3V供电线上没有104电容,会出现刷新往一半花。在模块电源脚并联一个10uF电解电容能明显改善。
5.5 ADC数值跳动剧烈
ADC测MQ-2数值乱跳,先做两件事:第一,PA1的输入信号有没有经过RC滤波,我原理图里加的那个104电容不能省;第二,软件是否做了平均值滤波,如果没有,用3.3里的滤波函数。这两步做完,跳动幅度至少下降70%。
如果还跳,检查给STM32供电的电源是否稳定,USB供电在传感器负载波动时波动尤其明显,改成DC电源或者加一级LC滤波。还有一点,MQ-2在预热阶段读数本来就会持续漂移,等3分钟再判断阈值。
5.6 实装到充电桩之前的安全提醒
这套系统最大功率路径是220V交流供电,所有改造都必须断电作业。继电器模块只能用来控制接触器或者排风扇这类设备,不要用继电器触点直接去切断大电流充电回路。监测板本身要装在充电桩的弱电腔体里,和强电区域做好物理隔离。如果安装在户外或地下车库,PCB打样回来先刷三防漆,接线端子用防水接头,避免凝露导致板卡短路。
还有一点要特别说明:这套系统是辅助监测,不是替代充电桩原有保护的合规设备。它适合做预警、做联动、做数据采集,但如果要用于商业运营场所,必须再经过电气安全认证,不能拿DIY板卡直接上线。
6. 后续扩展方向与我的迭代体会
6.1 扩展方向
这套系统的硬件资源其实留了不少余量,可以往上叠功能。最容易加的是水浸传感器,地下车库充电桩最怕泡水,在板子底部加两个探针做成干接点检测,进水就报警,成本几块钱。其次是火焰传感器,可以检测红外火焰特征,适合装在充电桩枪座附近,和烟雾形成双保险。
如果你想做多桩联网,可以把RS485模块加上,让多块监测板走MODBUS总线到汇聚网关,网关再通过4G或者以太网把数据送到云端平台。这样就是一套小规模的充电站环境监测网络了。SPI引脚我恰好没有占用,加一块SPI接口的以太网模块也很方便。
6.2 我的迭代体会
这套系统我前后迭代了三版。第一版直接在面包板上飞线,逻辑验证通过后第二版画了PCB打样,结果犯了两个经典错误:MQ-2的输出没加分压直接接到了ADC,上电就烧坏了一个引脚;BOOT0悬空,导致板子在下载器拔掉后偶尔起不来。这两个错误让我明白,原理图上最不起眼的细节,往往是实测时最要命的坑。
后来把阈值都改成宏定义、加上消抖状态机、再补了Proteus仿真文件,整个项目才算真正可以从零复现。如果你要拿这个项目练手,我建议按“先仿真→再面包板→最后打板”的顺序来。仿真跑通代表你的逻辑没问题,面包板跑通代表你的传感器接线没问题,打板跑通才代表你的硬件设计没问题。千万不要跳过前两步直接画PCB,尤其是第一次画STM32板子,省下的打样钱远不够填调试时间。开源包里我放了完整的keil工程、KiCad原理图和Proteus仿真工程,拿去就能直接开工。