资讯动态

STM32 从 0 到 1 做智能家居(五):工程结构与代码骨架

发布时间:2026/10/27 2:03:28 来源:尧图企业网站定制
文章目录一、为什么不能把所有代码都塞进 main.c二、最终的文件构成三、把 App 加进 Keil两件事别只做一件3.1 .c 文件加进工程组3.2 .h 文件加进 Include Paths3.3 顺手勾上 Use MicroLIB四、写代码前必须懂的五个概念4.1 volatile中断和主循环共享的变量必须加4.2 HAL_GetTick()所有周期任务的闸门4.3 USER CODE 段只在这里写代码4.4 中断里只做最少的事4.5 状态机整机只有三个模式五、main.c 的四块骨架六、主循环的纪律七、头文件与命名规范7.1 每个驱动一对 .c/.h7.2 命名约定八、第一次编译与最小验证九、本章易错点总结十、小结与下一篇系列第五篇。CubeMX 已经把底层配好了但从这一篇开始代码要由我们自己组织。先搭骨架、定规矩再往里填驱动。这一篇不写具体传感器只解决一件事让代码有一个不会越写越乱的结构。一、为什么不能把所有代码都塞进 main.c我做第一个版本时main.c写到 900 多行DHT11 时序、I2C 读寄存器、PWM、串口解析、按键判断全在一起。当时觉得挺方便直到有一次 DHT11 读失败。我想单独测一下传感器结果发现根本没法测——一上电就初始化 WiFi初始化完还要等 3 秒传感器代码和主循环搅在一起注释掉哪一段都编译不过。后来我把它拆成三块底层初始化CubeMX 生成只读不改自己的驱动DHT11、ENS160、MQTT独立文件业务逻辑模式切换、自动控制留在main.c。拆完之后同一类问题只需要看一个文件。结构不是为了好看是为了让排错范围变小。二、最终的文件构成HAL_SmartHome\ ├─ App\ ← 你自己写的驱动CubeMX 不管 │ ├─ DHT11.c / DHT11.h 单总线温湿度 微秒延时 │ ├─ ens160.c / ens160.h I2C 空气质量 │ └─ mqtt.c / mqtt.h ESP-AT MQTT ├─ Core\ │ ├─ Inc\ main.h tim.h usart.h i2c.h gpio.h adc.h │ └─ Src\ main.c tim.c usart.c i2c.c gpio.c adc.c stm32f4xx_it.c ├─ Drivers\ ← HAL 库与 CMSIS只看不改 ├─ MDK-ARM\HAL_SmartHome.uvprojx ← Keil 工程 └─ HAL_SmartHome.ioc ← CubeMX 工程三层职责要分清楚层谁生成能不能改放什么Drivers\CubeMX不要改HAL 库、CMSISCore\CubeMX只能改USER CODE段初始化、中断入口、业务逻辑App\你自己随便改传感器、通信驱动这样分的好处很直接CubeMX 重新生成代码时只可能动Core永远动不到App。你辛苦调通的 DHT11 时序不会因为改了一个引脚就消失。三、把 App 加进 Keil两件事别只做一件3.1.c文件加进工程组Keil 左侧Project窗口右键Target 1→Manage Project Items新建组App组名和文件夹同名便于对应选中App→Add Files→ 加入DHT11.c、ens160.c、mqtt.c点 OK。.c是函数实现必须参与编译。只放文件夹不加工程链接时会报L6218E: Undefined symbol ReadData (referred from main.o)3.2.h文件加进 Include Paths头文件不参与编译靠的是搜索路径。在Options for Target → C/C → Include Paths里加入..\App ..\Core\Inc ..\Drivers\STM32F4xx_HAL_Driver\Inc ..\Drivers\CMSIS\Device\ST\STM32F4xx\Include ..\Drivers\CMSIS\Include其中后四条 CubeMX 生成工程时通常已经有了真正需要你手动补的是..\App。如果漏了它编译会报cannot open source input file DHT11.h补充一句把.h也 Add 进App组只是方便在工程树里双击查看不能代替 Include Paths。3.3 顺手勾上 Use MicroLIBOptions for Target → Target → 勾选 Use MicroLIB。不勾的话printf重定向后可能一个字符都没有或者程序卡在fputc里。这个坑在第二篇讲过这里再提醒一次因为它到这一章才真正开始影响你。四、写代码前必须懂的五个概念4.1volatile中断和主循环共享的变量必须加volatile的作用是告诉编译器“这个变量可能被别的地方改别把它优化到寄存器里。”本工程里这些变量全部是中断里改、主循环里读volatileuint8_tg_modeMODE_MANUAL;// 按键中断改主循环 switchvolatileuint8_tg_print_req0;// 按键中断置 1主循环清 0volatileuint8_tg_net_req0;// 中断请求切协议主循环执行volatileuint8_tesp_sub_ready0;// 串口中断置 1主循环处理不加会怎样开优化后主循环可能只在进入时读一次g_mode之后一直用寄存器里的旧值。现象就是按键明明触发了串口也打印了但模式就是不变。一个很典型的翻车现场代码逻辑完全正确调试器里看变量也确实变了但程序行为就是不跟着变。4.2HAL_GetTick()所有周期任务的闸门它返回开机以来的毫秒数uint32_t。本工程所有每隔一段时间做一次都靠它if(HAL_GetTick()-last_tick2000)// 2 秒到了{last_tickHAL_GetTick();Sensors_Update();}为什么要用减法而不是if(HAL_GetTick() last_tick 2000)因为HAL_GetTick()是 32 位大约 49.7 天会溢出归零。用无符号减法溢出时结果依然正确用加法比较则会在溢出点出现一次异常。这是个很少人提、但确实存在的坑。4.3 USER CODE 段只在这里写代码CubeMX 生成的main.c、tim.c、usart.c里只有USER CODE BEGIN/END之间是安全的。这条纪律从第二篇一直贯穿到最后每次点 GENERATE CODE 前都要检查。4.4 中断里只做最少的事中断服务函数的原则能一秒内做完就别做两秒的事。允许做的事置一个标志g_print_req 1读一个寄存器、存一个字节翻转一个 LED。绝对不允许HAL_Delay()等串口发送完成调用Mqtt_Server_Init()这种要十几秒的函数在中断里写while(...)等某个标志。本工程的按键中断只做一件事记录时间戳、判断消抖、置标志。真正的模式切换、命令执行全部放到主循环。这样按键永远是秒响应不会被 WiFi 初始化拖住。4.5 状态机整机只有三个模式#defineMODE_MANUAL0// 手动按键直接控制设备#defineMODE_AUTO1// 自动按温湿度和空气质量控制#defineMODE_REMOTE2// 远程蓝牙 / MQTT 控制三个模式决定了按键的含义、串口数据怎么处理、设备由谁控制。所以看这份代码有一个诀窍先找g_mode当前值再看对应的switch分支。同一个 KEY4在自动模式是进远程在手动模式是开关加热行为完全不同——不是代码写错了是模式不同。另外切换模式时必须先关输出风扇、加热、舵机归位。否则从自动切到手动时风扇可能还在转而手动标志却是关两边状态对不上后面会很难查。五、main.c 的四块骨架不要一上来就写满先按这四块搭好/* ① 定义区模式、阈值、全局状态 */#defineMODE_MANUAL0#defineMODE_AUTO1#defineMODE_REMOTE2volatileuint8_tg_modeMODE_MANUAL;// 上电默认手动/* ② 工具函数声明读传感器、打印、执行命令、切模式 */staticvoidSensors_Update(void);staticvoidPrint_Env(void);staticvoidRemote_Cmd_Exec(constchar*cmd,uint8_techo_back);staticvoidSwitch_Mode(uint8_tnew_mode);/* ③ 中断回调按键 串口接收 */voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin);voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart);/* ④ main()初始化 → while(1) 轮询 */intmain(void){HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();/* 其余外设初始化 ... */while(1){/* 只做判断标志 → 执行一次 → 立刻返回 */}}四个部分各自解决一个问题区块作用写错会怎样① 定义区统一模式、阈值、状态阈值散落各处改一个忘一个② 工具函数把一件事封装成一个函数主循环越写越长无法定位③ 中断回调只置标志快速返回中断里做耗时操作整机卡死④ main初始化和轮询调度顺序错、阻塞调用功能互相拖累关于static给函数加static表示只在本文件可见可以避免和别的文件重名。本工程的工具函数都只服务于main.c所以全部加static。六、主循环的纪律while(1)里有一条硬规则不允许任何形式的死等。不允许的写法while(USART2-SRUSART_SR_RXNE0);// 等一个字节卡死整机HAL_Delay(5000);// 等 5 秒按键全无响应while(!mqtt_ready);// 等 WiFi永远出不来允许的写法是查一下、没到就返回while(1){if(g_print_req){g_print_req0;Print_Env();}if(g_net_req){/* 切协议内部也必须是有限步骤 */}if(esp_sub_ready){esp_sub_ready0;Remote_Cmd_Exec(esp_sub_data,0);}if(HAL_GetTick()-last_sensor2000){last_sensorHAL_GetTick();Sensors_Update();}Mqtt_Poll();// 只看缓冲区有没有完整行没有就立刻返回}判断标准很简单问自己这个函数里有没有while等待或HAL_Delay如果有它就不该出现在主循环的快速路径里。WiFi 初始化要 10~16 秒怎么办不是把主循环卡住而是上电先等 3 秒让 ESP 启动完主循环里检测该初始化了进入初始化初始化期间用一个g_net_busy标志忽略按键避免指令流被打断初始化完成立刻回到正常轮询。这样最坏的情况是初始化这十几秒按键没反应而不是整机永久卡死。七、头文件与命名规范这一节内容不多但能帮你避免 80% 的重复定义和未定义报错。7.1 每个驱动一对.c/.h/* DHT11.h */#ifndef__DHT11_H#define__DHT11_H#includemain.hvoidDHT11_Init(void);uint8_tDHT11_ReadData(uint8_t*temp,uint8_t*humi);#endif函数声明放.h实现放.cmain.c只#include#ifndef/#define/#endif防重复包含少了它两个文件同时包含就会报重复定义头文件里不要定义变量。要共享变量在.c里定义在.h里写extern/* mqtt.c */volatileuint8_tesp_sub_ready0;/* mqtt.h */externvolatileuint8_tesp_sub_ready;如果直接把volatile uint8_t esp_sub_ready 0;写进.h每个包含它的.c都会生成一份链接时报multiply defined。7.2 命名约定前缀含义例子g_全局变量g_mode、g_fan_onMODE_模式常量MODE_AUTO大写宏阈值、引脚TEMP_HIGH、FAN_PWM大驼峰函数模块内操作DHT11_ReadData()统一命名不是为了好看是为了你三个月后回来还能一眼看出这是个全局状态还是局部临时变量。八、第一次编译与最小验证骨架搭好后不要直接写传感器。先做一次最小验证第一步编译空骨架三个.c里只放一个空函数或者先只建文件、写头文件声明main.c里#include它们编译。要求0 Error 0 Warning。这一步验证的是文件加对了、Include Paths 加对了、头文件保护写对了。第二步串口打印计数在main.c里实现fputc重定向到 USART1主循环里每 1 秒打印一次计数printf(tick %lu\r\n,HAL_GetTick()/1000);能看到数字稳定递增说明时钟、串口、MicroLIB、重定向全部正常。第三步LED 翻转用HAL_GPIO_TogglePin(LED_T_GPIO_Port, LED_T_Pin)每 500ms 翻一次。LED 会闪说明 GPIO 和HAL_GetTick()的节拍都对。这三步全过骨架就算立住了。后面每加一个驱动都只在这个骨架上挂一块出问题时范围很小。九、本章易错点总结现象真正原因处理链接报Undefined symbol ReadDataApp里的.c没加进 Keil 工程组Manage Project Items → Add Files编译报cannot open source input file DHT11.hInclude Paths 里没有..\AppC/C → Include Paths 添加printf完全没输出没勾 Use MicroLIB或没实现fputc重定向勾 MicroLIBfputc指向 USART1按键触发了但模式不变g_mode没加volatile被优化成寄存器缓存中断与主循环共享的变量全部加volatile链接报multiply defined变量定义写进了.h被多个.c包含.c里定义.h里用extern编译报重复定义/结构体重定义头文件没写#ifndef保护加头文件保护宏系统卡死十几秒按键无响应在中断或主循环里调用了Mqtt_Server_Init这类长耗时函数耗时操作拆成有限步骤或放到主循环主循环里等串口其他功能全停写了while(!收到数据);改成中断收 主循环查标志所有代码堆在main.c无法单独测试没拆驱动文件按App目录拆分一个模块一对.c/.h改了一次 CubeMX代码少了一半代码写在 USER CODE 标记外面只写在 BEGIN/END 之间上电后风扇自己转、状态混乱切换模式时没有先关输出Switch_Mode()里先清 CCR、清标志定时任务偶尔漏一次用HAL_GetTick() last 2000比较溢出点出错用HAL_GetTick() - last 2000十、小结与下一篇这一篇把代码的骨架立起来了Drivers / Core / App三层分工CubeMX 只动前两层.c加进工程组.h加进 Include Paths五个概念volatile、HAL_GetTick()、USER CODE、中断最小化、状态机主循环只做判断 执行 返回绝不阻塞头文件声明、extern、命名规范避免重复定义。下一篇开始写第一个真正的驱动DHT11 温湿度采集。你会看到delay_us为什么要自己写、40µs 采样点是怎么来的、以及为什么读完 40 位数据必须关中断。如果这篇文章对你有帮助欢迎点赞收藏。系列更新中下一篇见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