3. GPIO点灯实验:F1C200s GPIO模块寄存器映射、时钟使能、引脚复用配置、输出控制寄存器操作、实现LED闪烁
点灯实验,是嵌入式开发里的"Hello World"。说白了,就是让你第一次真正感受到:代码能控制硬件了。
我个人习惯,拿到一个新芯片,第一件事就是点灯。为什么?因为GPIO是最基础的模块,它验证了你的开发环境、编译链、下载工具、寄存器操作,这一整套链路是否跑通。如果灯都不亮,后面那些复杂的外设想都别想。
3.1 GPIO模块寄存器映射
F1C200s的GPIO模块,挂在哪个地址上?
看数据手册,GPIO的基地址是 0x01C20800。这个地址怎么来的?全志芯片的惯例,外设模块都挂在 0x01C20000 这个段上,GPIO从偏移 0x800 开始。
每个GPIO端口(PA、PB、PC...)都有一组寄存器。以PA为例,常用的寄存器有:
| 寄存器名称 | 偏移地址 | 作用 |
|---|---|---|
| PA_CFG0 | 0x00 | 配置PA0~PA7的引脚功能 |
| PA_CFG1 | 0x04 | 配置PA8~PA15的引脚功能 |
| PA_DAT | 0x10 | 数据寄存器,读引脚电平,写输出电平 |
| PA_DRV0 | 0x14 | 驱动能力控制 |
| PA_PUL0 | 0x1C | 上下拉控制 |
嗯,这里要注意:每个引脚的功能配置,在CFG寄存器里占4位。比如PA0对应CFG0的bit[3:0],PA1对应bit[7:4],以此类推。4位能表示16种功能,但F1C200s的引脚通常只用到其中几种。
核心思路:操作GPIO,就是操作这些寄存器的值。C语言里用指针直接访问地址即可。
3.2 时钟使能
很多新手会栽在这里——寄存器地址写对了,值也配了,但灯就是不亮。为什么?因为GPIO模块的时钟没开。
全志芯片的外设,默认时钟都是关闭的。你得先到时钟管理模块(CCU)那里,把对应GPIO端口的时钟门控打开。
CCU的基地址是 0x01C20000。GPIO的时钟使能寄存器是 CCU_BUS_CLK_GATE0,偏移 0x60。
这个寄存器的bit[2]控制PIO(GPIO)的时钟。写1使能,写0关闭。
// 时钟使能代码示例
#define CCU_BASE 0x01C20000
#define CCU_BUS_CLK_GATE0 (CCU_BASE + 0x60)
// 使能GPIO时钟
*(volatile unsigned int *)CCU_BUS_CLK_GATE0 |= (1 << 2);
我曾经在一个项目里,调了半天I2C不工作,最后发现是时钟门控没开。从那以后,我写外设驱动第一件事就是:先查时钟使没使能。
小技巧:写代码时,把时钟使能单独封装成一个函数。每次初始化新外设,先调一下这个函数,养成习惯。
3.3 引脚复用配置
F1C200s的引脚,大多是多功能的。同一个引脚,可以当GPIO用,也可以当UART、SPI、I2C等。怎么选?靠的就是CFG寄存器。
以PA0为例,如果我想让它当普通的GPIO输出,就把CFG0的bit[3:0]设为 0001(全志的惯例,0000是输入,0001是输出,其他值对应具体外设功能)。
你想想看,如果忘了配这个,直接去写DAT寄存器,那灯能亮吗?大概率不会。因为引脚还处于默认的输入状态,或者被复用到其他功能上了。
// 配置PA0为输出模式
#define GPIO_BASE 0x01C20800
#define PA_CFG0 (GPIO_BASE + 0x00)
// 先清零PA0的配置位,再设置为输出(0x01)
*(volatile unsigned int *)PA_CFG0 &= ~(0xF << 0);
*(volatile unsigned int *)PA_CFG0 |= (0x01 << 0);
注意:修改CFG寄存器时,一定要用"读-改-写"的方式。直接赋值会影响到其他引脚。我见过有人直接写 *(reg) = 0x01,结果把PA0~PA7全改了,调试了半天才发现。
3.4 输出控制寄存器操作
引脚配成输出了,时钟也开了,接下来就是控制电平高低。
DAT寄存器,偏移 0x10。写1输出高电平,写0输出低电平。
注意:DAT寄存器是可读可写的。读的时候,返回的是引脚当前的实际电平(不是输出寄存器的值)。写的时候,设置输出电平。
// 控制PA0输出高电平
#define PA_DAT (GPIO_BASE + 0x10)
*(volatile unsigned int *)PA_DAT |= (1 << 0);
// 控制PA0输出低电平
*(volatile unsigned int *)PA_DAT &= ~(1 << 0);
这里有个细节:如果你用 |= 和 &= 操作,只会影响目标引脚,其他引脚不受影响。但如果你用 *(reg) = value,那所有引脚的电平都会被改写。
3.5 实现LED闪烁
好了,所有知识点都齐了。现在把它们串起来,写一个完整的LED闪烁程序。
假设LED接在PA0上,高电平点亮。
// 延时函数(粗略延时)
void delay(volatile unsigned int count) {
while (count--);
}
// GPIO初始化
void gpio_init(void) {
// 1. 使能GPIO时钟
*(volatile unsigned int *)CCU_BUS_CLK_GATE0 |= (1 << 2);
// 2. 配置PA0为输出
*(volatile unsigned int *)PA_CFG0 &= ~(0xF << 0);
*(volatile unsigned int *)PA_CFG0 |= (0x01 << 0);
}
// 主函数
int main(void) {
gpio_init();
while (1) {
// 点亮LED
*(volatile unsigned int *)PA_DAT |= (1 << 0);
delay(0x100000);
// 熄灭LED
*(volatile unsigned int *)PA_DAT &= ~(1 << 0);
delay(0x100000);
}
return 0;
}
这个程序,编译后烧到F1C200s里,接上LED,你应该能看到它一闪一闪的。
如果灯不亮,按这个顺序排查:
- 硬件连接对不对?LED正极接PA0,负极通过电阻接地。
- 时钟使能了吗?检查CCU寄存器。
- 引脚复用配了吗?CFG寄存器是不是设成了输出?
- DAT寄存器写对了吗?电平方向对不对?
说实话,点灯实验虽然简单,但它背后涉及的寄存器映射、时钟管理、引脚复用,是后面所有外设驱动的基础。你把这个流程走通了,后面学UART、SPI、I2C,无非就是换一组寄存器、换一套配置而已。
嗯,下一章我们会讲如何用串口打印调试信息。有了串口,调试效率会高很多。但在此之前,先把灯点起来,给自己一点信心。