JPG转MIF/TXT/COE:FPGA图像初始化文件的Matlab生成指南

发布时间:2026/10/14 22:10:34
JPG转MIF/TXT/COE:FPGA图像初始化文件的Matlab生成指南
做FPGA图像处理的人迟早会被同一个问题拦住手里有一张JPG图片怎么把它变成FPGA里ROM能认识的数据这种需求太常见了——图像采集模块要仿真、边缘检测算法要输入测试图、神经网络加速模块要加载权重甚至做一个简单的VGA显示Demo也绕不开“把一张图塞进存储器”这一步。而每次看到新人在这一步卡住我都想把Matlab和三种初始化文件的关系一次讲清楚。这篇文章是FPGA仿真必备系列的第一篇专门解决JPG图片转mif、转txt、转coe的问题全程用Matlab操作代码可以直接抄。1. 先从场景说起图像要进FPGA到底卡在哪1.1 仿真缺图是常态在做图像处理相关的FPGA项目时大部分验证工作都发生在“没有摄像头、没有显示屏、没有开发板”的阶段。你写好了一个图像缩放模块或者一个边缘检测模块总得有点输入数据让它跑起来。办法无非两种一种是直接在Testbench里给每个像素赋值另一种就是读初始化文件。前者适合几个像素的小实验一旦遇到几百乘几千的图手写像素数据完全不现实。后者才是实际项目中天天用的方案。但这里有个尴尬的地方FPGA综合工具不认识JPG、PNG这些图片格式它只认特定格式的初始化文件。你需要先把图片数据“翻译”成存储器初始化文件再通过工具提供的ROM/RAM IP核加载进去或者直接在仿真时用文件读取命令导入。这个翻译过程如果手动处理一张图动辄几千上万个像素点再加上字节序、行列顺序、地址映射这些问题人能算到崩溃。所以才有了文章要讲的Matlab方案。1.2 三份初始化文件的真实分工mif、txt、coe这三种文件本质上都是文本文件数据内容完全一样区别在于“谁认识它”。mif文件是某A厂FPGA开发环境Quartus用来初始化ROM/RAM的标准格式。你在Quartus里生成ROM核时可以直接把mif文件填进去综合布线后ROM里就是你要的数据。coe文件是对面某X厂FPGA开发环境Vivado的初始化格式。Block Memory Generator生成ROM核时指定coe文件作为初始化来源。两个厂家的工具不互通在Quartus里你一般不会去加载coe在Vivado里也不会去加载mif。txt文件则属于“通用格式”。它没有行业标准限制最常见的是配合Verilog的$readmemh/$readmemb在仿真器里读入也可以给Python、C语言等工具做数据交换。如果你只是做仿真验证不打算上板跑综合txt反而是最灵活的选择因为它不需要经过IP核加载Testbench里写两行代码就能把整个图片喂进存储器。打个比方mif和coe相当于你分别给两家餐厅看过定制菜单txt则是你自己留的一份通用清单。做仿真txt足够要上板根据开发环境选mif或coe。2. 拆解三种初始化文件mif、txt、coe分别管什么2.1 图片在FPGA内部的表示方式FPGA里的存储器是一块接一块的RAM单元地址从0开始往后排每个地址存一个固定位宽的数据。要把一张图片放进去核心工作就是做一个“像素矩阵到一维数组”的映射。一张灰度图本质是一个二维矩阵每个位置存放0到255的亮度值。比如一张128x128的灰度图展开成一维之后就是16384个像素值。按行的顺序第一个地址存第一行第一个像素第二个地址存第一行第二个像素……依次类推。彩色图要稍微麻烦一点常见的处理方式有两种一种是直接取R、G、B中的一个通道当灰度图另一种是把RGB打包成一个数据。比如用RGB565格式一个像素占16bit高5位是R、中间6位是G、低5位是B。本文先以灰度图为主彩色格式作为扩展再提。用Matlab做转换时最容易出错的地方就在这个展开顺序上。reshape函数默认按列读取矩阵而图像的显示和扫描通常是按行进行的。如果不先对矩阵做转置就直接reshape最终生成的ROM数据就是“竖着读”的图像在FPGA上还原出来会旋转90度镜像错乱也是类似情况。2.2 灰度化与位宽选择的门道为什么要转灰度因为大多数FPGA图像算法的入门练习不管是边缘检测、二值化还是直方图均衡都只需要亮度信息。彩色图转灰度后数据量直接降为三分之一而且一个像素正好对应8bit也就是一个字节一位非常贴合FPGA的典型存储位宽。灰度转换有标准公式Y 0.299R 0.587G 0.114B。Matlab里的rgb2gray函数用的就是这组权重不需要自己写。但这里有个巨大的坑rgb2gray返回值是double类型取值范围是0到1的浮点数不是0到255的整数。直接拿这种数据去写十六进制%X要么报错要么输出一堆没意义的值。正确做法是转换后先乘255再取整转成uint8。这一点在后面的代码中会体现。位宽的选择同样要结合存储深度来看。一张128x128的灰度图深度是16384数据位宽是8bit地址位宽正好14bit。如果把图片缩到64x64深度就变成4096地址位宽12bit。所以选图像大小时除了考虑画质需求还要考虑存储器的深度是否合理。虽然非2的幂次深度也能用但是地址寻址会浪费位宽调试时也更容易出错。2.3 三种文件格式的硬规范mif文件的标准结构是固定几行头信息加一段CONTENT内容DEPTH表示深度WIDTH表示数据位宽ADDRESS_RADIX和DATA_RADIX分别表示地址和数据的进制常见值是HEX或DEC。CONTENT BEGIN之后按“地址 : 数据;”的格式逐行列出地址和数据都可以是十六进制以分号结尾最后用END;收尾。如果你手头没有Matlab直接手工编辑小文件也很快。coe文件则是另一种风格开头用memory_initialization_radix16;声明数据进制再用memory_initialization_vector引出数据列表数据之间用逗号分隔最后一行以分号结束。值得注意的是coe文件没有显式的地址字段它默认按从低到高的地址顺序依次存放数据所以数据的顺序就是存储器的地址顺序。txt文件最自由无非就是一行一个数据。但要注意如果要配合Verilog的$readmemh读取那每个数必须是十六进制不能带“0x”前缀不能出现小数也不建议有空行。如果配合$readmemb读取就必须是二进制。很多新手拿十进制txt去喂$readmemh读出来的数据完全错乱就是因为进制不匹配。3. Matlab实操一张JPG如何变成mif文件3.1 预处理四步走生成mif之前先把图片在Matlab里处理好。第一步用imread读取图片第二步判断图片是不是彩色图是彩色图就转灰度第三步根据需求用imresize缩放图片第四步把二维矩阵按行扫描顺序展开成一维数组。其中第三步的缩放我认为是实际项目里最值得花心思的一步。之前我给某个图像处理Demo做ROM初始化时直接用了一张1920x1080的图生成的mif有几十MBQuartus加载时明显变慢仿真也是卡顿。后来把图片缩成128x128文件只有原来的百分之一处理速度立刻上来了。如果只是算法验证这个小尺寸完全够用。展开一维数组的原理是先对灰度矩阵做转置再用reshape把它按行拉直。这一步决定了你的图片在FPGA里还原出来是不是正向的。代码里就一句话data_row reshape(gray_img, [], 1);3.2 mif生成代码可直接抄作业clear; clc; % 用户配置区 img_file test_img.jpg; % 输入图片 mif_file image_128x128.mif; % 输出mif文件名 target_h 128; % 目标高 target_w 128; % 目标宽 % % 1. 读取图像 img imread(img_file); % 2. 转灰度如果是彩色图 if ndims(img) 3 gray rgb2gray(img); % 注意返回double类型范围0~1 gray uint8(round(gray * 255)); % 转回0~255的整数 else gray img; end % 3. 缩放 gray imresize(gray, [target_h target_w]); % 4. 按行扫描顺序展开成一维 data_row reshape(gray, [], 1); % 5. 写mif文件 depth length(data_row); width 8; % 灰度数据8bit fid fopen(mif_file, w); fprintf(fid, DEPTH %d;\n, depth); fprintf(fid, WIDTH %d;\n, width); fprintf(fid, ADDRESS_RADIX HEX;\n); fprintf(fid, DATA_RADIX HEX;\n); fprintf(fid, CONTENT BEGIN\n); for i 0 : depth - 1 fprintf(fid, \t%X : %X;\n, i, data_row(i 1)); end fprintf(fid, END;\n); fclose(fid); fprintf(mif文件已生成%s深度%d位宽%d\n, mif_file, depth, width);代码逻辑不复杂配置好文件路径和尺寸然后读取灰度图、缩放、展开最后用fprintf一行一行写。有一点经验分享fprintf(fid, \t%X : %X;\n, ...)这行的制表符和分号尽量保持与示例一致。mif格式虽然不强制要求对齐但统一风格的文件在后续人工检查时会舒服很多。3.3 必须注意的三个细节第一个细节是数据类型。我在前面反复强调过rgb2gray返回double其实还有一种情况容易被忽略如果输入图片本身是灰度PNGimread直接返回uint8矩阵这时就不需要额外转换。判断条件ndims(img)3能区分彩色图和灰度图但在某些极端情况下比如索引图像返回的可能是HxWx1这时要单独看class(img)确认类型。我习惯在转换之后加一句assert(isa(gray,uint8))防止后续出错。第二个细节是缩放比例。imresize默认是直接拉伸不会保持宽高比。如果你的图片不是正方形直接缩成128x128看起来会变形。对算法验证来说通常无所谓但如果对图像内容有要求建议先裁剪出中心区域再缩放。裁剪可以用imcrop也可以手动计算起点和范围我在项目中更常用后者因为能精确控制输入尺寸。第三个细节是文件路径。Matlab的当前工作目录不一定是脚本所在目录如果你用相对路径保存mif文件最后生成的文件可能在你想不到的地方。稳妥的办法是全篇使用绝对路径或者至少在文件头部加一行cd到目标目录。另外我踩过的一个真实教训是路径里不要有中文和空格否则后面Quartus或者ModelSim加载文件时可能出现莫名其妙的问题。4. 一套数据三种输出txt和coe的生成套路4.1 生成txt仿真场景下的轻量方案txt在这三兄弟里最不受拘束它唯一的使命就是让仿真工具能读到数据。生成txt的代码基本就是fprintf一行一个像素值如果你打算用$readmemh配合Verilog仿真就写成十六进制大写的格式一行一个数从data_row(1)写到data_row(end)。如果你是要把数据交给Python或者Excel做后续分析写成十进制反而更方便。所以txt的“进制”不是固定的取决于下游工具读什么格式。我用得最多的场景是ModelSim。Testbench里一行$readmemh(E:/fpga_work/image_data.txt, mem);就把txt内容读进了定义好的reg数组。用txt做仿真初始化的好处是不需要经过Quartus或Vivado的IP核配置改数据不用重新综合测试不同图片时只要替换文件内容即可而且仿真器加载txt文件的速度比加载大型mif快得多。缺点是它不能直接用于上板综合上板之前仍然要转换成mif或coe。4.2 生成coeVivado环境的必要步骤coe的写法跟mif天差地别。它没有逐行地址而是一个数据列表按顺序排列。生成时要注意两点第一数据之间用英文逗号分隔最后一个数据后面必须跟分号不是逗号。第二头部声明行memory_initialization_radix16;告诉Vivado数据是十六进制的。这里的进制可以和mif的数据进制不同但建议统一免得同一套数据在不同格式中维护起来精神分裂。% 在数据预处理完成后追加下面的代码段 coe_file image_128x128.coe; fid fopen(coe_file, w); fprintf(fid, memory_initialization_radix16;\n); fprintf(fid, memory_initialization_vector\n); for i 1 : length(data_row) if i length(data_row) fprintf(fid, %X;\n, data_row(i)); else fprintf(fid, %X,\n, data_row(i)); end end fclose(fid);这段代码生成的是每个数据单独占一行的coe非常长但在功能上完全正确。你也可以让每行多个数据比如每隔16个数据换行自己用mod(i,16)0判断即可。文件行数少一点便于浏览但本质上不影响Vivado识别。4.3 升级成参数化函数每次改图片名、改尺寸、改输出格式都要修改脚本主体这在反复做实验时很烦。我更推荐把上面这些逻辑封装成函数顺便把三种格式的输出合并成一个流程。大致结构如下function [mif_file, coe_file, txt_file] img2init(img_path, target_h, target_w, file_tag) img imread(img_path); if ndims(img) 3 gray uint8(round(rgb2gray(img) * 255)); else gray img; end gray imresize(gray, [target_h target_w]); data_row reshape(gray, [], 1); depth length(data_row); mif_file [file_tag .mif]; coe_file [file_tag .coe]; txt_file [file_tag .txt]; % 然后分别调用三个子函数写文件... end封装之后一行代码就能生成三种文件并且文件名、尺寸都通过参数控制。这样做的好处是换图片、换尺寸时不需要整段脚本复制粘贴也减少了“这版代码改了输出忘了改格式”这种低级失误。5. 文件生成之后怎么把它接进FPGA工程5.1 Quartus工程加载mif在Quartus里首先用Tools菜单打开MegaWizard Plug-In Manager选择创建一个新的宏功能在Memory Compiler分类下找到ROM: 1-PORT。然后在向导中设置存储器的数据位宽和深度比如位宽8、深度16384。到初始化文件的步骤时选择“Use initialization file”加载你生成的mif文件即可。有几个容易出错的地方要说明。第一IP核里设置的深度必须和mif文件的DEPTH一致。如果mif里DEPTH写的是16384而IP核深度设成了8192Quartus会报错或直接截断数据。第二mif文件最好放在工程目录下或者IP核所在目录下。很多版本的工具对路径特别敏感换一台电脑后绝对路径失效又要重新指定。第三如果修改了mif内容在Quartus里要重新生成IP核或者至少重新运行综合否则旧的初始化数据还留在工程里。5.2 Vivado工程加载coeVivado这边先在IP Catalog里搜索Block Memory Generator配置成Single Port ROM读写宽度设8深度设16384。在Other Options页签里勾选Load Init File然后选择.coe文件。Vivado对coe文件的校验比较严格格式不对会直接拒绝加载。最常见的报错就是最后那个分号丢了——如果你看到“ERROR: Syntax error in COE file”之类的提示先怀疑结尾分号再怀疑逗号。加载成功之后Vivado会显示“Read from file completed”或者类似提示并在IP核的摘要界面显示初始化状态。有一点值得注意Block Memory的初始化支持多种内存类型图片数据量大时优先选Block RAM如果只是小的查表选Distributed RAM更省块资源。另外在综合之后改coe文件必须重新生成IP核或执行Update IP否则综合结果不会变化。5.3 仿真测试平台里用txt如果只是想快速验证算法功能我强烈建议直接用txt加$readmemh完全跳过IP核。在Verilog Testbench里这样写reg [7:0] rom_mem [0:16383]; initial begin $readmemh(E:/fpga_work/image_128x128.txt, rom_mem); end这样定义出的rom_mem数组行为就和异步读ROM几乎一样地址作为索引取出对应的像素值。在Modelsim或者QuestaSim里跑仿真加载速度非常快修改数据只需更新txt文件再重新仿真。如果想验证具体模块对上板后行为的一致性再专门生成mif/coe去走IP核流程也不迟。不过用$readmemh时有一个小坑它的解析规则是每行读一个数从第0个地址开始但如果你在txt文件末尾加了一行空行仿真器可能报“invalid token”或者在末尾多出一个无效数据。所以生成txt时确保最后一行是数据不要多出换行以外的内容。上面Matlab代码里用fprintf(fid, %02X\n, data_row);写整个数组时Matlab会在最后一个元素后面也加换行符这是安全的因为$readmemh会忽略最后一个换行符。6. 高频报错与避坑记录6.1 常见错误速查表下面这张表是我在不同项目里反复见到过的错误直接按现象对号入座就能定位现象根本原因解决方法Quartus加载mif报格式错误文件头字段写错、数字含小数点、数据带0x前缀检查DEPTH/WIDTH/ADDRESS_RADIX/DATA_RADIX四行字段数字必须是纯整数Vivado加载coe显示Syntax Error结尾分号缺失、中间多出分号、进制声明不合法确认最后一行是数据;radix只有2、10、16可选Modelsim读txt后数据不对txt内容不是十六进制或者混入非数字字符按%X格式生成txt去掉空行和中文注释图像还原后左右颠倒列顺序和行顺序没有匹配检查reshape前是否做了转置统一行扫描顺序mif文件很大综合很慢图像分辨率过大缩到128x128或256x256仿真阶段用txt替代RGB转灰度后数据全是0rgb2gray返回double但没有量化和取整加uint8(round(gray * 255))$readmemh读入后少数据或少地址txt行数和深度不对应核对txt文件行数是否等于ROM深度改了coe没生效Vivado综合缓存未刷新重新Generate IP或执行Update IP6.2 几个值得养成的小习惯第一个习惯是给生成文件加注释信息。mif文件支持“--”开头的注释行coe文件支持“;”开头的注释行txt文件如果配合Verilog则支持“//”注释。我会在文件头部写一行图像尺寸、生成时间、生成工具版本这样几周后回来看文件还能知道这张数据是怎么来的。别小看这行字它能在排错时帮你快速排除“是不是用错了版本数据”这类问题。第二个习惯是“先仿真、后上板”。我在生成初始化文件的流程上推荐两层工作方式仿真阶段用txt加$readmemh快速迭代算法逻辑需要上板输出图像时再用Matlab生成mif或coe接入IP核。这样可以避免每次修改图片都要重新综合一遍的漫长等待。实测下来一套128x128的图片数据在Modelsim里改图只需几秒而走完一遍Quartus综合布线可能要十几分钟差别非常明显。第三个习惯是MIF深度写完后自己要核对一遍。特别是当你用非2的幂次图像尺寸时比如320x240深度是76800对应ROM IP核也能做但地址位宽17bit。此时最好在Matlab脚本里顺手算一个地址位宽打印出来方便你在IP核配置界面填参数时参考。具体方法不过就是ceil(log2(depth))。第四个习惯是保留一份“转换专用的Matlab脚本”。很多项目会用不同尺寸、不同格式的图片如果把转换逻辑写死在某个算法脚本里下次想用时还得翻旧工程。我自己的做法是维护一个名为img2init.m的通用函数文件所有FPGA图片初始化需求都调它参数一换就能出结果。时间久了这个小函数就成了整个FPGA开发工具链里最常被调用的部分。回到开头那个问题JPG图片怎么变成FPGA能用的数据答案不难但细节不少。我个人在实际项目里的体会是用工具永远比自己手算靠谱但前提是你理解了工具背后的格式和约定。希望看完这篇文章后你也能用Matlab顺手完成mif、coe、txt的批量生成然后在仿真和上板两条路上都走得不卡壳。