声明:本博客仅供学习参考,请勿作出直接抄袭等违反学术诚信的行为
实验环境
- 软件:Vivado 2020.2
- 硬件:Nexys A7-100T开发板
本门课程的实验环境似乎有两种,代码逻辑可能有所不同,请自行注意
实验主要目标
- 结合键盘模块,按键逻辑正常
- 屏幕有显示
- 在前面的基础上,实现删除、退格、清屏等进阶操作
- 输入特殊字符串,按下回车后显示对应自定义内容
个人思路与实现
前言
首先本次实验附件实现的内容是正确的,不需要你改动,即vgadraw、vgactrl、vgasim的逻辑没有问题,不需要你更改。vgactrl中的参数建议改成640*480比例的,后面实验会方便一点。参数更改如下:
parameter H_Sync_Width = 96;parameter H_Back_Porche = 48;parameter H_Active_Pixels = 640;parameter H_Front_Porch = 16;parameter H_Totals = 800;parameter V_Sync_Width = 2;parameter V_Back_Porche = 33;parameter V_Active_Pixels = 480;parameter V_Front_Porch = 10;parameter V_Totals = 525;
总览
然后我们先梳理一下给出的实验文件结构:.coe文件都是静态图像部分的,scancode文件是键盘部分的,clkgen文件是生成时钟的模块(我没有使用),ASC16是字模文件,vgactrl负责生成同步信号和x轴y轴位置,vgadraw是一个函数,根据vgactrl部分的x轴y轴信息生成颜色信号,vgasim模块将vgactrl和vgadraw实例化,然后整理后统一输出VGA的各种信息
梳理完再看实验要求,我们要实现键盘和屏幕的交互,还要实现简单的伪终端行为,还要实现伪应用功能切换显示动态图片/静态图片/文字。所以我们的设计思路如下:以xterm为主文件,和屏幕键盘交互;先更改移植之前的键盘模块,使之能正常输出信号;然后设计实现伪终端界面,包括键盘信号译码、按字母取字模、按字模信号在屏幕上对应位置显示黑白像素等基础操作,以及按下del键、回车键、方向键等的进阶逻辑;然后设计动态图像模块、静态图像模块、文字模块,将其在xterm中实例化,并设计状态机实现终端到这些模块再回到终端的转换
键盘模块
这个前面的实验实现过了,所以没什么好说的,我的实现如下
module kbd(input fastclk,input PS2_CLK,input PS2_DATA,output val,output [7:0] ascii,output [7:0] ctl,output c50hz);reg [7:0] kb_mem[255:0];initialbegin$readmemh("?/scancode.txt", kb_mem, 0, 255);endreg [20:0]clk_dis = 0;reg clk_50hz = 0;always @(posedge fastclk) begin //100mhzif (clk_dis >= 1000000) begin //50hzclk_dis <= 0;clk_50hz <= ~clk_50hz;end else beginclk_dis <= clk_dis + 1;endendreg [3:0]cnt = 0;reg [7:0]datacur = 0;reg [7:0]dataprev = 0;reg [7:0]dataprepre = 0;reg [7:0]asciicode = 0;reg [20:0]pressrecord = 0;always@(negedge PS2_CLK)begincase(cnt)0:begin end 1:datacur[0]<=PS2_DATA;2:datacur[1]<=PS2_DATA;3:datacur[2]<=PS2_DATA;4:datacur[3]<=PS2_DATA;5:datacur[4]<=PS2_DATA;6:datacur[5]<=PS2_DATA;7:datacur[6]<=PS2_DATA;8:datacur[7]<=PS2_DATA;9:begin end10:beginif(datacur == 8'hf0 || datacur == 8'he0)begin endelse begin pressrecord <= pressrecord + 20'd1;dataprepre <= dataprev;dataprev <= datacur;asciicode <= kb_mem[datacur];end enddefault:begin endendcaseif(cnt<=9) begin cnt<=cnt+1; endelse begin cnt<=0; endendreg shift_pressed = 0;reg cap_pressed = 0;reg [7:0] current_ascii = 0;reg [7:0] ctlcode = 0;reg valid = 0;reg [20:0] recordrecord = 0;always @(posedge clk_50hz) beginif(datacur == dataprev && recordrecord != pressrecord && dataprepre != dataprev) beginendelse if(datacur == dataprev && recordrecord != pressrecord) beginif(datacur == 8'h58)begincap_pressed <= !cap_pressed;endelse if (datacur == 8'h12 || datacur == 8'h59) beginshift_pressed <= 1;endelse begin shift_pressed <= 0;endrecordrecord <= pressrecord;ctlcode <= datacur;if (cap_pressed&&!shift_pressed || shift_pressed&&!cap_pressed) begincurrent_ascii <= asciicode - 8'h20;end else begincurrent_ascii <= asciicode;endvalid <= 1;endelse beginvalid <= 0;endendassign val = valid;assign ascii = current_ascii;assign ctl = ctlcode;assign c50hz = clk_50hz;endmodule
动态图像模块
这块就是实验附件给出的vgasim,在xterm中实例化使用即可,vgasim中实例化了vgactrl和vgadraw文件,记得包括进项目文件
静态图像模块
按实验pdf创建rom的ip核,把你想显示的coe导入进去,然后模仿vgasim的文件结构实例化vgactrl和ip核即可,记得根据你的coe文件更改addr计算方式
module sim(input CLk,input BTNC,output [3:0] VGA_R,output [3:0] VGA_G,output [3:0] VGA_B, output VGA_HS,output VGA_VS);wire [11:0] cvga_data;wire cvalid;VGACtrl svgactl(.pix_x(),.pix_y(),.hsync(VGA_HS),.vsync(VGA_VS),.pix_valid(cvalid),.pix_clk(CLk),.pix_rst(BTNC));vga_mem my_pic(.clka(CLk),.ena(1'b1),.wea(1'b0),.addra(svgactl.pix_y*640+svgactl.pix_x),.dina(12'd0),.douta(cvga_data));assign VGA_R=cvga_data[11:8];assign VGA_G=cvga_data[7:4];assign VGA_B=cvga_data[3:0];endmodule
文字模块
其具体逻辑和伪终端是相似的,其实就是简化的伪终端,进行初始化后即可进行VGA输出,具体实现原理可以参考下面伪终端模块
伪终端逻辑
首先我们需要一块较大的内存来存储字母信息,这里一般有两种选择:选用分布式ram操作方便,但生成比特流文件需要15-30分钟甚至更长;选用ip核的ram生成比特流速度快,但操作受限。此处我使用的是分布式的ram,读者自行取舍
显存需要进行初始化的操作,若使用ip核可以类比前面的操作,使用coe文件进行初始化;否则需要写一个initial模块初始化一下
//示例reg [7:0] ascii_mem [0:2399]; // 30 * 80个块integer initi;initial beginascii_mem[0] = 8'h78; // 'x'ascii_mem[1] = 8'h74; // 't'ascii_mem[2] = 8'h65; // 'e'ascii_mem[3] = 8'h72; // 'r'ascii_mem[4] = 8'h6d; // 'm'ascii_mem[5] = 8'h69; // 'i'ascii_mem[6] = 8'h6e; // 'n'ascii_mem[7] = 8'h61; // 'a'ascii_mem[8] = 8'h6c; // 'l'ascii_mem[9] = 8'h2d; // '-'ascii_mem[10] = 8'h2d; // '-'ascii_mem[11] = 8'h2d; // '-'for(initi = 12;initi < 2400;initi = initi+1)beginascii_mem[initi] = 8'h20; // ' 'endendreg [12:0] ascii_mem_index = 80;
然后我们示例化键盘模块,按照实验pdf根据你选择的分辨率实例化时钟ip核模块
wire clk_25mhz;clk_wiz_0 cw0(.reset(BTNC),.clk_in1(CLK100MHZ),.locked(),.clk_out1(clk_25mhz));
然后就是状态机和键盘操作了,这里我们以伪终端为主体,先把字母信息以ascii码形式存下来,后面再根据不同状态进行选择VGA输出,具体大概如下
reg [3:0] mode = 0; // 0 terminal 1 dynamic pic 2 static pic 3 txt 4 clearinteger tp;always @ (posedge Keybd.c50hz) beginif(Keybd.val) begin // 键盘信号有效if(mode == 0)begin // 伪终端状态if(Keybd.ascii >= 8'h20) begin // 非功能键ascii_mem[ascii_mem_index]<=Keybd.ascii;ascii_mem_index <= ascii_mem_index +1;endelse begin // 功能键if(Keybd.ctl == 8'h5a)begin // 按下回车ascii_mem[ascii_mem_index] <= 8'h20;if((ascii_mem_index%80) == 7 && ascii_mem[ascii_mem_index - (ascii_mem_index%80)] == 8'h67 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 1] == 8'h72 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 2] == 8'h61 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 3] == 8'h71 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 4] == 8'h68 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 5] == 8'h69 &&ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 6] == 8'h63)beginmode <= 1; // 动态图片endelse if((ascii_mem_index%80) == 5 && ascii_mem[ascii_mem_index - (ascii_mem_index%80)] == 8'h69 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 1] == 8'h6d && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 2] == 8'h61 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 3] == 8'h67 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 4] == 8'h65)beginmode <= 2; // 静态图片endelse if((ascii_mem_index%80) == 3 && ascii_mem[ascii_mem_index - (ascii_mem_index%80)] == 8'h74 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 1] == 8'h78 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 2] == 8'h74)beginmode <= 3; // 文章endelse if((ascii_mem_index%80) == 5 && ascii_mem[ascii_mem_index - (ascii_mem_index%80)] == 8'h63 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 1] == 8'h6c && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 2] == 8'h65 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 3] == 8'h61 && ascii_mem[ascii_mem_index - (ascii_mem_index%80) + 4] == 8'h72)beginmode <= 4;ascii_mem_index <= 80;endelse beginendif(ascii_mem_index>=2320)begin // 超过2400容量时重置位置mode <= 4;ascii_mem_index <= 80;endelse beginascii_mem_index <= ascii_mem_index - (ascii_mem_index%80) + 80; // 否则转到下一行就行endendelse if(Keybd.ctl == 8'h66 || Keybd.ctl == 8'h71)begin // 按下退格或者del键,此处我定义其行为相同,del不具有删除回车功能if(ascii_mem_index%80!=0)beginascii_mem[ascii_mem_index]<=8'h20;ascii_mem[ascii_mem_index-1]<=8'h20;ascii_mem_index <= ascii_mem_index - 1;endendelse if(Keybd.ascii == 8'h12)begin // 按下下方向键ascii_mem[ascii_mem_index]<=asre;ascii_mem_index <= (ascii_mem_index + 80) > 2399 ? ascii_mem_index : ascii_mem_index + 80;endelse if(Keybd.ascii == 8'h18)begin // 按下上方向键ascii_mem[ascii_mem_index]<=asre;ascii_mem_index <= (ascii_mem_index - 80) < 80 ? ascii_mem_index : ascii_mem_index - 80;endelse if(Keybd.ascii == 8'h14)begin // 按下左方向键ascii_mem[ascii_mem_index]<=asre;ascii_mem_index <= ascii_mem_index % 80 == 0 ? ascii_mem_index : ascii_mem_index - 1;endelse if(Keybd.ascii == 8'h16)begin // 按下右方向键ascii_mem[ascii_mem_index]<=asre;ascii_mem_index <= ascii_mem_index % 80 == 79 ? ascii_mem_index : ascii_mem_index + 1;endelse begin endendendelse if(mode == 1 || mode == 2 || mode == 3)begin // 若为其他状态,按下任意按键后回到伪终端mode <= 0;endelse if(mode == 4)begin // 若为重置状态,直接重置mode <= 0;ascii_mem_index <= 80;for(tp = 80;tp<2400;tp = tp+1)beginascii_mem[tp]<=8'h20;endendelse beginmode <= 0;endendelse begin endend
上面我们处理的字母的信息,是以ascii码的形式存在内存中的,现在我们要显示文字,要按ascii码找到字模,按字模的信息在对应位置显示黑白颜色,达到显示文字的效果。首先我们要读入字模信息,这里建议使用二维的寄存器堆[7:0] [0:1535]并更改后面的计算逻辑,使用三维堆大概率报警告无法读入信息(经验之谈)
reg [7:0] asc96 [0:95] [0:15];initial begin$readmemh("?/ASC16-96.txt", asc96);end
最后就是实例化与选择输出阶段,对伪终端,我们需要套用一下vgactrl获取x轴y轴位置信息,算出现在处在哪个块中,并算出块内偏移,后面按块位置取ascii码,按ascii码取字模,按偏移算出具体点像素是白还是黑
//mode 0VGACtrl myctl(.pix_clk(clk_25mhz),.pix_rst(BTNC),.pix_x(),.pix_y(),.hsync(),.vsync(),.pix_valid());reg [12:0] current_block = 0;reg [3:0] x_offest = 0;reg [3:0] y_offest = 0;always @ (posedge clk_25mhz)begincurrent_block <= ((myctl.pix_x - (myctl.pix_x % 8)) / 8) + (5 * (myctl.pix_y - (myctl.pix_y % 16))); //乘以80再除以16即乘5x_offest <= 7 - (myctl.pix_x % 8);y_offest <= myctl.pix_y % 16;end
其他几个状态就实例化对应模块就行
//mode 1VGASim vgasim(.Clk(clk_25mhz),.BTNC(BTNC),.VGA_R(),.VGA_G(),.VGA_B(),.VGA_HS(),.VGA_VS());//mode 2sim sta(.CLk(clk_25mhz),.BTNC(BTNC),.VGA_R(),.VGA_G(),.VGA_B(),.VGA_HS(),.VGA_VS());//mode 3text mytdis(.TCLk(clk_25mhz),.BTNC(BTNC),.VGA_R(),.VGA_G(),.VGA_B(),.VGA_HS(),.VGA_VS());
然后按状态进行选择VGA信息输出
// ff和00是黑白assign VGA_R = (mode == 0 || mode == 4) ? (asc96[ascii_mem[current_block]-8'h20][y_offest][x_offest] ? 8'hff : 8'h00) : (mode == 1 ? vgasim.VGA_R : (mode == 2 ? sta.VGA_R : mytdis.VGA_R));assign VGA_G = (mode == 0 || mode == 4) ? (asc96[ascii_mem[current_block]-8'h20][y_offest][x_offest] ? 8'hff : 8'h00) : (mode == 1 ? vgasim.VGA_G : (mode == 2 ? sta.VGA_G : mytdis.VGA_G));assign VGA_B = (mode == 0 || mode == 4) ? (asc96[ascii_mem[current_block]-8'h20][y_offest][x_offest] ? 8'hff : 8'h00) : (mode == 1 ? vgasim.VGA_B : (mode == 2 ? sta.VGA_B : mytdis.VGA_B));assign VGA_HS = (mode == 0 || mode == 4) ? myctl.hsync : (mode == 1 ? vgasim.VGA_HS : (mode == 2 ? sta.VGA_HS : mytdis.VGA_HS));assign VGA_VS = (mode == 0 || mode == 4) ? myctl.vsync : (mode == 1 ? vgasim.VGA_VS : (mode == 2 ? sta.VGA_VS : mytdis.VGA_VS));
附
当时做这个实验的时候感觉十分恶心,因为一路做过来,难度在此突增,而且我使用分布式寄存器堆导致跑比特流慢的夸张,一旦哪里写错了屏幕上一点显示都没有,心态极其容易爆炸。所以当时做完之后就想着要写篇博客帮助后来人,虽然不一定有人读(QAQ)。过完年总算有点时间写了,却又发现忘得差不多了,看着之前写的代码慢慢回忆,写下这篇不是很尽善尽美的文章,希望对你有所帮助
另外如果你选了这门课而且还有退课机会,请快跑。功利地说,前面这些实验只占比60%,最后一个难如登天的大实验占比40%。请不要等到期末月事务繁忙再来后悔谩骂当初的自己。不功利地说,这门课虽然是选修课,但学到的东西和付出的时间并不对等,平时不上课,就嗯自学自己搞实验,我的评价是要么选其他课更好地提高能力,要么直接不选空出时间来享受生活
另外如果你跑不了了,那请你明白有舍有得这个简单的道理。如果最后一个实验实在做不出来了(验收没成果只有及格分),参考本实验,你还有瞒天过海计。算了,到那时你自然会理解我的意思