
🔥铅笔小新z:个人主页
🎬博客专栏:stm32学习
💫滴水不绝,可穿石;步履不休,能至渊。

13.1 PWM 呼吸灯效果 - 中断
平台:STM32F103ZET6
外设:TIM3 通道 4(TIM3_CH4),对应引脚 PB1,驱动高亮 LED
效果:PB1 输出 1kHz PWM,占空比在 10% 到 99.9% 之间往返,LED 约 2 秒渐亮、2 秒渐暗
适合:刚学完 GPIO 和基础定时器,想在 CubeMX 里把 PWM 和中断真正跑通的人
这篇笔记只抓一条主线:定时器产生 1kHz PWM,更新中断每 2ms 改一次 CCR,LED 就呼吸起来。
目录
1. 呼吸灯的原理
1.1 视觉暂留:LED 其实在飞快开关
LED 只有"亮"和"灭"两个状态。如果让它以足够高的频率开关,人眼分辨不出每一次闪烁,看到的就是平均亮度。
- 1ms 内亮 0.9ms:平均电流大,看起来亮;
- 1ms 内亮 0.1ms:平均电流小,看起来暗。
这个"亮的时间占整个周期的比例"叫占空比:
text
占空比 D = 高电平时间 / 周期 = CCR / (ARR + 1)
下面把 CCR 递增和递减时的波形画出来。每一行是一个 1ms 周期,每个方块代表一个时间格,黄色是高电平,灰色是低电平;按周期顺序看,高电平格数逐行变化,灯就逐渐变亮或变暗。
CCR 递增:高电平逐行变宽,灯渐亮
#mermaid-svg-XnWKl3GLz9SNtXf8{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-XnWKl3GLz9SNtXf8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XnWKl3GLz9SNtXf8 .error-icon{fill:#552222;}#mermaid-svg-XnWKl3GLz9SNtXf8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XnWKl3GLz9SNtXf8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XnWKl3GLz9SNtXf8 .marker.cross{stroke:#333333;}#mermaid-svg-XnWKl3GLz9SNtXf8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XnWKl3GLz9SNtXf8 p{margin:0;}#mermaid-svg-XnWKl3GLz9SNtXf8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XnWKl3GLz9SNtXf8 .cluster-label text{fill:#333;}#mermaid-svg-XnWKl3GLz9SNtXf8 .cluster-label span{color:#333;}#mermaid-svg-XnWKl3GLz9SNtXf8 .cluster-label span p{background-color:transparent;}#mermaid-svg-XnWKl3GLz9SNtXf8 .label text,#mermaid-svg-XnWKl3GLz9SNtXf8 span{fill:#333;color:#333;}#mermaid-svg-XnWKl3GLz9SNtXf8 .node rect,#mermaid-svg-XnWKl3GLz9SNtXf8 .node circle,#mermaid-svg-XnWKl3GLz9SNtXf8 .node ellipse,#mermaid-svg-XnWKl3GLz9SNtXf8 .node polygon,#mermaid-svg-XnWKl3GLz9SNtXf8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XnWKl3GLz9SNtXf8 .rough-node .label text,#mermaid-svg-XnWKl3GLz9SNtXf8 .node .label text,#mermaid-svg-XnWKl3GLz9SNtXf8 .image-shape .label,#mermaid-svg-XnWKl3GLz9SNtXf8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-XnWKl3GLz9SNtXf8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XnWKl3GLz9SNtXf8 .rough-node .label,#mermaid-svg-XnWKl3GLz9SNtXf8 .node .label,#mermaid-svg-XnWKl3GLz9SNtXf8 .image-shape .label,#mermaid-svg-XnWKl3GLz9SNtXf8 .icon-shape .label{text-align:center;}#mermaid-svg-XnWKl3GLz9SNtXf8 .node.clickable{cursor:pointer;}#mermaid-svg-XnWKl3GLz9SNtXf8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XnWKl3GLz9SNtXf8 .arrowheadPath{fill:#333333;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XnWKl3GLz9SNtXf8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XnWKl3GLz9SNtXf8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XnWKl3GLz9SNtXf8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XnWKl3GLz9SNtXf8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XnWKl3GLz9SNtXf8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XnWKl3GLz9SNtXf8 .cluster text{fill:#333;}#mermaid-svg-XnWKl3GLz9SNtXf8 .cluster span{color:#333;}#mermaid-svg-XnWKl3GLz9SNtXf8 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-XnWKl3GLz9SNtXf8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XnWKl3GLz9SNtXf8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-XnWKl3GLz9SNtXf8 .icon-shape,#mermaid-svg-XnWKl3GLz9SNtXf8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XnWKl3GLz9SNtXf8 .icon-shape p,#mermaid-svg-XnWKl3GLz9SNtXf8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XnWKl3GLz9SNtXf8 .icon-shape .label rect,#mermaid-svg-XnWKl3GLz9SNtXf8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XnWKl3GLz9SNtXf8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XnWKl3GLz9SNtXf8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XnWKl3GLz9SNtXf8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-XnWKl3GLz9SNtXf8 .high>*{fill:#f6d365!important;stroke:#b8860b!important;color:#000!important;font-size:20px!important;}#mermaid-svg-XnWKl3GLz9SNtXf8 .high span{fill:#f6d365!important;stroke:#b8860b!important;color:#000!important;font-size:20px!important;}#mermaid-svg-XnWKl3GLz9SNtXf8 .high tspan{fill:#000!important;}#mermaid-svg-XnWKl3GLz9SNtXf8 .low>*{fill:#e8e8e8!important;stroke:#999!important;color:#333!important;font-size:20px!important;}#mermaid-svg-XnWKl3GLz9SNtXf8 .low span{fill:#e8e8e8!important;stroke:#999!important;color:#333!important;font-size:20px!important;}#mermaid-svg-XnWKl3GLz9SNtXf8 .low tspan{fill:#333!important;} 周期 3 · 高 3 格
周期 2 · 高 2 格
周期 1 · 高 1 格
高
低
低
低
高
高
低
低
高
高
高
低
CCR 递减:高电平逐行变窄,灯渐暗
#mermaid-svg-tb7JMprMIhORsrM5{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-tb7JMprMIhORsrM5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tb7JMprMIhORsrM5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tb7JMprMIhORsrM5 .error-icon{fill:#552222;}#mermaid-svg-tb7JMprMIhORsrM5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tb7JMprMIhORsrM5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tb7JMprMIhORsrM5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tb7JMprMIhORsrM5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tb7JMprMIhORsrM5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tb7JMprMIhORsrM5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tb7JMprMIhORsrM5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tb7JMprMIhORsrM5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tb7JMprMIhORsrM5 .marker.cross{stroke:#333333;}#mermaid-svg-tb7JMprMIhORsrM5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tb7JMprMIhORsrM5 p{margin:0;}#mermaid-svg-tb7JMprMIhORsrM5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tb7JMprMIhORsrM5 .cluster-label text{fill:#333;}#mermaid-svg-tb7JMprMIhORsrM5 .cluster-label span{color:#333;}#mermaid-svg-tb7JMprMIhORsrM5 .cluster-label span p{background-color:transparent;}#mermaid-svg-tb7JMprMIhORsrM5 .label text,#mermaid-svg-tb7JMprMIhORsrM5 span{fill:#333;color:#333;}#mermaid-svg-tb7JMprMIhORsrM5 .node rect,#mermaid-svg-tb7JMprMIhORsrM5 .node circle,#mermaid-svg-tb7JMprMIhORsrM5 .node ellipse,#mermaid-svg-tb7JMprMIhORsrM5 .node polygon,#mermaid-svg-tb7JMprMIhORsrM5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tb7JMprMIhORsrM5 .rough-node .label text,#mermaid-svg-tb7JMprMIhORsrM5 .node .label text,#mermaid-svg-tb7JMprMIhORsrM5 .image-shape .label,#mermaid-svg-tb7JMprMIhORsrM5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-tb7JMprMIhORsrM5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tb7JMprMIhORsrM5 .rough-node .label,#mermaid-svg-tb7JMprMIhORsrM5 .node .label,#mermaid-svg-tb7JMprMIhORsrM5 .image-shape .label,#mermaid-svg-tb7JMprMIhORsrM5 .icon-shape .label{text-align:center;}#mermaid-svg-tb7JMprMIhORsrM5 .node.clickable{cursor:pointer;}#mermaid-svg-tb7JMprMIhORsrM5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tb7JMprMIhORsrM5 .arrowheadPath{fill:#333333;}#mermaid-svg-tb7JMprMIhORsrM5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tb7JMprMIhORsrM5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tb7JMprMIhORsrM5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tb7JMprMIhORsrM5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tb7JMprMIhORsrM5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tb7JMprMIhORsrM5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tb7JMprMIhORsrM5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tb7JMprMIhORsrM5 .cluster text{fill:#333;}#mermaid-svg-tb7JMprMIhORsrM5 .cluster span{color:#333;}#mermaid-svg-tb7JMprMIhORsrM5 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-tb7JMprMIhORsrM5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tb7JMprMIhORsrM5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-tb7JMprMIhORsrM5 .icon-shape,#mermaid-svg-tb7JMprMIhORsrM5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tb7JMprMIhORsrM5 .icon-shape p,#mermaid-svg-tb7JMprMIhORsrM5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tb7JMprMIhORsrM5 .icon-shape .label rect,#mermaid-svg-tb7JMprMIhORsrM5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tb7JMprMIhORsrM5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tb7JMprMIhORsrM5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tb7JMprMIhORsrM5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-tb7JMprMIhORsrM5 .high>*{fill:#f6d365!important;stroke:#b8860b!important;color:#000!important;font-size:20px!important;}#mermaid-svg-tb7JMprMIhORsrM5 .high span{fill:#f6d365!important;stroke:#b8860b!important;color:#000!important;font-size:20px!important;}#mermaid-svg-tb7JMprMIhORsrM5 .high tspan{fill:#000!important;}#mermaid-svg-tb7JMprMIhORsrM5 .low>*{fill:#e8e8e8!important;stroke:#999!important;color:#333!important;font-size:20px!important;}#mermaid-svg-tb7JMprMIhORsrM5 .low span{fill:#e8e8e8!important;stroke:#999!important;color:#333!important;font-size:20px!important;}#mermaid-svg-tb7JMprMIhORsrM5 .low tspan{fill:#333!important;} 周期 3 · 高 1 格
周期 2 · 高 2 格
周期 1 · 高 3 格
高
高
高
低
高
高
低
低
高
低
低
低
频率保持 1kHz 不变,只改变高电平占比,就能得到不同的平均亮度,这就是 PWM 调光。线性改变 CCR 时,人眼可能觉得"亮得快、暗得慢"。
知识小结|PWM 为什么能调亮度
PWM 调光不是改变 LED 的供电电压,而是让 LED 以远高于人眼分辨能力的频率快速开关,通过占空比改变平均电流。频率固定时,占空比越大,平均电流越大,灯越亮;占空比越小,灯越暗。呼吸灯就是让占空比慢慢变大再变小。
1.2 参数计算:72MHz 怎么变成 1kHz
这里使用 72MHz 的定时器时钟,目标是 1kHz、1ms 一个周期:
text
f_CNT = 72MHz / (PSC + 1) = 72MHz / 72 = 1MHz
T = (ARR + 1) / f_CNT = 1000 / 1MHz = 1ms
f_PWM = 1 / T = 1kHz
所以取 PSC = 71、ARR = 999。计数器每 1us 走一格,走 1000 格正好 1ms。
#mermaid-svg-C2dA6DVJPfq8s031{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-C2dA6DVJPfq8s031 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C2dA6DVJPfq8s031 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C2dA6DVJPfq8s031 .error-icon{fill:#552222;}#mermaid-svg-C2dA6DVJPfq8s031 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C2dA6DVJPfq8s031 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C2dA6DVJPfq8s031 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C2dA6DVJPfq8s031 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C2dA6DVJPfq8s031 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C2dA6DVJPfq8s031 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C2dA6DVJPfq8s031 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C2dA6DVJPfq8s031 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C2dA6DVJPfq8s031 .marker.cross{stroke:#333333;}#mermaid-svg-C2dA6DVJPfq8s031 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C2dA6DVJPfq8s031 p{margin:0;}#mermaid-svg-C2dA6DVJPfq8s031 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C2dA6DVJPfq8s031 .cluster-label text{fill:#333;}#mermaid-svg-C2dA6DVJPfq8s031 .cluster-label span{color:#333;}#mermaid-svg-C2dA6DVJPfq8s031 .cluster-label span p{background-color:transparent;}#mermaid-svg-C2dA6DVJPfq8s031 .label text,#mermaid-svg-C2dA6DVJPfq8s031 span{fill:#333;color:#333;}#mermaid-svg-C2dA6DVJPfq8s031 .node rect,#mermaid-svg-C2dA6DVJPfq8s031 .node circle,#mermaid-svg-C2dA6DVJPfq8s031 .node ellipse,#mermaid-svg-C2dA6DVJPfq8s031 .node polygon,#mermaid-svg-C2dA6DVJPfq8s031 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C2dA6DVJPfq8s031 .rough-node .label text,#mermaid-svg-C2dA6DVJPfq8s031 .node .label text,#mermaid-svg-C2dA6DVJPfq8s031 .image-shape .label,#mermaid-svg-C2dA6DVJPfq8s031 .icon-shape .label{text-anchor:middle;}#mermaid-svg-C2dA6DVJPfq8s031 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C2dA6DVJPfq8s031 .rough-node .label,#mermaid-svg-C2dA6DVJPfq8s031 .node .label,#mermaid-svg-C2dA6DVJPfq8s031 .image-shape .label,#mermaid-svg-C2dA6DVJPfq8s031 .icon-shape .label{text-align:center;}#mermaid-svg-C2dA6DVJPfq8s031 .node.clickable{cursor:pointer;}#mermaid-svg-C2dA6DVJPfq8s031 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C2dA6DVJPfq8s031 .arrowheadPath{fill:#333333;}#mermaid-svg-C2dA6DVJPfq8s031 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C2dA6DVJPfq8s031 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C2dA6DVJPfq8s031 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C2dA6DVJPfq8s031 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C2dA6DVJPfq8s031 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C2dA6DVJPfq8s031 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C2dA6DVJPfq8s031 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C2dA6DVJPfq8s031 .cluster text{fill:#333;}#mermaid-svg-C2dA6DVJPfq8s031 .cluster span{color:#333;}#mermaid-svg-C2dA6DVJPfq8s031 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-C2dA6DVJPfq8s031 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C2dA6DVJPfq8s031 rect.text{fill:none;stroke-width:0;}#mermaid-svg-C2dA6DVJPfq8s031 .icon-shape,#mermaid-svg-C2dA6DVJPfq8s031 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C2dA6DVJPfq8s031 .icon-shape p,#mermaid-svg-C2dA6DVJPfq8s031 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C2dA6DVJPfq8s031 .icon-shape .label rect,#mermaid-svg-C2dA6DVJPfq8s031 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C2dA6DVJPfq8s031 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C2dA6DVJPfq8s031 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C2dA6DVJPfq8s031 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PSC = 71
ARR = 999
CCR
72MHz
定时器时钟
1MHz
每格 1us
1kHz
周期 1ms
占空比
D = CCR / 1000
LED 平均亮度
CCR 和占空比的对应关系:
| CCR | 高电平时间 | 占空比 | 现象 |
|---|---|---|---|
| 0 | 0us | 0% | 全灭 |
| 100 | 100us | 10% | 很暗 |
| 500 | 500us | 50% | 半亮 |
| 999 | 999us | 99.9% | 接近最亮 |
| 1000 | 1000us | 100% | 全亮 |
频率越高,周期越短,同样定时器时钟下能分的亮度档位越少;1kHz 配 1000 档,是"不闪"和"够细"的折中。
知识小结|PSC、ARR、CCR 的分工
PSC 是预分频,决定计数时钟多快;ARR 是自动重装值,决定一个周期有多少个计数格;CCR 是比较值,决定一个周期里高电平占多少格。三者满足 f_PWM = f_TIM / ((PSC+1)(ARR+1)) 和 D = CCR/(ARR+1)。本实验取 PSC=71、ARR=999,得到 1kHz、1ms 的 PWM。
1.3 呼吸节奏:为什么每 2ms 改一次 CCR
1ms 溢出一次,如果每次溢出都把 CCR 加一,从 0 走到 999 需要 1000 × 1ms = 1s,也就是 1 秒从暗到亮。
目标是 2 秒渐亮,所以让软件每两次溢出才更新一次 CCR,相当于 2ms 走一档:
CCR4 软件计数 cnt 更新事件 TIM3 计数器 CCR4 软件计数 cnt 更新事件 TIM3 计数器 #mermaid-svg-doi9Yi00ay5lUjr3{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-doi9Yi00ay5lUjr3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-doi9Yi00ay5lUjr3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-doi9Yi00ay5lUjr3 .error-icon{fill:#552222;}#mermaid-svg-doi9Yi00ay5lUjr3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-doi9Yi00ay5lUjr3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-doi9Yi00ay5lUjr3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-doi9Yi00ay5lUjr3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-doi9Yi00ay5lUjr3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-doi9Yi00ay5lUjr3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-doi9Yi00ay5lUjr3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-doi9Yi00ay5lUjr3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-doi9Yi00ay5lUjr3 .marker.cross{stroke:#333333;}#mermaid-svg-doi9Yi00ay5lUjr3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-doi9Yi00ay5lUjr3 p{margin:0;}#mermaid-svg-doi9Yi00ay5lUjr3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-doi9Yi00ay5lUjr3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-doi9Yi00ay5lUjr3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-doi9Yi00ay5lUjr3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-doi9Yi00ay5lUjr3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-doi9Yi00ay5lUjr3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-doi9Yi00ay5lUjr3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-doi9Yi00ay5lUjr3 .sequenceNumber{fill:white;}#mermaid-svg-doi9Yi00ay5lUjr3 #sequencenumber{fill:#333;}#mermaid-svg-doi9Yi00ay5lUjr3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-doi9Yi00ay5lUjr3 .messageText{fill:#333;stroke:none;}#mermaid-svg-doi9Yi00ay5lUjr3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-doi9Yi00ay5lUjr3 .labelText,#mermaid-svg-doi9Yi00ay5lUjr3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-doi9Yi00ay5lUjr3 .loopText,#mermaid-svg-doi9Yi00ay5lUjr3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-doi9Yi00ay5lUjr3 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-doi9Yi00ay5lUjr3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-doi9Yi00ay5lUjr3 .noteText,#mermaid-svg-doi9Yi00ay5lUjr3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-doi9Yi00ay5lUjr3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-doi9Yi00ay5lUjr3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-doi9Yi00ay5lUjr3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-doi9Yi00ay5lUjr3 .actorPopupMenu{position:absolute;}#mermaid-svg-doi9Yi00ay5lUjr3 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-doi9Yi00ay5lUjr3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-doi9Yi00ay5lUjr3 .actor-man circle,#mermaid-svg-doi9Yi00ay5lUjr3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-doi9Yi00ay5lUjr3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 周期 1(1ms) 保持当前占空比 周期 2(1ms) 下一个更新事件生效 溢出 1 cnt = 1 溢出 2 cnt = 2 写入新的 CCR
每两个周期为一组,组内占空比保持不变,下一组才变宽:
#mermaid-svg-we5A6Rgs8SHR5vqE{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-we5A6Rgs8SHR5vqE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-we5A6Rgs8SHR5vqE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-we5A6Rgs8SHR5vqE .error-icon{fill:#552222;}#mermaid-svg-we5A6Rgs8SHR5vqE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-we5A6Rgs8SHR5vqE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-we5A6Rgs8SHR5vqE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-we5A6Rgs8SHR5vqE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-we5A6Rgs8SHR5vqE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-we5A6Rgs8SHR5vqE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-we5A6Rgs8SHR5vqE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-we5A6Rgs8SHR5vqE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-we5A6Rgs8SHR5vqE .marker.cross{stroke:#333333;}#mermaid-svg-we5A6Rgs8SHR5vqE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-we5A6Rgs8SHR5vqE p{margin:0;}#mermaid-svg-we5A6Rgs8SHR5vqE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-we5A6Rgs8SHR5vqE .cluster-label text{fill:#333;}#mermaid-svg-we5A6Rgs8SHR5vqE .cluster-label span{color:#333;}#mermaid-svg-we5A6Rgs8SHR5vqE .cluster-label span p{background-color:transparent;}#mermaid-svg-we5A6Rgs8SHR5vqE .label text,#mermaid-svg-we5A6Rgs8SHR5vqE span{fill:#333;color:#333;}#mermaid-svg-we5A6Rgs8SHR5vqE .node rect,#mermaid-svg-we5A6Rgs8SHR5vqE .node circle,#mermaid-svg-we5A6Rgs8SHR5vqE .node ellipse,#mermaid-svg-we5A6Rgs8SHR5vqE .node polygon,#mermaid-svg-we5A6Rgs8SHR5vqE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-we5A6Rgs8SHR5vqE .rough-node .label text,#mermaid-svg-we5A6Rgs8SHR5vqE .node .label text,#mermaid-svg-we5A6Rgs8SHR5vqE .image-shape .label,#mermaid-svg-we5A6Rgs8SHR5vqE .icon-shape .label{text-anchor:middle;}#mermaid-svg-we5A6Rgs8SHR5vqE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-we5A6Rgs8SHR5vqE .rough-node .label,#mermaid-svg-we5A6Rgs8SHR5vqE .node .label,#mermaid-svg-we5A6Rgs8SHR5vqE .image-shape .label,#mermaid-svg-we5A6Rgs8SHR5vqE .icon-shape .label{text-align:center;}#mermaid-svg-we5A6Rgs8SHR5vqE .node.clickable{cursor:pointer;}#mermaid-svg-we5A6Rgs8SHR5vqE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-we5A6Rgs8SHR5vqE .arrowheadPath{fill:#333333;}#mermaid-svg-we5A6Rgs8SHR5vqE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-we5A6Rgs8SHR5vqE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-we5A6Rgs8SHR5vqE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-we5A6Rgs8SHR5vqE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-we5A6Rgs8SHR5vqE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-we5A6Rgs8SHR5vqE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-we5A6Rgs8SHR5vqE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-we5A6Rgs8SHR5vqE .cluster text{fill:#333;}#mermaid-svg-we5A6Rgs8SHR5vqE .cluster span{color:#333;}#mermaid-svg-we5A6Rgs8SHR5vqE 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-we5A6Rgs8SHR5vqE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-we5A6Rgs8SHR5vqE rect.text{fill:none;stroke-width:0;}#mermaid-svg-we5A6Rgs8SHR5vqE .icon-shape,#mermaid-svg-we5A6Rgs8SHR5vqE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-we5A6Rgs8SHR5vqE .icon-shape p,#mermaid-svg-we5A6Rgs8SHR5vqE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-we5A6Rgs8SHR5vqE .icon-shape .label rect,#mermaid-svg-we5A6Rgs8SHR5vqE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-we5A6Rgs8SHR5vqE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-we5A6Rgs8SHR5vqE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-we5A6Rgs8SHR5vqE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-we5A6Rgs8SHR5vqE .high>*{fill:#f6d365!important;stroke:#b8860b!important;color:#000!important;}#mermaid-svg-we5A6Rgs8SHR5vqE .high span{fill:#f6d365!important;stroke:#b8860b!important;color:#000!important;}#mermaid-svg-we5A6Rgs8SHR5vqE .high tspan{fill:#000!important;}#mermaid-svg-we5A6Rgs8SHR5vqE .low>*{fill:#e8e8e8!important;stroke:#999!important;color:#333!important;}#mermaid-svg-we5A6Rgs8SHR5vqE .low span{fill:#e8e8e8!important;stroke:#999!important;color:#333!important;}#mermaid-svg-we5A6Rgs8SHR5vqE .low tspan{fill:#333!important;} 第 2 组:CCR 增大
周期 2
高
高
低
低
高
高
低
低
第 1 组:CCR 较小
周期 2
高
低
低
低
高
低
低
低
代码里就是一个软件计数器:
c
cnt++;
if (cnt >= 2)
{
cnt = 0;
// 每 2ms 才改一次 CCR
}
溢出中断方案把下限设为 100,CCR 在 100 到 999 之间变化,共 900 档,2ms 一档,单程约 1.8 秒;说"2 秒"是工程上的近似。比较中断方案把下限设为 10,单程约 1.98 秒,更接近 2 秒。
知识小结|呼吸时间怎么算
单程时间 = 亮度变化的步数 × 每步时间。PWM 每 1ms 溢出一次,如果每次溢出更新一档,1000 档就是 1 秒;每两次溢出更新一次,就是 2ms 一档,1000 档约 2 秒。实际步数等于"上限减下限加一",保留下限会让灯在最低点留一点底光。
1.4 为什么改 CCR,不改 ARR
- ARR 决定周期和频率:改 ARR,PWM 频率会变,呼吸节奏会忽快忽慢;
- CCR 决定占空比:在频率不变的前提下改 CCR,才是我们想要的"只变亮度"。
PSC 和 ARR 是定时器级参数,同一个定时器的所有通道共用;CCR 是通道级参数,改 CH4 不影响其他通道。所以硬件配置配好后就固定,呼吸快慢交给软件调整 CCR 的步进和间隔。
知识小结|ARR 和 CCR 的区别
ARR 决定 PWM 的周期和频率,CCR 决定某个通道的占空比。呼吸灯要在固定频率下改变亮度,所以保持 PSC、ARR 不变,只动态修改 CCR。改 ARR 会让频率变化,也会影响同一定时器的其他通道。
1.5 硬件结构:PB1 怎么点亮 LED
板子上的高亮 LED 接在 PB1,而 PB1 正好是 TIM3_CH4。信号和电流的关系如下:
#mermaid-svg-GJ1BSXEdxnD6SKgb{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-GJ1BSXEdxnD6SKgb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GJ1BSXEdxnD6SKgb .error-icon{fill:#552222;}#mermaid-svg-GJ1BSXEdxnD6SKgb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GJ1BSXEdxnD6SKgb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GJ1BSXEdxnD6SKgb .marker.cross{stroke:#333333;}#mermaid-svg-GJ1BSXEdxnD6SKgb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GJ1BSXEdxnD6SKgb p{margin:0;}#mermaid-svg-GJ1BSXEdxnD6SKgb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GJ1BSXEdxnD6SKgb .cluster-label text{fill:#333;}#mermaid-svg-GJ1BSXEdxnD6SKgb .cluster-label span{color:#333;}#mermaid-svg-GJ1BSXEdxnD6SKgb .cluster-label span p{background-color:transparent;}#mermaid-svg-GJ1BSXEdxnD6SKgb .label text,#mermaid-svg-GJ1BSXEdxnD6SKgb span{fill:#333;color:#333;}#mermaid-svg-GJ1BSXEdxnD6SKgb .node rect,#mermaid-svg-GJ1BSXEdxnD6SKgb .node circle,#mermaid-svg-GJ1BSXEdxnD6SKgb .node ellipse,#mermaid-svg-GJ1BSXEdxnD6SKgb .node polygon,#mermaid-svg-GJ1BSXEdxnD6SKgb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GJ1BSXEdxnD6SKgb .rough-node .label text,#mermaid-svg-GJ1BSXEdxnD6SKgb .node .label text,#mermaid-svg-GJ1BSXEdxnD6SKgb .image-shape .label,#mermaid-svg-GJ1BSXEdxnD6SKgb .icon-shape .label{text-anchor:middle;}#mermaid-svg-GJ1BSXEdxnD6SKgb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GJ1BSXEdxnD6SKgb .rough-node .label,#mermaid-svg-GJ1BSXEdxnD6SKgb .node .label,#mermaid-svg-GJ1BSXEdxnD6SKgb .image-shape .label,#mermaid-svg-GJ1BSXEdxnD6SKgb .icon-shape .label{text-align:center;}#mermaid-svg-GJ1BSXEdxnD6SKgb .node.clickable{cursor:pointer;}#mermaid-svg-GJ1BSXEdxnD6SKgb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GJ1BSXEdxnD6SKgb .arrowheadPath{fill:#333333;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GJ1BSXEdxnD6SKgb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GJ1BSXEdxnD6SKgb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GJ1BSXEdxnD6SKgb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GJ1BSXEdxnD6SKgb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GJ1BSXEdxnD6SKgb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GJ1BSXEdxnD6SKgb .cluster text{fill:#333;}#mermaid-svg-GJ1BSXEdxnD6SKgb .cluster span{color:#333;}#mermaid-svg-GJ1BSXEdxnD6SKgb 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-GJ1BSXEdxnD6SKgb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GJ1BSXEdxnD6SKgb rect.text{fill:none;stroke-width:0;}#mermaid-svg-GJ1BSXEdxnD6SKgb .icon-shape,#mermaid-svg-GJ1BSXEdxnD6SKgb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GJ1BSXEdxnD6SKgb .icon-shape p,#mermaid-svg-GJ1BSXEdxnD6SKgb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GJ1BSXEdxnD6SKgb .icon-shape .label rect,#mermaid-svg-GJ1BSXEdxnD6SKgb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GJ1BSXEdxnD6SKgb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GJ1BSXEdxnD6SKgb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GJ1BSXEdxnD6SKgb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 基极
集电极
发射极
PB1
TIM3_CH4
H1 7-8
跳线帽
LED_CTRL
R50 1kΩ
Q3 SS8050
NPN 三极管
VDD 3.3V
R48 470Ω
LED6
高亮 LED
GND
R53 10kΩ
基极下拉
几个关键点:
- PB1 输出高电平,Q3 基极有电流,三极管导通,LED 亮;输出低电平,Q3 截止,LED 灭。所以 PWM 通道极性选 High。
- R50 是基极限流电阻,R53 是基极下拉电阻,保证默认关断;R48 限制 LED 电流。
- H1 的 7、8 号默认是断开的,必须插上跳线帽,MCU 才能驱动高亮 LED。这是"代码没问题但灯不亮"的头号原因。
- 普通 LED 的亮度变化不明显,所以这里用高亮 LED 做呼吸效果。
知识小结|硬件链路
PB1 复用为 TIM3_CH4,PWM 信号经 H1 的 7、8 号跳线送到 LED_CTRL,再经 R50 驱动 SS8050 的基极。三极管作为低边开关:PB1 高电平导通,LED 亮;低电平截止,LED 灭。R48 限制 LED 电流,R53 保证默认关断。实验前必须插好 H1 的 7、8 号跳线。
2. CubeMX 配置
2.1 调试口、时钟和 TIM3 时钟
SYS→Debug选Serial Wire,保留 SWD 调试。RCC→High Speed Clock (HSE)选Crystal/Ceramic Resonator,使用外部晶振,时钟更稳。Clock Configuration里把 HCLK 设到 72MHz。



