How to Design and Access a Memory-Mapped Device in Programmable Logic from Linaro Ubuntu Linux on Xilinx Zynq on the ZedBoard, Without Writing a Device Driver – Part One


Last time we discussed how to run desktop Linaro Ubuntu Linux on the ZedBoard. In this post, and part two that follows, we’ll cover two different ways for application software to access a memory-mapped device implemented in Zynq’s programmable logic fabric.

Memory-mapped device access is straightforward in a “standalone” “bare-metal” application. You initialize a (volatile) pointer with the physical address of the memory-mapped device control/status register(s) and simply load and store to your device registers through that pointer.

In contrast, under Linux, user-mode processes run in a virtual memory environment in which all addresses are virtual addresses, mapped to physical addresses by the processor’s memory management unit. The Linux kernel owns the mapping of virtual addresses to physical addresses, and by default it provides no access (no valid mapping) to your device registers.

So to access your device under Linux, you must open it its device file and then establish a valid virtual memory mapping from some address range in your process to the underlying physical address range of your device registers. Optionally, if your device generates interrupts, your software must await the next interrupt and handle it as it occurs.

In this tutorial series we’ll discuss two ways to do this, neither of which require your writing or debugging Linux device drivers.

  1. Open /dev/mem (as root) and mmap a valid virtual address view onto your memory-mapped device registers.
  2. Configure a user-mode I/O device driver for your device, open /dev/your-device, and again mmap a valid virtual address view onto your memory-mapped device registers.

Again with the disclaimers: I am sharing what works for me. It may not work for you or it may fail over time. You may suffer data loss or worse. I disclaim all warranties and representations. I am not supporting this. I am not a Linux kernel hacker. This is not necessarily the best way, nor does it comprise best practices; for example a proper engineering methodology would include extensive bus functional simulation of our peripheral core and so forth. Your mileage may vary.

Key URLs

Cut to the chase: UIO: key lessons learned

