资讯动态

STM32F103C8T6呼吸灯实战:从GPIO到PWM的完整外设链路

发布时间:2026/10/24 6:58:23 来源:尧图企业网站定制
刚拿到 STM32F103C8T6 这块板子想找个“看起来很简单、但足够把整个外设链路串起来”的实验我第一个想到的就是呼吸灯。这名字听着高大上拆开看其实就是一件事让 LED 的亮度按照一个缓慢的规律先升后降像人呼吸一样均匀地亮起来、暗下去。它把 GPIO、定时器、PWM、中断这些嵌入式入门必碰的东西全串在了一起而且最后效果肉眼可见非常有成就感。这篇文章就把我从“点亮一颗灯”到“做出舒服呼吸曲线”的完整过程、参数计算、踩坑记录写出来给刚接触单片机的朋友和想用 C8T6 做进阶练习的人一个参考。1. 为什么第一个练手项目选呼吸灯从需求到原理1.1 呼吸灯到底在做一件什么事很多朋友第一次看到呼吸灯以为要写什么复杂的算法。真的不是。呼吸灯的本质就是一个“可变的亮度”控制而这个可变亮度的核心指标只有一个LED 两端有效功率的时间占比。如果用 GPIO 直接输出高电平灯是满亮输出低电平灯是灭的。想让灯“半亮”办法不是在 3.3V 和 0V 之间输出一个 1.65V——F103C8T6 没有模拟电压输出能力而是用一连串快速切换的脉冲让灯在一个很小的时间窗口里“一半时间通电、一半时间断电”。人眼有视觉暂留看到的就是中间亮度。这个时间占比专业叫法就是占空比。占空比 0% 灯灭100% 灯全亮50% 半亮。呼吸灯要做的事情就非常明确了让占空比从 0 慢慢爬到 100再从 100 慢慢降到 0如此往复。整个过程里你并不需要关心“多少个脉冲”你只要关心每个瞬间占空比应该是多少。1.2 为什么不用模拟电压而用 PWM这一点我一开始也没想明白总觉得模拟电压自然、连续肯定比这种“开关切换”高级。后来在一次用万用表测输出时突然意识到STM32F103C8T6 这类中低端 MCU 上压根就没有真正的 DAC 模块F103 系列内部没有C8T6 更是直接用 GPIO 高速翻转来实现“伪模拟”。PWM 的好处还不止“没有 DAC 也能做模拟”这一条。它把“控制量”变成了一个精确的时间参数比如 ARR 设定的周期是 100 个计数单位你只需要改变 CCR 比较值就能得到 0% 到 100% 的任意占空比。这个精度非常高72MHz 主频下一个计数单位是十几纳秒级别控制亮度绰绰有余。而且 PWM 信号天然抗干扰就算线路有点噪声占空比也不会漂移这是直接调电压做不到的。所以不要嫌弃 PWM“粗”它在电机调速、开关电源、舵机控制里都是一等一的主力方案。1.3 为什么是 STM32F103C8T6选这颗芯片做呼吸灯很多人觉得大材小用其实刚刚好。C8T6 是 F103 里的“性价比甜点”64KB Flash、20KB RAM、72MHz 主频板子便宜、资料多、开发工具链成熟哪怕只是点亮一颗 LED它也能让你把“看手册、配时钟、查外设框图”这一套完整流程走一遍。更关键的是它的定时器资源很够用。做呼吸灯需要至少一个高级控制定时器或通用定时器输出 PWMF103C8T6 有多个定时器其中一个专门干这个事剩下的还能留给你以后接编码器、做串口超时、产生系统 tick。你用 51 单片机也能做呼吸灯但那种“软件延时翻转 GPIO”的办法效率低、精度差还占死 CPU。用 C8T6 的硬件定时器做 PWMCPU 几乎不用参与这才是嵌入式该有的思路。2. 动手前的准备硬件清单与工程脚手架2.1 硬件清单与接线方式我做这个项目用的东西很省基本是手边随便翻出来的STM32F103C8T6 核心板一块板载 8MHz 晶振那种红色 LED 一只直径 5mm 的普通直插灯珠就行220Ω 限流电阻一个面包板一块或者直接飞线焊接SWD 接口的下载器一个杜邦线若干接线非常简单把 PA0 引脚通过 220Ω 电阻接到 LED 正极LED 负极直接接 GND。PA0 输出高电平时电流从 PA0 经电阻流过 LED 到地灯亮输出低电平时两端电压一样灯灭。这是“高电平点亮”的接法。有些教程会让你用“灌电流”方式接也就是 LED 正极接 3.3V负极经电阻接 PA0低电平点亮。两种都行但高电平点亮的方式更直观排查问题时用万用表量 PA0 电压大于 1.5V 就是灯亮逻辑比较清晰。我建议新手先用这种。2.2 限流电阻是怎么算出来的电阻不是随便拿一个就行的。LED 是电流型器件正常工作时压降基本固定红色 LED 典型压降约 1.8V 到 2.2VC8T6 的 GPIO 输出高电平大约是 3.3V。如果直接接电流可能冲到几十毫安以上轻则亮度刺眼、寿命骤降重则损伤引脚。目标电流定在 5mA 到 10mA 之间就够了室内环境 5mA 已经可以看得很清楚。按 5mA 算的公式是R (3.3V - 2.0V) / 0.005A 260Ω取常见的 220Ω 或 330Ω 都可以。选 220Ω 实际电流约 5.9mA刚好在安全区如果选 330Ω电流约 3.9mA亮度会柔和一些。我用的 220Ω实测在室内看呼吸效果非常自然也没有刺眼感。2.3 工程初始化和定时器配置这个项目我推荐用图形化初始化工具先把外设配置好再手写逻辑代码效率最高。打开工具后芯片型号直接选 STM32F103C8Tx然后按下面几步操作RCC 配置里打开 HSE 外部高速晶振方式选 Crystal/Ceramic Resonator这样系统时钟可以走 72MHz。时钟树设置HSE 8MHz 经过 PLL 倍频到 72MHz这是 F103 的满速状态。别漏这一步漏了就是默认 8MHzPWM 频率会差很多。找到 TIM2把 Channel1 的模式设为 PWM Generation CH1对应引脚会自动指定到 PA0。在 TIM2 的参数面板里设 Prescaler预分频为 71Counter Period自动重载值为 99。生成工程后在 IDE 里打开 main.c准备写逻辑代码。这里预分频 71 和自动重载 99 的来历要说清楚。PWM 信号的频率 72MHz / (预分频 1) / (自动重载值 1)。代入数值就是 72MHz / 72 / 100 10kHz。这个频率是人眼完全察觉不到闪烁的同时占空比调节步数有 100 档足够让呼吸过程细腻平滑。如果你想调节更细可以把自动重载值加到 199但要注意频率会降到 5kHz依然可用。3. 核心代码让灯先“亮起来”再“呼吸起来”3.1 最小 PWM 代码框架工程生成后定时器初始化已经自动写好了你只要在 main 函数的 while 循环前启动 PWM 输出。最关键的启动代码是这一行HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);这行不写配置再漂亮也没用PA0 不会有波形输出。启动之后如果你不修改比较寄存器灯会保持初始占空比 0% 的状态也就是灭的。现在我们先做一个最简单的静态亮度测试__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 50);把比较值设为 50也就是 50% 占空比灯应该半亮。这一步如果灯亮且亮度适中说明硬件接线和 PWM 配置全部正常可以进入呼吸逻辑了。3.2 从线性渐变到正弦呼吸曲线很多人的第一版呼吸逻辑长这样每几毫秒把占空比加 1加到 100 再减 1。代码很短但实测你会发现效果有点“机械化”从暗到亮的过程中前 30% 变化很快很明显后 70% 几乎看不出变化然后猛地一下亮起来。这不是幻觉。LED 的亮度与电流大致成正比但人眼对亮度的感知是对数关系同一档电流增量在弱光区感觉变化明显在高亮区感觉差异很小。线性改变占空比等于在亮端“花了大量时间却看不到变化”在暗端“一眨眼就跳过去了”呼吸感自然很生硬。解决办法是改用正弦曲线准确说是 1-cos 曲线。这条曲线在 0 和 100 两端变化非常平缓在中间 50 附近变化最快正好和人眼的感知曲线互补。效果就是灯从熄灭状态慢慢“苏醒”中间加速变亮接近顶部又缓缓收住然后对称地暗下去非常像人的呼吸节律。计算方式有两种。偷懒一点直接在循环里用浮点函数现场算#include math.h while (1) { for (uint16_t step 0; step 100; step) { float rad 3.1415926f * step / 100.0f; uint32_t ccr (uint32_t)((1.0f - cosf(rad)) * 50.0f 0.5f); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr); HAL_Delay(10); } for (uint16_t step 100; step 0; step--) { float rad 3.1415926f * step / 100.0f; uint32_t ccr (uint32_t)((1.0f - cosf(rad)) * 50.0f 0.5f); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr); HAL_Delay(10); } }解释一下这里的计算cos 在 0 到 π 之间从 1 降到 -1所以(1 - cos) / 2的取值是从 0 到 1再乘 100 就对应 0% 到 100% 的占空比。 0.5f是四舍五入防止浮点截断产生偏差。C8T6 没有硬件浮点单元float 运算靠软件模拟慢是慢一点但每次循环也就算一次延时 10ms 占大头完全没问题。不过我更喜欢查表法程序启动时先把 101 个点的曲线算好存进数组主循环里只做查表和赋值逻辑更干净也方便以后直接改表格做指数曲线、对数曲线。3.3 步进、延时和频率的配合逻辑很多朋友在这三个参数上随意组合结果要么呼吸太快像闪光灯要么一次呼吸要五六秒急死人。其实它们之间有个简单的换算关系。一次完整的呼吸包含 101 步变亮和 101 步变暗共约 202 个周期。如果每步延时 10ms一次呼吸就是 202 × 0.01 2.02 秒。这是非常接近自然呼吸频率的。如果你想要急促一点延时 5ms一次呼吸刚好约 1 秒想要舒缓一点延时 15ms 到 20ms一次呼吸约 3 到 4 秒。PWM 频率 10kHz 在这个方案里已经足够平稳但要注意占空比每改变一次ΔCCR 1 对应 1% 的亮度变化。如果延时太短比如 1ms亮度变化速度快肉眼能看到明显的“一档一档”跳动呼吸感会打折。反过来说如果你把自动重载值改成 199即 200 档调节同样一次呼吸下每档变化只有 0.5%肉眼分辨率下就是绝对平滑代价是 PWM 频率降一半。两种方案我都试过100 档加 10ms 延时已经足够顺滑没必要为了极端效果牺牲频率。4. 点亮过程中的坑与排查方法4.1 常见故障排查速查表这个项目虽然简单但问题往往集中在配置细节上。我记录了自己和一起搞机的朋友踩过的典型问题整理成一张速查表现象可能原因处理办法灯完全不亮没有调用 HAL_TIM_PWM_Start在 while 循环前启动 PWM 通道灯完全不亮PA0 的 GPIO 配置不是复用推挽输出检查初始化里 GPIO 模式是否为 AF_PP灯完全不亮LED 接反或电阻太大确认 LED 正极接 PA0 侧负极接地灯只有常亮不呼吸while 循环里没有修改比较值检查是否每次循环都调用了 SET_COMPARE亮度变化有跳变感步进值太大或延时太短步进保持为 1延时至少 5ms灯开始闪烁PWM 频率太低低于 1kHz检查预分频和重载值按公式重新算下载程序失败SWD 线接触不良或 BOOT0 电平不对检查接线BOOT0 必须拉低进入运行模式编译报找不到 PWM 函数没有包含对应头文件在 main.c 里包含定时器外设头文件这里特别想强调 GPIO 模式的问题。很多人第一次配 PWM会把 PA0 设成普通的推挽输出结果灯要么不亮要么一亮就是长亮。定时器要输出波形引脚必须工作在复用推挽输出模式也就是让片内外设接管引脚控制权。图形化工具通常会自动生成正确的配置但如果你手动建工程很容易在这个环节踩坑。4.2 波形观察与验证方法“灯亮不亮、呼吸顺不顺畅”这种主观判断不够精确我建议有条件的话把手边的逻辑分析仪或者示波器接在 PA0 上看实际波形。PWM 静态测试时波形应该是一个频率 10kHz 的方波高电平占整个周期的比例正好等于你设置的比较值。比如 CCR 设为 50高电平宽度应该占 50%。如果占空比稳定、频率准确说明定时器配置是对的。运行呼吸程序时观察波形的包络线会非常有意思方波本身一直在以 10kHz 高频翻转但它的高电平宽度也就是脉冲宽度会随着时间缓慢地拉宽再收窄这个过程正好复现了你设定的 1-cos 曲线。如果包络线出现明显的折线感说明步进值太大或延时不一致如果包络线有抖动检查是不是中断代码干扰了主循环。没有示波器的话用万用表也能做基本验证。把万用表拨到直流电压档量 PA0 对地电压静态 0% 占空比读数为 0V100% 占空比读数为 3.3V50% 占空比读数应约为 1.65V。动态呼吸时表盘读数会在 0 到 3.3V 之间缓慢摆动摆动周期就是呼吸周期。这个方法虽然粗糙但能帮你确认占空比确实在变。4.3 实测体验什么样的呼吸感才算舒服参数改来改去最后还是要回到人的感受。我实测下来的推荐组合是PWM 10kHz占空比 0 到 100 共 101 档每档延时 10ms一次完整呼吸约 2 秒。这个节奏放在桌面上盯着看会觉得灯特别“平静”不会让人烦躁。有一个细节很多人没注意LED 在超低占空比下可能直接不亮。有些红色 LED 在 1% 到 3% 占空比时电流太小发光微弱到肉眼看不见于是整个呼吸过程会有一段“静止熄灭”的时间看起来像灯先灭着、然后突然跳亮。解决方法是把占空比下限从 0 抬到 5 或 8也就是给一个最小的底亮。这样曲线两端的“缓慢”依然保留但灯始终有点余光呼吸感反而更连续。另外把占空比上限限制在 95% 左右不要冲到 100%可以避免灯在峰值时过于刺眼。表面上你只是改了数值实际上整个呼吸的“收放”更从容视觉上会高级很多。5. 硬件电路与供电注意事项5.1 核心板供电方式的选择大部分 C8T6 核心板上有 USB 接口和 AMS1117 稳压芯片通常允许通过 USB 5V 供电板载稳压到 3.3V。但如果你同时用 SWD 下载器供电就可能出现两个电源互相竞争的问题轻则下载失败重则电流倒灌。我的习惯是下载器只连 SWDIO、SWCLK、GND 三根线不连下载器上的 3.3V 输出目标板用 USB 单独供电。这样两边的地连在一起保证通信电源各自独立非常干净。如果手头没有 USB 线也可以让下载器供电但那时务必保证核心板的 USB 线拔掉绝不能两边同时供。5.2 复位、B0 跳线和下载状态C8T6 核心板上的 BOOT0 跳线默认应该处于低电平位置也就是接地。这个状态决定芯片从 Flash 启动下载完程序之后才能正常跑。如果 BOOT0 被拉高芯片会进入系统存储器启动模式你下载再多次程序跑起来也不是 Flash 里的代码。有段时间我反复下载都成功但一复位程序就回到初始状态排查半天才发现是 BOOT0 跳线被碰掉了。这个坑太隐蔽了新手尤其容易遇到。另外下载失败时不要急着怀疑芯片坏先按一下板上的复位按键重新连接很多时候是调试器握手超时跟芯片本身没任何关系。5.3 引脚电平安全呼吸灯实验只用了 PA0负载电流不到 10mA很安全。但如果你以后在同一个工程里同时点亮多个 LED就要注意 F103 单个 GPIO 引脚的输出能力限制以及整个芯片的总电流预算。C8T6 这种核心板供电能力本来就有限十几个 LED 同时满亮就可能让 3.3V 电压跌落出现奇怪的闪烁甚至复位。做扩展实验时一个稳妥的原则是控制信号归控制信号功率电流请走外部驱动。无论是 LED 灯带还是蜂鸣器涉及电流超过 20mA 的负载都应该用三极管或 MOSFET 做开关而不是直接挂在 GPIO 上。呼吸灯实验虽然简单但这个理念越早建立越好。6. 后续扩展方向和个人体会呼吸灯做完之后不要急着删工程。同一套 PWM 思路可以立刻迁移到好几个好玩的场景。比如把 LED 换成有源蜂鸣器用同样的曲线控制 PWM 占空比就能做出音量从无到有、再徐徐消失的“呼吸音效”把控制对象换成舵机通过缓慢改变脉冲宽度可以让舵机从一个角度平滑摆动到另一个角度过程中没有任何猛拉猛停再把 LED 换成 MOSFET 驱动的大功率灯带呼吸灯直接变成氛围灯放在桌面上相当有感觉。还可以玩得更深一点用定时器的更新中断或者 DMA 自动修改比较寄存器让芯片在 CPU 完全不干预的情况下自己完成一次呼吸。这样主循环彻底解放出来可以同时处理按键扫描、串口通信等任务。你会真正领会到“外设为主、CPU 为辅”的嵌入式设计思路。说说我个人实际做下来的体会吧。呼吸灯这个项目看起来太“基础”了网上随便一搜就是满屏代码但它恰恰是检验一个人对 PWM、定时器、时钟树理解程度最好的试金石。参数表永远比代码本身重要为什么预分频是 71、为什么重载值是 99、为什么延时选 10ms、为什么曲线要用 1-cos 而不是 sin这些问题全想明白了才算是真正会做呼吸灯而不是“会抄呼吸灯”。我有个小习惯每做完一个项目都会把当时的硬件接法、参数计算过程、遇到的坑截图保存到本地笔记里标上日期。过几个月再回头翻很多当时觉得“玄学”的问题已经有了清晰的答案这个积累过程比项目本身值钱得多。希望这篇记录对你有用也希望你能在这种“小实验”里找到属于自己的调试手感。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