3、STM32最小系统搭建:时钟树配置、GPIO初始化、串口调试、LED指示灯驱动
好,咱们正式开始动手了。
这一章,我带你搭建STM32的最小系统。说白了,就是让芯片能跑起来,能跟你对话,还能给你点个灯看看。
很多新手一上来就急着写业务逻辑,结果灯不亮、串口没输出,查半天发现是时钟没配好。嗯,我当年也干过这种事。
3.1 时钟树配置——芯片的心脏跳动
STM32是个复杂的家伙,它内部有好几个时钟源。你得告诉它:用哪个时钟、跑多快、分频多少。这就是时钟树配置。
常用的时钟源有这些:
- HSI(高速内部时钟):8MHz,芯片自带,精度一般。我一般只在调试初期用一下。
- HSE(高速外部时钟):通常接8MHz晶振,精度高,稳定。项目量产我必用这个。
- PLL(锁相环):把时钟倍频上去。比如HSE 8MHz,通过PLL倍频到72MHz或168MHz。
我个人习惯,只要不是成本敏感到极致,一律上外部晶振。为什么?因为HSI受温度影响大,串口通信容易丢包。我在一个农业大棚项目里吃过这个亏,夏天高温时数据乱飞,换了HSE立马解决。
核心配置思路:
选择HSE → 配置PLL倍频系数 → 设置系统时钟源为PLL → 配置AHB、APB1、APB2分频器。
来看一段HAL库的配置代码,我用的是STM32F103,72MHz主频:
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 配置HSE振荡器,使能PLL
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 配置系统时钟、AHB、APB1、APB2分频
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // HCLK = 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2 = 72MHz
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
小提示:APB1最高只能到36MHz,APB2可以到72MHz。串口、I2C这些外设挂在APB1上,别超频了。我曾经把APB1设成72MHz,结果I2C死活不工作,查了两天。
3.2 GPIO初始化——让芯片的脚有灵魂
时钟配好了,芯片活了。但它的引脚还处于高阻态,啥也干不了。你得初始化GPIO,告诉它:这个脚是输出还是输入?推挽还是开漏?速度多快?
GPIO的配置模式,我列个表,你一看就明白:
| 模式 | 用途 | 典型场景 |
|---|---|---|
| 推挽输出 | 输出高低电平 | LED、蜂鸣器、继电器 |
| 开漏输出 | 需要外部上拉,实现线与 | I2C总线、电平转换 |
| 浮空输入 | 电平不确定,需外部驱动 | 按键(配合外部上拉) |
| 上拉/下拉输入 | 内部电阻固定电平 | 按键(省外部电阻) |
| 复用功能 | 引脚交给外设控制 | 串口TX/RX、PWM输出 |
拿LED来说,我们一般用推挽输出。为什么?因为LED需要明确的亮和灭,推挽输出能提供足够的驱动电流。
看代码,初始化一个LED引脚(PA0):
void LED_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE(); // 别忘了开时钟!我经常忘
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不需要上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // LED切换慢,低频即可
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 初始灭
}
注意:GPIO的速度不是越高越好。高速意味着更陡的边沿、更大的EMI干扰。LED这种低频设备,用LOW就够了。我见过有人把所有引脚都配成HIGH,结果板子成了一个小电台。
3.3 串口调试——芯片跟你说话了
时钟有了,灯能亮了。但芯片在想什么,你不知道。这时候就需要串口了。
串口调试,说白了就是让芯片通过TX引脚把数据发出来,你在电脑上用串口助手看。这是嵌入式开发最常用的调试手段,没有之一。
配置串口,需要做这几件事:
- 使能串口和对应GPIO的时钟
- 配置TX为复用推挽输出,RX为浮空输入(或上拉输入)
- 设置波特率、数据位、停止位、校验位
- 使能串口
我一般用115200波特率,8位数据,1位停止位,无校验。这个组合最通用,也最稳定。
来看USART1的初始化代码(PA9=TX, PA10=RX):
void UART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
UART_HandleTypeDef huart1;
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// TX: PA9, 复用推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// RX: PA10, 浮空输入
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 串口参数配置
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
}
调试技巧:初始化完成后,先发一个"Hello World"测试一下。如果收不到,先检查波特率是否匹配,再用示波器看TX引脚有没有波形。没有波形?八成是时钟没配好或者GPIO模式错了。
3.4 LED指示灯驱动——最朴素的反馈
串口能打印信息了,但有时候你不想开电脑。比如在现场调试,手边只有一块板子。这时候LED就是最好的伙伴。
LED驱动其实很简单,就是控制GPIO的高低电平。但怎么用得好,有讲究。
我常用的LED指示方案:
- 系统运行指示:500ms闪烁一次,表示系统活着
- 通信指示:收到数据时快速闪一下
- 错误指示:长亮或特定频率闪烁
来看一个简单的LED闪烁函数,放在主循环里调用:
void LED_Blink(void)
{
static uint32_t last_tick = 0;
uint32_t now = HAL_GetTick();
if (now - last_tick > 500) // 500ms切换一次
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
last_tick = now;
}
}
这里用了HAL_GetTick()做非阻塞延时。千万别用HAL_Delay()做闪烁,否则整个系统都被卡住了。你想想看,如果延时的时候串口来数据了,你根本收不到。
避坑指南:
我曾经在一个项目里,用HAL_Delay(500)做LED闪烁,结果串口接收中断响应不及时,数据频繁丢失。后来改成定时器或系统滴答计时,问题就解决了。记住:主循环里不要用阻塞延时。
3.5 整合测试——让一切跑起来
好,现在我们把时钟、GPIO、串口、LED都配好了。来写个main函数,验证一下:
int main(void)
{
HAL_Init();
SystemClock_Config(); // 配置时钟
LED_GPIO_Init(); // 初始化LED
UART1_Init(); // 初始化串口
printf("System Started!\r\n"); // 串口打印
while (1)
{
LED_Blink(); // LED闪烁
// 其他任务...
}
}
下载程序后,你应该看到:
- LED以500ms的频率闪烁
- 串口助手收到"System Started!"
如果灯不亮?先查硬件连接,再查GPIO时钟有没有使能。如果串口没输出?检查波特率、TX引脚配置、还有你的USB转串口模块有没有接对。
嗯,这一章的内容就到这里。最小系统搭好了,后面的事情就好办了。下一章,我们开始接传感器,让温室真正"感知"环境。