MCDMA, or Multi-Channel Direct Memory Addressing, enables the transfer of two (or more) data streams from one resource to another. If you're new to DMA and find it a bit challenging, I recommend starting with this post, it will give you some pointers to learn about DMA and help in getting to know how a Linux Kernel Module (LKM) works.
To use MCDMA on Linux effectively, following "best practices" involves loading a dedicated Linux Kernel Module (LKM) into the kernel. Detailed instructions for creating and using such an LKM can be found here, but I noticed some changes were needed. In this post, I’ll walk you through how I successfully implemented MCDMA on the Kria KR260, utilizing a modified version of the user-space program mentioned on that site to interface with the LKM. I also made minor adjustments to the hardware design to get everything up and running.
The primary goal of this setup is to send two audio streams (in WAV format) to the Programmable Logic (PL) for processing (low pass/high pass filters, reverb, gain), and then output them to speakers via I2S.
To achieve this, I’ll use Vivado 2024.1 to design and export the hardware, and XSCT to generate the device tree. The device tree requires slight modifications, which I’ll also cover.
Hardware designI wanted the design to receive 2 AXI streams via MCDMA from the PS. Then those streams need to go to the I2S output. I reused the AXIS to I2S transmitter which I also used in the previous project. When creating the hardware design as stated in the Linux MCDMA resource (see introduction), I noticed the AXI4-Stream Switch did not do what it is supposed to do. The TDEST field, which is set by the MCDMA and should be used to split the stream to two destinations, was not being used. I ended up changing the AXI4-Stream Switch IP to an AXI4-Stream Interconnect which does use the destination field correctly. This way the two dma channels get sent via one dma stream and are being separated in the PL as I wanted.
Let's start, create a new project, select the KR260 starter kit and set the custom slots, make it an extensible Vitis platform. You can set both settings afterwards if needed.
Create a block design with the following IPs (refer to the previous post to find Whitney Knitter's article that describes which settings you need to set in the Zynq Ultrascale+ IP):
- 2x Processor System Reset, you need one for each clock domain, we have a clock domain for I2S and one for the rest of the system.
- 2x AXI Memory Mapped to Stream Mapper, these will control the configuration and reloading of the FIR compiler.
- AXI Smartconnect
- 2x Constants to config coherency of the zynq
- Concat to combine interrupts
- Zynq Ultrascale+
- AXI Interconnect
- AXI BRAM Controller used to control our audioplayer from Linux
- Block Memory Generator with memory type: True Dual Port RAM
- AXI Multi Channel Direct Memory Access
- AXI4-Stream Interconnect
- 2x AXI4-Stream Subset Converter
- FIR Compiler
- Slice
- 2x AXIS_I2S
I used two clock domains, one for the I2S @12.288MHz and one for the rest @100MHz:
Set the properties of the AXI MCDMA:
Set the properties of the AXI4-Stream-Interconnect
Set the properties of the AXI4-Stream Subset Converter:
Build the bitstream and export the Platform (include the bitstream). Save the XSA as kr260_filter.xsa in the KR260_base_dma folder, so one above the project folder.
Device treeNext we use xsct
to create the devicetree.
This is my folder structure (on my laptop):
yuri@desktop:~/KR260_base_dma$ tree
.
├── bd
│ ├── kria_bd
├── dma_file_transfer
├── extracted
│ ├── kria_bd_wrapper
├── KR260_base
│ ├── KR260_base.cache
│ ├── KR260_base.gen
│ ├── KR260_base.hw
│ ├── KR260_base.ip_user_files
│ ├── KR260_base.runs
│ ├── KR260_base.sim
│ └── KR260_base.srcs
├── kr260_custom_platform
│ ├── dtg_output_filter
├── src
├── tb
└── xdc
In a terminal run these commands to start creating the devicetree
$ cp KR260_base/KR260_base.runs/impl_1/kria_bd_wrapper.bin dma_file_transfer/kr260_filter.bit.bin
$ cd kr260_custom_platform/
$ rm -rf dtg_output_filter/
$ source /tools/Xilinx/Vitis/2024.1/settings64.sh
$ xsct
In xsct
run these 3 commands:
xsct% hsi::open_hw_design ../kr260_filter.xsa
xsct% createdts -hw ../kr260_filter.xsa -zocl -platform-name kr260_filter -git-branch xlnx_rel_v2024.1 -overlay -compile -out ./dtg_output_filter/
xsct% hsi::open_hw_design ../kr260_filter.xsa
xsct% exit
Next cd
into the folder where the source pl.dtsi is located and change it:
$ cd dtg_output_filter/dtg_output_filter/kr260_filter/psu_cortexa53_0/device_tree_domain/bsp/
$ micro pl.dtsi
Add the following snippet right after the mcdma block:
dma_proxy {
compatible ="xlnx,dma_proxy";
dmas = <&axi_mcdma_0 0 &axi_mcdma_0 1>;
dma-names = "dma_proxy_tx_0", "dma_proxy_tx_1";
};
This makes sure the dma_proxy LKM will recognize the 2 dma channels to transmit data.
Next compile the devicetree using dtc
, you may have to use Linux (in a virtualbox) to do this (no idea if dtc works on Windows), share your project folder with the virtualbox in such a case :
$ dtc -I dts -O dtb -o pl.dtbo pl.dtsi
$ cp pl.dtbo /data/vakken/2425/S1/RND/git/KR260_base_dma/dma_file_transfer/kr260_filter.dtbo
$ cd ../../../../../../..
Create shell.json :
$ micro dma_file_transfer/shell.json
Copy/paste this content:
{
"shell_type" : "XRT_FLAT",
"num_slots" : "1"
}
Copy files to Kria KR260Now copy those 3 files to the Kria into a folder for the hardware design. These have to be located in a subfolder in /lib/firmware/xilinx/
So login to the Kria via ssh (you might need to copy your public key in .ssh/authorized_keys
first and start ssh using sudo systemctl start ssh
):
$ sudo mkdir /lib/firmware/xilinx/filter
$ sudo scp yuri@10.42.0.1:/KR260_base_dma/dma_transfer_files/* /lib/firmware/xilinx/filter/
And now load the hardware design:
$ sudo xmutil listapps
$ sudo xmutil unloadapp
$ sudo xmutil loadapp filter
Kernel codeClone the git repository and copy the dma-proxy.c to dma_audio_driver.c (in case you would like to change it, this way we also have a nice name for our module):
$ mkdir ~/Programming
$ cd ~/Programming
$ git clone https://github.com/Xilinx-Wiki-Projects/software-prototypes.git
$ cd software-prototypes/linux-user-space-dma/Software/Kernel
$ cp dma-proxy.c dma_audio_driver.c
Create a Makefile :
$ micro Makefile
Copy/Paste this content into the Makefile :
INC = /home/ubuntu/Programming/software-prototypes/linux-user-space-dma/Software/Common/
EXTRA_CFLAGS += -I$(INC)
obj-m += dma_audio_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Check that when you copy/paste, the spaces before the make command is a <tab> and not <space>'s. Otherwise you get an error.
Compile the kernel code :
$ make
$ ls -al
You should see a dma_audio_driver.ko file now. Load the driver:
$ sudo insmod dma_audio_driver.ko
$ lsmod | grep dma
Userspace codeThe code needed some small changes to be able to stream to wav files.
In main() the arguments are being checked and then the list of wav-files in the given folders is populated.
Two threads are being started, one for each channel. We read each wav-file in chunks of 2048 bytes, put those into memory where the dma controller can find them and we start the dma controller.
I also wanted our application to listen to control signals to be able to Start/Stop/Pause or switch to Next/Previous songs (not all of these have been implemented in the code attached below, but you'll be able to add the code). So I added an AXI BRAM controlled 'audiocontroller', in the loop of the TxThread you will see some code that checks /dev/mem
this reads BRAM at 0x90050000 to see if some bits are set, if so it executes a command (next chan0/next chan1/stop) and clears that bit.
Create a file audioplayer.c and copy the contents of the file at the bottom of this article into it.
TestingMake sure the correct hardware design and the kernel module are loaded:
$ sudo xmutil listapps
$ lsmod | grep dma_proxy
Time to collect some sample wav-files. Create a wav folder with two folders in your home directory and copy files in them.
$ mkdir -p ~/wav/chan0
$ mkdir -p ~/wav/chan1
You will have to find some wav-files yourself.
Build and run the application, it requires 4 parameters, the two folders with files for each channel and two numbers which are indices of which number to start playing:
$ gcc audioplayer.c -o audioplayer -I ../Common/
$ sudo ./audioplayer "~/wav/chan0" "~/wav/chan1" 3 8
Write to BRAM to control the audioplayer, following controls are currently implemented:
- "1": next track on channel0
- "2": next track on channel1
- "4": stop/exit
$ sudo devmem2 0x90050000 w 2
This could be another program or even the PL controlling the player.
Finishing upEach time you update your hardware design and want to renew it on the Kria I suggest to :
xmutil unloadapp
your previous hardware designrmmod
dma_audio_driver the kernel driver- copy the files via
scp
xmutil loadapp
your new hardware designinsmod
dma_audio_driver.ko the kernel again- start your application
That's it, it is not hard at all! Small note on the /dev/mem
being used in the audioplayer code, this is not best practice as it requires us to start the audioplayer with sudo
. We should use UIO for this... next time! Thanks for reading the story.
Comments
Please log in or sign up to comment.