STM32串口控制PWM调光实战:CubeMX配置与代码详解

发布时间:2026/9/1 12:14:24

STM32串口控制PWM调光实战:CubeMX配置与代码详解
简介一套基于STM32F103微控制器的串口接收控制PWM调节LED亮度完整工程源码面向嵌入式初学者与物联网产品开发者解决上位机通过串口下发数据实时改变LED亮度的典型需求。压缩包共131个文件大小仅1.25MB其中108个头文件与14个C源文件构成核心代码另有Keil工程文件、汇编启动文件、库文件、文本说明等结构规整可直接在MDK中打开编译调试。代码覆盖USART串口中断接收与数据解析、定时器PWM输出配置、STemWin图形界面中滑动条控件的创建与事件回调以及底层LCD驱动与GUI_X移植层可清晰看到从接收亮度指令到修改比较寄存器占空比的完整链路。已有5598人次浏览学习工程具备良好模块化拆解适合需要快速搭建串口-PWM控制链路或拓展STemWin GUI的开发者移植复用。1. 串口控制PWM调光STM32这三个外设是如何配合的1.1 项目需求拆解串口、定时器、LED各自扮演什么角色很多人学STM32都是从点灯开始的但点完灯往往不知道下一步该做什么。这个项目——串口接收控制PWM调节LED亮度刚好把点灯这件事往工程方向推进了一步有输入串口命令、有输出PWM波形、中间还夹着协议解析和参数换算。做完这个你会对STM32F103的串口、定时器、GPIO复用这些基础外设有一个整体认识而不是只会在main函数里翻转电平。需求一句话就能说清PC或手机通过USB转TTL给STM32F103发一串文本比如PWM60LED亮度就变成60%。再发PWM25LED变成25%。就这么简单但它背后拆出来有三个独立任务第一串口通信。板子要能收到外部发来的数据并且能解析出“PWM60”里的60。这里涉及串口初始化、中断接收、缓冲区设计、结束符判断任何一个环节处理不好命令就会丢或错。第二定时器产生PWM波形。决定了LED的亮度是通过占空比实现的而不是GPIO高低电平那种“要么全亮要么全灭”的状态。占空比怎么映射到定时器的比较寄存器这里有一个计算过程。第三协议与逻辑联动。收到命令之后怎么判断命令合法、怎么提取数值、怎么把数值换算成定时器比较值最后写入寄存器让LED亮度真正变化。这套流程放到一个完整的工程项目里就是“上位机下发指令→下位机解析响应→执行机构动作”的标准雏形。理解了它后面做电机调速、舵机控制、恒流驱动调光基本就是换个外设、换个参数的事。1.2 硬件选型与接线清单芯片选STM32F103C8T6这是目前最容易买到、资料最多、也最便宜的一款Cortex-M3芯片。48脚封装64KB Flash20KB RAM3个串口3个通用定时器加1个高级定时器做这种小项目绰绰有余。搭配几块钱的“最小系统板”焊排针就能直接插面包板。我这边用的硬件清单如下硬件说明STM32F103C8T6最小系统板蓝色板或类似的都能用注意PA0上是否接了板载按键跳线ST-Link V2SWD接口下载程序四根线SWDIO、SWCLK、GND、3.3VUSB转TTL模块选CH340芯片的比较稳妥装好驱动即可面包板 杜邦线连接最小系统板、LED、USB转TTL模块5mm红色LED红色LED正向压降约1.8~2V用3.3V供电时压差足够容易点亮220Ω~330Ω电阻用作LED限流电阻电流控制在4~10mA左右接线方式也很直接。USB转TTL模块的TXD接单片机PA10USART1_RXRXD接PA9USART1_TXGND必须共地。PA0是TIM2_CH1的PWM输出脚通过限流电阻接LED阳极LED阴极接GND。下载器ST-Link占用SWD口不影响串口使用。这里有个注意点PA0在部分“Blue Pill”类型的最小系统板上会通过跳线连到按键使用前要把相关跳线帽拔掉或避开这个引脚否则外部电路会干扰PWM信号。如果你手上只有白色/蓝色的高亮LED正向压降可能到3.0~3.2V3.3V供电下压差只剩0.1V此时220Ω电阻基本不亮建议换红色LED或者改用三极管驱动。2. PWM调光到底怎么调定时器参数从头算起2.1 PWM调光的物理原理与频率选择PWM的本质是一个固定频率的方波核心参数是占空比也就是高电平时间占整个周期的百分比。LED的亮度由流过它的平均电流决定而平均电流又由方波的平均电压决定。占空比60%灯上平均电压就是高电平电压的60%亮度近似就是最大亮度的60%。这个原理可以类比成快速开关灯如果开关频率足够高眼睛根本来不及分辨每一次明暗交替看到的就是一个“变暗了的灯”。这也是为什么PWM频率不能太低——低于50Hz人眼会明显看到闪烁也就是常说的频闪。工程上做LED调光1kHz到10kHz是比较常用的范围。再高了LED本身响应跟得上、亮度也能正常出来但开关损耗和电磁干扰会上升PCB布线或长线驱动时波形也可能出现振铃。在STM32F103上做LED调光我习惯把PWM频率设在1kHz。这个频率既远超人眼闪烁感知阈值又不会因为频率过高给GPIO和线路带来额外压力同时参数计算时还特别整齐后面你会看到。2.2 用72MHz时钟推算出PSC和ARRSTM32F103的定时器参数计算是整个项目里最需要动脑筋的地方。先看一个关键前提F103的外部晶振通常是8MHz通过PLL倍频到系统主频72MHz。APB1总线最高36MHz但定时器很特殊当APB1预分频设置为2时挂在APB1上的TIM2/TIM3/TIM4会获得一个2倍频的时钟也就是72MHz。CubeMX里配置好时钟树后TIM2的输入时钟就是72MHz。PWM输出频率公式是f_pwm TIM_CLK / ((PSC1) * (ARR1))其中PSC是预分频值ARR是自动重装载值。我想要1kHz输出取PSC71那么(ARR1) 72MHz / 72 / 1kHz 1000 ARR 999这样计数器的计数频率是1MHz也就是1微秒加1计满1000个数需要1毫秒PWM周期正好1kHz。占空比调节的分辨率是1/10000.1%也就是说我可以把LED亮度分成1000个等级对人类眼睛来说已经非常细腻了。这里有个容易混淆的点PSC要减1ARR也要减1。CubeMX里填写的就是71和999而不是72和1000。因为寄存器值从0开始计数PSC71意味着实际分频是72分频ARR999意味着自动重装载值是1000。2.3 串口命令协议为什么从PWM60开始串口本身只负责收发字节它不知道你发的是命令还是乱码。要让单片机识别命令必须定义一套双方都认可的协议。协议设计越简单越不容易出错但也别太简单导致没法扩展。我用的协议是文本格式PWM60\r\nPWM是固定前缀60是0到100的整数\r\n是回车换行结束符。选择这种格式有几个原因第一直接用串口调试助手就能测试不需要把数据转成十六进制再发人眼可读性极强。如果协议用二进制比如0xAA 0x3C 0x0F你不查表根本不知道发了什么内容排查问题很痛苦。第二文本协议天然兼容串口助手的“发送新行”功能。你在串口助手里勾上“发送新行”它会自动在命令末尾追加\r\n单片机收到后就能判断一条命令结束了。第三将来扩展也容易。比如加通道编号变成PWM1:60加亮度渐变时长变成FADE1:60:3000解析逻辑只增加不推翻。为什么命令末尾要有结束符因为串口接收是一个字节一个字节进来的接收方必须知道“数据什么时候算完”。最笨的办法是固定长度比如规定总是发8个字节但那样不方便。更合理的方式是用特殊字符标记结束\r\n是业界最通用的行结束符串口助手、终端工具、单片机代码都认。3. CubeMX初始化到代码落地的完整过程3.1 CubeMX关键配置步骤这个项目我推荐用STM32CubeMX生成初始化代码然后用HAL库写逻辑。CubeMX的图形化配置能把时钟树、引脚复用、外设参数这些容易出错的部分一次性处理好比手工翻参考手册初始化寄存器快得多。在CubeMX里配置步骤可以归纳如下第一步新建工程选择STM32F103C8T6。RCC选项里选择Crystal/Ceramic Resonator启用外部8MHz晶振。SYS里Debug选择Serial Wire这样才能正常使用ST-Link的SWD调试接口。第二步配置时钟树。外部HSE 8MHzPLL倍频到72MHzAPB1预分频为2。配置完成后在Clock Configuration页面确认APB1 Timer Clocks显示为72MHz这个数值直接影响后面的定时器参数。第三步配置TIM2。时钟源选Internal ClockChannel1选择PWM Generation CH1。参数区填写Prescaler71Counter Period999Pulse初始值可以设500对应50%占空比。自动重装载预装载Auto-reload preload建议Enable这样修改比较值时不会在周期中间被打断输出更平滑。输出极性选High。第四步配置USART1。模式选Asynchronous波特率115200字长8位无校验1位停止位。引脚默认PA9/PA10。第五步生成工程代码。Project Manager里选择生成单独的.c/.h文件Toolchain选择MDK-ARM或IDE对应选项。生成后打开工程补充业务逻辑。打开生成的main.c你会看到CubeMX已经把GPIO、TIM2、USART1的初始化代码都生成好了。这时候项目其实已经做完了一大半后面只需要加接收逻辑和业务逻辑。3.2 串口中断接收一字节一中断的设计取舍串口接收有三种方式轮询、中断、DMA。轮询会在主循环里空转等待数据浪费CPUDMA适合接收大块或者不定长数据但要配合空闲中断IDLE使用调试起来对新手不太友好。所以我选“每收一个字节触发一次中断”的方式代码简单、逻辑清晰完全满足命令帧很短的应用场景。在主循环之前先启动接收uint8_t rx_byte 0; uint8_t rx_buf[64]; uint8_t rx_len 0; volatile uint8_t cmd_ready 0; HAL_UART_Receive_IT(huart1, rx_byte, 1);然后重写中断回调函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (rx_byte \r || rx_byte \n) { if (rx_len 0) { rx_buf[rx_len] \0; cmd_ready 1; rx_len 0; } } else { if (rx_len 63) { rx_buf[rx_len] rx_byte; } } HAL_UART_Receive_IT(huart1, rx_byte, 1); } }这段代码里有两个细节特别值得说。第一为什么收到\r或\n都要结束因为不同的串口工具和终端软件发出来的结尾不一样有的发\r\n有的只发\n。既然都兼容就把两种都当作结束符。同时只处理rx_len 0的情况这样\r\n连续到达时\n会被静默忽略不会重复触发命令解析。第二为什么在回调里要再次调用HAL_UART_Receive_ITHAL库的HAL_UART_Receive_IT接收指定字节数这里是1字节后会清除接收状态并触发回调。如果不在回调里重新启动串口就只能收到第一个字节之后再也不进中断了。这个坑几乎每个用HAL的新手都会踩一次。另外中断回调里只做“把数据放进缓冲区、标记命令就绪”这两件事没有做任何耗时的解析操作。解析放到主循环去这是中断编程的基本原则——中断里越短越好。3.3 主循环解析与亮度联动主循环的逻辑很直白等待cmd_ready标志一旦置位就解析缓冲区里的命令然后设置PWM比较值。这里我没有用sscanf因为sscanf和sprintf这类格式化函数会引入不小的Flash开销嵌入式里手写一个轻量解析函数更合适。uint8_t parse_duty_value(const char *buf, uint8_t *duty) { if (buf[0] ! P || buf[1] ! W || buf[2] ! M || buf[3] ! ) { return 0; } uint8_t value 0; uint8_t i 4; while (buf[i] 0 buf[i] 9) { value value * 10 (buf[i] - 0); if (value 100) { return 0; } i; } if (i 4 || buf[i] ! \0) { return 0; } *duty value; return 1; }这个解析函数的要求很严格必须完整匹配PWMxxx格式数字长度至少一位且整条命令必须到字符串结束才合法。如果发PWM50abc会被判定非法避免协议歧义。主循环里这样处理while (1) { if (cmd_ready) { uint8_t duty 0; if (parse_duty_value((char*)rx_buf, duty)) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, (uint16_t)duty * 10); HAL_UART_Transmit(huart1, (uint8_t*)OK\r\n, 4, 100); } else p a hrefhttps://download.csdn.net/download/fuyanhuangyan/9850565 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

