LoRa網關項目——OLED(SSD1306)開發(一)
#前言
最近在做一個LoRa物聯網網關的項目,網關的作用主要是管理連接的LoRa傳感器終端,將傳感數據通過協議轉換向上轉發到Internet,當然,也要處理下行的數據。
使用到的LoRa射頻芯片是SX1278,MCU為STM32F103RCT6,連接Internet用的是ESP8266+AT,且移植了FreeRTOS(單純是為了學習),開發環境是STM32CubeMX+Keil 5。由於之前沒負責過整個系統的開發,所以開此貼記錄一下開發過程,由於本人上學以來語文一直不好,所以文筆正在努力進步中,如果此文章有您覺得我說的不明白的地方,可以發送郵件到wanglu082@yeah.net,或者在文章下方評論,我看到會盡快回復您,多謝諒解!
為了方便查看網關的狀態,本系統加入了一個0.96‘OLED模塊來顯示一些調試信息。
一. 對SSD1306驅動改進
1.1 初始化函數改進
拿SSD1306的初始化來說,OLED模塊廠商給的驅動是使用MCU發送多次命令對SSD1306進行配置,這就要啟動多次IIC通訊,但實際上,通過DataSheet中給出的實例通信時序,SSD1306是支持一次發送多次命令/數據的:

所以我們可以將初始化的命令序列組成一個數組(1Byte 命令、1Byte數據、1Byte 命令、1Byte數據……),通過一次IIC通信發送給SSD1306。改進后 OLED_Init()
函數如下:
void OLED_Init(void) {
/* GPIO的初始化在MX_GPIO_Init()中進行 */
/* 初始化命令數組必須定義為 全局變量 或 局部靜態變量,
若定義為局部變量,則可能 OLED_Init 執行結束,DMA沒有傳輸完成 */
static u8 OledInitCmd[29] =
{0xAE,0x00,0x10,0x40,0xB0,0x81,0xFF,0xA1,0xA6,0xA8,\
0x3F,0xC8,0xD3,0x00,0xD5,0x80,0xD8,0x05,0xD9,0xF1,\
0xDA,0x12,0xDB,0x30,0x8D,0x14,0xAF,0x20,0x00
};
u16 OledInitCmdLength = sizeof(OledInitCmd);
HAL_I2C_Mem_Write_DMA(&hi2c1, OLED_IIC_ADDR, OLED_CMD, I2C_MEMADD_SIZE_8BIT, OledInitCmd, OledInitCmdLength);
}
這里為什么用 HAL_I2C_Mem_Write_DMA()
而不是 HAL_I2C_Master_Transmit_DMA()
呢?
單看函數的描述很難分別其區別,通過對比這兩個函數的輸入參數的不同,HAL_I2C_Mem_Write_DMA()
多了兩個形參:MemAddress 、MemAddSize 。其實看看源碼也就明白了,HAL_I2C_Mem_Write_DMA()
會在發送完Salve Address后在發送8/16 bit的 MemAddress ,然后才是n byte的 數據。
那么為什么SSD1306要發送 MemAddress 呢?再看通訊時序圖,在控制位的前面有2bit的Co和D/C#,先說D/C#用於標志該字節的數據是命令(Command)還是數據(Data),對應到SSD1306就是該字節是一條控制命令還是對GDDRAM的修改,Co是非常重要的1個控制位,Co為0表示接下來的每個字節都是數據,這個數據不是與命令相區別的數據,而是代表以下的每個字節都不含Co和D/C# 位,默認與前面相同。

