Stories

Detail Return Return

《ESP32-S3使用指南—IDF版 V1.6》第二十一章 IIC_OLED實驗 - Stories Detail

第二十一章 IIC_OLED實驗

1)實驗平台:正點原子DNESP32S3開發板

2)章節摘自【正點原子】ESP32-S3使用指南—IDF版 V1.6

3)購買鏈接:https://detail.tmall.com/item.htm?&id=768499342659

4)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html

5)正點原子官方B站:https://space.bilibili.com/394620890

6)正點原子DNESP32S3開發板技術交流羣:132780729

本章我們來學習使用OLED液晶顯示屏,在開發板上我們預留了OLED模塊接口,需要準備一個OLED顯示模塊。下面我們一起來點亮OLED,並實現ASCII字符的顯示。本章分為如下幾個小節:
21.1 OLED簡介
21.2 硬件設計
21.3 程序設計
21.4 下載驗證

21.1 OLED簡介

OLED,即有機發光二極管(Organic Light-Emitting Diode),又稱為有機電激光顯示(Organic Electroluminesence Display,OLED)。OLED可按發光材料分為兩種:小分子OLED和高分子OLED(也可稱為PLED)。OLED是一種利用多層有機薄膜結構產生電致發光的器件,它很容易製作,而且只需要低的驅動電壓,OLED由於同時具備自發光(不需背光源)、對比度高、厚度薄、視角廣、反應速度快、功耗低、柔性好等優異特性,目前主要用於顯示領域,OLED在節能照明領域的開發也成為全球趨勢。
本章我們將介紹ALINETEK的OLED顯示模塊及其使用方法,該模塊有以下特點:
l 模塊有單色和雙色兩種可選,單色為純藍色,而雙色則為黃藍雙色(分區域的雙色,前16行為黃色,後48行為藍色,且黃藍色之間有一行不顯示的間隔區。)。
l 尺寸小,顯示尺寸為0.96寸,而模塊的尺寸僅為27mm * 26mm大小。
l 高分辨率,該模塊的分辨率為128 * 64。
l 多種接口方式,該模塊提供了總共4種接口包括:6800、8080兩種並行接口方式、4線SPI接口方式以及IIC接口方式(只需要2根線就可以控制OLED了!)。
l 不需要高壓,直接接3.3V就可以工作了。
這裏要提醒大家的是,該模塊不和5.0V接口兼容,所以請大家在使用的時候一定要小心,別直接接到5V的系統上去,否則可能燒壞模塊。以下4種模式通過模塊的BS1和BS2設置,BS1和BS2的設置與模塊接口模式的關係如表21.1.1所示:

表21.1.1 OLED模塊接口方式設置表
表21.1.1中:“1”代表接VCC,而“0”代表接GND。該模塊的外觀圖如圖21.1.1所示:

圖21.1.1 正點原子 OLED模塊外觀圖
正點原子OLED模塊默認設置是:BS1和BS2接VCC,即使用8080並口方式,如果你想要設置為其他模式,則需要在OLED的背面,用烙鐵修改BS1和BS2的設置。
對於本實驗而言,是需要將OLED模塊的接口方式修改為IIC,所以就需要把BS2接到GND,並把D1和D2連接起來。最終使用IIC接口的OLED模塊圖如下圖所示。

圖21.1.2 IIC接口的OLED模塊

圖21.1.3 正點原子 OLED模塊原理圖
該模塊採用8 * 2的2.54排針與外部連接,總共有16個管腳,在16條線中,我們只用了15條,有一個是懸空的。15條線中,電源和地線佔了2條,還剩下13條信號線。在不同模式下,我們需要的信號線數量是不同的,在8080模式下,需要全部13條,而在該模塊的IIC模式下,需要4條線,分別為OLED_D0(SCL)、OLED_D1和D2(SDA)、OLED_DC、OLED_RST。在IIC模式下,OLED_D0是作為IIC的SCL線,OLED_D1和D2連接一起作為IIC的SDA線,而OLED_DC是作為SA0,用於設置IIC器件地址,OLED_RST是復位線,RST上的低電平,將導致OLED復位,在每次初始化之前,都應該復位一下OLED模塊。
要進行IIC通信,首先得知道器件地址,OLED器件地址是7位的,具體格式如下表所示。

