简介基于STM32微控制器的智能家居监测系统代码包面向嵌入式与物联网开发者解决室内温度、湿度、光照、PM2.5和甲醛等环境参数的实时采集与远程查看问题。整体设计涵盖多传感器接入、ESP8266 WiFi上云、QT上位机可视化以及手动/自动双模式控制逻辑。压缩包共3个文件容量仅6KB以HTML页面、inscode工程配置和Git配置文件为主方便快速了解项目构成或导入在线开发环境。该资源已有115人学习浏览适合需要完成课设或入门云平台开发的读者。虽然包体精简但结合项目描述可还原出完整的系统链路自动模式下根据预设阈值控制净化设备异常时触发蜂鸣器与LED声光报警并支持远程手动干预有助于开发者掌握从底层采集到上层应用的全流程设计思路。1. 把 STM32 智能家居监测系统做成能跑起来的代码先想清楚四个问题STM32 智能家居监测系统最常见的问题不是传感器不灵敏而是代码没有一个稳定的骨架。很多人把温湿度、烟雾、人体红外全部接到开发板跑通一个串口打印就觉得能用等真正要上云或做产品样机时功耗、阈值、多通道采集顺序都会变成坑。我经手过类似的模拟项目X早期因为没有提前定好数据通路整套代码从采集、显示到云平台反复返工教训非常直接。下面这套方案是围绕“可复现代码”来组织的覆盖传感器选型、HAL 库多通道采集、MQTT 上云、低功耗与调试。适合正在做课程设计、宿舍改造或产品原型的人照着搭最小系统再按自己的传感器换参数就能跑起来。2. 硬件选型和测量链路从传感器信号到 STM32 引脚分配很多新手问代码能不能先写答案是不能。你连传感器输出的信号类型都没定代码里的 ADC 通道、I2C 地址、串口波特率全是空的。这一章先把硬件链路理清楚后面写代码才不会改一次引脚崩一次逻辑。智能家居监测系统通常至少包括温度、湿度、可燃气体或烟雾、人体活动这几项我一般会按“信号类型”而不是按“买的传感器”来设计。2.1 传感器选型温湿度、可燃气体、烟雾、人体红外怎么选先列一张选型表这张表基本决定了代码里要开几个外设。不同传感器输出信号不同有的走 ADC有的走 I2C有的就是一个 GPIO 电平如果选型时混着来后续移植会很痛苦。监测项常见传感器输出信号接口方式选型提醒温湿度DHT22 或 SHT30数字 / 数字单总线 / I2CDHT22 读取时序敏感SHT30 更稳可燃气体MQ-2 或 MQ-7模拟电压ADC上电后需要预热不能立刻判断光照光敏电阻或数字光照传感器模拟 / 数字ADC / I2C光敏电阻非线性需要单独校准人体活动热释电红外模块开关电平GPIO需要 5~10 秒触发延时否则误报严重实际项目里我建议温湿度用 I2C 模块气体和光照用模拟输出。原因是 STM32 的 ADC 通道数量有限哪怕是 F103 的 10 个 ADC 通道如果你把温湿度也做成模拟量气体、光照、水浸、电流采样就挤不下了。数字温湿度接到 I2C 总线上和 OLED 显示模块共用两条线地址不同互不冲突。人体红外模块就是一对电平输入状态读进来即可。另外要注意气体传感器的功耗。MQ 系列内部有加热丝工作电流通常在 150mA 以上如果和 STM32 共用同一个 LDO加热丝一启动3.3V 会被拉低ADC 参考电压跟着漂读出来的气体浓度就是“玄学”。常见做法是单独给传感器供 5V主控和传感器之间只走信号线和地线地线要共地。2.2 信号调理电路与 ADC 采样为什么不能直接怼传感器引脚我见过很多同学把模块输出直接接到 PA0 就开始读确实能出数据但数据能不能信就是另一回事。传感器模块的输出阻抗通常较高而 STM32 的 ADC 采样电容在转换瞬间会抽取电流如果传感器输出阻抗过大采样电容还没充满就被拿去转换读到的值会比真实值偏低而且不稳定。常见做法是在传感器输出到 STM32 引脚之间加一个 RC 低通滤波比如 1kΩ 电阻串联再在引脚对地加 100nF 电容。这样既能滤掉高频噪声又能降低信号源阻抗对 ADC 的影响。如果传感器模块输出的是 5V 满量程必须先用电阻分压到 3.3V 以下再考虑钳位保护否则长期使用会把 ADC 引脚烧掉。电阻分压公式很简单U_out U_source × R2 / (R1 R2)比如某气体模块最大输出 5V想让最大采样电压接近 3V可以选 R1 4.7kΩR2 6.8kΩ算出最大约 2.96V。这样留了约 0.34V 的余量不会在电压纹波时顶到 3.3V。代价是分辨率损失了一小部分但是换来了 ADC 安全性这个取舍是值得的。如果对精度要求更高就改用外部基准电压源。2.3 引脚分配表与供电规划我习惯在写代码前先画一张引脚分配表然后对着 CubeMX 做初始化不然写一半发现串口引脚和 I2C 引脚复用冲突只能返工。下面是一套很典型的分配方式适用于一块 STM32F103C8T6 核心板外设信号引脚说明ADC1_IN0可燃气体传感器输出PA00~3.3V 模拟输入ADC1_IN1光照传感器输出PA10~3.3V 模拟输入I2C1SHT30 温湿度PB8 / PB9上拉 4.7kΩI2C1OLED 显示PB8 / PB9地址与 SHT30 不同可共用GPIO 输入人体红外模块PB0内部下拉检测高电平GPIO 输出蜂鸣器PB1低电平触发USART1调试串口PA9 / PA101152008N1USART2ESP8266 Wi-Fi 模块PA2 / PA3115200与 ESP8266 交叉连接这套分配的核心思路是模拟信号集中放在 ADC1 的通道 0 和通道 1数字传感器和显示共用 I2C1ESP8266 单独走 USART2调试串口用 USART1 独立出来。这样即使后期要加继电器控制、水浸检测也还有 PB10、PB11 等引脚可以扩展。供电规划上3.3V 给 STM32、SHT30、OLED5V 给 MQ 气体传感器和人体红外模块。如果要用继电器控制灯光、空调继电器线圈需要独立电源不能从 STM32 的 3.3V 取电否则大电流一冲击单片机直接复位。这是硬件上最常见的翻车点。3. 用 STM32 的 HAL 库搭建最小监测固件从时钟配置到多通道采集开发方式我推荐先用 CubeMX 生成工程再把业务代码填进去。自己从启动文件开始写寄存器也可以但开发速度慢而且后期要做低功耗时HAL 库对 STM32L4 系列的适配会省非常多事。下面的代码基于 STM32F103C8T6主频 72MHz如果你用的是其他型号时钟树和中断名称需要重新映射。3.1 CubeMX 配置要点时钟、ADC 多通道、DMA 与定时器触发CubeMX 里第一件事是配时钟。使用 8MHz 外部晶振经过 PLL 倍频到 72MHz。注意 STM32F103 的 ADC 最高工作频率是 14MHz如果你直接把 PCLK2 的 72MHz 送给 ADCADC 会乱跳。常见做法是把 ADC 时钟分频到 12MHz留一点安全余量。ADC 部分要开 ScanConvMode把 NumberOfConversion 设为 2分别对应气体传感器和光照传感器。ContinuousConvMode 一般选 Disable然后用定时器触发转换这样采样频率固定不会占用 CPU 过重。我习惯用 TIM3 的 TRGO 做触发源配置 TIM3 的预分频为 7200周期为 999这样每秒触发 10 次采样足够覆盖气体和光照这类慢变信号。DMA 开 ADC1 的请求Mode 选 CircularData Width 选 Word。这里很容易踩坑如果 Data Width 选了 Half WordDMA 搬运的数据宽度和外设寄存器不匹配读出来的值会错位。DMA 方向是 Peripheral to Memory。初始化完成后记得在 main 里调用 HAL_ADCEx_Calibration_Start 做一次校准。3.2 采样代码ADCDMA 轮询与平均值滤波CubeMX 生成完工程后业务代码写在 USER CODE 区。下面这段是我在监测项目里常用的骨架/* 宏和全局变量放在 user code begin 0/1 */ #define ADC_CH_NUM 2 #define ADC_SAMPLE_N 8 static uint32_t adc_dma_buf[ADC_CH_NUM] {0}; static volatile uint8_t adc_conv_flag 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_conv_flag 1; // 整轮 ADC 扫描完成 } }主循环里不要直接在回调里做累加把标志位放到主循环处理uint32_t raw_value[ADC_CH_NUM] {0}; uint32_t sum_buf[ADC_CH_NUM] {0}; uint8_t sample_cnt 0; while (1) { if (adc_conv_flag) { adc_conv_flag 0; for (int i 0; i ADC_CH_NUM; i) { raw_value[i] adc_dma_buf[i] 0xFFFF; // 12位有效数据在低16位 sum_buf[i] raw_value[i]; } sample_cnt; if (sample_cnt ADC_SAMPLE_N) { for (int i 0; i ADC_CH_NUM; i) { raw_value[i] sum_buf[i] / ADC_SAMPLE_N; } // 此时 raw_value[0] 是气体raw_value[1] 是光照 System_Process(raw_value[0], raw_value[1]); sum_buf[0] 0; sum_buf[1] 0; sample_cnt 0; } } // 其他任务如 I2C 温湿度读取、串口上报、OLED 刷新 }这种做法的关键是 DMA 以循环模式持续写入缓冲区每一轮转换结束后触发回调。回调置标志主循环累加平均。10ms 触发一次 ADC8 次平均后得到一个有效值相当于 80ms 更新一次对于家庭环境监测完全够用。如果传感器信号变化快比如要监测人体红外引起的电压跳变可以把 ADC_SAMPLE_N 降到 4响应速度翻倍代价是噪声略大。另一个细节是 DMA 读取必须做 0xFFFF。因为外设数据寄存器是 32 位ADC 转换结果虽然只有 12 位但 DMA 按 Word 搬运时值存在低 16 位高位可能是残留数据。很多人读到的 ADC 值突然变成 6 万多就是因为没做掩码。3.3 数据校准与阈值判断把 ADC 读数换算成物理量ADC 原始值 0~4095 本身没有任何物理意义必须换算成电压再通过传感器标定曲线变成浓度或光照强度。对于非线性很强的传感器用查表法比公式法更靠谱。这里我给一个线性校准的做法#define VREF 3.3f #define ADC_RES 4095.0f typedef struct { float gain; // 电压到物理量的增益 float offset; // 零点偏移 } LinearCal; static float Sensor_Voltage(uint32_t raw) { return (float)raw * VREF / ADC_RES; } static float Sensor_Linear(LinearCal cal, float voltage) { return voltage * cal.gain cal.offset; } static int16_t Check_Threshold(float val, float upper, float lower) { static uint8_t state 0; if (state 0 val upper) { state 1; return 1; } else if (state 1 val lower) { state 0; return 0; } return state; }调用时先测一下传感器在清洁空气中的输出电压记为电压值 A然后把传感器放到一个已知浓度的气体环境中记下电压值 B通过这两点求出 gain 和 offset。如果手头没有标准气体可以先不标定直接上报原始电压后续在手机端或者上位机做二次换算。阈值判断用滞回比较是为了防止在报警临界点反复震荡。比如气体浓度达到 1% 报警回落到 0.6% 才解除这样蜂鸣器不会在 0.9%~1.0% 之间来回响。参数 upper 和 lower 的差值一般取量程的 5%~10%太大会让报警响应迟钝太小又压不住噪声。4. 让监测数据走出去串口协议、ESP8266 MQTT 与本地显示数据采回来只是第一步智能家居监测系统得让用户看得到消息。常见做法是本地 OLED 显示再通过 Wi-Fi 模块把数据推到 MQTT 服务器手机端订阅主题。这一章集中解决“数据怎么出去”的问题。一个最容易忽略的问题是不同 AT 固件的指令差异很大老固件没有 MQTT 指令需要先刷支持 MQTT 的固件否则后面全白搭。4.1 串口调试与数据帧协议设计如果只是调试用 printf 直接打印可读字符串确实快但一旦要接上位机或网关最好用二进制帧。我习惯定义帧头、长度、类型、负载、CRC16 校验。这样即使中间丢一个字节接收端也能通过帧头和 CRC 把坏帧丢掉不会把整条流数据解析乱。#define FRAME_HEAD1 0xA5 #define FRAME_HEAD2 0x5A #define FRAME_MAX 32 uint16_t crc16_modbus(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; } void Send_EnvFrame(float temp, float humi, float gas) { uint8_t frame[FRAME_MAX] {0}; int idx 0; frame[idx] FRAME_HEAD1; frame[idx] FRAME_HEAD2; frame[idx] 8; // 净荷长度temp 4字节 humi 4字节 frame[idx] 0x10; // 帧类型环境数据 memcpy(frame[idx], temp, 4); // 按小端字节序复制 float idx 4; memcpy(frame[idx], humi, 4); idx 4; uint16_t crc crc16_modbus(frame, idx); frame[idx] crc 0xFF; frame[idx] (crc 8) 0xFF; HAL_UART_Transmit(huart1, frame, idx, 1000); }这里用 float 的裸二进制传输好处是解析端只要按相同字节序 memcpy 就能拿回 float。坏处是不同硬件平台对 float 的内存布局可能有差异如果你要对接的是网页后台建议改成整数线性值。比如把温度乘以 10 转成 int16减少 float 传输的不确定性。HAL_UART_Transmit 最后一个参数是超时时间 1000ms如果放在主循环里碰到长字符串发送会阻塞最好后期改成中断或 DMA 发送。4.2 用 AT 指令控制 ESP8266 发送 MQTTESP8266 模块通过串口收 AT 指令STM32 只管发字符串。下面是一套典型的 AT 流程连接 Wi-Fi然后配置 MQTT 参数最后发布主题。密码和 SSID 我这里用宏占位实际项目里不要写死在源码里建议放到配置区。/* 连接 Wi-Fi等待 “WIFI GOT IP” */ char cmd[64]; snprintf(cmd, ATCWJAP\%s\,\%s\\r\n, WIFI_SSID, WIFI_PWD); ESP_SendCmd(cmd, WIFI GOT IP, 15000); /* 配置 MQTT 用户参数 */ snprintf(cmd, ATMQTTUSERCFG0,1,\%s\,\%s\,\%s\,0,0,\\\r\n, MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS); ESP_SendCmd(cmd, OK, 2000); /* 连接 MQTT 服务器 */ snprintf(cmd, ATMQTTCONN0,\%s\,1883,1,0,\%s\\r\n, MQTT_HOST, MQTT_CLIENT_ID); ESP_SendCmd(cmd, MQTT CONNECTED, 10000);ESP_SendCmd 函数里要注意不能只发送指令就结束还要等待模块返回预期字符串。常见做法是开一个环形接收缓冲主循环里做字符串匹配。很多新手卡在这一步是因为发送完 AT 指令后立刻去读串口返回数据还没到自然读不到。发布数据的代码更简单void ESP_Publish(const char *topic, const char *payload) { char cmd[160]; snprintf(cmd, ATMQTTPUB0,\%s\,\%s\,0,0,0\r\n, topic, payload); ESP_SendCmd(cmd, OK, 3000); } /* 组装 JSON 报文 */ char payload[64]; snprintf(payload, sizeof(payload), {\t\:%.1f,\h\:%.1f,\gas\:%.2f}, temp, humi, gas); ESP_Publish(home/env, payload);MQTT 发布的时候 QoS 参数选 0意思是只管发不管确认适合实时环境监测这种丢一条还能接受的场景。如果做的是烟雾报警这类安全事件建议 QoS 1确保消息到达但代价是模块会保存消息直到收到确认内存占用会变大。4.3 本地 OLED 显示与异常报警云平台再好人在家里还是希望抬头就能看到数据。我在这套系统里挂了 0.96 寸 OLED用 I2C 和 SHT30 共用 PB8/PB9。库一般自带初始化函数你只需要在 main 里调用显示刷新即可。void Display_Update(float temp, float humi, float gas) { char line[20]; snprintf(line, sizeof(line), T:%4.1fC H:%4.1f%%, temp, humi); OLED_Clear(); OLED_ShowString(0, 0, line); snprintf(line, sizeof(line), Gas:%4.2f%%, gas); OLED_ShowString(0, 2, line); }OLED 刷新间隔太长会显得卡太短则会有闪烁感。我一般把刷新周期放在 500ms 左右这样用户看起来是平滑更新也不会占用太多 I2C 总线带宽。蜂鸣器报警逻辑要放在环境处理函数里注意使用前面讲的滞回判断if (Check_Threshold(gas, 1.0f, 0.6f)) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); // 低电平触发蜂鸣器 } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); }这里的电平极性是低电平触发很多蜂鸣器模块正好相反接上之后如果不响先翻模块的说明书而不是改代码死循环。5. 监测系统代码移植与调试的 5 个常见坑避坑指南这套系统做完后真正花时间的是调试。下面这 5 个坑是我自己踩过、也常看到别人翻车的点。前三个集中在 ADC 和 DMA 这条信号链后两个在外围通信和低功耗。如果你跑代码时遇到“数据偶尔乱跳”“上电乱报警”大概率能在这里面找到原因。5.1 ADC 多通道首通道值漂移现象ADC 扫描模式下通道 0 的数据总是比实际值高或低后几个通道正常。换通道顺序后漂移也跟着跑到最前面。原因多通道转换时第一个通道的外部信号源阻抗较大采样电容还没完全充满就开始转换。尤其直接用高阻输出传感器时这种现象很明显。STM32 的 ADC 对信号源阻抗有一个上限超过之后采样误差会快速放大。解决CubeMX 里把每个通道的 Sampling Time 调大比如选 239.5 个周期硬件上在 ADC 引脚对地并联 100nF 电容。软件上更省事的方法是开启 DMA 后先忽略第一轮转换结果第二轮再开始累计。代码里可以在 DMA 启动后先清一次 flagHAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_dma_buf, ADC_CH_NUM); adc_conv_flag 0; // 丢弃第一轮转换结果5.2 DMA 缓存不同步导致数据“跳变”现象ADC 数据显示大多数时候正常但每隔一段时间突然出现一个很大的值或零值。原因DMA 在 Circular 模式下持续循环写入缓冲区主循环读取数据时DMA 可能已经把第 0 通道写成了新数据第 1 通道还没写完结果一组数据里既有新值又有旧值组合出一个“四不像”。解决最稳妥的是用 DMA 半传输和传输完成两个中断把缓冲区切成两个半区。主循环只在标志位正常时读取对应半区。示例static volatile uint8_t dma_half 0; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { dma_half 0; // 当前可以读缓冲区前半段 } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { dma_half 1; // 当前可以读缓冲区后半段 }这样每个半区的数据都是完整的一轮不会出现半新半旧。如果不想改中断可以在主循环读数据前短暂关中断读完再开但这对实时性要求高的场合不友好。5.3 传感器上电瞬间的尖峰误报警现象开发板一上电蜂鸣器立刻响气体浓度显示爆表过一阵子又恢复正常。原因气体传感器内部的加热丝冷态电阻很小上电瞬间电流很大会把电源电压拉低造成 ADC 的地和参考电压出现尖峰。同时传感器需要几十秒预热热稳定之前输出不在正常范围。解决代码里做两件事。一是上电后延时 30~60 秒预热期间不判断阈值二是报警逻辑加去抖连续多次超过阈值才真正报警static uint8_t alarm_cnt 0; if (value upper) { if (alarm_cnt 3) alarm 1; } else { alarm_cnt 0; alarm 0; }这个去抖次数要根据采样周期换算。采样周期 80ms连续 3 次就是 240ms既能滤掉毛刺又不至于太迟钝。如果你用的是继电器输出最好加一个上电延迟避免继电器在上电瞬间误动作。5.4 串口数据丢帧与 AT 应答不完整现象ESP8266 发送 MQTT 数据时偶尔发布失败AT 指令返回的字符串被截断成半段插拔一次串口后恢复正常但过一会又断。原因串口接收中断优先级太低被其他中断打断后丢字节或者 ESP8266 启动时串口有乱码没有等待稳定就发指令。另一个常见原因是 STM32 和 ESP8266 的 TX/RX 接反或者共地不好导致数据线噪声大。解决在 NVIC 里把 USART2 中断优先级调到最高用环形缓冲区接收 AT 应答主循环里做匹配不要在中断里做字符串解析。接法上STM32 的 TXD 接 ESP8266 的 RXDRXD 接 TXD并且一定要共地。还有一个血泪经验ESP8266 复位后需要大约 200~500ms 才准备好接收 AT 指令立刻发 AT 会收到空响应。5.5 低功耗模式下唤醒后 ADC 不准现象系统进入 STOP 低功耗模式用 RTC 唤醒后ADC 读数明显偏大要等好几秒才能恢复。原因STOP 模式下 ADC 的电源可能被关断唤醒后 ADC 内部校准寄存器丢失直接开始采样就会出现偏差。如果唤醒后系统时钟还在切换也会影响 ADC 时钟稳定。解决每次唤醒后重新校准 ADC并且等待时钟稳定后再启动采集HAL_RCC_ClockConfig(...); // 等待系统时钟切回 HSI/外部晶振 HAL_ADCEx_Calibration_Start(hadc1); // 重新校准 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_dma_buf, ADC_CH_NUM);这里多说一句低功耗不是简单调用 PWR_EnterSTOPMode 就完了。唤醒后所有外设都要重新初始化如果你想省电最好规划好“哪些外设掉电、哪些保持供电”。这套系统如果只是定时上传温度完全可以每 10 分钟唤醒一次采完立即再睡续航能提升不少。6. 让这套监测代码更抗造掉电存储、看门狗与远程配置技巧代码能跑通之后下一步是让它更抗造。这里推荐三个我觉得最值的进阶技巧内部 Flash 存校准参数、独立看门狗、远程配置阈值。第一个技巧把校准系数和报警阈值存到内部 Flash。外挂 EEPROM 当然也可以但 STM32 内部 Flash 足够存这些少量参数省一个 I2C 设备。写入前要擦除整个扇区所以不要频繁写只在参数变化时写一次。代码大致是#define FLASH_SAVE_ADDR 0x0800FC00 void Save_Cal(float gain, float offset) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase {0}; uint32_t err 0; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress FLASH_SAVE_ADDR; erase.NbPages 1; HAL_FLASHEx_Erase(erase, err); uint32_t data[2]; memcpy(data, gain, 4); memcpy((uint8_t *)data 4, offset, 4); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SAVE_ADDR, data[0]); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SAVE_ADDR 4, data[1]); HAL_FLASH_Lock(); }第二个技巧开独立看门狗 IWDG。传感器初始化卡死很常见比如 I2C 总线上某个模块没应答代码在 while 里等信号量整个系统就冻住。看门狗能强制复位。喂狗的位置要放在主循环最频繁执行的地方不要在长串口发送之前才喂否则发送卡住时看门狗先饿死复位后数据还没发出去。while (1) { HAL_IWDG_Refresh(hiwdg); // 放在主循环第一行 // 其余业务逻辑 }第三个技巧是远程配置。不需要每次改阈值都重新烧录固件。可以在 MQTT 订阅一个配置主题收到合法帧后更新阈值并写入 Flash。我一般还会加一个分隔符校验只有在帧里包含特定 magic 时才更新防止误触。最终结果是手机端下一条消息就能调整报警门限不用碰调试器。我后来养成的习惯是每次改完硬件或移植到新板子先把所有通道的裸读数通过串口打印出来确认硬件链路稳定再开始做校准和 MQTT。否则你永远不知道数据跳变是传感器问题、接线问题还是代码问题。这套思路帮我省了大量排查时间希望帮到你。本文还有配套的精品资源点击获取