GD32微控制器(特别是基于ARM Cortex-M内核的系列,如GD32F1xx, GD32F3xx, GD32E2xx等)可以通过内部时钟源,在特定的引脚上输出时钟信号。
此功能的核心是配置复用功能重映射和调试支持(AFIO) 以及相应的定时器(Timer)。
最典型的两种方法
GD32输出时钟主要有两种常见方式:
- 从MCO引脚输出系统时钟
- 从定时器的通道引脚输出时钟
方法一:从MCO引脚输出主时钟
MCO(Microcontroller Clock Output)是芯片专门用于时钟输出的引脚。它可以直接输出内部的主要时钟源(如内部高速RC振荡器HSI、外部高速晶振HSE、PLL输出、系统时钟SYSCLK等)。
步骤(以GD32F103系列为例,输出SYSCLK):
-
使能GPIO端口时钟:MCO引脚通常是PA8。
crcu_periph_clock_enable(RCU_GPIOA);
-
配置GPIO模式:将PA8设置为复用推挽输出模式,并选择较高的速度。
cgpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8);
-
配置时钟源:选择你想要从MCO引脚输出的时钟源。
c/* 例如,输出PLL的2分频时钟(这是最常用的SYSCLK来源) */ rcu_ckout_config(RCU_CKOUT_SRC_PLLPSC2); /* 其他可选时钟源: RCU_CKOUT_SRC_HSE - 外部高速晶振 RCU_CKOUT_SRC_HSI - 内部高速RC振荡器 RCU_CKOUT_SRC_PLLPSC2 - PLL时钟的2分频(通常是SYSCLK) RCU_CKOUT_SRC_LSE - 外部低速晶振(32.768KHz) RCU_CKOUT_SRC_LSI - 内部低速RC振荡器(40KHz) */
注意 :MCO输出的时钟可能会经过一个预分频器(例如PLLPSC2),具体请参考对应型号的参考手册,查看RCU_CFG1
寄存器中MCO相关的配置位。
方法二:从定时器输出时钟
这种方法更为灵活,可以输出一个频率可编程的、非常稳定的方波。其原理是将定时器配置为某种特定的PWM模式(翻转模式),使其通道引脚在每个定时器周期都产生一次翻转,从而输出一个频率为定时器更新频率一半的方波。
步骤(以使用TIMER1的CH0(PA8)输出1MHz时钟为例,假设系统时钟为108MHz):
-
使能时钟:使能定时器和GPIO的时钟。
crcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_TIMER1);
-
配置GPIO:将定时器通道对应的引脚(如PA8)配置为复用推挽输出。
cgpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8);
-
配置定时器:
- 预分频器(PSC):用来对定时器时钟进行分频。
- 自动重载值(ARR):决定计数周期。
我们希望输出的时钟频率为
Fout = 1MHz
。定时器通道引脚会在每个更新事件时翻转一次,因此引脚翻转的频率就是定时器的更新频率
Fupdate
。而方波频率
Fout = Fupdate / 2
。所以,我们需要定时器的更新频率
Fupdate = 2 * Fout = 2MHz
。假设定时器的输入时钟
Ftimer = 108MHz
。则所需的定时器分频系数为:
Ftimer / Fupdate = 108M / 2M = 54
。配置如下:
ctimer_parameter_struct timer_initpara; timer_oc_parameter_struct timer_ocinitpara; /* 初始化定时器基本参数 */ timer_struct_para_init(&timer_initpara); timer_initpara.prescaler = 53; // PSC = 53, 实际分频系数为 53+1=54 timer_initpara.alignedmode = TIMER_COUNTER_EDGE; timer_initpara.counterdirection = TIMER_COUNTER_UP; timer_initpara.period = 1; // ARR = 1, 计数2次(0和1)后产生更新 timer_initpara.clockdivision = TIMER_CKDIV_DIV1; timer_init(TIMER1, &timer_initpara); /* 验证频率: Fout = (108MHz / (53+1)) / (1+1) / 2? 更正思路: 定时器时钟 = 108MHz / (PSC+1) = 108M / 54 = 2MHz。 更新事件频率 = 2MHz / (ARR+1) = 2M / 2 = 1MHz。 引脚在每次更新时翻转,输出频率 = 更新频率 / 2? 这里需要修正理解。 更准确的计算: - 引脚被设置为"翻转"模式。 - 定时器从0计数到ARR,然后溢出产生更新事件。 - 每次更新事件,引脚电平翻转一次。 - 因此,引脚要完成一个完整周期(高->低->高),需要两次更新事件。 - 所以,输出频率 = 定时器更新频率 / 2。 定时器更新频率 = Ftimer / ((PSC+1) * (ARR+1)) 所以 Fout = [108MHz / (54 * 2)] / 2 = (1MHz) / 2 = 500KHz? 这不对。 正确配置和计算: 目标 Fout = 1MHz。 因为 Fout = Fupdate / 2, 所以 Fupdate = 2MHz。 又因为 Fupdate = Ftimer / ((PSC+1) * (ARR+1)) 令 ARR = 0, 则 (ARR+1) = 1, 则 Fupdate = Ftimer / (PSC+1) 所以 PSC+1 = 108MHz / 2MHz = 54 -> PSC = 53. 此时 Fupdate = 2MHz, Fout = 1MHz。 正确。 所以配置应为: PSC=53, ARR=0. */ timer_initpara.period = 0; // ARR = 0, 计数2次(0和1?)不对,从0到0是1次?标准理解:ARR=0时,计数值从0到0,即只计数1次就更新。 修正: 定时器计数值从0到ARR,计数次数是ARR+1次。 所以,更新频率 = Ftimer_input / ( (PSC+1) * (ARR+1) ) 要输出1MHz, 令 ARR=1, 则: Fupdate = 108M / (54 * 2) = 1MHz。 Fout = Fupdate / 2 = 500KHz。 不符合目标。 重新计算,目标是 Fout=1MHz: Fout = Ftimer_input / (2 * (PSC+1) * (ARR+1)) 所以 (PSC+1) * (ARR+1) = 108M / (2 * 1M) = 54. 我们可以取 PSC=53, ARR=0 -> (54 * 1) = 54. 正确。 所以配置应为 PSC=53, ARR=0. */ // 因此,更正后的配置: timer_initpara.prescaler = 53; timer_initpara.period = 0; // ARR = 0 timer_init(TIMER1, &timer_initpara);
-
配置输出比较通道为翻转模式:
c/* 初始化输出比较参数 */ timer_channel_output_struct_para_init(&timer_ocinitpara); timer_ocinitpara.outputstate = TIMER_CCX_ENABLE; timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH; timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW; /* 设置通道0为翻转模式 */ timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_TOGGLE); timer_channel_output_config(TIMER1, TIMER_CH_0, &timer_ocinitpara);
-
使能定时器:
ctimer_auto_reload_shadow_enable(TIMER1); timer_enable(TIMER1);
总结与选择
特性 | MCO 输出 | 定时器输出 |
---|---|---|
时钟源 | 固定的系统时钟源(HSI, HSE, PLL, SYSCLK等) | 来自定时器的时钟(通常也是系统时钟) |
频率 | 固定或有限分频(取决于RCU配置) | 高度可编程,频率范围广,非常灵活 |
精度 | 高,直接来源于时钟源 | 高,依赖于系统时钟精度 |
用途 | 为外部芯片提供系统主时钟、时钟监控 | 生成任意频率的方波、驱动需要时钟的器件 |
占用资源 | 几乎不占用 | 占用一个定时器通道 |
建议:
- 如果需要输出一个与系统核心时钟相关的固定频率(例如为另一个MCU提供时钟),使用MCO。
- 如果需要生成一个特定频率的方波信号(例如1.8432MHz用于UART),使用定时器。