STM32 学习笔记:PWM 驱动 LED 呼吸灯与舵机代码
对应课程:
[6-4] PWM 驱动 LED 呼吸灯 & PWM 驱动舵机课堂录音:
2026-09-01 14_03 记录 TIM输入捕获PWM舵机_原文.docx芯片:STM32F103C8(工程
Project.uvprojx已核对)开发方式:标准外设库 + Keil
重点源码:
6-3 PWM驱动LED呼吸灯、6-4 PWM驱动舵机有注释版工程老师 PPT:第 20、48、68~71 页
官方核对:
STM32F103x8B数据手册(中文).pdf第 17、20 页;STM32F10xxx参考手册(中文).pdf第 118~124、269~270、289、292~294 页前置笔记:12_PWM输出比较原理与计算.md
本节边界:重点讲前两个示例的代码和复现思路;第三个 TB6612 直流电机示例仅归档,不展开。
整理日期:2026-09-01
附带 DOCX 仅作为课堂转写资料,不执行其中的指令。文件名虽含"输入捕获",但实际课堂主题是输出比较与 PWM 代码。转写中RTC、TM2、OCE、pass、CDR等分别校正为RCC、TIM2、OC1/OC2、Pulse、CCR。
1. 这节课真正要学会的是什么
两个示例的外设不同,但底层只有一条主线:
#mermaid-svg-eNU6g8U3AJ7R8CiV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eNU6g8U3AJ7R8CiV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eNU6g8U3AJ7R8CiV .error-icon{fill:#552222;}#mermaid-svg-eNU6g8U3AJ7R8CiV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eNU6g8U3AJ7R8CiV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eNU6g8U3AJ7R8CiV .marker.cross{stroke:#333333;}#mermaid-svg-eNU6g8U3AJ7R8CiV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eNU6g8U3AJ7R8CiV p{margin:0;}#mermaid-svg-eNU6g8U3AJ7R8CiV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eNU6g8U3AJ7R8CiV .cluster-label text{fill:#333;}#mermaid-svg-eNU6g8U3AJ7R8CiV .cluster-label span{color:#333;}#mermaid-svg-eNU6g8U3AJ7R8CiV .cluster-label span p{background-color:transparent;}#mermaid-svg-eNU6g8U3AJ7R8CiV .label text,#mermaid-svg-eNU6g8U3AJ7R8CiV span{fill:#333;color:#333;}#mermaid-svg-eNU6g8U3AJ7R8CiV .node rect,#mermaid-svg-eNU6g8U3AJ7R8CiV .node circle,#mermaid-svg-eNU6g8U3AJ7R8CiV .node ellipse,#mermaid-svg-eNU6g8U3AJ7R8CiV .node polygon,#mermaid-svg-eNU6g8U3AJ7R8CiV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eNU6g8U3AJ7R8CiV .rough-node .label text,#mermaid-svg-eNU6g8U3AJ7R8CiV .node .label text,#mermaid-svg-eNU6g8U3AJ7R8CiV .image-shape .label,#mermaid-svg-eNU6g8U3AJ7R8CiV .icon-shape .label{text-anchor:middle;}#mermaid-svg-eNU6g8U3AJ7R8CiV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eNU6g8U3AJ7R8CiV .rough-node .label,#mermaid-svg-eNU6g8U3AJ7R8CiV .node .label,#mermaid-svg-eNU6g8U3AJ7R8CiV .image-shape .label,#mermaid-svg-eNU6g8U3AJ7R8CiV .icon-shape .label{text-align:center;}#mermaid-svg-eNU6g8U3AJ7R8CiV .node.clickable{cursor:pointer;}#mermaid-svg-eNU6g8U3AJ7R8CiV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eNU6g8U3AJ7R8CiV .arrowheadPath{fill:#333333;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eNU6g8U3AJ7R8CiV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eNU6g8U3AJ7R8CiV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eNU6g8U3AJ7R8CiV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eNU6g8U3AJ7R8CiV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eNU6g8U3AJ7R8CiV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eNU6g8U3AJ7R8CiV .cluster text{fill:#333;}#mermaid-svg-eNU6g8U3AJ7R8CiV .cluster span{color:#333;}#mermaid-svg-eNU6g8U3AJ7R8CiV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eNU6g8U3AJ7R8CiV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eNU6g8U3AJ7R8CiV rect.text{fill:none;stroke-width:0;}#mermaid-svg-eNU6g8U3AJ7R8CiV .icon-shape,#mermaid-svg-eNU6g8U3AJ7R8CiV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eNU6g8U3AJ7R8CiV .icon-shape p,#mermaid-svg-eNU6g8U3AJ7R8CiV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eNU6g8U3AJ7R8CiV .icon-shape .label rect,#mermaid-svg-eNU6g8U3AJ7R8CiV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eNU6g8U3AJ7R8CiV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eNU6g8U3AJ7R8CiV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eNU6g8U3AJ7R8CiV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 明确负载要什么波形
查 TIMx_CHy 对应引脚
计算 PSC、ARR、CCR
配置 GPIO 复用推挽
配置 TIM 时基
配置输出比较通道
使能 TIM
先输出固定 PWM 验证
运行中修改 CCR
封装成亮度或角度接口
两个程序的区别可以先压缩成一张表:
| 项目 | LED 呼吸灯 | SG90 舵机 |
|---|---|---|
| 定时器通道 | TIM2_CH1 | TIM2_CH2 |
| 默认引脚 | PA0 | PA1 |
| 目标频率 | 1 kHz | 50 Hz(20 ms) |
| PSC | 720-1 |
72-1 |
| ARR | 100-1 |
20000-1 |
| CCR 有用范围 | 0~100 | 500~2500(课堂模型) |
| CCR 的直观含义 | 数值恰好等于占空比百分数 | 数值恰好等于高电平宽度,单位 µs |
| 上层接口 | PWM_SetCompare1() |
Servo_SetAngle() |
1.1 课堂时间线
| 录音时间 | 内容 |
|---|---|
00:07--02:47 |
LED 接线、PWM 模块创建和五步配置框架 |
02:47--05:43 |
输出比较库函数和课堂问题:如何分类记忆 |
05:43--11:28 |
PWM_Init():时基、OC 结构体、模式、极性、使能、CCR |
11:28--14:41 |
TIM2_CH1--PA0 复用映射和 GPIO_Mode_AF_PP |
14:41--19:40 |
1 kHz/1% 参数计算、示波器验证、CCR 渐变生成呼吸灯 |
19:40--24:28 |
TIM2_CH1 从 PA0 部分重映射到 PA15,以及 JTAG/SWD 引脚处理 |
24:28--29:30 |
舵机接线、切换 TIM2_CH2/PA1、50 Hz 与 0.5~2.5 ms 计算 |
29:30--33:13 |
Servo 模块封装、角度到 CCR 线性映射、按键与 OLED |
33:27--结束 |
第三个直流电机示例,本笔记不展开 |
2. 写代码前先把硬件链路想明白

