4. GPIO外设驱动

定时器,说白了就是芯片的“心跳”和“节拍器”。没有它,你的程序要么傻等,要么乱跑。GD32V的定时器资源挺丰富,从最简单的SysTick到功能强大的通用定时器,再到高级定时器,一层层递进。这一章,咱们先把最核心的SysTick和通用定时器(TIMERx)啃下来。

4.1 SysTick:系统的心跳

SysTick是个24位的递减计数器。它挂在系统总线上,专门为操作系统(或者裸机延时)提供周期性中断。我刚开始用GD32V时,第一件事就是配好SysTick,不然连个精准的延时都写不出来。

4.1.1 工作原理

SysTick的核心逻辑很简单:从你设定的重装载值开始,每个系统时钟周期减1,减到0时触发中断,然后自动重装,继续下一轮。嗯,就这么简单。

关键寄存器:

  • SysTick_CTRL:控制寄存器。使能、选择时钟源、使能中断、读取计数是否到0的标志。
  • SysTick_LOAD:重装载值寄存器。写入你要计数的初始值。
  • SysTick_VAL:当前值寄存器。读它得到当前计数值,写它会清空计数器。

举个例子,系统时钟是108MHz,你想让SysTick每1ms中断一次。那重装载值就是108000000 / 1000 = 108000。注意,这个值不能超过2^24 - 1(约1677万),108MHz下1ms完全够用。

4.1.2 配置步骤

  1. 关闭SysTick(CTRL寄存器清零)。
  2. 设置重装载值到LOAD寄存器。
  3. 清除当前值(写VAL寄存器任意值)。
  4. 选择时钟源(AHB时钟或AHB/8)。
  5. 使能中断(如果需要)。
  6. 使能SysTick。
void systick_config(uint32_t ticks)
{
    // 1. 关闭SysTick
    SysTick->CTRL = 0;
    // 2. 设置重装载值
    SysTick->LOAD = ticks - 1;
    // 3. 清除当前值
    SysTick->VAL = 0;
    // 4. 选择时钟源:AHB时钟
    SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk;
    // 5. 使能中断
    SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;
    // 6. 使能SysTick
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
}

我的习惯: 我会在系统初始化时调用 systick_config(108000),然后在SysTick_Handler里累加一个全局变量 g_tick_count。这样,任何需要延时的地方,直接读这个变量做比较就行,比死循环精准得多。

4.2 通用定时器TIMERx:结构与时基单元

SysTick虽然好用,但功能太单一。真正干活的是通用定时器(TIMER0~TIMER5)。GD32V的通用定时器是一个16位的自动重装载计数器,支持向上、向下、中央对齐三种计数模式。

4.2.1 核心结构

一个通用定时器,内部由几个关键部分组成:

  • 时基单元:包括预分频器(PSC)、计数器(CNT)、自动重装载寄存器(CAR)。这是定时器的“心脏”。
  • 捕获/比较通道:用于输入捕获、输出比较、PWM生成。
  • 中断/事件控制器:处理更新事件、捕获事件、比较事件等。

你想想看,预分频器把时钟源分频后送给计数器,计数器在时钟驱动下递增或递减,当计数值与重装载值匹配时,产生更新事件。这就是时基单元的基本流程。

通用定时器TIMERx内部结构 时钟源 预分频器 计数器 自动重装载 更新事件 捕获/比较通道 说明:预分频器将时钟源分频后驱动计数器。计数器与自动重装载值匹配时产生更新事件。 捕获/比较通道则直接从计数器读取或写入值,用于输入捕获或输出PWM。

4.2.2 三种计数模式

模式 方向 典型应用
向上计数 0 → CAR 普通定时、PWM
向下计数 CAR → 0 对称波形生成
中央对齐 0 → CAR → 0 电机控制、无刷驱动

向上计数最常用。中央对齐模式在电机控制里很常见,因为它能产生对称的PWM波形,谐波更少。我记得有一次做无刷电机驱动,一开始用了向上计数,电机噪音大得离谱,换成中央对齐后,噪音立刻降下来了。

4.3 PWM模式:让定时器输出波形

PWM(脉冲宽度调制),说白了就是通过调节高低电平的占空比来控制功率。GD32V的通用定时器支持两种PWM模式:PWM模式1和PWM模式2。区别在于输出电平的极性。

4.3.1 配置流程

  1. 使能定时器时钟。
  2. 配置GPIO为复用功能输出。
  3. 设置预分频器和自动重装载值,确定PWM频率。
  4. 配置捕获/比较通道为PWM模式。
  5. 设置占空比(写入捕获/比较寄存器)。
  6. 使能定时器。
void timer_pwm_config(void)
{
    // 1. 使能定时器时钟
    rcu_periph_clock_enable(RCU_TIMER1);
    // 2. 配置GPIO(PA1为TIMER1_CH0)
    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_1);
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1);
    gpio_af_set(GPIOA, GPIO_AF_2, GPIO_PIN_1);

    // 3. 设置预分频器和自动重装载值
    timer_prescaler_config(TIMER1, 107, TIMER_PSC_RELOAD_NOW);  // 108MHz / 108 = 1MHz
    timer_autoreload_value_config(TIMER1, 999);                 // 1MHz / 1000 = 1kHz

    // 4. 配置通道0为PWM模式1
    timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_PWM1);
    timer_channel_output_state_config(TIMER1, TIMER_CH_0, TIMER_CCX_ENABLE);

    // 5. 设置占空比50%
    timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 500);

    // 6. 使能定时器
    timer_enable(TIMER1);
}

我曾经踩过的坑: 配置GPIO复用功能时,一定要查数据手册确认正确的复用功能号(AF号)。GD32V的引脚复用不像STM32那么直观,同一个引脚可能对应多个外设。我一开始没仔细看,把AF_1写成了AF_2,结果PWM死活没输出,查了半天才发现是引脚复用配错了。

4.3.2 占空比与频率计算

频率公式:F_pwm = F_timer / (PSC + 1) / (CAR + 1)

占空比公式:Duty = CCR / (CAR + 1) * 100%

举个例子,系统时钟108MHz,PSC=107,CAR=999,那么PWM频率就是108MHz / 108 / 1000 = 1kHz。CCR=500时,占空比就是50%。

我的建议: 实际项目中,PWM频率和占空比往往需要动态调整。我会把PSC和CAR的计算封装成一个函数,传入目标频率,自动算出最接近的整数值。这样代码复用性高,也方便调试。

4.4 本章小结

这一章我们聊了SysTick和通用定时器的核心用法。SysTick适合做系统心跳和精准延时,通用定时器则能输出PWM、捕获输入信号。配置定时器时,时钟源、预分频器、自动重装载值这三者决定了定时精度和PWM频率。嗯,这些基础打牢了,后面做更复杂的协议解析和电机控制才能游刃有余。