3. STM32F4 HAL库基础:GPIO、UART、定时器的基础配置与中断处理
好,咱们直接进入正题。这一章我打算聊聊STM32F4的HAL库基础。说实话,很多初学者一上来就被HAL库的庞大结构吓住了。其实没那么复杂,你把它当成一个工具包就行。我个人的习惯是,先搞懂三个最常用的外设:GPIO、UART和定时器。这三样玩明白了,剩下的基本就是触类旁通。
3.1 GPIO:最基础也最容易踩坑
GPIO,说白了就是管脚控制。你要让一个引脚输出高电平,或者读取外部信号的高低电平。HAL库把这事儿封装成了几个函数,但背后的寄存器操作你得心里有数。
配置一个GPIO,通常分三步:
- 使能时钟:别笑,我见过有人调了半天灯不亮,最后发现是时钟没开。STM32F4的外设时钟默认是关闭的,你得手动打开。
- 配置模式:输入、输出、复用功能、模拟模式,选哪个?
- 设置速度与上下拉:速度别乱设,高频场合用高速,低频场合用低速,省电还减少干扰。
核心代码示例:
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 定义GPIO初始化结构体
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 配置PA5为推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 输出高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
嗯,这里要注意。配置中断时,Mode要选GPIO_MODE_IT_FALLING或GPIO_MODE_IT_RISING。我曾经在项目里把中断模式配成了输出模式,结果中断死活进不去,查了半天才发现是这里写错了。
个人经验:调试GPIO中断时,先别急着看代码。用示波器或逻辑分析仪抓一下引脚波形,确认外部信号确实来了。很多时候是硬件没给信号,软件调破天也没用。
3.2 UART:串口通信的坑与解
UART是嵌入式调试的命脉。printf打印全靠它。但配置UART时,有几个地方特别容易翻车。
先看基本配置流程:
- 使能UART和对应GPIO的时钟
- 配置TX/RX引脚为复用功能
- 设置波特率、数据位、停止位、校验位
- 使能UART
UART初始化示例:
UART_HandleTypeDef huart1;
// 使能时钟
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA9(TX)和PA10(RX)
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置UART参数
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;
HAL_UART_Init(&huart1);
你想想看,为什么有时候串口打印出来是乱码?多半是波特率对不上。但还有一种情况——时钟源配错了。STM32F4的USART时钟可以来自APB总线,也可以来自外部时钟。如果APB时钟分频系数没算对,实际波特率就和设置值差了十万八千里。
避坑指南:我曾经在调试一个工控项目时,串口数据总是丢字节。查了两天才发现,是中断优先级没配好。UART中断被更高优先级的中断一直抢占,导致数据溢出。解决办法:把UART中断优先级设高一点,或者用DMA。
3.3 定时器:不只是计时
定时器是STM32F4里功能最丰富的外设之一。很多人只用它来产生周期中断,其实它还能做PWM、输入捕获、编码器接口等等。
基础定时器配置很简单:
- 使能定时器时钟
- 设置预分频器(PSC)和自动重装载值(ARR)
- 使能更新中断(如果需要)
- 启动定时器
定时器中断示例:
TIM_HandleTypeDef htim2;
__HAL_RCC_TIM2_CLK_ENABLE();
htim2.Instance = TIM2;
htim2.Init.Prescaler = 16000 - 1; // 16MHz / 16000 = 1kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1000 - 1; // 1kHz / 1000 = 1Hz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
// 使能更新中断
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
// 启动定时器
HAL_TIM_Base_Start_IT(&htim2);
中断回调函数里写什么?我一般习惯这样:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
// 1秒执行一次的任务
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}
这里有个细节——预分频器的值要减1,自动重装载值也要减1。为什么?因为计数器是从0开始计数的。比如预分频器设成16000-1,实际分频系数就是16000。这个坑我刚开始也踩过,算出来的时间总是多1个时钟周期。
我的建议:调试定时器时,先用一个GPIO来输出波形,用示波器看实际频率。别光靠printf打印时间戳,那个本身就有延迟。我习惯在中断里翻转一个测试引脚,然后拿示波器一看,周期对不对一目了然。
3.4 中断处理:别让CPU累死
中断是嵌入式系统的灵魂。但很多人把中断当主循环用,啥事都在中断里干。这是大忌。
中断处理的原则:
- 快进快出:中断服务函数里只做最必要的事,比如置一个标志位、读一个数据寄存器。
- 不要调用HAL_Delay:HAL_Delay依赖SysTick中断,如果中断优先级比SysTick高,就会死锁。
- 注意临界区:中断和主循环共享的变量,要用volatile修饰,必要时关中断保护。
血的教训:我曾经在一个项目中,把UART的数据处理逻辑全放在了中断里。结果数据量一大,中断执行时间过长,导致其他中断丢失。最后改成中断只收数据放到环形缓冲区,主循环再处理,问题就解决了。
中断优先级怎么设?STM32F4有16级抢占优先级和16级子优先级。我个人的习惯是:
- 时间敏感的中断(如定时器)设高优先级
- 数据量大的中断(如UART、SPI)设中等优先级
- 非关键中断(如按键)设低优先级
嗯,差不多就这些。GPIO、UART、定时器这三个外设,是STM32F4开发的基石。你把这几个玩熟了,后面学SPI、I2C、DMA都会轻松很多。下一章咱们聊聊FreeRTOS的任务调度,到时候你会发现,有了操作系统,中断处理的方式又不一样了。