STM32 从 0 到 1 做智能家居(四):用 CubeMX 把工程骨架搭起来

本文详细讲解使用STM32CubeMX为STM32F411RETx芯片搭建智能家居工程骨架的关键步骤:包括芯片选型、时钟树配置(25MHz HSE→100MHz主频)、GPIO User Label规范、TIM3/TIM5 PWM定时器配置(1kHz风扇/50Hz舵机)、USART多串口分工(调试/蓝牙/语音)、I2C3(ENS160传感器)、ADC预留及NVIC中断优先级设定。强调参数准确性对后续外设功能的决定性影响。

系列第四篇。这一章决定后面所有代码能不能跑:时钟、引脚、定时器、串口、I2C、中断优先级,全部在这里定死。

建议完全照做,不要凭感觉改参数。这一章改错一个数字,后面可能要花一整天去找。

一、为什么这一章必须"照抄"

先说一个我真实踩过的坑。

我第一次用 CubeMX 配时钟,核心板上明明是 25MHz 晶振,我在 Input frequency 里顺手填了默认的 8MHz。编译、烧录、串口打开——全是乱码。

当时我以为是串口线的问题,换了三根线、换了两个串口助手、重装了 CH340 驱动,最后才发现:时钟频率错了,波特率就跟着错,打印出来必然是乱码。

类似的还有:

  • PLL 参数填错,主频不是 100MHz,delay_us 全部失准,DHT11 永远读不出来;
  • 定时器 Period 少填 1,PWM 占空比换算全变,风扇该 80% 却只有 8%;
  • I2C3 引脚改到别的口,ENS160 死活读不到。

这些问题共同的特点是:编译不报错,运行没现象,而且极难往配置上想。

所以这一章的目标不是"讲原理",而是"配完能跑,并且知道每个数字为什么是这个值"。

二、新建工程(路径有个隐藏陷阱)

2.1 选芯片

  1. CubeMX → File → New Project;
  2. 在 Part Number 里输入 STM32F411RET;
  3. 选中 STM32F411RETx(LQFP64),点 Start Project。

注意别选成 STM32F411CEUx(LQFP48)或 STM32F411VETx(LQFP100),封装不同,可用引脚完全不同。

2.2 Project Manager 四项设置

项目填什么
Project NameHAL_SmartHome
Project Location...\8.HAL_SmartHome\HAL_SmartHome
Application StructureAdvanced
Toolchain / IDEMDK-ARM,Version 选 V5

2.3 Code Generator 三项必勾

切到 Code Generator 页:

  • 勾选 Copy only the necessary library files(只拷贝用到的库,工程体积小);
  • 勾选 Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral(每个外设单独生成 tim.c、usart.c、i2c.c、gpio.c、adc.c);
  • 勾选 Keep User Code when re-generating(保留 USER CODE 段,这条是命根子)。

还有一个选项容易让人犯迷糊:Generate Under Root。它藏在 Toolchain / IDE 那一行的最右边,和 Min Version 并排,字很小。

结构这个选项的状态生成位置
Advanced(本项目)灰色,不可点直接生成在 Project Location 下:.ioc、Core\、Drivers\、MDK-ARM\
Basic可选不勾选会多套一层同名文件夹;勾选才直接放在根目录

所以本项目选 Advanced 之后,这个勾不用管,也点不了。真正要确认的是生成完后的实际位置:.ioc、Core\、MDK-ARM\ 三个东西应该和你在 Project Location 里填的目录在同一层。

如果发现多套了一层同名文件夹,说明 Project Location 和你以为的位置不一致(或者用了 Basic 结构),此时 Keil 打开的可能是里层那份,你改外层 main.c 就会"明明改了却没生效"。

2.4 生成后的目录长什么样

HAL_SmartHome\
├─ HAL_SmartHome.ioc        ← CubeMX 工程文件,必须保留
├─ 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
├─ Drivers\   HAL 库与 CMSIS
└─ MDK-ARM\   HAL_SmartHome.uvprojx  ← Keil 真正打开的工程

HAL_SmartHome.ioc 是整个工程的"源头",改引脚、改时钟都靠它。备份时别只备份 Core 目录,.ioc 丢了等于配置全丢。

三、时钟树:25MHz 怎么变成 100MHz

3.1 打开 HSE

Pinout & Configuration → System Core → RCC:

  • High Speed Clock (HSE):选 Crystal/Ceramic Resonator;
  • Low Speed Clock (LSE):Disable。

这里有个细节:如果核心板装的是有源晶振(四个脚、通电就出时钟),要选 BYPASS Clock Source,不能选 Crystal。选错了,程序会卡在 HAL_RCC_OscConfig() 里一直等 HSE 起振,表现为"烧录成功但不运行"。

3.2 填参数