图 1 老师 PPT 第 68 页:PWM 的时基单元、输出比较单元和 GPIO 信号链。
2.1 为什么 GPIO 一定要用复用推挽

图 2 老师 PPT 第 20 页:输出驱动器前可在 GPIO 输出数据和片上外设输出之间选择。
GPIO_Mode_Out_PP 时,控制源是 GPIO 的 ODR/BSRR;GPIO_Mode_AF_PP 时,控制源切换为定时器输出比较通道。
text
普通推挽:CPU 写 ODR/BSRR → 引脚高低电平
复用推挽:TIM2_CHx 的 OCx 波形 → 引脚高低电平
如果定时器已经在内部计数,但引脚还配成普通输出,内部 OCxREF 再正确也无法通过引脚输出。
2.2 为什么 CH1 不能随便接任意 GPIO
定时器通道与引脚之间是芯片内部已布线的复用关系。STM32F103C8 的 TIM2 默认映射为:
| TIM2 通道 | 默认 GPIO |
|---|---|
| CH1/ETR | PA0 |
| CH2 | PA1 |
| CH3 | PA2 |
| CH4 | PA3 |
这个结论应查 STM32F103x8B数据手册 第 17、20 页的引脚定义/复用功能表,不是从库函数名猜出来的。
2.3 为什么不需要 NVIC 和中断
这两个例子都是硬件 PWM:CNT 计数、CNT--CCR 比较、引脚翻转都由 TIM2 内部自动完成。CPU 只在需要改亮度或角度时写一次 CCR,所以不需要:
TIM_ITConfig();NVIC_Init();TIM2_IRQHandler()。
这也是硬件 PWM 比在定时中断里手动翻转 GPIO 更稳定的原因。
3. 课堂问题:PWM 相关函数怎样分类记忆
录音 05:09 的 「列一个表格,帮我总结一下PWM的相关函数???」 可以按"必须掌握"和"特殊功能了解"分类:
3.1 必须掌握
| 函数 | 作用 | 最终影响的硬件 |
|---|---|---|
TIM_TimeBaseInit() |
配置时基 | PSC、ARR、计数方向 |
TIM_OCStructInit() |
先给 OC 结构体全成员默认值 | 避免未赋值成员带随机值 |
TIM_OC1Init() ~ TIM_OC4Init() |
配置对应输出比较通道 | CCMRx、CCER、CCRx |
TIM_SetCompare1() ~ TIM_SetCompare4() |
运行中改对应 CCR | 改占空比/脉宽 |
TIM_Cmd() |
启停定时器 | CR1.CEN |
3.2 本节了解即可
| 函数类别 | 用途 |
|---|---|
TIM_ForcedOCxConfig() |
强制通道为有效/无效电平 |
TIM_OCxPreloadConfig() |
使 CCR 新值在更新事件时统一生效 |
TIM_OCxFastConfig() |
单脉冲等特殊场景的快速比较 |
TIM_ClearOCxRef() |
外部事件清除 OCxREF |
TIM_OCxPolarityConfig() |
运行中单独改输出极性 |
TIM_CCxCmd() / TIM_CCxNCmd() |
单独使能主通道/互补通道 |
TIM_SelectOCxM() |
运行中单独改输出比较模式 |
记忆方法:初始化时用 TIM_OCxInit(),运行时最常用 TIM_SetComparex()。
4. 两个示例共用的 PWM_Init() 思维
以 LED 工程的 Hardware/PWM.c 为基准,初始化可拆成 6 个硬件问题。
4.1 打开 TIM2 和 GPIOA 时钟
c
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
- TIM2 挂在 APB1,所以用
RCC_APB1PeriphClockCmd(); - GPIOA 挂在 APB2,所以用
RCC_APB2PeriphClockCmd(); - 开 GPIOA 时钟不等于开 TIM2 时钟,两条时钟链都要打通。
本工程默认时钟树下 TIM2 的计时器输入时钟为 72 MHz。注意 APB1 总线频率通常是 36 MHz,但 APB1 预分频器不为 1 时,定时器时钟会按 STM32F1 规则倍频。移植到其他时钟配置时,不能永远假设是 72 MHz。
4.2 把引脚交给 TIM2
LED 例子:
c
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
舵机例子只把 GPIO_Pin_0 改为 GPIO_Pin_1。50MHz 是 STM32F1 GPIO 输出级的速度档位,不是 PWM 频率。
4.3 选择内部时钟
c
TIM_InternalClockConfig(TIM2);
TIM 复位后默认就是内部时钟,不写通常也能工作。老师保留它是为了让"时钟源选择"这一步显式、便于以后改外部时钟时对照。
4.4 配置时基单元
c
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = ARR;
TIM_TimeBaseInitStructure.TIM_Prescaler = PSC;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
| 成员 | 真正含义 |
|---|---|
TIM_ClockDivision |
数字滤波/死区采样基准,不是 PSC,本例不直接影响 CNT |
TIM_CounterMode_Up |
CNT 从 0 数到 ARR |
TIM_Period |
写 ARR,决定一个周期有几个计数拍 |
TIM_Prescaler |
写 PSC,决定 CNT 每秒走几拍 |
TIM_RepetitionCounter |
高级定时器功能,TIM2 不使用 |
4.5 配置输出比较通道
c
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OCStructInit() 不是"可有可无的格式"。TIM_OCInitTypeDef 还有高级定时器会用到的成员;局部变量若没有全部赋值,未赋值成员会带不确定值。先用库函数赋默认值,再覆盖本例需要的成员,移植到 TIM1 时更安全。
| 配置 | 为什么这样选 |
|---|---|
TIM_OCMode_PWM1 |
向上计数时,CNT<CCR 为有效 |
TIM_OCPolarity_High |
有效电平为高;与 LED 正极接信号、舵机高脉冲命令一致 |
TIM_OutputState_Enable |
打开对应 CCx 通道输出 |
TIM_Pulse=0 |
初始 CCR=0,先从 0%/无高脉冲开始 |
然后根据使用的通道调用:
c
TIM_OC1Init(TIM2, &TIM_OCInitStructure); // CH1 / PA0
// 或
TIM_OC2Init(TIM2, &TIM_OCInitStructure); // CH2 / PA1
4.6 启动定时器
c
TIM_Cmd(TIM2, ENABLE);
这一行设置 CR1.CEN。之前的代码只是把参数写入定时器,到这里 CNT 才开始计数。
5. 示例一:PWM 驱动 LED 呼吸灯
5.1 硬件连接与极性