正因為有這種機制,才需要調用HAL_I2C_Mem_Write_DMA()
,使在發送SlaveAddr后能發送一個字節的控制命令(0x00或0x40)
-
0x00 ;Co 和 D/C# 均為0,表示接下來都是純命令,不需要再攜帶Co 和 D/C#。
-
0x40;Co 為0, D/C# 為1,表示接下來都是純數據,對GDDRAM的修改,也不需要再攜帶Co 和 D/C#。
1.2 顯示函數重寫
拿顯示一個point來說,SSD1306的驅動中使用如下的流程:
- 定位需要操作的page
- 對該page的特定位進行操作
這兩步操作是兩次IIC通訊過程,看似沒什么問題,但是我們的顯示器通常不是一直顯示一個頁面的。如配合按鍵實現的多級菜單例程中,我最初是使用先對局部寫0,再寫入數據的方法來實現。這樣也能做,就是延遲有點大。
在生活中,其實顯示器都會以一定的頻率在不斷的刷新,顯示顯存中的內容。如果還是使用官方實例中的方式,那么在While循環中實現刷新就必須要保存當前顯示的信息,而且整個IIC通訊過程還必須要CPU參與,極大的降低了CPU的利用率,這時候就可以讓DMA出場了。
DMA是數據的搬運工,它獨立於CPU工作,只需要在開始傳送和傳送結束時通知CPU,CPU就有工夫去干大事。那怎么才能讓DMA參與進來呢?畢竟它只能干搬數據的活,通過DataSheet中以下的文字描述可以了解到,SSD1306可以用過IIC通信向其內部GDDRAM進行操作(內部RAM結構講解:STM32使用OLED模塊(SSD1306):OLED_DrawBMP()),只需要將IIC的 D/C 位置 1。

那我們是不是就能一次性將128*64 bit的GDDRAM全部寫入,在STM32-Flash中定義一個8*128的數組(表示8個page,每個page有128個column),每次要顯示圖片、數字等的時候直接寫入這個STM32-Flash,再定時將這個數組的元素一次性寫入SSD1306的GDDRAM,DMA就負責這個傳輸過程。
然而,SSD1306內部GDDRAM的讀寫指針變化的方式只能在一個page中增加column,不能自動的切換page。

這時我們就要將尋址的模式改位能自動切換到下一個page的模式,SSD1306給我們提供了這種模式的修改方法。
只需要發送20h命令,在修改成00即可,這就是為什么你發現我上節的初始化命令后面跟了個 0x20 0x00.


最終實現的刷新函數如下,可以使用定時器實現固定頻率刷新,也可以在IIC的發送完成標志BTF的回調函數中自動調用,就能實現自動刷新。
void OLED_Refreash(void)
{
/* 里面會進行回調函數的具體賦值 */
HAL_I2C_Mem_Write_DMA(&hi2c1, OLED_IIC_ADDR, OLED_DATA, I2C_MEMADD_SIZE_8BIT, *OledFrameBuf, OLED_FRAMEBUF_LEN);
}
二、理順IIC事件回調函數
在HAL_I2C_Mem_Write_DMA
中會對各類標志的回調函數進行注冊,這里我感覺庫文件寫的好像有點問題,它對hi2c->hdmatx進行傳輸完成的回調函數注冊,但是賦值的I2C_DMAXferCplt()
中卻都是接收完成相關的弱定義函數。

——————————————————————————————————————————————————

那我覺得發送完成的回調函數應該叫HAL_I2C_MemTxCpltCallback()
,果不其然,搜了一下,還真有這個函數,它被I2C_MasterTransmit_BTF()
和I2C_MasterTransmit_TXE()
調用,這里又很混亂了,即然上面把Mem_Write與Master_Transmit分開,那這里為啥又混起來了呢?感覺HAL庫整的有一點亂。
問題又來了,BTF和TXE這兩個標志有什么區別?找到我們的STM32官方手冊,BTF和TXE分別是 I2C_SR1 寄存器的 bit2 和 bit7 ,然而手冊上寫的解釋我根本沒看懂,寄存器為空和發送完字節都是什么意思?不懂。那就去看看源碼吧,說不定能找到頭緒。
先看I2C_MasterTransmit_TXE()
,下面截取了幾個重要的片段,可以分析出TXE標志是每發送一個字節就產生,它控制着每個字節的發送過程。


而I2C_MasterTransmit_BTF()
則是對一次IIC通訊過程的結束進行處理:

到這里需要梳理一下,IIC一次傳輸完成后(也就是寫入全部的GDDRAM后)會置 BTF 標志為1,此時會執行I2C_MasterTransmit_BTF()
,此函數完成標志位清除、狀態改寫的步驟后,會執行一個HAL_I2C_MemTxCpltCallback()
,這是一個弱定義的函數,用戶可以自行實現。而我們要在一次GDDRAM傳輸完成后馬上開啟下一次傳輸實現自動刷新的目的,所以需要在HAL_I2C_MemTxCpltCallback()
中再次調用OLED_Refreash()
。
但是其實,我們還忘了,STM32並不是默認開啟 IIC 各類事件的中斷捕獲,需要在STM32CubeMX中開啟IIC的事件中斷。
