電気素人のプルアップ回路解説

電気素人のプルアップ回路解説

ブログの内容

プルアップ回路についてざっくり以下のように暗記していますが正直その原理が不明でした.

  • スイッチが OFF 時にマイコン入力状態が ON になる
  • スイッチが ON 時にマイコン入力状態が OFF になる

というのも, 例えば以下のような 5V 電源の回路の場合, スイッチが OFF 時マイコンの入力端子には 5V が印加される...というのは理解できます. スイッチが ON の場合はマイコン入力端子には 0V が印加される...なぜ?電源と回路的につながっているため 5V が印加されるのでは?と考えてしまいます.

f:id:kita127:20210725145127p:plain

このあたりについて, あまりどのブログにも原理が書かれておらず, おそらくは基本的な内容のためわざわざ書いていないのではないかと思われます. そこまで基本的な原理であれば恐らくは義務教育で習う範疇の知識ではないかと思ったので中学2年の理科の教科書をやりなおした後, プルアップ回路についてリベンジしてみました.

その結果, 一応, 自分なりには結論を導き出せた気分なので, それを本ブログにまとめます.

なお, プルアップ回路については以下の「プルアップ・プルダウン抵抗とは?電子回路に必須の考え方」を参考にさせていただきました. 本ブログは以下のページを読んだ上で, 電気まわりで不明点がある場合に読んでいただくと良いかと思います.

https://voltechno.com/blog/pullup-pulldown/

プルアップ回路とは

ここではプルアップ回路の利用意図について説明しますが詳細については参考サイトの「プルアップ抵抗・プルダウン抵抗とは?電子回路に必須の考え方」を参照してください.

マイコンの入力端子は常に電源側かグランド側に接続しなければならず, どちらにも接続されていない浮いた状態(ハイインピーダンス)になってはいけません. プルアップ, プルダウン回路はマイコンの入力端子が浮いた状態にならないようスイッチが ON/OFF どちらの状態でも マイコン入力端子を Hi/Lo いずれかに倒すための回路です.

スイッチ ON 時の原理

それではプルアップ回路でのスイッチ ON 時にマイコンの入力端子に 0V が印加される自分なりの考察を説明します.

f:id:kita127:20210725145241p:plain

まず以下の前提があります.

  • 電源電圧は 5V
  • プルアップ抵抗とマイコン入力端子の抵抗(Rm)ではマイコン入力端子の抵抗のほうが遥かに大きい
  • スイッチが閉じられた時のスイッチの抵抗はほぼ 0Ω とする

この図では抵抗は以下の3つが存在します.

  • プルアップ抵抗
  • 閉じられたスイッチ
  • マイコンの入力端子抵抗
    • 抵抗 Rm とする

ここで求めたいこととしてはスイッチ ON 時の Rm にかかる電圧になります.

Rm とスイッチは並列回路のためスイッチと Rm のそれぞれにかかる電圧は等しくなります. そのため, スイッチにかかる電圧を求めると Rm の電圧がわかります.

f:id:kita127:20210725145300p:plain

なぜ並列回路のそれぞれの抵抗にかかる電圧が等しくなるかのメカニズムまでは私にはわかりません. とりあえず中学2年の理科の教科書でそのように書いているためそうとしています. ただ実地で考えてもテスタなどで電圧を計測する際は並列につないで計測対象の電圧を計るところから, 現実的にそうなることはテスタを使ったことがある人は体験していると思います.

話はそれましたが, スイッチにかかる電圧を求めれば Rm の電圧もわかるため, スイッチの電圧を求めます. 求め方はオームの法則を使用します. オームの法則は以下の式です.

電圧(V) = 電流(I) × 抵抗(R)

スイッチの抵抗はほぼ 0Ω のため

V = スイッチに流れる電流 × 0Ω = 0V

となり, 抵抗が 0Ω なのでスイッチにかかる電圧は 0V となります. そのため Rm の電圧も 0V となります.

以上によりプルアップ回路でスイッチを ON した時にはマイコンの入力端子には 0V が印加されます.

