C ソースをパースしてシンボルを抽出する Go ライブラリ symc を作った

C ソースをパースしてシンボルを抽出する Go ライブラリ symc を作った

注意 このライブラリは個人的な問題を解決するために作成したツールのため、かなり適当です. ご使用の場合はご注意ください.

C ソースをパースして変数, 関数の定義や使用箇所を抽出する Go ライブラリを作成した.

https://github.com/kita127/symc1

作った背景

普段 C ソースを相手に仕事をしてるが, ソースを静的に解析する機会など多い. ソース内にある識別子がどこで定義されどこで使用されるかなどの情報がそういった解析時に欲しくなる時がある. ソースを解析して識別子情報だけを手軽に抽出するライブラリを作成すれば, 応用して色々なツールが作れるのではないかと考えた. ツール類は Go で作成する機会が多いため, Go ライブラリとして作成.

機能

主な機能は以下.

  • プリプロ展開後の C ソースを入力とする
    • プリプロ解析からやるのは環境による差異など色々困難なため妥協
    • プリプロ展開していないソースを入力した場合, エラーで死にます
  • 解析したモジュールに対して以下の情報を抽出する
    • 定義している変数
    • 定義している関数
    • extern 宣言している変数
    • プロトタイプ宣言している関数
    • 関数内で参照している変数
    • 関数内でコールしている関数の情報

使い方

使い方は以下のとおり.

package main

import (
    "fmt"

    "github.com/kita127/symc"
)

func main() {

    cSrc := `
int variable;

int extFunc( int a );

int func( void ){

    variable++;

    return extFunc( variable );
}

`

    module := symc.ParseModule(string(cSrc))
    fmt.Println(module)
}
$go build && ./main
Module : Symbols={ VariableDef : Name=variable, PrototypeDecl : Name=extFunc, FunctionDef : Name=func, Params=[], Symbols=[Assigne : Name=variable CallFunc : Name=extFunc, Args=[RefVar : Name=variable]] }

github.com/kita127/symc をインポートして, symc.ParseModule() に C ソースの文字列を渡せば, そのソースの識別子情報を AST の構造体として返してくれる.

上記では C ソース解析後, そのソースの情報を持つ AST である symc.Module 構造体のポインタを取得している.

取得した symc.Module 構造体のポインタは 変数の定義VariableDef や関数の定義FunctionDef 情報などを保持してる.

関数の定義情報の中にはその関数内で参照している変数などの情報も取得できる.

その他の機能

PrettyString()

解析した情報を少し見やすくする Pretty string 機能.

package main

import (
    "fmt"
    "io/ioutil"
    "os"

    "github.com/kita127/symc"
)

    cSrc := `
int variable;

int extFunc( int a );

int func( void ){

    variable++;

    return extFunc( variable );
}

`


func main() {
    module := symc.ParseModule(string(cSrc))
    fmt.Println(module.PrettyString())
}
>go build && ./main
DEFINITION variable
PROTOTYPE extFunc
FUNC func() {
    ASSIGNE variable
    extFunc(variable)
}

Inspect()

引数に解析済みの AST と処理関数を受け取り, AST を深さ優先で走査しながら引数の関数を適用する.
go 標準の ast パッケージにも同様の機能がありそちらを参考にした.

以下は AST を走査し, 変数定義の場合は変数名を大文字に変更する例.

引数で渡す関数が false を返すとそのタイミングで走査をやめることもできる. 以下の例は常に true を返すため, AST を走査し終わるまで走査をやめない.

symc.Symbol は変数定義や関数定義などシンボル情報が実装しているインターフェースであり, AST を構成している型は全て実装している. そのため Inspect() に渡す値は *Module 以外の *VariableDef*FunctionDef 等でも良い.

package main

import (
    "fmt"
    "strings"

    "github.com/kita127/symc"
)

func main() {

    cSrc := `
int g_var;

void func(int a)
{
    int hoge;
    char fuga;

    fuga = cal( hoge );
}
`
    module := symc.ParseModule(cSrc)

    // To uppercase only variable definitions.
    symc.Inspect(module, func(s symc.Symbol) bool {
        if v, ok := s.(*symc.VariableDef); ok {
            v.Name = strings.ToUpper(v.Name)
        }
        return true
    })

    fmt.Println(module)

}
$ go build && ./main
Module : Symbols={ VariableDef : Name=G_VAR, FunctionDef : Name=func, Params=[VariableDef : Name=A], Symbols=[VariableDef : Name=HOGE VariableDef : Name=FUGA 
Assigne : Name=fuga CallFunc : Name=cal, Args=[RefVar : Name=hoge]] }

ReduceLocalVar()

ローカル変数の情報は不要だったり, 冗長だったりする場合もあるため, 削除するメソッドを実装.

以下の例では, module の情報を生成後, ReduceLocalVar() メソッドでローカル変数の定義, 参照, 代入の情報のみ削除している.

