
简介基于STM32与ESP8266的智能物联网家居系统项目资料主要面向嵌入式开发学习者、物联网方向学生以及需要快速搭建智能家居原型的工程师。项目以源码为核心聚焦STM32配合ESP8266实现设备控制、数据采集与无线网络通信可用于毕业设计、课程设计或项目练手。压缩包包含2000个文件大小约40.72MB核心文件包括80个C源码与86个H头文件用于STM32固件逻辑同时包含1511个JS、102个JSON及Vue/CSS等文件可能是物联网控制页面或上位机界面相关源码另有159个Markdown文档和若干配置文件方便阅读与工程部署。目前已有1862人学习/下载。资料从单片机底层驱动到上位机界面均有涉及目录中可见STM32标准外设库文件如stm32f10x_tim.c、stm32f10x_adc.c等能够帮助读者理解定时器、ADC、Flash、RCC等模块的初始化与使用方式整体具备较强的参考价值与自学引导性。1. 拆解一套STM32ESP8266智能家居源码的正确姿势这套“基于STM32ESP8266的智能物联网家居系统”源码包第一眼看上去没什么花哨压缩包里的 stm32f10x_tim.c、stm32f10x_rcc.c、stm32f10x_adc.c、stm32f10x_i2c.c、stm32f10x_flash.c 全是 ST 标准外设库的文件。但正因为如此它比那些套着 HAL 库的模板更值得拆——你看到的是底层外设的初始化顺序、中断配合和外设之间的数据流而不是被封装成一行行 API。项目的主角虽然是 ESP8266 联网但真正决定系统稳不稳定的是 STM32 如何把传感器数据采准、把 WiFi 参数存住、把控制指令消化掉。适合刚拿到智能家居毕设题目的学生也适合从 Cortex-M3 入手做物联网设备的老开发。2. 从源码反推硬件架构RCC/ADC/I2C/Flash 的外设分工没有原理图时我会先从文件列表反推硬件连接。这套工程把 ST 标准外设库直接编进来说明业务代码是在标准库上写的而不是 HAL。于是出现一个清晰的依赖图RCC 负责给其他外设提供时钟TIM 负责产生节拍ADC 负责采集光敏/烟雾这类模拟量I2C 负责挂载 OLED 或温湿度传感器Flash 负责保存 WiFi 配置和阈值ESP8266 通过 UART 单独挂在一边。下面按这个顺序把每条链路拆开。2.1 RCC 与 GPIO所有外设的地基RCC 是 STM32 启动后第一个要碰的外设。智能家居系统考虑到稳定性和功耗通常用 8MHz 无源晶振作为 HSE再倍频到 72MHz 系统时钟。这里有个容易踩的坑APB1 总线上的 I2C、USART2、TIM2/3/4 最高只能跑 36MHz所以分频必须是 /2APB2 上的 ADC 和 GPIO 最高 72MHz设为 /1。如果照抄别人的 RCC 配置而不看自己的晶振频率串口波特率会出现“配置对了但收不到数据”的怪问题。void SystemClock_Config(void) { ErrorStatus HSEStartUpStatus; RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); /* 打开外部高速晶振 */ HSEStartUpStatus RCC_WaitForHSEStartUp(); if (HSEStartUpStatus SUCCESS) { FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); FLASH_SetLatency(FLASH_Latency_2); /* 72MHz 时 Flash 需要 2 个等待周期 */ RCC_HCLKConfig(RCC_SYSCLK_Div1); /* AHB 时钟 72MHz */ RCC_PCLK1Config(RCC_HCLK_Div2); /* APB1 时钟 36MHz */ RCC_PCLK2Config(RCC_HCLK_Div1); /* APB2 时钟 72MHz */ RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); /* 8MHz * 9 72MHz */ RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET) {} RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); /* 切换系统时钟到 PLL */ while (RCC_GetSYSCLKSource() ! 0x08) {} } }这个函数看上去是模板代码但参数直接影响后面每个外设。RCC_PCLK1Config(RCC_HCLK_Div2)把 APB1 限制在 36MHzI2C 和 USART2 挂在这条总线上时钟如果超过规格外设可能直接不工作。RCC_PCLK2Config(RCC_HCLK_Div1)让 ADC 跑在 72MHz 的 APB2再通过 ADC 内部分频到 12MHz 左右。改晶振频率时需要同步调整RCC_PLLConfig的 PLL 倍频系数否则串口波特率会整体偏移。源码包里的库文件外设在智能家居中的职责stm32f10x_rcc.cRCC系统时钟分配外设时钟开关stm32f10x_tim.cTIM定时采样、PWM 调光/风扇调速stm32f10x_adc.cADC读取光敏电阻、烟雾传感器模拟量stm32f10x_i2c.cI2C读取数字温湿度传感器驱动 OLEDstm32f10x_flash.cFlash保存 WiFi 账号、配置阈值GPIO 的复用功能配置也依赖 RCC。ADC 引脚要设为GPIO_Mode_AINI2C 引脚要设为开漏复用输出UART 引脚要设为GPIO_Mode_AF_PP。这些模式没配对外设初始化后读到的永远是同一个值。很多“传感器没反应”的问题最后都出在 GPIO 模式而不是外设本身。2.2 ADC 通道分配模拟传感器如何进寄存器智能家居里常见的光敏电阻、土壤湿度模块、MQ 系列气体传感器输出的都是 0-3.3V 模拟电压。STM32F103 的 ADC 是 12 位分辨率约 0.8mV/bit测量温湿度模拟量足够。需要注意通道和引脚的对应关系例如 PA1 对应 ADC1_IN1PA4 对应 ADC1_IN4不能想当然地“接到 PA2 就一定是 ADC1_IN2”。如果引脚被其他复用功能占用ADC 数据会漂或者说读出来是满量程。采样时间也要按传感器输出阻抗来选。源码里如果沿用默认的 1.5 周期对高阻抗信号源来说电荷还没充满就被转换了结果就是同一光照条件下采样值跳几十个 LSB。我一般会先用ADC_SampleTime_55Cycles5跑通再根据实际波形往下调。如果使用电池供电还要注意 ADC 的参考电压 VREF 是否干净参考电压抖动比信号噪声更容易被忽略。2.3 I2C 挂载设备OLED 与数字温湿度传感器在同一套智能家居系统里I2C 总线最常见的用途是接 0.96 寸 OLED 和数字温湿度传感器。OLED 的从机地址通常是 0x3CAHT20 是 0x38SHT30 是 0x44。地址对不上I2C_CheckEvent就会一直等程序像死循环一样卡住。因此在写读写函数之前我会先用主机发送地址字节观察 ACK 是否回来这一步能在十分钟内排除硬件连接和芯片损坏的可能。硬件 I2C 对引脚模式有硬性要求SCL 和 SDA 必须配置成开漏输出外部再接 4.7kΩ 上拉电阻到 3.3V。如果直接复用推挽输出总线会被强制拉死。ESP8266 模块虽然也有 I2C 引脚但在这种架构里不要让两个控制器出现在同一条 I2C 总线上。ESP8266 只做网络通路传感器和显示都由 STM32 掌控这样排查问题时边界更清楚。2.4 ESP8266 的接口选型为什么走 UART 而不是 SPI很多人在搜“ESP8266与STM32连接原理图”时会疑惑ESP8266 明明支持 SPI为什么不走 SPI原因很简单绝大多数 ESP8266 模组出厂烧的是 AT 固件AT 固件的输入输出接口就是 UART。你要让它进入 SPI 透传模式得先刷特殊固件对毕设项目来说不划算。更实用的做法是STM32 的 USART2_TXPA2接 ESP8266 的 RXDUSART2_RXPA3接 ESP8266 的 TXD然后 GND 共地。供电是另一个高频坑。ESP8266 在连接 WiFi 的瞬间电流可以冲到 300mA如果直接用 STM32 开发板上的 3.3V 引脚供电电压会被拉低导致模块反复重启、AT 指令收不到响应。常见做法是给 ESP8266 单独用一片 AMS1117-3.3 降压供电输入端接 5V输出端并两个 10μF 和 0.1μF 电容。STM32 与 ESP8266 之间只需要三根线TX、RX、GND逻辑电平同为 3.3V不需要电平转换。3. 裸机外设驱动实战定时器、ADC、I2C 与 Flash 参数存储外设驱动写得好不好看两个地方一是初始化参数是否会根据硬件调整二是主循环是否被 delay 占满。这套源码包没有贴出完整业务代码但按 stm32f10x_tim.c 等文件的参与程度可以还原出一套裸机时间片调度方案。3.1 TIM 定时中断把主循环变成时间片调度器智能家居系统通常有三类周期任务高速采样10-50ms、传感器读取200-500ms、网络上报1-5s。如果用while(1){ delay(50); 采集; delay(200); 读传感器; }这种方式任何一次阻塞都会拖垮后面所有任务。标准做法是用一个定时器产生 1ms 中断维护一个全局 tick。volatile uint32_t g_tick_ms 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); g_tick_ms; } }初始化 TIM2 时这样配置void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; /* 72MHz 分频到 1MHz */ TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 1000 - 1; /* 1MHz 再计 1000 次 1ms */ TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }TIM_Prescaler72-1是关键参数72MHz 时钟被分成 1MHz计数器每 1μs 加一TIM_Period1000-1让计数器计到 999 就溢出这样一个中断就是 1ms。如果外部晶振不是 8MHzPLL 倍频不是 9这个 72 就得跟着改否则所有基于 tick 的超时判断都会变慢或变快。系统里每个任务只需要比较g_tick_ms与上次执行时刻例如每 500ms 读一次温湿度每 2000ms 通过 ESP8266 上报一次互不阻塞。3.2 ADC 多通道采样与滤波ADC 按单次转换模式读一个通道代码最简单适合只有光敏一个模拟传感器的场景。如果是烟雾、火焰、温湿度模拟输出并存就得考虑扫描加 DMA。先看单通道的样子void ADC1_Init(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); } uint16_t Read_ADC1(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET) {} return ADC_GetConversionValue(ADC1); }ADC_SampleTime_55Cycles5表示采样保持 55.5 个 ADC 时钟周期。对光敏电阻这类输出阻抗较高的传感器太短的采样时间会让内部采样电容充不满。读取任务里不要对单次采样值直接使用至少做四次取平均或者用一阶低通滤波value value * 0.8 new_sample * 0.2;。注意 0.8/0.2 要在整数上做移位近似比如value (value * 4 new_sample) / 5;避免浮点开销。采样时间参数适合场景实际效果ADC_SampleTime_1Cycles5低阻抗信号源速度快噪声大ADC_SampleTime_55Cycles5常见传感器输出速度与稳定性折中ADC_SampleTime_239Cycles5高阻抗信号源更稳定吞吐率下降如果多个模拟量需要采集把ADC_ScanConvMode置ENABLEADC_NbrOfChannel改成通道数再配一个 DMA 通道把结果搬到数组。采样时间较长的通道要排在优先级前面否则后一个通道还没采完就被打断。3.3 I2C 读传感器时的地址、上拉与错误恢复I2C 读一个寄存器的经典步骤是START发从机地址写位发寄存器地址重复 START发从机地址读位读数据STOP。标准库把每一步拆成了事件代码写起来很啰嗦但逻辑清楚void I2C1_Init(void) { I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_ClockSpeed 400000; /* 400kHz 快速模式 */ I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; /* 高低电平比 2:1 */ I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }I2C_ClockSpeed 400000意味着跑 400kHz 快速模式前提是总线电容不大、上拉电阻合适。如果 OLED 和传感器挂在一起线长超过 10cm我一般会降到 100kHz速度虽然慢但极少出现 ACK 丢失。硬件 I2C 最难受的是总线忙从机没释放 SDA或者代码在错误时机打断传输I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)会一直为 SET。这时候先把I2C_Cmd(I2C1, DISABLE)再重新初始化外设或者用 PB6/PB7 对应的 GPIO 手动拉几个时钟脉冲把卡住的从机释放掉。3.4 Flash 掉电保存参数持久化与磨损控制WiFi 账号、上报间隔、阈值这些参数不能每次都靠串口重新输入所以要用 Flash 保存。STM32F103 内部 Flash 按页擦除大容量型号一页是 2KB写操作最小单位是半字。一个简单但可用的函数是void Flash_WriteParam(uint32_t page_addr, uint16_t *buf, uint16_t len) { FLASH_Unlock(); FLASH_ErasePage(page_addr); for (uint16_t i 0; i len; i) { FLASH_ProgramHalfWord(page_addr i * 2, buf[i]); } FLASH_Lock(); }调用前先确认page_addr是页对齐的也就是能被 2048 整除。这个函数的代价是每次写入都先擦除整页如果在一个循环里不停保存阈值会迅速消耗 Flash 擦写寿命。常见做法是只在参数被用户修改后写一次并写两份备份启动时比较两份是否一致不一致就用默认参数。掉电瞬间如果正好在擦除数据可能损坏所以需要判断FLASH_GetFlagStatus(FLASH_FLAG_BSY)或者在关键写入前加一个掉电检测引脚等电容余电把写操作完成。4. ESP8266 联网AT 指令、MQTT 上报与断线重连4.1 串口收发状态机与 AT 指令握手ESP8266 的 AT 固件通过串口接收文本指令例如ATCWMODE1设置 Station 模式ATCWJAPssid,password连接路由器ATCIPSTARTTCP,192.168.1.100,1883建立 TCP 连接。AT 指令的难点不是发而是收不同的 AT 版本返回回车换行的方式不完全一样应答有OK、ERROR、busy p...还有主动上报的IPD数据。如果只是发送指令 - delay 500ms - 清理接收缓冲网络一慢就会丢失数据。一个实用的轮询处理函数可以把接收字节暂存到缓冲区再在每帧结束位置做关键词匹配void ESP8266_Poll(void) { static char line[256]; static uint16_t len 0; while (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) ! RESET) { char ch USART_ReceiveData(USART2); if (ch \n) { line[len] \0; if (strstr(line, IPD) ! NULL) { /* 从 IPD 后面的数据段里解析 MQTT 下发的控制指令 */ Parse_IPD(line); } else if (strstr(line, OK) ! NULL) { esp8266_state ESP_STATE_IDLE; } else if (strstr(line, ERROR) ! NULL) { esp8266_state ESP_STATE_CMD_FAIL; } len 0; } else { if (len sizeof(line) - 1) { line[len] ch; } } } }代码里用USART_FLAG_RXNE判断接收寄存器是否非空。把line凑到换行符再判断是因为 AT 指令的应答往往以\r\n分隔这样不会把半截数据拿来匹配。strstr找到IPD是入网数据的关键ESP8266 会以IPD,len:payload的形式把 MQTT 服务器下发的数据推到串口Parse_IPD需要先跳过逗号后的长度字段再按 topic 和 payload 的约定解析。实际工程里我会把这段逻辑移到状态机里而不是在主循环里暴力strstr但原理是一样的。4.2 MQTT/HTTP 上报选型ESP8266 和 STM32 的分工不同决定上报方式也不同。如果沿用 AT 固件可以直接用乐鑫新 AT 提供的 MQTT 指令如果想让 ESP8266 自己跑 MQTT 协议节省 STM32 内存就给它刷 NodeMCU 固件或 Arduino 固件STM32 只通过串口发 JSON 字符串。两类方式各有取舍。方案STM32 侧改动量ESP8266 固件适用场景AT MQTT 指令发 AT 指令并解析结果出厂/乐鑫 MQTT AT快速原型量小AT TCP 透传自组 HTTP/MQTT 报文出厂 AT自由度最高NodeMCU 固件只收发 JSON 字符串Lua 脚本需要本地逻辑快速迭代我一般会优先用 AT TCP 透传因为 AT 版本差异带来的踩坑最少。以 MQTT 为例先用ATCIPSTARTTCP,broker.emqx.io,1883建链再用ATCIPSEND长度发送 CONNECT 报文和 PUBLISH 报文。要注意 MQTT 报文的长度字段是可变长度的计算不对服务端会直接断开。如果你的 AT 固件支持ATMQTTCON那么发送方式会简化很多。ATMQTTCONlinkID,clientID,user,pass,host,port,secure ATMQTTSUBlinkID,topic,qos ATMQTTPUBlinkID,topic,data,qos,retainqos一般用 0 或 1。影响传感器数据上报场景用 QoS 0丢失一帧也无所谓控制指令下发用 QoS 1重试机制能保证继电器状态不丢。secure表示 TLS/SSL 是否开启开启会影响连接速度和内存占用局域网调试时先关掉。4.3 心跳、复位与独立看门狗兜底ESP8266 长时间运行后偶尔会假死AT 指令发出去没有响应。软件层可以做心跳每 30 秒发一次ATPING或者 MQTT 的 PINGREQ连续失败两次就拉低 ESP8266 的 RST 引脚重启模块。void ESP8266_Reset(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_12); /* 连接 ESP8266 RST 引脚 */ delay_ms(200); GPIO_SetBits(GPIOB, GPIO_Pin_12); ESP8266_WaitReady(2000); /* 等待模块返回 ready */ ESP8266_Init(); }这里的GPIOB_Pin_12是示例引脚实际以你的原理图为准。拉低 RST 的时间不能太短至少 100ms太短模块可能没有完成复位。重启之后不能立刻发 AT要等模块上电打印ready否则第一条指令会被吞掉。硬件层建议打开 STM32 的独立看门狗IWDG在主循环中喂狗一旦任何一个传感器读取函数、联网函数堵死看门狗把整机重启。这个兜底对无人值守的智能家居设备尤其重要。5. 从 Keil 到实物编译烧录与五个高频坑源码拿到手后真正决定成败的是从 Keil 到板子的这一小步。下面五个问题是我在类似 STM32ESP8266 项目里反复遇到的。坑一外部晶振电容计算失误系统实际上没有跑在 72MHz。8MHz 晶振通常配 10-22pF 负载电容PCB 走线越长电容越要偏小。如果串口打印乱码先看RCC_GetSYSCLKSource()是否返回 PLL再用 PA8 复用输出 MCO 引脚量一下时钟频率。不要一上来怀疑波特率配置。坑二ESP8266 和 USB 转串口共用 3.3V。ESP8266 启动时拉电流很大共用电源会在它连接 AP 时把电压拉低导致 STM32 也复位。解决办法是把 ESP8266 的 VCC 接到单独的 AMS1117-3.3且输入地必须和 STM32 共地。如果不共地串口电平参考点不同偶尔正常偶尔乱码。坑三ADC 采样值跳动问题不在代码而在参考电压。VREF 和 VDDA 需要单独接 0.1μF 和 1μF 电容如果和数字电源离得太近开关噪声会直接叠加在采样值上。另外模拟信号线不要在 ESP8266 的天线下方走线WiFi 发射时采集值会周期性跳变。坑四硬件 I2C 卡在 Busy。I2C 总线上电时如果 SCL 或 SDA 被拉低外设会一直报告忙。最直接的处理是复位 I2C 外设或者把引脚配成普通 GPIO 手动翻转 SCL 九次让从机释放总线。这里不要用大循环死等加个超时计数器超时后走软件 I2C 作为降级方案。坑五Flash 参数频繁丢失。内部 Flash 擦写寿命通常是一万到十万次如果业务代码在每次按键触发时都写一次参数很容易提前报废。我一般把最新参数放到 RAM同时做一份 Flash 备份只有掉电前或参数变化时才写。判断掉电可以用一个电压检测引脚电压低于阈值后禁止新的写操作。如果手头有逻辑分析仪先同时挂到 STM32 的 USART2 发送脚和 ESP8266 的 RXD看 AT 指令到底有没有从引脚上发出来这一步能排除掉至少一半的“模块没反应”问题。本文还有配套的精品资源点击获取