一个一个来吧,按照顺序来吧,我想都跑一遍,先第一个支柱:存储器映射(Memory Map),给一个具体日常的经典的案例,手把手教我打通,包括每一步操作的讲解一下为什么需要这样做,详细一点,越具体越好,我不嫌细,因为我是小白,谢谢您,开始吧,
同学,坐。你能静下心来,决定把这四根支柱一根一根地"啃透",老师非常高兴。这就像建大楼,如果不把地基打得坚如磐石,上面的楼盖得再高也会塌。
咱们今天就从**第一根支柱:存储器映射(Memory Map)**开始。
为了让你真正"打通"这个概念,咱们不搞那些空泛的理论,老师直接给你一个最经典、最日常、且不需要写一行 C 代码就能验证的实战案例 :用 ARM 处理器(PS端)直接控制 Zynq 的 MIO 引脚来点亮一个 LED,并且用代码去读取一个按键的状态。
在这个案例中,你会亲眼看到"存储器映射"是怎么把 C 语言的数值变成物理电平的。
🎯 第一步:查字典------地址映射是怎么定的?(Vivado 里的硬件图纸)
在 C 语言的世界里,你要操作硬件,必须先知道它的"门牌号(地址)"。这个门牌号是谁分配的?是 Vivado 分配的。
-
打开 Vivado:新建工程,选好你的芯片(xc7z010clg400-1)。
-
配置 Zynq :在 Zynq 配置界面里,找到 MIO Configuration。
-
假设你的开发板上,LED 接在 MIO 50 上,按键 接在 MIO 51 上。
-
将 MIO 50 配置为
GPIO (Output),MIO 51 配置为GPIO (Input)。
-
-
查地址 :在 Vivado 左侧的
Address Editor中,你会看到GPIO模块被分配了一个基地址,比如0xE000A000。
❓ 为什么要这样做?
在 FPGA 里,你写 assign led = 1'b1; 综合工具会自动帮你连线。但在 PS 端,ARM 处理器没有专用的"点灯引脚",它只认地址。Vivado 的作用,就是告诉你:"在 ARM 看来,GPIO 这个外设,就放在 0xE000A000 这个地址开始的仓库里。"
🎯 第二步:查说明书------0xE000A000 里面到底放了什么?(UG585 手册)
现在你知道了大仓库在 0xE000A000。但是,仓库里有成百上千个柜子(寄存器)。你想让 MIO 50 输出高电平,该去哪个柜子?
你需要查阅 Zynq 的技术参考手册(UG585),找到 GPIO 那一章。
-
你会查到,控制 MIO 输出的寄存器叫
DATA_0(数据寄存器 0),它的偏移地址是0x040。 -
所以,控制 MIO 0~31 引脚电平的绝对地址是
0xE000A000 + 0x040 = 0xE000A040。 -
控制 MIO 32~51 引脚电平的绝对地址是
0xE000A000 + 0x044 = 0xE000A044。
❓ 为什么要这样做?
这就是底层硬件设计。每个寄存器(柜子)都有特定的功能。你要让 MIO 50 亮灯(50 在 32~51 之间),你就必须往 0xE000A044 这个地址里写数据。
🎯 第三步:底层驱动------BSP 是怎么帮你干脏活的?(xgpio_extra.c)
如果你纯手写,你得这样写:
c
// 纯寄存器操作,极度硬核
*(volatile unsigned int *)(0xE000A044) |= (1 << (50 - 32));
但我们在 Vitis 里,通常是用 Xilinx 提供的 BSP(板级支持包) 库函数:
c
XGpioPs_WritePin(&Gpio, 50, 0x01); // 让 50 号引脚输出高电平
你看,BSP 帮我们把那一长串地址计算给封装了。
❓ 为什么要这样做?
-
XGpioPs_WritePin的底层实现,其实就是帮你算了地址,然后执行了Xil_Out32。 -
如果你写的是
XGpioPs_WritePin(&Gpio, 32, 0x01),底层代码会自动判断 32 属于哪一组,然后去操作0xE000A040。这就是**硬件抽象层(BSP)**的价值。
🚀 第四步:手把手实战(在 Vitis 里跑起来)
好,理论讲完,咱们上机(请确保你手头有一块 Zynq 板子):
1. 新建空工程
-
打开 Vitis,基于你的
.xsa创建一个新工程,选Empty Application (C)。 -
在
src文件夹下新建一个main.c。
2. 敲入以下"打通任督二脉"的代码:
c
#include "xparameters.h"
#include "xgpiops.h"
#include "xil_printf.h"
// 1. 声明一个 GPIO 驱动对象
XGpioPs Gpio;
XGpioPs_Config *Config;
int main()
{
// 2. 初始化 GPIO
// 去查字典(xparameters.h),找到 GPIO 的硬件设备号
Config = XGpioPs_LookupConfig(XPAR_XGPIOPS_0_DEVICE_ID);
// 把硬件地址绑定到我们的 Gpio 对象上
XGpioPs_CfgInitialize(&Gpio, Config, Config->BaseAddr);
// 3. 配置 MIO 50(LED)为输出
// 第一个参数是 Gpio 对象,第二个是引脚号,第三个 1 代表输出
XGpioPs_SetDirectionPin(&Gpio, 50, 1);
// 开启输出使能(有些板子不开启这个,引脚没反应)
XGpioPs_SetOutputEnablePin(&Gpio, 50, 1);
// 4. 配置 MIO 51(按键)为输入
// 第三个参数 0 代表输入
XGpioPs_SetDirectionPin(&Gpio, 51, 0);
xil_printf("Memory Map Test Start!\n\r");
while(1)
{
// 5. 读取 MIO 51(按键)的电平状态
// 如果按键按下(假设低电平有效,读到 0),就把对应的值赋给变量
u32 key_val = XGpioPs_ReadPin(&Gpio, 51);
if (key_val == 0) // 按键被按下了
{
// 6. 往 MIO 50 写 1(高电平),点亮 LED
XGpioPs_WritePin(&Gpio, 50, 1);
xil_printf("Key Pressed, LED ON!\n\r");
}
else
{
// 往 MIO 50 写 0(低电平),熄灭 LED
XGpioPs_WritePin(&Gpio, 50, 0);
}
// 加一点软件延时,防止 CPU 跑太快
for(volatile int i = 0; i < 100000; i++);
}
return 0;
}
3. 运行与观察:
-
编译,下载到板子。
-
打开串口终端(115200),你会看到
Memory Map Test Start!。 -
当你按下板子上的按键时,LED 亮起,串口打印
Key Pressed, LED ON!。
💡 老师的灵魂拷问(为什么需要这样做?)
同学,当你按下按键,LED 亮起的那一刻。你想想底层发生了什么?
你的 C 代码 XGpioPs_WritePin(&Gpio, 50, 1); 被编译成了机器指令。
ARM 核对这条指令说:"去 0xE000A044 这个内存地址,写入一个数值 1。"
AXI 总线把数据搬运过去,GPIO 控制器里的寄存器被改写,瞬间,MIO 50 的物理引脚从 0V 变成了 1.8V(或 3.3V)。
这就是"存储器映射"的终极奥义:
你在软件里写内存,硬件里就产生电信号。
没有魔法,全凭地址。
🎯 你的课后作业(极其重要!)
为了证明你真的懂了第一根支柱,请你做这件事:
-
在这个工程里,不要用 BSP 提供的
XGpioPs_WritePin函数。 -
请你在代码里,直接用
Xil_Out32(地址, 数值);这个底层函数,把 LED 点亮。 -
思考题 :你的板子 MIO 50 对应的绝对是
0xE000A044这个地址吗?如果不是,该怎么查?
把课后的代码写出来,发给老师看看。这第一根支柱,你一旦打通,后面的路就好走多了!去试试吧!
Xil_Out32(地址, 数值)这个底层函数,与XGpioPs_WritePin 函数区别在哪,还请指点,谢谢啦
同学,坐。你问出的这个问题,含金量极高!
很多学 Zynq 的人,用了一辈子 XGpioPs_WritePin,却根本不知道它底层到底在干嘛。你能把这两个函数放在一起对比,说明你已经看透了"软件控制硬件"的本质。
老师用最直白的话给你解释:Xil_Out32 是纯手动挡(直接干硬件),而 XGpioPs_WritePin 是全自动挡(走正规流程,带安全保护)。
咱们来详细拆解:
🎯 1. Xil_Out32(地址, 数值) ------ 纯手动挡,暴力直男
-
它的本质 :这是 Xilinx 提供的一个最底层的宏/函数 。它不管什么 GPIO,不管什么外设,它的唯一动作就是:往你给的"物理内存地址"里,强行塞入一个 32 位的数值。
-
底层汇编 :它其实就是一句 C 语言指针强转(假设地址是
0xE000A044,数值是1):c
*(volatile unsigned int *)0xE000A044 = 1; -
优点:极快、极简,没有任何中间环节。
-
缺点(致命) :它完全不讲武德,没有任何安全检查。
-
如果你把地址写错了一位,比如写成了
0xE000A045,它也会毫不犹豫地把数据塞进去,结果就是导致系统崩溃(跑飞)或者影响其他引脚。 -
它不知道你要操作的是哪个外设。
-
🎯 2. XGpioPs_WritePin(&Gpio, 50, 1) ------ 全自动挡,大管家
-
它的本质 :这是 Xilinx 官方 BSP(板级支持包)里写好的高级驱动函数。你只要告诉它"我要让 50 号引脚输出 1",剩下的它全包了。
-
底层发生了什么? 如果你点进这个函数的源码(通常在
xgpio_extra.c里),你会发现它内部其实也是用Xil_Out32来干活的,但在写之前,它做了大量的**"翻译和算计"**:-
查字典(查芯片手册):它会先检查 50 号引脚是 MIO 还是 EMIO?(50 是 MIO)。
-
算门牌号(地址换算) :50 号引脚属于 Bank 1(32~51号),所以它去操作
DATA_1寄存器。 -
算掩码(位运算) :50 号引脚在寄存器里是第
50 - 32 = 18位。所以它不能直接写 1,而是要把 1 左移 18 位(变成0x00040000)。并且,为了不影响这个寄存器里的其他引脚,它还要做&和|的安全操作。 -
最终执行 :算好之后,它调用
Xil_Out32(计算好的物理地址, 计算好的值)。
-
💡 总结对比表(你可以存在脑子里)
| 对比维度 | Xil_Out32 (底层手写) |
XGpioPs_WritePin (BSP 封装) |
|---|---|---|
| 操作对象 | 物理内存地址(如 0xE000A044) |
逻辑引脚号(如引脚 50) |
| 安全性 | 极低(写错地址直接崩) | 极高(自动算地址、防越界) |
| 代码可读性 | 差(一长串十六进制地址,不懂硬件看不懂) | 极好(看函数名就知道在干嘛) |
| 可移植性 | 极差(换一块芯片,地址全变,代码全废) | 极好(换芯片,只要 BSP 重新生成,代码不用改) |
| 执行速度 | 极快(直接写内存) | 稍微慢一点点(多了计算地址的开销) |
🚀 为什么老师要你做那个课后作业?
老师让你用 Xil_Out32 去点灯,不是让你以后写项目都用它 (那会让你累死)。老师是为了让你打破"BSP 函数"这个黑盒,让你确信:"哪怕我不调用高级函数,我直接对着物理地址写数据,硬件它也得乖乖听话!"
当你亲手用 Xil_Out32(0xE000A044, 0x00040000) 把 LED 点亮的那一瞬间,你对"存储器映射"和"软硬件结合"的理解,就超越了 90% 的初学者。
去试试吧!把代码写出来跑一下。如果成功了,把那个十六进制的地址和数值发给老师看看!