STM32入门:输出比较(OC)函数配置详解 —— 从PWM波形到代码实战


前言
大家好!今天我们来聊聊STM32中一个非常实用的功能——输出比较(Output Compare,简称OC)。这是定时器模块中用于产生PWM波形的核心机制。跟着江科大的课程学到这里,我发现输出比较这块内容虽然函数多,但只要理清楚逻辑,其实非常好上手。同时这一知识点还是非常重要的,下面我把笔记整理成文章,希望能帮到正在学习的你!
一、什么是输出比较(OC)?
1.1 基本概念
输出比较(Output Compare),顾名思义,就是把计数器CNT的值和捕获/比较寄存器CCR的值进行比较,然后根据比较结果来控制输出引脚的电平状态。
我的理解:你可以把CNT想象成一个正在跑步的运动员,CCR就是终点线。当运动员跑到终点线的时候,就触发一个动作(比如让灯亮一下)。通过不断调整终点线的位置,我们就能控制灯亮的时间长短,这就是PWM的基本原理。
输出比较可以实现以下操作:
-
置1:当CNT == CCR时,输出高电平
-
置0:当CNT == CCR时,输出低电平
-
翻转:当CNT == CCR时,电平翻转
1.2 输出比较的核心用途
输出比较最主要的功能就是输出一定频率和占空比的PWM波形,这在电机控制、LED调光、舵机控制等场景中非常常用。
1.3 通道数量
-
每个高级定时器和通用定时器都拥有 4个输出比较通道(对应CH1~CH4)
-
高级定时器的前3个通道(CH1~CH3)额外拥有死区生成和互补输出的功能(带N的通道)
我的理解:4个通道意味着一个定时器可以同时输出4路PWM,控制4个舵机或者4个电机,非常强大!而高级定时器的互补输出主要是给电机驱动用的,比如H桥需要两路互补的PWM信号。
1.4函数的配置步骤
我先以一张图为例来讲解,希望能帮助大家理解