表21.1.2 OLED設備地址
OLED模塊的主控芯片為SSD1306,從上表可以知道,SSD1306器件地址由兩部分組成,一部分就是“固定部分”即“011110”;另一部分就是“可變部分”即SA0引腳,在程序上會讓該引腳輸出低電平,所以該位為“0”。最終可得到,SSD1306器件地址為“0111100”即0x3C。讀操作地址就為0x79,即0111 1001;寫操作地址就為0x78,即0111 1000。

21.1.1 硬件驅動接口模式
l 8080並口模式
首先我們介紹一下模塊的8080並行接口,8080並行接口的發明者是INTEL,該總線也被廣泛應用於各類液晶顯示器,正點原子 OLED模塊也提供了這種接口,使得MCU可以快速的訪問OLED。正點原子 OLED模塊的8080接口方式需要如下一些信號線:
CS:OLED片選信號。
WR:向OLED寫入數據。
RD:從OLED讀取數據。
D[7:0]:8位雙向數據線。
RST(RES):硬復位OLED。
DC:命令/數據標誌(0,讀寫命令;1,讀寫數據)。
模塊的8080並口寫的過程為:先根據要寫入的數據的類型,設置DC為高(數據)/低(命令),設置WR起始電平為高,然後拉低片選,選中SSD1306,接着我們在整個讀時序上保持RD為高電平,然後拉低WR的電平準備寫入數據,向數據線(D[7:0])上輸入要寫的信息;
拉高WR,這樣得到一個WR的上升沿,在這個上升沿,使數據寫入到SSD1306裏面;
SSD1306的8080並口寫時序圖如圖21.1.1.1所示:

圖21.1.1.1 8080並口寫時序圖
模塊的8080並口讀的過程為:先根據要寫入的數據的類型,設置DC為高(數據)/低(命令),設置RD起始電平為高,然後拉低片選CS信號,選中SSD1306,接着我們在整個讀時序上保持WR為高電平,然後類似寫時序,同樣的,在RD的上升沿,使數據鎖存到數據線(D[7:0])上;SSD1306的8080並口讀時序圖如圖21.1.1.2所示:

圖21.1.1.2 8080並口讀時序圖
SSD1306的8080接口方式下,控制腳的信號狀態所對應的功能如表21.1.1.1:

表21.1.1.1 控制腳信號狀態功能表
在8080方式下讀數據操作的時候,我們有時候(例如讀顯存的時候)需要一個假讀命(Dummy Read),以使得微控制器的操作頻率和顯存的操作頻率相匹配。在讀取真正的數據之前,由一個的假讀的過程。這裏的假讀,其實就是第一個讀到的字節丟棄不要,從第二個開始,才是我們真正要讀的數據。
一個典型的讀顯存的時序圖,如圖21.1.1.3所示:

圖21.1.1.3 讀顯存時序圖
可以看到,在發送了列地址之後,開始讀數據,第一個是Dummy Read,也就是假讀,我們從第二個開始,才算是真正有效的數據。並行接口模式就介紹到這裏。
l SPI模式
我們的代碼同時兼容SPI方式的驅動,如果你使用的是這種驅動方式,則應該把代碼中的宏OLED_MODE設置為0,但對於硬件,則需要查看PCB背面的電阻設置以確定當前使用的是否為SPI模式:

#define OLED_MODE       0  /* 0: 4線串行模式 */