打开 Clock Configuration 页,按这张表填:

项目值
Input frequency25 MHz(核心板晶振,不是 8MHz)
PLL SourceHSE
PLL /M25
PLL ×N400
PLL /P4
PLL /Q4
System Clock MuxPLLCLK
AHB Prescaler/1
APB1 Prescaler/2
APB2 Prescaler/1

最终结果:

SYSCLK = 100 MHz
HCLK   = 100 MHz
PCLK1  =  50 MHz(APB1)
PCLK2  = 100 MHz(APB2)

3.3 这三个数字为什么是 25 / 400 / 4

PLL 的计算链条是固定的:

① 25MHz ÷ M(25) = 1MHz     ← PLL 输入频率,规范要求 1~2MHz
② 1MHz × N(400) = 400MHz   ← VCO 输出,规范要求 100~432MHz
③ 400MHz ÷ P(4) = 100MHz   ← 系统时钟 SYSCLK
④ 400MHz ÷ Q(4) = 100MHz   ← USB / SDIO 用的时钟

所以不是随便凑出来的:

  • M 必须让输入落在 1~2MHz。填 25 得到 1MHz 是标准做法;
  • N 决定 VCO,400 刚好在合法区间内;
  • P 决定主频,除以 4 得到 F411 的上限 100MHz。

三个值任何一个改了,主频就不是 100MHz。而主频不对,HAL_Delay、delay_us、串口波特率、PWM 频率会同时出错——这就是为什么它能坑你一整天。

3.4 APB1 的定时器时钟要 ×2

这一条必须单独讲,因为它是"看代码时最容易迷糊"的地方。

总线分频总线时钟挂在上面的定时器时钟
APB1/250MHz100MHz(×2)
APB2/1100MHz100MHz(不分频时 ×1)

规则是:APB 分频系数 ≠ 1 时,定时器时钟 = PCLK × 2。

所以 TIM3、TIM5 实际跑在 100MHz,我们后面算 PSC 和 ARR 时,统一按 100MHz 算,不要按 50MHz 算。

3.5 Flash Latency 必须是 3WS

主频 100MHz 时,CubeMX 会自动把 Flash Latency 设成 3WS(3 个等待周期)。

这个值如果手动改小了,会出现非常诡异的故障:大部分时候正常,偶尔死机或进 HardFault。因为 Flash 读不过来,取指出错。

生成后可以对照 SystemClock_Config():

RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 400;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4;
RCC_OscInitStruct.PLL.PLLQ = 4;
// ...
__HAL_FLASH_SET_LATENCY(FLASH_LATENCY_3);

四个值和你填的一致,时钟这块才算过关。

四、GPIO 配置(重点在 User Label)

4.1 User Label 是什么

在芯片图上点引脚、选好功能后,一定要在下方 GPIO Settings 里填 User Label。

填了之后,CubeMX 会在 main.h 里自动生成宏:

#define KEY1_Pin        GPIO_PIN_2
#define KEY1_GPIO_Port  GPIOC

代码里就能直接写 HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin),不用记 PC2 这种编号。

两个注意点:

  • Label 只能由字母、数字、下划线组成,不能有空格和中文;
  • 同一个 Label 不能重复,否则生成代码时会报错;
  • 后期改 Label,等于改了宏名,所有引用它的代码都会编译失败。所以一开始就想好名字。

4.2 配置表(照抄)

引脚模式上拉/下拉速度User Label
PC13Output Push PullNo pullLowLED_T
PC1Output Push PullPull-upLowB_RST
PC2EXTI2 FallingPull-up—KEY1
PC3EXTI3 FallingPull-up—KEY2
PA0TIM5_CH1(在 TIM5 里配)——SG90
PA1EXTI1 FallingPull-up—KEY3
PA2USART2_TX——B_TXD
PA3USART2_RX——B_RXD
PA4Output Push PullNo pullLowDHT11_LED
PA5Output Push PullNo pullLowLED4
PA6TIM3_CH1(在 TIM3 里配)——FAN
PA7TIM3_CH2(在 TIM3 里配)——HER
PB0TIM3_CH3(在 TIM3 里配)——CUR
PB1Output Open DrainPull-upVery HighDHT11
PB2Output Push PullNo pullLowENS_LED
PB10EXTI15_10 FallingPull-up—KEY4
PC6USART6_TX——CI_TXD
PC7USART6_RX——CI_RXD
PC0ADC1_IN10——(自动)
PA8I2C3_SCL——(自动)
PC9I2C3_SDA——(自动)

4.3 三个必须特别处理的引脚

① PB1(DHT11)必须是开漏输出

DHT11 是单总线结构,总线靠上拉电阻拉高,从机只能"拉低"。

