??OLED即有机发光二极管,本次介绍的是正点原子的OLED显示模块。
(1)模块有单色和双色两种可选,单色为纯蓝色,双色为黄蓝双色(分区域的双色,前16行为黄色,后48行为蓝色,黄蓝之间有一行不显示的间隔区)
(2)尺寸小,显示尺寸为0.96寸,模块大小为27mm26mm大小
(3)高分辨率,模块分辨率为12864
(4)多种接口模式,该模块提供了4种接口,包括:6800、8080两种并行接口方式4线SPI接口方式和IIC接口方式。
(5)不需要高压,直接3.3V就可以工作了。
接口方式 | 4线SPI | IIC | 8位6800 | 8位8080 |
---|---|---|---|---|
BS1 | 0 | 1 | 0 | 1 |
BS2 | 0 | 0 | 1 | 1 |
模块外观如下图所示:
??正点原子使用的是8080并口模式,原理图如下:
??正点原子的OLED控制器是SSD1306.下面将学习如何使用该芯片来控制模块来显示字符和数字。
??8080接口需要的信号线如下所示
信号名称 | 作用 |
---|---|
CS | OLED片选信号 |
WR | 向OLED写入数据 |
RD | 从OLED读取数据 |
D[7:0] | 8位双向数据线 |
RST(RES) | 硬复位OLED |
DC | 命令/数据标志(0,读写命令; 1,读写数据) |
??8080接口下,控制脚的信号状态所对应的功能表:
功能 | RD | WR | CS | DC |
---|---|---|---|---|
写命令 | H | P | L | L |
读状态 | P | H | L | L |
写数据 | H | P | L | H |
读数据 | P | H | 1 | H |
??8080并口写入的过程为:
8080并口写时序
??8080并口读入的过程为:
??在8080方式下读数据操作的时候,我们会有一个假读命令(Dummy Read),使得微控制器的操作频率与显存的操作频率匹配。其实就是读到的第一个数据不要,从第二个数据开始才是我们真正要读的数据。
??SSD1306的显存大小共有128* 64bit大小,对应了OLED的128* 64的分辨率,SSD1306将这些显存分为了8页。可以看出共有128行,列为8列,每个字节8bit,所以是64bit。
??为了能实时知道目前的显示屏显示状态,可以在写入之前读取像素数据,但是这样的效率很慢,于是我们选择在STM32内创建一个虚拟的OLED的GRAM(128*8=1024字节),修改时只需要修改STM32上的GRAM,然后将GRAM内的数据全部写入SSD1306中即可。这样做的坏处是对于SRAM很小的单片机不友好,另外由于每次都会写入全部的数据,所以刷新率会很低。
??对于SSD1306的命令,只介绍几个常用的命令:
??0X81,用于设置对比度的,这个命令包含两个字节,第一个0X81为命令,随后发送的字节为要设置的对比度的值,这个值越大屏幕就越亮。
??0XAE/0XAF。0XAE为关闭显示命令,0XAF为开启显示命令。
??0X8D,该命令也包含;两个字节,第一个为,命令字,第二个为设置值,第二个字节的BIT2为电荷泵的开关状态,该位为1,则开启电荷泵,为0则关闭,在模块初始化的时候,这个必须开启,否则是看不到屏幕显示的。
??0XB0~B7,该命令用于设置页地址,其低三位用于设置对应的GRAM的页地址。
??0X00~0x0F,该指令用于设置显示时的起始列地址低四位
??0X10~0X1F,该指令用于设置显示时的起始列地址高八位
??OLED模块的初始化过程如下图所示:
??在STM32中驱动OLED的步骤如下:
????(1)设置STM32F103与OLED模块连接的IO
????(2)初始化OLED
????(3)通过函数将字符和数字显示到OLED模块上
??本次接线采用的8080并口。
开发板IO | OLED模块IO | STM32芯片对应的IO |
---|---|---|
VCC3.3 | VCC3.3 | |
OV_WRSTOLED_CS | PD6 | |
OV_RRST | OLED_RW | PG14 |
OV_OE | OLED_RST | PG15 |
OV_D1 | OLED_D1 | PC1 |
OV_D3 | OLED_D3 | PC3 |
OV_D5 | OLED_D5 | PC5 |
OV_D7 | OLED_D7 | PC7 |
GND | GND | |
OV_SCL | OLED_DC | PD3 |
OV_SDA | OLED_RD | PG13 |
OV_D2 | OLED_D2 | PC2 |
OV_D4 | OLED_D4 | PC4 |
OV_D6 | OLED_D6 | PC6 |
OV_RCLK | 悬空 | 悬空 |
??以下源码均来自正点原子开源教程。
GPIO_InitTypeDef gpio_init_struct;
__HAL_RCC_GPIOC_CLK_ENABLE(); /* 使能PORTC时钟 */
__HAL_RCC_GPIOD_CLK_ENABLE(); /* 使能PORTD时钟 */
__HAL_RCC_GPIOG_CLK_ENABLE(); /* 使能PORTG时钟 */
#if OLED_MODE==1 /* 使用8080并口模式 */
/* PC0 ~ 7 设置 */
gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速 */
HAL_GPIO_Init(GPIOC, &gpio_init_struct); /* PC0 ~ 7 设置 */
gpio_init_struct.Pin = GPIO_PIN_3|GPIO_PIN_6; /* PD3, PD6 设置 */
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速 */
HAL_GPIO_Init(GPIOD, &gpio_init_struct); /* PD3, PD6 设置 */
gpio_init_struct.Pin = GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速 */
HAL_GPIO_Init(GPIOG, &gpio_init_struct); /* WR/RD/RST引脚模式设置 */
/*设置初始值*/
OLED_WR(1);
OLED_RD(1);
????数据线为PC0~7,
/**
* @brief 通过拼凑的方法向OLED输出一个8位数据
* @param data: 要输出的数据
* @retval 无
*/
static void oled_data_out(uint8_t data)
{
/*数据线位PC0~7,(GPIOC->ODR & 0XFF00)将高位不变低位置零*/
/*(data & 0X00FF)低位不变高位置零*/
/*两者相或即可实现PC0~15的高位不变低位为data的低八位*/
GPIOC->ODR = (GPIOC->ODR & 0XFF00) | (data & 0X00FF);
}
/**
* @brief 向OLED写入一个字节
* @param data: 要输出的数据
* @param cmd: 数据/命令标志 0,表示命令;1,表示数据;
* @retval 无
*/
static void oled_wr_byte(uint8_t data, uint8_t cmd)
{
oled_data_out(data);
OLED_RS(cmd);
OLED_CS(0);
OLED_WR(0);
OLED_WR(1);
OLED_CS(1);
OLED_RS(1);
}
/**
* @brief 初始化OLED(SSD1306)
* @param 无
* @retval 无
*/
void oled_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
__HAL_RCC_GPIOC_CLK_ENABLE(); /* 使能PORTC时钟 */
__HAL_RCC_GPIOD_CLK_ENABLE(); /* 使能PORTD时钟 */
__HAL_RCC_GPIOG_CLK_ENABLE(); /* 使能PORTG时钟 */
/* PC0 ~ 7 设置 */
gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速 */
HAL_GPIO_Init(GPIOC, &gpio_init_struct); /* PC0 ~ 7 设置 */
gpio_init_struct.Pin = GPIO_PIN_3|GPIO_PIN_6; /* PD3, PD6 设置 */
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速 */
HAL_GPIO_Init(GPIOD, &gpio_init_struct); /* PD3, PD6 设置 */
gpio_init_struct.Pin = GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速 */
HAL_GPIO_Init(GPIOG, &gpio_init_struct); /* WR/RD/RST引脚模式设置 */
OLED_WR(1);
OLED_RD(1);
OLED_CS(1);
OLED_RS(1);
OLED_RST(0);
delay_ms(100);
OLED_RST(1);
oled_wr_byte(0xAE, OLED_CMD); /* 关闭显示 */
oled_wr_byte(0xD5, OLED_CMD); /* 设置时钟分频因子,震荡频率 */
oled_wr_byte(80, OLED_CMD); /* [3:0],分频因子;[7:4],震荡频率 */
oled_wr_byte(0xA8, OLED_CMD); /* 设置驱动路数 */
oled_wr_byte(0X3F, OLED_CMD); /* 默认0X3F(1/64) */
oled_wr_byte(0xD3, OLED_CMD); /* 设置显示偏移 */
oled_wr_byte(0X00, OLED_CMD); /* 默认为0 */
oled_wr_byte(0x40, OLED_CMD); /* 设置显示开始行 [5:0],行数. */
oled_wr_byte(0x8D, OLED_CMD); /* 电荷泵设置 */
oled_wr_byte(0x14, OLED_CMD); /* bit2,开启/关闭 */
oled_wr_byte(0x20, OLED_CMD); /* 设置内存地址模式 */
oled_wr_byte(0x02, OLED_CMD); /* [1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10; */
oled_wr_byte(0xA1, OLED_CMD); /* 段重定义设置,bit0:0,0->0;1,0->127; */
oled_wr_byte(0xC8, OLED_CMD); /* 设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数 */
oled_wr_byte(0xDA, OLED_CMD); /* 设置COM硬件引脚配置 */
oled_wr_byte(0x12, OLED_CMD); /* [5:4]配置 */
oled_wr_byte(0x81, OLED_CMD); /* 对比度设置 */
oled_wr_byte(0xEF, OLED_CMD); /* 1~255;默认0X7F (亮度设置,越大越亮) */
oled_wr_byte(0xD9, OLED_CMD); /* 设置预充电周期 */
oled_wr_byte(0xf1, OLED_CMD); /* [3:0],PHASE 1;[7:4],PHASE 2; */
oled_wr_byte(0xDB, OLED_CMD); /* 设置VCOMH 电压倍率 */
oled_wr_byte(0x30, OLED_CMD); /* [6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc; */
oled_wr_byte(0xA4, OLED_CMD); /* 全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) */
oled_wr_byte(0xA6, OLED_CMD); /* 设置显示方式;bit0:1,反相显示;0,正常显示 */
oled_wr_byte(0xAF, OLED_CMD); /* 开启显示 */
oled_clear();
}
????由于我们有刷新函数,所以打点函数只是将g_oled_gram这个二维数组里面的值改变就行,后面的刷新函数会将该数组的值全部写入SSD1306中。
static uint8_t g_oled_gram[128][8];
/**
* @brief OLED画点
* @param x : 0~127
* @param y : 0~63
* @param dot: 1 填充 0,清空
* @retval 无
*/
void oled_draw_point(uint8_t x, uint8_t y, uint8_t dot)
{
uint8_t pos, bx, temp = 0;
if (x > 127 || y > 63) return; /* 超出范围了. */
pos = y / 8; /* 计算GRAM里面的y坐标所在的字节, 每个字节可以存储8个行坐标 */
bx = y % 8; /* 取余数,方便计算y在对应字节里面的位置,及行(y)位置 */
temp = 1 << bx; /* 高位表示高行号, 得到y对应的bit位置,将该bit先置1 */
if (dot) /* 画实心点 */
{
g_oled_gram[x][pos] |= temp;
}
else /* 画空点,即不显示 */
{
g_oled_gram[x][pos] &= ~temp;
}
}
/**
* @brief OLED填充区域填充
* @note: 注意:需要确保: x1<=x2; y1<=y2 0<=x1<=127 0<=y1<=63
* @param x1,y1: 起点坐标
* @param x2,y2: 终点坐标
* @param dot: 1 填充 0,清空
* @retval 无
*/
void oled_fill(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t dot)
{
uint8_t x, y;
for (x = x1; x <= x2; x++)
{
for (y = y1; y <= y2; y++)oled_draw_point(x, y, dot);
}
oled_refresh_gram(); /* 更新显示 */
}
????先计算字符所占的字节数,然后根据字符串来对字符库进行索引,获取对应字模的首地址。然后根据字模依次往像素点里面写入数据。
/**
* @brief 在指定位置显示一个字符,包括部分字符
* @param x : 0~127
* @param y : 0~63
* @param size: 选择字体 12/16/24
* @param mode: 0,反白显示;1,正常显示
* @retval 无
*/
void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode)
{
uint8_t temp, t, t1;
uint8_t y0 = y;
uint8_t *pfont = 0;
uint8_t csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 得到字体一个字符对应点阵集所占的字节数 */
chr = chr - ' '; /* 得到偏移后的值,因为字库是从空格开始存储的,第一个字符是空格 */
if (size == 12) /* 调用1206字体 */
{
pfont = (uint8_t *)oled_asc2_1206[chr];
}
else if (size == 16) /* 调用1608字体 */
{
pfont = (uint8_t *)oled_asc2_1608[chr];
}
else if (size == 24) /* 调用2412字体 */
{
pfont = (uint8_t *)oled_asc2_2412[chr];
}
else /* 没有的字库 */
{
return;
}
for (t = 0; t < csize; t++)
{
temp = pfont[t];
for (t1 = 0; t1 < 8; t1++)
{
if (temp & 0x80)oled_draw_point(x, y, mode);
else oled_draw_point(x, y, !mode);
temp <<= 1;
y++;
if ((y - y0) == size)
{
y = y0;
x++;
break;
}
}
}
}
????根据数字的位数来将要显示的数字拆分成单个数字,然后转化成对应的字符串输出。
/**
* @brief 显示len个数字
* @param x,y : 起始坐标
* @param num : 数值(0 ~ 2^32)
* @param len : 显示数字的位数
* @param size: 选择字体 12/16/24
* @retval 无
*/
void oled_show_num(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
{
uint8_t t, temp;
uint8_t enshow = 0;
for (t = 0; t < len; t++) /* 按总显示位数循环 */
{
temp = (num / oled_pow(10, len - t - 1)) % 10; /* 获取对应位的数字 */
if (enshow == 0 && t < (len - 1)) /* 没有使能显示,且还有位要显示 */
{
if (temp == 0)
{
oled_show_char(x + (size / 2)*t, y, ' ', size, 1); /* 显示空格,站位 */
continue; /* 继续下个一位 */
}
else
{
enshow = 1; /* 使能显示 */
}
}
oled_show_char(x + (size / 2)*t, y, temp + '0', size, 1); /* 显示字符 */
}
}
/**
* @brief 显示字符串
* @param x,y : 起始坐标
* @param size: 选择字体 12/16/24
* @param *p : 字符串指针,指向字符串首地址
* @retval 无
*/
void oled_show_string(uint8_t x, uint8_t y, const char *p, uint8_t size)
{
while ((*p <= '~') && (*p >= ' ')) /* 判断是不是非法字符! */
{
if (x > (128 - (size / 2))) /* 宽度越界 */
{
x = 0;
y += size; /* 换行 */
}
if (y > (64 - size)) /* 高度越界 */
{
y = x = 0;
oled_clear();
}
oled_show_char(x, y, *p, size, 1); /* 显示一个字符 */
x += size / 2; /* ASCII字符宽度为汉字宽度的一半 */
p++;
}
}
????刷新函数是将定义的static uint8_t g_oled_gram[128][8]; 这个二维数组的数据写入到SSD1306里面的函数,由于我们之前的操作都是针对二维数组的,并没有将数据写入SSD1306,所以我们需要将二维数组的数据写入SSD1306里面让信息显示出来。
/**
* @brief 更新显存到OLED
* @param 无
* @retval 无
*/
void oled_refresh_gram(void)
{
uint8_t i, n;
for (i = 0; i < 8; i++)
{
oled_wr_byte (0xb0 + i, OLED_CMD); /* 设置页地址(0~7) */
oled_wr_byte (0x00, OLED_CMD); /* 设置显示位置—列低地址 */
oled_wr_byte (0x10, OLED_CMD); /* 设置显示位置—列高地址 */
for (n = 0; n < 128; n++)
{
oled_wr_byte(g_oled_gram[n][i], OLED_DATA);
}
}
}
int main(void)
{
uint8_t t = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
oled_init(); /* 初始化OLED */
oled_show_string(0, 0, "ALIENTEK", 24);
oled_show_string(0, 24, "0.96' OLED TEST", 16);
oled_show_string(0, 52, "ASCII:", 12);
oled_show_string(64, 52, "CODE:", 12);
oled_refresh_gram(); /* 更新显示到OLED */
t = ' ';
while (1)
{
oled_show_char(36, 52, t, 12, 1); /* 显示ASCII字符 */
oled_show_num(94, 52, t, 3, 12); /*显示ASCII字符的码值 */
oled_refresh_gram(); /*更新显示到OLED */
t++;
if (t > '~')
{
t = ' ';
}
delay_ms(500);
LED0_TOGGLE(); /* LED0闪烁 */
}
}