我們接下來介紹一下4線串行(SPI)方式,4先串口模式使用的信號線有如下幾條:
CS:OLED片選信號。
RST(RES):硬復位OLED。
DC:命令/數據標誌(0,讀寫命令;1,讀寫數據)。
SCLK:串行時鐘線。在4線串行模式下,D0信號線作為串行時鐘線SCLK。
SDIN:串行數據線。在4線串行模式下,D1信號線作為串行數據線SDIN。
模塊的D2需要懸空,其他引腳可以接到GND。在4線串行模式下,只能往模塊寫數據而不能讀數據。
在4線SPI模式下,每個數據長度均為8位,在SCLK的上升沿,數據從SDIN移入到SSD1306,並且是高位在前的。DC線還是用作命令/數據的標誌線。在4線SPI模式下,寫操作的時序如圖21.1.1.4所示:

圖21.1.1.4 4線SPI寫操作時序圖
4線串行模式就為大家介紹到這裏。其他還有幾種模式,可以參考SSD1306的數據手的介紹,我們把資料放到“開發板資料A盤->7,硬件資料\3,液晶資料\OLED資料\SSD1306-Revision1.1(Charge Pump).pdf”,如果要使用這些方式,請大家參考該手冊並自行實現相應的功能代碼。

21.1.2 OLED顯存
接下來,我們介紹一下模塊的顯存,SSD1306的顯存總共為128 * 64bit大小,SSD1306將這些顯存分為了8頁,不使用顯存對應的行列的重映射,其對應關係如表21.1.2.1所示:

表21.1.2.1 SSD1306顯存與屏幕對應關係表
可以看出,SSD1306的每頁包含了128個字節,總共8頁,這樣剛好是128 * 64的點陣大小。當GRAM的寫入模式為頁模式時,需要設置低字節起始的列地址(0x00~0x0F)和高字節的起始列地址(0x10~0x1F),芯片手冊中給出了寫入GRAM與顯示的對應關係,寫入列地址在寫完一字節後自動按列增長,如圖21.1.2.1所示:

圖21.1.2.1 SSD1306頁2顯存寫入字節與屏幕座標的關係
因為每次寫入都是按字節寫入的,這就存在一個問題,如果我們使用只寫方式操作模塊,那麼,每次要寫8個點,這樣,我們在畫點的時候,就必須把要設置的點所在的字節的每個位都搞清楚當前的狀態(0/1?),否則寫入的數據就會覆蓋掉之前的狀態,結果就是有些不需要顯示的點,顯示出來了,或者該顯示的沒有顯示了。這個問題在能讀的模式下,我們可以先讀出來要寫入的那個字節,得到當前狀況,在修改了要改寫的位之後再寫進GRAM,這樣就不會影響到之前的狀況了。但是這樣需要能讀GRAM,對於4線SPI模式/IIC模式,模塊是不支持讀的,而且讀à改à寫的方式速度也比較慢。
所以我們採用的辦法是在ESP32的內部建立一個虛擬的OLED的GRAM(共128 * 8=1024個字節),每次修改時,只修改ESP32上的GRAM(實際上就是SRAM),在修改完成後一次性把ESP32上的GRAM寫入到OLED的GRAM。當然這個方法也有壞處,一個對於那些SRAM很小的單片機(比如51系列)不太友好,另一個是每次都寫入全屏,屏幕刷新率會變低。
SSD1306的命令比較多,這裏我們僅介紹幾個比較常用的命令,如表21.1.2.2所示:

表21.1.2.2 SSD1306常用命令表
第0個命令為0X81,用於設置對比度的,這個命令包含了兩個字節,第一個0X81為命令,隨後發送的一個字節為要設置的對比度的值。這個值設置得越大屏幕就越亮。
第1個命令為0XAE/0XAF。0XAE為關閉顯示命令;0XAF為開啓顯示命令。
第2個命令為0X8D,該指令也包含2個字節,第一個為命令字,第二個為設置值,第二個字節的BIT2表示電荷泵的開關狀態,該位為1,則開啓電荷泵,為0則關閉。在模塊初始化的時候,這個必須要開啓,否則是看不到屏幕顯示的。
第3個命令為0XB0~B7,該命令用於設置頁地址,其低三位的值對應着GRAM的頁地址。
第4個指令為0X00~0X0F,該指令用於設置顯示時的起始列地址低四位。
第6個指令為0X10~0X1F,該指令用於設置顯示時的起始列地址高四位。
其他命令,我們就不在這裏一一介紹了,大家可以參考SSD1306 datasheet的第28頁。從這頁開始,對SSD1306的指令有詳細的介紹。
最後,我們再來介紹一下OLED模塊的初始化過程,SSD1306典型初始化框圖如圖21.1.2.2:

