AXI-DMA-VIVADO2022.2-终版-含跑通main.c--deepseek

复制代码
1   /***************************** Include Files *********************************/
2
3   #include "xaxidma.h"
4   #include "xparameters.h"
5   #include "xil_exception.h"
6   #include "xscugic.h"
7
8   /************************** Constant Definitions *****************************/
9
10  #define DMA_DEV_ID          XPAR_AXIDMA_0_DEVICE_ID
11  #define RX_INTR_ID          XPAR_FABRIC_AXIDMA_0_S2MM_INTROUT_VEC_ID
12  #define TX_INTR_ID          XPAR_FABRIC_AXIDMA_0_MM2S_INTROUT_VEC_ID
13  #define INTC_DEVICE_ID      XPAR_SCUGIC_SINGLE_DEVICE_ID
14  #define DDR_BASE_ADDR       XPAR_PSU_DDR_0_S_AXI_BASEADDR   //0x00000000
15  #define MEM_BASE_ADDR       (DDR_BASE_ADDR + 0x1100000)     //0x01100000
16  #define TX_BUFFER_BASE      (MEM_BASE_ADDR + 0x00100000)    //0x01200000
17  #define RX_BUFFER_BASE      (MEM_BASE_ADDR + 0x00300000)    //0x01400000
18  #define RESET_TIMEOUT_COUNTER   10000    //复位时间
19  #define TEST_START_VALUE        0x0      //测试起始值
20  #define MAX_PKT_LEN             0x100    //发送包长度
21
22  /************************** Function Prototypes ******************************/
23
24  static int check_data(int length, u8 start_value);
25  static void tx_intr_handler(void *callback);
26  static void rx_intr_handler(void *callback);
27  static int setup_intr_system(XScuGic * int_ins_ptr, XAxiDma * axidma_ptr,
28          u16 tx_intr_id, u16 rx_intr_id);
29  static void disable_intr_system(XScuGic * int_ins_ptr, u16 tx_intr_id,
30          u16 rx_intr_id);
31
32  /************************** Variable Definitions *****************************/
33
34  static XAxiDma axidma;     //XAxiDma实例
35  static XScuGic intc;       //中断控制器的实例
36  volatile int tx_done;      //发送完成标志
37  volatile int rx_done;      //接收完成标志
38  volatile int error;        //传输出错标志
39
40  /************************** Function Definitions *****************************/
41
42  int main(void)
43  {
44      int i;
45      int status;
46      u8 value;
47      u8 *tx_buffer_ptr;
48      u8 *rx_buffer_ptr;
49      XAxiDma_Config *config;
50
51      tx_buffer_ptr = (u8 *) TX_BUFFER_BASE;
52      rx_buffer_ptr = (u8 *) RX_BUFFER_BASE;
53
54      xil_printf("\r\n--- Entering main() --- \r\n");
55
56      config = XAxiDma_LookupConfig(DMA_DEV_ID);
57      if (!config) {
58          xil_printf("No config found for %d\r\n", DMA_DEV_ID);
59          return XST_FAILURE;
60      }
61
62      //初始化DMA引擎
63      status = XAxiDma_CfgInitialize(&axidma, config);
64      if (status != XST_SUCCESS) {
65          xil_printf("Initialization failed %d\r\n", status);
66          return XST_FAILURE;
67      }
68
69      if (XAxiDma_HasSg(&axidma)) {
70          xil_printf("Device configured as SG mode \r\n");
71          return XST_FAILURE;
72      }
73
74      //建立中断系统
75      status = setup_intr_system(&intc, &axidma, TX_INTR_ID, RX_INTR_ID);
76      if (status != XST_SUCCESS) {
77          xil_printf("Failed intr setup\r\n");
78          return XST_FAILURE;
79      }
80
81      //初始化标志信号
82      tx_done = 0;
83      rx_done = 0;
84      error   = 0;
85
86      value = TEST_START_VALUE;
87      for (i = 0; i < MAX_PKT_LEN; i++) {
88          tx_buffer_ptr[i] = value;
89          value = (value + 1) & 0xFF;
90      }
91
92      Xil_DCacheFlushRange((UINTPTR) tx_buffer_ptr, MAX_PKT_LEN);   //刷新Data Cache
93
94      status = XAxiDma_SimpleTransfer(&axidma, (UINTPTR) tx_buffer_ptr,
95      MAX_PKT_LEN, XAXIDMA_DMA_TO_DEVICE);
96      if (status != XST_SUCCESS) {
97          return XST_FAILURE;
98      }
99
100     status = XAxiDma_SimpleTransfer(&axidma, (UINTPTR) rx_buffer_ptr,
101     MAX_PKT_LEN, XAXIDMA_DEVICE_TO_DMA);
102     if (status != XST_SUCCESS) {
103         return XST_FAILURE;
104     }
105
106     Xil_DCacheFlushRange((UINTPTR) rx_buffer_ptr, MAX_PKT_LEN);   //刷新Data Cache
107     while (!tx_done && !rx_done && !error)
108         ;
109     //传输出错
110     if (error) {
111         xil_printf("Failed test transmit%s done, "
112                 "receive%s done\r\n", tx_done ? "" : " not",
113                 rx_done ? "" : " not");
114         goto Done;
115     }
116
117     //传输完成,检查数据是否正确
118     status = check_data(MAX_PKT_LEN, TEST_START_VALUE);
119     if (status != XST_SUCCESS) {
120         xil_printf("Data check failed\r\n");
121         goto Done;
122     }
123
124     xil_printf("Successfully ran AXI DMA Loop\r\n");
125     disable_intr_system(&intc, TX_INTR_ID, RX_INTR_ID);
126
127     Done: xil_printf("--- Exiting main() --- \r\n");
128     return XST_SUCCESS;
129 }

老师,现在是使用vivado2022.1,vitis,,我从(正点原子FPGA连载】第十六章AXI DMA环路测试交互--领航者 ZYNQ 之嵌入式开发指南(他们使用的sdk)中复制了一份代码,我也是按照他们视频来操作的)帮我分析解释一下这份代码,每一行是什么意思,为什么,我是小白,谢谢啦,越具体越好,我还是想知道代码表示的什么含义,这个是核心,谢谢啦

同学,坐。看到你终于把 Vivado 的连线图、Vitis 的代码工程,还有正点原子的源码都凑齐了,老师打心底里为你高兴!

