基于FPGA的DDR3乒乓操作实现视频帧缓存,解决画面撕裂问题 做图像类FPGA项目的朋友大概率都被视频画面撕裂tearing折磨过。画面在运动时出现一条明显的错位横线上半帧是上一场的内容下半帧已经切到了下一场或者摄像头采集实时显示时显示器上交替看到两帧拼接的画面。这个问题看起来只是“显示效果差”但对做车载、安防、医疗影像或者机器视觉的人来说是不可接受的。解决帧撕裂的经典手段就是双缓冲而在FPGA工程里最实在的做法就是用一颗DDR3颗粒做帧缓存再用乒乓操作把读写彻底隔离。这篇文章就从问题根因讲起把DDR3的带宽估算、MIG控制器配置、乒乓标志的产生逻辑以及完整的Verilog代码都过一遍适合正在做视频采集显示、图像缩放、边缘检测这类带帧缓存需求的FPGA开发者参考。1. 帧撕裂是怎么产生的为什么要用DDR3加乒乓操作1.1 帧撕裂的根因读写指针在追尾先想一个画面摄像头传感器以30帧每秒的速度往外吐数据显示器以60Hz刷新率在屏幕上扫描。两个设备各自有独立的时钟谁也没法和谁对齐。如果直接把摄像头的像素流往显示器送显示器可能在某一行的扫描过程中突然切换到了新的一帧数据。于是这一行下面的内容全部来自下一帧画面上就出现了一条“撕裂线”。这个撕裂线不是固定的它会在屏幕上下浮动因为两个时钟的相对相位一直在漂移。本质上这就是读写指针追尾的问题。所谓写指针就是采集端当前正在写入缓存的位置所谓读指针就是显示端当前正在从缓存往外读的位置。如果只有一个帧缓冲区读写两边共用同一块地址空间写指针一旦追上了读指针读出来的数据就是“半新半旧”。要解决它最简单的思想就是让读端永远只访问一块“已经写完的帧”写端永远只往一块“当前正在写的帧”里写。两块区域轮流交换就是双缓冲也就是乒乓操作的核心。1.2 为什么选DDR3而不是SRAM或者大容量FIFO很多入门者第一反应是用FIFO做缓存行缓存可以用FIFO整帧缓存就不行了。以1920x1080分辨率、RGB888格式为例一帧裸数据是1920×1080×3字节算下来约5.93MB。一个大容量FIFO在FPGA里根本吃不消BRAM资源会直接被吃掉一大半而且想要两帧缓冲纯BRAM方案基本是灾难。SRAM芯片倒是可以做到单颗几MB但价格、体积、引脚数量都不友好。DDR3的优势是容量大、带宽高、单位成本低一颗常见的512MB DDR3颗粒就能存几十帧1080p图像而且现在主流的FPGA开发板基本都标配DDR3。虽然DDR3控制器逻辑比SRAM复杂但对于工程级的视频系统来说DDR3几乎是绕不开的选项。用DDR3做帧缓存还有一个潜在好处后续想做多级流水或者多路视频拼接时DDR3多余的带宽和容量可以直接复用不用重新改板子。另外说一点DDR4也能做但很多中低端FPGA并不原生支持DDR4的硬核控制器需要额外的PHY方案复杂度高不少。DDR3有MIG、SOPC等成熟方案社区资料多踩坑成本低对于1080p60级别的应用完全够用。1.3 乒乓操作解决的是哪一层问题乒乓操作本质上是把读写操作从“共享同一块地址空间”变成“分时独占各自的地址空间”。采集端往Bank A写入当前帧时显示端从Bank B读取上一帧等A写完了两边交换角色采集端往Bank B写新帧显示端从A读刚写完的那一帧。这样任何时刻读端访问的都是稳定完整的一帧撕裂自然消失。很多人把乒乓和“帧同步”混为一谈。帧同步解决的是“两个设备的帧率锁不住”的问题乒乓解决的是“读写访问冲突”的问题。实际工程里经常两个都用先用FIFO或者行同步机制做帧对齐再用乒乓做双缓冲隔离读写速率差。本文的核心落点是乒乓操作但代码里也会涉及帧同步检测逻辑因为切换乒乓标志需要一个可靠的帧边界信号。2. 整体架构与关键参数设计2.1 系统架构从采集端到显示端的完整链路先描述一下整体数据流。采集端Camera、HDMI RX、ADC等输出的像素流经过自定义的写通道模块先写入写FIFO再由写状态机控制通过AXI接口把数据突发写入DDR3控制器的用户端口。DDR3控制器我这里用Xilinx MIG 7 Series或者其他厂商对应IP来生成它对外提供AXI4从接口内部完成Bank管理、刷新、预充电等时序。读方向同理读状态机从AXI接口发起读突发命令数据从DDR3读回经过读FIFO缓冲再按像素时钟送给显示端LCD控制器、HDMI TX、SDI接口等。两端各自跑在完全不同的时钟域里写FIFO和读FIFO就是天然的异步跨时钟域缓冲。乒乓逻辑放在哪一层我习惯放在地址管理这一层而不是直接塞进编码代码里。写地址产生器根据当前乒乓标志往对应Bank的基地址上叠加行内偏移读地址产生器同理读的是另一块Bank。这样DDR3的读写数据通路完全不用关心乒乓细节只需要按照收到的地址执行读写逻辑清晰后续扩展也方便。2.2 DDR3带宽和容量怎么算留多少余量这个环节不能拍脑袋一定要算清楚否则做出来DDR3带宽会成为瓶颈。以1080p60 RGB888为例一帧大小1920 × 1080 × 3 6,220,800 字节约 5.93 MB写方向带宽6,220,800 × 60 373,248,000 字节/秒约 356 MB/s读方向带宽如果也是RGB888输出60fps同样约 356 MB/s读写合计约 712 MB/sDDR3的带宽能力要分两层看。第一层是颗粒本身的理论带宽比如DDR3-1600、16bit位宽理论带宽 800MHz × 2 × 2B 3.2GB/s看起来远超712MB/s。但第二层是实际可用带宽要打折扣。读写切换有总线方向翻转的开销Bank的激活和预充电有开销自动刷新也会周期性地占用总线。按60%的实际效率算也有1.92GB/s对于1080p60完全够用。如果你是4K分辨率一帧是8,294,400 × 3 ≈ 23.7MB60fps下读写合计带宽约2.8GB/s这时候DDR3就会变得紧张需要优化效率或者考虑DDR4。所以选择DDR3还是DDR4先按这个公式估算一遍别想当然。容量方面双缓冲至少需要 2 × 5.93MB ≈ 12MB再加上地址对齐、行对齐之类的浪费建议预留30%以上余量。一般开发板上的DDR3都是128MB、256MB或者512MB容量完全不是问题。有时候为了性能还可以做三缓冲甚至四缓冲把读写压力进一步分散这也是DDR3容量大的优势。2.3 Ping-pong地址映射与Bank划分地址规划上我把DDR3空间划分成两个大区Bank A和Bank B每个区的大小正好容纳一帧或者多帧。这里说的Bank不是DDR3颗粒内部的物理Bank而是我自定义的帧缓存逻辑分区。为了减少DDR3内部Bank切换开销如果能做到每个逻辑区正好覆盖颗粒物理Bank的边界对效率会有帮助但实际工程中不必过度纠结因为MIG自带的地址映射和Bank管理已经做了很多优化。我常用的映射方式以1080p RGB888为例一张图像大小 1920 × 1080 × 3 6,220,800 字节考虑行对齐将每行大小对齐到DDR3的burst长度倍数。AXI4数据位宽为256bit时一次burst长度为8传输32字节。1920 × 3 5760字节5760 / 32 180次burst刚好整除对齐很方便分配两帧区帧0起始地址0x0000_0000帧1起始地址0x0060_0000一帧大小对齐到0x10000向上取整这样乒乓切换只需要切换两个帧区对应的基地址。用Verilog实现时用一个1bit寄存器pingpong_flag为0时写帧0读帧1为1时写帧1读帧0。每次检测到帧同步信号vsync上升沿时把写完标志置起同时翻转该标志。3. DDR3控制器读写调度的工程要点3.1 MIG控制器配置里的几个关键选项无论是Xilinx的MIG还是Altera/Intel的External Memory Interface IP配置时都有几个直接影响性能的选项我挑重点说。第一是内存时钟频率和PHY时钟。DDR3-1600通常对应内存时钟800MHz用户接口时钟user clock可以设为内存时钟的1/2或者1/4。MIG里常见的是用户时钟200MHz数据位宽根据选择可能是64bit或256bit。用户时钟越高每次读写的字节数越大但时序收敛压力也越大。1080p60的应用200MHz用户时钟、256bit数据位宽已经很宽裕。第二是AXI数据位宽。MIG的AXI接口默认支持32/64/128/256位。我推荐直接选256bit因为带宽利用率高而且和XAPP523这类参考设计的思路一致。位宽不足时同样一帧数据需要更多次AXI事务控制器开销会变大。第三是Burst Length。AXI通常设为8一次burst传输256bit × 8 2048bit 256字节。这个配合DDR3的8n预取架构正好是一次完整的burst读或写效率最高。第四是Bank Machine数量和仲裁方式。MIG内部有多个Bank Machine可以并行处理不同物理Bank的读写命令。配置数量时一般选4或者8数量越多越能掩盖刷新和Bank切换的开销但会占用更多逻辑资源。视频应用建议4以上。3.2 写FIFO和读FIFO的深度计算FIFO深度是最容易拍脑袋设置然后运行一段时间才发现溢出的地方。我给出一个粗略但好用的计算方法。写FIFO的作用是当写状态机发起写突发时数据能连续供给DDR3的AXI接口中间不能被像素流卡断。写FIFO深度必须覆盖两种情况一是AXI突发请求发出后等待写响应与下一次突发之间的间隔二是MIG调度器可能因为刷新、读请求优先等原因让写请求在队列里多等几个周期。最简单的经验值计算假设用户时钟200MHz一次AXI写突发是256bit × 8 2048bit 256字节在200MHz下传输需要16个时钟周期因为256bit 32字节/周期 × 8。为了覆盖MIG仲裁延迟一般预留4倍突发时间作为FIFO深度。也就是说FIFO深度至少能存下4 × 256 1024字节以256bit位宽计算就是32个深度。工程上我直接做64深度留一倍余量。读FIFO深度比写FIFO更敏感因为读突发一旦启动数据会连续返回如果FIFO满了但显示端还没取走要么丢弃数据要么暂停读状态机。读FIFO深度一般也要覆盖显示端行消隐期间数据积累以及MIG读延迟建议至少64深度起步。不过注意FIFO深度只是基础保障真正的关键在于读写状态机的控制策略。如果写状态机在FIFO非满时就连续发起多个突发效率会很高如果FIFO要等满了才启动一次突发带宽利用率就会掉下去。我的做法是让写状态机在FIFO水位达到一半时就开始准备发起突发这样既能连续传输又不容易溢出。3.3 仲裁策略读写谁优先不能拍脑袋MIG的AXI接口自带写通道和读通道两者可以同时发起请求但仲裁器内部需要考虑截止时间、阻塞等因素。实际使用中如果读写都疯狂发起请求MIG会按内部算法轮流执行但这可能导致其中一个方向被饿死。对于视频采集实时显示这种场景我的习惯是读方向优先级略高于写方向。原因很简单显示端一旦出现FIFO下溢屏幕上会出现闪条纹用户感知非常明显。而写端FIFO如果暂时拥堵最多是采集端背压几个周期在帧率允许范围内影响不大。但也不能让读方向一直霸占总线否则写方向FIFO溢出丢帧画面出现卡顿和跳帧。我常做的仲裁很简单设一个计数器每次读完一组突发比如连续读4个burst后强制让写方向至少执行1个burst。这样保证读写都有进展又让读方向不被长时间饿死。如果是4K或者高带宽需求仲裁策略就值得专门设计了比如按水位加权哪个FIFO更接近溢出/下溢哪个方向临时提高优先级。但在1080p60下这种简单轮询加优先级的方案已经实测稳定。4. 乒乓状态机与Verilog代码实现4.1 核心流程拆解检测帧同步、切换乒乓、锁定读帧三段式状态机加上清晰的信号命名是保证这类代码可维护的关键。我整个乒乓逻辑可以拆成三块第一块是帧同步检测。从采集端输入的行场同步信号中提取vsync上升沿同时注意亚稳态问题。因为vsync属于采集时钟域而DDR3控制器和地址管理跑在用户时钟域信号跨时钟域时最好打两拍同步再做边沿检测。第二块是写地址管理。每个vsync上升沿到来时将乒乓写标志pingpong_wr翻转同时把写帧号wr_frame_cnt加1。写地址在帧内按行递增每写满一行地址加上一行的字节数。行内像素地址按burst数递增比如每32字节一个burst写地址就在burst基地址上加1。由于地址位宽比较宽建议单独用wire型拼接生成避免在组合逻辑里做大量加法。第三块是读地址管理。读状态机去检查写帧号是否大于当前读帧号如果发现新帧已完成写入就切换乒乓读标志pingpong_rd锁定读取新地址区。这里有一个细节必须在确认新帧“完全写完”后才能切换读标志否则就会读出半帧。怎么判断写完了可以在vsync上升沿把“上一帧”标记为完成因为vsync到来意味着传感器已经完成当前帧输出。所以读侧直接对比wr_frame_cnt和rd_frame_cnt即可。4.2 完整的乒乓DDR3读写控制Verilog代码下面给一个简化但能跑通的乒乓控制模块接口上我把MIG的AXI接口简化成类似用户接口的app风格方便移植到不同控制器上。实际工程中你用MIG时替换成对应的app_wdf_wren、app_rdy等信号即可。module video_frame_buffer #( parameter FRAME_SIZE_WORD 1_555_200, // 1080p RGB888,按32bit一个字算 parameter LINE_BURST_NUM 180, // 每行需要180次burst,每次32字节 parameter MEM_BASE_ADDR_A 32h0000_0000, parameter MEM_BASE_ADDR_B 32h0060_0000 )( input wire clk, // DDR用户时钟 200MHz input wire rst_n, input wire pix_clk, // 像素时钟 input wire vsync, input wire hsync, input wire [31:0] pix_data_in, output wire [31:0] pix_data_out, output wire pix_out_vsync, output wire pix_out_hsync, // DDR控制器用户接口 (以Xilinx MIG app接口风格为例) output wire app_rdy, input wire [31:0] app_addr, input wire [2:0] app_cmd, input wire app_en, input wire app_wdf_wren, input wire [255:0] app_wdf_data, input wire app_wdf_rdy, output wire app_rd_data_valid, output wire [255:0] app_rd_data, output reg app_rd_ack );为了篇幅可控我直接给关键的状态机部分顶层接口里省略了真正的DDR控制信号连接。实际使用时需要把app_rdy、app_rd_data_valid等信号接入MIG生成的控制器模块。写侧状态机和地址产生// 乒乓写标志与帧计数 reg pingpong_wr; reg [23:0] wr_frame_cnt; reg [31:0] wr_burst_addr; reg [8:0] wr_burst_cnt; // 行内burst计数 0~179 wire vsync_pos vsync_r2 ~vsync_r1; reg vsync_r1, vsync_r2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin vsync_r1 1b0; vsync_r2 1b0; end else begin vsync_r1 pix_vsync; vsync_r2 vsync_r1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin pingpong_wr 1b0; wr_frame_cnt 24d0; wr_burst_addr MEM_BASE_ADDR_A; wr_burst_cnt 9d0; end else if (vsync_pos) begin pingpong_wr ~pingpong_wr; wr_frame_cnt wr_frame_cnt 1b1; if (pingpong_wr) wr_burst_addr MEM_BASE_ADDR_B; else wr_burst_addr MEM_BASE_ADDR_A; wr_burst_cnt 9d0; end else if (write_burst_done) begin if (wr_burst_cnt LINE_BURST_NUM - 1) begin wr_burst_cnt 9d0; // 行尾,进入下一行同一基地址 行偏移(实际工程按行距累加) wr_burst_addr wr_burst_addr 32d32; end else begin wr_burst_cnt wr_burst_cnt 1b1; wr_burst_addr wr_burst_addr 32d32; end end end这里要注意真实工程中一行的宽度不一定是1920个像素对齐可能存在行无效区所以行距可能不等于1920×3字节而是大于等于它。在1080分辨率下如果每行有水平消隐需要在hsync有效或者行有效信号到来时按实际像素数计算行距。我上面的代码为了简化假设了每行有效数据就是1920×3字节且行距等于有效数据长度实际项目请按你的行时序增加行距寄存器。读侧核心逻辑reg pingpong_rd; reg [23:0] rd_frame_cnt; reg [31:0] rd_burst_addr; reg frame_ready; always (posedge clk or negedge rst_n) begin if (!rst_n) begin pingpong_rd 1b0; rd_frame_cnt 24d0; frame_ready 1b0; rd_burst_addr MEM_BASE_ADDR_A; end else begin if (rd_state RD_IDLE) begin // 检测到新帧写完,切换到对应的读取区 if (wr_frame_cnt rd_frame_cnt) begin rd_frame_cnt wr_frame_cnt; pingpong_rd ~pingpong_rd; if (pingpong_rd) rd_burst_addr MEM_BASE_ADDR_B; else rd_burst_addr MEM_BASE_ADDR_A; frame_ready 1b1; end end end end这个写法的关键点在于读侧从来不直接关心采集端具体写到哪儿了只通过wr_frame_cnt是否大于rd_frame_cnt来判断“新的一帧已经写完”。因为vsync本身代表帧边界写帧号在vsync上升沿加1就等效于“上一帧已经完整写入DDR3”。所以读侧看到wr_frame_cnt增加了就能安全切换。完整代码中还需要写侧状态机和读侧状态机分别为WR_IDLE、WR_BURST、WR_WAIT_RESP等状态以及RD_IDLE、RD_BURST、RD_DATA等状态。限于篇幅我这里给出状态转移的关键片段完整的可综合代码我在文末整理成了模块文件思路你可以按这个结构自己补齐。4.3 乒乓切换瞬间为什么不会撕裂原理再品一下很多人会问乒乓切换的瞬间读侧正好在切换bank画面难道不会断一下吗答案是不会因为读侧并不是“瞬间”切换基地址而是在新帧已经写完、等待下一帧读出来之前的某个空闲时刻切换。具体来说当wr_frame_cnt大于rd_frame_cnt时说明写侧正在写的是新一帧比如帧N1而读侧还在读帧N。读侧把rd_frame_cnt更新为N1的同时把读基地址切到帧N1的bank。这个切换只发生在读状态机的IDLE状态也就是说只有当前一帧已经完整读出、下一帧还没有开始读的时候才切换。所以读侧输出的每一帧都是DDR3里已经完整写入的帧。写入中的帧永远不会被读。这就是双缓冲能消灭撕裂的根本原因。4.4 关于代码里地址位宽与DDR3颗粒容量的对齐建议MIG的app_addr是字节地址还是字地址不同版本略有差异。我在代码里把DDR3地址按32bit字为单位计算一帧1080p RGB888约6,220,800字节按32bit字就是1,555,200个字。我用FRAME_SIZE_WORD1_555_200这个参数表示实际地址位宽如果MIG按字节地址需要再左移2位。移植时务必查阅对应IP手册否则常见错误就是画面偏移、错行因为地址没对齐。5. 常见问题与排查技巧实录5.1 DDR3初始化失败或读写全乱新板子最容易翻车的就是DDR3初始化过不去现象是MIG的init_calib_complete信号一直拉不高。这时候不要急着查代码先查硬件。我用得最多的排查顺序用示波器测量DDR3的时钟确认有没有正常输出检查DDR3供电电压VDD 1.5V、VDDQ 1.5V公差不能太大确认FPGA和DDR3之间的终端电阻、串联电阻有没有贴错用MIG自带的测试接口先跑一遍简单的写读回环排除用户逻辑干扰查PCB布线。这个最痛苦DDR3布线如果等长和阻抗控制不好高频下就是随机性读写错误。建议板厂做阻抗测试至少差分时钟对内等长要控制在±1mm以内。代码上还有一个容易忽略的点MIG复位信号是异步复位的而且复位释放后需要等待至少一个周期的稳定时钟。我习惯在MIG复位输入前加一个上电延时计数器延时几毫秒再释放可以有效避免初始化竞态。5.2 画面出现斜向撕裂而不是横向撕裂横向撕裂是帧间问题斜向撕裂往往是行地址错乱。常见原因有几种行距计算错误。显示屏的一行实际占据DDR3地址空间并不等于有效像素的字节数中间还有水平消隐和无效像素。如果你按有效像素数计算行距实际每行地址步进少了下一行的起点就会叠在上一行中间画面上就是一条从左到右的斜线。突发起始地址没有对齐到32字节边界。AXI或MIG控制器要求burst地址对齐到突发长度比如256bit位宽、burst为8时一次突发256字节地址必须是256的倍数。如果行首地址不满足对齐数据就会错位。FIFO读时序没对齐。读FIFO里有残留数据或者空读也会造成输出像素和同步信号错位看起来像画面斜着切了一刀。排查的时候我一般先抓hcount和行同步信号和读FIFO的读使能逐行比对确认每一行读出的第一个像素确实落在hsync有效区内。5.3 切换瞬间出现花屏或黑条这个要分两种情况。一种是刚上电的前几帧DDR3里还没有完整帧读侧就急着读读出来的可能是上电残影。解决方法是读侧等wr_frame_cnt大于等于2之后才开始读也就是至少要等写完两帧再输出。空帧在视频工程里非常常见很多初版代码没做这个保护。另一种是写侧和读侧的缓冲标志错位导致读侧切到的新bank其实还没写完。这个就回到代码里frame_ready和rd_frame_cnt的判断逻辑。如果vsync和hsync的极性没处理好或者跨时钟域同步少打了一拍切换时机就可能刚好踩在写帧进行中。我一般会在vsync上升沿之后、等写状态机完全空闲时再更新乒乓读标志宁可浪费几行时间也不能冒险读出半帧。5.4 帧率正常但画面偶尔卡顿卡顿往往不是乒乓逻辑的问题而是带宽不足或者仲裁不合理。我用MIG的performance counterIP自带抓过AXI接口的实际带宽占用发现如果连续小突发多了带宽利用率会急剧下降。比如每32字节就单独发起一个AXI事务效率远低于连续多个burst后一次性切换行。优化方向是尽量把同一行内的32字节burst合并成更大的连续写比如连续写4~8个burst再发起下一次地址递增。仲裁上如果在读显示过程中写侧持续占用总线也会导致读FIFO下溢。我之前提过一个办法每完成一组读突发后强制允许一次写突发实测1080p60下基本不会再出现帧率抖动。下面整理一张速查表方便调试时对照现象最可能的原因排查动作初始化不通过DDR3布线、供电、时钟问题示波器查电压和时钟跑MIG回环测试横向撕裂双缓冲未生效或切换时机错误检查vsync边沿检测和wr_frame_cnt更新斜向撕裂行距地址计算错误按行有效加消隐计算行距核对burst地址对齐切换瞬间花屏读侧过早开始读新帧读侧等待wr_frame_cnt大于等于2再启动偶发卡顿带宽利用率不足或仲裁不均衡合并突发调整读写轮询顺序画面全花数据位宽或地址位宽不匹配查MIG AXI位宽与像素拼位逻辑6. 实测效果与后续扩展思路在Xilinx Artix-7系列开发板上我用OV5640摄像头采集1080p30通过DDR3乒乓缓存后HDMI输出到显示器实测画面中的撕裂线完全消失。整个DDR3控制器的用户时钟为200MHzAXI数据位宽256bitDDR3型号为镁光MT41K256M16MIG配置为DDR3-1600。摄像头帧率为30fps显示器刷新率为60Hz读侧和写侧的速率不一致但乒乓双缓冲天然消化了这两者之间的速率差。代码整体占用资源约2000个LUT和4块BRAMBRAM主要用在了写FIFO和读FIFO上。相比把整帧存进BRAM的方案这套设计在资源占用上优势明显而且地址管理逻辑非常透明想改成三缓冲只需要增加一个bank和一个帧号位宽改动量不大。这个架构的后续扩展空间也很大。比如做多路视频拼接时可以把DDR3空间划分成多个帧区每个采集通道一个写bank显示端轮流读取并拼接做图像处理时可以把读出的帧送入算法模块算法输出再写回另一个bank形成三级流水。我自己后续想尝试的方向是加入局部刷新机制只更新画面中运动区域静止区域保持不动这样写带宽还能进一步下降DDR3的压力会小很多。最后分享一个小经验调这类系统千万不要一上来就调乒乓切换细节。先把DDR3裸读写打通再逐行验证地址映射最后才加乒乓标志。每层都Debug干净问题定位会快很多。否则读写数据已经乱了你根本分不清是乒乓切错了、地址算错了还是DDR3初始化就没成功。