资讯动态

ZYNQ图像采集:从DDR3帧缓存到SD卡保存BMP图片的实现方法

发布时间:2026/10/14 1:32:29 来源:尧图企业网站定制
做ZYNQ图像采集迟早会遇到一个问题视频流在DDR3里跑得好好的显示器上画面也正常但你怎么证明采集链路真的没问题怎么把某一帧图像拿出来放到PC上细看之前两篇我讲了VDMA的基础读写和帧缓存的管理逻辑这一篇接着往下走把图像从DDR3帧缓存里抠出来以BMP格式写进SD卡形成一张可以直接在电脑上打开查看的图片文件。这个功能的定位很实用调试图像采集、验证ISP效果、回放数据都是刚需。相比串口打印像素值或者网口回传SD卡保存的优点是简单直接、不依赖上位机协议拔卡插电脑就能看。适合已经跑通VDMA读写、想进一步做数据落盘的初学者也适合做产品原型时快速抓帧验证的人。这篇的内容我按实际做项目时的思路来组织先说明数据链路怎么走通再解释BMP格式里最容易踩的坑然后给关键代码逻辑和FATFS的实操细节最后把我在调试中遇到的几次典型异常拿出来复盘。用的板子是常见的ZYNQ SoC平台摄像头为OV5640图像尺寸我用的是1280x720彩色RGB888来举例其他分辨率逻辑完全一致。1. 全链路数据流帧缓存里的像素怎么一步步变成SD卡上的文件很多人在这一步卡住不是不会写文件而是没想明白“哪块数据才是要存的”。VDMA把摄像头数据写进DDR3之后帧缓存里存的是连续的像素矩阵但你不能直接把这个区域的数据原样扔给FATFS——因为这里没有文件头、没有格式信息就算存下来也只是一坨无意义的裸数据。我习惯把这整条链路拆成四段来理解摄像头采集OV5640输出RGB888或YUV422格式的像素流由VDMA写入DDR3指定的帧缓存地址帧缓存定位当前帧的起始地址由VDMA的读通道地址决定或者直接从寄存器里读回来格式转换把帧缓存里的像素数据按BMP规范重新组织生成54字节的文件头加像素区行序要自底向上文件写入通过FATFS库在SD卡上创建文件把组织好的数据分块写入。这里有个容易被忽略的点VDMA的帧缓存里每一行的数据是连续的但行与行之间在物理地址上也是连续的。也就是说如果你把一整块DDR3区域的内容直接读出来它就是按行扫描顺序排列的像素数据先第一行从左到右再第二行从左到右。这个特性对后面的行序反转操作非常重要因为我们只需要“按行倒序读取数据”而不需要对整块内存做复杂的搬移。我在实际操作中用的是AXI接口直接把DDR3数据读回PS侧代码如下逻辑示意// 从DDR3帧缓存读取一行数据 // base_addr: 帧缓存的物理基地址 // line_index: 要读取的行号 // width: 图像宽度(像素) // buf: 存放读取数据的缓冲区 void read_line_from_fb(u32 base_addr, u32 line_index, u32 width, u8 *buf) { u32 line_bytes width * 3; // RGB888 每像素3字节 u32 offset line_index * line_bytes; memcpy(buf, (u8 *)(base_addr offset), line_bytes); }注意这里的memcpy直接操作物理地址在ZYNQ上是可行的因为PS侧的AXI_GP接口可以访问DDR3地址空间。实际工程里我更推荐用带缓存的地址映射方式但裸机上用这个简单的办法也能跑。理解这条链路之后你才能明白后面每个步骤的取舍为什么文件头要单独构造、为什么行序要反过来、为什么写入要分块而不一次性写完整帧。2. BMP文件格式的细节54字节头、行序反转与像素补零BMP格式看起来简单但细节非常多。不把格式吃透就动手写八成会生成一张打不开的图片。我在这里把关键点拆开讲。2.1 文件头结构每个字节都不能错BMP文件由文件头、信息头、像素数据三部分组成文件头加信息头一共54字节这是固定长度。严格来说每个字段都有明确含义稍有不慎图片就打不开。BITMAPFILEHEADER14字节偏移字段大小说明0x00bfType2字节固定为B、M即 0x424D0x02bfSize4字节整个文件的大小0x06bfReserved12字节保留必须为00x08bfReserved22字节保留必须为00x0AbfOffBits4字节像素数据的偏移量固定为54BITMAPINFOHEADER40字节偏移字段大小说明0x0EbiSize4字节信息头大小固定为400x12biWidth4字节图像宽度像素用有符号整数表示0x16biHeight4字节图像高度正数表示自底向上存储0x1AbiPlanes2字节固定为10x1CbiBitCount2字节每像素位数24位即RGB8880x1EbiCompression4字节0表示不压缩0x22biSizeImage4字节像素数据大小也可以为00x26biXPelsPerMeter4字节水平分辨率可设00x2AbiYPelsPerMeter4字节垂直分辨率可设00x2EbiClrUsed4字节使用的颜色数0表示全用0x32biClrImportant4字节重要颜色数0表示全重要在实际代码里我习惯这么填// 构造BMP文件头返回是否成功 int make_bmp_header(u8 *header, u32 width, u32 height) { u32 file_size 54 width * height * 3; header[0] B; header[1] M; // bfSize header[2] file_size 0xFF; header[3] (file_size 8) 0xFF; header[4] (file_size 16) 0xFF; header[5] (file_size 24) 0xFF; // bfReserved1/2 保持为0 // bfOffBits 54 header[10] 54; // biSize 40 header[14] 40; // biWidth header[18] width 0xFF; header[19] (width 8) 0xFF; header[20] (width 16) 0xFF; header[21] (width 24) 0xFF; // biHeight 写入正数 header[22] height 0xFF; header[23] (height 8) 0xFF; header[24] (height 16) 0xFF; header[25] (height 24) 0xFF; // biPlanes 1 header[26] 1; header[27] 0; // biBitCount 24 header[28] 24; header[29] 0; // 其余字段填充0 return 0; }如果发现某些字段不对最常见的现象是Windows图片查看器提示“文件已损坏”。所以文件头一定要逐字节核对不要嫌麻烦。2.2 行序反转BMP底行开头但帧缓存顶行开头这个坑我踩过两次才彻底记住。BMP格式的像素数据在高度为正数时是从图像最后一行开始存储的也就是自底向上。而VDMA的帧缓存第一行是图像最上面那行。直接顺序把帧缓存数据写到BMP文件里出来的图片就是上下颠倒的。所以在写像素数据时必须先读帧缓存的最后一行写到BMP文件的第一行然后读倒数第二行写第二行以此类推。代码逻辑如下// 把帧缓存数据写为BMP像素区 // fb_base: 帧缓存起始地址 // width, height: 图像尺寸 // fil: FATFS文件对象指针 void write_bmp_pixels(u32 fb_base, u32 width, u32 height, FIL *fil) { u32 line_bytes width * 3; u8 line_buf[1920*3]; // 按最大分辨率分配实际建议用malloc for (int i 0; i height; i) { // i0时读最后一行i1时读倒数第二行 u32 src_line height - 1 - i; u32 offset src_line * line_bytes; memcpy(line_buf, (u8 *)(fb_base offset), line_bytes); UINT written 0; f_write(fil, line_buf, line_bytes, written); if (written ! line_bytes) { // 处理写入失败 break; } } }这里有一个细节每一行读出来是连续的写完一行再写下一行中间不需要任何间隔。只要是24位BMP行与行之间是紧挨着的。2.3 行宽补零RGB888没有补零问题但RGB32有另一个容易踩的坑是行宽度对齐。BMP规范要求每行像素数据的字节数必须是4的倍数如果不够则需要补零到4的倍数。24位RGB、宽度1280时每行字节数是1280x33840已经是4的倍数不需要补零。但如果图像宽度是例如100100x3300不是4的倍数就必须补零。我的项目里实测1280x720和640x480都不需要补零但如果你换了特殊分辨率一定要检查一下这个对齐规则。否则图片尾部会多出几个垃圾字节虽然大部分看图软件能容忍但严格的校验工具会报错。判断方法很简单u32 line_bytes width * 3; u32 padded_bytes (line_bytes 3) ~3; // 对齐到4字节 u32 pad padded_bytes - line_bytes; // 需要补零的字节数如果pad不为0在每行写入完成后补pad个0x00字节。3. FATFS实操文件创建失败、写入长度与效率陷阱FATFS是嵌入式领域最常用的文件系统库几乎每个ZYNQ裸机工程都会用到。官方提供的基本操作不复杂但真正常遇到的问题是各种隐藏限制。3.1 驱动层的准备工作SD卡初始化和挂载在ZYNQ上SD卡控制器通常工作在SDIO模式下需要先把SD卡初始化好然后才能调用f_mount挂载文件系统。我用的流程是初始化SDIO控制器并配置引脚检测SD卡是否插入发送CMD0进入空闲态发送CMD8获取卡信息发送ACMD41进行OCR协商判断卡的类型和电压范围发送CMD2获取CIDCMD3获取RCA选择数据传输宽度并设置块长度调用f_mount挂载文件系统。SD卡初始化返回值错误的概率最高。我遇到的情况多半是电压配置不正确或者卡座检测引脚没有配置上拉导致卡始终处于未插入状态。这块如果初始化不通过后面的所有步骤都无从谈起。我一般会用f_printf打印错误码来定位问题。FATFS的每个API都返回FRESULT类型错误码常见的有错误码含义常见原因FR_NOT_READY设备未就绪SD卡初始化失败或未插入FR_NO_FILESYSTEM无有效文件系统SD卡未格式化或者分区表不识别FR_DENIED访问被拒绝文件属性是只读或者SD卡处于写保护FR_NOT_ENABLED卷未挂载忘记调用f_mountFR_EXIST文件已存在用FA_CREATE_NEW创建重名文件3.2 文件打开模式的选择FATFS的打开模式比标准C的fopen要多几种组合容易搞混的是FA_CREATE_NEW、FA_CREATE_ALWAYS和FA_OPEN_ALWAYS的区别FA_CREATE_NEW如果文件不存在就创建存在则返回FR_EXISTFA_CREATE_ALWAYS无论是否存在都创建且会截断旧文件FA_OPEN_ALWAYS存在则打开不存在则创建打开后文件指针默认在开头FA_OPEN_APPEND存在则打开并把文件指针移到末尾。连续抓帧保存多张图片时文件名不能重复所以要么用FA_CREATE_NEW配合动态修改文件名要么用FA_CREATE_ALWAYS覆盖之前的文件。我的做法是文件名里加上递增序号比如cap0001.bmp、cap0002.bmp用sprintf拼出来。注意FATFS默认不支持长文件名8.3格式下文件名最长8个字符扩展名3个字符所以类似12345678.bmp这种命名一定不会出错。3.3 写入长度和效率为什么写一个54字节的文件头也要走文件系统FATFS底层是按扇区方式操作的SD卡的一个扇区通常是512字节。每次f_write的时候FATFS内部会先把操作结果缓存起来等凑够一个扇区才真正落盘。这带来一个效率问题如果你一次只写几个字节会产生大量缓存操作和状态判断写完整帧的速度会非常慢。我实测后摸索出一套推荐的分块策略文件头一次性写入54字节像素数据按行写入每次写入一行3840字节恰好是7.5个扇区FATFS会自己处理跨扇区逻辑写完所有像素后调用f_sync强制落盘确保数据完整。最初版本我是一次写整个帧缓存退出后文件经常不完整SD卡拔出来插电脑一看文件大小是对的但内容不对因为FATFS的缓存没有正确刷新。后来改成每写完一帧立刻f_sync问题就没再出现过。注意f_close本身会刷新缓存但我还是建议在f_close之前显式调用f_sync尤其是连续写多张图的时候可以避免文件系统异常。还有一点写入缓冲区要尽量大。FATFS内部会维护一个窗口缓冲默认大小是FF_FS_MIN_SZID相关的如果你把单次写入数据控制在扇区对齐级别效率会好很多。不要写一行就f_sync一次那会把速度拖到不可接受的地步。3.4 单次写入长度上限16位还是32位这个坑非常隐蔽。FATFS有两种配置模式FF_FS_TINY和FF_FS_NORTC这些不直接影响但如果你用的是FF_FS_READONLY或FF_FS_MINIMIZE等级较低的模式配合FF_FS_EXFAT等选项f_write的btw参数类型会不同。具体来说f_write的第二个参数是写入长度类型在标准配置下是UINT在FF_FS_LOCK、FF_FS_REENTRANT等配置下可能变成其他类型。但更关键的是在你定义单次写入大小的时候不要超过UINT_MAX。这个问题在裸机上几乎不会遇到但如果你在SDK环境里开了大文件支持单次写入尽量控制在4MB以内比较稳妥。我见过有同事一次写入完整帧结果在图像分辨率比较大时f_write返回FR_INT_ERR查了半天才发现是写入缓冲区溢出。后来改成按行或按块写入就再也没出过这问题。4. 关键代码逻辑拆解从帧缓存定位到文件落盘这一部分我把完整流程串起来讲。前面已经分别讲了帧缓存读取和BMP文件头构造现在把它们组合成可以实际运行的函数。4.1 捕获一帧的触发时机保存图片最怕抓到的帧是“半帧”——VDMA还在写入这帧的时候你就开始从DDR3读数据结果读到一半后面的数据被VDMA新一帧覆盖了。所以要有一个同步机制。我的方案不算复杂但非常有效利用VDMA的frame_count寄存器或者中断标志。流程是等待VDMA统计到新的一帧开始读FRM_CNT寄存器值变化延时一小段确保这一帧的写入已经完成开始保存。延时时间怎么确认我实测1280x72060fps时一帧的写入时间大约在14.7ms左右。VDMA写完整帧后有一个小窗口期可以安全读取。更稳妥的方法是配置VDMA的写通道完成中断在中断里置标志位然后主循环检测到标志位后再保存。不过这个方案涉及中断服务程序的编写篇幅所限我就不展开了。4.2 完整保存流程代码下面这段伪代码可以直接参考我用的是单帧保存的逻辑多帧连续保存只需要在外层循环文件名加序号即可。// 保存当前VDMA帧缓存为BMP文件 // fb_base: 视频帧缓存的物理地址 // width, height: 图像尺寸 // filename: 目标文件名 int save_frame_as_bmp(u32 fb_base, u32 width, u32 height, char *filename) { FATFS fs; FIL fil; u8 header[54]; UINT written; // 1. 挂载SD卡 if (f_mount(fs, 0:/, 1) ! FR_OK) { return -1; } // 2. 创建文件 if (f_open(fil, filename, FA_CREATE_NEW | FA_WRITE) ! FR_OK) { return -2; } // 3. 写BMP文件头 make_bmp_header(header, width, height); f_write(fil, header, 54, written); // 4. 写像素数据行序反转 write_bmp_pixels(fb_base, width, height, fil); // 5. 强制刷新到SD卡 f_sync(fil); // 6. 关闭文件 f_close(fil); f_mount(NULL, 0:/, 0); // 卸载 return 0; }这段代码有几点需要注意FA_CREATE_NEW要求目标文件不存在如果之前已经保存过同名文件会返回FR_EXIST。要么在每次保存前删除旧文件要么文件名带序号递增f_mount的第一参数传NULL是卸载文件系统这个操作在保存完一张图后做很重要否则频繁挂载可能导致文件系统状态异常保存的过程中全程关闭中断或禁止任务调度否则可能被其他中断打断导致写文件期间SD卡状态被破坏。4.3 行序反转和写入的顺序关系再强调一次行序反转的正确实现方式像素区的第一行对应图像的最下面一行像素区的最后一行对应图像的最上面一行。代码里的src_line height - 1 - i就是干这个的。如果你不想用一个大的line_buf也可以分两个缓冲交替使用但我建议在裸机环境直接分配一个行缓冲区就够了1280x720也就3840字节DDR3里随便放。如果分辨率特别大比如1920x1080一行5760字节仍然可以用栈上数组但要注意栈空间是否够。更稳妥的做法是定义成全局变量。5. 实测中的三个异常现场图片打不开、上下颠倒、写入极慢代码写通之后实际跑起来的调试过程才是真正学东西的地方。我把遇到过的几个典型的异常情况都记录下来对应的排查思路也可以举一反三。5.1 异常一生成的BMP文件在电脑上打不开提示格式错误现象SD卡能看到文件大小正常就是我预期的字节数但双击图片查看器提示无法打开或文件损坏。排查过程先用十六进制工具看文件头发现bfType字段是0x4D42还是0x424D的问题——注意内存中的字节序。小端序下BMP的bfType在文件里实际存储的是42 4D对应ASCII字符B和M。如果你直接用一个16位整数0x4D42赋值给bfType在内存里就变成了42 4D这恰好是正确顺序。真正的坑在于bfSize计算错误或者biHeight写成了负数。biHeight如果是负数代表自顶向下的行序很多看图软件也支持但有些严格工具不认。我的经验是直接写正数配合自己实现行序反转兼容性最好。修复把文件头每个字段用字节数组逐个赋值不要用结构体直接赋值。5.2 异常二图片能打开但整个画面上下颠倒现象保存的图片看起来一切正常颜色也没问题就是倒过来的。排查过程这个一看就知道是行序没有反转。很多人在这一步会怀疑是摄像头数据本身的问题但你可以先在PC上看VDMA直通显示器时的画面——如果显示器上画面是正确的那就说明帧缓存里行序正确问题出在保存时不加反转。修复把写入顺序从src_line i改成src_line height - 1 - i即可。验证反转是否生效有个技巧保存你已知内容的全屏纯色图比如上半部分红色、下半部分蓝色。如果保存后图片是倒的红蓝位置正好反过来说明行序反了。5.3 异常三写入速度慢得离谱存一张720p图要好几秒现象保存一帧1280x720的图耗时大概2~3秒这在调试时还能忍但如果你想连续保存几十帧做数据分析就完全不能接受了。排查过程我最初以为是SD卡本身写入慢后来发现是每次f_write之前都做了一次f_open/f_close且每次写入的数据长只有54字节或一行3840字节时频繁的文件系统调用产生了大量开销。另外一个关键是FATFS默认把文件系统的缓存放在一个内部缓冲区里每次调用f_write都可能搬运数据。如果你单次写入长度不是扇区对齐的FATFS会先读回旧扇区、修改、再写回这个读改写流程非常耗时。修复优化的方向有三个加大单次写入长度尽量满扇区对齐不要频繁打开关闭文件一次性写完再关闭合理的DMA传输方式把数据先从DDR3搬到一个空闲DDR缓冲再由FATFS从这个缓冲写入SD卡中间可以避免CPU逐字节搬移。实测优化后保存一张720p图的时间从2.8秒降到0.6秒左右效果非常明显。6. 进阶优化与扩展方向连续抓帧、RAW格式与文件系统选择写完基础版本后这个功能还能往好几个方向扩展按实用性排序我建议这么看6.1 连续抓帧和帧号管理把单帧保存封装成一个函数后连续抓帧只需要外层加循环文件名序号递增。比较麻烦的是连续抓帧时的时序控制VDMA写入帧率60fps你保存一帧需要0.6秒这期间已经过去36帧意味着保存会漏帧。所以“连续抓帧”在裸机下不可能做到逐帧全存只能隔一段时间抓一帧。有个办法是改用双缓冲甚至多缓冲让VDMA写A帧时你保存B帧但代价是内存占用翻倍而且裸机上同时维护文件系统和DMA传输要小心处理。真正的批量连续抓帧我建议换Linux或者RTOS的方案配合设备树和V4L2框架效率高得多。6.2 保存RAW格式做图像分析很多算法分析只需要原始的RGB数据不需要BMP文件头。这种情况下完全可以不用FATFS直接把帧缓存地址区域的数据按固定偏移复制到SD卡的裸扇区里速度会快很多但前提是SD卡上没有文件系统或者你指定某个未使用的分区地址做原始写入。我个人不建议对新手用这种方式因为容易把SD卡的分区表破坏掉造成已有数据丢失。老老实实走FATFS是更稳的路。6.3 文件系统选型FATFS之外还有什么很多用ZYNQ的人最后会考虑上LittleFS或者ext4。LittleFS针对Flash优化掉电保护和磨损均衡做得比FATFS好但在SD卡上意义不大ext4功能强但需要Linux环境支持裸机上基本不用想。所以我的建议是裸机场景继续用FATFS能离线的功能基本都能满足真想换更复杂的文件系统不如直接上Linux。从这块的经验来看我认为整个SD卡保存图片的调试过程最大的收益不在于“让图片显示出来”而是通过这个功能把ZYNQ的数据通路从头到尾理了一遍——CPU怎么访问DDR3、DMA怎么搬运、文件系统怎么和底层驱动交互这些内容在不同的项目里都能迁移复用。下次你要在ZYNQ上存数据到SD卡无论是传感器数据、日志文件还是其他格式的图像核心思路都是一样的先搞清楚数据源结构和时序再选对文件格式和写入策略最后逐个排查异常。这条路走通一次后面就顺了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