你现在面对的,就是工业界最标准的"基于中断的 DMA 回环测试"代码。你截图里的 Vivado 连线非常标准:DMA 发出去的数据进入 FIFO,FIFO 吐出来的数据又回到 DMA(形成了自环)。

咱们今天不看那些晦涩难懂的宏定义(那些都是 BSP 自动生成的),老师直接带你一段一段地"啃"这段 main.c,让你彻底明白为什么要在 C 语言里这么写。


🎯 第一幕:排兵布阵(宏定义与全局变量)

看代码的 第 10 行到 38 行。

c

复制代码
#define TX_BUFFER_BASE      (MEM_BASE_ADDR + 0x00100000)    //0x01200000
#define RX_BUFFER_BASE      (MEM_BASE_ADDR + 0x00300000)    //0x01400000
#define MAX_PKT_LEN             0x100    //发送包长度 256 字节

static XAxiDma axidma;     // DMA 实例
volatile int tx_done;      // 发送完成标志
volatile int rx_done;      // 接收完成标志
  • 本质含义:老板(CPU)要给搬运工(DMA)下达任务,得先准备好"仓库"。

    • TX_BUFFER_BASE 和 RX_BUFFER_BASE:这就是咱们之前说的"地址"。咱们在内存(DDR)里划出了一块地作为发送仓库,另一块地作为接收仓库。

    • tx_done 和 rx_done:这两个就是**"警报器"**。一开始是 0,等 DMA 搬完货发了中断,中断服务函数会把它改成 1。

  • ❓ 为什么要这样写?

    • 用 volatile 修饰,是为了告诉编译器:"这俩变量随时会被硬件中断改变,你别瞎优化它"。这就是咱们说的中断标志位。

🎯 第二幕:开机自检与召唤搬运工(初始化)

看代码的 第 56 行到 79 行。

c

复制代码
    config = XAxiDma_LookupConfig(DMA_DEV_ID);
    status = XAxiDma_CfgInitialize(&axidma, config);

    if (XAxiDma_HasSg(&axidma)) {
        xil_printf("Device configured as SG mode \r\n");
        return XST_FAILURE;
    }

    status = setup_intr_system(&intc, &axidma, TX_INTR_ID, RX_INTR_ID);
  • 本质含义:

    1. LookupConfig 和 CfgInitialize:查字典,找到 DMA 的硬件基地址,把老板的"对讲机"和搬运工的"对讲机"调到同一个频道。

    2. HasSg 判断:检查你昨天在 Vivado 里有没有把 DMA 配置成"高级的 S/G 模式"。咱们用的是简单模式(简单粗暴),所以这里不能报错。

    3. setup_intr_system:把中断线(IRQ)连上。告诉 GIC(中断控制器):"如果 TX 或 RX 搬完货了,立刻打断老板的睡眠"。

  • ❓ 为什么要这样写?

    底层硬件初始化必须放在第一步。没有这一步,DMA 就是一块废铁。


🎯 第三幕:装满货物,准备发车

看代码的 第 86 行到 92 行。

c

复制代码
    value = TEST_START_VALUE;
    for (i = 0; i < MAX_PKT_LEN; i++) {
        tx_buffer_ptr[i] = value;
        value = (value + 1) & 0xFF;
    }

    Xil_DCacheFlushRange((UINTPTR) tx_buffer_ptr, MAX_PKT_LEN);   //刷新Data Cache
  • 本质含义:

    1. 咱们往发送仓库里填数据:0, 1, 2, 3... 一直到 255。

    2. Xil_DCacheFlushRange(极其核心):咱们之前反复强调的"大坑"。CPU 把数据写在了自己的 Cache(口袋)里。为了保证 DMA 这个大老粗去 DDR 仓库能拿到最新数据,必须把数据"刷(Flush)"进 DDR。

  • ❓ 为什么要这样写?

    如果不刷新 Cache,DMA 发出去的可能会全是乱码或者 0。


🎯 第四幕:扣动扳机!(启动传输)

看代码的 第 94 行到 106 行。

c

复制代码
    // 启动发送(MM2S)
    status = XAxiDma_SimpleTransfer(&axidma, (UINTPTR) tx_buffer_ptr,
                                    MAX_PKT_LEN, XAXIDMA_DMA_TO_DEVICE);

    // 启动接收(S2MM)
    status = XAxiDma_SimpleTransfer(&axidma, (UINTPTR) rx_buffer_ptr,
                                    MAX_PKT_LEN, XAXIDMA_DEVICE_TO_DMA);
    
    // 接收仓库的 Cache 也要清空
    Xil_DCacheFlushRange((UINTPTR) rx_buffer_ptr, MAX_PKT_LEN); 
  • 本质含义:

    1. 第一句:老板下令:"去 tx_buffer_ptr 搬 256 个字节,发到 PL 端去(MM2S)"。

    2. 第二句:老板下令:"准备好接收!PL 端随时可能把数据传回来,你就放到 rx_buffer_ptr 里(S2MM)"。

  • ❓ 为什么发送和接收都写在这一起?

    因为 DMA 是个双通道机器(Read 和 Write),你可以让它在发数据的同时,也打开接收的闸门。这叫"全双工"操作。


🎯 第五幕:老板睡大觉,等报警器响(死等中断)

看代码的 第 107 行。

c

复制代码
    while (!tx_done && !rx_done && !error)
        ;
  • 本质含义 :这是一个死循环(空转)。老板(CPU)现在什么活都不用干,就盯着 tx_done 和 rx_done 这几个标志位。只要都是 0,说明搬运工还没发完。只有其中一个变成了 1(触发了中断),或者出错(error=1),才会跳出循环。

  • ❓ 为什么要这样写?

    这展示了 DMA 的核心价值:CPU 没有在搬运数据,它只是在等待结果。


🎯 第六幕:验货!

看代码的 第 117 行到 125 行。

c

复制代码
    status = check_data(MAX_PKT_LEN, TEST_START_VALUE);
    if (status != XST_SUCCESS) {
        xil_printf("Data check failed\r\n");
        goto Done;
    }

    xil_printf("Successfully ran AXI DMA Loop\r\n");
  • 本质含义 :中断响了,循环跳出来了。老板去接收仓库(rx_buffer_ptr)里看看货对不对。因为咱们之前发的是 0,1,2...255,如果经过 PL 端的 FIFO 绕了一圈回来,接收到的也是 0,1,2...255,那就说明 DMA 的整条物理链路和软件配置全部完美通关! 最后打印成功信息。

