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,功能强了,但上手门槛也高了点。不过别担心,核心流程没变。

创建工程的步骤,我习惯这么来:

  1. 导出硬件描述文件——在Vivado里完成综合实现后,File → Export → Export Hardware。记得勾选“Include bitstream”。
  2. 启动Vitis——选择工作空间目录。我个人习惯把每个项目单独建一个workspace,避免文件混乱。
  3. 创建应用工程——File → New → Application Project。输入工程名,选择刚才导出的.xsa文件。
  4. 选择模板——选“Empty Application(C/C++)”。别选那些带Hello World的模板,咱们要自己掌控一切。
  5. 编写代码——在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 知识体系图

下面这张图,把本章的核心逻辑串起来了。从工程创建到最终读写,每一步都离不开地址映射和函数调用。

PS端裸机驱动开发流程 PS端裸机驱动开发核心流程 Vivado导出硬件 Vitis创建工程 地址映射 Xil_In32 / Xil_Out32 寄存器读写操作 调试与验证 关键点: 物理地址直接访问 | volatile防止优化 | 4字节对齐 | 小端模式

4.7 常见问题与避坑

  • 读回全F或全0——检查地址是否正确,确认PL侧是否已配置且运行。
  • 写操作无反应——检查GPIO方向寄存器是否设置正确,或者PL侧逻辑有误。
  • Data Abort异常——大概率是非对齐访问,或者访问了不存在的地址空间。
  • 优化后行为异常——检查是否漏了volatile,或者编译器优化级别太高。

重要提醒:千万不要在中断服务函数里用Xil_Out32写大量数据。我曾经这么干过,结果中断响应时间暴增,系统直接崩了。中断里只做标志位设置,数据搬运放到主循环里做。