3. GPIO与PWM基础:GPIO配置与输出、PWM原理、定时器生成PWM、占空比与频率控制
好,咱们今天聊点实在的。GPIO和PWM,这俩东西在电机控制里,就像人的手和脚。GPIO负责发号施令,PWM负责精确出力。很多新手一上来就急着调PID,结果连灯都点不亮,电机嗡嗡响就是不转。我当年也干过这事儿,折腾了一下午,最后发现是GPIO模式配错了。
所以,咱们先把地基打牢。这一节,我会带着你从GPIO的配置讲起,再到PWM的原理,最后用定时器亲手生成一个可调频率、可调占空比的PWM波。嗯,都是实战干货。
3.1 GPIO配置与输出:不只是点灯那么简单
GPIO,全称是General Purpose Input Output,通用输入输出口。在电机控制里,它通常用来做三件事:控制使能信号、读取霍尔传感器电平、输出PWM波(部分MCU的PWM也是从GPIO复用出来的)。
我个人习惯,在项目开始前,先把所有用到的GPIO列个表。别嫌麻烦,这能省下后面排查问题的80%时间。
3.1.1 配置步骤(以STM32为例)
配置一个GPIO,说白了就是告诉MCU三件事:
- 模式:是输入还是输出?输出的话是推挽还是开漏?
- 速度:跑多快?电机控制一般用高速(50MHz以上)
- 上下拉:内部要不要接个电阻?
举个例子,控制电机驱动芯片的EN(使能)引脚:
// 使能引脚配置
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE(); // 先开时钟,这是很多新手容易忘的
GPIO_InitStruct.Pin = GPIO_PIN_8; // PA8
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_8, GPIO_PIN_SET);
3.1.2 输出电平的几种方式
控制GPIO输出,其实就三种操作:置高、置低、翻转。你想想看,电机控制里的使能、刹车、方向切换,本质上都是这些操作的组合。
| 操作 | HAL库函数 | 寄存器操作(更快) |
|---|---|---|
| 置高 | HAL_GPIO_WritePin(port, pin, SET) | GPIOA->BSRR = pin |
| 置低 | HAL_GPIO_WritePin(port, pin, RESET) | GPIOA->BRR = pin |
| 翻转 | HAL_GPIO_TogglePin(port, pin) | GPIOA->ODR ^= pin |
我个人建议,在电机控制这种对时序要求高的场合,尽量用寄存器操作。HAL库虽然方便,但函数调用开销大,尤其是在中断里频繁翻转引脚时,差别很明显。
3.2 PWM原理:说白了就是“开关”的艺术
PWM,Pulse Width Modulation,脉冲宽度调制。名字听着高大上,其实原理特别简单:用数字信号模拟模拟电压。
你想想看,要让电机转得快一点,是不是得给它加更高的电压?但MCU只能输出0V或3.3V,怎么办?那就快速开关,让平均电压变高。开关得越快,电机越平稳。
3.2.1 两个核心参数
- 频率(Frequency):一秒内开关多少次。单位Hz。电机控制常用10kHz~20kHz,低于人耳听觉范围会有噪音。
- 占空比(Duty Cycle):一个周期内,高电平时间占多少百分比。50%占空比,平均电压就是1.65V。
举个例子:
- 频率 = 10kHz,周期 = 100μs
- 占空比 = 50%,高电平时间 = 50μs
- 电机得到的平均电压 ≈ 3.3V × 50% = 1.65V
3.3 定时器生成PWM:这才是核心
MCU怎么产生PWM?靠定时器。说白了,定时器就是一个计数器,从0数到某个值(ARR,自动重装载值),然后归零重新数。在数的过程中,和另一个值(CCR,捕获比较值)做比较,一旦相等,就翻转电平。
你想想看:
- ARR决定周期 → 决定频率
- CCR决定翻转点 → 决定占空比
3.3.1 频率与占空比的计算公式
这个公式我建议你记在笔记本上,每次配定时器都会用到:
PWM频率 = 定时器时钟频率 / (PSC + 1) / (ARR + 1)
占空比 = CCR / (ARR + 1) × 100%
其中:
- PSC:预分频器,把高频时钟降下来
- ARR:自动重装载值,决定计数周期
- CCR:捕获比较值,决定占空比
举个例子,我想生成一个16kHz、50%占空比的PWM:
// 假设定时器时钟 = 72MHz
// 目标频率 = 16kHz
// 目标占空比 = 50%
// 第一步:确定ARR和PSC
// 16kHz = 72MHz / (PSC+1) / (ARR+1)
// 我习惯先定ARR,再算PSC
// 取ARR = 4499,则 (PSC+1) = 72MHz / 16kHz / 4500 = 1
// 所以 PSC = 0
// 第二步:计算CCR
// 占空比50%,CCR = (ARR+1) × 50% = 2250
TIM_HandleTypeDef htim2;
TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0; // PSC = 0
htim2.Init.Period = 4499; // ARR = 4499
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 2250; // CCR = 2250
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
3.4 占空比与频率的动态控制
电机控制中,频率和占空比不是一成不变的。启动时需要低频大占空比,运行时需要高频小占空比。怎么动态调整?
3.4.1 运行时修改占空比
这个简单,直接修改CCR寄存器就行:
// 运行时修改占空比为75%
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 3375); // 3375 = 4500 × 75%
注意:修改CCR时,最好在定时器更新事件发生后再改,否则可能产生不完整的脉冲。HAL库的底层已经处理了这个问题,但如果你直接操作寄存器,记得在中断里同步。
3.4.2 运行时修改频率
修改频率需要改ARR,但ARR变了,占空比也会跟着变。所以需要同时调整CCR:
// 假设要把频率从16kHz改为20kHz
// 新ARR = 72MHz / 1 / 20kHz - 1 = 3599
// 要保持占空比50%,新CCR = (3599+1) × 50% = 1800
__HAL_TIM_SET_AUTORELOAD(&htim2, 3599); // 改ARR
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1800); // 改CCR
3.5 实战经验总结
好了,这一节的内容差不多就这些。最后给你几个我这些年总结出来的经验:
- GPIO配置别偷懒:每个引脚的模式、速度、上下拉都要明确配置。不要依赖默认值,不同MCU的默认值可能不一样。
- PWM频率选16kHz左右:太低有噪音,太高MOS管发热严重。16kHz是个折中点。
- 占空比不要从0直接跳到100%:电机启动时,我习惯用软件做个软启动,占空比从10%逐渐增加到目标值。否则电流冲击很大,容易烧MOS管。
- 调试时先看波形:别一上来就接电机。先用示波器确认PWM波形正确,频率和占空比都对了,再接负载。
嗯,这一节就到这里。下一节我们会讲定时器的更高级用法——输入捕获和编码器接口,那是做闭环控制的基础。到时候见。