STM32串口动态调整PWM频率占空比:电机控制与LED调光实战
1. 项目概述一个动态可调的PWM信号发生器在嵌入式开发尤其是电机控制、LED调光、舵机驱动这些场景里PWM脉冲宽度调制信号是再基础不过的“硬通货”。但很多时候我们需要的不是一个固定不变的PWM而是一个能根据外部指令实时调整频率和占空比的动态信号源。比如你想通过上位机软件实时调整电机的转速和扭矩或者通过手机APP远程改变LED灯的亮度这就需要MCU能接收指令并立刻改变PWM的输出特性。这个项目要做的就是基于STM32打造一个这样的动态PWM信号发生器。它的核心逻辑非常清晰STM32的定时器负责产生高精度的PWM波形而串口则扮演“遥控器”的角色。我们通过串口调试助手发送简单的指令比如“F1000 D50”MCU收到后就能立刻将PWM频率设置为1000Hz占空比调整为50%。这相当于给你的STM32项目装上了一双“外部可调”的手无论是前期调试参数还是后期实现复杂的人机交互控制都极其方便。无论你是刚接触STM32的新手想深入理解定时器和串口还是正在做智能硬件需要灵活控制接口的老手这个项目都能提供一套可直接“抄作业”的稳定方案。2. 核心硬件与软件环境搭建2.1 硬件选型与电路连接要点硬件是项目的基石选择不当会为后续调试埋下无数深坑。对于这个项目硬件核心就三样STM32最小系统板、一个用于观察波形的示波器或逻辑分析仪、以及连接电脑的串口模块。1. STM32型号选择我强烈推荐从STM32F103C8T6俗称“蓝桥杯”或“最小系统板”开始。它价格低廉资源丰富拥有多个高级定时器TIM1和通用定时器TIM2、TIM3、TIM4完全满足PWM输出需求。而且社区资料极其丰富遇到问题几乎都能找到答案。当然如果你手头是F4、H7甚至G0系列原理完全相通只是寄存器名称或库函数稍有差异。2. PWM输出引脚确认不是所有GPIO都能直接输出PWM。必须查阅你所使用型号的《数据手册》中的“复用功能映射”章节。以STM32F103C8T6的TIM3通道1为例它可能映射到PA6或PB4。你需要根据你的板子布局和走线方便程度来选择。一个关键细节确保该引脚没有被其他功能如JTAG/SWD下载口默认占用。例如PA15JTDI、PB3JTDO等引脚在上电后默认是调试功能需要先禁用JTAG才能作为普通GPIO使用。3. 串口连接方案STM32的USART1PA9-TX, PA10-RX通常最常用。连接电脑有两种主流方式使用板载的USB转串口芯片如CH340、CP2102、FT232。这是最方便的方案用一根USB线就能完成供电和通信。外接USB转TTL模块如果你的核心板没有集成就需要一个单独的USB转TTL模块常用CH340或PL2303芯片。接线时必须牢记TX接RXRX接TXGND接GND。VCC一般不需要连接除非你的模块需要从MCU取电。注意烧录程序用的ST-Link的SWD接口SWDIO SWCLK和串口通信接口USART1_TX/RX是两套完全独立的物理线路不要混淆。程序是通过SWD口下载到芯片里的而下载好的程序运行时是通过USART口与电脑通信的。2.2 软件开发环境与关键库配置软件环境我首选Keil MDKKeil uVision 5因为它对ARM Cortex-M系列的支持最成熟、最稳定。当然如果你喜欢VSCodePlatformIO的清爽或者想用STM32CubeIDE也完全可以底层逻辑是一致的。1. 工程创建与固件库新手我建议直接使用STM32标准外设库Standard Peripheral Library或HAL库来创建工程。标准库更贴近寄存器有助于理解本质HAL库抽象程度高开发速度快。这里以标准库为例你需要手动或利用工具添加对应的库文件stm32f10x_gpio.c,stm32f10x_tim.c,stm32f10x_usart.c等到工程并正确配置头文件包含路径。2. 时钟树配置——一切精度的源头PWM的频率和串口的波特率其精度都根植于系统时钟。STM32的时钟树略显复杂但我们必须搞清楚。以72MHz系统时钟SYSCLK为例定时器时钟通用定时器TIM2-4的时钟通常来源于APB1总线。APB1的预分频器默认是2所以APB1时钟是36MHz。但定时器连接到APB1时如果预分频系数≠1则定时器时钟会倍频x2因此TIM2-4的时钟CK_INT通常是72MHz。这个频率直接决定了我们计算重装载值ARR和预分频值PSC的基准。串口时钟USART1挂在APB2总线下72MHzUSART2/3挂在APB1下36MHz。计算波特率时要用对应的总线时钟。在代码初始化时我们通常调用SystemInit()函数它会根据system_stm32f10x.c中的宏定义将系统时钟配置为72MHz。这是后续所有计算的前提。3. 串口调试助手的选择电脑端推荐使用功能清晰、稳定的串口助手如SSCOM、XCOM或AccessPort。关键是要设置对参数波特率与代码中配置一致如115200、数据位8、停止位1、无校验位。发送格式可以选择“字符串”或“十六进制”本项目我们用字符串指令所以选“字符串”即可。3. PWM生成原理与定时器深度配置3.1 PWM的本质与关键参数解析PWM不是什么神秘信号你可以把它想象成一个高速开关的水龙头。这个水龙头只有“全开”和“全关”两种状态。占空比就是在一个开关周期内“全开”时间所占的比例。比如50%占空比意味着半个周期开半个周期关。频率就是这个“开-关-开-关”动作每秒钟重复的次数。1KHz就是每秒开关1000次。在STM32的定时器里实现PWM输出主要依靠三个核心寄存器自动重装载寄存器ARR它决定了计数器的上限。计数器从0开始累加加到ARR值后产生一个更新事件然后清零重新开始。这个“从0到ARR”的时间就是一个PWM的周期Period。预分频器寄存器PSC它用来对定时器的输入时钟CK_INT进行分频。如果输入时钟是72MHzPSC设置为71即72-1那么分频后的计数器时钟频率就是72MHz / (711) 1MHz。PSC决定了计数器累加的“步长”它和ARR共同决定了PWM的频率。捕获/比较寄存器CCRx这是我们控制占空比的关键。定时器会不断将计数器的当前值CNT与CCRx的值进行比较。当CNT CCRx时输出一种电平通常为高当CNT ≥ CCRx且小于ARR时输出另一种电平通常为低。因此CCRx的值相对于ARR的比例就决定了占空比。例如ARR999CCRx300则占空比为 (3001) / (9991) 30.1%。这里1是因为计数器从0开始计数。PWM频率的计算公式为Fpwm Ftimer / [(ARR 1) * (PSC 1)]其中Ftimer是定时器的输入时钟频率如72MHz。3.2 定时器初始化代码与参数计算实战理解了原理我们来看代码。以下是以TIM3的通道1假设映射到PA6输出PWM的初始化步骤// 1. 开启时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 开启TIM3时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟 // 2. 初始化GPIO为复用推挽输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; // PA6 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 初始化定时器时基单元 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 999; // ARR值自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC值预分频系数 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分频与采样相关通常不改 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 4. 初始化PWM输出模式通道1 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; // 使能输出 TIM_OCInitStructure.TIM_Pulse 500; // CCR1值初始占空比脉冲宽度对应50%占空比500/1000 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 输出极性高电平有效 TIM_OC1Init(TIM3, TIM_OCInitStructure); // 初始化通道1 TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能预装载寄存器 // 5. 使能定时器的预装载寄存器ARR和计数器 TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能ARR预装载 TIM_Cmd(TIM3, ENABLE); // 使能定时器开始计数参数计算实例假设我们需要一个1KHz周期1ms、占空比50%的PWM定时器时钟为72MHz。先确定计数周期我们希望1ms计数N次。计数器时钟频率需要是1ms / N的倒数。为了计算方便我们先设定目标计数器频率为1MHz即每个计数周期1us。那么PSC 72MHz / 1MHz - 1 71。1MHz的时钟下1ms需要计数1000次。因为计数器从0开始所以ARR 1000 - 1 999。验证频率Fpwm 72MHz / [(9991)*(711)] 72MHz / (1000*72) 1000Hz正确。50%占空比则CCR1 ARR * 50% 999 * 0.5 ≈ 500。实操心得在调试时如果完全看不到波形首先检查GPIO模式是否正确。GPIO_Mode_AF_PP复用推挽输出是必须的。如果模式是GPIO_Mode_Out_PP通用推挽输出即使定时器配置正确引脚也不会受定时器控制。4. 串口通信协议设计与数据解析4.1 串口初始化与中断配置串口是我们与外界对话的窗口。我们采用中断方式接收数据这样MCU就不需要一直死等串口数据可以同时处理其他任务。// 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. 初始化GPIOPA9-TX复用推挽输出PA10-RX浮空输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 初始化USART参数 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate 115200; // 波特率 USART_InitStructure.USART_WordLength USART_WordLength_8b; // 8位数据 USART_InitStructure.USART_StopBits USART_StopBits_1; // 1位停止位 USART_InitStructure.USART_Parity USART_Parity_No; // 无校验 USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; // 无硬件流控 USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; // 收发模式 USART_Init(USART1, USART_InitStructure); // 4. 配置串口接收中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能“接收寄存器非空”中断 // 5. 配置NVIC嵌套向量中断控制器 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 6. 使能串口 USART_Cmd(USART1, ENABLE);4.2 自定义简单通信协议与解析逻辑为了让指令清晰可辨我们设计一个简单的文本协议。例如F1000设置频率为1000Hz。D50设置占空比为50%。F500 D25同时设置频率500Hz占空比25%。在串口中断服务函数中我们需要解析这些指令#define RX_BUF_SIZE 64 char uart_rx_buf[RX_BUF_SIZE]; uint8_t uart_rx_index 0; uint8_t uart_rx_flag 0; // 接收完成标志 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { char received_char USART_ReceiveData(USART1); // 读取一个字节 // 简单判断以回车换行(\r\n)作为一帧数据的结束 if(received_char \n || received_char \r) { if(uart_rx_index 0) { // 防止空帧 uart_rx_buf[uart_rx_index] \0; // 添加字符串结束符 uart_rx_flag 1; // 设置标志位通知主循环处理 uart_rx_index 0; } } else { if(uart_rx_index (RX_BUF_SIZE - 1)) { uart_rx_buf[uart_rx_index] received_char; } else { // 缓冲区溢出清空缓冲区 uart_rx_index 0; } } } }在主循环中我们检查uart_rx_flag然后解析缓冲区内的字符串void parse_and_execute_command(char* cmd) { char* token; token strtok(cmd, ); // 用空格分割指令 while(token ! NULL) { if(token[0] F || token[0] f) { // 解析频率值 uint32_t freq atoi(token[1]); // 跳过首字符F转换后面的数字 set_pwm_frequency(freq); // 调用设置频率的函数 printf(Set Freq to: %lu Hz\r\n, freq); } else if(token[0] D || token[0] d) { // 解析占空比值 uint8_t duty atoi(token[1]); // 跳过首字符D set_pwm_duty_cycle(duty); // 调用设置占空比的函数 printf(Set Duty to: %u %%\r\n, duty); } token strtok(NULL, ); // 继续分割下一个参数 } }注意事项atoi函数在遇到非数字字符时会停止转换。如果用户输入了F1000a它仍然会转换出1000。对于工业级应用需要更健壮的解析比如检查整个字符串是否都是数字。此外指令间最好有明确的分隔符如空格或逗号并做好缓冲区溢出保护。5. 动态调整PWM参数的实现与优化5.1 实时更新ARR与CCR寄存器的策略收到新的频率或占空比指令后我们需要动态修改定时器的ARR和CCRx寄存器。这里有一个关键陷阱直接修改这些寄存器可能会在不当的时机比如计数器正在从ARR向0翻转时导致输出波形出现毛刺或短时间的异常。安全的更新策略是使用定时器的“预装载寄存器”功能。我们之前初始化时已经通过TIM_ARRPreloadConfig和TIM_OCxPreloadConfig使能了预装载。这意味着我们修改的TIMx-ARR或TIMx-CCRx值并不会立即生效而是先写入一个“影子寄存器”。等到当前计数周期结束产生下一次“更新事件”Update EventUEV时影子寄存器的值才会被真正加载到工作寄存器中。这样就保证了在一个完整的PWM周期内参数是稳定的。void set_pwm_frequency(uint32_t freq_hz) { if(freq_hz 0 || freq_hz 100000) return; // 简单的参数范围检查 // 假设定时器时钟为72MHzPSC固定为71得到1MHz计数器时钟 uint32_t timer_clock 72000000; uint32_t counter_clock timer_clock / (71 1); // 1MHz uint32_t arr_value (counter_clock / freq_hz) - 1; if(arr_value 65535) return; // 对于16位定时器ARR不能超过65535 TIM3-ARR arr_value; // 修改ARR值会在下次更新事件时生效 // 通常改变频率后为了保持占空比比例需要同步调整CCR // 可以在这里读取当前占空比百分比然后根据新的ARR重新计算CCR // uint32_t new_ccr (arr_value 1) * current_duty_percent / 100; // TIM3-CCR1 new_ccr; } void set_pwm_duty_cycle(uint8_t duty_percent) { if(duty_percent 100) duty_percent 100; uint32_t arr_value TIM3-ARR; // 计算CCR值。注意占空比 (CCR1) / (ARR1)所以 CCR (ARR1) * duty / 100 - 1 // 但为了直观通常近似为 CCR ARR * duty / 100 uint32_t ccr_value (arr_value 1) * duty_percent / 100; // 确保CCR值有效 if(ccr_value arr_value) ccr_value arr_value; // 对于某些应用如舵机CCR为0可能意味着0脉宽需要特殊处理 if(ccr_value 0 duty_percent 0) ccr_value 1; TIM3-CCR1 ccr_value; // 修改CCR1值会在下次更新事件或根据模式时生效 }5.2 参数边界处理与系统稳定性保障动态调整时必须考虑参数的合法性和系统的稳定性。频率范围限制下限频率不能为0。同时过低的频率如1Hz意味着ARR值会非常大。对于16位定时器ARR最大65535。在1MHz计数器时钟下最低频率约为1MHz / 65536 ≈ 15.26Hz。如果需要更低的频率可以增大PSC值但会损失分辨率。上限受限于定时器时钟和分辨率要求。频率太高ARR值会很小。例如72MHz时钟PSC0ARR最小为1此时频率高达36MHz但这几乎不再是PWM而是方波。同时高频率下占空比调节的步进分辨率会变粗。需要根据实际应用权衡。占空比边界与死区占空比理论上在0%到100%之间。但实际应用中0%和100%可能对应着常低或常高的电平失去了调制意义。有些电机驱动或电源电路还需要设置“死区时间”防止上下桥臂直通。这通常需要使用定时器的互补输出和刹车死区功能本项目暂不涉及。原子操作与临界区保护 在更复杂的系统中如使用了RTOS修改ARR和CCR的代码可能被中断打断导致参数不匹配。虽然本例中单线程修改问题不大但养成好习惯可以使用__disable_irq()和__enable_irq()临时关闭中断或者使用RTOS提供的信号量、互斥锁来保护这些关键操作。反馈与验证 修改参数后可以通过串口将计算出的ARR、CCR值或实际频率占空比回传给电脑形成一个闭环方便调试。例如在set_pwm_frequency函数最后可以计算实际设置成功的频率real_freq counter_clock / (TIM3-ARR 1)并打印出来。6. 系统集成、调试与问题排查实录6.1 主程序逻辑与系统集成将PWM初始化和串口初始化的代码整合起来主函数的逻辑就非常清晰了#include stm32f10x.h #include stdio.h // 用于printf重定向 #include string.h // ... 上述初始化函数和解析函数定义 ... int main(void) { // 初始化系统时钟通常由SystemInit()在启动文件调用 // 初始化延时函数如果需要 delay_init(); // 初始化串口1 uart1_init(115200); // 初始化PWMTIM3, CH1, 1KHz, 50% pwm_init(1000, 50); printf(System Ready. Send Fxxx or Dxx to adjust PWM.\r\n); while(1) { if(uart_rx_flag 1) { uart_rx_flag 0; parse_and_execute_command(uart_rx_buf); } // 这里可以添加其他任务如按键扫描、LED闪烁等 delay_ms(10); } } // 重定向printf到串口1需在工程设置中勾选“Use MicroLIB” int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t) ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); return ch; }6.2 典型问题排查与解决方法在实际焊接电路和调试代码时你几乎一定会遇到下面这些问题。我把它们和排查思路整理成了表格希望能帮你快速定位。现象可能原因排查步骤与解决方法完全无PWM波形1. GPIO模式错误。2. 定时器未使能。3. 引脚被其他功能占用如JTAG。4. 硬件连接问题线断了。1. 确认GPIO初始化为GPIO_Mode_AF_PP。2. 检查TIM_Cmd(TIMx, ENABLE)是否执行。3. 查阅数据手册确认引脚复用功能。对于JTAG占用的引脚PA15, PB3, PB4需要在代码早期调用GPIO_PinRemapConfig禁用JTAG启用SWD。4. 用万用表测量引脚电压是否有变化或用逻辑分析仪抓取信号。有波形但频率/占空比不对1. 时钟配置错误。2. ARR/PSC/CCR计算错误。3. 更新时机不对读到了旧值。1. 确认SystemInit()是否正确将系统时钟配置为72MHz或你的目标频率。2. 重新推导计算公式打印出计算出的ARR、CCR值进行核对。3. 确保修改的是TIMx-ARR而不是TIMx-CNT。使用预装载功能确保同步更新。串口无法接收数据1. 波特率不匹配。2. TX/RX线接反。3. 串口中断未正确配置或使能。4. 电脑端串口驱动未安装或端口被占用。1. 检查代码和串口助手波特率是否精确一致如115200。2.反复检查MCU的TX接模块的RXMCU的RX接模块的TX。3. 检查NVIC配置确认中断服务函数名是否正确USART1_IRQHandler。4. 在设备管理器中查看端口号确认串口助手选择了正确的COM口。串口能收但数据乱码1. 时钟错误导致波特率发生器偏差大。2. 数据位、停止位、校验位不匹配。3. 中断中数据处理不当数组越界。1. 这是最常见原因。确保系统时钟配置正确。对于STM32F103外部晶振是8M还是12Msystem_stm32f10x.c中的宏定义要匹配。2. 检查代码和串口助手设置必须同为8N18数据位无校验1停止位。3. 检查接收缓冲区索引uart_rx_index是否在增加前判断了上限。修改参数后波形有毛刺1. 直接修改工作寄存器未使用预装载。2. 在中断中频繁修改参数导致参数冲突。1. 确保初始化时调用了TIM_ARRPreloadConfig和TIM_OCxPreloadConfig。2. 确保在主循环或低优先级任务中修改参数。如果必须在高频率中断中修改考虑使用双缓冲机制设置一组“目标参数”在定时器更新中断TIMx_UP_IRQHandler中安全地将其拷贝到ARR/CCR影子寄存器。占空比调到0%或100%时输出不变1. CCR值计算为0或等于ARR。2. PWM输出极性理解有误。1. 检查计算逻辑。当期望占空比0%时CCR应设为0期望100%时CCR应大于等于ARR在PWM模式1下。2. 理解TIM_OCPolarity_High和TIM_OCPolarity_Low的区别。High有效时CNTCCR输出高Low有效时则相反。一个高级调试技巧使用定时器的“强制输出”功能。在初始化后可以调用TIM_ForcedOCxConfig(TIM3, TIM_ForcedAction_Active)来强制通道输出有效电平高或低取决于极性。这可以帮你快速判断是定时器配置问题还是后续比较逻辑问题。如果强制输出有电平变化但正常PWM模式没有那问题很可能出在CCR寄存器的比较匹配环节。7. 项目扩展与进阶应用思路这个基础框架搭建好后你可以根据实际需求进行无限扩展让它从一个实验项目变成真正产品的一部分。1. 指令协议增强增加校验和在指令末尾添加一个简单的校验和如所有字节相加取低8位提高通信可靠性。采用更高效的二进制协议对于需要高速调整的场景可以将“F”、“D”和数值打包成固定的二进制帧例如0x46 0x03 0xE8代表F10000x03E81000减少数据量加快解析速度。加入查询指令发送“?F”或“?D”MCU返回当前的频率和占空比值。2. 控制精度与平滑性优化使用32位定时器对于需要极宽频率范围或高分辨率占空比的应用可以选用STM32的32位定时器如TIM2/TIM5某些型号支持。实现软启动/软停止在改变电机速度时突然的频率或占空比跳变可能引起冲击。可以在set_pwm_frequency函数中实现一个循环让ARR或CCR在若干毫秒内渐变到目标值。加入闭环反馈如果控制的是电机可以结合编码器读取实际转速构成PID闭环控制。串口指令设定目标转速MCU自动调整PWM占空比来维持转速稳定。3. 应用场景拓展智能LED调光台灯通过手机APP通过蓝牙或Wi-Fi模块转串口发送指令无级调节亮度和色温需要多路PWM控制不同颜色的LED。直流电机调速器结合H桥驱动电路如L298N、TB6612用本系统生成的PWM控制电机转速和方向方向可用另一个GPIO控制。简易信号发生器通过精细计算ARR和PSC可以输出特定频率和占空比的方波作为数字电路的测试信号源。舵机控制器舵机控制需要的是50Hz周期20ms的PWM且高电平脉冲宽度在0.5ms到2.5ms之间对应0-180度角度。你可以固定频率通过串口指令改变CCR值来控制角度CCR (PulseWidth_us / (1000000 / Ftimer)) - 1。我个人在多次项目中实践下来的体会是“串口调PWM”这个功能看似简单但它像一把瑞士军刀是调试和验证电机驱动、电源管理、照明控制等几乎所有涉及模拟量输出场景的利器。前期花点时间把它做稳定、做灵活后期调试效率会成倍提升。最后再分享一个小技巧在开发初期可以固定一个LED灯到PWM输出引脚上。通过观察LED的亮度变化你就能非常直观地“看到”占空比的变化而无需总是依赖示波器这会让你的调试过程生动很多。