简介:基于STM32的智能家居系统源代码与配套文档,面向物联网、嵌入式方向的在校学生及开发者,适用于课程设计、毕业设计、项目初期开发等场景。项目实现温湿度、光照、可燃气体浓度等居家数据的实时采集,依据检测结果联动舵机、电机、蜂鸣器、继电器等外设,完成自动窗帘、高温自动开窗散热、燃气泄漏声光报警与排风等智能联动功能,并配套四点三寸电容屏本地UI,数据可上传至云端数据库,通过前端页面与手机远程监控和控制电器。包内共四百三十一个文件,以C与H源码文件为主体(含两百一十个H文件、一百六十六个C文件),涵盖外设驱动、系统组件、主控逻辑与工程配置;另含PNG效果图、Markdown说明文档及Keil工程文件,压缩包约四点六五MB,目录结构分明,便于检索和二次开发。目前已有四百二十七人学习下载,代码均经测试运行成功,既适合初学者对照学习,也可在现有框架上扩展更多智能场景。
1. 它不是一个Demo,而是一套可裁剪的家庭控制骨架
“基于STM32的智能家居系统”是嵌入式学习路径上最不缺的项目,但也是最容易做废的题目。市面上大量开源资料把传感器、屏幕、继电器堆在一起,跑通就发帖,真正能放进家里连续运行一周、敢说“坏了我知道怎么查”的,并不多。这篇文章要拆的,正是这个标题背后最常见的可靠做法:用STM32做控制核心,采集温湿度、光照和燃气浓度,驱动继电器控制灯和风扇,用OLED显示实时状态,再用蓝牙串口给手机下发指令。核心不在于“智能”两个字,而在于你手里的MCU如何用有限的中断、定时器和GPIO,把多路外设调度得不出乱子。本文适合做过基本点灯实验、想往综合项目迈一步的开发者,也适合准备把这类系统作为毕业设计框架的人——你能从这里拿走的,是选型依据、初始化顺序、状态机设计和一套能直接改的代码骨架。
2. STM32智能家居系统怎么拆:硬件选型与最小系统设计
2.1 主控选型的边界:为什么C8T6是多数人的默认起点
标题只写了STM32,没指定型号,这给开发留了余地,也给新手埋了选型坑。常见做法是用STM32F103C8T6,因为它48脚封装、64KB Flash、20KB RAM,做传感器采集和继电器控制绰绰有余,而且市面上最小系统板价格低、资料多、调试工具透明。如果你的系统要加摄像头、跑人形检测、挂音频,那就不该选它,直接跳向STM32F429或H7系列。判断依据很简单:先把所有外设的数据量估一遍,比如DHT11温湿度每秒最多40bit,MQ-2燃气传感器输出是模拟量,一个ADC通道就够,OLED屏幕一屏显示也就1KB左右,这些数据对C8T6的负载几乎可以忽略。
2.2 外设器件清单与理由
| 模块 | 型号/接口 | 作用 | 选型理由 |
|---|---|---|---|
| 温湿度传感器 | DHT11,单总线 | 采集室内温湿度 | 协议简单、代码成熟,适合跑通第一版 |
| 燃气/烟雾传感器 | MQ-2,ADC输出 | 检测可燃气体浓度 | 模拟量输出,便于用ADC做阈值判断 |
| 光照传感器 | 光敏电阻+电阻分压 | 感知环境光强度 | 电路简单,一个GPIO加上ADC即可 |
| 继电器 | 5V低电平触发,常开/常闭 | 控制灯具、风扇、插座 | 强弱电隔离,扩展场景最多 |
| 显示 | OLED SSD1306,I2C | 本地状态展示 | 4针接线,I2C总线可挂多设备 |
| 通信 | HC-05蓝牙或ESP8266 | 手机端下发指令 | 与STM32串口直连,协议自己定义 |
| 供电 | 5V/2A适配器+AMS1117降压 | 给传感器和逻辑供电 | 继电器模块需要5V,STM32板载3.3V稳压 |
选型时要特别留意继电器的驱动方式,5V继电器不能直接由STM32的3.3V GPIO驱动,必须经过三极管或光耦隔离,否则拉不动线圈,还会反噬GPIO。常见的继电器模块大多自带驱动电路,直接接上VCC、GND、IN三个引脚即可工作。
2.3 从零开始搭建可复现的STM32开发环境
开发环境不是文章重点,但无数工程死在这个环节。标准做法是STM32CubeMX生成初始化代码,Keil MDK做编译下载。打开CubeMX后选择芯片型号,按下图思路配置:
RCC -> HSE -> Crystal/Ceramic Resonator SYS -> Debug -> Serial Wire I2C1 -> Standard Mode,默认100KHz ADC1 -> IN1(光敏电阻分压点) USART1 -> Asynchronous,波特率115200 GPIO -> PA0(继电器)、PA1(风扇)、PA2(蜂鸣器)设为Output这个配置完成CubeMX会自动生成main.c的初始化骨架。需要解释的是Debug模式必须选Serial Wire,否则编译下载正常,但第二次烧录会提示找不到芯片——选错了等于把SWD引脚复用成了普通IO,一次性锁死调试口。
用Keil打开生成工程后,第一个该验证的不是业务代码,而是点灯。把HAL_GPIO_TogglePin放进主循环,确认GPIO输出正常,再进行下一步。整套环境验证通过后,再开始写传感器驱动,这样能隔离“硬件没接对”和“代码写错”两类问题。
3. 核心驱动与底层初始化:定时器、ADC与串口的正确打开方式
3.1 定时器不是用来计时的,是用来做精密延时的
DHT11的时序要求微秒级延时,标准库的Delay函数精度不够,HAL库的HAL_Delay只支持毫秒。解决方式是用定时器做微秒延时,基础定时器TIM4配置为1MHz计数时钟,__HAL_TIM_SET_COUNTER实现计时。
void TIM4_Init(void) { htim4.Instance = TIM4; htim4.Init.Prescaler = 72 - 1; // 72MHz/72 = 1MHz htim4.Init.CounterMode = TIM_COUNTERMODE_UP; htim4.Init.Period = 0xFFFF - 1; // 最大计数区间 HAL_TIM_Base_Init(&htim4); } void delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(&htim4, 0); while (__HAL_TIM_GET_COUNTER(&htim4) < us); }这段代码的Prescaler=71将72MHz系统时钟分频为1MHz,计数值每增加1代表1微秒。查询计数器值小于目标延时在执行循环,误差约1-2个时钟周期,完全满足DHT11的时序要求。常见误用是把HAL_Delay直接用到单总线协议里,在优化等级高的编译环境下会出现莫名其妙的读值错误。
3.2 光照与燃气采集:ADC的DMA读数与平均滤波
MQ-2和光敏电阻都是模拟量,STM32的ADC可以顺序扫描多通道。仅读取单次值时,直接调用HAL_ADC_GetValue没有大问题,但无人值守的智能家居场景里,传感器信号会有抖动,应当开启DMA连续采样配合软件平均。
uint32_t adc_buf[10]; void ADC_DMA_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 10); } uint16_t get_adc_average(void) { uint32_t sum = 0; for (int i = 0; i < 10; i++) { sum += adc_buf[i]; } return (uint16_t)(sum / 10); }代码里ContinuousConvMode设为使能后,ADC会始终把采样值写入DMA缓冲区,循环覆盖。取平均前必须先确认采样值写入稳定,否则前几次读到的全是零。adc_buf的类型是uint32_t,这是HAL库对DMA传输位宽的要求,用uint16_t接收会导致数据错位。
3.3 DHT11时序解析:单总线协议的一次完整握手
DHT11是智能家居项目里最容易翻车的传感器,尤其是时序判断和超时处理。驱动代码的核心在于把一次数据读取拆成“主机拉低→释放→传感器响应→40bit数据”四个阶段,每个阶段都用微秒延时函数做边界判定。
uint8_t DHT11_Read(void) { uint8_t data[5] = {0}; GPIO_OutputLow(); HAL_Delay(18); // 主机拉低至少18ms GPIO_OutputHigh(); delay_us(30); // 释放总线 if (GPIO_ReadPin() == 0) { // 检查传感器响应信号 while (GPIO_ReadPin() == 0); // 等待低电平结束 while (GPIO_ReadPin() == 1); // 等待高电平结束 for (int i = 0; i < 40; i++) { while (GPIO_ReadPin() == 0); delay_us(40); if (GPIO_ReadPin() == 1) { data[i / 8] |= (1 << (7 - (i % 8))); } while (GPIO_ReadPin() == 1); } } // 校验 data[0]+data[1]+data[2]+data[3] == data[4] if ((data[0] + data[1] + data[2] + data[3]) == data[4]) { return data[2] * 256 + data[3]; // 温度和湿度的高低字节组合 } return 0xFFFF; }这里的细节在于delay_us(40)之后的电平判断:DHT11用28微秒高电平表示0,70微秒高电平表示1,采样点放在40微秒处能稳定区分两者。响应信号的检测顺序也必须和手册时序一致,先低后高,顺序错一次,后面40个bit全乱。还有一个常见坑:DHT11对供电电压毛刺敏感,供电线超过20厘米就容易读值漂移,调试时在VCC和GND之间加10uF电解电容,问题往往自己消失。
4. 业务逻辑与STM32智能家居系统的状态机设计
4.1 手动与自动双模式的控制流程
智能家居系统调试的难点,在于多路传感器、多路执行器交织在一起,线性的if-else逻辑一旦超过三层,后期加需求几乎不可维护。常见的做法是维护一个系统状态结构体,将工作模式、传感器阈值、当前状态全部收拢到一个位置。
typedef enum { MODE_AUTO, MODE_MANUAL } SystemMode; typedef struct { uint8_t temperature; uint8_t humidity; uint16_t gas_value; uint16_t light_value; SystemMode mode; uint8_t relay_state; uint8_t fan_state; uint8_t alarm_state; } HouseStatus; HouseStatus house = { .mode = MODE_AUTO, .relay_state = 0, .fan_state = 0, .alarm_state = 0 };把全局状态独立出来,控制逻辑只负责修改HouseStatus,显示逻辑只负责读取,再分开两个函数实现。这样做的直接回报是:手动模式下你改继电器开关,OLED上的状态自动跟着刷新,不会出现“屏幕显示关灯实际灯还亮着”的分离问题。
4.2 自动控制中的阈值判定与迟滞比较
自动模式不能简单写成“温度超过30度就开风扇”,因为温度在阈值附近抖动时,继电器会频繁吸合,触点寿命被快速消耗。需要引入迟滞比较,也叫滞回控制。
void Auto_Control(void) { if (house.mode != MODE_AUTO) return; // 温度控制风扇,迟滞3度 if (house.temperature > 28) { house.fan_state = 1; } else if (house.temperature < 25) { house.fan_state = 0; } // 燃气浓度控制蜂鸣器,迟滞50个单位 if (house.gas_value > 800) { house.alarm_state = 1; } else if (house.gas_value < 750) { house.alarm_state = 0; } HAL_GPIO_WritePin(FAN_GPIO_Port, FAN_Pin, house.fan_state); HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, house.alarm_state); }迟滞区间的宽度要结合被控设备的物理特性:风扇起停的迟滞范围设大(3到5度),因为温度变化本身慢;燃气报警的迟滞要小(50个ADC单位),因为燃气浓度上升是危险信号,报警后要尽快复位但不能来回报警。另一个要点是阈值读取走get_adc_average,直接拿原始值比较会被采样噪声干扰到迟滞形同虚设。
4.3 蓝牙串口指令解析:OLED状态同步与手动联动
手机通过蓝牙模块发来的数据是一串字符流,直接对收到的字节做判断会踩进“半包”“粘包”的坑。指令协议需要自定义帧格式,常用方案是固定帧头+指令位+校验位。
uint8_t uart_rx_buf[16]; uint8_t rx_index = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { uint8_t byte = uart_rx_buf[0]; if (byte == 0xAA) { // 帧头 rx_index = 0; } rx_buffer[rx_index++] = byte; if (rx_index >= 4) { if (rx_buffer[3] == (rx_buffer[1] ^ rx_buffer[2])) { Process_Command(rx_buffer[1], rx_buffer[2]); } rx_index = 0; } HAL_UART_Receive_IT(&huart1, uart_rx_buf, 1); } }void Process_Command(uint8_t cmd, uint8_t param) { if (cmd == 0x01) { // 01: 模式切换 house.mode = param ? MODE_MANUAL : MODE_AUTO; } else if (cmd == 0x02) { // 02: 继电器开关 house.relay_state = param; HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, param); } else if (cmd == 0x03) { // 03: 查询全部状态 char str[64]; sprintf(str, "T:%d,H:%d,G:%d,L:%d,Mode:%d", house.temperature, house.humidity, house.gas_value, house.light_value, house.mode); HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), 100); } OLED_Refresh(); }rx_buffer[3]是校验位,这里用异或校验而不是累加和,因为异或实现简单、计算量小,适合资源受限的MCU。指令帧设计为4字节定长,避免处理变长帧的缓存边界问题。代码里的OLED_Refresh放在每条指令执行完之后,只为保证手动控制后屏幕上显示的状态立刻可见。
4.4 主循环调度:轮询加中断的边界划分
整个系统的调度框架采用“主循环轮询传感器+串口中断收指令”的方式。定时器中断里只做计时标记,不在中断里跑DHT11读取或OLED刷新,因为这些操作耗时过长,会阻断其他中断响应。
while (1) { if (dht_tick >= 2000) { // 每2秒读一次温湿度 house.temperature = DHT11_Read_Temperature(); house.humidity = DHT11_Read_Humidity(); dht_tick = 0; } if (adc_tick >= 500) { // 每500ms读一次ADC house.gas_value = get_adc_average(); adc_tick = 0; } Auto_Control(); OLED_Refresh(); HAL_Delay(10); }这里把传感器读取的周期错开:DHT11每次读取耗时在20ms级别,ADC的DMA采样全程不阻塞CPU,OLED刷新虽然慢但数据量小。三个周期用不同时间片错开,系统仍然能保持串口中断的实时响应,不会出现蓝牙指令被长时间延迟执行的情况。主循环的调度周期不能无限缩短,一般10ms足够人机交互,再小只会浪费CPU空转。
5. 联调中排查键的3个方向与进阶部署
5.1 调试前的必备工具配置
程序下载用ST-Link,串口监视用USB转TTL模块,这两个是最低配置。联调时把HAL_UART_Transmit打点信息放在关键状态切换处——不要怀疑打印是否多余,没有串口输出,你根本分不清是DHT11时序问题还是MQ-2的预热漂移。用串口助手观察原始接收字节,用逻辑分析仪抓DHT11波形,确认时序和手册一致后再怀疑代码逻辑。
5.2 三个反复出现的故障场景与修复顺序
从多个类似项目中总结出的高频故障,按排查优先级排列:
第一优先级:继电器不动作。先测模块上的指示灯是否亮,再用万用表量IN引脚电压,最后查GPIO配置是否漏了GPIO_MODE_OUTPUT_PP。继电器模块供电一定要用5V,3.3V供电时线圈电压不足,表现为偶尔吸合偶尔不吸合。
第二优先级:OLED亮屏不显示字。先减慢I2C时钟到100KHz,再确认地址是0x78还是0x7A,两种屏幕物理地址不同。如果屏幕显示乱码,检查DMA和中断是否占用了I2C总线,中断里刷新屏幕的优先级一定高于显示内容本身。
第三优先级:DHT11数据偶尔跳变。给传感器加装滤波电容,数据线长度缩短到20cm以内,读取间隔拉长到2秒以上。若仍然跳变,把DHT11换成AHT20可以解决,但代价是驱动代码需要适配新的I2C协议。
5.3 移植与扩展:把系统从F103换到低功耗/强算力芯片
关键词“STM32系列”覆盖面很广,不同型号间移植要抓住三个点:引脚复用表、时钟树配置、外设句柄。以STM32L4系列为例,它多了低功耗模式,进入STOP模式前需要把所有外设时钟关闭,唤醒后再重新初始化I2C和UART。这套系统迁移过去的方法是:CubeMX重新生成引脚配置,保留HouseStatus结构体和业务逻辑不动,驱动层只替换延时函数为HAL_Delay适配后的版本。
5.4 验证系统长期运行稳定性的一个收尾技巧
完成功能后,别急着收工,做一轮96小时持续运行测试:串口每隔一分钟输出一次温度、ADC值和工作模式,数据存到电脑日志,第二天早上检查是否有无响应时间段。还可以在程序里加一个看门狗:独立看门狗IWDG在每5秒内必须刷新一次,哪怕传感器挂了、DHT11卡在while循环里,系统也会在复位后自动恢复。这个操作,比任何断言代码都更能保护你不被家人半夜叫醒。
配置IWDG的方式是HAL_IWDG_Init将预分频器设为64,重装载值设为3125,对应的喂狗周期是3100毫秒,这样即使主循环被某段阻塞代码拖住4秒,系统仍然能自动复位而不是永久卡死。调试时喂狗放在主循环最末尾,如果代码死在某个子程序里,狗先叫,问题定位范围就缩到了那次循环周期内。
本文还有配套的精品资源,点击获取