1. 从舵机控制说起:为什么需要PWM和重映射?

大家好,我是老李,一个在嵌入式领域摸爬滚打了十多年的工程师。今天想和大家聊聊STM32F103C8T6这个“小钢炮”上的两个核心技巧:定时器PWM输出和GPIO重映射。咱们不从枯燥的理论开始,先从一个非常实际的项目——舵机控制——来切入。

相信很多朋友都玩过舵机,比如常见的SG90。它有三根线:电源、地线和信号线。控制舵机转动的秘密,就藏在信号线上的PWM波里。PWM,中文叫脉冲宽度调制,你可以把它想象成一个不断开关的电子水龙头。开关的频率是固定的,但每次“开”的时间长短可以变化。舵机内部有个小电路,它通过检测这个“开”的时间(也就是高电平的宽度)来对应到不同的角度。通常,一个周期为20毫秒(也就是50Hz)的PWM波,其高电平宽度在0.5毫秒到2.5毫秒之间变化时,舵机就能在0度到180度之间转动。

那么问题来了,STM32F103C8T6怎么产生这个PWM波呢?答案就是用它的定时器。这颗芯片有好几个定时器,像TIM1、TIM2、TIM3、TIM4等,大部分都能产生PWM。但另一个问题随之而来:芯片的引脚是有限的,而且每个定时器的PWM输出通道,在出厂时已经被“默认”分配到了某些特定的引脚上。比如,TIM3通道2的默认输出引脚是PA7。可如果你的电路板上,PA7这个引脚已经被用来接屏幕或者传感器了,而舵机信号线恰好接到了PB5上,这该怎么办?难道要为了一个PWM功能去重新画板子吗?

当然不用!这就是GPIO重映射大显身手的时候了。重映射,顾名思义,就是“重新安排映射关系”。它允许我们把定时器PWM输出这个“功能”,从它默认的“家”(比如PA7),搬到另一个我们指定的“新家”(比如PB5)去。这样一来,硬件布线的灵活性就大大增加了。你可以根据电路板的实际布局,把信号“路由”到最方便、干扰最小的引脚上,而不用被芯片的默认设计束缚住手脚。所以,学会PWM和重映射的结合,是玩转STM32,进行硬件资源优化和信号控制的一项核心技巧。接下来,我就手把手带你走通这个全流程。

2. 庖丁解牛:深入理解GPIO重映射

在动手写代码之前,咱们得先把重映射这个事儿彻底搞明白。很多新手容易把它和“引脚复用”搞混,这里我帮你捋清楚。

2.1 重映射 vs. 引脚复用:不是一回事!

先说引脚复用。STM32的每个GPIO引脚都不是“单功能”的,它们像瑞士军刀一样,身兼数职。一个引脚可以是普通的数字输入输出(GPIO),也可以是串口TX、SPI时钟、I2C数据线,当然也可以是定时器的PWM输出。当你把这个引脚配置成这些特殊功能时,就叫作“复用”。比如,你把PA9配置成USART1_TX,这就是引脚复用。在代码里,你会把GPIO的模式设置为 GPIO_Mode_AF_PP(复用推挽输出)或 GPIO_Mode_AF_OD(复用开漏输出)。

重映射又是什么呢?你可以把它理解为“高级的、可选的复用”。芯片设计时,一个外设(比如TIM3)的某个功能(比如通道2输出)已经有一个默认的复用引脚了(PA7)。但设计者很贴心,他们预见到我们布线时可能会遇到困难,所以额外提供了另一套甚至几套备用的引脚连接方案。通过配置特定的寄存器,我们就可以把TIM3通道2的输出功能,“切换”到另一组备用的引脚(比如PB5)上去。这个“切换”的过程,就是重映射。

所以简单记:复用是让引脚拥有第二职业;重映射是在几个备用的第二职业工作地点之间做选择。

2.2 部分重映射与完全重映射:按需选择

重映射还分两种类型:部分重映射和完全重映射。这个“部分”和“完全”,是针对一个外设的所有相关引脚而言的。

