3. FPGA开发板与工具链

好了,到了真正动手写代码的时候了。这一章我们要做的事很简单——让板子上的LED灯按照我们的心意亮起来、灭下去。听起来是不是有点小儿科?但别小看这一步,它背后牵扯的东西可不少:GPIO模块的寄存器映射、内存地址怎么理解、软件延时怎么实现,还有怎么用逻辑分析仪验证你的代码真的在干活。

我记得刚入行那会儿,第一次点亮LED,看着灯闪起来,那种成就感比后来调通一个复杂的DDR控制器还要强烈。说白了,这就是你和硬件之间的第一次对话。

3.1 GPIO模块寄存器映射

蜂鸟E203的GPIO模块,挂在系统总线上。你要操作它,就得知道它的基地址。E203的GPIO基地址是0x10012000。这个数字怎么来的?去看芯片手册或者RTL代码里的地址分配表。

GPIO模块内部有好几个寄存器,我们最常用的是这三个:

寄存器名称 偏移地址 功能描述
GPIO_INPUT_VAL 0x00 读取引脚电平状态
GPIO_OUTPUT_VAL 0x08 设置引脚输出电平
GPIO_OUTPUT_EN 0x10 使能引脚为输出模式

每个寄存器是32位的。E203的GPIO一共支持32个引脚,每一位对应一个引脚。比如GPIO_OUTPUT_EN寄存器的bit 0写1,就把GPIO0设为输出模式。

在C语言里,我们怎么访问这些寄存器?用指针。把基地址加上偏移量,强制转换成volatile uint32_t *,然后读写它。为什么加volatile?因为编译器太聪明了,它可能会优化掉你对同一个地址的多次读写,而硬件寄存器恰恰需要每次读写都真实发生。我在项目中遇到过编译器优化把延时循环直接删掉的情况,从那以后我再也不敢漏掉volatile。

// GPIO寄存器地址映射
#define GPIO_BASE       0x10012000UL
#define GPIO_INPUT_VAL  (*(volatile uint32_t *)(GPIO_BASE + 0x00))
#define GPIO_OUTPUT_VAL (*(volatile uint32_t *)(GPIO_BASE + 0x08))
#define GPIO_OUTPUT_EN  (*(volatile uint32_t *)(GPIO_BASE + 0x10))
小技巧:我习惯把寄存器地址定义成宏,放在一个单独的头文件里。这样代码看起来干净,移植到其他芯片时也只需要改这个头文件。

3.2 点亮板载LED

拿到开发板,先看看原理图。板载LED通常接在GPIO的某个引脚上,比如GPIO0。LED的另一端通过限流电阻接到VCC或GND。如果是低电平点亮(灌电流),那你就得输出0;如果是高电平点亮(拉电流),就输出1。

假设我们的LED接在GPIO0上,低电平点亮。那代码怎么写?

#include "e203_gpio.h"  // 上面定义的宏放在这个头文件里

void led_init(void)
{
    // 先把GPIO0设为输出模式
    GPIO_OUTPUT_EN |= (1 << 0);
}

void led_on(void)
{
    // 输出低电平,点亮LED
    GPIO_OUTPUT_VAL &= ~(1 << 0);
}

void led_off(void)
{
    // 输出高电平,熄灭LED
    GPIO_OUTPUT_VAL |= (1 << 0);
}

int main(void)
{
    led_init();
    led_on();
    while(1);
    return 0;
}

编译、下载、复位。如果LED亮了,恭喜你,第一个裸机程序跑通了。如果不亮,别慌,先检查硬件连接,再用万用表量一下引脚电平。我曾经有一次折腾了半天,结果是LED焊反了。

3.3 软件延时实现流水灯

单个LED点亮太单调了,我们来做个流水灯。流水灯的核心就是:依次点亮每个LED,保持一段时间,然后熄灭,再点亮下一个。

保持时间怎么实现?最简单的办法就是软件延时——让CPU空转。虽然不高效,但对于裸机入门来说足够了。

// 简单的软件延时函数
void delay(volatile uint32_t count)
{
    while(count--);
}

// 假设有4个LED,接在GPIO0~GPIO3
#define LED_NUM 4

void flow_led(void)
{
    uint32_t i;
    for(i = 0; i < LED_NUM; i++)
    {
        // 点亮第i个LED
        GPIO_OUTPUT_VAL &= ~(1 << i);
        delay(500000);  // 延时
        // 熄灭第i个LED
        GPIO_OUTPUT_VAL |= (1 << i);
    }
}

int main(void)
{
    uint32_t i;
    // 初始化所有LED引脚为输出
    for(i = 0; i < LED_NUM; i++)
    {
        GPIO_OUTPUT_EN |= (1 << i);
    }
    
    while(1)
    {
        flow_led();
    }
    return 0;
}

你可能会问:delay(500000)到底延时了多久?这取决于CPU的主频。E203默认主频是16MHz,每条指令大概需要几个时钟周期。算下来,500000次循环大概在几十到一百毫秒之间。具体多少?用逻辑分析仪抓一下就知道了。

注意:软件延时不可靠。它依赖CPU频率,而且会被中断打断。实际项目中要用硬件定时器。但作为入门学习,软件延时是最直观的方式。

3.4 逻辑分析仪抓取波形验证

代码写完了,灯也闪了。但你真的确定你的代码按预期执行了吗?肉眼只能看到灯亮灭,看不到时序细节。这时候就需要逻辑分析仪出场了。

逻辑分析仪,说白了就是多通道的示波器,专门抓数字信号。把探针夹在LED对应的GPIO引脚上,设置好采样率和触发条件,然后运行程序。你会看到这样的波形:

波形分析要点:

  • 每个通道对应一个LED,低电平表示点亮
  • 波形宽度就是延时时间,可以测量出来
  • 观察是否有毛刺或异常跳变

我习惯把逻辑分析仪的采样率设为10MHz以上,触发方式设为下降沿触发(因为LED是低电平点亮)。抓到的波形可以导出成CSV或VCD格式,方便后续分析。

有一次我在调试一个I2C设备时,逻辑分析仪帮我发现了一个时序违规——SCL高电平时间不够。这种问题用示波器也能看,但逻辑分析仪能同时抓多个通道,还能自动解码协议,效率高得多。

GPIO流水灯波形示意图 时间 GPIO0 GPIO1 GPIO2 GPIO3 低电平点亮LED 高电平熄灭LED

从波形图上可以清楚看到,每个LED依次被拉低一段时间,然后恢复高电平。这就是流水灯的效果。你还可以测量低电平的持续时间,验证你的延时函数是否准确。

调试建议:如果波形不对,先检查GPIO_OUTPUT_EN有没有设置正确。我见过有人忘了使能输出,结果引脚一直是高阻态,LED死活不亮。

嗯,这一章的内容就到这里。从寄存器映射到点亮LED,再到流水灯和波形验证,每一步都是后面更复杂程序的基础。你想想看,掌握了GPIO的操作,后面操作UART、SPI、I2C,其实都是类似的套路——找到基地址,配置寄存器,读写数据。只不过每个外设的寄存器功能不同罢了。