3. STM32F4 HAL库基础:GPIO、UART、定时器的基础配置与中断处理

好,咱们直接进入正题。这一章我打算聊聊STM32F4的HAL库基础。说实话,很多初学者一上来就被HAL库的庞大结构吓住了。其实没那么复杂,你把它当成一个工具包就行。我个人的习惯是,先搞懂三个最常用的外设:GPIO、UART和定时器。这三样玩明白了,剩下的基本就是触类旁通。

3.1 GPIO:最基础也最容易踩坑

GPIO,说白了就是管脚控制。你要让一个引脚输出高电平,或者读取外部信号的高低电平。HAL库把这事儿封装成了几个函数,但背后的寄存器操作你得心里有数。

配置一个GPIO,通常分三步:

  1. 使能时钟:别笑,我见过有人调了半天灯不亮,最后发现是时钟没开。STM32F4的外设时钟默认是关闭的,你得手动打开。
  2. 配置模式:输入、输出、复用功能、模拟模式,选哪个?
  3. 设置速度与上下拉:速度别乱设,高频场合用高速,低频场合用低速,省电还减少干扰。

核心代码示例:

// 使能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_FALLINGGPIO_MODE_IT_RISING。我曾经在项目里把中断模式配成了输出模式,结果中断死活进不去,查了半天才发现是这里写错了。

个人经验:调试GPIO中断时,先别急着看代码。用示波器或逻辑分析仪抓一下引脚波形,确认外部信号确实来了。很多时候是硬件没给信号,软件调破天也没用。

3.2 UART:串口通信的坑与解

UART是嵌入式调试的命脉。printf打印全靠它。但配置UART时,有几个地方特别容易翻车。

先看基本配置流程:

  1. 使能UART和对应GPIO的时钟
  2. 配置TX/RX引脚为复用功能
  3. 设置波特率、数据位、停止位、校验位
  4. 使能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、输入捕获、编码器接口等等。

基础定时器配置很简单:

  1. 使能定时器时钟
  2. 设置预分频器(PSC)和自动重装载值(ARR)
  3. 使能更新中断(如果需要)
  4. 启动定时器

定时器中断示例:

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的任务调度,到时候你会发现,有了操作系统,中断处理的方式又不一样了。