2、D1s 内存映射与 GPIO 控制:查看数据手册、系统内存地址映射、GPIO 寄存器详解、点亮第一颗 LED。

好,咱们正式开始动手了。

上一章我们搭好了开发环境,装好了交叉编译链。但说实话,光有工具链,你连一颗灯都点不亮。为啥?因为你还不知道芯片的「门牌号」——也就是内存地址。

这一章,我们就来啃下这块硬骨头。我会带你一起翻数据手册,找到D1s的内存映射表,然后死磕GPIO寄存器。最后,亲手点亮那颗象征「Hello World」的LED。

2.1 为什么要看数据手册?

很多初学者喜欢直接搜代码,复制粘贴。我个人习惯是:先看手册,再看代码。为什么?因为代码是人写的,可能有bug,但手册是芯片的「宪法」。

D1s的数据手册,全称叫《Allwinner D1s User Manual》。你可以在全志官网或者一些开源社区找到它。PDF大概有上千页,别怕,我们只看需要的部分。

嗯,这里要注意:看手册不是从头读到尾,而是带着问题去查。比如我们现在的问题就是:GPIO的寄存器地址在哪里?

我的小技巧: 把数据手册的「Memory Mapping」章节(通常是第2章或第3章)打印出来,贴在工位上。我当年做项目时,这张表都快被我翻烂了。

2.2 系统内存地址映射

打开手册,找到「Memory Mapping」或者「Address Space」章节。你会看到一张巨大的表格,列出了所有外设的基地址。

对于D1s,关键地址如下:

外设 基地址 大小
SRAM (内部) 0x00000000 64KB
DDR (外部) 0x40000000 取决于硬件
GPIO 0x02000000 0x1000 (4KB)
UART0 0x02500000 0x400
TIMER 0x02050000 0x400

看到了吗?GPIO的基地址是 0x02000000。这意味着,所有GPIO相关的寄存器,都藏在这个地址开始的4KB空间里。

你可能会问:「为什么是4KB?」

因为D1s的GPIO控制器有多个Bank(PA、PB、PC...),每个Bank需要一组寄存器。4KB的空间,足够塞下所有Bank的配置了。

2.3 GPIO 寄存器详解

好,现在我们把目光聚焦到GPIO上。在手册里搜索「GPIO Controller」,你会找到每个寄存器的详细定义。

对于每个GPIO Bank(比如PA),主要寄存器有这几个:

  • GPIO_CFG (配置寄存器):设置引脚是输入、输出、还是复用功能。每个引脚占4位,一个寄存器控制8个引脚。
  • GPIO_DAT (数据寄存器):读取引脚电平,或者设置输出电平。每个引脚占1位。
  • GPIO_DRV (驱动能力寄存器):设置引脚的驱动电流强度。一般用默认值就行。
  • GPIO_PUL (上拉/下拉寄存器):设置内部上拉或下拉电阻。

以PA0为例,它的配置寄存器地址怎么算?

公式很简单:基地址 + Bank偏移 + 寄存器偏移

PA的Bank偏移是 0x000(第一个Bank)。GPIO_CFG0寄存器的偏移是 0x00。所以PA0的配置寄存器地址就是:

0x02000000 + 0x000 + 0x00 = 0x02000000

等等,这里有个坑。GPIO_CFG0控制的是PA0~PA7,每个引脚占4位。PA0对应的是bit[3:0]。所以如果你想设置PA0为输出模式,需要把bit[3:0]写成 0x1

我曾经踩过的坑: 有一次我直接把整个寄存器写成了0x1,结果把PA1~PA7全部清成了输入模式。后来排查了半天,才发现是忘了做「读-改-写」操作。切记:修改寄存器时,先读后写,只改你要动的位

2.4 点亮第一颗 LED

理论说完了,咱们来点实际的。假设你的开发板上,LED接在 PE12 上(高电平点亮)。

PE是第4个Bank(PA=0, PB=1, PC=2, PD=3, PE=4)。Bank偏移是 0x000 + 4 * 0x24 = 0x90?不对,D1s的Bank偏移是连续的,每个Bank占用 0x24 字节。PE的基地址是 0x02000000 + 4 * 0x24 = 0x02000090

PE12属于PE8~PE15这一组,由GPIO_CFG1寄存器控制(偏移 0x04)。PE12的配置位是bit[19:16](因为每个引脚4位,12号引脚对应第3组,3*4=12,所以是bit[19:16])。

好,代码来了:

// 定义基地址
#define GPIO_BASE       0x02000000

// PE Bank 偏移 (第4个Bank)
#define PE_BASE         (GPIO_BASE + 4 * 0x24)

// 寄存器偏移
#define GPIO_CFG1       0x04
#define GPIO_DAT        0x10

// PE12 配置位: bit[19:16]
#define PE12_CFG_MASK   (0xF << 16)
#define PE12_CFG_OUT    (0x1 << 16)

// PE12 数据位: bit[12]
#define PE12_DAT_MASK   (0x1 << 12)

void led_init(void)
{
    unsigned int *cfg_reg = (unsigned int *)(PE_BASE + GPIO_CFG1);
    unsigned int val;

    // 读-改-写:先读当前值
    val = *cfg_reg;
    // 清除PE12的配置位
    val &= ~PE12_CFG_MASK;
    // 设置为输出模式 (0x1)
    val |= PE12_CFG_OUT;
    // 写回寄存器
    *cfg_reg = val;
}

void led_on(void)
{
    unsigned int *dat_reg = (unsigned int *)(PE_BASE + GPIO_DAT);
    unsigned int val;

    val = *dat_reg;
    val |= PE12_DAT_MASK;   // 置1,输出高电平
    *dat_reg = val;
}

void led_off(void)
{
    unsigned int *dat_reg = (unsigned int *)(PE_BASE + GPIO_DAT);
    unsigned int val;

    val = *dat_reg;
    val &= ~PE12_DAT_MASK;  // 清0,输出低电平
    *dat_reg = val;
}

int main(void)
{
    led_init();

    while(1)
    {
        led_on();
        // 简单延时,后面我们会用定时器替代
        for(volatile int i = 0; i < 1000000; i++);
        led_off();
        for(volatile int i = 0; i < 1000000; i++);
    }

    return 0;
}

编译、烧录、上电。如果一切顺利,你会看到LED开始闪烁。

如果没亮?别急,检查这几件事:

  • LED的极性对不对?有些板子是低电平点亮。
  • GPIO引脚号对不对?我见过有人把PE12当成PE13用。
  • 有没有忘记初始化时钟?D1s的GPIO默认是使能的,但有些外设需要手动开启时钟。

核心要点:

  • 内存映射是芯片的「门牌号」,必须熟记关键外设的基地址。
  • GPIO寄存器操作遵循「读-改-写」原则,避免影响其他引脚。
  • 点亮LED看似简单,但背后涉及地址计算、位操作、寄存器理解——这是嵌入式开发的基石。

好了,第一颗LED已经点亮。下一章,我们会深入定时器,让闪烁变得更精确。到时候,那个丑陋的for循环延时就可以退休了。