OLED显示实验

2023-12-29 11:53:52

简介

??OLED即有机发光二极管,本次介绍的是正点原子的OLED显示模块。
(1)模块有单色和双色两种可选,单色为纯蓝色,双色为黄蓝双色(分区域的双色,前16行为黄色,后48行为蓝色,黄蓝之间有一行不显示的间隔区)
(2)尺寸小,显示尺寸为0.96寸,模块大小为27mm26mm大小
(3)高分辨率,模块分辨率为128
64
(4)多种接口模式,该模块提供了4种接口,包括:6800、8080两种并行接口方式4线SPI接口方式和IIC接口方式。
(5)不需要高压,直接3.3V就可以工作了。

接口方式4线SPIIIC8位68008位8080
BS10101
BS20011

模块外观如下图所示:
在这里插入图片描述
??正点原子使用的是8080并口模式,原理图如下:在这里插入图片描述
??正点原子的OLED控制器是SSD1306.下面将学习如何使用该芯片来控制模块来显示字符和数字。

硬件驱动接口模式

8080并口模式

??8080接口需要的信号线如下所示

信号名称作用
CSOLED片选信号
WR向OLED写入数据
RD从OLED读取数据
D[7:0]8位双向数据线
RST(RES)硬复位OLED
DC命令/数据标志(0,读写命令; 1,读写数据)

??8080接口下,控制脚的信号状态所对应的功能表:

功能RDWRCSDC
写命令HPLL
读状态PHLL
写数据HPLH
读数据PH1H

8080并口写过程

??8080并口写入的过程为:

Created with Rapha?l 2.3.0 设置DC为高(数据)/低(命令) 拉低片选CS选中SSD1306 保持RD为高电平 拉低WR准备写入数据 向数据线(D[7:0])上输入要写的信息 拉高WR,在此上升沿数据被写入SSD1306

8080并口写时序
在这里插入图片描述

8080并口读过程

??8080并口读入的过程为:

Created with Rapha?l 2.3.0 设置DC为高(数据)/低(命令) 拉低片选CS选中SSD1306 保持WR为高电平 拉低RD准备读出数据 在读时序的上升沿锁存数据线D[7:0]上

??在8080方式下读数据操作的时候,我们会有一个假读命令(Dummy Read),使得微控制器的操作频率与显存的操作频率匹配。其实就是读到的第一个数据不要,从第二个数据开始才是我们真正要读的数据。

OLED显存模块

??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模块的初始化过程如下图所示:

复位SSD1306
初始化
开启显示
清零显存
开始显示
rst=0delay_ms10RST=1
建议复位所有寄存器

??在STM32中驱动OLED的步骤如下:
????(1)设置STM32F103与OLED模块连接的IO
????(2)初始化OLED
????(3)通过函数将字符和数字显示到OLED模块上

硬件设计

??本次接线采用的8080并口。

开发板IOOLED模块IOSTM32芯片对应的IO
VCC3.3VCC3.3
OV_WRSTOLED_CSPD6
OV_RRSTOLED_RWPG14
OV_OEOLED_RSTPG15
OV_D1OLED_D1PC1
OV_D3OLED_D3PC3
OV_D5OLED_D5PC5
OV_D7OLED_D7PC7
GNDGND
OV_SCLOLED_DCPD3
OV_SDAOLED_RDPG13
OV_D2OLED_D2PC2
OV_D4OLED_D4PC4
OV_D6OLED_D6PC6
OV_RCLK悬空悬空

软件设计

流程图

开始
系统级别的初始化,Hal库系统时钟延时和串口初始化
用户初始化LED灯初始化OLED初始化
OLED显示起始信息更新显示到OLED
显示ASCII字符
显示ASCII字符码值
更新显示到OLED
延时LED0翻转

程序解析

??以下源码均来自正点原子开源教程。

??初始化与OLED有关的GPIO
    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);
        }
    }
}
??main函数
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闪烁 */
    }
}

文章来源:https://blog.csdn.net/q1594/article/details/135254075
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。