如果配成推挽输出,主机会主动输出高电平,从机拉低时就是两个管子硬顶,轻则读错,重则发热损坏。所以必须选 Output Open Drain,并且开内部上拉(外部再加 4.7k~10k 更稳)。

② PC13(LED_T)只能小电流

F411 的 PC13 驱动能力有限(数据手册里明确限流),只能点亮小 LED,不能直接驱动继电器或大电流负载,输出速度也必须选 Low。

③ 初始电平别设错

生成代码后,CubeMX 会在 MX_GPIO_Init() 里给每个输出脚写初始电平:

引脚初始电平原因
B_RST(PC1)SET(高)低电平会让蓝牙模块一直复位
DHT11(PB1)SET(高)单总线空闲状态是高
LED_T / LED4 / ENS_LEDRESET(低)上电默认灭
DHT11_LEDRESET(低)上电默认灭

如果 B_RST 初始化成低,现象是蓝牙怎么都连不上,而且你会一直以为是波特率问题。

4.4 按键的 EXTI 配置

四个按键都是按下接地,所以:

  • 模式选 EXTIx Falling(下降沿触发);
  • 上拉选 Pull-up(不按的时候保持高电平)。

如果忘了上拉,引脚悬空,按键会自己乱触发,现象是模式莫名其妙跳变。

五、外设配置

5.1 TIM3:风扇 + 加热(1kHz PWM)

Timers → TIM3:

  • Clock Source:Internal Clock
  • Channel1:PWM Generation CH1(PA6,风扇)
  • Channel2:PWM Generation CH2(PA7,加热)
  • Channel3:PWM Generation CH3(PB0,窗帘电机预留)

Parameter Settings:

参数值
Prescaler100-1
Counter ModeUp
Counter Period1000-1
Auto-reload preloadEnable
PWM ModePWM mode 1
Pulse0
PolarityHigh

计算:

100MHz ÷ 100 = 1MHz(计数频率)
1MHz ÷ 1000 = 1kHz(PWM 频率)
CCR = 0~1000 对应占空比 0~100%

所以后面代码里 FAN_PWM = 800 就是 80% 占空比,CCR = 0 就是停转。

注意 Period 是 1000-1,不是 1000。定时器从 0 数到 ARR,数 1000 个数需要 ARR=999。写成 1000 也能跑,但频率变成 999Hz,CCR 换算会差一点点——不致命,但没必要留这个误差。

5.2 TIM5:SG90 舵机(50Hz)

Timers → TIM5 → Channel1:PWM Generation CH1(PA0)

参数值
Prescaler100-1
Counter Period20000-1
PWM ModePWM mode 1
Pulse0

计算:

100MHz ÷ 100 = 1MHz
1MHz ÷ 20000 = 50Hz(周期 20ms)

因为计数频率正好是 1MHz,CCR 的数值就是高电平的微秒数:

  • CCR = 500 → 0.5ms → 本项目"拉开";
  • CCR = 2000 → 2.0ms → 本项目"关闭"。

实测这颗 SG90 的有效量程就是 500~2000,写 2500 完全不动。所以代码里必须限幅,不要随便传值。

5.3 USART1:调试口(115200)

Connectivity → USART1:

  • Mode:Asynchronous
  • Baud Rate:115200
  • Word Length:8 Bits
  • Parity:None
  • Stop Bits:1
  • Hardware Flow Control:None

NVIC 里勾上 USART1 global interrupt。

虽然 printf 用的是阻塞发送,但后面如果要做接收或 DMA,这个中断迟早要开,一次配好省事。

5.4 USART2:蓝牙/WiFi 复用口(初始 9600)

USART2:

  • Mode:Asynchronous
  • Baud Rate:9600
  • 8N1,无流控

勾上 USART2 global interrupt。

只配 9600,不要在这里配 115200。

115200 是运行时用 Uart2_SetBaudrate() 动态切换的。如果 CubeMX 里也配成 115200,代码里初始值、切换值、拨码开关状态三者会互相打架,排查时你根本不知道当前生效的是哪个。

5.5 USART6:语音模块(预留)

USART6:Asynchronous,115200,8N1,引脚 PC6/PC7。

本项目不用,但保留初始化不影响运行。不用的外设也建议保持配置一致,以后想加功能时不用重新生成工程。

5.6 I2C3:ENS160(100kHz)

Connectivity → I2C3:

参数值
I2CI2C
Speed ModeStandard Mode
Clock Speed100000(100kHz)
Addressing7-bit

引脚自动落到 PA8(SCL)/ PC9(SDA),复用功能 AF4,开漏。

这是 F411 的硬件固定映射,不要手动改到别的引脚。改了之后 CubeMX 可能不报错,但总线上永远没有波形。

I2C 还需要外部上拉电阻(模块一般自带 4.7k)。CubeMX 里配的只是"开漏模式",不是"上拉"。