圖21.1.2.2 SSD1306初始化框圖
驅動IC的初始化代碼,我們直接使用廠家推薦的設置就可以了,只要對細節部分進行一些修改,使其滿足我們自己的要求即可,其他不需要變動。
OLED的介紹就到此為止,我們重點向大家介紹了正點原子OLED模塊的相關知識,接下來我們將使用這個模塊來顯示字符和數字。通過以上介紹,我們可以得出OLED顯示需要的相關設置步驟如下:
1)設置ESP32與OLED模塊相連接的IO。
這一步,先將我們與OLED模塊相連的IO口設置為輸出,具體使用哪些IO口,這裏需要根據連接電路以及OLED模塊所設置的通訊模式來確定。這些將在硬件設計部分向大家介紹。
2)初始化OLED模塊。
其實這裏就是上面的初始化框圖的內容,通過對OLED相關寄存器的初始化,來啓動OLED的顯示。為後續顯示字符和數字做準備。
3)通過函數將字符和數字顯示到OLED模塊上。
這裏就是通過我們設計的程序,將要顯示的字符送到OLED模塊就可以了,這些函數將在程序設計部分向大家介紹。
通過以上三步,我們就可以使用正點原子OLED模塊來顯示字符和數字了,在後面我們還將會給大家介紹顯示漢字的方法。這一部分就先介紹到這裏。

21.2 硬件設計

21.2.1 例程功能
使用IIC模式驅動OLED模塊,不停的顯示ASCII碼和碼值。
21.2.2 硬件資源

  1. LED燈
    LED - IO1
  2. XL9555
    IIC_SDA-IO41
    IIC_SCL-IO42
  3. OLED
    IIC_SCL - IO4
    IIC_SDA - IO5
    D2 - IO6
    DC - IO38
    RST - IO_05(XL9555)
    21.2.3 原理圖
    OLED模塊的原理圖在前面已有詳細説明了,這裏我們介紹OLED模塊與我們開發板的連接,開發板上有一個OLED/CAMERA的接口(P6接口)可以和正點原子OLED模塊直接對插(靠左插!),連接如圖21.2.3.1所示:

    圖21.2.3.1 OLED模塊與開發板連接示意圖

21.3 程序設計

21.3.1 程序流程圖
程序流程圖能幫助我們更好的理解一個工程的功能和實現的過程,對學習和設計工程有很好的主導作用。下面看看本實驗的程序流程圖:

圖21.3.1.1 OLED實驗程序流程圖

21.3.2 IIC_OLED函數解析
由於OLED是基於IIC協議進行通訊的,並且ESP32的IIC函數在前面的章節已經詳細講解過了,因此,關於ESP32的IIC函數這一部分內容,請讀者們回顧之前的內容,筆者在此不再贅述。

21.3.3 IIC_OLED驅動解析
這裏我們只講解核心代碼,詳細的源碼請大家參考光盤本實驗對應源碼,OLED的驅動主要包括三個文件:oled.c、oled.h和oledfont.h。oledfont.h頭文件存放的是ASCII字符集,oled.h存放的是引腳接口宏定義和函數聲明等,oled.c則是驅動代碼。
1,oledfont.h文件
首先看oledfont.h文件的ASCII字符集內容:

/* 常用ASCII表
* 偏移量32
* ASCII字符集: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]
^_`abcdefghijklmnopqrstuvwxyz{|}~
* PC2LCD2002取模方式設置:陰碼+逐列式+順向+C51格式
* 總共:3個字符集(12*12、16*16和24*24),用户可以自行新增其他分辨率的字符集。
* 每個字符所佔用的字節數為:(size/8+((size%8)?1:0))*(size/2),其中size:是字庫生成時的點
*  陣大小(12/16/24...)
*/
/* 12*12 ASCII字符集點陣 */
const unsigned char oled_asc2_1206[95][12]={ ...這裏省略字符集庫... };
/* 16*16 ASCII字符集點陣 */
const unsigned char oled_asc2_1608[95][16]={ ...這裏省略字符集庫... };
/* 24*24 ASICII字符集點陣 */
const unsigned char oled_asc2_2412[95][36]={ ...這裏省略字符集庫... };
該頭文件中包含三個大小不同的ASCII字符集點陣,其中包括:12*12ASCII字符集點陣、16*16ASCII字符集點陣、24*24ASICII字符集點陣。每個字符集點陣都包含95個常用的ASCII字符集,從空格符開始(即ASCII碼錶編號的32~127對應的字符),分別為:!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~。

上面的ASCII字符集,我們可以使用一個款很好的字符提取軟件來製作獲取。字符提取軟件為:PCtoLCD2002完美版,該軟件可以提供各種字符,包括漢字(字體和大小都可以自己設置)陣提取,且取模方式可以設置好幾種,常用的取模方式,該軟件都支持。該軟件還支持圖形模式,也就是用户可以自己定義圖片的大小,然後畫圖,根據所畫的圖形再生成點陣數據,這功能在製作圖標或圖片的時候很有用。
該軟件的界面如圖21.3.3.1所示:

圖21.3.3.1 PCtoLCD2002軟件界面
然後我們選擇設置,在設置裏面設置取模方式如圖21.3.3.2所示:

圖21.3.3.2 設置取模方式
上圖設置的取模方式,在右上角的取模説明裏面有,即:從第一列開始向下每取8個點作為一個字節,如果最後不足8個點就補滿8位。取模順序是從高到低,即第一個點作為最高位。如*-------取為10000000。其實就是按如圖21.3.3.3所示的這種方式:

圖21.3.3.3 取模方式圖解
從上到下,從左到右,高位在前。我們按這樣的取模方式,然後把ASCII字符集按12 6大小、16 8和24 12大小取模出來(對應漢字大小為12 12、16 16和24 24,字符的只有漢字的一半大!),每個12 6的字符佔用12個字節,每個16 8的字符佔用16個字節,每個24 * 12的字符佔用36個字節。
2,oled.h文件
下面我們先解析oled.h的程序。對OLED模塊IIC引腳和OLED器件地址做了相關定義。

#define OLED_ADDR           0X3C    /* 7位器件地址 */
#define OLED_CMD        0x00    /* 寫命令 */
#define OLED_DATA       0x40    /* 寫數據 */
#define OLED_SCL_PIN       4
#define OLED_SDA_PIN       5
#define OLED_D2_PIN        6
#define OLED_DC_PIN        38

我們選擇使用IO4作為IIC的時鐘線,IO5作為IIC的數據線,IO6為OLED_D2_PIN,IO38為OLED_DC_PIN。OLED的器件地址為0x3C。
由於會使用到RST引腳,而RST引腳是XL9555器件上的IO,這裏便有OLED_RST宏用來控制RST引腳輸出高低電平,實現OLED的硬件復位。

#define OLED_RST(x)  xl9555_pin_set(OV_RESET, x ? IO_SET_HIGH : IO_SET_LOW);

3,oled.c文件
最後就是oled.c文件的驅動源碼介紹。先是OLED(SSD1306)的初始化函數,以8080並口方式為例,其定義如下:

