2 使用方法

2.1 示例设计

Xillinux(1.1 及以上版本)包含一个示例设计,该设计由一个已连接的 IP 核、一个预安装的 Linux 驱动程序以及几个简单的演示用户空 间程序组成。

在逻辑侧,xillydemo.v(hd) 模块源文件包含一个从数组推断出的 32x32 位 RAM 实现。主机上的示例程序会访问这个 RAM。 可以直接在 Xillinux 上按如下方式编译并运行该程序:

# make
gcc -g -Wall -I. -O3 -c -o uiotest.o uiotest.c
gcc -g -Wall -I. -O3 uiotest.o -o uiotest
gcc -g -Wall -I. -O3 -c -o intdemo.o intdemo.c
gcc -g -Wall -I. -O3 intdemo.o -o intdemo
# ./uiotest /dev/uio0 4096
0123

C 语言源文件可在 Xillinux 文件系统的 /usr/src/xillinux/xillybus-lite/ 目录下找到(1.1 及以上版本)。

“uiotest”程序仅向寄存器数组的前四个 32 位元素写入四个值,然后读回并打印这些值,但可以很容易地修改为更有用的功 能。

“intdemo”程序展示了如何处理中断。由于示例逻辑不会触发任何中断,因此直接运行它没有意义,但它展示了如何等待中 断。

2.2 与主机应用程序的接口

Xillybus Lite 基于 Linux 的用户 I/O 接口(UIO),该接口将外设表示为主要通过内存映射访问的设备文件。要获得访问权限,可使用以下 代码:

#include <sys/mman.h>

int fd;
void *map_addr;
int size = ...;

fd = open("/dev/uio0", O_RDWR);

if (fd < 0) {
  perror("Failed to open devfile");
  exit(1);
}

map_addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,
                fd, 0);

if (map_addr == MAP_FAILED) {
  perror("Failed to mmap");
  exit(1);
}

除错误检查外,这段代码执行了两个操作:

  • 调用 open() 函数打开设备文件(获得文件句柄)。

  • 调用 mmap() 函数获得访问设备的地址。第二个参数(“size”)是映射的字节数。根据设备树,它不能超过为外设分配的字节 数(未修改的 Xillinux 上为 4096)。

map_addr 是进程虚拟内存空间中的一个地址,但完全可以将其视为裸机环境(即无操作系统)中外设所映射到的物理地址。

允许的访问范围从 mem_addr 到 mem_addr + size - 1,其中 “size” 是传递给 mmap() 的第二个参数。尝试访问超出此范围 的内存可能会导致段错误。

获得地址后,对外设基地址(偏移量为零)处的寄存器进行 32 位字的写和读操作只需:

volatile unsigned int *pointer = map_addr;


*pointer = the_value_to_write;
the_value_read_from_register = *pointer;

在该特定内存区域上,Linux 驱动程序禁用了内存缓存,并且指针被标记为 volatile。因此,程序中的每次读写操作都会触发 一次总线操作,从而在 Xillybus Lite 的逻辑接口信号上产生一次访问周期。

重要 的:
指针必须使用“volatile”关键字标记为 volatile,如上例所示。缺少此标记将允许 C 编译器对 I/O 操作进行重新排序,并可能 优化掉这些操作。

如果逻辑支持字节粒度访问,也可以使用 8 位 volatile char 指针或 16 位 volatile short int 指针来访问外设。

在上面的示例中,假设只有一个 Xillybus Lite 外设存在,因此打开了第一个实例“/dev/uio0”。如果存在其他 UIO 设备(例 如,有多个 Xillybus Lite 实例),它们将表示为 /dev/uio1、/dev/uio2 等。

为了知道哪个设备文件属于哪个逻辑元件,应用程序应获取 /sys/class/uio/ 中的信息(例如 /sys/class/uio/uio0/name 或 /sys/class/uio/uio0/maps/map0/addr)。当创建多个 UIO 设备时,建议使用 udev 框架来实现设备文件的一致命名。

2.3 与逻辑设计的接口

2.3.1 寄存器相关信号

Xillybus Lite IP 核向应用逻辑提供七个信号,这里以 Verilog 格式给出:

output        user_clk;

output [31:0] user_addr;

output        user_wren;
output [3:0] user_wstrb;
output [31:0] user_wr_data;

output       user_rden;
input [31:0] user_rd_data;

该接口是同步的,基于由 Xillybus Lite 提供的 user_clk(它连接到处理器的 AXI Lite 时钟)。