package main

import (
    "fmt"
    "github.com/kita127/symc"
)

func main() {

    cSrc := `
int g_var;
static int s_var;
extern short ext_var;

void func(int arg)
{
    int i;
    int l_var;

    for(i = 0; i < 10; i++){
        g_var++;
    }

    l_var = ext_var;
    l_var += s_var;
    cal( l_var );

    g_var = l_var;
}
`

    module := symc.ParseModule(cSrc)

    fmt.Println("before reducing")
    fmt.Println(module.PrettyString())

    // Reducing local variable infomation.
    fmt.Println("after reducing")
    module.ReduceLocalVar()
    fmt.Println(module.PrettyString())
}
$ go build && ./main
before reducing
DEFINITION g_var
DEFINITION s_var
DECLARE ext_var
FUNC func(DEFINITION arg) {
    DEFINITION i
    DEFINITION l_var
    ASSIGNE i
    i
    ASSIGNE i
    ASSIGNE g_var
    ASSIGNE l_var
    ext_var
    ASSIGNE l_var
    s_var
    cal(l_var)
    ASSIGNE g_var
    l_var
}


after reducing
DEFINITION g_var
DEFINITION s_var
DECLARE ext_var
FUNC func(DEFINITION arg) {
    ASSIGNE g_var
    ext_var
    s_var
    cal()
    ASSIGNE g_var
}

注意点

Macgcc(clang?) でしか確認していないため他のコンパイラプリプロ展開したソースは試していない. モダンな C 言語の構文には対応していない. 多分, C90 くらいまでの構文はいけると思う(適当).

また冒頭の注意でも記載している通り, 個人的な問題を解決するために作成したライブラリのため, さほど厳格には作っていない. ご使用の場合はその辺りを留意した上でお願いします.

反省点とその他

ライブラリ作成途中に goyacc という Go 用のパーサジェネレータの存在を知った. 本来であればこういった構文解析系のライブラリは既存のパーサジェネレータ等を使うべきだが, かなり作ってから知ったのでそのまま作り切った. (ちなみに自前で字句解析・構文解析している)

一応, ユニットテストをこまめに作りながら作成を進めたので大体いけているとは思う.

Mac Catalina で 32bit GCC コンパイル

Mac Catalina で docker を使用して 32bit GCC クロススコンパイルをするための環境構築手順

背景

書籍「30日でできる!OS自作入門」を Mac(OS は Catalina) で構築する際に、 C 言語の 32bit GCCコンパイル環境構築の時点でつまずいた。 まずつまずきの一つ目はもともと HomeBrew でインストールできた i386 向けの GCC コンパイラ i386-elf-gcc が 2020/09/14 現在インストールできないということ。 参考にさせていただいた Catalina 向けのOS自作入門の記事ではこのコンパイラをインストールする手順となっていることが多いが、自分がトライした時点では このバージョンの GCC は HomeBrew ではなくなっており、代わりに x86_64-elf-gcc になっていた。 そして、この GCC で 32bit コンパイル用のオプションを指定してコンパイルを試みるも Bad CUP みいたなエラーが出てうまくいかなかった。

次に 32bit 用 GCC を自前でビルドしようとトライしましたが、これもビルドするためのライブラリなどがうまく入手できず諦めました。

色々調べてみたんですが、どうも Mac が 32bit 向けのツールやライブラリを非推奨にする流れっぽくそのあたりの影響もあるのかなと思います。

32bitOS_macOS

解決方法

あまり無茶なやり方をせずいい方法がないかなと考えました。

その結果 docker 上に ubuntu コンテンナを作成し、 そこに 32bit 向け GCC ビルド環境を構築しコンパイルすることで解決しました。

やりかた

  • Mac に docker をインストールする
    • この記事では説明を省きます
    • mac docker などで検索すればたくさん情報が見つかります
  • DockerHub から ubuntuリポジトリを取得する
    • 今回はバージョン 18.04 を取得する
docker pull ubunntu:18.04
  • ubuntu のコンテナを生成する
    • コンテナ名を ubuntu で作成
docker run -it -d --name ubuntu ubuntu:18.04
  • ubuntu コンテナ上で GCC をインストールする
# 作成した ubuntu の bash を起動。カレントディレクトリをホームディレクトリ(/root)にする
docker exe -it -w /root ubuntu /bin/bash
# GCC をインストールする
apt update && upgrade
apt install gcc
# リンカスクリプト(os.ld) をコンテナ内の /root に渡す
docker cp os.ld ubuntu:/root
  • 環境構築した ubuntu コンテナから新たにイメージファイルを作成する
    • ここまででクロスコンパイルするための環境が整ったため一旦このコンテナからイメージファイルを作成する
    • 再度同じ環境が欲しくなった時はこのイメージからコンテナを作成する
