项目成果
见投稿视频
项目设计
注:由于开发板存储量较小,采用纯黑白显示以减小数据量,实际上彩色显示设计方式是完全相同的。
使用的软件
- Vivado 2019.1
- VSCode(仅用作辅助Vivado编写Verilog程序)
- MATLAB R2022a (9.12.0.1884302) - Academic Use
- Adobe Photoshop CC 2018
使用的硬件
- BASYS 3 FPGA开发板
- COMPAQ 显示器
- VGA-VGA转接线等
用户端设计
- 利用VGA-VGA线将BASYS3 FPGA开发板接入显示屏,并在显示屏上显示动态gif(取自《Bad Apple!!》中开头的博丽灵梦,共5帧,帧率为7fps)
其中一帧
- 开发板最右侧的开关为电源开关,可以实现显示屏开启并显示动态图片,或显示屏关闭并动态图片复位至初始的操作,其上方配有电源指示灯
- 最左侧自左向右五盏LED灯为流水灯,分别顺序对应gif的五帧,表示当前显示的是第几帧
- 中间按钮为pause键,按下可以实现暂停功能,即停在当前帧,放手即继续
- 上侧按钮为reset键,按下可以实现回到第一帧,并停在第一帧,放手即继续
BASYS 3 FPGA开发板
程序设计
- 采用三模块并行设计,即Display(vga显示模块)、Player(播放器模块)与Storage(数据调用模块)
三模块并行设计
- 其中的Storage模块中调用了开发板的BRAM(Block RAM)IP核,用于存储图片数据
- BRAM IP核数据来自.coe初始化文件,此文件利用Matlab程序将.bmp(24位图)图像解码处理后生成
VGA显示模块(顶层模块):vga_bad_apple_display.v
- 设置好VGA显示标准的行扫描与列扫描参数、播放相关参数,方便后续调用
- 接入开发板系统时钟100MHz,用作各模块进一步分频处理使用
- 接入switch开关,用于开闭电源
- 接入pause和reset按钮,将其接入播放器模块,用于暂停与复位(其中reset信号将开关与按钮的信号结合后再接入Player模块,因为两者都可以提供复位信号)
- 采用640*480、12位RGB的VGA显示标准,使用25MHz时钟进行行、场扫描,并输出行、场同步信号,以及RGB数据
- 将行、场扫描的位置(即当前像素位置)传给数据调用模块,请求色彩信息
- 获取数据调用模块返回的的色彩信息,用于输出至VGA接口的RGB数据
- 将switch、reset及帧位置、帧速度反映在六盏LED灯
播放器模块:vga_bad_apple_player.v
- 设置好动图显示的帧率、帧数相关参数,方便后续调用
- 从顶层模块接入100MHz时钟信号,用作生成帧率(帧切换速度)所需时钟
- 从顶层模块接入pause信号,控制帧计数(切换至第几帧)暂停与继续
- 从顶层模块接入处理后的reset信号,控制帧复位
- 向数据调用模块输出帧编号,确定VGA显示模块调用的是数据调用模块中哪一帧的数据
数据调用模块:vga_bad_apple_storage.v
- 设置好动图显示的长、宽及帧数参数,方便后续调用
- 从VGA显示模块和播放器模块接入需要的像素位置、帧编号信息,生成所需要的数据地址
- 从顶层模块接入100MHz时钟信号,再接入BRAM
- 根据数据地址从BRAM中调取数据,并确定调出的数据中的第几位为所需要的对应的色彩信息
- 向VGA显示模块返回当前需要显示的色彩信息
IP核BRAM模块:bad_apple_bram.xci
- 利用IP核生成一个用于存储数据的BRAM模块,位宽为2bit,位深为768000bit(是由于BRAM的位深至多为1048576bit,不然最方便调用的方式为位宽为1bit且位深为1536000bit)
- 将模块设置为always enable(永远工作)
- 利用.coe文件初始化此存储模块(本质即存入图片数据)
- 从数据调用模块接入100MHz系统时钟
- 将wea端(读写使能端,当置0则只读,当置1则只写)置0
- 将dina端(数据写入端)置0
- 从数据调用模块接入所需数据地址
- 向数据调用模块传回对应的位宽为2bit的数据
其他设计
利用Matlab解码.bmp(24位图片文件)生成.coe(二进制数据文件)用于BRAM的初始化
- 参考了CSDN上的代码,程序读取了.bmp文件中每一像素点的24位RGB值,削去每个R、G、B值的后四位,生成每个像素点的12位RGB值,并以16进制形式打印在.coe文件中,同时显示所打印的图片
- 由于本项目只需要像素点的黑白值,且要求位宽为2位,故将代码做部分改动,使得若像素点RGB值为0,则打印0,否则打印1,并且每逢偶数个像素点打印一个逗号并换行,并在最后生成一个分号而不是逗号,作为终止标志
- 由于项目帧数较少,采用5帧分别打印在5个文件,再将5个文件合并这一方式生成最终的.coe文件
利用Photoshop将.gif文件转化为只含纯黑白像素点的各帧的.bmp(24位图)文件
- 直接使用Photoshop打开.gif文件,发现各帧自动按顺序分离在各图层,此时的各帧依然有灰色像素点
抽帧出的原图
- 选取所需要的帧图层重新命名,用于区分其他不需要的帧图层
- 将所需处理的帧图层可视,其他不可视
- 在左上方“图像”菜单中选择“调整”中的“阈值”
- 调整“阈值”的数值,至看起来轮廓较为流畅,并记下这一数值
- 确定后,放大图片至像素点清晰可见,可以看到只有黑像素和白像素
处理完的帧
- 对于所有所需帧图层进行相应操作后,点击左上角的“文件”菜单,在其中找到“导出”,选择”将图层导出至文件“,并选择.bmp24位图格式与需要导出的文件夹
- Photoshop会自行进行操作,完毕后会提示导出完毕
全部处理完毕并重命名完毕的24位图bmp文件
项目思路整理与思考
注:后文中可能会出现以下人物
- Starskyer:我的室友,同样选择荣誉课程的、一同想好尝试做动图显示的好友
- Yosame:我的好友,是一位计算机学院的大佬
关于最终呈现方式的不断改变
- 最开始尝试完成的是彩色图片,并且希望完成的是彩色的动图显示
- 但是将一帧图片以简单粗暴的方式导入进代码中后发现占用了99%的LUT空间,并且完成从零开始综合、实现、生成比特流总共要花掉二十多分钟的时间
- 在这之后,思考了各种各样的办法:
关于直接将数据粘贴在代码中
- 最初的那张无声铃鹿的《うまぴょい伝説》使用了13万行代码,跑了20分钟终于成功显示了出来
无声铃鹿主C《うまぴょい伝説》静态图片显示
- 在后来的《Bad Apple》中,一开始也尝试使用30万行代码作为显示数据,但是无论是在我自己的电脑上还是在Starskyer的电脑上都跑不出来(跑了一个半小时还卡在第一步)
- 所以这个方案:驳回
关于开发板的BRAM IP核作为读写过渡存储器
- 由于“将数据直接粘贴进代码”这一方式不可行,因此我们只能读取BRAM IP核,而不能写入,这就导致BRAM变成了一个ROM,因此只能把它初始化
关于开发板的Flash
- 这一方面确实有相关的资料,但是由于过程非常复杂,没有探索下去
关于开发板的USB接口
- 当跳线帽决定开发板的编程模式为“USB”时,USB存储设备才会发挥作用,并且只不过是找到其中的编程文件,并对开发板进行编程
- 当跳线帽不在“USB”模式时,USB存储设备只能作为一个PS/2接口使用,即只能识别键盘和鼠标
- 因此如果是想通过USB口外接存储设备供开发板提取数据的话,应该是办不到的
关于Yosame提到的拟合方法
- Yosame提到说对于动图的显示可以采用每个像素点拟合一个函数的形式,减小数据存储量
- 我非常喜欢这个办法,因为这相当于是只需要一帧的数据,而将后续的数据所需的存储量以拟合的运算量这一形式呈现在函数之中了,这正是一种很棒的减小数据存储量的办法
- 但是由于时间有限,一方面我们毫无头绪如何去做这个事,另一方面我们也没有时间或者算力去完成这一工作
最终选择:开发板的BRAM IP核作为只读存储器
- 我们前面提到将BRAM作为读写过渡存储器难以实现,但是我们如果干脆将其变成一个ROM,然后减小一定的数据量,是可以实现弱一些的功能的
- 可以说是无奈之举了吧
- 本BRAM可装载的最大数据量是1800Kbits,而一张640480的图片便包含307.2Kbits,因此要么使用小彩图,要么使用大的黑白图,而且都是在帧数很少的情况下,因此Starskyer选择了160120的4帧彩色动图,而我选择了640*480的5帧黑白动图
- 使用BRAM IP核的优点在于,程序跑的非常快,实际数据量却和直接粘贴入代码相当,一般来说大约2~3分钟就可以生成好比特流了
关于代码的不断改进
单模块
- 最最开始使用的是单模块设计,原因是是从东大实验书上借鉴下来的
- Vivado本身也会提醒你“单模块设计不利于层次结构设计”,另一方面确实所有东西混在一起很难看也很混乱
- 最混乱的还属时序的always块,在静态图片显示中还体现不出来,但是一旦到了动图显示,各种时钟交杂,会非常混乱
三模块初设计
最初的三模块串行设计
- 到项目收工的前一天晚上,我的代码还是基于这张图的,那天晚上结果已经出来了,和最后的结果只是差几个组合电路设计的不同
- 问题最大的是Player模块,其实当时就有点感觉奇怪了,怎么这么多线接进去又原封不动接出来,这也太浪费资源了
- 项目收工当天早上,我开始给我的代码系统性地加注释,在工程中慢慢把各种没用的输入输出接口给删掉了
三模块最终设计
三模块并行设计
- 说实话非常清爽,本来的Player模块杂七杂八要接了靠十根线,最后只剩下三根线,reset这根线还是我最后在检查组合电路设计的时候加上去的
- 一开始我是没有接入reset按钮的,但是后来我发现我把switch和reset完全搞混了,因此加入了reset按键,一个负责控制总开关,一个负责控制播放器的开关
关于仿真与测试
- 说实话,并不是很想用Simulation文件来进行仿真与测试,因为非常复杂并且很难看到直观结果
- 因此更多是使用直接改程序的方式来进行测试
- 实际使用的比如说是先不输入数据,而是把模块的数据接入一个简单的选择器,设置第一帧到第五帧分别是从小到大的方块等等,这样很直观,也帮助我找出了问题并改正
- 再有就是利用LED灯的闪烁来查看,这一设计帮助我找出了两次帧编号变量设置的问题(一次是将bit数当成总数使用了,一次是使用了1位变量接周期为5的计数器了)
关于图片解码
- 我一开始只是看到在粘贴过来的matlab程序中详细写了各种注释,了解到是取用的24位RGB中各自的前5、6、5位作为16位RGB,或是前4、4、4位作为12位RGB,并没有深入思考
- 后来在与Starskyer的讨论之中恍然大悟,原来取前几位是做除法的意思,那就没问题了
- 但是我在显示这块依然有两个完全不明白的点
为什么电脑上显示的640*480的清晰程度和显示屏显示出来的感觉完全不一样?
- 有没有一种可能,是显示器更大了?这个我感觉不太可能,因为连显示出来的图片本身都有些边缘崩坏
- 因此我强烈怀疑是显示器自己对于像素信号进行了处理,但是这块我的了解并不多
为什么使用一模一样的程序解码出来的图片在显示屏上显示出来只有第一帧的边缘时崩坏的,而后面4帧边缘都是很顺滑的
第一帧显示出来的样子
后面4帧中的一帧
- 这方面我进行了一定的探索,包括重新用Photoshop对图片进行处理,以及改变那特定第一帧的选取,还有在Matlab代码里改变黑白阈值这些尝试,都没能解决这一问题
- 因此承上一个我问题,我估计大概率还是这个显示屏自行对信号进行了处理,然而这点也是存疑的,因为为什么只有第一帧处理坏了,后面都是好的呢
- 总体而言对于这个问题还是毫无想法
项目感想
- 是真的很有意思,把一张自己想要显示的图显示到屏幕上,甚至还可以是动图,确实是很有成就感的事情
- 在完成自己的想法的过程中,在网上查了各种资料,与好友、学长、助教老师等等身边的人进行了各种讨论,也都非常有益,学到了非常多东西
- 还有我爸非常支持我,直接让我从家里搬来台式机的显示屏,并且提供了VGA-VGA线和电源线,直接解决了我们寝室的显示屏缺位问题,可以说是我们顺利做项目的先决条件,在做的过程中也常常同我讨论各种问题,也都是很有益的过程
- 当迷雾渐渐拨开,最后发现其实一切根本没想象中的那么难,我整个项目总的代码量不过230行左右,但是可以说很搞脑子,而且在探索软件、硬件方面需要下很多功夫,不像别的同学设计游戏、设计交通灯、设计计时器,可能更多是在设计方面需要下功夫,侧重点不同
- 在代码编写的过程中,也慢慢形成了自己的代码风格,一方面体现了一些自己的个性,另一方面也让自己看着很愉悦
- 这只是荣誉课程最终项目的先导,最后的压轴:RISC CPU设计还在等着我们,感觉可以拿出干劲好好研究研究了!