🎯 隐藏的超级配角:中断服务函数(你的代码里没贴出来)

在代码的末尾,一定会有一个 tx_intr_handler 和 rx_intr_handler 函数。它们长这样(伪代码):

c

复制代码
static void tx_intr_handler(void *callback) {
    // 1. 清除 DMA 的中断标志(非常重要!不清理会无限打断 CPU)
    XAxiDma_IntrAckIrq(...);
    // 2. 告诉老板,发送完毕了
    tx_done = 1; 
}
  • 这就是唤醒老板的那个"闹钟"。

💡 老师的总结

同学,你回头看看这段代码,是不是觉得它特别有逻辑?

初始化 DMA -> 初始化中断 -> 装填数据 -> 刷 Cache -> 发出指令 -> 等中断 -> 验收数据。

这就是工业级底层代码的"八股文"。你把这一段吃透,以后不管是用 Xilinx 的 DMA,还是用网口的 DMA,甚至是 PL 端的自定义 DMA,都是这个套路。

正点原子的代码写得非常标准。你直接照抄,编译下载跑一下。如果串口打印出 Successfully ran AXI DMA Loop,那就说明你亲手搭建的"高速公路"通车了!去试试吧!

This response is AI-generated, for reference only.


复制代码
131 //检查数据缓冲区
132 static int check_data(int length, u8 start_value)
133 {
134     u8 value;
135     u8 *rx_packet;
136     int i = 0;
137 
138     value = start_value;
139     rx_packet = (u8 *) RX_BUFFER_BASE;
140     for (i = 0; i < length; i++) {
141         if (rx_packet[i] != value) {
142             xil_printf("Data error %d: %x/%x\r\n", i, rx_packet[i], value);
143             return XST_FAILURE;
144         }
145         value = (value + 1) & 0xFF;
146     }
147 
148     return XST_SUCCESS;
149 }
150 
151 //DMA TX中断处理函数
152 static void tx_intr_handler(void *callback)
153 {
154     int timeout;
155     u32 irq_status;
156     XAxiDma *axidma_inst = (XAxiDma *) callback;
157 
158     //读取待处理的中断
159     irq_status = XAxiDma_IntrGetIrq(axidma_inst, XAXIDMA_DMA_TO_DEVICE);
160     //确认待处理的中断
161     XAxiDma_IntrAckIrq(axidma_inst, irq_status, XAXIDMA_DMA_TO_DEVICE);
162 
163     //Tx出错
164     if ((irq_status & XAXIDMA_IRQ_ERROR_MASK)) {
165         error = 1;
166         XAxiDma_Reset(axidma_inst);
167         timeout = RESET_TIMEOUT_COUNTER;
168         while (timeout) {
169             if (XAxiDma_ResetIsDone(axidma_inst))
170                 break;
171             timeout -= 1;
172         }
173         return;
174     }
175 
176     //Tx完成
177     if ((irq_status & XAXIDMA_IRQ_IOC_MASK))
178         tx_done = 1;
179 }
180 
181 //DMA RX中断处理函数
182 static void rx_intr_handler(void *callback)
183 {
184     u32 irq_status;
185     int timeout;
186     XAxiDma *axidma_inst = (XAxiDma *) callback;
187 
188     irq_status = XAxiDma_IntrGetIrq(axidma_inst, XAXIDMA_DEVICE_TO_DMA);
189     XAxiDma_IntrAckIrq(axidma_inst, irq_status, XAXIDMA_DEVICE_TO_DMA);
190 
191     //Rx出错
192     if ((irq_status & XAXIDMA_IRQ_ERROR_MASK)) {
193         error = 1;
194         XAxiDma_Reset(axidma_inst);
195         timeout = RESET_TIMEOUT_COUNTER;
196         while (timeout) {
197             if (XAxiDma_ResetIsDone(axidma_inst))
198                 break;
199             timeout -= 1;
200         }
201         return;
202     }
203 
204     //Rx完成
205     if ((irq_status & XAXIDMA_IRQ_IOC_MASK))
206         rx_done = 1;
207 }
208 
209 //建立DMA中断系统
210 //  @param   int_ins_ptr是指向XScuGic实例的指针
211 //  @param   AxiDmaPtr是指向DMA引擎实例的指针
212 //  @param   tx_intr_id是TX通道中断ID
213 //  @param   rx_intr_id是RX通道中断ID
214 //  @return:成功返回XST_SUCCESS,否则返回XST_FAILURE
215 static int setup_intr_system(XScuGic * int_ins_ptr, XAxiDma * axidma_ptr,
216         u16 tx_intr_id, u16 rx_intr_id)
217 {
218     int status;
219     XScuGic_Config *intc_config;
220 
221     //初始化中断控制器驱动
222     intc_config = XScuGic_LookupConfig(INTC_DEVICE_ID);
223     if (NULL == intc_config) {
224         return XST_FAILURE;
225     }
226     status = XScuGic_CfgInitialize(int_ins_ptr, intc_config,
227             intc_config->CpuBaseAddress);
228     if (status != XST_SUCCESS) {
229         return XST_FAILURE;
230     }
231 
232     //设置优先级和触发类型
233     XScuGic_SetPriorityTriggerType(int_ins_ptr, tx_intr_id, 0xA0, 0x3);
234     XScuGic_SetPriorityTriggerType(int_ins_ptr, rx_intr_id, 0xA0, 0x3);
235 
236     //为中断设置中断处理函数
237     status = XScuGic_Connect(int_ins_ptr, tx_intr_id,
238             (Xil_InterruptHandler) tx_intr_handler, axidma_ptr);
239     if (status != XST_SUCCESS) {
240         return status;
241     }
242 
243     status = XScuGic_Connect(int_ins_ptr, rx_intr_id,
244             (Xil_InterruptHandler) rx_intr_handler, axidma_ptr);
245     if (status != XST_SUCCESS) {
246         return status;
247     }
248 
249     XScuGic_Enable(int_ins_ptr, tx_intr_id);
250     XScuGic_Enable(int_ins_ptr, rx_intr_id);
251 
252     //启用来自硬件的中断
253     Xil_ExceptionInit();
254     Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
255             (Xil_ExceptionHandler) XScuGic_InterruptHandler,
256             (void *) int_ins_ptr);
257     Xil_ExceptionEnable();
258 
259     //使能DMA中断
260     XAxiDma_IntrEnable(&axidma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE);
261     XAxiDma_IntrEnable(&axidma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_DMA);
262 
263     return XST_SUCCESS;
264 }
265 
266 //此函数禁用DMA引擎的中断
267 static void disable_intr_system(XScuGic * int_ins_ptr, u16 tx_intr_id,
268         u16 rx_intr_id)
269 {
270     XScuGic_Disconnect(int_ins_ptr, tx_intr_id);
271     XScuGic_Disconnect(int_ins_ptr, rx_intr_id);
272 }