スイッチ OFF 時の原理

次はプルアップ回路でのスイッチ OFF 時にマイコンの入力端子に 5V が印加される理由の自分なりの考察です.

f:id:kita127:20210725145319p:plain

前提はスイッチ ON 時の前提と同じです.

スイッチ OFF の場合プルアップ抵抗とマイコン入力端子の抵抗は直列回路となります.

直列回路の場合「プルアップ抵抗にかかる電圧 + Rm の電圧 = 電源電圧(5V)」となります. また, プルアップ抵抗と Rm それぞれにかかる電圧の比は抵抗の大きさに比例します. (このあたりも中学2年理科の範疇です)

f:id:kita127:20210725145334p:plain

前提より抵抗値は Rm の方がプルアップ抵抗より遥かに大きいため Rm 側に 5V ほぼすべての電圧が印加されます.

そのためスイッチ OFF 時の Rm は 5V となります.

Node.js + Express + Sequelize で Web アプリ作成手順

Node.js + Express + Sequelize で Web アプリ作成手順

本記事の目的

Node.js + Express + Sequelize で Web アプリケーション作成のための環境構築から手順までをまとめる.

本記事は以下の書籍「作りながら学ぶWebプログラミング実践入門」の内容を自分なりに消化, 改修した内容となっている.

https://book.mynavi.jp/ec/products/detail/id=112778

今回作成したアプリケーションのリポジトリは以下

https://github.com/kita127/todo-app

各技術要素の概略

Sequelize と Mocha は「作りながら学ぶWebプログラミング実践入門」には登場しない要素.

環境構築

Node.js の導入

まずはメイン言語となる Node.js の導入.

nodebrew の導入

Node.js はバージョンの更新が目まぐるしいため, 通常は直接インストールせずバージョンマネージャをインストールして 任意のバージョンに切り替えつつ使用するのが一般的らしい.

Mac 環境では nodebrew というバージョンマネージャがよく使われるらしいのでそちらを導入する.

https://github.com/hokaccha/nodebrew

curl でインストールする

$ curl -L git.io/nodebrew | perl - setup

.bashrc 等の設定ファイルの PATH の設定に nodebrew のパスを追加する.

$ export PATH=$HOME/.nodebrew/current/bin:$PATH

設定ファイルをリロードする.

$ source ~/.bashrc

nodebrew のコマンドが使用できることを確認する.

$ nodebrew help
nodebrew 1.1.0

Usage:
    nodebrew help                         Show this message
    nodebrew install <version>            Download and install <version> (from binary)
    nodebrew compile <version>            Download and install <version> (from source)
    nodebrew install-binary <version>     Alias of `install` (For backward compatibility)
    nodebrew uninstall <version>          Uninstall <version>
    nodebrew use <version>                Use <version>
    nodebrew list                         List installed versions
    nodebrew ls                           Alias for `list`

nodebrew から Node.js のインストール

今回は最新のバージョンを導入する.

$ nodebrew install latest

意図したバージョンの Node.js がインストールされたことを確認する.

$ node -v
v16.0.0

Express の導入

Express はプロジェクトの雛形を生成するツール express-generator をグローバル環境に導入する.

$ npm install express-generator -g

プロジェクトの雛形を作成する. 以下のコマンドでテンプレートファイルに ejs を指定し, project-name で Express プロジェクトの雛形を作成する.

$ express -v ejs project-name

作成したプロジェクトフォルダに移動すし, 依存パッケージをインストールする.

$ cd ./project-name
$ npm install

作成したプロジェクトを試しに動かしてみる.

$ npm start

ブラウザから http://localhost:3000 にアクセスする. 以下が表示されることを確認する.

Express
Welcom to Express

Express の説明

Express の使い方

routes/ フォルダに任意の url にアクセスした際の制御を記述する.

以下はrouter/hoge.js を作成し http://localhost:3000/hoge にアクセスした際の制御. res.render() の第一引数にレンダリングする ejs ファイルを指定する. 第二引数には ejs ファイルに渡すオブジェクトを指定する.

