STM32: 通过I2C总线驱动1602 LCD显示模块

老早之前就做了一块基于STM32F030F4P6 MCU的开发板,并且也预留了一个I2C总线接口。至于开发板上的这个I2C总线能否正常使用,却是从来没有验证过。

这几天又买了不少的1602 LCD液晶显示模块与转接板,通过转接板,可以使用I2C总线接口控制液晶显示模块。这使得我有机会去验证STM32F030F4P6开发板上的I2C单元能否正常工作。

关于STM32F030F4P6开发板的相关信息,请看这里:

STM32: 含nRF24L01无线模块的STM32F030F4P6开发板

  • 关于1602 LCD显示模块

1602 LCD可显示两行,每行可显示16个字符,每个字符由5×8的点阵构成。

2016_12_09_lcd1602

(图片来自:http://www.buydisplay.com/default/3-3v-5v-lcd-module-16×2-1602-character-display-black-on-white)

可以是如下字符集(这些字符是固化在LCD控制器中的,不能随意修改。但是0x000 ~ 0x007这8个字符用户可以自定义):

2016_12_09_char_patern

(图片来自:http://www.buydisplay.com/download/ic/SPLC780.pdf)

- 1602 LCD模块与MCU的接口

1602 LCD所使用的控制器是与Hitachi HD44780控制器兼容的,模块的管脚定义如下:

管脚名称 说明
1 VSS Ground
2 VDD +3.3V or +5V
3 VO Contrast adjustment
4 RS Register Select: RS=0: Command, RS=1: Data
5 RW Read/Write: RW=0: Write, RW=1: Read
6 E Clock Enable. Falling edge triggered
7 D0 Bit 0 (Not used in 4-bit operation)
8 D1 Bit 1 (Not used in 4-bit operation)
9 D2 Bit2 (Not used in 4-bit operation)
10 D3 Bit3 (Not used in 4-bit operation)
11 D4 Bit 4
12 D5 Bit 5
13 D6 Bit 6
14 D7 Bit 7
15 A Backlight Anode(+)
16 K Backlight Cathode(-)

NOTE:

1. 对于供电电压,有3.3V与5V之分,需要5V供电电压的模块,不能用3.3V的供电电压,否则LCD上无显示。两种版本区别如下:

2016_12_09_lcd_3v3_5v

(图片来自:http://www.buydisplay.com/default/3-3v-5v-lcd-module-16×2-1602-character-display-black-on-white)

2. 将5v模块改3.3v的方法

请看这里:https://mewpro.cc/2014/12/13/utilize-unused-footprints-on-5v-lcd-modules/

2016_12_20_1602_lcd_5v_to_3v3

2. 对比度调整(VO)会影响LCD的显示,如果LCD无显示,可以试试调整该电压。

- 4-bit与8-bit模式选择(https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller):

Selecting 4-bit or 8-bit mode requires careful selection of commands. There are two primary considerations. First, with D3-D0 unconnected, these lines will always appear low (0b0000) to the HD44780 when it is in 8-bit mode. Second, the LCD may initially be in one of three states:
- (State1) 8-bit mode
- (State2) 4-bit mode, waiting for the first set of 4 bits
- (State3) 4-bit mode, waiting for the second set of 4 bits
State3 may occur, for example, if a prior control was aborted after sending only the first 4 bits of a command while the LCD was in 4-bit mode.
The following algorithm ensures that the LCD is in the desired mode:
1. Set D7-D4 to 0b0011, and toggle the enable bit.
    1. If in State1, the LCD will see the command as 0b0011_0000, and thus remain in 8-bit mode (State1).
    2. If in State2, the LCD will simply latch the value 0b0011 into bits 7-4 and then move to State3.
    3. If in State3, the LCD will latch the value 0b0011 into bits 3-0, and then execute a random command based on the (unknown to us) values in bits 7-4, after which it will either be in State1 (if the unknown bits happened to be 0b0011), or State2 (if the unknown bits were anything else).
2. Repeat the above, setting D7-D4 to 0b0011 and toggling the enable bit again.
    1. If in State1, the LCD will remain in 8-bit mode (State1) just as above.
    2. If in State2, it will latch the value into bits 7-4 and move to State3, just as above.
    3. If in State3, the LCD will latch the value into bits 3-0 just as above and execute a command. However, the command will no longer be random, but will be the 0b0011 that was latched from State2 in the previous iteration. Thus, the LCD will switch to 8-bit mode and change to State1.
3. The LCD is now in either State1 or State 3. Repeat the previous step one more time.
    1. If in State1, the LCD will remain in 8-bit mode (and thus State1).
    2. The LCD can no longer be in State2 at this point.
    3. If in State3, the LCD will latch the value into bits 3-0 and execute a command, which will be the 0b0011 that was latched from State2 in the previous iteration, thus switching the LCD to 8-bit mode and State1.
4. Now that the LCD is definitely in 8-bit mode, it can be switched to 4-bit mode if desired. To do so, set D7-D4 to 0b0010 and toggle the enable bit. This will leave the LCD in 4-bit mode, configured for a single line and 5x8 fonts.
5. Issue any desired additional Function Set commands to specify the number of lines and the font to use, being sure to use the appropriate value for bit 4 so as to remain in the desired mode (0 for 4-bit and 1 for 8-bit).
Once in 4-bit mode, character and control data are transferred as pairs of 4-bit "nibbles" on the upper data pins, D7-D4. The four most significant bits (7-4) must be written first, followed by the four least significant bits (3-0).

简单来说,往命令寄存器写入三次0b0011_xxxx, 就可以确保LCD控制器工作在8-bit模式,之后如果写入0b0010_xxxx就可以将控制器切换到4-bit模式。

在4-bit模式下,只需要使用D4, D5, D6, D7这四条数据线,发送数据时先发送高4位再发送低4位到这几个引脚。

-指令集

2016_12_09_lcd_instruction_set

(图片来自:https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller)

NOTE:

这里需要注意各个指令执行时所需要的时间,执行Clear display与Cursor home的时间较长,需要1.52ms。

  • LCD液晶模块转接板

转接板使用的主芯片为PCF8574T, 是一个I2C总线8位远程IO扩展口芯片:

2016_12_09_lcdiic

(原图来自:http://www.playrobot.com/display/1148-arduino-iici2clcd1602.html)

转接板的原理图如下:

2016_12_09_pcf8574_sch

可以看到扩展出来的8个IO与LCD模块引脚之间的关系,LCD模块需要工作在4-bit模式。可通过I2C总线写入数据,也可以通过I2C总线读取数据(busy flag, CGRAM & DDRAM),同时还可以控制背光灯!

关于PCF8574模块需要特别注意的是:

1. 模块I2C地址的分配,对于PCF8574T芯片来说,模块默认的地址为0x3f。

2. IO引脚作为输入时,要将相应的IO引脚设置为高电平。

最后,LCD模块与转接板装配图如下:

2016_12_09_i2clcd

(图片来自:https://alselectro.wordpress.com/2016/05/12/serial-lcd-i2c-module-pcf8574/)

  • 软件实现及相关代码

由于执行LCD模块命令时需要耗费一定的时间,下面再分析一下对于PCF8574进行一次的读写需要消耗多长的时间。由于PCF8574 I2C总线最大的工作频率为100kHz,所以会将MCU的I2C接口配置为100kHz。

-写入操作

写入时序如下:

2016_12_09_pcf8574_write

往LCD控制器中写一次数据(4-bit模式)需要进行如下操作:

1. 设置P0(RS), P1(RW), P4(D4), P5(D5), P6(D6), P7(D7)

2. 设置P2(E/CS) 为高电平

3. 设置P2(E/CS)为低电平 (执行命令)

也就是说PCF8574的IO被更新了3次:

\\如果每次只写入一个数据:

1<S> + 8<slave address + write> + 1<ACK> + 8<data out>+1<ACK> = 19clk = 190uS

\\如果一次写入三个数据:

1<S> + 8<slave address + write> + 1<ACK> + (8<data out>+1<ACK>) * 3 = 34clk = 340uS

相关的代码如下:

static void __lcdiicWrite4bitLocked(LCDIICDriver *drvp, uint8_t val) {
	PCF8574Driver *portdrvp = drvp->config->drvp;

	drvp->port.u.dt = val & 0x0f;
	pcf8574SetPort(portdrvp, drvp->port.v);

	drvp->port.u.en = 0x01;
	pcf8574SetPort(portdrvp, drvp->port.v);
	(drvp->delayUs)(1);

	drvp->port.u.en = 0x00;
	pcf8574SetPort(portdrvp, drvp->port.v);
	(drvp->delayUs)(1);
}

static void lcdiicWriteLocked(LCDIICDriver *drvp, lcdiic_bus_mode_t mode, uint8_t val) {
	__lcdiicWrite4bitLocked(drvp, val >> 4);

	if (mode != LCDIIC_BUS_MODE_8BIT) {
		__lcdiicWrite4bitLocked(drvp, val);
	}

	// Max execution time(!Clear display & !Return home) is 37us when f(OSC) is 270kHz
	(drvp->delayUs)(37);
}

NOTE:

由于EN/CS的周期只需要短短的500ns, 其实是可以将多个的写入操作进行合并,当然实现的时候没有注意到这点:

2016_12_09_lcd_write_timing

2016_12_09_lcd_write

[2016-12-18 23:44:29] 更新后的写入操作可以是这样的:

static void lcdiicWriteLocked(LCDIICDriver *drvp, lcdiic_bus_mode_t mode, uint8_t val) {
    PCF8574Driver *portdrvp = drvp->config->drvp;
    uint8_t buf[6], cnt = 0;

    drvp->port.u.dt = (val >> 4) & 0x0F;
    buf[cnt++] = drvp->port.v;

    drvp->port.u.en = 0x01;
    buf[cnt++] = drvp->port.v;

    drvp->port.u.en = 0x00;
    buf[cnt++] = drvp->port.v;

    if (mode == LCDIIC_BUS_MODE_8BIT) {
        goto done;
    }

    drvp->port.u.dt = (val >> 0) & 0x0F;
    buf[cnt++] = drvp->port.v;

    drvp->port.u.en = 0x01;
    buf[cnt++] = drvp->port.v;

    drvp->port.u.en = 0x00;
    buf[cnt++] = drvp->port.v;

done:
    pcf8574SetPortMulti(portdrvp, 1, buf, cnt);

    // Max execution time(!Clear display & !Return home) is 37us when f(OSC) is 270kHz
    drvp->delayUs(37);
}

可以看到只执行了一次的pcf8574SetPortMulti()就可以将一个字节的数据写入到1602 LCD模块中。

-读取操作

读取时序如下:

2016_12_09_pcf8574_read

可以一次只读取一个字节也可以连续读取。

从LCD控制器中读取一次数据(4-bit模式)需要进行如下操作:

1. 设置P0(RS), P1(RW), P4(D4)=1, P5(D5)=1, P6(D6)=1, P7(D7)=1

2. 设置P2(E/CS) 为高电平

3. 读取数据

3. 设置P2(E/CS)为低电平

相关的代码如下:

static msg_t __lcdiicRead4bitLocked(LCDIICDriver *drvp, uint8_t *val) {
	PCF8574Driver *portdrvp = drvp->config->drvp;
	lcdiic_port_cfg portval;
	msg_t ret;

	drvp->port.u.dt = 0x0F;
	pcf8574SetPort(portdrvp, drvp->port.v);

	drvp->port.u.en = 0x01;
	pcf8574SetPort(portdrvp, drvp->port.v);
	(drvp->delayUs)(1);

	ret = pcf8574GetPort(portdrvp, &portval.v);
	if (ret == MSG_OK) *val = portval.u.dt;

	drvp->port.u.en = 0x00;
	pcf8574SetPort(portdrvp, drvp->port.v);
	(drvp->delayUs)(1);

	return ret;
}

static msg_t lcdiicReadLocked(LCDIICDriver *drvp, lcdiic_bus_mode_t mode, uint8_t *val) {
	msg_t ret = __lcdiicRead4bitLocked(drvp, val);

	if (mode != LCDIIC_BUS_MODE_8BIT && ret == MSG_OK) {
		uint8_t tmp;
		ret = __lcdiicRead4bitLocked(drvp, &tmp);
		if (ret == MSG_OK) *val = (*val) << 4 | tmp;
	}

	return ret;
}

[2016-12-18 23:44:29] 更新后的读取操作如下:

static msg_t lcdiicReadLocked(LCDIICDriver *drvp, lcdiic_bus_mode_t mode, uint8_t *val) {
    PCF8574Driver *portdrvp = drvp->config->drvp;
    lcdiic_port_cfg portval;
    msg_t ret;
    uint8_t buf[2];

    drvp->port.u.dt = 0x0f;
    buf[0] = drvp->port.v;
    drvp->port.u.en = 0x01;
    buf[1] = drvp->port.v;
    pcf8574SetPortMulti(portdrvp, 1, buf, 2);

    ret = pcf8574GetPortOnce(portdrvp, &portval.v);
    if (ret != MSG_OK) goto out;

    *val = portval.u.dt << 4;

    if (mode == LCDIIC_BUS_MODE_8BIT) goto done;

    drvp->port.u.en = 0x00;
    buf[0] = drvp->port.v;
    drvp->port.u.en = 0x01;
    buf[1] = drvp->port.v;
    pcf8574SetPortMulti(portdrvp, 1, buf, 2);

    ret = pcf8574GetPortOnce(portdrvp, &portval.v);
    if (ret != MSG_OK) goto out;

    *val |= portval.u.dt;

done:
    drvp->port.u.en = 0x00;
    pcf8574SetPortOnce(portdrvp, 1, drvp->port.v);

    if (drvp->port.u.rs != 0x00 && drvp->port.u.rw != 0x01) {
        drvp->delayUs(37);
    }

out:
    return ret;
}

[2016-12-18 23:44:29] 完整的代码

请从这里下载:

https://github.com/brobwind/chibios_stm32f030f4_dev_lcdiic

-运行效果

2016_12_10_lcdiic

-已知问题

设置显示的坐标有问题:只能设置每行的开始位置,影响的函数有lcdiicMoveTo()及lcdiicDrawText()。

  • 相关的参考文档
  1. http://www.chibios.org/dokuwiki/doku.php
  2. http://www.buydisplay.com/download/ic/SPLC780.pdf
  3. https://www.sparkfun.com/datasheets/LCD/HD44780.pdf
  4. https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller
  5. http://www.nxp.com/documents/data_sheet/PCF8574_PCF8574A.pdf
  6. http://www.ti.com/lit/ds/symlink/pcf8574.pdf

评论

2 Comments on "STM32: 通过I2C总线驱动1602 LCD显示模块"

提醒我
avatar

David
游客
David
1 年 5 月 之前

您好,請教有無完整的代碼可以分享呢?謝謝您。

wpDiscuz