/**
* @brief       初始化OLED
* @param       i2c_obj_t self: 傳入的IIC初始化參數,用以判斷是否已經完成IIC初始化
* @retval      無
*/
void oled_init(i2c_obj_t self)
{
    if (self.init_flag == ESP_FAIL)
    {
        iic_init(I2C_NUM_1);                                 /* 初始化IIC */
    }
    oled_master = self;
    gpio_config_tgpio_init_struct = {0};
   gpio_init_struct.intr_type = GPIO_INTR_DISABLE;         /* 失能引腳中斷 */
   gpio_init_struct.mode = GPIO_MODE_INPUT;              /* 輸入輸出模式 */
   gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;       /* 使能上拉 */
   gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */
   gpio_init_struct.pin_bit_mask = 1ULL << OLED_D2_PIN;   /* 設置的引腳的位掩碼 */
    gpio_config(&gpio_init_struct);
   gpio_init_struct.intr_type = GPIO_INTR_DISABLE;         /* 失能引腳中斷 */
   gpio_init_struct.mode = GPIO_MODE_OUTPUT;               /* 輸入輸出模式 */
   gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;       /* 使能上拉 */
   gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */
   gpio_init_struct.pin_bit_mask = 1ULL << OLED_DC_PIN;   /* 設置的引腳的位掩碼 */
    gpio_config(&gpio_init_struct);
   
    OLED_DC(0);
   
    /* 配置復位引腳電平 */
   xl9555_pin_write(OV_RESET_IO, 1);
    /*復位OLED*/
    OLED_RST(0);
    vTaskDelay(100);
    OLED_RST(1);
    vTaskDelay(100);
    /* 初始化代碼 */
    oled_write_Byte(0xAE, OLED_CMD);    /* 關閉顯示 */
    oled_write_Byte(0xD5, OLED_CMD);    /* 設置時鐘分頻因子,震盪頻率 */
    oled_write_Byte(80, OLED_CMD);      /* [3:0],分頻因子;[7:4],震盪頻率 */
    oled_write_Byte(0xA8, OLED_CMD);    /* 設置驅動路數 */
    oled_write_Byte(0X3F, OLED_CMD);    /* 默認0X3F(1/64) */
    oled_write_Byte(0xD3, OLED_CMD);    /* 設置顯示偏移 */
    oled_write_Byte(0X00, OLED_CMD);    /* 默認為0 */
    oled_write_Byte(0x40, OLED_CMD);    /* 設置顯示開始行 [5:0],行數 */
    oled_write_Byte(0x8D, OLED_CMD);    /* 電荷泵設置 */
    oled_write_Byte(0x14, OLED_CMD);    /* bit2,開啓/關閉 */
oled_write_Byte(0x20, OLED_CMD);    /* 設置內存地址模式 */
/* [1:0],00,列地址模式;01,行地址模式;10,頁地址模式;默認10; */
    oled_write_Byte(0x02, OLED_CMD);
oled_write_Byte(0xA1, OLED_CMD);    /* 段重定義設置,bit0:0,0->0;1,0->127; */
/* 設置COM掃描方向;bit3:0,普通模式;1,重定義模式 COM[N-1]->COM0;N:驅動路數 */
    oled_write_Byte(0xC0, OLED_CMD);
    oled_write_Byte(0xDA, OLED_CMD);    /* 設置COM硬件引腳配置 */
    oled_write_Byte(0x12, OLED_CMD);    /* [5:4]配置 */
            
    oled_write_Byte(0x81, OLED_CMD);    /* 對比度設置 */
    oled_write_Byte(0xEF, OLED_CMD);    /* 1~255;默認0X7F (亮度設置,越大越亮) */
    oled_write_Byte(0xD9, OLED_CMD);    /* 設置預充電週期 */
    oled_write_Byte(0xf1, OLED_CMD);    /*[3:0],PHASE 1;[7:4],PHASE 2; */
oled_write_Byte(0xDB, OLED_CMD);    /* 設置VCOMH 電壓倍率 */
/* [6:4]000,0.65*vcc;001,0.77*vcc;011,0.83*vcc; */
    oled_write_Byte(0x30, OLED_CMD);
/* 全局顯示開啓;bit0:1,開啓;0,關閉;(白屏/黑屏) */
    oled_write_Byte(0xA4, OLED_CMD);
    oled_write_Byte(0xA6, OLED_CMD);    /* 設置顯示方式;bit0:1,反相顯示;0,正常顯示 */
    oled_write_Byte(0xAF, OLED_CMD);    /* 開啓顯示 */
    /* 打開oled */
    oled_on();
    oled_clear();
}

