STM32H743 驅動 0.96彩屏OLED (一)
| 數據通訊方式 | 4-SPI |
| 屏幕尺寸 | 0.96寸 |
| 分辨率 | 160*80*3 |
| 色彩模式 | RGB888/565 |
| 顯示IC | SP5210 |
| 模塊制造商 | 臺灣YEEBO |
| 產地 | 蘇州 |
| 物理接口形式 | 25PIN0.3FPC |
| 主要引腳 | VCC VSS RES A0 CS SCL MOSI VPP FRM |
| 顯示類型 | OLED |
單片機 STM32H743
工作中的任務,給產品增加一個狀態指示屏,由于初期SPEC要求的頁面不多,復雜度也小,就決定從驅動到應用層都自己寫了。最開始使用單個數據的硬件SPI發送,軟件觸發傳輸,之后改為DMA觸發SPI,最后增加垂直同步外部中斷。
從IC手冊和設備手冊中查好接口定義,網購了那個0.3間距25PIN的FPC排線插座(各種各樣的,我喜歡前插口后鎖定板的,有同事覺得前插口前鎖定的更好)和轉2.54的轉接PCB板,按照推薦電路接好電阻電容,用直流電源給VPP外接16V供電,雖然容易誤碰電壓旋鈕燒屏,但是這個屏幕不是那種正負4.5V的供電方式,沒辦法。。。
調試用的H743 最小開發板,上面的晶振12M,與產品樣機上用的25M晶振不一樣,所以在給不同板下載測試時,要把system_stm32h7xx.c里面的
#define HSE_VALUE ((uint32_t)12000000)
#define HSE_VALUE ((uint32_t)25000000)
修改成實際使用的晶振頻率,不改的話,很大概率也能運行起來,但時不時看門狗定期重啟或者SPI傳輸不正確等現象。
而且后續的PLLM、PLLN、PLLP,以及選用的SPI所在PLL分段也要對應修改一下。
----------此處路段掉坑很多,請小心駕駛。------------
(怕踢電源,先commit一下)
配置完時鐘,繼續配置引腳。為了盡量降低CPU負載,選用硬件SPI4進行通訊。
#define IO_PE2 GPIOE, GPIO_PIN_2 /* OLED SCK (HARD) SPI4_SCL */
#define IO_PE15_O_OLED_RESET GPIOE, GPIO_PIN_3 /* OLED RESET (SOFT) */
#define IO_PE11_O_OLED_CS GPIOE, GPIO_PIN_4 /* OLED CS (SOFT) SPI4_NSS */
#define IO_PE13_O_OLED_A0 GPIOE, GPIO_PIN_5 /* OLED DATA OR PARAM SELECT (SOFT) SPI4_MISO */
#define IO_PE6 GPIOE, GPIO_PIN_6 /* OLED SDA_IN (HARD) SPI4_MOSI*/
為方便整體修改,做宏定義
#define SPI_CH2_SPI SPI4
#define SPI_CH2_AF GPIO_AF5_SPI4
#define SPI_CH2_GRP_CLK LL_AHB4_GRP1_PERIPH_GPIOE
#define SPI_CH2_PORT GPIOE
#define SPI_CH2_PINS LL_GPIO_PIN_2 | LL_GPIO_PIN_6
#define SPI_CH2_SCK_PORT GPIOE
#define SPI_CH2_SCK_PIN LL_GPIO_PIN_2
#define SPI_CH2_MOSI_PORT GPIOE
#define SPI_CH2_MOSI_PIN LL_GPIO_PIN_6
#define SPI_CH2_CLK_ENABLE LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SPI4)
(不知道發布后是否可以追加編輯,試試)
配置SPI外設
/* Enable GPIO Clock */
LL_AHB4_GRP1_EnableClock(LL_AHB4_GRP1_PERIPH_GPIOE);
/* Enable SPI Clock */
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SPI4);
/* SPI SCK GPIO pin configuration*/
{LL_GPIO_InitTypeDef io_initStructure_s;
io_initStructure_s.Pin = SPI_CH2_PINS ;
io_initStructure_s.Mode = LL_GPIO_MODE_ALTERNATE;
io_initStructure_s.Speed = LL_GPIO_SPEED_FREQ_MEDIUM;
io_initStructure_s.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
io_initStructure_s.Pull = LL_GPIO_PULL_NO;
io_initStructure_s.Alternate = LL_GPIO_AF_5;
LL_GPIO_Init(SPI_CH2_PORT , &io_initStructure_s);}
LL_SPI_Disable(SPI4);
/* Configure the SPI parameters */
{LL_SPI_InitTypeDef spi_ch_s;
spi_ch_s.TransferDirection = LL_SPI_SIMPLEX_TX;
spi_ch_s.Mode = LL_SPI_MODE_MASTER;
spi_ch_s.DataWidth = LL_SPI_DATAWIDTH_8BIT;
spi_ch_s.ClockPolarity = LL_SPI_POLARITY_HIGH;
spi_ch_s.ClockPhase = LL_SPI_PHASE_2EDGE;
spi_ch_s.NSS = LL_SPI_NSS_SOFT;
spi_ch_s.BaudRate = LL_SPI_BAUDRATEPRESCALER_DIV4; /* PLL SET 40MHz clock DIV2 for 20MHz /DIV4 for 10MHz*/
spi_ch_s.BitOrder = LL_SPI_MSB_FIRST;
spi_ch_s.CRCCalculation = LL_SPI_CRCCALCULATION_DISABLE;
LL_SPI_Init(SPI_CH2_SPI , &spi_ch_s);}
LL_SPI_SetTransferSize(SPI_CH2_SPI , 0);
LL_SPI_Enable(SPI_CH2_SPI );
LL_SPI_IsActiveFlag_TXC(SPI_CH2_SPI );
LL_SPI_StartMasterTransfer(SPI_CH2_SPI );
使能IO時鐘,SPI時鐘,IO初始化,SPI初始化。
主要注意 DataWidth = LL_SPI_DATAWIDTH_8BIT;
OLED的芯片手冊上沒有限定8BIT還是16BIT,想提高傳輸速度,自然想用16BIT寬度,但是發現有些指令僅8BIT,有些是7*8BIT,所以為了不增加麻煩,退而求其次,選了8
還有時鐘空閑極性ClockPolarity和數據沿位置ClockPhase 也要按照OLED的要求。雖然設置錯了可能也能驅動,但是在速度變化或者某些特殊數據時就會造成數據不完整,所以一定要按照芯片手冊的要求配置。
因為4線SPI的OLED一般不提供讀功能,所以SPI方式為僅發送的主機模式,也可以選雙向,區別不大。
這樣,一個由軟件觸發傳輸一次8BIT的SPI就配置好了,可以進一步調試屏幕了。
(commit)
屏幕初始化:(部分IO配置簡化表述)
io_cfgOutput(IO_PE11_O_OLED_CS);
io_cfgOutput_pull(IO_PE13_O_OLED_A0,LL_GPIO_PULL_DOWN);
io_cfgOutput(IO_PE15_O_OLED_RESET);
#define LCD_CS_CLR() io_clrOutput(IO_PE11_O_OLED_CS)
#define LCD_CS_SET() io_setOutput(IO_PE11_O_OLED_CS)
#define LCD_RST_CLR() io_clrOutput(IO_PE15_O_OLED_RESET)
#define LCD_RST_SET() io_setOutput(IO_PE15_O_OLED_RESET)
#define LCD_RS_CLR() io_clrOutput(IO_PE13_O_OLED_A0)
#define LCD_RS_SET() io_setOutput(IO_PE13_O_OLED_A0)
配置除SPI SCK、MOSI以外3個控制IO,其中A0給下拉的原因是由于它在使用中負責數據/指令選擇,瞬間的高低變化可能會引起顯示錯誤,由此給他一個下拉防止積累電荷。
按照屏幕的說明,寫數據和寫指令都是使用SPI發數據,區分點就是那個A0的高低,所以把寫指令 單獨 寫個函數:
void disp_WriteIndex(BYTE Index)
{
while(!LL_SPI_IsActiveFlag_TXC(SPI_CH2_SPI )){}
LCD_RS_CLR();
LL_SPI_TransmitData8(SPI_CH2_SPI,Index);
LL_SPI_StartMasterTransfer(SPI_CH2_SPI);
}
這個函數先檢查SPI的發送完成標記,等待其發送完成。再拉低A0線,再向SPI數據寄存器寫入要發送的數據,最后啟動傳輸。因為在傳輸后沒有繼續等待發送完成,因此再次調用SPI發送任何數據、指令之前必須先檢查發送狀態,再調整A0線高低。
BYTE device_display_initialize(BYTE channel_)
{
LCD_CS_CLR();
LCD_RST_SET();
OS_Delay(5);
LCD_RST_CLR();
OS_Delay(5);
LCD_RST_SET();
OS_Delay(10);
disp_WriteIndex(0xAEu); /* DISPLAY OFF */
disp_WriteIndex(0xACu); /* Color/Gray Mode 0-ColorMode */
disp_WriteIndex(0x00u); /* --0--for ColorMode,1 for GrayMode */
disp_WriteIndex(0xFDu); /*removed function MLA--FD ADPS--D6*/
disp_WriteIndex(0x5Bu); /* ---- */
disp_WriteIndex(0x00u); /* ---- */
disp_WriteIndex(0xD6u); /* ---- */
disp_WriteIndex(0x20u); /* Set refresh direction 2lines command*/
disp_WriteIndex(0x00u); /* Set horiz refresh*/
disp_WriteIndex(0xD4u); /* Set color format 2lines command*/
disp_WriteIndex(0x10u); /* Set 65K RGB */
disp_WriteIndex(0xA1u); /* Horizontal Mirror */
disp_WriteIndex(0xD5u); /* Set Display Clock Divide Ratio/Oscillator Frequency */
disp_WriteIndex(0x13u); /* --12--V_sync limit from 10MHzSPI 20.8ms trans_time ,0x13 the min speed */
disp_WriteIndex(0x80u); /* Set Contrast */
disp_WriteIndex(0xFFu); /* --255--Contrast Max from 0-255*/
disp_WriteIndex(0xA2u); /* Set Display Start Line */
disp_WriteIndex(0x00u); /* --00--default*/
disp_WriteIndex(0x00u); /* --00--default*/
disp_WriteIndex(0xA8u); /* Set Multiplex Ratio: */
disp_WriteIndex(0x4Fu); /* --4F--default*/
disp_WriteIndex(0xADu); /* Set iRef resist */
disp_WriteIndex(0x00u); /* --04--internal 300uA */
disp_WriteIndex(0xDDu); /* VSEGH control no use for user */
disp_WriteIndex(0x1Fu); /* ----VSEGH control no use for user */
disp_WriteIndex(0xD9u); /* Set Discharge/Pre-charge Period */
disp_WriteIndex(0x07u); /* ---- */
disp_WriteIndex(0x0Au); /* ---- */
disp_WriteIndex(0x0Du); /* ---- */
disp_WriteIndex(0x0Du); /* ---- */
disp_WriteIndex(0x0Du); /* ---- */
disp_WriteIndex(0x0Du); /* ---- */
disp_WriteIndex(0x29u); /* ---- */
disp_WriteIndex(0xA4u); /* Set Entire Display hold A5 or normal A4 */
Lcd_SetRegion(0u,0u,159u,79u);
disp_WriteIndex(0xAFu); /* DISPLAY ON */
_Delay(20);
return 1u;
}
其中設置顯示區域由于 假設 要經常調用,
所以單獨寫了一個函數:
void Lcd_SetRegion(WORD x_start,WORD y_start,WORD x_end,WORD y_end)
{
while(!LL_SPI_IsActiveFlag_TXC(SPI_CH2_SPI)){}
LCD_RS_CLR();
disp_WriteIndex(0x22u);
disp_WriteIndex(0x02u);
disp_WriteIndex((BYTE)y_start);
disp_WriteIndex((BYTE)y_end);
disp_WriteIndex(0x21u);
disp_WriteIndex(0x01u);
disp_WriteIndex((BYTE)x_start);
disp_WriteIndex((BYTE)x_end);
while(!LL_SPI_IsActiveFlag_TXC(SPI_CH2_SPI)){}
}
如果一切正常,執行到這一步,屏幕就能看到點雜亂的顏色了,之后就可以刷新顯示了。
由于不能從屏幕讀數據,即便能讀也比讀內存慢,所以先開辟一塊顯存,讀寫先到顯存,再整體刷新到屏幕,此思路來自于emWin圖形框架。
WORD display_ram_wa[80*160]@ "SRAM1_section"={0};
void Refresh_Gram_Async(void)
{
WORD i;
BYTE mid_b;
SCB_CleanInvalidateDCache();
LCD_RS_SET();
for(i=0u;i<12800u;i++)
{
mid_b=(BYTE)(display_ram_wa[i]>>8);
disp_WriteData((BYTE)(display_ram_wa[i]));
disp_WriteData(mid_b);
}
}
函數中,SCB_CleanInvalidateDCache();是由于H7的芯片帶有Cache,由于顯存的數據量較大,寫入后立即使用其他方式刷新可能會讀不到正確的內容,所以強制刷新一下Cache。由于屏幕像素160*80*RGB565,所以定義了160*80大小的WORD數組;
先解釋下RGB565,常規的Windows繪圖軟件可供用戶選擇的顏色其紅綠藍色彩分量各有256檔,能組成256*256*256=16,777,216種色彩,即RGB888格式,每一個8代表8位,可存儲0x00~0xFF共256檔。所以存儲一個RGB888數據需要24位空間,當然也可以用更多的存儲空間存儲顏色,進行更細致的劃分,但是這樣就會增加所需要的內存空間或者調色板尺寸,由于一般的顯示設備,并不需要專業級的色彩細節,所以一般彩色顯示屏提供RGB888,但是對于常規的單片機,24位并不是一個常用的數據類型,16位或者32位才是更常用的,所以用32位存儲RGB888數據當然可以,浪費8位空間,這也是計算機上常規顏色會出現ARGB的原因,富裕出來的8位用來記錄透明度。但是選用32位存儲顏色不僅僅在存儲時增大了空間,還在傳輸時增加了傳輸時間。因此簡單的顯示設備中還提供一種低位寬的顏色格式,RGB565,加和后5+6+5=16,剛好一個WORD,而這種數據僅僅是忽略了各顏色數據的最后兩、三位,還原RGB888顏色時將不夠的位數尾部補0湊夠8位。
刷新顯存時只需要將每個像素點共12800個WORD發送出去,由于使用的8BIT寬度配置SPI,所以要將WORD拆分成兩塊發送。
且發送時最好按照先高位后低位的MSB順序,至于為什么后面再說,MSB就導致了顯存中的顏色數據與實際的RGB565有個區別,如下:
#define Color_RED_MSB565 (0x00F8u) /* 0xF800-MSB>0x00F8 */
實際的紅色RGB565格式 應該是0xF800,但是發送時會先發送內存中的地址較低的一位,而屏幕要求傳入的16位顏色數據順序還是RGB,所以要將真實數據前后位交換后再存入顯存,即存入的內容為WORD格式的0x00F8,這樣通過SPI逐byte發送,屏幕收到的順序是0xF8,0x00,才能正確表達位0xF800.
(commit)
將顏色或圖片數據繪制到顯存,并進行鏡像翻轉旋轉90°等操作的函數示例,再加入蒙版數據還可以進行透明疊加等動作,函數種類太多,并未來得及整理,就先放一個。
(commit -- 2024 08 26)

浙公網安備 33010602011771號