上面的信号名称是 Xillydemo 模块(Xillinux 工具包的一部分)中出现的名称。处理器模块中信号的名称略有不同,例如 user_wren 可能显示为 xillybus_lite_0_user_wren_pin。

这些信号可以直接连接到标准块 RAM,此时主机可以直接访问该 RAM(如果选择双端口 RAM,则可用作“邮箱”)。它们也 可以连接到逻辑中定义的寄存器,如下所述。

2.3.2 模块层次结构

当 AMD 逻辑设计涉及嵌入式处理器时,会有一个表示该处理器的模块,通常在顶层模块中实例化。通常,该模块暴露的端 口都直接连接到物理引脚,遵循处理器是核心,其周围的任何逻辑都是某种外设的范式。

Xillybus Lite 旨在与大量的应用逻辑接口,因此在一定程度上打破了这种常见结构:其 user_* 信号旨在路由到顶层模块,因此自定义逻辑 也在该顶层模块中实例化。整个项目的结构最终分为两大块:一个实例化模块,包含处理器及其 IP 核(包括 Xillybus Lite IP 核);另一个模 块包含应用逻辑。user_* 信号在两者之间连接。

因此,尽管 Xillybus Lite IP 核本身是由 AMD 的工具在处理器层次结构深处的某个地方实例化的,但它是从顶层模块进行接 口连接的。

这是 Xillinux 演示工具包中的选定布局(如下图所示),也是本指南所假定的布局。也可以将 Xillybus Lite 的信号在处理器层 次结构内部进行连接,但这不一定会使事情变得更简单。

2.3.3 32 位对齐寄存器访问

要访问逻辑中的 32x32 位数组(下面称为“litearray”),可以使用如下代码。仅当主机坚持使用 32 位字访问(例如仅使用 unsigned int 指针)时,这才正常工作:

在 Verilog 中:

always @(posedge user_clk)
  begin
    if (user_wren)
      litearray[user_addr[6:2]] <= user_wr_data;

    if (user_rden)
       user_rd_data <= litearray[user_addr[6:2]];
  end

或者在 VHDL 中:

lite_addr <= conv_integer(user_addr(6 DOWNTO 2));

