LCD1602是一种工业字符型液晶,能够同时显示16x02即32个字符。LCD1602液晶显示的原理是利用液晶的物理特性,通过电压对其显示区域进行控制,即可以显示出图形。在这一章我们就来讨论LCD1602液晶显示屏驱动的设计与实现。

1、功能概述

  LCD1602液晶又被称作1602字符型液晶,这是一种只用来显示字母、数字、符号等的点阵型液晶模块。LCD1602里面存储器一般有三种:CGROM、CGRAM、DDRAM。其中DDRAM(Display Data RAM)就是显示数据RAM,用来寄存待显示的字符代码。共80个字节,其地址和屏幕的对应关系如下如图所示:

HAL库驱动esp8266_HAL库驱动esp8266

  LCD1602使用三条控制线:EN、RW、RS。 其中EN的作用其实就是中线的功能,RW和RS指示了读、它写的是写的方向和内容。在读数据(或者Busy标志)期间,EN线必须保持高电平;而在写指令(或者数据)过程中,EN线上必须送出一个正脉冲。RW、RS的组合一共有四种情况,分别对应四种操作:
  (1)、RS=0、RW=0——表示向LCD写入指令。
  (2)、RS=0、RW=1——表示读取Busy标志。
  (3)、RS=1、RW=0——表示向LCD写入数据。
  (4)、RS=1、RW=1——表示从LCD读取数据。
  LCD1602利用指令码来区分不同的操作,主要的有两类:一是用于初始化配置的指令码;二是用于数据控制的指令码。第一类用于LCD初始化配置的指令码基本上都是在系统启动时,用于对LCD1602的一次性配置。而第二类数据操作的指令码主要用于设置数据指针的位置,现实信息的实现与清楚等。这两类指令码从使用上并无太大区别,后续我们将详细说明。

2、驱动设计与实现

  我们已经了解了LCD1602的基本情况,接下来我们将给予对LCD1602的基本了解设计LCD602的驱动程序。

2.1、对象定义

  在使用一个对象之前我们需要获得一个对象。同样的我们想要LCD1602液晶显示屏就需要先定义LCD1602液晶显示屏的对象。

2.1.1、对象的抽象

  我们要得到LCD1602液晶显示屏对象,需要先分析其基本特性。一般来说,一个对象至少包含两方面的特性:属性与操作。接下来我们就来从这两个方面思考一下LCD1602液晶显示屏的对象。
  先来考虑属性,作为属性肯定是用于标识或记录对象特征的东西。我们来考虑LCD1602液晶显示屏对象属性。对于LCD1602显示屏,它主要的功能就是显示信息,为了标识当前的状态,我们将状态寄存器的值作为对象的属性。
  接着我们还需要考虑LCD1602液晶显示屏对象的操作问题。首先我们需要控制LCD1602的3个控制引脚以实现对LCD1602的控制,但这些控制引脚的操作都与具体的操作平台相关,所以我们将其作为对象的操作来实现。同样的我们还需要向LCD1602发送命令和数据以及从LCD1602获取消息,而读取和发送都是依赖于具体的操作平台的所以我们将其作为LCD1602的两个操作。我们对LCD1602进行操作,免不了要进行时序控制,所以我们需要有延时操作,但我们都明白演示操作依赖于具体的软硬件平台,所以我们将延时处理函数也作为对象的操作。
  根据上述我们对LCD1602液晶显示屏的分析,我们可以定义LCD1602液晶显示屏的对象类型如下:

/* 定义LCD1602的对象类型 */
typedef struct LCD1602Object {
  uint8_t status;
  LCD1602PinSetType *PinHandle;
  void(*SendByte)(uint8_t data);
  uint8_t(*GetByte)(void);
  void (*Delayus)(volatile uint32_t period);    //微秒延时函数
  void (*Delayms)(volatile uint32_t nTime);     //毫秒秒延时函数
}LCD1602ObjectType;

2.1.2、对象初始化

  我们知道,一个对象仅作声明是不能使用的,我们需要先对其进行初始化,所以这里我们来考虑LCD1602液晶显示屏对象的初始化函数。一般来说,初始化函数需要处理几个方面的问题。一是检查输入参数是否合理;二是为对象的属性赋初值;三是对对象作必要的初始化配置。据此我们设计LCD1602液晶显示屏对象的初始化函数如下:

