文章目录
系列第四篇。这一章决定后面所有代码能不能跑:时钟、引脚、定时器、串口、I2C、中断优先级,全部在这里定死。
建议完全照做,不要凭感觉改参数。这一章改错一个数字,后面可能要花一整天去找。
一、为什么这一章必须"照抄"
先说一个我真实踩过的坑。
我第一次用 CubeMX 配时钟,核心板上明明是 25MHz 晶振,我在 Input frequency 里顺手填了默认的 8MHz。编译、烧录、串口打开——全是乱码。
当时我以为是串口线的问题,换了三根线、换了两个串口助手、重装了 CH340 驱动,最后才发现:时钟频率错了,波特率就跟着错,打印出来必然是乱码。
类似的还有:
- PLL 参数填错,主频不是 100MHz,
delay_us全部失准,DHT11 永远读不出来; - 定时器 Period 少填 1,PWM 占空比换算全变,风扇该 80% 却只有 8%;
- I2C3 引脚改到别的口,ENS160 死活读不到。
这些问题共同的特点是:编译不报错,运行没现象,而且极难往配置上想。
所以这一章的目标不是"讲原理",而是"配完能跑,并且知道每个数字为什么是这个值"。
二、新建工程(路径有个隐藏陷阱)
2.1 选芯片
- CubeMX →
File → New Project; - 在
Part Number里输入STM32F411RET; - 选中 STM32F411RETx(LQFP64),点
Start Project。
注意别选成 STM32F411CEUx(LQFP48)或 STM32F411VETx(LQFP100),封装不同,可用引脚完全不同。
2.2 Project Manager 四项设置
| 项目 | 填什么 |
|---|---|
| Project Name | HAL_SmartHome |
| Project Location | ...\8.HAL_SmartHome\HAL_SmartHome |
| Application Structure | Advanced |
| Toolchain / IDE | MDK-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 frequency | 25 MHz(核心板晶振,不是 8MHz) |
| PLL Source | HSE |
PLL /M | 25 |
PLL ×N | 400 |
PLL /P | 4 |
PLL /Q | 4 |
| System Clock Mux | PLLCLK |
| 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 | /2 | 50MHz | 100MHz(×2) |
| APB2 | /1 | 100MHz | 100MHz(不分频时 ×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 |
|---|---|---|---|---|
| PC13 | Output Push Pull | No pull | Low | LED_T |
| PC1 | Output Push Pull | Pull-up | Low | B_RST |
| PC2 | EXTI2 Falling | Pull-up | — | KEY1 |
| PC3 | EXTI3 Falling | Pull-up | — | KEY2 |
| PA0 | TIM5_CH1(在 TIM5 里配) | — | — | SG90 |
| PA1 | EXTI1 Falling | Pull-up | — | KEY3 |
| PA2 | USART2_TX | — | — | B_TXD |
| PA3 | USART2_RX | — | — | B_RXD |
| PA4 | Output Push Pull | No pull | Low | DHT11_LED |
| PA5 | Output Push Pull | No pull | Low | LED4 |
| PA6 | TIM3_CH1(在 TIM3 里配) | — | — | FAN |
| PA7 | TIM3_CH2(在 TIM3 里配) | — | — | HER |
| PB0 | TIM3_CH3(在 TIM3 里配) | — | — | CUR |
| PB1 | Output Open Drain | Pull-up | Very High | DHT11 |
| PB2 | Output Push Pull | No pull | Low | ENS_LED |
| PB10 | EXTI15_10 Falling | Pull-up | — | KEY4 |
| PC6 | USART6_TX | — | — | CI_TXD |
| PC7 | USART6_RX | — | — | CI_RXD |
| PC0 | ADC1_IN10 | — | — | (自动) |
| PA8 | I2C3_SCL | — | — | (自动) |
| PC9 | I2C3_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_LED | RESET(低) | 上电默认灭 |
DHT11_LED | RESET(低) | 上电默认灭 |
如果 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:
| 参数 | 值 |
|---|---|
| Prescaler | 100-1 |
| Counter Mode | Up |
| Counter Period | 1000-1 |
| Auto-reload preload | Enable |
| PWM Mode | PWM mode 1 |
| Pulse | 0 |
| Polarity | High |
计算:
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)
| 参数 | 值 |
|---|---|
| Prescaler | 100-1 |
| Counter Period | 20000-1 |
| PWM Mode | PWM mode 1 |
| Pulse | 0 |
计算:
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:
| 参数 | 值 |
|---|---|
| I2C | I2C |
| Speed Mode | Standard Mode |
| Clock Speed | 100000(100kHz) |
| Addressing | 7-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_10 | 1 |
| USART2 / USART6 | 2 |
| USART1 | 3 |
规则:数值越小,优先级越高。
为什么这么排:
- 按键最高:要保证按下就有反应,而且按键中断里只置标志,执行放在主循环;
- USART2 次之:ESP-AT 和蓝牙的数据要及时收进缓冲区,否则会丢字节;
- 调试口最低:打印慢一点没关系,不能挡住按键和通信。
CubeMX 默认使用 4 位抢占优先级(NVIC_PRIORITYGROUP_4),Sub Priority 全部保持 0 即可。
六、生成代码与首次编译
- 点右上角 GENERATE CODE,弹窗选择
Open Project,Keil 会自动打开; - Keil 里按 F7 编译;
- 此时应该是 0 Error 0 Warning。
如果报错,先解决再往下走,常见原因:
| 报错 | 原因 |
|---|---|
找不到 stm32f4xx_hal.h | DFP 或固件包版本不匹配 |
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 |
| 偶尔死机、进 HardFault | Flash Latency 不是 3WS | 100MHz 必须 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,频率不是 50Hz | PSC=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 interrupt | NVIC 里逐个勾选 |
编译 0 错误但 printf 没输出 | Keil 没勾 Use MicroLIB | Options 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 驱动。你会看到这一章配的参数,是怎么一行一行变成实际波形的。
如果这篇文章对你有帮助,欢迎点赞收藏。系列更新中,下一篇见。
本文详细讲解使用STM32CubeMX为STM32F411RETx芯片搭建智能家居工程骨架的关键步骤:包括芯片选型、时钟树配置(25MHz HSE→100MHz主频)、GPIO User Label规范、TIM3/TIM5 PWM定时器配置(1kHz风扇/50Hz舵机)、USART多串口分工(调试/蓝牙/语音)、I2C3(ENS160传感器)、ADC预留及NVIC中断优先级设定。强调参数准确性对后续外设功能的决定性影响。
4811

被折叠的 条评论
为什么被折叠?