以我们常用的USART3为例,它的默认引脚是PB10(TX)、PB11(RX)、PB12(CK)、PB13(CTS)、PB14(RTS)。如果你只需要把TX和RX换到别处,而其他引脚(CK、CTS、RTS)仍然留在原来的PB12~PB14上,那么你就可以使用部分重映射。部分重映射后,TX和RX会移动到PC10和PC11,其他引脚不动。

如果你觉得PB10~PB14这整组引脚我全都要挪走,给其他更重要的功能腾地方,那就可以使用完全重映射。完全重映射后,USART3的所有五个引脚会整体搬迁到PD8~PD12。

这里有个非常重要的点,也是我早期踩过的坑:部分重映射和完全重映射的目标引脚是芯片硬件固定死的,不是你想映射到哪个引脚就能映射到哪个引脚! 你只能查阅芯片的参考手册(Datasheet)或编程手册,在它提供的几套“套餐”里选择。比如TIM3,它的重映射选项是固定的,你不能自己发明一个把通道2映射到PC13的方案。这一点一定要牢记,所有重映射的对应关系,都需要查表确认。

2.3 重映射的“总开关”:AFIO时钟

想要操作重映射功能,有一个关键的“总开关”必须先打开,那就是AFIO时钟。AFIO是“Alternate Function I/O”的缩写,可以理解为管理这些复用和重映射功能的“后勤部门”。

在STM32的标准外设库中,开启AFIO时钟的语句是:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

这条命令必须在任何重映射配置操作之前执行! 否则,你后续调用重映射函数是不会生效的。我见过不少朋友代码逻辑都对,但PWM就是出不来,最后发现就是忘了开AFIO时钟。所以,请把它当成一个固定动作,只要用到重映射,就先写上这行代码。

3. 定时器PWM生成的核心原理

搞清楚了重映射,我们再来看看PWM是怎么从定时器里“流”出来的。理解了原理,配置代码时就不会懵。

3.1 定时器如何“雕刻”出PWM波?

你可以把一个定时器想象成一个在跑圈的数字运动员。它核心的部件是:

  1. 计数器(CNT):这个运动员当前所在的圈数位置。
  2. 自动重装载寄存器(ARR):跑道的总长度。当运动员(CNT)跑完一圈(达到ARR值),他就会瞬间回到起点,开始下一圈。这个“一圈”的时间,就决定了PWM波的周期(或频率)
  3. 捕获/比较寄存器(CCRx):在跑道上设置的一个“标记线”。我们用它来和运动员的位置(CNT)做比较。

PWM输出模式,就是基于这个比较结果来工作的。以向上计数PWM模式1为例:

  • 当运动员还没跑到标记线(CNT < CCRx)时,对应的输出通道(比如TIMx_CH2)就输出一种电平(比如低电平)。
  • 当运动员跑到或跑过标记线(CNT >= CCRx)时,输出通道就翻转成另一种电平(比如高电平)。

这样,在一个周期内,输出高电平的时间长度,就由CCRx的值决定了。ARR决定了周期,CCRx决定了高电平的宽度,占空比就是 (CCRx / ARR)。通过修改CCRx的值,我们就能像捏橡皮泥一样,改变PWM波的形状,从而控制舵机的角度或者LED的亮度。

3.2 PWM模式1与模式2:谁有效谁无效?

在库函数配置时,你会遇到 TIM_OCMode_PWM1TIM_OCMode_PWM2 两个选项。它们区别在于对“有效电平”的定义不同。

  • PWM模式1:在向上计数时,CNT < CCRx为有效电平。这个“有效电平”具体是高是低,由另一个极性参数 TIM_OCPolarity 决定。如果你设置极性为高(TIM_OCPolarity_High),那么有效电平就是高电平,此时占空比就是高电平时间占比。
  • PWM模式2:逻辑正好相反,在向上计数时,CNT < CCRx为无效电平。

怎么选呢?这通常取决于你的外部电路。比如,很多共阳极的LED,是低电平点亮。那么你可能就需要配置为PWM模式1,但极性设为低(TIM_OCPolarity_Low),这样“有效电平”是低电平,才能控制LED。对于舵机,它只关心高电平脉冲的宽度,所以通常用PWM模式1+高极性即可。关键是理解“有效电平”这个概念,然后根据硬件需求来搭配模式和极性。