/*对显示屏作初始化配置*/
void LCD1602Initialization(LCD1602ObjectType *lcd,              //LCD1602对象指针
                           LCD1602PinSetType *PinHandle,         //控制引脚操作函数指针数组
                           LCD1602SendByteType sendByte,        //发送一个字节函数指针
                           LCD1602GetByteType getByte,          //读取一个字节函数指针
                           LCD1602DelayType delayus,            //微秒延时函数指针
                           LCD1602DelayType delayms             //毫秒延时函数指针
                             )
{
  if((lcd==NULL)||(PinHandle==NULL)||(sendByte==NULL)||(getByte==NULL)||(delayus==NULL)||(delayms==NULL))
  {
    return;
  }
  
  lcd->PinHandle=PinHandle;
  lcd->SendByte=sendByte;
  lcd->GetByte=getByte;
  lcd->Delayus=delayus;
  lcd->Delayms=delayms;
  
  lcd->Delayus(15);
  WriteCommandToLCD1602(lcd,0x38);
  lcd->Delayms(5);
  WriteCommandToLCD1602(lcd,0x38);
  lcd->Delayms(5);
  WriteCommandToLCD1602(lcd,0x38);
  
  /*后续需要检测BUSY,等待10Mms*/
  lcd->Delayms(10);
  WriteCommandToLCD1602(lcd,0x38);//显示模式设置
  lcd->Delayms(10);
  WriteCommandToLCD1602(lcd,0x08);//显示关闭
  lcd->Delayms(10);
  WriteCommandToLCD1602(lcd,0x01);//显示清屏
  lcd->Delayms(10);
  WriteCommandToLCD1602(lcd,0x06);//显示光标移动位置
  lcd->Delayms(10);
  WriteCommandToLCD1602(lcd,0x0C);//显示开及光标设置

  lcd->PinHandle[LCD1602_EN](Low);
  
  lcd->status=ReadStatusFromLCD1602(lcd);
}

2.2、对象操作

我们已经完成了LCD1602液晶显示屏对象类型的定义和对象初始化函数的设计。但我们的主要目标是获取对象的信息,接下来我们还要实现面向LCD1602液晶显示屏的各类操作。

2.2.1、读数据操作

  我们需要从LCD1602液晶显示屏获取一定的数据,包括读取状态信息和数据信息,唯一的区别只是RS控制引脚的电平,其他的操作都一样,读取数据的时序图如下所示:

HAL库驱动esp8266_初始化_02

  根据我们前面的描述及上面的时序图,我们可以实现获取状态信息及数据的操作函数如下:

/*从LCD1602读状态*/
static uint8_t ReadStatusFromLCD1602(LCD1602ObjectType *lcd)
{
  uint8_t status;
  lcd->PinHandle[LCD1602_RS](Low);
  lcd->PinHandle[LCD1602_RW](High);
  lcd->PinHandle[LCD1602_EN](High);
  
  lcd->Delayus(20);
  status=lcd->GetByte();
  lcd->PinHandle[LCD1602_EN](Low);
  lcd->Delayus(5);
  return status;
}
/*从LCD1602读数据*/
static uint8_t ReadDataFromLCD1602(LCD1602ObjectType *lcd)
{
  uint8_t data;
  lcd->PinHandle[LCD1602_RS](High);
  lcd->PinHandle[LCD1602_RW](High);
  lcd->PinHandle[LCD1602_EN](High);
  
  lcd->Delayus(20);
  data=lcd->GetByte();
  lcd->PinHandle[LCD1602_EN](Low);
  lcd->Delayus(5);
  return data;
}

2.2.2、写数据操作

  我们想要在LCD1602显示屏上显示我们想要的消息就需要向LCD1602显示屏发送命令和数据。发送数据和发送命令的区别仅是RS控制引脚的操作电平不同,具体的操作时序如下所示:

HAL库驱动esp8266_函数指针_03

/*向LCD1602写指令*/
static void WriteCommandToLCD1602(LCD1602ObjectType *lcd,uint8_t command)
{
  lcd->PinHandle[LCD1602_RS](Low);
  lcd->PinHandle[LCD1602_RW](Low);

  lcd->SendByte(command);

  lcd->PinHandle[LCD1602_EN](High);
  lcd->Delayus(20);
  lcd->PinHandle[LCD1602_EN](Low);
  lcd->Delayus(5);
}
/*向LCD1602写数据*/
static void WriteDatatoLCD1602(LCD1602ObjectType *lcd,uint8_t data)
{
  lcd->PinHandle[LCD1602_RS](High);
  lcd->PinHandle[LCD1602_RW](Low);

  lcd->SendByte(data);

  lcd->PinHandle[LCD1602_EN](High);
  lcd->Delayus(20);
  lcd->PinHandle[LCD1602_EN](Low);
  lcd->Delayus(5);
}

3、驱动的使用

  我们已经实现了LCD1602液晶显示屏驱动程序,在接下来我们还需要设计一个简单的应用验证这一驱动设计是否正确。

