正点原子TFT_LCD显示屏驱动学习笔记
* LCD的画笔颜色和背景色 *//* 默认红色 *//* 背景颜色.默认为白色 *//* LCD扫描方向和颜色 定义 *//* 扫描方向定义 */#define L2R_U2D 0 /* 从左到右,从上到下 */#define L2R_D2U 1 /* 从左到右,从下到上 */#define R2L_U2D 2 /* 从右到左,从上到下 */#define R2L_D2U 3 /* 从右到左,从
一、模块相关信息
(1)模块的基本参数

(2)模块的电气参数

(3)模块原理图

(4)模块各排针引脚说明

(5)模块FPC引脚说明

(6)16位8080接口读写时序图


基本读的时序操作如下:
-
根据要发送的是命令还是数据,设置D/CX电平。
-
将CSX拉低(选中芯片)
-
将数据放到并行数据总线DB[17:0]上。
-
产生WRX的下降沿,再产生上升沿,完成数据写入。
-
将CSX拉高(取消选中)
具体的写时序图如下:

具体的读时序图如下:

(7)16 位数据与显存对应关系图
二、相关的操作命令
正点原子的资料中所写为IL9341芯片的命令,此教程主要作用的梳理控制原理流程并且写代码,ST7789的这些流程与IL9341的类似,所以后面的操作指令直接与IL9341进行,后续再自己编写ST7789的控制。由于TFT模块的引脚名与IL9341的名字有所差异,下表先展示两者引脚的对应关系。


(1)读取LCD控制器的ID

(2)存储访问控制指令


(3)列地址设置指令

(4)页地址设置指令

(5)写 GRAM 指令

ILI9341 在收到该指令后,第一次输出的是 dummy 数据,也就是无效的数据,第二次开始,读取到的才是有效的 GRAM 数据(从坐标: SC,SP 开始),输出规律为:每个颜色分量占 8 个位,一次输出 2 个颜色分量。比如:第一次 输出是 R1G1,随后的规律为B1R2→G2B2→R3G3→B3R4→G4B4→R5G5... 以此类推。如果 我们只需要读取一个点的颜色值,那么只需要接收到参数 3 即可,如果要连续读取(利用 GRAM 地址自增,方法同上),那么就按照上述规律去接收颜色数据。
三、TFTLCD模块使用一般流程

四、开发板与模块的引脚关系
使用的开发板为正点原子STMminiV4.5开发板,芯片型号为STM32F103RCT6

五、程序设计
(1)驱动 LCD 显示配置步骤
①设置 STM32F103 与 TFTLCD 模块相连接的 IO
②初始化 TFTLCD 模块
这里我们没有硬复位 LCD,因为 MiniSTM32 开发板的 LCD 接口将 TFTLCD 的 RST 同 STM32F103 的 RESET 连接在一起了,只要按下开发板的 RESET 键,就会对 LCD 进行硬复位。初始化序列,就是向 LCD 控制器写入一系列的设置值(比如伽马校准),这些初始化序列一般 LCD 供应商会提供给客户,我们直接使用这些序列即可,不需要深入研究。在初始化之后,LCD 才可以正常使用。
③通过函数将字符和数字显示到 TFTLCD 模块上。
设置坐标→写 GRAM 指令→写 GRAM 来实现, 但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而达到显示字符/数字的目的,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数,就可以实现数字/字符的显示了。
(2)程序流程图