該函數的結構比較簡單,開始是對GPIO口的初始化,這裏我們用了宏定義來決定要設置的IO口,後面的就是一些初始化序列了,我們按照廠家提供的資料來做就可以。值得注意一點的是,因為OLED是無背光的,在初始化之後,我們把顯存都清空了,所以我們在屏幕上是看不到任何內容的,就像沒通電一樣,不要以為這就是初始化失敗,要寫入數據模塊才會顯示的。
接着,要介紹的是oled_refresh_gram更新顯存到OLED函數,該函數的作用是把我們在程序中定義的二維數組g_oled_gram的值一次性刷新到OLED的顯存GRAM中。我們在oled.c文件開頭定義瞭如下一個二維數組:

/*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顯存GRAM值一一對應。在操作的時候我們只需要先修改該數組的值,然後再通過調用oled_refresh_gram函數把數組的值一次性刷新到OLED 的GRAM上即可。oled_refresh_gram函數定義如下:

/**
* @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], OLED_DATA);
        }
    }
}

oled_refresh_gram函數先設置頁地址,然後寫入列地址(也就是縱座標),然後從0開始寫入128個字節,寫滿該頁,最後循環把8頁的內容都寫入,就實現了整個從ESP32顯存到OLED顯存的拷貝。
oled_refresh_gram()函數還調用了oled_write_Byte()這個函數,也就是我們接着要介紹的函數:該函數和硬件相關,該函數定義如下:

/**
* @brief       oled 寫命令
* @param       tx_data:數據
* @param       command:命令值
* @retval      無
*/
void oled_write_Byte(unsigned char tx_data, unsigned char command)
{
    unsigned char data[2] = {command, tx_data};
    oled_write(data, sizeof(data));
}
oled_write_Byte()函數還調用oled_write()函數,其定義如下:
/**
* @brief       oled IIC寫數據
* @param       data_wr:發送的數據或者命令
* @param       size  :發送數據的大小
* @retval      0:發送成功;非0值:發送失敗
*/
esp_err_t oled_write(uint8_t* data_wr, size_t size)
{
    i2c_buf_t bufs = {
        .len = size,
        .buf = data_wr,
    };
    i2c_transfer(&oled_master, OLED_ADDR, 1, &bufs, I2C_FLAG_STOP);
    return ESP_OK;
}

oled_write()函數的處理方法,是通過調用IIC控制塊下的收發函數來發送數據和命令。先將傳入的參數統一存放到通過IIC控制塊定義的一個結構體數組中,由該數組統一管理髮送的數據和命令,最後以指針類型的方式傳遞給收發函數i2c_transfer()。關於函數i2c_transfer()的描述請參照19.3.3小節的內容。
g_oled_gram 128二維數組中的128代表列數(x座標),而8代表的是頁,每頁又包含8行,總共64行(y座標),從高到低對應行數從小到大,如表21.3.3.1所示:

表21.3.3.1 OLED_GRAM和OLED屏座標對應關係
上表中G代表OLED_GRAM,G0就表示OLED_GRAM 0。比如,我們要在x=3,y=9這個點寫入1,則可以用這個句子實現:

                                          OLED_GRAM[3][1] |= 1<<1;

一個通用的在點(x,y)置1表達式為:

                                          OLED_GRAM[x][y/8] |= 1<<(y%8);

其中x的範圍為:0~127;y的範圍為:0~63。
因此,我們可以得出接下來介紹的這個比較重要的函數:OLED畫點函數,其定義如下:

/**
* @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)         /* 畫實心點 */
    {
        OLED_GRAM[x][pos] |= temp;
    }
    else              /* 畫空點,即不顯示 */
    {
        OLED_GRAM[x][pos] &= ~temp;
    }
}

