43. FSMC驱动LCD屏
一、LCD简介
液晶显示器,即 Liquid Crystal Display,利用了液晶导电后透光性可变的特性,配合显示器光源、彩色滤光片和电压控制等工艺,最终可以在液晶阵列上显示彩色的图像。目前液晶显示技术以 TN、STN、TFT 三种技术为主,TFT-LCD 即采用了 TFT(Thin Film Transistor)技术的液晶显示器,也叫薄膜晶体管液晶显示器。
TFT-LCD 与无源 TN-LCD、STN-LCD 的简单矩阵不同的是,它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。TFT 式显示器具有很多优点:高响应度,高亮度,高对比度等等。TFT 式屏幕的显示效果非常出色,广泛应用于手机屏幕、笔记本电脑和台式机显示器上。
由于液晶本身不会发光,加上液晶本身的特性等原因,使得液晶屏的成像角受限,我们从屏幕的的一侧可能无法看清液晶的显示内容。液晶显示器的成像角的大小也是评估一个液晶显示器优劣的指标,目前,规格较好的液晶显示器成像角一般在 120° ~ 160° 之间。
二、液晶显示控制器
这里,我们使用的 LCD 的显示控制器 ILI9481 控制芯片。ILI9481 液晶控制器自带显存,可配置支持 8/9/16/18 位的总线中的一种,可以通过 3/4 线串行协议或 8080 并口驱动。在 16 位模式下,ILI9481 采用 RGB565 格式存储颜色数据,此时 ILI9481 的 18 位数据线与 MCU 的 16 位数据线以及 LCD GRAM 的对应关系如下图所示:
ILI9481 在 16 位模式下面,数据线有用的是:D17 ~ D13 和 D11 ~ D1,D0 和 D12 没有用到。这样,ILI9481 的 D17 ~ D13 和 D11 ~ D1 对应 MCU 的 D15 ~ D0。这样 MCU 的 16 位数据,最低 5 位代表蓝色,中间 6 位为绿色,最高 5 位为红色。数值越大,表示该颜色越深。另外,特别注意 ILI9481 所有的指令都是 8 位的(高 8 位无效),且参数除了读写 GRAM 的时候是 16 位,其他操作参数,都是 8 位的。
三、8080时序
ILI9481 在 8080 接口方式下,控制脚的信号状态所对应的功能如下所示:
硬件 IO 连接关系:
LCD 信号引脚 | MCU 引脚 | 作用 |
---|---|---|
LCD_REST | RESET | LCD 复位引脚,连接 MCU 复位引脚,一起复位 |
LCD_CS/NE4 | PC8 | LCD 片选,选中 LCD,低电平有效 |
LCD_RS/A10 | PC9 | LCD 命令/数据线,表示当前是读写命令还是数据 |
LCD_WR/NWE | PC10 | LCD 写信号,上升沿有效,用于数据写入 |
LCD_RD/NOE | PC11 | LCD 读信号,上升沿有效,用于数据读取 |
LCD_DB0 ~ LCD_DB15 | PD0 ~ PD15 | LCD 数据线,16 位,一次可以写入一个像素 |
LCD_BL | VCC | LCD 背光引脚,控制 LCD 背光亮灭,高电平亮 |
#define LCD_CS_GPIO_PORT GPIOC
#define LCD_CS_GPIO_PIN GPIO_PIN_8
#define RCC_LCD_CS_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
#define LCD_RS_GPIO_PORT GPIOC
#define LCD_RS_GPIO_PIN GPIO_PIN_9
#define RCC_LCD_RS_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
#define LCD_WR_GPIO_PORT GPIOC
#define LCD_WR_GPIO_PIN GPIO_PIN_10
#define RCC_LCD_WR_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
#define LCD_RD_GPIO_PORT GPIOC
#define LCD_RD_GPIO_PIN GPIO_PIN_11
#define RCC_LCD_RD_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
#define LCD_DB_GPIO_PORT GPIOD
#define LCD_DB_GPIO_PIN GPIO_PIN_All
#define RCC_LCD_DB_GPIO_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE()
#define LCD_CS(x) (LCD_CS_GPIO_PORT->BSRR = LCD_CS_GPIO_PIN << (16 * (!x)))
#define LCD_RS(x) (LCD_RS_GPIO_PORT->BSRR = LCD_RS_GPIO_PIN << (16 * (!x)))
#define LCD_WR(x) (LCD_WR_GPIO_PORT->BSRR = LCD_WR_GPIO_PIN << (16 * (!x)))
#define LCD_RD(x) (LCD_RD_GPIO_PORT->BSRR = LCD_RD_GPIO_PIN << (16 * (!x)))
#define LCD_DATA_OUT(x) (LCD_DB_GPIO_PORT->ODR = x)
#define LCD_DATA_IN() (LCD_DB_GPIO_PORT->IDR)
#define DATA_IN_MODE() do{ \
LCD_DB_GPIO_PORT->MODER = 0x00000000; \
LCD_DB_GPIO_PORT->PUPDR = 0x55555555; \
}while(0);
#define DATA_OUT_MODE() do{ \
LCD_DB_GPIO_PORT->MODER = 0x55555555; \
LCD_DB_GPIO_PORT->OTYPER = 0x00000000; \
LCD_DB_GPIO_PORT->OSPEEDR = 0xAAAAAAAA; \
LCD_DB_GPIO_PORT->PUPDR = 0x55555555; \
}while(0);
3.1、8080并口写时序
- 先根据要写入的数据的类型,设置 DC 为高电平(数据)还是低电平(命令)。
- 设置 WR 起始电平为高电平。
- 拉低片选 CS,选中 ILI9481。
- 在整个写时序上保持 RD 为高电平。
- 拉低 WR 的电平准备写入数据。
- 向数据线(D[15:0])上输入要写的信息。
- 拉高 WR,这样得到一个 WR 的上升沿,在这个上升沿,使数据写入到 ILI9481 里面。
/**
* @brief LCD使用8080时序写命令/数据函数
*
* @param mode 0:命令;1:数据
* @param data 命令/数据
*/
void LCD_WriteHalfword(uint8_t mode, uint16_t data)
{
LCD_RS(mode); // 数据类型,由传参决定
LCD_CS(0); // 拉低片选
LCD_DATA_OUT(data); // WR低电平期间,准备数据
LCD_WR(0); // 拉低WR线,准备数据
LCD_WR(1); // 在WR上升沿,数据发出
LCD_CS(1); // 取消片选
}
3.2、8080并口读时序
- 根据要读取的数据的类型,设置 DC 为高电平(数据)还是低电平(命令)。
- 设置 RD 起始电平为高电平。
- 拉低片选 CS 信号,选中 ILI9481。
- 在整个读时序上保持 WR 为高电平。
- 拉低 RD 的电平准备读取数据。
- 读取数据线(D[15:0])上的信息。
- 拉高 WR,在 RD 的上升沿,使数据锁存到数据线(D[15:0])上。
/**
* @brief LCD使用8080时序读命令/数据函数
*
* @param mode 0:命令;1:数据
* @return uint16_t 命令/数据
*/
uint16_t LCD_ReadHalfword(uint8_t mode)
{
uint16_t data = 0;
DATA_IN_MODE(); // 设置数据输入
LCD_RS(mode); // 数据类型,由传参决定
LCD_CS(0); // 拉低片选
LCD_RD(0); // 拉低RD线,准备读取数据
data = LCD_DATA_IN(); // RD低电平期间,准备读取数据
LCD_RD(1); // 在RD上升沿,读取数据
LCD_CS(1); // 取消片选
DATA_OUT_MODE(); // 设置数据输出
return data;
}
四、ILI9481驱动芯片简介
ILI9481 用于控制 LCD 的各种显示功能和效果,整体功能比较复杂。一般我们只需要 6 条指令就可以完成对 LCD 的基本使用。
指令(HEX) | 名称 | 作用 |
---|---|---|
0xD3 | 读 ID | 用于读取 LCD 控制器的 ID,区分型号用 |
0x36 | 访问控制 | 设置 GRAM 读写方向,控制显示方向 |
0x2A | 列地址 | 一般用于设置 X 坐标 |
0x2B | 页地址 | 一般用于设置 Y 坐标 |
0x2C | 写 GRAM | 用于往 LCD 写 GRAM 数据 |
0x2E | 读 GRAM | 用于读取 LCD 的 GRAM 数据 |
4.1、读ID指令
用于读取 LCD 控制器型号,通过型号可以执行不同的 LCD 初始化,以兼容不同的 LCD。
从上表可以看出,0xD3 指令后面跟了 4 个参数,最后 2 个参数,读出来是 0x93 和 0x41,刚好是我们控制器 ILI9481 的数字部分,从而,通过该指令,即可判别所用的 LCD 驱动器是什么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动 IC 的初始化代码,从而兼容不同驱动 IC 的屏,使得一个代码支持多款 LCD。
4.2、访问控制指令
这是存储访问控制指令,可以控制 ILI9481 存储器的读写方向,简单的说,就是在连续写 GRAM 的时候,可以控制 GRAM 指针的增长方向,从而控制显示方式(读 GRAM 也是一样)。
从上表可以看出,0x36 指令后面,紧跟一个参数,这里主要关注:MY、MX、MV 这三个位,通过这三个位的设置,我们可以控制整个 ILI9481 的全部扫描方向,如下表所示:
一般,我们默认使用从左到右,从上到下的扫描方式,即 MY=0,MX=0,MV=0。
位 D3 BGR 位可以控制 RGB、BGR 顺序。
4.3、设置列地址指令
在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置横坐标(x 坐标)。
在默认扫描方式时,该指令用于设置 x 坐标,该指令带有 4 个参数,实际上是 2 个坐标值:SC 和 EC,即列地址的起始值和结束值,SC 必须小于等于 EC,且 0 ≤ SC/EC ≤ 320(320 为 LCD 屏幕的像素宽)。一般在设置 x 坐标的时候,我们只需要带 2 个参数即可,也就是设置 SC 即可,因为如果 EC 没有变化,我们只需要设置一次即可(在初始化 ILI9481 的时候设置),从而提高速度。
4.4、设置页地址指令
在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置纵坐标(y 坐标)。
在默认扫描方式时,该指令用于设置 y 坐标,该指令带有 4 个参数,实际上是 2 个坐标值:SP 和 EP,即页地址的起始值和结束值,SP 必须小于等于 EP,且 0 ≤ SP/EP ≤ 480(480 为 LCD 屏幕的像素高)。一般在设置 y 坐标的时候,我们只需要带 2 个参数即可,也就是设置 SP 即可,因为如果 EP 没有变化,我们只需要设置一次即可(在初始化 ILI9481 的时候设置),从而提高速度。
4.5、写GRAM指令
在发送该指令之后,我们便可以往 LCD 的 GRAM 里面写入颜色数据了,该指令支持连续写。
由上表可知,在收到指令 0X2C 之后,数据有效位宽变为 16 位,我们可以连续写入 LCD GRAM 值,而 GRAM 的地址将根据 MY/MX/MV 设置的扫描方向进行自增。例如:假设设置的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过 SC,SP 设置)后,每写入一个颜色值,GRAM 地址将会自动自增 1(SC++),如果碰到 EC,则回到 SC,同时 SP++,一直到坐标:EC,EP 结束,期间无需再次设置的坐标,从而大大提高写入速度。
4.6、读GRAM指令
该指令是读 GRAM 指令,用于读取 ILI9481 的显存(GRAM)。
ILI9481 在收到该指令后,第一次输出的是 dummy 数据,也就是无效的数据,第二次开始,读取到的才是有效的 GRAM 数据(从坐标:SC,SP 开始),输出规律为:每个颜色分量占 8 个位,一次输出 2 个颜色分量。比如:第一次输出是 R1G1,随后的规律为:B1R2 → G2B2 → R3G3 → B3R4 → G4B4 → R5G5...以此类推。如果我们只需要读取一个点的颜色值,那么只需要接收到参数 3 即可,如果要连续读取(利用 GRAM 地址自增,方法同上),那么就按照上述规律去接收颜色数据。
五、原理图
从原理图上,我们可以看出 LCD 的 D0 ~ D15 数据线接在 FSMC D0 ~ D15。LCD 的 RD 是读控制引脚,上升沿读数据,接在 FSMC-NOE 引脚。WR 是写控制引脚,上升沿写数据,接在 FSMC-NEW 引脚。LCD 的 RS 引脚是数据或命令选择引脚,RS=0,读写命令,RS=1,读写数据,接在 FSMC-A10 引脚。LCD 的 CS 是片选引脚,接在 FSMC-NE4 引脚。
六、程序源码
6.1、LCD初始化函数
6.1.1、LCD初始化函数
/**
* @brief LCD初始化函数
*
*/
void LCD_Init(void)
{
LCD_GPIO_Init();
FSMC_LCD_Init();
LCD_ILI9481_Init();
HAL_Delay(10);
LCD_Clear(WHITE);
}
6.1.2、FSMC初始化函数
/*
* 我们一般使用FSMC的块1(BANK1)来驱动SRAM, 块1地址范围总大小为256MB,均分成4块:
* 存储块1(FSMC_NE1)地址范围: 0X6000 0000 ~ 0X63FF FFFF
* 存储块2(FSMC_NE2)地址范围: 0X6400 0000 ~ 0X67FF FFFF
* 存储块3(FSMC_NE3)地址范围: 0X6800 0000 ~ 0X6BFF FFFF
* 存储块4(FSMC_NE4)地址范围: 0X6C00 0000 ~ 0X6FFF FFFF
*/
#define LCD_FSMC_NEX 4 // 使用FSMC_NE4接LCD_CS
/**
* @brief FSMC驱动LCD函数
*
*/
static void FSMC_LCD_Init(void)
{
SRAM_HandleTypeDef hsram = {0};
FSMC_NORSRAM_TimingTypeDef WriteDataTimingStruct = {0};
FSMC_NORSRAM_TimingTypeDef ReadDataTimingStruct = {0};
hsram.Instance = FSMC_NORSRAM_DEVICE;
hsram.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
hsram.Init.NSBank = (LCD_FSMC_NEX == 1) ? FSMC_NORSRAM_BANK1 : \
(LCD_FSMC_NEX == 2) ? FSMC_NORSRAM_BANK2 : \
(LCD_FSMC_NEX == 3) ? FSMC_NORSRAM_BANK3 :
FSMC_NORSRAM_BANK4; // 根据配置选择FSMC_NE1~4
hsram.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; // 地址/数据线不复用
hsram.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; // SRAM
hsram.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16; // 16位数据宽度
hsram.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; // 是使能突发访问,仅对同步突发存储器有效
hsram.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW; // 等待信号的极性,仅在突发模式访问下有
hsram.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS; // 存储器是在等待周期之前的一个时钟周期还是等待周期期间使能NWAIT
hsram.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE; // 存储器写使能
hsram.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE; // 等待使能位
hsram.Init.ExtendedMode = FSMC_EXTENDED_MODE_ENABLE; // 读写使用不同的时序
hsram.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE; // 是否使能同步传输模式下的等待信号
hsram.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; // 禁止突发写
hsram.Init.ContinuousClock = FSMC_CONTINUOUS_CLOCK_SYNC_ASYNC;
// FSMC写时序控制寄存器
WriteDataTimingStruct.AccessMode = FSMC_ACCESS_MODE_A; // 模式A
WriteDataTimingStruct.AddressSetupTime = 0x09; // 写数据地址建立时间(ADDSET),可以理解为RD/WR的高电平时间
WriteDataTimingStruct.AddressHoldTime = 0x00; // 写数据地址保持时间(ADDHLD)模式A未用到
WriteDataTimingStruct.DataSetupTime = 0x08; // 写数据数据保存时间,可以理解为RD/WR的低电平时间
WriteDataTimingStruct.BusTurnAroundDuration = 0x00;
// FSMC读时序控制寄存器
ReadDataTimingStruct.AccessMode = FSMC_ACCESS_MODE_A; // 模式A
ReadDataTimingStruct.AddressSetupTime = 0x0F; // 读数据地址建立时间(ADDSET),可以理解为RD/WR的高电平时间
ReadDataTimingStruct.AddressHoldTime = 0x00; // 读数据地址保持时间(ADDHLD)模式A未用到
ReadDataTimingStruct.DataSetupTime = 60; // 读数据数据保存时间,可以理解为RD/WR的低电平时间
ReadDataTimingStruct.BusTurnAroundDuration = 0x00;
HAL_SRAM_Init(&hsram, &WriteDataTimingStruct, &ReadDataTimingStruct);
}
6.1.3、LCD的GPIO初始化
/**
* @brief LCD底层初始化
*
*/
static void LCD_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_FSMC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
/** FSMC GPIO Configuration
PF12 ------> FSMC_A6
PG12 ------> FSMC_NE4
PE7 ------> FSMC_D4 PE8 ------> FSMC_D5 PE9 ------> FSMC_D6
PE10 ------> FSMC_D7 PE11 ------> FSMC_D8 PE12 ------> FSMC_D9
PE13 ------> FSMC_D10 PE14 ------> FSMC_D11 PE15 ------> FSMC_D12
PD0 ------> FSMC_D2 PD1 ------> FSMC_D3 PD8 ------> FSMC_D13
PD9 ------> FSMC_D14 PD10 ------> FSMC_D15 PD14 ------> FSMC_D0
PD15 ------> FSMC_D1
PD4 ------> FSMC_NOE PD5 ------> FSMC_NWE
*/
// PF12
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF12_FSMC;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
// PG12
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
// PE7~PE15
GPIO_InitStruct.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 |
GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 |
GPIO_PIN_15;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
// PD0~PD1 PD4~PD5 PD8~PD10 PD14~PD15
GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_14 |
GPIO_PIN_15 | GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 |
GPIO_PIN_5;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}
6.1.4、ILI9341初始化函数
/**
* @brief ILI9481初始化函数
*
*/
static void LCD_ILI9481_Init(void)
{
LCD_WriteCmd(0xFF);
LCD_WriteCmd(0xFF);
HAL_Delay(5);
LCD_WriteCmd(0xFF);
LCD_WriteCmd(0xFF);
LCD_WriteCmd(0xFF);
LCD_WriteCmd(0xFF);
HAL_Delay(10);
LCD_WriteCmd(0xB0);
LCD_WriteData(0x00);
LCD_WriteCmd(0xB3);
LCD_WriteData(0x02);
LCD_WriteData(0x00);
LCD_WriteData(0x00);
LCD_WriteData(0x00);
LCD_WriteCmd(0xC0);
LCD_WriteData(0x13);
LCD_WriteData(0x3B); // 480
LCD_WriteData(0x00);
LCD_WriteData(0x00);
LCD_WriteData(0x00);
LCD_WriteData(0x01);
LCD_WriteData(0x00); // NW
LCD_WriteData(0x43);
LCD_WriteCmd(0xC1);
LCD_WriteData(0x08);
LCD_WriteData(0x1B); // CLOCK
LCD_WriteData(0x08);
LCD_WriteData(0x08);
LCD_WriteCmd(0xC4);
LCD_WriteData(0x11);
LCD_WriteData(0x01);
LCD_WriteData(0x73);
LCD_WriteData(0x01);
LCD_WriteCmd(0xC6);
LCD_WriteData(0x00);
LCD_WriteCmd(0xC8);
LCD_WriteData(0x0F);
LCD_WriteData(0x05);
LCD_WriteData(0x14);
LCD_WriteData(0x5C);
LCD_WriteData(0x03);
LCD_WriteData(0x07);
LCD_WriteData(0x07);
LCD_WriteData(0x10);
LCD_WriteData(0x00);
LCD_WriteData(0x23);
LCD_WriteData(0x10);
LCD_WriteData(0x07);
LCD_WriteData(0x07);
LCD_WriteData(0x53);
LCD_WriteData(0x0C);
LCD_WriteData(0x14);
LCD_WriteData(0x05);
LCD_WriteData(0x0F);
LCD_WriteData(0x23);
LCD_WriteData(0x00);
LCD_WriteCmd(0x35);
LCD_WriteData(0x00);
LCD_WriteCmd(0x44);
LCD_WriteData(0x00);
LCD_WriteData(0x01);
LCD_WriteCmd(0xD0);
LCD_WriteData(0x07);
LCD_WriteData(0x07); // VCI1
LCD_WriteData(0x1D); // VRH
LCD_WriteData(0x03); // BT
LCD_WriteCmd(0xD1);
LCD_WriteData(0x03);
LCD_WriteData(0x5B); // VCM
LCD_WriteData(0x10); // VDV
LCD_WriteCmd(0xD2);
LCD_WriteData(0x03);
LCD_WriteData(0x24);
LCD_WriteData(0x04);
LCD_WriteCmd(0x2A); // 设置列地址
LCD_WriteData(0x00); // 发送列地址的起始地址高8位
LCD_WriteData(0x00); // 发送列地址的起始地址低8位
LCD_WriteData(0x01); // 发送列地址的结束地址高8位
LCD_WriteData(0x3F); // 发送列地址的结束地址低8位
LCD_WriteCmd(0x2B); // 设置页地址
LCD_WriteData(0x00); // 发送页地址的起始地址高8位
LCD_WriteData(0x00); // 发送页地址的起始地址低8位
LCD_WriteData(0x01); // 发送页地址的结束地址高8位
LCD_WriteData(0xDF); // 发送页地址的结束地址低8位
LCD_WriteCmd(0x36); // 访问控制,设置GRAM读写方向,控制显示方向
LCD_WriteData(0x00); // 从左到右,从上到下,颜色为RGB顺序
LCD_WriteCmd(0xC0);
LCD_WriteData(0x13);
LCD_WriteCmd(0x3A);
LCD_WriteData(0x55);
LCD_WriteCmd(0x11);
HAL_Delay(150);
LCD_WriteCmd(0x29);
HAL_Delay(30);
}
6.1.5、LCD读写命令/数据函数
当我们使用 16 位宽的外部存储时,用 HADDR[25:1] 表示外部的 FSMC_A[24:0],内部地址相当于左移了一位。LCD 我们选择的是 16 位宽度的数据线,选择地址线时,我们选择的是 A6 接 LCD 的 DC(命令/数据选择引脚),当 A6=0 时,表示读写命令,所以地址是:0x6C00 0000,当 A6=1 时,表示读写数据,所以地址是 0x6C00 0000 | 1 << 7。
#define LCD_FSMC_AX 6 // 使用FSMC_A6接LCD_DC
#define LCD_BASE_ADDRESS (0x60000000 + (0x4000000 * (LCD_FSMC_NEX - 1)))
#define LCD_CMD *(uint16_t *)LCD_BASE_ADDRESS
#define LCD_DATA *(uint16_t *)(LCD_BASE_ADDRESS | (1 << (LCD_FSMC_AX + 1)))
修改 LCD 写命令函数:
/**
* @brief LCD写命令函数
*
* @param cmd 命令
*/
void LCD_WriteCmd(uint16_t cmd)
{
LCD_CMD = cmd;
}
修改 LCD 写数据函数:
/**
* @brief LCD写数据函数
*
* @param data 数据
*/
void LCD_WriteData(uint16_t data)
{
LCD_DATA = data;
}
修改 LCD 读数据函数:
/**
* @brief LCD读数据函数
*
* @return uint16_t 读取到RGB565值
*/
uint16_t LCD_ReadData(void)
{
return LCD_DATA;
}
6.2、常用功能
6.2.1、LCD设置光标函数
/**
* @brief 设置光标位置
*
* @param x 列
* @param y 行
*/
void LCD_SetCursor(uint16_t x, uint16_t y)
{
LCD_WriteCmd(0x2A); // 设置列地址
LCD_WriteData(x >> 8); // 发送列地址的起始地址高8位
LCD_WriteData(x & 0xFF); // 发送列地址的起始地址低8位
LCD_WriteCmd(0x2B); // 发送页地址
LCD_WriteData(y >> 8); // 发送页地址的起始地址高8位
LCD_WriteData(y & 0xFF); // 发送页地址的起始地址低8位
}
6.2.2、LCD设置光标范围函数
/**
* @brief 设置LCD的光标范围
*
* @param x1 光标的起始位置的列
* @param y1 光标的起始位置的行
* @param x2 光标的结束位置的列
* @param y2 光标的结束位置的行
*/
void LCD_SetCursorArea(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
LCD_WriteCmd(0x2A); // 设置列地址
LCD_WriteData(x1 >> 8); // 发送列地址的起始地址高8位
LCD_WriteData(x1 & 0xFF); // 发送列地址的起始地址低8位
LCD_WriteData(x2 >> 8); // 发送列地址的起始地址高8位
LCD_WriteData(x2 & 0xFF); // 发送列地址的起始地址低8位
LCD_WriteCmd(0x2B); // 发送页地址
LCD_WriteData(y1 >> 8); // 发送页地址的起始地址高8位
LCD_WriteData(y1 & 0xFF); // 发送页地址的起始地址低8位
LCD_WriteData(y2 >> 8); // 发送页地址的起始地址高8位
LCD_WriteData(y2 & 0xFF); // 发送页地址的起始地址低8位
}
6.2.3、LCD设置显示方向函数
/**
* @brief LCD设置显示方向函数
*
* @param direction 0:从左到右,从上到下
* 1:从上到下,从左到右
* 2:从右到左,从上到下
* 3:从上到下,从右到左
* 4:从左到右,从下到上
* 5:从下到上,从左到右
* 6:从右到左,从下到上
* 7:从下到上,从右到左
*/
void LCD_DisplayDirection(uint8_t mode)
{
LCD_WriteCmd(0x36); //设置彩屏显示方向的寄存器
LCD_WriteData(0x00 | (mode << 5));
switch (mode)
{
case 0:
case 2:
case 4:
case 6:
LCD_SetCursorArea(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1);
break;
case 1:
case 3:
case 5:
case 7:
LCD_SetCursorArea(0, 0, LCD_HEIGHT - 1, LCD_WIDTH - 1);
default:
break;
}
}
6.2.4、LCD清屏函数
#define LCD_WIDTH 320
#define LCD_HEIGHT 480
/* 常用画笔颜色 */
#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 // 浅棕蓝色(选择条目的反色)
/**
* @brief LCD清屏函数
*
* @param color 颜色
*/
void LCD_Clear(uint16_t color)
{
uint32_t total_point = LCD_WIDTH * LCD_HEIGHT; // 得到总点数
LCD_SetCursor(0, 0); // 设置光标位置
LCD_WriteCmd(0x2C); // 发送写GRAM指令
for (uint32_t index = 0; index < total_point; index++)
{
LCD_WriteData(color);
}
}
6.2.5、LCD局部清屏函数
/**
* @brief LCD局部清屏函数
*
* @param x 要清空的区域的左上角的列坐标
* @param y 要清空的区域的左上角的行坐标
* @param width 要清空的区域的宽度
* @param height 要清空的区域的高度
* @param color 要清空的区域的颜色
*/
void LCD_ClearArea(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t color)
{
for (uint8_t i = y; i < y + height; i++)
{
LCD_SetCursor(x, i); // 设置光标位置
LCD_WriteCmd(0x2C); // 发送写GRAM指令
for (uint8_t j = x; j < x + width; j++)
{
LCD_WriteData(color);
}
}
}
6.2.6、画点函数
/**
* @brief LCD画点函数
*
* @param x 列
* @param y 行
* @param color 颜色
*/
void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color)
{
LCD_SetCursor(x, y); // 设置坐标
LCD_WriteCmd(0x2C); // 发送写GRAM指令
LCD_WriteData(color); // 写入颜色值
}
/**
* @brief LCD读点函数
*
* @param x 列数
* @param y 行数
* @return uint16_t 读取的RGB565的颜色值
*/
uint16_t LCD_ReadPoint(uint16_t x, uint16_t y)
{
uint16_t r = 0, g = 0, b = 0;
LCD_SetCursor(x, y); // 设置坐标
LCD_WriteCmd(0x2E); // 读GRAM数据指令
LCD_ReadData(); // 假读
r = LCD_ReadData(); // 读取R通道和G通道的值
b = LCD_ReadData(); // 读取B通道的值
g = r & 0xFF; // 获取G通道的值
return (((r >> 11) << 11) | ((g >> 2) << 5) | (b >> 11));
}
6.3、显示字符
6.3.1、LCD显示字符函数
这里,我们用取模软件生成字符的数据,这里采用:阴码 + 列行式 + 逆向 + C51
的格式。
/* 常用ASCII表
* 偏移量32
* ASCII字符集: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
* PC2LCD2002取模方式设置:阴码 + 列行式 + 逆向 + C51
* 总共:4个字符集(12*12、16*16、24*24和32*32),用户可以自行新增其他分辨率的字符集。
* 每个字符所占用的字节数为:(size/8+((size%8)?1:0))*(size/2),其中size:是字库生成时的点阵大小(12/16/24/32...)
*/
/**
* @brief LCD显示字符函数
*
* @param x 列
* @param y 行
* @param chr 显示的字符
* @param size 字体大小,这里字符的高度等于字重,字符的宽度等于字重的一半
* @param fColor 字符的颜色
* @param bColor 背景色
* @param displayMode 显示模式的枚举值
*/
void LCD_ShowChar(uint16_t x, uint16_t y, char chr, uint16_t size, uint16_t fColor, uint16_t bColor, LCD_DisplayMode displayMode)
{
uint8_t *pfont = NULL;
uint8_t temp = 0;
uint8_t cHigh = size / 8 + ((size % 8) ? 1 : 0); // 得到一个字符对应的字节数
switch (size)
{
case 12:
pfont = (uint8_t *)ascii_06x12[chr - ' ']; // 调用06x12字体
break;
case 16:
pfont = (uint8_t *)ascii_08x16[chr - ' ']; // 调用08x16字体
break;
case 24:
pfont = (uint8_t *)ascii_12x24[chr - ' ']; // 调用12x24字体
break;
case 32:
pfont = (uint8_t *)ascii_16x32[chr - ' ']; // 调用16x32字体
break;
default:
return ;
}
for (uint8_t h = 0; h < cHigh; h++) // 遍历字符的高度
{
for (uint8_t w = 0; w < size / 2; w++) // 遍历字符的宽度
{
temp = pfont[h * size / 2 + w]; // 获取字符对应的字节数据
for (uint8_t k = 0; k < 8; k++) // 一个字节8个像素点
{
if (temp & 0x01) // 绘制字符
{
LCD_DrawPoint(x + w, y + k + 8 * h , fColor);
}
else
{
if (displayMode == LCD_DISPLAY_NORMAL) // 是否绘制背景
{
LCD_DrawPoint(x + w, y + k + 8 * h , bColor);
}
}
temp >>= 1;
}
}
}
}
6.3.2、LCD显示字符串函数
/**
* @brief LCD显示字符串函数
*
* @param x 列
* @param y 行
* @param str 显示的字符串
* @param size 字体大小,这里字符的高度等于字重,字符的宽度等于字重的一半
* @param fColor 字符串的颜色
* @param bColor 背景色
* @param displayMode 显示模式的枚举值
*/
void LCD_ShowString(uint16_t x, uint16_t y, char *str, uint16_t size, uint16_t fColor, uint16_t bColor, LCD_DisplayMode displayMode)
{
uint16_t x0 = x;
for (uint16_t i = 0; str[i] != '\0'; i++)
{
if (str[i] == '\n')
{
x = x0;
y += size;
continue;
}
LCD_ShowChar(x, y, str[i], size, fColor, bColor, displayMode);
x += (size / 2);
}
}
6.3.3、LCD显示汉字
这里,我们定义一个结构体数组用来存放汉字的取模数据。其中,单个结构体变量的成员定义如下:
typedef struct ChineseCell_32x32
{
char index[4];
unsigned char data[128];
} ChineseCell_32x32;
const ChineseCell_32x32 chinese_32x32[] = {
{"小", {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFC,0xF8,0x08,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xE0,0x7C,0x38,0x08,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0x00,0x08,0x10,0x60,0xC0,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x40,0x30,0x18,0x0E,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x0F,0x3E,0x7C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x08,0x10,0x30,0x70,0x3F,0x1F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"小",0*/},
{"樱", {0x00,0x00,0x00,0x00,0x00,0x00,0xFC,0x08,0x00,0x00,0x00,0x08,0xF0,0x10,0x10,0x90,0x90,0x10,0xF8,0x10,0x00,0xF0,0xF0,0x10,0x90,0x90,0x10,0xF0,0xF0,0x00,0x00,0x00,0x00,0x02,0x02,0x02,0x02,0xFA,0xFF,0x22,0x62,0xC3,0x82,0x20,0x1F,0x80,0xF8,0x3F,0x40,0xC0,0x9F,0x80,0x00,0x1F,0x9F,0xE0,0x3F,0x43,0x40,0x9F,0x9F,0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x07,0x01,0xFF,0x00,0x00,0x10,0x15,0x12,0x13,0x11,0x10,0xD0,0xF0,0x3E,0x1D,0x11,0x10,0x12,0xD1,0xF0,0x30,0x10,0x10,0x18,0x19,0x11,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7F,0x00,0x00,0x40,0x40,0x40,0x40,0x20,0x21,0x21,0x32,0x12,0x1A,0x0A,0x04,0x07,0x0F,0x08,0x18,0x18,0x30,0x60,0x00,0x00,0x00,0x00},/*"樱",1*/},
{"", {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},},
};
这里,我们采用一种映射关系来存储汉字的取模数据。其中,index 存放的是单个汉字,例如:樱,而 data 用来存储对应汉字取模的数据。我们在显示汉字时,首先会依次取出每个汉字(UTF-8 编码一般用 3 个字节存储汉字,GBK 编码一般会使用 2 个字节存储汉字),然后与结构体数组中的 index 数组存放的单个汉字进行比较(C 语言中,可以用 string.h 库文件提供的 strcmp() 函数进行比较),如果找到则记录对应的索引,然后根据此索引取出对应的 data 中数据。如果没有找到,则不显示或显示默认字符。
/**
* @brief LCD显示汉字函数
*
* @param x 列
* @param y 行
* @param chinese 要显示的汉字
* @param size 要显示的汉字大小
* @param fColor 汉字的颜色
* @param bColor 背景色
* @param displayMode 显示模式的枚举值
*/
void LCD_ShowChinese(uint16_t x, uint16_t y, char *chinese, uint16_t size, uint16_t fColor, uint16_t bColor, LCD_DisplayMode displayMode)
{
char sigleChinese[4] = {0}; // 存储单个汉字
uint16_t index = 0; // 汉字索引
uint16_t cHigh = size / 8 + ((size % 8) ? 1 : 0); // 得到一个字符对应的字节数
uint16_t temp = 0;
uint16_t j = 0;
switch (size)
{
case 32:
for (uint16_t i = 0; chinese[i] != '\0'; i++) // 遍历汉字字符串
{
// 获取单个汉字,一般UTF-8编码使用3个字节存储汉字,GBK编码使用2个字节存储汉字
sigleChinese[index] = chinese[i];
index++;
index = index % 3;
if (index == 0) // 汉字索引为0,说明已经获取了一个汉字
{
for (j = 0; strcmp(chinese_32x32[j].index, "") != 0; j++) // 遍历汉字数组
{
if (strcmp(chinese_32x32[j].index, sigleChinese) == 0) // 找到汉字
{
break;
}
}
for (uint16_t h = 0; h < cHigh; h++) // 遍历字符的高度
{
for (uint16_t w = 0; w < size; w++) // 遍历字符的宽度
{
temp = chinese_32x32[j].data[h * size + w]; // 获取字符对应的字节数据
for (uint16_t k = 0; k < 8; k++) // 一个字节8个像素点
{
if (temp & 0x01) // 绘制字体
{
// ((i + 1) / 3)定位到第几个汉字
LCD_DrawPoint(x + w + ((i + 1) / 3 - 1) * size, y + k + 8 * h , fColor);
}
else
{
if (displayMode == LCD_DISPLAY_NORMAL) // 是否绘制背景
{
LCD_DrawPoint(x + w + ((i + 1) / 3 - 1) * size, y + k + 8 * h , bColor);
}
}
temp >>= 1;
}
}
}
}
}
break;
default:
break;
}
}
6.3.4、LCD显示数字函数
typedef struct LCD_DataFormat
{
uint8_t digit; // 位数
uint8_t decimal; // 小数位数
uint8_t symbol : 1; // 是否显示符号
uint8_t align_right : 1; // 是否右对齐
uint8_t display_mode : 1; // 是否叠加显示
} LCD_DataFormat;
/**
* @brief LCD显示数字函数
*
* @param x 列
* @param y 行
* @param num 要显示的数字
* @param size 要显示的数字大小
* @param fColor 要显示的数字的颜色
* @param bColor 背景色
* @param dataFormat 格式化结构体
*/
void LCD_ShowNumber(uint16_t x, uint16_t y, int num, uint16_t size, uint16_t fColor, uint16_t bColor, LCD_DataFormat dataFormat)
{
char str[20] = {0};
if (dataFormat.align_right)
{
if (dataFormat.symbol)
{
sprintf(str, "%+*d", dataFormat.digit, num); // 显示符号右对齐
}
else
{
sprintf(str, "%*d", dataFormat.digit, num); // 不显示符号右对齐
}
}
else
{
if (dataFormat.symbol)
{
sprintf(str, "%+d", num); // 显示符号左对齐
}
else
{
sprintf(str, "%d", num); // 不显示符号左对齐
}
}
LCD_ShowString(x, y, str, size, fColor, bColor, dataFormat.display_mode);
}
6.3.6、LCD显示小数函数
/**
* @brief LCD显示浮点数函数
*
* @param x 列
* @param y 行
* @param num 要显示的数字
* @param size 要显示的数字大小
* @param fColor 要显示的数字的前景色
* @param bColor 背景色
* @param dataFormat 格式化结构体
*/
void LCD_ShowDecimal(uint16_t x, uint16_t y, double decimal, uint16_t size, uint16_t fColor, uint16_t bColor, LCD_DataFormat dataFormat)
{
int num = (int)decimal;
int temp = num;
int len = 0;
int x1 = x;
if (x < 0 || x > 127 || y < 0 || y > 63) // 超出范围
{
return;
}
dataFormat.decimal = dataFormat.decimal ? dataFormat.decimal : 2; // 小数部分的位数默认为两位
// 获取整数的位数
while (temp)
{
temp /= 10;
len++;
}
// 获取整数的位数
if (dataFormat.digit > len)
{
dataFormat.digit -= dataFormat.decimal; // 获取整数的位数
dataFormat.align_right ? dataFormat.digit-- : dataFormat.digit; // 如果右对齐的话,还要左移一位,显示小数点
dataFormat.digit = (dataFormat.digit > len) ? dataFormat.digit : len; // 确保整数的位数大于等于指定值的整数位
}
LCD_ShowNumber(x, y, num, size, fColor, bColor, dataFormat);
// 显示小数点
if (dataFormat.digit > len)
{
if (dataFormat.align_right)
{
x1 += (dataFormat.digit * (size / 2));
}
else
{
x1 += (len * (size / 2));
x1 = ((num < 0) || (dataFormat.symbol)) ? x1 + (size / 2) : x1;
}
}
else
{
x1 += (len * (size / 2));
x1 = ((num < 0) || (dataFormat.symbol)) ? x1 + (size / 2) : x1;
}
LCD_ShowChar(x1, y, '.', size, fColor, bColor, dataFormat.display_mode);
x1 += (size / 2);
// 显示小数部分
dataFormat.symbol = 0; // 小数部分不显示符号
dataFormat.digit = dataFormat.decimal; // 放大后,重新设置要显示的位数
dataFormat.align_right = 0; // 小数部分为左对齐
// 放大小数位,并且确保得到的结果为整数
temp = 1;
for (int i = 0; i < dataFormat.decimal; i++)
{
temp *= 10;
}
num = decimal * temp - num * temp;
num = num > 0 ? num : -num;
LCD_ShowNumber(x1, y, num, size, fColor, bColor, dataFormat);
}
6.3.7、LCD打印格式化字符串
/**
* @brief LCD打印函数
*
* @param x 列
* @param y 行
* @param size 要显示的字符串大小
* @param fColor 字符串的景色
* @param bColor 背景色
* @param displayMode 显示模式的枚举值
* @param format 指定要显示的格式化字符串
* @param ... 格式化字符串参数列表
*
* @note 此函数对浮点数的支持不好,使用浮点数时无法显示
*/
void LCD_Printf(uint16_t x, uint16_t y, uint16_t size, uint16_t fColor, uint16_t bColor, LCD_DisplayMode displayMode, char *format, ...)
{
char str[256]; // 定义字符数组
va_list arg; // 定义可变参数列表数据类型的变量arg
va_start(arg, format); // 从format开始,接收参数列表到arg变量
vsprintf(str, format, arg); // 使用vsprintf打印格式化字符串和参数列表到字符数组中
va_end(arg); // 结束变量arg
LCD_ShowString(x, y, str, size, fColor, bColor, displayMode); // OLED显示字符数组(字符串)
}
6.4、LCD显示图片函数
这里的图片采用如下方式:水平扫描,16 位真彩色,低位在前。
/**
* @brief LCD显示图片函数
*
* @param image 图片数据
* @param x 列
* @param y 行
* @param width 图片的宽度
* @param height 图片的高度
*/
void LCD_ShowPicture(uint8_t image[], uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{
uint16_t color = 0;
uint32_t i = 0, j = 0, k = 0;
for (i = 0; i < height; i++)
{
LCD_SetCursor(x, y + i); // 设置光标位置
LCD_WriteCmd(0x2C); // 发送写GRAM指令
for (j = 0; j < width; j++)
{
color = (image[k]) | (image[k + 1] << 8); // 16位颜色
k += 2;
LCD_WriteData(color); // 写入颜色值
}
}
}
6.5、绘制几何图形
6.5.1、绘制直线
/**
* @brief LCD画线函数
*
* @param x1 第一个点的列
* @param y1 第一个点的行
* @param x2 第二个点的列
* @param y2 第二个点的行
* @param size 线宽
* @param color 线的颜色
*/
void LCD_DrawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color)
{
int16_t x = 0, y = 0, dx = 0, dy = 0, d = 0, incrE = 0, incrNE = 0, temp = 0;
int16_t x0_t = x0, y0_t = y0, x1_t = x1, y1_t = y1;
uint16_t yFlag = 0, xyFlag = 0;
if (y0_t == y1_t) // 横线单独处理
{
if (x0_t > x1_t) // 0号点x坐标大于1号点x坐标,则交换两点x坐标
{
temp = x0_t;
x0_t = x1_t;
x1_t = temp;
}
for (x = x0_t; x <= x1_t; x++) // 遍历x坐标,依次画点
{
LCD_DrawPoint(x, y0, color);
}
}
else if (x0_t == x1_t) // 竖线单独处理
{
if (y0_t > y1_t) // 0号点y坐标大于1号点y坐标,则交换两点y坐标
{
temp = y0_t;
y0_t = y1_t;
y1_t = temp;
}
for (y = y0_t; y <= y1_t; y++) // 遍历y坐标,依次画点
{
LCD_DrawPoint(x0, y, color);
}
}
else // 斜线
{
// 使用Bresenham算法画直线,可以避免耗时的浮点运算,效率更高
if (x0_t > x1_t) // 0号点x坐标大于1号点x坐标,交换两点坐标
{
// 交换后不影响画线,但是画线方向由第一、二、三、四象限变为第一、四象限
temp = x0_t; x0_t = x1_t; x1_t = temp;
temp = y0_t; y0_t = y1_t; y1_t = temp;
}
if (y0_t > y1_t) // 0号点y坐标大于1号点y坐标,将y坐标取负
{
// 取负后影响画线,但是画线方向由第一、四象限变为第一象限
y0_t = -y0_t;
y1_t = -y1_t;
yFlag = 1; // 置标志位yflag,记住当前变换,在后续实际画线时,再将坐标换回来
}
if (y1_t - y0_t > x1_t - x0_t) // 画线斜率大于1,将x坐标与y坐标互换
{
// 互换后影响画线,但是画线方向由第一象限0~90度范围变为第一象限0~45度范围
temp = x0_t; x0_t = y0_t; y0_t = temp;
temp = x1_t; x1_t = y1_t; y1_t = temp;
xyFlag = 1; // 置标志位xyflag,记住当前变换,在后续实际画线时,再将坐标换回来
}
// 以下为Bresenham算法画直线,算法要求,画线方向必须为第一象限0~45度范围
dx = x1_t - x0_t;
dy = y1_t - y0_t;
incrE = 2 * dy;
incrNE = 2 * (dy - dx);
d = 2 * dy - dx;
x = x0_t;
y = y0_t;
// 画起始点,同时判断标志位,将坐标换回来
if (yFlag && xyFlag) {
LCD_DrawPoint(y, -x, color);
} else if (yFlag) {
LCD_DrawPoint(x, -y, color);
} else if (xyFlag) {
LCD_DrawPoint(y, x, color);
} else {
LCD_DrawPoint(x, y, color);
}
while (x < x1_t) // 遍历x轴的每个点
{
x++;
if (d < 0) // 下一个点在当前点东方
{
d += incrE;
}
else // 下一个点在当前点东北方
{
y++;
d += incrNE;
}
// 画每一个点,同时判断标志位,将坐标换回来
if (yFlag && xyFlag) {
LCD_DrawPoint(y, -x, color);
} else if (yFlag) {
LCD_DrawPoint(x, -y, color);
} else if (xyFlag) {
LCD_DrawPoint(y, x, color);
} else {
LCD_DrawPoint(x, y, color);
}
}
}
}
6.5.2、绘制矩形
/**
* @brief LCD绘制矩形
*
* @param x 列
* @param y 行
* @param width 要绘制矩形的宽
* @param height 要绘制矩形的高
* @param color 要绘制的颜色
* @param isFilled 是否要填充 1: 填充,0: 不填充
*/
void LCD_DrawRectangle(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t color, uint8_t isFilled)
{
int16_t i = 0, j = 0;
if (!isFilled) // 指定矩形不填充
{
for (i = x; i < x + width; i++) // 遍历上下x坐标,画矩形上下两条线
{
LCD_DrawPoint(i, y, color);
LCD_DrawPoint(i, y + height - 1, color);
}
for (i = y; i < y + height; i++) // 遍历左右y坐标,画矩形左右两条线
{
LCD_DrawPoint(x, i, color);
LCD_DrawPoint(x + width - 1, i, color);
}
}
else // 指定矩形填充
{
for (i = x; i < x + width; i++) // 遍历x坐标
{
for (j = y; j < y + height; j++) // 遍历y坐标
{
LCD_DrawPoint(i, j, color); // 在指定区域画点,填充满矩形
}
}
}
}
6.5.3、绘制三角形
/**
* @brief LCD绘制三角形
*
* @param x0 第一个点所在的列
* @param y0 第一个点所在的行
* @param x1 第二个点所在的列
* @param y1 第二个点所在的行
* @param x2 第三个点所在的列
* @param y2 第三个点所在的行
* @param color 要绘制的颜色
* @param isFilled 是否要填充 1: 填充,0: 不填充
*/
void LCD_DrawTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color, uint8_t isFilled)
{
int16_t minX = x0, minY = y0, maxX = x0, maxY = y0;
int16_t i = 0, j = 0;
int vertexX[] = {x0, x1, x2};
int vertexY[] = {y0, y1, y2};
if (!isFilled) // 指定三角形不填充
{
// 调用画线函数,将三个点用直线连接
LCD_DrawLine(x0, y0, x1, y1, color);
LCD_DrawLine(x0, y0, x2, y2, color);
LCD_DrawLine(x1, y1, x2, y2, color);
}
else // 指定三角形填充
{
// 找到三个点最小的x、y坐标
if (x1 < minX) {minX = x1;}
if (x2 < minX) {minX = x2;}
if (y1 < minY) {minY = y1;}
if (y2 < minY) {minY = y2;}
// 找到三个点最大的x、y坐标
if (x1 > maxX) {maxX = x1;}
if (x2 > maxX) {maxX = x2;}
if (y1 > maxY) {maxY = y1;}
if (y2 > maxY) {maxY = y2;}
// 最小最大坐标之间的矩形为可能需要填充的区域
for (i = minX; i <= maxX; i++)
{
for (j = minY; j <= maxY; j++)
{
// 调用pnpoly算法,判断指定点是否在指定三角形之中,如果在,则画点,如果不在,则不做处理
if (pnpoly(3, vertexX, vertexY, i, j)) {LCD_DrawPoint(i, j, color);}
}
}
}
}
/**
* @brief 判断指定点是否在指定多边形内部
*
* @param nVertex 多边形的顶点数
* @param vertexX 多边形的顶点x坐标集合
* @param vertexY 多边形的顶点y坐标集合
* @param x 指定点的x坐标
* @param y 指定点的y坐标
* @return uint8_t 0: 不在多边形内部; 1: 在多边形内部;
*/
uint8_t pnpoly(uint8_t nVertex, int *vertexX, int *vertexY, int x, int y)
{
int i = 0, j = 0;
int c = 0;
for (i = 0, j = nVertex-1; i < nVertex; j = i++)
{
if (((vertexY[i] > y) != (vertexY[j] > y)) &&
(x < (vertexX[j] - vertexX[i]) * (y - vertexY[i]) / (vertexY[j] - vertexY[i]) + vertexX[i])) {
c = !c;
}
}
return c;
}
6.5.4、绘制圆
/**
* @brief LCD绘制圆
*
* @param x 圆心所在的列
* @param y 圆心所在的行
* @param radius 半径
* @param color 颜色
* @param isFilled 是否要填充 1: 填充,0: 不填充
*/
void LCD_DrawCircle(uint16_t x, uint16_t y, uint16_t radius, uint16_t color, uint8_t isFilled)
{
// 使用Bresenham算法画圆,可以避免耗时的浮点运算,效率更高
int16_t x_t = 0, y_t = radius;
int16_t d = 1 - radius;
int16_t j = 0;
// 画每个八分之一圆弧的起始点
LCD_DrawPoint(x + x_t, y + y_t, color);
LCD_DrawPoint(x - x_t, y - y_t, color);
LCD_DrawPoint(x + y_t, y + x_t, color);
LCD_DrawPoint(x - y_t, y - x_t, color);
if (isFilled) // 指定圆填充
{
// 遍历起始点y坐标,在指定区域画点,填充部分圆
for (j = -y_t; j < y_t; j++)
{
LCD_DrawPoint(x, y + j, color);
}
}
while (x_t < y_t) // 遍历x轴的每个点
{
x_t++;
if (d < 0) // 下一个点在当前点东方
{
d += 2 * x_t + 1;
}
else // 下一个点在当前点东南方
{
y_t--;
d += 2 * (x_t - y_t) + 1;
}
// 画每个八分之一圆弧的点
LCD_DrawPoint(x + x_t, y + y_t, color);
LCD_DrawPoint(x + y_t, y + x_t, color);
LCD_DrawPoint(x - x_t, y - y_t, color);
LCD_DrawPoint(x - y_t, y - x_t, color);
LCD_DrawPoint(x + x_t, y - y_t, color);
LCD_DrawPoint(x + y_t, y - x_t, color);
LCD_DrawPoint(x - x_t, y + y_t, color);
LCD_DrawPoint(x - y_t, y + x_t, color);
if (isFilled) // 指定圆填充
{
// 遍历中间部分,在指定区域画点,填充部分圆
for (j = -y_t; j < y_t; j++)
{
LCD_DrawPoint(x + x_t, y + j, color);
LCD_DrawPoint(x - x_t, y + j, color);
}
// 遍历两侧部分
for (j = -x_t; j < x_t; j++)
{
// 在指定区域画点,填充部分圆
LCD_DrawPoint(x - y_t, y + j, color);
LCD_DrawPoint(x + y_t, y + j, color);
}
}
}
}
6.5.5、绘制椭圆
/**
* @brief OLED绘制椭圆函数
*
* @param x 圆心所在的列
* @param y 圆心所在的行
* @param a 椭圆的横向半轴长度
* @param b 椭圆的纵向半轴长度
* @param isFilled 是否要填充 1: 填充,0: 不填充
*/
void LCD_DrawEllipse(uint16_t x, uint16_t y, uint16_t a, uint16_t b, uint16_t color, uint8_t isFilled)
{
int16_t x_t = 0;
int16_t y_t = b;
int16_t a_t = a, b_t = b;
int16_t j = 0;
float d1 = b_t * b_t + a_t * a_t * (-b_t + 0.5);
float d2 = 0;
// 使用Bresenham算法画椭圆,可以避免部分耗时的浮点运算,效率更高
if (isFilled) //指定椭圆填充
{
// 遍历起始点y坐标在指定区域画点,填充部分椭圆
for (j = -y_t; j < y_t; j++)
{
LCD_DrawPoint(x, y + j, color);
LCD_DrawPoint(x, y + j, color);
}
}
// 画椭圆弧的起始点
LCD_DrawPoint(x + x_t, y + y_t, color);
LCD_DrawPoint(x - x_t, y - y_t, color);
LCD_DrawPoint(x - x_t, y + y_t, color);
LCD_DrawPoint(x + x_t, y - y_t, color);
// 画椭圆中间部分
while (b_t * b_t * (x_t + 1) < a_t * a_t * (y_t - 0.5))
{
if (d1 <= 0) // 下一个点在当前点东方
{
d1 += b_t * b_t * (2 * x_t + 3);
}
else // 下一个点在当前点东南方
{
d1 += b_t * b_t * (2 * x_t + 3) + a_t * a_t * (-2 * y_t + 2);
y_t--;
}
x_t++;
if (isFilled) // 指定椭圆填充
{
// 遍历中间部分
for (j = -y_t; j < y_t; j++)
{
// 在指定区域画点,填充部分椭圆
LCD_DrawPoint(x + x_t, y + j, color);
LCD_DrawPoint(x - x_t, y + j, color);
}
}
// 画椭圆中间部分圆弧
LCD_DrawPoint(x + x_t, y + y_t, color);
LCD_DrawPoint(x - x_t, y - y_t, color);
LCD_DrawPoint(x - x_t, y + y_t, color);
LCD_DrawPoint(x + x_t, y - y_t, color);
}
// 画椭圆两侧部分
d2 = b_t * b_t * (x_t + 0.5) * (x_t + 0.5) + a_t * a_t * (y_t - 1) * (y_t - 1) - a_t * a_t * b_t * b_t;
while (y_t > 0)
{
if (d2 <= 0) // 下一个点在当前点东方
{
d2 += b_t * b_t * (2 * x_t + 2) + a_t * a_t * (-2 * y_t + 3);
x_t++;
}
else // 下一个点在当前点东南方
{
d2 += a_t * a_t * (-2 * y_t + 3);
}
y_t--;
if (isFilled) // 指定椭圆填充
{
// 遍历两侧部分,在指定区域画点,填充部分椭圆
for (j = -y_t; j < y_t; j++)
{
LCD_DrawPoint(x + x_t, y + j, color);
LCD_DrawPoint(x - x_t, y + j, color);
}
}
/*画椭圆两侧部分圆弧*/
LCD_DrawPoint(x + x_t, y + y_t, color);
LCD_DrawPoint(x - x_t, y - y_t, color);
LCD_DrawPoint(x - x_t, y + y_t, color);
LCD_DrawPoint(x + x_t, y - y_t, color);
}
}
6.6、main()函数
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
LCD_Init();
LCD_ShowChar(0, 0, 'A', 12, BLUE, RED, LCD_DISPLAY_OVERLAPPING);
LCD_ShowChar(10, 0, 'A', 16, BLUE, RED, LCD_DISPLAY_OVERLAPPING);
LCD_ShowString(10, 20, "Hello Shana!", 32, BLUE, RED, LCD_DISPLAY_OVERLAPPING);
LCD_ShowChinese(80, 60, "小樱", 32, BLUE, RED, LCD_DISPLAY_OVERLAPPING);
while (1)
{
}
return 0;
}