本帖最后由 xld0932 于 2022-4-15 17:46 编辑
#申请原创# @21小跑堂
在早期做终端的带显示密码键盘项目的时候,就是使用的LCD1602液晶显示屏,结合矩阵按键和语音芯片,在每次要求输入密码时第一行显示Please Input Password并滚动显示,第二行用星号代替显示,同时伴随着“请输入密码”的语音提示;在输入完成后,通过PS/2或者是RS-232又或者是CH372实现的USB接口与电脑通讯,把输入的密码上传到PC上;现在回想当前的项目功能可能很简单了。
现在市面上常用LCD1602的工作电压有3.3V和5.0V两种,对于MM32F0140这个芯片来说,都完全能够与之电压相匹配。LCD1602液晶屏可以显示2行16个字符,可以是LCD内置的固定字符,也可以显示用户自定义的字符。LCD的控制接口由电源、背光、对比度偏置电压、RS、RW、EN和D0~D7组成;其中RS是数据/命令选择控制,RS为低电平时写入指令,RS为高电平时写入数据;RW是读写操作选择控制,RW为高电平时读取数据,RW为低电平时写入数据;EN是模块使能信号控制,下降沿触发;D0~D7则是双向的数据接口,当MCU的端口引脚资源紧张时,也可以使用4位数据线D4~D7进行通讯。
本文主要讲述了LCD1602在常规8位数据线下的操作和结合I2C扩展IO口芯片在4位数据线下的操作,以及使用MM32F0140核心板在调试LCD1602过程中所有遇到的问题和解决办法,最后通过点阵工具取模实现一个显示的小游戏。
1、常规8位数据线下的显示实现
常规8位数据线是D0~D7,为了方便程序的实现,我们使用MCU的PB0~PB7端口引脚与之进行连接,除此之外,我们还需要RS、RW和EN这3个控制信号线,我们使用了MCU的PA5、PA6、PA7与之相连接;最后就是LCD的电源和背光了,我们直接根据LCD的工作电压进行供电就可以了;至于VO这个对比度偏置电压的设置,根据数据手册上的描述,接正电源时对比度最弱,接地电源时对比度最强,我们暂时先接正电源进行调试。
驱动程序: void LCD1602A_WriteCMD(uint8_t Command)
{
/* 当RS和RW同时为低电平时可以写入指令或者显示地址 */
LCD1602A_RS_L();
LCD1602A_RW_L();
GPIO_Write(GPIOB, (GPIO_ReadOutputData(GPIOB) & 0xFF00) | Command);
/* 当EN端由高电平跳变成低电平时, 液晶模块执行命令 */
LCD1602A_EN_H();
SysTick_DelayMS(2);
LCD1602A_EN_L();
}
void LCD1602A_WriteDAT(uint8_t Data)
{
/* 当RS为高电平RW为低电平时可以写入数据 */
LCD1602A_RS_H();
LCD1602A_RW_L();
GPIO_Write(GPIOB, (GPIO_ReadOutputData(GPIOB) & 0xFF00) | Data);
/* 当EN端由高电平跳变成低电平时, 液晶模块执行命令 */
LCD1602A_EN_H();
SysTick_DelayMS(2);
LCD1602A_EN_L();
}
void LCD1602A_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 控制线:RS/RW/EN */
RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOA, ENABLE);
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_WriteBit(GPIOA, GPIO_Pin_5, Bit_RESET);
GPIO_WriteBit(GPIOA, GPIO_Pin_6, Bit_RESET);
GPIO_WriteBit(GPIOA, GPIO_Pin_7, Bit_RESET);
/* 数据线:D0~D7 */
RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOB, ENABLE);
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 |
GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
LCD1602A_WriteCMD(0x38); SysTick_DelayMS(10);
LCD1602A_WriteCMD(0x38); /* 显示模式设置 */
LCD1602A_WriteCMD(0x08); /* 显示关闭 */
LCD1602A_WriteCMD(0x01); /* 显示清屏 */
LCD1602A_WriteCMD(0x06); /* 显示光标移动设置 */
LCD1602A_WriteCMD(0x0C); /* 显示开及光标设置 */
LCD1602A_DisplayString(0, 0, "Hello World!");
LCD1602A_DisplayString(0, 1, "21ic:xld0932");
TASK_Append(TASK_ID_LCD1602A, LCD1602A_Handler, 1000);
}
显示效果:
烧录程序后无显示,或显示不清晰的问题? 在将编写好的LCD1602代码烧录到MCU运行后,发现LCD1602根本就没有显示;因为使用的是MM32F0140核心板,再加上一个单独的LCD1602显示模块,并没有调节VO对比度的偏置电压电路,只能接在GND或者VCC上;当接到VCC的时候,LCD1602显示太弱了,看不出任何显示;而当接到GND时,LCD1602显示又太强了,能够影影约约的看到显示的字符,这个就是高对比度时产生的阴影现象。
在没有VO对比度偏置电压调节电路的情况下,如何实现对VO的调节呢? 因此为了解决对比度的问题,在没有硬件调节电路的时候,我们就来考虑一下,是不是可以用软件的方式来解决……正好在前面做LED实验时,我们可以通过PWM的方式来达到对LED亮度调节的效果,正好也可以用这个方法来实现对LCD1602偏置电压的控制,这样我们只需要占用一个PWM引脚就可以实现了,而且这个对比度偏置电压可以通过软件进行设定和调节,相比于硬件的方式节省了电路,也省去了产品在出厂时由于硬件参数不同需要对每一个VO都进行调节的步骤;在MCU资源允许的情况下,只需要一个带有PWM功能的引脚就可以实现了。 void LCD1602A_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
/* VO液晶显示偏压 */
RCC_APB1PeriphClockCmd(RCC_APB1ENR_TIM2, ENABLE);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 1000 - 1;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 450 - 1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
TIM_OC4Init(TIM2, &TIM_OCInitStructure);
TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM2, ENABLE);
TIM_Cmd(TIM2, ENABLE);
TIM_CtrlPWMOutputs(TIM2, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOA, ENABLE);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_2); /* TIM2_CH4 */
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
2、通过I2C扩展IO口芯片,4位数据线下的显示实现
在MCU资源紧张的情况下,我们可以使用LCD1602四线的通讯方式,但需要通过功能设置命令来配置工作方式为4位数据接口。在4位数据接口方式下,原先8位的数据需要分2次进行传输,先传输数据的高4位,再传输数据的低4位。但即便是4位数据接口方式,再加上RS、RW、EN这三个控制线的话,还需要占用MCU七个端口引脚资源;为了再节省些MCU资源,我们这个小节结合LCD1602硬件引入了PCF8574T这个I2C扩展IO口芯片,这样只需要MCU的I2C两根引脚就可以扩展出8个IO口出来,而且这些IO口都是双向的,用于控制LCD1602刚好满足要求,具体的电路可参考附件中的原理图。
驱动程序: void PCF8574T_I2C_Write(uint8_t Data)
{
I2C_SendData(I2C1, Data);
while(!I2C_GetFlagStatus(I2C1, I2C_STATUS_FLAG_TFE));
I2C_GenerateSTOP(I2C1, ENABLE);
while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_STOP_DET));
}
void LCD1602A_WriteCMD(uint8_t Command)
{
uint8_t Value = 0x00;
/* 当RS和RW同时为低电平时可以写入指令或者显示地址, 先写高4位 */
Value = ((Command & 0xF0) << 0) | 0x08;
PCF8574T_I2C_Write(Value);
/* 当EN端由高电平跳变成低电平时, 液晶模块执行命令 */
PCF8574T_I2C_Write(Value | 0x04);
SysTick_DelayMS(2);
PCF8574T_I2C_Write(Value & 0xFB);
/* 当RS和RW同时为低电平时可以写入指令或者显示地址, 再写低4位 */
Value = ((Command & 0x0F) << 4) | 0x08;
PCF8574T_I2C_Write(Value);
/* 当EN端由高电平跳变成低电平时, 液晶模块执行命令 */
PCF8574T_I2C_Write(Value | 0x04);
SysTick_DelayMS(2);
PCF8574T_I2C_Write(Value & 0xFB);
}
void LCD1602A_WriteDAT(uint8_t Data)
{
uint8_t Value = 0x00;
/* 当RS和RW同时为低电平时可以写入指令或者显示地址, 先写高4位 */
Value = ((Data & 0xF0) << 0) | 0x09;
PCF8574T_I2C_Write(Value);
/* 当EN端由高电平跳变成低电平时, 液晶模块执行命令 */
PCF8574T_I2C_Write(Value | 0x04);
SysTick_DelayMS(2);
PCF8574T_I2C_Write(Value & 0xFB);
/* 当RS和RW同时为低电平时可以写入指令或者显示地址, 再写低4位 */
Value = ((Data & 0x0F) << 4) | 0x09;
PCF8574T_I2C_Write(Value);
/* 当EN端由高电平跳变成低电平时, 液晶模块执行命令 */
PCF8574T_I2C_Write(Value | 0x04);
SysTick_DelayMS(2);
PCF8574T_I2C_Write(Value & 0xFB);
}
void LCD1602A_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C1_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1ENR_I2C1, ENABLE);
I2C_StructInit(&I2C1_InitStructure);
I2C1_InitStructure.I2C_Mode = I2C_Mode_MASTER;
I2C1_InitStructure.I2C_OwnAddress = 0;
I2C1_InitStructure.I2C_Speed = I2C_Speed_STANDARD;
I2C1_InitStructure.I2C_ClockSpeed = 100000;
I2C_Init(I2C1, &I2C1_InitStructure);
I2C_Send7bitAddress(I2C1, 0x4E, I2C_Direction_Transmitter);
I2C_Cmd(I2C1, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOB, ENABLE);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_1);
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_Init(GPIOB, &GPIO_InitStructure);
LCD1602A_WriteCMD(0x28); SysTick_DelayMS(10);
LCD1602A_WriteCMD(0x28); /* 显示模式设置 */
LCD1602A_WriteCMD(0x08); /* 显示关闭 */
LCD1602A_WriteCMD(0x01); /* 显示清屏 */
LCD1602A_WriteCMD(0x06); /* 显示光标移动设置 */
LCD1602A_WriteCMD(0x0C); /* 显示开及光标设置 */
LCD1602A_DisplayString(0, 0, "Hello World!");
LCD1602A_DisplayString(0, 1, "21ic:xld0932");
TASK_Append(TASK_ID_LCD1602A, LCD1602A_Handler, 1000);
}
显示效果:
3、显示小游戏
前面的两个例程显示的都是LCD1602内置的字体,LCD1602内置的字符库中00~0F是没有定义的,留给我们显示自字义字符用,但只能使用00~07或者08~0F之一。我们显示的内容可能通过对DDRAM地址的指定,再写入需要显示的内容字符的下标号就可以显示;用户自定义的字符则是需要先通过程序,将要显示的字模数据写入到LCD中去,这个操作可以通过CGRAM的指令来实现,具体可以参考下面的程序。
在LCD1602中显示的每个字符的大小都是5*8像素的,由于LCD硬件排布,字符间距和行间距都是特别的明显,你可以显示出一个10*8的汉字,但这个汉字需要占用2个字符,这两个字符又有明显的间隔,所以显示很不协调;在实际应用当中,也很少有这样的应用;我们找到了一个使用一个字符就可以显出完整内容,又带有趣味性的内容,共有21种类型,每一个张都是由不同的点经过排列组合而成的,而LCD1602对于单个像素点的显示又比较明显,所以使用LCD1602来显示效果很不错。
我们将所有的点阵排列组合通过字符取模软件进行取模得到一个数组;通过对CGRAM的操作,将这些数据写入到LCD当中并测试显示,如下图所示:
在下面演示的小游戏中只是举出了2张直接比大小:通过程序中的系统运行时间作为随机数种子,随机产生两个显示字符的下标,将这个字符的字库通过CGRAM指令写入到LCD当中,再通过DDRAM指令将这两个字符显示在LCD第1行居中的位置,驱动代码如下所示: void LCD1602A_DisplayChar(uint8_t x, uint8_t y, char ch)
{
uint8_t Address = 0;
x %= 16; /* 每一行最多16个字符, 对应的是0~15 */
y %= 2; /* 最多显示2行数据, 对应的是0~1 */
if(y == 0)
{
Address = x + 0x80; /* 设置第1行的数据指针起点 */
}
else
{
Address = x + 0xC0; /* 设置第2行的数据指针起点 */
}
LCD1602A_WriteCMD(Address); /* 设置数据指针起点 */
LCD1602A_WriteDAT(ch); /* 写入要显示的数据 */
}
void LCD1602A_DisplayString(uint8_t x, uint8_t y, char *str)
{
x %= 16; /* 每一行最多16个字符, 对应的是0~15 */
y %= 2; /* 最多显示2行数据, 对应的是0~1 */
while(*str != '\0') /* 取出字符串的数据显示到液晶屏上 */
{
LCD1602A_DisplayChar(x++, y, *str++);
if(x == 16) /* 如果显示到某一行的最后一个位置 */
{
x = 0; /* 将列位置移动到第一个字符的位置 */
if(y == 0) /* 显示换行 */
{
y = 1; /* 之前在第1行现在切换到第2行显示 */
}
else
{
y = 0; /* 之前在第2行现在切换到第1行显示 */
}
}
}
}
void LCD1602A_CGRAM(uint8_t *Buffer)
{
for(uint8_t i = 0; i < 8; i++)
{
for(uint8_t j = 0; j < 8; j++)
{
LCD1602A_WriteCMD(0x40 + i * 8 + j);
LCD1602A_WriteDAT(Buffer[i * 8 + j]);
}
}
}
uint8_t USER_FONT[21][8] =
{
{0x0A,0x0A,0x0A,0x00,0x0A,0x0A,0x0A,0x00}, /* 天 */
{0x04,0x00,0x00,0x00,0x00,0x00,0x04,0x00}, /* 地 */
{0x0A,0x00,0x0A,0x00,0x0A,0x00,0x0A,0x00}, /* 人 */
{0x08,0x04,0x02,0x00,0x00,0x00,0x04,0x00}, /* 鹅 */
{0x0A,0x04,0x0A,0x00,0x0A,0x04,0x0A,0x00}, /* 梅 */
{0x04,0x04,0x00,0x0A,0x00,0x04,0x04,0x00}, /* 长三 */
{0x0A,0x00,0x00,0x00,0x00,0x00,0x0A,0x00}, /* 板凳 */
{0x0A,0x04,0x0A,0x00,0x0A,0x0A,0x0A,0x00}, /* 斧头 */
{0x0A,0x00,0x0A,0x00,0x0A,0x0A,0x0A,0x00}, /* 红头十 */
{0x04,0x00,0x00,0x00,0x0A,0x0A,0x0A,0x00}, /* 高脚七 */
{0x04,0x00,0x00,0x00,0x0A,0x04,0x0A,0x00}, /* 铜锤六 */
{0x08,0x04,0x02,0x00,0x0A,0x0A,0x0A,0x00}, /* 黑九 */
{0x0A,0x00,0x0A,0x00,0x0A,0x04,0x0A,0x00}, /* 红九 */
{0x08,0x04,0x02,0x00,0x0A,0x04,0x0A,0x00}, /* 弯八 */
{0x0A,0x00,0x00,0x00,0x0A,0x0A,0x0A,0x00}, /* 平八 */
{0x08,0x04,0x02,0x00,0x0A,0x00,0x0A,0x00}, /* 红七 */
{0x0A,0x00,0x00,0x00,0x0A,0x04,0x0A,0x00}, /* 白七 */
{0x04,0x00,0x00,0x00,0x0A,0x00,0x0A,0x00}, /* 红五 */
{0x08,0x04,0x02,0x00,0x00,0x00,0x0A,0x00}, /* 白五 */
{0x04,0x00,0x00,0x00,0x00,0x00,0x0A,0x00}, /* 三 */
{0x0A,0x00,0x00,0x00,0x0A,0x00,0x0A,0x00}, /* 六 */
};
void LCD1602A_Handler(void)
{
srand(SysTick_Tick);
/* 随机产生两个牌九并更写入到CGRAM中 */
uint32_t Index1 = rand() % 21;
for(uint8_t j = 0; j < 8; j++)
{
LCD1602A_WriteCMD(0x40 + j);
LCD1602A_WriteDAT(USER_FONT[Index1][j]);
}
uint32_t Index2 = rand() % 21;
for(uint8_t j = 0; j < 8; j++)
{
LCD1602A_WriteCMD(0x48 + j);
LCD1602A_WriteDAT(USER_FONT[Index2][j]);
}
LCD1602A_WriteCMD(0x01); /* 显示清屏 */
/* 在第1行居中显示2个牌九 */
for(uint8_t i = 0; i < 2; i++)
{
LCD1602A_DisplayChar(i+7, 0, i);
}
}
显示效果:
附件: |