マイクロビットでのi2cの取り扱いが理解できてきました。
燻製などの調理の際に肉の温度や周辺温度を食の安全と品質向上のため調べたいことがある。肉の調理条件は63℃30分などの例がある。
プログラム作成に当たり、アナログ入力の使い方、i2c出力の扱い方がポイントになる。
利用機器
- micro:bit ver1.5
- マイクロビット用ブレイクアウトボード(秋月で500円くらい https://akizukidenshi.com/catalog/g/gK-13090/ )
- アナログ出力温度計 MCP9700 (秋月で50円くらい https://akizukidenshi.com/catalog/g/gI-14300/ )
- LCDモニタ GROVE - 16 x 2 LCD(スイッチサイエンスで1000円くらい 色違いでもOK https://www.switch-science.com/catalog/5342/ )
なぜmicrobitを選んだか
今回利用したい温度計はアナログ出力。分解能は高い方がいい。電源電圧2.3-5.5V。0℃で500mV。温度が1度上昇するごとに10mV出力電圧が上昇する。
マイクロビットなら3.3Vを1024分割
Arduino uno なら5Vを1024分割
Sony Spresenseも5Vを1024分割(spresense SDKを使いこなせれば16bit)
ラズパイはアナログ入力無し
以上より、マイクロビットを選択した。
202208追記
SpresenseとArduinoIDEでも簡単に16ビット拡張が使えるようでした。
温度センサ
今回はアナログ出力 3本線。(熱電対なら2本線で良いが、熱電対用のアンプなど必要。サーミスタはケーブルが長くなるとケーブル抵抗を考慮する必要がある)
mcp9700は3端子。VDDはマイクロビットの3V(3.3V)端子。GNDはGND、Voutはマイクロビットの0,1,2のいずれかにつなぐ。電気的特性は500mVで0℃、1℃上昇するごとに10mV増加する。
https://akizukidenshi.com/download/ds/microchip/mcp9700a.pdf
LCDモニタ
スイッチサイエンスのリンクを辿っていくとデータシートがある。製品名がJDH-1804、使われているチップがAIP31068Lらしい。
https://github.com/SeeedDocument/Grove-16x2_LCD_Series/raw/master/res/JDH_1804_Datasheet.pdf
i2cアドレスは0x3E = 62 サイトに載ってたはずだが確認したら見当たらない。接続してi2cのスキャナーで探すことが出来る。
https://www.sato-susumu.com/entry/i2c-address
16x2キャラクタディスプレイなんて、どれも同じと思っていたら違っていました。
キャラクタディスプレイにつながるパラレルの信号線をi2cに変換してくれるICが有るのだけれど、PCF8574AとかPCF8574がメジャーらしい。このため、マイクロビットの拡張機能からLCDで検索して取り込んでも使えない。改造等が必要。以下その例。カスタムブロックと言うらしい。
次のサイトのコードを使ったらうまくいきました。
秋月製AQM1602 LCDモジュールとのことです。i2cアドレスが0x3Eで同じだったので試しました。
https://jhalfmoon.com/dbc/2021/07/20/%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3%82%92%E7%A9%8D%E3%81%BF%E3%81%AA%E3%81%8C%E3%82%8928-bbc-microbit%E3%80%81%E5%A4%96%E4%BB%98%E3%81%91lcd%E3%82%92i2c%E6%8E%A5%E7%B6%9A/
液晶の2行目に表示する方法が分からなかったので、調べた。
秋月のサイトの簡易説明書https://akizukidenshi.com/download/ds/xiamen/AQM1602_rev2.pdf から
どうやら、書き込み開始アドレスの指定が必要らしい。どうやって指定するのか?
1行目の先頭が16進で00ということは、2進で00000000、
2行目の先頭が16進で40ということは2進で01000000
16進変換ツール(https://hogehoge.tk/tool/number.html )
本家のデータシート(https://github.com/SeeedDocument/Grove-16x2_LCD_Series/raw/master/res/JDH_1804_Datasheet.pdf )からDDRAM AD SETという命令コマンドを発見。DB7からDB0まで8ビット16進2桁。先頭(DB7)ビットが1(1???????)なので、先の先頭のアドレス指定から後半7ビットを合成すると
1行目の先頭の命令コマンドは、2進で10000000(10進で128)
2行目の先頭の命令コマンドは、2進で11000000(10進で192)
結局writeCommand(192)とすることで、2行目の書き込みをすることが出来ました。
初期化コマンド
上から順に
Function setは101000=40(10進) Xの部分は機能ナシなので0を挿入
Display setは1100=12
Display clearは1=1
Entry mode setは上に図を見るとinput setの一部誤植かと思うが、ナシで動作したので無視した。
最終的なコード
以上の知見を元に、3チャンネルのアナログ温度計をログしてLCDモニタに表示するプログラムにしました。スリープを1秒入れる関係で1分あたり45回程度更新されます。食肉加熱の目安になるように、63度に達してからの累計時間も表示するようにしました。温度表示を小数1位にするために、10倍して四捨五入してから0.1倍にしています(もっとスマートな記述はないでしょうか)
シリアル出力もしているので、一般的なシリアルモニタで監視できます。ArduinoIDEのシリアルプロッタを使うと簡易的ながらグラフっぽくなります。
参考にしたinitAQM1602用のコードも残っていますが、悪さはしていないようです。液晶のコントラストコマンドなんかは使えなさそうです。(元コマンドで56→57→20→115→86→108→56→1→12だったのを後日40→12→1と修正しました)
i2cではUInt16BEなどいろいろな記述法があります。もうすこし勉強する必要がありそうです。
とりあえず、マイクロビットの編集画面でJavaScriptを選んで書きコードを貼り付けると動かせます。
function initAQM1602 () {
basic.pause(100)
writeCommand(40)
writeCommand(12)
writeCommand(1)
}
// 62=0x3E
//
// 16384=0x4000
function writeData (dat: string) {
pins.i2cWriteNumber(
62,
16384 + dat.charCodeAt(0),
NumberFormat.UInt16BE,
false
)
basic.pause(1)
}
function writeCommand (command: number) {
pins.i2cWriteNumber(
62,
0 + command,
NumberFormat.UInt16BE,
false
)
basic.pause(20)
}
let msg2 = ""
let msg = ""
let P2電圧値 = 0
let P1電圧値 = 0
let P0電圧値 = 0
let P2入力値 = 0
let P1入力値 = 0
let P0入力値 = 0
let P2温度 = 0
let P1温度 = 0
let P0温度 = 0
let P1温度累計 = 0
let P0温度累計 = 0
initAQM1602()
let 電源電圧 = 3.3
serial.redirectToUSB()
basic.forever(function () {
P0入力値 = pins.analogReadPin(AnalogPin.P0)
P1入力値 = pins.analogReadPin(AnalogPin.P1)
P2入力値 = pins.analogReadPin(AnalogPin.P2)
P0電圧値 = P0入力値 * (電源電圧 / 1023)
P1電圧値 = P1入力値 * (電源電圧 / 1023)
P2電圧値 = P2入力値 * (電源電圧 / 1023)
P0温度 = (P0電圧値 - 0.5) / 0.01
P1温度 = (P1電圧値 - 0.5) / 0.01
P2温度 = (P2電圧値 - 0.5) / 0.01
// 中心温度63℃30分が目安
if (P0温度 >= 63) {
P0温度累計 += 1
}
if (P1温度 >= 63) {
P1温度累計 += 1
}
//serial.writeValue("temp0", Math.round(P0温度 * 10) * 0.1)
//serial.writeValue("temp1", Math.round(P1温度 * 10) * 0.1)
//serial.writeValue("temp2", Math.round(P2温度 * 10) * 0.1)
serial.writeLine("temp0:" + convertToText(Math.round(P0温度 * 10) * 0.1) + ",temp1:" + convertToText(Math.round(P1温度 * 10) * 0.1) + ",temp1:" + convertToText(Math.round(P2温度 * 10) * 0.1))
msg = "" + (Math.round(P0温度 * 10) * 0.1).toString() + " " + (Math.round(P1温度 * 10) * 0.1).toString() + " " + (Math.round(P2温度 * 10) * 0.1).toString()
// 45ループぐらいで1分
msg2 = ">63C " + P0温度累計.toString() + " " + (Math.round(P0温度累計 / 45 * 10) * 0.1).toString() + " " + (Math.round(P1温度累計 / 45 * 10) * 0.1).toString()
led.toggle(0, 0)
writeCommand(1)
writeCommand(0)
writeCommand(128)
for (let index = 0; index <= msg.length; index++) {
writeData(msg.charAt(index))
}
writeCommand(192)
for (let index2 = 0; index2 <= msg2.length; index2++) {
writeData(msg2.charAt(index2))
}
basic.pause(1000)
})
シリアル通信でPCから値の確認もできる(おまけ)
serial.writeValue("temp0", P0温度)などと記載すると、PCのシリアルモニタで値を確認できます。115200bpsに設定して適切なcomポートを選びましょう。テラタームもいいですがArduinoIDEのツールに入っているシリアルモニタは手軽です。
入力された数値をグラフ化するシリアルプロッタもあります。(3チャンネルの数字が同じ直線で結ばれてしまっていますが、何とか見えます)
https://nn-hokuson.hatenablog.com/entry/2021/10/04/145415 を参考にシリアル出力を1行で3つの数値が出るように修正しました。すっきりと表示できています。
以下のようなデータ形式で、シリアル出力しています。
temp0:23.5,temp1:24.2,temp1:24.2
temp0:23.5,temp1:24.2,temp1:24.2
考察:RSって何?なぜ0x4000を足しているの?
秋月のデータシートを見てみる。データのビットが並んでいて、左から順に送信する。
先ず、相手先のi2cアドレスと書込モード1か読み取りモード0かを記した信号を送る。あどれすは、7ビット0111110=0x3Eだが読み取りモードの意味の0が右側に来るので、それまで含めると01111100=0x7cとのこと。マイクロビットでは0x3E=62。
送信後、液晶側から「聞いてるよ!」の意味のAcknowlegementが返される。そして、8ビット2連で構成されるデータを送っていく。
先頭のCoはまだまだ続くよ、の意味。今回のプログラムでは1文字1文字別々に送っているので、C0=0。それに続く、RSは液晶のセッティングのデータか、液晶の表示のデータを送っているのかを示すビット:RS=0なら液晶コマンド、RS=1は液晶に表示させる文字を送ってる意味。
RS=1は16ビットの2番目の位置にあるので、0x4000=0100 0000 0000 0000と8ビットで構成される文字コードを足してやると文字を表示する信号になる。
考察:UInt16BEとかビックエンディアンってなに?
マイクロビットのサイトを見てみると、
UInt16BE: two bytes, unsigned, big endian と記載されている。
2バイトなので、8ビットが2こ(16ビット)、unsignedなので、正の整数、big endian ビックエンディアンとある。
(英語でつらいが、自動翻訳するともっと分からないので頑張って読む。)
10進の2552を16ビットの2進で表すと、00001001 11111000
このとき、
Big end: 00001001
Little end: 11111000
という。
2バイトのデータの左側(ビックエンド)と右側(リトルエンド)のどちらを先にメモリに格納するかがエンディアン順序である。左側(ビックエンド)を先にメモリーに格納するタイプがビックエンディアン(ナンバー)である。
1バイトの時は右も左もないので、エンディアン順序は無いが、互換性のために変数形はあるとのこと。
(1バイト中の8ビットの右から左からといったものではない。)
i2cのコマンド送信が、コントロールバイト→データバイトだからビックエンディアンを用いていたが、逆だったらリトルエンディアンを使うって事ね!
Javascriptのi2c Write Number命令
https://makecode.microbit.org/reference/pins/i2c-write-number
function pins.i2cWriteNumber(address: number, value: number, format: NumberFormat, repeated: boolean): void;
となっている。アドレスは7ビットアドレスを使う(送信時は一時的に書込の0を追加して8ビットになる)
アドレスが8ビットの場合は数字を半分にする(右端のビットを落とす)
書き込む数値は変数型UInt16BEなどを指定。
文字コード
液晶モニタの説明書に文字コード表があります。
これは、機種固有ではなく一般的な文字コードなので、文字からキャラクターコードを取得する一般的なコマンドで得ることができます。
micro:bitのシリアル通信のおまけ
マイクロビットでWindowsPCとUSB通信を行う。
PC側で受け取ったデータを、pythonなどで処理判定。
micro:bitのサンプルプログラム
受け取り側の設定
spresenseとArduinoIDEで16bitデータを受信する拡張
ソニー公式のanalogRead()関数とanalogReadMap() [独自拡張]を読むと以下の記述がある。
「analogReadMap() 関数の min と max に同じ値を設定した場合、analogRead() 関数は map() による変換を行わずに、ADC の生値を返します。」
これに従い、setupでanalogReadMap(pin,0,0);を実行したところ16ビットのsigned shortの値が得られるようだ。
値をグラフで確認したところ、飛び飛びの値になっていないので細かい値まで得られそうだ。
ちなみに、アナログピンはA0-A5まであるが、A4,A5が高速サンプリング対応らしい。(分解能は変わらない)