准备
屏幕
使用SPI接口的1.69寸240x280TFT彩屏.
主控
使用立创·梁山派GD32F470ZGT6.
屏幕与主控的连接
使用硬件SPI+DMA的方式刷屏。
一般我们拿到一个屏幕首先需要移植厂商提供的官方代码进行亮屏测试。这里我们就不使用DMA配置了,只用最基本Io模拟SPI或者硬件SPI的方法,先将屏幕点亮清屏或者整屏刷新颜色就行。 移植好代码后要仔细观察颜色刷新是否正确(色差),显示方法是否正确(横屏竖屏),颜色数据刷新是否正常(错,漏,闪烁)。
设计连线符合器件要求无误的情况下存在问题,就需要查看手册,通过手册发现GD32F470的SPI存在FIFO,所以发送数据的结尾需要等待数据发送完成再释放片选。否则容易导致屏幕显示出现问题(一般都是以缓冲区是否为空当作传输的判断标准,以此提高发送效率)。
屏幕的代码初始化需要根据屏幕厂商提供的初始化代码就行移植以减少开发难度。
硬件SPI初始化
需要使能硬件SPI的DMA发送功能。
void lcd_gpio_config(void)
{
spi_parameter_struct spi_init_struct;
rcu_periph_clock_enable(RCU_LCD_SCL);
rcu_periph_clock_enable(RCU_LCD_SDA);
rcu_periph_clock_enable(RCU_LCD_CS);
rcu_periph_clock_enable(RCU_LCD_DC);
rcu_periph_clock_enable(RCU_LCD_RES);
rcu_periph_clock_enable(RCU_LCD_BLK);
rcu_periph_clock_enable(RCU_SPI_HARDWARE); // 使能SPI
/* 配置 SPI的SCK GPIO */
gpio_af_set(PORT_LCD_SCL, LINE_AF_SPI, GPIO_LCD_SCL);
gpio_mode_set(PORT_LCD_SCL, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_LCD_SCL);
gpio_output_options_set(PORT_LCD_SCL, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SCL);
gpio_bit_set(PORT_LCD_SCL,GPIO_LCD_SCL);
/* 配置 SPI的MOSI GPIO */
gpio_af_set(PORT_LCD_SDA, LINE_AF_SPI, GPIO_LCD_SDA);
gpio_mode_set(PORT_LCD_SDA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_LCD_SDA);
gpio_output_options_set(PORT_LCD_SDA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SDA);
gpio_bit_set(PORT_LCD_SDA, GPIO_LCD_SDA);
/* 配置DC */
gpio_mode_set(PORT_LCD_DC,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_LCD_DC);
gpio_output_options_set(PORT_LCD_DC,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,GPIO_LCD_DC);
gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET);
/* 配置RES */
gpio_mode_set(PORT_LCD_RES,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_LCD_RES);
gpio_output_options_set(PORT_LCD_RES,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,GPIO_LCD_RES);
gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET);
/* 配置BLK */
gpio_mode_set(PORT_LCD_BLK, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_BLK);
gpio_output_options_set(PORT_LCD_BLK, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_BLK);
gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, SET);
/* 配置CS */
gpio_mode_set(PORT_LCD_CS,GPIO_MODE_OUTPUT,GPIO_PUPD_NONE,GPIO_LCD_CS);
gpio_output_options_set(PORT_LCD_CS,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,GPIO_LCD_CS);
gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET);
// 配置 SPI 参数
spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX;// 传输模式全双工
spi_init_struct.device_mode = SPI_MASTER; // 配置为主机
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; // 8位数据
spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE; // 极性高相位2
spi_init_struct.nss = SPI_NSS_SOFT; // 软件cs
spi_init_struct.prescale = SPI_PSC_2; // 2分频
spi_init_struct.endian = SPI_ENDIAN_MSB; // 高位在前
spi_init(PORT_SPI, &spi_init_struct);
//使能DMA发送
spi_dma_enable(PORT_SPI,SPI_DMA_TRANSMIT);
// 使能 SPI
spi_enable(PORT_SPI);
//初始化DMA
dma_spi_init();
}
配置DMA
使用DMA的通用流程都是先配置外设对应的DMA及通道,再配置自动或者软件触发DMA搬运方向。不过,我们需要根据屏幕的像素确定要传输的数据量,DMA的最大数据传输量为65535。而我使用的是1.69寸屏幕,像素为240x280,即全部像素为240 * 280 = 67200,已经超过了最大DMA传输量。所以将全部像素数据分两次进行传输,即67200 / 2 = 33600。但是,我的数据宽度设置为了8位,即一次传输8位的数据。而我使用的1.69寸屏,是TFT彩屏,一个像素点需要16位的彩色数据。DMA设置为8位传输,而屏幕一个像素是16位的数据,故实际的传输数据量为全部像素大小*2!即67200 * 2=134400。所以我们想要显示一帧图像,需要传输4次!
DMA通道的说明
我硬件SPI使用的是SPI0(需要根据使用引脚确定的),根据数据手册中关于DMA的请求表可以知道,要想使用DMA搬运硬件SPI0的数据,只可以通过DMA1的通道3,或者通道5。案例中选择的是DMA1的通道3。
void dma_spi_init(void)
{
dma_single_data_parameter_struct dma_init_struct;
/* 使能DMA1时钟 */
rcu_periph_clock_enable(RCU_DMA1);
/* 初始化DMA1的通道3 */
dma_deinit(DMA1, DMA_CH3);
dma_init_struct.direction = DMA_MEMORY_TO_PERIPH; //内存往外设
dma_init_struct.memory0_addr = (uint32_t)0; //内存地址
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //开启内存地址增量
dma_init_struct.periph_memory_width = DMA_MEMORY_WIDTH_8BIT; //内存数据宽度
dma_init_struct.number = LCD_W * LCD_H / 2; //数据量 240*280/2 = 67200/2 = 33600
dma_init_struct.periph_addr = (uint32_t)&SPI_DATA(PORT_SPI) ; //外设地址
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //关闭外设地址增量
dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH; //优先级高
dma_single_data_mode_init(DMA1, DMA_CH3, &dma_init_struct); //将以上参数填入DMA1的通道3
// 禁用DMA循环模式
dma_circulation_disable(DMA1, DMA_CH3);
// DMA通道外设选择 011
dma_channel_subperipheral_select(DMA1, DMA_CH3, DMA_SUBPERI3);
// 启用DMA1传输完成中断
dma_interrupt_enable(DMA1, DMA_CH3, DMA_CHXCTL_FTFIE);
// 配置中断优先级(必须为最高)
nvic_irq_enable(DMA1_Channel3_IRQn, 0, 0);
// 失能DMA1的通道3
dma_channel_disable(DMA1, DMA_CH3);
}
开启显示
DMA初始化配置后,需要再次传输的快捷操作为:清除标志位,设置传输量,设置传输地址,开始传输。
关于清除全部中断标志位的解释
因为我使用到的硬件SPI的是DMA1的通道3,实际上只需要往DMA_INTC0寄存器写入( 0x2f << 22)即可。我为了方便,直接将整个寄存器可以设置的地方全部置1了。
关于传输地址的说明
一个全屏缓存需要240 * 280 * 2=134400字节,我们可以先将缓存内的数据修改为需要显示的内容,在统一更新在屏幕里面了。最简单的方法是定义一个134400大小的数组;
uint16_t Show_Gram[ 134400 ];
但是我们的梁山派内部RAM有限,需要留给其他需要高速访问的RAM变量使用。所以这里我们使用梁山派上外置的SDRAM(32MB)。
//初始化SDRAM设备0内存(具体初始化代码请见文章下面的工程)
exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0); //sram初始化
定义一个使用SDRAM内存的LCD显示缓冲数组:
为了方便后续的DMA传输,这里定义的SRAM地址一定要4字节对齐( __align(32) )。
//LCD显示缓冲区
__align(32) uint16_t Show_Gram[LCD_RAM_NUMBER] __attribute__((at(SDRAM_DEVICE0_ADDR)));
完整的配置请看以下代码:
//开始显示
void LCD_Show_Gram(void)
{
//设置标志位为未显示完成状态
show_over_flag=1;
//设置显示范围
LCD_Address_Set(0,0,LCD_W-1,LCD_H-1);
LCD_CS_Clr();
//清除全部中断标志位(至少清除通道3的全部中断标志位)
DMA_INTC0(DMA1) = 0xfffffff;
//设置传输数据大小
DMA_CHCNT(DMA1, DMA_CH3) = 33600;
//设置传输地址
DMA_CH3M0ADDR(DMA1) = (uint32_t)Show_Gram;
//开始传输
DMA_CHCTL(DMA1, DMA_CH3) |= DMA_CHXCTL_CHEN;
}
//屏幕数据DMA搬运完成中断
void DMA1_Channel3_IRQHandler(void)
{
//搬运次数 (必须设置为静态)
static uint8_t Show_Number=0;
//全屏幕需要搬运4次
if((++Show_Number) < 4)
{
//清除全部DMA1中断标志位
DMA_INTC0(DMA1) = 0xfffffff;
//重新填充要搬运的数据量
DMA_CHCNT(DMA1, DMA_CH3) = 33600;
//内存搬运地址
DMA_CH3M0ADDR(DMA1) = (uint32_t)Show_Gram+33600*Show_Number;
//开启搬运
DMA_CHCTL(DMA1, DMA_CH3) |= DMA_CHXCTL_CHEN;
}
else //如果4次全部搬运完成
{
//清除DMA搬运完成中断标志位
dma_interrupt_flag_clear(DMA1, DMA_CH3, DMA_INT_FLAG_FTF);
//等待SPI发送完毕
while(SPI_STAT(PORT_SPI) & SPI_STAT_TRANS);
//SPI片选拉高
LCD_CS_Set();
//搬运次数清零
Show_Number=0;
//一帧搬运完成标志位
show_over_flag=0;
}
}
还开启了一个定时器作为屏幕刷新。这样就不需要手动去开启屏幕内容更新了。
//50ms周期
void Lcd_Show_Time_config(void)
{
timer_parameter_struct timer_initpara;
rcu_periph_clock_enable(RCU_TIMER3);
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);
timer_deinit(TIMER3); // 定时器复位
timer_initpara.prescaler = 200 - 1; // 预分频
timer_initpara.alignedmode = TIMER_COUNTER_EDGE; // 对齐模式
timer_initpara.counterdirection = TIMER_COUNTER_UP; // 计数方向
timer_initpara.period = 50000 -1; // 周期
timer_initpara.clockdivision = TIMER_CKDIV_DIV1; // 时钟分频
timer_initpara.repetitioncounter = 0; // 重复计数器
timer_init(TIMER3,&timer_initpara);
timer_master_output_trigger_source_select(TIMER3,TIMER_TRI_OUT_SRC_UPDATE);
timer_interrupt_enable(TIMER3,TIMER_INT_UP); // 中断使能
nvic_irq_enable(TIMER3_IRQn, 0, 1); // 设置中断优先级
timer_enable(TIMER3);
}
//显示帧刷新中断 50ms
void TIMER3_IRQHandler(void)
{
timer_interrupt_flag_clear(TIMER3, TIMER_INT_FLAG_UP);
//如果屏幕显示数据DMA搬运完成
if(show_update_flag)
{
//清除完成标志
show_update_flag=0;
//更新显示
LCD_Show_Gram();
}
}
LCD快速刷新完成。
验证部分
在main.c中编写如下:
#include "gd32f4xx.h"
#include "systick.h"
#include "bsp_led.h"
#include "lcdinit.h"
#include "lcdgui.h"
#include "bsp_usart.h"
#include "stdio.h"
#include "string.h"
uint16_t color_buff[7] = {WHITE,BLACK,BLUE,RED,GREEN,YELLOW,GRAY};
int main(void)
{
int i=0;
Subscribe_message_struct data;
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
//滴答定时器初始化 1us
systick_config();
//sram初始化
exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);
//lcd初始化
LCD_Init();
Lcd_Gram_Fill(BLACK);
LCD_Show_Gram();
//等待一帧数据搬运完成
while(get_show_over_flag());
//开启定时器固定刷屏
Lcd_Show_Time_config();
//串口0初始化(调试)
usart_gpio_config( 115200U );
printf("start\r\n");
while(1)
{
// LCD_Fill(0,0,280,240,color_buff[i]);
//定时器循环固定数据刷屏
while(get_show_over_flag());
set_show_update_flag(1);
while(get_show_update_flag());
Lcd_Gram_Fill(color_buff[i]);
i = ( i + 1 ) % 7;
delay_1ms(500);
}
}
测试单纯硬件SPI与硬件SPI+DMA的刷屏速度:
工程文件:
飞书链接:https://lceda001.feishu.cn/docx/JQoedFOW0o692uxgLEgc6q7Znuh?from=from_copylink 密码:3GKU