基于Lattice XO2-4000HC FPGA核心板的SSD1306 OLED12832驱动芯片指令及工作方式详述(Verilog)

📌 前言

本篇文章为《基于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;   				// 文字个数 最多16
    reg [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) begin
		if(!rst_n) begin
			cnt_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 begin
			case(state)
				IDLE: begin
					cnt_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;
				end

				MAIN: begin
					if(cnt_main >= 3'd6)
						cnt_main <= 3'd5;
					else 
						cnt_main <= cnt_main + 1'b1;
					case(cnt_main)	//MAIN状态
						3'd0: begin state <= INIT; end
						3'd1: begin ypage <= 8'hb0; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "ADC DATA DISPLAY";state <= SCAN; end
						3'd2: begin ypage <= 8'hb1; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "VOLTAGE:   . V  ";state <= SCAN; end
						3'd3: begin ypage <= 8'hb2; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "AUTHOR: ZIRU PAN";state <= SCAN; end
						3'd4: begin ypage <= 8'hb3; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "                ";state <= SCAN; end
						3'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; end
						3'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; end
						default: state <= IDLE;
					endcase
				end

				INIT: 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	//延时大于3us
						5'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	//延时大于220us
						5'd4: begin 
								if(cmd_addr >= CMD_DEPTH) begin
									cmd_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;
								end
							end
						5'd5: begin 
							cnt_init <= 1'b0; 
							state <= MAIN; 
						end
						default: state <= IDLE;
					endcase
				end

				SCAN: begin
					if(cnt_scan == 4'd12) begin
						if(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; end
						4'd4: begin char_addr <= char[(char_num*8)+:8]; end
						4'd5: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end	//5*8点阵变成8*8
						4'd6: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end	//5*8点阵变成8*8
						4'd7: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end	//5*8点阵变成8*8
						4'd8: begin oled_dcn <= DATA; wr_reg <= char_out[39:32]; state <= WRITE; state_last <= SCAN; end
						4'd9: begin oled_dcn <= DATA; wr_reg <= char_out[31:24]; state <= WRITE; state_last <= SCAN; end
						4'd10:begin oled_dcn <= DATA; wr_reg <= char_out[23:16]; state <= WRITE; state_last <= SCAN; end
						4'd11:begin oled_dcn <= DATA; wr_reg <= char_out[15:8] ; state <= WRITE; state_last <= SCAN; end
						4'd12:begin oled_dcn <= DATA; wr_reg <= char_out[7:0]  ; state <= WRITE; state_last <= SCAN; end
						4'd13:begin state <= MAIN; end
						default: state <= IDLE;
					endcase
				end

				WRITE: 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; end
						5'd3: begin oled_clk <= LOW; oled_data <= wr_reg[6]; end
						5'd4: begin oled_clk <= HIGH; end
						5'd5: begin oled_clk <= LOW; oled_data <= wr_reg[5]; end
						5'd6: begin oled_clk <= HIGH; end
						5'd7: begin oled_clk <= LOW; oled_data <= wr_reg[4]; end
						5'd8: begin oled_clk <= HIGH; end
						5'd9: begin oled_clk <= LOW; oled_data <= wr_reg[3]; end
						5'd10:begin oled_clk <= HIGH; end
						5'd11:begin oled_clk <= LOW; oled_data <= wr_reg[2]; end
						5'd12:begin oled_clk <= HIGH; end
						5'd13:begin oled_clk <= LOW; oled_data <= wr_reg[1]; end
						5'd14:begin oled_clk <= HIGH; end
						5'd15:begin oled_clk <= LOW; oled_data <= wr_reg[0]; end	//后发低位数据
						5'd16:begin oled_clk <= HIGH; end
						5'd17:begin oled_csn <= HIGH; state <= DELAY; end
						default: state <= IDLE;
					endcase
				end

				DELAY: begin
					if(cnt_delay >= num_delay) begin
						cnt_delay <= 15'd0; 
						state <= state_last; 
					end 
					else 
						cnt_delay <= cnt_delay + 1'b1;
				end

				default: state <= IDLE;
			endcase
		end
	end
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)。
FPGA GPIO与OLED连接电路原理图
在SCLK每个上升沿,SDIN上的数据按MSB~LSB顺序移位到一个8位移位寄存器中。每八个时钟对D/C进行一次采样,移位寄存器中的8位数据根据D/C的采样结果决定写入到图形显示数据RAM(GDDRAM)或命令寄存器。其写时序如下图所示:
4线SPI下的写时序
根据上述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点阵显示,如下图所示。
SSD1306 GDDRAM的页结构
注:重映射在后文说明。

当一个数据字节被写入GDDRAM时,当前列同页的所有行图像数据被填充(即列地址指针指向的整个列(8位)被填充)。数据位D0被写入顶部行。数据位D7写入底部一行。
数据填充方式

寻址模式

在SSD1306中有3种不同的内存寻址模式:页寻址模式、水平寻址模式和垂直寻址模式。设置寻址模式需要两个字节的命令:

  1. 20H,00H:水平寻址模式;
  2. 20H,01H,垂直寻址模式;
  3. 20H,02H,页寻址模式(复位);
  4. 20H,03H,无效。
页寻址模式

页面寻址模式(A[1:0]-10xb)页寻址模式。当GDDRAM被读写后,列地址指针自动增加1。如果列地址指针到达列结束地址,列地址指针将重置为列起始地址,页地址指针不变。用户必须设置新的页面和列地址,以便访问下一页RAM内容。页寻址模式的页和列地址的移动顺序如下图所示。
地址指针移动页面寻址模式
在正常GDDRAM读写和页寻址模式下,需要定义启动RAM访问指针的位置:

  1. 通过命令B0H~B7H设置目标显示位置的页面起始地址;
  2. 通过命令00H~0FH设置指针的较低列地址,高4位恒定为00H,低4位为要设置的起始列地址的低4位;
  3. 通过命令10H~1FH设置指针的较高列地址,高4位恒定为01H,低4位为要设置的起始列地址的高4位;
  4. *例如:设定B0H、00H、15H,则指针在PAGE1,COL80(50H = 80D)的COM0处。

例如,页面地址设置为B2H,较低列地址为03H,较高列地址为10H。这意味着起始列是PAGE2的SEG3。内存访问指针的位置如下图所示,输入数据字节将被写入RAM第3列的位置。
在页寻址模式下设置GDDRAM访问指针的示例
代码中,MIAN状态下设置了要显示的字符串,其中ypage指定了页,xpage_lowxpage_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。

垂直寻址方式下,页面和列地址的移动顺序如下图所示。当列地址指针和页地址指针都到达结束地址时,指针被重置为列起始地址和页起始地址(图中虚线)。
垂直寻址模式下地址指针的移动

重映射及多路复用
  1. A0/A1,设置段映射:
  • A0H:列地址0映射到SEG0(复位状态);
  • A1H:列地址127映射到SEG0,即原本:SEG0对应COL0→SEG127对应COL127,现在映射为:SEG0对应COL127→SEG127对应COL0。
  1. 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。

  2. C0/C8,设置COM输出扫描方向:

  • C0:正常模式(复位状态),从COM0→COM[N-1];
  • C1:重映射模式,扫描从COM[N-1]→COM0,其中N为复用率。上述复用率设置为20H,代码中指定为C0,则从COM0→COM31进行扫描。
  1. DA + A[5:4] (02H/12H/22H/32H):设置COM引脚的硬件配置(以复用率为64为例):
  • A[5:4] = 00H:顺序COM引脚配置,禁止COM左右重映射,当扫描方向为COM0→COM63,COM的配置如下图;当扫描方向为COM63→COM0,为第二幅图。
    A[5:4] = 00H 正向扫描
    A[5:4] = 00H 反向扫描

  • A[5:4] = 10H:顺序COM引脚配置,允许COM左右重映射,当扫描方向为COM0→COM63,COM配置如下图,当扫描方向为COM63→COM0,为第二幅图。
    A[5:4] = 10H 正向扫描
    A[5:4] = 10H 反向扫描

  • A[5:4] = 01H:备用COM引脚配置(奇偶间隔),禁止COM左右重映射,当扫描方向为COM0→COM63,COM配置如下图,当扫描方向为COM63→COM0,为第二幅图。
    A[5:4] = 01H 正向扫描
    A[5:4] = 01H 反向扫描

  • A[5:4] = 11H:备用COM引脚配置(奇偶间隔),允许COM左右重映射,当扫描方向为COM0→COM63,COM配置如下图,当扫描方向为COM63→COM0,为第二幅图。
    A[5:4] = 11H 正向扫描
    A[5:4] = 11H 反向扫描
    代码中,命令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”的字符编码为如下图所示:
字符2的编码显示-1
为正常显示,需要改变扫描方向和SEG重映射:
字符2的编码显示-2

字符串写入顺序

字符串由多个字符组成,因此相较于单个字符存在字符扫描顺序问题。该问题取决于页选址模式小节中GDDRAM访问指针的位置。

无段映射(SEG与COL一致,A0H)、扫描方向为正向(COM0→COM7,C0H),写入GDDRAM时先写入字符编码高位正序读入字符串时字符串“27”显示的效果为:
字符串27编码显示-1
指针从右上方红色箭头处开始朝下扫描。

而原文所提供的代码其字符串扫描顺序如下图所示,具体方式说明见后:
字符串27编码显示-2
指针从左下方红色箭头处开始朝上扫描。

oled_cmd_RAM中设置了段重映射,因此SEG0对应COL127→SEG127对应COL0;设置反向扫描,因此每一页(page)从下往上填充一个八位编码。

在读入显示数据(D/C = 1)时,先需要在D/C = 0下输入3个字节的命令以决定RAM访问指针起始位置:ypage(设置页起始地址)、xpage_lowxpage_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的控制。具体体现在代码中,由于是页寻址模式,扫描每一行字符串需要设置不同的ypagexpage_lowxpage_high

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值