第三章 STM32标准外设库与HAL库:GPIO、UART、定时器、ADC的配置与实战
好,咱们直接进入正题。这一章要聊的是STM32开发中最核心的两个库——标准外设库和HAL库。很多初学者会纠结到底学哪个,我的建议是:两个都得会。为什么?因为你在实际项目中,大概率会碰到两种代码混着用的场景。
3.1 标准外设库 vs HAL库:到底怎么选?
先说说标准外设库。它其实就是ST官方早期推出的固件库,把寄存器操作封装成了函数。比如你要点亮一个LED,直接调用 GPIO_SetBits() 就行。优点是代码执行效率高,逻辑清晰,适合对实时性要求高的场景。
HAL库呢,是后来推出的硬件抽象层。它更强调跨平台移植性。你写一套代码,换一颗STM32芯片,基本不用改。但代价是代码体积大,执行效率稍低。
我个人习惯是:核心控制逻辑用标准外设库,复杂外设驱动用HAL库。比如电机控制里的PWM生成,我肯定用标准库直接操作寄存器;但像USB、以太网这种复杂协议,用HAL库省心得多。
核心区别一览:
| 对比项 | 标准外设库 | HAL库 |
|---|---|---|
| 代码效率 | 高 | 中 |
| 移植性 | 低 | 高 |
| 学习曲线 | 陡峭 | 平缓 |
| 适合场景 | 实时控制、Bootloader | 复杂外设、快速原型 |
3.2 GPIO配置实战:点灯与按键检测
GPIO是最基础的外设。说白了就是控制引脚的高低电平。咱们先看标准外设库的写法。
// 标准外设库:GPIO初始化
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 点亮LED
GPIO_SetBits(GPIOB, GPIO_Pin_0);
// 或者
GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_SET);
再看HAL库的写法。嗯,这里要注意,HAL库的初始化结构体名字不一样。
// HAL库:GPIO初始化
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_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_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 点亮LED
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
我在项目中遇到过一个问题:用HAL库点灯,发现LED闪烁频率不对。查了半天,原来是HAL库的延时函数 HAL_Delay() 依赖SysTick中断,而我的中断优先级设置得太低,被其他中断抢占了。所以啊,用HAL库时一定要检查中断优先级分组。
3.3 UART通信:从轮询到中断
UART是嵌入式调试的必备工具。没有串口,你连程序跑没跑起来都不知道。咱们先看标准外设库的轮询方式。
// 标准外设库:UART发送一个字节
USART_SendData(USART1, 0x55);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
// 接收一个字节
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
uint8_t data = USART_ReceiveData(USART1);
这种轮询方式在简单场景下够用。但如果你要处理大量数据,或者系统有其他任务,轮询会阻塞CPU。这时候就得用中断。
// 标准外设库:UART中断接收
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 处理接收到的数据
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
HAL库的中断处理更自动化一些。它内部帮你做了标志位清除,你只需要实现回调函数。
// HAL库:UART中断接收回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
// 处理接收到的数据
// 注意:这里要重新启动中断接收
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
}
个人经验:用HAL库做UART接收时,记得在回调函数里重新调用 HAL_UART_Receive_IT()。否则中断只触发一次,后面就收不到数据了。我曾经因为这个bug排查了整整一个下午。
3.4 定时器:PWM生成与输入捕获
定时器是STM32里最灵活的外设。它可以做延时、PWM输出、输入捕获、编码器接口等等。咱们先看PWM生成的配置。
// 标准外设库:定时器PWM输出
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStructure.TIM_Period = 999; // 自动重装载值
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 预分频器
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 500; // 占空比50%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
HAL库的配置思路类似,但结构体名字和函数名不同。
// HAL库:定时器PWM输出
TIM_HandleTypeDef htim2;
TIM_OC_InitTypeDef sConfigOC = {0};
__HAL_RCC_TIM2_CLK_ENABLE();
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
输入捕获稍微复杂一点。它用来测量外部信号的频率或脉宽。我记得有一次做电机测速,用输入捕获测量霍尔传感器的脉冲宽度,结果发现数据抖动很大。后来加了软件滤波才稳定下来。
避坑指南:我曾经在配置定时器输入捕获时,忘记设置 TIM_ICPolarity_Rising 和 TIM_ICSelection_DirectTI,结果捕获到的值全是0。检查了半小时才发现是极性配置错了。所以配置定时器时,一定要仔细检查极性、映射关系和滤波器参数。
3.5 ADC采集:单次转换与连续转换
ADC是模拟世界和数字世界的桥梁。STM32的ADC精度是12位,也就是0-4095。咱们看标准外设库的配置。
// 标准外设库:ADC单次转换
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_Cmd(ADC1, ENABLE);
// 启动转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
uint16_t adc_value = ADC_GetConversionValue(ADC1);
HAL库的ADC配置更简洁,但要注意它默认使用DMA模式时,需要额外配置DMA。
// HAL库:ADC单次转换
ADC_HandleTypeDef hadc1;
__HAL_RCC_ADC1_CLK_ENABLE();
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
uint16_t adc_value = HAL_ADC_GetValue(&hadc1);
实战技巧:ADC采集到的数据往往有噪声。我一般会做软件滤波,比如连续采集10次,去掉最大值和最小值,然后取平均。这样采集到的数据稳定很多。另外,ADC的采样时间不要设得太短,否则输入阻抗大的信号源会采集不准。
3.6 综合实战:用定时器触发ADC采集并通过UART发送
咱们把前面学的知识串起来。做一个实际的小项目:用定时器每隔100ms触发一次ADC采集,然后把采集到的电压值通过UART发送到上位机。
// 伪代码思路
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 启动ADC转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
}
void ADC1_IRQHandler(void)
{
if(ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET)
{
ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);
uint16_t adc_value = ADC_GetConversionValue(ADC1);
float voltage = adc_value * 3.3f / 4096.0f;
// 通过UART发送电压值
printf("Voltage: %.2f V\r\n", voltage);
}
}
这个例子虽然简单,但包含了定时器、ADC、UART三个外设的协同工作。你在实际项目中,经常会遇到这种多外设联动的场景。
好了,这一章的内容就到这里。下一章我们会深入聊聊中断系统和RTOS的移植。记住,多动手写代码,光看是学不会的。找个开发板,把今天讲的GPIO、UART、定时器、ADC都跑一遍,遇到问题再回来翻翻这一章。