3.3 预装载寄存器:让变化更丝滑

在调整PWM参数时,比如你想改变舵机角度,就需要动态修改CCRx的值。这里有一个细节:你是希望这个新值立即生效,还是等当前这个PWM周期结束后再生效?

STM32提供了一个“预装载寄存器”功能。如果使能了预装载(调用 TIM_OCxPreloadConfig(TIM3, ENABLE)),那么你写入CCRx的值会先放在一个“缓冲区”里,等到当前周期结束,下一个周期开始时,才会被真正加载到工作寄存器中生效。这样做的好处是,可以避免在一个PWM周期中间突然改变占空比,导致产生一个被“切碎”的不完整脉冲,让输出信号更平滑。对于舵机控制,使能预装载是个好习惯。

4. 实战演练:配置TIM3 PWM输出并重映射至PB5

理论说了一箩筐,现在咱们真刀真枪地干。我们的目标是:使用STM32F103C8T6的TIM3通道2,产生一个50Hz的PWM波,并通过重映射功能,将其输出到PB5引脚,用以控制一个SG90舵机。

4.1 第一步:查表确认重映射可行性

动手写代码前,先翻手册(或者凭经验)。对于STM32F103系列,TIM3通道2(TIM3_CH2)的默认引脚是PA7。它的重映射情况是:可以部分重映射到PB5。注意,是部分重映射,对应的宏定义是 GPIO_PartialRemap_TIM3。完全重映射是另一组引脚,我们这里用不到。确认这一点,我们的硬件连接就可以放心地把舵机信号线接到PB5了。

4.2 第二步:编写PWM初始化函数

我们来创建一个 pwm.cpwm.h 文件。下面是 pwm.c 中初始化函数的详细代码和解读,我会把踩过的坑和注意事项都写在注释里。

#include "stm32f10x.h"
#include "pwm.h"

void TIM3_PWM_Init(u16 arr, u16 psc)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;

    /* 1. 开启所有相关的时钟 -- 这一步绝对不能漏! */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // PB5属于GPIOB,开B口时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // TIM3挂载在APB1总线上
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 重映射必备,AFIO时钟!

    /* 2. 配置GPIO引脚为复用推挽输出 -- 配置的是重映射后的新引脚PB5 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // PB5
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出,这是PWM输出的标准模式
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度选50M,输出波形边沿更陡峭
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* 3. 执行重映射操作 -- 将TIM3_CH2从PA7切换到PB5 */
    GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);
    // 注意:这个函数调用必须在GPIO初始化之后吗?其实先后顺序要求不严,但必须在所有相关时钟开启之后。

    /* 4. 配置定时器时基 -- 决定PWM的频率 */
    TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值ARR,决定周期
    TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频系数PSC,对定时器时钟进行分频
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分割,通常不分
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    /* 5. 配置PWM输出模式 -- 决定PWM的占空比和极性 */
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 选择PWM模式1
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 使能输出
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性高:有效电平为高
    // TIM_OCInitStructure.TIM_Pulse = 0; // 初始比较值CCR,也可以在后面用TIM_SetCompare2设置
    TIM_OC2Init(TIM3, &TIM_OCInitStructure); // 注意是OC2,对应通道2

    /* 6. 使能预装载和自动重装载预装载 */
    TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能通道2的CCR预装载
    TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能ARR预装载,周期变化也更平滑

    /* 7. 启动定时器 */
    TIM_Cmd(TIM3, ENABLE);
}

4.3 第三步:计算并设置PWM频率为50Hz

上面的初始化函数需要两个参数 arrpsc。我们需要计算它们,以得到舵机需要的50Hz PWM波。

STM32F103C8T6默认的内部高速时钟(HSI)是8MHz,但通常我们使用外部8MHz晶振,并经过PLL倍频到72MHz作为系统时钟(SYSCLK)。APB1总线上的定时器时钟(TIMxCLK)最大为72MHz。这里我们假设系统时钟为72MHz,且APB1预分频器为2(即APB1时钟为36MHz),但定时器时钟会倍频回72MHz(这是STM32的机制)。