相关新闻

速腾聚创感知算法岗笔试复盘:点云与激光雷达底层原理全解析

速腾聚创感知算法岗笔试复盘:点云与激光雷达底层原理全解析

2026/9/1 12:14:24

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

淘天数据岗笔试复盘:SQL、统计与业务分析备考指南

淘天数据岗笔试复盘:SQL、统计与业务分析备考指南

2026/9/1 12:14:24

2024年春招,我前后投了不少大厂的数据岗,淘天集团的笔试算是印象最深的一场。倒不是它最难,而是它的考察面特别全,题量也大,一场笔试下来基本能把数据岗的核心技能树扫一遍。我大概是在3月中旬收到的笔试通知&#xff…

BMS放电MOS管驱动电路设计:开关速度平衡与实战调试

BMS放电MOS管驱动电路设计:开关速度平衡与实战调试

2026/9/1 12:04:23

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

迅为Topeet Toolbox:图形化工具简化嵌入式开发板系统配置与烧录

迅为Topeet Toolbox:图形化工具简化嵌入式开发板系统配置与烧录

2026/9/1 13:04:26

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2023前端面试八股全解析:高频考点与底层原理精讲

2023前端面试八股全解析:高频考点与底层原理精讲

2026/9/1 13:04:26

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

连续投影算法(SPA)详解:光谱变量筛选原理与Python实战

