发布时间:2026/9/1 7:11:07
STM32F103串口控制PWM调节LED亮度:从原理到实战 简介这套STM32F103串口接收控制PWM调光的完整工程面向嵌入式初学者与进阶开发者解决通过串口指令实时调节LED亮度的问题并集成STemWin轻量级GUI便于在上位机或触摸屏上直观控制。压缩包共131个文件以C源码和H头文件为主包含USART、PWM、LCD驱动等模块化程序另有工程配置文件、链接脚本及清理脚本整体仅1.25MB结构清晰可直接导入Keil工程编译学习。已有5598人学习下载。资源完整覆盖USART中断接收与数据解析、定时器PWM占空比映射、STemWin界面资源及滑动条事件回调等实现细节读者可对照源码理解外设初始化顺序、寄存器配置和主循环调度方式掌握串口通信、PWM调光与GUI联动开发的整体流程既能用于课程设计也能为智能照明和物联网终端的原型验证提供参考。 很多朋友做完流水灯之后下一站多半会卡在同一个问题上我已经能让 LED 按顺序亮了可怎么让电脑这边的指令去操控硬件这个问题的标准答案就是今天要聊的项目STM32F103 通过串口接收数据再用 PWM 输出调节 LED 亮度。别小看这个 DEMO它背后把 GPIO 复用、定时器 PWM、串口中断、协议解析这四块嵌入式开发里出场率最高的内容串到了一起而且全程只需要一块 STM32F103 最小系统板、一个 USB 转串口模块和一个 LED投入极低收益却很高。1. 为什么拿这个项目练手一条指令穿起四大外设1.1 从“单向输出”到“输入-处理-输出”的跨越流水灯其实是“单向输出”CPU 只管按顺序改 GPIO 寄存器的值走完就结束了整个过程没有外界的反馈和干预。而串口控制 LED 这件事是完整的“输入处理输出”链路数据从 PC 端发出经过电平转换进到 MCU 的串口外设CPU 在中断里拿到字节解析出含义最后转化成定时器 PWM 的占空比LED 的亮度随之改变。这一步看起来简单但它的本质已经和真实产品里“远程控制一台设备”的逻辑完全一致了只不过把执行机构从大电机、屏幕、继电器简化成一颗 LED。很多人学完库函数、看完手册依然写不出一个像样的项目缺的就是这种把离散知识点串成链路的能力。1.2 数据流拆解从串口字节到 LED 亮度的完整路径我习惯先画数据流再写代码。这条链路的每一段都对应一个可以单独调试的模块PC 串口助手 → USB 转 TTL 模块CH340→ STM32F103 的 USART1_RXPA10→ 串口中断接收一个字节 → 协议解析得到目标占空比 → 通过 TIM2_CH1PA0输出 PWM → 限流电阻 → LED拆开以后你会看到每一段都可以单独验证。串口有没有数据进来用调试助手看回显PWM 波形对不对用万用表量 PA0 的平均电压或者用示波器直接看占空比LED 亮不亮肉眼就能判断。这样做的好处是万一出了故障不需要对着整个工程从头猜数据走到哪一段断了问题就出在哪一段。2. 硬件准备与接线一半的坑都发生在这里2.1 选型清单与驱动注意点硬件清单非常平民化STM32F103C8T6 最小系统板也就是常说的蓝丸或者黑丸。USB 转 TTL 模块最常用的是 CH340驱动去芯片厂商页面下载对应版本装完以后在设备管理器里确认串口号比如 COM3。LED 一个330Ω 左右电阻一个面包板加杜邦线若干。BOOT0 跳线帽拉到 GND让芯片从 Flash 启动。接线表我直接列出来照着插就行模块引脚STM32F103 引脚说明USB-TTL 的 TXDPA10USART1_RX交叉连接发送对接收USB-TTL 的 RXDPA9USART1_TX交叉连接接收对发送USB-TTL 的 GNDGND必须共地USB-TTL 的 3.3V3.3V可选给板子供电PA0TIM2_CH1通过电阻接 LED 正极LED 负极接 GND这里有一个常见误区TXD 接 PA10、RXD 接 PA9是交叉接不是直连。很多人第一次插反了发现串口助手发数据完全没反应还以为代码写错了。另外GND 一定要共地USB 转 TTL 和 STM32 板子各供各的电时地线不连上电平根本没有参考点通信必然失败。2.2 限流电阻的计算逻辑LED 不能直接跨在 PA0 和 GND 之间否则电流会超出引脚和 LED 的承受范围。红色 LED 的正向压降大约 2VPA0 在推挽输出模式下高电平是 3.3V那么限流电阻上的压降就是 3.3 - 2 1.3V。如果想让电流在 10mA 左右电阻值就是 1.3V / 0.01A 130Ω。实际用 220Ω 或 330Ω 都行电流小一点亮度依然可见还能省电、减小发热。我在调试时常用 330Ω因为哪怕占空比拉到 100%电流也不到 10mA对板载 LDO 的压力很小。顺带提醒一句这个项目里所有电路都用 3.3V 这一侧别把 LED 直接接到 USB 的 5V 上引脚电平匹配和器件耐压都要留余量。3. 时钟、串口、定时器CubeMX 里最容易忽略的三个设置3.1 时钟树为什么你算出来的 PWM 频率总不对打开 STM32CubeMX选好芯片型号之后第一件事是配置时钟树。如果板子上有 8MHz 外部晶振就用 HSE 作为时钟源把 SYSCLK 拉到 72MHz。如果没有外部晶振或者你想省电、省元件也可以把时钟源切到 HSI 内部 8MHz 振荡器同样能跑到 72MHz只是精度略差。这里有个绝大多数新手都踩过的坑F103 的定时器时钟并不直接等于 APB1 总线时钟。默认配置下 APB1 是 36MHz但定时器时钟会在此基础上自动乘以 2变成 72MHz。如果你拿着 APB1 的 36MHz 去算 PWM 频率算出来的结果会差一倍配出来的实际频率和你预期的完全对不上。PWM 频率的计算公式是F 定时器时钟 / (PSC 1) / (ARR 1)我把 TIM2 的预分频 PSC 设为 71自动重装值 ARR 设为 999那么算出来就是 72MHz / 72 / 1000 1kHz。分辨率就是 1/1000也就是占空比最小步进是 0.1%。LED 亮度控制用 1kHz 足够了人眼在这个频率下感受不到闪烁再低一点到两三百赫兹LED 就会明显闪这也是很多自己搭 PWM 的项目看起来“灯在闪”的原因。3.2 串口和定时器的参数配置CubeMX 里的配置项其实就几处RCCHSE 选 Crystal/Ceramic Resonator。SYSDebug 选 Serial Wire保证板载 ST-Link 还能下载调试。USART1模式选 Asynchronous波特率 115200数据位 8停止位 1无校验打开全局中断。TIM2Clock Source 选 Internal ClockChannel1 选 PWM Generation CH1PSC 写 71ARR 写 999初始 Pulse 写 0。PA0 和 PA9、PA10 的 GPIO 模式CubeMX 会自动根据外设配置设置好不需要手动改。这里再解释一下为什么用 TIM2 的通道 1。TIM2_CH1 的默认复用引脚是 PA0和串口用的 PA9、PA10 不冲突接线方便而且 TIM2 是 16 位定时器ARR 最大可以到 65535对于 LED 亮度控制来说分辨率空间非常充足。用 TIM1 的 PA8 也可以但对新手来说 TIM2 的默认映射更省心。4. 串口协议设计先想清楚“一字节”还是“带帧的报文”4.1 入门方案一字节直接映射占空比最直接的思路是串口每收到一个字节 0x00 ~ 0xFF直接把它作为占空比数值写入定时器的比较寄存器。如果 ARR 是 255那么发送 0 就是全灭发送 255 就是全亮中间值就是对应的灰度。代码量极小十几行就能跑通。这个方案的缺点是显而易见的没有帧边界也没有校验。万一数据在传输过程中被干扰或者串口助手设置了“发送新行”末尾多出 0x0D 0x0A那 LED 就会莫名其妙地跳到一个奇怪亮度。但作为第一次跑通整个链路我强烈建议先用这种方式做验证目的不是做产品而是确认“串口→中断→PWM→LED”这条路上没有硬件问题。4.2 实用方案带帧头、数据和校验的报文链路通了以后就该升级协议了。我常用的格式很简单可读性好串口助手调试也方便格式$PWM:xxx\r\n其中 xxx 是 0~100 的十进制数字表示百分比亮度。$PWM是帧头:后面跟数据\r\n是帧结束符。这样设计有几个考虑帧头让解析程序能识别“一条指令从哪开始”结束符让程序知道“一条指令在哪里结束”中间的数据段固定是 0~100 的整数方便转换成占空比。如果将来要上更复杂的系统比如同时控制多个参数再把协议扩展为$PWM:1,128;RGB:2,64,128,200\r\n这类结构化报文也不难。核心思想是一样的帧头定位、数据解析、结束符收尾。至于校验简单场景可以不放在协议里但如果你用无线模块或者在工业环境调试建议加一个累加和校验字节把所有数据字节累加取低 8 位放在帧尾接收端做同样的累加比对。这样哪怕偶尔串进一个干扰字节也能立刻知道这一帧是错的直接丢弃而不是让设备执行一个错误指令。5. 核心代码实现中断接收、解析状态机与 PWM 输出5.1 先让 PWM 跑起来无论用哪种协议第一步都是先启动 PWM并设置一个初始占空比。HAL 库的写法是HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 0);第一行启动 TIM2 通道 1 的 PWM 输出第二行把比较值设为 0也就是初始状态 LED 全灭。注意__HAL_TIM_SET_COMPARE是一个宏直接操作定时器的 CCR 寄存器第二个参数的范围必须在 0 到 ARR 之间。我之前的 ARR 是 999所以这个值合法范围就是 0~999。对应到标准库就是TIM_SetCompare1(TIM2, 0);逻辑是一样的本质上都是往 CCR1 寄存器写值。5.2 串口中断接收与回调HAL 库的串口接收用起来很顺手核心就两步先声明一个全局变量在主函数开始处调用uint8_t rx_byte 0; HAL_UART_Receive_IT(huart1, rx_byte, 1);这句的意思是让 USART1 在中断模式下接收 1 个字节收到以后存到 rx_byte 里然后自动调用回调函数。回调函数里写处理逻辑void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { parse_char(rx_byte); HAL_UART_Receive_IT(huart1, rx_byte, 1); } }这里有一个新手容易漏掉的细节HAL_UART_Receive_IT是一次性的收完一个字节以后中断配置就失效了所以必须在回调里再次调用它让串口继续等待下一字节。忘了这一步现象就是“第一次发数据有反应之后再发就没反应了”。5.3 解析状态机把字节流变成亮度指令如果采用一字节方案回调里直接写寄存器就行__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, rx_byte * 999 / 255);注意这里做了一个换算。串口发来的 0xFF255对应占空比 100%而 ARR 是 999所以要把 255 映射到 999 的范围。不做换算的话串口发 255PWM 比较值只有 255亮度只到 25.5%看起来就是“调到最大也不够亮”。如果采用$PWM:xxx\r\n这种帧格式我写了一个简洁的状态机typedef enum { ST_IDLE, ST_HEAD1, ST_HEAD2, ST_DATA } ParseState; ParseState state ST_IDLE; char buf[4]; uint8_t idx 0; void apply_duty(uint8_t percent) { uint16_t compare (uint16_t)(percent * 999 / 100); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, compare); } void parse_char(uint8_t c) { switch (state) { case ST_IDLE: if (c $) state ST_HEAD1; break; case ST_HEAD1: state (c P) ? ST_HEAD2 : ST_IDLE; break; case ST_HEAD2: if (c W) { idx 0; state ST_DATA; } else state ST_IDLE; break; case ST_DATA: if (c 0 c 9) { if (idx 3) buf[idx] c; } else if (c \r || c \n) { buf[idx] \0; uint8_t percent (uint8_t)atoi(buf); if (percent 100) percent 100; apply_duty(percent); idx 0; state ST_IDLE; } else { idx 0; state ST_IDLE; } break; } }状态机的逻辑并不复杂它有点像流水线每个字节进来先判断当前处于哪个状态再决定是跳转到下一个状态还是回到空闲态。$PWM三个字节必须连续正确才能进入数据接收状态。在数据状态里数字字符被逐个存进缓冲区最多存 3 位遇到回车或换行就解析数据。有一点要提醒atoi在中断回调里用虽然这个项目规模小问题不大但在正式产品里中断里尽量少做耗时操作更好的做法是先把原始字节存进环形缓冲区在主循环里做解析。如果你以后要处理高波特率、大量数据一定要尽早养成这个习惯。6. 实测中的典型故障与完整排查链路6.1 常见现象、原因和处理方式我把实际调试中遇到过的几个典型问题整理成一张表方便你对照排查现象常见原因排查方向串口助手显示乱码波特率不一致或时钟树配置错误检查 PC 端波特率是否 115200检查 CubeMX 时钟树发数据 LED 完全没反应TX/RX 没交叉、GND 没共地、串口助手发送的不是 Hex先用万用表测 PA10 电平再检查接线串口发 255 但亮度不够占空比数值没有映射到 ARR 范围检查比较值是否做了 0~255 到 0~999 的换算第一次收数据有反应之后失效回调里没有重新调用HAL_UART_Receive_IT检查回调函数是否递归调用了接收接口接收一段时间后任务卡死串口溢出错误 ORE 标志未清除在 ErrorCallback 里清标志并重新开启接收6.2 一个完整的真实排查过程有一次我在调试这个项目用串口助手发送“255”LED 没反应。我先没看代码而是用万用表量 PA0 的电压发现一直是 0V说明 PWM 输出就没起来。然后我回头查初始化发现 TIM2 的时钟源在 CubeMX 里没勾选 Internal Clock导致定时器压根没跑PWM 自然不会有输出。还有一次我串口助手下发$PWM:50\r\nLED 亮度纹丝不动但用$PWM:50就正常。问题出在自动发送新行的选项上串口助手发送时默认勾选了“发送新行”每次都多发一个\n。我的状态机里遇到\r就解析执行\n被当成了新的空闲数据逻辑上没问题但我在数据缓冲区里没有正确处理\r\n连发的情况。后来我把结束符条件改成同时兼容\r和\n问题就解决了。这个经验说明串口调试助手的设置项本身就是调试内容的一部分不要忽略。6.3 稳定性相关的几件小事串口溢出标志一定要处理。HAL 库在接收速度过快、中断处理不过来时会置上 ORE 错误标志如果不处理会导致后续接收全部失效。我一般会在错误回调里做清理void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { __HAL_UART_CLEAR_OREFLAG(huart1); HAL_UART_Receive_IT(huart1, rx_byte, 1); } }电源要稳。STM32F103 最小系统板上的 3.3V LDO 电流输出能力有限如果项目里同时驱动多个 LED 或者舵机、传感器最好用外部稳压供电并且和 USB 转 TTL 模块共地。串口助手的“发送新行”选项要心里有数。调试协议时建议先关掉这个选项确保你发出去的字节完全是你想发的字节等协议调试稳定了再考虑收尾符的处理。写在最后我个人的几点习惯这类项目我做过很多次现在每次拿到一个新的 MCU 开发板第一件事就是先跑通一个类似“串口控制 PWM”的最小链路。因为它能快速验证三件事开发环境好不好用、串口驱动有没有问题、定时器外设是否正常。链路通了后续加传感器、加屏幕、加控制算法都有了可靠的基础。调试过程中最大的心得是别一上来就在中断里堆业务逻辑。先用最简单的一字节方案把链路跑通再用帧协议做结构化控制最后再考虑加校验、加缓存。每一步只引入一个变量出了故障才能快速定位。这个习惯帮我省下了大量排查时间也推荐给你试试。本文还有配套的精品资源点击获取