# ubuntu コンテナから ubuntu_for_xcomp というイメージファイルを作成する
docker commit ubuntu ubuntu_for_xcomp
# ubuntu の /root にコンパイル対象(hoge.c)を渡す
docker cp hoge.c ubuntu:/root
# コンパイル対象を 32bit 向けにコンパイル。リンカスクリプトは os.ld を指定する
docker exec -w /root ubuntu gcc -march=i486 -m32 -nostdlib -fno-pic -T os.ld -o output hoge.c
# コンパイル結果である実行形式(output)を取得する
docker cp ubuntu:/root/output output
  • 完成
    • 以上の手順でOS自作入門用の実行形式ができました
    • 後は適宜コンテナを停止・削除してください
# コンテナを停止
docker stop ubuntu
# コンテナを削除
docker rm ubuntu

Go で Go のコード整形(Beautifire)ツールを作る

Go で Go のコード整形(Beautifire)ツールを作る

前回の記事では私が作成した Go の識別子ケース変換ツール goconvcaseを取り上げました. その際に使用したパッケージや手法などはそれ以外の所謂 Beautifire や Formatter のような Go 向けの コード整形ツールを作成する際に使用できるイディオムかと思いました. 本記事では Go で Go のコード整形ツールを作成した際の手順をまとめます.

整形ツール作成の流れ

コードを整形するツールの処理フローは以下の通りです.

  1. ソースをパースしデータ(AST)化する
  2. パースしたデータを更新する
  3. 更新したデータをソースに戻す

f:id:kita127:20200708202055p:plain
go_beautifire_dfd

実際に作る

作成手順の詳細を goconvcase のような識別子を変換するツールの作成を元に説明します.

仕様は以下とします.

  • ソース中のスネークケースの識別子をキャメルケースに変換する
    • 識別子とは変数名や関数名など

初めに完成したソースお見せし, 各手順を後に説明します.

package main

import (
    "bytes"
    "go/ast"
    "go/format"
    "go/parser"
    "go/token"
    "io/ioutil"
    "log"
)

func main() {
    src, err := ioutil.ReadFile("src.go")
    if err != nil {
        log.Fatal(err)
    }

    // src.go をパースして node(AST) を得る
    fset := token.NewFileSet()
    node, err := parser.ParseFile(fset, "", src, parser.ParseComments)
    if err != nil {
        log.Fatal(err)
    }

    // node(AST) を走査しスネークケースの識別子をキャメルケースに変換する
    ast.Inspect(node, func(n ast.Node) bool {
        switch n.(type) {
        case *ast.Ident:
            ident := n.(*ast.Ident)
            if isSnakeCase(ident.Name) {
                ident.Name = convertSnakeToCamel(ident.Name)
            }
        }
        return true
    })

    // buf に更新したソース書いて標準出力
    var buf bytes.Buffer
    err = format.Node(&buf, fset, node)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(buf)
}

ソースをパースする

ソースを更新するためにはソースをデータ化した方が都合かいいためデータ化します. ソースをパースしてデータ化したものを AST(Abstract Syntax Tree) といい, 日本語では抽象構文木と言います.

AST はプログラムを構成する様々な要素から成るツリー状のデータです. 簡単に説明すれば AST は文をいくつか持ち文は式や文から成り, 式は識別子やリテラルや演算式・・・ という感じでツリーを形成します.

Go ソースをパースし AST を得るには go/parser パッケージが便利です.

以下のように parser.ParseFile() にパースしたいソースのテキストを渡して node(AST) を取得します.

package main

import (
    "bytes"
    "go/ast"
    "go/format"
    "go/parser"
    "go/token"
    "io/ioutil"
    "log"
)

func main() {
    src, err := ioutil.ReadFile("src.go")
    if err != nil {
        log.Fatal(err)
    }

    // src.go をパースして node(AST) を取得する
    fset := token.NewFileSet()
    node, err := parser.ParseFile(fset, "", src, parser.ParseComments)
    if err != nil {
        return "", err
    }
}

AST を更新する

取得した AST を更新します. やりたいこととしては AST を走査しスネークケースの識別子があればキャメルケースの識別子に変換することです.

AST を走査するための APIgo/ast パッケージにあります. 今回は ast.Inspec() を使用して走査&更新を行います.

    // node(AST) を走査しスネークケースの識別子をキャメルケースに変換する
    ast.Inspect(node, func(n ast.Node) bool {
        switch n.(type) {
        case *ast.Ident:
            ident := n.(*ast.Ident)
            if isSnakeCase(ident.Name) {
                ident.Name = convertSnakeToCamel(ident.Name)
            }
        }
        return true
    })

スネークケースか判定する関数 isSnakeCase() と スネークケースからキャメルケースに変換する関数 convertSnakeToCamel() の実装の詳細は省きます.

