4. PS端裸机驱动开发:Xil_In32/Xil_Out32与地址转换
好,咱们进入第四章。前面几章我们把AXI总线的协议、时序和寄存器映射聊透了。这一章,咱们要真正动手写代码了——在PS端用裸机程序去控制PL逻辑。
说白了,就是让ARM Cortex-A9核通过AXI总线,直接读写PL里的寄存器。你想想看,这就像给两个独立的世界搭了一座桥。桥搭好了,怎么走?就是靠Xil_In32和Xil_Out32这两个函数。
核心要点:裸机驱动开发 = 地址映射 + 寄存器读写 + 中断处理。本章先搞定前两步。
4.1 工程创建:从SDK到Vitis
我记得最早用Xilinx SDK时,界面还比较朴素。后来升级到Vitis,功能强了,但上手门槛也高了点。不过别担心,核心流程没变。
创建工程的步骤,我习惯这么来:
- 导出硬件描述文件——在Vivado里完成综合实现后,File → Export → Export Hardware。记得勾选“Include bitstream”。
- 启动Vitis——选择工作空间目录。我个人习惯把每个项目单独建一个workspace,避免文件混乱。
- 创建应用工程——File → New → Application Project。输入工程名,选择刚才导出的.xsa文件。
- 选择模板——选“Empty Application(C/C++)”。别选那些带Hello World的模板,咱们要自己掌控一切。
- 编写代码——在src目录下新建main.c,开始写驱动。
小技巧:Vitis里有个“Board Support Package(BSP)”设置,里面可以配置驱动库。裸机开发时,记得把“xil_io”库勾上,否则Xil_In32会报未定义。
4.2 Xil_In32与Xil_Out32:最底层的读写工具
这两个函数,是Xilinx提供的底层IO访问接口。它们直接操作物理地址,不经过MMU,也不经过缓存。
函数原型很简单:
// 从指定地址读取32位数据
u32 Xil_In32(UINTPTR Addr);
// 向指定地址写入32位数据
void Xil_Out32(UINTPTR Addr, u32 Value);
参数Addr就是物理地址。在Zynq里,PL侧的AXI从设备地址由Vivado分配。比如你在Vivado里给AXI GPIO分配的基地址是0x4120_0000,那读写这个地址就能控制PL的寄存器。
我在项目中遇到过一个问题:直接用Xil_In32读回来的值总是0xFF。查了半天,原来是地址写错了——少写了一个0。嗯,这种低级错误最坑人。
4.3 内存映射与地址转换:别让指针乱飞
这里有个关键概念:物理地址 ≠ 虚拟地址。
在裸机环境下,MMU默认是关闭的。所以CPU发出的地址就是物理地址。但如果你开启了MMU(比如跑Linux时),那就要做地址转换。
咱们裸机开发,MMU默认关闭。所以Xil_In32的地址参数直接填物理地址就行。但有些情况下,你需要用指针操作——比如把外设地址映射到一个结构体上。
举个例子:
// 定义AXI GPIO寄存器结构体
typedef struct {
volatile u32 GPIO_DATA; // 0x00
volatile u32 GPIO_TRI; // 0x04
volatile u32 GPIO_2; // 0x08
} AXI_GPIO_Reg;
// 映射到物理地址
#define AXI_GPIO_BASEADDR 0x41200000
AXI_GPIO_Reg *gpio = (AXI_GPIO_Reg *)AXI_GPIO_BASEADDR;
// 读写操作
gpio->GPIO_DATA = 0x01; // 等价于 Xil_Out32(0x41200000, 0x01)
u32 val = gpio->GPIO_TRI; // 等价于 Xil_In32(0x41200004)
这里有个坑:volatile关键字不能少。编译器可能会优化掉看似无用的读操作,加上volatile能保证每次都从物理地址读取。
避坑指南:我曾经在一个项目里忘了加volatile,结果调试了整整两天。现象是:第一次读寄存器正常,第二次读永远返回相同值。加上volatile后立刻解决。所以,操作外设寄存器时,volatile是保命符。
4.4 地址对齐与字节序
Zynq的AXI总线要求32位访问必须4字节对齐。如果你传了一个非对齐地址给Xil_In32,轻则返回错误数据,重则触发Data Abort异常。
另外,ARM Cortex-A9是小端模式。PL侧如果也是小端,那就没问题。如果PL侧是大端,你就得手动做字节序转换。
| 特性 | 说明 | 注意事项 |
|---|---|---|
| 地址对齐 | 32位访问必须4字节对齐 | 非对齐访问会触发异常 |
| 字节序 | ARM默认小端 | PL侧需保持一致 |
| volatile | 防止编译器优化 | 操作寄存器时必须加 |
| 地址范围 | PL外设地址在0x4000_0000~0x7FFF_FFFF | 不要越界访问 |
4.5 实战:点亮PL侧的LED
咱们来写一个完整的例子。假设PL侧有一个AXI GPIO模块,基地址0x4120_0000,GPIO_DATA寄存器控制8个LED。
#include "xil_io.h"
#include "xparameters.h"
// AXI GPIO基地址
#define GPIO_BASEADDR 0x41200000
// 寄存器偏移
#define GPIO_DATA_OFFSET 0x00
#define GPIO_TRI_OFFSET 0x04
int main() {
// 设置GPIO方向:全部输出
Xil_Out32(GPIO_BASEADDR + GPIO_TRI_OFFSET, 0x00);
// 循环点亮LED
while(1) {
// 点亮所有LED
Xil_Out32(GPIO_BASEADDR + GPIO_DATA_OFFSET, 0xFF);
delay(1000000);
// 熄灭所有LED
Xil_Out32(GPIO_BASEADDR + GPIO_DATA_OFFSET, 0x00);
delay(1000000);
// 流水灯
for(int i = 0; i < 8; i++) {
Xil_Out32(GPIO_BASEADDR + GPIO_DATA_OFFSET, 1 << i);
delay(500000);
}
}
return 0;
}
// 简单的延时函数
void delay(volatile u32 count) {
while(count--);
}
这段代码很简单,但包含了裸机驱动的核心要素:地址定义、寄存器操作、循环控制。你可以在Vitis里编译下载,然后观察开发板上的LED闪烁。
调试技巧:如果LED不亮,先用Xil_In32读一下GPIO_TRI寄存器,看看方向设置是否生效。再用万用表量一下GPIO引脚电平。硬件问题往往比软件问题更隐蔽。
4.6 知识体系图
下面这张图,把本章的核心逻辑串起来了。从工程创建到最终读写,每一步都离不开地址映射和函数调用。
4.7 常见问题与避坑
- 读回全F或全0——检查地址是否正确,确认PL侧是否已配置且运行。
- 写操作无反应——检查GPIO方向寄存器是否设置正确,或者PL侧逻辑有误。
- Data Abort异常——大概率是非对齐访问,或者访问了不存在的地址空间。
- 优化后行为异常——检查是否漏了volatile,或者编译器优化级别太高。
重要提醒:千万不要在中断服务函数里用Xil_Out32写大量数据。我曾经这么干过,结果中断响应时间暴增,系统直接崩了。中断里只做标志位设置,数据搬运放到主循环里做。