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 |
以上是对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) {
}
}