ast.Inspect() の詳細ですが, 第一引数は処理したい node をとり, その node のトップから深さ優先で走査します. 第二引数で走査中の各ノードに対して行いたい処理が書かれた関数を引数に取ります. 関数が true を返す限り node の次の子へと nil になるまで走査を続けます.

AST はプログラムを表現する様々な型の要素からなりますが, すべて interface ast.Node を実装しているため, 全ての要素を走査することができ, 全ての要素に対して引数の関数を適用できます.

上記のコードでは node が 識別子(*ast.Ident)の場合でスネークケースであればキャメルケースに変換しています.

これで, AST のトップからスネークケースの識別子は全てキャメルケースに変換されました.

ソースを出力する

AST の更新が完了したらソースに戻します. これも go/format パッケージを使えば一発です.

    // buf に更新したソース書いて標準出力
    var buf bytes.Buffer
    err = format.Node(&buf, fset, node)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(buf)

format.Node()io.Writer(&buf) と パースの際に作成した FileSet(fset) と更新した node を渡せば AST をソースにしたものを io.Writer に書き込みます.

おわり

以上が Go のソース内にあるスネークケースをキャメルケースに変換するツールのイメージです. 色々端折っているので実際のケース変換ツールは goconvcase をご覧ください.

Go でスネークケースやっちまった人のための識別子変換ツール

Go でスネークケースやっちまった人のための識別子変換ツール

何を作った?

Go ソース内のスネークケースの識別子をキャメルケースに変換するツールを作りました.

GitHub : https://github.com/kita127/goconvcase

なぜ作った?