Please review Andersson’s page. Here is what I have subsequently puzzled out to enable UIO on my ZedBoard (with device tree configuration and the ADI / Xilinx Linux git tree).

  • Of the various UIO drivers in linux/drivers/uio/*.c in the Xilinx Linux git tree, only uio_pdrv_genirq can work out of the box with device tree configuration.
  • It is possible, if inelegant, to use that driver with a device that does not issue interrupts.
  • Device tree interrupt assignments are a little wonky.
  • It is also possible to use the non-interrupt version uio_pdrv if you bring forward some code from uio_pdrv_genirq.c, and add a “compatible” key for it to probe (initialize) from device tree configuration.
  • The UIO drivers are not built by default. You have to explicitly configure the Linux kernel build to build them.

The details are in the part two of this series.

Building a new system with a loadable up-counters device

So to demonstrate all of this we’ll build a memory-mapped up-counters device in the PL fabric and access it from Linux. First we’ll build the hardware. Start with the prerequisites, system, and tools we used earlier.

  • Run Xilinx Platform Studio. Select Create New Project Using BSB Wizard. Project file “zed_counters”. AXI system. OK.
  • In Base System Builder — AXI flow — Create a System for the Following Development Board, Select Board Vendor Avnet. Select Board Name ZedBoard. Next.
  • In Base System Builder — AXI flow — Peripheral Configuration, Remove the preselected buttons, LEDs, and switches. Remove. Remove. Remove. Finish.
  • In XPS, select the Bus Interfaces tab. All you have is the processing_system7_0. What a mouthful. Select its name, rename it “ps”. That’s better.

Now let’s create a loadable up-counters peripheral.

  • In the IP Catalog pane to the left, click on the rightmost button of eight called “Create and Import Peripheral” to invoke the Create and Import Peripheral Wizard. Next.
  • In Create and Import Peripheral Wizard — Peripheral Flow, choose default radio button Create templates for a new peripheral. Next.
  • In Create Peripheral — Repository or Project, I recommend you use or start a repository for your core(s) that you can share across projects. Click the first button and chose a new repository location. Next.
  • In Create Peripheral — Name and Version, chose a name such as “counters”. Next.
  • In Create Peripheral — Bus Interface, chose AXI4-Lite. Next.
  • In Create Peripheral — IPIF (IP Interface) Services, select only User logic software register. Next.
  • In Create Peripheral — User S/W Register, specify 2 software accessible registers. Next.
  • In Create Peripheral — IP Interconnect (IPIC), leave all defaults. Next.
  • In Create Peripheral — (OPTIONAL) Peripheral Simulation Support, leave Generate unselected. Next.
  • In Create Peripheral — (OPTIONAL) Peripheral Implementation Support, select Generate stub ‘user_logic’ in Verilog. Next.
  • In Create Peripheral — Congratulations!, Finish.
  • You now have a peripheral named counters which is a pair of simple read-write 32-bit registers.
  • Let’s modify that to make it more interesting. We’ll make the first register, at offset 0, automatically up-count every time it is read; and we’ll make the second register, at offset 4, a free-running counter, which up-counts every bus clock cycle, which is (by default) 100 MHz.
  • Edit Repository/MyProcessorIPLib/pcores/counters_v1_00_a/hdl/verilog/user_logic.v and change these two lines of code:
*** user_logic.v.0
--- user_logic.v
*** 163,170 ****
                if ( Bus2IP_BE[byte_index] == 1 )
                  slv_reg1[(byte_index*8) +: 8] <= Bus2IP_Data[(byte_index*8) +: 8];
            default : begin
!             slv_reg0 <= slv_reg0;
!             slv_reg1 <= slv_reg1;

--- 163,170 ----
                if ( Bus2IP_BE[byte_index] == 1 )
                  slv_reg1[(byte_index*8) +: 8] <= Bus2IP_Data[(byte_index*8) +: 8];
            default : begin
!             slv_reg0 <= slv_reg0 + (slv_reg_read_sel == 2'b10); // incr. each read
!             slv_reg1 <= slv_reg1 + 1;                           // incr. each cycle

Now let’s add these counters to our zed_counters project.

  • In the IP Catalog pane, under Peripheral Project Repository, click (expand) USER, and double-click COUNTERS.
  • The Add IP Instance to Design modal dialog appears. Select Yes.
  • In XPS Core Config, leave all defaults as-is and close the dialog.
  • The Instantiate and Connect IP modal dialog appears. Your “ps” processing system is preselected. OK.
  • In the Bus Interfaces pane you now have an axi_interconnect_1, a ps processing system, and a counters_0.
  • Select the Ports tab. Expand the design hierarchy. Observe all is wired up correctly. For example, the counters_0.S_AXI is connect to BUS axi_interconnect_1, and the counters_0.S_AXI.S_AXI_ACLK is connect to the ps::FCLK_CLK0. Great.
  • Select the Addresses tab. Change the counters_0 Base Address to 0x70000000 and the Size to 4K. Lock it. (Why? Just to keep all subsequent instructions in sync.)
  • We’re almost done. Let’s build it and ship it. Click Project >> Export Hardware Design to SDK.
  • The Export to SDK modal dialog appears. Ensure default “Include bistream and BMM file” is selected. Select Export & Launch SDK.
  • The XPS build of the design starts.
  • Celebrate the long cascade of chatty warnings. With Platform Studio, Xilinx continues its grand tradition of angst-inducing warnings which challenge the engineer to determine which are expected and benign, which are serious, and which are new since last build.
  • If all is well, the build finishes. Spend a moment to review the output. Notice our pair of 32-bit counters incurred >190 DFFs and >300 LUTs, much of which (I assume) is in AXI4 and IPIF interconnect overheads.

Aside: “standalone” application testing

Before we turn to building the software infrastructure needed to boot and access our counters from software under Linux, I should point out that you may wish to take a more deliberate crawl, walk, run approach to brining up your new PL hardware. In my case, back in October 2012, the first thing I did was to try to access my memory mapped hardware design by building a “bare metal” standalone C test application in the SDK. It used (physically addressed) pointer dereferences, and printf, to read and write to my device register and verify in this simple environment that all was working as intended.

Building the First Stage Boot Loader (FSBL) and BOOT.BIN

We’ll use the same process and tools as last time.

  • Once the XPS build finishes, it launches the Xilinx SDK. Its Workspace Launcher modal dialog appears. Browse to your zed_counters project and select its SDK directory. OK.
  • In Project Explorer tab, select zed_counters_hw_platform.
  • Double click system.xml. Review the zed_counters_hw_platform Address Map. There’s your counters_0, at address range 0x70000000-0x70000fff, as desired!
  • Select zed_counters_hw_platform. Run File >> New >> Application Project.
  • In New Project — Application Project, enter Project Name zed_fsbl. Keep defaults (e.g. standalone, C). Next.
  • In New Project — Templates, select Zynq FSBL. Finish. It builds automatically.
  • In Project Explorer tab, select zed_fsbl. Run Xilinx Tools >> Create Zynq Boot Image.
  • In Create Zynq Boot Image, the list of partitions in the boot image will already contain your zed_fsbl.elf and your system.bit PL configuration bitstream. Now you must add your u-boot.elf that you built last time. Click Add.
  • In Select a partition image file, enter the path to your u-boot.elf file. Open. It will be added as the third partition file in the BOOT.BIN image.
  • In Create Zynq Boot Image, note the output folder path, then click Create Image. Bootgen runs and builds you a BOOT.BIN, unfortunately called u-boot.bin.

Recap and to be continued…

So far we’ve built a new ZedBoard project from scratch. It has a pair-of-32-bit-counters peripheral in the programmable logic. Thanks to the XPS Base System Builder Wizard, its processing system is preconfigured with support for UART, GPIO, SD card, Quad SPI, USB, Ethernet, and 512 MB of DRAM. We’ve built an FSBL that sets this all up, loads the PL, and loads and launches u-boot; and we’re going to reuse the same good old u-boot.elf Linux boot loader from last time.

The tutorial continues in part two. Thank you for reading along. I hope it is of help in your Zynq Linux work. Please leave me your comments here or via twitter @jangray. Good luck and happy hacking.

One thought on “How to Design and Access a Memory-Mapped Device in Programmable Logic from Linaro Ubuntu Linux on Xilinx Zynq on the ZedBoard, Without Writing a Device Driver – Part One

  1. Marc D

    In the verilog code for the AXI lite, where the registers get updated (and where you change to code to make them increment), does the default case ever get reached? I can’t see the full code in your example, but in the XAPP1168 example, it seems like all cases are covered, and the default cases only assigns the registers to themselves (why is that anyway? does it server a purpose?) Thanks

Leave a Reply