相关新闻

2026/9/1 7:11:07

利用空闲中断实现串口不定长接收与printf重定向

简介:面向嵌入式开发者的USART串口通信实现方案,基于普冉PY32搭配STM32等MCU均可参照,重点解决不固定长度数据接收与printf发送重定向两个实用需求,适用于远程调试、设备监控与数据传输等场景。资源包为RAR压缩格式,共…

2026/9/1 7:06:07

五款主流安全认证芯片盘点

物联网设备身份认证落地,多半绕不开一颗安全认证芯片。这篇盘点五款有代表性的产品,覆盖国际大厂和国产方案,供选型参考。以下信息整理自各厂商公开资料。Microchip ATECC608B:生态最成熟的老牌选手可能是全球出货量最大的物联网安…

2026/9/1 7:26:07

STM32F407接MPU6050实战:I2C配置与DMP欧拉角输出

简介:这款基于 STM32F407 与 MPU6050 的六轴姿态解算工程包,面向嵌入式入门的开发者,围绕“陀螺仪加速度计数据采集、I2C 通信、互补滤波融合、欧拉角串口输出”这一完整链路展开,可直接借鉴到平衡车、机器人姿态参考等场景。压缩…

2026/9/1 7:26:07

Kotlin中级——Flow

Flows Flows 代表一串随着异步产生的值 Emitter:产生值中间操作:从流中消耗值,对其操作,并返回另一个流Collector:从流中消耗值 suspend fun main() {// The emitter produces valuesflowOf(0x4B, 0x6F, 0x74, 0x6C, 0…

2026/9/1 7:26:07

车载音响系统深度解析:从23扬声器架构到沉浸式声场调校

最近在车载音响系统升级项目中,遇到了一个典型需求:如何在有限的车内空间里,实现媲美高端家庭影院的沉浸式环绕声体验?这不仅仅是堆砌扬声器数量,更涉及到声学设计、功放匹配、音源处理和软件调校的系统工程。本文将围…

2026/9/1 7:26:07

机器人运动控制学习6——状态进阶

上篇文章学习总结:Base Position 是机器人主体在某个世界/参考坐标系中的位置。Base Velocity 积分可以得到 Base Position,但误差也会被一起积累。Odometry 更关注“从起点到现在移动了多少”,本质上主要是相对运动估计。Leg Odometry 可以利…

2026/9/1 7:26:07

【2014-12-23】【转】 C语言字节对齐问题详解

[历史归档] 本文原发布于 cstriker1407.info 个人博客,内容为历史存档,仅供参考。 发布时间: 2014-12-23 | 标题:【转】 C语言字节对齐问题详解 | 分类: 编程 / C && C 【转】 C语言…

2026/9/1 7:21:07

办公室扩展系统上线前,阿尔法测试为何是必做的质量关卡

办公室扩展系统上线前,为什么一定要先做一轮阿尔法测试?很多开发团队遇到过这种情况:功能开发完、联调也过了、演示给领导看也没问题,结果一到真实办公环境里试用,各种问题就冒出来了。排班错乱、权限越级、消息漏发、…

2026/8/31 1:05:20

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/8/31 2:14:20

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/1 7:04:43

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/1 0:00:42

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/1 0:00:42

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/1 0:00:42

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

2026/9/1 0:00:42

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行&#xff0c;Type-C接口算是典型的“看着简单&#xff0c;做起来全坑”的东西。光引脚就24个&#xff0c;高低速信号、电源、控制线全部塞在一个小小的连接器里&#xff0c;如果PCB布局不做规划&#xff0c;打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/1 0:00:42

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/1 0:00:42

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…