连续投影算法(SPA)详解:光谱变量筛选原理与Python实战

2026/9/1 13:04:26

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

EBR-bench:以人类基线为锚,审视AI评测的真实差距

EBR-bench:以人类基线为锚,审视AI评测的真实差距

2026/9/1 13:04:26

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

智能体编程时代:从低代码到代码的软件工程技能图谱

智能体编程时代:从低代码到代码的软件工程技能图谱

2026/9/1 13:04:26

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

用技术验收思维拆解DD 5e跑团项目:从博德之门到阿弗纳斯

用技术验收思维拆解DD 5e跑团项目:从博德之门到阿弗纳斯

2026/9/1 12:54:26

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

2026/9/1 1:53:39

每年校招季我都会接触不少准备数据库方向笔试的同学,看到最多的状态就是:简历上写着“熟悉 MySQL”“了解索引优化”,一碰到数据库管理工程师的笔试卷,却在索引、事务、锁、备份恢复这些题目上翻车。网易这套 2018 校园招聘数据库…

数字电路时序基石:深入理解建立时间与保持时间

数字电路时序基石:深入理解建立时间与保持时间

2026/9/1 9:55:14

1. 这不是“背公式”的事:时间参数到底在约束什么你翻过数字电路教材,一定见过这两个词:建立时间(Setup Time)和保持时间(Hold Time)。它们常被并列写在触发器(Flip-Flop&#xff09…

蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

2026/8/31 17:18:46

1. 项目缘起:从赛题到超声波测距机的诞生第八届蓝桥杯单片机设计与开发国赛的题目,我至今记忆犹新。它没有直接给出一个花哨的名字,而是用“超声波测距机”这个朴实无华的功能描述,精准地勾勒出了考核的核心。对于当时备赛的我而言…

远程协作的工作台整理

远程协作的工作台整理

2026/9/1 0:03:36

远程协作的工作台整理远程协作的核心不是再加一个工具,而是让交接信息足够完整。异步任务要写明目标、输入位置、完成标准和需要决策的人。 工作台的最小配置 将日程、待办、代码和沟通入口收拢到少数固定位置;通知按紧急程度分层。工作台不需要模仿办公…

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

2026/9/1 0:03:36

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

2026/9/1 0:03:36

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

远程协作的工作台整理

远程协作的工作台整理

2026/9/1 0:03:36

远程协作的工作台整理远程协作的核心不是再加一个工具,而是让交接信息足够完整。异步任务要写明目标、输入位置、完成标准和需要决策的人。 工作台的最小配置 将日程、待办、代码和沟通入口收拢到少数固定位置;通知按紧急程度分层。工作台不需要模仿办公…

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

2026/9/1 0:03:36

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

2026/9/1 0:03:36

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…