process (user_clk)
begin
  if (user_clk'event and user_clk = '1') then
    if (user_wren = '1') then
      litearray(lite_addr) <= user_wr_data;
    end if;

    if (user_rden = '1') then
       user_rd_data <= litearray(lite_addr);
    end if;
  end if;
end process;

对齐写入周期和任何读取周期的波形如下:


波形 1:对齐访问的写入周期


波形 2:读取周期

注意事项:

  • 在 XPS 中分配给 Xillybus Lite 外设的地址区域上的任何总线操作,都必然导致 user_wren 或 user_rden 恰好高电平一个时 钟周期。

  • Xillybus Lite 核仅在 user_rden 变为高电平后的一个时钟周期后对 user_rd_data 进行采样。因此实际上无需监控 user_rden:始终根据 user_addr 更新 user_rd_data(具有一个时钟的延迟)也是可以的,例如:

    always @(posedge user_clk)
      user_rd_data <= litearray[user_addr[6:2]];
    
  • 上面的代码演示了访问一个包含 32 个元素的 32 位宽数组。更常见的设置是访问寄存器,例如在 Verilog 中:

    always @(posedge user_clk)
      if ((user_wren) && (user_addr[6:2] == 5))
        myregister <= user_wr_data;
    

    用于将“myregister”映射到地址偏移量 0x14 处。

  • 同样,依赖于 user_addr 的 case 语句是实现 user_rd_data 值赋值的常见方式,例如:

    always @(posedge user_clk)
      case (user_addr[6:2])
        5: user_rd_data <= myregister;
        6: user_rd_data <= hisregister;
        7: user_rd_data <= herregister;
        default: user_rd_data <= 0;
      endcase
    
  • user_addr 是 32 位宽,保存正在访问的完整物理地址。由于仅当地址在分配范围内时使能信号才为高,因此无需验证地址的高位 (MSB)。

  • 始终忽略 user_addr[1:0]。在 32 位对齐的总线访问中,这两个最低有效位始终为零,并且如下所述,即使对于非对齐访问, 也应忽略它们。

2.3.4 非对齐寄存器访问

当主机可能以 32 位非对齐方式访问寄存器空间时,需要在逻辑中分别处理每个字节。

请注意,在总线上访问一个字节和访问一个 32 位字所花费的时间相同,因此非对齐访问的带宽效率只有四分之一。

假设 litearray3、litearray2、litearray1 和 litearray0 是包含 32 个元素(每个元素 8 位)的内存数组。以下代码片段演示了如 何重新编写 2.3.3 中的示例以支持非对齐访问。 在 Verilog 中:

always @(posedge user_clk)
  begin
    if (user_wstrb[0])
      litearray0[user_addr[6:2]] <= user_wr_data[7:0];

    if (user_wstrb[1])
      litearray1[user_addr[6:2]] <= user_wr_data[15:8];

    if (user_wstrb[2])
      litearray2[user_addr[6:2]] <= user_wr_data[23:16];

    if (user_wstrb[3])
      litearray3[user_addr[6:2]] <= user_wr_data[31:24];

    if (user_rden)
       user_rd_data <= { litearray3[user_addr[6:2]],
                         litearray2[user_addr[6:2]],
                         litearray1[user_addr[6:2]],
                         litearray0[user_addr[6:2]] };
  end

或者在 VHDL 中:

lite_addr <= conv_integer(user_addr(6 DOWNTO 2));

process (user_clk)
begin
  if (user_clk'event and user_clk = '1') then
    if (user_wstrb(0) = '1') then
      litearray0(lite_addr) <= user_wr_data(7 DOWNTO 0);
    end if;

    if (user_wstrb(1) = '1') then
      litearray1(lite_addr) <= user_wr_data(15 DOWNTO 8);
    end if;

    if (user_wstrb(2) = '1') then
      litearray2(lite_addr) <= user_wr_data(23 DOWNTO 16);
    end if;

    if (user_wstrb(3) = '1') then
      litearray3(lite_addr) <= user_wr_data(31 DOWNTO 24);
    end if;

    if (user_rden = '1') then
       user_rd_data <= litearray3(lite_addr) & litearray2(lite_addr) &
                       litearray1(lite_addr) & litearray0(lite_addr);
    end if;
  end if;
end process;

以下是基地址偏移 0x01 处单个字节的非对齐写入周期的波形。


波形 3:非对齐访问的写入周期(显示字节偏移量 0x01)

注意事项:

  • 在分配的地址区域上的写入总线操作必然导致 user_wren 以及 user_wstrb 中至少一个位同时高电平一个时钟周期。如上所 示,如果值赋值依赖于 user_wstrb,则无需检查 user_wren。

  • 逻辑对非对齐读取访问的处理与对齐读取相同。例如,当处理器上运行的程序读取一个字节时,总线上读取的是整个 32 位 字,处理器从中选取所需的部分。

  • 当处理器所需的地址是非对齐时,user_addr[1:0] 可能非零。这没有意义,因为在写入周期中逻辑的正确行为仅取决于 user_wstrb。因此,即使对于非对齐访问,最好也忽略这两个位。

2.4 中断

Xillybus Lite IP 核暴露了一个输入信号 user_irq,它允许应用逻辑向处理器发送硬件中断。该信号被视为同步正边沿触发的中断请求信 号,即当该信号从一个时钟周期到下一个时钟周期从低电平变为高电平时,会产生一个中断。

在 xillydemo.v(hd) 模块中,此信号被保持为零。

Xillybus Lite 采用 UIO 处理中断的方法:用户空间程序在尝试从设备文件读取数据时休眠。当中断到达时,读取四个字节的数据,从而唤 醒进程。这四个字节应视为一个 unsigned int,其值为自驱动程序加载以来已触发的中断总数。程序可以忽略此值,也可以使用它来检查是否 错过了中断,方法是验证该值是否比先前读取的值大 1。

请注意,在正常系统运行期间,此中断计数器永远不会归零。

例如,假设 “fd” 是 /dev/uio0 的文件句柄:

unsigned int interrupt_count;
int rc;

while (1) {
  rc = read(fd, &interrupt_count, sizeof(interrupt_count));

  if ((rc < 0) && (errno == EINTR))
    continue;

  if (rc < 0) {
    perror("read");
    exit(1);
  }

  printf("Received interrupt, count is %d\n", interrupt_count);
}

请注意,read() 函数调用必须要求 4 个字节。任何其他长度参数都将返回错误。 中断文件描述符可用于 select() 函数调用。

另请注意,检查 EINTR 的部分用于正确处理软件中断(例如进程被停止并重新启动),与硬件中断无关。