图 3 配套源码 6-3 PWM驱动LED呼吸灯 接线图:LED 正极接 PA0,负极接 GND。
这是高电平点亮的正极性接法:
text
PA0高 → 有电流流过LED → 亮
PA0低 → 无压差 → 灭
占空比大 → 平均电流大 → 更亮
**硬件勘误:**配套接线图画的是裸 LED 直接 PA0,没有画串联限流电阻。复现时应在 LED 回路串联限流电阻,常见可先用
330 Ω~1 kΩ。若使用已自带限流电阻的 LED 模块,则按模块标注接线。
5.2 先从需求反推参数
老师先设定:
- PWM 频率 1 kHz;
- 占空比可以 0%~100% 调节;
- 最小步进 1%。

图 4 老师 PPT 第 69 页:频率、占空比和分辨率公式。
1% 分辨率先决定 ARR:
1 A R R + 1 = 1 % ⇒ A R R + 1 = 100 ⇒ A R R = 99 \frac{1}{ARR+1}=1\%\Rightarrow ARR+1=100\Rightarrow ARR=99 ARR+11=1%⇒ARR+1=100⇒ARR=99
再用 72 MHz 和 1 kHz 反推 PSC:
1000 = 72 000 000 ( P S C + 1 ) × 100 1000=\frac{72\,000\,000}{(PSC+1)\times100} 1000=(PSC+1)×10072000000
P S C + 1 = 720 ⇒ P S C = 719 PSC+1=720\Rightarrow PSC=719 PSC+1=720⇒PSC=719
因此源码写:
c
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;
这个参数组还有一个很好的教学特点:
D = C C R 100 D=\frac{CCR}{100} D=100CCR
所以 CCR=37 恰好是 37%,数值很直观。CNT 每拍 10 µs,100 拍组成 1 ms周期。
5.3 LED 版 PWM_Init() 完整核心代码
c
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
定时器会一直输出 1 kHz、初始 0% 的 PWM。之后的呼吸动画不需要重新初始化 TIM2,只要改 CCR1:
c
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
这个封装的信号链是:
#mermaid-svg-H2oXR0f3t2VB7hvX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-H2oXR0f3t2VB7hvX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-H2oXR0f3t2VB7hvX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-H2oXR0f3t2VB7hvX .error-icon{fill:#552222;}#mermaid-svg-H2oXR0f3t2VB7hvX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-H2oXR0f3t2VB7hvX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-H2oXR0f3t2VB7hvX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-H2oXR0f3t2VB7hvX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-H2oXR0f3t2VB7hvX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-H2oXR0f3t2VB7hvX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-H2oXR0f3t2VB7hvX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-H2oXR0f3t2VB7hvX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-H2oXR0f3t2VB7hvX .marker.cross{stroke:#333333;}#mermaid-svg-H2oXR0f3t2VB7hvX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-H2oXR0f3t2VB7hvX p{margin:0;}#mermaid-svg-H2oXR0f3t2VB7hvX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-H2oXR0f3t2VB7hvX .cluster-label text{fill:#333;}#mermaid-svg-H2oXR0f3t2VB7hvX .cluster-label span{color:#333;}#mermaid-svg-H2oXR0f3t2VB7hvX .cluster-label span p{background-color:transparent;}#mermaid-svg-H2oXR0f3t2VB7hvX .label text,#mermaid-svg-H2oXR0f3t2VB7hvX span{fill:#333;color:#333;}#mermaid-svg-H2oXR0f3t2VB7hvX .node rect,#mermaid-svg-H2oXR0f3t2VB7hvX .node circle,#mermaid-svg-H2oXR0f3t2VB7hvX .node ellipse,#mermaid-svg-H2oXR0f3t2VB7hvX .node polygon,#mermaid-svg-H2oXR0f3t2VB7hvX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-H2oXR0f3t2VB7hvX .rough-node .label text,#mermaid-svg-H2oXR0f3t2VB7hvX .node .label text,#mermaid-svg-H2oXR0f3t2VB7hvX .image-shape .label,#mermaid-svg-H2oXR0f3t2VB7hvX .icon-shape .label{text-anchor:middle;}#mermaid-svg-H2oXR0f3t2VB7hvX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-H2oXR0f3t2VB7hvX .rough-node .label,#mermaid-svg-H2oXR0f3t2VB7hvX .node .label,#mermaid-svg-H2oXR0f3t2VB7hvX .image-shape .label,#mermaid-svg-H2oXR0f3t2VB7hvX .icon-shape .label{text-align:center;}#mermaid-svg-H2oXR0f3t2VB7hvX .node.clickable{cursor:pointer;}#mermaid-svg-H2oXR0f3t2VB7hvX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-H2oXR0f3t2VB7hvX .arrowheadPath{fill:#333333;}#mermaid-svg-H2oXR0f3t2VB7hvX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-H2oXR0f3t2VB7hvX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-H2oXR0f3t2VB7hvX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-H2oXR0f3t2VB7hvX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-H2oXR0f3t2VB7hvX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-H2oXR0f3t2VB7hvX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-H2oXR0f3t2VB7hvX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-H2oXR0f3t2VB7hvX .cluster text{fill:#333;}#mermaid-svg-H2oXR0f3t2VB7hvX .cluster span{color:#333;}#mermaid-svg-H2oXR0f3t2VB7hvX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-H2oXR0f3t2VB7hvX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-H2oXR0f3t2VB7hvX rect.text{fill:none;stroke-width:0;}#mermaid-svg-H2oXR0f3t2VB7hvX .icon-shape,#mermaid-svg-H2oXR0f3t2VB7hvX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-H2oXR0f3t2VB7hvX .icon-shape p,#mermaid-svg-H2oXR0f3t2VB7hvX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-H2oXR0f3t2VB7hvX .icon-shape .label rect,#mermaid-svg-H2oXR0f3t2VB7hvX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-H2oXR0f3t2VB7hvX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-H2oXR0f3t2VB7hvX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-H2oXR0f3t2VB7hvX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PWM_SetCompare1 i
TIM_SetCompare1
写 TIM2_CCR1
改变 CNT与CCR1 交点
改变 CH1 高电平宽度
PA0 PWM
LED 平均亮度改变
5.4 main.c 为什么能变成呼吸灯
c
uint8_t i;
int main(void)
{
OLED_Init();
PWM_Init();
while (1)
{
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(i);
Delay_ms(10);
}
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(100 - i);
Delay_ms(10);
}
}
}
第一个 for 循环让 CCR1 从 0 变到 100,占空比从 0% 变到 100%,LED 逐渐变亮。第二个循环做相反的事,LED 逐渐变暗。
每步延时 10 ms,单程约有 101 步,因此变亮约 1.01 s,变暗约 1.01 s,完整呼吸周期约 2.02 s(不计函数执行时间)。
Delay_ms(10)不是 PWM 的周期。PWM 由 TIM2 在后台以 1 kHz 连续运行;这个延时只决定 CPU 多久改一次 CCR,即"呼吸速度"。
5.5 先别急着写呼吸循环
最容易定位故障的复现顺序是:
- 只调
PWM_Init(),把TIM_Pulse或PWM_SetCompare1()固定为 50; - 确认 LED 中等亮度;
- 有示波器/逻辑分析仪时,验证周期 1 ms、高电平 0.5 ms;
- 再测
CCR=10、CCR=90,确认频率不变,只改脉宽; - 最后才加两个
for循环。
如果一开始就写全部动画逻辑,出问题时就很难区分是 PWM 初始化错,还是循环/延时错。
6. LED 例子中的可选内容:TIM2 重映射到 PA15

