Raspberry Pi でI2C: キャラクタLCD

秋月のACM1602NI-FLW-FBW-M01を使ったキャラクタLCDモジュールを購入しました。
Raspberry Piに接続するも、当初思ったように動かなくて困っていたのですが こちらのページ に答えがあり、
要するにモジュールのPICマイコンの処理が間に合っていないという残念な状態のようです。
I2Cクロックを下げてとりあえず動いたので、メモっときます。

接続

LCDモジュールのコネクタは7ピンありますが、Raspberry Pi 側からは VDD(3.3V), GND, SDL, SDA を引っ張ってくればOKです。
コントラスト(V0)は調整可能にしておいた方がいいですけど、0.3Vくらいを入れておけばとりあえず丁度いいです。


注意点

上記ページに書かれている通り、このLCDを使うにはいくつかの注意点があります。

  • リードアクセスしない(i2c-toolsのi2cdetectとかもダメ)
  • I2Cクロックを50kHzくらいに落とす

以上を踏まえ、進めます。

ドライバのロード

i2c-bcm2708ドライバのオプションに転送レートを指定してロードします。
i2c関係のドライバがロードされている場合は、一旦アンロード

% sudo rmmod i2c-dev i2c-bcm2708

I2Cクロックを50kHzに指定してモジュールをロード

% sudo modprobe i2c-bcm2708 baudrate=50000
% sudo modprobe i2c-dev

これを恒久的な設定とする場合は、/etc/modprobe.d/i2c.conf として以下の内容で作成します。

options i2c_bcm2708 baudrate=50000

設定とデータ送信

i2c-toolsを使用して表示テストをしてみます。

% sudo apt-get i2c-tools
I2Cアドレス

このモジュールでI2CをしゃべっているのはPICのコントローラなのですが
(この人があまりきちんとしていないためにいろいろが制限ある)、以下のアドレスにマップされています。

スレーブアドレス: 0x50
レジスタアドレス
 コマンド: 0x00
 データ: 0x80

コントロールコマンド

コマンド内容については、製品添付の仕様書は印刷が読みづらい上に必要な説明があんまりないので、
LCDコントローラの仕様書を見たほうがいいです。

以下はi2csetを使った設定例です。

% sudo /usr/sbin/i2cset -y 1 0x50 0x00 0x01     # clear display
% sudo /usr/sbin/i2cset -y 1 0x50 0x00 0x38     # function set: 8bit, 2lines, 5x8
% sudo /usr/sbin/i2cset -y 1 0x50 0x00 0x0c     # display on, no cursor
% sudo /usr/sbin/i2cset -y 1 0x50 0x00 0x06     # direction=INC, no shift
データ書き込み

同じくi2csetを使ってデータを書き込みます。

% sudo /usr/sbin/i2cset -y 1 0x50 0x80 0x41     # 'A'
% sudo /usr/sbin/i2cset -y 1 0x50 0x80 0x42     # 'B'
% sudo /usr/sbin/i2cset -y 1 0x50 0x80 0x43     # 'C'
% sudo /usr/sbin/i2cset -y 1 0x50 0x00 0xc0     # move to second line
% sudo /usr/sbin/i2cset -y 1 0x50 0x80 0x61     # 'a'
% sudo /usr/sbin/i2cset -y 1 0x50 0x80 0x62     # 'b'
% sudo /usr/sbin/i2cset -y 1 0x50 0x80 0x63     # 'c'

以上で基本的な動作が確認できました。

サンプルアプリ

簡単にCでコード書いてみました。コマンドのビット定義はこんなに気合入れるつもり無かったんですが...
busyloop() でタイミング調整してるところは時間は適当なのであしからず。

/*
 *            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *                    Version 2, December 2004
 * 
 * Copyright (C) 2013 penkoba
 *
 * Everyone is permitted to copy and distribute verbatim or modified
 * copies of this license document, and changing it is allowed as long
 * as the name is changed.
 *
 *            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 *
 *  0. You just DO WHAT THE FUCK YOU WANT TO.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>

#define RPI_I2C_DEV             "/dev/i2c-1"    /* Rev.2 */
#define ACM1602N1_SLAVE         0x50
#define ACM1602N1_ADR_CMD       0x0
#define ACM1602N1_ADR_DAT       0x80

#define ACM1602N1_CMD_CLEAR_DISP                0x01
#define ACM1602N1_CMD_RETURN_HOME               0x02

#define ACM1602N1_CMD_ENTRY_MODE_ID_INC         0x02
#define ACM1602N1_CMD_ENTRY_MODE_ID_DEC         0x00
#define ACM1602N1_CMD_ENTRY_MODE_S_SHIFT        0x01
#define ACM1602N1_CMD_ENTRY_MODE_S_NOSHIFT      0x00