- 开启 TIM 外设时钟 + 对应 GPIO 外设时钟 使能定时器和复用引脚的系统时钟,硬件才能上电工作。
- 配置定时器时基单元 设置预分频器 PSC、自动重装载值 ARR,确定 PWM 的周期和计数频率。
- 配置输出比较单元(捕获比较通道) 设置 CCR 比较寄存器数值、选择 PWM 模式、配置输出极性(正极性:匹配时输出高电平)。
- 配置对应 GPIO 引脚 将 GPIO 设置为定时器复用推挽输出模式,绑定对应 TIM 通道。
- 启动定时器,运行控制 使能 TIM 计数器,后续修改 CCR 值即可改变占空比,实现呼吸灯渐亮渐灭。
二、输出比较常用函数详解
下面我把笔记中涉及的函数按照功能分类,逐个讲解,但我们暂时重点看我标红的一些,其它的了解即可。
2.1 配置输出比较函数(重要)
这是初始化输出比较通道的核心函数,每个通道对应一个:
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); // 初始化通道1
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); // 初始化通道2
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); // 初始化通道3
void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); // 初始化通道4
参数说明:
TIMx:指定定时器,如TIM2、TIM3等
TIM_OCInitStruct:输出比较初始化结构体指针,里面包含了模式、极性、使能状态等配置
2.2 给结构体赋默认值函数
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct); // 给输出比较结构体赋默认值
我的理解:和GPIO初始化一样,先调用这个函数把结构体填满默认值,然后再修改我们关心的那几个参数。这是个好习惯,避免结构体里有些未初始化的垃圾值导致奇怪的问题。
2.3 配置强制输出模式函数
void TIM_ForcedOC1Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction); // 强制通道1输出
void TIM_ForcedOC2Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction); // 强制通道2输出
void TIM_ForcedOC3Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction); // 强制通道3输出
void TIM_ForcedOC4Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction); // 强制通道4输出
功能说明:在运行中想要暂停输出波形,并且强制输出高电平或低电平。
我的理解:这个函数就像是PWM的"紧急刹车"。比如电机正在转,你想立刻让它停住,就可以强制输出低电平。强制输出高电平相当于占空比100%,强制输出低电平相当于占空比0%。
2.4 配置CCR寄存器的预装功能函数
void TIM_OC1PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload); // 通道1预装配置
void TIM_OC2PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload); // 通道2预装配置
void TIM_OC3PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload); // 通道3预装配置
void TIM_OC4PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload); // 通道4预装配置
预装功能就是影子寄存器:写入的值不会立即生效,而是在更新事件(比如计数器溢出)时才会生效。
2.5 配置快速使能函数
void TIM_OC1FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast); // 通道1快速使能
void TIM_OC2FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast); // 通道2快速使能
void TIM_OC3FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast); // 通道3快速使能
void TIM_OC4FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast); // 通道4快速使能
我的理解:快速使能主要用于单脉冲模式(OPM),平时用PWM模式时一般不常用。它的作用是在触发事件时尽快把输出置为有效电平,减少延迟。
2.6 外部事件时清除REF信号函数
void TIM_ClearOC1Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear); // 清除通道1的REF信号
void TIM_ClearOC2Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear); // 清除通道2的REF信号
void TIM_ClearOC3Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear); // 清除通道3的REF信号
void TIM_ClearOC4Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear); // 清除通道4的REF信号
我的理解:REF信号是输出比较的参考信号。这个函数可以在外部事件(比如另一个定时器的触发)发生时,清除当前通道的输出状态。在一些需要同步多个定时器的场景下会用到。
2.7 单独设置输出比较的极性函数
void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity); // 通道1极性
void TIM_OC1NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity); // 通道1互补极性(带N)
void TIM_OC2PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity); // 通道2极性
void TIM_OC2NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity); // 通道2互补极性
void TIM_OC3PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity); // 通道3极性
void TIM_OC3NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity); // 通道3互补极性
void TIM_OC4PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity); // 通道4极性
我的理解:带N的是高级定时器里互补通道的配置函数。极性决定了什么是"有效电平"——如果配置为高电平有效,那PWM输出高的时候就是"亮/转",低电平有效则相反。互补通道的极性配置在驱动H桥时特别重要,要确保上下桥臂不会同时导通。
2.8 单独修改输出使能参数函数
void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCx); // 使能/失能通道
void TIM_CCxNCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCxN); // 使能/失能互补通道
我的理解:这两个函数可以在程序运行中动态开启或关闭某个通道的输出,不需要重新初始化整个定时器。比如你想临时关闭某个舵机,就调用
TIM_CCxCmd(TIM2, TIM_Channel_1, DISABLE)。
2.9 选择输出比较模式函数
void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);
我的理解:这个函数可以在运行时切换输出比较的模式,比如从PWM1模式切换到PWM2模式,或者切换到翻转模式等。参数
TIM_OCMode可以选TIM_OCMode_PWM1、TIM_OCMode_PWM2等。
2.10 单独更改CCR寄存器的值的函数(重要)
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1); // 设置通道1的CCR值
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2); // 设置通道2的CCR值
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3); // 设置通道3的CCR值
void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4); // 设置通道4的CCR值
我的理解:这是PWM调速最核心的函数! 改变CCR的值就是改变占空比。比如ARR=100,CCR=50,占空比就是50%。在程序运行中反复调用这个函数,就能实现PWM占空比的动态调整。
2.11 高级定时器PWM主输出使能函数
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
我的理解:这个函数只有高级定时器(TIM1、TIM8)才需要调用! 通用定时器不需要。高级定时器有一个"主输出使能"(MOE)位,必须把它置1,PWM波形才能真正从引脚输出。这是STM32的一个安全设计,防止高级定时器在配置未完成时就输出信号损坏硬件。
2.12 引脚重映射函数
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
重映射分为两类:
-
完全重映射:四个引脚全换
-
部分重映射:前面两个引脚变了,或者后面两个引脚变了
我的理解:当默认的引脚被其他功能占用时,就可以用重映射把定时器通道换到别的引脚上。比如TIM2的CH1默认是PA0,如果PA0被占用了,可以重映射到PA15。调用这个函数前记得先开启AFIO时钟!(更详细的可以参考手册AFIO那一节的详细解析)
三、程序示例:PWM初始化完整代码
接线图

