3、第一个裸机程序:GPIO输出控制
好了,到了真正动手写代码的时候了。这一章我们要做的事很简单——让板子上的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次循环大概在几十到一百毫秒之间。具体多少?用逻辑分析仪抓一下就知道了。
3.4 逻辑分析仪抓取波形验证
代码写完了,灯也闪了。但你真的确定你的代码按预期执行了吗?肉眼只能看到灯亮灭,看不到时序细节。这时候就需要逻辑分析仪出场了。
逻辑分析仪,说白了就是多通道的示波器,专门抓数字信号。把探针夹在LED对应的GPIO引脚上,设置好采样率和触发条件,然后运行程序。你会看到这样的波形:
波形分析要点:
- 每个通道对应一个LED,低电平表示点亮
- 波形宽度就是延时时间,可以测量出来
- 观察是否有毛刺或异常跳变
我习惯把逻辑分析仪的采样率设为10MHz以上,触发方式设为下降沿触发(因为LED是低电平点亮)。抓到的波形可以导出成CSV或VCD格式,方便后续分析。
有一次我在调试一个I2C设备时,逻辑分析仪帮我发现了一个时序违规——SCL高电平时间不够。这种问题用示波器也能看,但逻辑分析仪能同时抓多个通道,还能自动解码协议,效率高得多。
从波形图上可以清楚看到,每个LED依次被拉低一段时间,然后恢复高电平。这就是流水灯的效果。你还可以测量低电平的持续时间,验证你的延时函数是否准确。
嗯,这一章的内容就到这里。从寄存器映射到点亮LED,再到流水灯和波形验证,每一步都是后面更复杂程序的基础。你想想看,掌握了GPIO的操作,后面操作UART、SPI、I2C,其实都是类似的套路——找到基地址,配置寄存器,读写数据。只不过每个外设的寄存器功能不同罢了。