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,你应该能看到它一闪一闪的。

如果灯不亮,按这个顺序排查:

  1. 硬件连接对不对?LED正极接PA0,负极通过电阻接地。
  2. 时钟使能了吗?检查CCU寄存器。
  3. 引脚复用配了吗?CFG寄存器是不是设成了输出?
  4. DAT寄存器写对了吗?电平方向对不对?

说实话,点灯实验虽然简单,但它背后涉及的寄存器映射、时钟管理、引脚复用,是后面所有外设驱动的基础。你把这个流程走通了,后面学UART、SPI、I2C,无非就是换一组寄存器、换一套配置而已。

嗯,下一章我们会讲如何用串口打印调试信息。有了串口,调试效率会高很多。但在此之前,先把灯点起来,给自己一点信心。