所以,定时器时钟频率 = 72MHz = 72,000,000 Hz。

PWM频率的计算公式为:PWM频率 = 定时器时钟频率 / ((arr + 1) * (psc + 1))

我们需要50Hz,即周期为20ms。代入公式: 50 = 72,000,000 / ((arr+1)*(psc+1))

为了计算方便,我们通常先设定一个预分频系数 psc。如果不想分频太多,设 psc = 7199,这样 psc+1 = 7200,定时器时钟被7200分频后,计数频率变为 72,000,000 / 7200 = 10,000 Hz。

那么,arr+1 = 10,000 Hz / 50 Hz = 200,所以 arr = 199

这样,arr=199, psc=7199 的组合,就能得到精确的50Hz。在 main.c 中,我们这样调用:

TIM3_PWM_Init(199, 7199); // 初始化产生50Hz的PWM波

4.4 第四步:动态控制舵机角度

初始化完成后,PWM波就已经在PB5引脚上输出了,只不过此时的占空比还是初始值(如果没设置,可能是0)。我们需要动态改变CCR2的值来控制舵机。

对于SG90舵机,0.5ms高电平对应0度,2.5ms高电平对应180度。我们的PWM周期是20ms(50Hz)。

  • 0度对应的CCR2值 = (0.5ms / 20ms) * (arr+1) = 0.025 * 200 = 5
  • 180度对应的CCR2值 = (2.5ms / 20ms) * (arr+1) = 0.125 * 200 = 25

所以,CCR2的值在5到25之间变化,就能让舵机从0度转到180度。在 main.c 的循环中,你可以这样控制:

#include "stm32f10x.h"
#include "pwm.h"
#include "delay.h" // 需要一个简单的延时函数

int main(void)
{
    Delay_Init(); // 延时初始化
    TIM3_PWM_Init(199, 7199); // 初始化PWM

    while(1)
    {
        // 让舵机缓慢从0度转到180度
        for(int i=5; i<=25; i++)
        {
            TIM_SetCompare2(TIM3, i); // 动态修改CCR2的值
            Delay_ms(100); // 延时100ms,让舵机有足够时间转动到位
        }
        // 再从180度转回0度
        for(int i=25; i>=5; i--)
        {
            TIM_SetCompare2(TIM3, i);
            Delay_ms(100);
        }
    }
}

5. 调试技巧与常见问题排查

代码写完了,下载到板子上,舵机没反应?别着急,这是学习过程中最常遇到的情况。我来分享几个实用的调试方法和常见坑点。

1. 确认硬件连接 这听起来像废话,但却是最高频的错误来源。务必确认:

  • 舵机红线接3.3V或5V(根据你的板子供电能力),黑线接GND,黄线(信号线)接PB5。
  • 使用万用表测量一下PB5和GND之间是否有电压?或者用示波器直接看波形是最直接的。

2. 检查时钟配置 这是软件层面最容易出错的地方。请按顺序核对:

  • 你的系统时钟真的配置成72MHz了吗?如果用的是默认的HSI(8MHz),那么计算 arrpsc 的参数就要全部推倒重来。我建议在工程中明确调用 SystemInit() 函数,并检查 SystemCoreClock 变量的值。
  • AFIO时钟开了吗?RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); 这行代码有没有被漏掉或放在重映射函数之后?
  • GPIOB和TIM3的时钟都开启了吗?

3. 验证GPIO配置模式 输出PWM必须将GPIO模式设置为 GPIO_Mode_AF_PP(复用推挽输出)。如果你误设为 GPIO_Mode_Out_PP(普通推挽输出),定时器是无法控制这个引脚的,自然没有PWM输出。

