こんにちは。
まだ2023年の初頭だと思い込んで暮らしていたら、いつのまにか師走を迎えていた @nakochi です。
たまたま秋月電子で「ACM2004D-FLW-FBW-IIC(P-17381)」というI2C接続の20x4キャラクタLCDを購入して遊ぼうとしたところ、思いのほか情報やライブラリがなく、どうしようもないのでGoLang用ライブラリを作ったという話を軽く書こうと思います。
I2Cとは
もはや電子工作勢には説明不要というか自分の理解の方が浅すぎてマサカリが飛んで来かねない気もするのですが、電子工作をしない方のために一応軽くI2Cについて触れます。
I2Cはシリアル通信の規格です。データ線(SDA)とクロック線(SCL)、あとは電源のVcc及びGNDのみで通信ができ、電子工作の際に配線をめちゃくちゃ減らせる便利な規格です。
ちなみにI2Cと書いて「アイ・スクエアド・シー」と読みます。アイツーシーじゃないです。
キーポイント
ライブラリを書いている際に重要だと感じたポイントたちです。わりとこの液晶とコントローラー固有のポイントだとは思います。
初期化時に20ミリ秒スリープする
InitLcdメソッド中にいくつか time.Sleep(20 * time.Millisecond)
という記述があります。これは動かなかったからお気持ちで追加したわけではなく、ドキュメントにしっかりとXXミリ秒以上待てと記載されているためです。今回はArduino向けコードに習ってすべて20ミリ秒でスリープをかけています。
何かしら失敗したらI2Cを閉じる
忘れがちですが閉じないとプロセスがデバイスを掴んだままになりかねないです。
20文字を超えて出力され続けないようにする
コード中以下のように文字数を制限しています。
for i := 0; i < len(data); i++ {
if i >= 20 {
return nil
}
...略
}
LCDの一行あたりの上限が20文字であること、またそれを無視して出力し続けると例えば一行目であれば三行目に連続して出力されてしまうため制限をかけます。
サンプルコード
かなりシンプルに扱えるようにしてみました。
スレーブアドレスは対象ホストに i2c-tools
を入れて i2cdetect -y ${バス番号}
で取得できます。ライブラリ側に入れてしまってもよかったのですが、同じコントローラー(RW1063)を搭載した液晶等で使い回しできるように、あえて初期化時に引数で取れる設計としました。今回購入したACM2004Dは 0x3F
となっています。
func main() {
// acm2004d.InitLcd(スレーブアドレス, バス番号)
lcd, err := acm2004d.InitLcd(0x3F, 1)
if err != nil {
lcd.Close()
os.Exit(1)
}
// lcd.Write(行番号, 表示文字のバイト配列([]byte(str)でStringからのキャストでも可))
err = lcd.Write(1, []byte("Hello, World! KDDI Technology"))
// エラー処理省略(上の if err != nil と同じ中身)
lcd.Close()
os.Exit(0)
}
動作イメージ
ステマではありません。理芽が大好きな個人の感想です。理芽チすき
ライブラリのコード全文
コードのライセンスはWTFPLにて配布します。ご自由にご利用ください。
なお、かなり急いで作ったため結構雑であり、そのためこのあと少しずつリファクタリングしていこうと思っています。
package acm2004d
import (
"fmt"
"time"
"github.com/d2r2/go-i2c"
)
type LCD struct {
I2C *i2c.I2C
}
func InitLcd(addr uint8, bus int) (*LCD, error) {
i2c, err := i2c.NewI2C(addr, bus)
if err != nil {
i2c.Close()
return nil, err
}
// Func Set
time.Sleep(20 * time.Millisecond)
_, err = i2c.WriteBytes([]byte{0x00, 0x38})
if err != nil {
i2c.Close()
return nil, err
}
// Clear Display
time.Sleep(20 * time.Millisecond)
_, err = i2c.WriteBytes([]byte{0x00, 0x01})
if err != nil {
i2c.Close()
return nil, err
}
// Return Home
time.Sleep(20 * time.Millisecond)
_, err = i2c.WriteBytes([]byte{0x00, 0x02})
if err != nil {
i2c.Close()
return nil, err
}
// Display On
time.Sleep(20 * time.Millisecond)
_, err = i2c.WriteBytes([]byte{0x00, 0x0C})
if err != nil {
i2c.Close()
return nil, err
}
time.Sleep(20 * time.Millisecond)
res := &LCD{
I2C: i2c,
}
return res, nil
}
func (LCD *LCD) Write(line int, data []byte) error {
var err error
// Set lines
switch line {
case 1:
_, err = LCD.I2C.WriteBytes([]byte{0x00, 0x80})
case 2:
_, err = LCD.I2C.WriteBytes([]byte{0x00, 0xC0})
case 3:
_, err = LCD.I2C.WriteBytes([]byte{0x00, 0x94})
case 4:
_, err = LCD.I2C.WriteBytes([]byte{0x00, 0xD4})
default:
return fmt.Errorf("Error: Undefined Line Number")
}
if err != nil {
return err
}
for i := 0; i < len(data); i++ {
if i >= 20 {
return nil
}
_, err = LCD.I2C.WriteBytes([]byte{0x40, data[i]})
if err != nil {
return err
}
}
return nil
}
func (LCD *LCD) Close() error {
return LCD.I2C.Close()
}
余談
今回は d2r2/go-i2c
をI2Cライブラリとして利用していますが、そもそもLinuxのI2Cはデバイスをファイルとして取得して読み書きを行うことで扱えるため、直接読み書きしてもシンプルに実装できたなと実装してから思いました。時間のあるときにライブラリへの依存をなくしてみようと思います。
おわりに
今回作成にあたって、秋月電子通商さんの販売ページにて配布されているドキュメント及びArduino用サンプルコードをとても参考にさせていただきました。
秋月電子通商さんたすかりすぎる。ありがとうございました。