FPGA驱动OLED Verilog代码 (四)------ 字符和汉字显示

一、概述:

        首先展示一下成果图,使用RAM的读写来完成,下面依次介绍各个模块


 二、OLED显示原理(部分)

        oled分为7页,每一页有128个字节用来显示

        首先先设置页地址,然后设置列的低地址和高地址(这里不是很理解,设置扫描方向吗?)

        设置好后依次写入128个显示数据,完成一页的显示,如果你的字模为8*8的,那这样就能完整显示出一个字了(如下图x一样)

         但是如果字体的高度大于8位怎么办了?

        就需要通过两页或者多页来进行显示了,字体被分成两半或者多半,然后在两页或者多页显示拼接出来(有时候会出现错位的问题,那就要注意写进去数据的位置了)


 三、RAM IP核的创建

        1、使用双端口的RAM,读写地址分开

         2、使用1024个字节

         3、勾选读使能信号

         4、取消勾选(这个好像会让输出延时一个时钟)

         5、没有提到的步骤就一路next然后finsh


四、RAM读模块

        先贴代码


/****************************************
该模块用来不断读取ram中的数据,然后刷新OLED的显示
****************************************/
module ram_read(
	input clk,					//时钟信号
	input rst_n,				//按键复位信号
	input write_done,			//spi写完成信号
	input init_done,			//初始化完成
	input[7:0] ram_data,		//读取到的ram数据
	output reg rden,			//ram ip核的读使能信号
	output [9:0] rdaddress,	//ram ip核读地址
	output reg ena_write,	//spi 写使能信号
	output reg oled_dc,		//oled的dc写数据 写命令控制信号
	output reg[7:0] data		//传给 spi写的数据
);

parameter DELAY = 100_000;	//刷新率1000_000/100_000 = 10Hz
reg [20:0] us_cnt;			//us计数器 上电延时等待
reg us_cnt_clr;				//计数器清零信号

//状态说明
//等待初始化完成 写命令 等待写命令完成
//读ram数据 写数据 等待写数据完成
//数据读取完成一遍
parameter WaitInit=0,WriteCmd=1,WaitWriteCmd=2,ReadData=3,WriteData=4,WaitWriteData=5,Done=6;
reg[2:0] state,next_state;	//当前状态 和 下一个状态

reg [7:0] write_cmd[24:0];	//清零命令存储
reg [4:0] write_cmd_cnt;	//清零命令计数
reg [10:0] address_cnt;		//地址计数器 

