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时序

LCD引脚与液晶控制器的对应关系

  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并口写时序

  1. 先根据要写入的数据的类型,设置 DC 为高电平(数据)还是低电平(命令)。
  2. 设置 WR 起始电平为高电平。
  3. 拉低片选 CS,选中 ILI9481。
  4. 在整个写时序上保持 RD 为高电平。
  5. 拉低 WR 的电平准备写入数据。
  6. 向数据线(D[15:0])上输入要写的信息。
  7. 拉高 WR,这样得到一个 WR 的上升沿,在这个上升沿,使数据写入到 ILI9481 里面。

8080并口写时序图

/**
 * @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并口读时序

  1. 根据要读取的数据的类型,设置 DC 为高电平(数据)还是低电平(命令)。
  2. 设置 RD 起始电平为高电平。
  3. 拉低片选 CS 信号,选中 ILI9481。
  4. 在整个读时序上保持 WR 为高电平。
  5. 拉低 RD 的电平准备读取数据。
  6. 读取数据线(D[15:0])上的信息。
  7. 拉高 WR,在 RD 的上升沿,使数据锁存到数据线(D[15:0])上。

8080并口读时序图

/**
 * @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。

读ID指令

  从上表可以看出,0xD3 指令后面跟了 4 个参数,最后 2 个参数,读出来是 0x93 和 0x41,刚好是我们控制器 ILI9481 的数字部分,从而,通过该指令,即可判别所用的 LCD 驱动器是什么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动 IC 的初始化代码,从而兼容不同驱动 IC 的屏,使得一个代码支持多款 LCD。

4.2、访问控制指令

  这是存储访问控制指令,可以控制 ILI9481 存储器的读写方向,简单的说,就是在连续写 GRAM 的时候,可以控制 GRAM 指针的增长方向,从而控制显示方式(读 GRAM 也是一样)。

访问控制指令

  从上表可以看出,0x36 指令后面,紧跟一个参数,这里主要关注:MY、MX、MV 这三个位,通过这三个位的设置,我们可以控制整个 ILI9481 的全部扫描方向,如下表所示:

设置LCD扫描方向

  一般,我们默认使用从左到右,从上到下的扫描方式,即 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 里面写入颜色数据了,该指令支持连续写。

写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)。

读GRAM指令

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

五、原理图

LCD模块

FSMC_NE4

FSMC_A6

FSMC_D0_D1

FSMC_D2_D3

FSMC_D4_D12

FSMC_D13_D15

FSMC_NOE_NWE

  从原理图上,我们可以看出 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;
}
posted @ 2024-01-13 21:18  星光映梦  阅读(205)  评论(0编辑  收藏  举报