ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Zynq裸机开发中DDR内存分配与Cache一致性实战指南

Zynq裸机开发中DDR内存分配与Cache一致性实战指南 1. 项目概述为什么DDR内存分配是Zynq裸机开发的“生死线”Zynq这个芯片我从2013年Vivado 2013.2版本就开始用到现在手头还压着三块Zynq-7020和两块Zynq UltraScale MPSoC的板子在做工业相机固件迭代。很多人一上来就猛敲SDK里的hello world跑通UART就以为PS端开发入门了——其实那只是站在悬崖边往里迈了半步。真正决定你后续能不能跑起图像处理算法、能不能稳定收发千兆以太网数据、甚至能不能让FreeRTOS调度不崩的底层命门就是DDR内存的分配与使用。不是夸张我亲眼见过三个项目因为DDR配置错一个参数在量产前两周突然出现DMA传输丢帧、中断响应延迟跳变、甚至FSBL校验失败重启的诡异问题最后全栽在DDR地址映射和缓存属性设置上。所谓“PS端裸机开发”本质是绕过Linux内核直接用ARM Cortex-A9Zynq-7000或Cortex-A53UltraScale的指令去操作硬件寄存器。这时候没有MMU帮你做虚拟地址转换没有内核内存管理器帮你切页、分配、回收所有内存空间都得你自己画地为牢、立碑为界。DDR不是一块大蛋糕随便切它是一条高速铁路轨道你得亲手铺枕木、定轨距、设信号灯——哪段给FSBL用哪段留给应用程序代码段哪段划作堆heap供malloc动态申请哪段专供DMA引擎做缓冲区哪段必须禁用cache避免coherency冲突……每一块区域的起始地址、长度、访问权限、cache策略都得在链接脚本.ld文件里白纸黑字写死在启动代码startup.s或ps7_init.c里逐字节初始化在应用层调用时严格遵守边界。稍有越界轻则数据错乱重则总线锁死连JTAG都救不回来。关键词里反复出现的“Zynq”“SDK”“裸机开发”“PS”“DDR”其实串起了一条清晰的技术链路Zynq是Xilinx的异构SoCPSProcessing System是其ARM处理器子系统SDKSoftware Development Kit是Xilinx早期配套的集成开发环境现已被Vitis取代但老项目仍大量沿用而DDR则是PS唯一能大规模、高带宽访问的外部主存。你不搞懂DDR怎么分、怎么管、怎么用SDK里写的任何一行C代码都像在没图纸的核电站里拧阀门——表面看转得动心里全是悬的。这篇文章就是把我过去十年踩过的坑、调过的波形、抓过的trace浓缩成一套可直接抄作业的DDR内存分配实操指南。适合正在用Zynq-7000系列做工业控制、嵌入式视觉、通信协议栈的工程师也适合刚从STM32转过来、对ARM Cache和AXI总线还发懵的新手。核心就一条把DDR当成你PS端开发的“操作系统”而不是一块被动等待读写的存储器。2. DDR内存空间的整体设计逻辑与关键约束2.1 Zynq PS端DDR地址空间的物理拓扑结构Zynq的PS端DDR控制器DDR PHY Controller并非简单地把DDR颗粒挂到ARM总线上。它通过AXI Interconnect总线矩阵将DDR作为整个PS子系统的共享资源池同时服务于ARM CPU、DMA引擎如AXI DMA、SDIO、GEM、GPU仅UltraScale、以及PL端通过AXI HP/ACP端口发起的访问。因此DDR的地址空间不是线性的“0x00000000开始的一整块”而是被划分为多个逻辑区域每个区域对应不同的访问主体和安全策略。理解这个拓扑是设计内存分配方案的第一步。Zynq-7000系列以Zynq-7020为例的DDR物理地址空间默认从0x00100000开始到0x3FFFFFFF结束共1GB实际可用容量取决于焊接的DDR颗粒规格常见512MB或1GB。但这个1GB空间绝非全部开放给你自由挥霍。它被硬性分割为以下几大区块FSBL保留区0x00100000 - 0x001FFFFF64KB。这是First Stage Boot LoaderFSBL的专属领地。FSBL在启动时会把自己从QSPI Flash拷贝到这里执行完成PS初始化包括DDR控制器配置、时钟树设置、MIO引脚复位等然后跳转到SSBLSecond Stage Boot Loader或应用程序入口。你绝对不能在这里放自己的代码或数据否则FSBL运行时会覆盖你的内容或者你的代码会破坏FSBL的校验和导致启动失败。BootROM OCM映射区0x00000000 - 0x000FFFFF1MB。这部分地址空间映射的是片上存储器On-Chip Memory, OCM而非DDR。OCM是Zynq内部的高速SRAM256KB访问延迟极低常用于存放中断向量表、关键中断服务程序ISR、或需要极致实时响应的小数据结构。虽然地址在DDR空间下方但它物理上与DDR无关强行往这里写DDR数据是无效的。新手常误以为0x00000000是DDR起点结果调试时发现数据根本没写进DDR根源就在这里。应用程序代码与数据区0x00200000起这是你最常打交道的区域。SDK/Vitis默认将应用程序的.text代码段、.rodata只读数据、.data已初始化全局变量、.bss未初始化全局变量全部链接到此区域。但具体起始地址和大小完全由你控制的链接脚本lscript.ld决定。这里的关键约束是必须避开FSBL区并留出足够空间给堆heap和栈stack。堆Heap与栈Stack预留区堆用于malloc/free动态内存分配栈用于函数调用时的局部变量和返回地址。它们通常紧挨着应用程序代码区之后向上高地址生长。SDK默认堆起始地址为0x00300000大小为0x0001000064KB栈起始地址为0x00400000大小为0x000020008KB。但这些值极其脆弱——一旦你的应用程序代码体积超过预留空间或者malloc申请的内存超过堆大小就会发生栈溢出或堆越界后果是随机崩溃且极难定位。DMA缓冲区专用区High Address Region这是最容易被忽视、却最致命的区域。当你的应用需要使用AXI DMA、GEM千兆以太网MAC或USB控制器进行高速数据搬运时DMA引擎要求其访问的缓冲区必须满足两个苛刻条件一是物理地址连续DDR是连续的这点满足二是必须位于Cacheable内存区域之外或必须显式禁用Cache。原因在于ARM的Cache一致性协议如MESI无法自动同步DMA引擎绕过CPU Cache直接写入DDR的数据。如果缓冲区在Cacheable区域CPU读取时可能拿到旧的Cache副本导致数据错乱。因此最佳实践是将DMA缓冲区单独划出一块区域例如0x3F000000 - 0x3FFFFFFF并在链接脚本中将其标记为NOLOAD且ATTRIBUTES WAWrite-Through, Non-Cacheable确保CPU访问时强制穿透Cache。提示Zynq-7000的DDR控制器支持最多4个AXI Master端口HP0-HP3每个端口可配置独立的地址范围和访问属性。这意味着你可以为PL端的高速数据采集模块如ADC采样单独分配一个HP端口和对应的DDR地址段实现与PS端应用的物理隔离避免总线争抢。2.2 内存属性与Cache策略决定性能与正确性的双刃剑在裸机开发中“内存”不是一个抽象概念而是一个带有明确物理属性的实体。ARM Cortex-A9的内存管理单元MMU虽被关闭裸机无MMU但其内存属性寄存器MPU或更准确地说是ARMv7的Domain和Region属性依然生效它决定了CPU如何访问某段内存是否允许读写、是否可执行、最关键的是——是否启用Cache。Zynq PS端的内存属性由两个层面共同决定链接脚本lscript.ld中的SECTION属性通过AT 指定加载地址 REGION指定运行地址ALIGN对齐BLOCK填充以及最重要的ATTRIBUTES字段。ATTRIBUTES WA表示Write-Through, CacheableUN表示UncachedWB表示Write-Back, Cacheable。启动代码ps7_init.c中的AXI GP Master配置在FSBL执行后SDK生成的ps7_init.c会初始化所有AXI Master端口包括ARM CPU的AXI接口。其中Xil_SetTlbAttributes()函数会为不同地址范围设置TLBTranslation Lookaside Buffer条目尽管裸机下TLB不参与地址翻译但它会将内存属性如Cacheable/Non-Cacheable写入ARM的CP15协处理器寄存器从而影响CPU的访问行为。为什么Cache策略如此关键举个真实案例我们曾开发一个基于Zynq-7020的激光雷达点云处理模块PL端FPGA以100MHz频率将点云数据通过AXI HP0端口写入DDR缓冲区地址0x3F000000PS端ARM CPU通过memcpy()将数据拷贝到应用处理区。最初我们将缓冲区设为CacheableWA结果CPU拷贝出来的数据总是旧的——因为PL写入DDR后CPU的L1 Cache里还存着该地址的旧副本memcpy()读取的是Cache而非DDR。解决方法只有两个要么将缓冲区设为UncachedUN要么在每次PL写完后手动执行Xil_DCacheInvalidateRange()刷新Cache。前者简单粗暴但牺牲性能所有访问都走慢速DDR总线后者高效但要求你精确知道数据更新的时机稍有遗漏就出错。最终我们选择了折中方案将缓冲区设为Write-ThroughWA并配合Xil_DCacheFlushRange()在PL写入完成后主动刷出CPU写Cache确保数据一致性。注意Xil_DCacheInvalidateRange()用于使Cache中对应地址范围的副本失效下次读取时强制从DDR重新加载Xil_DCacheFlushRange()用于将Cache中已修改但尚未写回DDR的数据强制写回。两者用途截然不同混淆使用会导致严重数据错误。2.3 方案选型背后的深层考量为什么不用Linux而坚持裸机网络热词里频繁出现“petalinux”“android sdk”这恰恰反衬出裸机开发的价值所在。PetaLinux是Xilinx官方的Linux发行版功能强大驱动齐全但它的代价是确定性Determinism的丧失。Linux内核的进程调度、内存管理、中断延迟对于毫秒级甚至微秒级响应的工业控制、实时通信、图像采集来说是不可接受的噪声源。一个典型的例子某客户用PetaLinux跑GEM以太网当系统负载升高时TCP ACK包的发送延迟从50us飙升至5ms导致上位机认为连接断开。而同样的硬件改用裸机lwIP协议栈延迟稳定在30-60us。裸机开发的另一个核心优势是内存占用的极致可控。Linux内核本身就要占用数MB内存加上用户空间进程、动态库、文件系统缓存留给应用的“干净”内存非常有限。而在裸机下你可以精确控制每一个字节的用途256KB OCM全给中断向量和关键ISR512MB DDR中划出128MB给DMA缓冲区64MB给应用程序剩下的留给堆和栈。这种粒度的控制是构建高可靠性、长周期无人值守设备如野外基站、航天遥测终端的基石。所以当你看到“zynq裸机usb”“qt zynq serialport 库 编译”这类需求时背后的真实诉求往往是在资源受限的嵌入式环境下实现确定性的USB Host通信或串口协议解析而非追求桌面级的GUI体验。3. 核心细节解析从链接脚本到启动代码的逐行拆解3.1 链接脚本lscript.ld的定制化改造定义你的内存疆域SDK/Vitis自动生成的链接脚本通常名为lscript.ld是内存分配的宪法。它定义了.text、.data、.bss等段在物理内存中的位置和大小。默认脚本往往过于保守或不符合你的硬件配置必须手动修改。下面以Zynq-7020DDR 512MB起始0x00100000为例展示一个生产级的lscript.ld关键片段及其原理/* 定义内存区域 */ MEMORY { ps7_ddr_0 : ORIGIN 0x00200000, LENGTH 0x1FE00000 /* 510MB, 起始于0x00200000, 避开FSBL区 */ ps7_ocm_ram_0 : ORIGIN 0x00000000, LENGTH 0x00040000 /* 256KB OCM */ } /* 定义堆和栈的起始地址与大小 */ _stack_size DEFINED(_stack_size) ? _stack_size : 0x00002000; /* 默认8KB栈 */ _heap_size DEFINED(_heap_size) ? _heap_size : 0x00020000; /* 默认128KB堆 */ SECTIONS { .text : { *(.text) *(.text.*) . ALIGN(32); *(.rodata) *(.rodata.*) . ALIGN(32); *(.eh_frame) } ps7_ddr_0 /* 代码段放在DDR主区 */ .data : { *(.data) *(.data.*) . ALIGN(32); *(.sdata) *(.sdata.*) } ps7_ddr_0 AT ps7_ddr_0 /* 数据段加载并运行于DDR */ .bss : { *(.bss) *(.bss.*) *(COMMON) . ALIGN(32); _heap_start .; /* 堆的起始地址即.bss段结束处 */ . _heap_size; /* 堆的大小 */ _heap_end .; /* 堆的结束地址 */ . ALIGN(32); _stack_top .; /* 栈的起始地址即堆结束处 */ . _stack_size; /* 栈的大小 */ _stack_bottom .; /* 栈的结束地址向下生长 */ } ps7_ddr_0 /* .bss段及后续堆栈均在DDR */ /* 关键DMA缓冲区专用段显式标记为Non-Cacheable */ .dma_buffer (NOLOAD) : { . ALIGN(4096); /* 4KB对齐符合AXI DMA要求 */ _dma_buffer_start .; . 0x00100000; /* 分配1MB DMA缓冲区 */ _dma_buffer_end .; } ps7_ddr_0 /* 确保所有未定义段被放置到安全区域 */ . ALIGN(4096); _end .; }这段脚本的核心要点解析MEMORY段定义了ps7_ddr_0区域的ORIGIN为0x00200000LENGTH为0x1FE00000510MB。为什么是510MB因为总DDR 512MB减去FSBL区64KB0x00100000-0x001FFFFF和OCM映射区1MB0x00000000-0x000FFFFF剩余510MB。ORIGIN必须严格大于0x001FFFFF否则链接器会报错。_stack_size和_heap_size使用DEFINED()宏允许你在SDK工程属性中通过-D_stack_size0x00004000等编译选项动态覆盖默认值提供安全底线。.bss段末尾的_heap_start和_heap_end是C运行时库crt0.o初始化堆的依据。malloc()函数内部就是通过这两个符号来管理空闲内存块的。.dma_buffer段使用(NOLOAD)属性意味着该段在生成的ELF文件中不包含实际数据节省Flash空间但其地址和大小在链接时被固定下来。ATTRIBUTES UNUncached需在后续启动代码中设置链接脚本本身不负责属性。实操心得我习惯在lscript.ld顶部添加注释块记录本次修改的日期、原因和影响范围。例如“2024-03-15: 为支持1080p30fps视频流将.dma_buffer大小从512KB增至1MB相应调整ps7_ddr_0 LENGTH为0x1FD00000”。这样当半年后同事接手维护时能瞬间理解改动意图避免误操作。3.2 启动代码ps7_init.c的深度定制固化内存属性ps7_init.c是SDK在综合Vivado工程后自动生成的PS初始化代码它包含了DDR控制器的PHY训练、时序参数配置、时钟分频等关键步骤。但默认版本对内存属性的设置是“最小化”的即只保证FSBL能跑起来对应用层的Cache需求考虑不足。我们必须在此文件中插入关键代码为DMA缓冲区等特殊区域设置正确的内存属性。在ps7_init.c的ps7_init()函数末尾即所有外设初始化完成后添加如下代码#include xil_cache.h #include xil_mmu.h void ps7_init_custom(void) { // 1. 为DMA缓冲区设置Non-Cacheable属性 // 地址范围0x3F000000 - 0x3FFFFFFF (16MB)假设你已在lscript.ld中定义 // ARM Cortex-A9使用Section Descriptor (1MB granularity) 设置内存属性 // Section Descriptor格式[31:20] 物理地址高位 | [19:16] Domain | [15:12] TEX | [11:10] C/B | [9:5] AP | [4] XN | [3:0] Type // 对于Non-Cacheable, Shareable, Read/Write, Execute-Never: 0x10DE2 (详细计算见下文) // 计算Section Descriptor值 // 物理地址0x3F000000 20 0x3F0 // Domain 0 (Domain 0) // TEX 001 (Outer Write-Back, Inner Write-Back) // C/B 00 (Non-Cacheable, Non-Bufferable) - 这是关键 // AP 11 (Full Access) // XN 1 (Execute-Never) // Type 10 (Section) // 组合0x3F0 20 | 0x0 16 | 0x1 12 | 0x0 10 | 0x3 5 | 0x1 4 | 0x2 0x3F0010DE2 // 但CP15寄存器只接受32位故取低32位0x000010DE2 // 将Descriptor写入TTBR0指向的页表裸机下通常使用一级页表 // 此处简化直接调用Xilinx封装好的API Xil_SetTlbAttributes(0x3F000000, 0x10DE2); // 2. 刷新TLB使新属性生效 Xil_InvalidateTlbAll(); // 3. 可选为OCM区域设置Strongly-Ordered属性适用于GPIO寄存器等 // Xil_SetTlbAttributes(0x00000000, 0x10C0E); // Strongly-Ordered, Non-Executable }这段代码的原理在于ARM Cortex-A9通过CP15协处理器的TTBR0Translation Table Base Register 0寄存器指向一个一级页表Page Table该页表的每一项Descriptor描述了1MB地址空间的属性。Xil_SetTlbAttributes()函数的作用就是将指定物理地址0x3F000000所在Section的Descriptor更新为你传入的值0x10DE2。这个值的计算过程如下0x3F000000的Section号是0x3F000000 20 0x3F0所以Descriptor在页表中的索引是0x3F0。Descriptor的Bit[31:20]是物理地址高位即0x3F0。Bit[19:16]是Domain裸机常用Domain 0值为0。Bit[15:12]是TEXTexture影响Cache策略001表示Outer/Inner Write-Back。Bit[11:10]是C/B位00表示Non-Cacheable and Non-Bufferable这是DMA缓冲区的铁律。Bit[9:5]是APAccess Permission11表示Full AccessSupervisor and User mode Read/Write。Bit[4]是XNeXecute Never1表示禁止执行防止代码注入。Bit[3:0]是Type10表示Section Descriptor。最终组合得到0x3F0 20 | 0x0 16 | 0x1 12 | 0x0 10 | 0x3 5 | 0x1 4 | 0x2 0x3F0010DE2取低32位即0x000010DE2。这个值写入页表后CPU访问0x3F000000到0x3FFFFFFF之间的任何地址都会遵循Non-Cacheable规则。注意Xil_InvalidateTlbAll()是必须的。它清空TLBTranslation Lookaside Buffer中所有缓存的页表项强制CPU下次访问时重新从页表中读取Descriptor。否则旧的Cacheable属性可能还在TLB中生效导致设置无效。3.3 应用层内存使用的规范与技巧让malloc和DMA协同工作链接脚本和启动代码搞定后应用层的内存使用就成了“守规矩”的问题。以下是我在多个Zynq项目中总结出的黄金法则法则一DMA缓冲区必须静态分配严禁malloc理由再强调一次malloc()分配的内存地址是动态的且默认位于Cacheable区域。即使你用Xil_DCacheInvalidateRange()去刷新也无法保证每次分配的地址都在你预设的Non-Cacheable区域内。正确的做法是在全局变量中声明一个大数组并用__attribute__((section(.dma_buffer)))将其链接到.dma_buffer段// 在main.c或dma_driver.c中 #define DMA_BUFFER_SIZE (1024*1024) // 1MB static u8 dma_rx_buffer[DMA_BUFFER_SIZE] __attribute__((section(.dma_buffer))); static u8 dma_tx_buffer[DMA_BUFFER_SIZE] __attribute__((section(.dma_buffer))); // 初始化DMA时直接传入缓冲区地址 XAxiDma_Config *Config; Config XAxiDma_LookupConfig(XPAR_AXI_DMA_0_DEVICE_ID); XAxiDma_CfgInitialize(AxiDma, Config); XAxiDma_SimpleTransfer(AxiDma, (u32)dma_rx_buffer, DMA_BUFFER_SIZE, XAXIDMA_DEVICE_TO_DMA);法则二堆内存的使用必须配对检查裸机下没有内存泄漏检测工具malloc()和free()的滥用是隐形杀手。我的习惯是在每次malloc()后立即检查返回值并在free()后将指针置为NULLu32 *large_array (u32*)malloc(1024*1024*sizeof(u32)); // 分配4MB if (large_array NULL) { xil_printf(ERROR: malloc failed for large_array!\r\n); return XST_FAILURE; // 或进入安全模式 } // ... 使用large_array ... free(large_array); large_array NULL; // 防止野指针法则三栈空间必须“可视化”监控栈溢出是裸机开发中最难调试的问题之一。我采用两种手段编译期预警在SDK工程属性中勾选-fstack-protector-strong它会在函数栈帧中插入canary值函数返回时检查若被篡改则触发abort()。运行期监控在main()函数开头用memset()将整个栈空间_stack_bottom到_stack_top填满一个特定值如0xAA然后在主循环中定期调用一个函数扫描栈空间统计从_stack_top向下有多少字节还是0xAA。如果这个数字小于某个阈值如1KB就说明栈快用完了可以触发告警或降频运行。// 在main()中 memset((void*)_stack_bottom, 0xAA, _stack_top - _stack_bottom); // 在主循环中 u32 stack_usage_check(void) { u32 *ptr (u32*)_stack_bottom; u32 count 0; while (ptr (u32*)_stack_top *ptr 0xAAAAAAAA) { ptr; count; } return count * sizeof(u32); // 返回已使用的栈字节数 }4. 实操过程从Vivado工程到SDK烧写的一站式验证4.1 Vivado工程中的DDR控制器配置要点Vivado是Zynq开发的起点DDR控制器DDR3/DDR4 SDRAM Controller的配置直接决定了硬件能否稳定工作进而影响软件内存分配的可行性。很多“DDR分配失败”的问题根源其实在Vivado里。在Vivado Block Design中双击ZYNQ7 Processing SystemIP核打开Configuration窗口重点检查以下几项PS-PL Configuration DDR ConfigurationMemory Type必须与你板子上焊接的DDR颗粒型号严格一致如DDR3-1600, DDR4-2400。选错会导致PHY训练失败FSBL报错DDR Initialization Failed。Data Width常见为16-bit单颗芯片或32-bit两颗并联。必须与PCB布线匹配否则读写错位。Address Map选择AXI HP0-3端口的地址范围。默认HP0是0x00000000-0x3FFFFFFF但如果你的板子只焊了512MB DDR这里应改为0x00000000-0x1FFFFFFF否则Vivado会生成错误的地址映射。Clock Configuration FCLK_CLK0这是DDR控制器的参考时钟。Zynq-7000的DDR PHY要求FCLK_CLK0频率必须是DDR数据速率的1/4DDR3-1600对应800MHzFCLK_CLK0需为200MHz。务必在Clocking选项卡中确认FCLK_CLK0的输出频率与DDR时序要求匹配。实操心得每次修改DDR配置后必须点击Validate Design确保Block Design无错误。然后右键ZYNQ7 Processing System选择Generate Output Products...勾选Global Reset和Include bitstream生成新的system_wrapper.bit。这一步生成的ps7_init.c才是与你硬件配置完全匹配的千万别用旧的。4.2 SDK/Vitis工程创建与链接脚本导入Vivado导出硬件平台File Export Export Hardware后在SDK或Vitis中创建新应用工程File New Application ProjectHardware Platform选择你导出的.hdf文件Processor选择ps7_cortexa9_0Zynq-7000OS Platform选择standalone裸机Template选择Hello World作为起点此时SDK会自动生成src/lscript.ld。切勿直接在此文件上修改正确流程是将你精心定制的lscript.ld文件复制到工程的src/目录下。右键工程名 PropertiesC/C Build Settings Tool Settings ARM v7 gcc linker Miscellaneous。在Linker flags中添加-T ./src/lscript.ld强制链接器使用你的脚本。在C/C Build Settings Tool Settings ARM v7 gcc compiler Preprocessor中添加-D_stack_size0x00004000 -D_heap_size0x00040000覆盖链接脚本中的默认值。提示在Properties C/C Build Settings Tool Settings ARM v7 gcc linker General中勾选Use newlib nano。它能显著减小C库体积为你的应用程序腾出更多DDR空间。4.3 烧写与调试用XMD和Vivado Hardware Manager验证内存烧写不是终点验证才是关键。我推荐三步验证法第一步FSBL启动日志分析将boot.bin包含FSBL、bitstream、application.elf烧写到QSPI Flash上电后通过UART查看FSBL输出。成功日志应包含Xilinx Zynq First Stage Boot Loader Release 2018.3 Jun 12 2018 10:23:45 ... DDR initialization completed如果卡在DDR initialization failed说明Vivado中的DDR配置与硬件不匹配需回溯检查。第二步XMD内存探针测试在SDK中Xilinx Tools Open Xilinx Terminal启动XMD命令行connect arm hw stop # 读取FSBL区确认未被覆盖 mrd -32 0x00100000 4 # 读取应用程序代码段起始地址确认代码已加载 mrd -32 0x00200000 4 # 读取DMA缓冲区起始地址确认为0 mrd -32 0x3F000000 4 # 向DMA缓冲区写入测试数据 mwr -32 0x3F000000 0xDEADBEEF # 再次读取确认写入成功 mrd -32 0x3F000000 1如果mrd读到的值与mwr写入的值一致说明DDR物理访问正常。第三步应用层Cache一致性测试编写一个最简测试程序// main.c #include xil_printf.h #include xil_cache.h #include xparameters.h // 声明DMA缓冲区 extern u8 dma_rx_buffer[]; extern u8 dma_tx_buffer[]; int main() { init_platform(); // SDK自动生成的初始化 // 测试1Cacheable区域写入读取是否一致 u32 *test_ptr (u32*)0x00200000; *test_ptr 0x12345678; xil_printf(Test1: %x\r\n, *test_ptr); // 应输出12345678 // 测试2Non-Cacheable DMA缓冲区写入读取是否一致 u32 *dma_ptr (u32*)0x3F000000; *dma_ptr 0x87654321; xil_printf(Test2: %x\r\n, *dma_ptr); // 应输出87654321 // 测试3模拟PL写入CPU读取关键 // 假设PL已将0xFEDCBA98写入0x3F000000 // 此时CPU读取应为FEDCBA98 // 如果之前没设置Non-Cacheable这里可能还是87654321 xil_printf(Test3: %x\r\n, *dma_ptr); cleanup_platform(); return 0; }
返回列表