(3)各个函数的具体实现
/******************************************************************************************/
/* LCD RST/WR/RD/BL/CS/RS 引脚 定义
* LCD_D0~D15,由于引脚太多,就不在这里定义了,直接在lcd_init里面修改.所以在移植的时候,除了改
* 这6个IO口, 还得改LCD_Init里面的D0~D15所在的IO口.
*/
/* RESET 和系统复位脚共用 所以这里不用定义 RESET引脚 */
//#define LCD_RST_GPIO_PORT GPIOx
//#define LCD_RST_GPIO_PIN GPIO_PIN_x
//#define LCD_RST_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOx_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
//背光控制引脚
#define LCD_BL_GPIO_PORT GPIOC
#define LCD_BL_GPIO_PIN GPIO_PIN_10
#define LCD_BL_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
//写入数据引脚
#define LCD_WR_GPIO_PORT GPIOC
#define LCD_WR_GPIO_PIN GPIO_PIN_7
#define LCD_WR_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
//读取数据引脚
#define LCD_RD_GPIO_PORT GPIOC
#define LCD_RD_GPIO_PIN GPIO_PIN_6
#define LCD_RD_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
//片选信号引脚
#define LCD_CS_GPIO_PORT GPIOC
#define LCD_CS_GPIO_PIN GPIO_PIN_9
#define LCD_CS_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
//命令/数据引脚
#define LCD_RS_GPIO_PORT GPIOC
#define LCD_RS_GPIO_PIN GPIO_PIN_8
#define LCD_RS_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
//18位数据线,我们只使用其中的16位,ALL就是运用全部16个IO口
#define LCD_DATA_GPIO_PORT GPIOB
#define LCD_DATA_GPIO_PIN GPIO_PIN_All /* 16个IO都用到 */
#define LCD_DATA_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
/* LCD 端口控制函数定义 */
/* 利用操作寄存器的方式控制IO引脚提高屏幕的刷新率 */
#define LCD_BL(x) LCD_BL_GPIO_PORT->BSRR = LCD_BL_GPIO_PIN << (16 * (!x)) /* 设置BL引脚 */
#define LCD_WR(x) LCD_WR_GPIO_PORT->BSRR = LCD_WR_GPIO_PIN << (16 * (!x)) /* 设置WR引脚 高16位的对应位被置1为复位(0),反之则(1)*/
#define LCD_RD(x) LCD_RD_GPIO_PORT->BSRR = LCD_RD_GPIO_PIN << (16 * (!x)) /* 设置RD引脚 */
#define LCD_CS(x) LCD_CS_GPIO_PORT->BSRR = LCD_CS_GPIO_PIN << (16 * (!x)) /* 设置CS引脚 */
#define LCD_RS(x) LCD_RS_GPIO_PORT->BSRR = LCD_RS_GPIO_PIN << (16 * (!x)) /* 设置RS引脚 */
#define LCD_DATA_OUT(x) LCD_DATA_GPIO_PORT->ODR = x //STM32的输出数据寄存器 /* 写B0~B15引脚 */
#define LCD_DATA_IN LCD_DATA_GPIO_PORT->IDR //STM32的输入数据寄存器 /* 读B0~B15引脚 */
1.LCD参数结构体
/* LCD重要参数集 */
typedef struct
{
uint16_t width; /* LCD 宽度 */
uint16_t height; /* LCD 高度 */
uint16_t id; /* LCD ID */
uint8_t dir; /* 横屏还是竖屏控制:0,竖屏;1,横屏。 */
uint16_t wramcmd; /* 开始写gram指令 */
uint16_t setxcmd; /* 设置x坐标指令 */
uint16_t setycmd; /* 设置y坐标指令 */
} _lcd_dev;
/* LCD参数 */
extern _lcd_dev lcddev; /* 管理LCD重要参数 */
2.相关的颜色等定义
/* LCD的画笔颜色和背景色 */
extern uint32_t g_point_color; /* 默认红色 */
extern uint32_t g_back_color; /* 背景颜色.默认为白色 */
/******************************************************************************************/
/* LCD扫描方向和颜色 定义 */
/* 扫描方向定义 */
#define L2R_U2D 0 /* 从左到右,从上到下 */
#define L2R_D2U 1 /* 从左到右,从下到上 */
#define R2L_U2D 2 /* 从右到左,从上到下 */
#define R2L_D2U 3 /* 从右到左,从下到上 */
#define U2D_L2R 4 /* 从上到下,从左到右 */
#define U2D_R2L 5 /* 从上到下,从右到左 */
#define D2U_L2R 6 /* 从下到上,从左到右 */
#define D2U_R2L 7 /* 从下到上,从右到左 */
#define DFT_SCAN_DIR L2R_U2D /* 默认的扫描方向 */
/* 常用画笔颜色 */
#define WHITE 0xFFFF /* 白色 */
#define BLACK 0x0000 /* 黑色 */
#define RED 0xF800 /* 红色 */
#define GREEN 0x07E0 /* 绿色 */
#define BLUE 0x001F /* 蓝色 */
#define MAGENTA 0XF81F /* 品红色/紫红色 = BLUE + RED */
#define YELLOW 0XFFE0 /* 黄色 = GREEN + RED */
#define CYAN 0X07FF /* 青色 = GREEN + BLUE */
/* 非常用颜色 */
#define BROWN 0XBC40 /* 棕色 */
#define BRRED 0XFC07 /* 棕红色 */
#define GRAY 0X8430 /* 灰色 */
#define DARKBLUE 0X01CF /* 深蓝色 */
#define LIGHTBLUE 0X7D7C /* 浅蓝色 */
#define GRAYBLUE 0X5458 /* 灰蓝色 */
#define LIGHTGREEN 0X841F /* 浅绿色 */
#define LGRAY 0XC618 /* 浅灰色(PANNEL),窗体背景色 */
#define LGRAYBLUE 0XA651 /* 浅灰蓝色(中间层颜色) */
#define LBBLUE 0X2B12 /* 浅棕蓝色(选择条目的反色) */
3.SSD1963相关配置参数
/* SSD1963相关配置参数(一般不用改) */
/* LCD分辨率设置 */
#define SSD_HOR_RESOLUTION 800 /* LCD水平分辨率 */
#define SSD_VER_RESOLUTION 480 /* LCD垂直分辨率 */
/* LCD驱动参数设置 */
#define SSD_HOR_PULSE_WIDTH 1 /* 水平脉宽 */
#define SSD_HOR_BACK_PORCH 46 /* 水平前廊 */
#define SSD_HOR_FRONT_PORCH 210 /* 水平后廊 */
#define SSD_VER_PULSE_WIDTH 1 /* 垂直脉宽 */
#define SSD_VER_BACK_PORCH 23 /* 垂直前廊 */
#define SSD_VER_FRONT_PORCH 22 /* 垂直前廊 */
/* 如下几个参数,自动计算 */
#define SSD_HT (SSD_HOR_RESOLUTION + SSD_HOR_BACK_PORCH + SSD_HOR_FRONT_PORCH)
#define SSD_HPS (SSD_HOR_BACK_PORCH)
#define SSD_VT (SSD_VER_RESOLUTION + SSD_VER_BACK_PORCH + SSD_VER_FRONT_PORCH)
#define SSD_VPS (SSD_VER_BACK_PORCH)
4.写数据函数
/* LCD写数据, 将函数改成宏定义函数, 以达到最高速度
* -O2优化时, 如果lcd_wr_data使用普通函数定义, 只能到15帧刷屏
* -O2优化时, 如果lcd_wr_data使用__forceinline函数定义, 能到39帧刷屏
* -O2优化时, 如果lcd_wr_data使用宏定义函数, 能到51帧刷屏
*/
#define lcd_wr_data(data)\
{\
LCD_RS(1);\
LCD_CS(0);\
LCD_DATA_OUT(data);\
LCD_WR(0);\
LCD_WR(1);\
LCD_CS(1);\
}
5.写寄存器数据函数,地址并不是要写入的数据,所以依旧是命令模式
/**
* @brief LCD写寄存器编号/地址函数
* @param regno: 寄存器编号/地址
* @retval 无
* 使用内联函数直接复制插入函数,没有调用开销
*/
__attribute__((always_inline)) void lcd_wr_regno(volatile uint16_t regno)
{
LCD_RS(0);
LCD_CS(0);
LCD_DATA_OUT(regno); /* 写入要写的寄存器序号 */
LCD_WR(0);
LCD_WR(1);
LCD_CS(1);
}
/**
* @brief LCD写寄存器
* @param regno:寄存器编号/地址
* @param data:要写入的数据
* @retval 无
*/
void lcd_write_reg(uint16_t regno, uint16_t data)
{
lcd_wr_regno(regno); /* 写入要写的寄存器序号 */
lcd_wr_data(data); /* 写入数据 */
}
6.LCD读数据函数
流程:①先将数据总线对应的IO口设置为上拉输入模式
②RS置为0,代表操作数据
③RD引脚置0,此时ST7789会将数据输出到stm32单片机的输入寄存器中
④读取输入寄存器的值
⑤RD置为1
⑥恢复片选
/**
* @brief LCD读数据
* @param 无
* @retval 读取到的数据
*/
static uint16_t lcd_rd_data(void)
{
volatile uint16_t ram; /* 防止被优化 */
GPIO_InitTypeDef gpio_init_struct;
/* LCD_DATA 引脚模式设置, 上拉输入, 准备接收数据 */
gpio_init_struct.Pin = LCD_DATA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_INPUT;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(LCD_DATA_GPIO_PORT, &gpio_init_struct);
LCD_RS(1); /* RS=1,表示操作数据 */
LCD_CS(0);
LCD_RD(0);
lcd_opt_delay(2);
ram = LCD_DATA_IN; /* 读取数据 */
LCD_RD(1);
LCD_CS(1);
/* LCD_DATA 引脚模式设置, 推挽输出, 恢复输出状态 */
gpio_init_struct.Pin = LCD_DATA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(LCD_DATA_GPIO_PORT, &gpio_init_struct);
return ram;
}
7.准备写GRAM函数
实际上就是写入对应的命令,因为lcd_wr_regno这个函数与写入命令操作是一致的
/**
* @brief 准备写GRAM
* @param 无
* @retval 无
*/
__attribute__((always_inline)) void lcd_write_ram_prepare(void)
{
lcd_wr_regno(lcddev.wramcmd);
}
8.设置光标坐标函数
(只需设置起始,末尾已在初始化函数里完成)
/**
* @brief 设置光标位置(对RGB屏无效)
* @param x,y: 坐标
* @retval 无
*/
void lcd_set_cursor(uint16_t x, uint16_t y)
{
if (lcddev.id == 0X1963)
{
if (lcddev.dir == 0) /* 竖屏模式, x坐标需要变换 */
{
x = lcddev.width - 1 - x;
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(0);
lcd_wr_data(0);
lcd_wr_data(x >> 8);
lcd_wr_data(x & 0XFF);
}
else /* 横屏模式 */
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(x >> 8);
lcd_wr_data(x & 0XFF);
lcd_wr_data((lcddev.width - 1) >> 8);
lcd_wr_data((lcddev.width - 1) & 0XFF);
}
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(y >> 8);
lcd_wr_data(y & 0XFF);
lcd_wr_data((lcddev.height - 1) >> 8);
lcd_wr_data((lcddev.height - 1) & 0XFF);
}
else if (lcddev.id == 0X5510)
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(x >> 8);
lcd_wr_regno(lcddev.setxcmd + 1);
lcd_wr_data(x & 0XFF);
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(y >> 8);
lcd_wr_regno(lcddev.setycmd + 1);
lcd_wr_data(y & 0XFF);
}
else /* 9341/5310/7789/7796/9806 等 设置坐标 */
{
lcd_wr_regno(lcddev.setxcmd); //这个命令是读8位的,且是高位先行
lcd_wr_data(x >> 8);
lcd_wr_data(x & 0XFF);
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(y >> 8);
lcd_wr_data(y & 0XFF);
}
}
9.读取某坐标点颜色的函数
①先进行坐标设置
②第一次读取到的是空值
③第二次才是真正的值
④继续读取,最后使用公式转换(具体看颜色获取命令的操作结果)