4. 排查引脚冲突:JTAG/SWD的干扰 这是一个经典的“坑”!STM32F103C8T6的PB3、PB4、PA15等引脚默认是JTAG调试接口的功能。如果你不小心把PWM重映射到了这些引脚,或者这些引脚被JTAG占用,就会产生冲突。

  • 如果你只用SWD模式下载和调试(通常只需要PA13和PA14),可以禁用JTAG,释放PB3、PB4、PA15。在初始化开始处加入:
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // 禁用JTAG,保留SWD
    
  • 确保你的重映射目标引脚(如PB5)不是这些特殊功能引脚。

5. 使用软件调试工具

  • 库函数状态检查:单步调试,观察每个配置函数(如 GPIO_Init, TIM_TimeBaseInit)的返回值或执行流程。
  • 寄存器查看:在调试器中,直接查看TIM3的寄存器组。重点看:
    • TIM3->CR1:确保计数器使能位(CEN)为1。
    • TIM3->CCER:确保对应通道(通道2是CC2E位)的捕获/比较使能位为1。
    • TIM3->CCR2:看看你设置的比较值是否成功写入。
  • 逻辑分析仪或示波器:这是终极武器。接到PB5上,看是否有波形。如果没有,检查以上步骤;如果有波形但频率不对,检查 arrpsc 的计算;如果占空比不对,检查CCR2的设置。

6. 关于重映射函数的顺序 网上有些资料会争论 GPIO_PinRemapConfigGPIO_Init 谁先谁后。根据我的经验,只要在开启对应时钟之后,在启动定时器之前调用,顺序影响不大。但一个清晰的好习惯是:先配置GPIO为复用模式,再执行重映射,这样逻辑上更顺。

6. 举一反三:拓展到其他定时器与场景

掌握了TIM3通道2的重映射,其他定时器和通道就是触类旁通的事情了。这里给你一些拓展思路和更复杂的场景。

1. 多路PWM输出 STM32F103C8T6的TIM3有4个独立通道(CH1-CH4)。你可以用同样的方法,初始化多个通道,并分别重映射到不同的引脚,实现同时控制多个舵机或者多个LED灯。例如,TIM3_CH1默认PA6,可以重映射到PB4;TIM3_CH3默认PB0,可以重映射到PC8(完全重映射时)。这样,一个定时器就能输出4路独立的PWM,资源利用率很高。

2. 使用高级定时器TIM1 TIM1是高级定时器,功能更强大,可以输出带死区互补的PWM,常用于电机驱动。它的重映射配置方式类似,但需要额外注意刹车、死区等高级功能的配置。例如,TIM1_CH1默认PA8,可以重映射到PE9。

3. 动态调整PWM频率 上面的例子中,PWM频率是固定的50Hz。但在一些场景,比如控制无刷电机,需要动态改变PWM频率。这时,你可以在程序运行中,通过修改ARR寄存器的值来实时调整频率。记得,如果使能了ARR预装载,新值会在下一个周期生效,避免产生毛刺。调用 TIM_SetAutoreload(TIM3, new_arr) 函数即可。

4. 结合ADC实现闭环控制 一个更高级的应用是:用PWM驱动一个电机,同时用ADC采集电机转速反馈(比如通过编码器或霍尔传感器),然后根据反馈值动态调整PWM的占空比(CCRx),形成一个简单的闭环速度控制系统。这就是很多智能小车和平衡车的基本原理。STM32的定时器甚至可以直接连接编码器接口,硬件自动计数,非常方便。

5. 注意定时器的时钟源 本文例子使用的是内部时钟(CK_INT)。定时器还可以选择外部时钟、其他定时器的输出等作为时钟源。在复杂的同步系统中,这会非常有用。例如,你可以让TIM2作为主模式,输出触发信号给TIM3作为从模式,让两个定时器精确同步启动。

最后,我想说,STM32的定时器和重映射功能就像一套强大的乐高积木。刚开始照着说明书(例程)拼,可能会遇到零件找不到(引脚冲突)或者拼不紧(配置错误)的情况。但一旦你理解了每一块积木的形状和作用(原理),就能自由地搭建出各种有趣的作品(项目)。多动手,多调试,遇到问题耐心查阅手册和寄存器,这些经验积累下来,你就会发现自己已经从一个“调参侠”成长为真正的“硬件魔法师”了。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