2. STM32基础回顾:GPIO配置与寄存器操作、系统时钟树与HAL库时钟配置、中断优先级与NVIC管理
好,咱们正式开始。这一章我打算带大家快速过一遍STM32的几个基础模块。别觉得基础就掉以轻心,我见过太多项目翻车,最后查出来都是GPIO没配好、时钟不对、或者中断优先级乱套了。说白了,这些是嵌入式开发的“地基”,地基不稳,上层建筑再漂亮也白搭。
2.1 GPIO配置与寄存器操作
GPIO,通用输入输出口。每个STM32芯片都有几十个甚至上百个引脚,它们通过GPIO模块与外界交互。我个人习惯把GPIO分成三类:推挽输出(驱动LED、蜂鸣器)、浮空输入(读取按键)、复用功能(串口、SPI等外设接管引脚)。
2.1.1 寄存器层面:别怕,其实很简单
HAL库封装得很好,但如果你只看HAL,永远不知道底层发生了什么。我建议你至少看一遍寄存器手册。GPIO相关的寄存器就那么几个:
| 寄存器 | 功能 | 我常用的配置 |
|---|---|---|
| GPIOx_MODER | 模式选择(输入/输出/复用/模拟) | 00=输入,01=输出,10=复用,11=模拟 |
| GPIOx_OTYPER | 输出类型(推挽/开漏) | 0=推挽,1=开漏 |
| GPIOx_OSPEEDR | 输出速度 | 一般选10(中速)或11(高速) |
| GPIOx_PUPDR | 上拉/下拉 | 01=上拉,10=下拉,00=无 |
| GPIOx_ODR | 输出数据寄存器 | 写1拉高,写0拉低 |
| GPIOx_IDR | 输入数据寄存器 | 读引脚电平 |
举个例子,你想把PA5配置成推挽输出,驱动一个LED。寄存器操作就这几行:
// 开启GPIOA时钟
RCC->AHB1ENR |= (1 << 0);
// 配置PA5为输出模式(MODER[11:10] = 01)
GPIOA->MODER &= ~(0x3 << 10);
GPIOA->MODER |= (0x1 << 10);
// 推挽输出(OTYPER[5] = 0)
GPIOA->OTYPER &= ~(1 << 5);
// 中速(OSPEEDR[11:10] = 10)
GPIOA->OSPEEDR |= (0x2 << 10);
// 无上拉下拉
GPIOA->PUPDR &= ~(0x3 << 10);
// 输出高电平,点亮LED
GPIOA->ODR |= (1 << 5);
嗯,看着是不是很直接?HAL库的HAL_GPIO_WritePin()底层就是这么干的。
2.1.2 HAL库配置:更安全,但别无脑用
HAL库的GPIO初始化函数是HAL_GPIO_Init(),它接受一个GPIO_InitTypeDef结构体。我一般这样写:
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL库的好处是跨芯片兼容,换个型号只要改改时钟使能宏就行。但注意,HAL_GPIO_Init()内部会先复位整个GPIO端口的所有配置,再按你的结构体设置。如果你在中断里频繁调用它,可能会有性能问题。
2.2 系统时钟树与HAL库时钟配置
时钟是STM32的心脏。没有时钟,外设就是一堆死电路。STM32的时钟树比较复杂,但核心就三个时钟源:HSI(内部高速,8MHz)、HSE(外部晶振,通常4-16MHz)、PLL(锁相环,倍频用)。
我画了一张图,帮你理清时钟树的主干:
时钟配置的核心是:先选源,再倍频,最后分频给各总线。HAL库提供了HAL_RCC_ClockConfig()和HAL_RCC_OscConfig()两个函数。我一般这样配:
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.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 8; // 分频
RCC_OscInitStruct.PLL.PLLN = 336; // 倍频
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; // 最终得到 8/8*336/2 = 168MHz
RCC_OscInitStruct.PLL.PLLQ = 7;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 配置系统时钟、AHB、APB分频
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 = 168MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; // APB1 = 42MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; // APB2 = 84MHz
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
2.3 中断优先级与NVIC管理
中断是嵌入式系统的灵魂。没有中断,CPU就得轮询,效率极低。STM32使用NVIC(嵌套向量中断控制器)来管理中断。NVIC支持最多16级抢占优先级(0-15,0最高),以及最多16级子优先级(部分芯片不同)。
优先级分组由NVIC_PriorityGroupConfig()决定。HAL库默认使用NVIC_PriorityGroup_4,即4位抢占优先级,0位子优先级。说白了,就是只有抢占优先级,没有子优先级。
2.3.1 配置中断的步骤
- 使能外设中断:比如USART的接收中断、定时器的更新中断。
- 配置NVIC优先级:设置抢占优先级和子优先级。
- 使能NVIC通道:调用
HAL_NVIC_EnableIRQ()。 - 编写中断服务函数:在
stm32f4xx_it.c中实现。
举个例子,配置USART2接收中断:
// 使能USART2中断
__HAL_UART_ENABLE_IT(&huart2, UART_IT_RXNE);
// 配置NVIC
HAL_NVIC_SetPriority(USART2_IRQn, 1, 0); // 抢占优先级1
HAL_NVIC_EnableIRQ(USART2_IRQn);
中断服务函数里,我习惯这样写:
void USART2_IRQHandler(void)
{
// 检查中断标志
if(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_RXNE))
{
uint8_t data = (uint8_t)(huart2.Instance->DR & 0xFF);
// 处理接收到的数据
// ...
__HAL_UART_CLEAR_FLAG(&huart2, UART_FLAG_RXNE);
}
}
2.3.2 优先级分组的选择
不同项目对中断优先级的需求不一样。我一般遵循这个原则:
| 场景 | 推荐分组 | 说明 |
|---|---|---|
| 简单项目,中断少 | NVIC_PriorityGroup_4 | 4位抢占,0位子优先级,简单粗暴 |
| 多外设,需要嵌套 | NVIC_PriorityGroup_3 | 3位抢占,1位子优先级,灵活一些 |
| 实时性要求高 | NVIC_PriorityGroup_2 | 2位抢占,2位子优先级,适合复杂嵌套 |
我个人习惯用NVIC_PriorityGroup_4,因为简单。如果项目里中断数量超过8个,我会考虑切换到Group_3。你想想看,优先级分组一旦在初始化时设定,整个项目周期都不能改,所以一开始就要想清楚。
好了,这一章的内容就这些。GPIO、时钟、中断,这三个模块是STM32开发的“三板斧”。你把这章吃透了,后面串口DMA和ADC的配置就会顺畅很多。记住:寄存器操作让你理解硬件,HAL库让你快速开发,两者缺一不可。