2. CMS32 时钟系统与 GPIO:从时钟树到点灯实战
各位同学,欢迎来到第二章。上一章我们把开发环境搭好了,也跑了个空工程。这一章要聊点实在的——时钟和 GPIO。说白了一个是芯片的心跳,一个是芯片的四肢。心跳不对,四肢再灵活也没用。我在项目里见过太多因为时钟配错导致通信失败的案例,所以这一章我会把时钟树掰开了讲,再带你把 GPIO 玩透。
2.1 时钟树:芯片的“心跳”怎么来的?
CMS32 的时钟源有好几个,就像家里有自来水、井水、桶装水一样,各有各的用途。先看这张时钟树结构图,我建议你把它存下来,以后配时钟时对照着看。
从上图你能看到,CMS32 的时钟源分两类:高速和低速。高速的给 CPU 和外设跑,低速的给 RTC 和看门狗用。我个人习惯是:如果板子上有外部晶振,优先用 HSE + PLL,精度高、噪声小。如果只是做简单测试,HSI 内部振荡器也够用,省两个引脚。
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 |
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 做“读-改-写”,中间万一来了个中断,很容易出问题。
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;
}
最后来个综合小实验:按键控制 LED 亮灭。把上面两段代码合起来,主循环里调用 KEY_Scan(),检测到按键就翻转 LED。你可以在开发板上试试,效果很直观。
好了,这一章的内容就这些。时钟树的结构、系统时钟的配置流程、GPIO 的几种模式、以及 LED 和按键的驱动方法,都是嵌入式开发最基础也最常用的技能。把这些练熟了,后面的 CAN 通信和 Bootloader 才能跑得稳。