Verilog实现BMP图像旋转:从BMP解析到双RAM缓存与坐标重映射

发布时间:2026/11/5 2:22:36

Verilog实现BMP图像旋转:从BMP解析到双RAM缓存与坐标重映射 简介这份资源面向数字电路设计初学者与FPGA课程学习者围绕Verilog实现图像翻转这一典型大作业场景提供从BMP位图格式解析到像素重排的完整设计说明。压缩包内共1个doc文档约267KB内容涵盖作业简述、设计思路、Verilog与C源代码及参考资料其中C程序负责将BMP文件去除文件头并转为txt二进制流Verilog模块则按时钟节拍读取像素、缓存后重新编排存放位置实现256×256真彩色图像顺时针旋转90度并给出模块划分与ModelSim仿真验证方法。文档还系统梳理了BMP文件头、信息头、彩色板与像素阵列的结构细节便于读者对照理解数据读写流程。目前已有1104人学习适合需要完成图像处理类硬件设计作业、想掌握Verilog文件读写与模块化设计思路的读者参考借鉴。1. 从一张 256×256 的 Lena 图说起这个 Verilog 图像旋转项目到底在做什么一张 256×256 的 24 位 BMP 图片像素阵列 196608 字节文件头 54 字节总共 196662 字节。这个项目要干的事就是把这 196608 字节的像素数据用 Verilog 在仿真环境里按顺时针 90 度重新排一遍位置再写回文件最后拼上文件头还原成一张能打开的 BMP。听起来像软件活但它偏偏要求用硬件描述语言完成中间还得经过 txt 二进制流、双 RAM 缓存、时钟节拍驱动这一整套流程。适合谁看正在做大作业的电子类专业学生、想搞懂 Verilog 文件读写和存储器建模的初学者以及需要把图像数据流塞进硬件仿真管线的开发者。它不涉及综合下板核心价值在于用 RTL 思维描述一个数据重排流水线把 BMP 格式、$readmemb/$fwrite 文件操作、双端口 RAM 建模这几件事串起来。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆。2. 工程结构与数据流三个模块如何把 BMP 拆成可旋转的像素流2.1 整体链路C 预处理 Verilog 旋转 C 还原这个项目的工程结构不复杂但链路拉得比较长跨了 C 和 Verilog 两种语言。完整数据流是这样的file1.bmp ──[C 去头转二进制]── file.txt ──[$readmemb]── ram8bits │ │ │ [三字节拼 24bit] │ ▼ │ ram24bit1 │ │ │ [坐标重映射] │ ▼ │ ram24bit2 │ │ │ [$fwrite %h] │ ▼ │ ram24bit2.txt │ │ │ [$readmemh] │ ▼ │ mem1 │ │ │ [拆回三字节] │ ▼ │ final.txt │ │ └──[C 拼回头文件]──────────────────────────────── tupian.bmp整条链路里Verilog 只负责中间「像素重排」这一段前后两头都是 C 在干脏活。这种分工在课程作业里很常见因为 Verilog 直接读写 BMP 文件头比较别扭用 C 做格式转换更省事。但要注意这个设计里 Verilog 部分是一个initial块驱动的仿真模型不是可综合的 RTL时钟是always #5 clk~clk自己产生的所有操作都在 initial 里顺序执行。这一点决定了它只能跑仿真不能下板。2.2 三个核心存储器的角色划分代码里定义了三个关键存储结构理解它们的尺寸和用途是读懂整个设计的前提存储器声明深度位宽作用ram8bitsreg[0:7] ram8bits[0:size8]1966088 bit存放 txt 读入的原始字节流ram24bit1reg[0:23] ram24bit1[0:size24]6553624 bit三字节拼成一个像素原始排列ram24bit2reg[0:23] ram24bit2[0:size24]6553624 bit旋转后的像素排列其中size2465535size8(size241)*3-1196607正好对应 256×256 个像素、每像素 3 字节。ram8bits 是字节级缓存ram24bit1 和 ram24bit2 是像素级缓存。这种「先按字节收、再按像素拼、最后按像素重排」的三级结构是处理非对齐位宽数据时的常见做法。我一般会把这层叫「位宽对齐层」因为文件读进来是字节流而旋转算法关心的是像素整体必须先做一次 8bit→24bit 的聚合。2.3 坐标重映射顺时针 90 度的核心公式旋转的本质是坐标变换。对于 256×256 的图像原图坐标(row, column)旋转后落到新图的哪个位置代码里是这么写的for(column0;column256;columncolumn1) begin for(row0;row256;rowrow1) begin post_position column*256row; // 原图线性地址 current_position 256*(255-row)column; // 旋转后线性地址 (posedge clk) ram24bit2[current_position] ram24bit1[post_position]; end end这里post_position是原图按行优先展开的线性索引current_position是目标位置。把公式拆开看新地址 256*(255-row) column。其中255-row表示行号翻转column作为新行号参与乘 256。这实际上完成的是「转置 行翻转」合起来就是顺时针 90 度。如果只做256*columnrow那就是纯转置相当于逆时针 90 度再镜像加上255-row才把方向掰正。这个公式是整个项目最值得记住的一行改旋转方向基本就是改它。提示(posedge clk)放在 for 循环体内意味着每搬一个像素消耗一个时钟周期65536 个像素需要 65536 个时钟。按#5半周期算仿真时间约 655360 个时间单位。这个「一个像素一拍」的节奏是理解后面写文件时序的关键。3. 跑通全流程从 BMP 到 txt 再到旋转结果的实际操作3.1 第一步用 C 把 BMP 剥成纯像素二进制流原始 BMP 前 54 字节是文件头和信息头24 位真彩色没有调色板所以从第 55 字节开始全是像素数据。C 程序干的事就是跳过头部把每个字节拆成 8 位二进制字符写进 txt#includeiostream.h #includefstream.h void main(){ short int shi1[27]; // 27 个 short 54 字节正好吃掉文件头 bool shi2[3]; // 每次读 3 字节 1 个像素的 BGR int n256*256; ifstream picture(file1.bmp,ios::binary); ofstream outfile(file.txt,ios::binary); picture.read((char *)shi1,sizeof(shi1)); // 跳过 54 字节头 for (int i0;in;i){ picture.read((char *)shi2,sizeof(shi2)); for(int j0;j3;j){ int shiint(shi2[j]); int s[8]; for(int k0;k8;k){ s[k]shi%2; shishi/2; } for(k0;k8;k) outfiles[7-k]; // 高位先写 outfile ; } } picture.close(); outfile.close(); }参数说明shi1[27]是 54 字节的容器用short int数组是为了凑够头部长度shi2[3]每次读一个像素的蓝、绿、红三个分量。注意这里用bool存字节其实有隐患bool在部分编译器里只有 1 位有效读 0x80 以上的值可能被截断稳妥做法是换成unsigned char。输出格式是每个字节 8 个 0/1 字符加一个空格这样$readmemb才能按位正确解析。这一步产出的file.txt是纯文本体积会比原图大很多但换来的是 Verilog 能直接读。3.2 第二步Verilog 读入、拼接、旋转、写出Verilog 顶层模块zprotate没有端口全靠 initial 块顺序执行。核心分四段读文件、拼像素、旋转、写文件。先看读入和拼接initial $readmemb(file.txt,ram8bits); // 按二进制读入字节流 initial for(counter0;countersize24;countercounter1) ram24bit1[counter] {ram8bits[3*counter], ram8bits[3*counter1], ram8bits[3*counter2]};$readmemb按二进制格式读正好匹配上一步写的 0/1 字符串。拼接时把连续三个字节拼成一个 24 位像素注意 BMP 的像素存储顺序是 B、G、R这里原样保留旋转阶段不关心颜色通道顺序只要字节整体跟着走就行。拼接完成后 ram24bit1 里就是 65536 个完整像素。旋转段就是 2.3 节那个双重 for 循环这里不再重复。重点看写文件段它有个容易翻车的时序依赖initial begin wait (column256) // 等旋转循环跑完 for(x0;xsize24;xx1) $fwrite(file,%h ,ram24bit2[x]); // 十六进制写出 $fclose(file); $readmemh(ram24bit2.txt,mem1); // 再读回来 file2$fopen(final.txt); for(i0;isize24;ii1) begin (posedge clk) {mem2[i*3],mem2[i*31],mem2[i*32]} mem1[i]; // 24bit 拆回 3 字节 for(j0;j3;jj1) $fwrite(file2,%d ,mem2[i*3j]); // 十进制写出 end $fclose(file2); end这里wait(column256)是个同步点因为旋转循环结束时 column 刚好加到 256。但column是 integer在旋转循环里是阻塞赋值递增的wait 能等到它说明两个 initial 块在同一个时间线上推进这种写法依赖仿真器的调度顺序换仿真器可能出问题。更稳的做法是用一个done标志位旋转完置位写文件块等这个标志。另外%h写出的是十六进制$readmemh读回来正好匹配%d写出十进制是给后面 C 还原用的因为 C 那段是按整数读的。3.3 第三步C 把像素流拼回 BMP最后一步把旋转后的十进制像素值转回二进制字节前面接上原始文件头#includeiostream.h #includefstream.h void main(){ char ch; int a[256*256],s[8]; ifstream touwenjian(file1.dat,ios::in|ios::binary); // 之前备份的 54 字节头 ifstream xiangsuzhi(result.txt,ios::in|ios::binary); // Verilog 输出的十进制像素 ofstream shuchutu(tupian.bmp,ios::out|ios::binary); while(touwenjian.get(ch)) shuchutu.put(ch); // 先写回头部 for(int i0;i256*256;i) xiangsuzhia[i]; for(int j0;j256*256;j){ for(int k0;k8;k){ s[k]a[j]%2; a[j]a[j]/2; } for(k0;k8;k) shuchutus[7-k]; // 十进制转回 8 位二进制 } touwenjian.close(); xiangsuzhi.close(); shuchutu.close(); }注意这里读的是file1.dat而不是file1.bmp说明第一步之前得先把 54 字节头单独备份一份否则原图被覆盖或找不到就麻烦了。result.txt对应 Verilog 输出的final.txt中间可能改过名。这段代码把每个十进制像素值反复除 2 取余还原成 8 位再按高位先写拼回字节最后和头部拼成完整 BMP。跑完打开tupian.bmp如果看到 Lena 图被顺时针转了 90 度整条链路就通了。4. 避坑与排查这类仿真项目最容易翻车的五个地方4.1 现象$readmemb读进来全是 x拼接后像素全黑原因通常是 txt 格式和读函数不匹配。$readmemb要求每段是纯二进制 0/1如果 C 写的时候带了多余空格、换行位置不对或者用了bool导致高位丢失读进来就是 x。解决把 C 里的bool shi2[3]改成unsigned char shi2[3]输出时严格每 8 位加一个空格不要有多余空行。读之前先用文本编辑器抽查前几行确认是01011010 00110011 ...这种规整格式。4.2 现象旋转结果图出现斜条纹或错位多半是坐标公式写反了。current_position 256*(255-row)column里如果误写成256*(255-column)row得到的是逆时针 90 度如果漏掉255-得到的是转置。排查方法拿一个 2×2 的小图手工推一遍四个像素的落点和公式算出来的地址对一遍比盯着 256×256 的结果猜要快得多。我一般会先造一个 4×4 的测试图跑通再上大图血泪经验。4.3 现象wait(column256)永远等不到仿真卡死column在旋转循环里是integer循环条件是column256正常退出时 column 确实等于 256。但如果仿真器对多个 initial 块的调度顺序不同写文件块可能在旋转块之前就开始 wait而旋转块的 column 递增是阻塞赋值理论上能推进。真正卡死的情况往往是旋转循环里(posedge clk)没等到时钟比如 clk 的 initial 被注释掉了。解决确认always #5 clk~clk存在并且把同步方式改成显式握手标志别依赖 integer 的终值。4.4 现象final.txt里像素数量不对BMP 打不开$fwrite(file2,%d ,...)每个像素写 3 个十进制数65536 个像素应该写 196608 个数。如果数量少了检查mem2的声明尺寸是否够大——代码里mem2声明为reg[0:7] mem2[0:size8]size8 是 196607刚好够。如果mem2声明小了后面的写入会越界被丢弃。另外 C 还原时读result.txt是按int读如果 Verilog 写的是十六进制%h而 C 按十进制读数值会全错两边格式必须对齐。4.5 现象换一张图就崩或者图像上下颠倒这个设计硬编码了 256×256size2465535是写死的。换尺寸必须同步改size24、size8、循环边界和坐标公式里的 256。另外 BMP 有「行倒序」特性文件里第一行数据其实是图像最底下一行。如果原图不是标准自底向上存储或者用了自顶向下标志旋转结果会上下颠倒。排查时先用画图工具确认原图是 256×256×24bit 标准格式再检查文件头第 22 字节附近的高度符号位。5. 二次开发把仿真模型改成参数化、可扩展的旋转流水线这个项目作为作业是完整的但作为可复用代码硬编码太多。我一般会从三个方向改。第一参数化尺寸。把256抽成parameter WIDTH256, HEIGHT256size24改成WIDTH*HEIGHT-1坐标公式改成WIDTH*(HEIGHT-1-row)column这样换图只改参数不改逻辑。第二把 initial 块里的顺序逻辑改成真正的时序流水线读地址、读数据、算目标地址、写数据分成四级寄存器用 valid 信号串起来这样才有机会往可综合方向靠。第三文件 IO 和计算解耦Verilog 只负责地址映射读写交给 testbench 的$readmemh/$writememh主模块保持纯净。验证方法上别一上来就跑 256×256。先造一个 4×4 的 BMP手工算出顺时针 90 度的期望结果跑完逐像素比对再上 16×16 看边界最后才跑 Lena。每次改完坐标公式都用小图回归一遍这比盯着大图肉眼找错靠谱得多。下面这个参数化骨架可以直接抄module rotate_core #( parameter WIDTH 256, parameter HEIGHT 256 )( input wire clk, input wire rst_n, input wire start, output reg done, output reg [17:0] rd_addr, // 读地址最大 65535 input wire [23:0] rd_data, output reg [17:0] wr_addr, output reg [23:0] wr_data, output reg wr_en ); reg [17:0] row, col; // 顺时针 90 度新地址 WIDTH*(HEIGHT-1-row) col always (posedge clk or negedge rst_n) begin if(!rst_n) begin row0; col0; done0; wr_en0; end else if(start) begin rd_addr col*WIDTH row; wr_addr WIDTH*(HEIGHT-1-row) col; wr_data rd_data; wr_en 1b1; if(col WIDTH-1 row HEIGHT-1) begin done 1b1; wr_en 1b0; end else if(row HEIGHT-1) begin row 0; col col 1; end else begin row row 1; end end end endmodule这个骨架把地址计算和读写握手拆开了rd_addr和wr_addr各自独立外部接双端口 RAM 就能跑。参数说明WIDTH/HEIGHT控制图像尺寸rd_addr/wr_addr是 18 位宽因为 65536 需要 17 位、留一位余量done给上层做结束判断。改旋转方向只需改wr_addr那一行公式比如逆时针就是WIDTH*row (HEIGHT-1-col)。从那以后我每次做这类图像搬运算例都强制先用 4×4 小图把地址公式跑通再放大这个习惯帮我省了太多对着大图猜错位的夜晚。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/5 2:22:36

