STM32F103 PB4重映射PWM+DMA避坑指南:从TIM3-CH1配置到波形稳定全流程

📅 发布时间:2026/7/15 21:20:45 👁️ 浏览次数:
STM32F103 PB4重映射PWM+DMA避坑指南:从TIM3-CH1配置到波形稳定全流程
STM32F103 PB4引脚重映射PWMDMA实战从原理到波形稳定的深度解析最近在为一个高速电机控制项目折腾STM32F103核心需求是通过PB4引脚输出一组高频、可动态调整的PWM波形。本以为是个常规操作没想到一脚踩进了“重映射”和“DMA配置”交织的深坑里。特别是当PWM频率推到接近1MHz再结合DMA进行波形数据流传输时各种诡异问题接踵而至波形毛刺、DMA传输卡死、甚至引脚根本没输出。经过几天的调试和查阅手册总算理清了从引脚重映射、定时器配置到DMA通道匹配这一整套链路里的关键陷阱。这篇文章我就把这些实战中积累的经验、容易忽略的细节以及可复用的代码框架分享出来希望能帮你绕过我走过的弯路。1. 理解PB4引脚的重映射困局与破局之道在STM32F103系列中PB4引脚默认并非一个“自由身”。它上电后的初始功能是JNTRST即JTAG调试接口的复位引脚。这意味着如果你不进行任何处理直接将其配置为通用推挽输出或者复用推挽输出AFIO很可能无法得到预期的PWM信号。这种“引脚功能冲突”是许多开发者遇到的第一个拦路虎。重映射的正确开启姿势远不止调用一个GPIO_PinRemapConfig那么简单。它需要一个严谨的初始化序列顺序错了时钟没开都会导致重映射失败。核心步骤有三步且环环相扣时钟使能是基石必须同时使能GPIOB的时钟和AFIO复用功能IO的时钟。AFIO模块掌管着引脚复用和重映射的配置寄存器缺少它的时钟后续的重映射配置指令根本无法写入生效。RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);释放默认功能关键易漏点在将PB4映射到TIM3_CH1之前必须先解除其JNTRST的默认绑定。这是通过GPIO_Remap_SWJ_NoJTRST这个重映射选项实现的。它属于“SWJ调试端口重映射”的一部分作用是禁用JNTRST功能但保留SWD调试接口PA13 PA14可用这对于我们日常调试至关重要。GPIO_PinRemapConfig(GPIO_Remap_SWJ_NoJTRST, ENABLE);执行目标重映射最后才是将TIM3的通道1部分重映射到PB4引脚。这里使用的是GPIO_PartialRemap_TIM3。GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);注意上述三步的顺序不能颠倒。必须先有时钟再解除默认功能最后进行目标映射。我曾试过跳过第二步结果PB4引脚始终被JTAG逻辑拉死无法输出任何波形。完成重映射后才能将PB4初始化为复用推挽输出模式GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 高频输出建议50MHz GPIO_Init(GPIOB, GPIO_InitStructure);2. TIM3高频PWM模式的核心配置与“有效电平”陷阱定时器是PWM的发生器。在高频如0.847MHz应用下定时器的预分频器PSC和自动重载值ARR设置需要精打细算。以72MHz的系统时钟为例要产生0.847MHz约847kHz的PWM每个计数周期约为1.18微秒。计算如下定时器时钟 72MHz / (PSC1)PWM频率 定时器时钟 / (ARR1)为了得到尽可能高的分辨率我们通常将PSC设为0即不分频此时定时器时钟为72MHz。那么ARR (72MHz / 847kHz) - 1 ≈ 84。所以设置TIM_Period 84TIM_Prescaler 0。但这里有一个更隐蔽的坑PWM模式1与模式2的理解偏差会导致逻辑完全相反。很多新手配置完发现占空比调节效果是反的问题就出在这里。PWM模式1向上计数时计数器CNT小于比较寄存器CCR时通道输出为“有效电平”大于等于CCR时输出“无效电平”。PWM模式2向上计数时计数器CNT小于比较寄存器CCR时通道输出为“无效电平”大于等于CCR时输出“有效电平”。这里的“有效电平”由TIM_OCPolarity参数定义High或Low。假设我们设置有效电平为高TIM_OCPolarity_High那么使用PWM模式1CCR值越大高电平时间越长占空比越大。使用PWM模式2CCR值越大高电平时间越短占空比越小。如果你希望CCR值直接正比于高电平时间请选择PWM模式1。配置代码如下TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 42; // 初始占空比50% (ARR84) TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 有效电平为高 TIM_OC1Init(TIM3, TIM_OCInitStructure);3. DMA与TIM3的通道绑定一个地址引发的“血案”使用DMA自动更新PWM占空比即TIM3-CCR1寄存器是实现复杂波形流输出的关键。这里最核心、也最容易出错的两个配置是外设基地址和DMA请求与通道的匹配。首先外设基地址绝不能写错。我们的目标是让DMA把内存中的数据搬运到TIM3-CCR1这个寄存器里。因此外设地址必须是这个寄存器的地址。注意TIM3-CCR1本身就是一个指向寄存器内容的指针我们需要再取它的地址即指针的指针作为DMA传输的目标。DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)((TIM3-CCR1)); // 正确取CCR1寄存器的地址 // DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)TIM3-CCR1; // 错误这是把CCR1的值当成了地址其次DMA通道与定时器请求的映射关系是硬连线不可随意指定。这是本文最核心的“避坑点”。STM32的DMA控制器DMA1有7个通道每个通道可以响应一组特定的外设请求。TIM3的更新事件Update和比较/捕获事件CC1分别映射到不同的DMA通道。查阅STM32F103参考手册的DMA章节可以找到如下映射关系部分DMA1 通道外设请求源 (从外设到DMA)通道 1...通道 2...通道 3TIM3_UP(定时器3更新)通道 4...通道 5...通道 6TIM3_CH1(定时器3通道1)这个表格揭示了问题的本质如果你使用TIM_DMACmd(TIM3, TIM_DMA_Update, ENABLE);来使能更新事件触发DMA那么必须使用DMA1的通道3因为只有通道3连接着TIM3_UP这个请求信号。如果你使用TIM_DMACmd(TIM3, TIM_DMA_CC1, ENABLE);来使能通道1比较匹配事件触发DMA那么必须使用DMA1的通道6。我最初犯的错误就是混用了我使能了TIM_DMA_Update却错误地将DMA初始化到了通道6。结果程序运行时DMA控制器一直在等待一个永远不会从通道6发出的请求信号导致DMA_GetFlagStatus永远等不到传输完成标志程序卡死。正确的配置组合应该是二选一组合A使用更新事件// 定时器配置 TIM_DMACmd(TIM3, TIM_DMA_Update, ENABLE); // DMA配置 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)((TIM3-CCR1)); DMA_Init(DMA1_Channel3, DMA_InitStructure); // 使用通道3组合B使用通道1事件// 定时器配置 TIM_DMACmd(TIM3, TIM_DMA_CC1, ENABLE); // DMA配置 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)((TIM3-CCR1)); DMA_Init(DMA1_Channel6, DMA_InitStructure); // 使用通道6提示两种方式有何区别TIM_DMA_Update在每个PWM周期计数器溢出时触发一次DMA传输适合周期性地更新下一个占空比值。TIM_DMA_CC1在计数器与CCR1匹配时触发理论上更精确但在某些高频或复杂场景下时序可能更敏感。对于大多数波形流输出使用更新事件通道3更为常见和稳定。4. 完整代码框架与高频下的稳定性调优将上述所有要点整合下面是一个经过实战检验的、针对高频PWMDMA输出的初始化函数框架。这个框架以使用TIM_DMA_Update和DMA1_Channel3为例。/** * brief 初始化TIM3 CH1 (重映射至PB4) 的PWM输出并配置DMA1通道3用于自动更新CCR1。 * param arr: 自动重装载值决定PWM频率。 * param psc: 时钟预分频系数。 * param pulseBuffer: 存储占空比数据的源内存数组地址。 * param bufferSize: 数据缓冲区大小单位半字16位。 * retval None */ void TIM3_PB4_PWM_DMA_Init(uint16_t arr, uint16_t psc, uint16_t *pulseBuffer, uint16_t bufferSize) { GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_OCInitTypeDef TIM_OCInitStruct; DMA_InitTypeDef DMA_InitStruct; /* 1. 开启相关时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); /* 2. PB4引脚重映射三步曲 */ GPIO_PinRemapConfig(GPIO_Remap_SWJ_NoJTRST, ENABLE); // 解除JNTRST GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); // 映射到TIM3_CH1 GPIO_InitStruct.GPIO_Pin GPIO_Pin_4; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStruct); /* 3. 配置TIM3时基产生目标频率的PWM */ TIM_TimeBaseInitStruct.TIM_Period arr; TIM_TimeBaseInitStruct.TIM_Prescaler psc; TIM_TimeBaseInitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseInitStruct); /* 4. 配置TIM3通道1为PWM模式1 */ TIM_OCInitStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse 0; // 初始占空比可由DMA立即更新 TIM_OCInitStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStruct); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能预装载确保波形稳定 /* 5. 配置DMA1通道3 (对应TIM3_UP请求) */ DMA_DeInit(DMA1_Channel3); DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)((TIM3-CCR1)); DMA_InitStruct.DMA_MemoryBaseAddr (uint32_t)pulseBuffer; DMA_InitStruct.DMA_DIR DMA_DIR_PeripheralDST; // 内存 - 外设 DMA_InitStruct.DMA_BufferSize bufferSize; DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址固定 DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; // 16位 DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; // 16位 DMA_InitStruct.DMA_Mode DMA_Mode_Normal; // 正常模式传输一次后停止 DMA_InitStruct.DMA_Priority DMA_Priority_VeryHigh; // 高频传输建议高优先级 DMA_InitStruct.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, DMA_InitStruct); /* 6. 使能TIM3的更新事件DMA请求 */ TIM_DMACmd(TIM3, TIM_DMA_Update, ENABLE); /* 7. 使能TIM3的DMA输出比较预装载可选增强稳定性*/ TIM_DMAConfig(TIM3, TIM_DMABase_CCR1, TIM_DMABurstLength_1Transfer); }高频稳定性调优技巧GPIO速度务必设置为GPIO_Speed_50MHz以支持高频信号边沿的快速翻转。DMA优先级在多个DMA通道共存或系统繁忙时将本通道优先级设为DMA_Priority_VeryHigh可以减少传输延迟导致的波形抖动。预装载机制同时使能定时器的TIM_OCPreload和TIM_ARRPreload如果修改频率可以确保参数在更新事件时同步加载避免PWM周期中间出现毛刺。内存数据对齐确保你的占空比数据缓冲区pulseBuffer在内存中的地址是半字2字节对齐的非对齐访问在某些情况下可能引发硬件错误或性能下降。调试手段当波形不稳定时首先用示波器检查PWM基础波形不开启DMA时是否纯净。然后开启DMA观察在DMA传输完成中断触发时波形是否出现异常跳变。这有助于判断问题是出在定时器配置、DMA传输时序还是内存数据本身。5. 实战流程与波形调试心法配置完成后实际启动DMA传输并输出波形的流程也需要规范以避免竞争条件或状态残留。下面是一个典型的传输函数/** * brief 启动一次PWM波形序列的DMA传输。 * param buffer: 占空比数据数组。 * param size: 数据个数。 * retval None */ void PWM_DMA_StartTransmission(uint16_t *buffer, uint16_t size) { /* 确保DMA和TIM处于禁用状态以便安全配置 */ DMA_Cmd(DMA1_Channel3, DISABLE); TIM_Cmd(TIM3, DISABLE); /* 清除可能存在的旧标志位 */ DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_HT3 | DMA1_FLAG_TE3); /* 重新设置要传输的数据量 */ DMA_SetCurrDataCounter(DMA1_Channel3, size); /* 重新指定内存地址如果缓冲区地址变化*/ DMA1_Channel3-CMAR (uint32_t)buffer; /* 先使能DMA通道 */ DMA_Cmd(DMA1_Channel3, ENABLE); /* 再使能定时器开始产生PWM并触发DMA请求 */ TIM_Cmd(TIM3, ENABLE); /* 等待本次DMA传输完成阻塞式可根据需要改为中断非阻塞*/ while(DMA_GetFlagStatus(DMA1_FLAG_TC3) RESET) { // 可在此处加入超时处理 } /* 传输完成停止定时器根据应用需求决定是否停止*/ TIM_Cmd(TIM3, DISABLE); DMA_Cmd(DMA1_Channel3, DISABLE); /* 清除传输完成标志 */ DMA_ClearFlag(DMA1_FLAG_TC3); }在调试过程中如果遇到波形异常可以按照以下清单进行排查无输出检查GPIOB和AFIO时钟是否使能。检查GPIO_Remap_SWJ_NoJTRST和GPIO_PartialRemap_TIM3两条重映射语句是否都执行且顺序正确。用万用表或逻辑分析仪检查PB4引脚是否仍被拉低可能被JTAG占用。波形频率不对核对TIM_Period和TIM_Prescaler的计算值。确认定时器时钟源APB1的频率是否正确通常是36MHz或72MHz取决于时钟树配置。占空比不受DMA控制或紊乱反复核对DMA_PeripheralBaseAddr确保是(TIM3-CCR1)。确认DMA通道与TIM_DMACmd使能的事件是否匹配通道3对应Update通道6对应CC1。检查DMA传输方向DMA_DIR_PeripheralDST、数据宽度半字、内存地址递增是否设置正确。检查内存缓冲区数据是否在传输前已被正确填充。DMA传输卡死90%的原因是上述的通道与请求不匹配。检查DMA初始化后是否调用了DMA_Cmd使能通道。检查定时器是否已使能TIM_Cmd。在while循环等待标志位处设置超时退出机制并打印错误信息便于定位。最后分享一个我调试时的小技巧在初始化完成后先不开启DMA而是手动设置一个固定的TIM3-CCR1值用示波器看是否能输出稳定的、占空比固定的PWM。这一步能隔离定时器本身的问题。确认基础PWM正常后再开启DMA并让DMA传输一个简单的递增数组如{10, 20, 30, 40, 50}用示波器观察波形占空比是否按预期阶梯变化。这种由简入繁、分步验证的方法能高效地定位问题所在的模块。