代码见:https://github.com/qinyunti/py32f403-nes.git
一.前言
前面我们已经完成了平台很多的基础功能,现在来实现TFT驱动,毕竟实现游戏机最重要的就是显示部分。
我这里屏幕规格是240x320,驱动是ST7789V,SPI接口。
二.驱动
新增以下文件
St7789.c/h完全可移植无需任何修改,st7789_itf.c/h是st7789驱动的实例实现,需要实现IO和SPI接口等,对外提供接口。St7789_test.c提供刷屏测试。
2.1引脚
PC0-DC
PC1-RESET
PA8-背光
PB15-MOSI AF3
PB14-MISO AF3
PB13-SCK AF3
PB12-NSS 使用IO方式控制
Spi初始化见spi.c
/* PB12 NSS1 LCD
* PB13 SCK AF3 SPI2 APB1
* PB14 MISO AF3
* PB15 MOSI AF3
*/
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_SPI2_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
GPIO_InitStruct.Pin = GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF3_SPI1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
SPI_HandleTypeDef hspi;
hspi.Instance = SPI2;
uint32_t clk = HAL_RCC_GetPCLK1Freq();
uint32_t div = clk/baud;
if(div<=2){
hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
}else if(div<=4){
hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
}else if(div<=8){
hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
}else if(div<=16){
hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
}else if(div<=32){
hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32;
}else if(div<=64){
hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64;
}else if(div<=128){
hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128;
}else{
hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
}
if(mode & 0x01){
hspi.Init.CLKPhase = SPI_PHASE_2EDGE;
}else{
hspi.Init.CLKPhase = SPI_PHASE_1EDGE;
}
if(mode & 0x02){
hspi.Init.CLKPolarity = SPI_POLARITY_HIGH;
}else{
hspi.Init.CLKPolarity = SPI_POLARITY_LOW;
}
hspi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi.Init.CRCPolynomial = 0;
hspi.Init.DataSize = SPI_DATASIZE_8BIT;
hspi.Init.Direction = SPI_DIRECTION_2LINES;
hspi.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi.Init.Mode = SPI_MODE_MASTER;
HAL_SPI_Init(&hspi);
LL_SPI_Enable(SPI2);
Spi传输见spi_trans暂时使用查询方式,后面再优化为DMA方式。
Io初始化lcd.c中
void lcd_bk_ctl(int level)
{
if(level){
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
}else{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
}
}
void lcd_reset_ctl(int level)
{
if(level){
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET);
}else{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET);
}
}
void lcd_dc_ctl(int level)
{
if(level){
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET);
}else{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET);
}
}
void lcd_init(void)
{
/* DC PC0
* RESET PC1
* BK PA8
*/
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET);
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
}
2.2 st7789数据结构
初始化结构
/**
* \struct st7789_cmd_st
* 命令结构体
*/
typedef struct
{
uint8_t cmd; /**< 命令 */
uint8_t data[5]; /**< 参数,最多5个参数 */
uint8_t datalen; /**< 参数长度 */
uint16_t delay; /**< 延时时间 */
} st7789_cmd_st;
写命令,写数接口
/**
* \fn st7789_write_cmd
* 写命令
* \param[in] dev \ref st7789_dev_st
* \param[in] cmd 命令字节
* \retval 0 成功
* \retval 其他值 失败
*/
static int st7789_write_cmd(st7789_dev_st* dev,uint8_t cmd)
{
uint8_t tmp;
#if ST7789_CHECK_PARAM
if(dev == (st7789_dev_st*)0)
{
return -1;
}
if(dev->set_dcx == (st7789_set_dcx_pf)0)
{
return -1;
}
if(dev->write == (st7789_spi_write_pf)0)
{
return -1;
}
#endif
tmp = cmd;
dev->enable(1);
dev->set_dcx(0);
dev->write(&tmp,1);
dev->enable(0);
return 0;
}
/**
* \fn st7789_write_data
* 写数据
* \param[in] dev \ref st7789_dev_st
* \param[in] data 待写入数据
* \param[in] len 待写入数据长度
* \retval 0 成功
* \retval 其他值 失败
*/
static int st7789_write_data(st7789_dev_st* dev,uint8_t* data, uint32_t len)
{
#if ST7789_CHECK_PARAM
if(dev == (st7789_dev_st*)0)
{
return -1;
}
if(dev->set_dcx == (st7789_set_dcx_pf)0)
{
return -1;
}
if(dev->write == (st7789_spi_write_pf)0)
{
return -1;
}
#endif
dev->enable(1);
dev->set_dcx(1);
dev->write(data,len);
dev->enable(0);
return 0;
}
2.3st7789初始化
st7789.c中
s_st7789_cmd_init_list中是初始化序列定义
st7789_init时执行初始化序列
具体寄存器含义参考规格书,这里不再赘述
static st7789_cmd_st s_st7789_cmd_init_list[]=
{
{ST7789_CMD_SLPOUT,{0 },0,120}, /**< SLPOUT (11h): Sleep Out */
{ST7789_CMD_MADCTL,{0 },1,0}, /**< MADCTL (36h): Memory Data Access Control */
{ST7789_CMD_COLMOD,{0x55},1,0}, /**< 16bit/pixel 65K */
//{ST7789_CMD_PORCTRL,{0x0C,0x0C,0x00,0x33,0x33},5,0}, /**< 默认值 */
//{ST7789_CMD_GCTRL,{0x35},1,0}, /**< 默认值 */
{ST7789_CMD_VCOMS,{0x28},1,0}, /**< 0x28(1.1V) 默认值是0x20(0.9) */
{ST7789_CMD_INVON,{0x00},0,0},
//{ST7789_CMD_LCMCTRL,{0x2C},1,0}, /**< 默认值 */
//{ST7789_CMD_VDVVRHEN,{0x01,0xFF},2,0}, /**< 默认值 */
//{ST7789_CMD_VRHS,{0x0B},1,0}, /**< 默认值 */
//{ST7789_CMD_VDVS,{0x20},1,0}, /**< 默认值 */
//{ST7789_CMD_FRCTRL2,{0x0F},1,0}, /**< 默认值 (60Hz帧率) */
//{ST7789_CMD_PWCTRL1,{0xA4,0xA1},2,0}, /**< 默认值 */
//{ST7789_CMD_PVGAMCTRL,{0xD0,0xD1,0x08,0x0F,0x11,0x2A,0x36,0x55,0x44,0x3A,0x0B,0x06,0x11,0x20},2,0},
//{ST7789_CMD_NVGAMCTRL,{0xD0,0x02,0x07,0x0A,0x0B,0x18,0x34,0x43,0x4A,0x2B,0x1B,0x1C,0x22,0x1F},2,0},
//{0xE4,{0x1D,0,0},3,0},
//{0x21,{0,0,0},0,0},
//{ST7789_CMD_NORON, {0,0},0,0}, /**< NORON (13h): Normal Display Mode On */
{ST7789_CMD_DISPON,{0},0,0}, /**< DISPON (29h): Display On */
};
/**
* \fn st7789_init
* 初始化
* \param[in] dev \ref st7789_dev_st
* \retval 0 成功
* \retval 其他值 失败
*/
int st7789_init(st7789_dev_st* dev)
{
#if ST7789_CHECK_PARAM
if(dev == (st7789_dev_st*)0)
{
return -1;
}
#endif
if(dev->init != 0)
{
dev->init();
}
dev->set_reset(1);
dev->delay(10);
dev->set_reset(0);
dev->delay(10);
dev->set_reset(1);
dev->delay(120);
if(dev->buffer != 0){
memset(dev->buffer,0,sizeof(ST7789_HSIZE*ST7789_VSIZE*2));
}
/* 初始化序列 */
for(uint32_t i=0; i<sizeof(s_st7789_cmd_init_list)/sizeof(s_st7789_cmd_init_list[0]); i++)
{
st7789_write_cmd(dev, s_st7789_cmd_init_list.cmd);
if(s_st7789_cmd_init_list.datalen > 0)
{
st7789_write_data(dev, s_st7789_cmd_init_list.data,s_st7789_cmd_init_list.datalen);
if(s_st7789_cmd_init_list.delay > 0)
{
dev->delay(s_st7789_cmd_init_list.delay);
}
}
}
return 0;
}
2.4 写显存
设置窗口
/**
* \fn st7789_set_windows
* 设置窗口范围(行列地址)
* \param[in] dev \ref st7789_dev_st
* \param[in] data 待写入数据
* \param[in] len 待写入数据长度
* \retval 0 成功
* \retval 其他值 失败
*/
static int st7789_set_windows(st7789_dev_st* dev, uint16_t x0, uint16_t x1, uint16_t y0, uint16_t y1)
{
uint8_t data[4];
st7789_write_cmd(dev, ST7789_CMD_CASET);
data[0] = (x0>>8) & 0xFF; /* 列开始地址 大端 */
data[1] = x0 & 0xFF;
data[2] = (x1>>8) & 0xFF; /* 列结束地址 大端 */
data[3] = x1 & 0xFF;
st7789_write_data(dev, data, 4);
st7789_write_cmd(dev, ST7789_CMD_RASET);
data[0] = (y0>>8) & 0xFF; /* 行开始地址 大端 */
data[1] = y0 & 0xFF;
data[2] = (y1>>8) & 0xFF; /* 行结束地址 大端 */
data[3] = y1 & 0xFF;
st7789_write_data(dev, data, 4);
return 0;
}
写显存数据
/**
* \fn st7789_sync
* 现存写入st7789
* \param[in] dev \ref st7789_dev_st
* \paran[in] x0 列开始地址
* \paran[in] x1 列结束地址
* \paran[in] y0 行开始地址
* \paran[in] y1 行结束地址
* \paran[in] buffer 待写入数据
* \paran[in] len 待写入数据长度
* \retval 0 成功
* \retval 其他值 失败
*/
int st7789_sync(st7789_dev_st* dev, uint16_t x0, uint16_t x1, uint16_t y0, uint16_t y1, uint16_t* buffer, uint32_t len)
{
(void)dev;
st7789_set_windows(dev, x0, x1, y0, y1);
st7789_write_cmd(dev,ST7789_CMD_RAMWR);
st7789_write_data(dev, (uint8_t*)buffer, len);
return 0;
}
三.接口适配
St7789_itf.c中以下实例
/******************************************************************************
* 以下是ST7789设备实例
*
******************************************************************************/
/* 设备实例 */
static st7789_dev_st s_st7789_itf_dev =
{
.set_dcx = port_st7789_set_dcx,
.set_reset = port_st7789_set_reset,
.write = port_st7789_spi_write,
.enable = port_st7789_spi_enable,
.delay = port_st7789_delay_ms,
.init = port_st7789_init,
.deinit = port_st7789_deinit,
.buffer = (uint16_t*)0,
};
适配上述接口
static void port_st7789_set_dcx(uint8_t val)
{
if(val){
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET);
}else{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET);
}
}
static void port_st7789_set_bk(uint8_t val)
{
if(val){
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
}else{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
}
}
static void port_st7789_set_reset(uint8_t val)
{
if(val){
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET);
}else{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET);
}
}
static void port_st7789_spi_write(uint8_t* buffer, uint32_t len)
{
spi_trans(1,0,buffer,0,len,1);
}
static void port_st7789_spi_enable(uint8_t val)
{
(void)val;
}
static void port_st7789_delay_ms(uint32_t t)
{
vTaskDelay((t*1000/configTICK_RATE_HZ));
}
static void port_st7789_init(void)
{
/* DC PC0
* RESET PC1
* BK PA8
*/
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET);
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
port_st7789_set_bk(0);
port_st7789_set_dcx(1);
port_st7789_set_reset(0);
vTaskDelay((100*1000/configTICK_RATE_HZ));
port_st7789_set_reset(1);
vTaskDelay((100*1000/configTICK_RATE_HZ));
port_st7789_set_bk(1);
}
static void port_st7789_deinit(void)
{
}
对外提供操作接口
/******************************************************************************
* 以下是对外操作接口
*
******************************************************************************/
/**
* \fn st7789_itf_init
* 初始化
* \retval 0 成功
* \retval 其他值 失败
*/
int st7789_itf_init(void)
{
return st7789_init(&s_st7789_itf_dev);
}
/**
* \fn st7789_itf_deinit
* 解除初始化
* \retval 0 成功
* \retval 其他值 失败
*/
int st7789_itf_deinit(void)
{
return st7789_deinit(&s_st7789_itf_dev);
}
/**
* \fn st7789_itf_sync
* 刷新显示
* \retval 0 成功
* \retval 其他值 失败
*/
int st7789_itf_sync(void)
{
return st7789_sync(&s_st7789_itf_dev, 0, ST7789_HSIZE-1, 0, ST7789_VSIZE-1, s_st7789_itf_dev.buffer, ST7789_HSIZE*ST7789_VSIZE*2);
}
/**
* \fn st7789_itf_set_pixel
* 写点
* \param[in] x x坐标位置
* \param[in] y y坐标位置
* \param[in] rgb565 颜色
*/
void st7789_itf_set_pixel(uint16_t x, uint16_t y, uint16_t rgb565)
{
//if(x >= ST7789_HSIZE)
//{
// return -1;
//}
//if(y >= ST7789_VSIZE)
//{
// return -1;
//}
s_st7789_itf_dev.buffer[y*ST7789_HSIZE + x] = (uint16_t)((rgb565>>8)&0xFF) | (uint16_t)((rgb565<<8) & 0xFF00);
}
/**
* \fn st7789_itf_set_pixel_0
* 写点
* \param[in] offset 偏移位置
* \param[in] rgb565 颜色
*/
void st7789_itf_set_pixel_0(uint32_t offset, uint16_t rgb565)
{
s_st7789_itf_dev.buffer[offset] = (uint16_t)((rgb565>>8)&0xFF) | (uint16_t)((rgb565<<8) & 0xFF00);
}
/**
* \fn st7789_itf_get_pixel
* 读点
* \param[in] x x坐标位置
* \param[in] y y坐标位置
* \return rgb565颜色
*/
uint16_t st7789_itf_get_pixel(uint16_t x, uint16_t y)
{
uint16_t color = s_st7789_itf_dev.buffer[y*ST7789_HSIZE + x];
return ((uint16_t)(color>>8) | (uint16_t)(color<<8));
}
/**
* \fn st7789_itf_fill_direct
* 直接填充区域
* \param[in] x x开始坐标位置
* \param[in] w 宽度
* \param[in] y y开始坐标位置
* \param[in] h 高度
* \param[in] buffer rgb565颜色缓存区
*/
void st7789_itf_fill_direct(uint16_t x, uint16_t w, uint16_t y, uint16_t h, uint16_t* buffer)
{
st7789_sync(&s_st7789_itf_dev, x, x+w-1, y, y+h-1, buffer, w*h*2);
}
/**
* \fn st7789_itf_set_pixel_direct
* 直接写点
* \param[in] x x坐标位置
* \param[in] y y坐标位置
* \param[in] rgb565 颜色
*/
void st7789_itf_set_pixel_direct(uint16_t x, uint16_t y, uint16_t rgb565)
{
uint16_t tmp = rgb565;
st7789_sync(&s_st7789_itf_dev, x, x, y, y, &tmp, 2);
}
四.测试
St7789_test.c中
static void rgb_test(void)
{
for(int x=0;x<240;x++)
{
for(int y=0;y<320;y++)
{
st7789_itf_set_pixel(x, y, 0xF800);
}
}
st7789_itf_sync();
vTaskDelay((1000*1000/configTICK_RATE_HZ));
for(int x=0;x<240;x++)
{
for(int y=0;y<320;y++)
{
st7789_itf_set_pixel(x, y, 0x07E0);
}
}
st7789_itf_sync();
vTaskDelay((1*1000/configTICK_RATE_HZ));
for(int x=0;x<240;x++)
{
for(int y=0;y<320;y++)
{
st7789_itf_set_pixel(x, y, 0x001F);
}
}
st7789_itf_sync();
vTaskDelay((1*1000/configTICK_RATE_HZ));
}
static void rgb_test_direct(void)
{
for(int x=0;x<240;x++)
{
for(int y=0;y<320;y++)
{
st7789_itf_set_pixel_direct(x, y, 0xF800);
}
}
vTaskDelay((1000*1000/configTICK_RATE_HZ));
for(int x=0;x<240;x++)
{
for(int y=0;y<320;y++)
{
st7789_itf_set_pixel_direct(x, y, 0x07E0);
}
}
vTaskDelay((1*1000/configTICK_RATE_HZ));
for(int x=0;x<240;x++)
{
for(int y=0;y<320;y++)
{
st7789_itf_set_pixel_direct(x, y, 0x001F);
}
}
vTaskDelay((1*1000/configTICK_RATE_HZ));
}
int st7789_test(void)
{
st7789_itf_init();
rgb_test();
uint32_t start;
uint32_t end;
uint32_t ftime = 0;
while(1)
{
start = xTaskGetTickCount();
for(int i=0;i<100;i++)
{
rgb_test_direct();
}
end = xTaskGetTickCount();
ftime = (end - start);
uint32_t fps = (ftime*2+100)/(100*2); /* 刷新一次的时间uS */
if(fps > 0)
{
xprintf("FPS:%d\r\n",1000000/fps);
}
else
{
xprintf("FPS:%d\r\n",0);
}
}
}
在shell任务中调用
st7789_test进行RGB刷屏测试,效果如下
五.总结
以上实现了TFT的驱动,可以进行显示,为NES移植做好了准备。后面还需要从两个方面优化,1:SPI使用DMA,2:实现PSRAM驱动,实现帧缓存提高效率。