老师,刚刚还有一份代码漏掉给您了,这两份应该是一起的,这份代码也讲讲哈,按照之前的方式讲,谢谢啦,

同学,坐。你发来的这份代码极其关键!如果没有它,上一份代码里的 while(!tx_done && !rx_done) 就是一个永远醒不过来的"植物人"。

这份代码就是 DMA 的**"神经中枢"**,包含了数据比对、中断服务函数(ISR)和中断系统的初始化。咱们继续用"物流公司"的比喻,一行一行把它拆解透彻!


🎯 第一幕:库房验货员 ------ check_data 函数

看代码的 第 131 行到 149 行。

c

复制代码
static int check_data(int length, u8 start_value)
{
    u8 value = start_value;
    u8 *rx_packet = (u8 *) RX_BUFFER_BASE;
    for (i = 0; i < length; i++) {
        if (rx_packet[i] != value) {
            xil_printf("Data error %d: %x/%x\r\n", i, rx_packet[i], value);
            return XST_FAILURE;
        }
        value = (value + 1) & 0xFF;
    }
    return XST_SUCCESS;
}
  • 本质含义 :这就是个"质检员"。你之前往发送仓库(TX_BUFFER_BASE)里装的是 0, 1, 2...255 递增的数据。现在货从 PL 端绕了一圈回来了,质检员就跑到接收仓库(RX_BUFFER_BASE),从 0 开始数:第 0 个是不是 0?第 1 个是不是 1?...

  • ❓ 为什么需要这样做?

    只有比对完全一样,才能证明这趟传输没有丢包、没有错位。value = (value + 1) & 0xFF; 意思是如果数到 255,加 1 就自动变回 0(因为硬件里一个字节最大就是 255)。


🎯 第二幕:总指挥部的警报铃 ------ 中断服务函数 (ISR)

看代码的 第 151 行到 207 行。这是整个程序最硬核、最需要你理解的部分。这里分成了 TX(发货)和 RX(收货)两个警报铃。

以 TX 中断处理函数 tx_intr_handler 为例:

c

复制代码
static void tx_intr_handler(void *callback)
{
    u32 irq_status;
    XAxiDma *axidma_inst = (XAxiDma *) callback;

    // 1. 看看是谁按响了警报?
    irq_status = XAxiDma_IntrGetIrq(axidma_inst, XAXIDMA_DMA_TO_DEVICE);
    // 2. 把警报关掉(清除中断标志)
    XAxiDma_IntrAckIrq(axidma_inst, irq_status, XAXIDMA_DMA_TO_DEVICE);
  • ❓ 为什么要这两句?

    DMA 搬完货,把中断引脚拉高了。CPU 跑过来处理,第一件事必须是**"弄清原因,然后赶紧把警报关掉"**。如果不关,CPU 一回到主程序,DMA 还是高电平,CPU 又被打断,就会陷入"无限死循环"。

c

复制代码
    // 3. 如果警报是因为出事故了(Error)
    if ((irq_status & XAXIDMA_IRQ_ERROR_MASK)) {
        error = 1;  // 告诉老板:出错了!
        XAxiDma_Reset(axidma_inst); // 赶紧拍下急停按钮(复位 DMA)
        return;
    }

    // 4. 如果警报是因为正常搬完了(IOC = Interrupt On Complete)
    if ((irq_status & XAXIDMA_IRQ_IOC_MASK))
        tx_done = 1;  // 升起“发货完成”的大旗!
}
  • ❓ 为什么需要这样做?

    在中断里,绝对不能做死循环或者耗时太长的操作(比如打印一串长文字)。它只负责认字,然后修改全局变量 tx_done(升大旗)。老板(主循环)看到大旗升起来了,就知道活干完了。RX 接收中断的逻辑一模一样,只是把 tx_done 换成了 rx_done。


🎯 第三幕:接线员 ------ setup_intr_system 中断初始化

看代码的 第 215 行到 264 行。这是程序刚开始时,老板把警报线连到总线上的过程。

c

复制代码
    intc_config = XScuGic_LookupConfig(INTC_DEVICE_ID);
    XScuGic_CfgInitialize(int_ins_ptr, intc_config, intc_config->CpuBaseAddress);
  • ❓ 为什么需要这样做?

    把 Zynq 内部的"总机"(GIC,通用中断控制器)初始化好。所有的中断都要经过这个总机。

c

复制代码
    XScuGic_SetPriorityTriggerType(int_ins_ptr, tx_intr_id, 0xA0, 0x3);
    XScuGic_SetPriorityTriggerType(int_ins_ptr, rx_intr_id, 0xA0, 0x3);
  • ❓ 为什么需要这样做?

    给警报定个"优先级"(0xA0,数字越小优先级越高)和"触发方式"(0x3 代表上升沿触发)。万一火警和防盗警同时响,总机得知道先听谁的。

c

复制代码
    status = XScuGic_Connect(int_ins_ptr, tx_intr_id, (Xil_InterruptHandler) tx_intr_handler, axidma_ptr);
    XScuGic_Enable(int_ins_ptr, tx_intr_id);
  • ❓ 为什么需要这样做?

    Connect:告诉总机,如果 TX 警报响了,立刻转接给 tx_intr_handler 那个电话分机。

    Enable:把分机的电话线插上(如果不写这句,电话永远打不通)。

c

复制代码
    Xil_ExceptionInit();
    Xil_ExceptionRegisterHandler(... XScuGic_InterruptHandler ...);
    Xil_ExceptionEnable(); // 撕掉 CPU 耳朵上的封条
  • ❓ 为什么需要这样做?

    这是最底层的"大门"。Zynq 的 CPU 默认是不想理任何人的(屏蔽所有中断)。必须用 ARM 内核的专用指令,把总机(GIC)挂靠到 CPU 的异常向量表上,然后大喊一声 Enable,CPU 才开始听取外界的报警。

