2. CMS32 时钟系统与 GPIO:从时钟树到点灯实战

各位同学,欢迎来到第二章。上一章我们把开发环境搭好了,也跑了个空工程。这一章要聊点实在的——时钟和 GPIO。说白了一个是芯片的心跳,一个是芯片的四肢。心跳不对,四肢再灵活也没用。我在项目里见过太多因为时钟配错导致通信失败的案例,所以这一章我会把时钟树掰开了讲,再带你把 GPIO 玩透。

2.1 时钟树:芯片的“心跳”怎么来的?

CMS32 的时钟源有好几个,就像家里有自来水、井水、桶装水一样,各有各的用途。先看这张时钟树结构图,我建议你把它存下来,以后配时钟时对照着看。

CMS32 时钟树结构图 CMS32 时钟树结构图 HSE 外部晶振 HSI 内部高速 RC LSE 外部低速晶振 LSI 内部低速 RC PLL 锁相环 (x4~x16) AHB/APB 预分频器 RTC/看门狗时钟 系统时钟 SYSCLK 选择器 AHB 总线时钟 (HCLK) APB1 外设时钟 (PCLK1) APB2 外设时钟 (PCLK2) CPU / Flash / SRAM 定时器 / UART / I2C GPIO / ADC / SPI 说明:系统时钟 SYSCLK 可选择 HSE、HSI 或 PLL 输出,经分频后供给各总线与外设

从上图你能看到,CMS32 的时钟源分两类:高速和低速。高速的给 CPU 和外设跑,低速的给 RTC 和看门狗用。我个人习惯是:如果板子上有外部晶振,优先用 HSE + PLL,精度高、噪声小。如果只是做简单测试,HSI 内部振荡器也够用,省两个引脚。

小技巧: 内部 HSI 出厂时已经校准到 ±1% 精度,对于普通 UART 通信完全够用。但如果你要做 CAN 通信,我建议还是用外部晶振,CAN 对时钟容差要求比较严。

2.2 系统时钟配置实战

配时钟其实就三步:选源、设倍频、配分频。CMS32 的库函数把这事封装得很简单。来看一个典型配置——把系统时钟跑到 48MHz:

// 系统时钟配置:HSE + PLL -> 48MHz
void SystemClock_Config(void)
{
    // 1. 使能 HSE 外部晶振
    RCC_HSE_Config(RCC_HSE_ON);
    while(!RCC_GetFlagStatus(RCC_FLAG_HSERDY));  // 等待就绪
    
    // 2. 配置 PLL:HSE 4MHz * 12 = 48MHz
    RCC_PLL_Config(RCC_PLLSource_HSE, RCC_PLLMul_12);
    RCC_PLL_Cmd(ENABLE);
    while(!RCC_GetFlagStatus(RCC_FLAG_PLLRDY));
    
    // 3. 切换系统时钟到 PLL
    RCC_SYSCLK_Config(RCC_SYSCLKSource_PLL);
    while(RCC_GetSYSCLKSource() != 0x08);  // 0x08 表示 PLL
    
    // 4. 配置 AHB/APB 分频
    RCC_HCLK_Config(RCC_SYSCLK_Div1);     // HCLK = 48MHz
    RCC_PCLK1_Config(RCC_HCLK_Div2);      // PCLK1 = 24MHz
    RCC_PCLK2_Config(RCC_HCLK_Div1);      // PCLK2 = 48MHz
}

嗯,这里要注意:APB1 最高只能跑到 24MHz,所以必须二分频。我曾经有个项目就是因为没注意这个,APB1 上的定时器死活不工作,查了两天才发现是超频了。

时钟域 最大频率 挂载外设举例
SYSCLK 48 MHz CPU 内核
HCLK (AHB) 48 MHz Flash、SRAM、DMA
PCLK1 (APB1) 24 MHz 定时器2~5、UART4、I2C1
PCLK2 (APB2) 48 MHz GPIO、SPI1、USART1、ADC
避坑指南: 切换时钟源时,一定要等目标时钟稳定后再切。我曾经在切换 PLL 时没检查就绪标志,结果系统直接跑飞。记住:先使能、等就绪、再切换,顺序不能乱。

2.3 GPIO 模式与寄存器

时钟配好了,接下来就是 GPIO。CMS32 的 GPIO 比传统 51 单片机复杂不少,但功能也强得多。每个 GPIO 引脚可以配置成多种模式:

  • 输入模式:浮空输入、上拉输入、下拉输入、模拟输入
  • 输出模式:推挽输出、开漏输出
  • 复用功能:把引脚交给片上外设(如 UART、SPI)控制

控制 GPIO 的核心寄存器就这几个,我列个表方便你对照:

寄存器 功能 位宽
GPIOx_CRL / CRH 配置模式(输入/输出/复用)和速度 每引脚 4 位
GPIOx_IDR 输入数据寄存器(只读) 16 位
GPIOx_ODR 输出数据寄存器 16 位
GPIOx_BSRR 位设置/复位寄存器(原子操作) 32 位
GPIOx_BRR 位复位寄存器 16 位

我个人特别喜欢用 BSRR 寄存器。为什么?因为它可以一次操作同时设置和复位多个引脚,而且不会被中断打断。你想想看,如果用 ODR 做“读-改-写”,中间万一来了个中断,很容易出问题。

核心要点: 配置 GPIO 时,先开时钟(RCC_APB2PeriphClockCmd),再配模式(GPIO_Init),最后操作数据(BSRR/ODR)。顺序反了,引脚不干活。

2.4 GPIO 驱动 LED 与按键实战

理论讲完了,咱们直接上手。先来个最简单的——点亮 LED。假设 LED 接在 PB0 上,高电平点亮:

// LED 初始化
void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;   // 推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;  // 速度选高点
    GPIO_Init(GPIOB, &GPIO_InitStruct);
}

// 点亮 LED
#define LED_ON()   GPIO_SetBits(GPIOB, GPIO_Pin_0)
#define LED_OFF()  GPIO_ResetBits(GPIOB, GPIO_Pin_0)
#define LED_TOGGLE() GPIOB->ODR ^= GPIO_Pin_0

按键稍微复杂点,因为要考虑消抖。假设按键接在 PA0 上,按下为低电平:

// 按键初始化
void KEY_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;   // 上拉输入
    GPIO_Init(GPIOA, &GPIO_InitStruct);
}

// 按键扫描(带消抖)
uint8_t KEY_Scan(void)
{
    if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)  // 检测到按下
    {
        Delay_ms(10);  // 消抖延时
        if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
        {
            while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0);  // 等待松开
            return 1;  // 有效按键
        }
    }
    return 0;
}
避坑指南: 按键消抖延时不能太短,也不能太长。10ms 是我试过比较合适的值。太短(比如 1ms)消不掉抖动,太长(比如 50ms)会感觉按键反应迟钝。我曾经用 5ms 消抖,结果在强电磁干扰环境下按键会误触发,后来改成 10ms 就稳了。

最后来个综合小实验:按键控制 LED 亮灭。把上面两段代码合起来,主循环里调用 KEY_Scan(),检测到按键就翻转 LED。你可以在开发板上试试,效果很直观。

扩展思考: 如果你想让 LED 呼吸效果,可以用 PWM 控制。CMS32 的定时器可以输出 PWM,后面章节会详细讲。这里先留个悬念。

好了,这一章的内容就这些。时钟树的结构、系统时钟的配置流程、GPIO 的几种模式、以及 LED 和按键的驱动方法,都是嵌入式开发最基础也最常用的技能。把这些练熟了,后面的 CAN 通信和 Bootloader 才能跑得稳。