STM32F103ZET6驱动OLED显示屏详解

STM32F103ZET6 驱动 OLED

目录

  • 前言
  • OLED模块的基本了解
  • OLED驱动程序的开发
  • 前言

    ​ 大家好,这是我第一次发帖,由于,我的技术并不成熟,程序难免有编写不规范的地方,希望读者能够指正,也希望这篇帖子能够让读者对OLED模块有个大致的了解。很高兴能与大家交流。

    OLED模块的基本了解

    OLED模块的引脚:

    ​ 图片转载自淘宝商家

    ​ 我使用的OLED模块有以下几个引脚:

    引脚名 功能 驱动电压 相连接MCU的端口
    GND 接地 GND
    VCC 电源电压 3.3v ~ 5v 3.3v
    DO 时钟线 2.2v ~ 5v SCLK(PA5)
    D1 数据线 2.2v ~ 5v MOSI(PA7)
    RES 复位线 2.2v ~ 5v PC5
    DC 数据/命令控制线 2.2v ~ 5v PC4
    CS 片选线 2.2v ~ 5v PA4
  • RES 为复位线,低电平复位
  • 当GPIO向DC端口输出高电平时,MCU会通过GPIO向D1端口发送数据,使其显示在OLED屏上。当GPIO向DC端口输出低电平时,MCU会通过GPIO向D1端口发送命令,通过这些命令,可以配置OLED模块。
  • 以上是对OLED模块引脚的简要了解,在编写驱动程序之前,我们还需要了解OLED模块的寻址方式。

    OLED模块的寻址方式:

    OLED有三种寻址模式:

  • 水平寻址

  • 垂直寻址

  • 页寻址

    最常用的寻址模式是页寻址模式,本驱动程序所使用的寻址方法也是页寻址。我会重点介绍页寻址和水平寻址,简要地介绍垂直寻址。

  • 在介绍寻址模式之前,我们需要了解一些预备知识:

    0.96寸的OLED显示屏有128列,64行,即128×64,64行被分为8组,每个128×8的区域被称为页,如下图:

    可以看到COM0~COM7,也就是0 ~ 7行从属于Page0,也就是第0页,依此类推。

    通过这种划分,可以把整个屏幕划为8份。

    通过这样的划分,当我们往某页某一列中写入数据0x5A时,就能控制哪一列的哪个格子是亮或者灭的,如下图:

    0x5A

    1
    0
    1
    0
    0
    1
    0
    1

    (注:从顶行到底行,由地位到高位)

    ​ 在OLED模块中,我们会通过指令来设置OLED模块,假设,我们通过WriteCmd(uint8_t command)函数向OLED模块发送指令。你会发现,有一条就会生效的指令,有多条配合使用才会生效的指令。

    void OLED_Init(void) {
    	
    	// 使能相关的GPIO口 配置SPI外设
    	OLED_SPI_Init();	
    	
    	OLED_SPI_RES_HIGH;
    	// 延时200ms
    	Delay_us(200);
    	/**************************************** 以下是官方的代码******************************/
    	WriteCmd(0xAE); //display off
    	WriteCmd(0x20);	//Set Memory Addressing Mode	
    	WriteCmd(0x10);	//00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
    	WriteCmd(0xb0);	//Set Page Start Address for Page Addressing Mode,0-7
    	WriteCmd(0xc8);	//Set COM Output Scan Direction
    	WriteCmd(0x00); //---set low column address
    	WriteCmd(0x10); //---set high column address
    	WriteCmd(0x40); //--set start line address
    	WriteCmd(0x81); //--set contrast control register
    	WriteCmd(0xff); //亮度调节 0x00~0xff
    	WriteCmd(0xa1); //--set segment re-map 0 to 127
    	WriteCmd(0xa6); //--set normal display
    	WriteCmd(0xa8); //--set multiplex ratio(1 to 64)
    	WriteCmd(0x3F); //
    	WriteCmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
    	WriteCmd(0xd3); //-set display offset
    	WriteCmd(0x00); //-not offset
    	WriteCmd(0xd5); //--set display clock divide ratio/oscillator frequency
    	WriteCmd(0xf0); //--set divide ratio
    	WriteCmd(0xd9); //--set pre-charge period
    	WriteCmd(0x22); //
    	WriteCmd(0xda); //--set com pins hardware configuration
    	WriteCmd(0x12);
    	WriteCmd(0xdb); //--set vcomh
    	WriteCmd(0x20); //0x20,0.77xVcc
    	WriteCmd(0x8d); //--set DC-DC enable
    	WriteCmd(0x14); //
    	WriteCmd(0xaf); //--turn on oled panel
    }
    

    以上面官方提供的初始化代码片段为例子:

    WriteCmd(0xAE); //display off 关闭显示
    
    WriteCmd(0x81); //--set contrast control register
    WriteCmd(0xff); //亮度调节 0x00~0xff
    // 以上两条命令是设置对比度控制
    // 通过0x81命令选择相应的寄存器,然后,设置范围0x00 ~ 0xff的亮度大小,控制OLED屏的亮度
    

    如果想熟悉命令,请参考相应的官方手册。

    下面简要介绍几条命令:

    WriteCmd(0x00);  // 设置为水平寻址
    WriteCmd(0x01);  // 设置为垂直寻址
    WriteCmd(0x10);  // 设置为页寻址
    
    WriteCmd(0x8D); // 设置电荷泵
    WriteCmd(0x14); // 开启电荷泵
    WriteCmd(0xAF); // OLED唤醒
    WriteCmd(0xAE); // 关闭OLED
    

    设置页地址:

    0xby

    7 6 5 4 3 2 1 0
    1 0 1 1 Y3 Y2 Y1 Y0

    这就是设置页地址命令的格式,[7:4]位是固定的,而[3:0]位可以通过写入0000b ~ 0111b,这个值域的二进制数来指定程序在屏幕显示时的起始地址。

    设置列地址:

    关于设置列地址,有两条指令0x1[x7:x4]0x0[x3:x0]

    高四位的值为1的指令表示的是设置列地址的高四位,高四位的值为0的指令表示设置列地址的第四位。

    然后,指令**0x1[x7:x4]的值左移四位,然后与0x0[x3:x0]**的值相加,组成一个8位二进制数,来决定选择的是0 ~127行中的第几行为初始地址

    7 6 5 4 3 2 1 0
    0 0 0 1 x7 x6 x5 x4
    7 6 5 4 3 2 1 0
    0 0 0 0 x3 x2 x1 x0

    所以下面的一个函数,大家应该看得懂:

    // 设置起始坐标
    void SetPos(uint8_t x, uint8_t y) {
    	
    	// 设置页地址
    	WriteCmd(0xb0 + y);
    	// 取列高位
    	WriteCmd((x & 0xf0)>>4 | 0x10);
    	// 取列低位
    	WriteCmd((x & 0x0f) | 0x01);
    }
    
    // 以上代码的意思是,将每个页地址的起始地址设置为0列
    

    对以上的知识有个基本了解之后,下面将介绍几种寻址模式:

  • 首先是页寻址

  • ​ 如上图所示,页寻址的办法是,横向对每行进行读和写,当到达每行的末尾的时候,会立即返回每行的开头。

    如果到达该行的末尾时,我们不想返回该行的开头,那么我们就需要坐标的起始值。

  • 水平寻址

    ​ 如上图所示,当完成对一行的遍历后,页地址值会自增1,然后,会跳到下一页进行遍历,当对0 ~7 页都遍历之后,会返回0页0列。
  • OLED驱动程序的开发

    本次实验使用了STM32F103ZET6最小系统板,面包板,以及一个电源模块和JLINK OB仿真器,如下图:

    SPI模块的程序如下:

    #ifndef _SPI_H
    #define _SPI_H
    
    #include "stm32f10x.h"
    
    // DO SCLK 
    #define OLED_SPI_DO_Pin			GPIO_Pin_5
    #define OLED_SPI_DO_Port		GPIOA
    #define OLED_SPI_DO_CLK			RCC_APB2Periph_GPIOA
    #define OLED_SPI_DO_CLK_FUN		RCC_APB2PeriphClockCmd
    
    // D1 MOSI 
    #define OLED_SPI_MOSI_Pin		GPIO_Pin_7
    #define OLED_SPI_MOSI_Port		GPIOA
    #define OLED_SPI_MOSI_CLK		RCC_APB2Periph_GPIOA
    #define OLED_SPI_MOSI_CLK_FUN	RCC_APB2PeriphClockCmd
    
    // CS NSS
    #define OLED_SPI_CS_Pin			GPIO_Pin_4
    #define OLED_SPI_CS_Port		GPIOA
    #define OLED_SPI_CS_CLK			RCC_APB2Periph_GPIOA
    #define OLED_SPI_CS_CLK_FUN		RCC_APB2PeriphClockCmd
    
    // DC Data or Cammand 数据或者命令选择
    #define OLED_SPI_DC_Pin			GPIO_Pin_4
    #define OLED_SPI_DC_Port		GPIOC
    #define OLED_SPI_DC_CLK			RCC_APB2Periph_GPIOC
    #define OLED_SPI_DC_CLK_FUN		RCC_APB2PeriphClockCmd
    
    // RES RESET
    #define OLED_SPI_RES_Pin		GPIO_Pin_5
    #define OLED_SPI_RES_Port		GPIOC
    #define OLED_SPI_RES_CLK		RCC_APB2Periph_GPIOC
    #define OLED_SPI_RES_CLK_FUN	RCC_APB2PeriphClockCmd
    
    #define OLED_SPI				SPI1
    #define OLED_SPI_CLK			RCC_APB2Periph_SPI1
    #define OLED_SPI_CLK_FUN		RCC_APB2PeriphClockCmd
    #define OLED_SPI_GPIO_CLK_FUN	RCC_APB2PeriphClockCmd
    
    // 复位引脚的高低电平的输出
    #define OLED_SPI_RES_HIGH		GPIO_SetBits(OLED_SPI_RES_Port, OLED_SPI_RES_Pin)		
    #define OLED_SPI_RES_LOW		GPIO_ResetBits(OLED_SPI_RES_Port, OLED_SPI_RES_Pin)
    
    // DC命令选择的高低电平的输出
    #define OLED_SPI_DC_HIGH		GPIO_SetBits(OLED_SPI_DC_Port, OLED_SPI_DC_Pin)
    #define OLED_SPI_DC_LOW			GPIO_ResetBits(OLED_SPI_DC_Port, OLED_SPI_DC_Pin)
    
    void OLED_SPI_Init(void);
    void OLED_SPI_Write(uint8_t data);
    
    #endif
    
    
    
    #include "spi.h"
    
    void OLED_SPI_Init(void) {
    
    	SPI_InitTypeDef SPI_InitStructure;
    	GPIO_InitTypeDef GPIO_InitStructure;
    	
    	// 打开时钟
    	OLED_SPI_CLK_FUN(OLED_SPI_CLK, ENABLE);
    	OLED_SPI_GPIO_CLK_FUN(OLED_SPI_DO_CLK | OLED_SPI_MOSI_CLK | OLED_SPI_CS_CLK | OLED_SPI_DC_CLK
    		| OLED_SPI_RES_CLK, ENABLE);
    	
    	// DO SCLK
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    	GPIO_InitStructure.GPIO_Pin = OLED_SPI_DO_Pin;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	
    	GPIO_Init(OLED_SPI_DO_Port, &GPIO_InitStructure);
    	
    	// D1 MOSI
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    	GPIO_InitStructure.GPIO_Pin = OLED_SPI_MOSI_Pin;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	
    	GPIO_Init(OLED_SPI_MOSI_Port, &GPIO_InitStructure);
    	
    	// CS NSS
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    	GPIO_InitStructure.GPIO_Pin = OLED_SPI_CS_Pin;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	
    	GPIO_Init(OLED_SPI_CS_Port, &GPIO_InitStructure);
    	
    	// DC 命令数据选择
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    	GPIO_InitStructure.GPIO_Pin = OLED_SPI_DC_Pin;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	
    	GPIO_Init(OLED_SPI_DC_Port, &GPIO_InitStructure);
    	
    	// RES RESET 低电平复位
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    	GPIO_InitStructure.GPIO_Pin = OLED_SPI_RES_Pin;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	
    	GPIO_Init(OLED_SPI_RES_Port, &GPIO_InitStructure);
    	
    	// 初始化 SPI结构体
    	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制
    	SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式
    	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 大端数据先行
    	SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; // 单线发送
    	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 数据长度为8
    	SPI_InitStructure.SPI_CRCPolynomial = 7;
    	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
    	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 空闲时为高电平,偶数边沿
    	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
    	
    	// 初始化SPI
    	SPI_Init(OLED_SPI, &SPI_InitStructure);
    	
    	// 使能SPI
    	SPI_Cmd(OLED_SPI, ENABLE);
    }
    
    void OLED_SPI_Write(uint8_t data) {
    	
    	// 判断 SPI的 发送缓冲区是否为空?
    	while(SPI_I2S_GetFlagStatus(OLED_SPI, SPI_I2S_FLAG_TXE) == RESET);
    	
    	SPI_I2S_SendData(OLED_SPI, data);
    }
    
    
    

    SPI模块的配置如上面的代码所示,关于SPI协议,在本贴中,不会提及,请感兴趣的读者去阅读STM32的参考手册。要提及的一点是,SPI_InitStructure.SPI_NSS需要设置成软件控制,如果有硬件控制,可能OLED无法显示数据,我个人认为,可能是如果通过硬件控制,NSS端口可能以极快的频率在高低电平之间来回切换,导致采集数据时,OLED无法被作为从设备选中,这是一家之言,如果有不正确之处,请大家指正。

    OLED模块如下:

    #ifndef __OLED_H
    #define	__OLED_H
    
    #include "stm32f10x.h"
    
    #define OLED_ADDRESS	0x78 //通过调整0R电阻,屏可以0x78和0x7A两个地址 -- 默认0x78
    #define OLED_CMD		0
    #define OLED_DATA		1
    
    void WriteCmd(uint8_t SPI_Command);
    void WriteData(uint8_t SPI_Data);
    void OLED_Init(void);
    void OLED_ON(void);
    void OLED_OFF(void);
    void SetPos(uint8_t x, uint8_t y);
    void OLED_Fill(uint8_t Fill_Data);
    void OLED_Clean(void);
    void OLED_ShowStr(uint8_t x, uint8_t y, char ch[], uint8_t TextSize);
    
    #endif
    
    #include "spi.h"
    #include "oled.h"
    #include "bsp_systick.h"
    #include "codetab.h"
    
    // 设置oled的显存
    // 存放格式如下
    // [0] 0 1 2 3 ... 127
    // [1] 0 1 2 3 ... 127
    // [2] 0 1 2 3 ... 127
    // [3] 0 1 2 3 ... 127
    // [4] 0 1 2 3 ... 127
    // [5] 0 1 2 3 ... 127
    // [6] 0 1 2 3 ... 127
    // [7] 0 1 2 3 ... 127
    uint8_t OLED_GRAM[128][8];
    
    // 初始化 OLED模块0
    void OLED_Init(void) {
    	
    	// 使能相关的GPIO口 配置SPI外设
    	OLED_SPI_Init();	
    	
    	OLED_SPI_RES_HIGH;
    	// 延时200ms
    	Delay_us(200);
    	
    	WriteCmd(0xAE); //display off
    	WriteCmd(0x20);	//Set Memory Addressing Mode	
    	WriteCmd(0x10);	//00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
    	WriteCmd(0xb0);	//Set Page Start Address for Page Addressing Mode,0-7
    	WriteCmd(0xc8);	//Set COM Output Scan Direction
    	WriteCmd(0x00); //---set low column address
    	WriteCmd(0x10); //---set high column address
    	WriteCmd(0x40); //--set start line address
    	WriteCmd(0x81); //--set contrast control register
    	WriteCmd(0xff); //亮度调节 0x00~0xff
    	WriteCmd(0xa1); //--set segment re-map 0 to 127
    	WriteCmd(0xa6); //--set normal display
    	WriteCmd(0xa8); //--set multiplex ratio(1 to 64)
    	WriteCmd(0x3F); //
    	WriteCmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
    	WriteCmd(0xd3); //-set display offset
    	WriteCmd(0x00); //-not offset
    	WriteCmd(0xd5); //--set display clock divide ratio/oscillator frequency
    	WriteCmd(0xf0); //--set divide ratio
    	WriteCmd(0xd9); //--set pre-charge period
    	WriteCmd(0x22); //
    	WriteCmd(0xda); //--set com pins hardware configuration
    	WriteCmd(0x12);
    	WriteCmd(0xdb); //--set vcomh
    	WriteCmd(0x20); //0x20,0.77xVcc
    	WriteCmd(0x8d); //--set DC-DC enable
    	WriteCmd(0x14); //
    	WriteCmd(0xaf); //--turn on oled panel
    }
    
    void WriteCmd(uint8_t SPI_Command) {
    	
    	OLED_SPI_DC_LOW;
    	
    	OLED_SPI_Write(SPI_Command);
    }
    
    void WriteData(uint8_t SPI_Data) {
    	
    	OLED_SPI_DC_HIGH;
    	
    	OLED_SPI_Write(SPI_Data);
    }
    
    // 打开OLED
    void OLED_ON(void) {
    	
    	WriteCmd(0x8D); // 设置电荷泵
    	WriteCmd(0x14); // 开启电荷泵
    	WriteCmd(0xAF); // OLED唤醒
    }
    
    // 关闭OLED
    void OLED_OFF(void) {
    
    	WriteCmd(0x8D); // 设置电荷泵
    	WriteCmd(0x10); // 关闭电荷泵
    	WriteCmd(0xAE); // 关闭OLED
    }
    
    // 设置起始坐标
    void SetPos(uint8_t x, uint8_t y) {
    	
    	// 设置页地址
    	WriteCmd(0xb0 + y);
    	// 取列高位
    	WriteCmd((x & 0xf0)>>4 | 0x10);
    	// 取列低位
    	WriteCmd((x & 0x0f) | 0x01);
    }
    
    // 全屏填充
    void OLED_Fill(uint8_t Fill_Data) {
    	
    	uint8_t m, n;
    	// 设置起始地址
    	for (m = 0; m < 8; m++) {
    		WriteCmd(0xb0 + m);
    		WriteCmd(0x00);
    		WriteCmd(0x10);
    	}
    	
    	// 填充数据
    	for (n = 0; n < 128; n++) {
    		WriteData(Fill_Data);
    	}
    }
    
    // 清屏
    void OLED_Clean(void) {
    	
    	OLED_Fill(0x00);
    }
    
    // 显示字符串
    void OLED_ShowStr(uint8_t x, uint8_t y, char ch[], uint8_t TextSize) {
    	
    	uint8_t c = 0, i =0, j = 0;	
    	switch(TextSize) {
    		
    		// 模式1:6x8点阵
    		// 6列 1组
    		case 1: {
    			while(ch[j] != '\0') {
    				
    				c = ch[j] - 32;
    				if (x > 126) {
    					x = 0;
    					y++;
    				}
    				SetPos(x, y);
    				for (i = 0; i < 6; i++) {
    					WriteData(F6x8[c][i]);
    				}
    				x += 6;
    				j++;
    			}
    		}break;
    		
    		// 模式2: 8x16点阵 两页
    		case 2: {
    			
    			while(ch[j] != '\0') {
    				
    				c = ch[j] - 32; // 字符偏移量,字库上的字体序号和ASCII码表上相差32
    				if (x > 120) {
    					x = 0;
    					y++;
    				}
    				SetPos(x, y);
    				for (i = 0; i < 8; i++) {
    					WriteData(F8X16[c*16+i]);
    				}
    				SetPos(x,y+1);
    				for(i=0;i<8;i++) {
    					WriteData(F8X16[c*16+i+8]);
    				}
    				x += 8;
    				j++;
    			}
    		}break;
    	}
    }
    

    对于OLED模块中的相关函数,重点介绍一下OLED_ShowStr(uint8_t x, uint8_t y, char ch[], uint8_t TextSize),这是官方提供的函数。

    对于OLED_ShowStr函数,它的参数x, y代表在屏幕中进行写操作时的起始坐标值,如果我们选择的是6×8模式,也就是6列,8行来表示一个字符,那么0 ~ 127中,最大能整除6的数字是126,即x的最大值是125(包括0),当x大于126时,便会开启下一页的写操作,如下:

    if (x > 126) {
    	x = 0;
    	y++;
    }
    

    那么问题来了,如何理解下面的表达式:

    c = ch[j] - 32;
    

    比如要想显示A字母,A字母的ASCII码值是65,那么65 – 32 = 33,33就对应着字库(注:字库文件在codetab.h文件中定义,因为篇幅原因,我就不贴出来了,大家可以通过淘宝厂家提供的百度网盘链接找到)中A字母的编号33,那么我们可以通过找到A的编号,从而找到它的编码,从而在屏幕中打印出来。

    以此类推,F8X16是一个数组,每个元素是一个8位的char类型,字库中将其分行,每行有16个字符,A在33行,所以,33*16能取到A的第一个编码值,然后按顺序,从1 ~ 16,一列列地将A字母从屏幕上打印出来。

    #include "stm32f10x.h"
    #include "led.h"
    #include "bsp_systick.h"
    #include "spi.h"
    #include "oled.h"
    
    
    int main() {
    	
    	SysTick_init();
    	
    	LED_Init();
    	
    	OLED_Init();
    	
    	OLED_Clean();
    	
    	OLED_ShowStr(0, 1, "hello world", 1);
    	OLED_ShowStr(0, 2, "hello world", 2);
    	
    	while(1) {
    	
    	}
    }
    
    c = ch[j] - 32;
    

    比如要想显示A字母,A字母的ASCII码值是65,那么65 – 32 = 33,33就对应着字库(注:字库文件在codetab.h文件中定义,因为篇幅原因,我就不贴出来了,大家可以通过淘宝厂家提供的百度网盘链接找到)中A字母的编号33,那么我们可以通过找到A的编号,从而找到它的编码,从而在屏幕中打印出来。

    以此类推,F8X16是一个数组,每个元素是一个8位的char类型,字库中将其分行,每行有16个字符,A在33行,所以,33*16能取到A的第一个编码值,然后按顺序,从1 ~ 16,一列列地将A字母在屏幕上打印出来。

    #include "stm32f10x.h"
    #include "led.h"
    #include "bsp_systick.h"
    #include "spi.h"
    #include "oled.h"
    
    
    int main() {
    	
    	SysTick_init();
    	
    	LED_Init();
    	
    	OLED_Init();
    	
    	OLED_Clean();
    	
    	OLED_ShowStr(0, 1, "hello world", 1);
    	OLED_ShowStr(0, 2, "hello world", 2);
    	
    	while(1) {
    	
    	}
    }
    
    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32F103ZET6驱动OLED显示屏详解

    发表评论