3.1、声明并初始化对象

  使用基于对象的操作我们需要先得到这个对象,所以我们先要使用前面定义的LCD1602液晶显示屏对象类型声明一个LCD1602液晶显示屏对象变量,具体操作格式如下:
  LCD1602ObjectType lcd;
  声明了这个对象变量并不能立即使用,我们还需要使用驱动中定义的初始化函数对这个变量进行初始化。这个初始化函数所需要的输入参数如下:

LCD1602ObjectType *lcd,              //LCD1602对象指针
LCD1602PinSetType *PinHandle,         //控制引脚操作函数指针数组
LCD1602SendByteType sendByte,        //发送一个字节函数指针
LCD1602GetByteType getByte,          //读取一个字节函数指针
LCD1602DelayType delayus,            //微秒延时函数指针
LCD1602DelayType delayms             //毫秒延时函数指针

  对于这些参数,对象变量我们已经定义了。主要的是我们需要定义几个函数,并将函数指针作为参数。这几个函数的类型如下:

/*定义引脚操作函数指针类型*/
typedef void (*LCD1602PinSetType)(uint8_t value);
/*定义发送一个字节操作函数指针*/
typedef void(*LCD1602SendByteType)(uint8_t data);
/*定义读取一个字节操作函数指针*/
typedef uint8_t(*LCD1602GetByteType)(void);
/*定义延时操作函数指针*/
typedef void (*LCD1602DelayType)(volatile uint32_t time);

  对于这几个函数我们根据样式定义就可以了,具体的操作可能与使用的硬件平台有关系。控制引脚的操作函数实际是3个,组成一个函数指针数组,分别对应RS、RW、EN控制引脚。具体函数定义如下:

LCD1602PinSetType pinSets[3]={RsPinOperation,RwPinOperation,EnPinOperation};

/*RS控制引脚操作*/
static void RsPinOperation(uint8_t value)
{
  HAL_GPIO_WritePin(GPIOD,GPIO_PIN_7,(GPIO_PinState)value);
}

/*RW控制引脚操作*/
static void RwPinOperation(uint8_t value)
{
  HAL_GPIO_WritePin(GPIOD,GPIO_PIN_8,(GPIO_PinState)value);
}

/*EN控制引脚操作*/
static void EnPinOperation(uint8_t value)
{
  HAL_GPIO_WritePin(GPIOD,GPIO_PIN_9,(GPIO_PinState)value);
}

/*从LCD1602读一个字节*/
static uint8_t ReadByteFromLCD(void)
{
  uint8_t data=0;
  
  data=(uint8_t)(GPIOD->IDR);
  
  return data;
}
                               
/*向LCD1602写一个字节*/
static void WriteByteToLCD(uint8_t data)
{
  uint16_t value=GPIOD->ODR;
  
  value=(value&&0xFF00)||data;
  
  GPIOD->ODR=value;
}

  对于延时函数我们可以采用各种方法实现。我们采用的STM32平台和HAL库,所以毫秒延时函数可以直接使用HAL_Delay()函数。微秒延时函数采用我们编写的delayus于是我们可以调用初始化函数如下:

LCD1602Initialization(&lcd,              //LCD1602对象指针
                 pinSets,         //控制引脚操作函数指针数组
                 WriteByteToLCD,        //发送一个字节函数指针
                 ReadByteFromLCD,          //读取一个字节函数指针
                 Delayus,            //微秒延时函数指针
                 HAL_Delay             //毫秒延时函数指针
               );

3.2、基于对象进行操作

  我们定义了对象变量并使用初始化函数给其作了初始化。接着我们就来考虑操作这一对象获取我们想要的数据。我们在驱动中已经将获取数据并转换为转换值的比例值,接下来我们使用这一驱动开发我们的应用实例。

/*在LCD1602中显示数据*/
void LCD1602Display(void)
{
  float temp=20.5;
  float pres=101.35;
  float humi=34.6;
  
  LCD1602DisplayClear(&lcd,LCD1602_AllLine),
  
  Lcd1602ContentDisplay(&lcd,0x80, "T%0.1fC,P%0.1fKPa,H%0.1f%%", temp,pres, humi);
  Lcd1602ContentDisplay(&lcd,0xC0, "T%0.1fC,P%0.1fKPa,H%0.1f%%", temp,pres, humi);
}

  我们将显示器清屏,然后再每一行都显示温度、压力和湿度数据。

4、应用总结

  我们已经设计并实现了LCD1602的驱动程序,并在此基础上设计了简单的验证应用。我们可以正常读写LCD1602显示屏,并且在LCD1602显示屏正确心事我们想要的信息,说明我们的驱动设计是没有问题的。
  在使用驱动时,有一点需要注意。因为在初始化函数中,对控制引脚的操作采用的时函数指针数组,但这个数组元素的顺序不是随意的,而是必须与枚举类型LCD1602PinType中定义的顺序一致才能正确操作。