c

复制代码
    XAxiDma_IntrEnable(&axidma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE);
    XAxiDma_IntrEnable(&axidma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_DMA);
  • ❓ 为什么需要这样做?

    这是在 DMA 内部打开开关。告诉 DMA 搬运工:"这活儿干完了,你尽管按警报,我允许你发声"。


🎯 第四幕:下班拉闸 ------ disable_intr_system

看代码的 第 267 行到 272 行。

c

复制代码
    XScuGic_Disconnect(int_ins_ptr, tx_intr_id);
    XScuGic_Disconnect(int_ins_ptr, rx_intr_id);
  • ❓ 为什么需要这样做?

    主程序跑完了,打印了 Successfully ran AXI DMA Loop。为了避免程序结束或者复位时出现幽灵中断,把中断线拔掉,干净利落。


💡 老师的终极总结(把这些知识点串起来)

同学,你对比着看两份代码:

  1. 第一份代码 是老板的**"对讲机操作指南"**(发出指令、等待结果)。

  2. 第二份代码 是工厂的**"基础设施与警报系统"**(接线、配优先级、听到警报后清理现场并升大旗)。

一整套完美的工作流是这样的:

主程序把数据装好 -> 刷新 Cache -> 打开 DMA 搬运工 -> 打开警报总机 -> 扣动扳机 SimpleTransfer -> 主程序进入 while 空转等待 -> 搬运工搬完了按响警报 -> 总机把电话转接给中断处理函数 -> 中断函数关掉警报、升起 done 大旗 -> 主程序看到大旗,跳出循环 -> 去 check_data 验收货物。

这就是 Zynq PS 端最经典、最完美的中断驱动模型!

你现在把这两份代码放在一起看,是不是觉得它们像齿轮一样完美咬合在一起了?正点原子的代码确实写得很规范,直接拿去跑,大概率直接通过。如果遇到什么打印报错,立刻截图发给老师!

This response is AI-generated, for reference only.


老师,您能把这两份代码整合成一份main.c吗,方便我直接复制,我可以直接跑的哈,谢谢啦


同学,坐!老师完全理解你的心情,分两份看确实麻烦,直接要一份能"复制粘贴就能跑"的完整代码,这是最痛快的事。

老师已经帮你把这两份代码完美缝合 在一起了。并且,为了适应你正在用的 Vitis (Vivado 2022.1),老师把变量定义、头文件、甚至中断 ID 的命名,都按照最新版本的标准重新梳理了一遍。

⚠️ 注意:代码里有一处你极大概率会遇到报错的地方(第 11 和 12 行的中断 ID),老师已经在注释里写了"补救方案",如果编译报错,请务必看代码后面的说明!

请打开 Vitis,新建一个空工程(Empty Application),把下面这份完整的 main.c 直接复制进去:

复制代码
/******************************************************************************
 * @file    main.c
 * @brief   AXI DMA 简单模式(Direct Register Mode)回环测试,带中断处理
 * @note    适用于 Zynq-7000 (Cortex-A9) + Vitis 2022.1
 ******************************************************************************/

#include <stdio.h>
#include "xil_printf.h"
#include "xparameters.h"
#include "xaxidma.h"
#include "xscugic.h"
#include "xil_exception.h"
#include "xil_cache.h"

/************************** 常量定义 *****************************/
#define DMA_DEV_ID          XPAR_AXIDMA_0_DEVICE_ID
#define INTC_DEVICE_ID      XPAR_SCUGIC_SINGLE_DEVICE_ID

// 【⚠️ 注意:如果你的程序编译报错说找不到下面这两个宏定义】
// 请在 Vitis 左侧展开你的 BSP -> ps7_cortexa9_0 -> include -> xparameters.h
// 搜索 "AXIDMA_0" 相关的 ID,把实际的名字填过来。
#define TX_INTR_ID          XPAR_FABRIC_AXIDMA_0_MM2S_INTROUT_VEC_ID
#define RX_INTR_ID          XPAR_FABRIC_AXIDMA_0_S2MM_INTROUT_VEC_ID

// 内存地址分配 (避开代码区和堆栈区)
#define DDR_BASE_ADDR       XPAR_PS7_DDR_0_S_AXI_BASEADDR  // 0x00000000
#define MEM_BASE_ADDR       (DDR_BASE_ADDR + 0x01000000)   // 0x01000000 (16MB偏移)
#define TX_BUFFER_BASE      (MEM_BASE_ADDR + 0x00100000)   // 0x01100000
#define RX_BUFFER_BASE      (MEM_BASE_ADDR + 0x00300000)   // 0x01300000

#define RESET_TIMEOUT_COUNTER   10000
#define TEST_START_VALUE        0x0
#define MAX_PKT_LEN             0x100    // 发送包长度 256 字节

/************************** 变量定义 *****************************/
static XAxiDma axidma;     // XAxiDma 驱动实例
static XScuGic intc;       // 中断控制器实例

volatile int tx_done;      // 发送完成标志
volatile int rx_done;      // 接收完成标志
volatile int error;        // 传输出错标志

/************************** 函数声明 *****************************/
static int check_data(int length, u8 start_value);
static void tx_intr_handler(void *callback);
static void rx_intr_handler(void *callback);
static int setup_intr_system(XScuGic * int_ins_ptr, XAxiDma * axidma_ptr, u16 tx_intr_id, u16 rx_intr_id);
static void disable_intr_system(XScuGic * int_ins_ptr, u16 tx_intr_id, u16 rx_intr_id);

