FPGA驱动OLED动态显示(Verilog代码)——向OLED写数据(关键)
1、代码太长贴到了‘代码笔记’里,链接:向OLED写数据write_data.v
这段代码实现的是:Freq:xxxxxHz; x是动态数据,取模软件PCtoLCD2002设置:宋体,字宽16,字高16,逆向取模,列行式;实际得到的字模是8x16的,对应到OLED上是8列16行;
2、部分释义:
2.1、
reg [27:0] cnt;
reg [15:0] num;
always @(posedge clk_1m or negedge rst_n)
begin
if(!rst_n)
begin
cnt <= 28'd0;
num <= 16'd43400;
end
else
if(cnt == 28'h4C4B40)
begin
num <= num + 1'b1;
cnt <= 28'd0;
end
else
if(num == 16'd45000)
num <= 16'd44000;
else
cnt <= cnt + 1'b1;
end
目的:产生43400-45000计数,做测试用,实际中‘num’是由其他模块采集进来的数据;
2.2、
reg [3:0] ge;
reg [3:0] shi;
reg [3:0] bai;
reg [3:0] qian;
reg [3:0] wan;
always @(posedge clk_1m or negedge rst_n)
begin
if(!rst_n)
begin
ge <= 4'd0;
shi <= 4'd0;
bai <= 4'd0;
qian<= 4'd0;
wan <= 4'd0;
end
else
begin
ge <= num % 10;
shi <= num / 10 % 10;
bai <= num / 100 % 10;
qian<= num / 1000 % 10;
wan <= num / 10000;
end
end
目的:很显然是为了实现个、十、百、千、万的取值,分别显示在OLED的不同位置上。
2.3、
//----------------------------------------------------
//清屏
8'd0,8'd4,8'd8,8'd12,8'd16,8'd20,8'd24,8'd28:
if(spi_write_done)
begin start <= 1'b0; i <= i + 1'b1; end
else
begin data <= {2'b00,4'hb,y}; start <= 1'b1; end
//列高位
8'd1,8'd5,8'd9,8'd13,8'd17,8'd21,8'd25,8'd29:
if(spi_write_done)
begin start <= 1'b0; i <= i + 1'b1; end
else
begin data <= {2'b00,4'h1,4'h0}; start <= 1'b1; end
//列低位
6'd2,8'd6,8'd10,8'd14,8'd18,8'd22,8'd26,8'd30:
if(spi_write_done)
begin start <= 1'b0; i <= i + 1'b1; end
else
begin data <= {2'b00,4'h0,4'h0}; start <= 1'b1; end
8'd3,8'd7,8'd11,8'd15,8'd19,8'd23,8'd27,8'd31:
if(x==8'd128)
begin y <= y + 1'b1; x <= 8'd0; i <= i + 1'b1; end
else
if(spi_write_done)
begin start <= 1'b0; x <= x + 1'b1; end
else
begin data <= {2'b01,8'd00}; start <= 1'b1; end
//---------------------------------------------------------------
目的:很重要的一端代码,其目的是为了实现清屏;
x:表示列=128列,先写高4位,再写第4位;高低地址都用8bit表示,高地址命令是0x1?;地地址命令是0x0?;
y:表示页=8页,8'hb0~8'hb7=第0页~第7页;
e.g.:如果想在第3页的第29列开始显示数据,那么需要写入的数据为:页:data <={2'b00,8'hb2};列高地址:data<={2'b00,4'h1,4'h1},列地4位:data<={2'b00,4'h0,4'hD},前两位2'b00是CS和RES位,29=8'h1D;
i=0~3:填充0页的128列,i= 4~7填充1页的128列,以此类推。
注明:开始向OLED写数据时建议先清屏,我在实际操作过程中,发现如果没有清零步骤的话原先写过的位置还是会显示。
2.4、
8'd32:
begin y <= 4'h0; i <= i + 1'b1; end
//----------------------------------------------------------------------------
8'd33:
begin F_flag <= 1'b1; i <= i + 1'b1; end
注释:8'd32步是将页地址寄存器y的值清零;清零完之后y的值会等于7,如果没有第32步实际效果会从OLED的第7页开始显示,而我们是想让它从0页开始显示;
注释:第33步开始显示字符‘F’,但必须得有一个标志位F_flag,目的是管理使用ROM的次序,毕竟有那么多字符都共用一个ROM,不管理怎么能行。
2.5、
8'd34: //'F'
case(j)
//设置页地址
6'd0,6'd4:
if(spi_write_done)
begin start <= 1'b0; j <= j + 1'b1; end
else
begin data <= {2'b00,4'hb,y}; start <= 1'b1; end
//高地址
6'd1,6'd5:
if(spi_write_done)
begin start <= 1'b0; j <= j + 1'b1; end
else
begin data <= {2'b00,4'h1,4'h0}; start <= 1'b1; end
//低地址
6'd2,6'd6:
if(spi_write_done)
begin start <= 1'b0; j <= j + 1'b1; end
else
begin data <= {2'b00,4'h0,4'h0}; start <= 1'b1; end
//填充8次
6'd3,6'd7:
if(x==8'd7)
begin y <= y + 1'b1; x <= 8'd0; j <= j + 1'b1; end
else
if(spi_write_done)
begin start <= 1'b0; x <= x + 1'b1; end
else
begin data <= {2'b01,rom_data}; start <= 1'b1; end
6'd8:
begin y <= 4'h0; j <= 6'd0; i <= i + 1'b1; end
endcase
注释:
这段代码跟清零代码基本是相似的,因为他们的执行原理和过程是一样的。不同的是,清零的时候是if(x==127),因为是要填充整整一页,那x就是127;这里的代码是if(x==7),原因是这个字符‘F’取模时只占用了OLED的8列,16行,所以x只需要填充8列即可,填充完之后就开始换页。‘F’占用了第0页第1页。
这段代码实现字符‘F’,从0页的0列开始显示,F的页以及高低地址设置都是0;
2.6、
8'd35:
begin F_flag <= 1'b0; r_flag <= 1'b1; i <= i + 1'b1; end
注释:这段代码目的是结束字符‘F’占用ROM,F_flag=0,开始让字符‘r’使用ROM,因为下面要开始显示字符‘r’了;
在每个字符显示完之后都要结束当前ROM占用,释放给下一个字符;字符的显示代码跟字符‘F’的代码除了高低地址不同,其他不变。
2.7、
当显示动态数字时有所变化。以显示个位为列,其他位置的数字显示跟个位显示相同。
8'd47:
begin z_flag <= 1'b0; ge_flag <= 1'b1; i <= i + 1'b1; end
8'd48:
case(ge)
4'd0,4'd1,4'd2,4'd3,4'd4,4'd5,4'd6,4'd7,4'd8,4'd9:
case(j)
//设置页地址
6'd0,6'd4:
if(spi_write_done)
begin start <= 1'b0; j <= j + 1'b1; end
else
begin data <= {2'b00,4'hb,y}; start <= 1'b1; end
//高地址
6'd1,6'd5:
if(spi_write_done)
begin start <= 1'b0; j <= j + 1'b1; end
else
begin data <= {2'b00,4'h1,4'h4}; start <= 1'b1; end
//低地址
6'd2,6'd6:
if(spi_write_done)
begin start <= 1'b0; j <= j + 1'b1; end
else
begin data <= {2'b00,4'h0,4'h6}; start <= 1'b1; end
//填充8次
6'd3,6'd7:
if(x==8'd7)
begin y <= y + 1'b1; x <= 8'd0; j <= j + 1'b1; end
else
if(spi_write_done)
begin start <= 1'b0; x <= x + 1'b1; end
else
begin data <= {2'b01,rom_data}; start <= 1'b1; end
6'd8:
begin y <= 4'h0; j <= 6'd0; i <= i + 1'b1; end
endcase
default: ;
endcase
8'd49:
begin ge_flag <= 1'b0; shi_flag <= 1'b1; i <= i + 1'b1; end
注释:
个位0-9的数字显示代码相同,共用一段代码;十位0-9的数字显示跟个位的代码相同,也是共用,但是要修改一些列地址,因为十位跟个位的显示位置不同,百、十、千、万的显示也是如此。注意列地址。
2.8、
8'd57:
begin wan_flag <= 1'b0; i <= i + 1'b1; end
8'd58:
i <= 6'd47;
当显示完所有的数据和字符后,要有第58步骤,转到第47步,看看第47步是不是ge_flag开始的地方,意思就是数字位循环更新显示,那不就是动态实时了吗。
源码中的第59、60步可以忽略,我没删掉。
2.9:、
assign rom_addr = F_flag ? (y ? (x + 8'd168) : (x + 8'd160)) :
(r_flag ? (y ? (x + 8'd184) : (x + 8'd176)) :
(e_flag ? (y ? (x + 8'd200) : (x + 8'd192)) :
(q_flag ? (y ? (x + 8'd216) : (x + 8'd208)) :
(maohao_flag ? (y ? (x + 8'd232) : (x + 8'd224)) :
(H_flag ? (y ? (x + 8'd248) : (x + 8'd240)) :
(z_flag ? (y ? (x + 12'd264) : (x + 12'd256)) :
(ge_flag ? (y ? (x + (ge << 4) + 4'd8) : (x + (ge << 4))):
(shi_flag ? (y ? (x + (shi << 4) + 4'd8) : (x + (shi << 4))):
(bai_flag ? (y ? (x + (bai << 4) + 4'd8) : (x + (bai << 4))):
(qian_flag ? (y ? (x + (qian << 4) + 4'd8) : (x + (qian << 4))):
(y ? (x + (wan << 4) + 4'd8) : (x + (wan << 4)))))))))))));
注释:这里就是通过标志位x_flag对ROM的管理,嵌套的‘ ? :’语句,清屏时是直接写入的0,没有调用ROM,也就没有定义clear_flag;
注释:敲黑板的时候到了。
首先说一说ROM。根据OLED的显示特点:共8页,每页128列,每列8bit。因此ROM设置为:8bit*1024。
ROM中的数据是先写入0-9的字模值,再写‘F’‘r’‘e’‘q’‘:’‘H’‘z’的字模。
e.g:字符‘F’的字模是:{0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,
0x20,0x3F,0x20,0x00,0x03,0x00,0x00,0x00}。
在ROM中填入:
起地址160对应‘F’的上半部分字模,也就是第0页的显示,起地址168对应‘F’的下半部分字模,也就是第1页的显示。所以F_flag ? (y ? (x + 8'd168) : (x + 8'd160))这句话的意思是:F_flag=1时表示现在要显示字符‘F’,然后判断y值,y=0时表示正在显示第0页,那么x+8'd160,x=0~7,刚好把0页的8列数据显示完毕,这是y+1=>y=1,表示要显示第1页,那么x+8'd168,x=0~7,刚好把第1页的8列数据显示完。显示完之后F_flag就置0了,前面已经说过这个flag置1置0的意义。接下来r_flag就置1了,就开始执行这句话:(r_flag ? (y ? (x + 8'd184) : (x + 8'd176)),x在这里加的8'd184和8'd176是因为字符‘r’的字模就放在ROM的这个起地址处,以此类推,x要加的数取决于你把字符字模数据放在了ROM的哪个地址。字符的rom_addr管理都可以这样理解。
数字的rom_addr管理稍有不同。
以‘个’位的显示为列:
注:个、十、百、千、万位的数字显示都是共用ROM中的0-9字模;
注:我将0-9的字模在ROM的起始地址0处依次写入;
(ge_flag ? (y ? (x + (ge << 4) + 4'd8) : (x + (ge << 4)));y=0时表示显示第0页,开始执行代码 (x + (ge << 4))。
假设程序case(ge)时判断此时ge=1,这时要显示‘1’所对应的字模数据,‘1’的第0页字模起始地址是16,第1页起始地址是24,(x + (ge << 4) = x + (1*16) = x+16(x=0~7),这样是不是依次把‘1’的第0页也就是上半部分的字模数据从ROM里取走了。同样y=1时,表明要显示第1页的字模数据了,执行x + (ge << 4) + 4'd8,即x+16+8=x+24(x=0~7),这样是不是依次把‘1’的下半部分的字模数据从ROM里取走了。其他的十、百、千、万位的ROM管理跟个位的管理是相同的。可以对照着下面ROM里的字模内容计算一下。
到这里FPGA驱动OLED实现动态实时显示的关键部分就说完了。这个Demo的工程压缩包可以在前面的文章中找下载链接。