4. STM32底层驱动开发:GPIO、定时器PWM、正交编码器接口、中断系统、串口通信
做AGV底盘控制,说白了就是跟STM32的这几个外设打交道。GPIO、定时器、编码器接口、中断、串口——这五个东西你玩熟了,底盘驱动就搞定了一半。我这些年调试过的AGV,从两轮差速到四轮麦克纳姆,底层逻辑都没跳出这个圈子。
4.1 GPIO:底盘控制的“手脚”
GPIO是STM32最基础的外设。你想想看,电机驱动器的使能引脚、方向控制、刹车信号,哪个不是靠GPIO?
我个人习惯把GPIO配置分成三步:
- 时钟使能——别漏了这一步,我见过太多新手卡在这里
- 模式配置——推挽输出、开漏输出、浮空输入,选对模式很重要
- 速度设置——控制信号用低速,PWM用高速
核心要点:AGV的电机使能引脚,我建议用推挽输出,速度选50MHz就够了。别用开漏,除非你外接上拉电阻。
举个例子,控制一个直流电机正反转:
// 电机A控制引脚初始化
void Motor_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE(); // 时钟使能,这步忘了就全白搭
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; // PA0:方向, PA1:使能
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// 正转
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 方向高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // 使能高电平
小技巧:调试时我喜欢用逻辑分析仪抓GPIO波形。有一次电机抖动,查了半天发现是GPIO初始化顺序错了——先使能了电机才配置方向。嗯,顺序很重要。
4.2 定时器PWM:控制电机速度的灵魂
AGV要跑多快,全靠PWM说了算。STM32的定时器功能很强,但咱们做底盘控制,主要用它的PWM输出和编码器模式。
配置PWM的几个关键参数:
- 预分频器(PSC):决定计数时钟频率
- 自动重装载值(ARR):决定PWM周期
- 比较值(CCR):决定占空比
我一般这样算:目标PWM频率50Hz(周期20ms),系统时钟72MHz。
// 定时器2 PWM初始化,通道1输出
void PWM_Init(void) {
TIM_OC_InitTypeDef sConfigOC = {0};
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz
htim2.Init.Period = 20000 - 1; // 1MHz / 20000 = 50Hz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1; // PWM模式1
sConfigOC.Pulse = 1500; // 初始占空比1500us
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}
// 设置占空比,范围1000~2000对应-90度~+90度
void Set_PWM_Duty(uint16_t duty) {
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty);
}
注意:我曾经在项目里直接用HAL_Delay()做PWM控制,结果电机一卡一卡的。后来才发现,HAL_Delay依赖SysTick中断,中断一多就乱套。PWM一定要用硬件定时器,别用软件模拟。
4.3 正交编码器接口:AGV的“眼睛”
AGV要知道自己走了多远、转了多少角度,全靠编码器。STM32的定时器自带编码器接口模式,用起来很方便。
编码器接口的核心原理:
- 两路信号(A相、B相)相位差90度
- 正转时A相超前B相,反转时B相超前A相
- 定时器自动计数,方向自动判断
我习惯用4倍频模式,精度更高:
// 定时器3编码器模式初始化
void Encoder_Init(void) {
TIM_Encoder_InitTypeDef sEncoderConfig = {0};
TIM_HandleTypeDef htim3;
htim3.Instance = TIM3;
htim3.Init.Prescaler = 0; // 不分频
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 0xFFFF; // 最大计数值
sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 4倍频模式
sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}
// 读取编码器计数值
int16_t Get_Encoder_Count(void) {
return (int16_t)__HAL_TIM_GET_COUNTER(&htim3);
}
避坑指南:我曾经遇到过编码器读数跳变的问题。查了半天,发现是编码器线太长,信号有干扰。后来加了RC滤波,问题就解决了。编码器线建议用屏蔽线,长度别超过3米。
4.4 中断系统:让CPU“忙而不乱”
AGV控制里,中断无处不在。定时器溢出中断、编码器更新中断、串口接收中断——没有中断,CPU就得一直轮询,效率低得可怜。
STM32的中断优先级分组,我建议用NVIC_PriorityGroup_2(2位抢占优先级,2位子优先级)。这样够用,也不复杂。
中断处理的原则:
- 快进快出——中断里别做复杂运算
- 标志位传递——用全局变量通知主循环
- 优先级合理——编码器中断优先级高于串口
// 定时器更新中断回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM2) {
// 50ms执行一次,用于速度计算
speed_calc_flag = 1; // 设置标志位,主循环处理
}
}
// 主循环中处理
while(1) {
if(speed_calc_flag) {
speed_calc_flag = 0;
Calculate_Speed(); // 计算速度
}
}
经验之谈:中断里千万别用HAL_Delay()!SysTick中断优先级比你的外设中断低时,HAL_Delay会死等,直接卡死。我刚开始学STM32时就踩过这个坑,调试了一下午。
4.5 串口通信:AGV的“嘴巴”
AGV要跟主控通信,串口是最常用的方式。ROS里serial包、自定义协议,都离不开串口。
串口配置的几个要点:
- 波特率——115200是AGV常用值,够快也稳定
- 数据位8位、停止位1位、无校验——标准配置
- DMA+空闲中断——接收不定长数据的最佳方案
我推荐用DMA+空闲中断的方式接收数据,效率最高:
// 串口DMA接收初始化
void UART_DMA_Init(void) {
// 配置串口1,波特率115200
// 使能DMA1通道5(USART1_RX)
// 使能空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能空闲中断
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}
// 空闲中断处理
void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 计算接收到的数据长度
rx_len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
// 处理接收到的数据
Process_Received_Data(rx_buffer, rx_len);
// 重新开始接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}
}
注意:串口通信一定要加协议!我见过太多人直接发裸数据,结果一有干扰就乱套。最简单的协议:帧头(0xAA)+数据长度+数据+校验和。别嫌麻烦,这是血的教训。
4.6 知识体系总览
这五个外设的关系,我用一张图来总结:
这张图把五个外设的关系理清楚了。GPIO和PWM负责输出控制,编码器负责输入反馈,中断负责实时响应,串口负责对外通信。它们配合起来,AGV底盘才能动起来、跑得稳。
总结一下:做AGV底层驱动,别想着一步到位。先把GPIO点亮LED,再用定时器输出PWM让电机转起来,接着接上编码器读位置,然后用中断做定时控制,最后用串口跟ROS通信。一步一步来,稳扎稳打。
嗯,这五个外设的驱动开发就讲到这里。代码我都实际跑过,你照着写应该没问题。遇到坑了别慌,先查时钟配置,再看引脚复用,八成能解决。