Go ではキャメルケースが推奨です.(http://go.shibu.jp/effective_go.html) しかし、定数などはさすがにいいだろうと思い以下のようなソースを書くと...

package sample

// SNAKE_CASE identifires
const (
    HOGE_CONST = iota
    FUGA_CONST
    PIYO_CONST
)

lint が許しません(^^)

$golint .\sample.go
sample.go:5:2: don't use ALL_CAPS in Go names; use CamelCase
sample.go:6:2: don't use ALL_CAPS in Go names; use CamelCase
sample.go:7:2: don't use ALL_CAPS in Go names; use CamelCase

なんとなく Go ではキャメルだと知っていても定数までダメとは思わずついつい書いてしまう人もいるのではないでしょうか.

私は書きました.

こまめに lint をかければ気づくのでしょうがズボラなのでコーディングも後半になってから lint をかけて, そのころには大量のスネークで作られた定数が...という人もいるのではないでしょうか.

私はそうでした.

もしかすると正規表現力が高ければ一括で置換できるかもしれませんが, 正規表現に精通していない人もいるでしょう.

私にはできませんでした.

そんな人のために goconvcase を作りました.

インストール方法

利用対象の方は基本的に Go をお使いと思いますので go get でインストールお願いします.

go get github.com/kita127/goconvcase/cmd/goconvcase

使用方法

先ほどの例に出てきた以下のファイル(sample.go)に対して,

package sample

// SNAKE_CASE identifires
const (
        HOGE_CONST = iota
        FUGA_CONST
        PIYO_CONST
)

以下のコマンド実行でスネークケースの識別子がキャメルケースに変換されたソースが標準出力されます.

$goconvcase --from us --to uc .\sample.go
package sample

// SNAKE_CASE identifires
const (
        HogeConst = iota
        FugaConst
        PiyoConst
)

解説すると --from で変換対象となる識別子のケースを指定します. us は Upper Snake Case の略で大文字のスネークケースです. そして --to で変換後のケースを指定します. uc は Upper Camel Case の略で大文字のキャメルケースです. 最後に変換対象のファイルを指定します.

変換対象は識別子だけです. コメントの // SNAKE_CASE identifires はスネークケースのままです.

ファイルを上書きする場合は, gofmtgoimports と同じように -w を指定します.

$goconvcase -w --from us --to uc .\sample.go

指定可能な全てのケースを知りたい場合は --list を指定してください.

$goconvcase --list
us : UPPER_SNAKE_CASE like this.
uc : UpperCamelCase like this.
ls : lower_snake_case like this.
lc : lowerCamelCase like this.

現在以上の4種類のケースが相互に変換可能です. (キャメルからスネークに変換する必要性はないと思いますが...)

終わり

基本的には「命名ルールをしっかり把握する」「lint をこまめにかける」が大事かと思いますが, もし私と同じようにやらかしてしまった方いればよければ使ってみてください.

mbed + LCD(AQM0802A) でオリジナルキャラクタ(自作文字)の表示

mbed + LCD(AQM0802A) でオリジナルキャラクタ(自作文字)の表示

mbed + AQM0802A ピッチ変換モジュール(完成品) で自作文字(オリジナルキャラクタ)の表示のやり方メモ.

通常のキャラクタ表示はライブラリ検索 AQM0802A で調べれば先人達が作成した素敵なライブラリが公開されていますが, オリジナルのキャラクタを作成して表示しようとするとそのための便利ライブラリはなく, データシートを読んで自前実装が必要でした. 結構ちゃんとデータシートを読まなければならず, まあ, 当然英語でして...うぐぅ, 頑張って読んで何とかオリジナルのキャラクタの 表示までできたので自分用のメモとして本ブログをしたためます.

また、SC1602 シリーズの LCD が大体同じような感じでオリジナルキャラクタを表示できるっぽいので AQM0802A に限らず 役立つノウハウな気がします.

参考

参考にしたサイトおよびデータシートです.

環境

  • Windows10
  • mbed EA LPC11U35
  • AQM0802A(LCD)
    • ピッチ変換モジュール(完成品)

I2C について

mbed からは I2C による通信で AQM0802A を制御します. 正確には AQM0802A に搭載されている ST7032i という IC が I2C で受信したデータやコマンドを元にディスプレイを制御します.

mbed から AQM0802A にデータやコマンドを送信する際のフォーマットにはルールがあり大まかに以下です.

  1. 初めに Slave アドレスを 0x7C を送信する
    • Slave アドレスは固定
    • サイズは 1byte
  2. 続いて control byte を送信する
    • 次に送信する data byte がコマンドの指令かデータの書き込みかを決める
      • コマンドには Clear Display や Return Home など色々
    • control byte には Co, RS のビットがあるが詳細は後述
    • Co, RS 以外のビットは 0 固定
    • サイズは 1byte
  3. 続いて data byte を送信する
    • コマンドの場合は実行するコマンドを決める
    • データの場合はデータを送信する
    • サイズは 1byte

f:id:kita127:20200509154419p:plain
i2c_format

パラメータの説明

以上のフォーマットに従って AQM0802A を I2C で制御するわけなのですが, いろんなパラメータが登場し それらが何なのかある程度把握してないと操作の仕方がそもそもわからないです. とりあえずオリジナルのキャラクタを表示するのに必要な最低限を解説します.

Co

連続でデータを送信する際に最終 byte を決めるためのビット. control byte の 7bit 目に割り当てられている. control byte と data byte を1セットとし数セット連続で送信可能ですが, その場合受け手が最終セットの判断がつかないため, 最終セットの Co のみ 0 を設定することにより受け手に最後の1セットを伝える.

control byte と data byte を1セットしか送信しない場合はそれがすなわち最終セットとなるため Co=0 となります.

RS

data byte を IR(Instruction Register) に書き込むか DR(Data Register) に書き込むか決めるビット. control byte の 6bit 目に割り当てられている. 要するに「ディスプレイを消去しろ」や「カーソルを先頭に戻せ」など何らかのコマンドを送信したい場合は RS=L とし データを書き込む場合は RS=H とします. コマンドを送信したいかデータを書きたいかを決めるビットです.

Address Counter(AC)

後述の DDRAM/CGRAM/ICON RAM のアドレスを記憶する. DDRAM/CGRAM/ICON RAM を書き込んだ後、AC は自動的に 1 加算される.

Display Data RAM(DDRAM)

表示するデータを 8bitキャラコードとして保持する RAM. DDRAMアドレスカウンターは AC に16進数として設定される.

Character Generator RAM(CGRAM)

オリジナルキャラクタ作成時のキャラクタパターンを指定するための RAM. この RAM にパターンを書き込むことによりオリジナルキャラクタを作成する.

起動時の設定

起動時に所定のおまじないをする必要がある. 内容は AQM0802A のデータシート「初期設定例」に記載の通り 決まったコマンドを決まった順序で送信する. コマンドとコマンドの間は一定時間の wait を設ける必要がある.

初期設定についてはデータシートに記載の通りなのと, 先人達のソースを見ればわかるため本記事では取り上げません.

通常のキャラクタ表示

データシートの「CHARACTER PATTERNS」に載っている, 既に定義済み文字の表示手順説明です. 通常の文字であればすでに公開されているライブラリが使いやすいインタフェースを提供してくれているので自前実装する必要はないんですが, AQM0802A への I2C を使った基本的なコマンド送信方法の説明も兼ねて書こうかと思います.

実際に2行目左から4番目の位置に 'K' を表示する手順で説明します. 以下のイメージです.

□□□□□□□□
□□□K□□□□

Set DDRAM address で書き込み位置を指定

「Set DDRAM address」が「コマンド」にあたります. 各コマンドの送信方法は AQM0802A データシート記載の「DISPLAY INSTRUCTION TABLE」 一覧に載っています. Set DDRAM address で指定するアドレスとは AQM0802A ディスプレイの 8x2 のどの位置に文字を表示するかになります. 表示位置と DDRAM アドレスの対応は AQM0802A データシートの「液晶表示 DDRAMアドレス」から判断します.

f:id:kita127:20200509154552p:plain
display_instruction_table

f:id:kita127:20200509154621p:plain
lcd_display_ddram_address

表示位置は2行目の4番目なので「液晶表示 DDRAMアドレス」一覧から 0x43 が該当します.

Write data to RAM で書き込む文字を指定

書き込み位置が決まったら次に「Write data to RAM」により表示する文字のコードを送信します. データ送信の場合は control byte の RS に 1 を設定します. 文字コードは「CHARACTER PATTERNS」一覧に記載の値ですが, 通常の ASCII でも表示可能です.

サンプルコード

以上を踏まえると mbed ソースは以下のような感じです. Slave アドレス以降は control byte と data byte の送信を交互に繰り返す感じですね.

int main()
{
    // I2C の初期化
    i2c = new I2C(p26, p25);
    i2c->frequency(400000);    // I2C bus speed

    // 初期化コマンドシーケンス
    // 初期化シーケンスについては本記事では取り上げない
    init_seq();

    char buf[4];
    // Slave address
    const unsigned char slave_addr = 0x7C;

    // control byte
    // Co=1, RS=0
    // RS=0 はコマンドの指定となる
    buf[0] = 0x80;
    // data byte
    // Set DDRAM address 0x80(0b1XXXXXXX)
    // DDRAM Address 0x43
    buf[1] = (char)(0x80 | 0x43);

    // control byte
    // Co=0(最終データ), RS=1
    // RS=1 はデータの送信
    buf[2] = 0x40;
    // data byte
    // Write Data to RAM
    buf[3] = 'K';

    // I2C 送信
    i2c->write(slave_addr, buf, sizeof(buf));

    while(1) {
    }
}

オリジナルのキャラクタ表示

本題のオリジナルキャラクタを作成して表示する手順です. 画像のオリジナルキャラクタ(にこにこハート)を表示します.

f:id:kita127:20200509154744p:plain
original_character

表示手順は大まかに以下です.

  1. オリジナルキャラクタを設定するキャラクタコードを決める
  2. オリジナルキャラクタのドットパターンを CGRAM に設定する
  3. 作成したオリジナルキャラクタを表示する

データシート, 「CHARACTER PATTERNS」の「CGRAM」と書かれた箇所がオリジナルのキャラクタ登録用に 割り当てられたコードです. このいずれかのコードにこれから作成するオリジナルのキャラクタを紐付けます.

f:id:kita127:20200509154843p:plain
character_patterns

どのコードに割り当てるかは ST7032i データシートの 「Table4 Relationship between CGRAM Addresses 〜」 を 見て判断します.

f:id:kita127:20200509154918j:plain
relation_cgram

CGRAM address の 5bit - 3bit がどのコード(DDRAM data)に登録するかを決定します. 2bit - 0bit が CGRAM に書き込むドットパターンの行を決めます. つまり コード 0x00 にオリジナルキャラクタを設定する場合は CGRAM address 0x00 - 0x07 に ドットパターンを登録します. CGRAM address の指定コマンドは「DISPLAY INSTRUCTION TABLE」の「Set CGRAM」が該当します. 一度 CGRAM address を決めれば AC は自動的にインクリメントされるため以降は CGRAM address の指定を省けます.

CGRAM へのドットパターンの書き込みが完了したら、そのコードに作成したキャラクタが登録されますので, 通常のキャラクタ表示と同じ要領でそのコードを表示すればよいわけです.

そして実際にコードに落とし込んだのが以下.

char pattern1[8] = {0b00000110,
                    0b00001001,
                    0b00010010,
                    0b00010010,
                    0b00001000,
                    0b00000101,
                    0b00000010,
                    0b00000001
                   };

char pattern2[8] = {0b00001100,
                    0b00010010,
                    0b00001001,
                    0b00001001,
                    0b00000010,
                    0b00010100,
                    0b00001000,
                    0b00010000
                   };

int main()
{
    // I2C の初期化
    i2c = new I2C(p26, p25);
    i2c->frequency(400000);    // I2C bus speed

    // 初期化コマンドシーケンス
    init_seq();

    char buf[128];
    // Slave address
    const unsigned char slave_addr = 0x7C;
    int i = 0;

    // control byte
    // Co=1, RS=0
    buf[i] = 0x80;
    i++;
    // data byte
    // Set CGRAM address
    // 初回に設定以降、AC は自動的にインクリメントされるため
    // 続きはドットパターンのデータ設定のみ
    buf[i] = (0x40 | 0x00);
    i++;

    // CGRAM にドットパターンのデータを設定する
    int j;
    for(j = 0; j < 8; i += 2, j++) {
        // control byte
        // Co=1, RS=1
        buf[i] = 0xC0;
        // data byte
        // ドットパターンを書き込み
        buf[i+1] = pattern1[j];
    }
    for(j = 0; j < 8; i += 2, j++) {
        // control byte
        // Co=1, RS=1
        buf[i] = 0xC0;
        // data byte
        // ドットパターンを書き込み
        buf[i+1] = pattern2[j];
    }

    // control byte
    // Co=1, RS=0
    buf[i] = 0x80;
    i++;
    // data byte
    // Set DDRAM address
    // 2行目4番目に表示する
    buf[i] = (0x80 | 0x43);
    i++;

    // contorol byte
    // Co=1, RS=1
    buf[i] = 0xC0;
    i++;
    // data byte
    // Write data to RAM
    // 作成した pattern1 の Character Code
    buf[i] = 0x00;
    i++;
    // contorol byte
    // Co=0, RS=1
    buf[i] = 0x40;
    i++;
    // data byte
    // Write data to RAM
    // 作成した pattern2 の Character Code
    buf[i] = 0x01;

    int size = i + 1;

    // I2C 送信
    i2c->write(slave_addr, buf, size);

    while(1) {
    }
}

おわり

オリジナルのドット文字を表示できるようになったら無性にゲームボーイ的な何かを作りたくなりますね. そのうち安価なパーツだけを駆使してオレオレゲームボーイをつくるのもありですね.

そして全く触れなかった ICON RAM についてもそのうち掘り下げたい...

Go と mbed で USB Serial 通信をする

Go と mbed で USB Serial 通信をする

前回の記事で mbed(LPC11U35) と TeraTerm を使用して USB Serial 通信を実現できました. やったー

次は適当なテキストの内容をまるっと mbed に USB Serial 送信したいなという気持ちになったんですが, 軽く調べた感じ TeraTerm からではやりかたがわからない, あるいはできない模様.

また, 単純なテキストの送受信をしたいだけに対して TeraTerm は少し大仰な気もするので...

テキストをまるっと送信するのと簡単な対話をするだけの軽量な CLI ツールが欲しい, ということでつくろー, となりました.

仕様

  • 標準入力から受け取ったテキストを対象のポートに USB Serial 送信する
  • 対象と対話的に USB Serial 通信するモードを備える
    • 改行区切りで入力を受け取り対象に送信する
    • 対象からのデータを受信して標準出力する

環境

  • Windows10
  • mbed EA LPC11U35

プログラムを作成

そしてできたのが以下. Go 製です.

一応、release ページに Windows 用のバイナリを置いてあるのでそちらをダウンロード&解凍すれば使用できるはずです.

Go 言語用の USB Serial 通信のパッケージはいくつかありますが今回は以下をチョイス.

もうコード量それほどないのですべて載せます.

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"

    "github.com/jacobsa/go-serial/serial"
    "gopkg.in/alecthomas/kingpin.v2"
)