基于STFT与LBP特征的海杂波小目标VFSVM检测实战

简介:该资源面向雷达信号处理与海杂波目标检测方向的学习者和研究者,提供基于IPIX数据集的时间多普勒频谱视觉特征分类方案,用于在海杂波背景下检测小浮标目标。核心思路是先将连续回波转换为时间多普勒频谱图像,再计算每幅图像的…

2026/11/5 2:22:36

铅锤面内无控弹道仿真:Python工程实现与RK4数值积分

简介:这份资源面向学习数值计算与工程仿真的Python使用者,聚焦铅锤面内无控飞行导弹的弹道求解与可视化问题。包内共7个文件,以3个py脚本为主,分别承担数据读取、三维插值与弹道计算,另配2份doc说明文档、1个xlsx参数/…

2026/11/5 2:22:36

Linux密码安全与系统防护实践指南

1. 密码安全与系统防护基础在Linux系统管理中,密码安全是系统防护的第一道防线。现代Linux系统普遍采用shadow密码机制,将加密后的密码哈希值存储在/etc/shadow文件中,而非早期的/etc/passwd文件。这种设计使得密码文件与用户信息分离&#x…

2026/11/5 3:22:45

dsh-purge 完整指南:三步生效的 Harness 破甲,每步可回退

dsh-purge 完整指南:三步生效的 Harness 破甲,每步可回退 【免费下载链接】dsh-purge DeepSeek Harness 破甲:让所有模型都能破甲,不同模型可换不同提示词;默认提示词面向国模「小码酱」。Jailbreak for every model —…

2026/11/5 3:22:45

【Java基础知识】关键字与标识符

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 3:22:45

【java基础知识】 快捷键使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 3:22:45

【Java基础知识】数据类型与类型转换

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 3:17:45

ADS122U04与PIC18实现高精度PT100温度采集全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 0:02:07

多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中…

2026/11/5 0:02:07

大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#…

2026/11/5 0:02:07

鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