該函數有3個形參,前兩個是橫縱座標,第三個t為要寫入1還是0。該函數實現了我們在OLED模塊上任意位置畫點的功能。
前面我們知道取模方式是:從上到下,從左到右,高位在前。下面根據取模的方式來編寫顯示字符oled_show_char函數,其定義如下:

/**
* @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;
            }
        }
    }
}

該函數為字符以及字符串顯示的核心部分,函數中chr = chr - ' ';這句是要得到在字符點陣數據裏面的實際地址,因為我們的取模是從空格鍵開始的,例如oled_asc2_1206 0,代表的是空格符開始的點陣碼。在接下來的代碼,我們也是按照從上到小(先y++),從左到右(再x++)的取模方式來編寫的,先得到最高位,然後判斷是寫1還是0,畫點;接着讀第二位,如此循環,直到一個字符的點陣全部取完為止。這其中涉及到列地址和行地址的自增,根據取模方式來理解,就不難了。
oled.c的內容比較多,其他的函數請大家自行理解。

21.3.4 CMakeLists.txt文件
打開本實驗BSP下的CMakeLists.txt文件,其內容如下所示:

set(src_dirs
           IIC
           LED
           OLED
           XL9555)
set(include_dirs
           IIC
           LED
           OLED
           XL9555)
set(requires
           driver
           esp_lcd)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

上述的紅色OLED驅動以及esp_lcd依賴庫需要由開發者自行添加,以確保OLED驅動能夠順利集成到構建系統中。這一步驟是必不可少的,它確保了OLED驅動的正確性和可用性,為後續的開發工作提供了堅實的基礎。

21.3.5 實驗應用代碼
打開main/main.c文件,該文件定義了工程入口函數,名為app_main。該函數代碼如下。

i2c_obj_t i2c0_master;
i2c_obj_t i2c1_master;
/**
* @brief       程序入口
* @param       無
* @retval      無
*/
void app_main(void)
{
    uint8_t t = 0;
    led_init();                              /* 初始化LED */
    i2c0_master = iic_init(I2C_NUM_0);       /* 初始化IIC0 */
    i2c1_master = iic_init(I2C_NUM_1);       /* 初始化IIC1 */
    xl9555_init(i2c0_master);                 /* 初始化XL9555 */
    oled_init(i2c1_master);                   /* 初始化OLED */
   oled_show_string(0, 0, "ALIENTEK", 24);
   oled_show_string(0, 24, "0.96' OLED TEST", 16);
   oled_show_string(0, 40, "ATOM 2023/8/26", 12);
   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 = ' ';
        }
        vTaskDelay(500);
        LED_TOGGLE();                           /* LED閃爍 */
    }
}

在實驗章節中我們也介紹到OLED以IIC協議進行通訊,肯定要分配一個IIC端口,然而IO擴展芯片XL9555也需要分配到一個IIC端口(IIC0)。為了保證兩個設備之間不被幹擾,我們分配另一個IIC端口(IIC1)給OLED使用,如此一來避免了因端口使用衝突而帶來的影響,這也是我們在主函數中定義了兩個IIC端口的原因。
main.c主要功能就是在OLED上顯示一些實驗信息字符,然後開始從空格鍵開始不停的循環顯示ASCII字符集,並顯示該字符的ASCII值。最後LED閃爍提示程序正在運行。

21.4 下載驗證

下載代碼後,LED不停的閃爍,提示程序已經在運行了。同時OLED模塊顯示ASCII字符集等信息,如圖21.4.1所示:

圖21.4.1 OLED顯示效果
OLED顯示了三種尺寸的字符:2412(ALIENTEK)、168(0.96’ OLED TEST)和12 * 6(剩下的內容)。説明我們的實驗是成功的,實現了三種不同尺寸ASCII字符的顯示,在最後一行不停的顯示ASCII字符以及其碼值。

user avatar shumile_5f6954c414184 Avatar manongtuwei Avatar robert_yan Avatar masutaadashi Avatar
Favorites 4 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.