2. S32K1xx时钟系统与GPIO:时钟树详解、时钟配置实战、GPIO模块原理、GPIO驱动开发、LED闪烁实验
各位同学,欢迎来到第二章。这一章我们聊聊S32K1xx的时钟系统和GPIO。说实话,时钟是芯片的“心跳”,GPIO则是芯片与外界沟通的“手脚”。这两块搞不明白,后面的CANFD和UDS诊断栈移植根本跑不起来。我当年第一次调S32K1xx的CANFD时,就是因为时钟配置错了,折腾了两天才找到问题——嗯,这种坑咱们今天就得提前填上。
2.1 时钟树详解
S32K1xx的时钟树,说白了就是一套“时钟分发网络”。芯片内部有好几个时钟源,你需要根据应用场景选一个,然后通过PLL倍频、分频,最后喂给各个外设。
时钟源有哪些?
- SOSC:外部主振荡器,通常接8MHz或40MHz晶振。精度高,适合CANFD这种对时序敏感的场景。
- SIRC:内部慢速RC振荡器,约32kHz。省电,但精度一般。
- FIRC:内部快速RC振荡器,约48MHz。上电默认就用它,启动快,但温漂大。
- LPO:低功耗振荡器,约1kHz。给看门狗或低功耗模式用的。
我个人习惯,做CANFD项目时首选SOSC+PLL方案。为什么?因为CANFD对位时序要求严格,内部RC的精度不够,跑高速时容易丢帧。我曾经在一个项目里偷懒用了FIRC,结果CANFD在125℃高温下频频报错,后来换成外部晶振才搞定。
时钟树的关键路径
从时钟源到外设,中间要经过几个节点:
- 时钟源选择(SCS位)
- PLL倍频(SPLL)
- 系统时钟分频(CORE_CLK_DIV、BUS_CLK_DIV等)
- 外设时钟门控(比如CANFD的时钟使能)
你想想看,每个节点配置错了,外设要么不工作,要么工作异常。所以,我建议你画一张时钟树图贴在工位上——真的有用。
核心要点:S32K1xx的系统时钟最高可以跑到112MHz(比如S32K148),但BUS_CLK和FLASH_CLK有上限,分别是56MHz和28MHz。别一股脑全跑最高,小心Flash读取出错。
2.2 时钟配置实战
好,理论讲完了,咱们直接上手配置。我用的是S32K144,外部晶振8MHz,目标系统时钟80MHz。
第一步:配置PLL
PLL的公式很简单:PLL输出 = 晶振频率 × (SPLL_MFI + SPLL_FRAC/32) / SPLL_PREDIV / SPLL_REDIV。看着复杂?其实你只需要记住:先分频,再倍频,再分频。
/* 配置PLL:8MHz晶振 -> 80MHz系统时钟 */
SCG->SPLLCFG = SCG_SPLLCFG_SPLL_PREDIV(1) /* 预分频:8MHz / 1 = 8MHz */
| SCG_SPLLCFG_SPLL_REDIV(1) /* 后分频:80MHz / 1 = 80MHz */
| SCG_SPLLCFG_SPLL_MFI(10) /* 整数倍频:8MHz * 10 = 80MHz */
| SCG_SPLLCFG_SPLL_FRAC(0); /* 小数倍频:不用 */
这里有个坑:SPLL_PREDIV和SPLL_REDIV不能为0,否则PLL直接罢工。我曾经手滑写了个0,结果芯片死在那,调试器都连不上——只能按复位键。
第二步:选择系统时钟源
PLL稳定后,把系统时钟切到SPLL:
SCG->RCCR = SCG_RCCR_SCS(0x2); /* 0x2 = SPLL */
while(SCG->CSR & SCG_CSR_SCS_MASK != SCG_CSR_SCS(0x2)); /* 等待切换完成 */
嗯,这里要注意:切换时钟源前,确保目标时钟已经稳定。我习惯加一个超时判断,防止死循环。
第三步:配置分频器
系统时钟80MHz,但BUS_CLK不能超过56MHz,所以:
SCG->RCCR |= SCG_RCCR_DIVBUS(1); /* BUS_CLK = 80MHz / 2 = 40MHz */
SCG->RCCR |= SCG_RCCR_DIVSLOW(2); /* SLOW_CLK = 80MHz / 4 = 20MHz */
我的小技巧:调试阶段,可以把系统时钟降到40MHz跑,等所有外设调通了再升频。这样万一出问题,排查范围小很多。
2.3 GPIO模块原理
GPIO,全称General Purpose Input/Output,通用输入输出口。S32K1xx的GPIO模块,说白了就是一堆寄存器,控制着每个引脚的电平、方向、上下拉等。
GPIO的关键寄存器
| 寄存器 | 功能 | 我常用的场景 |
|---|---|---|
| PDOR | 输出数据寄存器 | 写1拉高,写0拉低 |
| PSOR | 输出置位寄存器 | 只置位,不影响其他位 |
| PCOR | 输出清零寄存器 | 只清零,不影响其他位 |
| PTOR | 输出翻转寄存器 | LED闪烁时超好用 |
| PDIR | 输入数据寄存器 | 读引脚电平 |
| PDDR | 方向寄存器 | 1=输出,0=输入 |
你想想看,如果每次操作GPIO都要读-改-写,多麻烦。S32K1xx提供了PSOR、PCOR、PTOR这些“原子操作”寄存器,直接写1就能置位或清零,不用担心中断打断。我在项目中一直用这个方式,效率高还安全。
引脚复用功能
每个引脚除了做GPIO,还能当CAN、SPI、UART等外设的引脚。通过PORT模块的PCR寄存器配置:
PORT->PCR[PTB0] = PORT_PCR_MUX(0x5); /* 复用功能5:CAN0_TX */
这里有个容易忽略的点:配置完复用功能后,别忘了使能对应外设的时钟门控。我见过有人配了PCR,但CAN模块时钟没开,结果CAN总线一直没波形——查了半天才发现是时钟门控的问题。
2.4 GPIO驱动开发
好,咱们自己写一个轻量级的GPIO驱动。别用SDK,自己动手写一遍,印象才深。
头文件:gpio.h
#ifndef GPIO_H
#define GPIO_H
#include "S32K144.h"
typedef enum {
GPIO_DIR_INPUT = 0,
GPIO_DIR_OUTPUT = 1
} gpio_dir_t;
void GPIO_Init(uint32_t base, uint32_t pin, gpio_dir_t dir);
void GPIO_Write(uint32_t base, uint32_t pin, uint8_t level);
uint8_t GPIO_Read(uint32_t base, uint32_t pin);
void GPIO_Toggle(uint32_t base, uint32_t pin);
#endif
实现文件:gpio.c
#include "gpio.h"
void GPIO_Init(uint32_t base, uint32_t pin, gpio_dir_t dir)
{
/* 使能PORT时钟(以PTB为例,实际项目中需根据base计算) */
PCC->PCCn[PCC_PORTB_INDEX] |= PCC_PCCn_CGC_MASK;
/* 配置为GPIO功能(MUX=1) */
PORTB->PCR[pin] = PORT_PCR_MUX(1);
/* 配置方向 */
if (dir == GPIO_DIR_OUTPUT) {
((GPIO_Type *)base)->PDDR |= (1 << pin);
} else {
((GPIO_Type *)base)->PDDR &= ~(1 << pin);
}
}
void GPIO_Write(uint32_t base, uint32_t pin, uint8_t level)
{
if (level) {
((GPIO_Type *)base)->PSOR = (1 << pin); /* 置位 */
} else {
((GPIO_Type *)base)->PCOR = (1 << pin); /* 清零 */
}
}
uint8_t GPIO_Read(uint32_t base, uint32_t pin)
{
return (((GPIO_Type *)base)->PDIR >> pin) & 0x01;
}
void GPIO_Toggle(uint32_t base, uint32_t pin)
{
((GPIO_Type *)base)->PTOR = (1 << pin); /* 翻转 */
}
注意:GPIO_Init函数里,我直接硬编码了PORTB的时钟使能。实际项目中,你应该根据base地址动态计算对应的PORT和PCC索引。否则换个引脚就得改代码,太不优雅了。
2.5 LED闪烁实验
终于到了动手环节。咱们用PTB0引脚接一个LED,让它以1Hz的频率闪烁。说白了,就是每隔500ms翻转一次电平。
硬件连接
- PTB0 -> LED阳极(串一个330Ω限流电阻)
- LED阴极 -> GND
主程序代码
#include "S32K144.h"
#include "gpio.h"
/* 简单延时函数(不精确,仅供演示) */
void delay_ms(uint32_t ms)
{
for (uint32_t i = 0; i < ms * 4000; i++) {
__NOP();
}
}
int main(void)
{
/* 关闭看门狗 */
WDOG->CNT = 0xD928C520; /* 解锁 */
WDOG->CNT = 0x6B9A8E73;
WDOG->TOVAL = 0xFFFF; /* 设置超时最大值 */
WDOG->CS = 0x0000; /* 禁用看门狗 */
/* 初始化GPIO:PTB0,输出 */
GPIO_Init(PTB_BASE, 0, GPIO_DIR_OUTPUT);
while (1) {
GPIO_Toggle(PTB_BASE, 0); /* 翻转LED */
delay_ms(500); /* 延时500ms */
}
}
运行这段代码,LED应该以1Hz的频率闪烁。如果不闪,先检查硬件连接,再用万用表量PTB0引脚电平是否在变化。我遇到过最奇葩的问题:LED正负极焊反了,结果怎么调代码都不亮——嗯,硬件问题往往比软件问题更隐蔽。
进阶思考:实际项目中,别用这种“傻等”延时。你可以用Systick定时器或者LPTMR来实现精确延时,这样CPU还能干别的事。后面讲定时器章节时,咱们再细聊。
好了,这一章的内容就到这里。时钟树和GPIO是嵌入式开发的“基本功”,练扎实了,后面移植UDS诊断栈时才能游刃有余。下一章,咱们聊聊CANFD控制器——这可是UDS诊断的“高速公路”。