图 5 老师 PPT 第 48 页:AFIO 负责外设引脚重映射。
默认使用 PA0 时不需要 AFIO。只有想把 TIM2_CH1 挪到 PA15 时,才需要解除源码中的注释:
c
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
并把 GPIO 从 GPIO_Pin_0 改成 GPIO_Pin_15。
逻辑上分三件事:
- 打开 AFIO 时钟;
- 选择 TIM2 部分重映射 1,将 CH1/ETR 改到 PA15;
- PA15 默认是 JTAG 调试引脚 JTDI,所以关掉 JTAG,但保留 SWD。
不要随手调用完全关闭 SWJ 的选项,否则 PA13/PA14 的 SWD 也会丢失,ST-Link 可能无法继续下载。TIM2 重映射关系可查参考手册第 118~124 页的 AFIO 重映射表和
AFIO_MAPR。
7. 示例二:PWM 驱动 SG90 舵机
7.1 先分清"供电"和"命令"

图 6 配套源码 6-4 PWM驱动舵机 接线图:舵机信号线接 PA1,按键接 PB1。
舵机三根线的作用:
| 常见颜色 | 作用 | 课堂接法 |
|---|---|---|
| 棕/黑 | GND | 与 STM32 GND 共地 |
| 红 | 舵机功率电源 | 5 V |
| 橙/黄 | PWM 命令信号 | PA1 / TIM2_CH2 |
舵机的 PWM 线只传递位置命令,不为电机线圈供电。供电地和 STM32 地必须连在一起,否则 3.3 V PWM 没有共同电平参考。
**供电修正:**课堂与配套图从 ST-Link 5 V 给舵机供电,简单空载演示可能能工作,但不应把它视为所有情况都可靠的方案。舵机启动和堵转电流较大,正式复现建议使用有足够电流余量的独立稳压 5 V 电源,并与 STM32 共地。
7.2 PPT 与当前源码的引脚差异