本实验的时钟关系:
| 项目 | 值 | 说明 |
|---|---|---|
| HSE | 8MHz | 外部晶振 |
| SYSCLK / HCLK | 72MHz | PLL 倍频 |
| APB1 外设时钟 | 36MHz | APB1 二分频 |
| APB1 定时器时钟 | 72MHz | 预分频不为 1 时,定时器时钟 = 2 × PCLK |
| APB2 | 72MHz | 本实验不重点使用 |
TIM3 挂在 APB1 上,但它的定时器时钟是 72MHz,不是 36MHz。算 PSC 时要用 72MHz,否则实际频率会翻倍。
2.2 打开 TIM3 通道 4
在 Timers → TIM3 里,把 Channel4 设为 PWM Generation CH4。
CubeMX 会自动把 PB1 配成 TIM3_CH4,GPIO 模式是 Alternate Function Push Pull。其他通道保持 Disable,Combined Channels 也保持 Disable。
TIM3 是通用定时器,才有 PWM 输出;TIM6、TIM7 是基本定时器,没有这些通道,打开看不到是正常的。

2.3 PWM 参数
| 参数 | 值 | 作用 |
|---|---|---|
| Prescaler (PSC) | 71 | 72MHz → 1MHz |
| Counter Mode | Up | 向上计数 0 到 ARR |
| Counter Period (ARR) | 999 | 1000 格 = 1ms |
| PWM Mode | PWM mode 1 | CNT < CCR 时输出有效电平 |
| Pulse | 0 | 初始 CCR,上电先全灭 |
| auto-reload preload | Enable | ARR 预装载,避免改周期时出毛刺 |
| Output compare preload | Enable | CCR 预装载,避免半周期改占空比 |
| Fast Mode | Disable | 本实验不用 |
| CH Polarity | High | 高电平为有效电平,三极管点亮 |
Pulse 就是 CCR 的初始值。设 0 表示上电先灭,代码里再写成 100。Output compare preload 开启后,改 CCR 的新值要等下一个更新事件才生效,最多晚一个 PWM 周期,但波形不会出现缺口。
2.4 NVIC
在 TIM3 的 NVIC Settings 里勾选 TIM3 global interrupt。没有这一步,即使代码里调用了 HAL_TIM_Base_Start_IT,CPU 也不会进中断。
优先级这里设为抢占 0、子优先级 0。这个实验只有 TIM3 一个中断,设 0 没问题;实际项目里不要把所有中断都设成 0。

