十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32G431 HRTIM工程实战:从蓝桥杯国赛源码到互补PWM配置

STM32G431 HRTIM工程实战:从蓝桥杯国赛源码到互补PWM配置 简介面向蓝桥杯嵌入式竞赛的STM32G431RBT6完整源码与项目说明压缩包适用于电子信息、计算机等专业备赛学生及需要课程设计、期末大作业或毕设参考的开发者。工程基于STM32G4系列HAL库编写包含驱动层与应用层完整项目可直接编译下载到开发板运行。压缩包共2000个文件以c源文件与h头文件为主辅以uvprojx工程配置、hex烧录文件及axf调试文件便于在Keil环境中打开调试整体大小116.81MB目录结构清晰可对照项目说明快速定位模块。所有源码均为调试通过的可运行版本并附带详细项目文档方便学习者理解初始化流程、外设配置与中断处理逻辑适合有一定基础、愿意钻研代码的读者作为参考资料深度实践。目前已有854人学习下载适合备赛与课程设计参考。1. 拆开这份G431RBT6工程HRTIM才是蓝桥杯国赛真正的分水岭解压“蓝桥杯嵌入式STM32G431RBT6程序源码项目说明.zip”后第一眼看到的是Source.uvguix.Administrator和重复出现的stm32g4xx_hal_hrtim.c。这个文件名组合很典型uvguix只是Keil的界面状态真正有价值的是HRTIM驱动。在蓝桥杯嵌入式竞赛里STM32G431RBT6已经连续多届成为指定芯片170 MHz的Cortex-M4F加上高分辨率定时器HRTIM让试卷里可以考数字电源、半桥驱动这类需要纳秒级PWM的题目。很多备赛者用普通定时器做PWM能过关但一碰到HRTIM就暴露了问题时基结构、死区配置、输出通道选择和触发链路都对不上。这篇文章就顺着这份源码与项目说明把工程搭建、HRTIM初始化、竞赛外设联动和验证排错完整走一遍。适合备赛蓝桥杯嵌入式、做课程设计以及需要搞懂HRTIM的嵌入式开发者。2. 工程骨架Source.uvguix与STM32G4 HAL库的项目说明拿到资源后最容易踩的第一个坑是把Source.uvguix.Administrator当成Keil工程文件双击打开。实际上uvguix只记录窗口布局、断点位置和寄存器观察界面删掉它也不影响代码编译。真正的工程文件是.uvprojx而这个压缩包为了控制体积把编译中间文件保留了却漏掉了主工程文件。所以不要指望双击就能跑按下面的方式重建工程顺便把G431的HAL库依赖理顺。2.1 认清文件结构哪几个文件决定工程完整性蓝桥杯嵌入式历年真题给的官方工程通常很完整但网上下载的“参考源码”经常缺东少西。打开压缩包后先按下面表格做一次文件体检能省下后面大量排错时间。文件作用是否必须Source.uvguix.AdministratorKeil MDK窗口布局XML格式记录断点和寄存器窗口否Source.uvprojx真正的Keil工程保存芯片型号、宏定义、源文件列表是Source.axf上一次编译生成的ELF调试文件下载器会用但源码包可留可删否stm32g4xx_hal_hrtim.cHRTIM外设HAL驱动源码本资源的业务核心是判断一个源码包是否可以直接用关键看uvprojx有没有、HAL库驱动文件路径指向哪里。如果只有uvguix就说明作者整理时只导出了界面状态你的第一个任务不是写代码而是在Keil里重建工程并把所有HAL库源文件加回来。2.2 重建Keil工程并接入HRTIM驱动常见做法是在MDK中新建工程Device选择STM32G431RBTx然后手动把Drivers/STM32G4xx_HAL_Driver/Src下需要的源文件加入工程。这一步不要只加stm32g4xx_hal_hrtim.cHAL库的底层依赖是一个闭合集合至少还需要stm32g4xx_hal.c、stm32g4xx_hal_rcc.c、stm32g4xx_hal_gpio.c、stm32g4xx_hal_cortex.c。头文件路径指向Drivers/STM32G4xx_HAL_Driver/Inc否则编译时会出现cannot open file stm32g4xx_hal_conf.h。#include stm32g4xx_hal.h void Error_Handler(void) { __disable_irq(); while (1) { } }这段代码是HAL工程公用的错误陷阱。__disable_irq()会关闭全局中断让程序停在错误点。调试时这样做很好定位问题但正式比赛里如果掉进这里整个系统会静默死机所以实际项目中我会在Error_Handler里加一个OLED显示和串口打印把错误源透传出来。2.3 时钟树配置把24 MHz外部晶振变成170 MHz主频G431的HRTIM挂在APB2域但它的计数时钟来自PLL1而不是简单的APB2总线分频。这一点不搞清楚后面的死区时间怎么算都不对。下面是一份常见的SystemClock_Config模板void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM 6; RCC_OscInitStruct.PLL.PLLN 85; RCC_OscInitStruct.PLL.PLLP 2; RCC_OscInitStruct.PLL.PLLQ 6; RCC_OscInitStruct.PLL.PLLR 2; if (HAL_RCC_OscConfig(RCC_OscInitStruct) ! HAL_OK) { Error_Handler(); } RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_4) ! HAL_OK) { Error_Handler(); } }关键在PLLM、PLLN、PLLR的组合外部24 MHz先除以6得到4 MHz的PLL输入参考PLLN85把VCO倍频到340 MHz再除以PLLR2得到170 MHz系统时钟。FLASH_LATENCY_4表示Flash等待周期设置为4这是170 MHz主频下的标准取值。如果开发板外部晶振是8 MHz把PLLM改成2即可其余保持不变。很多源码包自带的HAL_RCC_OscConfig在HSE启动失败后会卡死建议把返回值打印出来确认外部晶振是否起振。3. HRTIM定时器配置从stm32g4xx_hal_hrtim.c到双通道互补PWM3.1 高分辨率定时器的底层逻辑普通定时器TIM1虽然也能输出互补PWM但受限于计数时钟和预分频精度在高频开关电源、无刷电机矢量控制这类场景下很难达到亚微秒级分辨率。HRTIM1内部有DLL倍频单元可以把PLL1时钟倍频到GHz量级再通过Timer A到Timer E等多个时基单元做独立或同步波形生成。蓝桥杯嵌入式国赛真题里反复出现的“多路PWM移相”“死区互补PWM”等描述本质就是考察对HRTIM时基结构、比较值和输出通道之间映射关系是否熟练。第十七届蓝桥杯嵌入式国赛真题也延续了这个出题导向HRTIM在题目里不再是选配外设而是和ADC、外部中断结合在一起的完整控制链路。我在准备蓝桥杯嵌入式历年真题时发现很多考生仍然用TIM1去模拟HRTIM结果分辨率和触发链路都满足不了题目要求。与其临时换芯片不如把源码包里HRTIM初始化流程吃透。HAL库把寄存器封装成了层状结构代码顺序有严格讲究先配时基再配定时器工作模式接着配输出通道最后才启动。这个顺序和F1系列HAL库相似但参数命名完全不通用。3.2 最小可运行的HRTIM初始化代码这段代码是从源码包中提炼出的核心子集去掉了与PWM无关的Fault和Burst功能。项目说明中作者也强调先看这个文件再动其他外设因为它由HAL_HRTIM_TimeBaseInit、HAL_HRTIM_TimerInit、HAL_HRTIM_OutputInit三步构成顺序不能颠倒。HRTIM_HandleTypeDef hrtim1; static void MX_HRTIM1_Init(void) { HRTIM_TimeBaseCfgTypeDef sTimeBaseCfg {0}; HRTIM_TimerCfgTypeDef sTimerCfg {0}; HRTIM_OutputCfgTypeDef sOutputCfg {0}; HRTIM_DeadTimeCfgTypeDef sDeadTimeCfg {0}; /* 1. 时基主时基提供周期所有子定时器共享 */ sTimeBaseCfg.Period 8500 - 1; /* PWM计数周期 */ sTimeBaseCfg.RepetitionCounter 0; sTimeBaseCfg.PrescalerRatio HRTIM_PRESCALERRATIO_DIV2; /* 计数时钟85MHz */ sTimeBaseCfg.Mode HRTIM_MODE_CONTINUOUS; HAL_HRTIM_TimeBaseInit(hrtim1, HRTIM_TIMERINDEX_MASTER, sTimeBaseCfg); /* 2. 定时器A使用主时基不从外部同步 */ sTimerCfg.InterruptRequests 0; sTimerCfg.DACTrigger HRTIM_TIMDACTRIGGER_NONE; sTimerCfg.ResetUpdate HRTIM_TIMRESETUPDATE_MASTER; sTimerCfg.StartOnSync HRTIM_TIMER_STARTONSYNC_MASTER; sTimerCfg.ResetOnSync HRTIM_TIMER_RESETONSYNC_MASTER; sTimerCfg.UpdateGating HRTIM_TIM_UPDATEGATING_INDEPENDENT; HAL_HRTIM_TimerInit(hrtim1, HRTIM_TIMERINDEX_TIMER_A, sTimerCfg); /* 3. 输出通道TA1/TA2互补输出 */ sOutputCfg.Polarity HRTIM_OUTPUTPOLARITY_HIGH; sOutputCfg.IdleState HRTIM_OUTPUTIDLESTATE_INACTIVE; sOutputCfg.IdleLevel HRTIM_OUTPUTIDLELEVEL_INACTIVE; sOutputCfg.FaultState HRTIM_OUTPUTFAULTSTATE_INACTIVE; sOutputCfg.ChopperEnable HRTIM_OUTPUTCHOPPER_DISABLED; sOutputCfg.BurstModeEntry HRTIM_OUTPUTBURSTMODE_MAIN; HAL_HRTIM_OutputInit(hrtim1, HRTIM_TIMERINDEX_TIMER_A, HRTIM_OUTPUT_TA1, sOutputCfg); HAL_HRTIM_OutputInit(hrtim1, HRTIM_TIMERINDEX_TIMER_A, HRTIM_OUTPUT_TA2, sOutputCfg); /* 4. 死区上升沿和下降沿各约94ns */ sDeadTimeCfg.RisingValue 1; sDeadTimeCfg.RisingPrescaler HRTIM_DEADTIMEPRESCALERRATIO_MUL8; sDeadTimeCfg.RisingSign HRTIM_DEADTIME_SIGN_POSITIVE; sDeadTimeCfg.FallingValue 1; sDeadTimeCfg.FallingPrescaler HRTIM_DEADTIMEPRESCALERRATIO_MUL8; sDeadTimeCfg.FallingSign HRTIM_DEADTIME_SIGN_POSITIVE; HAL_HRTIM_DeadTimeConfig(hrtim1, HRTIM_TIMERINDEX_TIMER_A, sDeadTimeCfg); /* 5. 比较值占空比50% */ HAL_HRTIM_WaveformCompareConfig(hrtim1, HRTIM_TIMERINDEX_TIMER_A, HRTIM_COMPARATOR_1, 4250); /* 6. 启动 */ HAL_HRTIM_BaseStart(hrtim1, HRTIM_TIMERINDEX_TIMER_A); }第1步把主时基周期设为8499为什么减1因为HRTIM周期寄存器表示的是从0计数到目标值的上限0到8499共8500个时钟周期。计数时钟来自170 MHz的PLL1经PrescalerRatioDIV2分频后得到85 MHz所以PWM频率是85 MHz除以8500等于10 kHz。第2步HRTIM_TIM_UPDATEGATING_INDEPENDENT让定时器A独立更新避免主时基复位时重载被跳过。第3步输出通道配置里IdleLevel和IdleState容易混淆前者是空闲时的电平值后者是保护触发时是否允许输出。第4步死区配置RisingPrescalerMUL8表示死区时钟预分频扩大8倍此时死区步长约等于1/85 MHz乘8再乘1约94 ns接近100 ns目标。第5步比较值4250正好是周期一半占空比50%。第6步HAL_HRTIM_BaseStart只启动时基输出会自动跟随波形发生器翻转不需要额外调用输出使能函数。3.3 参数对照表与触发ADC配置要快速改出不同PWM频率按下面表格调整Period和PrescalerRatio即可目标PWM频率Period寄存器值PrescalerRatio计数时钟10 kHz8499DIV285 MHz20 kHz4249DIV285 MHz100 kHz1699DIV1170 MHz目标死区RisingValueRisingPrescaler约94 ns1MUL8约188 ns2MUL8约11.7 ns1DIV1这些值基于PLL1输出170 MHz计算实际以数据手册和示波器测量为准。HRTIM的另一大用途是触发ADC转换。蓝桥杯国赛闭环控制题目经常要求用PWM中心对齐触发ADC采样保证在计数器周期中点采集电流信号避开开关噪声。可以这样打开周期中断HAL_HRTIM_ActivateTimerInterrupt(hrtim1, HRTIM_TIMERINDEX_TIMER_A, HRTIM_TIM_IT_PER); HAL_NVIC_SetPriority(HRTIM1_TIMER_A_IRQn, 1, 0); HAL_NVIC_EnableIRQ(HRTIM1_TIMER_A_IRQn);HRTIM_TIM_IT_PER对应周期事件和PWM周期同步中断回调里按需启动ADC采样点会固定在这个周期的特定位置。优先级的抢占值设为1留0给更紧急的故障保护中断避免ADC回调打断HRTIM周期事件。4. 竞赛实战扩展ADC采样、按键调节和串口调试的联动改法4.1 变量修饰符和中断回调才是主战场蓝桥杯嵌入式历年的套路基本是初始化外设然后while(1)里轮询按键、刷新LCD同时要求PWM实时调整。很多人把HRTIM初始化和ADC初始化写在main函数里但采样在中断里共享变量没有volatile修饰被编译器优化后输出永远不变。正确的做法是把共享变量声明为volatile uint32_t duty_cycle;。volatile uint32_t duty_cycle 4250; void HAL_HRTIM_TimerPeriodElapsedCallback(HRTIM_HandleTypeDef *hhrtim, uint32_t TimerIdx) { if (TimerIdx HRTIM_TIMERINDEX_TIMER_A) { HAL_ADC_Start(hadc1); /* 每个PWM周期启动一次ADC */ } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc hadc1) { adc_value (adc_value hadc1.Instance-DR) 1; /* 滑动滤波 */ } }HAL_HRTIM_TimerPeriodElapsedCallback是HAL库定时器周期中断的弱回调我们在自己代码里实现同名函数后系统会优先调用这个版本。回调第一行必须判断TimerIdx因为同一个HRTIM里多个Timer都会进入这个回调不加过滤会出现数据错乱。ADC转换完成回调里读DR寄存器并做滑动滤波1代替除以2在竞赛代码里虽然省不了多少时间但能体现对寄存器操作的理解。4.2 主循环里更新占空比的正确时序不要在中断里调用HAL_HRTIM_WaveformCompareConfig因为HAL库会等待寄存器标志位在中断上下文里容易阻塞其他高优先级任务。正确做法是按键按下时只置位标志位主循环检测到后再修改HRTIMwhile (1) { if (key_pressed KEY1_FLAG) { if (duty_cycle 8000) duty_cycle 50; HAL_HRTIM_WaveformCompareConfig(hrtim1, HRTIM_TIMERINDEX_TIMER_A, HRTIM_COMPARATOR_1, duty_cycle); key_pressed ~KEY1_FLAG; } LCD_DisplayStringLine(line, Duty:%d, duty_cycle); }按键扫描一般用10 ms间隔消抖key_pressed由定时器中断置1主循环里发现标志位改变一次就执行一次比较值更新这样不会出现边按按键边刷屏导致的卡顿。LCD刷新最好和PWM更新解耦比如固定每50 ms刷一次屏幕否则每次更新占空比都刷新LCD屏幕刷新时间会掩盖按键扫描的触发频率。4.3 源码包目录规划面对蓝桥杯多题变化怎么组织项目说明里写的是“作为参考资料”但真正备赛时源码的组织方式决定了你能不能在考场上快速改功能。我建议把这份源码和后续自己添加的外设按下面表格分层目录用途对应模块Core/Src用户主程序main.c、gpio.c、adc.cApp业务逻辑层key_scan、lcd_ui、pidDrivers/STM32G4xx_HAL_DriverST官方HAL库驱动源文件ProjectKeil工程文件uvprojx、uvguix这样组织以后即使Keil工程文件丢失也可以用CubeMX重新生成一份再把App层文件加进去。蓝桥杯嵌入式源码包常见问题在于只有Core/Src和Drivers两层一旦需要移植到G474或G491调试时间全花在找文件上。我习惯额外保留一个Doc目录把改动过的寄存器值、中断优先级和常见报错记录成Markdown形成自己的项目说明比压缩包里那几行描述有用得多考场上能直接节省调试时间。5. 验证与排错用逻辑分析仪核对HRTIM波形并用串口CSV画曲线5.1 快速验证HRTIM是否输出正确直接用逻辑分析仪量PA8和PA9是最快的方法。PA8配置成TA1输出PA9配置成TA2输出正常时应看到两路互补波形中间有约94 ns的死区缺口。如果两路波形完全重叠说明死区配置没生效如果PA9没输出先检查GPIO复用是否设成了GPIO_AF1_HRTIM1。逻辑分析仪采样率建议取PWM频率的10倍以上例如10 kHz PWM用200 kHz采样率就能清楚看到死区间隔。5.2 三个高频调用坑现象排查方向程序卡在HAL_HRTIM_Init检查PLL1是否真正输出170 MHz用HAL_RCC_GetSysClockFreq()打印输出只有单路PWM检查TA2的GPIO复用是否漏配PA9的AF不是默认引脚复用死区设置与期望不符Prescaler和Value组合超出范围参考第3.3节的死区表格重新算HAL_HRTIM_Init内部会读时钟配置如果主频异常会反复触发回调。之前我在外部8 MHz晶振版本上做过测试必须把PLLM从6改成2否则HRTIM周期全乱死区时间也会跟着偏移。5.3 串口CSV加Python没有示波器也能看控制环曲线调试闭环时串口打印是成本最低的方案。重定向fputc后在中断回调或主循环里输出int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart2, (uint8_t *)ch, 1, 100); return ch; } printf(%u,%u\r\n, (unsigned)adc_value, (unsigned)duty_cycle);PC端用Python实时画曲线import serial, matplotlib.pyplot as plt ser serial.Serial(COM5, 115200, timeout1) adc_data, duty_data [], [] plt.ion() try: while True: line ser.readline().decode().strip() if not line: continue vals line.split(,) if len(vals) 2: adc_data.append(int(vals[0])) duty_data.append(int(vals[1])) plt.clf() plt.plot(adc_data, labeladc) plt.plot(duty_data, labelduty) plt.legend() plt.pause(0.02) except KeyboardInterrupt: ser.close()这段脚本按逗号解析串口数据分别保存ADC采样值和占空比到两个列表。plt.ion()开交互模式避免每次绘图都阻塞主循环。PySerial和matplotlib需要提前用pip安装。没有示波器时这套方案能直观看到控制环里的震荡和超调。把波特率提高到460800后还可以同时打印多路数据每个字段加逗号Python端按序号拆分调试效率和精度都会再上一个台阶。本文还有配套的精品资源点击获取
返回列表