2. STM32基础外设实战:GPIO、定时器、PWM生成
做机器人控制,说白了就是跟三个东西打交道:信号输入、信号处理、信号输出。而STM32的GPIO、定时器和PWM,恰好覆盖了这三块。我刚开始做电机驱动时,总觉得这些外设太基础,结果一上伺服电机就翻车——不是PWM频率不对,就是定时器溢出导致电机抖动。嗯,这一章咱们就把这些坑填上。
2.1 GPIO:机器人的“手脚”
GPIO(通用输入输出)是STM32最基础的外设。你可以把它想象成芯片的“手脚”——用来感知外部信号,或者控制外部设备。
2.1.1 GPIO工作模式
STM32的GPIO有8种工作模式,但做机器人控制,我们主要用这几种:
| 模式 | 用途 | 典型场景 |
|---|---|---|
| 推挽输出 | 输出高低电平,驱动能力强 | 控制LED、继电器、电机使能脚 |
| 开漏输出 | 需要外部上拉,实现“线与”逻辑 | I2C通信、多设备共享中断线 |
| 浮空输入 | 电平由外部决定,内部无上下拉 | 读取外部信号(需外部上拉/下拉) |
| 上拉/下拉输入 | 内部有弱上拉或弱下拉 | 按键检测、编码器信号读取 |
| 复用功能 | GPIO引脚被外设接管 | 定时器PWM输出、串口通信 |
2.1.2 GPIO配置实战
用STM32CubeMX配置GPIO很简单,但有几个细节要注意:
- 输出速度:控制电机使能脚用低速(2MHz)就够了,但PWM输出脚建议用高速(50MHz或100MHz),否则波形会变形。
- 上下拉电阻:编码器信号输入,我习惯用上拉输入。因为编码器输出通常是开漏,需要外部上拉才能读到正确电平。
- GPIO初始化顺序:先配置时钟,再配置GPIO模式,最后设置初始电平。顺序错了,引脚可能会先输出一个不确定的电平。
// 以STM32F407为例,配置PA0为推挽输出
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
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_0, GPIO_PIN_RESET);
2.2 定时器:机器人的“心跳”
机器人运动控制,本质上就是“在正确的时间做正确的事”。定时器就是那个负责“计时”的核心部件。
2.2.1 STM32定时器家族
STM32的定时器分三类,各有各的用途:
| 类型 | 位数 | 主要功能 | 机器人应用 |
|---|---|---|---|
| 基本定时器(TIM6/TIM7) | 16位 | 基础定时、触发DAC | 周期性任务调度 |
| 通用定时器(TIM2~TIM5) | 16/32位 | PWM、输入捕获、输出比较 | 电机PWM、编码器测速 |
| 高级定时器(TIM1/TIM8) | 16位 | 带死区插入的PWM、刹车功能 | 伺服电机控制、BLDC驱动 |
2.2.2 定时器工作原理
定时器的核心是一个计数器,它从0开始计数,计到预设值(ARR)后溢出,然后重新从0开始。这个过程就是一次“定时”。
计算公式很简单:
定时时间 = (ARR + 1) × (PSC + 1) / 定时器时钟频率
举个例子:定时器时钟是84MHz,我想产生1ms的中断:
- 预分频器PSC设为83(84MHz / (83+1) = 1MHz,即1us计数一次)
- 自动重装载ARR设为999(计数1000次,即1000us = 1ms)
// 配置TIM2产生1ms中断
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 83; // 84MHz / 84 = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999; // 计数1000次
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器并开启中断
2.3 PWM生成:伺服电机的“油门”
PWM(脉冲宽度调制),说白了就是通过调节高电平的占空比来控制输出功率。伺服电机就是靠PWM信号来控制转速和位置的。
2.3.1 PWM关键参数
| 参数 | 含义 | 伺服电机典型值 |
|---|---|---|
| 频率 | 每秒PWM周期数 | 10kHz~50kHz |
| 占空比 | 高电平时间占整个周期的比例 | 0%~100% |
| 分辨率 | 占空比可调节的精细程度 | 8位(256级)~16位(65536级) |
你想想看,分辨率越高,电机控制就越精细。但分辨率高了,ARR值就大,在相同时钟频率下,PWM频率就会降低。这是个取舍问题。
2.3.2 用定时器生成PWM
STM32生成PWM,本质就是让定时器的输出通道在计数器值小于CCR(捕获比较寄存器)时输出高电平,大于CCR时输出低电平。
// 配置TIM1通道1输出PWM,频率20kHz,占空比50%
TIM_OC_InitTypeDef sConfigOC = {0};
// 假设定时器时钟168MHz,预分频167,ARR值499
// PWM频率 = 168MHz / (167+1) / (499+1) = 20kHz
htim1.Instance = TIM1;
htim1.Init.Prescaler = 167;
htim1.Init.Period = 499;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim1);
sConfigOC.OCMode = TIM_OCMODE_PWM1; // PWM模式1
sConfigOC.Pulse = 250; // 占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 启动PWM输出
2.3.3 动态调整占空比
控制伺服电机,占空比需要实时变化。直接修改CCR值就行:
// 动态调整占空比,pulse范围0~499
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulse);
但要注意:不要在中断里频繁调用这个函数。我见过有人每1us改一次占空比,结果CPU全耗在PWM更新上了,其他任务全卡死。一般控制周期设在1ms~10ms就够用了。
2.4 本章知识体系
下面这张图把GPIO、定时器、PWM的关系串起来了:
2.5 避坑指南
做伺服电机控制,这几个坑我几乎都踩过:
- PWM频率太低:电机发出“滋滋”声,甚至振动。解决方案:频率提到10kHz以上。
- GPIO驱动能力不足:直接驱动伺服电机的使能脚,结果电平拉不上去。解决方案:加三极管或光耦隔离。
- 定时器溢出中断优先级设置不当:导致电机控制周期抖动。解决方案:把定时器中断优先级设高,避免被其他中断打断。
- PWM占空比突变:电机电流瞬间飙升,可能烧驱动。解决方案:做占空比渐变处理,每次只改变一小步。