
1. 项目缘起与整体设计思路1.1 为什么选XDMA做PCIe数据通路搞FPGA加速卡或者数据采集卡的朋友绕不开的一个话题就是主机和FPGA之间怎么高速传数据。早期大家用PCIe硬核自己写TLP收发或者用Xilinx的PCIe Gen2软核代码量大、调试周期长一个LTSSM状态机就能耗掉两周。后来Xilinx推出了XDMADMA Subsystem for PCIe把PCIe控制器、DMA引擎、AXI接口桥接全部打包成一个IP核配置几下就能跑起来省掉了大量底层协议栈的活。我这次的项目背景是一块Kintex-7 FPGA板卡需要把采集到的数据通过PCIe Gen2 x4传到上位机同时上位机要能通过寄存器读写控制FPGA内部逻辑。选XDMA的核心理由有三点第一它原生支持AXI4和AXI4-Lite两套接口AXI4走DMA大流量数据AXI4-Lite走寄存器控制分工明确第二XDMA的驱动在Linux内核里已经有成熟的xdma.ko不用自己从零写字符设备驱动第三BAR映射空间灵活可以按需分配不会浪费地址资源。注意XDMA IP核在Vivado中免费提供但生成bitstream后需要配合驱动使用。如果你用的是Windows平台Xilinx也提供了对应的WDF驱动但Linux下的生态更成熟调试工具也更丰富。1.2 整体架构拆解整个系统的数据流是这样的上位机通过PCIe总线发起DMA读或DMA写请求XDMA IP核收到请求后把PCIe TLP转换成AXI4事务通过AXI4-Stream或者AXI4-MM接口访问FPGA内部的BRAM或DDR。反过来FPGA内部逻辑也可以通过AXI4-Lite接口被上位机读写寄存器实现控制通道。这里的关键在于BAR空间的划分。XDMA通常需要三个BARBAR0用于DMA寄存器映射BAR1用于AXI4-Lite主机接口也就是用户寄存器BAR2用于DMA旁路或者MSI-X中断表。实际项目中我把BAR0配成128KBBAR1配成1MBBAR2配成64KB。为什么这么分BAR0的DMA寄存器其实只用到几KB但留大一点方便后续扩展BAR1的1MB是为了映射足够多的用户寄存器比如多通道ADC的配置寄存器、状态寄存器、FIFO水位寄存器等BAR2的64KB是给中断向量表留的余量。1.3 方案选型的几个取舍有人会问为什么不直接用AXI4-MM做所有事情非要分AXI4和AXI4-Lite这里涉及一个带宽和延迟的权衡。AXI4-MM接口支持突发传输适合大块数据搬运但它的协议开销大每次传输都要握手、地址对齐、突发长度协商。AXI4-Lite则是单次传输协议简单延迟低适合寄存器读写这种小数据量操作。XDMA把两者分开DMA引擎走AXI4用户逻辑走AXI4-Lite互不干扰效率最高。另一个取舍是中断方式。XDMA支持MSI、MSI-X和Legacy中断。MSI-X最灵活支持多个中断向量适合多通道场景。但MSI-X需要BAR2空间来存放中断表而且驱动要正确配置。我一开始图省事用了Legacy中断结果发现中断延迟大而且多个中断源会共享一个IRQ号驱动里还得做中断源判别。后来换成MSI-X每个通道独立中断驱动里直接按向量号处理清爽很多。2. BAR映射与地址空间配置细节2.1 BAR空间的基本概念BAR全称Base Address Register是PCIe配置空间里的一个寄存器用来告诉主机这段地址空间有多大、是什么类型内存还是IO、是否可预取。主机在枚举PCIe设备时会读取BAR的初始值然后写入全1再读回来根据返回的0的个数判断空间大小。比如一个BAR返回0xFFFFF000说明低12位是0空间大小就是4KB。XDMA IP核在Vivado里配置时会让你指定每个BAR的尺寸和类型。这里有个坑BAR的尺寸必须是2的幂次方而且不能小于4KB。我见过有人想配个3KB的BAR结果Vivado直接报错。另外BAR0通常用来映射DMA的控制寄存器和状态寄存器这些寄存器在XDMA内部有固定的偏移地址不能随意改动。2.2 XDMA的BAR分配策略在Vivado的XDMA配置界面里PCIe BARs选项卡下有几个关键设置BAR用途建议尺寸类型BAR0DMA寄存器128KBMemory, 32-bit, Non-prefetchableBAR1AXI4-Lite主机接口1MBMemory, 32-bit, Non-prefetchableBAR2MSI-X中断表64KBMemory, 32-bit, Non-prefetchableBAR0的128KB其实大部分是保留的实际用到的只有前几KB。但为什么不留小一点因为XDMA的DMA寄存器在BAR0里的偏移是固定的比如0x0000是H2C Channel 0的控制寄存器0x1000是C2H Channel 0的状态寄存器。如果你把BAR0配成4KB这些寄存器就放不下了。所以128KB是个保险值。BAR1的1MB是给用户逻辑用的。在XDMA的AXI4-Lite主机接口上你可以挂载自己的寄存器组。比如我挂了16个通道的ADC配置寄存器每个通道32个寄存器每个寄存器4字节总共2KB。1MB的空间绰绰有余后续加通道也不用改BAR。BAR2的64KB是给MSI-X用的。MSI-X中断表每个条目16字节64KB可以放4096个条目远远够用。但实际项目中我建议BAR2不要配太大因为有些主机的PCIe根复合体对BAR空间有限制配太大可能导致枚举失败。2.3 地址映射的实操步骤在Linux下BAR空间的映射是通过sysfs或者直接mmap /dev/mem实现的。XDMA驱动加载后会在/sys/class/xdma/下生成设备节点。你可以用lspci -vv查看BAR的分配情况lspci -vv -s 01:00.0 | grep -A 10 Region输出类似Region 0: Memory at f7c00000 (32-bit, non-prefetchable) [size128K] Region 1: Memory at f7c20000 (32-bit, non-prefetchable) [size1M] Region 2: Memory at f7c40000 (32-bit, non-prefetchable) [size64K]这里f7c00000就是BAR0的物理地址f7c20000是BAR1的物理地址。在用户态程序里你可以用mmap把这段物理地址映射到虚拟地址空间int fd open(/dev/mem, O_RDWR | O_SYNC); void *bar1 mmap(NULL, 0x100000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0xf7c20000);然后就可以像访问普通内存一样读写寄存器了uint32_t val *((volatile uint32_t *)(bar1 0x100)); *((volatile uint32_t *)(bar1 0x104)) 0xDEADBEEF;注意/dev/mem的访问需要root权限而且有些内核配置了CONFIG_STRICT_DEVMEM会限制对PCIe BAR的mmap。如果遇到mmap返回-EPERM需要检查内核配置或者改用XDMA驱动提供的字符设备接口。2.4 BAR映射的常见坑第一个坑是地址对齐。BAR的物理地址必须是BAR尺寸的整数倍。比如BAR1是1MB那么它的物理地址必须是1MB对齐的。如果主机枚举时分配了一个非对齐的地址XDMA会工作不正常。这种情况通常出现在主机BIOS的PCIe资源分配策略比较保守的时候。解决办法是在BIOS里开启Above 4G Decoding让主机有更多的地址空间可以分配。第二个坑是BAR空间冲突。如果主板上插了多块FPGA板卡每块板卡的BAR空间不能重叠。PCIe枚举时主机会自动分配不重叠的地址但如果BIOS的枚举算法有bug可能会分配重叠的地址。这时候需要手动在BIOS里调整或者用setpci工具重新分配。第三个坑是BAR的预取属性。XDMA的BAR通常配成Non-prefetchable因为DMA寄存器的读操作有副作用比如读状态寄存器会清除中断标志不能预取。如果你不小心配成了Prefetchable主机可能会提前读取寄存器导致状态丢失。3. AXI4-Lite主机接口配置与寄存器设计3.1 AXI4-Lite接口的信号定义XDMA的AXI4-Lite主机接口M_AXI_LITE是一组标准的AXI4-Lite信号包括AWADDR写地址通常32位AWVALID/AWREADY写地址握手WDATA写数据32位WSTRB写字节选通4位WVALID/WREADY写数据握手BRESP写响应2位ARADDR读地址ARVALID/ARREADY读地址握手RDATA读数据RRESP读响应RVALID/RREADY读数据握手在Vivado里XDMA的M_AXI_LITE接口可以直接连接到你的用户逻辑。如果你用的是MicroBlaze或者Zynq的PS端也可以挂到AXI Interconnect上。3.2 用户寄存器的地址分配在XDMA的AXI4-Lite接口上地址空间是1MB对应BAR1。我通常把地址空间分成几个区域地址范围用途说明0x00000-0x00FFF全局控制寄存器使能、复位、版本号0x01000-0x01FFF通道0寄存器ADC配置、状态、FIFO0x02000-0x02FFF通道1寄存器同上.........0x0F000-0x0FFFF中断控制寄存器MSI-X向量配置每个通道的寄存器组里我定义了以下寄存器// 通道0寄存器定义 localparam CH0_CTRL 12h000; // 控制寄存器 localparam CH0_STATUS 12h004; // 状态寄存器 localparam CH0_ADC_CFG 12h008; // ADC配置 localparam CH0_FIFO_LEVEL 12h00C; // FIFO水位 localparam CH0_DATA 12h010; // 数据寄存器在Verilog里AXI4-Lite从机的实现可以用Xilinx提供的AXI4-Lite IP模板也可以自己写状态机。我倾向于自己写因为更灵活而且代码量不大。3.3 AXI4-Lite从机的Verilog实现下面是一个简化的AXI4-Lite从机实现支持读写寄存器module axi_lite_slave #( parameter ADDR_WIDTH 20, parameter DATA_WIDTH 32 )( input wire aclk, input wire aresetn, // 写地址通道 input wire [ADDR_WIDTH-1:0] awaddr, input wire awvalid, output reg awready, // 写数据通道 input wire [DATA_WIDTH-1:0] wdata, input wire [DATA_WIDTH/8-1:0] wstrb, input wire wvalid, output reg wready, // 写响应通道 output reg [1:0] bresp, output reg bvalid, input wire bready, // 读地址通道 input wire [ADDR_WIDTH-1:0] araddr, input wire arvalid, output reg arready, // 读数据通道 output reg [DATA_WIDTH-1:0] rdata, output reg [1:0] rresp, output reg rvalid, input wire rready, // 用户寄存器接口 output reg [31:0] reg_ctrl, output reg [31:0] reg_adc_cfg, input wire [31:0] reg_status, input wire [31:0] reg_fifo_level ); // 写地址握手 always (posedge aclk or negedge aresetn) begin if (!aresetn) begin awready 1b0; end else if (!awready awvalid) begin awready 1b1; end else begin awready 1b0; end end // 写数据握手 always (posedge aclk or negedge aresetn) begin if (!aresetn) begin wready 1b0; end else if (!wready wvalid) begin wready 1b1; end else begin wready 1b0; end end // 写操作 always (posedge aclk or negedge aresetn) begin if (!aresetn) begin reg_ctrl 32h0; reg_adc_cfg 32h0; end else if (awready awvalid wready wvalid) begin case (awaddr[11:0]) 12h000: reg_ctrl wdata; 12h008: reg_adc_cfg wdata; default: ; // 忽略其他地址 endcase end end // 写响应 always (posedge aclk or negedge aresetn) begin if (!aresetn) begin bvalid 1b0; bresp 2b00; end else if (awready awvalid wready wvalid !bvalid) begin bvalid 1b1; bresp 2b00; // OKAY end else if (bvalid bready) begin bvalid 1b0; end end // 读地址握手 always (posedge aclk or negedge aresetn) begin if (!aresetn) begin arready 1b0; end else if (!arready arvalid) begin arready 1b1; end else begin arready 1b0; end end // 读数据 always (posedge aclk or negedge aresetn) begin if (!aresetn) begin rvalid 1b0; rdata 32h0; rresp 2b00; end else if (arready arvalid !rvalid) begin rvalid 1b1; rresp 2b00; case (araddr[11:0]) 12h000: rdata reg_ctrl; 12h004: rdata reg_status; 12h008: rdata reg_adc_cfg; 12h00C: rdata reg_fifo_level; default: rdata 32hDEADBEEF; endcase end else if (rvalid rready) begin rvalid 1b0; end end endmodule这个模块实现了基本的AXI4-Lite读写功能。注意几个细节写地址和写数据是独立握手的但实际写入操作要等两者都握手完成读操作在地址握手后立即返回数据BRESP和RRESP通常返回OKAY2b00除非地址越界才返回SLVERR2b10。3.4 寄存器读写时序的注意事项AXI4-Lite的读写时序有几个容易踩坑的地方。第一AWVALID和WVALID可以同时有效也可以先后有效从机必须等两者都握手才能执行写操作。我见过有人只等AWVALID就写寄存器结果写进去的是旧数据。第二RVALID和RREADY的握手主机在RREADY为高时才能接收数据如果主机一直不拉高RREADY从机不能撤RVALID。第三写响应BRESP必须在写操作完成后才能返回不能提前返回。提示在Vivado里可以用AXI Protocol Checker IP来验证你的AXI4-Lite从机是否符合协议。这个IP会监控所有AXI信号发现违规就报错省去了大量手动检查的时间。4. 实操过程与核心环节实现4.1 Vivado工程搭建与IP配置第一步是创建Vivado工程选好FPGA型号我的是xc7k325tffg900-2。然后添加XDMA IP核在IP Catalog里搜索XDMA就能找到。双击打开配置界面几个关键选项卡需要设置Basic选项卡选择PCIe Gen2 x4参考时钟100MHzAXI时钟125MHz。这里注意AXI时钟频率决定了DMA的吞吐量。125MHz下AXI4数据位宽64位理论带宽是125M * 8 1GB/s实际能跑到800MB/s左右。PCIe BARs选项卡按前面说的BAR0128KBBAR11MBBAR264KB。BAR1的类型选Memory32-bitNon-prefetchable。Misc选项卡勾选AXI4-Lite Master接口这样XDMA才会生成M_AXI_LITE端口。中断方式选MSI-X中断向量数设为16。DMA选项卡使能H2C和C2H通道各设4个通道。每个通道的Buffer长度设为64KB描述符个数设为16。配置完成后Vivado会生成XDMA IP核。接下来需要把M_AXI_LITE接口连接到你的用户逻辑。我通常用一个AXI Interconnect把M_AXI_LITE分成多路分别连接到不同的寄存器组。4.2 约束文件的编写XDMA的约束文件主要涉及PCIe参考时钟、复位信号和AXI时钟。PCIe参考时钟是100MHz差分信号需要约束到专用的时钟引脚create_clock -period 10.000 -name pcie_ref_clk [get_ports pcie_ref_clk_p] set_property PACKAGE_PIN F6 [get_ports pcie_ref_clk_p] set_property IOSTANDARD LVDS [get_ports pcie_ref_clk_p]复位信号通常是低电平有效需要约束到按键或者外部复位芯片set_property PACKAGE_PIN G4 [get_ports sys_rst_n] set_property IOSTANDARD LVCMOS15 [get_ports sys_rst_n]AXI时钟是XDMA内部产生的不需要外部约束但需要在时序约束里声明create_clock -period 8.000 -name axi_aclk [get_pins xdma_inst/axi_aclk]注意PCIe的参考时钟质量直接影响链路稳定性。如果参考时钟抖动太大LTSSM可能卡在Polling状态。建议用专用的时钟芯片不要用FPGA的普通IO输出时钟。4.3 驱动加载与设备识别Vivado生成bitstream后下载到FPGA然后在Linux下加载XDMA驱动。Xilinx的XDMA驱动源码在GitHub上可以找到编译后生成xdma.ko。加载驱动sudo insmod xdma.ko加载成功后dmesg会输出类似xdma 0000:01:00.0: XDMA driver loaded xdma 0000:01:00.0: BAR0 mapped at f7c00000, size 128K xdma 0000:01:00.0: BAR1 mapped at f7c20000, size 1M xdma 0000:01:00.0: BAR2 mapped at f7c40000, size 64K然后在/dev/下会生成xdma0_c2h_0、xdma0_h2c_0等字符设备节点。用lspci确认设备识别lspci -d 10ee:10ee是Xilinx的Vendor ID。如果能看到设备说明PCIe链路已经建立。4.4 寄存器读写测试用devmem工具可以直接读写BAR1的寄存器# 读版本号寄存器 devmem 0xf7c20000 32 # 写控制寄存器 devmem 0xf7c20000 32 0x00000001如果devmem返回0xDEADBEEF说明读到了默认值寄存器映射正常。如果返回0xFFFFFFFF说明地址映射有问题需要检查BAR1的物理地址是否正确。更可靠的方法是用XDMA驱动提供的ioctl接口。XDMA驱动支持XDMA_IOC_READ和XDMA_IOC_WRITE两个ioctl命令可以在用户态直接读写BAR空间struct xdma_ioctl_data { uint64_t addr; uint32_t data; uint32_t size; }; struct xdma_ioctl_data ioctl_data; ioctl_data.addr 0x0; // BAR1偏移 ioctl_data.size 4; ioctl(fd, XDMA_IOC_READ, ioctl_data); printf(Read: 0x%08X\n, ioctl_data.data);4.5 DMA数据传输测试DMA传输是XDMA的核心功能。H2CHost to Card方向上位机把数据写入FPGAC2HCard to Host方向FPGA把数据传给上位机。测试H2C# 生成1MB测试数据 dd if/dev/urandom oftest.bin bs1M count1 # 通过DMA写入FPGA dd iftest.bin of/dev/xdma0_h2c_0 bs1M count1测试C2H# 从FPGA读取1MB数据 dd if/dev/xdma0_c2h_0 ofrecv.bin bs1M count1 # 比较发送和接收的数据 cmp test.bin recv.bin如果cmp没有输出说明数据一致DMA传输正常。如果cmp报错需要检查DMA通道的配置和FPGA内部的数据通路。提示DMA传输的吞吐量受限于PCIe带宽和AXI总线带宽。Gen2 x4的理论带宽是2GB/s实际能跑到1.6GB/s左右。如果实测只有几百MB/s可能是AXI总线位宽不够或者DMA描述符太少。5. 常见问题与排查技巧实录5.1 PCIe链路建立失败最常见的现象是lspci看不到设备或者dmesg报Link training failed。排查思路现象可能原因解决方法lspci无设备参考时钟未连接检查时钟引脚约束和硬件连接Link training failed复位信号未释放检查sys_rst_n是否拉高LTSSM卡在Polling参考时钟抖动大更换时钟芯片或调整PCB布局LTSSM卡在ConfigurationBAR配置错误检查BAR尺寸是否为2的幂次方我遇到过一次LTSSM卡在Polling状态查了半天发现是参考时钟的差分对极性接反了。PCIe的参考时钟对极性敏感P和N接反会导致链路无法建立。后来在约束文件里加了set_property DIFF_TERM TRUE和set_property IBUF_LOW_PWR FALSE才解决。5.2 BAR空间映射失败如果mmap返回-EPERM或者devmem读到0xFFFFFFFF可能是以下原因第一内核配置了CONFIG_STRICT_DEVMEM。这个选项会限制对PCIe BAR的mmap访问。解决办法是在内核启动参数里加iomemrelaxed或者重新编译内核去掉这个选项。第二BAR的物理地址不对。用lspci -vv确认BAR的物理地址注意有些主机会把BAR映射到64位地址空间这时候需要用mmap64或者指定MAP_32BIT标志。第三BAR空间被其他设备占用。用cat /proc/iomem查看地址分配情况如果发现冲突需要在BIOS里调整PCIe资源分配。5.3 AXI4-Lite读写超时AXI4-Lite读写超时通常表现为驱动报AXI transaction timeout或者FPGA内部逻辑挂死。原因可能是AXI4-Lite从机没有正确握手。检查AWREADY、WREADY、ARREADY是否在正确的时候拉高。地址越界。如果访问了未定义的地址从机可能不返回响应导致主机一直等待。时钟域 crossing 问题。如果AXI4-Lite从机在另一个时钟域需要加异步FIFO或者握手同步。我踩过一次坑AXI4-Lite从机的RVALID一直不拉高导致主机读操作超时。后来发现是读数据的状态机在地址握手后没有正确跳转。修复方法是在ARREADY拉高的同时立即准备读数据并在下一个时钟周期拉高RVALID。5.4 DMA传输数据错位DMA传输数据错位通常是因为地址对齐问题。XDMA的DMA引擎要求传输地址和长度都是4字节对齐的。如果传输长度不是4的倍数最后一个TLP会包含填充字节导致数据错位。解决办法是在驱动里做对齐处理或者用XDMA的Descriptor Bypass模式手动指定传输长度。另一个原因是AXI4总线的位宽转换。如果XDMA的AXI4接口是64位而用户逻辑是32位需要加一个位宽转换器。位宽转换器要正确处理小端序和大端序的转换否则数据会高低位颠倒。5.5 中断不触发MSI-X中断不触发先检查BAR2的中断表是否正确初始化。XDMA驱动在加载时会自动配置MSI-X表但需要FPGA逻辑正确响应中断请求。在FPGA侧中断请求信号要连接到XDMA的usr_irq_req端口并且要等待usr_irq_ack应答后才能撤销请求。如果中断触发了但驱动没响应检查/proc/interrupts里对应的IRQ号是否有计数增加。如果没有说明中断没有到达CPU可能是MSI-X向量号配置错误。如果有计数但驱动没处理检查驱动里的中断处理函数是否正确注册。提示调试MSI-X中断时可以用cat /proc/interrupts | grep xdma查看中断计数。如果计数不增加说明中断没有到达如果计数增加但数据没处理说明驱动有问题。5.6 常见问题速查表问题排查命令可能原因解决方案设备不识别lspci -d 10ee:链路未建立检查参考时钟和复位BAR映射失败lspci -vvBAR配置错误检查BAR尺寸和对齐寄存器读写异常devmem地址错误确认BAR物理地址DMA传输慢dd timeAXI位宽不足增加AXI位宽或描述符中断不触发cat /proc/interruptsMSI-X配置错误检查中断表和向量号数据错位cmp地址未对齐4字节对齐传输6. 调试心得与进阶建议6.1 用ILA抓AXI4-Lite波形Vivado的ILAIntegrated Logic Analyzer是调试AXI4-Lite的利器。把ILA核挂到M_AXI_LITE接口上抓取AWADDR、AWVALID、AWREADY、WDATA、WVALID、WREADY、BRESP等信号。触发条件设为AWVALID1 AWREADY1这样每次写操作都会触发。通过观察波形可以确认地址是否正确、数据是否对齐、握手是否正常。我通常会在ILA里同时抓取用户寄存器的值比如reg_ctrl、reg_adc_cfg这样可以直接看到写操作是否生效。如果AWVALID和AWREADY握手了但reg_ctrl没变说明地址译码有问题。6.2 用XDMA的Debug模式XDMA IP核有一个Debug模式可以在Vivado里使能。使能后XDMA会输出一些内部状态信号比如LTSSM状态、DMA通道状态、中断状态等。把这些信号连接到ILA可以实时监控XDMA的工作状态。特别是在调试DMA传输时Debug模式可以看到DMA描述符的读取和写入过程帮助定位描述符错误或者数据通路阻塞。6.3 性能优化建议如果DMA吞吐量不达标可以从以下几个方面优化第一增加AXI4数据位宽。XDMA支持64位、128位、256位AXI4接口。位宽越大单次传输的数据量越大吞吐量越高。但位宽增加会消耗更多的FPGA逻辑资源需要权衡。第二增加DMA描述符个数。描述符越多DMA引擎可以连续处理多个传输请求减少等待时间。但描述符太多会占用更多的BRAM资源。第三使用AXI4-Stream接口。XDMA支持AXI4-Stream接口可以直接把数据流接入FPGA内部的处理流水线省去AXI4-MM的地址握手开销。AXI4-Stream的吞吐量通常比AXI4-MM高20%左右。第四优化PCIe配置。Gen2 x4的理论带宽是2GB/s如果实测只有1GB/s可能是PCIe的Max Payload Size设置太小。在Vivado里把Max Payload Size设为256字节或512字节可以减少TLP开销。6.4 后续扩展方向这个项目的基础框架搭好后可以往几个方向扩展。一是增加DMA通道数支持多路数据同时传输。XDMA最多支持4个H2C和4个C2H通道每个通道独立中断适合多通道数据采集。二是加入DDR缓存把采集到的数据先存入DDR再通过DMA批量传输这样可以平滑数据流避免丢包。三是用XDMA的Descriptor Bypass模式手动控制DMA传输实现更灵活的调度策略。我在实际使用中发现XDMA的默认配置已经能满足大部分场景的需求只有在极高性能或者极低延迟的场景下才需要深度定制。对于大多数项目来说把BAR映射配好、AXI4-Lite寄存器设计好、DMA通道调通就已经完成了80%的工作。剩下的20%是调试和优化这部分最耗时间但也最能积累经验。