1. 简介
GD32F4系列中的EXMC外设可以用来驱动外部的储存器,如SDRAM、SRAM等等,在之前的文章中有介绍其用法。
但EXMC还有一种比较特别的玩法就是用来驱动8080并口屏;之前讲过,EXMC的工作原理就是把外部的储存器地址映射到芯片内部的一段地址中。而显示屏的驱动其实也是把数据写到显示芯片的RAM中,这个RAM本质还是储存器的一种,因此我们也可以用EXMC外设把显示芯片的RAM映射到单片机中,这样可以大大提高通讯速度。
2. 硬件设计
下面的例程是基于我之前的一个小项目,项目的介绍已经开源在立创开源社区中了,跳转链接。
开发板使用的是立创梁山派,开发板上带有一个8080并口屏接口;屏幕买的是一块IPS的并口屏,屏幕驱动芯片是ST7796U。
因为屏幕是支持触摸的,所以忽略掉I2C部分的管脚。LCD_D0~16为显示屏的16根数据管脚,LCD_CS为片选线,LCD_DC脚控制数据或命令传输,LCD_WR为写使能管脚,LCD_RD为读使能管脚,LCD_BLC为显示屏背光使能。
3. 原理
驱动屏幕使用的是EXMC的NOR Flash模式,因此使用的是EXMC的Bank0,总共256MB;Bank0又分为了4个Region,每个Region有64MB 。梁山派的这个8080并口引出的是Region3的片选线(EXMC_NE3)。
由参考手册可以查看Region3的基地址映射,从0x6C000000 开始。接下来我们要确认命令地址和数据地址,LCD_DC线是连接到EXMC_A10上的;屏幕驱动芯片的RAM是16位宽度,根据参考手册,使用16位宽度时,EXMC的HADDR[25:1]与EXMC_A[24:0]连接,另外LCD_DC高电平表示数据传输,低电平表示数据传输;综上可以得出数据地址为0x6C000800,命令地址为0x6C000000。
针对NOR Flash,EXMC提供了非常多的驱动时序,这块屏幕使用的是模式B来驱动,它的读时序和写时序如下面。
芯片读过程,EXMC先拉低片选(EXMC_NEx);拉低输出使能(EXMC_NOE),其实就是读使能;拉高写使能(EXMC_NWE);在经过地址建立时间后拉高地址有效线(我们没有用到);再经过数据建立时间后就可以读取到稳定的数据了。
芯片写过程差不多, 先拉低片选(EXMC_NEx);拉高输出使能(EXMC_NOE);拉低写使能(EXMC_NWE);在经过地址建立时间后拉高地址有效线(我们没有用到);经过地址建立时间后EXMC开始通过数据线(EXMC_Dx)向芯片输出数据;经过数据建立时间后拉高写使能;再经过一个HCLK时钟周期再恢复管脚状态。
对于模式B的时序,我们要设置其地址建立时间(WASET)和数据建立时间(WDSET),这两个时间可以在屏幕驱动芯片的数据手册中找到。
上面的和就是地址建立时间和数据建立时间,最小值分别为0ns和10ns。
4. 代码
先看GPIO的初始化。代码有省略,因为都大差不差。主要注意的是设置复用模式,EXMC的复用组为GPIO_AF_12 ,EXMC的所有管脚最好上拉,速度设置为最高速。
cpp
static void ST7796_GPIOInit(void)
{
/* config GPIO */
rcu_periph_clock_enable(ST7796_D0_CLK);
...
gpio_mode_set(ST7796_D0_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, ST7796_D0_PIN);
gpio_output_options_set(ST7796_D0_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, ST7796_D0_PIN);
gpio_af_set(ST7796_D0_PORT, EXMC_AF, ST7796_D0_PIN);
...
}
比较关键的是EXMC的初始化。
cpp
static void ST7796_EXMCInit(void)
{
exmc_norsram_parameter_struct exmc_init_struct = {0};
exmc_norsram_timing_parameter_struct exmc_timing_struct = {0};
/* config FMC clock */
rcu_periph_clock_enable(EXMC_CLK);
exmc_timing_struct.asyn_access_mode = EXMC_ACCESS_MODE_B;
exmc_timing_struct.asyn_address_setuptime = 5; // 5 / 240MHz = 20ns
exmc_timing_struct.asyn_data_setuptime = 4; // (4 + 1) / 240MHz = 20ns
exmc_init_struct.address_data_mux = DISABLE;
exmc_init_struct.asyn_wait = DISABLE;
exmc_init_struct.burst_mode = DISABLE;
exmc_init_struct.databus_width = EXMC_NOR_DATABUS_WIDTH_16B;
exmc_init_struct.extended_mode = DISABLE;
exmc_init_struct.memory_type = EXMC_MEMORY_TYPE_NOR;
exmc_init_struct.memory_write = ENABLE;
exmc_init_struct.norsram_region = EXMC_BANK0_NORSRAM_REGION3;
exmc_init_struct.nwait_signal = DISABLE;
exmc_init_struct.wrap_burst_mode = DISABLE;
exmc_init_struct.write_mode = EXMC_ASYN_WRITE;
exmc_init_struct.read_write_timing = &exmc_timing_struct;
exmc_init_struct.write_timing = &exmc_timing_struct;
exmc_norsram_init(&exmc_init_struct);
exmc_norsram_enable(EXMC_BANK0_REGIONx);
}
先初始化exmc_norsram_timing_parameter_struct结构体配置时序;asyn_access_mode成员设置异步模式,这里为模式B;asyn_address_setuptime设置地址建立时间,这里设置为5,即20ns;asyn_data_setuptime设置数据建立时间,这里设置为4,也是20ns;这两个时间的话可以适当设得大点,因为硬件的体质是有差异的,设得离理论值很接近的话有可能会通讯失败。
再初始化exmc_norsram_parameter_struct结构体配置EXMC; address_data_mux设置地址线复用,这里不复用;aync_wait设置异步等待,这里不等待;burst_mode设置突发模式,这里不使用;databus_width设置数据宽度,这里为16位;extended_mode设置扩展模式,这里不使用;memory_type设置储存器类型,这里为NOR Flash;memory_write设置储存器写,这里使能;norsram_region设置使用的Region;这里设置为Region3;nwait_signal设置NWAIT信号,这里不使用;wrap_burst_mode设置突发模式中的数据包裹,这里不使用;write_mode设置写模式,这里使用异步模式;read_write_timing和write_timing填之前初始化好的时序结构体。
最后初始化并使能EXMC外设就可以使用了。对屏幕芯片读和写操作只需要像操作指针一样即可。
cpp
#define EXMC_Addr_ST7796_CMD ((uint32_t)0x6C000000)
#define EXMC_Addr_ST7796_DATA ((uint32_t)0x6C000800) // 0x6C000000 | (1 << (1 + Ax))
__inline void ST7796_WriteCmd(uint16_t cmd)
{
*( __IO uint16_t*)(EXMC_Addr_ST7796_CMD) = cmd;
}
__inline void ST7796_WriteData(uint16_t data)
{
*( __IO uint16_t*)(EXMC_Addr_ST7796_DATA) = data;
}
当然我们还需要对屏幕芯片寄存器进行初始化后才能,执行其他的操作,但这里就不介绍了,因为每个芯片的寄存器都不同,只需要根据芯片手册进行编写即可。