第一次接触Zynq的PL和PS数据传输时,我完全被各种专业术语搞晕了。后来在实际项目中踩过几次坑才明白,其实可以把它想象成两个城市之间的物流系统:PL端(可编程逻辑)是生产工厂,PS端(处理器系统)是仓储中心,而AXI_DMA就是负责两地货物运输的高速公路和卡车车队。
AXI_DMA最核心的优势在于解放CPU。传统的数据搬运需要CPU全程参与,就像让公司CEO亲自去搬货;而DMA模式下,CEO只需要下达指令,具体运输工作交给专门的物流部门(DMA控制器)完成。实测在Zynq-7020平台上,使用AXI_DMA传输1MB数据,CPU占用率能从90%降到不足5%。
这里有个容易混淆的概念:AXI_DMA实际上包含两个独立通道:
我在调试时经常用彩色标签区分这两个通道,红色标签贴MM2S相关代码,蓝色标签贴S2MM部分,这个方法帮我避免了很多接线错误。
创建Block Design时,AXI_DMA的配置页面有多个容易踩坑的参数:
DMA中断配置是另一个关键点。建议将mm2s_introut和s2mm_introut分别接到不同的IRQ通道,这样在驱动中可以通过中断号直接判断是哪个通道触发的中断。我们的配置如下:
verilog复制connect_bd_net [get_bd_pins axi_dma_0/mm2s_introut] [get_bd_pins xlconcat_0/In0]
connect_bd_net [get_bd_pins axi_dma_0/s2mm_introut] [get_bd_pins xlconcat_0/In1]
用Verilog实现的控制寄存器模块,我习惯添加这些状态指示灯:
verilog复制always @(posedge clk) begin
if (reset) begin
dma_status <= 3'b000; // 空闲状态
end else begin
case(dma_status)
3'b000: if (start) dma_status <= 3'b001; // 启动
3'b001: dma_status <= 3'b010; // 传输中
3'b010: if (done) dma_status <= 3'b100; // 完成
default: dma_status <= 3'b000;
endcase
end
end
这些状态可以通过LED灯直观显示,调试时非常有用。记得在约束文件中给状态指示灯分配具体的FPGA管脚。
自动生成的设备树有个大坑:两个DMA通道的device-id默认都是0。必须修改为0和1,否则会导致通道冲突。修改后的设备树片段应该是这样:
dts复制dma-channel@40400000 {
compatible = "xlnx,axi-dma-mm2s-channel";
xlnx,device-id = <0x0>;
};
dma-channel@40400030 {
compatible = "xlnx,axi-dma-s2mm-channel";
xlnx,device-id = <0x1>;
};
建议在设备树中添加字符设备节点,方便应用层调用:
dts复制axidma_chrdev: axidma_chrdev@0 {
compatible = "xlnx,axidma-chrdev";
dmas = <&axi_dma_0 0 &axi_dma_0 1>;
dma-names = "tx_channel", "rx_channel";
};
从GitCode获取的开源驱动需要修改config.mk文件,这里分享我的配置模板:
makefile复制CROSS_COMPILE = arm-linux-gnueabihf-
ARCH = arm
KBUILD_DIR = $(HOME)/xilinx/linux-xlnx
OUTPUT_DIR = $(PWD)/build
编译时遇到头文件找不到的问题,可以尝试在Makefile中添加:
makefile复制EXTRA_CFLAGS += -I$(KBUILD_DIR)/include/uapi
EXTRA_CFLAGS += -I$(KBUILD_DIR)/include
使用axidma_malloc分配的内存默认是cache非一致的,对于需要CPU处理的数据,建议手动同步缓存:
c复制void *buf = axidma_malloc(axidma_dev, size);
// 写入数据后
axidma_cache_flush(buf, size);
// 读取数据前
axidma_cache_invalidate(buf, size);
对于高频采样数据,我设计了一个双缓冲机制:
c复制#define BUF_NUM 2
struct {
void *addr;
size_t size;
bool ready;
} dma_buf[BUF_NUM];
// 初始化
for(int i=0; i<BUF_NUM; i++) {
dma_buf[i].addr = axidma_malloc(axidma_dev, BUF_SIZE);
dma_buf[i].size = BUF_SIZE;
dma_buf[i].ready = false;
}
结合pthread实现的生产者-消费者模型:
c复制pthread_mutex_t buf_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t buf_cond = PTHREAD_COND_INITIALIZER;
void *dma_thread(void *arg) {
while(1) {
axidma_oneway_transfer(axidma_dev, rx_chan, dma_buf[write_idx].addr, BUF_SIZE, true);
pthread_mutex_lock(&buf_mutex);
dma_buf[write_idx].ready = true;
write_idx = (write_idx + 1) % BUF_NUM;
pthread_cond_signal(&buf_cond);
pthread_mutex_unlock(&buf_mutex);
}
}
void *process_thread(void *arg) {
while(1) {
pthread_mutex_lock(&buf_mutex);
while(!dma_buf[read_idx].ready) {
pthread_cond_wait(&buf_cond, &buf_mutex);
}
process_data(dma_buf[read_idx].addr);
dma_buf[read_idx].ready = false;
read_idx = (read_idx + 1) % BUF_NUM;
pthread_mutex_unlock(&buf_mutex);
}
}
通过实测发现,调整DMA burst长度可以显著提升吞吐量。在Vivado中设置:
在驱动加载时可以通过dmesg查看实际的DMA配置:
bash复制dmesg | grep dma
# 理想输出应包含类似信息:
# xilinx_dma 40400000.dma: Xilinx AXI DMA Engine Driver Probed!!
# xilinx_dma 40400000.dma: AXI DMA MM2S Channel registered
# xilinx_dma 40400000.dma: AXI DMA S2MM Channel registered
遇到传输中断时,按这个顺序检查:
bash复制devmem 0x40400000 32
bash复制cat /proc/interrupts | grep dma
bash复制hexdump -C /dev/axidma_chrdev
有次遇到数据错位问题,最终发现是PL端数据位序与PS端不一致。解决方案是在DMA配置中统一使用小端模式,并在应用层添加字节序转换:
c复制uint32_t swap_endian(uint32_t val) {
return ((val >> 24) & 0xff) | ((val << 8) & 0xff0000) |
((val >> 8) & 0xff00) | ((val << 24) & 0xff000000);
}
最近完成的工业传感器项目采用了这样的数据处理流程:
c复制void process_adc_data(void *buf) {
uint16_t *p = (uint16_t *)buf;
for(int i=0; i<64; i++) {
ch1[i] = p[i*4 + 0];
ch2[i] = p[i*4 + 1];
ch3[i] = p[i*4 + 2];
ch4[i] = p[i*4 + 3];
// 应用校准系数
ch1[i] = (ch1[i] - offset[0]) * gain[0];
// ...其他通道类似处理
}
// 触发FFT分析等后续处理
}
这个方案实现了稳定的20MB/s持续数据传输,CPU占用率保持在10%以下。关键点在于:
对于需要更高性能的场景,可以考虑:
在调试复杂问题时,我总结了一套有效的方法论:
c复制printk(KERN_DEBUG "DMA status: 0x%x\n",
ioread32(dma->regs + XILINX_DMA_REG_SR));
记得在正式产品中移除所有调试代码和打印语句,我们曾经因为忘记移除调试打印导致系统日志爆满。