【stm32】PWM呼吸灯

🔥铅笔小新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. 呼吸灯的原理
  2. [CubeMX 配置](#CubeMX 配置)
  3. 代码实现
  4. 验证与调试
  5. 完整代码
  6. 一页纸总结

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 时钟

  1. SYS → Debug 选 Serial Wire,保留 SWD 调试。
  2. RCC → High Speed Clock (HSE) 选 Crystal/Ceramic Resonator,使用外部晶振,时钟更稳。
  3. 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--)

这里有三个容易忽略的小点:

  1. 先写 CCR,再算下一次亮度。 这样第一次更新写入的是初始值 100,序列是 100、101、102......;
  2. 到边界要夹住。 999 和 100 都是有效范围,不要让无符号数继续加或减;
  3. 预装载会晚一拍。 你在更新中断里写 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 号

最后用六个问题检查自己是否真的理解:

  1. PWM 为什么能调亮度? LED 快速开关,人眼看到平均亮度;占空比越大越亮。
  2. PSC、ARR、CCR 各管什么? PSC 管计数时钟,ARR 管周期,CCR 管占空比。
  3. 为什么每 2ms 改一次 CCR? 1000 档 × 2ms ≈ 2 秒,正好是想要的呼吸速度。
  4. 为什么改 CCR 不改 ARR? ARR 改频率,CCR 改占空比;呼吸要固定频率。
  5. 更新中断怎么走到回调? 更新事件 → NVIC → TIM3_IRQHandler → HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback。
  6. 更新中断和比较中断差在哪? 更新中断在周期结束触发,比较中断在 CNT == CCR 时触发,回调和用途不同。

先把灯点亮,再让它呼吸,最后用串口和示波器证明"中断进对了、波形也对"。这三步走通,PWM 和定时器中断这条线就真正掌握了。


本文围绕 STM32F103ZET6 的 TIM3_CH4 PWM 呼吸灯与更新中断展开,波形、硬件连接、中断链和串口数据流均用 Mermaid 绘制;代码以 STM32 HAL 库和 Keil MDK 工程为参考。不同开发板、CubeMX 版本和 HAL 版本的细节可能略有差异,请以实际工程和芯片手册为准。


相关推荐
深圳老胡1 小时前
RTX 与其他 RTOS 操作系统的差异及优缺点分析
笔记·stm32·单片机·代码规范
洞察物理世界2 小时前
【SI_PRBS 01】快速了解光通信PRBS
单片机·嵌入式硬件
happygrilclh2 小时前
LVGL 移植整体流程
stm32
续写烂尾3 小时前
SDNAND 硬件电路设计:供电、上拉与走线全景
单片机·嵌入式硬件·sd nand·sdnand
Brilliantwxx4 小时前
【STM32】 RTC 实时时钟详解:从概念到预分频器
stm32·单片机·嵌入式硬件·mcu·bash
lhbweilai7 小时前
分析FreeRTOS内核对象API的源码实现(四)
c语言·stm32·单片机
inrichtt10 小时前
第八课时:SPI总线揭秘:四线全双工通信的奥
c语言·数据结构·stm32·单片机·嵌入式硬件
半杯咖啡半行码11 小时前
Linux字符设备驱动开发基础:从设备号到ioctl的完整实战
stm32·单片机·嵌入式硬件
碧海银沙音频科技研究院12 小时前
基于炬芯ATS3089X的usb U盘/usb audio声卡/usb cdc虚拟串口/录音dump到本地功能实现
stm32·单片机·嵌入式硬件