简介:STM32H750 PWM输出工程基于寄存器库驱动开发,面向需要深入掌握STM32H7系列定时器与PWM配置的嵌入式开发者。代码围绕PWM通道选择、时钟分频、预装载与自动重载值(PSC/ARR)、比较值(CCR)设置等关键步骤给出完整实现,可直接用于电机控制、电源管理、LED调光等场景,并可根据芯片差异快速移植到其他H7型号。压缩包共28个文件,以C源码与头文件为主体(9个c、14个h),辅以Keil工程文件(.uvprojx/.uvoptx)、STM32H750启动文件(.s)、链接脚本(.scf)以及编译好的hex固件,整体仅307KB,目录区分LED、TIMER、KEY、EXTI等模块,便于按需查阅。配套代码覆盖定时器初始化、PWM模式配置、GPIO复用映射等环节,注释清晰,并预留中断/DMA与多通道同步扩展接口,帮助读者快速验证输出波形、掌握寄存器级底层配置逻辑。已有800余人学习浏览,适合具备一定单片机基础、希望从标准库转向寄存器开发的工程人员。
1. STM32H750 的 PWM 输出与寄存器库驱动路线
STM32H750 在 H7 系列里处在特殊位置:480MHz 的 Cortex-M7 内核,片内 Flash 只有 128KB,靠外部 QSPI 代码的玩法成为成本受限团队也会认真评估的高性能 MCU。PWM 输出是这类芯片最常用的外设能力之一,舵机控制、LED 调光、电机驱动都会用到。网上数量最多的例程基于 HAL,寄存器级实现被 wrapper 包住,遇到 480MHz 下分频算不准、中心对齐模式下找不到 ADC 采样点这类问题,反而查无可查。本文用寄存器库方式,把 STM32H750 上 TIM1 输出 PWM 的最小可运行路径完整走一遍:RCC 时钟怎么配,ARR、PSC、CCR1、CCMR1、CCER 各自职责,以及 100% 占空比和死区设置的边界。读者定位是已经能点灯、想掌握定时器底层的人;看完后可以脱离 HAL 自写驱动。
2. 先通时钟再谈 PWM:H7 的 RCC 与定时器时钟树
2.1 第一处容易想当然:APB2 的频率并不是定时器内核时钟
从 F1 转到 H7 最常见的翻车点,是直接照搬 F1 的 RCC_CFGR 配置,最后量 PWM 频率差了一倍甚至两倍。H7 的时钟树里,定时器内核时钟并不直接等于 APBx 总线时钟。STM32H750 的参考手册是 RM0433,寄存器分布在 RCC_CR、RCC_PLLCKSELR、RCC_PLL1DIVR、RCC_D1CFGR、RCC_D2CFGR 这一组里;其中 D1CFGR 管内核和 AHB,D2CFGR 管 APB1/APB2,D3CFGR 管 APB4。
规律是这样的:当 APB1/APB2 的预分频系数为 1 时,定时器内核时钟等于 PCLK;当预分频系数大于 1 时,默认会把定时器时钟乘回 2 倍;如果同时把 D2CFGR 里的 TIMPRE 位置 1,则乘回 4 倍。这意味着你配置 APB2=120MHz 后,TIM1 的内核时钟实际是 240MHz。很多人在这里少乘一个 2,PWM 频率直接翻倍,舵机或电机控制器表现异常。
| APB2 预分频 | PCLK2 | TIM1 内核时钟(默认) | 典型用途 |
|---|---|---|---|
| 1 | 240MHz | 240MHz | APB2 外设较少时 |
| 2 | 120MHz | 240MHz | 推荐组合 |
| 4 | 60MHz | 120MHz | 保守功耗场景 |
| 8 | 30MHz | 60MHz | 低频 PWM 足够 |
上表的默认值对应 TIMPRE=0;如果 TIMPRE=1,定时器时钟会在上表基础上再乘 2,达到 480MHz。但注意,480MHz 定时器时钟要评估电源和封装速度等级,实际项目中通常不追求这个极限,240MHz 已经能满足绝大多数 PWM 需求。
2.2 RCC 寄存器配置:从 HSE 经过 PLL1 到 480MHz
以常见 25MHz HSE 为例,标准配置路径是:HSE 进 PLL1,DIVM1=5 得到 5MHz 参考频率,DIVN1=192 倍频到 960MHz VCO,DIVP1=2 分频得到 480MHz 的 pll1_p_ck,作为系统时钟候选。之后通过 D1CFGR 设置内核 480MHz、AHB 240MHz、APB3 120MHz,通过 D2CFGR 设置 APB1=120MHz、APB2=120MHz。完整的寄存器序列如下。
void system_clock_480mhz(void) { // 1) 开启 HSE,等待稳定 RCC->CR |= RCC_CR_HSEON; while (!(RCC->CR & RCC_CR_HSERDY)) { } // 2) 关掉 PLL1,避免配置过程中锁定旧参数 RCC->CR &= ~RCC_CR_PLL1ON; while (RCC->CR & RCC_CR_PLL1RDY) { } // 3) PLL 源选择 HSE,DIVM1 = 5,即 25MHz / 5 = 5MHz RCC->PLLCKSELR &= ~RCC_PLLCKSELR_PLLSRC_Msk; RCC->PLLCKSELR |= RCC_PLLCKSELR_PLLSRC_HSE; RCC->PLLCKSELR &= ~RCC_PLLCKSELR_DIVM1_Msk; RCC->PLLCKSELR |= (5U << RCC_PLLCKSELR_DIVM1_Pos); // 4) VCO = 5MHz * 192 = 960MHz // DIVP1 = 2 -> 480MHz 系统时钟 // DIVQ1 = 2 -> 240MHz // DIVR1 = 2 -> 240MHz RCC->PLL1DIVR = (192U << RCC_PLL1DIVR_N1_Pos) | (1U << RCC_PLL1DIVR_P1_Pos) | (2U << RCC_PLL1DIVR_Q1_Pos) | (2U << RCC_PLL1DIVR_R1_Pos); // 5) 重新打开 PLL1 RCC->CR |= RCC_CR_PLL1ON; while (!(RCC->CR & RCC_CR_PLL1RDY)) { } // 6) 480MHz 下 Flash 需要 4 个等待周期,开 ICache/DCache FLASH->ACR = FLASH_ACR_LATENCY_4WS | FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_PRFTEN; // 7) D1CPRE /1 -> 480MHz,HPRE /2 -> 240MHz,D1PPRE /2 -> 120MHz // HPRE 编码:0b1000 表示 /2,DxPPRE 编码:0b100 表示 /2 RCC->D1CFGR = (0U << RCC_D1CFGR_D1CPRE_Pos) | (8U << RCC_D1CFGR_HPRE_Pos) | (4U << RCC_D1CFGR_D1PPRE_Pos); // 8) APB1 /2,APB2 /2,TIMPRE 保持 0 RCC->D2CFGR = (4U << RCC_D2CFGR_APB1PRE_Pos) | (4U << RCC_D2CFGR_APB2PRE_Pos) | (0U << RCC_D2CFGR_TIMPRE_Pos); // 9) 软件切换到 PLL1 RCC->CFGR |= RCC_CFGR_SW_PLL1; while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL1) { } }参数说明:DIVM1 是输入分频,它决定进入 PLL 的参考频率,STM32 要求参考频率在 1~2MHz 到几十 MHz 范围内,5MHz 是稳妥选择。DIVN1 决定 VCO 频率,960MHz 在 H750 的 PLL 允许范围内。DIVP1 是系统时钟分频,这里取 2 得到 480MHz;如果你想保守一点跑 400MHz,可以把 DIVN1 改 160、DIVP1 保持 2。D1CFGR 中 D1CPRE 编码 0 表示不分频,HPRE 编码 8 表示 2 分频,D1PPRE 编码 4 表示 2 分频。D2CFGR 中 APB1PRE 和 APB2PRE 的 4 都表示 2 分频。
这段代码最后切换完成后,SYSCLK=480MHz,AHB=240MHz,PCLK2=120MHz。TIM1 挂在 APB2 域,按 2.1 节的规则,TIM1 内核时钟 = 120MHz × 2 = 240MHz。如果实际跑出来不对,优先检查第 8 步的预分频编码有没有写错,而不是怀疑后面 TIM 的配置。
2.3 初始化后验证时钟的快速方法
时钟树是否配正确,最直接的办法不是马上接示波器,而是读 RCC 状态位。RCC->CFGR的 SWS 字段会反映当前系统时钟源;如果切换成功,SWS 应为 0b011(PLL1)。再用 TIM 的计数值反推:配置 TIM1 的 PSC 和 ARR 后,把 CNT 清零并单步观察 1ms 内 CNT 的增长量,就能反推定时器时钟。这个方法在调试外部 QSPI 启动、bootloader 跳转后卡死这类问题时尤其有用——很多“片外 app 卡死”的现象,根因就是 app 里重新初始化了 RCC,但 APB 分频和 bootloader 不一致,定时器中断频率全部错乱。
3. 用寄存器配置 TIM1 输出 PWM,最小可运行程序
3.1 PA8 的复用配置:GPIO 寄存器层
PA8 是 TIM1_CH1 的默认映射引脚。H7 的 GPIO 通过 MODER、OTYPER、OSPEEDR、PUPDR、AFRL/AFRH 控制;把 PA8 切到复用模式,选择 AF1,就能把 TIM1_CH1 信号引出来。
void gpio_pa8_tim1_ch1(void) { // 使能 GPIOA 时钟,GPIO 挂在 AHB4 域 RCC->AHB4ENR |= RCC_AHB4ENR_GPIOAEN; // MODER 每引脚占 2 位,PA8 对应第 16、17 位 // 0b10 = 复用功能模式 GPIOA->MODER &= ~(3U << (8 * 2)); GPIOA->MODER |= (2U << (8 * 2)); // OSPEEDR:PA8 拉高速,PWM 边沿更陡 GPIOA->OSPEEDR &= ~(3U << (8 * 2)); GPIOA->OSPEEDR |= (3U << (8 * 2)); // PUPDR:无上下拉,避免影响电平 GPIOA->PUPDR &= ~(3U << (8 * 2)); // AFRL 管 PA0~PA7,AFRH 管 PA8~PA15 // PA8 是 AFRH 的第 0 个,写 AF1 = TIM1_CH1 GPIOA->AFR[1] &= ~(0x0FUL << 0); GPIOA->AFR[1] |= (0x01UL << 0); }参数说明:MODER 的 2 位编码 0b10 是复用模式,0b11 是模拟模式,如果误写成 0b11,PWM 信号不会出现在引脚上。OSPEEDR 的 0b11 对应最高速度档,对 PWM 输出建议直接拉满;如果只是低速 LED 调光,0b01 也够,还能减少 EMI。AFR 是 4 位一组,TIM1 在 H7 上的复用编号是 AF1,有些引脚是 AF2,比如 TIM2 的 PA0 是 AF1、PB10 是 AF1,不同定时器映射要先查数据手册的复用表,不能想当然。
3.2 ARR、PSC 和 PWM 频率计算
定时器配置的核心是三个寄存器:PSC 做预分频,ARR 决定周期,CCR1 决定占空比。PWM 频率公式如下。
f_pwm = f_timer / ((PSC + 1) * (ARR + 1))这里的 ARR+1 是新手最容易漏的:计数从 0 加到 ARR,一共 ARR+1 个计数状态。在 240MHz 定时器时钟下,少算一个计数就是 4ns 的误差,比例不高,但在同步整流、LLC 这类对死区敏感的拓扑中会变成温度漂移一样难以定位的问题。
占空比公式分情况:向上计数、PWM 模式 1、极性高有效时:
duty = CCR1 / (ARR + 1) * 100%选择 100kHz 作为目标频率,PSC=0,ARR=2399,则 f = 240MHz / 2400 = 100kHz。如果目标频率是 20kHz 且想用同样的 CCR 分辨率,可以 PSC=0、ARR=11999,这样占空比步进约 0.008%,足够精确控制电机或舵机;如果定时器时钟不是 240MHz,比如配置成了 120MHz,同样参数输出就变成 50kHz,所以时钟树和 PWM 频率永远是绑定检查的。
3.3 CCMR1、CCER、BDTR:输出模式的寄存器组合
CCMR1 控制通道 1 的方向和输出模式,其中 CC1S 选择输入/输出方向,OC1M 选择输出比较模式,OC1PE 使能预装载。CCER 的 CC1E 负责打开引脚输出,CC1P 选择极性。H7 的 TIM1 是高级定时器,和通用定时器不一样的地方在于,输出级还有一个主输出使能位 MOE,在 BDTR 寄存器里。MOE 为 0 时,即使 CC1E 置 1,引脚也不会有波形。这是 H7 上最容易出现的“配置了但没输出”的原因。
| 寄存器 | 位段 | 值 | 含义 |
|---|---|---|---|
| TIM1->CR1 | 位0 CEN | 1 | 启动计数 |
| TIM1->CR1 | 位7 ARPE | 1 | ARR 预装载使能 |
| TIM1->CCMR1 | 位0 CC1S | 0 | 输出比较通道 |
| TIM1->CCMR1 | 位3 OC1PE | 1 | CCR1 预装载使能 |
| TIM1->CCMR1 | 位8~11 OC1M | 0b0110 | PWM 模式 1 |
| TIM1->CCER | 位0 CC1E | 1 | CH1 输出使能 |
| TIM1->CCER | 位1 CC1P | 0 | 高电平有效 |
| TIM1->BDTR | 位15 MOE | 1 | 主输出使能 |
| TIM1->EGR | 位0 UG | 1 | 触发更新事件 |
PWM 模式 1 的物理行为是:向上计数时,CNT < CCR1 输出高电平,CNT ≥ CCR1 输出低电平。预装载位的作用是让 CCR1 的修改在下一个更新事件才生效,避免运行中改占空比时输出一个残缺脉冲。ARPE 同理,保护 ARR 不被撕裂。
3.4 完整初始化代码:直接可跑的 TIM1 PWM
#include "stm32h7xx.h" void tim1_pwm_100khz_init(void) { // TIM1 时钟在 APB2 域 RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // 先停止计数,避免配置被更新事件打断 TIM1->CR1 &= ~TIM_CR1_CEN; // 240MHz / (0+1) / (2399+1) = 100kHz TIM1->PSC = 0; TIM1->ARR = 2399; TIM1->CCR1 = 1200; // 初始 50% 占空比 // 输出比较 1,PWM 模式 1,预装载使能 TIM1->CCMR1 &= ~TIM_CCMR1_CC1S_Msk; TIM1->CCMR1 &= ~TIM_CCMR1_OC1M_Msk; TIM1->CCMR1 |= (0x6U << TIM_CCMR1_OC1M_Pos); TIM1->CCMR1 |= TIM_CCMR1_OC1PE; // 打开主输出 MOE,使能 CH1,极性高有效 TIM1->BDTR |= TIM_BDTR_MOE; TIM1->CCER &= ~TIM_CCER_CC1P_Msk; TIM1->CCER |= TIM_CCER_CC1E; // 生成更新事件,将 ARR/PSC 立即装载 TIM1->EGR |= TIM_EGR_UG; // ARR 预装载 + 启动计数 TIM1->CR1 |= TIM_CR1_ARPE; TIM1->CR1 |= TIM_CR1_CEN; } void tim1_set_duty(uint16_t percent_x100) { uint32_t ccr; if (percent_x100 >= 10000U) { // 100% 占空比:让 CCR 大于 ARR,避免窄脉冲 ccr = (uint32_t)TIM1->ARR + 1U; } else { ccr = (uint32_t)TIM1->ARR * percent_x100 / 10000U; } TIM1->CCR1 = (uint16_t)ccr; }代码逻辑说明:初始化函数先通过 CC1S 清 0 把通道设成输出比较方向,再写 OC1M 为 0b0110 选 PWM 模式 1,OC1PE 置 1 后,运行时对 CCR1 的修改才会在更新事件生效。BDTR 的 MOE 是高级定时器特有的输出总开关,清 0 后引脚被强制为无效电平,这也是刹车功能能立即切断 PWM 输出的原理。EGR 的 UG 位写 1 会产生一次更新事件,把 ARPE 置位前写入的 ARR 和 PSC 实际装载进影子寄存器,如果不做这一步,第一次周期的频率可能是乱的。tim1_set_duty 的入参是百分数乘以 100,即 50.00% 传 5000,计算时先乘后除避免整数除法的截断误差;超过 10000 直接按 100% 处理,CCR 写成 ARR+1,后面会解释为什么不能写 CCR=ARR。
3.5 运行时改频率和占空比的注意事项
ARR 和 CCR1 都开了预装载后,运行时任意时刻修改这两个寄存器都是安全的,新值在下一个更新事件才生效。如果你想立刻让新参数生效,可以手动置 EGR 的 UG 位;但这会同时把计数器 CNT 清零,在中心对齐模式下计数方向会立刻改变,ADC 触发时刻会偏移。正确的做法是:改频率后等一个更新事件标志,再开始新一轮数据采样。这是 PWM 调频和 ADC 采样同步最常见的隐性 bug。
4. 100% 占空比、死区与中心对齐模式:H7 定时器的边界问题
4.1 CCR=ARR 时的 100% 占空比异常
这个问题在 PWM 调试中出现频率极高:把 CCR1 设成 ARR,预期输出恒高,示波器上却看到每隔一段就有极窄的低电平毛刺。原因出在 PWM 模式 1 的比较逻辑上:CNT 递增到 ARR 时满足 CNT ≥ CCR1,输出立即翻低;紧接着溢出事件把 CNT 清零并重新装载,输出又变高。这个中间过程持续正好一个定时器时钟周期。在 240MHz 内核时钟下就是约 4ns 的窄脉冲。
这个窄脉冲在逻辑分析仪上不一定看得见,但驱动功率管时可能让半桥直通。解决方式有三种:
- 把 CCR1 设成 ARR+1。只要 ARR 不是 16 位最大值 0xFFFF,CNT 永远小于 CCR1,输出保持高电平。前文 tim1_set_duty 已经按这个方式处理。
- 反转极性。CCER 的 CC1P 置 1 后,引脚输出和 OC1REF 反相。此时设 CCR1=0,参考输出为恒低,引脚实际输出恒高,同样得到真正的 100% 占空比。这个方法的额外好处是 0% 和 100% 切换时 CCR 不用跨过 ARR 边界。
- 接受窄脉冲并认为应用层无影响。在非安全场景可以,但我不建议把它留在电机驱动代码里。
反过来 0% 占空比也有类似问题:CCR1=0 时,第一个计数周期 CNT 从 0 开始,比较结果为 CNT=CCR1,输出低电平;如果极性不反转,PWM 启动后首个周期可能有一个时钟周期的高电平脉冲。对功率驱动而言,初始化时先让 MOE=0,等 CCR1 设置好后再开 MOE,能完全规避首脉冲。
4.2 BDTR 死区配置:DTG 编码与 tDTS
TIM1 和 TIM8 支持互补输出,CH1 和 CH1N 之间可以插入死区。死区时间由 BDTR 的 DTG[7:0] 决定,基准时间 tDTS 是定时器内核时钟的倒数。在 240MHz 时钟下,tDTS = 4.17ns。DTG 的编码分段规则如下。
| DTG 条件 | 死区时间公式 |
|---|---|
| DTG[7] = 0 | DT = DTG × tDTS |
| DTG[7] = 1,DTG[6:5] = 00 | DT = (64 + DTG[5:0]) × 2 × tDTS |
| DTG[7] = 1,DTG[6:5] = 01 | DT = (32 + DTG[5:0]) × 8 × tDTS |
| DTG[7] = 1,DTG[6:5] = 10 | DT = (32 + DTG[5:0]) × 16 × tDTS |
| DTG[7] = 1,DTG[6:5] = 11 | DT = (32 + DTG[5:0]) × 32 × tDTS |
比如要 500ns 死区,用第一段公式:500 / 4.17 ≈ 120,即 DTG = 0x78。代码实现如下。
void tim1_deadtime_set(uint32_t ns) { uint32_t dtg = ns * 240UL / 1000UL; // 粗略折算 1/240MHz if (dtg > 127U) { // 超过第一段范围时,按第二段公式重新折算 // 实际项目中极少用到,这里直接钳位 dtg = 127U; } TIM1->BDTR &= ~TIM_BDTR_DTG_Msk; TIM1->BDTR |= (dtg << TIM_BDTR_DTG_Pos) | TIM_BDTR_MOE; }参数说明:ns 是期望的死区时间,240 来自 240MHz 定时器时钟;当 DTG ≤ 127 时死区就是线性段,钳位到 127 是为了避免跳到第二段编码后实际死区和预期相差太多。BDTR 里同时置 MOE,是因为改 DTG 不影响 MOE 状态,分开写反而容易在调试时误关主输出。注意死区只在互补输出对 CH1/CH1N 同时使能时才有意义;单端输出 CH1 时,BDTR 的 DTG 不产生任何效果。
4.3 中心对齐模式与 ADC 采样时刻
中心对齐模式是 H7 高级定时器在电机控制中被大量使用的模式,CMS 位设为非 0 后,计数器从 0 递增到 ARR,再递减回 0,产生对称三角波。和边沿对齐相比,PWM 频率计算变成:
f_pwm_center = f_timer / (2 * ARR * (PSC + 1))也就是同样 ARR 和 PSC 下,中心对齐的输出频率是边沿对齐的一半。之所以仍用它,是因为中心对齐 PWM 的电流纹波更小、EMI 更低,且上下桥臂的开关时刻对称。
中心对齐模式下比频率更重要的是 ADC 采样点。计数器在向上和向下两个方向跨越 CCR1 时,输出各翻转一次,所以一个 PWM 周期里有两次翻转。如果 ADC 在这个翻转点附近触发采样,采到的可能是开关噪声而不是真实信号。常规做法是:让 TIM1 的 TRGO 触发 ADC 注入组,MMS 选更新事件,这样采样点在 CNT=0 或 CNT=ARR 的时刻,避开翻转沿。配置如下。
// 中心对齐模式 1 TIM1->CR1 &= ~TIM_CR1_CMS_Msk; TIM1->CR1 |= (0x01U << TIM_CR1_CMS_Pos); // TRGO 选择更新事件 TIM1->CR2 &= ~TIM_CR2_MMS_Msk; TIM1->CR2 |= (0x00U << TIM_CR2_MMS_Pos); // ADC 触发源选择 TIM1_TRGO,使能注入组 ADC1->CR2 &= ~ADC_CR2_EXTEN_Msk; ADC1->CR2 |= (0x01U << ADC_CR2_EXTEN_Pos); // 上升沿触发 ADC1->CR2 &= ~ADC_CR2_EXTSEL_Msk; ADC1->CR2 |= (0x09U << ADC_CR2_EXTSEL_Pos); // TIM1_TRGO 对应编码逻辑说明:中心对齐模式下,CR1 的 DIR 位被硬件忽略,计数方向完全由 CMS 位决定。CMS=0b01 对应模式 1,匹配时刻的行为和 PWM 模式 1 相同,即向上匹配时输出翻转;CMS=0b10 是模式 2,向下匹配翻转;CMS=0b11 是模式 3,向上和向下都会产生比较事件并翻转。ADC 触发通常加一个小的硬件延迟,让采样点落在 PWM 电平稳定之后。如果你发现 ADC 采样值在某个占空比附近抖动明显,八成是采样时刻压在了翻转沿上。
5. 实机验证:频率测量、死区核对与 DMA 波形序列输出
5.1 用示波器直接判断初始化是否成功
PA8 接探头,示波器设上升沿触发,先看频率:预期 100kHz,周期 10μs。如果显示 200kHz,说明 APB2 预分频和定时器时钟倍频关系没对上;如果显示 50kHz,说明 ARR 少算了 1。再用光标量高电平时间,初始 CCR1=1200、ARR=2399,占空比应为 50%。把 tim1_set_duty(10000) 拉满,波形应当是恒高电平,如果看到 10μs 周期内存在极窄低脉冲,就把 CCR 设 ARR+1 再量一次。死区验证需要接 CH1 和 CH1N 两个通道,开余辉模式观察两路信号的交叉间隔,确认与 DTG 折算一致。
还有一个容易被忽略的点:把 BDTR 的 MOE 清 0,PA8 应立即变为低电平。这是高级定时器刹车功能的基本行为,测试通过后,后续接入故障保护信号才有信心。
5.2 用 DMA 连续刷新 CCR1,输出任意波形序列
某些应用要求 PWM 占空比按预先算好的序列自动变化,比如软启动、扫频,这时逐次写 CCR1 的 CPU 开销太大。常见做法是用 DMA 把内存里的波形表搬到 CCR1,以定时器更新事件作为 DMA 请求源。配置要点如下。
static uint16_t wave_table[256]; void dma_pwm_start(void) { // DMA1 和 DMAMUX1 挂在 AHB1 域 RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN | RCC_AHB1ENR_DMAMUX1EN; // DMAMUX1 通道 0 选择 TIM1 更新事件作为请求源 // 请求号以具体型号数据手册的 DMAMUX 映射表为准 DMAMUX1_Channel0->CCR = (36U << DMAMUX_CxCR_DMAREQ_ID_Pos); DMA1_Stream0->PAR = (uint32_t)(&TIM1->CCR1); DMA1_Stream0->M0AR = (uint32_t)wave_table; DMA1_Stream0->NDTR = 256; // 内存到外设、内存地址递增、16 位宽度 DMA1_Stream0->CR = DMA_SxCR_DIR_M2P | DMA_SxCR_MINC | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0 | DMA_SxCR_PL_1; DMA1_Stream0->CR |= DMA_SxCR_EN; }参数说明:PAR 指向 CCR1 的地址,M0AR 指向波形表;NDTR 是本次要传输的次数,即 256 个更新事件。DIR=M2P 表示从内存读数据写向外设寄存器;MINC 打开后每传一次,内存地址自动加 2(16 位宽度),而 PAR 不变,始终写入 CCR1。传输完一轮后 DMA 会置传输完成标志,需要重新配置 NDTR 才能再启动,这个复位逻辑要在中断里处理。使用 DMA 后,PWM 的波形序列更新延迟就是一次 DMA 请求的间隔,完全不受中断响应时间影响。
5.3 H750 到 H7 全系列的移植要点
这套寄存器逻辑不只适用于 H750。H743、H753 以及 H7A3 的 TIM1 寄存器布局完全一致,差异主要在 RCC 时钟树:不同型号的 PLL 最大频率、Flash 等待周期数、外部晶振范围都不同。移植时优先检查 system_clock_480mhz 里的 DIVM1/DIVN1/DIVP1 是否符合目标型号,再确认 APB2 分频后定时器时钟是否仍为 240MHz。GPIO 的 AF 编号、TIM 寄存器的位定义在 H7 系列内是统一的,所以只要时钟树正确,这篇的最小代码可以直接编译跑通。
本文还有配套的精品资源,点击获取