普冉PY32系列(六) 通过I2C接口驱动PCF8574扩展的1602LCD
目录
- 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介
- 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境
- 普冉PY32系列(三) PY32F002A资源实测 - 这个型号不简单
- 普冉PY32系列(四) PY32F002A/003/030的时钟设置
- 普冉PY32系列(五) 使用JLink RTT代替串口输出日志
- 普冉PY32系列(六) 通过I2C接口驱动PCF8574扩展的1602LCD
- 普冉PY32系列(七) SOP8,SOP10,SOP16封装的PY32F002A/PY32F003管脚复用
- 普冉PY32系列(八) GPIO模拟和硬件SPI方式驱动无线收发芯片XN297LBW
- 普冉PY32系列(九) GPIO模拟和硬件SPI方式驱动无线收发芯片XL2400
- 普冉PY32系列(十) 基于PY32F002A的6+1通道遥控小车I - 综述篇
- 普冉PY32系列(十一) 基于PY32F002A的6+1通道遥控小车II - 控制篇
- 普冉PY32系列(十二) 基于PY32F002A的6+1通道遥控小车III - 驱动篇
- 普冉PY32系列(十三) SPI驱动WS2812全彩LED
- 普冉PY32系列(十四) 从XL2400迁移到XL2400P
1602 LCD
1602LCD 是工业上常用的模块, 在工厂交通运输设备上经常能见到. 1602LCD 的字符显示为两行, 每行16个字符, 字符基于5×8的像素矩阵
驱动芯片为 HD44780, 工作电压为5V, 市场上还有使用型号 HD44780U 的1602LCD, 这个型号可以兼容3.3V的电压
PIN脚功能
Pin | Name | Function |
---|---|---|
1 | Ground | 地 (0V) |
2 | Vcc | 供电 5V (4.7V – 5.3V), 注意不能用3.3V供电 |
3 | Vo / VEE | 对比度调节. 连接一个可变电阻, 过高无法分辨显示的字符, 过低字符太淡无显示 |
4 | RS | 寄存器选择(Register Select), 低电平为命令寄存器, 高电平为数据寄存器 |
5 | Read/write | 读写选择, 低电平写入, 高电平读取 |
6 | Enable | EN 闲时处于低电平, 当需要执行指令前几个毫秒将EN拉高, 执行完再拉低 |
7~14 | DB0~DB7 | 8-bit 数据pin |
15 | Led+ | LED 背光电源 |
16 | Led- | LED 背光接地 |
RS (Register Select)
1602LCD有两组寄存器, 命令寄存器和数据寄存器, RS用于数据和命令寄存器的切换
实际使用时, 需要配合 EN 和 R/W
- 在 EN = 1, R/W = 0, RS = 1 时, 往数据寄存器写入的字符会用于展示.
- EN = 1, R/W = 0, RS = 0 时, 往命令寄存器写入, 用于发送指令, 例如: 初始化, 清空屏幕, 设置光标位置, 控制显示等
指令编码
以下是各指令位的说明
/*-------------------------------------------------------------
* Instruction D7 D6 D5 D4 D3 D2 D1 D0
* ==============================================
* Display clear 0 0 0 0 0 0 0 1
* Cursor home 0 0 0 0 0 0 1 *
* Entry Mode Set 0 0 0 0 0 1 I/D S
* Display On/Off 0 0 0 0 1 D C B
* Curs/Disp shift 0 0 0 1 S/C R/L * *
* Function Set 0 0 1 DL N F * *
* CG RAM addr set 0 1 ---------Acg---------
* DD RAM addr set 1 -------------Add---------
*
* Meaning:
* * - nonvalid bit
* Acg - CG RAM address (CHARACTER GENERATOR)
* Add - DD RAM address (DATA DISPLAY)
* AC - adress counter
*
* I/D - 1-increment, 0-decrement
* S - 1-display shift, 0-no display shift
* D - 1-display ON, 0-display OFF
* C - 1-cursor ON, 0-cursor OFF
* B - 1-blink ON, 0-blink OFF
* S/C - 1-display shift, 0-cursor movement
* R/L - 1-right shift, 0-left shift
* DL - 1-8 bits data transfer, 0-4 bits data transfer
* N - 1-1/16 duty, 0-1/8 or 1/11 duty
* F - 1-5x10 dot matrix, 0-5x7 dot matrix
* BF - 1-internal operation in progress, 0-display ready
*
\**************************************************************/
配合 EN = 1, R/W = 0, RS = 0 时, 往命令寄存器写入, 可以执行以下指令
No. | Hex | Binary | 命令说明 |
---|---|---|---|
1 | 01 | 0000 0001 | 清除显示 |
2 | 02 | 0000 001x | 光标回原位 |
3 | 04 | 0000 0100 | 向左移动光标(两个bit分别控制方向左右, 光标还是屏幕) |
4 | 06 | 0000 0110 | 向右移动光标 |
5 | 05 | 0000 0101 | 向右移显示 |
6 | 07 | 0000 0111 | 向左移动显示 |
7 | 08 | 0000 1000 | 显示关闭, 光标关闭(三个bit分别控制屏幕显示, 光标显示, 光标闪烁) |
8 | 0A | 0000 1010 | 显示关闭, 光标打开 |
9 | 0C | 0000 1100 | 显示打开, 光标关闭 |
10 | 0E | 0000 1110 | 显示打开, 光标闪烁 |
11 | 0F | 0000 1111 | 显示打开, 光标闪烁 |
12 | 10 | 0001 00xx | 将光标位置向左移动(两个bit分别控制光标还是屏幕, 左移还是右移) |
13 | 14 | 0001 01xx | 将光标位置向右移动 |
14 | 18 | 0001 10xx | 将整个显示屏向左移动 |
15 | 1C | 0001 11xx | 将整个显示屏向右移动 |
16 | 28 | 0010 10xx | 4位, 2行, 5x8矩阵(三个bit分别控制4位还是8位,一行还是两行,5x10还是5x8) |
17 | 20 | 0010 00xx | 4位, 1行, 5x8矩阵 |
18 | 38 | 0011 10xx | 8位, 2行, 5×8 |
19 | 4x | 01xx xxxx | 设置 CGRAM 地址, 后面6个bit是地址, 在这个指令之后发送或接收数据 |
20 | 8x | 1xxx xxxx | 设置 DDRAM 地址, 后面7个bit是地址, 在这个指令之后发送或接收数据 |
21 | 80 | 1000 0000 | 将光标强制移动到开头(第一行) |
22 | C0 | 1100 0000 | 将光标强制移动到开头(第二行) |
显示自定义字符
自定义字符要在 CG-RAM 中设置. CG-RAM地址从 0x40 开始到 0x7F 共 64 byte, 可以创建8个字符, 每个字符8个byte. 在这些地址创建字符后, 就可以在LCD中显示.
CG-RAM 地址和命令
No. | Addr. | Command |
---|---|---|
0 | 0x40 | 0 |
1 | 0x48 | 1 |
2 | 0x50 | 2 |
3 | 0x58 | 3 |
4 | 0x60 | 4 |
5 | 0x68 | 5 |
6 | 0x70 | 6 |
7 | 0x78 | 7 |
上面的表中可以看到每个字符的起始地址及其打印命令, 例如第一个字符的地址是 [0x40, 0x47], 使用命令0
可以输出这个字符, 第二个字符的地址[0x48, 0x55], 使用命令1
输出.
自定义字符时, 每个字符是5x8的点阵, 5是列数, 8是行数. 对应字母b
的点阵可以表示为
char b[7] = {0x10,0x10,0x16,0x19,0x11,0x11,0x1E};
将其发送到对应的地址就可以创建字符.
I2C 接口 PCF8574 扩展板
因为1602LCD本身刷新率不高, 为节省IO, 可以通过 PCF8574 扩展模块, 将I2C协议转为并口输出. 1602屏直连MCU, 需要至少7个IO(RS, R/W, EN, 4位对应4根数据线)才能驱动起来, 使用 PCF8574 模块只需要2个IO口.
PCF8574 是一个IIC协议的IO口扩展芯片, 包含一个8位准双向口, 一个总线接口, 还有三条地址线. 每个IO口可以单独的分配为输入或者输出. 作为输入时, 可以用于监控中断或者键盘. 作为输出时, 可以用于驱动LED. 可以通过单独的寄存器读取输入端口状态或者配置输出端口状态. PCF8574 的三个地址管脚, 可以分配8个地址, 也就是同一个系统中可以最多存在8个这样的模块.
PCF8574 电流消耗很低, 并且输出锁存, 具有大电流驱动能力, 可直接驱动LED. 还带一个中断接线, 可以作为MCU的外部中断. 通过中断通知MCU 是否有数据输入, 因此 PCF8574 也可以作为一个单被控器.
通过 PCF8574 控制 1602LCD, 可以将命令发送到 I2C 接口, PCF8574 的输出与 1602LCD 的连接为
[tu]
VSS = Ground
VDD = Connects to VCC on the I2C header
VO = Display contrast. Connects to the potentiometer on the module
RS = P0 on PCF8574
RW = P1 on PCF8574
E = P2 on PCF8574
D0 – D3 no connects
D4 = P4 on PCF8574
D5 = P5 on PCF8574
D6 = P6 on PCF8574
D7 = P7 on PCF8574
A = Backlight Anode. Typically connects to 5V.
K = Backlight Cathode. Connects to ground
基于 PY32F0 的演示
硬件
- 已焊接 PCF8574 扩展模块的 1602LCD
- 基于 PY32F0 系列的开发板
- USB2TTL 用于观察地址输出
接线
大多数 PY32F0 都有 PF1/PF0, 可以通过复用将I2C接口换成其他的pin脚.
注意 PCF8574+1602LCD 供电电压为5V, 3.3V供电无法驱动字符显示. 如果 PY32F0 使用 3.3V 供电, 则需要另接5V电源, 与 PY32F0 共地即可.
PY32 PCF8574 1602 LCD USB2TTL
PF1/PA9 SCL
PF0/PA10 SDA
VCC -> 5V
GND GND -> GND GND
PA2 RX
PA3 TX
软件
以下的实现基于LL库, 完整的示例代码位于
https://github.com/IOsetting/py32f0-template/tree/main/Examples/PY32F0xx/LL/I2C/PCF8574_1602LCD
指令和地址的宏定义
/* I2C address
* - 7 bit slave address, left aligned, bits 7:1 are used, LSB bit is not used
* - 0x4E or 0x7E
*/
#define LCD1602_I2C_ADDR 0x7E
/* Delay in millisecond */
#define LCD1602_DELAY 5
/* Register selection */
#define PIN_RS (1 << 0)
/* Read/Write */
#define PIN_RW (1 << 1)
/* Chip enable */
#define PIN_EN (1 << 2)
/* Back light - might not be available on some PCF8574 modules */
#define BACKLIGHT (1 << 3)
/* Clear display */
#define LCD1602_CMD_CLEAR_DISPLAY 0b00000001
/* Move cursor home */
#define LCD1602_CMD_HOME 0b00000010
// Entry Mode, Set cursor/display moving direction
#define LCD1602_CMD_DIRECTION_RIGHT 0b00000110
#define LCD1602_CMD_DIRECTION_LEFT 0b00000100
#define LCD1602_CMD_DIRECTION_RIGHT_SHIFT 0b00000111
#define LCD1602_CMD_DIRECTION_LEFT_SHIFT 0b00000101
// Display mode
#define LCD1602_CMD_MODE_OFF 0b00001000
#define LCD1602_CMD_MODE_ON_CURSOR_OFF 0b00001100
#define LCD1602_CMD_MODE_ON_CURSOR_ON 0b00001110
#define LCD1602_CMD_MODE_ON_CURSOR_BLNK 0b00001111
// Cursor/Display Shift
#define LCD1602_CMD_CURSOR_MOVE_LEFT 0b00010000
#define LCD1602_CMD_CURSOR_MOVE_RIGHT 0b00010100
#define LCD1602_CMD_DISPLAY_SHIFT_LEFT 0b00011000
#define LCD1602_CMD_DISPLAY_SHIFT_RIGHT 0b00011100
/* Function set: 4-bit, 1 row, 5X8 matrix */
#define LCD1602_CMD_FUNC_4B_1L_5X8 0b00100000
/* Function set: 4-bit, 2 row, 5X8 matrix */
#define LCD1602_CMD_FUNC_4B_2L_5X8 0b00101000
/* Function set: 8-bit, 1 row, 5X8 matrix */
#define LCD1602_CMD_FUNC_8B_1L_5X8 0b00110000
/* Function set: 8-bit, 2 row, 5X8 matrix */
#define LCD1602_CMD_FUNC_8B_2L_5X8 0b00111000
/* Set/Read CGRAM address */
#define LCD1602_CMD_CGRAM_ADDR 0b01000000
/* Set/Read DDRAM address */
#define LCD1602_CMD_DDRAM_ADDR 0b10000000
/* First row address */
#define LCD1602_DDRAM_ROW0 0b10000000
/* Second row address */
#define LCD1602_DDRAM_ROW1 0b11000000
基础方法
发送指令, 数据和文本
ErrorStatus LCD_SendInternal(uint8_t lcd_addr, uint8_t data, uint8_t flags)
{
ErrorStatus status;
for(;;)
{
status = BSP_I2C_IsDeviceReady(lcd_addr, 5000);
if(status == SUCCESS)
{
break;
}
}
uint8_t up = data & 0xF0;
uint8_t lo = (data << 4) & 0xF0;
uint8_t data_arr[4];
data_arr[0] = up|flags|BACKLIGHT|PIN_EN;
data_arr[1] = up|flags|BACKLIGHT;
data_arr[2] = lo|flags|BACKLIGHT|PIN_EN;
data_arr[3] = lo|flags|BACKLIGHT;
status = BSP_I2C_MasterTransmit(lcd_addr, data_arr, sizeof(data_arr), 5000);
LL_mDelay(LCD1602_DELAY);
return status;
}
void LCD_SendCommand(uint8_t lcd_addr, uint8_t cmd)
{
LCD_SendInternal(lcd_addr, cmd, 0);
}
void LCD_SendData(uint8_t lcd_addr, uint8_t data)
{
LCD_SendInternal(lcd_addr, data, PIN_RS);
}
void LCD_SendString(uint8_t lcd_addr, char *str)
{
while (*str)
{
LCD_SendData(lcd_addr, (uint8_t)(*str));
str++;
}
}
初始化设置
void LCD_Init(uint8_t lcd_addr)
{
// need at least 40ms after power rises above 2.7V
LL_mDelay(50);
// start in 8-bit mode, 3 commands
LCD_SendCommand(lcd_addr, LCD1602_CMD_FUNC_8B_1L_5X8);
LCD_SendCommand(lcd_addr, LCD1602_CMD_FUNC_8B_1L_5X8);
LCD_SendCommand(lcd_addr, LCD1602_CMD_FUNC_8B_1L_5X8);
// set it to 4-bit mode, interface is still 8-bit
LCD_SendCommand(lcd_addr, LCD1602_CMD_FUNC_4B_1L_5X8);
// now interface is 4-bit, set it to 2 lines and 5x8 font
LCD_SendCommand(lcd_addr, LCD1602_CMD_FUNC_4B_2L_5X8);
// display & cursor home
LCD_SendCommand(lcd_addr, LCD1602_CMD_HOME);
// display on, right shift, underline off, blink off
LCD_SendCommand(lcd_addr, LCD1602_CMD_MODE_ON_CURSOR_BLNK);
// move direction right
LCD_SendCommand(lcd_addr, LCD1602_CMD_DIRECTION_RIGHT);
// clear display (optional here)
LCD_SendCommand(lcd_addr, LCD1602_CMD_CLEAR_DISPLAY);
}
功能操作
清除屏幕
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_CMD_CLEAR_DISPLAY);
移动光标, 输出文字
// move cursor to 0,0
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_DDRAM_ROW0|0);
LCD_SendString(LCD1602_I2C_ADDR, " Using 1602 LCD");
// move cursor to 1,0
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_DDRAM_ROW1|0);
LCD_SendString(LCD1602_I2C_ADDR, " over I2C bus");
通过 CGRAM 设置 自定义字符
// CGRAM test
for (i = 0; i < 8; i++)
{
LCD_SetCGRAM(LCD1602_I2C_ADDR, i, &cgrom[i * 8]);
}
展示自定义字符
for (i = 0; i < 8; i++)
{
LCD_SendData(LCD1602_I2C_ADDR, i);
LL_mDelay(200);
}
显示整体左右平移
// Shift display test
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_CMD_CLEAR_DISPLAY);
LL_mDelay(500);
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_DDRAM_ROW0|9);
LCD_SendString(LCD1602_I2C_ADDR, "Shift");
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_DDRAM_ROW1|8);
LCD_SendString(LCD1602_I2C_ADDR, "<<<->>>");
LL_mDelay(500);
for (i = 0; i < 8; i++)
{
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_CMD_DISPLAY_SHIFT_LEFT);
LL_mDelay(200);
}
LL_mDelay(500);
for (i = 0; i < 8; i++)
{
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_CMD_DISPLAY_SHIFT_RIGHT);
LL_mDelay(200);
}
左右移动光标
// Move cursor test
for (i = 0; i < 11; i++)
{
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_CMD_CURSOR_MOVE_LEFT);
LL_mDelay(200);
}
LL_mDelay(500);
for (i = 0; i < 12; i++)
{
LCD_SendCommand(LCD1602_I2C_ADDR, LCD1602_CMD_CURSOR_MOVE_RIGHT);
LL_mDelay(200);
}
常见问题
1. 屏幕不显示
不显示的原因有很多, 如果确认代码和接线无误, 可能的原因有
- 检查1602LCD的供电电压是不是5V, 在3.3V下无法驱动, 只有背光没有字符
- 检查I2C地址是否正确. 查看串口扫描到的实际的设备I2C地址, 是否和程序中的地址一致, 通常情况下, PCF8574T 的地址是 0x4E, PCF8574AT 的地址是 0x7E
2. 字符显示乱码
HD44780对启动的指令顺序和延时是有要求的, 可以参考其数据手册的P45, 如果延时不够或指令顺序不正确, 会导致屏幕未进入4-bit模式而导致显示错乱. 对于部分屏幕, 启动时需要增大延时, 如果等待时间不足, 会导致输出乱码.
参考
- https://www.electronicsforu.com/technology-trends/learn-electronics/16x2-lcd-pinout-diagram
- https://github.com/cehberlin/bajos/blob/master/bajos/PLATFORMS/CHARON/lcd.h
- 设置CG-RAM https://github.com/h0nzZik/school/blob/master/2013_fall/pv198/examples/avr-src-pv198/avr-src-pv198/04.04.lcd/lcd.c
- https://github.com/afiskon/stm32-i2c-lcd-1602/blob/master/Src/main.c