var (
    portFlag    = kingpin.Flag("port", "port name (--port=COM3)").Required().String()
    baudRate    = kingpin.Flag("baud-rate", "baud rate (--baud-rate=9600)").Default("9600").Int()
    readTime    = kingpin.Flag("read-time", "read cycle time(ms)").Default("100").Int()
    interactive = kingpin.Flag("interactive", "interactive mode").Short('i').Bool()
)

func main() {
    kingpin.Parse()

    // Set up options.
    options := serial.OpenOptions{
        PortName:        *portFlag,
        BaudRate:        uint(*baudRate),
        DataBits:        8,
        StopBits:        1,
        MinimumReadSize: 4,
    }

    // Open the port.
    port, err := serial.Open(options)
    if err != nil {
        log.Fatalf("serial.Open: %v", err)
    }

    // Make sure to close it later.
    defer port.Close()

    if *interactive {
        // interactive mode
        interactiveMode(port)
    } else {
        // one-shot mode
        text, err := ioutil.ReadAll(os.Stdin)
        if err != nil {
            log.Fatal(err)
        }
        _, err = port.Write([]byte(text))
        if err != nil {
            log.Fatalf("port.Write: %v", err)
        }
    }
}

func interactiveMode(port io.ReadWriteCloser) {

    fmt.Fprintln(os.Stdout, "This is Katuobushi interactive mode.")
    fmt.Fprintln(os.Stdout, "Please enter the sending texts...")

    go func() {
        t1 := time.NewTicker(time.Duration(*readTime) * time.Millisecond)
        defer t1.Stop()
        for {
            select {
            case <-t1.C:
                //Read
                buf := make([]byte, 128)
                n, err := port.Read(buf)
                if n != 0 {
                    if err != nil {
                        if err != io.EOF {
                            fmt.Fprintln(os.Stdout, "Error reading from serial port: ", err)
                        }
                    } else {
                        buf = buf[:n]
                        //fmt.Println("n =", n)
                        fmt.Fprintf(os.Stdout, "%s", buf)
                    }
                }
            }
        }
    }()

    // Write
    go func() {
        f := bufio.NewScanner(os.Stdin)
        for f.Scan() {
            text := f.Text()
            text = text + "\n"
            _, err := port.Write([]byte(text))
            if err != nil {
                log.Fatalf("port.Write: %v", err)
            }
        }
    }()

    for {
        // loop
    }
}