图 7 老师 PPT 第 71 页:概念图将舵机 PWM 标为 PA0。
PPT 第 71 页画的是 PA0,但本次 6-4 PWM驱动舵机 实际工程和配套接线图均使用 PA1/TIM2_CH2。
复现本工程时以源码为准:橙色 PWM 信号线接 PA1。若想改用 PA0,必须同时把 GPIO、
TIM_OC2Init、TIM_SetCompare2改为 CH1 对应版本,不能只换实物线。
7.3 舵机需要什么 PWM

图 8 老师 PPT 第 70 页:本课采用 20 ms 周期、0.5~2.5 ms 高脉宽映射 0~180°。
本课模型:
- 周期
20 ms,频率50 Hz; - 0.5 ms 高电平 → 0°;
- 1.5 ms 高电平 → 90°;
- 2.5 ms 高电平 → 180°。
这里舵机解码的是"高电平持续了多少微秒",不是把 2.5%~12.5% 占空比当成给电机的平均电压。
0.5~2.5 ms 是课堂采用的 SG90 映射。不同舵机、不同批次的可靠端点可能不同,初次使用应查模块资料并从中位置向两端逐步试探。
7.4 为什么舵机参数是 72-1、20000-1
先把 CNT 设成每 1 µs 走一格:
f C N T = 72 M H z 72 = 1 M H z f_{CNT}=\frac{72\,MHz}{72}=1\,MHz fCNT=7272MHz=1MHz
因此:
c
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
20 ms 共有 20 000 个 1 µs:
c
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;
这样一来:
f P W M = 72 000 000 72 × 20000 = 50 H z f_{PWM}=\frac{72\,000\,000}{72\times20000}=50\,Hz fPWM=72×2000072000000=50Hz
而 CCR 的数值可以直接当作微秒:
| CCR2 | 高电平宽度 | 占空比 | 课堂角度 |
|---|---|---|---|
| 500 | 500 µs | 2.5% | 0° |
| 1500 | 1500 µs | 7.5% | 90° |
| 2500 | 2500 µs | 12.5% | 180° |
这是一种很好的参数设计思路:不只追求公式算对,还让寄存器数值具有直观的物理单位。
7.5 舵机版 PWM_Init() 与 LED 版的差异
共享部分不再重复,需要同时改的只有三组:
c
/* 1. 通道引脚:TIM2_CH2 默认是 PA1 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
/* 2. 50 Hz,1 µs/计数拍 */
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
/* 3. 配置通道2 */
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
运行时改 CCR2:
c
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare);
}
这里有一个必须整体替换的"通道套件":
text
CH1 版:PA0 + TIM_OC1Init + TIM_SetCompare1 + CCR1
CH2 版:PA1 + TIM_OC2Init + TIM_SetCompare2 + CCR2
只改其中一项,就会出现"编译通过但引脚没波形"。
7.6 为什么要再封装一层 Servo
底层 PWM 接口:
c
PWM_SetCompare2(1500);
对定时器很直观,但对应用层不直观:1500 是微秒,还是占空比,还是角度?
老师因此增加 Servo.c:
c
void Servo_Init(void)
{
PWM_Init();
}
void Servo_SetAngle(float Angle)
{
PWM_SetCompare2(Angle / 180 * 2000 + 500);
}
现在上层可以直接写:
c
Servo_SetAngle(90);
调用链变成:
#mermaid-svg-v42sp1l64fyST3Dr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-v42sp1l64fyST3Dr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-v42sp1l64fyST3Dr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-v42sp1l64fyST3Dr .error-icon{fill:#552222;}#mermaid-svg-v42sp1l64fyST3Dr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-v42sp1l64fyST3Dr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-v42sp1l64fyST3Dr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-v42sp1l64fyST3Dr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-v42sp1l64fyST3Dr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-v42sp1l64fyST3Dr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-v42sp1l64fyST3Dr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-v42sp1l64fyST3Dr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-v42sp1l64fyST3Dr .marker.cross{stroke:#333333;}#mermaid-svg-v42sp1l64fyST3Dr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-v42sp1l64fyST3Dr p{margin:0;}#mermaid-svg-v42sp1l64fyST3Dr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-v42sp1l64fyST3Dr .cluster-label text{fill:#333;}#mermaid-svg-v42sp1l64fyST3Dr .cluster-label span{color:#333;}#mermaid-svg-v42sp1l64fyST3Dr .cluster-label span p{background-color:transparent;}#mermaid-svg-v42sp1l64fyST3Dr .label text,#mermaid-svg-v42sp1l64fyST3Dr span{fill:#333;color:#333;}#mermaid-svg-v42sp1l64fyST3Dr .node rect,#mermaid-svg-v42sp1l64fyST3Dr .node circle,#mermaid-svg-v42sp1l64fyST3Dr .node ellipse,#mermaid-svg-v42sp1l64fyST3Dr .node polygon,#mermaid-svg-v42sp1l64fyST3Dr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-v42sp1l64fyST3Dr .rough-node .label text,#mermaid-svg-v42sp1l64fyST3Dr .node .label text,#mermaid-svg-v42sp1l64fyST3Dr .image-shape .label,#mermaid-svg-v42sp1l64fyST3Dr .icon-shape .label{text-anchor:middle;}#mermaid-svg-v42sp1l64fyST3Dr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-v42sp1l64fyST3Dr .rough-node .label,#mermaid-svg-v42sp1l64fyST3Dr .node .label,#mermaid-svg-v42sp1l64fyST3Dr .image-shape .label,#mermaid-svg-v42sp1l64fyST3Dr .icon-shape .label{text-align:center;}#mermaid-svg-v42sp1l64fyST3Dr .node.clickable{cursor:pointer;}#mermaid-svg-v42sp1l64fyST3Dr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-v42sp1l64fyST3Dr .arrowheadPath{fill:#333333;}#mermaid-svg-v42sp1l64fyST3Dr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-v42sp1l64fyST3Dr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-v42sp1l64fyST3Dr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-v42sp1l64fyST3Dr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-v42sp1l64fyST3Dr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-v42sp1l64fyST3Dr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-v42sp1l64fyST3Dr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-v42sp1l64fyST3Dr .cluster text{fill:#333;}#mermaid-svg-v42sp1l64fyST3Dr .cluster span{color:#333;}#mermaid-svg-v42sp1l64fyST3Dr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-v42sp1l64fyST3Dr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-v42sp1l64fyST3Dr rect.text{fill:none;stroke-width:0;}#mermaid-svg-v42sp1l64fyST3Dr .icon-shape,#mermaid-svg-v42sp1l64fyST3Dr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-v42sp1l64fyST3Dr .icon-shape p,#mermaid-svg-v42sp1l64fyST3Dr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-v42sp1l64fyST3Dr .icon-shape .label rect,#mermaid-svg-v42sp1l64fyST3Dr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-v42sp1l64fyST3Dr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-v42sp1l64fyST3Dr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-v42sp1l64fyST3Dr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Servo_SetAngle Angle
0~180度 线性映射
500~2500 µs
PWM_SetCompare2
写 TIM2_CCR2
TIM2_CH2 生成高脉冲
PA1
舵机内部控制器解码角度
7.7 角度映射公式怎样推出
输入范围 0~180,输出范围 500~2500。输出跨度是 2500-500=2000,所以:
C C R = 500 + A n g l e 180 × 2000 CCR=500+\frac{Angle}{180}\times2000 CCR=500+180Angle×2000
验证端点:
text
Angle = 0 → CCR = 500
Angle = 90 → CCR = 1500
Angle = 180 → CCR = 2500
源码中 Angle 是 float,因此 Angle / 180 会做浮点运算。若以后改成整数参数,不要写成先做整数除法的形式,否则 0~179 都可能被截成 0。
更明确的写法是:
c
float compare = 500.0f + Angle * (2000.0f / 180.0f);
PWM_SetCompare2((uint16_t)compare);
7.8 对源码的安全性补强
原版 Servo_SetAngle() 默认调用者一定传入 0~180。当 Angle<0 或 Angle>180 时,它会生成超出课堂范围的脉宽。复现成功后,可以再增加边界保护:
c
void Servo_SetAngle(float Angle)
{
uint16_t Compare;
if (Angle < 0.0f)
{
Angle = 0.0f;
}
if (Angle > 180.0f)
{
Angle = 180.0f;
}
Compare = (uint16_t)(500.0f + Angle * (2000.0f / 180.0f) + 0.5f);
PWM_SetCompare2(Compare);
}
+0.5f 是在转成整数微秒前做四舍五入。这是"工程增强版",不是录音中老师的原始代码。
7.9 main.c 的上层逻辑
c
uint8_t KeyNum;
float Angle;
int main(void)
{
OLED_Init();
Servo_Init();
Key_Init();
OLED_ShowString(1, 1, "Angle:");
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
Angle += 30;
if (Angle > 180)
{
Angle = 0;
}
}
Servo_SetAngle(Angle);
OLED_ShowNum(1, 7, Angle, 3);
}
}
这里的分层非常重要:
| 层级 | 关心的问题 |
|---|---|
main.c |
按键如何改角度,OLED 显示什么 |
Servo.c |
角度如何变成高电平微秒数 |
PWM.c |
TIM2_CH2 如何产生指定脉宽 |
| 标准库 | 如何写 CCR2、CCMR1、CCER、PSC、ARR |
| 硬件 | PA1 输出波形,舵机内部解码 |
这样封装后,如果以后换了舵机脉宽范围,主要改 Servo.c;换了定时器或引脚,主要改 PWM.c;main.c 的业务逻辑不必跟着大改。
8. 用同一个定时器输出多路 PWM 时要知道什么
TIM2_CH1~CH4 共享一套 PSC、ARR、CNT,但各自有 CCR1~CCR4。所以:
- 同一个 TIM 的多个通道天然共享频率;
- 各通道可以有不同占空比;
- 边沿基于同一 CNT,波形周期起点同步。
因此 LED 的 1 kHz 和舵机的 50 Hz 不能在当前两个独立工程的参数下,简单合并到同一个 TIM2 同时输出。要同时保留 1 kHz LED PWM 和 50 Hz 舵机 PWM,通常应让它们使用不同定时器,或重新设计整个时序方案。
9. 复现一个新 PWM 项目的通用思路
不要从"复制 PWM.c"开始,而要先写一张参数单。
第 1 步:定义负载需求
text
需要固定频率还是固定脉宽?
需要多大的占空比范围?
需要多细的步进?
高电平有效还是低电平有效?
第 2 步:先查引脚,再选通道
- 在数据手册查空闲 GPIO 是哪个
TIMx_CHy; - 确认是默认映射还是重映射;
- 记下定时器挂在 APB1 还是 APB2;
- 确认该引脚没有与 OLED、串口、调试口等冲突。
第 3 步:确认真实 f_TIM
从 RCC 时钟树出发,不要盲抄 72 MHz。记录:
text
f_TIM = ______ Hz
第 4 步:设计计数拍的物理单位
- LED 例子:让 100 拍等于一周期,CCR 直接等于百分数;
- 舵机例子:让 1 拍等于 1 µs,CCR 直接等于脉宽微秒数。
能让数值带有直观单位,后续封装和调试会简单很多。
第 5 步:建立"通道套件"
text
TIMx = ______
CHy = ______
GPIO = ______
TIM_OCyInit = ______
TIM_SetComparey = ______
CCRy = ______
换通道时逐项核对,不要只搜索替换一个数字。
第 6 步:先输出一个固定波形
先不接按键、OLED、呼吸循环、角度算法。只做:
c
PWM_Init();
PWM_SetComparex(一个容易验证的固定值);
先验证引脚、频率、脉宽、极性,再加动态逻辑。
第 7 步:最后做应用层封装
- 呼吸灯封装"亮度 0~100";
- 舵机封装"角度 0~180";
- 调用者不应需要知道 CCR 的具体数字。
10. 故障定位表
| 现象 | 优先检查 | 原因逻辑 |
|---|---|---|
| 编译通过,引脚没波形 | TIM_Cmd、TIM_OutputState_Enable、TIM/GPIO 时钟 |
定时器未启动或通道被关闭 |
| 某个引脚没波形,其他引脚有 | GPIO--CH 映射、TIM_OCxInit |
引脚和通道不是一套 |
| 使用 PA15 后仍没波形 | AFIO 时钟、TIM2 重映射、JTAG 释放 | PA15 默认还被 JTDI 占用 |
| LED 亮暗反了 | LED 接法、TIM_OCPolarity |
有效电平与外部电路极性不匹配 |
| LED 一直亮/一直灭 | CCR 范围、通道函数、循环 | CCR 没被改或写错 CCRy |
| LED 看不出呼吸 | Delay_ms(10)、占空比范围 |
CCR 变化太快或幅度太小 |
| 舵机完全不动 | 5 V 供电、共地、PA1 波形 | PWM 信号线不是功率电源 |
| 舵机抖动/MCU 复位 | 电源电流余量、去耦、共地 | 舵机启动电流造成供电跌落 |
| 舵机顶住嗡响 | 脉宽是否超出实际端点 | 机械已到限位,但控制器还在继续用力 |
| 频率恰好差 2 倍 | APB 定时器倍频规则 | 把 PCLK1 直接当成 TIM2CLK |
| 改了 ARR 后 CCR 含义变了 | 重新计算 CCR/(ARR+1) |
TIM_SetComparex() 写的是 CCR,不是天然的百分比 |
11. 源码与资料中需要记住的勘误
- DOCX 文件名不准确:本课是输出比较 PWM,不是输入捕获。
- PPT 与源码引脚不同 :PPT 舵机图写 PA0,当前
6-4源码使用 PA1/TIM2_CH2。 - LED 图省略了限流电阻:裸 LED 复现时必须串联限流电阻。
- 舵机供电是演示接法:ST-Link 5 V 并非所有舵机负载下都可靠,正式使用建议独立 5 V 电源并共地。
- 舵机版
PWM_SetCompare2()注释过期:源码注释仍写"范围 0~100",这是从 LED 工程复制后未更新;本工程 ARR=19999,舵机实际使用 500~2500。 Servo_SetAngle()未限幅 :当前main.c保证 Angle 是 0~180,但函数自身没防止非法输入。- CCR 不总是占空比百分数 :LED 例子只因
ARR+1=100才看起来相等;舵机例子中 CCR 是微秒数。 - 运行中直接改 CCR 的边界 :原工程未开启 OC 预装载,入门实验可工作;对无毛刺更新要求更高时,可启用
TIM_OCxPreloadConfig()和 ARR 预装载,让参数在更新事件边界生效。
12. 两个示例的最小复现检查表
12.1 LED 呼吸灯
- 裸 LED 已串限流电阻;
- LED 正极接 PA0,负极通过限流电阻接 GND;
- TIM2_CH1 与 PA0 使用默认映射;
- GPIOA 配成 PA0 复用推挽;
- PSC=719,ARR=99;
- OC1 使用 PWM1、高极性、输出使能;
- 先用 CCR1=50 验证 1 kHz/50%;
- 最后加 0→100→0 循环。
12.2 SG90 舵机
- 供电已确认为合适的 5 V,电流余量足够;
- 舵机电源地和 STM32 GND 已共地;
- PWM 信号线接 PA1,不是照 PPT 概念图接 PA0;
- GPIOA 配成 PA1 复用推挽;
- PSC=71,ARR=19999;
- OC2 使用 PWM1、高极性、输出使能;
- 先测 CCR2=1500,验证 50 Hz/1.5 ms;
- 再小心测 500 和 2500,观察机械端点;
- 最后加
Servo_SetAngle()、按键和 OLED。
13. 复习自测
- 为什么定时器输出 PWM 时 GPIO 要配成
GPIO_Mode_AF_PP? - TIM2_CH1 和 TIM2_CH2 在默认映射下分别对应哪个引脚?
- 为什么两个示例都不需要 NVIC?
- LED 例子中为什么 CCR 恰好等于占空比百分数?
Delay_ms(10)改变的是 PWM 频率还是呼吸速度?- 舵机例子中为什么 CCR=1500 可以直接理解为 1.5 ms?
- 从 CH1 改成 CH2 时,哪四个"通道套件"要同步替换?
- PPT 舵机图和当前源码引脚不同时,应以什么为准?
- 为什么不建议盲目用 ST-Link 5 V 长期给舵机供电?
- 如果想同时保留 1 kHz LED PWM 和 50 Hz 舵机 PWM,为什么不能简单使用 TIM2_CH1 和 TIM2_CH2?
答案要点
- 要把引脚输出驱动的控制权交给定时器通道。
- CH1--PA0,CH2--PA1。
- PWM 由定时器硬件自动生成,CPU 不需逐周期处理。
- 因为
ARR+1=100,所以Duty=CCR/100。 - 呼吸速度;PWM 仍由 TIM2 以 1 kHz 运行。
- PSC 已把 CNT 设成 1 µs/拍,因此 CCR 的计数拍数就等于微秒数。
- GPIO、
TIM_OCxInit、TIM_SetComparex、CCRx。 - 复现当前项目时以经核对的源码、配套接线图和数据手册为准。
- 舵机启动/堵转电流可超出供电链能力,造成跌压、抖动或 MCU 复位。
- 同一 TIM 的通道共享 PSC、ARR、CNT,因而共享频率。
14. 资料与页码索引
| 资料 | 位置 | 用途 |
|---|---|---|
| 课堂录音转写 | 2026-09-01 14_03 记录 TIM输入捕获PWM舵机_原文.docx,00:07--33:13 |
两个示例的讲解顺序、库函数、调试与现象 |
| LED 工程 | STM32Project-有注释版/6-3 PWM驱动LED呼吸灯/Hardware/PWM.c、User/main.c |
TIM2_CH1/PA0、1 kHz PWM、CCR 渐变 |
| 舵机工程 | STM32Project-有注释版/6-4 PWM驱动舵机/Hardware/PWM.c、Servo.c、User/main.c |
TIM2_CH2/PA1、50 Hz PWM、角度封装 |
| 源码接线图 | 1-1 接线图/6-3 PWM驱动LED呼吸灯.jpg |
LED 引脚与极性;需额外注意限流电阻 |
| 源码接线图 | 1-1 接线图/6-4 PWM驱动舵机.jpg |
PA1、PB1、5 V、GND 连接 |
| 老师 PPT | 第 20 页 | GPIO 普通输出与复用输出的内部通路 |
| 老师 PPT | 第 48 页 | AFIO 复用和重映射概念 |
| 老师 PPT | 第 68~69 页 | PWM 基本结构和参数公式 |
| 老师 PPT | 第 70~71 页 | 舵机脉宽模型和概念接线 |
| STM32F103x8B 数据手册 | PDF 第 17、20 页 | PA0/TIM2_CH1、PA1/TIM2_CH2 默认复用关系 |
| STM32F10xxx 参考手册 | PDF 第 118~124 页 | SWJ 引脚、TIM2 重映射和 AFIO_MAPR |
| STM32F10xxx 参考手册 | PDF 第 269~270 页 | 输出比较配置步骤和 PWM 模式 1/2 |
| STM32F10xxx 参考手册 | PDF 第 289、292~294 页 | OCxM、OCxPE、CCxP、CCxE、PSC、ARR、CCRx |
15. 一句话总结
复现 PWM 项目的关键不是背
PWM_Init(),而是从负载需要的频率和脉宽出发,先查清 TIMx_CHy--GPIO 映射,再设计 PSC/ARR/CCR,把 GPIO 交给复用通道,先验证固定波形,最后才把 CCR 封装为"亮度"或"角度"这种上层物理量。