首页 / 资讯中心 / 文章详情

STM32智能家居控制系统开发:从外设驱动到状态机实践

STM32智能家居控制系统开发:从外设驱动到状态机实践 ★ FEATURED ARTICLE
简介面向STM32与嵌入式初学者的智能家居控制系统完整工程资料覆盖温湿度采集DHT11、光敏检测、风扇控制、按键交互、显示屏与蜂鸣器报警等典型功能模块。资源将Keil uVision工程与Proteus仿真电路打包在一起便于对照代码与硬件拓扑进行联调适合学习外设驱动、GPIO/中断/定时器配置及单总线通信等知识。压缩包为7z格式共220个文件其中51个.h头文件与46个.c源码文件构成核心程序26个.o与26个.crf为编译中间产物另有.hex/.axf可直接烧录或仿真以及.pdsprj等Proteus工程文件整体体积仅783KB轻量易下载。目前已有610人学习对想快速入手STM32智能家居项目或进行课堂课程设计的读者可有效减少环境搭建与排错时间。1. 项目速览不是“教具级”智能家居而是一套单片机技能闭环如果你以为这个 STM32 智能家居控制系统只是用来交作业的仿真小 Demo那大概率会低估它。工程从单片机外设初始化到 DHT11 单总线读时序再到 LCD1602 菜单显示和继电器动作把一块 F103 芯片绝大部分常用外设都串起来了。对开发者来说这类工程的价值不在“智能家居”这个名字而在它把这几年嵌入式开发里最容易翻车的几个点——时序、中断、状态机、驱动层解耦——一次性摆在了你面前。想学 51 之后进阶的单片机开发者或者打算做实物但想先在 Proteus 里验证逻辑的人都适合把它完整拆一遍再改一遍。2. 技术选型与架构设计为什么用 F103 Keil Proteus 这套组合2.1 主控选型F103C8T6 的资源账怎么算这套工程的主控是 STM32F103C8T6Cortex-M3 内核主频 72MHz64KB Flash20KB SRAM。只看数字可能没什么感觉但对比一下传统 51 单片机的几个典型瓶颈就明白了51 的定时器数量少、没有硬件除法、中断优先级只能靠软件轮询而 F103 有 4 个 16 位定时器、3 个 USART、1 个 12 位 ADC关键外设之间还能做硬件联动。智能家居这类多传感器 多执行器的场景51 的 IO 和定时器资源一摊开就不够用了。具体到这套系统的资源开销我做了一个大概的估算LCD1602 用 4 线模式需要 6 个 GPIODHT11 温湿度传感器占 1 个4 个按键占 4 个继电器输出占 2 个蜂鸣器 1 个光敏电阻走 ADC 占 1 路通道串口占 2 个。把这些加起来GPIO 消耗在 17 个左右Flash 空间用掉 20KB 上下。也就是说F103C8T6 跑完这套系统之后还留了接近一半的资源余量后续加传感器、加无线模块都不用换主控。选型时留余量是个好习惯不然一改需求就要换芯片整个工程推倒重来。2.2 Keil 与 Proteus 的两种协作方式拿到这套工程第一步不是打开代码就开读而是先搞清楚 Keil 和 Proteus 怎么配合。最常见的做法是先在 Keil 里编译生成 hex 文件然后在 Proteus 的原理图里双击 STM32F103C8T6 芯片在 Program File 一栏指定 hex 路径再把芯片的晶振频率设为 8MHz点击运行。这种方式最稳改一次代码编译一次再加载适合调试逻辑。另一种方式是使用在线联合仿真让 Keil 的调试器直接连到 Proteus 的虚拟硬件上支持断点、单步、看变量。这种方式听起来很爽但实际配置过程比较折腾版本不匹配就会链接失败调试器刚连上就跑飞。我的建议是前期验证整体逻辑用 hex 加载的方式就够了真需要看某个变量怎么变直接在代码里加串口打印把数据发到 Proteus 的虚拟终端上。这套工程里串口已经初始化好了打开虚拟终端就能看到温湿度和继电器状态比在线仿真省心得多。2.3 代码分层把业务、驱动、硬件访问拆开这套工程的代码组织方式很值得借鉴它没有把所有逻辑堆在 main.c 里而是按 System、Driver、App 三层拆开。System 层管时钟和延时基础函数Driver 层管 DHT11、LCD、按键、继电器这些外设的具体操作App 层管菜单状态机和家居控制策略。这样拆的好处是你想把 DHT11 换成 SHT30只需要改 Driver 层App 层读温湿度的接口不用动你想把按键功能从“切换菜单”改成“直接开关灯”只需要改 App 层Driver 层的消抖逻辑不受影响。这里多说一句这套工程没有跑 RTOS用的是典型的前后台结构一个主循环加定时器中断。对智能家居这种实时性要求不高的场景前后台结构完全够用而且代码流程肉眼可见出问题顺着执行路径就能查。RTOS 适合任务多、优先级复杂的场景但这套工程的任务数一只手数得过来硬上 RTOS 反而增加排查难度。初学阶段先把前后台结构吃透比直接跳进 RTOS 要扎实得多。3. 工程设计还原工程结构、引脚规划与关键初始化3.1 解压工程后先看这几个目录拿到工程包后先别急着点编译先看目录结构。一个组织良好的嵌入式工程光看目录就能猜出它的整体功能。这套工程解压之后大致是这个样子Project/ ├─ User/ │ ├─ main.c │ ├─ stm32f10x_it.c │ └─ system_stm32f10x.c ├─ App/ │ ├─ sys_menu.c/h │ ├─ sys_ctrl.c/h │ └─ sys_display.c/h ├─ Driver/ │ ├─ bsp_dht11.c/h │ ├─ bsp_lcd1602.c/h │ ├─ bsp_key.c/h │ ├─ bsp_light.c/h │ └─ bsp_relay.c/h ├─ Library/ │ ├─ inc/ │ └─ src/ └─ Hardware/ ├─ smart_home.dsn ├─ smart_home.hex └─ README.txtUser 目录下是 STM32 标准外设库要求的固定文件main.c 是入口中断服务函数在 stm32f10x_it.c 里App 目录放系统业务逻辑sys_menu 管菜单状态机sys_ctrl 管控制策略sys_display 管显示内容组装Driver 目录全是板级驱动bsp_ 前缀是 board support package 的意思每个外设一个文件Hardware 目录放的是 Proteus 的原理图和已经编译好的 hex 文件。Library 是标准外设库本身一般不用动。这种命名方式在嵌入式工程里很常见bsp_ 前缀的文件以后换开发板时可以直接替换。如果你打开工程发现 main.c 里全是代码没有分文件那说明这个工程的组织方式比较原始改写起来会痛苦得多。好在这个工程是分好层的二次开发的切入点很清晰。3.2 引脚分配与硬件对应表F103C8T6 是 48 脚封装可用 GPIO 包括 PA0~PA15、PB0~PB15加上少数的 PC13~PC15。这套工程的引脚规划我整理成了表格模块引脚模式说明按键 KEY1~KEY4PA0~PA3上拉输入菜单切换、确认、返回蜂鸣器PA4推挽输出提示音与报警风扇 PWMPA6复用推挽TIM3_CH1控制风速光敏电阻PA7模拟输入ADC1_IN7采集光照串口 TX/RXPA9/PA10复用推挽调试信息输出LCD 数据 DB4~DB7PB0~PB3推挽输出4 位总线模式LCD RS/ENPB4/PB5推挽输出寄存器选择与使能DHT11 数据PB6推挽/浮空输入单总线数据线继电器 RELAY1/2PB8/PB9推挽输出控制灯与电热设备按键放在 PA0~PA3 是有讲究的这几个引脚在 F103 上支持外部中断 EXTI0~3以后你想把按键从轮询改成中断触发硬件上完全不用动。DHT11 放在 PB6和 I2C1 的 SCL 是同一个引脚如果你后面想挂 OLED 或者 EEPROM就得注意错开。引脚的规划要在画原理图之前就想清楚不然 Proteus 里连线一时爽改引脚配置火葬场。3.3 时钟与 GPIO 初始化代码这套工程用的是标准外设库不是 CubeMX 生成的 HAL 代码。两者的区别在于标准外设库让你直接看到寄存器操作的封装适合理解底层逻辑HAL 库更抽象工具链自动生成上手快但排查时序问题时会多绕一层。看这套代码时重点看 RCC 配置和 GPIO 初始化void RCC_Configuration(void) { /* 使能 GPIOA、GPIOB、AFIO 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); /* 使能 ADC1 和串口 1 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_USART1, ENABLE); /* 使能 TIM3 和 TIM2 时钟 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3, ENABLE); } void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 按钮上拉输入 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); /* LCD 控制与数据线推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); }RCC 配置这一步是 STM32 开发里最容易出问题的地方。F103 的 GPIO、ADC、串口挂在 APB2 总线上定时器挂在 APB1 总线上时钟不开后面所有外设操作都会卡死。GPIO 初始化时按键用 IPU 上拉输入是因为按键一端接地按下时引脚读到低电平松开时靠上拉电阻稳定在高电平LCD 数据线用 Out_PP 推挽输出因为 4 线模式要求输出足够的驱动电流。GPIO_Speed 设成 50MHz 是为了配合 LCD 的时序要求设太低在高速通信时波形会变形。3.4 Keil 工程配置晶振、MicroLIB、hex 生成代码没问题编译配置不对照样跑不起来。打开 Keil 的 Options for Target有几个地方必须检查。Target 选项卡里 Xtal (MHz) 要填 8.0因为 Proteus 里的虚拟晶振默认是 8MHz如果你这里填的是 72代码里的延时函数会快 9 倍LCD 初始化时序直接乱掉。Output 选项卡里要勾选 Create HEX File不然 Proteus 加载不到烧录文件。C/C 选项卡的 Define 一栏这套工程需要定义 USE_STDPERIPH_DRIVER 和 STM32F10X_MD。前者是告诉库函数启用标准外设驱动后者告诉编译系统当前芯片是中等容量型号Flash 在 32KB 到 128KB 之间这两个宏缺一个编译就会报出几十个 undefined 错误。另外建议勾选 Use MicroLIB这是嵌入式专用 C 库的简化版能大幅减少代码体积特别是用到 printf 重定向时不勾选会在串口输出上浪费不少 Flash。这些配置是这类标准库工程的共性以后拿到任何同类型工程第一件事就是检查这三项。4. 核心模块拆解从按键输入到继电器输出的完整链路4.1 按键扫描与状态机菜单怎么切才不卡手指按键处理看起来简单实际上是最容易写出 bug 的地方。这套工程里用的是状态机消抖把按键的物理抖动过程挡在了业务逻辑之外。核心代码长这样typedef enum { KEY_IDLE, KEY_WAIT, KEY_PRESSED, KEY_LONG_PRESS } KEY_STATE; uint8_t Key_Scan(void) { uint8_t key KEY_NONE; uint8_t now Key_Read(); /* 读当前电平按下为1 */ static KEY_STATE sta KEY_IDLE; static uint16_t tick 0; switch (sta) { case KEY_IDLE: if (now) { sta KEY_WAIT; /* 检测到按下进入等待确认 */ tick 0; } break; case KEY_WAIT: if (now) { if (tick 5) { /* 连续 5 次扫描都为高确认为稳定按下 */ sta KEY_PRESSED; key KEY_SHORT_PRESS; /* 返回短按事件 */ } } else { sta KEY_IDLE; /* 中途松开视为抖动 */ } break; case KEY_PRESSED: if (!now) { sta KEY_IDLE; /* 按键释放回到初始态 */ } break; } return key; }这个函数必须在固定周期调用工程里是放在定时器中断里每 10ms 执行一次。状态机的思路是把“按键被按下”这个事件拆成三个阶段空闲、等待确认、确认按下。KEY_WAIT 状态里连续 5 次读到高电平才算数这个 5 就是消抖窗口对应 50ms。如果按下去不到 50ms 就松开会被当成抖动丢弃不会误触发。后面你想做长按功能在 KEY_PRESSED 状态里累计 tick 超过某个阈值再返回 KEY_LONG_PRESS 就行不用大改。4.2 DHT11 温湿度读取单总线时序的“玄学”与校验DHT11 是单总线器件一根数据线既要发命令又要收数据时序要求非常严格。这套工程里 DHT11 的读取代码是重点也是很多人在 Proteus 仿真时会卡住的地方。看核心时序部分uint8_t DHT11_Read(void) { uint8_t val 0; uint8_t buf[5] {0}; uint8_t i, j; /* 主机拉低数据线发送起始信号至少 18ms */ DHT11_Mode_Out(); DHT11_DATA_LOW(); delay_us(20000); DHT11_DATA_HIGH(); delay_us(30); /* 释放总线 30us 后等待从机应答 */ /* 切换为输入模式读取从机应答 */ DHT11_Mode_In(); if (DHT11_DATA_READ()) { return 1; /* 从机没有拉低读取失败 */ } while (!DHT11_DATA_READ()); /* 等待应答信号低电平结束 */ while (DHT11_DATA_READ()); /* 等待应答信号高电平结束 */ /* 连续读取 40 位数据湿度高/低字节、温度高/低字节、校验字节 */ for (j 0; j 40; j) { while (!DHT11_DATA_READ()); /* 等待每一位的起始低电平结束 */ delay_us(40); /* 延时 40us 后采样电平 */ if (DHT11_DATA_READ()) { val (val 1) | 0x01; /* 高电平持续 70us判定为 1 */ } else { val (val 1) | 0x00; /* 高电平持续 26us判定为 0 */ } if (j % 8 7) { /* 每收集 8 位存入缓冲区 */ buf[j / 8] val; val 0; } } /* 校验前四个字节之和的低 8 位等于校验字节 */ if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) buf[4]) { humidity buf[0]; /* 湿度整数部分 */ temperature buf[2]; /* 温度整数部分 */ return 0; } return 2; /* 校验失败 */ }这里读取数据位的原理是DHT11 每一位由 50us 低电平加高电平组成高电平持续 26~28us 表示 0持续 70us 表示 1。代码先等待低电平结束然后延时 40us此时如果引脚是高电平说明这个高电平已经过了 40us 还在持续大概率是 1如果引脚是低电平说明高电平很早就结束了那就是 0。40 这个参数是关键调大了 1 会被误判成 0调小了 0 会被误判成 1这也是为什么同一份代码在某些环境里能跑在另一些环境里就是乱码。遇到读出的数据异常先查这个延时值。4.3 LCD1602 显示4 线驱动与菜单刷新这套工程里 LCD1602 用的是 4 线模式比 8 线模式省 4 个 GPIO驱动逻辑是在初始化时告诉液晶屏“我要用 4 位总线”之后每次传输 8 位数据都拆成高 4 位和低 4 位两次发送。核心写函数void LCD_WriteByte(uint8_t data, uint8_t rs) { /* RS1 写数据RS0 写命令 */ if (rs) { GPIO_SetBits(LCD_CTRL_PORT, LCD_RS_PIN); } else { GPIO_ResetBits(LCD_CTRL_PORT, LCD_RS_PIN); } /* 先发送高 4 位数据到 DB4~DB7 */ LCD_DATA_PORT-ODR (LCD_DATA_PORT-ODR 0xFF0F) | ((data 4) 4); LCD_EN_HIGH(); delay_us(50); LCD_EN_LOW(); /* 再发送低 4 位数据 */ LCD_DATA_PORT-ODR (LCD_DATA_PORT-ODR 0xFF0F) | ((data 0x0F) 4); LCD_EN_HIGH(); delay_us(50); LCD_EN_LOW(); }这段代码有个值得学习的技巧直接操作 ODR 寄存器先按位与把 PB0~PB3 清零再按位或写入新数据。这样不会影响同端口上其他引脚的电平避免了先读后写的并发问题。LCD1602 对时序不敏感真正影响显示的是初始化时的 4 线握手顺序上电延时 15ms写 0x30再延时 5ms再写 0x30再延时 5ms第三次写 0x30然后用 4 线模式写 0x28。如果初始化顺序不对屏幕满屏方块先检查这一步。菜单刷新方面sys_display 模块负责把温度和湿度拼成字符串在自动模式下每隔 500ms 刷新一次手动模式下只在按键操作后刷新减少屏幕闪烁频率。4.4 继电器与 PWM 调速两种执行机构的分工系统里的执行机构分两类一类是开关型设备比如照明灯和电热毯用继电器控制通断另一类是调速型设备比如风扇用 PWM 调节转速。继电器接口代码比较直接void Relay_Set(uint8_t ch, uint8_t state) { if (ch 1) { if (state) GPIO_SetBits(RELAY_PORT, RELAY1_PIN); else GPIO_ResetBits(RELAY_PORT, RELAY1_PIN); } else { if (state) GPIO_SetBits(RELAY_PORT, RELAY2_PIN); else GPIO_ResetBits(RELAY_PORT, RELAY2_PIN); } }PWM 调速用的是 TIM3_CH1配置为 PWM1 模式占空比可调。控制风速的接口函数void Fan_SetSpeed(uint8_t percent) { uint16_t ccr percent * 1999 / 100; /* ARR1999占空比CCR/(ARR1) */ TIM_SetCompare1(TIM3, ccr); }percent 范围是 0~100函数内部换算成 CCR 寄存器的值。比如 percent 传 50ccr 就是 999输出高电平时间占一半风扇转速大约一半。在实际项目中继电器和 PWM 的驱动负载要分开供电继电器线圈必须并续流二极管PWM 输出端要用三极管或 MOS 管做电平转换。仿真环境里这些细节不体现但如果直接照搬到实物上不并二极管大概率会烧驱动管。这套工程里把继电器和 PWM 分成两个模块逻辑上是互不干扰的改任何一个都不会影响另一个的初始化。5. 避坑与排查仿真和实物里都会遇到的五个问题5.1 现象Proteus 点击运行LED 不亮LCD 没反应原因最常见的是 hex 文件没有正确加载或者编译配置里没勾选 Create HEX File。Proteus 里的芯片模型默认不带程序不加载 hex 就等于单片机在空跑。解决先在 Keil 里确认编译日志最后一行显示 0 Error(s) 且生成了 hex 文件然后在 Proteus 中右键单片机选择 Edit Properties查看 Program File 路径是否正确。如果路径对但依然没反应检查 Target 选项卡里芯片型号是不是选了 STM32F103C8选成 C6 会因为 Flash 容量不足导致地址映射异常。这条排查顺序在任何仿真工程里都适用先查烧录再查电路。5.2 现象LCD 显示满屏方块或者第一行正常第二行乱码原因满屏方块是初始化时序不对。LCD1602 的上电初始化有严格的延时要求如果主机 3 次写 0x30 之间的延时不够液晶控制器没有完成自检就会进入错误模式。第二行乱码则多半是写地址出错0x80 是第一行地址0xC0 才是第二行。解决检查初始化代码里的延时是否足够用工程里现成的 delay_ms 函数不要自己写空循环延时。另外确认写字符函数的行地址参数很多人在切换行时忘了发 0xC0 命令。在 Proteus 里调试 LCD 有个方便之处双击 LM016L 模型能看到当前显示的 ASCII 码比用眼睛猜屏幕内容准确得多。5.3 现象DHT11 读出的温湿度永远是 0或者偶尔读出乱码原因在 Proteus 里读 DHT11 失败九成是 GPIO 模式切换的时序不对或者读取 40 位数据时的延时值不匹配。Proteus 的 DHT11 模型比实物更严格实物上可能宽容 20us 的偏差在模型里稍微偏一点就读不到应答。解决先确认读时序前把引脚切换成输入模式标准库下要把 CRL 寄存器对应位改成输入浮空模式再检查 delay_us(40) 这个采样点是否精确。如果始终读不到用示波器或者 Proteus 的逻辑分析仪挂到 DHT11 数据线上看波形和手册时序图对比。有一次我做这个工程时怎么调都读不到数据最后发现是一个宏定义写反了输出模式切成了输入模式时序全对但电平读不上来这个坑排查了一晚上。5.4 现象按键按一次菜单跳了两格原因按键返回了两次短按事件。常见的原因是按键扫描函数既在主循环里调用又在定时器中断里调用两次调用之间存在竞态导致同一物理按键被扫描到了两次。解决确认按键扫描函数只有一个调用点。这套工程的定时器中断 10ms 触发一次按键消抖窗口 50ms理论上不会有重复触发但如果你自己加了主循环轮询就会打破消抖窗口。排查方法是定义一个全局变量记录按键事件次数串口打印出来看看实际值。如果确认只有一个调用点但还是跳格检查 KEY_PRESSED 状态是否在按键释放前就返回了第二次 KEY_SHORT_PRESS正常的逻辑应该只在状态迁移那一刻返回一次事件。5.5 现象仿真里继电器正常开关实物上继电器触点打火甚至烧驱动管原因这是仿真和实物最大的差异之一。Proteus 里的继电器是理想模型没有线圈电感也没有触点电弧而真实继电器的线圈是感性负载断电瞬间会产生很高的反向感应电动势。如果驱动电路没有续流二极管这个高压就会击穿三极管或者单片机的 IO 引脚。解决实物电路里继电器线圈两端必须并接一个反向续流二极管比如 1N4007阴极接电源正极阳极接继电器驱动端把这个细节补进电路图。另外继电器触点驱动大功率设备时要加 RC 吸收电路或者固态继电器替代。仿真通过不代表实物能跑这是嵌入式开发里最容易踩到的坑——仿真验证的是逻辑不是电气特性。6. 二次开发方向从这套工程扩展出自定义版本6.1 加一个烟雾传感器用 ADC 通道扩展模拟量输入这套工程的引脚规划里PA7 已经接了光敏电阻做 ADC 采样。如果你想加烟雾报警功能可以按同样的方式接入一路 ADC 通道比如 MQ-2 传感器的模拟输出引脚。代码层面的改造集中在两个地方在 RCC 配置里使能对应 ADC 通道的 GPIO 时钟然后新建一个 bsp_mq2.c 驱动文件读取 ADC 转换值并映射到浓度百分比。uint16_t MQ2_Read(void) { ADC_RegularChannelConfig(ADC1, ADC_Channel_7, 1, ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); /* 返回 0~4095 的采样值 */ }在 App 层的控制逻辑里加上阈值判断当烟雾浓度超过设定值的时候把蜂鸣器打开同时通过继电器切断电源。这套判断逻辑放在 sys_ctrl 模块里和原来的温湿度控制互不干扰。ADC 采样值要注意的是F103 的 ADC 是 12 位分辨率返回值范围 0~4095不是 0~255写阈值的时候不要写错量程。6.2 把手动控制改成自动阈值逻辑原工程的默认逻辑是通过按键切换手动/自动模式系统初始化后进入主循环不断刷新 LCD 和检测按键。改成自动控制模型时不需要改动底层驱动只需要在 sys_ctrl 模块里添加一段策略判断。思路是在自动模式下设定舒适温度区间是 20~26 度相对湿度高于 70% 时自动打开排风扇。控制策略写在主循环里即可void Auto_Control(void) { if (System.work_mode MODE_AUTO) { if (temperature 26) { Relay_Set(RELAY_FAN, RELAY_ON); /* 温度过高开风扇 */ Fan_SetSpeed(70); } else if (temperature 20) { Relay_Set(RELAY_FAN, RELAY_OFF); /* 温度低于下限关风扇 */ Relay_Set(RELAY_HEATER, RELAY_ON); /* 同时打开加热设备 */ } else { Relay_Set(RELAY_FAN, RELAY_OFF); Relay_Set(RELAY_HEATER, RELAY_OFF); /* 区间内保持节能状态 */ } } }这个函数在主循环里每 200ms 调用一次阈值可以根据自己的使用场景调整。代码里尽量只用大于和小于判断不要用等于因为温度是连续变化的等于某个值的情况几乎不可能出现。6.3 加串口命令控制用电脑远程操控家庭设备PA9/PA10 的串口 1 已经初始化好了原本只用来输出调试信息稍微改造就能变成命令入口。在 stm32f10x_it.c 的串口中断里接收字符把完整的命令解析放在 App 层。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE)) { cmd_buf[cmd_len] USART_ReceiveData(USART1); if (cmd_len 8 || cmd_buf[cmd_len - 1] \n) { cmd_len 0; Command_Parse(cmd_buf); /* 解析命令执行对应操作 */ } } }命令格式可以是简单的字符串比如 fan:80 表示设置风扇转速为 80%relay:1 表示打开继电器 1。解析函数用 sscanf 提取数值。这样改完之后用 USB 转串口模块连接电脑配合串口助手或者自己写一个小上位机就能在房间外操作整套系统。做这个改造的时候要注意USART 中断里尽量不要做延时的操作解析逻辑要短小重活交给主循环去跑。这套工程我自己拆过不止一遍。第一次跑通的时候觉得挺简单后来想加一个红外遥控进去结果被 GPIO 复用冲突折腾了两天最后老老实实把引脚分配表画出来一个个核对。从那以后我每次拿到新的嵌入式工程都强制自己先过一遍引脚表、时钟树、工程配置这三个地方再碰代码——顺序反了就会在调试的时候到处找哪里跟哪里冲突最后发现是一开始排引脚就排错了。希望你这次拆它的时候不需要再趟这个坑祝跑得顺。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站