本文主要是介绍基于Lattice XO2-4000HC FPGA核心板的SSD1306 OLED12832驱动芯片指令及工作方式详述(Verilog),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
- :pushpin: 前言
- :gift: oled_driver_adc驱动顶层模块代码
- :umbrella: OLED驱动原理简述
- :paperclip: 结构及引脚分配
- :game_die: MCU总线接口:4线SPI
- :microscope: OLED驱动模块工作分析(结合代码)
- :art: 图形显示数据RAM(GDDRAM)
- 寻址模式
- 页寻址模式
- 水平模式寻址
- 垂直模式寻址
- 重映射及多路复用
- :fish_cake: 代码中使用的工作方式
- 单个字符编码
- 字符串写入顺序
- 多行字符串写入顺序
📌 前言
本篇文章为《基于Lattice XO2-4000HC FPGA核心板及电子森林综合训练底板的ADC数字电压表及OLED显示设计(Verilog)》一文的延伸,针对该项目中的SSD1306 OLED显示屏驱动的指令及工作(扫描)方式进行较为详细的说明,并结合原文中oled_driver_adc
驱动模块的部分代码进行详细分析。
👉 注意事项及源代码参见原文。
🎁 oled_driver_adc驱动顶层模块代码
为便于后续对照代码进行说明,在此先贴出源代码,子模块的代码在原文中给出,简要而言为两个只读ROM,分别读取命令编码及字符编码。
module oled_driver_adc #(parameter CMD_WIDTH = 8, // LCD命令宽度parameter CMD_DEPTH = 5'd25, // LCD初始化的命令的数量parameter CHAR_WIDTH = 40, // 一个文字的数据宽度parameter CHAR_DEPTH = 7'd123 // 文字库数量
)(input sys_clk,input rst_n,input [7:0] oled_display_digital, // 两位ADC数据(一位小数)output reg oled_csn, //OLCD液晶屏使能output reg oled_rst, //OLCD液晶屏复位output reg oled_dcn, //OLCD数据指令控制output reg oled_clk, //OLCD时钟信号output reg oled_data //OLCD数据信号
);localparam IDLE = 3'b0, MAIN = 3'b1, INIT = 3'b10;localparam SCAN = 3'b11, WRITE = 3'b100, DELAY = 3'b101;localparam HIGH = 1'b1, LOW = 1'b0;localparam DATA = 1'b1, CMD = 1'b0;wire [CMD_WIDTH-1:0] cmd_out; // cmd_RAM输出的8位命令wire [CHAR_WIDTH-1:0] char_out; // data_RAM输出的40位文字reg [7:0] wr_reg;reg [7:0] ypage, xpage_high, xpage_low;reg [(8*21-1):0] char; // 字符串reg [4:0] char_num; // 文字个数 最多16reg [4:0] cmd_addr;reg [7:0] char_addr;reg [2:0] cnt_main;reg [2:0] cnt_init;reg [3:0] cnt_scan;reg [4:0] cnt_write;reg [14:0]num_delay, cnt_delay;reg [2:0] state, state_last;oled_cmd_RAM #(.RAM_WIDTH(CMD_WIDTH),.RAM_DEPTH(CMD_DEPTH),.ADDR_WIDTH(5)) CMD_RAM(.clk(sys_clk),.rst_n(rst_n),.re(oled_dcn),.addr(cmd_addr),.data(cmd_out));oled_char_RAM #(.RAM_WIDTH(CHAR_WIDTH),.RAM_DEPTH(CHAR_DEPTH),.ADDR_WIDTH(8)) CHAR_RAM(.clk(sys_clk),.rst_n(rst_n),.re(oled_dcn),.addr(char_addr),.data(char_out));always @(posedge sys_clk or negedge rst_n) beginif(!rst_n) begincnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;wr_reg <= 1'b0;ypage <= 1'b0;xpage_high <= 1'b0; xpage_low <= 1'b0;char <= 1'b0; char_num <= 1'b0;cmd_addr <= 1'b0;char_addr <= 1'b0;num_delay <= 15'd5; cnt_delay <= 1'b0; oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_data <= LOW;state <= IDLE; state_last <= IDLE;end else begincase(state)IDLE: begincnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;wr_reg <= 1'b0;ypage <= 1'b0;xpage_high <= 1'b0; xpage_low <= 1'b0;char <= 1'b0; char_num <= 1'b0; cmd_addr <= 1'b0;char_addr <= 1'b0;num_delay <= 15'd5; cnt_delay <= 1'b0; oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_data <= LOW;state <= MAIN; state_last <= MAIN;endMAIN: beginif(cnt_main >= 3'd6)cnt_main <= 3'd5;else cnt_main <= cnt_main + 1'b1;case(cnt_main) //MAIN状态3'd0: begin state <= INIT; end3'd1: begin ypage <= 8'hb0; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "ADC DATA DISPLAY";state <= SCAN; end3'd2: begin ypage <= 8'hb1; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "VOLTAGE: . V ";state <= SCAN; end3'd3: begin ypage <= 8'hb2; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "AUTHOR: ZIRU PAN";state <= SCAN; end3'd4: begin ypage <= 8'hb3; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= " ";state <= SCAN; end3'd5: begin ypage <= 8'hb1; xpage_high <= 8'h15; xpage_low <= 8'h00; char_num <= 5'd1 ; char <= oled_display_digital[7:4]; state <= SCAN; end3'd6: begin ypage <= 8'hb1; xpage_high <= 8'h16; xpage_low <= 8'h00; char_num <= 5'd1 ; char <= oled_display_digital[3:0]; state <= SCAN; enddefault: state <= IDLE;endcaseendINIT: begin //初始化状态case(cnt_init)5'd0: begin oled_rst <= LOW; cnt_init <= cnt_init + 1'b1; end //复位有效5'd1: begin num_delay <= 15'd25000; state <= DELAY; state_last <= INIT; cnt_init <= cnt_init + 1'b1; end //延时大于3us5'd2: begin oled_rst <= HIGH; cnt_init <= cnt_init + 1'b1; end //复位恢复5'd3: begin num_delay <= 15'd25000; state <= DELAY; state_last <= INIT; cnt_init <= cnt_init + 1'b1; end //延时大于220us5'd4: begin if(cmd_addr >= CMD_DEPTH) begincmd_addr <= 1'b0;cnt_init <= cnt_init + 1'b1;end else begin cmd_addr <= cmd_addr + 1'b1; num_delay <= 15'd5;oled_dcn <= CMD; wr_reg <= cmd_out;state <= WRITE; state_last <= INIT;endend5'd5: begin cnt_init <= 1'b0; state <= MAIN; enddefault: state <= IDLE;endcaseendSCAN: beginif(cnt_scan == 4'd12) beginif(char_num) cnt_scan <= 4'd3;else cnt_scan <= cnt_scan + 1'b1;end else if(cnt_scan == 4'd13) cnt_scan <= 4'd0;else cnt_scan <= cnt_scan + 1'b1;case(cnt_scan)4'd0: begin oled_dcn <= CMD; wr_reg <= ypage; state <= WRITE; state_last <= SCAN; end //定位列页地址4'd1: begin oled_dcn <= CMD; wr_reg <= xpage_low; state <= WRITE; state_last <= SCAN; end //定位行地址低位4'd2: begin oled_dcn <= CMD; wr_reg <= xpage_high; state <= WRITE; state_last <= SCAN; end //定位行地址高位4'd3: begin char_num <= char_num - 1'b1; end4'd4: begin char_addr <= char[(char_num*8)+:8]; end4'd5: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end //5*8点阵变成8*84'd6: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end //5*8点阵变成8*84'd7: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end //5*8点阵变成8*84'd8: begin oled_dcn <= DATA; wr_reg <= char_out[39:32]; state <= WRITE; state_last <= SCAN; end4'd9: begin oled_dcn <= DATA; wr_reg <= char_out[31:24]; state <= WRITE; state_last <= SCAN; end4'd10:begin oled_dcn <= DATA; wr_reg <= char_out[23:16]; state <= WRITE; state_last <= SCAN; end4'd11:begin oled_dcn <= DATA; wr_reg <= char_out[15:8] ; state <= WRITE; state_last <= SCAN; end4'd12:begin oled_dcn <= DATA; wr_reg <= char_out[7:0] ; state <= WRITE; state_last <= SCAN; end4'd13:begin state <= MAIN; enddefault: state <= IDLE;endcaseendWRITE: begin //WRITE状态,将数据按照SPI时序发送给屏幕if(cnt_write >= 5'd17) cnt_write <= 5'd0;else cnt_write <= cnt_write + 1'b1;case(cnt_write)5'd0: begin oled_csn <= LOW; end //9位数据最高位为命令数据控制位5'd1: begin oled_clk <= LOW; oled_data <= wr_reg[7]; end //先发高位数据5'd2: begin oled_clk <= HIGH; end5'd3: begin oled_clk <= LOW; oled_data <= wr_reg[6]; end5'd4: begin oled_clk <= HIGH; end5'd5: begin oled_clk <= LOW; oled_data <= wr_reg[5]; end5'd6: begin oled_clk <= HIGH; end5'd7: begin oled_clk <= LOW; oled_data <= wr_reg[4]; end5'd8: begin oled_clk <= HIGH; end5'd9: begin oled_clk <= LOW; oled_data <= wr_reg[3]; end5'd10:begin oled_clk <= HIGH; end5'd11:begin oled_clk <= LOW; oled_data <= wr_reg[2]; end5'd12:begin oled_clk <= HIGH; end5'd13:begin oled_clk <= LOW; oled_data <= wr_reg[1]; end5'd14:begin oled_clk <= HIGH; end5'd15:begin oled_clk <= LOW; oled_data <= wr_reg[0]; end //后发低位数据5'd16:begin oled_clk <= HIGH; end5'd17:begin oled_csn <= HIGH; state <= DELAY; enddefault: state <= IDLE;endcaseendDELAY: beginif(cnt_delay >= num_delay) begincnt_delay <= 15'd0; state <= state_last; end else cnt_delay <= cnt_delay + 1'b1;enddefault: state <= IDLE;endcaseendend
endmodule
☔️ OLED驱动原理简述
📎 结构及引脚分配
该部分参看SSD1306 OLED驱动芯片 详细介绍、百度云:SSD1306 英文手册(提取码csdn,由于CSDN存在相同资源但是不免费)及百度文库:SSD1306 中文手册,在此不做详述。
🎲 MCU总线接口:4线SPI
关于SSD1306的总线接口,在上述参考链接中均有说明,在此对原文FPGA核心板中所使用的4线SPI接口做详细说明。
FPGA GPIO与OLED(其实是与SSD1306,该驱动集成在显示屏背部)结构连接电路图如下图所示,包括VCC、串行时钟(SCLK)、串行数据(SDIN)、数据/命令控制(D/C)、片选(CS#)及一复位(RES)。
在SCLK每个上升沿,SDIN上的数据按MSB~LSB顺序移位到一个8位移位寄存器中。每八个时钟对D/C进行一次采样,移位寄存器中的8位数据根据D/C的采样结果决定写入到图形显示数据RAM(GDDRAM)或命令寄存器。其写时序如下图所示:
根据上述4线SPI接口的工作原理,即:片选有效后,命令解码模块根据D/C#确定输入数据是被解释为数据还是命令。D/C#引脚为HIGH,SDIN输入的D[7:0]则被解释为写入图形显示数据RAM (Graphic Display Data RAM, GDDRAM)的显示数据。如果是LOW,则D[7:0]被解释为命令,然后被解码并写入相应的命令寄存器。
代码中,在INIT
状态下5’d4处cmd_addr <= cmd_addr + 1'b1; oled_dcn <= CMD; wr_reg <= cmd_out;
即指定D/C#为低电平,而后输入的cmd_out
则被解释为命令编码,并存入相应寄存器中。
SCAN
状态中,oled_dcn <= DATA
则指定D/C#为高电平,而后输入wr_reg
的数据为字符编码的一个八位,连续输入5次,则代表一个字母的全部显示编码,这杯解释为图形显示数据,则存入GDDRAM中。
🔬 OLED驱动模块工作分析(结合代码)
🎨 图形显示数据RAM(GDDRAM)
GDDRAM是一个位映射静态RAM,保存要显示的位模式。内存大小为128×64位,分成8页,从PAGE0到PAGE7,用于黑白128×64点阵显示,如下图所示。
注:重映射在后文说明。
当一个数据字节被写入GDDRAM时,当前列同页的所有行图像数据被填充(即列地址指针指向的整个列(8位)被填充)。数据位D0被写入顶部行。数据位D7写入底部一行。
寻址模式
在SSD1306中有3种不同的内存寻址模式:页寻址模式、水平寻址模式和垂直寻址模式。设置寻址模式需要两个字节的命令:
- 20H,00H:水平寻址模式;
- 20H,01H,垂直寻址模式;
- 20H,02H,页寻址模式(复位);
- 20H,03H,无效。
页寻址模式
页面寻址模式(A[1:0]-10xb)页寻址模式。当GDDRAM被读写后,列地址指针自动增加1。如果列地址指针到达列结束地址,列地址指针将重置为列起始地址,页地址指针不变。用户必须设置新的页面和列地址,以便访问下一页RAM内容。页寻址模式的页和列地址的移动顺序如下图所示。
在正常GDDRAM读写和页寻址模式下,需要定义启动RAM访问指针的位置:
- 通过命令B0H~B7H设置目标显示位置的页面起始地址;
- 通过命令00H~0FH设置指针的较低列地址,高4位恒定为00H,低4位为要设置的起始列地址的低4位;
- 通过命令10H~1FH设置指针的较高列地址,高4位恒定为01H,低4位为要设置的起始列地址的高4位;
- *例如:设定B0H、00H、15H,则指针在PAGE1,COL80(50H = 80D)的COM0处。
例如,页面地址设置为B2H,较低列地址为03H,较高列地址为10H。这意味着起始列是PAGE2的SEG3。内存访问指针的位置如下图所示,输入数据字节将被写入RAM第3列的位置。
代码中,MIAN
状态下设置了要显示的字符串,其中ypage
指定了页,xpage_low
与xpage_high
指定了低与高列地址。由于该OLED屏幕为128×32,因此分为4页(page)和SEG0~SEG127,则ypage = 8'hb0~8'hb3
。
由于每个字符为5×8,保持恰当的字间距使之扩充为8×8,因此一行可以显示16个字符,对于代码中需要不断动态刷新的一行字符串 “VOLTAGE:空空空.空V空空”,两个不断更新的数字需要在第三和第四个“空”字处,因此单独设置这两个字符的访问指针为:
ypage <= 8'hb1; xpage_high <= 8'h15; xpage_low <= 8'h00; // 50H = 80(COL)
ypage <= 8'hb1; xpage_high <= 8'h16; xpage_low <= 8'h00; // 60H = 96(COL)
对于满满一行字符串,则设置高低列地址均为00H,则指针在该页COL0处。
水平模式寻址
在水平寻址模式下,当GDDRAM被读写后,列地址指针自动增加1。如果列地址指针到达列结束地址。列地址指针被重置为列起始地址,页地址指针增加1。
水平寻址模式下,页面和列地址的移动顺序如下图所示。当列地址指针和页地址指针都到达结束地址时,指针被重置为列起始地址和页起始地址(图中虚线)。
垂直模式寻址
在垂直寻址模式下,当GDDRAM被读写后,页面地址指针自动增加1。如果页面地址指针到达页面结束地址,页面地址指针被重置为页面开始地址,列地址指针加1。
垂直寻址方式下,页面和列地址的移动顺序如下图所示。当列地址指针和页地址指针都到达结束地址时,指针被重置为列起始地址和页起始地址(图中虚线)。
重映射及多路复用
- A0/A1,设置段映射:
- A0H:列地址0映射到SEG0(复位状态);
- A1H:列地址127映射到SEG0,即原本:SEG0对应COL0→SEG127对应COL127,现在映射为:SEG0对应COL127→SEG127对应COL0。
-
A8 + A[5:0] (00H~3FH),设置多路复用率(MUX Ratio)为N+1:A[5:0]从0到14的取值都是无效的。代码中,命令RAM中
8'ha8
后接8'h1F
,则指定复用率(MUX Ratio) = 1FH + 1 = 20H,则复用率为20H。 -
C0/C8,设置COM输出扫描方向:
- C0:正常模式(复位状态),从COM0→COM[N-1];
- C1:重映射模式,扫描从COM[N-1]→COM0,其中N为复用率。上述复用率设置为20H,代码中指定为C0,则从COM0→COM31进行扫描。
- DA + A[5:4] (02H/12H/22H/32H):设置COM引脚的硬件配置(以复用率为64为例):
-
A[5:4] = 00H:顺序COM引脚配置,禁止COM左右重映射,当扫描方向为COM0→COM63,COM的配置如下图;当扫描方向为COM63→COM0,为第二幅图。
-
A[5:4] = 10H:顺序COM引脚配置,允许COM左右重映射,当扫描方向为COM0→COM63,COM配置如下图,当扫描方向为COM63→COM0,为第二幅图。
-
A[5:4] = 01H:备用COM引脚配置(奇偶间隔),禁止COM左右重映射,当扫描方向为COM0→COM63,COM配置如下图,当扫描方向为COM63→COM0,为第二幅图。
-
A[5:4] = 11H:备用COM引脚配置(奇偶间隔),允许COM左右重映射,当扫描方向为COM0→COM63,COM配置如下图,当扫描方向为COM63→COM0,为第二幅图。
代码中,命令RAM中8'hda
后接8'h02
,则指定A[5:4] = 00H,顺序配置COM引脚,禁止引脚左右重映射,扫描方向为COM31→COM0。
🍥 代码中使用的工作方式
OLED屏幕上显示出字符的方式不唯一,这与OLED屏幕加载的命令、输入字符编码的顺序等均有关系。
⚠️ 注意:经作者实验,OLED屏幕正面看右上角可能为每一页的COL0、ROW0处;从上至下依次为PAGE3/2/1/0。
单个字符编码
首先考虑代码中,oled_char_RAM
中储存的字符编码是怎么一回事。例如:
Mem[ 50] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46}; // 50 2
在无段映射(SEG与COL一致,SEG0)、扫描方向为正向(COM0→COM7),写入GDDRAM时先写入字符编码高位,WRITE
状态下先写入一个字节编码的高位时表示字符“2”的字符编码为如下图所示:
为正常显示,需要改变扫描方向和SEG重映射:
字符串写入顺序
字符串由多个字符组成,因此相较于单个字符存在字符扫描顺序问题。该问题取决于页选址模式小节中GDDRAM访问指针的位置。
在无段映射(SEG与COL一致,A0H)、扫描方向为正向(COM0→COM7,C0H),写入GDDRAM时先写入字符编码高位,正序读入字符串时字符串“27”显示的效果为:
指针从右上方红色箭头处开始朝下扫描。
而原文所提供的代码其字符串扫描顺序如下图所示,具体方式说明见后:
指针从左下方红色箭头处开始朝上扫描。
在oled_cmd_RAM
中设置了段重映射,因此SEG0对应COL127→SEG127对应COL0;设置反向扫描,因此每一页(page)从下往上填充一个八位编码。
在读入显示数据(D/C = 1)时,先需要在D/C = 0下输入3个字节的命令以决定RAM访问指针起始位置:ypage
(设置页起始地址)、xpage_low
与xpage_high
(共同设置COL地址)。
在SCAN
状态下,记录字符个数的计数器char_num
是倒序的,由于char_addr <= char[(char_num*8)+:8]
,字符串从最左侧(高位)开始取字符编码(示例中先扫描“2”)。程序中,由于字符编码为5×8,需要扩充至8×8才可在每个字符间保持恰当的间距,因此先输入3列8'h00
(空白),而后再读入字符编码。读入字符编码时,程序中先读入字符“2”的高位(8’h42)→低位(8’h46),并且写入COM中的一个八位编码(8’h42)顺序为:01000010。
多行字符串写入顺序
多行字符串写入需要增加PAGE的控制。具体体现在代码中,由于是页寻址模式,扫描每一行字符串需要设置不同的ypage
、xpage_low
、xpage_high
。
这篇关于基于Lattice XO2-4000HC FPGA核心板的SSD1306 OLED12832驱动芯片指令及工作方式详述(Verilog)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!