ターミナルで実行してそのまま USB Serial 通信が可能です. 使い方の説明の前に mbed 側にプログラムを書き込みましょう.

mbed のプログラム

前回ブログで取り上げたのと同様 USB Serial で受信したデータに "recv: " の文字列を頭に付与して送り返すプログラムです. こちらを mbed に書き込みます.

#include "mbed.h"
#include "USBSerial.h"
DigitalOut myled(LED1);
USBSerial serial;
int main() {
    uint8_t buf[128];
    while(1) {
        myled = 1; // LED is ON
        wait(0.5); // 200 ms
        myled = 0; // LED is OFF
        serial.scanf("%s", buf);
        serial.printf("recv: %s\n\r", buf);
        wait(0.5); // 1 sec
    }
}

COM ポートを調べる

mbed を USB で PC と接続します. 使用している COM ポートとボーレートを調べるためコマンドプロンプトMODE コマンドを実行して確認します. 今回使用している仮想ポートは COM3、ボーレートは 9600 みたいです.

$ MODE
デバイス状態 COM3:
------------
    ボー レート:        9600
    パリティ:           None
    データ ビット:      8
    ストップ ビット:    1
    タイムアウト:       ON
    XON/XOFF:           OFF
    CTS ハンドシェイク: OFF
    DSR ハンドシェイク: OFF
    DSR の検知:         OFF
    DTR サーキット:     ON
    RTS サーキット:     OFF