知识小结|CubeMX 配置清单
SYS 开 Serial Wire;RCC 开 HSE;时钟树设 HCLK 72MHz,确认 APB1 定时器时钟为 72MHz;TIM3 CH4 设为 PWM Generation CH4,PSC=71、ARR=999、PWM mode 1、Pulse=0,两个预装载都 Enable,极性 High;NVIC 勾选 TIM3 global interrupt。生成代码后,用户代码只写在 USER CODE 区域。
3. 代码实现
3.1 方案一:主循环里改 CCR(只适合快速验证)
最短的写法是在 while(1) 里不断改 CCR,再用 HAL_Delay 控制节奏。
c
/* USER CODE BEGIN 2 */
// 启动 TIM3_CH4 的 PWM 输出,此时 CCR=0,灯先灭
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
uint16_t pwm_value = 100; // 当前 CCR,100/1000 = 10% 亮度
uint8_t dir = 1; // 1:渐亮;0:渐暗
/* USER CODE END 2 */
while (1)
{
// 把当前亮度写进 CCR4
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, pwm_value);
if (dir) // 渐亮
{
pwm_value++;
if (pwm_value >= 999)
{
dir = 0; // 到顶,改为渐暗
}
}
else // 渐暗
{
pwm_value--;
if (pwm_value <= 100)
{
dir = 1; // 到底,改为渐亮
}
}
// 阻塞 1ms;这 1ms 里 CPU 只能在这里等
HAL_Delay(1);
}
如果 Keil 提示"变量声明必须放在语句之前",把 pwm_value 和 dir 移到 main 函数开头即可,这是 C89 和 C99 的差异。
这段代码能亮、能呼吸,适合第一次确认硬件通路。但 HAL_Delay 会占住 CPU,循环本身还有开销,实际周期会略大于 1ms;代码一多,呼吸节奏就会变。所以这里把它定位为"可以快速验证,但不推荐正式使用"。
知识小结|主循环调光
启动 PWM 后,在主循环里用 __HAL_TIM_SET_COMPARE 改 CCR,再用 HAL_Delay 控制修改间隔,就能做出呼吸灯。优点是直观、代码短;缺点是 CPU 被延时占住、节奏不精确,加入其他任务后呼吸会卡顿。它适合验证硬件,不适合作为正式方案。
3.2 方案二:更新中断里改 CCR(主方案)
启动 PWM 和更新中断
c
/* USER CODE BEGIN 2 */
// 启动 PWM 输出:计数器开始跑,PB1 输出方波,但不会产生中断
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
// 启动更新中断:每 1ms 进入一次 HAL_TIM_PeriodElapsedCallback
HAL_TIM_Base_Start_IT(&htim3);
/* USER CODE END 2 */
两个函数的分工:
HAL_TIM_PWM_Start:打开计数器(CEN)和通道 4 输出(CC4E),不打开中断;HAL_TIM_Base_Start_IT:打开计数器(CEN)和更新中断(UIE),不负责 PWM 输出。
CEN 被设置了两次,但重复置 1 没有危害。代码注释里也提醒了这一点:这里主要是借 HAL_TIM_Base_Start_IT 打开更新中断。
两个函数都返回 HAL_StatusTypeDef,常见值是 HAL_OK、HAL_ERROR、HAL_BUSY;正式代码建议判断返回值。
中断是怎么走到回调的
#mermaid-svg-OiHYELeyexWAFag0{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-OiHYELeyexWAFag0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OiHYELeyexWAFag0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OiHYELeyexWAFag0 .error-icon{fill:#552222;}#mermaid-svg-OiHYELeyexWAFag0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OiHYELeyexWAFag0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OiHYELeyexWAFag0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OiHYELeyexWAFag0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OiHYELeyexWAFag0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OiHYELeyexWAFag0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OiHYELeyexWAFag0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OiHYELeyexWAFag0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OiHYELeyexWAFag0 .marker.cross{stroke:#333333;}#mermaid-svg-OiHYELeyexWAFag0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OiHYELeyexWAFag0 p{margin:0;}#mermaid-svg-OiHYELeyexWAFag0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OiHYELeyexWAFag0 .cluster-label text{fill:#333;}#mermaid-svg-OiHYELeyexWAFag0 .cluster-label span{color:#333;}#mermaid-svg-OiHYELeyexWAFag0 .cluster-label span p{background-color:transparent;}#mermaid-svg-OiHYELeyexWAFag0 .label text,#mermaid-svg-OiHYELeyexWAFag0 span{fill:#333;color:#333;}#mermaid-svg-OiHYELeyexWAFag0 .node rect,#mermaid-svg-OiHYELeyexWAFag0 .node circle,#mermaid-svg-OiHYELeyexWAFag0 .node ellipse,#mermaid-svg-OiHYELeyexWAFag0 .node polygon,#mermaid-svg-OiHYELeyexWAFag0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OiHYELeyexWAFag0 .rough-node .label text,#mermaid-svg-OiHYELeyexWAFag0 .node .label text,#mermaid-svg-OiHYELeyexWAFag0 .image-shape .label,#mermaid-svg-OiHYELeyexWAFag0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-OiHYELeyexWAFag0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OiHYELeyexWAFag0 .rough-node .label,#mermaid-svg-OiHYELeyexWAFag0 .node .label,#mermaid-svg-OiHYELeyexWAFag0 .image-shape .label,#mermaid-svg-OiHYELeyexWAFag0 .icon-shape .label{text-align:center;}#mermaid-svg-OiHYELeyexWAFag0 .node.clickable{cursor:pointer;}#mermaid-svg-OiHYELeyexWAFag0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OiHYELeyexWAFag0 .arrowheadPath{fill:#333333;}#mermaid-svg-OiHYELeyexWAFag0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OiHYELeyexWAFag0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OiHYELeyexWAFag0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OiHYELeyexWAFag0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OiHYELeyexWAFag0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OiHYELeyexWAFag0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OiHYELeyexWAFag0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OiHYELeyexWAFag0 .cluster text{fill:#333;}#mermaid-svg-OiHYELeyexWAFag0 .cluster span{color:#333;}#mermaid-svg-OiHYELeyexWAFag0 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-OiHYELeyexWAFag0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OiHYELeyexWAFag0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-OiHYELeyexWAFag0 .icon-shape,#mermaid-svg-OiHYELeyexWAFag0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OiHYELeyexWAFag0 .icon-shape p,#mermaid-svg-OiHYELeyexWAFag0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OiHYELeyexWAFag0 .icon-shape .label rect,#mermaid-svg-OiHYELeyexWAFag0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OiHYELeyexWAFag0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OiHYELeyexWAFag0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OiHYELeyexWAFag0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TIM3 更新事件
UIF = 1
NVIC
TIM3 global interrupt
TIM3_IRQHandler()
stm32f1xx_it.c
HAL_TIM_IRQHandler(htim3)
stm32f1xx_hal_tim.c
HAL_TIM_PeriodElapsedCallback(htim3)
你的 main.c
判断 htim 的 Instance 是否为 TIM3
每 2 次更新一次 CCR4
CubeMX 在 NVIC 里打开 TIM3 中断后,会在 stm32f1xx_it.c 生成 TIM3_IRQHandler。它调用 HAL 的通用处理函数,HAL 判断是更新事件、清除标志,最后调用你写的 HAL_TIM_PeriodElapsedCallback。
这个回调是所有定时器共用的,所以第一件事要判断 htim->Instance == TIM3,否则 TIM2、TIM4 等定时器溢出也会跑进来。
回调代码
c
/* USER CODE BEGIN PV */
/*
* 这三个变量会被中断修改,加 volatile 防止编译器优化成旧值。
*/
volatile uint16_t pwm_value = 100; // 当前 CCR,100 表示 10% 亮度
volatile uint8_t breath_dir = 1; // 1:渐亮;0:渐暗
volatile uint8_t update_cnt = 0; // 软件分频:每 2 次中断更新一次
/* USER CODE END PV */
/* USER CODE BEGIN 4 */
/**
* @brief 定时器更新中断回调,每 1ms 进入一次
* @param htim 触发回调的定时器句柄
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
// 只处理 TIM3
if (htim->Instance != TIM3)
{
return;
}
// 1ms 一次太快,每 2 次才改一次亮度,相当于 2ms 一档
update_cnt++;
if (update_cnt < 2)
{
return;
}
update_cnt = 0;
// 把当前亮度写进 CCR4。
// 预装载开启时,新值在下一个更新事件生效,最多晚 1ms。
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_4, pwm_value);
if (breath_dir) // 渐亮
{
pwm_value++;
if (pwm_value >= 999)
{
pwm_value = 999; // 夹住上限
breath_dir = 0; // 掉头渐暗
}
}
else // 渐暗
{
pwm_value--;
if (pwm_value <= 100)
{
pwm_value = 100; // 夹住下限,保留 10% 底光
breath_dir = 1; // 掉头渐亮
}
}
}
/* USER CODE END 4 */
呼吸方向就是一个很简单的状态机:
#mermaid-svg-fCGAjgNNa2gQgJsH{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-fCGAjgNNa2gQgJsH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fCGAjgNNa2gQgJsH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fCGAjgNNa2gQgJsH .error-icon{fill:#552222;}#mermaid-svg-fCGAjgNNa2gQgJsH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fCGAjgNNa2gQgJsH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fCGAjgNNa2gQgJsH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fCGAjgNNa2gQgJsH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fCGAjgNNa2gQgJsH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fCGAjgNNa2gQgJsH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fCGAjgNNa2gQgJsH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fCGAjgNNa2gQgJsH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fCGAjgNNa2gQgJsH .marker.cross{stroke:#333333;}#mermaid-svg-fCGAjgNNa2gQgJsH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fCGAjgNNa2gQgJsH p{margin:0;}#mermaid-svg-fCGAjgNNa2gQgJsH defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-fCGAjgNNa2gQgJsH g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-fCGAjgNNa2gQgJsH g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-fCGAjgNNa2gQgJsH g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-fCGAjgNNa2gQgJsH g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-fCGAjgNNa2gQgJsH g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-fCGAjgNNa2gQgJsH .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-fCGAjgNNa2gQgJsH .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-fCGAjgNNa2gQgJsH .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-fCGAjgNNa2gQgJsH .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fCGAjgNNa2gQgJsH .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-fCGAjgNNa2gQgJsH .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-fCGAjgNNa2gQgJsH .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-fCGAjgNNa2gQgJsH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fCGAjgNNa2gQgJsH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fCGAjgNNa2gQgJsH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fCGAjgNNa2gQgJsH .edgeLabel .label text{fill:#333;}#mermaid-svg-fCGAjgNNa2gQgJsH .label div .edgeLabel{color:#333;}#mermaid-svg-fCGAjgNNa2gQgJsH .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-fCGAjgNNa2gQgJsH .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-fCGAjgNNa2gQgJsH .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-fCGAjgNNa2gQgJsH .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-fCGAjgNNa2gQgJsH .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-fCGAjgNNa2gQgJsH .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fCGAjgNNa2gQgJsH .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fCGAjgNNa2gQgJsH #statediagram-barbEnd{fill:#333333;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fCGAjgNNa2gQgJsH .cluster-label,#mermaid-svg-fCGAjgNNa2gQgJsH .nodeLabel{color:#131300;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-fCGAjgNNa2gQgJsH .note-edge{stroke-dasharray:5;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-note text{fill:black;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram-note .nodeLabel{color:black;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagram .edgeLabel{color:red;}#mermaid-svg-fCGAjgNNa2gQgJsH #dependencyStart,#mermaid-svg-fCGAjgNNa2gQgJsH #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-fCGAjgNNa2gQgJsH .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fCGAjgNNa2gQgJsH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每 2ms,CCR 加 1
CCR 达到 999
每 2ms,CCR 减 1
CCR 降到 100
渐亮(CCR++)
渐暗(CCR--)
这里有三个容易忽略的小点:
- 先写 CCR,再算下一次亮度。 这样第一次更新写入的是初始值 100,序列是 100、101、102......;
- 到边界要夹住。 999 和 100 都是有效范围,不要让无符号数继续加或减;
- 预装载会晚一拍。 你在更新中断里写 CCR,新值在下一个更新事件才真正生效,但每 2ms 的节奏不变,肉眼看不出来。
volatile 只保证编译器每次都重新读变量,不保证原子性;这个实验里 8 位、16 位对齐变量在 Cortex-M3 上读写是原子的,所以直接用即可。更复杂的共享数据才需要临界区。
知识小结|更新中断方案
HAL_TIM_PWM_Start 负责启动 PWM 波形,HAL_TIM_Base_Start_IT 负责打开 1ms 更新中断。更新事件经 NVIC 进入 TIM3_IRQHandler,再由 HAL_TIM_IRQHandler 分发到 HAL_TIM_PeriodElapsedCallback。回调里先判断 htim->Instance == TIM3,再用 update_cnt 每两次中断执行一次:写 CCR4、根据 breath_dir 加减、在 999 和 100 处夹住并翻转方向。CPU 其余时间可以做别的事,节奏由硬件保证。
3.3 方案三:比较匹配中断(了解即可)
比较匹配中断不看"周期结束",而是在 CNT == CCR 时触发。启动函数换成:
c
// 同时启动 PWM 输出和通道 4 的比较匹配中断
HAL_TIM_PWM_Start_IT(&htim3, TIM_CHANNEL_4);
它设置 CEN、CC4E 和 CC4IE,但不打开更新中断 。因此进入的不是 HAL_TIM_PeriodElapsedCallback,而是:
c
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim);
HAL_TIM_PWM_Start_IT 的参数是定时器句柄和通道号,返回 HAL_OK、HAL_ERROR、HAL_BUSY 或 HAL_TIMEOUT。如果通道配置成 Output Compare 模式,回调会变成 HAL_TIM_OC_DelayElapsedCallback;本实验是 PWM 模式,所以用 PulseFinished。
两种中断的区别可以这样看:
#mermaid-svg-oKbnTLgONy7t4CuM{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-oKbnTLgONy7t4CuM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oKbnTLgONy7t4CuM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oKbnTLgONy7t4CuM .error-icon{fill:#552222;}#mermaid-svg-oKbnTLgONy7t4CuM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oKbnTLgONy7t4CuM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oKbnTLgONy7t4CuM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oKbnTLgONy7t4CuM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oKbnTLgONy7t4CuM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oKbnTLgONy7t4CuM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oKbnTLgONy7t4CuM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oKbnTLgONy7t4CuM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oKbnTLgONy7t4CuM .marker.cross{stroke:#333333;}#mermaid-svg-oKbnTLgONy7t4CuM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oKbnTLgONy7t4CuM p{margin:0;}#mermaid-svg-oKbnTLgONy7t4CuM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oKbnTLgONy7t4CuM .cluster-label text{fill:#333;}#mermaid-svg-oKbnTLgONy7t4CuM .cluster-label span{color:#333;}#mermaid-svg-oKbnTLgONy7t4CuM .cluster-label span p{background-color:transparent;}#mermaid-svg-oKbnTLgONy7t4CuM .label text,#mermaid-svg-oKbnTLgONy7t4CuM span{fill:#333;color:#333;}#mermaid-svg-oKbnTLgONy7t4CuM .node rect,#mermaid-svg-oKbnTLgONy7t4CuM .node circle,#mermaid-svg-oKbnTLgONy7t4CuM .node ellipse,#mermaid-svg-oKbnTLgONy7t4CuM .node polygon,#mermaid-svg-oKbnTLgONy7t4CuM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oKbnTLgONy7t4CuM .rough-node .label text,#mermaid-svg-oKbnTLgONy7t4CuM .node .label text,#mermaid-svg-oKbnTLgONy7t4CuM .image-shape .label,#mermaid-svg-oKbnTLgONy7t4CuM .icon-shape .label{text-anchor:middle;}#mermaid-svg-oKbnTLgONy7t4CuM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oKbnTLgONy7t4CuM .rough-node .label,#mermaid-svg-oKbnTLgONy7t4CuM .node .label,#mermaid-svg-oKbnTLgONy7t4CuM .image-shape .label,#mermaid-svg-oKbnTLgONy7t4CuM .icon-shape .label{text-align:center;}#mermaid-svg-oKbnTLgONy7t4CuM .node.clickable{cursor:pointer;}#mermaid-svg-oKbnTLgONy7t4CuM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oKbnTLgONy7t4CuM .arrowheadPath{fill:#333333;}#mermaid-svg-oKbnTLgONy7t4CuM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oKbnTLgONy7t4CuM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oKbnTLgONy7t4CuM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oKbnTLgONy7t4CuM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oKbnTLgONy7t4CuM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oKbnTLgONy7t4CuM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oKbnTLgONy7t4CuM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oKbnTLgONy7t4CuM .cluster text{fill:#333;}#mermaid-svg-oKbnTLgONy7t4CuM .cluster span{color:#333;}#mermaid-svg-oKbnTLgONy7t4CuM 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-oKbnTLgONy7t4CuM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oKbnTLgONy7t4CuM rect.text{fill:none;stroke-width:0;}#mermaid-svg-oKbnTLgONy7t4CuM .icon-shape,#mermaid-svg-oKbnTLgONy7t4CuM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oKbnTLgONy7t4CuM .icon-shape p,#mermaid-svg-oKbnTLgONy7t4CuM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oKbnTLgONy7t4CuM .icon-shape .label rect,#mermaid-svg-oKbnTLgONy7t4CuM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oKbnTLgONy7t4CuM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oKbnTLgONy7t4CuM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oKbnTLgONy7t4CuM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 比较中断
CNT == CCR
CC4IF = 1
PulseFinishedCallback
更新中断
周期结束
CNT 到 ARR
UIF = 1
PeriodElapsedCallback
比较中断版本的代码:
c
/* USER CODE BEGIN PV */
volatile uint32_t g_pwmcnt = 0; // 软件分频计数
volatile uint16_t g_pwm_value = 100; // 当前亮度
volatile uint8_t g_dir = 1; // 1:渐亮;0:渐暗
/* USER CODE END PV */
/* USER CODE BEGIN 4 */
/**
* @brief PWM 比较匹配回调,CNT == CCR 时进入
*/
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance != TIM3)
{
return;
}
// 每 2 次比较匹配才更新一次亮度
g_pwmcnt++;
if (g_pwmcnt < 2)
{
return;
}
g_pwmcnt = 0;
// 写入当前亮度
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_4, g_pwm_value);
if (g_dir) // 渐亮
{
g_pwm_value++;
if (g_pwm_value >= 999)
{
g_pwm_value = 999;
g_dir = 0;
}
}
else // 渐暗
{
g_pwm_value--;
if (g_pwm_value <= 10) // 留约 1% 底光
{
g_pwm_value = 10;
g_dir = 1;
}
}
}
/* USER CODE END 4 */
比较中断也能做呼吸灯,但它的触发位置跟着 CCR 走:CCR 小的时候在周期前段触发,CCR 大的时候在周期后段触发。对呼吸灯这种只需要"稳定节拍"的任务,更新中断更直观;比较中断更适合"在占空比边界做某件事"的场景。这个实验数据规律简单,也不需要 DMA,用"大炮打蚊子"来形容很贴切。
知识小结|两种中断怎么选
更新中断在计数器数到 ARR、周期结束时触发,标志是 UIF,回调是 HAL_TIM_PeriodElapsedCallback;比较匹配中断在 CNT == CCR 时触发,标志是 CC4IF,PWM 模式下回调是 HAL_TIM_PWM_PulseFinishedCallback。前者的节拍与 CCR 无关,适合呼吸灯这种固定节奏任务;后者适合在占空比边界做动作。呼吸灯用更新中断更简单稳定,比较中断了解即可。
3.4 HAL 库原函数拆解
前面用到了不少 HAL 函数。下面把签名、参数、返回值和关键寄存器放在一起,方便以后查。实际源码还会做参数断言、状态检查、触发模式判断和错误处理,这里只保留主线。
| 函数 | 参数 | 返回 | 关键动作 | 中断 |
|---|---|---|---|---|
HAL_TIM_PWM_Start |
htim, Channel |
HAL_StatusTypeDef |
CCxE=1,CEN=1,高级定时器还要 MOE=1 | 无 |
HAL_TIM_Base_Start_IT |
htim |
HAL_StatusTypeDef |
UIE=1,CEN=1 | 更新中断 |
HAL_TIM_PWM_Start_IT |
htim, Channel |
HAL_StatusTypeDef |
CCxIE=1,CCxE=1,CEN=1 | 比较中断 |
HAL_TIM_PWM_Stop |
htim, Channel |
HAL_StatusTypeDef |
CCxE=0,CEN=0 | 关闭 PWM |
HAL_TIM_PWM_Stop_IT |
htim, Channel |
HAL_StatusTypeDef |
CCxIE=0,CCxE=0,CEN=0 | 关闭 PWM 和比较中断 |
HAL_TIM_Base_Stop_IT |
htim |
HAL_StatusTypeDef |
UIE=0,CEN=0 | 关闭更新中断 |
__HAL_TIM_SET_COMPARE |
htim, Channel, Compare |
无 | 写 CCRx | 无 |
HAL_StatusTypeDef 常见值:
HAL_OK:成功;HAL_ERROR:错误;HAL_BUSY:外设忙;HAL_TIMEOUT:超时。
HAL_TIM_PWM_Start
c
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel)
{
// 打开通道输出:CCxE = 1
TIM_CCxChannelCmd(htim->Instance, Channel, TIM_CCx_ENABLE);
// 高级定时器需要主输出使能 MOE;TIM3 是通用定时器,不走这里
if (IS_TIM_BREAK_INSTANCE(htim->Instance) != RESET)
{
__HAL_TIM_MOE_ENABLE(htim);
}
// 启动计数器:CEN = 1
__HAL_TIM_ENABLE(htim);
return HAL_OK;
}
它只负责"让计数器跑起来、让通道输出波形",不会打开任何中断。
HAL_TIM_Base_Start_IT
c
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)
{
// 打开更新中断:UIE = 1
__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);
// 启动计数器:CEN = 1
__HAL_TIM_ENABLE(htim);
return HAL_OK;
}
它只负责"计数 + 更新中断",不配置 PWM 通道。和 HAL_TIM_PWM_Start 一起用时,CEN 会被设置两次,但没有危害。
HAL_TIM_PWM_Start_IT
c
HAL_StatusTypeDef HAL_TIM_PWM_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel)
{
// 打开对应通道的比较中断:CCxIE = 1
switch (Channel)
{
case TIM_CHANNEL_1: __HAL_TIM_ENABLE_IT(htim, TIM_IT_CC1); break;
case TIM_CHANNEL_2: __HAL_TIM_ENABLE_IT(htim, TIM_IT_CC2); break;
case TIM_CHANNEL_3: __HAL_TIM_ENABLE_IT(htim, TIM_IT_CC3); break;
case TIM_CHANNEL_4: __HAL_TIM_ENABLE_IT(htim, TIM_IT_CC4); break;
default: break;
}
// 打开通道输出:CCxE = 1
TIM_CCxChannelCmd(htim->Instance, Channel, TIM_CCx_ENABLE);
// 高级定时器需要主输出使能 MOE;TIM3 是通用定时器,不走这里
if (IS_TIM_BREAK_INSTANCE(htim->Instance) != RESET)
{
__HAL_TIM_MOE_ENABLE(htim);
}
// 启动计数器:CEN = 1
__HAL_TIM_ENABLE(htim);
return HAL_OK;
}
注意它打开的是 CCxIE,不是 UIE。所以进入的是比较匹配回调,不是更新回调。
__HAL_TIM_SET_COMPARE
这个宏最终写的是 CCR 寄存器:
c
#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
(*(__IO uint32_t *)(&((__HANDLE__)->Instance->CCR1) + ((__CHANNEL__) >> 2U)) = (__COMPARE__))
TIM_CHANNEL_1 到 TIM_CHANNEL_4 的值经过 >> 2 偏移,分别落到 CCR1 到 CCR4。所以 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, 500) 写的就是 TIM3->CCR4。
HAL_TIM_IRQHandler
中断入口只做一件事:把中断请求转给 HAL。HAL 里会根据标志位分发:
c
void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim)
{
// 更新中断:UIF 置位,并且 UIE 打开
if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE) != RESET &&
__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);
HAL_TIM_PeriodElapsedCallback(htim);
}
// 通道 4 比较匹配:CC4IF 置位,并且 CC4IE 打开
if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC4) != RESET &&
__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_CC4) != RESET)
{
__HAL_TIM_CLEAR_IT(htim, TIM_IT_CC4);
if (htim->Mode == HAL_TIM_MODE_PWM)
{
HAL_TIM_PWM_PulseFinishedCallback(htim);
}
else
{
HAL_TIM_OC_DelayElapsedCallback(htim);
}
}
}
实际源码还会处理 CC1、CC2、CC3、刹车、触发等分支,这里只保留本实验用到的两条。另外,实际源码在调用 PWM 回调前还会检查 CCMR 的 OCxM 位,确认通道确实工作在 PWM 模式;这里简化成 htim->Mode 判断。
CubeMX 生成的初始化函数
MX_TIM3_Init() 里通常能看到这样的结构:
c
static void MX_TIM3_Init(void)
{
htim3.Instance = TIM3;
// 时基参数:对应 PSC、ARR、向上计数、预装载
htim3.Init.Prescaler = 71;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 999;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&htim3);
// 通道参数:PWM 模式 1、初始 CCR、极性、快速模式
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_4);
}
两个函数的分工:
HAL_TIM_PWM_Init:初始化时基,最后会调用HAL_TIM_PWM_MspInit;HAL_TIM_PWM_ConfigChannel:配置通道模式、CCR 初始值、极性和预装载。
HAL_TIM_PWM_MspInit 是弱函数,CubeMX 通常会在 stm32f1xx_hal_msp.c 里生成它,负责开 TIM3 时钟、配置 PB1 复用和 NVIC;具体函数名以你的工程为准。你在 GPIO Settings 和 NVIC 里点的东西,最终都会落到对应的 MSP 初始化函数里。
知识小结|HAL 函数和寄存器
HAL_TIM_PWM_Start 打开通道输出和计数器,HAL_TIM_Base_Start_IT 打开更新中断,HAL_TIM_PWM_Start_IT 打开比较中断,三者分别对应 CCxE、UIE、CCxIE 和 CEN 的不同组合。__HAL_TIM_SET_COMPARE 直接写 CCRx,HAL_TIM_IRQHandler 根据 UIF/CCxIF 分发到更新回调或比较回调。CubeMX 生成的 MX_TIM3_Init 负责时基和通道参数,HAL_TIM_PWM_MspInit 负责时钟、GPIO 和 NVIC 这些底层初始化。
4. 验证与调试
4.1 串口验证:确认进了哪个中断
呼吸灯能亮,不代表中断一定进对了。这里用串口打印做两个独立测试:
- 更新中断:
HAL_TIM_PWM_Start+HAL_TIM_Base_Start_IT,打印HAL_TIM_PeriodElapsedCallback; - 比较中断:只调用
HAL_TIM_PWM_Start_IT,打印HAL_TIM_PWM_PulseFinishedCallback。
c
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM3_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
// 测试一:更新中断
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
HAL_TIM_Base_Start_IT(&htim3);
// 测试二:比较中断(测试时只保留这一句,注释掉上面两句)
// HAL_TIM_PWM_Start_IT(&htim3, TIM_CHANNEL_4);
/* USER CODE END 2 */
while (1)
{
}
}
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
printf("HAL_TIM_PeriodElapsedCallback\r\n");
HAL_TIM_Base_Stop_IT(&htim3); // 打印一次就停,避免刷屏
}
}
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
printf("HAL_TIM_PWM_PulseFinishedCallback\r\n");
HAL_TIM_PWM_Stop_IT(&htim3, TIM_CHANNEL_4);
}
}
/* USER CODE END 4 */
HAL_TIM_Base_Stop_IT 停止计数并关闭更新中断;HAL_TIM_PWM_Stop_IT 停止 PWM 输出并关闭比较中断。
串口数据流如下:
串口助手 USART1 STM32F103 串口助手 USART1 STM32F103 #mermaid-svg-NV1ZKfd6Kgl1O1qu{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-NV1ZKfd6Kgl1O1qu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .error-icon{fill:#552222;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .marker.cross{stroke:#333333;}#mermaid-svg-NV1ZKfd6Kgl1O1qu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NV1ZKfd6Kgl1O1qu p{margin:0;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-NV1ZKfd6Kgl1O1qu text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-NV1ZKfd6Kgl1O1qu .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-NV1ZKfd6Kgl1O1qu #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .sequenceNumber{fill:white;}#mermaid-svg-NV1ZKfd6Kgl1O1qu #sequencenumber{fill:#333;}#mermaid-svg-NV1ZKfd6Kgl1O1qu #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .messageText{fill:#333;stroke:none;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .labelText,#mermaid-svg-NV1ZKfd6Kgl1O1qu .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .loopText,#mermaid-svg-NV1ZKfd6Kgl1O1qu .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-NV1ZKfd6Kgl1O1qu .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .noteText,#mermaid-svg-NV1ZKfd6Kgl1O1qu .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .actorPopupMenu{position:absolute;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-NV1ZKfd6Kgl1O1qu .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-NV1ZKfd6Kgl1O1qu .actor-man circle,#mermaid-svg-NV1ZKfd6Kgl1O1qu line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-NV1ZKfd6Kgl1O1qu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 显示 HAL_TIM_PeriodElapsedCallback 显示 HAL_TIM_PWM_PulseFinishedCallback alt 验证更新中断 验证比较中断 "HAL_TIM_PeriodElapsedCallback" 115200,8N1 HAL_TIM_Base_Stop_IT "HAL_TIM_PWM_PulseFinishedCallback" 115200,8N1 HAL_TIM_PWM_Stop_IT
实际界面会显示对应的回调字符串。
串口助手设 115200、8 数据位、无校验、1 停止位,选对 COM 口。printf 要先重定向到 USART1,Keil 下通常是重写 fputc 并勾选 MicroLIB。不要在 1kHz 中断里长期打印,这里只是打印一次就停。
知识小结|中断验证
验证更新中断时,用 HAL_TIM_PWM_Start 加 HAL_TIM_Base_Start_IT,在 HAL_TIM_PeriodElapsedCallback 里打印后调用 HAL_TIM_Base_Stop_IT;验证比较中断时,只用 HAL_TIM_PWM_Start_IT,在 HAL_TIM_PWM_PulseFinishedCallback 里打印后调用 HAL_TIM_PWM_Stop_IT。串口设为 115200、8N1,看到对应字符串就说明中断链路通了。两个测试一次只做一个,打印一次就停。
4.2 示波器和现象验证
有示波器或逻辑分析仪时,探头接 PB1、地夹接 GND,应该看到:
- 频率 1kHz,周期 1ms;
- 幅值约 3.3V;
- 高电平时间随呼吸变化,大约 100us 到 999us。
| CCR | 占空比 | 高电平时间 |
|---|---|---|
| 100 | 10% | 100us |
| 500 | 50% | 500us |
| 999 | 99.9% | 999us |
没有示波器也没关系,直接看高亮 LED 是否平滑地渐亮、渐暗;用万用表直流档看 PB1 平均电压也能判断占空比是否在变,但不如示波器直观。
知识小结|波形验证
PB1 应该输出 1kHz、约 3.3V 的方波,周期固定 1ms,高电平宽度随呼吸在 100us 到 999us 之间变化。周期也在变,说明 PSC 或 ARR 被改了;高电平宽度不变,说明 CCR 没更新;完全没有波形,先查 PWM 启动、PB1 复用和 H1 跳线。
4.3 常见问题
| 现象 | 优先检查 |
|---|---|
| LED 完全不亮 | H1 的 7、8 号跳线是否插好 |
| LED 完全不亮 | PB1 是否配置为 TIM3_CH4 复用 |
| LED 完全不亮 | 是否调用了 HAL_TIM_PWM_Start |
| LED 完全不亮 | CCR 是否一直是 0,或初始值没写入 |
| LED 一直最亮 | CCR 是否越界,PB1 是否一直是高电平 |
| 不进更新中断 | NVIC 是否勾选 TIM3,是否调用 HAL_TIM_Base_Start_IT |
| 不进比较中断 | 是否调用 HAL_TIM_PWM_Start_IT,而不是 HAL_TIM_PWM_Start |
| 回调不执行 | 函数名是否拼对,通道是否写成 TIM_CHANNEL_4 |
| 串口无输出 | 波特率、COM 口、printf 重定向 |
| LED 闪烁 | PWM 频率是否真的 1kHz,时钟和 ARR 是否正确 |
| 呼吸只有一个方向 | dir 是否在 999 和 100 处翻转 |
| 重新生成代码后逻辑丢了 | 用户代码是否写在 USER CODE 区域 |
排错顺序建议:先看 H1 跳线和 PB1 波形,再看 PWM 启动和 CCR,最后看 NVIC 和回调。先确认"有没有波形",再确认"波形对不对",比盯着代码猜更快。
知识小结|排错顺序
先硬件后软件:H1 的 7、8 号跳线、PB1 是否复用为 TIM3_CH4、PWM 是否启动、CCR 是否写入;再看 NVIC 和启动函数,确认更新中断或比较中断有没有打开;最后用串口确认回调、用示波器确认波形。大部分"灯不亮"都是跳线、复用或 CCR 的问题。
5. 完整代码
5.1 main.c:更新中断版本
c
/* USER CODE BEGIN Includes */
#include <stdio.h> // 串口验证时使用,不用可以删
/* USER CODE END Includes */
/* USER CODE BEGIN PV */
/*
* 呼吸灯状态变量。
* volatile:这些变量会在中断里被修改,防止编译器优化成旧值。
*/
volatile uint16_t pwm_value = 100; // 当前 CCR,100 表示 10% 亮度
volatile uint8_t breath_dir = 1; // 1:渐亮;0:渐暗
volatile uint8_t update_cnt = 0; // 每 2 次 1ms 中断更新一次
/* USER CODE END PV */
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM3_Init();
MX_USART1_UART_Init(); // 不用串口可以删掉
/* USER CODE BEGIN 2 */
// 启动 PWM 输出
if (HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4) != HAL_OK)
{
Error_Handler();
}
// 启动 1ms 更新中断
if (HAL_TIM_Base_Start_IT(&htim3) != HAL_OK)
{
Error_Handler();
}
// 写入初始亮度,避免上电后一直是 CCR=0
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, pwm_value);
/* USER CODE END 2 */
while (1)
{
/*
* 主循环可以空闲,也可以处理按键、显示、通信等任务。
* 呼吸节奏由 TIM3 更新中断保证。
*/
}
}
/* USER CODE BEGIN 4 */
/**
* @brief 定时器更新中断回调,每 1ms 进入一次
* @param htim 定时器句柄
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
// 只处理 TIM3
if (htim->Instance != TIM3)
{
return;
}
// 软件再分频:每 2 次中断才改一次亮度
update_cnt++;
if (update_cnt < 2)
{
return;
}
update_cnt = 0;
// 写 CCR4;预装载开启时,下一个更新事件生效
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_4, pwm_value);
if (breath_dir) // 渐亮
{
pwm_value++;
if (pwm_value >= 999)
{
pwm_value = 999; // 夹住上限
breath_dir = 0; // 改为渐暗
}
}
else // 渐暗
{
pwm_value--;
if (pwm_value <= 100)
{
pwm_value = 100; // 夹住下限,保留 10% 底光
breath_dir = 1; // 改为渐亮
}
}
}
/* USER CODE END 4 */
5.2 stm32f1xx_it.c:中断入口
c
/**
* @brief This function handles TIM3 global interrupt.
*/
void TIM3_IRQHandler(void)
{
/* USER CODE BEGIN TIM3_IRQn 0 */
/* USER CODE END TIM3_IRQn 0 */
HAL_TIM_IRQHandler(&htim3);
/* USER CODE BEGIN TIM3_IRQn 1 */
/* USER CODE END TIM3_IRQn 1 */
}
这个函数由 CubeMX 生成,名字必须和启动文件里的中断向量一致。NVIC 没开 TIM3 中断时,可能不会生成它。
5.3 切换到比较中断版本
只改三处:
text
1. HAL_TIM_PWM_Start + HAL_TIM_Base_Start_IT
→ HAL_TIM_PWM_Start_IT
2. HAL_TIM_PeriodElapsedCallback
→ HAL_TIM_PWM_PulseFinishedCallback
3. 下限 100
→ 下限 10(可选,底光更暗)
两种启动方式不要同时保留,否则更新中断和比较中断都会进来,呼吸节奏会乱。
知识小结|完整工程
完整工程由 CubeMX 生成的初始化代码、用户写在 USER CODE 区域的启动代码和回调、以及 stm32f1xx_it.c 里的 TIM3_IRQHandler 组成。更新中断版本用 HAL_TIM_PWM_Start + HAL_TIM_Base_Start_IT;比较中断版本用 HAL_TIM_PWM_Start_IT,回调换成 HAL_TIM_PWM_PulseFinishedCallback。代码改动只放在 USER CODE 区域,重新生成工程时才不会丢。
6. 一页纸总结
#mermaid-svg-9VoMqCVRpxLPX6Xm{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-9VoMqCVRpxLPX6Xm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9VoMqCVRpxLPX6Xm .error-icon{fill:#552222;}#mermaid-svg-9VoMqCVRpxLPX6Xm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9VoMqCVRpxLPX6Xm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9VoMqCVRpxLPX6Xm .marker.cross{stroke:#333333;}#mermaid-svg-9VoMqCVRpxLPX6Xm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9VoMqCVRpxLPX6Xm p{margin:0;}#mermaid-svg-9VoMqCVRpxLPX6Xm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9VoMqCVRpxLPX6Xm .cluster-label text{fill:#333;}#mermaid-svg-9VoMqCVRpxLPX6Xm .cluster-label span{color:#333;}#mermaid-svg-9VoMqCVRpxLPX6Xm .cluster-label span p{background-color:transparent;}#mermaid-svg-9VoMqCVRpxLPX6Xm .label text,#mermaid-svg-9VoMqCVRpxLPX6Xm span{fill:#333;color:#333;}#mermaid-svg-9VoMqCVRpxLPX6Xm .node rect,#mermaid-svg-9VoMqCVRpxLPX6Xm .node circle,#mermaid-svg-9VoMqCVRpxLPX6Xm .node ellipse,#mermaid-svg-9VoMqCVRpxLPX6Xm .node polygon,#mermaid-svg-9VoMqCVRpxLPX6Xm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9VoMqCVRpxLPX6Xm .rough-node .label text,#mermaid-svg-9VoMqCVRpxLPX6Xm .node .label text,#mermaid-svg-9VoMqCVRpxLPX6Xm .image-shape .label,#mermaid-svg-9VoMqCVRpxLPX6Xm .icon-shape .label{text-anchor:middle;}#mermaid-svg-9VoMqCVRpxLPX6Xm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9VoMqCVRpxLPX6Xm .rough-node .label,#mermaid-svg-9VoMqCVRpxLPX6Xm .node .label,#mermaid-svg-9VoMqCVRpxLPX6Xm .image-shape .label,#mermaid-svg-9VoMqCVRpxLPX6Xm .icon-shape .label{text-align:center;}#mermaid-svg-9VoMqCVRpxLPX6Xm .node.clickable{cursor:pointer;}#mermaid-svg-9VoMqCVRpxLPX6Xm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9VoMqCVRpxLPX6Xm .arrowheadPath{fill:#333333;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9VoMqCVRpxLPX6Xm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9VoMqCVRpxLPX6Xm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9VoMqCVRpxLPX6Xm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9VoMqCVRpxLPX6Xm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9VoMqCVRpxLPX6Xm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9VoMqCVRpxLPX6Xm .cluster text{fill:#333;}#mermaid-svg-9VoMqCVRpxLPX6Xm .cluster span{color:#333;}#mermaid-svg-9VoMqCVRpxLPX6Xm 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-9VoMqCVRpxLPX6Xm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9VoMqCVRpxLPX6Xm rect.text{fill:none;stroke-width:0;}#mermaid-svg-9VoMqCVRpxLPX6Xm .icon-shape,#mermaid-svg-9VoMqCVRpxLPX6Xm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9VoMqCVRpxLPX6Xm .icon-shape p,#mermaid-svg-9VoMqCVRpxLPX6Xm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9VoMqCVRpxLPX6Xm .icon-shape .label rect,#mermaid-svg-9VoMqCVRpxLPX6Xm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9VoMqCVRpxLPX6Xm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9VoMqCVRpxLPX6Xm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9VoMqCVRpxLPX6Xm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CubeMX
72MHz / TIM3_CH4 / PB1
HAL_TIM_PWM_Start
开波形
HAL_TIM_Base_Start_IT
开 1ms 更新中断
TIM3 更新中断
每 2ms 改一次 CCR4
PB1 输出 1kHz PWM
Q3 驱动高亮 LED
约 2s 渐亮 + 2s 渐暗
| 关键项 | 值 |
|---|---|
| 定时器 / 通道 | TIM3_CH4 / PB1 |
| 定时器时钟 | 72MHz |
| PSC / ARR | 71 / 999 |
| PWM 频率 / 周期 | 1kHz / 1ms |
| CCR 范围 | 100 到 999 |
| 更新间隔 | 2ms |
| 更新回调 | HAL_TIM_PeriodElapsedCallback |
| 比较回调 | HAL_TIM_PWM_PulseFinishedCallback |
| 跳线 | H1 的 7、8 号 |
最后用六个问题检查自己是否真的理解:
- PWM 为什么能调亮度? LED 快速开关,人眼看到平均亮度;占空比越大越亮。
- PSC、ARR、CCR 各管什么? PSC 管计数时钟,ARR 管周期,CCR 管占空比。
- 为什么每 2ms 改一次 CCR? 1000 档 × 2ms ≈ 2 秒,正好是想要的呼吸速度。
- 为什么改 CCR 不改 ARR? ARR 改频率,CCR 改占空比;呼吸要固定频率。
- 更新中断怎么走到回调? 更新事件 → NVIC →
TIM3_IRQHandler→HAL_TIM_IRQHandler→HAL_TIM_PeriodElapsedCallback。 - 更新中断和比较中断差在哪? 更新中断在周期结束触发,比较中断在
CNT == CCR时触发,回调和用途不同。
先把灯点亮,再让它呼吸,最后用串口和示波器证明"中断进对了、波形也对"。这三步走通,PWM 和定时器中断这条线就真正掌握了。
本文围绕 STM32F103ZET6 的 TIM3_CH4 PWM 呼吸灯与更新中断展开,波形、硬件连接、中断链和串口数据流均用 Mermaid 绘制;代码以 STM32 HAL 库和 Keil MDK 工程为参考。不同开发板、CubeMX 版本和 HAL 版本的细节可能略有差异,请以实际工程和芯片手册为准。