//读地址最多到1023 但是状态转换需要1024 所以使用额外的一个计数器来作为状态转换,同时也提供地址信号
//只是在地址计数器超过1024时,读地址就为0
assign rdaddress = (address_cnt >= 11'd1024) ? 10'd0 : address_cnt;

//oled清零命令
//也就是设置页地址,设置显示的低地址和设置显示的高地址
//第7页在靠近引脚的位置,从高页写到地页,这么写方便自己查看
initial begin
	write_cmd[0] = 8'hB7;write_cmd[1] = 8'h00;write_cmd[2] = 8'h10;//第7页
	write_cmd[3] = 8'hB6;write_cmd[4] = 8'h00;write_cmd[5] = 8'h10;//第6页
	write_cmd[6] = 8'hB5;write_cmd[7] = 8'h00;write_cmd[8] = 8'h10;//第5页
	write_cmd[9] = 8'hB4;write_cmd[10] = 8'h00;write_cmd[11] = 8'h10;//第4页
	write_cmd[12] = 8'hB3;write_cmd[13] = 8'h00;write_cmd[14] = 8'h10;//第3页
	write_cmd[15] = 8'hB2;write_cmd[16] = 8'h00;write_cmd[17] = 8'h10;//第2页
	write_cmd[18] = 8'hB1;write_cmd[19] = 8'h00;write_cmd[20] = 8'h10;//第1页
	write_cmd[21] = 8'hB0;write_cmd[22] = 8'h00;write_cmd[23] = 8'h10;//第0页
end
	
//1微秒计数器
always @ (posedge clk,negedge rst_n) begin
    if (!rst_n)
        us_cnt <= 21'd0;
    else if (us_cnt_clr)
        us_cnt <= 21'd0;
    else 
        us_cnt <= us_cnt + 1'b1;
end 

//下一个状态确认
always @(*) begin
	if(!rst_n) 
		next_state = WaitInit;
	else begin
		case(state)
			//等待初始化
			WaitInit: next_state = init_done ? WriteCmd : WaitInit;
			
			//写命令
			WriteCmd:
				next_state = WaitWriteCmd;
			
			//等待写命令
			//这些和初始化的地方的写法是一样的
			WaitWriteCmd:
				next_state = (write_cmd_cnt % 2'd3 == 0 && write_done) ? ReadData : (write_done ? WriteCmd: WaitWriteCmd);
			
			//读数据
			ReadData: 
				next_state = WriteData;
			
			//写数据
			WriteData:
				next_state = WaitWriteData;
			
			//等待写数据
			//这些和初始化的地方的写法是一样的
			WaitWriteData: 
				next_state = (address_cnt == 11'd1024&&write_done) ? Done : (address_cnt % 11'd128 == 0&&write_done ? WriteCmd : (write_done ? ReadData : WaitWriteData));
			
			//一次读写完成,等待100ms,进入下一次读写
			Done:begin
				if(us_cnt>DELAY)
					next_state = WriteCmd;
				else
					next_state = Done;
			end
				
		endcase
	end
end

//寄存器赋值和组合逻辑的状态转换分开
always @(posedge clk,negedge rst_n) begin
	if(!rst_n) begin
		oled_dc <= 1'b1;
		ena_write <= 1'b0;
		rden <= 1'b0;
		us_cnt_clr <= 1'b1;
		data <= 8'd0;
	end
	else begin
		case(state)			
			WriteCmd:begin
				ena_write <= 1'b1;						//写命令 使能写信号
				oled_dc <= 1'b0;							//写命令 dc置0
				data <= write_cmd[write_cmd_cnt];	//获取写的数据
			end
			
			WaitWriteCmd:begin
				ena_write <= 1'b0;						//写使能信号拉低,等待写完成
			end
			
			ReadData: begin
			rden <= 1'b1;									//ram读使能信号拉高 开始读数据 这个信号可以一直拉高,因为地址不变,读出来的数据都是保持不变的
			end
			
			WriteData:begin
				ena_write <= 1'b1;						//写数据 写使能信号拉高
				oled_dc <= 1'b1;							//写的是数据 dc置1
				data <= ram_data;							//为即将要写的数据赋值
			end
			
			WaitWriteData: begin
				ena_write <= 1'b0;						//等待写完成 写使能信号拉低
			end
			
			Done:begin
				us_cnt_clr <= 1'b0;						//计数器复位信号拉低,开始计数
			end
				
		endcase
	end
end	

//状态转换
always @(posedge clk,negedge rst_n) begin
	if(!rst_n)
		state <= WaitInit;
	else
		state <= next_state;
end

//计数器计数
always @(posedge clk,negedge rst_n) begin
	if(!rst_n) begin
		write_cmd_cnt <= 5'd0;
		address_cnt <= 11'd0;
	end
	else begin
		case(state)
			Done:begin						//完成状态 各计数器复位
				write_cmd_cnt <= 5'd0;
				address_cnt <= 11'd0;
			end
												
			WriteCmd: //写命令状态 写命令计数器增加
				write_cmd_cnt <= write_cmd_cnt + 1'b1;
			
			ReadData: //读数据状态 读地址增加
				address_cnt <= address_cnt + 1'b1;
			
			default:begin//其他状态 计数器值保持不变
				write_cmd_cnt <= write_cmd_cnt;
				address_cnt <= address_cnt;
			end
		endcase
	end
end


endmodule

        这个模块就是重复的写命令,读数据和写数据,写完3个命令,读1个数据,写1个数据,然后继续读写,直到写完128个数据,然后开始下一次的写命令,读数据和写数据直到1024个数据全部写完,然后等待100ms开始下一次读写。这个和之前初始化里面的读写是一样的,只是加入RAM的读数据,也就是在写数据前增加一次地址,读取一次数据,读使能信号可以一直拉高的。因为地址没有改变,读出的数据也是保持不变的。


五、RAM写数据

        先贴代码

/***************************************
该模块用来向ram中写入显示的数据
地址0~127:第7页
地址128~255:第6页
地址256~383:第5页
地址384~511:第4页
地址512~639:第3页
地址640~767:第2页
地址768~895:第1页
地址896~1023:第0页
****************************************/
module ram_write(
	input clk,							//时钟信号
	input rst_n,						//按键复位信号
	input en_ram_wr,					//模块开始写信号
	output reg wren,					//ram写使能
	output reg [9:0] wraddress,	//ram写地址
	output reg [7:0] data			//写到ram的数据
);

//状态说明
//等待模块使能 写数据 完成
parameter WaitInit=0,WriteData=1,Done=2;
reg[2:0] state,next_state;
reg [7:0] zm[383:0];//写进ram的静态数据
reg [8:0] cnt_zm;//数据计数器

//字模数据初始化 字号大小16
initial begin
	zm[0]=8'h07;zm[1]=8'hF0;zm[2]=8'h08;zm[3]=8'h08;
	zm[4]=8'h10;zm[5]=8'h04;zm[6]=8'h10;zm[7]=8'h04;
	zm[8]=8'h10;zm[9]=8'h04;zm[10]=8'h08;zm[11]=8'h08;
	zm[12]=8'h07;zm[13]=8'hF0;zm[14]=8'h00;zm[15]=8'h00;//"O",0
	
	zm[16]=8'h10;zm[17]=8'h04;zm[18]=8'h1F;zm[19]=8'hFC;
	zm[20]=8'h10;zm[21]=8'h04;zm[22]=8'h00;zm[23]=8'h04;
	zm[24]=8'h00;zm[25]=8'h04;zm[26]=8'h00;zm[27]=8'h04;
	zm[28]=8'h00;zm[29]=8'h0C;zm[30]=8'h00;zm[31]=8'h00;//"L",1
	
	zm[32]=8'h10;zm[33]=8'h04;zm[34]=8'h1F;zm[35]=8'hFC;
	zm[36]=8'h11;zm[37]=8'h04;zm[38]=8'h11;zm[39]=8'h04;
	zm[40]=8'h17;zm[41]=8'hC4;zm[42]=8'h10;zm[43]=8'h04;
	zm[44]=8'h08;zm[45]=8'h18;zm[46]=8'h00;zm[47]=8'h00;//"E",2
	
	zm[48]=8'h10;zm[49]=8'h04;zm[50]=8'h1F;zm[51]=8'hFC;
	zm[52]=8'h10;zm[53]=8'h04;zm[54]=8'h10;zm[55]=8'h04;
	zm[56]=8'h10;zm[57]=8'h04;zm[58]=8'h08;zm[59]=8'h08;
	zm[60]=8'h07;zm[61]=8'hF0;zm[62]=8'h00;zm[63]=8'h00;//"D",3
	
	zm[64]=8'h00;zm[65]=8'h02;zm[66]=8'h00;zm[67]=8'h42;
	zm[68]=8'h00;zm[69]=8'h22;zm[70]=8'h7F;zm[71]=8'h1A;
	zm[72]=8'h49;zm[73]=8'h02;zm[74]=8'h49;zm[75]=8'hFE;
	zm[76]=8'h49;zm[77]=8'h02;zm[78]=8'h49;zm[79]=8'h02;
	zm[80]=8'h49;zm[81]=8'h02;zm[82]=8'h49;zm[83]=8'hFE;
	zm[84]=8'h49;zm[85]=8'h02;zm[86]=8'h7F;zm[87]=8'h0A;
	zm[88]=8'h00;zm[89]=8'h12;zm[90]=8'h00;zm[91]=8'h62;
	zm[92]=8'h00;zm[93]=8'h02;zm[94]=8'h00;zm[95]=8'h00;//"显",4
	
	zm[96]=8'h02;zm[97]=8'h04;zm[98]=8'h02;zm[99]=8'h08;
	zm[100]=8'h42;zm[101]=8'h10;zm[102]=8'h42;zm[103]=8'h60;
	zm[104]=8'h42;zm[105]=8'h00;zm[106]=8'h42;zm[107]=8'h02;
	zm[108]=8'h42;zm[109]=8'h01;zm[110]=8'h43;zm[111]=8'hFE;
	zm[112]=8'h42;zm[113]=8'h00;zm[114]=8'h42;zm[115]=8'h00;
	zm[116]=8'h42;zm[117]=8'h00;zm[118]=8'h42;zm[119]=8'h40;
	zm[120]=8'h42;zm[121]=8'h20;zm[122]=8'h02;zm[123]=8'h10;
	zm[124]=8'h02;zm[125]=8'h0C;zm[126]=8'h00;zm[127]=8'h00;//"示",5
	
	zm[128]=8'h10;zm[129]=8'h04;zm[130]=8'h1F;zm[131]=8'hFC;
	zm[132]=8'h11;zm[133]=8'h04;zm[134]=8'h11;zm[135]=8'h04;
	zm[136]=8'h11;zm[137]=8'h04;zm[138]=8'h0E;zm[139]=8'h88;
	zm[140]=8'h00;zm[141]=8'h70;zm[142]=8'h00;zm[143]=8'h00;//"B",6
	
	zm[144]=8'h01;zm[145]=8'h00;zm[146]=8'h01;zm[147]=8'h81;
	zm[148]=8'h01;zm[149]=8'h61;zm[150]=8'h00;zm[151]=8'h1E;
	zm[152]=8'h00;zm[153]=8'h18;zm[154]=8'h01;zm[155]=8'h60;
	zm[156]=8'h01;zm[157]=8'h80;zm[158]=8'h01;zm[159]=8'h00;//"y",7
	
	zm[160]=8'h20;zm[161]=8'h80;zm[162]=8'h28;zm[163]=8'h91;
	zm[164]=8'h35;zm[165]=8'h11;zm[166]=8'hE2;zm[167]=8'h12;
	zm[168]=8'h25;zm[169]=8'h14;zm[170]=8'h28;zm[171]=8'h98;
	zm[172]=8'h30;zm[173]=8'h70;zm[174]=8'h00;zm[175]=8'h90;
	zm[176]=8'h38;zm[177]=8'h90;zm[178]=8'h25;zm[179]=8'h12;
	zm[180]=8'h22;zm[181]=8'h11;zm[182]=8'h25;zm[183]=8'h12;
	zm[184]=8'h29;zm[185]=8'h1C;zm[186]=8'h30;zm[187]=8'h80;
	zm[188]=8'h00;zm[189]=8'h80;zm[190]=8'h00;zm[191]=8'h00;//"努",8
	
	zm[192]=8'h00;zm[193]=8'h00;zm[194]=8'h08;zm[195]=8'h01;
	zm[196]=8'h08;zm[197]=8'h02;zm[198]=8'h08;zm[199]=8'h04;
	zm[200]=8'h08;zm[201]=8'h18;zm[202]=8'h08;zm[203]=8'h60;
	zm[204]=8'hFF;zm[205]=8'h80;zm[206]=8'h08;zm[207]=8'h00;
	zm[208]=8'h08;zm[209]=8'h04;zm[210]=8'h08;zm[211]=8'h02;
	zm[212]=8'h08;zm[213]=8'h01;zm[214]=8'h08;zm[215]=8'h02;
	zm[216]=8'h0F;zm[217]=8'hFC;zm[218]=8'h00;zm[219]=8'h00;
	zm[220]=8'h00;zm[221]=8'h00;zm[222]=8'h00;zm[223]=8'h00;//"力",9
	
	zm[224]=8'h00;zm[225]=8'h00;zm[226]=8'h1F;zm[227]=8'hFF;
	zm[228]=8'h10;zm[229]=8'h00;zm[230]=8'h10;zm[231]=8'h00;
	zm[232]=8'h30;zm[233]=8'h00;zm[234]=8'h53;zm[235]=8'hF8;
	zm[236]=8'h92;zm[237]=8'h10;zm[238]=8'h12;zm[239]=8'h10;
	zm[240]=8'h12;zm[241]=8'h10;zm[242]=8'h13;zm[243]=8'hF8;
	zm[244]=8'h10;zm[245]=8'h00;zm[246]=8'h10;zm[247]=8'h02;
	zm[248]=8'h10;zm[249]=8'h01;zm[250]=8'h1F;zm[251]=8'hFE;
	zm[252]=8'h00;zm[253]=8'h00;zm[254]=8'h00;zm[255]=8'h00;//"向",10
	
	zm[256]=8'h10;zm[257]=8'h00;zm[258]=8'h10;zm[259]=8'h00;
	zm[260]=8'h17;zm[261]=8'hFF;zm[262]=8'h94;zm[263]=8'h90;
	zm[264]=8'h74;zm[265]=8'h92;zm[266]=8'h14;zm[267]=8'h91;
	zm[268]=8'h17;zm[269]=8'hFE;zm[270]=8'h10;zm[271]=8'h00;
	zm[272]=8'h10;zm[273]=8'h00;zm[274]=8'h13;zm[275]=8'hF0;
	zm[276]=8'h30;zm[277]=8'h02;zm[278]=8'hD0;zm[279]=8'h01;
	zm[280]=8'h17;zm[281]=8'hFE;zm[282]=8'h10;zm[283]=8'h00;
	zm[284]=8'h10;zm[285]=8'h00;zm[286]=8'h00;zm[287]=8'h00;//"前",11
	
	zm[288]=8'h00;zm[289]=8'h00;zm[290]=8'h1F;zm[291]=8'hFE;
	zm[292]=8'h30;zm[293]=8'h84;zm[294]=8'hD0;zm[295]=8'h84;
	zm[296]=8'h10;zm[297]=8'h84;zm[298]=8'h10;zm[299]=8'h84;
	zm[300]=8'h1F;zm[301]=8'hFE;zm[302]=8'h02;zm[303]=8'h00;
	zm[304]=8'h0C;zm[305]=8'h00;zm[306]=8'hF1;zm[307]=8'h00;
	zm[308]=8'h10;zm[309]=8'hC2;zm[310]=8'h10;zm[311]=8'h01;
	zm[312]=8'h10;zm[313]=8'h02;zm[314]=8'h1F;zm[315]=8'hFC;
	zm[316]=8'h00;zm[317]=8'h00;zm[318]=8'h00;zm[319]=8'h00;//"的",12
	
	zm[320]=8'h00;zm[321]=8'h10;zm[322]=8'h00;zm[323]=8'h20;
	zm[324]=8'h00;zm[325]=8'hC0;zm[326]=8'h07;zm[327]=8'h00;
	zm[328]=8'h00;zm[329]=8'h00;zm[330]=8'h00;zm[331]=8'h02;
	zm[332]=8'h00;zm[333]=8'h01;zm[334]=8'hFF;zm[335]=8'hFE;
	zm[336]=8'h00;zm[337]=8'h00;zm[338]=8'h00;zm[339]=8'h00;
	zm[340]=8'h00;zm[341]=8'h00;zm[342]=8'h04;zm[343]=8'h00;
	zm[344]=8'h02;zm[345]=8'h00;zm[346]=8'h01;zm[347]=8'h80;
	zm[348]=8'h00;zm[349]=8'h70;zm[350]=8'h00;zm[351]=8'h00;//"小",13
	
	zm[352]=8'h08;zm[353]=8'h80;zm[354]=8'h11;zm[355]=8'h00;
	zm[356]=8'h23;zm[357]=8'hFF;zm[358]=8'hCC;zm[359]=8'h00;
	zm[360]=8'h00;zm[361]=8'h04;zm[362]=8'h04;zm[363]=8'h88;
	zm[364]=8'h08;zm[365]=8'hB0;zm[366]=8'h14;zm[367]=8'h82;
	zm[368]=8'h24;zm[369]=8'h81;zm[370]=8'hC7;zm[371]=8'hFE;
	zm[372]=8'h24;zm[373]=8'h80;zm[374]=8'h14;zm[375]=8'hA0;
	zm[376]=8'h08;zm[377]=8'h90;zm[378]=8'h04;zm[379]=8'h8C;
	zm[380]=8'h04;zm[381]=8'h00;zm[382]=8'h00;zm[383]=8'h00;//"徐",14

end

//下一个状态确认
always @(*) begin
	if(!rst_n)
		next_state = WaitInit;
	else begin
		case(state)
			//等待模块使能
			WaitInit: next_state = en_ram_wr ? WriteData : WaitInit;
			//写数据
			WriteData: next_state = (cnt_zm==9'd383) ? Done : WriteData;
			//数据写完成
			Done: next_state = Done;
		endcase
	end
end

//每一个状态的逻辑变量赋值
always @(posedge clk,negedge rst_n) begin
	if(!rst_n) begin
		wren <= 1'b0;			//写使能信号复位
		data <= 8'd0;			//数据值复位
	end
	else begin
		case(state)
			WaitInit:begin
				wren <= 1'b0;	//等待模块使能状态 信号复位
				data <= 8'd0;
			end
			
			WriteData:begin
				wren <= 1'b1;	//写使能信号拉高
				data <= zm[cnt_zm];//写到ram中的数据赋值
			end
			Done:begin
				wren <= 1'b0;
				data <= 8'd0;
			end	
		endcase
	end

end

//数据计数器计数
always @(posedge clk,negedge rst_n) begin
	if(!rst_n) begin
		cnt_zm <= 9'd0;//计数值复位
		wraddress <= 10'd0+23;//地址复位,加入偏移量23,使得显示靠中间位置
	end
	else begin
		case(cnt_zm)
			9'd126: cnt_zm <= 9'd1;		//第1页写完毕 转到第2页
			9'd127: cnt_zm <= 9'd128;	//第2页写完毕 转到第3页
			9'd158: cnt_zm <= 9'd129;	//第3页写完毕 转到第4页
			9'd159: cnt_zm <= 9'd160;	//第4页写完毕 转到第5页
			9'd382: cnt_zm <= 9'd161;	//第5页写完毕 转到第6页
			default:
				if(state == WriteData)	//写数据状态下,计数器自增,加2是因为一个字模的高度为16,它本页的下一个数据应该在和当前数据间隔着一个
					cnt_zm <= cnt_zm + 2'd2;
				else
					cnt_zm <= cnt_zm;		//其他状态保持不变
		endcase
		
		//页数说明:主要看你想把字体显示在哪一行
		case(cnt_zm)
			9'd1: wraddress<=10'd128+24;		//进入第2页,地址重新赋值,加入偏移量,显示靠中间位置
			9'd128: wraddress<=10'd256+48;	//进入第3页
			9'd129: wraddress<=10'd384+48;	//进入第4页
			9'd160: wraddress<=10'd512;		//进入第5页
			9'd161: wraddress<=10'd640;		//进入第6页
			default:begin
				if(state==WriteData)				//在写数据的时候地址加1
					wraddress <= wraddress + 1'b1;
				else
					wraddress <= wraddress;		//其他状态下地址保持不变
			end
		endcase
	end
end

//状态转换
always @(posedge clk,negedge rst_n) begin
	if(!rst_n)
		state <= WaitInit;
	else
		state <= next_state;
end

endmodule

        这个模块需要注意的就是什么时候换页(这个看你自己的需求),然后就是注意换页后应该从RAM的哪一个地址开始写,可以加入一些地址偏移量,每一页不同的位置显示,但是如果两页或多页显示一个字的话就需要注意地址对齐,不然字体会错位。

        在顶层模块里面例化这几个模块,然后连接模块(注意信号的输入只能有一个来源)这样就可以实现上面最开始的效果了

 顶层模块代码

module oled_drive(
	input clk,			//时钟信号 50MHz
	input rst_n,		//按键复位
	input ram_rst,		//ram复位 高电平复位
	output oled_rst,	//oled res 复位信号
	output oled_dc,	//oled dc 0:写命令 1:写数据
	output oled_sclk,	//oled do 时钟信号
	output oled_mosi	//oled d1 数据信号
);

wire clk_1m;			//分频后的1M时钟
wire ena_write;		//spi写使能信号
wire [7:0] data;		//spi写的数据

wire init_done;		//初始化完成信号
wire [7:0] init_data;//初始化输出给spi的数据
wire init_ena_wr;		//初始化的spi写使能信号
wire init_oled_dc;

wire [7:0] ram_data;	//读到的ram数据
wire [7:0] show_data;//输出给spi写的数据
wire rden;				//ram的读使能信号
wire [9:0] rdaddress;//ram读地址信号
wire ram_ena_wr;		//ram使能写信号
wire ram_oled_dc;		//ram模块中的oled dc信号

wire wren;				//ram写使能信号
wire [9:0] wraddress;//ram写地址
wire [7:0] wrdata;	//写到ram中的数据

//一个信号只能有由一个信号来驱动,所以需要选择一下
assign data = init_done ? show_data : init_data;
assign ena_write = init_done ? ram_ena_wr : init_ena_wr;
assign oled_dc = init_done ? ram_oled_dc : init_oled_dc;

//时钟分频模块 产生1M的时钟
clk_fenpin clk_fenpin_inst(
	.clk(clk),
	.rst_n(rst_n),
	.clk_1m(clk_1m)
);

//spi传输模块
spi_writebyte spi_writebyte_inst(
	.clk(clk_1m),
	.rst_n(rst_n),
	.ena_write(ena_write),
	.data(data),
	.sclk(oled_sclk),
	.mosi(oled_mosi),
	.write_done(write_done)
);

//oled初始化模块 产生初始化数据
oled_init oled_init_inst(
	.clk(clk_1m),
	.rst_n(rst_n),
	.write_done(write_done),
	.oled_rst(oled_rst),
	.oled_dc(init_oled_dc),
	.data(init_data),
	.ena_write(init_ena_wr),
	.init_done(init_done)
);

//ram读模块
ram_read ram_read_inst(
	.clk(clk_1m),
	.rst_n(rst_n),
	.write_done(write_done),
	.init_done(init_done),
	.ram_data(ram_data),
	.rden(rden),
	.rdaddress(rdaddress),
	.ena_write(ram_ena_wr),
	.oled_dc(ram_oled_dc),
	.data(show_data)
);

//ram写模块
ram_write ram_write_inst(
	.clk(clk_1m),
	.rst_n(rst_n),
	.en_ram_wr(1'b1),
	.wren(wren),
	.wraddress(wraddress),
	.data(wrdata)
);

//ram ip核
ram_show ram_show_inst(
	.clock(clk_1m),
	.aclr(!ram_rst),
	.data(wrdata),
	.rdaddress(rdaddress),
	.rden(rden),
	.wraddress(wraddress),
	.wren(wren),
	.q(ram_data)
);

endmodule

        下面介绍字模的生成和一个不用手打数组下标的方法(java语言 菜鸡随便写的)


六、字模生成

使用PCtoLCD2002生成字模

        设置生成选项

        选择16*16,生成字模并保存字模

使用下面的java代码填充数组下标(把str里面的字符串换成你的就好了)

public static void main(String[] args) {
		StringBuffer str = new StringBuffer("zm[]=8'h07;zm[]=8'hF0;zm[]=8'h08;zm[]=8'h08;zm[]=8'h10;zm[]=8'h04;zm[]=8'h10;zm[]=8'h04;zm[]=8'h10;zm[]=8'h04;zm[]=8'h08;zm[]=8'h08;zm[]=8'h07;zm[]=8'hF0;zm[]=8'h00;zm[]=8'h00;\"O\",0\r\n"
				+ "zm[]=8'h10;zm[]=8'h04;zm[]=8'h1F;zm[]=8'hFC;zm[]=8'h10;zm[]=8'h04;zm[]=8'h00;zm[]=8'h04;zm[]=8'h00;zm[]=8'h04;zm[]=8'h00;zm[]=8'h04;zm[]=8'h00;zm[]=8'h0C;zm[]=8'h00;zm[]=8'h00;\"L\",1\r\n"
				+ "zm[]=8'h10;zm[]=8'h04;zm[]=8'h1F;zm[]=8'hFC;zm[]=8'h11;zm[]=8'h04;zm[]=8'h11;zm[]=8'h04;zm[]=8'h17;zm[]=8'hC4;zm[]=8'h10;zm[]=8'h04;zm[]=8'h08;zm[]=8'h18;zm[]=8'h00;zm[]=8'h00;\"E\",2\r\n"
				+ "zm[]=8'h10;zm[]=8'h04;zm[]=8'h1F;zm[]=8'hFC;zm[]=8'h10;zm[]=8'h04;zm[]=8'h10;zm[]=8'h04;zm[]=8'h10;zm[]=8'h04;zm[]=8'h08;zm[]=8'h08;zm[]=8'h07;zm[]=8'hF0;zm[]=8'h00;zm[]=8'h00;\"D\",3\r\n"
				+ "zm[]=8'h00;zm[]=8'h02;zm[]=8'h00;zm[]=8'h42;zm[]=8'h00;zm[]=8'h22;zm[]=8'h7F;zm[]=8'h1A;zm[]=8'h49;zm[]=8'h02;zm[]=8'h49;zm[]=8'hFE;zm[]=8'h49;zm[]=8'h02;zm[]=8'h49;zm[]=8'h02;zm[]=8'h49;zm[]=8'h02;zm[]=8'h49;zm[]=8'hFE;zm[]=8'h49;zm[]=8'h02;zm[]=8'h7F;zm[]=8'h0A;\r\n"
				+ "zm[]=8'h00;zm[]=8'h12;zm[]=8'h00;zm[]=8'h62;zm[]=8'h00;zm[]=8'h02;zm[]=8'h00;zm[]=8'h00;\"显\",4\r\n"
				+ "zm[]=8'h02;zm[]=8'h04;zm[]=8'h02;zm[]=8'h08;zm[]=8'h42;zm[]=8'h10;zm[]=8'h42;zm[]=8'h60;zm[]=8'h42;zm[]=8'h00;zm[]=8'h42;zm[]=8'h02;zm[]=8'h42;zm[]=8'h01;zm[]=8'h43;zm[]=8'hFE;zm[]=8'h42;zm[]=8'h00;zm[]=8'h42;zm[]=8'h00;zm[]=8'h42;zm[]=8'h00;zm[]=8'h42;zm[]=8'h40;\r\n"
				+ "zm[]=8'h42;zm[]=8'h20;zm[]=8'h02;zm[]=8'h10;zm[]=8'h02;zm[]=8'h0C;zm[]=8'h00;zm[]=8'h00;\"示\",5\r\n"
				+ "zm[]=8'h10;zm[]=8'h04;zm[]=8'h1F;zm[]=8'hFC;zm[]=8'h11;zm[]=8'h04;zm[]=8'h11;zm[]=8'h04;zm[]=8'h11;zm[]=8'h04;zm[]=8'h0E;zm[]=8'h88;zm[]=8'h00;zm[]=8'h70;zm[]=8'h00;zm[]=8'h00;\"B\",6\r\n"
				+ "zm[]=8'h01;zm[]=8'h00;zm[]=8'h01;zm[]=8'h81;zm[]=8'h01;zm[]=8'h61;zm[]=8'h00;zm[]=8'h1E;zm[]=8'h00;zm[]=8'h18;zm[]=8'h01;zm[]=8'h60;zm[]=8'h01;zm[]=8'h80;zm[]=8'h01;zm[]=8'h00;\"y\",7\r\n"
				+ "zm[]=8'h20;zm[]=8'h80;zm[]=8'h28;zm[]=8'h91;zm[]=8'h35;zm[]=8'h11;zm[]=8'hE2;zm[]=8'h12;zm[]=8'h25;zm[]=8'h14;zm[]=8'h28;zm[]=8'h98;zm[]=8'h30;zm[]=8'h70;zm[]=8'h00;zm[]=8'h90;zm[]=8'h38;zm[]=8'h90;zm[]=8'h25;zm[]=8'h12;zm[]=8'h22;zm[]=8'h11;zm[]=8'h25;zm[]=8'h12;\r\n"
				+ "zm[]=8'h29;zm[]=8'h1C;zm[]=8'h30;zm[]=8'h80;zm[]=8'h00;zm[]=8'h80;zm[]=8'h00;zm[]=8'h00;\"努\",8\r\n"
				+ "zm[]=8'h00;zm[]=8'h00;zm[]=8'h08;zm[]=8'h01;zm[]=8'h08;zm[]=8'h02;zm[]=8'h08;zm[]=8'h04;zm[]=8'h08;zm[]=8'h18;zm[]=8'h08;zm[]=8'h60;zm[]=8'hFF;zm[]=8'h80;zm[]=8'h08;zm[]=8'h00;zm[]=8'h08;zm[]=8'h04;zm[]=8'h08;zm[]=8'h02;zm[]=8'h08;zm[]=8'h01;zm[]=8'h08;zm[]=8'h02;\r\n"
				+ "zm[]=8'h0F;zm[]=8'hFC;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;\"力\",9\r\n"
				+ "zm[]=8'h00;zm[]=8'h00;zm[]=8'h1F;zm[]=8'hFF;zm[]=8'h10;zm[]=8'h00;zm[]=8'h10;zm[]=8'h00;zm[]=8'h30;zm[]=8'h00;zm[]=8'h53;zm[]=8'hF8;zm[]=8'h92;zm[]=8'h10;zm[]=8'h12;zm[]=8'h10;zm[]=8'h12;zm[]=8'h10;zm[]=8'h13;zm[]=8'hF8;zm[]=8'h10;zm[]=8'h00;zm[]=8'h10;zm[]=8'h02;\r\n"
				+ "zm[]=8'h10;zm[]=8'h01;zm[]=8'h1F;zm[]=8'hFE;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;\"向\",10\r\n"
				+ "zm[]=8'h10;zm[]=8'h00;zm[]=8'h10;zm[]=8'h00;zm[]=8'h17;zm[]=8'hFF;zm[]=8'h94;zm[]=8'h90;zm[]=8'h74;zm[]=8'h92;zm[]=8'h14;zm[]=8'h91;zm[]=8'h17;zm[]=8'hFE;zm[]=8'h10;zm[]=8'h00;zm[]=8'h10;zm[]=8'h00;zm[]=8'h13;zm[]=8'hF0;zm[]=8'h30;zm[]=8'h02;zm[]=8'hD0;zm[]=8'h01;\r\n"
				+ "zm[]=8'h17;zm[]=8'hFE;zm[]=8'h10;zm[]=8'h00;zm[]=8'h10;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;\"前\",11\r\n"
				+ "zm[]=8'h00;zm[]=8'h00;zm[]=8'h1F;zm[]=8'hFE;zm[]=8'h30;zm[]=8'h84;zm[]=8'hD0;zm[]=8'h84;zm[]=8'h10;zm[]=8'h84;zm[]=8'h10;zm[]=8'h84;zm[]=8'h1F;zm[]=8'hFE;zm[]=8'h02;zm[]=8'h00;zm[]=8'h0C;zm[]=8'h00;zm[]=8'hF1;zm[]=8'h00;zm[]=8'h10;zm[]=8'hC2;zm[]=8'h10;zm[]=8'h01;\r\n"
				+ "zm[]=8'h10;zm[]=8'h02;zm[]=8'h1F;zm[]=8'hFC;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;\"的\",12\r\n"
				+ "zm[]=8'h00;zm[]=8'h10;zm[]=8'h00;zm[]=8'h20;zm[]=8'h00;zm[]=8'hC0;zm[]=8'h07;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h02;zm[]=8'h00;zm[]=8'h01;zm[]=8'hFF;zm[]=8'hFE;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;zm[]=8'h04;zm[]=8'h00;\r\n"
				+ "zm[]=8'h02;zm[]=8'h00;zm[]=8'h01;zm[]=8'h80;zm[]=8'h00;zm[]=8'h70;zm[]=8'h00;zm[]=8'h00;\"小\",13\r\n"
				+ "zm[]=8'h08;zm[]=8'h80;zm[]=8'h11;zm[]=8'h00;zm[]=8'h23;zm[]=8'hFF;zm[]=8'hCC;zm[]=8'h00;zm[]=8'h00;zm[]=8'h04;zm[]=8'h04;zm[]=8'h88;zm[]=8'h08;zm[]=8'hB0;zm[]=8'h14;zm[]=8'h82;zm[]=8'h24;zm[]=8'h81;zm[]=8'hC7;zm[]=8'hFE;zm[]=8'h24;zm[]=8'h80;zm[]=8'h14;zm[]=8'hA0;\r\n"
				+ "zm[]=8'h08;zm[]=8'h90;zm[]=8'h04;zm[]=8'h8C;zm[]=8'h04;zm[]=8'h00;zm[]=8'h00;zm[]=8'h00;\"徐\",14");
		
		int j = 0;
		for(int i=0;i<str.length();i++) {
			if(str.charAt(i) == ']') {
				str.insert(i, j);
				//插入字符后,索引需要增加,不然会卡死
				if(j<=9)
					i++;
				else if (j<=99){
					i+=2;
				}
				else {
					i+=3;
				}
				j++;
			}
		}
		System.out.println(str);
}

        在Eclipse或其他的软件中运行后,在控制台可以得到有数组下表标的字符串

        在放入Quartus中时记得把后面的字模注释用“//”注释了,然后嫌格式不好看可以自己更改一下,就可以开始使用了。但是记得更改RAM地址跳转的位置

七、小结

        在遇到显示不正常的时候一定要多去查查写进去的数据有没有写错,读出来的数据有没有问题,多看看仿真,不行降低时钟速度,使用signal tap看看数据的值,有的时候仿真的和实际的值可能会存在一些偏差。

        贴一下RAM读写+spi写的testbench代码(可以增删一些模块来实现你想要的仿真)

`timescale 1ns/1ns //仿真单位为1ns,精度为1ns

module ram_read_tb();

reg clk;
reg rst_n;
wire write_done;
reg init_done;
wire [7:0]ram_data;
wire rden;
wire[9:0] rdaddress;
wire ena_write;
wire oled_dc;
wire[7:0] data;

wire wren;
wire [9:0] wraddress;
wire [7:0] wrdata;

wire oled_sclk;
wire oled_mosi;

ram_read ram_read_inst(
	.clk(clk),
	.rst_n(rst_n),
	.write_done(write_done),
	.init_done(init_done),
	.rden(rden),
	.rdaddress(rdaddress),
	.ena_write(ena_write),
	.oled_dc(oled_dc),
	.ram_data(ram_data),
	.data(data)
);

ram_write ram_write_inst(
	.clk(clk),
	.rst_n(rst_n),
	.init_done(1'b1),
	.wren(wren),
	.wraddress(wraddress),
	.data(wrdata)
);

ram_show ram_show_inst(
	.clock(clk),
	.aclr(1'b0),
	.data(wrdata),
	.rdaddress(rdaddress),
	.rden(rden),
	.wraddress(wraddress),
	.wren(wren),
	.q(ram_data)
);

//spi传输模块
spi_writebyte spi_writebyte_inst(
	.clk(clk),
	.rst_n(rst_n),
	.ena_write(ena_write),
	.data(data),
	.sclk(oled_sclk),
	.mosi(oled_mosi),
	.write_done(write_done)
);

initial begin
	#0 	clk = 0;
			rst_n = 0;
			init_done = 1;
	
	#20 rst_n = 1;

end

always #5 clk = ~clk;

endmodule

  • 4
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值