普冉PY32系列(六) 通过I2C接口驱动PCF8574扩展的1602LCD

发布时间 2023-03-22 19:13:58作者: Milton

目录

1602 LCD

1602LCD 是工业上常用的模块, 在工厂交通运输设备上经常能见到. 驱动芯片为 HD44780, 1602LCD 的字符显示为两行, 每行16个字符, 字符基于5×8的像素矩阵

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/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. 屏幕不显示

不显示的原因有很多, 如果确认代码和接线无误, 可能的原因有

  1. 检查1602LCD的供电电压是不是5V, 在3.3V下无法驱动, 只有背光没有字符
  2. 检查I2C地址是否正确. 查看串口扫描到的实际的设备I2C地址, 是否和程序中的地址一致, 通常情况下, PCF8574T 的地址是 0x4E, PCF8574AT 的地址是 0x7E

2. 字符显示乱码

HD44780对启动的指令顺序和延时是有要求的, 可以参考其数据手册的P45, 如果延时不够或指令顺序不正确, 会导致屏幕未进入4-bit模式而导致显示错乱. 对于部分屏幕, 启动时需要增大延时, 如果等待时间不足, 会导致输出乱码.

参考