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很简单,但有几个细节要注意:

  1. 输出速度:控制电机使能脚用低速(2MHz)就够了,但PWM输出脚建议用高速(50MHz或100MHz),否则波形会变形。
  2. 上下拉电阻:编码器信号输入,我习惯用上拉输入。因为编码器输出通常是开漏,需要外部上拉才能读到正确电平。
  3. 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驱动
我曾经踩过的坑:用通用定时器做PWM控制伺服电机,频率设了50Hz,结果电机嗡嗡响。后来才发现,伺服电机PWM频率一般要10kHz以上,50Hz是人耳能听到的频段,电机当然会振动。伺服电机PWM频率建议设在10kHz~50kHz之间。

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频率就会降低。这是个取舍问题。

实战经验:我做六轴机器人时,关节电机PWM频率统一用20kHz,分辨率12位(4096级)。这个组合既能避开人耳听觉范围,又能保证控制精度。频率再高,MOS管的开关损耗就上去了。

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的关系串起来了:

STM32基础外设与伺服电机控制关系图 STM32基础外设与伺服电机控制 STM32微控制器 GPIO 定时器 PWM生成 使能信号 方向控制 故障检测 控制周期定时 编码器脉冲计数 速度测量 速度控制 位置控制 力矩控制 伺服电机 GPIO提供使能和方向信号,定时器提供控制节拍,PWM提供驱动信号

2.5 避坑指南

做伺服电机控制,这几个坑我几乎都踩过:

  • PWM频率太低:电机发出“滋滋”声,甚至振动。解决方案:频率提到10kHz以上。
  • GPIO驱动能力不足:直接驱动伺服电机的使能脚,结果电平拉不上去。解决方案:加三极管或光耦隔离。
  • 定时器溢出中断优先级设置不当:导致电机控制周期抖动。解决方案:把定时器中断优先级设高,避免被其他中断打断。
  • PWM占空比突变:电机电流瞬间飙升,可能烧驱动。解决方案:做占空比渐变处理,每次只改变一小步。
我曾经犯过的错:调试六轴机器人时,PWM频率设了50kHz,结果电机驱动器过热。查了半天才发现,频率太高导致MOS管开关损耗剧增。后来降到20kHz,问题解决。记住:不是频率越高越好,要综合考虑驱动器的承受能力。