前回作成したWBGT時計(室温・湿度計)のプログラム(wbgt_clock.asm)について、少し掘り下げて解説しておきます。
前回解説した通り、WBGT(推定値)は以下の式で計算します。
WBGT(推定値)=
0.735×Ta+
0.0374×RH+
0.00292×Ta×RH+
7.619×SR−4.557×SR2−0.0572×WS−4.064
ここで、Ta=気温[℃]、RH=湿度[%RH]、SR=全天日射量[kW/m2]、WS=平均風速[m/s]
測定値Ta,RHも、Ta=31.2℃などの小数値だし、係数もすべて小数値ですね……。WBGTの計算には浮動小数演算ユニット(H-FPU)が必要でしょうか??
確かに、H-FPUがあると一発なのですが、今回はI2C通信が必要なので、MoscoviumマイコンはMCOC115TW0408を使います。TWエディションには、乗除算コプロセッサが搭載されていますが、H-FPUは搭載されていません。最上位記述mcoc115tw0408.vの先頭付近にあるコンパイルオプションでH-FPUを有効化して使うのが1つの手です。
`define MCVM_COPR_NOFPU // FPUが必要な時は`defineしない
ですが、小数点以下1桁程度のTa,RH実測値に対して、そこまでして浮動小数演算を実行する意味は無いので、今回は固定小数演算で実現しようと思います。
固定小数演算とは、ざっくり言うと、小数演算を整数演算で実行する手法となります。
WBGT推定式の演算を例に説明します。
例)WBGT推定式より: 0.735×Ta を計算する。
例)WBGT推定式より: 0.00292×Ta×RH を計算する。
取り得る数値の最大値を意識しながら、演算結果のビット幅も意識しながら、演算精度(有効数字)も意識しながら、掛け算と割り算の順番を設計して行くと、小数演算がただの整数演算に帰着します。
WBGT推定式の全項を同じ×1,000倍固定小数値として演算したら、全ての数値を加算して終了です。桁が合っていないと、そのまま加算することが出来ないので、全ての項を同じ倍率の固定小数として演算しておくことが重要です。
32ビット演算は、16ビットCPUコアMoscoviumでは複数命令を組み合わせて実行します(こちらで解説済みです)。
今回は10進数の固定小数を使いましたので、×10倍とか、1/1,000とか、10のべき乗での乗除算が必要となります。これを2進数の固定小数にすると、×16倍とか、1/1,024とか、2のべき乗での演算ができます。
ご存知の通り、2冪数での乗除算はただのシフト演算ですので、その方がプログラムし易いと思うかもしれません。が、それは間違いです。
なぜなら、Moscoviumは16ビットCPUだからです。16ビット幅のバレルシフト演算命令は持っていますが、32ビット幅のバレルシフト演算命令は持っていません。
例題)Moscoviumで、0x12345678>>10 を演算するプログラムを作ってください。
答え)
ldwi r0,0x1234
ldwi r1,0x5678
// ここから5命令15サイクル
ldbiu r2,0
ldwi r3,1024
divlu // divlu,divlq合わせて12サイクル(DR=0)。
divlq // 15サイクル以内に10ビットシフト演算を実行できる?
結局、32ビット割り算命令(divlu命令,divlq命令)を使うのが、一番速くてコードサイズも小さいはずです(1^^;)。どうせ割り算するなら、直観的に解り易い10進数の固定小数の方が楽でしょ?
※一応検証してみたところ、以下の様なコードとなりました。15命令15サイクル掛かったので、割り算命令と同着でしたm(1__)m。ただし、コードサイズは割り算の方が圧倒的に小さいです。ご参考。
// lsftli r0:r1,-10 (15 cycles)
// lsft -8
mov r2,r0
lsfti r1,-8
lsfti r0,-8
lsfti r2,8
add r1,r2
// lsft -2
ldwi r2,0x8000
rept 2
lsfti r1,-1
lsfti r0,-1
bcc pcnt+2
add r1,r2
endr
WBGTを計算(推定)するために全天日射量SRが必要で、SRは時間帯によって大きく値が変わることから、時計機能が必須となりました。
RTC(DS1302等)を接続して時間と日付を知ると、季節に合わせてSR参照テーブルを変化させるとか、更にインテリジェントにできるかもしれません。が、今回は、1年分の平均値をSRテーブルとして持っているので、日付まで知る必要は無く、また、16文字×2行の表示領域では日付を表示するスペースも無かったことから、時刻だけの時計としました。
Moscoviumマイコンには、実時間を刻むシステムタイマSYSTIMユニットがありますので、これを使って時計機能を実装してあります(↓)。
全天日射量SRを時間毎(0時〜23時)にテーブル参照して決定するため、昼の11時と夜の11時を区別する必要があり、24時間制の時カウンタclk_hhが必須です。
表示系は、秒カウンタclk_ssをインクリメントする度にLCD1602の表示を更新します。また、分カウンタclk_mmをインクリメントする度に、DHT11から室温・湿度データを読み出してWBGT値を計算(推定)します。
以上で時計機能の実装が完了しました。
今回のテーマの真打ち登場です。
LCD1602(下図左)に、LCDCモジュール(下図右)を使って文字列を表示する方法について解説します。
LCDCモジュールには、PCF8574というLSIが載っています。このチップはI2Cで受信した8ビットデータに従って変化する8本のI/Oポートを拡張できる、I/Oエクスパンダと言う種類のチップです。上図右側のLCDCモジュールは、以下図の様に接続することでLCD1602を制御できるようにピン配置が最適化されています。
マイコンとの通信方式はI2Cで、スレーブアドレスは0x27に設定されています(デフォルト。ボード上のはんだジャンパで0x20〜0x27に設定可能)。
PCF8574のデータシートを検索する(別ウインドウが開きます)
LCD1602の制御に必要な、DB[7:4],K,CS(E),RW,RS信号が、PCF8574のI/Oポート8本に以下表の様に割り当たっています。
PCF8574 | LCD1602信号名 | 備考 |
ビット7 | DB[7] | 4ビットデータバス |
ビット6 | DB[6] | |
ビット5 | DB[5] | |
ビット4 | DB[4] | |
ビット3 | K | バックライトLEDのカソード制御 |
ビット2 | CS(E) | イネーブルストローブ |
ビット1 | RW | リード/ライトストローブ |
ビット0 | RS | インストラクション/データストローブ |
I2C通信で任意の8ビットデータをPCF8574に送信すると、送ったデータに対応して各信号線が駆動されます。連続して複数データを送信すると、新しいデータを受信する度に、信号値が変化します。
PCF8574は入力信号も扱うことが可能で、実はLOWレベルはシンクするのですが、HIGHレベルはプルアップされています。従って、データバスなど信号を入力として扱いたい場合には、その端子をHIGH出力に設定しておくことで、入力信号として読み出すことが可能となります。
I2Cリードサイクルを発行してPCF8574からデータを読み出すと、I2Cアドレスバイト[ADR,R]を受信したタイミングのI/Oレベルを読み出すことが出来るようになっています。
昔4ビットバスモードで直接制御した時と同様にして、I2Cのリード/ライト通信を使って、アクセスに必要な信号を制御することになります。
以下図にLCD1602へインストラクション/データを書き込むためのバスサイクルを示します。
I2Cバスサイクルは以下の通りです(凡例はこちら、上図中では少し簡略化してあります)。
S [ADR,W] [D1] [D2] [D3] [D4] [D5] [D6] P
LCD1602のバスサイクルに着目すると、以下となります。
4ビットバスモードで直接制御した時(「#3-1 LCD1602に表示する」)は、アセンブラを使って高速アクセスすることがテーマの1つでしたが、今回の制御は、上図の通りI2Cで7バイトのデータ送信が必要です。PCF8574はI2C標準モード(100kHz)の通信が可能なLSIですので、1回書き込みするのに7バイト×9ビット/100kHz=630μs以上かかります。ちょっとスピード的には見劣りします。
※I2Cの1バイト転送は、データ8ビット+アクノリッジ1ビットの計9ビットです。
同様に、LCD1602からのデータ読み出し手順を下図に示します。
I2Cバスサイクルは以下の通りです(凡例)。
S [ADR,W] [D1] [D2] R [ADR,R] [[R1]] R [ADR,W] [D3] [D4]
R [ADR,R] [[R2]] R [ADR,W] [D5] P
今度は、I2Cバスサイクルの途中でリード⇔ライト方向が入れ替わるため、その都度反復STARTコンディションとアドレスバイトの送信が必要となり、ライトサイクルの時よりずいぶんと複雑で長くなっています。
Moscovium用に設計したI2C通信可能なSTWSERユニットを使って、LCDCモジュールを制御して行きます。「#17-6 I2Cマスタ通信」で作成したマスタドライバを使うのが楽ですから、まずはI2Cフレームを作成してみましょう。
I2C通信 | I2Cフレーム | 備考(追加) |
lcd_write | ||
S [ADR,W] | 0x4100 | +(ADR<<1) |
[D1] | 0x40f8 | +RS |
[D2] | 0x400c | +RS+(DB[7:4]<<4) |
[D3] | 0x4008 | +RS+(DB[7:4]<<4) |
[D4] | 0x400c | +RS+(DB[3:0]<<4) |
[D5] | 0x4008 | +RS+(DB[3:0]<<4) |
[D6] | 0x40f8 | +RS |
P | 0x0000 | |
lcd_read | ||
S [ADR,W] | 0x4100 | +(ADR<<1) |
[D1] | 0x40fa | +RS |
[D2] | 0x40fe | +RS |
R [ADR,R] | 0x4101 | +(ADR<<1) |
[[R1]](N) | 0x8200 | データ上位DB[7:4]読み出し |
R [ADR,W] | 0x4100 | +(ADR<<1) |
[D3] | 0x40fa | +RS |
[D4] | 0x40fe | +RS |
R [ADR,R] | 0x4101 | +(ADR<<1) |
[[R2]](N) | 0x8200 | データ下位DB[3:0]読み出し |
R [ADR,W] | 0x4100 | +(ADR<<1) |
[D5] | 0x40fa | +RS |
P | 0x0000 |
今回のプログラムでは、スレーブアドレス(ADR<<1)は定数としてデータに埋め込んでしまいました。RSは、アクセス対象がインストラクションなのか、データなのかを指示するストローブなので、パラメータになっていて、I2CフレームをROMからRAMに展開する時に足し込んでいます。書き込みデータDB[7:0]も同様です。
その他、K,RW,CS(E)など固定値として制御可能な信号については、上表のI2Cフレーム内に定数として反映されています。尚、バックライトLEDのカソード制御については、K=1(バックライトON)に固定してあります。
上表の様なI2CフレームをRAM上に展開したら、その先頭アドレスを汎用レジスタr1に設定して、mcvm_wireサブルーチンをコール「jalwr7 mcvm_wire」で、I2C通信が実行されます。
制御の全体像については、完成したプログラム(wbgt_clock.asm)をご確認ください。
プログラム(wbgt_clock.asm)を使った実験の様子は前回の記事にあります。
今回のアプリケーション(wbgt_clock.asm)では、DHT11の読み出し、LCD1602への表示制御(I2C)、WBGT計算(推定)等々、色々と制御・処理が必要でした。気が付いたら、アセンブル後のコードサイズが3Kバイトを超えていました。さすがに4KバイトROMでは足りなくなってきたかな〜〜🐱?