通信してみる

全ての準備が整ったのでGo製の簡易 USB Serial 通信ツール Katuobushi と mbed で通信してみます.

対話モード

mbed と PC を USB で接続し, mbed をリセットします. Katuobushi.exe を実行します. まずは TeraTerm のように対話的に通信してみましょう. 以下のオプションを付与して実行します.

$katuobushi.exe --port=COM3 -i

仮想ポートは先ほど調べた際に COM3 だったためそれをコマンドラインオプションで指定. ボーレートはデフォルトで 9600 なので指定なしで OK です. そして -i オプションを指定します.これは対話モードで実行するためのオプションで --interactive のショートフラグです.

helloEmbed と入力して recv: helloEmbed が返ってきました.どうやらうまくいったみたいです.

$go run main.go --port=COM3 -i
This is Katuobushi interactive mode.
Please enter the sending texts...
helloEmbed
recv: helloEmbed

ワンショットモード

それでは次に適当なファイルの内容をまるっと送信してみましょう.

適当な入力用のファイルを作成.

$echo hoge > input.txt
$echo fuga >> input.txt
$echo piyo >> input.text
$type input.txt
hoge
fuga
piyo

標準入力から katuobushi.exe に入力して実行します.

$type input.txt | katuobushi.exe --port=COM3

・・・すぐに実行終了しました. --interactive オプションを使用しない場合はワンショットモードになり 標準入力の内容を丸っと送信したら実行を終了します. これだと返信があったのかわかりませんが, 一応、mbed 側で内臓 LED を 3 回点滅しているので まあ, 受信できているようです.気になる場合は外付けの LCD などで受信したデータを出力させてみても良いかもです.

まとめ

といった感じで mbed と USB Serial 通信を実現したついでに軽量な USB Serial 通信ツールを作成しました. 題材に取り上げている mbed 自体は安価なものなんですが, PC と USB 通信できるようになっただけで色々広がりますね.

mbed(LPC11U35) で USB シリアル通信

mbed(LPC11U35) で USB シリアル通信

mbed LPC11U35 で USB シリアル通信をしたのでメモ

USB CDC(Communications Device Class)と呼ばれる
USB上でデバイス間のデータのやりとりを行うための通信規格を利用して実現している

環境

準備

  1. TeraTerm のインストール
  2. mbed ワークスペースから USBDevice ライブラリをインポートする
  3. コーディングし mbed に書き込む
    • コードは以下を参照
  4. mbed をリセット
  5. OS が USB ポートを認識することを確認する
  6. TeraTerm を起動
  7. 新しい接続は シリアル を選択
    • COMx ポートとかになっているはず
  8. TeraTerm から文字列を送信できるようにする
    • 「設定」タブ
    • 端末
    • 「ローカルエコー」にチェック
    • 「OK」
  9. TeraTerm から文字列を入力し、mbed からエコーバックされれば成功

コード

端末から文字列を受け取ってエコーバックするプログラム

#include "mbed.h"
#include "USBSerial.h"
DigitalOut myled(LED1);
USBSerial serial;
int main() {
    uint8_t buf[128];
    while(1) {
        myled = 1; // LED is ON
        wait(0.5); // 200 ms
        myled = 0; // LED is OFF
        serial.scanf("%s", buf);
        serial.printf("recv: %s\n\r", buf);
        wait(0.5); // 1 sec
    }
}

出典 : https://os.mbed.com/handbook/USBSerial