從一開始的 Xilinx SoC 開發,PetaLinux 使用(三)
前面的系列中,我們設定好 PetaLinux 並編譯出一個能用來在 SD 卡上開機的檔案系統了。這篇文章中,我們將會寫出一份 C code 讓 PL side 的 DMA 動起來,在 PL/PS side 的 DDR 之間搬運資料。
SoC 設定回顧
回顧一下我們的兩個 interconnect 的 slaves 的 address 設定是這樣:
- Interconnect 1
- Master
- PS CPU 控制界面
- Slave
- DMA 控制界面:從 2GB 位置 (
0x8000_0000
) 開始算起 64KB
- DMA 控制界面:從 2GB 位置 (
- Master
- Interconnect 2
- Master
- DMA 資料界面
- Slave
- PS DRAM 資料:從 0GB 位置 (
0x0
) 開始算起 2GB - PL DRAM 資料:從 4GB 位置 (
0x1_0000_0000
) 開始算起 4GB
- PS DRAM 資料:從 0GB 位置 (
- Master
在這個 SoC 中,這篇文章將會說明如何從頭到尾進行以下操作:
- 用 CPU 在 PS DRAM 寫入一些資料,方便起見叫 PSMem1。
- 用 CPU 操作 DMA 控制界面,叫他把資料從 PS DRAM 搬到 PL DRAM (PSMem1 → PLMem1)。
- 用 CPU 操作 DMA 控制界面,叫他把資料從 PL DRAM 互相搬移 (PLMem1 → PLMem2)。
- 用 CPU 操作 DMA 控制界面,叫他把資料從 PL DRAM 搬回 PS DRAM (PLMem2 → PSMem2)。
- 用 CPU 在驗證 PS DRAM 的資料是否正確 (PSMem2)。
在 PS DRAM 中寫入資料
由於作業系統的緣故,在 PS DRAM 中寫入資料不是簡單的事情。例如說我們要把資料放在 DDR 中第 1.5GB 的位置好了,我們不能直接寫:
1unsigned *ptr = 0x60000000;
2ptr[0] = 0;
因為作業系統不可能讓你直接存取實際的記憶體位置,因此就算這個程式是用 root 來執行都無法達到我們要的效果。
幸好,Linux 提供一個簡便的方式可以讓我們繞過,也就是透過 /dev/mem
來存取 PS DRAM,甚至是 DMA 控制界面,透過 open("/dev/mem")
加上 mmap
的組合,我們還是可以拿到一個 pointer 來使用,雖然麻煩了許多。
1int fd = open("/dev/mem", O_RDWR | O_SYNC);
2size_t desired_bytes = 1<<20; // 1MB
3unsigned *volatile ptr = (unsigned*) mmap(
4 0,
5 desired_bytes,
6 PROT_READ | PROT_WRITE,
7 MAP_SHARED,
8 fd,
9 0x60000000 // 1.5GB
10);
11ptr[0] = 0;
然而現在還有第二個問題,由於整個作業系統都是 Linux 管理的,就算我們寫入了想要的位置,說不定 Linux 已經把這塊記憶體分配給其他程式使用了。因此我們還需要保證 Linux 不會把這塊記憶體分配下去,這時候就是前面一篇文章中 project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
派上用場的時候了。
在這個檔案裡面,本來有一個這樣的片段。
1/include/ "system-conf.dtsi"
2/ {
3 xlnk {
4 compatible = "xlnx,xlnk-1.0";
5 };
6};
加上這樣的一段描述,就可以讓 Linux 知道從 1.5GB (0x60000000
) 開始的 64MB (0x04000000
),這塊記憶體是不能使用的。注意有時候這個位置會跟其他已經保留的位置衝突,有時候需要調整一下才不會衝突,但是我目前也不確定要怎麼樣才知道有沒有衝突……
1/include/ "system-conf.dtsi"
2/ {
3 xlnk {
4 compatible = "xlnx,xlnk-1.0";
5 };
6 reserved-memory {
7 #address-cells = <1>;
8 #size-cells = <1>;
9 ranges;
10 reserved: buffer@0x60000000 {
11 no-map;
12 reg = <0x60000000 0x04000000>;
13 };
14 };
15};
接著再編譯一次 PetaLinux,把 BOOT.BIN
、image.ub
、boos.scr
重新複製到 SD 卡的第一個磁區。
1petalinux-build
2cd images/linux/
3petalinux-package --boot --fsbl zynqmp_fsbl.elf --fpga system.bit --pmufw pmufw.elf --atf bl31.elf --u-boot u-boot.elf --force
操作 DMA
要操作位於 0x80000000
的 DMA,我們也只要對 DMA 的位置 mmap
出一塊空間。
1int fd = open("/dev/mem", O_RDWR | O_SYNC);
2size_t desired_bytes = 64<<10; // 64KB
3unsigned *volatile ptr = (unsigned*) mmap(
4 0,
5 desired_bytes,
6 PROT_READ | PROT_WRITE,
7 MAP_SHARED,
8 fd,
9 0x80000000
10);
11// Then do something to DMA by manipulating ptr
然而問題來了,我們要怎麼對這個 pointer 操作,才能叫 CDMA 做事呢?我們在 官方論壇 上找到了答案,我把他稍微修改了一下,包裝成一個函數。詳細內容就不要深究了,總之就是照著標準對 CDMA 的 register 進行操作。
1#define XAXICDMA_CR_OFFSET 0x00000000 /* Control register */
2#define XAXICDMA_SR_OFFSET 0x00000004 /* Status register */
3#define XAXICDMA_SRCADDR_OFFSET 0x00000018 /* Source address register */
4#define XAXICDMA_DSTADDR_OFFSET 0x00000020 /* Destination address register */
5#define XAXICDMA_BTT_OFFSET 0x00000028 /* Bytes to transfer */
6
7unsigned int dma_set(unsigned* dma_virtual_address, int offset, unsigned int value)
8{
9 dma_virtual_address[offset>>2] = value;
10}
11
12unsigned int dma_get(unsigned* dma_virtual_address, int offset)
13{
14 return dma_virtual_address[offset>>2];
15}
16
17void dma_memcpy(unsigned* volatile vadd_cdma, off_t src, off_t dst, size_t nbyte)
18{
19 unsigned reg = dma_get(vadd_cdma, XAXICDMA_CR_OFFSET);
20 unsigned status = dma_get(vadd_cdma, XAXICDMA_SR_OFFSET);
21 dma_set(vadd_cdma, XAXICDMA_CR_OFFSET, XAXICDMA_XR_IRQ_SIMPLE_ALL_MASK);
22 reg = dma_get(vadd_cdma, XAXICDMA_CR_OFFSET);
23 status = dma_get(vadd_cdma, XAXICDMA_SR_OFFSET);
24 dma_set(vadd_cdma, XAXICDMA_SRCADDR_OFFSET, src);
25 reg = dma_get(vadd_cdma, XAXICDMA_SRCADDR_OFFSET);
26 status = dma_get(vadd_cdma, XAXICDMA_SR_OFFSET);
27 dma_set(vadd_cdma, XAXICDMA_DSTADDR_OFFSET, dst);
28 reg = dma_get(vadd_cdma, XAXICDMA_DSTADDR_OFFSET);
29 status = dma_get(vadd_cdma, XAXICDMA_SR_OFFSET);
30 dma_set(vadd_cdma, XAXICDMA_BTT_OFFSET, nbyte);
31 while(1) {
32 status = dma_get(vadd_cdma, XAXICDMA_SR_OFFSET);
33 if(status & (1 << 12)) {
34 printf("DMA transfer is completed \n");
35 break;
36 }
37 }
38 reg = dma_get(vadd_cdma, XAXICDMA_BTT_OFFSET);
39 status = dma_get(vadd_cdma, XAXICDMA_SR_OFFSET);
40}
接著,我們用這個 function 去建構我們的測試,先把 PSMem1 初始化,依序搬到 PLMem1、PLMem2、PSMem2,最後檢查 PSMem2 的內容是不是跟 PSMem1 一樣:
1off_t PS = 0x060000000ll;
2off_t PL = 0x100000000ll;
3size_t SIZE = 1<<10;
4// Initialize PSMem1 using mmap'ed pointer
5for (int i = 0; i < SIZE/sizeof(unsigned); ++i) {
6 ptr_ps[i] = i;
7}
8// Copy from PSMem1 -> PLMem1 -> PLMem2 -> PSMem2
9dma_memcpy(ptr_cdma, PS , PL , SIZE);
10dma_memcpy(ptr_cdma, PL , PL+SIZE, SIZE);
11dma_memcpy(ptr_cdma, PL+SIZE, PS+SIZE, SIZE);
12// Check PSMem2 using mmap'ed pointer
13for (int i = 0; i < SIZE/sizeof(unsigned); ++i) {
14 assert(ptr_ps[i+SIZE] == i);
15}
如果之前 rootfs 上面有安裝 gcc 的話,完成這個程式之後,把他放到 SD 卡上面的隨便一個位置上面開機,接著跟一般的 Linux 一樣 gcc 編譯就好了。
1gcc main.c -o dma_test
2./dma_test
如果 assertion 有通過的話,這樣應該就是有完成測試了。
結論
這篇文章用了最簡單粗暴的方式,把 /dev/mem
打開,然而其實一般來說不應該使用這麼 hacky 的界面,而應該用 Linux kernel 裡面現有的 driver 去達成。下一篇文章中,我將會說明如何寫一個 Linux kernel driver,取代掉 dma_memcpy
這麼 hacky 的作法。
系列文連結
-
從一開始的 Xilinx SoC 開發,PetaLinux 使用(一)
-
從一開始的 Xilinx SoC 開發,PetaLinux 使用(二)
-
從一開始的 Xilinx SoC 開發,PetaLinux 使用(三)
- 從一開始的 Xilinx SoC 開發,PetaLinux 使用(一)
- 從一開始的 Xilinx SoC 開發,PetaLinux 使用(二)
- 從一開始的 Xilinx SoC 開發,PetaLinux 使用(三)