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)