下面是我以江科大PWM驱动呼吸灯的工程代码为例来讲解的,更多详细工程代码可看STM32_Project: 基于江科大的STM32的教程编写的程序
#include "stm32f10x.h" // 引入STM32F1标准库核心头文件,提供外设所有函数、结构体、宏定义
/**
* @brief TIM2通道1 PWM初始化函数
* @note 配置PA0引脚为TIM2_CH1复用输出,生成PWM波形
* 时钟72MHz,PSC=719,ARR=99,PWM基础频率 = 72000000 / (719+1)/(99+1) = 1000Hz
*/
void PWM_Init(void)
{
// 1. 开启外设时钟:TIM2挂载APB1总线,GPIOA挂载APB2总线,不开启时钟外设无法工作
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
// ========== 引脚重映射代码(备用,注释状态)==========
// 若需要把TIM2_CH1引脚从PA0切换到PA15,取消注释下方代码
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 重映射必须开启AFIO复用时钟
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); // TIM2部分重映射,CH1映射至PA15
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);// 关闭JTAG调试,释放PA15引脚功能
// 2. GPIO引脚配置:PWM输出必须使用复用推挽输出模式,交由定时器外设控制引脚电平
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出,定时器接管引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 选择PA0引脚,对应TIM2_CH1
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 引脚IO翻转速度50MHz
GPIO_Init(GPIOA,&GPIO_InitStructure); // 将配置写入GPIOA寄存器
// 3. 定时器时钟源配置:选择内部72MHz系统时钟作为TIM计数时钟,默认就是内部时钟,可省略
TIM_InternalClockConfig(TIM2);
// 4. 定时器时基单元配置:决定PWM输出基础频率
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 定时器时钟不分频,采样滤波用
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式:CNT从0递增到ARR溢出
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; // ARR自动重装载寄存器,计数上限0~99,共100个计数点
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; // PSC预分频器,72MHz/720=100KHz定时器计数时钟
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数器,仅高级定时器(TIM1/TIM8)生效,通用定时器填0即可
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure); // 写入时基配置到TIM2寄存器
// 5. 输出比较单元(PWM核心)配置
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); // 给输出比较结构体填充默认值,避免未初始化脏数据导致波形异常
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1:CNT<CCR时输出有效电平
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 有效电平为高电平
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 使能通道输出
TIM_OCInitStructure.TIM_Pulse = 0; // CCR捕获比较寄存器初始值,初始占空比0%
TIM_OC1Init(TIM2,&TIM_OCInitStructure); // 将配置写入TIM2通道1输出比较寄存器
// 6. 使能TIM2定时器,定时器开始计数,PWM波形持续输出
TIM_Cmd(TIM2,ENABLE);
}
/**
* @brief 动态修改TIM2_CH1的CCR值,实时调节PWM占空比
* @param Compare:要设置的CCR数值,范围0~99,数值越大占空比越高
* @note 无需重新初始化定时器,运行中直接调用即可修改波形
*/
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare); // 库函数底层直接修改CCR1寄存器
}
四、💡 个人理解 / 心得
学输出比较这块,我觉得最关键的一点是要理解 CNT、CCR、ARR 三者之间的关系。
CNT是那个一直在跑的计数器,ARR是它跑到的最大值(终点),CCR是中间的一个标记点。PWM1模式下,CNT从0开始往上跑,跑到CCR之前输出高电平,跑到CCR之后输出低电平,跑到ARR后归零重新开始。所以CCR越大,高电平持续时间越长,占空比就越大。
另外,我发现输出比较的函数虽然看起来很多,但其实有规律可循:凡是带数字1/2/3/4的,就是对应4个通道;凡是带N的,就是高级定时器的互补通道。记住这个规律,看到函数名就能猜出它的功能。
还有就是预装功能(Preload)一定要开启,特别是做电机调速的时候。如果不开启,你改CCR的值会立刻生效,可能导致电机突然抖动一下。开启预装后,新值会在下一个周期生效,调速过程就很平滑。
五、🐛 踩坑记录
坑1:GPIO模式一定要选复用推挽输出(AF_PP) 我最开始写的时候惯性思维选了
GPIO_Mode_Out_PP(普通推挽输出),结果PWM怎么都不出来。后来才想起来,定时器的PWM输出属于复用功能,引脚控制权要交给片上外设,必须用GPIO_Mode_AF_PP。这个坑我踩了半小时,大家一定要注意!坑2:TIM2 不需要
TIM_CtrlPWMOutputs我用 TIM2 做 PWM 的时候一切正常,后来换成 TIM1 就不出波形了。查资料才知道只有高级定时器(TIM1、TIM8)需要调用TIM_CtrlPWMOutputs来使能主输出。TIM2 是通用定时器,直接TIM_Cmd(TIM2, ENABLE)就够了,千万别画蛇添足!坑3:引脚重映射前要先开AFIO时钟 调用
GPIO_PinRemapConfig之前,必须先RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE),否则重映射不生效。这个和GPIO复用功能一样,AFIO的时钟是独立的。坑4:CCR值不要超过ARR 有一次我代码里算错了,CCR的值大于了ARR,结果PWM波形直接变成100%占空比(或者0%,取决于极性配置)。因为CNT永远跑不到CCR,所以输出一直保持在一种状态。调试的时候要注意检查CCR的范围。
六、PWM关键参数速查
| 参数 | 寄存器 | 作用 |
|---|---|---|
| PSC | 预分频器 | 对72MHz时钟分频,决定计数频率 |
| ARR | 自动重装载寄存器 | 决定PWM周期(频率) |
| CCR | 捕获/比较寄存器 | 决定PWM占空比 |
| 频率公式 | — | f=72MHz/(PSC+1)/(ARR+1) |
| 占空比公式 | — | D=CCR/(ARR+1)×100% |
写这篇文章的时候,我把江科大的视频又回顾了一遍,把 TIM2 输出比较相关的函数和引脚对应关系重新梳理了一下。TIM2 作为最常用的通用定时器之一,做 PWM 输出非常方便,只要记住开时钟 → 配 GPIO(复用推挽)→ 配时基 → 配输出比较 → 开定时器这几步,基本不会出错。
如果你也在学江科大的 STM32 课程,欢迎在评论区交流心得。特别是做 PWM 实验的时候,记得先用示波器或逻辑分析仪看一下波形,确认频率和占空比都对上了,再接到电机或舵机上,这样不容易烧东西。
📌 开发环境:Keil5 + STM32F103C8T6最小系统板 + ST-Link
📌 参考资料:江科大STM32入门教程、《STM32F10xxx参考手册》
📌 前置文章:STM32定时器超详解:从时钟树到影子寄存器,一文吃透定时中断
如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

更多推荐


所有评论(0)