前面已经介绍了向RAM中写入静态字模数据来显示静态的字符和汉字。接下来实现动态显示字符在OLED屏的不同位置。
动态显示字符的核心就是从ROM中读取字符的字模,但取出来的字模数据如果直接写进RAM的话,只能实现字符在某一页的显示,而不能实现任意坐标下的显示。所以在写进RAM之前,我们应该对字模数据做一定处理,然后再写进RAM中。接着RAM读取模块(前面已经介绍过了,本次会改变等待的值,提高一下刷新率)会不断的读取字模数据进行显示。
这里可以先参考一下C语言实现的任意坐标下画点。
/*************************************************************************/
/*函数功能: 画点 */
/*入口参数: */
/* x:横坐标 0~127 */
/* y:纵坐标 0~63 */
/* dot:0,清空;1,填充 */
/*************************************************************************/
void OLED_DrawPoint(u8 x,u8 y,u8 t)
{
u8 pos,bx,temp=0;
if(x>127||y>63)return;//超出范围了.
pos=7-y/8;
bx=y%8;
temp=1<<(7-bx);
if(t)OLED_GRAM[x][pos]|=temp;
else OLED_GRAM[x][pos]&=~temp;
}
module oled_show_char(
input clk, //时钟信号
input rst_n, //按键复位
input [7:0] ascll, //需要显示字符的ascll码
input [4:0] font_size, //显示字符的字体大小 12,16和24
input [6:0] x, //显示的x坐标
input [5:0] y, //显示的y坐标
input en_ram_wr, //模块使能信号
input add_dec_x, //按键改变x坐标
input add_dec_y, //按键改变y坐标
output reg wren, //ram写使能
output [9:0]wraddress, //ram写地址
output reg [7:0]data //写进ram的数据
);
//状态说明
//清除RAM中的数据 等待模块使能 读取rom中的数据 保存数据1 保存数据2(rom地址改变地址后要第二个时钟值才会改变)
//根据坐标改变数据 写数据 完成
parameter ClearRAM=0,WaitEn=1,ReadData=2,SaveData1=3,SaveData2=4,ChangeData=5,WriteData=6,Done=7;
reg [2:0] state,next_state; //状态存储
reg [7:0] zm; //rom中取出的数据
reg [7:0] ram_zm[127:0][7:0]; //写进ram的数据 因为需要根据坐标来变换 所以寄存一下数据 然后一次性写入
reg [3:0] zm_w_cnt; //字模每一个字节的位计数器
reg [5:0] zm_cnt; //字模个数计数器
reg [9:0] rom_address12; //12号字体的rom地址
reg [10:0] rom_address16; //16号字体的rom地址
reg [11:0] rom_address24; //24号字体的rom地址
wire [7:0] zm12_data,zm16_data,zm24_data; //存储12,16和24号字体读出来的数据
reg [6:0] ram_zm_cntx; //读取ram_zm时用到的x计数器
reg [2:0] ram_zm_cnty; //读取ram_zm时用到的x计数器
reg [10:0] wraddress_cnt; //ram地址计数器 用11位的有1024
reg [4:0]zm_cnty; //y坐标计数
reg [6:0] xr;//x增加的坐标
reg [5:0] yr;//y增加的坐标
//ram写地址赋值
assign wraddress = wraddress_cnt<11'd1024 ? wraddress_cnt : 10'd0;
//状态机下一个状态确认
always @(*) begin
if(!rst_n)
next_state = ClearRAM; //复位进入初始状态
else begin
case(state)
//清除RAM
ClearRAM: next_state = (wraddress_cnt == 11'd1023) ? WaitEn : ClearRAM;
//等待模块使得能
WaitEn: next_state = en_ram_wr ? ReadData : WaitEn;
//读取rom数据(rden拉高)
ReadData: next_state = SaveData1;
//rom地址变化后,两个时钟周期才会出值
SaveData1: next_state = SaveData2;
SaveData2: next_state = ChangeData;
//根据坐标变换数据
ChangeData:begin
case(font_size)
//因为进入这个状态后 zm_cnt的值已经加到1了,所以判断的时候要多一个1
//判断是否已经读取到最后一个字模数据了且最后一个字模的8位也全部改变完成则进入下一个状态
//否则判断是否一个字模数据的8位已经改变完成 完成则读取下一个字模数据
5'd12: next_state = (zm_cnt == 6'd13) ? WriteData : (zm_w_cnt == 4'd7) ? ReadData : ChangeData;
5'd16: next_state = (zm_cnt == 6'd17) ? WriteData : (zm_w_cnt == 4'd7) ? ReadData : ChangeData;
5'd24: next_state = (zm_cnt == 6'd37) ? WriteData : (zm_w_cnt == 4'd7) ? ReadData : ChangeData;
endcase
end
//向RAM中写数据
WriteData: begin
next_state = (wraddress_cnt == 11'd1024) ? Done : WriteData;
end
//完成
//完成一次写操作后开始下一次写操作
//可以通过按键改变坐标
//拨码开关改变ascll输入
//显示不同的字符在不同的坐标
Done:next_state = ClearRAM;
endcase
end
end
//状态逻辑变量赋值
always @(posedge clk,negedge rst_n) begin
if(!rst_n) begin
rom_address12 <= 12'd0;
rom_address16 <= 12'd0;
rom_address24 <= 12'd0;
zm <= 8'd0;
wren <= 1'b1;
data <= 8'd0;
end
else begin
case(state)
//RAM清零
ClearRAM:begin
ram_zm[ram_zm_cntx][ram_zm_cnty] = 8'd0;//ram数据寄存器清零
wren <= 1'b0;//写使能信号拉高 清除RAM中的数据//持续刷新的话,复位状态就不清除RAM中的数据了,把ram寄存器中数据清除就好
end
//等待模块使能
WaitEn: begin
wren <= 1'b0;//写使能信号拉低
//到改变数据的时候地址已经加过1了,所以初始地址减1
case(font_size)
5'd12:rom_address12 <= (ascll-33)*12-1;//12号字体一个字模数据占12字节
5'd16:rom_address16 <= (ascll-33)*16-1;//16号字体一个字模数据占16字节
5'd24:rom_address24 <= (ascll-33)*36-1;//24号字体一个字模数据占36字节
endcase
end
//读取ROM中的数据
ReadData: begin
case(font_size)//地址增加
5'd12:rom_address12 <= rom_address12 + 1'b1;
5'd16:rom_address16 <= rom_address16 + 1'b1;
5'd24:rom_address24 <= rom_address24 + 1'b1;
endcase
end
//保存读取到的值
SaveData2:begin
case(font_size)//保存读取到的数据
5'd12: zm <= zm12_data;
5'd16: zm <= zm16_data;
5'd24: zm <= zm24_data;
endcase
end
//根据坐标给要写进RAM中的数据赋值
ChangeData:begin
if(zm[7-zm_w_cnt]) begin
//zm_cnt多加了一个 所以减1
//如果该位为1 就把这个坐标的点置1
if(font_size == 5'd24)
ram_zm[x+(zm_cnt-1)/3][(y+yr+zm_cnty)/8] <= ram_zm[x+(zm_cnt-1)/3][(y+yr+zm_cnty)/8] | 1<<(7-(y+yr+zm_w_cnt)%8);//1左移多少位
else
ram_zm[x+(zm_cnt-1)/2][(y+yr+zm_cnty)/8] <= ram_zm[x+(zm_cnt-1)/2][(y+yr+zm_cnty)/8] | 1<<(7-(y+yr+zm_w_cnt)%8);//1左移多少位
end
else begin
if(font_size == 5'd24)
ram_zm[x+(zm_cnt-1)/3][(y+yr+zm_cnty+1)/8] <= ram_zm[x+(zm_cnt-1)/3][(y+yr+zm_cnty+1)/8];
else
ram_zm[x+(zm_cnt-1)/2][(y+yr+zm_cnty+1)/8] <= ram_zm[x+(zm_cnt-1)/2][(y+yr+zm_cnty+1)/8];
end
end
//把数据写进RAM中
WriteData:begin
wren <= 1'b1;//写使能信号拉高
data <= ram_zm[ram_zm_cntx][ram_zm_cnty];//数据赋值
end
//完成
Done:begin
wren <= 1'b0;//写使能信号拉低
data <= 8'd0;
end
endcase
end
end
//当前状态赋值
always @(posedge clk,negedge rst_n) begin
if(!rst_n)
state <= ClearRAM;
else
state <= next_state;
end
//各种计数器控制
always @(posedge clk,negedge rst_n) begin
if(!rst_n) begin
zm_cnt <= 6'd0; //字模字节计数器
zm_w_cnt <= 4'd0; //一个字节的位计数器
//比如一个16号的字体,按照从左往右的扫描就需要y坐标增加16x坐标才加1
zm_cnty <= 5'd0; //当前坐标开始的y坐标计数器
ram_zm_cnty <= 3'd0; //字模存储数组读取或者清零时用的计数器
ram_zm_cntx <= 7'd0;
wraddress_cnt <= 11'd0; //写地址计数器
end
else begin
case(state)
ClearRAM:begin
//写地址增加 清除RAM中的数据
wraddress_cnt <= wraddress_cnt + 1'b1;
//字模寄存器计数器增加 将寄存器中内容清空
ram_zm_cnty <= ram_zm_cnty + 1'b1;
if(ram_zm_cnty == 3'd7)
ram_zm_cntx <= ram_zm_cntx + 1'b1;
else
ram_zm_cntx <= ram_zm_cntx;
end
WaitEn:begin
//等待模块使能 计数器的值清零
wraddress_cnt <= 11'd0;
ram_zm_cnty <= 3'd0;
ram_zm_cntx <= 7'd0;
zm_cnt <= 6'd0;
zm_w_cnt <= 4'd0;
zm_cnty <= 5'd0;
end
//读取rom中的数据
ReadData:begin
zm_cnt <= zm_cnt + 1'b1;//每读取字模的一个字节数据 计数器加1
zm_w_cnt <= 4'd0; //位计数器清零
//存进ROM的字模数据是从上到下,从左到右扫描的
//比如16号字体,一列就有两个字节,所以要读取两个字节后,增加的y坐标才清零
//12号字体一列也是两个字节,24号字体一列是3个字节
if(font_size==5'd24)
zm_cnty <= (zm_cnt%3==0) ? 5'd0 : zm_cnty;
else
zm_cnty <= (zm_cnt%2==0) ? 5'd0 : zm_cnty;
end
ChangeData:begin
zm_w_cnt <= zm_w_cnt + 1'b1;//位计数器增加
zm_cnty <= zm_cnty + 1'b1;//y坐标增加
end
WriteData: begin
wraddress_cnt <= wraddress_cnt + 1'b1;//写地址计数器增加
ram_zm_cntx <= ram_zm_cntx + 1'b1;//字模寄存器 计数器增加,读取寄存器中的内容写进ram中
if(ram_zm_cntx == 7'd127)
ram_zm_cnty <= ram_zm_cnty + 1'b1;
else
ram_zm_cnty <= ram_zm_cnty;
end
Done:begin
wraddress_cnt <= 11'd0;//计数器清零
ram_zm_cntx <= 7'd0;
ram_zm_cnty <= 6'd0;
end
endcase
end
end
//下面这段代码用来按键增加x坐标和y坐标
//y坐标增减可以
//但是xr加进去之后,逻辑组合节点就会增加2.3倍
//暂时不知道什么原因
reg dirx,diry;//增加或者减少坐标的方向控制
reg ac_x_r1,ac_x_r2,ac_y_r1,ac_y_r2;//按键边缘检测信号
//如果直接使用negedege rst_n异步信号的话,无法消抖
always @(posedge clk) begin
if(!rst_n) begin
ac_x_r1 <= 1'b0;
ac_x_r2 <= 1'b0;
ac_y_r1 <= 1'b0;
ac_y_r2 <= 1'b0;
xr <= 7'd0;
yr <= 6'd0;
dirx <= 1'b0;
diry <= 1'b0;
end
else begin
ac_x_r1 <= add_dec_x;
ac_x_r2 <= ac_x_r1;
ac_y_r1 <= add_dec_y;
ac_y_r2 <= ac_y_r1;
dirx = (xr>7'd100) ? 1'b1 : (xr<5'd20) ? 1'b0 : dirx;
diry = (yr<6'd40) ? 1'b1 : (yr<3'd5) ? 1'b0 : diry;
if(ac_x_r1 & ~ac_x_r2)//上升沿检测
if(dirx)
xr <= xr+3'd5;
else
xr <= xr-3'd5;
if(ac_y_r1 & ~ac_y_r2)//上升沿检测
yr <= diry ? yr+3'd5 : yr-3'd5;
else
yr <= yr;
end
end
//12号字体rom
zm_12 zm_12_inst(
.clock(clk),
.address(rom_address12),
.q(zm12_data)
);
//16号字体rom
zm_16 zm_16_inst(
.clock(clk),
.address(rom_address16),
.q(zm16_data)
);
//24号字体rom
zm_24 zm_24_inst(
.clock(clk),
.address(rom_address24),
.q(zm24_data)
);
endmodule
最后x坐标增加的地方存在问题,在坐标显示上加入改变的x坐标后,会使得组合逻辑节点数量增加2-3倍,但是y坐标加进去不会,暂时也没有找到原因(有看懂的朋友可以回复一下)。
reg [7:0] ram_zm[127:0][7:0]; //写进ram的数据 因为需要根据坐标来变换 所以寄存一下数据 然后一次性写入
将要写进RAM的1024个字节,拆写成x和y坐标的形式,便于观察,后面写进RAM中的时候再转换成连续的1024个字节
//RAM清零
ClearRAM:begin
ram_zm[ram_zm_cntx][ram_zm_cnty] = 8'd0;//ram数据寄存器清零
wren <= 1'b0;//读使能信号拉高 清除RAM中的数据//持续刷新的话,复位状态就不清除RAM中的数据了,把ram寄存器中数据清除就好
end
ClearRAM:begin
//写地址增加 清除RAM中的数据
wraddress_cnt <= wraddress_cnt + 1'b1;
//字模寄存器计数器增加 将寄存器中内容清空
ram_zm_cnty <= ram_zm_cnty + 1'b1;
if(ram_zm_cnty == 3'd7)
ram_zm_cntx <= ram_zm_cntx + 1'b1;
else
ram_zm_cntx <= ram_zm_cntx;
en
每次写字模数据前,先把之前寄存的字模数据清除了,RAM的话就不需要清除了,因为是持续写RAM的,下次写进去的时候会覆盖之前的数据。
还有就是感觉使用for循环不是很好,所以选择使用计数器的方式在时序电路里面来实现清零。
ReadData:begin
zm_cnt <= zm_cnt + 1'b1;//每读取字模的一个字节数据 计数器加1
zm_w_cnt <= 4'd0; //位计数器清零
//存进ROM的字模数据是从上到下,从左到右扫描的
//比如16号字体,一列就有两个字节,所以要读取两个字节后,增加的y坐标才清零
//12号字体一列也是两个字节,24号字体一列是3个字节
if(font_size==5'd24)
zm_cnty <= (zm_cnt%3==0) ? 5'd0 : zm_cnty;
else
zm_cnty <= (zm_cnt%2==0) ? 5'd0 : zm_cnty;
end
然后就是写进RAM中的y坐标,这个坐标值不应该是写完一个字节后就回到初始的y值(我一开始就想错了),因为得到的字模数据是从上到下,从左到右扫描出来的,所以要一直增加到一列数据写完(12号和16号字体一列两字节,24号字体一列3字节)之后再回到初始的y值,也就是增值清零。
//根据坐标给要写进RAM中的数据赋值
ChangeData:begin
if(zm[7-zm_w_cnt]) begin
//zm_cnt多加了一个 所以减1
//如果该位为1 就把这个坐标的点置1
if(font_size == 5'd24)
ram_zm[x+(zm_cnt-1)/3][(y+yr+zm_cnty+1)/8] <= ram_zm[x+(zm_cnt-1)/3][(y+yr+zm_cnty+1)/8] | 1<<(7-(y+yr+zm_w_cnt+1)%8);//1左移多少位
else
ram_zm[x+(zm_cnt-1)/2][(y+yr+zm_cnty+1)/8] <= ram_zm[x+(zm_cnt-1)/2][(y+yr+zm_cnty+1)/8] | 1<<(7-(y+yr+zm_w_cnt+1)%8);//1左移多少位
end
else begin
if(font_size == 5'd24)
ram_zm[x+(zm_cnt-1)/3][(y+yr+zm_cnty+1)/8] <= ram_zm[x+(zm_cnt-1)/3][(y+yr+zm_cnty+1)/8];
else
ram_zm[x+(zm_cnt-1)/2][(y+yr+zm_cnty+1)/8] <= ram_zm[x+(zm_cnt-1)/2][(y+yr+zm_cnty+1)/8];
end
end
最后就是这个画点的地方,x坐标变化就是给定的x坐标(原始的x坐标)加上字模字节个数的偏移量,12号字体和16号字体,每读两字节,x坐标加1,而24号字体,每读3字节,x坐标加1;y坐标则是给定的y坐标加上当前字节读取的位数,每读一个字节的一位,y坐标加1,12号和16号字体读取两字节后增加的y坐标清零,24号字体则读取3字节后清零。
后面就是对一个字节的位操作,判断输入字节的某一位是否为1,为1则将当前的y坐标对应字节的那一位清零(描述有点不太清除)。就是y坐标是8个字节组成的,所以y坐标不能简单的用一个数来表示,而是某一个字节的某一位来表示y坐标。(用下面的图片简单说明一下)
图中有个错误:(0,3)应该是从第一个字节的第4位开始往低位写
使用PCtoLCD2002+C2MIF生成字模,然后转换为mif文件,最后存进ROM中
软件提取:
链接:https://pan.baidu.com/s/1vHe-5lWX-mxuCFA_y8aZpg
提取码:1poq
参考链接:C2MIF的使用
打开PCtoLCD2002软件,设置选项
然后输入下面的字符并分别生成12号,16号和24号字体大小的字模
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
以16号字体为例,生成的字模文本是这样的
我们先将头部的那些说明删掉,然后再删除数据后面的注释
有个使用小技巧,打开Nodepad++,按住Alt键,然后再使用鼠标左键就可以选择多列进行操作(如图,然后点击删除键就可以删除后面的注释)
删除后的样子
接着我们直接CTAL+A赋值所有数据到C2MIF中(C2MIF记得使用管理员身份打开,因为生成的文件是放桌面的,需要管理员权限)
我也不知道头部内容会不会有影响,但我还是把头部的那些内容删除了
最后的样子
然后我们就可以创建ROM IP核,将mif文件放进去了。
ROM IP核的创建比较简单,就不赘述了,注意ROM的大小的选择。因为不同字号字体需要的ROM深度是不一样的,创建时可以查看一下mif文件中的深度,然后ROM的深度应该大于等于这个值。
在顶层模块中增加显示字符的模块并对输出信号做切换来显示静态或者动态的数据。
module oled_drive(
input clk, //时钟信号 50MHz
input rst_n, //按键复位
input ram_rst, //ram复位 高电平复位
input change_show,//用来切换显示静态数据还是动态数据
input [2:0] ascll_add,//ascll字符输入 用来动态显示
input add_dec_x, //按键控制显示的x坐标加减
input add_dec_y, //按键控制显示的y坐标加减
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中的数据
//下面这样做 主要是把静态显示和动态显示分开,可以通过乒乓开关切换
wire wren_ramw; //ram写模块输出的写使能信号
wire [9:0] wraddress_ramw;//ram写模块输出的写地址信号
wire [7:0] data_ramw;//ram写模块输出的数据信号
wire wren_showchar; //显示字符模块输出的写使能信号
wire [9:0] wraddress_showchar;//显示字符模块输出的写地址信号
wire [7:0] data_showchar;//显示字符模块输出的数据
//一个信号只能由一个信号来驱动,所以需要选择一下
//判断是否初始化完成 完成则显示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;
//通过乒乓开关切换静态显示还是动态显示
assign wren = change_show ? wren_showchar : wren_ramw;
assign wraddress = change_show ? wraddress_showchar : wraddress_ramw;
assign wrdata = change_show ? data_showchar : data_ramw;
//改变模块使能信号
wire en_ram_wr = change_show ? 0 : 1;
wire en_showchar = change_show ? 1 : 0;
//时钟分频模块 产生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(en_ram_wr),
.wren(wren_ramw),
.wraddress(wraddress_ramw),
.data(data_ramw)
);
//oled显示字符
oled_show_char oled_show_char_inst(
.clk(clk_1m),
.rst_n(rst_n),
.ascll(8'd48+ascll_add),
.font_size(5'd24),
.x(7'd60),
.y(6'd10),
.add_dec_x(add_dec_x),
.add_dec_y(add_dec_y),
.en_ram_wr(en_showchar),
.wren(wren_showchar),
.wraddress(wraddress_showchar),
.data(data_showchar)
);
//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
当实验现象和自己预期的结果不一样的时候,多使用仿真查看状态变化,计数器值等等可以更快的发现问题并解决问题。
`timescale 1ns/1ns //仿真单位为1ns,精度为1ns
module oled_show_char_tb();
reg clk;
reg rst_n;
reg [7:0]ascll;
reg [4:0]font_size;
reg [6:0]x;
reg [5:0]y;
reg en_ram_wr;
wire wren;
wire [9:0] wraddress;
wire [7:0] data;
oled_show_char oled_show_char_inst(
.clk(clk),
.rst_n(rst_n),
.ascll(ascll),
.font_size(font_size),
.x(x),
.y(y),
.en_ram_wr(en_ram_wr),
.wren(wren),
.wraddress(wraddress),
.data(data)
);
initial begin
#0 clk = 0;
rst_n = 0;
ascll = 8'd48;
font_size = 5'd16;
x = 7'd0;
y = 6'd3;
en_ram_wr = 1'b1;
#20 rst_n = 1;
end
always #5 clk = ~clk;
endmodule