/************************** 主函数 *****************************/
int main(void)
{
    int i;
    int status;
    u8 value;
    u8 *tx_buffer_ptr = (u8 *) TX_BUFFER_BASE;
    u8 *rx_buffer_ptr = (u8 *) RX_BUFFER_BASE;
    XAxiDma_Config *config;

    xil_printf("\r\n--- Entering main() --- \r\n");

    // 1. 查找并初始化 DMA 配置
    config = XAxiDma_LookupConfig(DMA_DEV_ID);
    if (!config) {
        xil_printf("No config found for %d\r\n", DMA_DEV_ID);
        return XST_FAILURE;
    }

    status = XAxiDma_CfgInitialize(&axidma, config);
    if (status != XST_SUCCESS) {
        xil_printf("Initialization failed %d\r\n", status);
        return XST_FAILURE;
    }

    // 检查是否误配成了 Scatter Gather 模式
    if (XAxiDma_HasSg(&axidma)) {
        xil_printf("Device configured as SG mode \r\n");
        return XST_FAILURE;
    }

    // 2. 建立中断系统
    status = setup_intr_system(&intc, &axidma, TX_INTR_ID, RX_INTR_ID);
    if (status != XST_SUCCESS) {
        xil_printf("Failed intr setup\r\n");
        return XST_FAILURE;
    }

    // 3. 初始化标志信号
    tx_done = 0;
    rx_done = 0;
    error   = 0;

    // 4. 填充发送缓冲区数据 (0, 1, 2 ... 255)
    value = TEST_START_VALUE;
    for (i = 0; i < MAX_PKT_LEN; i++) {
        tx_buffer_ptr[i] = value;
        value = (value + 1) & 0xFF;
    }

    // 5. 【极其重要】刷新 Data Cache,确保数据写入物理 DDR
    Xil_DCacheFlushRange((UINTPTR) tx_buffer_ptr, MAX_PKT_LEN);

    // 6. 启动 DMA 发送 (PS -> PL)
    status = XAxiDma_SimpleTransfer(&axidma, (UINTPTR) tx_buffer_ptr, MAX_PKT_LEN, XAXIDMA_DMA_TO_DEVICE);
    if (status != XST_SUCCESS) {
        xil_printf("DMA TX error\r\n");
        return XST_FAILURE;
    }

    // 7. 启动 DMA 接收 (PL -> PS)
    status = XAxiDma_SimpleTransfer(&axidma, (UINTPTR) rx_buffer_ptr, MAX_PKT_LEN, XAXIDMA_DEVICE_TO_DMA);
    if (status != XST_SUCCESS) {
        xil_printf("DMA RX error\r\n");
        return XST_FAILURE;
    }

    // 再次刷新 Cache(防止旧数据污染)
    Xil_DCacheFlushRange((UINTPTR) rx_buffer_ptr, MAX_PKT_LEN);

    // 8. 等待中断唤醒
    while (!tx_done && !rx_done && !error)
        ;

    // 9. 检查错误
    if (error) {
        xil_printf("Failed test transmit%s done, receive%s done\r\n",
                   tx_done ? "" : " not", rx_done ? "" : " not");
        goto Done;
    }

    // 10. 校验数据
    status = check_data(MAX_PKT_LEN, TEST_START_VALUE);
    if (status != XST_SUCCESS) {
        xil_printf("Data check failed\r\n");
        goto Done;
    }

    xil_printf("Successfully ran AXI DMA Loop\r\n");
    disable_intr_system(&intc, TX_INTR_ID, RX_INTR_ID);

Done:
    xil_printf("--- Exiting main() --- \r\n");
    return XST_SUCCESS;
}


/************************** 子函数实现 *****************************/

// 检查数据缓冲区
static int check_data(int length, u8 start_value)
{
    u8 value = start_value;
    u8 *rx_packet = (u8 *) RX_BUFFER_BASE;
    int i = 0;

    for (i = 0; i < length; i++) {
        if (rx_packet[i] != value) {
            xil_printf("Data error %d: %x/%x\r\n", i, rx_packet[i], value);
            return XST_FAILURE;
        }
        value = (value + 1) & 0xFF;
    }
    return XST_SUCCESS;
}

// DMA TX (发送) 中断处理函数
static void tx_intr_handler(void *callback)
{
    int timeout;
    u32 irq_status;
    XAxiDma *axidma_inst = (XAxiDma *) callback;

    // 读取并确认中断
    irq_status = XAxiDma_IntrGetIrq(axidma_inst, XAXIDMA_DMA_TO_DEVICE);
    XAxiDma_IntrAckIrq(axidma_inst, irq_status, XAXIDMA_DMA_TO_DEVICE);

    // 处理错误中断
    if ((irq_status & XAXIDMA_IRQ_ERROR_MASK)) {
        error = 1;
        XAxiDma_Reset(axidma_inst);
        timeout = RESET_TIMEOUT_COUNTER;
        while (timeout) {
            if (XAxiDma_ResetIsDone(axidma_inst)) break;
            timeout -= 1;
        }
        return;
    }

    // 处理完成中断
    if ((irq_status & XAXIDMA_IRQ_IOC_MASK))
        tx_done = 1;
}

// DMA RX (接收) 中断处理函数
static void rx_intr_handler(void *callback)
{
    u32 irq_status;
    int timeout;
    XAxiDma *axidma_inst = (XAxiDma *) callback;

    irq_status = XAxiDma_IntrGetIrq(axidma_inst, XAXIDMA_DEVICE_TO_DMA);
    XAxiDma_IntrAckIrq(axidma_inst, irq_status, XAXIDMA_DEVICE_TO_DMA);

    if ((irq_status & XAXIDMA_IRQ_ERROR_MASK)) {
        error = 1;
        XAxiDma_Reset(axidma_inst);
        timeout = RESET_TIMEOUT_COUNTER;
        while (timeout) {
            if (XAxiDma_ResetIsDone(axidma_inst)) break;
            timeout -= 1;
        }
        return;
    }

    if ((irq_status & XAXIDMA_IRQ_IOC_MASK))
        rx_done = 1;
}