#define ACM1602N1_CMD_ENTRY_MODE(id, s) \
        (0x04 | \
         ACM1602N1_CMD_ENTRY_MODE_ID_##id | \
         ACM1602N1_CMD_ENTRY_MODE_S_##s)

#define ACM1602N1_CMD_DISP_ON_OFF_DISP_ON       0x04
#define ACM1602N1_CMD_DISP_ON_OFF_DISP_OFF      0x00
#define ACM1602N1_CMD_DISP_ON_OFF_CUR_ON        0x02
#define ACM1602N1_CMD_DISP_ON_OFF_CUR_OFF       0x00
#define ACM1602N1_CMD_DISP_ON_OFF_BLINK_ON      0x01
#define ACM1602N1_CMD_DISP_ON_OFF_BLINK_OFF     0x00

#define ACM1602N1_CMD_DISP_ON_OFF(disp, cursor, blink) \
        (0x08 | \
         ACM1602N1_CMD_DISP_ON_OFF_DISP_##disp | \
         ACM1602N1_CMD_DISP_ON_OFF_CUR_##cursor | \
         ACM1602N1_CMD_DISP_ON_OFF_BLINK_##blink)

#define ACM1602N1_CMD_CUR_DISP_SHIFT_SC_DISP    0x08
#define ACM1602N1_CMD_CUR_DISP_SHIFT_SC_CUR     0x00
#define ACM1602N1_CMD_CUR_DISP_SHIFT_DIR_RIGHT  0x04
#define ACM1602N1_CMD_CUR_DISP_SHIFT_DIR_LEFT   0x00

#define ACM1602N1_CMD_CUR_DISP_SHIFT(sc, dir) \
        (0x10 | \
         ACM1602N1_CMD_CUR_DISP_SHIFT_SC_##sc | \
         ACM1602N1_CMD_CUR_DISP_SHIFT_DIR_##dir)

#define ACM1602N1_CMD_FUNCTION_DATA_LEN_8BIT            0x10
#define ACM1602N1_CMD_FUNCTION_DATA_LEN_4BIT            0x00
#define ACM1602N1_CMD_FUNCTION_LINE_NUMBER_2LINES       0x08
#define ACM1602N1_CMD_FUNCTION_LINE_NUMBER_1LINE        0x00
#define ACM1602N1_CMD_FUNCTION_FONT_5x11                0x04
#define ACM1602N1_CMD_FUNCTION_FONT_5x8                 0x00

#define ACM1602N1_CMD_FUNCTION_SET(data_len, line_number, font) \
        (0x20 | \
         ACM1602N1_CMD_FUNCTION_DATA_LEN_##data_len | \
         ACM1602N1_CMD_FUNCTION_LINE_NUMBER_##line_number | \
         ACM1602N1_CMD_FUNCTION_FONT_##font)

#define ACM1602N1_CMD_SET_CGRAM_ADR(adr)        (0x40 | adr)
#define ACM1602N1_CMD_SET_DDRAM_ADR(adr)        (0x80 | adr)

static void busyloop(unsigned long n)
{
        volatile unsigned long i;       /* volatile not to be optimized */
        for (i = 0; i < n; i++);
}

static int acm_cmd_write(int fd, unsigned char cmd)
{
        const unsigned char buf[2] = {ACM1602N1_ADR_CMD, cmd};

        if (write(fd, buf, 2) != 2) {
                fprintf(stderr, "ACM1602N1 command write error: 0x%02x\n", cmd);
                return -1;
        }

        return 0;
}

static int acm_dat_write(int fd, const unsigned char *dat, int n)
{
        unsigned char buf[n * 2];
        int i;

        for (i = 0; i < n; i++) {
                buf[i * 2] = ACM1602N1_ADR_DAT;
                buf[i * 2 + 1] = dat[i];
        }
        if (write(fd, buf, n * 2) != n * 2) {
                fprintf(stderr,
                        "ACM1602N1 data write error: data=[0x%02x, ...]\n",
                        dat[0]);
                return -1;
        }

        return 0;
}

static int init_acm(int fd)
{
        if (acm_cmd_write(fd, ACM1602N1_CMD_CLEAR_DISP) < 0)
                return -1;
        busyloop(0x10000);      /* clear display takes some millisecs */
        if (acm_cmd_write(fd,
                          ACM1602N1_CMD_FUNCTION_SET(8BIT, 2LINES, 5x8)) < 0)
                return -1;
        if (acm_cmd_write(fd, ACM1602N1_CMD_DISP_ON_OFF(ON, OFF, OFF)) < 0)
                return -1;
        if (acm_cmd_write(fd, ACM1602N1_CMD_ENTRY_MODE(INC, NOSHIFT)) < 0)
                return -1;

        return 0;
}

int main(void)
{
        int fd;
        const unsigned char test_dat1[] = "good morning!";
        const unsigned char test_dat2[] = {
                0xb5, 0xca, 0xd6, 0xb3, 0xba, 0xde, 0xbb, 0xde, 0xb2, 0xcf, 0xbd, 0x00
        };

        if ((fd = open(RPI_I2C_DEV, O_RDWR)) < 0) {
                printf("Faild to open i2c port\n");
                return 1;
        }
        if (ioctl(fd, I2C_SLAVE, ACM1602N1_SLAVE) < 0) {
                printf("Unable to get bus access to talk to slave\n");
                return 1;
        }

        if (init_acm(fd) < 0)
                return 1;
        if (acm_dat_write(fd, test_dat1, sizeof(test_dat1) - 1) < 0)
                return 1;
        if (acm_cmd_write(fd, ACM1602N1_CMD_SET_DDRAM_ADR(0x40)) < 0)
                return 1;
        if (acm_dat_write(fd, test_dat2, sizeof(test_dat2) - 1) < 0)
                return 1;

        return 0;
}