
简介面向嵌入式开发者的STM32F103老人病人护理监测仪完整工程适用于物联网单片机项目的快速原型开发。代码基于Keil标准库编写注释详细各传感器接线引脚均在工程头文件中明确。项目同时接入DHT11温湿度、微波生命雷达、红外体温、一氧化碳气体、液晶屏显示与WiFi无线传输等多种外设适合作为多传感器数据融合及远程监测的参考框架也可自行修改芯片型号与Flash容量后移植至同系列其他单片机。压缩包共184个文件除了C语言源文件与头文件还包含uvprojx/uvoptx工程配置、hex/axf烧录文件以及map和lst编译过程信息整体约3.8MB结构清晰便于阅读。目前已有46人学习下载适合需要快速搭建老人或病人看护监测系统、或学习STM32标准库外设驱动开发的初中级工程师能够有效节省项目验证与排错时间。1. 老人护理监测仪为什么选 STM32F103先盘一盘六个传感器的信号形态凌晨两点老人从床沿滑落按不到呼叫器这时能做主的只有床头那台监测仪。它要在 10 秒内判断出“有人、体温异常、环境一氧化碳浓度有风险”本地液晶屏亮起提示再通过 wifi 把状态发给子女。标题里这六个外设看起来各管一摊但放到 STM32F103 上恰好覆盖了嵌入式系统最基本的四类信号单总线、GPIO 电平、I2C、模拟电压外加一路串口 wifi 做数据上报。选 STM32F103 而不是 Arduino不是因为 Arduino 不会写而是护理类设备需要稳定的 ADC 采样、独立看门狗、多个串口而且芯片成本低到可以整机埋进一个外壳。这套方案的核心不是把六个传感器接亮而是让任何一个传感器失效时都不拖垮主循环。下面按接线、驱动、显示、报警、调试的顺序展开所有代码按 STM32F103 标准外设库来写理论能独立搭起来。2. 传感器接线与电源分配DHT11、微波生命雷达、红外体温、一氧化碳模块怎么接 STM32F1032.1 按信号类型给传感器分配 GPIO、I2C 与 ADC先把接线关系摆清楚。DHT11 是单总线数据脚需要外部上拉微波生命雷达模块常见为 RCWL-0516输出就一根 TTL 电平线检测到人体活动会拉高并保持约 3 秒红外体温用 MLX90614走 I2C一氧化碳用 MQ-7输出模拟电压接 ADC液晶屏选 0.96 寸 SSD1306也是 I2Cwifi 用 ESP8266接串口。下面是我常用的引脚分配表原则是 I2C 设备共用总线、模拟量和数字量分开。不能把所有 GPIO 都塞在 PA 口因为 PA0 用作 ADC 后就不能再复用为普通输入PB 口要留给雷达和 DHT11。模块输出信号STM32F103 引脚关键参数DHT11单总线PB8外部上拉 4.7kΩ建议 5V 供电后分压到 3.3V微波生命雷达 RCWL-0516TTL 高电平PB9检测到人输出高约 3s重复触发可调MLX90614 红外体温I2CPB6/PB77 位地址 0x5A数据为开尔文 × 50MQ-7 一氧化碳模拟电压PA0需要加热控制上电预热不低于 60sSSD1306 OLEDI2CPB6/PB7地址 0x3C128×64ESP8266 wifiUARTPA2/PA3USART2115200bpsAT 固件DHT11 的 3.3V 供电不是不能用但低温下时序会明显变差。我一般让 DHT11 本体吃 5V数据输出经过 5.1kΩ 串联 10kΩ 分压后进 PB8这样既能保证时序稳定又不会把 5V 电平灌进 STM32。MLX90614 和 SSD1306 共用 I2C1地址一个 0x5A 一个 0x3C总线上不会冲突。需要注意的是STM32F103 的 PB6/PB7 作为 I2C 引脚时必须配置为开漏输出靠外部上拉电阻把电平拉高。SSD1306 模块板上通常自带 4.7kΩ 上拉但如果 I2C 线长度超过 20 厘米建议把总线上拉换成 2.2kΩ否则 400kHz 模式下容易读到乱码。2.2 电源分配不要让 MQ-7 的加热丝把 MCU 拉复位很多人拿到这种项目先画一个 AMS1117-3.3 给所有模块供电这是第一个坑。MQ-7 内部加热丝冷态电流能达到 150mA 以上ESP8266 在 wifi 发射瞬间电流也会冲到 300mA。如果这几个都挂在同一个 3.3V 稳压器后面压降一抖动STM32F103 立刻复位。更严重的是 MQ-7 加热丝断电后不会马上冷却重新上电时电流冲击还会再出现一次。常见的做法是把电源分成两路5V 输入端加 500mA 自恢复保险丝一路 5V 直接给 MQ-7 加热端、微波雷达、DHT11 供电另一路 5V 经过 AMS1117-3.3 给 MCU、MLX90614、OLED 供电ESP8266 单独一路 3.3V就近放一个 100μF 电解电容防止发射瞬间拉低电压。如果做的是“stm32f103 最小系统”加杜邦线调试至少要让 MQ-7 的 5V 和 MCU 的 3.3V 共地且供电线尽量粗。不要把 MQ-7 输出直接接到 PA0因为 5V 供电时输出最高接近 5V超过 STM32 模拟输入耐压。正确做法是经过 20kΩ/10kΩ 分压把最高电压压到约 3.3V。ADC 里再按比例反推原电压这样量程才不会丢。2.3 微波雷达、红外体温的安装位置影响判断结论RCWL-0516 这类微波雷达对运动物体很敏感但也会被风扇叶片、窗帘晃动触发。护理仪放在床头柜时雷达面要朝床铺中心不要对着窗口。它的输出是“有无人活动”不是心率也不是“在不在床上”因此只能做辅助判据不能单独靠它判定跌倒。MLX90614 的非接触测温则更容易偏差视场角约 35°距额头 5 厘米时读数接近真实体温拉到 20 厘米后可能偏低 1 到 2℃。我一般会在软件里加一个距离补偿项temp_body temp_obj 0.05 × (distance_cm - 5)具体系数用红外耳温计在同一人身上做两点标定。这部分会在第 3 章的 I2C 读取后一起处理。3. STM32F103 驱动 DHT11 和 MQ-7单总线时序、ADC 采样与校准3.1 寄存器级 DHT11 读取微秒延时必须自建DHT11 的协议不复杂难在时序。主机先把总线拉低至少 18ms然后释放传感器应答时先拉低 80μs再拉高 80μs之后开始输出 40bit 数据。每个 bit 的起始都是 50μs 低电平区别在高电平宽度高电平约 2628μs 表示“0”约 70μs 表示“1”。很多人直接用 HAL 库的HAL_Delay来做微秒延时精度不够。我倾向于用标准外设库 v3.5 那种直接操作寄存器的方式配合 DWT 的 CYCCNT 做delay_us。关键点是读取数据位时在低电平结束后延时 40μs 再去读引脚。为什么是 40μs因为“0”的高电平只有 2628μs延时 40μs 后已经回落到低电平“1”的高电平有 70μs延时 40μs 后仍然维持高电平这样一次采样就能区分 0 和 1。// dht11.c 核心读取逻辑使用 GPIOB Pin 8 static void DHT11_Pin_Output(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin GPIO_Pin_8; gpio.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); } static void DHT11_Pin_Input(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin GPIO_Pin_8; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; // 输入浮空 GPIO_Init(GPIOB, gpio); } uint8_t DHT11_Read(float *humi, float *temp) { uint8_t buf[5] {0}, i, j, bit; uint32_t timeout; DHT11_Pin_Output(); GPIO_ResetBits(GPIOB, GPIO_Pin_8); delay_us(20 * 1000); // 主机拉低至少 18ms GPIO_SetBits(GPIOB, GPIO_Pin_8); // 释放总线 delay_us(30); DHT11_Pin_Input(); timeout 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8) SET) { if (timeout 100) return 1; // 等待传感器响应超时退出 } timeout 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8) RESET) { if (timeout 100) return 1; } for (j 0; j 5; j) { for (i 0; i 8; i) { while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8) RESET) { // 跳过 50us 低电平 } delay_us(40); // 采样点放在高电平中间 bit GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8); if (bit) { buf[j] | (0x80u i); while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8) SET) { // 等当前高电平结束否则会多读一个 bit } } } } if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) { return 2; // 校验失败 } *humi (float)(buf[0] buf[1] * 0.1f); *temp (float)(buf[2] buf[3] * 0.1f); return 0; }buf[0]是湿度整数部分buf[1]是湿度小数buf[2]是温度整数buf[3]是温度小数buf[4]是校验字节。读取失败返回 1 表示无响应返回 2 表示校验不对。代码里的while (GPIO_ReadInputDataBit(...))都没有超时保护实际工程里要加一个timeout否则 DHT11 没插好时整个 MCU 会死等。DHT11 每次读取间隔不能小于 1 秒连续读或者上电立刻读传感器会不响应。我在主循环里用 2 秒采样一次 DHT11读取失败连续五次后才标记传感器故障单次失败不报警。3.2 MQ-7 需要间歇式加热ADC 采样要滤掉脉冲噪声MQ-7 的 datasheet 写明它需要高电压加热 60 秒、低电压加热 90 秒交替工作。但如果只做“环境中一氧化碳是否超标”的粗略判断可以简化成恒压 5V 供电每 60 秒读一次 ADC。缺点是对低浓度 CO 不敏感大约只有 100ppm 以上才能稳定触发。要是做真正护理监测我建议用 MCU 的定时器输出 PWM 控制加热电压60 秒 5V90 秒 1.5V在低电压段最后 5 秒采样。采样期间加热状态要固定不能在 ADC 转换过程中切换加热否则读数会剧烈波动。PA0 配置为模拟输入ADC1 采用单通道软件触发模式。采样时间我取 239.5 周期也就是大约 20μs对 MQ-7 这种高内阻输出更稳。只采一次不够MQ-7 输出电压存在 1% 左右随机波动工程上连续采 8 次取平均。// adc_mq7.c 初始化 ADC1 通道 0 void CO_ADC_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, gpio); adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode DISABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_Cmd(ADC1, ENABLE); } uint16_t CO_ReadRaw(void) { uint16_t sum 0, i; ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); for (i 0; i 8; i) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); sum ADC_GetConversionValue(ADC1); } return sum 3; // 8 次平均 }如果你直接把这个 ADC 值换算成 ppm会发现 MQ-7 的灵敏度曲线在双对数坐标系里不同温度和湿度下漂移很大。护理监测不追求精确浓度我更推荐先计算电压然后分三档电压低于 0.5V 视为正常0.5V0.9V 提示通风大于 0.9V 才触发报警。这个分档阈值需要在自己环境中用标准气体或者至少用电化学 CO 检测仪对比标定。注意 MQ-7 输出端要并一个 100nF 电容否则脉冲干扰会让单次 ADC 值跳变。3.3 MLX90614 的红外测温读取I2C 帧格式不能按普通 EEPROM 写MLX90614 的寄存器里0x07 存物体温度0x06 存芯片环境温度数据格式是开尔文乘以 50。读取时要用两次 START 信号第一次是写寄存器地址第二次是读数据。STM32F103 的 I2C 外设事件标志比较多写代码时最容易漏掉第二个 START 前的发送完成判断。// mlx90614.c 读取 16bit 寄存器原始值 uint16_t MLX_Read16(uint8_t reg) { uint16_t val 0; uint8_t i; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0x5A 1, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, reg); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0x5A 1, I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); for (i 0; i 2; i) { if (i 0) { while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); val I2C_ReceiveData(I2C1); } else { I2C_AcknowledgeConfig(I2C1, DISABLE); // 最后一个字节前关闭 ACK while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); val | (uint16_t)(I2C_ReceiveData(I2C1)) 8; } } I2C_AcknowledgeConfig(I2C1, ENABLE); I2C_GenerateSTOP(I2C1, ENABLE); return val; } float MLX_GetObjectTemp(void) { uint16_t raw MLX_Read16(0x07); return (float)(raw * 0.02) - 273.15f; }I2C_AcknowledgeConfig(I2C1, DISABLE)这一句不能漏否则读最后一个字节后 MCU 会多送回一个 ACKSMBus 设备在接收端没有准备好时可能产生错误。原始值乘 0.02 再减 273.15 就是摄氏温度。MLX90614 和 SSD1306 共用 I2C1只要初始化一次两个设备都可用。需要注意 MLX90614 刚上电的前几帧可能读到 0x0000这不是故障跳过前 10 次采样即可。我在实际项目中还会对它做滑动平均缓存最近 5 次物体温度取中间值这样能去掉扑空到床单时的异常值。4. 液晶屏与 wifi 上报SSD1306 显示状态、ESP8266 AT 指令不阻塞4.1 SSD1306 刷屏只刷变化区域0.96 寸 OLED 的显存是 128×64内部按 8 像素分成 8 页每页 128 字节。用 I2C 写满整屏需要传 1024 字节在 400kHz 下大约 20ms看起来不慢但如果主循环每 50ms 就全刷一次I2C 连续传输会占用大量时间DHT11 的 20ms 起始信号就可能被切碎。护理监测屏不需要动画我一般只刷新有变化的区域。SSD1306 的初始化命令序列可以精简成下面这组const uint8_t init_cmds[] { 0xAE, // 关闭显示 0x8D, 0x14, // 开启内部电荷泵 0xD5, 0x80, // 显示时钟分频 0xA8, 0x3F, // 64 行显示 0xD3, 0x00, // 显示偏移 0 0x40, // 起始行 0 0xA1, 0xC8, // 左右反置上下反置 0xDA, 0x12, // COM 引脚硬件配置 0x81, 0xCF, // 对比度 207 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH 配置 0xA4, 0xA6, // 关闭整屏点亮正常显示 0xAF // 打开显示 };之后每次写数据先用命令设置页地址和列地址再连续写该行的 128 个字节。页面寻址模式下列地址会自动递增写满一行后必须重新设置页地址。如果只更新体温数字那么只需要改对应字符所在页的 8 字节。维护一个和显存等大小的framebuffer[8][128]改数据时同步改 framebuffer再把这个字节推送到 SSD1306避免读回显存这种麻烦操作。OLED 在强光下可读性一般但护理仪放在床头夜间显示比 TFT 更合适功耗也更低。4.2 ESP8266 用 USART2不占用调试串口wifi 模块接到 USART2把 USART1 留给调试输出这是工程上的习惯。ESP8266 AT 固件默认 115200 波特率。最忌讳的是在接收 AT 响应时用阻塞等待因为ATCWJAP的响应可能超过一百字节UART 硬件缓冲只有一两个字节稍慢就丢。我用一个 256 字节环形缓冲区把串口接收中断里读到的字节全部缓存下来主循环再匹配关键字。volatile uint8_t uart2_rxbuf[256]; volatile uint16_t uart2_rx_head 0, uart2_rx_tail 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { uint8_t b USART_ReceiveData(USART2); uart2_rxbuf[uart2_rx_head] b; uart2_rx_head (uart2_rx_head 1) % 256; } }发送一条 AT 指令的流程是先清空环形缓冲区发指令然后等待期望的关键字出现同时设一个超时时间超时后返回错误。下面是一组典型交互ATE0 ATCWJAPFamily,12345678 WIFI GOT IP ATCIPSTARTTCP,192.168.1.100,8080 CONNECT ATCIPSEND13 {p:1,t:26} SEND OKATE0关闭回显后收到的内容只有结果解析容易很多。ATCWJAP成功后模块会主动打印WIFI GOT IP此时才能继续建 TCP 连接。如果ATCIPSTART返回ERROR先查路由器是否开启了 AP 隔离很多家用路由器默认开启隔离后局域网内两个设备虽然都能上网但互相连不上端口表现就是 wifi 能连、TCP 建不起来。手机热点也可能因为需要网页认证而导致 TCP 连接失败排查时先换一个无密码的普通路由器做验证。4.3 wifi 上报和本地显示要解耦本地液晶屏可以 500ms 刷新一次但 wifi 上报不要跟着刷。无线发射一次约 1030ms如果每 500ms 上报一次模块长时间占着串口其他传感器数据会被延误。我一般设置 5 秒上报周期并且把上报时机放在 DHT11 完成读取之后、下一次读取之前。上报内容用紧凑 JSON{dev:a01,h:55.5,t:26.8,bt:36.2,co:12,mov:1,al:0}发送前用snprintf生成字符串再取出长度拼成ATCIPSENDlen。这里要特别注意ATCIPSEND回车换行后模块会返回一个提示符必须在收到后立即发送数据中间不能夹别的字符否则模块认为长度不匹配。发送完成后等待SEND OK如果收到ERROR可能是数据里含中文编码或长度数错。为了避免这个问题JSON 字段全部使用 ASCII 字符。5. 报警判据、看门狗与传感器故障自检5.1 阈值持续判定不要被单次跳变误报护理仪报警要稳不能体温一次超过 37.3℃ 就响。MLX90614 受距离和角度影响很大单次读数波动能有 0.5℃所以要连续 5 次采样都超过阈值才确认。MQ-7 的电压噪声更大报警前至少连续 3 次超过 30ppm 才触发。下面是我常用的报警判定表监测项正常区间报警条件持续条件红外体温35.5℃37.3℃≥ 38.0℃连续 5 次约 10 秒红外体温低温35.0℃35.5℃≤ 35.0℃连续 5 次约 10 秒一氧化碳电压 0.5V≥ 30ppm 或电压 ≥ 0.9V连续 3 次约 3 分钟DHT11 环境温度16℃30℃≥ 33℃ 且持续 20 分钟防止短暂午后高温误报微波雷达无人/有人有人且体温低于 35℃5 分钟内持续这里要强调雷达“有人”和体温“偏低”必须同时成立才提示低体温风险否则老人只是短暂离开床铺也会触发误报。报警发生后状态不能马上撤销需要恢复条件持续一段时间。比如温度回到 37.3℃ 以下且 CO 降到阈值一半连续 10 次采样确认后才关闭报警这能避免报警在临界值附近反复抖动。5.2 报警状态机与消抖我用一个简单的三态状态机来管理报警。正常状态只计数连续超阈值进入报警状态报警状态下如果数据恢复正常进入恢复计数恢复计数足够多才回到正常。代码结构如下typedef enum { ALARM_NORMAL 0, ALARM_RAISE, ALARM_RELEASE } AlarmState; AlarmState alarm_state ALARM_NORMAL; uint8_t alarm_count 0; void Alarm_Poll(float body_temp, uint16_t co_value, uint8_t sensor_valid) { if (!sensor_valid) { return; // 传感器无效时不参与报警判定 } switch (alarm_state) { case ALARM_NORMAL: if (body_temp 38.0f || co_value CO_ALARM_LEVEL) { if (alarm_count 5) { alarm_state ALARM_RAISE; Buzzer_On(); alarm_count 0; Wifi_SendAlert(); } } else { alarm_count 0; } break; case ALARM_RAISE: if (body_temp 37.3f co_value CO_ALARM_LEVEL / 2) { if (alarm_count 10) { alarm_state ALARM_NORMAL; Buzzer_Off(); alarm_count 0; } } else { alarm_count 0; } break; default: alarm_state ALARM_NORMAL; break; } }ALARM_NORMAL进入ALARM_RAISE需要 5 次连续超阈值也就是大约 10 秒释放报警需要 10 次连续正常值也就是 20 秒左右。这样做的好处是一次偶然的挥手导致红外体温采样扑空不会触发报警真正低体温的老人也不会因为一次正常读数而漏报。Wifi_SendAlert()只在状态切换时执行一次而不是每 5 秒重复推送否则子女手机一晚上会被消息刷爆。如果需要重复提醒在报警状态中每 30 分钟再补一条即可。5.3 独立看门狗与传感器失效自检STM32F103 的独立看门狗最合适做护理仪的总闸因为一旦主程序跑飞独立看门狗能从系统层面拉复位。配置如下void IWDG_Config(void) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_64); // LSI 约 40kHz分频后约 625Hz IWDG_SetReload(625); // 625 / 625Hz 1 秒 IWDG_ReloadCounter(); IWDG_Enable(); }这段配置把看门狗设为 1 秒超时主循环必须在 1 秒内喂狗。喂狗放在主循环最后一行IWDG_ReloadCounter()。所有传感器的读取函数必须带超时返回不能出现一个while死等。比如 DHT11 断线时如果没有超时程序会在等待电平变化的地方卡死看门狗也救不回来。我习惯给 DHT11 的每个while加一个timeout变量超时后返回错误码。传感器失效自检也有固定套路。DHT11 连续 5 次读取失败标记温湿度无效MLX90614 连续 3 次 I2C 无响应标记体温无效MQ-7 连续多次 ADC 值达到 4095判定输出线断开如果一直为 0判定加热丝损坏。数据无效期间报警状态机不动作只在液晶屏显示SENSOR ERR同时通过 wifi 发送一次错误日志。要记住报警逻辑宁可漏报一次瞬时故障也不能用一个错误的传感器值去触发虚假警情否则护理人员会习惯性忽略声音。6. 调通这套监测仪的三个技巧逻辑分析仪时序、AT 回显、日志分级6.1 抓 DHT11 时序用逻辑分析仪别去看毛刺DHT11 的时序问题用万用表测不出来。把逻辑分析仪通道夹在 PB8 和 GND 上采样率设 10MS/s触发方式选下降沿抓按下读取按钮的那一段。重点看三处波形起始低电平是否大于 18ms、传感器响应低电平是否接近 80μs、每个数据位的高电平宽度是否在 2670μs 范围内。如果起始低电平只有 1ms说明delay_us函数被编译器优化掉了把优化等级改成 -O0 或给延时变量加volatile。如果所有数据位高电平宽度一致大概率是上拉电阻太大导致上升沿过慢或 GPIO 没有切换成输入浮空。采样率不要低于 5MS/s否则 26μs 的高电平只能采到十几个点看不出差分。6.2 ESP8266 先关闭回显再查连接状态调试 wifi 模块时先把串口助手波特率设为 115200接上模块后发ATE0关闭回显。之后输入ATCWJAP?返回结果里如果包含CWJAP:Family说明已连接如果返回No AP说明路由器把模块踢下线了需要重新发ATCWJAP。做局域网传输时最隐蔽的问题是路由器开了 AP 隔离现象是ATCIPSTART一直超时但ATCWJAP?又是连接状态。这时候换一个普通无线路由器关闭隔离或者把手机热点设为“无密码开放”复测一下就能定位。接口机代码里还要识别WIFI DISCONNECT字符串收到后主动停止 TCP 连接一段时间等模块重新拿到 IP 再继续报数。6.3 日志分级与错误计数整个系统有六个传感器串口日志不分级就很难查。我用[DBG]、[WARN]、[ERR]三种前缀[DBG]只在调试版里打印正式固件只保留[WARN]和[ERR]。每个传感器维护一个err_count字段错误率超过 50% 时就进入降级模式比如停止 wifi 上报只在本地屏幕提示。调试时最有用的是主循环周期日志把某个 GPIO 每隔一次循环翻转一次用示波器看频率正常应该稳定在几十赫兹以上如果某次翻转间隔突然拉长就能定位到是哪个传感器函数阻塞了主循环。把这几个计数器和主循环时间戳放进一个结构体之后接 4G 模块或者蓝牙调试时直接把这个结构体打包发出去就是一套能回放的现场日志。本文还有配套的精品资源点击获取