5.7 ADC1:光照(预留)

Analog → ADC1 → IN10(PC0):

  • 12 位分辨率;
  • 连续转换;
  • 软件触发;
  • 时钟预分频 /4(PCLK2 100MHz ÷ 4 = 25MHz,低于 ADC 上限 36MHz)。

本项目未使用,但配置保留。

5.8 NVIC:中断优先级

System Core → NVIC:

中断抢占优先级
EXTI1 / EXTI2 / EXTI3 / EXTI15_101
USART2 / USART62
USART13

规则:数值越小,优先级越高。

为什么这么排:

  • 按键最高:要保证按下就有反应,而且按键中断里只置标志,执行放在主循环;
  • USART2 次之:ESP-AT 和蓝牙的数据要及时收进缓冲区,否则会丢字节;
  • 调试口最低:打印慢一点没关系,不能挡住按键和通信。

CubeMX 默认使用 4 位抢占优先级(NVIC_PRIORITYGROUP_4),Sub Priority 全部保持 0 即可。

六、生成代码与首次编译

  1. 点右上角 GENERATE CODE,弹窗选择 Open Project,Keil 会自动打开;
  2. Keil 里按 F7 编译;
  3. 此时应该是 0 Error 0 Warning。

如果报错,先解决再往下走,常见原因:

报错原因
找不到 stm32f4xx_hal.hDFP 或固件包版本不匹配
Device not found选错芯片型号
一堆重复定义工程里混进了旧版本文件

生成后你会发现 Core\Src 下多了 tim.c、usart.c、i2c.c、gpio.c、adc.c。这些全部不要手写,以后只往里面 USER CODE 段加东西。

另外,CubeMX 不会帮你勾 Use MicroLIB。Keil 里要手动去 Options for Target → Target 勾上,否则后面的 printf 会没输出。

七、本章易错点总结

现象真正原因处理
串口打印全是乱码晶振填成 8MHz,波特率跟着错Input frequency 填 25MHz
程序烧进去但不运行,卡在时钟初始化HSE 类型选错(有源晶振选了 Crystal)有源晶振选 BYPASS Clock Source
主频不是 100MHz,延时全错PLL 参数填错(M/N/P)M=25、N=400、P=4、Q=4
偶尔死机、进 HardFaultFlash Latency 不是 3WS100MHz 必须 3 个等待周期
编译报 KEY1_Pin 未定义引脚没填 User Label每个引脚都在 GPIO Settings 里命名
代码改了但像没生效Generate Under Root 导致生成了两层同名目录,改错文件确认 .ioc 与 Core 直接在你指定目录
读 DHT11 永远是 0 或校验错PB1 配成推挽输出改成 Output Open Drain + 上拉
蓝牙一直连不上B_RST 初始电平是低,模块处于复位MX_GPIO_Init() 里初始化为 SET
按键自己乱触发、模式乱跳EXTI 没开上拉,引脚悬空Pull-up + 下降沿
风扇转速只有一点点、和预期不符Period 填错,CCR 与占空比换算变了TIM3:PSC=99、ARR=999
舵机抖动或完全不动TIM5 Period 不是 20000,频率不是 50HzPSC=99、ARR=19999
舵机写 2500 没反应超出 SG90 有效量程 500~2000代码限幅在 500~2000
USART2 时好时坏、AT 指令串台CubeMX 里也配了 115200,与运行时切换冲突只配 9600,115200 运行时改
ENS160 完全没响应I2C3 引脚被改到非 PA8/PC9保持 PA8=SCL、PC9=SDA
串口接收中断进不去NVIC 没勾 USARTx global interruptNVIC 里逐个勾选
编译 0 错误但 printf 没输出Keil 没勾 Use MicroLIBOptions for Target → Target → Use MicroLIB
生成代码后以前写的代码没了代码写在 USER CODE 外面只写在 BEGIN/END 之间

八、小结与下一篇

这一章做完,工程的"地基"就打好了:

  • 主频 100MHz,Flash 3WS,APB1 定时器按 100MHz 算;
  • 所有引脚都有 User Label,代码里不用记引脚号;
  • TIM3 输出 1kHz PWM,TIM5 输出 50Hz 舵机信号;
  • USART1 调试 115200,USART2 初始 9600 留给运行时切换;
  • I2C3 用 PA8/PC9,100kHz,给 ENS160;
  • 中断优先级:按键 > 通信 > 调试。

下一篇进入代码层:先搭好工程结构和骨架,再逐个写 DHT11、ENS160、PWM 驱动。你会看到这一章配的参数,是怎么一行一行变成实际波形的。

如果这篇文章对你有帮助,欢迎点赞收藏。系列更新中,下一篇见。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值