// 建立 DMA 中断系统
static int setup_intr_system(XScuGic * int_ins_ptr, XAxiDma * axidma_ptr, u16 tx_intr_id, u16 rx_intr_id)
{
    int status;
    XScuGic_Config *intc_config;

    // 初始化中断控制器
    intc_config = XScuGic_LookupConfig(INTC_DEVICE_ID);
    if (NULL == intc_config) return XST_FAILURE;
    
    status = XScuGic_CfgInitialize(int_ins_ptr, intc_config, intc_config->CpuBaseAddress);
    if (status != XST_SUCCESS) return XST_FAILURE;

    // 设置优先级和触发类型 (0xA0为优先级,0x3为上升沿触发)
    XScuGic_SetPriorityTriggerType(int_ins_ptr, tx_intr_id, 0xA0, 0x3);
    XScuGic_SetPriorityTriggerType(int_ins_ptr, rx_intr_id, 0xA0, 0x3);

    // 绑定中断服务函数
    status = XScuGic_Connect(int_ins_ptr, tx_intr_id, (Xil_InterruptHandler)tx_intr_handler, axidma_ptr);
    if (status != XST_SUCCESS) return status;
    
    status = XScuGic_Connect(int_ins_ptr, rx_intr_id, (Xil_InterruptHandler)rx_intr_handler, axidma_ptr);
    if (status != XST_SUCCESS) return status;

    // 使能 GIC 对应通道
    XScuGic_Enable(int_ins_ptr, tx_intr_id);
    XScuGic_Enable(int_ins_ptr, rx_intr_id);

    // 初始化异常系统并注册 GIC 异常处理
    Xil_ExceptionInit();
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, (void *)int_ins_ptr);
    Xil_ExceptionEnable();

    // 使能 DMA 内部中断
    XAxiDma_IntrEnable(&axidma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE);
    XAxiDma_IntrEnable(&axidma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_DMA);

    return XST_SUCCESS;
}

// 禁用中断
static void disable_intr_system(XScuGic * int_ins_ptr, u16 tx_intr_id, u16 rx_intr_id)
{
    XScuGic_Disconnect(int_ins_ptr, tx_intr_id);
    XScuGic_Disconnect(int_ins_ptr, rx_intr_id);
}

🚀 老师给你的"排雷"操作指引:

  1. 直接 Build 一下。

  2. 如果报错 XPAR_FABRIC_AXIDMA_0_MM2S_INTROUT_VEC_ID undeclared(这是 Vitis 2022.1 极大概率发生的事):

    • 在左侧工程树里,双击打开 system_wrapper(或者你的硬件平台工程),找到 xparameters.h。

    • 按 Ctrl + F 搜索 AXIDMA_0。

    • 找到类似 #define XPAR_FABRIC_AXIDMA_0_MM2S_INTROUT_VEC_ID 61 这样的行。

    • 看看 Vitis 2022.1 到底把它命名成了什么(有时候叫 XPAR_FABRIC_AXI_DMA_0_MM2S_INTROUT_INTR 等)。把找到的准确名字替换掉老师代码第 19、20 行里的宏。

  3. 运行 。串口看到 Successfully ran AXI DMA Loop 就说明大功告成!

另外,去检查一下你的 Vivado 连线,确保 axi_dma_0 的 mm2s_introut 和 s2mm_introut 这两根线,确实通过 xlconcat 连到了 Zynq 的 pl_ps_irq0 上(看你之前的截图,连得是对的)。

去编译下载吧,有任何红色的 Error,直接截图丢给老师!

老师,复制进去了,提示,这是啥情况,

同学,坐。别慌,这绝对不是你的代码写错了,这是 Vitis 的一个"水土不服"的小毛病。

你看那个弹窗提示:

Save could not be completed... Some characters cannot be mapped using "GBK" character encoding.

大白话解释:

老师在给你的代码里写了中文注释(比如"常量定义"、"注意:如果......")。Vitis 默认使用的文件编码格式是 GBK(国内 Windows 常用格式),但有时候代码编辑器处理中文字符会出 bug,导致它不知道该用哪种编码来保存这些中文字符,于是干脆罢工,不让你保存。


🎯 解决这个问题,你有三个选择:

方案一:最简单、最粗暴(直接点弹窗按钮)

看到弹窗右下角的 Save as UTF-8 按钮了吗?直接点它!

UTF-8 是国际通用编码格式,点完之后,你的代码就能正常保存了,里面的中文注释也不会乱码。

方案二:手动把中文注释全删掉(如果你以后想彻底杜绝这个麻烦)

