1. 认识GD32F303的通用定时器
第一次接触GD32F303的通用定时器时,我被它丰富的功能震撼到了。这颗国产MCU的定时器模块完全不输国际大厂的产品,而且文档非常详细。通用定时器在嵌入式系统中就像厨房里的定时器,你可以用它来精确控制各种任务的执行时间。比如让LED每隔1秒闪烁一次,或者每隔10毫秒采集一次传感器数据。
GD32F303的通用定时器主要有以下几个特点:
- 16位自动重装载计数器:这意味着你可以设置一个比较大的计数值,而不用担心溢出问题
- 可编程预分频器:可以把系统时钟分频到你需要的频率,灵活性很强
- 四种计数模式:向上计数、向下计数、中央对齐模式等,适应不同场景需求
- 丰富的中断源:更新中断、触发中断、捕获/比较中断等,让你可以精确控制中断触发时机
我刚开始学习时,最常用的是TIMER1。它挂在APB1总线上,最大时钟频率可以达到60MHz。这个速度对于大多数应用场景来说已经绰绰有余了。记得第一次配置时,我对着时钟树看了半天才搞明白各个时钟之间的关系,后面我会详细讲解这个部分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建开发环境
工欲善其事,必先利其器。在开始编码之前,我们需要准备好开发环境。我推荐使用Keil MDK作为开发IDE,它不仅对GD32系列支持良好,而且调试功能非常强大。
首先需要安装GD32F30x系列的设备支持包。这个可以在兆易创新官网找到,安装完成后,在Keil中新建工程时就能看到GD32F303的芯片选项了。我建议同时下载GD32F30x的标准外设库,这个库封装了很多底层操作,能大大简化开发工作。
硬件方面,你需要一块GD32F303的开发板。我用的是官方推出的GD32F303C-EVAL,这块板子资源丰富,还带调试器,非常适合初学者。如果手头没有官方板,市面上也有很多兼容的开发板可以选择,价格也很亲民。
连接好开发板后,别忘了安装USB转串口驱动。这个驱动通常随开发板配套提供,或者可以在CH340/CH341官网下载。有了它,我们才能通过串口打印调试信息。
3. 理解时钟系统
时钟是定时器的"心脏",理解GD32F303的时钟系统对正确配置定时器至关重要。这颗芯片的时钟架构稍微有点复杂,但掌握后会发现它设计得非常灵活。
GD32F303的主时钟源可以是内部RC振荡器(8MHz)或外部晶振(4-32MHz)。通过锁相环(PLL)倍频后,系统时钟最高可以达到120MHz。定时器TIMER1挂在APB1总线上,APB1的最大时钟频率是60MHz。
这里有个容易混淆的点:当APB1的预分频系数不为1时,定时器时钟会是APB1时钟的两倍。比如APB1时钟配置为30MHz,定时器实际时钟会是60MHz。这个设计是为了兼容一些需要更高定时器精度的应用场景。
在代码中配置时钟时,我通常会这样写:
c复制rcu_clock_freq_config(RCU_CKSYSSRC_PLL); // 选择PLL作为系统时钟源
rcu_pll_config(RCU_PLLSRC_HXTAL, 8, 336); // 8MHz晶振,8倍频得到64MHz
rcu_apb1_config(RCU_APB1_CKAHB_DIV2); // APB1时钟=AHB/2=32MHz
rcu_periph_clock_enable(RCU_TIMER1); // 使能TIMER1时钟
4. 定时器基础配置
现在我们来具体配置TIMER1。定时器的工作流程可以类比厨房里的微波炉:你设置好加热时间(计数值),启动后它开始倒计时,时间到了就"叮"的一声(触发中断)。
首先需要配置时基单元,主要包括三个参数:
- 预分频器(PSC):决定定时器时钟的分频系数
- 自动重装载值(ARR):决定计数器的最大值
- 计数模式:向上计数、向下计数或中央对齐
计算定时周期的公式是:
定时时间 = (ARR + 1) × (PSC + 1) / TIM_CLK
比如要实现1秒的定时,假设TIM_CLK=60MHz:
- 设置PSC=59999,ARR=999
- 定时时间 = (999+1)×(59999+1)/60,000,000 = 1秒
代码实现如下:
c复制void Timer1_Init(uint16_t prescale, uint16_t period)
{
// 使能TIMER1时钟
RCU_APB1EN |= (1<<0);
// 复位定时器配置
TIMER_CTL0(TIMER1) = 0;
// 配置时基单元
TIMER_PSC(TIMER1) = prescale;
TIMER_CAR(TIMER1) = period;
// 使能自动重装载预装载
TIMER_CTL0(TIMER1) |= (1<<7);
// 设置向上计数模式
TIMER_CTL0(TIMER1) &= ~(1<<4);
// 使能更新中断
TIMER_DMAINTEN(TIMER1) |= (1<<0);
// 使能定时器
TIMER_CTL0(TIMER1) |= (1<<0);
}
5. 中断配置与处理
定时器中断是精准控制的关键。GD32F303的中断系统基于ARM Cortex-M4的NVIC,配置起来稍微复杂但非常灵活。
首先需要配置NVIC(嵌套向量中断控制器):
- 设置中断优先级
- 使能中断通道
对于TIMER1更新中断,其中断号是28。在GD32F303中,优先级数值越小优先级越高,0是最高优先级,255是最低。
c复制// 配置NVIC
NVIC_SetPriority(TIMER1_IRQn, 1); // 设置优先级为1
NVIC_EnableIRQ(TIMER1_IRQn); // 使能TIMER1中断
接下来编写中断服务函数。这里有个重要注意事项:必须清除中断标志位,否则会不断进入中断。
c复制void TIMER1_IRQHandler(void)
{
if(TIMER_INTF(TIMER1) & (1<<0)) // 检查更新中断标志
{
TIMER_INTF(TIMER1) &= ~(1<<0); // 清除标志位
// 在这里处理你的定时任务
GPIO_BOP(GPIOB) = GPIO_PIN_1; // 翻转PB1引脚
}
}
在实际项目中,我建议在中断服务函数中尽量少做耗时操作,可以通过设置标志位的方式让主循环来处理具体任务。这样可以避免错过其他重要中断。
6. 调试技巧与常见问题
调试定时器中断时,我踩过不少坑,这里分享几个实用技巧:
问题1:中断不触发
- 检查定时器时钟是否使能
- 确认NVIC配置正确
- 确保中断标志位被正确清除
- 查看PSC和ARR值是否合理
问题2:定时时间不准确
- 确认系统时钟配置正确
- 检查APB1预分频设置
- 考虑是否需要在中断服务函数开头关闭全局中断
问题3:程序跑飞
- 检查堆栈大小是否足够
- 确认中断优先级设置合理
- 查看是否有未处理的中断
我常用的调试方法是结合逻辑分析仪和串口打印。逻辑分析仪可以直观显示GPIO引脚的电平变化,验证定时是否准确。串口打印则可以帮助了解程序执行流程。
这里有个小技巧:可以在中断服务函数开始和结束处翻转不同的GPIO,然后用逻辑分析仪测量中断处理时间:
c复制void TIMER1_IRQHandler(void)
{
GPIO_BOP(GPIOA) = GPIO_PIN_0; // PA0置高,标记进入中断
// 中断处理代码...
GPIO_BC(GPIOA) = GPIO_PIN_0; // PA0置低,标记离开中断
}
7. 进阶应用:PWM输出
掌握了基础定时器中断后,我们可以尝试更高级的应用,比如PWM输出。PWM在控制电机速度、LED亮度等方面非常有用。
GD32F303的通用定时器支持PWM模式,配置步骤如下:
- 配置定时器时基单元
- 设置通道为PWM模式
- 配置捕获/比较寄存器
- 使能通道输出
下面是一个生成1kHz PWM的示例:
c复制void PWM_Init(void)
{
// 时基配置:1kHz PWM,占空比50%
Timer1_Init(60-1, 1000-1); // 60MHz/60=1MHz, 1000计数=1ms周期
// 配置通道1为PWM模式1
TIMER_CHCTL2(TIMER1) |= (6<<4); // PWM模式1
TIMER_CHCTL2(TIMER1) |= (1<<3); // 输出使能
// 设置捕获/比较值(占空比)
TIMER_CH1CV(TIMER1) = 500; // 50%占空比
// 使能主输出
TIMER_CCHP(TIMER1) |= (1<<15);
// 使能定时器
TIMER_CTL0(TIMER1) |= (1<<0);
}
在实际项目中,你可能需要动态调整PWM占空比。这时可以直接修改CHxCV寄存器的值:
c复制void Set_PWM_Duty(uint16_t duty)
{
TIMER_CH1CV(TIMER1) = duty;
}
8. 实际项目中的应用案例
最后分享一个我在智能家居项目中实际应用的案例。我们需要精确控制多个LED的渐变效果,同时还要处理按键扫描和无线通信。
解决方案是使用TIMER1产生1ms的基准中断,在中断服务函数中维护一个软件定时器组:
c复制volatile uint32_t sys_tick = 0;
volatile uint8_t flag_1ms = 0;
volatile uint8_t flag_10ms = 0;
volatile uint8_t flag_100ms = 0;
void TIMER1_IRQHandler(void)
{
if(TIMER_INTF(TIMER1) & (1<<0))
{
TIMER_INTF(TIMER1) &= ~(1<<0);
sys_tick++;
flag_1ms = 1;
if(sys_tick % 10 == 0) flag_10ms = 1;
if(sys_tick % 100 == 0) flag_100ms = 1;
}
}
在主循环中根据这些标志位执行不同任务:
c复制while(1)
{
if(flag_1ms)
{
flag_1ms = 0;
LED_Process(); // LED渐变处理
}
if(flag_10ms)
{
flag_10ms = 0;
Key_Scan(); // 按键扫描
}
if(flag_100ms)
{
flag_100ms = 0;
Wireless_Process(); // 无线通信处理
}
}
这种架构既保证了定时精度,又避免了在中断中执行耗时操作。在实际运行中非常稳定,LED渐变效果平滑,按键响应及时。
