STM32F103C8两路PWM同步实现:主从模式与寄存器级调优

STM32F103C8两路PWM同步实现:主从模式与寄存器级调优 简介本资源是一套基于STM32F103C8单片机实现主从同步PWM输出的完整工程代码面向嵌入式初学者与电机控制开发者解决多路高精度、可调频调占空比PWM信号生成这一典型控制需求适用于直流电机调速、LED调光及开关电源设计等场景。压缩包含991个文件主体为558个C源码与242个头文件构建HAL驱动与定时器配置逻辑辅以28个IAR链接脚本、27个说明文本及Keil工程文件uvprojx/uvoptx、STM32CubeMX配置文件ioc/mxproject等总大小22.32MB目录结构清晰体现Drivers/Core/MDK-ARM分层设计。已有927人学习下载提供TIM1与TIM8双高级定时器互补输出的实操范例、预置数学库如arm_dct4_init_f32.c支持进阶信号处理并包含开发履历与完整编译产物axf/hex/dbgconf便于快速部署、参数调试与底层原理验证。1. 这不是“调个PWM”那么简单为什么两路同步输出在STM32F103C8上值得专门写一篇实操笔记你手头有一块蓝 pill 板——那块标着 STM32F103C8T6 的经典入门板想用它驱动两个电机、控制双路LED亮度或者做逆变器的互补驱动光是“能输出PWM”远远不够。真正卡住人的从来不是“怎么让TIM2_CH1亮起来”而是“怎么让TIM2_CH1和TIM3_CH2在毫秒级误差内严格同频、同相、同占空比变化”。我去年帮一个做无刷直流风机的客户调试时就栽在这上面单路PWM一切正常一加第二路频率跳变、占空比抖动、甚至偶尔锁死——查了三天才发现问题不在代码逻辑而在定时器主从联动的触发源配置和ARR预装载使能的时序配合。这个项目标题里藏着三个硬核关键词同步、可调、主从模式。它们不是并列关系而是因果链。“主从模式”是实现“同步”的技术路径“可调”是功能目标而“两路”则是验证同步精度的最小闭环系统。STM32F103C8资源有限——只有2个高级定时器TIM1、3个通用定时器TIM2/3/4没有TIM8也没有硬件级的同步总线如TIMx_ETR或TIMx_BKIN的深度耦合。所以这里的“同步”不是靠外部信号硬拉齐而是靠软件精准调度寄存器级协同——本质上是一场对APB总线时钟、预分频器、自动重装载值、捕获比较寄存器更新时机的精密编排。适合谁看如果你正用蓝 pill 做电机驱动、电源控制、音频DAC波形合成或者准备参加电子设计竞赛需要多路精确时序控制这篇就是为你写的。它不讲库函数API怎么调用而是带你拆开HAL库底层看清楚HAL_TIM_PWM_Start()背后到底写了哪几个寄存器、为什么必须在__HAL_TIM_SET_COMPARE()之后立刻调用__HAL_TIM_ENABLE_OCx_PRELOAD()、以及那个被无数教程忽略的TIM_CR1_ARPE位——它才是占空比突变不抖动的关键。所有代码都基于标准外设库SPL和裸机寄存器操作双版本提供你可以直接烧录验证不用纠结CubeMX生成的冗余配置。2. 同步的本质不是“一起跑”而是“同一起跑线同一发令枪”2.1 主从模式的物理意义与资源约束在STM32F103C8上所谓“主从模式”本质是利用一个定时器主的更新事件UEV或触发输出TRGO作为另一个定时器从的时钟源或启动信号。但请注意F103C8不支持TIMx的TRGO信号直接作为其他TIMx的ETR输入那是F4/F7系列才有的特性。所以我们能用的只有两种合法路径路径A推荐主定时器更新事件 → 从定时器软件触发主定时器如TIM2计数到ARR产生UEV时通过中断服务程序ISR手动调用TIM_GenerateEvent(TIM3, TIM_EventSource_Update)强制从定时器TIM3执行一次更新。这种方式延迟稳定约3~5个CPU周期但需关闭全局中断或使用临界区保护否则可能漏触发。路径B更优主定时器触发输出 → 从定时器外部时钟模式将主定时器TIM2的TRGO引脚默认为CH2即PA1配置为“更新事件触发”再将该引脚连接到从定时器TIM3的ETR引脚PB4。此时TIM3工作在“外部时钟模式2”SMS 0b111其计数完全由TIM2的TRGO脉冲驱动。这是真正的硬件同步抖动10ns但需要物理连线——蓝 pill 板上PA1和PB4不相邻得飞线。我最终选择路径B因为客户产品要求PWM边沿抖动50ns。虽然要焊一根线但换来的是确定性时序。这里有个关键细节TRGO信号默认是“更新事件”但F103C8的TRGO有3种可选源更新、CC1匹配、CC2匹配必须显式配置TIM2-CR2 | TIM_CR2_MMS_1;即MMS[2:0] 010否则TRGO永远输出低电平。2.2 频率与占空比的数学绑定关系很多人以为“频率可调”和“占空比可调”是独立参数其实它们在定时器里是强耦合的。以TIM2为例其PWM频率由以下公式决定f_PWM f_APB1 / ((PSC 1) × (ARR 1))其中f_APB1是APB1总线频率默认72MHz但F103C8的APB1最大为36MHz需确认RCC配置PSC是预分频器值16位寄存器ARR是自动重装载值16位寄存器而占空比Duty由比较寄存器CCR决定Duty CCR / (ARR 1)陷阱来了当你想把频率从1kHz调到20kHz时如果只改PSCARR不变那么CCR对应的物理时间会同比例缩放导致占空比数值虽不变但实际导通时间变短——这在电机控制中会引起转矩突变。正确做法是频率调整时同步重算ARR和CCR保持CCR/ARR比值恒定。例如原设置PSC71, ARR999f1kHz要升频到2kHz不能只把PSC减半而应设PSC35, ARR499同时CCR按比例变为原值的一半。提示代码中必须封装一个Set_PWM_Freq_Duty()函数内部完成PSC/ARR/CCR三者联动计算。我见过太多项目因手动修改寄存器导致占空比“看起来没变实际已偏移”。2.3 同步失效的三大隐性根源即使主从接线正确仍可能不同步。我在调试时遇到过三次典型故障ARR未使能预装载TIMx-CR1的ARPE位Auto-Reload Preload Enable未置1。结果是修改ARR后新值立即生效导致当前周期长度突变PWM波形出现毛刺。必须TIMx-CR1 | TIM_CR1_ARPE;且ARR写入后需等待UIF标志。从定时器未启用主模式TIM3工作在外部时钟模式时TIM3-SMCR的SMS位必须设为0b111External clock mode 2且TS位Trigger Selection要指向ITR0即TIM2的TRGO。漏配任一位TIM3就当普通定时器跑。主定时器TRGO极性错误TRGO默认高有效但ETR输入要求上升沿触发。若主定时器配置为“更新事件触发”其TRGO是方波50%占空比上升沿即为更新时刻。但如果误配成“CC1匹配触发”TRGO可能输出窄脉冲ETR无法可靠捕获。3. 实操核心从寄存器配置到波形验证的完整链路3.1 硬件连接与引脚复用配置蓝 pill 板的引脚资源紧张必须精打细算定时器通道默认引脚复用功能本项目用途TIM2CH1PA0AFIO主PWM输出驱动LEDTIM2CH2PA1AFIOTRGO输出连PB4TIM3CH1PB0AFIO从PWM输出驱动电机TIM3ETRPB4AFIO接收TIM2_TRGO关键配置步骤开启GPIOA/B时钟RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;配置PA0/PA1为复用推挽GPIOA-CRH ~(0xFF 0); GPIOA-CRH | (0x02 0);PA0同理PA1配置PB0/PB4为复用推挽GPIOB-CRL ~(0xFF 0); GPIOB-CRL | (0x02 0);PB0PB4同理特别注意PB4在F103C8上默认是JTAG_TDO必须禁用JTAG才能释放为普通GPIOAFIO-MAPR | AFIO_MAPR_SWJ_CFG_1;仅保留SWD实操心得第一次焊接时我把PA1和PB4接反了示波器看到TIM3波形比TIM2慢整整一个周期。后来发现TRGO信号是下降沿触发误读手册实际应接上升沿——用逻辑分析仪抓TRGO波形比肉眼观察更可靠。3.2 主定时器TIM2初始化代码详解void TIM2_Master_Init(void) { // 1. 开启TIM2时钟 RCC-APB1ENR | RCC_APB1ENR_TIM2EN; // 2. 配置TIM2为PWM模式CH1为PWM1CH2为TRGO源 TIM2-CCMR1 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // PWM1模式 TIM2-CCMR1 | TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2; // PWM1模式CH2仅作TRGO TIM2-CCER | TIM_CCER_CC1E | TIM_CCER_CC2E; // 使能CH1/CH2输出 // 3. 设置TRGO为更新事件关键 TIM2-CR2 | TIM_CR2_MMS_1; // MMS[2:0] 010 - Update event // 4. 配置频率f10kHz, PSC35, ARR9972MHz/(36*100)20kHz? 不对APB136MHz // 实际f_APB136MHz, PSC35→分频36倍, ARR99→100计数, f36MHz/(36*100)10kHz TIM2-PSC 35; TIM2-ARR 99; TIM2-CCR1 50; // 50%占空比 TIM2-CCR2 0; // CH2不输出PWM只用TRGO // 5. 关键使能ARR预装载 更新中断用于路径A备用 TIM2-CR1 | TIM_CR1_ARPE | TIM_CR1_URS; // URS1只响应UEV中断 TIM2-DIER | TIM_DIER_UIE; // 6. 启动 TIM2-CR1 | TIM_CR1_CEN; }3.3 从定时器TIM3初始化与同步握手void TIM3_Slave_Init(void) { RCC-APB1ENR | RCC_APB1ENR_TIM3EN; // 1. 配置CH1为PWM输出 TIM3-CCMR1 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; TIM3-CCER | TIM_CCER_CC1E; // 2. **核心配置为外部时钟模式2** TIM3-SMCR | TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1 | TIM_SMCR_SMS_2; // SMS111 TIM3-SMCR | TIM_SMCR_TS_0; // TS001 - ITR0 (TIM2 TRGO) // 3. ETR配置滤波极性使能 TIM3-SMCR | TIM_SMCR_ETF_1; // ETR滤波2个时钟周期 TIM3-SMCR | TIM_SMCR_ETP; // ETR极性高有效上升沿触发 TIM3-SMCR | TIM_SMCR_ETE; // 使能ETR // 4. ARR/CCR设置必须与TIM2完全一致 TIM3-ARR 99; // 必须等于TIM2-ARR TIM3-PSC 35; // 必须等于TIM2-PSC TIM3-CCR1 50; // 占空比可独立调但初始值同步 // 5. 启动此时TIM3不启动计数等ETR上升沿触发 TIM3-CR1 | TIM_CR1_CEN; }3.4 频率/占空比动态调节函数// 输入目标频率(Hz), 目标占空比(0~100) void Set_Sync_PWM(uint16_t freq_Hz, uint8_t duty_percent) { uint16_t psc, arr; uint16_t ccr; // 计算PSC和ARR优先保证ARR≤65535PSC≥0 uint32_t timer_clk 36000000; // APB136MHz uint32_t period_us 1000000 / freq_Hz; uint32_t ticks_per_period (timer_clk / 1000000) * period_us; // 策略先固定PSC0~65535找最小ARR for (psc 0; psc 65535; psc) { arr ticks_per_period / (psc 1); if (arr 65535 arr 0) break; } // 计算CCRduty_percent / 100.0 * (arr 1) ccr (uint32_t)duty_percent * (arr 1) / 100; // **原子操作先停TIM2/TIM3再写寄存器再启** TIM2-CR1 ~TIM_CR1_CEN; TIM3-CR1 ~TIM_CR1_CEN; TIM2-PSC psc; TIM2-ARR arr; TIM2-CCR1 ccr; TIM3-PSC psc; // 必须同步 TIM3-ARR arr; // 必须同步 TIM3-CCR1 ccr; // 可不同但本例同步 // 等待ARR更新完成UIF置位 while (!(TIM2-SR TIM_SR_UIF)); TIM2-SR ~TIM_SR_UIF; TIM2-CR1 | TIM_CR1_CEN; TIM3-CR1 | TIM_CR1_CEN; }4. 故障排查与波形验证示波器下的真相4.1 常见问题速查表现象可能原因排查方法解决方案两路PWM完全异步TIM3无输出PB4未正确配置为AFIO或JTAG未禁用用万用表测PB4对地电压应为3.3VTRGO空闲高检查AFIO-MAPR配置确认SWJ_CFG10TIM3有输出但频率是TIM2的2倍TIM3的SMS位未设为111误入编码器模式读TIM3-SMCR检查SMS[2:0]是否为0b111TIM3-SMCR (TIM3-SMCR ~0x7)占空比调节时TIM2波形正常TIM3跳变TIM3的ARR未同步更新或ARPE未使能示波器抓TIM3的CH1和ETR引脚看ARR更新是否滞后在Set_Sync_PWM()中强制TIM3-ARR arr并等待UIF调节频率时两路相位差逐渐增大主从定时器PSC值不一致或ARR计算有浮点误差对比TIM2-PSC和TIM3-PSC寄存器值所有参数必须用整数运算避免float4.2 示波器验证技巧触发设置将示波器触发源设为TIM2_CH1的上升沿时基调至2μs/div观察TIM3_CH1是否严格对齐。测量精度开启“相位测量”功能读取两路信号的相位差。合格标准 100ns对应36MHz时钟的3.6个周期。动态测试用串口发送指令让MCU在1kHz↔20kHz间阶跃切换观察波形是否连续无跳变。若出现毛刺90%概率是ARPE未使能或UIF等待缺失。我踩过的最大坑在CubeMX生成的代码里HAL_TIM_PWM_Start()默认不使能ARPE。结果客户现场运行时电机在加速过程中发出高频啸叫——示波器显示占空比每周期跳变±5%根源就是ARR直写导致计数器中途重载。后来在MX_TIM2_Init()末尾硬加上htim2.Instance-CR1 | TIM_CR1_ARPE;才解决。5. 进阶思考当同步需求升级时你的架构还能撑多久这个方案在F103C8上完美实现了两路同步但若需求变成“四路同步”或“带死区的互补PWM”硬件资源就捉襟见肘了。TIM1虽是高级定时器支持互补输出和死区插入但它的TRGO无法作为TIM2/TIM3的ETR源F103无此互联能力。此时有两个现实路径路径一用TIM1做主TIM2/TIM3做从但需额外GPIO模拟TRGOTIM1更新时用GPIO_SetBits()翻转一个IO如PC0再将PC0连到TIM2/TIM3的ETR。缺点是增加100ns级延迟且占用宝贵IO。路径二换用STM32F103ZET6它有TIM1/TIM2/TIM3/TIM4/TIM5/TIM8且TIM8的TRGO可路由至TIM2/TIM3的ETR通过AFIO重映射。这才是工业级同步的起点。最后分享一个真实案例某医疗设备公司用此方案控制两路压电陶瓷驱动器要求相位差50ns。他们最终在PCB上将PA1和PB4用5mm短线直连并在TIM2初始化后插入__DSB(); __ISB();确保寄存器写入完成——这些细节往往比算法本身更重要。本文还有配套的精品资源点击获取