SSD1306(OLED驱动芯片) 设置内存地址模式(Set Memory Addressing Mode)(20h)

11 篇文章 0 订阅

参考了notmine 的博客 :SSD1306(OLED驱动芯片)指令详解(https://blog.csdn.net/notMine/article/details/79317782)

 

今天在写ESP8266的OLED屏幕驱动的时候,发现了一个奇怪的问题,在调用画点程序画一条直线的时候,直线变成了连续的断点,直接画一幅图的时候确是正常的,看了SSD1306的手册之后,发现了问题的所在,原来是驱动程序有误。

 设置内存地址模式(Set Memory Addressing Mode)一共有三种:页地址模式, 水平地址模式和垂直地址模式,需要使用命令将地址模式设置为以上三种之一。我拿到官方的驱动上选用的是页地址模式,下面引用上面提到的参考博客的其中一部分。


页地址模式(A[1:0]=10b)
当处于此模式时, 在GDDRAM访问后(读/写), 列地址指针将自动增加1。如果列地址指针到达列终止地址, 列地址指针将复位到列起始地址, 但页地址指针不会改变。
为了访问GDDRAM中下一页的内容, 用户必须设置新的页地址和列地址。页地址模式下页以及列地址指针的行为如下图所示

通常在页地址模式下访问GDDRAM, 需要如下步骤来定义起始RAM访问指针指向:
-通过命令(B0h-B7h)设置目标显示位置页起始地址
-通过命令(00h-0Fh)设置列起始地址低位
-通过命令(10h-1Fh)设置列起始地址高位

例如, 如果页地址是B2h, 列地址低位是03h, 列地址高位是10h, 起始列将为PAGE2的SEG3, GDDRAM访问指针的指向如下图所示


水平地址模式(A[1:0]=00b)
当处于此模式时, 在GDDRAM访问后(读/写), 列地址指针将自动增加1。如果列地址指针到达列终止地址, 列地址指针将复位到列起始地址, 且页地址指针将自动增加1。
水平地址模式下页以及列地址指针的行为如下图所示, 如果列地址指针和页地址指针都到达各自的终止地址时, 他们都将复位到各自的起始地址。(图中虚线)


垂直地址模式(A[1:0]=01b)
当处于此模式时, 在GDDRAM访问后(读/写), 页地址指针将自动增加1。如果页地址指针到达页终止地址, 页地址指针将复位到页起始地址, 且列地址指针将自动增加1。
垂直地址模式下页以及列地址指针的行为如下图所示, 如果列地址指针和页地址指针都到达各自的终止地址时, 他们都将复位到各自的起始地址。(图中虚线)


通常在(垂直/水平)地址模式下访问GDDRAM, 需要如下步骤来定义起始RAM访问指针指向:
-通过命令(21h)设置目标显示位置列起始地址以及列终止地址
-通过命令(22h)设置目标显示位置页起始地址以及页终止地址

OLED设置内存地址模式的方法如下:

    oled_write_cmd(0x20);    // Set Memory Addressing Mode (20h)
    oled_write_cmd(0x02);    // Set 页地址模式(A[1:0]=10b) 水平地址模式(A[1:0]=00b) 垂直地址模式(A[1:0]=01b)

 

根据这个文档,我发现库中提供的设置点坐标的函数是有错误的 

错误的oled_set_pos函数如下:

esp_err_t oled_set_pos(uint8_t x_start, uint8_t y_start)
{
    oled_write_cmd(0xb0 + y_start);
    oled_write_cmd(((x_start & 0xf0) >> 4) | 0x10);
    oled_write_cmd((x_start & 0x0f) | 0x01);
    return ESP_OK;
}

从手册中可以看出 在页地址模式下访问GDDRAM, 需要
-通过命令(B0h-B7h)设置目标显示位置页起始地址
-通过命令(00h-0Fh)设置列起始地址低位
-通过命令(10h-1Fh)设置列起始地址高位

由此可见设置列起始地址低位时候 范围是 00H-0FH ,而驱动中却把最低位强制置1了,这样并不符合手册中设置坐标的方式,会让最后一位不是1的自动跳到下一位,也就是上图出现的隔一个点才显示一次的问题。

所以正确的oled_set_pos函数应该是这样的:

esp_err_t oled_set_pos(uint8_t x_start, uint8_t y_start)
{
    oled_write_cmd(0xb0 + y_start);
    oled_write_cmd(((x_start & 0xf0) >> 4) | 0x10);
    oled_write_cmd((x_start & 0x0f) );
    return ESP_OK;
}

进行修改之后发现 使用画点程序连续画一条时 显示的直线是正常的。

因为在网上看到很多OLED屏幕的驱动程序都把最低位强制置1了,我也想知道这些驱动中把最低位置1的理由,我把发现的问题写在这里,也希望了解的人能够和我进行交流。

 

另外 除了页地址模式 我还写了 水平地址模式的驱动

(垂直/水平)地址模式下访问GDDRAM, 需要如下步骤来定义起始RAM访问指针指向:
-通过命令(21h)设置目标显示位置列起始地址以及列终止地址
-通过命令(22h)设置目标显示位置页起始地址以及页终止地址

在需要设置本次写入的坐标时 需要如下操作

    oled_write_cmd(0x21);//COLUMNADDR
    oled_write_cmd(x轴起点);
    oled_write_cmd(x轴终点);

    oled_write_cmd(0x22);//PAGEADDR
    oled_write_cmd(y轴起点);
    oled_write_cmd(y轴终点);

 

下面是水平地址模式下 显示一张图片的源代码 是基于Arduino修改而来的


// Set 水平地址模式(A[1:0]=00b)
void display(void) 
{

    if(dispaly_image_buffer==NULL)
    {
        DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Not init display\n");
        return;
    }

#ifdef OLED_BACK_IMAGE_ENABLE

    if(dispaly_image_buffer_back==NULL)
    {
        DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Not BACK init display\n");
        return;
    }
        
    uint8_t minBoundY = UINT8_MAX;
    uint8_t maxBoundY = 0;

    uint8_t minBoundX = UINT8_MAX;
    uint8_t maxBoundX = 0;

    uint8_t x, y;

    // Calculate the Y bounding box of changes
    // and copy buffer[pos] to buffer_back[pos];
    for (y = 0; y < (displayHeight / 8); y++) {
        for (x = 0; x < displayWidth; x++) {
          uint16_t pos = x + y * displayWidth;
          if (dispaly_image_buffer[pos] != dispaly_image_buffer_back[pos]) {
            minBoundY = _min(minBoundY, y);
            maxBoundY = _max(maxBoundY, y);
            minBoundX = _min(minBoundX, x);
            maxBoundX = _max(maxBoundX, x);
          }
          dispaly_image_buffer_back[pos] = dispaly_image_buffer[pos];
        }
    }

    // If the minBoundY wasn't updated
    // we can savely assume that buffer_back[pos] == buffer[pos]
    // holdes true for all values of pos
    if (minBoundY == UINT8_MAX) return;

    oled_write_cmd(0x21);//COLUMNADDR
    oled_write_cmd(minBoundX);
    oled_write_cmd(maxBoundX);

    oled_write_cmd(0x22);//PAGEADDR
    oled_write_cmd(minBoundY);
    oled_write_cmd(maxBoundY);

    printf("minx:%d maxx:%d miny:%d maxy:%d\n",minBoundX,maxBoundX,minBoundY,maxBoundY);
    for (y = minBoundY; y <= maxBoundY; y++) {
        for (x = minBoundX; x <= maxBoundX; x++) {
           oled_write_byte(dispaly_image_buffer[x + y * displayWidth]);//x + y * displayWidth
        }
    }

#else
    // No double buffering
    oled_write_cmd(0x21);//COLUMNADDR
    oled_write_cmd(0x0);
    oled_write_cmd(0x7F);

    oled_write_cmd(0x22);//PAGEADDR
    oled_write_cmd(0x0);

    oled_write_cmd(0x7);

    for (uint16_t i=0; i<displayBufferSize; i++) {
        oled_write_byte(dispaly_image_buffer[i]);//x + y * displayWidth
    }

#endif
}

为OLED画一个点而纠结的原因也正在于是用了上面第一种驱动方式,使用这个函数,能最低限度的与OLED通信,节省了SPI通信的时间, 它可以做到画一个点只需要改动一个字节。

 

完整的代码我会在后续更新到我的Github上,欢迎访问~

以下是一个基于STM32的SSD1309 SPI驱动代码示例: ```c #include "stm32f10x.h" #include "stm32f10x_spi.h" #define SSD1309_DC_PIN GPIO_Pin_6 #define SSD1309_DC_PORT GPIOB #define SSD1309_CS_PIN GPIO_Pin_7 #define SSD1309_CS_PORT GPIOB #define SSD1309_RST_PIN GPIO_Pin_8 #define SSD1309_RST_PORT GPIOB #define SSD1309_WIDTH 128 #define SSD1309_HEIGHT 64 static uint8_t ssd1309_buffer[SSD1309_WIDTH * SSD1309_HEIGHT / 8]; void ssd1309_init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; /* GPIO clock enable */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); /* SPI clock enable */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); /* Configure SCK, MOSI and NSS pins as Alternate Function Push Pull */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); /* Configure DC, CS and RST pins as Output Push Pull */ GPIO_InitStructure.GPIO_Pin = SSD1309_DC_PIN | SSD1309_CS_PIN | SSD1309_RST_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(SSD1309_DC_PORT, &GPIO_InitStructure); /* Set RST pin low to reset SSD1309 */ GPIO_ResetBits(SSD1309_RST_PORT, SSD1309_RST_PIN); Delay(10); GPIO_SetBits(SSD1309_RST_PORT, SSD1309_RST_PIN); Delay(10); /* Configure SPI */ SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI2, &SPI_InitStructure); /* Enable SPI */ SPI_Cmd(SPI2, ENABLE); /* Initialize SSD1309 */ ssd1309_command(0xAE); // Display off ssd1309_command(0xD5); // Set display clock divide ratio/oscillator frequency ssd1309_command(0x80); // Set divide ratio ssd1309_command(0xA8); // Set multiplex ratio ssd1309_command(0x3F); // Set to 64 COM lines ssd1309_command(0xD3); // Set display offset ssd1309_command(0x00); // No offset ssd1309_command(0x40); // Set display start line ssd1309_command(0x8D); // Charge pump ssd1309_command(0x14); // Enable charge pump ssd1309_command(0x20); // Set memory addressing mode ssd1309_command(0x00); // Horizontal addressing mode ssd1309_command(0xA0); // Set segment remap ssd1309_command(0xC8); // Set COM output scan direction ssd1309_command(0xDA); // Set COM pins hardware configuration ssd1309_command(0x12); // Alternative configuration ssd1309_command(0x81); // Set contrast control ssd1309_command(0xCF); // Set contrast ssd1309_command(0xD9); // Set pre-charge period ssd1309_command(0xF1); // Phase 1 period of 15 DCLKs, Phase 2 period of 1 DCLK ssd1309_command(0xDB); // Set VCOMH deselect level ssd1309_command(0x40); // 0.77*VCC ssd1309_command(0xA4); // Set entire display on/off ssd1309_command(0xA6); // Set normal display ssd1309_command(0xAF); // Display on } void ssd1309_command(uint8_t cmd) { GPIO_ResetBits(SSD1309_DC_PORT, SSD1309_DC_PIN); GPIO_ResetBits(SSD1309_CS_PORT, SSD1309_CS_PIN); SPI_I2S_SendData(SPI2, cmd); while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) == SET); GPIO_SetBits(SSD1309_CS_PORT, SSD1309_CS_PIN); } void ssd1309_data(uint8_t *data, uint32_t size) { GPIO_SetBits(SSD1309_DC_PORT, SSD1309_DC_PIN); GPIO_ResetBits(SSD1309_CS_PORT, SSD1309_CS_PIN); while (size--) { SPI_I2S_SendData(SPI2, *data++); while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) == SET); } GPIO_SetBits(SSD1309_CS_PORT, SSD1309_CS_PIN); } void ssd1309_display(void) { ssd1309_command(0x21); // Set column address ssd1309_command(0x00); // Column start address ssd1309_command(0x7F); // Column end address ssd1309_command(0x22); // Set page address ssd1309_command(0x00); // Page start address ssd1309_command(0x07); // Page end address ssd1309_data(ssd1309_buffer, sizeof(ssd1309_buffer)); } void ssd1309_clear(void) { memset(ssd1309_buffer, 0, sizeof(ssd1309_buffer)); } ``` 这个驱动代码基于STM32F10x系列芯片,使用SPI2控制SSD1309 OLED显示屏。在初始化函数`ssd1309_init()`中,首先配置GPIO和SPI,然后通过I/O口控制SSD1309的复位和初始化。接着,发送一系列命令配置SSD1309的工作模式和显示参数。在`ssd1309_command()`和`ssd1309_data()`函数中,使用SPI接口向SSD1309发送命令和数据。最后,`ssd1309_display()`函数将屏幕缓存中的数据写入SSD1309,`ssd1309_clear()`函数将屏幕缓存清空。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值