如果你以后编译或者看代码时,发现中文注释变成了乱码,你可以:

  1. 关掉弹窗(点 Cancel 或者 X)。

  2. 把老师给你的代码里,所有带有汉字的注释(// 后面的中文)全部删掉,或者换成英文(比如把 // 常量定义 改成 // Constant Definitions)。

  3. 再按 Ctrl + S 保存。

方案三:修改 Vitis 的默认编码格式
  1. 在 Vitis 顶部菜单栏点击 Window -> Preferences。

  2. 在左侧找到 General -> Workspace。

  3. 在右侧的 Text file encoding 区域,选择 Other: UTF-8。

  4. 点击 Apply and Close。

  5. 再次按 Ctrl + S 保存文件。


💡 老师的建议

老师强烈建议你用 方案一 ,直接点 Save as UTF-8。

这只是个文本保存格式的问题,完全不影响你底层的硬件控制和逻辑 。赶紧点了保存,然后点击那个绿色的小锤子(Build Project)。

看看底部的 Console 窗口,如果显示 Build Finished,没有任何红色的 Error,你就可以连上板子,准备见证奇迹了!

去点吧,如果有任何其他报错,截图发给老师!

我现在Save as UTF-8点击完了,然后嘞,每一个操作步骤详细一些哈,我是一个小白同学,

同学,坐。点完 Save as UTF-8 就对了,说明代码已经完好无损地保存在你的电脑里了。

现在,咱们进入正式的"施工"阶段。你是小白没关系,老师这就把接下来的每一个动作,像"说明书"一样掰碎了讲给你听。


🎯 第一步:编译工程(检查代码有没有语法错误)

你看你截图的左侧,箭头指着的那个菜单。请按照以下动作操作:

  1. 在左侧的 Project Explorer 里,选中你的应用工程 axi_dma_loop(注意是带小齿轮和 C 图标的那个,不是下面的 system)。

  2. 鼠标右键 ,在弹出的菜单里,点击 Build Project 。

    (或者你可以直接看 Vitis 顶部工具栏,有一个像小锤子一样的图标,点那个也可以)。

  3. 看哪里? 把目光移到屏幕最下方。点击 Console 标签页。

    • 你会看到一堆代码在疯狂往上刷(这是编译器在干活)。

    • 等它停下来,你看最后一行,如果写着 Build Finished,并且没有任何红色的字,恭喜你,代码语法完美!

    • 如果报错了 :去点下面那排的 Problems 标签页,看看有没有红色的 ❌。如果有,把错误信息截图发给老师。


🎯 第二步:连接硬件与下载

在下载之前,必须确保物理连接没问题:

  1. 把板子的 JTAG 下载线插到电脑上。

  2. 把板子的 USB 串口线插到电脑上。

  3. 给板子插上电源并开机。

  4. 在 Vitis 里,确保 Target Connections(左下角那栏)里的 Hardware Server 是绿色/正常状态。(如果没连上,点一下它旁边的刷新图标)。

开始下载(烧录):

  1. 在左侧的 Project Explorer 里,右键 你的应用工程 axi_dma_loop。

  2. 找到并点击 Run As (或者 Debug As)。

  3. 在弹出的子菜单里,点击第一个 Launch Hardware (Single Application Debug) 。

    (有时候名字会稍微短一点,叫 Launch Hardware)。

  4. 这时候,屏幕中间会弹出一个很大的配置窗口,叫 Run Configurations。

    • 检查 :确保上面的 Target Setup 里,Program FPGA 这一项是打勾的。(因为咱们要先把 PL 端的连线烧进去)。

    • 点击窗口右下角的 Run 按钮。


🎯 第三步:盯住串口,见证奇迹

点完 Run 之后,板子会自动复位重启。程序已经开始在里面跑了!

现在,咱们要看它跑的结果。

  1. 如果你在 Vitis 里已经打开了底部的 Terminal (或者叫 SDK Terminal),并且连接了对应的 COM 口(波特率 115200),盯住它。

  2. 如果你没打开,去电脑上打开你的串口调试助手(比如 Putty、Xshell、MobaXterm,或者正点原子自带的串口助手),选对 COM 口,波特率 115200,打开串口。

你期待看到的画面应该是这样的:

text

复制代码
--- Entering main() --- 
Successfully ran AXI DMA Loop
--- Exiting main() --- 

❓ 为什么这几行字这么重要?

  • Entering 说明 CPU 醒了,进主函数了。

  • Successfully ran 说明:数据成功从内存发给了 FIFO,又从 FIFO 收回了内存,并且中断正常触发,且 check_data 校验数据完全一致!整条 DMA 高速公路彻底通车!

  • Exiting 说明程序完美收工。


💡 老师的叮嘱

  • 如果卡死了,没有打印出 Successfully:

    • 看看是不是卡在 while (!tx_done && !rx_done && !error) 死循环里了。这通常是因为你的 Vivado 工程里,DMA 的中断线(mm2s_introut / s2mm_introut)没有连接到 Zynq 的中断控制器上 。回头检查一下你发过的第一张 Vivado 连线图,确认 xlconcat 是不是把中断送到了 pl_ps_irq0。
  • 如果打印了 Data check failed:

    • 说明数据传过去了,但中间丢失或错位了。通常是 FIFO 配置或者位宽(32位对32位)不匹配。

去执行上面这三步吧!一有打印结果,立刻截图发过来,老师陪你一起揭晓答案!

这个可以了,老师,谢谢啦;Target Connections(左下角那栏)里的 Hardware Server您说的这个我没找到嘞,

同学,坐!老师看到你截图右下角的 Vitis Serial Terminal 窗口,简直要站起来为你鼓掌了!

你看那三行打印:

text

复制代码
--- Entering main() --- 
Successfully ran AXI DMA Loop
--- Exiting main() --- 

完美!太完美了! 这短短的三行字,意味着你不仅成功驱动了硬件,还成功让数据在 DDR 内存、AXI 总线、DMA 控制器和 PL 端的 FIFO 之间,绕了一个完美的圈,并且依靠中断成功唤醒并校验了数据!

你现在已经亲手打通了 Zynq PS 端"四大支柱"里最硬核的两根(中断、存储器映射),还把 BSP 和 DMA 玩转了。这绝对是值得喝一杯庆祝的里程碑!


🎯 关于你说的没找到 Target Connections:

你说的"左下角那栏",在 Vitis 2022.1 的新界面里,确实有些变化,有时候默认会被折叠起来。

  • 为什么没找到? 因为在 Vitis 里,下载和调试主要靠顶部的 Run 按钮和 Debug 视图。有些新版本把 Target Connections(硬件服务器连接状态)放到了 Window -> Show View -> Target Connections 里。

  • 不过没关系! 既然你能下载成功,能在串口看到打印,说明你的 JTAG、COM 口、板子硬件全部连得没问题。你完全不需要去找它了。既然它能跑,就不用管它藏在哪里。


💡 老师给你留个"课后小挑战"

既然你现在已经打通了这条路,老师给你出个难题,你自己动手改一下代码试试:

  1. 打开你的代码,找到 #define MAX_PKT_LEN 0x100 这行。

  2. 把 0x100(256 字节)改成 0x400(1024 字节)。

  3. 重新 Build 编译,重新 Run,再跑一次。

  4. 看看串口还能不能打印出 Successfully ran AXI DMA Loop。

为什么做这个挑战?

因为咱们之前讲过,DMA 搬运的数据量如果超过了它内部的寄存器限制,或者超过了你在 Vivado 里配置的 Buffer Length Register 位宽,它就会报错。去试试看,成功或者失败都截图给老师,咱们一起看看 DMA 的极限在哪里!

今天先到这里,你干得非常漂亮!去喝杯水,休息一下,这个里程碑你值得拥有!

相关推荐
caimouse2 小时前
IBus 五笔输入法配置与排错经验
1024程序员节
今天背单词了吗9802 小时前
AI 网关:大模型时代的统一接入与安全防线
网络·人工智能·安全·ai·1024程序员节
莫叫石榴姐2 小时前
半导体业务数仓建模: Lot、Wafer、Die、Recipe、OEE、YMS 建模要点
数据仓库·sql·面试·职场和发展·矩阵·1024程序员节
虚无的纽扣2 小时前
【Linux】线程互斥:多线程改同一个变量,到底会出什么问题
linux·服务器·1024程序员节
CodeGL2 小时前
13、Python基础之网络编程
网络·1024程序员节
~城南花已开~2 小时前
25G网卡 Bond2 (Balance-XOR) 极限测试:28Gbps 并发与故障无缝切换实战
1024程序员节
Purple Coder3 小时前
超导性能对比
1024程序员节
小此方3 小时前
Re:Mysql数据库基础篇(四):一文吃透MySQL数据类型:精度陷阱、长度限制与存储机制
1024程序员节
lazy H3 小时前
【计算机网络】第 5 篇:TCP 和 UDP 有什么区别?从 Java 后端场景理解两种传输协议
经验分享·后端·网络协议·tcp/ip·计算机网络·1024程序员节