AT32F421定时器实战:2秒按键检测切换PWM生成与捕获模式(附完整代码)

📅 发布时间:2026/7/7 13:44:17 👁️ 浏览次数:
AT32F421定时器实战:2秒按键检测切换PWM生成与捕获模式(附完整代码)
AT32F421定时器实战2秒按键检测切换PWM生成与捕获模式附完整代码在嵌入式开发中定时器堪称微控制器的“心脏”它不仅是精准计时的基石更是实现PWM脉冲宽度调制输出、输入捕获、电机控制等复杂功能的核心外设。对于AT32F421这类资源紧凑但性能不俗的MCU深入掌握其定时器的高级玩法往往意味着能在产品设计中实现更精巧的交互逻辑与更高效的控制策略。今天我们就来探讨一个极具实用价值的场景如何利用AT32F421的基础定时器TMR6和通用定时器TMR3构建一个基于超时检测的双模式自动切换系统。这个系统会在上电后启动一个2秒的“决策窗口”等待用户按键。如果2秒内按下按键MCU就切换到PWM生成模式驱动外部设备如果超时未按键则自动进入PWM捕获模式分析外部输入的信号。整个过程涉及GPIO中断、定时器中断、PWM参数计算、从模式配置等多个关键知识点我们将手把手拆解并提供可直接移植的完整代码。1. 项目架构与核心设计思路在动手写代码之前理清整个系统的运行逻辑至关重要。我们的目标不是简单地让两个定时器工作而是让它们协同完成一个带有状态判断的流程。这要求我们对硬件资源和软件流程有一个全局的规划。核心状态机是整个项目的灵魂。系统上电复位后进入“等待决策”状态。在此状态下TMR6被配置为单次定时模式定时时长为2秒。同时MCU会持续轮询或通过外部中断监听连接在PC13引脚上的按键。这里存在两条并行的“时间线”一条是TMR6的硬件定时线另一条是用户按键触发的事件线。系统的最终状态由这两条线谁先触发来决定。提示选择轮询还是外部中断检测按键取决于具体应用对响应速度和系统功耗的要求。在本例中由于主循环在等待期间没有其他繁重任务使用轮询方式代码更简洁且能避免中断嵌套带来的复杂性。为了实现状态的无缝切换我们需要对两个定时器进行清晰的“职责划分”TMR6基础定时器 仅用于产生精准的2秒超时。基础定时器通常只有最基本的计数和溢出中断功能没有PWM输出/输入捕获通道因此非常适合这种单纯的定时任务。TMR3通用定时器 这是一个功能强大的定时器我们将让它“身兼二职”。在PWM生成模式下它使用通道1PA6输出指定频率和占空比的方波在PWM捕获模式下它使用通道2PA7测量外部输入信号的频率和占空比。这种设计巧妙地利用了不同定时器的特性并将一个通用定时器通过软件逻辑重配置实现了两种功能体现了嵌入式开发中“资源复用”的思想。2. 硬件与软件环境搭建工欲善其事必先利其器。一个稳定高效的开发环境能让我们更专注于逻辑本身而非纠缠于工具链的配置问题。硬件清单主控芯片 AT32F421C8T7其他AT32F421系列型号亦可需注意引脚兼容性。调试器/编程器 AT-Link或兼容的SWD调试器这是下载程序和在线调试的必备工具。USB转TTL模块 用于连接MCU的USART引脚如PA9/TX, PA10/RX实现printf调试信息输出这是排查问题的“眼睛”。按键 一个轻触开关一端接PC13另一端接地。MCU内部配置上拉电阻这样按键未按下时PC13为高电平按下时为低电平。LED可选 接在PC13或其他GPIO上用于在PWM生成模式时作为状态指示。注意本例中PC13在模式切换后会被重配置为推挽输出驱动LED实现了引脚复用。示波器/逻辑分析仪强烈推荐 用于直观观察PA6输出的PWM波形以及PA7输入的信号是验证功能最直接的手段。软件工具链 我个人的开发环境组合是VSCode EIDE插件 Cortex-Debug。EIDE为AT32系列提供了非常好的项目管理和构建支持而Cortex-Debug则与OpenOCD无缝集成实现源码级调试。当然你也可以使用官方的AT32 IDE或Keil MDK核心代码逻辑是通用的。# 一个简单的构建命令示例在EIDE中通常自动完成 arm-none-eabi-gcc -mcpucortex-m4 -mthumb -O2 -c main.c -o main.o arm-none-eabi-gcc -mcpucortex-m4 -mthumb -O2 -c pulse_capture.c -o pulse_capture.o arm-none-eabi-gcc -mcpucortex-m4 -mthumb -T link.ld *.o -o project.elf -lc -lm -lnosys arm-none-eabi-objcopy -O binary project.elf project.bin工程初始化要点系统时钟配置 首先调用system_clock_config()将系统时钟设置为最高频率例如120MHz。定时器的计数时钟来源于AHB总线时钟频率直接影响定时精度。延时函数初始化 基于SysTick的delay_init()必须紧随时钟配置之后确保后续的delay_ms()等函数准确工作。串口初始化 在初始化任何可能调用printf的中断服务程序之前务必先完成uart_print_init(115200)。否则如果定时器初始化过程中触发了中断并在中断里调用了未初始化的串口打印程序将卡死。这是一个非常隐蔽的坑。3. 关键模块实现与代码精讲理解了整体框架我们来深入各个模块的代码实现。这里不会平铺直叙地罗列代码而是聚焦于几个最容易出错的难点和关键设计选择。3.1 TMR62秒超时检测的稳健实现TMR6的配置目标很明确产生一次2秒的溢出中断。计算公式是定时器溢出时间Tout (pr 1) * (div 1) / TMRx_clk。其中TMRx_clk等于AHB_Freq假设定时器时钟不分频。假设AHB时钟为120MHz我们需要Tout 2s。一种直观的想法是让pr取最大值65535然后计算div。div (Tout * TMRx_clk) / (pr 1) - 1 (2 * 120e6) / 65536 - 1 ≈ 3661。这个值在16位整数范围内是可行的。但为了获得更精细的定时分辨率尽管对2秒来说意义不大我们也可以反过来先设定一个较小的分频div再计算pr。例如设置div 59999则pr (Tout * TMRx_clk) / (div 1) - 1 (2 * 120e6) / 60000 - 1 3999。在代码中我们采用了后一种方式将pr设为9999div根据AHB频率动态计算div (crm_clocks_freq_struct.ahb_freq / 5000) - 1。这样确保了无论AHB频率如何配置定时都是准确的5kHz计数频率溢出周期为(99991)/5000 2秒。// main.c 片段 tmr_base_init(TMR6, 9999, (crm_clocks_freq_struct.ahb_freq / 5000) - 1); tmr_cnt_dir_set(TMR6, TMR_COUNT_UP); tmr_interrupt_enable(TMR6, TMR_OVF_INT, TRUE); // 关键启用单脉冲模式计数到溢出后自动停止 tmr_one_cycle_mode_enable(TMR6, TRUE); nvic_irq_enable(TMR6_GLOBAL_IRQn, 1, 0); tmr_counter_enable(TMR6, TRUE);这里有一个至关重要的细节tmr_base_init()函数在设置周期和分频寄存器时可能会软件触发一次溢出事件。如果此时溢出中断已使能且中断服务程序ISR中使用了未初始化的外设如串口系统就会崩溃。因此务必遵循“先初始化所有依赖的外设再初始化并开启定时器中断”的顺序。3.2 PWM生成模式精度优先的动态参数计算PWM生成的难点不在于配置寄存器让引脚输出方波而在于如何根据用户指定的周期微秒和占空比百分比高精度地计算出定时器的pr自动重载值、div预分频值和CxDT捕获/比较值。这涉及到在16位整数约束下的优化问题。我们的sigle_pwm_gen函数引入了一个accuracy_priority_type枚举让调用者选择是周期精度优先还是占空比精度优先。占空比精度优先DUTYCYCLE 将pr固定为最大值0xFFFF65535。此时CxDT的值可以在0到65535之间任意设置占空比分辨率高达1/65536 ≈ 0.0015%。然后通过调整div来匹配目标周期。这种方法适用于对占空比控制要求极高的场景如LED调光、数字电源。周期精度优先PERIOD 将div设置为0不分频。此时定时器以最高的系统时钟计数通过调整pr来匹配目标周期可以获得极高的频率分辨率。但此时pr值较小CxDT的取值范围受限占空比精度会下降。这适用于需要精确频率输出的场合如音频信号生成、通信波特率时钟。下面是核心计算逻辑的代码片段// pulse_capture.c 片段 if(priority DUTYCYCLE) { pr 0xFFFF; // 固定PR为最大值最大化占空比精度 // 计算分频值div Tout (pr1)*(div1) / TMR_Clk div period_us * (float32_t)crm_clocks_freq_struct.ahb_freq / 1000000 / (pr 1) 0.5 - 1; // 检查div是否在0~65535范围内 if(div 0 || div 0xFFFF) { printf(Error: Period out of range for duty-cycle priority mode.\r\n); return 0xFF; } } else { // PERIOD priority div 0; // 不分频最大化周期精度 // 计算自动重载值pr pr period_us * (float32_t)crm_clocks_freq_struct.ahb_freq / 1000000 / (div 1) 0.5 - 1; // 检查pr是否在0~65535范围内 if(pr 0 || pr 0xFFFF) { printf(Error: Period out of range for period priority mode.\r\n); return 0xFE; } } // 根据计算出的pr和div初始化定时器基础 tmr_base_init(TMR3, pr, div); // 设置PWM模式根据脉冲类型选择PWM mode A或B // 设置通道比较值CxDT duty_cycle * (pr 1) // ... 后续通道配置代码计算出的pulse_width即CxDT值决定了高电平的持续时间。这里需要注意边界情况当占空比设置为100%时理论上CxDT应等于pr1但受寄存器限制最大只能设置为pr因为CxDT是16位寄存器当它等于pr时输出在下一个周期开始前翻转实际占空比为pr/(pr1)。这是所有基于比较匹配的PWM生成方式的一个固有特性。3.3 PWM捕获模式利用从模式实现精准测量测量外部PWM信号的频率和占空比是定时器输入捕获功能的典型应用。AT32F421的通用定时器支持丰富的从模式Slave Mode我们可以利用复位从模式Reset Mode来简化测量逻辑。工作原理通道配置 将TMR3的通道2PA7配置为输入捕获模式捕获上升沿。从模式配置 设置定时器的触发源Trigger Input Select为通道2的滤波后数据输入TMR_SUB_INPUT_SEL_C2DF2。然后将从模式Sub Mode设置为复位模式TMR_SUB_RESET_MODE。测量过程当第一个上升沿到来时通道2捕获到当前的计数器值CNT同时由于配置了复位从模式这个上升沿信号作为触发信号会将计数器CNT清零。当第二个上升沿即下一个周期到来时通道2再次捕获CNT值。此时CNT的值恰好就是从上一个上升沿到当前上升沿所经过的计数周期数这个值直接对应输入信号的整个周期。同时我们使能通道1另一个通道捕获下降沿或配置为PWM输入模式可自动捕获上升沿和下降沿。通道1捕获到的值就是高电平期间的计数周期数。计算 有了周期计数值period_cnt和高电平计数值pulse_cnt频率和占空比就很容易计算了频率 AHB时钟频率 / period_cnt占空比 (pulse_cnt * 100) / period_cnt这种方法的妙处在于利用硬件从模式自动清零计数器省去了软件在中断中手动清零CNT的步骤并且避免了因中断响应延迟带来的测量误差精度更高。// pulse_capture.c 中的捕获初始化函数片段 void sigle_pwm_cap(void) { tmr_input_config_type tmr_ic_init_structure; tmr_input_default_para_init(tmr_ic_init_structure); // 配置通道2为输入捕获上升沿触发 tmr_ic_init_structure.input_channel_select TMR_SELECT_CHANNEL_2; tmr_ic_init_structure.input_polarity_select TMR_INPUT_RISING_EDGE; tmr_pwm_input_config(TMR3, tmr_ic_init_structure, TMR_CHANNEL_INPUT_DIV_1); // 关键配置从模式 tmr_trigger_input_select(TMR3, TMR_SUB_INPUT_SEL_C2DF2); // 选择通道2作为触发源 tmr_sub_mode_select(TMR3, TMR_SUB_RESET_MODE); // 设置为复位模式 tmr_sub_sync_mode_set(TMR3, TRUE); // 使能从同步模式 tmr_counter_enable(TMR3, TRUE); tmr_interrupt_enable(TMR3, TMR_C2_INT, TRUE); // 使能通道2捕获中断 }在中断服务程序中我们读取两个通道的捕获值并进行计算结果存入全局变量供主循环打印。3.4 中断服务程序与全局状态管理中断是嵌入式系统的“神经末梢”处理不当极易导致系统不稳定。在本项目中我们涉及TMR6的溢出中断和TMR3的捕获/比较中断。中断优先级NVIC配置 我们使用了nvic_priority_group_config(NVIC_PRIORITY_GROUP_4)即4位抢占优先级0位子优先级。在这种分组下抢占优先级数值越小优先级越高。我们将TMR6的中断抢占优先级设置为1TMR3设置为0意味着TMR3的中断可以打断TMR6的中断。这通常是合理的因为PWM捕获对实时性要求更高需要及时响应边沿信号。中断服务程序ISR的编写规范及时清除标志位 进入ISR后应首先读取并清除对应的中断标志位防止重复进入同一中断。避免耗时操作 ISR中应执行最精简的操作。例如我们的TMR3捕获中断中只进行数值读取、简单计算和赋值而将耗时的printf输出放在主循环中。使用__IO类型修饰全局变量 在ISR和主循环之间共享的变量如PWMin_frequency,PWMin_dutycycle,TIM6OVF应使用__IOvolatile关键字定义告诉编译器不要对这些变量进行优化确保每次访问都从内存中读取最新值。// at32f421_int.c 中的TMR3中断处理片段 void TMR3_GLOBAL_IRQHandler(void) { uint32_t capture 0; if (tmr_flag_get(TMR3, TMR_C2_FLAG) ! RESET) { tmr_flag_clear(TMR3, TMR_C2_FLAG); capture tmr_channel_value_get(TMR3, TMR_SELECT_CHANNEL_2); // 获取周期值 if (capture ! 0) { // 计算占空比和频率存入全局变量 PWMin_dutycycle (tmr_channel_value_get(TMR3, TMR_SELECT_CHANNEL_1) * 100) / capture; PWMin_frequency crm_clocks_freq_struct.ahb_freq / capture; } } // ... 处理其他中断标志 }4. 调试技巧与常见问题排查即使代码逻辑清晰在实际硬件调试中仍会遇到各种问题。分享几个我在调试这个项目时遇到的坑和解决思路。问题一诡异的“幽灵中断”在调试PWM生成时我通过GDB查看TMR3的寄存器发现即使我通过IDEN寄存器明确禁止了溢出中断OVF IE位为0ISTS寄存器中的溢出中断标志OVF IF位仍然会被置位并且似乎能触发中断服务程序。这看起来违反了常理。寄存器地址观察到的值预期值可能原因分析TMR3_IDEN0x4000040C0x0002 (仅使能了C1中断)0x0002配置正确OVF中断未使能TMR3_ISTS0x400004100x0001 (OVF标志位被置1)0x0000OVF标志位不应被置起经过反复查阅数据手册和测试我发现问题根源在于tmr_base_init()函数在设置pr自动重载寄存器时会内部产生一个软件更新事件这个事件会导致溢出标志UIF被置位。即使中断未使能这个标志位依然存在。而我的中断服务程序开头是检查所有可能的中断标志并清除它们。当程序运行到清除TMR_OVF_FLAG时虽然这个中断并未真正“触发”进入ISR但清除标志位的操作依然执行了。在调试器中单步执行时看到的就是标志位被意外清除的现象造成了“幽灵中断”的错觉。解决方案 在初始化序列中在调用tmr_base_init()之后、使能计数器之前手动清除一下溢出标志位tmr_flag_clear(TMR3, TMR_OVF_FLAG);。这是一个很好的编程习惯可以确保定时器从一个干净的状态开始工作。问题二主循环中的printf导致输出混乱在PWM捕获模式下我在主循环中不断打印测量到的频率和占空比。同时在TMR3的捕获中断里也使用了printf来调试。结果发现串口输出变得杂乱无章两个地方的打印信息相互穿插。这是因为printf函数本身不是可重入的它在中断和主循环这两个不同优先级的上下文中被调用导致了资源竞争。虽然在这个简单例子里可能不会造成死锁但输出混乱是必然的。解决方案 在ISR中避免使用printf等非可重入的、耗时的函数。可以将调试信息存入一个循环缓冲区在主循环中统一打印。或者仅在开发调试阶段在ISR中使用printf并在最终版本中移除。调试建议清单善用调试器 设置断点观察关键全局变量如TIM6OVF,TX的变化单步跟踪程序流程。逻辑分析仪是神器 用它同时抓取PC13按键、PA6PWM输出、PA7PWM输入的波形可以一目了然地看到2秒超时、按键响应、模式切换、PWM输出/捕获的整个过程。简化验证 可以先分别测试TMR6的2秒定时是否准确以及TMR3的PWM生成和捕获功能是否独立工作正常再将它们整合到一起。关注初始化顺序 再次强调外设初始化顺序特别是USART和可能调用它的中断是很多诡异问题的根源。5. 代码优化与扩展思路当前的实现已经完成了核心功能但作为一个可复用的项目框架还有不少可以优化和扩展的地方。1. 增强PWM生成的灵活性目前的sigle_pwm_gen函数提供了两种精度优先模式但参数范围是硬编码的。可以进一步优化算法实现一个“自动模式”函数内部根据输入的period_us自动判断是使用“周期优先”还是“占空比优先”策略以在给定周期下获得综合最优的精度。这涉及到定义一个更复杂的“精度损失”评价函数。2. 改进PWM捕获的稳健性当前的捕获计算在中断中完成。对于高频信号频繁中断会增加CPU负载。可以考虑使用DMA将捕获寄存器的值直接搬运到内存缓冲区主循环再定期处理这批数据计算频率和占空比的平均值或进行滤波这能有效抑制噪声并降低CPU中断频率。3. 状态管理的重构目前使用全局变量TX和TIM6OVF进行状态传递。对于更复杂的多状态系统可以引入一个明确的状态机枚举并使用一个专门的状态处理函数使逻辑更清晰。typedef enum { STATE_BOOT, STATE_WAITING, STATE_PWM_GEN, STATE_PWM_CAP, STATE_ERROR } system_state_t; volatile system_state_t g_system_state STATE_BOOT; // 在中断和主循环中修改和判断这个状态4. 低功耗考量在等待按键的2秒内如果没有其他任务MCU可以进入低功耗模式如Sleep模式由TMR6的溢出中断或按键的外部中断来唤醒。这对于电池供电的设备至关重要。5. 错误处理与参数校验增加更完善的参数范围检查、函数返回值处理。例如在PWM生成函数中如果计算的div或pr超出范围除了打印错误还可以返回特定的错误码并由上层调用者决定是使用默认参数还是进入错误状态。最后所有的代码片段都需要整合到一个完整的工程中并确保头文件包含、路径设置正确。通过这个项目你不仅学会了AT32F421定时器的两种高级应用更重要的是掌握了如何让多个外设协同工作、如何处理中断与状态、以及如何调试嵌入式系统中的实际问题。将这些代码下载到你的开发板上连接好按键和示波器亲眼看到模式随着你的按键或超时而切换那种成就感是阅读文档无法比拟的。在实际项目中这种“超时检测模式切换”的框架非常通用稍加修改就可以应用到产品启动自检、用户配置选择、故障安全模式切换等众多场景中。