简介:这份资源面向数字电路与FPGA学习者,聚焦用Verilog实现图像旋转这一典型课程设计任务,适合正在做相关大作业或想理解硬件描述语言如何操作图像数据的中高年级学生。压缩包内共1个doc文档,约267KB,为完整的大作业设计说明书,涵盖作业简述、设计思路、Verilog代码、C++代码及参考资料等部分。说明书详细拆解了BMP文件格式的四个组成部分,给出将256×256×24bit图像转为二进制流并存入txt文件的C++实现,再通过Verilog模块按时钟节拍读取像素、重新编排存储位置完成顺时针旋转90度,并涉及模块划分、testbench读写与ModelSim仿真验证等环节。目前已有1104人学习下载,读者可据此理解从图像格式解析到硬件模块设计、仿真测试的完整流程,并参考其中的模块划分与排错思路完成自己的设计任务。
1. 一份把 Lena 图转 90 度的 Verilog 大作业,到底能拿来干什么
手头这份资源,是一份完整的“图像旋转”课程设计包:用 Verilog 描述一个能把 256×256、24bit 的 BMP 图像顺时针旋转 90 度的数字电路,配套 C++ 做 BMP 与二进制文本之间的来回转换,外加设计说明书、模块划分思路和仿真结果。它解决的不是“怎么用软件旋转图片”这种随手就能做的事,而是“怎么让硬件按时钟节拍把像素搬对位置”——这件事在 FPGA 图像处理入门里几乎是必踩的一道坎。适合谁?正在做数字电路课程设计、想搞明白$readmemb/$fwrite怎么在 testbench 里读写真实图像文件、以及想把“存储器重排”这套思路迁移到其他图像变换上的从业者和学生。下面按“资源是什么 → 怎么跑通 → 坑在哪”的顺序拆。
2. 先立住原理:BMP 头、像素阵列与旋转映射
2.1 为什么必须先把 BMP 拆成裸像素流
Verilog 仿真器不认 BMP 的文件结构,它只认位向量。BMP 对 24bit 真彩色图像来说结构很规整:前 54 个字节是文件头(14 字节位图文件头 + 40 字节位图信息头),之后才是像素阵列,每个像素 3 字节,顺序是 B、G、R。256×256 的图,像素阵列正好 196608 字节,加头 196662 字节。资源里的 C++ 程序干的第一件事,就是读掉这 54 字节头,把剩下的每个字节拆成 8 位二进制、以空格分隔写进file.txt,供$readmemb逐位读入。
这里有个容易被忽略的细节:BMP 的行是按“从下到上”存储的,且每行字节数要补齐到 4 的倍数。256×256×3 = 196608,本身已是 4 的倍数,所以这份作业不用处理行补齐;但如果你换成 250 宽的图,每行 750 字节要补到 752,直接按 3 字节步进读就会错位。常见做法是先算stride = ((width*3 + 3) / 4) * 4,再按 stride 跳行。
2.2 顺时针 90 度的坐标映射怎么推
设原图坐标(row, column),row是行号(0 在图像顶部),column是列号。顺时针旋转 90 度后,新图宽高互换,原图第row行第column列的点,落到新图的第column行、第(255 - row)列。换成线性下标:原位置post = row*256 + column,新位置cur = column*256 + (255 - row)。资源里那段双重循环就是照这个式子写的,只不过它把post_position和current_position的赋值写反了语义——变量名叫 post 的其实是目标地址,读代码时别被名字骗了,对着公式核一遍最稳。
提示:旋转方向(顺/逆时针)只影响映射式里的
255-row还是255-column,先把 3×3 小图手推一遍再写循环,比在 256×256 上盲调快得多。
2.3 存储器重排 vs 流式搬移,选哪个
这份资源走的是“双 RAM 重排”路线:先把全部像素读进ram24bit1,再按映射写进ram24bit2,最后顺序写出。好处是逻辑直白、验证容易;代价是占两块 64K×24bit 的存储,综合到真实 FPGA 上 BRAM 吃紧。如果目标是上板而不是仿真,更常见的做法是流式:读地址按行扫描,写地址按列步进(写地址每次加 256),只缓存一行或一列,存储开销降到 1/256。课程设计阶段用重排法没问题,但答辩时被问“资源怎么优化”,你得答得出这个取舍。
3. 把流程跑通:三段式转换与 Verilog 读写
3.1 第一步:BMP 转二进制文本
资源给的 C++ 是老式iostream.h写法,在现在的编译器上直接编不过。我一般会改成标准头,并把“读 54 字节头”写死成常量,避免short int shi1[27]这种靠类型大小凑字节的写法:
#include <fstream> #include <iostream> using namespace std; int main() { const int HEADER = 54; // 24bit BMP 固定头长 const int PIXELS = 256 * 256; // 像素个数 ifstream pic("file1.bmp", ios::binary); ofstream out("file.txt", ios::binary); char header[HEADER]; pic.read(header, HEADER); // 丢弃文件头 unsigned char px[3]; for (int i = 0; i < PIXELS; ++i) { pic.read((char*)px, 3); // 每像素 B,G,R 三字节 for (int j = 0; j < 3; ++j) { for (int k = 7; k >= 0; --k) out << ((px[j] >> k) & 1); // 高位在前,逐位输出 out << ' '; } } pic.close(); out.close(); return 0; }逻辑说明:HEADER是 24bit BMP 的固定偏移,读掉即跳到像素区;内层k从 7 到 0 保证高位先出,和$readmemb按位读入的顺序对齐。参数上唯一要改的是PIXELS,换图尺寸时同步改,并确认新图仍是 24bit、无压缩(信息头偏移 30 处的压缩字段为 0)。
3.2 第二步:Verilog 读入、重排、写出
核心模块用$readmemb把file.txt读进 8bit 数组,再拼成 24bit 像素。下面是我整理过的可读版本,把原资源里混在initial里的时序逻辑拆清楚:
module img_rotate; parameter N = 256; parameter PIX = N*N; // 65536 reg [7:0] ram8 [0:PIX*3-1]; // 逐位文本读入 reg [23:0] src [0:PIX-1]; // 原始像素 reg [23:0] dst [0:PIX-1]; // 旋转后像素 integer f, i, row, col, post, cur; initial begin $readmemb("file.txt", ram8); for (i = 0; i < PIX; i = i + 1) src[i] = {ram8[3*i], ram8[3*i+1], ram8[3*i+2]}; for (col = 0; col < N; col = col + 1) for (row = 0; row < N; row = row + 1) begin post = row*N + col; // 原图线性地址 cur = col*N + (N-1-row); // 顺时针 90 度目标地址 dst[cur] = src[post]; end f = $fopen("result.txt"); for (i = 0; i < PIX; i = i + 1) $fwrite(f, "%d %d %d ", dst[i][23:16], dst[i][15:8], dst[i][7:0]); $fclose(f); end endmodule逻辑说明:ram8长度是像素数乘 3,因为每个像素拆成 3 个字节、每字节 8 位;拼接时按 B、G、R 顺序还原。写出用%d十进制,是为了让第三步的 C++ 直接>>读整数,省掉再解析二进制串。参数N改尺寸时,PIX自动跟着变,但file.txt和result.txt的生成脚本要同步改,这是最容易漏的一环。
3.3 第三步:文本转回 BMP
把原始 54 字节头拼回旋转后的像素流,就得到可查看的 BMP:
#include <fstream> using namespace std; int main() { const int HEADER = 54, PIXELS = 256*256; char header[HEADER]; ifstream hdr("file1.bmp", ios::binary); ifstream px("result.txt"); ofstream out("rotated.bmp", ios::binary); hdr.read(header, HEADER); out.write(header, HEADER); // 原样写回头 int r, g, b; for (int i = 0; i < PIXELS; ++i) { px >> b >> g >> r; // 注意 BMP 存的是 B,G,R out.put((char)b); out.put((char)g); out.put((char)r); } hdr.close(); px.close(); out.close(); return 0; }逻辑说明:头信息里的宽高字段在旋转后其实应该互换,但 256×256 是方图,互换后数值不变,所以这份作业直接复用原头也能正常显示;换成非方图就必须改偏移 18(宽)和 22(高)两个 4 字节字段,否则看图软件会按错误尺寸解析。px >> b >> g >> r的读取顺序必须和上一步$fwrite的写出顺序严格一致,错一个通道就是“颜色整体偏蓝/偏红”的经典翻车。
4. 避坑与排查:五个真实会卡住你的地方
4.1 现象:仿真跑完result.txt是空的
原因多半是$fopen返回 0 却没检查,或者$fwrite写在initial的阻塞赋值链里、被后面的语句覆盖。解决:$fopen后加一句if (f == 0) $display("open fail");,并把写文件放在所有重排完成之后,用wait或独立initial保证顺序。
4.2 现象:旋转出来的图是镜像或转了 180 度
原因是 BMP 行序自下而上,而代码按自上而下处理,或者映射式里255-row写成了row。解决:先用 3×3 手推表核对四个角点,确认(0,0)应落到(0,255)、(0,255)落到(255,255),再放大到 256。
4.3 现象:$readmemb报 “too many words” 或读入错位
原因是file.txt里空格/换行不规范,或每像素位数和数组宽度不匹配。解决:确保每个字节恰好 8 个 0/1 加一个分隔符,数组声明长度等于像素数×3,别用65535这种差一的魔数。
4.4 现象:综合上板后图像错行、花屏
原因是双 RAM 重排占满 BRAM,综合工具把部分存储推到分布式 RAM,时序或容量出问题。解决:仿真阶段够用就行;要上板就改流式写地址(写地址每次加 256),把缓存降到一行。
4.5 现象:换一张非 256×256 的图就全乱
原因是头长、行补齐、宽高字段三处都写死了 256 的假设。解决:把HEADER、stride、宽高字段偏移做成参数,转换脚本里从信息头动态读宽高,别信“反正方图不用改”的侥幸。
5. 进阶:把重排思路做成可复用的验证骨架
跑通一次旋转不算完,真正值钱的是把这套“文件进、文件出”的流程固化成验证骨架,换任何图像变换都能套。我的习惯是抽一个img_tb顶层,把读文件、调用变换模块、写文件三段分开,变换模块只留纯组合或单时钟的地址映射,这样换旋转、镜像、转置只改一个映射函数。
| 变换 | 目标地址公式(N×N) | 备注 |
|---|---|---|
| 顺时针 90 | col*N + (N-1-row) | 本资源采用 |
| 逆时针 90 | (N-1-col)*N + row | 与顺时针互为逆 |
| 水平镜像 | row*N + (N-1-col) | 只改列 |
| 垂直镜像 | (N-1-row)*N + col | 只改行 |
验证方法上,别只靠肉眼看图。我一般会额外生成一张“坐标图”——每个像素值直接填它的线性下标低 8 位,旋转后检查四个角的值是否符合公式,比对着 Lena 图找差异可靠得多。另外,$fwrite用%h还是%d要和回读脚本对齐,混用是黑匣子式排错的源头;我吃过一次亏,%h写出的十六进制被 C++ 当十进制读,颜色全错还查了半天。
从那以后我每次做这类文件级仿真,都强制先跑 3×3 小图、再跑坐标图、最后才上真实图像,三步都过才敢交。希望帮到你。
本文还有配套的精品资源,点击获取