/**
* @brief 读取个某点的颜色值
* @param x,y:坐标
* @retval 此点的颜色(32位颜色,方便兼容LTDC)
*/
uint32_t lcd_read_point(uint16_t x, uint16_t y)
{
uint16_t r = 0, g = 0, b = 0;
if (x >= lcddev.width || y >= lcddev.height)return 0; /* 超过了范围,直接返回 */
lcd_set_cursor(x, y); /* 设置坐标 */
if (lcddev.id == 0X5510)
{
lcd_wr_regno(0X2E00); /* 5510 发送读GRAM指令 */
}
else
{
lcd_wr_regno(0X2E); /* 9341/5310/1963/7789/7796/9806 等发送读GRAM指令 */
}
r = lcd_rd_data(); /* 假读(dummy read) */
if (lcddev.id == 0x1963)
{
return r; /* 1963直接读就可以 */
}
r = lcd_rd_data(); /* 实际坐标颜色 */
if (lcddev.id == 0x7796) /* 7796 一次读取一个像素值 */
{
return r;
}
/* ILI9341/NT35310/NT35510/ST7789/ILI9806 要分2次读出 */
b = lcd_rd_data();
g = r & 0XFF; /* 对于 9341/5310/5510/7789/9806, 第一次读取的是RG的值,R在前,G在后,各占8位 */
g <<= 8;
return (((r >> 11) << 11) | ((g >> 10) << 5) | (b >> 11)); /* 9341/5310/5510/7789/9806 需要公式转换一下 */
}
10.开启/关闭显示函数
/**
* @brief LCD开启显示
* @param 无
* @retval 无
*/
void lcd_display_on(void)
{
if (lcddev.id == 0X5510)
{
lcd_wr_regno(0X2900); /* 开启显示 */
}
else /* 9341/5310/1963/7789/7796/9806 等发送开启显示指令 */
{
lcd_wr_regno(0X29); /* 开启显示 */
}
}
/**
* @brief LCD关闭显示
* @param 无
* @retval 无
*/
void lcd_display_off(void)
{
if (lcddev.id == 0X5510)
{
lcd_wr_regno(0X2800); /* 关闭显示 */
}
else /* 9341/5310/1963/7789/7796/9806 等发送关闭显示指令 */
{
lcd_wr_regno(0X28); /* 关闭显示 */
}
}
11.设置扫描方向以及开窗(列和行的结束值)函数
/**
* @brief 设置LCD的自动扫描方向(对RGB屏无效),以及开窗大小(即列坐标的结束值,行坐标的结束值)
* @note
* 9341/5310/5510/1963/7789/7796/9806等IC已经实际测试
* 注意:其他函数可能会受到此函数设置的影响(尤其是9341),
* 所以,一般设置为L2R_U2D即可,如果设置为其他扫描方式,可能导致显示不正常.
*
* @param dir:0~7,代表8个方向(具体定义见lcd.h)
* @retval 无
*/
void lcd_scan_dir(uint8_t dir)
{
uint16_t regval = 0;
uint16_t dirreg = 0;
uint16_t temp;
/* 横屏时,对1963不改变扫描方向, 其他IC改变扫描方向!竖屏时1963改变方向, 其他IC不改变扫描方向 */
if ((lcddev.dir == 1 && lcddev.id != 0X1963) || (lcddev.dir == 0 && lcddev.id == 0X1963))
{
switch (dir) /* 方向转换 */
{
case 0:
dir = 6;
break;
case 1:
dir = 7;
break;
case 2:
dir = 4;
break;
case 3:
dir = 5;
break;
case 4:
dir = 1;
break;
case 5:
dir = 0;
break;
case 6:
dir = 3;
break;
case 7:
dir = 2;
break;
}
}
/* 根据扫描方式 设置 0X36/0X3600 寄存器 bit 5,6,7 位的值 */
switch (dir)
{
case L2R_U2D:/* 从左到右,从上到下 */
regval |= (0 << 7) | (0 << 6) | (0 << 5);
break;
case L2R_D2U:/* 从左到右,从下到上 */
regval |= (1 << 7) | (0 << 6) | (0 << 5);
break;
case R2L_U2D:/* 从右到左,从上到下 */
regval |= (0 << 7) | (1 << 6) | (0 << 5);
break;
case R2L_D2U:/* 从右到左,从下到上 */
regval |= (1 << 7) | (1 << 6) | (0 << 5);
break;
case U2D_L2R:/* 从上到下,从左到右 */
regval |= (0 << 7) | (0 << 6) | (1 << 5);
break;
case U2D_R2L:/* 从上到下,从右到左 */
regval |= (0 << 7) | (1 << 6) | (1 << 5);
break;
case D2U_L2R:/* 从下到上,从左到右 */
regval |= (1 << 7) | (0 << 6) | (1 << 5);
break;
case D2U_R2L:/* 从下到上,从右到左 */
regval |= (1 << 7) | (1 << 6) | (1 << 5);
break;
}
dirreg = 0X36; /* 对绝大部分驱动IC, 由0X36寄存器控制 */
if (lcddev.id == 0X5510)
{
dirreg = 0X3600; /* 对于5510, 和其他驱动ic的寄存器有差异 */
}
/* 9341 & 7789 & 7796 要设置BGR位 */
if (lcddev.id == 0X9341 || lcddev.id == 0X7789 || lcddev.id == 0x7796)
{
regval |= 0X08;
}
lcd_write_reg(dirreg, regval);
if (lcddev.id != 0X1963) /* 1963不做坐标处理 */
{
if (regval & 0X20)
{
if (lcddev.width < lcddev.height) /* 交换X,Y */
{
temp = lcddev.width;
lcddev.width = lcddev.height;
lcddev.height = temp;
}
}
else
{
if (lcddev.width > lcddev.height) /* 交换X,Y */
{
temp = lcddev.width;
lcddev.width = lcddev.height;
lcddev.height = temp;
}
}
}
/* 设置显示区域(开窗)大小 */
if (lcddev.id == 0X5510)
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(0);
lcd_wr_regno(lcddev.setxcmd + 1);
lcd_wr_data(0);
lcd_wr_regno(lcddev.setxcmd + 2);
lcd_wr_data((lcddev.width - 1) >> 8);
lcd_wr_regno(lcddev.setxcmd + 3);
lcd_wr_data((lcddev.width - 1) & 0XFF);
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(0);
lcd_wr_regno(lcddev.setycmd + 1);
lcd_wr_data(0);
lcd_wr_regno(lcddev.setycmd + 2);
lcd_wr_data((lcddev.height - 1) >> 8);
lcd_wr_regno(lcddev.setycmd + 3);
lcd_wr_data((lcddev.height - 1) & 0XFF);
}
else
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(0);
lcd_wr_data(0);
lcd_wr_data((lcddev.width - 1) >> 8);
lcd_wr_data((lcddev.width - 1) & 0XFF);
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(0);
lcd_wr_data(0);
lcd_wr_data((lcddev.height - 1) >> 8);
lcd_wr_data((lcddev.height - 1) & 0XFF);
}
}
12.写入某点的颜色的函数
/**
* @brief 画点
* @param x,y: 坐标
* @param color: 点的颜色(32位颜色,方便兼容LTDC)
* @retval 无
*/
void lcd_draw_point(uint16_t x, uint16_t y, uint32_t color)
{
lcd_set_cursor(x, y); /* 设置光标位置 */
lcd_write_ram_prepare(); /* 开始写入GRAM */
lcd_wr_data(color);
}
13.设置LCD显示方向的函数
实际上就是修改LCD参数结构体的值,随后再进行一次扫描设置(设置列以及行的起始位置就相当于设置了显示方向)
/**
* @brief 设置LCD显示方向
* @param dir:0,竖屏; 1,横屏
* @retval 无
*/
void lcd_display_dir(uint8_t dir)
{
lcddev.dir = dir; /* 竖屏/横屏 */
if (dir == 0) /* 竖屏 */
{
lcddev.width = 240;
lcddev.height = 320;
if (lcddev.id == 0x5510)
{
lcddev.wramcmd = 0X2C00;
lcddev.setxcmd = 0X2A00;
lcddev.setycmd = 0X2B00;
lcddev.width = 480;
lcddev.height = 800;
}
else if (lcddev.id == 0X1963)
{
lcddev.wramcmd = 0X2C; /* 设置写入GRAM的指令 */
lcddev.setxcmd = 0X2B; /* 设置写X坐标指令 */
lcddev.setycmd = 0X2A; /* 设置写Y坐标指令 */
lcddev.width = 480; /* 设置宽度480 */
lcddev.height = 800; /* 设置高度800 */
}
else /* 其他IC, 包括: 9341 / 5310 / 7789 / 7796 / 9806 等IC */
{
lcddev.wramcmd = 0X2C;
lcddev.setxcmd = 0X2A;
lcddev.setycmd = 0X2B;
}
if (lcddev.id == 0X5310 || lcddev.id == 0x7796) /* 如果是5310/7796 则表示是 320*480分辨率 */
{
lcddev.width = 320;
lcddev.height = 480;
}
if (lcddev.id == 0X9806) /* 如果是9806 则表示是 480*800 分辨率 */
{
lcddev.width = 480;
lcddev.height = 800;
}
}
else /* 横屏 */
{
lcddev.width = 320; /* 默认宽度 */
lcddev.height = 240; /* 默认高度 */
if (lcddev.id == 0x5510)
{
lcddev.wramcmd = 0X2C00;
lcddev.setxcmd = 0X2A00;
lcddev.setycmd = 0X2B00;
lcddev.width = 800;
lcddev.height = 480;
}
else if (lcddev.id == 0X1963 || lcddev.id == 0x9806)
{
lcddev.wramcmd = 0X2C; /* 设置写入GRAM的指令 */
lcddev.setxcmd = 0X2A; /* 设置写X坐标指令 */
lcddev.setycmd = 0X2B; /* 设置写Y坐标指令 */
lcddev.width = 800; /* 设置宽度800 */
lcddev.height = 480; /* 设置高度480 */
}
else /* 其他IC, 包括: 9341 / 5310 / 7789 / 7796 等IC */
{
lcddev.wramcmd = 0X2C;
lcddev.setxcmd = 0X2A;
lcddev.setycmd = 0X2B;
}
if (lcddev.id == 0X5310 || lcddev.id == 0x7796) /* 如果是5310/7796 则表示是 320*480分辨率 */
{
lcddev.width = 480;
lcddev.height = 320;
}
}
lcd_scan_dir(DFT_SCAN_DIR); /* 默认扫描方向 */
}
14.进行LCD屏幕的初始化
①先初始化IO口(配置输入输出模式,启动时钟等)
②判断是什么型号的驱动芯片
③进行对应的初始化(初始化代码为厂家提供,这里不仔细分析)
/**
* @brief 初始化LCD
* @note 该初始化函数可以初始化各种型号的LCD(详见本.c文件最前面的描述)
*
* @param 无
* @retval 无
*/
void lcd_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
LCD_BL_GPIO_CLK_ENABLE(); /* LCD_BL脚时钟使能 */
LCD_CS_GPIO_CLK_ENABLE(); /* LCD_CS脚时钟使能 */
LCD_WR_GPIO_CLK_ENABLE(); /* LCD_WR脚时钟使能 */
LCD_RD_GPIO_CLK_ENABLE(); /* LCD_RD脚时钟使能 */
LCD_RS_GPIO_CLK_ENABLE(); /* LCD_RS脚时钟使能 */
LCD_DATA_GPIO_CLK_ENABLE(); /* LCD_DATA脚时钟使能 */
__HAL_RCC_AFIO_CLK_ENABLE();
__HAL_AFIO_REMAP_SWJ_NOJTAG(); /* 禁止JTAG, 使能SWD, 释放PB3,PB4两个引脚做普通IO用 */
gpio_init_struct.Pin = LCD_BL_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(LCD_BL_GPIO_PORT, &gpio_init_struct); /* LCD_BL引脚模式设置(推挽输出) */
gpio_init_struct.Pin = LCD_CS_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽复用 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(LCD_CS_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_CS引脚 */
gpio_init_struct.Pin = LCD_WR_GPIO_PIN;
HAL_GPIO_Init(LCD_WR_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_WR引脚 */
gpio_init_struct.Pin = LCD_RD_GPIO_PIN;
HAL_GPIO_Init(LCD_RD_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_RD引脚 */
gpio_init_struct.Pin = LCD_RS_GPIO_PIN;
HAL_GPIO_Init(LCD_RS_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_RS引脚 */
gpio_init_struct.Pin = LCD_DATA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
HAL_GPIO_Init(LCD_DATA_GPIO_PORT, &gpio_init_struct); /* LCD_DATA引脚模式设置 */
LCD_WR(1); /* WR 默认高电平 */
LCD_RD(1); /* RD 默认高电平 */
LCD_CS(1); /* CS 默认高电平 */
LCD_RS(1); /* RS 默认高电平 */
LCD_DATA_OUT(0XFFFF); /* DATA 默认高电平 */
lcd_opt_delay(0X1FFFF);
/* 尝试9341ID的读取 */
lcd_wr_regno(0XD3);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 读到0X00 */
lcddev.id = lcd_rd_data(); /* 读取0X93 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 读取0X41 */
if (lcddev.id != 0X9341) /* 不是 9341 , 尝试看看是不是 ST7789 */
{
lcd_wr_regno(0X04);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 读到0X85 */
lcddev.id = lcd_rd_data(); /* 读取0X85 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 读取0X52 */
if (lcddev.id == 0X8552) /* 将8552的ID转换成7789 */
{
lcddev.id = 0x7789;
}
if (lcddev.id != 0x7789) /* 也不是ST7789, 尝试是不是 NT35310 */
{
lcd_wr_regno(0xD4);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 读回0x01 */
lcddev.id = lcd_rd_data(); /* 读回0x53 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 这里读回0x10 */
if (lcddev.id != 0x5310) /* 也不是NT35310,尝试看看是不是ST7796 */
{
lcd_wr_regno(0XD3);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 读到0X00 */
lcddev.id = lcd_rd_data(); /* 读取0X77 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 读取0X96 */
if (lcddev.id != 0x7796) /* 也不是ST7796,尝试看看是不是NT35510 */
{
/* 发送密钥(厂家提供) */
lcd_write_reg(0xF000, 0x0055);
lcd_write_reg(0xF001, 0x00AA);
lcd_write_reg(0xF002, 0x0052);
lcd_write_reg(0xF003, 0x0008);
lcd_write_reg(0xF004, 0x0001);
lcd_wr_regno(0xC500); /* 读取ID低八位 */
lcddev.id = lcd_rd_data(); /* 读回0x55 */
lcddev.id <<= 8;
lcd_wr_regno(0xC501); /* 读取ID高八位 */
lcddev.id |= lcd_rd_data(); /* 读回0x10 */
delay_ms(5); /* 等待5ms, 因为0XC501指令对1963来说就是软件复位指令, 等待5ms让1963复位完成再操作 */
if (lcddev.id != 0x5510) /* 也不是NT5510,尝试看看是不是ILI9806 */
{
lcd_wr_regno(0XD3);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 读回0X00 */
lcddev.id = lcd_rd_data(); /* 读回0X98 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 读回0X06 */
if (lcddev.id != 0x9806) /* 也不是ILI9806,尝试看看是不是SSD1963 */
{
lcd_wr_regno(0xA1);
lcddev.id = lcd_rd_data();
lcddev.id = lcd_rd_data(); /* 读回0x57 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 读回0x61 */
if (lcddev.id == 0x5761) lcddev.id = 0x1963; /* SSD1963读回的ID是5761H,为方便区分,我们强制设置为1963 */
}
}
}
}
}
}
/* 特别注意, 如果在main函数里面屏蔽串口1初始化, 则会卡死在printf
* 里面(卡死在f_putc函数), 所以, 必须初始化串口1, 或者屏蔽掉下面
* 这行 printf 语句 !!!!!!!
*/
printf("LCD ID:%x\r\n", lcddev.id); /* 打印LCD ID */
if (lcddev.id == 0X7789)
{
lcd_ex_st7789_reginit(); /* 执行ST7789初始化 */
}
else if (lcddev.id == 0X9341)
{
lcd_ex_ili9341_reginit(); /* 执行ILI9341初始化 */
}
else if (lcddev.id == 0x5310)
{
lcd_ex_nt35310_reginit(); /* 执行NT35310初始化 */
}
else if (lcddev.id == 0x7796)
{
lcd_ex_st7796_reginit(); /* 执行ST7796初始化 */
}
else if (lcddev.id == 0x5510)
{
lcd_ex_nt35510_reginit(); /* 执行NT35510初始化 */
}
else if (lcddev.id == 0x9806)
{
lcd_ex_ili9806_reginit(); /* 执行ILI9806初始化 */
}
else if (lcddev.id == 0x1963)
{
lcd_ex_ssd1963_reginit(); /* 执行SSD1963初始化 */
lcd_ssd_backlight_set(100); /* 背光设置为最亮 */
}
lcd_display_dir(0); /* 默认为竖屏 */
LCD_BL(1); /* 点亮背光 */
lcd_clear(WHITE);
}
14.清屏函数
此函数先计算总共要设置的点(开窗大小),随后进行逐个点涂色,由于此过程不需要重复一直进行CS操作,所以进行了速度的优化,具体看函数实现
/**
* @brief 清屏函数
* @param color: 要清屏的颜色
* @retval 无
*/
void lcd_clear(uint16_t color)
{
uint32_t index = 0;
uint32_t totalpoint = lcddev.width;
totalpoint *= lcddev.height; /* 得到总点数 */
lcd_set_cursor(0x00, 0x0000); /* 设置光标位置 */
lcd_write_ram_prepare(); /* 开始写入GRAM */
/* 为了提高写入速度, 将lcd_wr_data函数进行拆分, 避免重复设置
* RS, CS的操作, 从而提升速度, 从51帧提高到78帧左右, 提高50%
* 测试条件: -O2优化, 纯刷屏
* 在有速度要求的时候, 可以继续优化: lcd_fill, lcd_color_fill,
* lcd_set_cursor 和 lcd_draw_point 等函数, 大家可以自行优化
*/
LCD_RS(1); /* RS=1,表示写数据 */
LCD_CS(0);
for (index = 0; index < totalpoint; index++)
{
LCD_DATA_OUT(color); /* 写入要写的数据 */
LCD_WR(0);
LCD_WR(1);
}
LCD_CS(1);
}
15.指定区域填充单色/颜色块函数
/**
* @brief 在指定区域内填充单个颜色
* @param (sx,sy),(ex,ey):填充矩形对角坐标,区域大小为:(ex - sx + 1) * (ey - sy + 1)
* @param color: 要填充的颜色(32位颜色,方便兼容LTDC)
* @retval 无
*/
void lcd_fill(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, uint32_t color)
{
uint16_t i, j;
uint16_t xlen = 0;
xlen = ex - sx + 1;
for (i = sy; i <= ey; i++)
{
lcd_set_cursor(sx, i); /* 设置光标位置 */
lcd_write_ram_prepare(); /* 开始写入GRAM */
for (j = 0; j < xlen; j++)
{
lcd_wr_data(color); /* 写入数据 */
}
}
}
/**
* @brief 在指定区域内填充指定颜色块
* @param (sx,sy),(ex,ey):填充矩形对角坐标,区域大小为:(ex - sx + 1) * (ey - sy + 1)
* @param color: 要填充的颜色数组首地址
* @retval 无
*/
void lcd_color_fill(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, uint16_t *color)
{
uint16_t height, width;
uint16_t i, j;
width = ex - sx + 1; /* 得到填充的宽度 */
height = ey - sy + 1; /* 高度 */
for (i = 0; i < height; i++)
{
lcd_set_cursor(sx, sy + i); /* 设置光标位置 */
lcd_write_ram_prepare(); /* 开始写入GRAM */
for (j = 0; j < width; j++)
{
lcd_wr_data(color[i * width + j]); /* 写入数据 */
}
}
}
16.画各种线段以及各种图形的函数
/**
* @brief 画线
* @param x1,y1: 起点坐标
* @param x2,y2: 终点坐标
* @param color: 线的颜色
* @retval 无
*/
void lcd_draw_line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
uint16_t t;
int xerr = 0, yerr = 0, delta_x, delta_y, distance;
int incx, incy, row, col;
delta_x = x2 - x1; /* 计算坐标增量 */
delta_y = y2 - y1;
row = x1;
col = y1;
if (delta_x > 0)incx = 1; /* 设置单步方向 */
else if (delta_x == 0)incx = 0; /* 垂直线 */
else
{
incx = -1;
delta_x = -delta_x;
}
if (delta_y > 0)incy = 1;
else if (delta_y == 0)incy = 0; /* 水平线 */
else
{
incy = -1;
delta_y = -delta_y;
}
if ( delta_x > delta_y)distance = delta_x; /* 选取基本增量坐标轴 */
else distance = delta_y;
for (t = 0; t <= distance + 1; t++ ) /* 画线输出 */
{
lcd_draw_point(row, col, color); /* 画点 */
xerr += delta_x ;
yerr += delta_y ;
if (xerr > distance)
{
xerr -= distance;
row += incx;
}
if (yerr > distance)
{
yerr -= distance;
col += incy;
}
}
}
/**
* @brief 画水平线
* @param x,y: 起点坐标
* @param len : 线长度
* @param color: 矩形的颜色
* @retval 无
*/
void lcd_draw_hline(uint16_t x, uint16_t y, uint16_t len, uint16_t color)
{
if ((len == 0) || (x > lcddev.width) || (y > lcddev.height))return;
lcd_fill(x, y, x + len - 1, y, color);
}
/**
* @brief 画矩形
* @param x1,y1: 起点坐标
* @param x2,y2: 终点坐标
* @param color: 矩形的颜色
* @retval 无
*/
void lcd_draw_rectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
lcd_draw_line(x1, y1, x2, y1, color);
lcd_draw_line(x1, y1, x1, y2, color);
lcd_draw_line(x1, y2, x2, y2, color);
lcd_draw_line(x2, y1, x2, y2, color);
}
/**
* @brief 画圆
* @param x,y : 圆中心坐标
* @param r : 半径
* @param color: 圆的颜色
* @retval 无
*/
void lcd_draw_circle(uint16_t x0, uint16_t y0, uint8_t r, uint16_t color)
{
int a, b;
int di;
a = 0;
b = r;
di = 3 - (r << 1); /* 判断下个点位置的标志 */
while (a <= b)
{
lcd_draw_point(x0 + a, y0 - b, color); /* 5 */
lcd_draw_point(x0 + b, y0 - a, color); /* 0 */
lcd_draw_point(x0 + b, y0 + a, color); /* 4 */
lcd_draw_point(x0 + a, y0 + b, color); /* 6 */
lcd_draw_point(x0 - a, y0 + b, color); /* 1 */
lcd_draw_point(x0 - b, y0 + a, color);
lcd_draw_point(x0 - a, y0 - b, color); /* 2 */
lcd_draw_point(x0 - b, y0 - a, color); /* 7 */
a++;
/* 使用Bresenham算法画圆 */
if (di < 0)
{
di += 4 * a + 6;
}
else
{
di += 10 + 4 * (a - b);
b--;
}
}
}
/**
* @brief 填充实心圆
* @param x,y: 圆中心坐标
* @param r : 半径
* @param color: 圆的颜色
* @retval 无
*/
void lcd_fill_circle(uint16_t x, uint16_t y, uint16_t r, uint16_t color)
{
uint32_t i;
uint32_t imax = ((uint32_t)r * 707) / 1000 + 1;
uint32_t sqmax = (uint32_t)r * (uint32_t)r + (uint32_t)r / 2;
uint32_t xr = r;
lcd_draw_hline(x - r, y, 2 * r, color);
for (i = 1; i <= imax; i++)
{
if ((i * i + xr * xr) > sqmax)
{
/* draw lines from outside */
if (xr > imax)
{
lcd_draw_hline (x - i + 1, y + xr, 2 * (i - 1), color);
lcd_draw_hline (x - i + 1, y - xr, 2 * (i - 1), color);
}
xr--;
}
/* draw lines from inside (center) */
lcd_draw_hline(x - xr, y + i, 2 * xr, color);
lcd_draw_hline(x - xr, y - i, 2 * xr, color);
}
}
17.在指定位置写入字符的函数
/**
* @brief 在指定位置显示一个字符
* @param x,y : 坐标
* @param chr : 要显示的字符:" "--->"~"
* @param size : 字体大小 12/16/24/32
* @param mode : 叠加方式(1); 非叠加方式(0);
* @param color : 字符的颜色;
* @retval 无
*/
void lcd_show_char(uint16_t x, uint16_t y, char chr, uint8_t size, uint8_t mode, uint16_t color)
{
uint8_t temp, t1, t;
uint16_t y0 = y;
uint8_t csize = 0;
uint8_t *pfont = 0;
csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 得到字体一个字符对应点阵集所占的字节数 */
chr = chr - ' '; /* 得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库) */
switch (size)
{
case 12:
pfont = (uint8_t *)asc2_1206[chr]; /* 调用1206字体 */
break;
case 16:
pfont = (uint8_t *)asc2_1608[chr]; /* 调用1608字体 */
break;
case 24:
pfont = (uint8_t *)asc2_2412[chr]; /* 调用2412字体 */
break;
case 32:
pfont = (uint8_t *)asc2_3216[chr]; /* 调用3216字体 */
break;
default:
return ;
}
for (t = 0; t < csize; t++)
{
temp = pfont[t]; /* 获取字符的点阵数据 */
for (t1 = 0; t1 < 8; t1++) /* 一个字节8个点 */
{
if (temp & 0x80) /* 有效点,需要显示 */
{
lcd_draw_point(x, y, color); /* 画点出来,要显示这个点 */
}
else if (mode == 0) /* 无效点,不显示 */
{
lcd_draw_point(x, y, g_back_color); /* 画背景色,相当于这个点不显示(注意背景色由全局变量控制) */
}
temp <<= 1; /* 移位, 以便获取下一个位的状态 */
y++;
if (y >= lcddev.height)return; /* 超区域了 */
if ((y - y0) == size) /* 显示完一列了? */
{
y = y0; /* y坐标复位 */
x++; /* x坐标递增 */
if (x >= lcddev.width)return; /* x坐标超区域了 */
break;
}
}
}
}
18.在指定位置显示数字(没有前导0)
/**
* @brief 显示len个数字
* @param x,y : 起始坐标
* @param num : 数值(0 ~ 2^32)
* @param len : 显示数字的位数
* @param size: 选择字体 12/16/24/32
* @param color : 数字的颜色;
* @retval 无
*/
void lcd_show_num(uint16_t x, uint16_t y, uint32_t num, uint8_t len, uint8_t size, uint16_t color)
{
uint8_t t, temp;
uint8_t enshow = 0;
for (t = 0; t < len; t++) /* 按总显示位数循环 */
{
temp = (num / lcd_pow(10, len - t - 1)) % 10; /* 获取对应位的数字 */
if (enshow == 0 && t < (len - 1)) /* 没有使能显示,且还有位要显示 */
{
if (temp == 0)
{
lcd_show_char(x + (size / 2)*t, y, ' ', size, 0, color);/* 显示空格,占位 */
continue; /* 继续下个一位 */
}
else
{
enshow = 1; /* 使能显示 */
}
}
lcd_show_char(x + (size / 2)*t, y, temp + '0', size, 0, color); /* 显示字符 */
}
}
19.显示数字(含有前导0)
/**
* @brief 扩展显示len个数字(高位是0也显示)
* @param x,y : 起始坐标
* @param num : 数值(0 ~ 2^32)
* @param len : 显示数字的位数
* @param size: 选择字体 12/16/24/32
* @param mode: 显示模式
* [7]:0,不填充;1,填充0.
* [6:1]:保留
* [0]:0,非叠加显示;1,叠加显示.
* @param color : 数字的颜色;
* @retval 无
*/
void lcd_show_xnum(uint16_t x, uint16_t y, uint32_t num, uint8_t len, uint8_t size, uint8_t mode, uint16_t color)
{
uint8_t t, temp;
uint8_t enshow = 0;
for (t = 0; t < len; t++) /* 按总显示位数循环 */
{
temp = (num / lcd_pow(10, len - t - 1)) % 10; /* 获取对应位的数字 */
if (enshow == 0 && t < (len - 1)) /* 没有使能显示,且还有位要显示 */
{
if (temp == 0)
{
if (mode & 0X80) /* 高位需要填充0 */
{
lcd_show_char(x + (size / 2)*t, y, '0', size, mode & 0X01, color); /* 用0占位 */
}
else
{
lcd_show_char(x + (size / 2)*t, y, ' ', size, mode & 0X01, color); /* 用空格占位 */
}
continue;
}
else
{
enshow = 1; /* 使能显示 */
}
}
lcd_show_char(x + (size / 2)*t, y, temp + '0', size, mode & 0X01, color);
}
}
20.显示字符串
/**
* @brief 显示字符串
* @param x,y : 起始坐标
* @param width,height: 区域大小
* @param size : 选择字体 12/16/24/32
* @param p : 字符串首地址
* @param color : 字符串的颜色;
* @retval 无
*/
void lcd_show_string(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t size, char *p, uint16_t color)
{
uint8_t x0 = x;
width += x;
height += y;
while ((*p <= '~') && (*p >= ' ')) /* 判断是不是非法字符! */
{
if (x >= width)
{
x = x0;
y += size;
}
if (y >= height)break; /* 退出 */
lcd_show_char(x, y, *p, size, 0, color);
x += size / 2;
p++;
}
}
更多推荐



所有评论(0)