var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
    res.render('hoge', { title: 'hoge' });
});

module.exports = router;

レンダリングする ejs ファイルを view フォルダに作成する.

view/hoge.ejs を以下の通り作成.

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
  </body>
</html>

app.js に以下の設定を追加する. 以下の設定で作成した hoge.js が URL /hoge と紐づく. 以下の設定により /hogehoge.js におけるルートとなるため router.get('/', ....) の記述で http://localhost:3000/hoge にアクセスした場合の処理の記述となる.

// 作成したルータ hoge.js を require する
var hogeRouter = require('./routes/hoge');

    .
    .
    .

// 作成した hogeRouter に URL を紐付ける
app.use('/hoge', hogeRouter);

Sequelize

Sequelize を導入する. Sequelize は Node.js 用の OR マッパー.

導入

$ npm install sequelize

sequelize-cli の導入

sequelize を便利に使用するための CLI ツールを導入する.

$ npm install sequelize-cli

sequelize を初期化する

$ npx sequelize-cli init

以下のフォルダが作成されることを確認する.

  • config
    • 設定情報管理
  • models
    • データベースアクセスに使う「モデル」というオブジェクトを定義する

config.json に設定をする

SQLite3 を使用する場合は以下のような感じで設定する.

{
  "development": {
    "database": "db-development",
    "dialect": "sqlite",
    "storage": "seq-todo.sqlite3"
  },
  "test": {
    "database": "db-test",
    "dialect": "sqlite",
    "storage": "seq-todo.sqlite3"
  },
  "production": {
    "database": "db-product",
    "dialect": "sqlite",
    "storage": "seq-todo.sqlite3"
  }
}

モデルを作成する

データベースのテーブルにアクセスするためのオブジェクトであるモデルを作成する. モデルの作成は sequelize-cli のコマンドで行う.

以下は users というテーブルを作成, account, password, name, role といったカラムを持つ.

$ npx sequelize-cli model:generate --name users --attributes account:string,password:string,name:string,role:string

以下が生成されることを確認する.

  • models/users.js
  • migrations/yyyymmddxxxxxxxx-create-users.js

マイグレーションを実行する

データベースの内容を変更した場合にその差分をデータベースに適用することをマイグレーションと呼ぶ.

今回は新たにモデルを作成したため、その変更をデータベースに反映する. これによりモデルを作成した users テーブルが seq-todo.db に作成される.

$ npx sequelize-cli db:migrate --env development

シーディングによりレコードを作成する

seq-todo.db に users テーブルを作成したが, レコードは何もない. そのため, あらかじめレコードを作成する. こうしたはじめに用意しておくデータをシードと呼ぶ.

シーディング作成用のスクリプトファイルを生成する.

$ npx sequelize-cli seed:generate --name sample-users

seeders/yyyymmddxxxxxxxx-sample-users.js が生成されているためその up に生成するレコードの情報を記述する.

up: async (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('users', [
        {
            account: 'admin.com',
            password: 'admin',
            name: 'admin',
            role: 'admin',
            createdAt: new Date(),
            updatedAt: new Date()
        }
    ]);
    /**
     * Add seed commands here.
     *
     * Example:
     * await queryInterface.bulkInsert('People', [{
     *   name: 'John Doe',
     *   isBetaMember: false
     * }], {});
    */
},

シーディングを実行する.

$ npx sequelize-cli db:seed:all

Mocha

ユニットテスト用のフレームワークとして Mocha を導入する.

# mocha をインストール
$ npm install mocha --save-dev

# プロジェクト直下の test ディレクトリ内にある .js ファイルを対象に mocha はテストする
$ mkdir test

# テストモジュール作成
$ touch ./test/test-sample.js

テストモジュールに以下のようにテストを記述する.

describe('TEST SAMPLE', () => {
    it('test 1', (done) => {
        if ('aaa' === 'aaa') {
            done();
        }
        else {
            done('失敗');
        }
    });
});

テスト実行

$ npx mocha

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 についてもそのうち掘り下げたい...