AVRtiny13Aでつくるシリーズ番外編 LCD8812K4-01を動かしてみる
- 2016/04/07 13:42
- カテゴリー:make:
ATtiny13Aを使った工作,今回は番外編です。
先日,aitendoで99円で購入した,I2CのセグメントLCDを使ったTCXO時計が壊れてしまったことを書きました。
TCXOの時計だけに時刻はほとんど狂うことなく,それだけにLCDの破損は非常に残念でした。原因は未だ不明ですし,完動品のLCDが手元にないため交換して治ることも確認出来ず,本当にLCDが悪いのかどうかも実は断言出来ません。
しかし,tiny13AからはちゃんとI2Cで信号が出ていますし,Si5351Aの設定も出来ていることから,やはりLCDに問題がある可能性が濃厚でしょう。
同じLCDが手に入らないものかなあと探してみましたが,既に売り切れており同じ物は手に配送にありません。全く同じ物でなくてもいいと「I2Cでセグメント」という条件で探してみても,なかなか見つかりません。
そういえば,先日GPSモジュールをaitendoで買った時に,199円の特価LCDを2つ買ってあったことを思い出しました。これに交換しようと考えたのですが,それはそう簡単にできません。
このLCDは,「LCD8812K4-01」という品名で売られているものですが,少なくとも昨年の12月頃には売られていたようです。値段は199円と安いのですが,実はデータシートがなく,サンプルコードもありません。要するに動かすにはそれなりに努力が必要なものだということです。それでこの値段はちょっと高いかなあ。
壊してしまったI2CセグメントLCDの場合,ピン配置もサンプルコードも出ていました。発売されてからの時間も経っていたので,いろいろな人が実際に動かしていたので,そこも問題はありませんでした。
ですが,今回のLCD8812K4-01については情報が不足している上に,動作例がありません。分かっていることは,LCDコントローラが2つ載っていることと,それぞれ独立したインターフェースであること,セグメント側はSPI(でもHT1621の仕様書にはSPIとは一切書かれていないし,実際のところSPIとは違います),キャラクタ側は8ビットパラレルであること,電源は5V単電源であること,そしてピン配置です。
問題なのは,仮にセグメントだけを使う場合でも,SPIなのでI2CのLCDの代わりにはならないということです。SPIで繋げばいいだけじゃないかというなかれ。I2CのLCDだからこそSi5351Aとバスを共用でき,そのおかげで8ピンしかないtiny13Aでも時計が作れるのです。
ですがSPIのLCDを使うとなると,バスをSi5351Aと共用できませんから,I2Cとは別にSPIも用意しないといけません。SPIが3本,I2Cが2本,32,768kHz入力に1本,これですでに6本ですから,ボタンが置けません。うーん,2313を使うしかないのか。
もう一度考えます。
I2CはLCDを制御する役割がなくなり,Si5351Aの初期設定だけに使う物になったので,設定後は入力に切り替え,ボタンを置いても良さそうです。そうするとどうにかピンは足りますね。
どっちにしても,LCDを動かしてみないことには先に進めません。そこで,この前人未踏のLCD8812K4-01を,とりあえずセグメント側だけ動かしてみることにしました。
(1)ハードウェア
このLCDの端子は16個あります。電源とGNDは共通ですが,セグメントとキャラクタで別々の信号が出ています。今回はセグメントだけを使いますが,セグメントはSPIという情報から,前半の5ピンが関係しそうです。
aitendoのサイトでは,1ピンからCS,WR,A0,VDD,GNDと並んでいるということですが,SPIにA0なんて端子はありませんし,搭載されていると書かれているHT1621というLCDコントローラの仕様書を見ても,そういう端子はありません。
そこで,A0というのはDATAであると仮定しました。そして本来HT1621に備わっている読み出し用の端子RDは,このLCDでは外には出ていないものとしました。書き込み専用ということですね。
仕様書をさらに見ていると,読み出しの際にはRDをクロックにしてデータが出てくるようですけども,これは明らかにSPIじゃありません。ですのでこのLCDはSPIではないということになるのですが,書き込み専用とすれば,SPIと見なして動かす事が出来ます。
(2)SPIをソフトで書く
SPIは単純ですので,HT1621の仕様書に従ってソフトで実装することにします。マイコンはもちろんtiny13Aです。
仕様書を見ているとリードについても書かれていますが,RD端子が外に出ていないので今回は無視です。書き込みのみを実装します。
で,ちょこちょこと書いてみると,なにやら動いているようです。結構あっさり動いてしまったので拍子抜けです。
コマンドとデータの書き込みを行う関数をさっと書いて,一応簡単なライブラリのような物を作っておきます。
不安があるのは,ウェイトをどこにどれくらい置くべきかがさっぱりわからないことです。インターフェースのタイミングはわかりますが,例えば電源投入後最初のコマンドを投げるまどれくらい待てばいいかとか,内部発振器の発振安定時間はいくつかとか,コマンドを投げてそれが有効になるまでの時間がどれくらいか等,全然かかれていません。
とりあえず動いた,と言うやり方で進めるしかなさそうです。こういうのはトラブルの原因になるんだけどなあ。まぁしゃあないですね。
(3)初期設定をどうする
ここまでで一応LCDを叩けるようになったのですが,初期設定の手順やコマンドの役割などが不明なままです。HT1621の仕様書だけでは正確なところはわかりませんし,LCDごとに異なる設定が必要な箇所もあります。
とりあえず必要っぽいのは,SYS ENというコマンドと,LCD ONというコマンドを発行することのようです。ところが,これだけだとどうも動作が不安定になってしまいます。具体的には,4ビット目のデータが電源投入のタイミングによって,ちゃんと反映されたりしなかったりするのです。ですから,すべてのセグメントを点灯させようとしてすべて1を書き込んでいるのに,時々一部のセグメントが点灯しません。8になったり9になったりするのです。
きっとタイミングの問題だろう,特に最後に書き込むデータがおかしいのならデータ送信後に送るCSのタイミングじゃないかと,ここのウェイトを調整しましたが,発生頻度に変化こそあれ,完治しません。困った。
そこでもう少し調べて見ると,RC256kというコマンドを発行すれば安定して動作することがわかりました。
(4)マッピングを調べる
初期化までは出来ました。この時点でRAMにデータを書き込みさえすれば,何らかの表示が行われるようになりました。
次は思い通りにセグメントを点灯させるために,RAMとセグメントの対応を調べます。
HT1621は,32x4bitのRAMを持っており,これが各セグメントに繋がっています。よって全部で128のセグメントを個別に操作できるわけです。しかし,どこにどのセグメントが繋がっているのかが分かりません。これはもう,地道に実際に点灯させて調べていくしかないです。
私の場合,まず32のすべてのアドレスに対し1を書き込み,点灯したセグメントに「1」とLCDの写真に書き込みました。次にデータを2にして同じ事をやり,次に4,その次は8と繰り返します。
終わったら,アドレス1つに0x0fを書き込んで,点灯したセグメントに対してアドレスを書き込んでいきます。結局,128の組み合わせを総当たりで試したわけですね。
そして,分かったマッピングから,フォントを作ります。あまり複雑な関数を作るとメモリが足りないとか言われそうなので,必要最小限の機能に絞った小さな関数で済むようにします。
ということで,LCD8812K4-01を実際に動かしたソースです。これを実行すると写真のように左から0,1,2,3,4,5・・・と10桁分数字が並びます。
//
// LCD8812K4 TEST for tiny13 April 5, 2016
// Clock : 1.2MHz internal
//
// Fuse bit Configuration (check = 0)
// Clock:1.2MHz(Internal)
// CKSEL1-0 : 10 9.6MHz
// SUT1-0 : 10
// CKDIV8 : 0
#include <avr/io.h>
#include <util/delay.h>
#define F_CPU 1200000UL
#define CS _BV(PB0) // CS
#define DAT _BV(PB1) // DATA
#define WR _BV(PB2) // WR
void LCD_WRITE_BIT(unsigned char c);
void LCD_COMMAND(unsigned char c);
void LCD_DATA(unsigned char addr, unsigned char data);
void LCD_DISP(unsigned char c, unsigned char n);
int main(void)
{
unsigned char i;
DDRB = 0b11111111; // PB = All output
PORTB = 0b00000000; // PB = All Low
// LCD Init
LCD_COMMAND(0x02); // SYS EN
LCD_COMMAND(0x06); // LCD ON
LCD_COMMAND(0x52); // RC256k
//LCD Clear
for (i=0 ; i<32 ; i++){
LCD_DATA(i, 0x00);
}
LCD_DATA(15, 0x04);
LCD_DISP(5, 9);
LCD_DISP(7, 8);
LCD_DISP(10, 7);
LCD_DISP(13, 6);
LCD_DISP(16, 5);
LCD_DISP(18, 4);
LCD_DISP(21, 3);
LCD_DISP(23, 2);
LCD_DISP(25, 1);
LCD_DISP(28, 0);
while(1);
}
void LCD_WRITE_BIT(unsigned char c)
{
PORTB &= ~WR;
_delay_us(1);
if (c>0){
PORTB |= DAT;
}else{
PORTB &= ~DAT;
}
_delay_us(1);
PORTB |= WR;
_delay_us(1);
}
void LCD_COMMAND(unsigned char c)
{
unsigned char i;
PORTB &= ~CS;
LCD_WRITE_BIT(1);
LCD_WRITE_BIT(0);
LCD_WRITE_BIT(0);
LCD_WRITE_BIT(0);
for (i=0 ; i<8 ; i++){
LCD_WRITE_BIT(c & 0x80);
c <<= 1;
}
PORTB |= CS;
}
void LCD_DATA(unsigned char addr, unsigned char data)
{
unsigned char i;
PORTB &= ~CS;
LCD_WRITE_BIT(1);
LCD_WRITE_BIT(0);
LCD_WRITE_BIT(1);
if (addr == 8){
data++;
}
for (i=0 ; i<6 ; i++){
LCD_WRITE_BIT(addr & 0x20);
addr <<= 1;
}
for (i=0 ; i<4 ; i++){
LCD_WRITE_BIT(data & 0x01);
data >>= 1;
}
PORTB |= CS;
}
void LCD_DISP(unsigned char c, unsigned char n)
//
// Column 10 9 8 7 6 5 4 3 2 1
// 28 25 23 21 18 16 13 10 7 5
{
if (n==0){
LCD_DATA(c , 0b00001111);
LCD_DATA(c+1, 0b00001010);
}else if (n==1){
LCD_DATA(c , 0b00000110);
LCD_DATA(c+1, 0b00000000);
}else if (n==2){
LCD_DATA(c , 0b00001011);
LCD_DATA(c+1, 0b00001100);
}else if (n==3){
LCD_DATA(c , 0b00001111);
LCD_DATA(c+1, 0b00000100);
}else if (n==4){
LCD_DATA(c , 0b00000110);
LCD_DATA(c+1, 0b00000110);
}else if (n==5){
LCD_DATA(c , 0b00001101);
LCD_DATA(c+1, 0b00000110);
}else if (n==6){
LCD_DATA(c , 0b00001101);
LCD_DATA(c+1, 0b00001110);
}else if (n==7){
LCD_DATA(c , 0b00000111);
LCD_DATA(c+1, 0b00000010);
}else if (n==8){
LCD_DATA(c , 0b00001111);
LCD_DATA(c+1, 0b00001110);
}else{
LCD_DATA(c , 0b00001111);
LCD_DATA(c+1, 0b00000110);
}
}
あんまり綺麗なソースではないのですが,ややこしいことはしていませんので読むのは簡単でしょう。
関数は,まずはビット単位の書き込みを行うLCD_WRITE_BITがベースになります。そして,コマンドを発行するLCD_WRITE_COMMANDとデータをRAMに書き込むLCD_WRITE_DATA,最後に数字を任意の桁に表示することの出来るLCD_DISPを作ります。
LCD_WRITE_BITは説明も不要なくらい愚直なもので,データを置いてからWRを立ち上げてデータをシフト,WRをバタバタさせて繰り返し,すべて終わったらCSをHighにするだけです。
LCD_WRITE_COMMANDは「100」で始まるコマンドを発行するものです。コマンドを示すIDである先頭の100に続けて,8ビットのコマンドと1ビットのダミーの,合計9ビットを続けて送ります。
そうすると合計12ビットです。関数内であまりビット操作をしたくないなあと思ってちょっと考えたのですが,12ビットのうち,後半8ビットだけを引数で渡し,すべてのコマンドに共通となる先頭4ビットは関数内部で付加します。
そうするとコマンドのコードが仕様書と違ってしまいます。例えばSYS ENは100に続けて00000001を送り,最後にダミーを1ビット送ることになっているので,続けて書くと100000000010と12ビットになります。
このうち,1ビット目から9ビット目までの8ビットが,仕様書に書かれる「コマンドコード」になるため,0x01がコマンドとなるわけです。
今回は,この関数に渡す値として,100にもう1つ0をくっつけ4ビットとし,残った8ビットである0x02を送る事にしました。
同様にLCD ONは0x06を,RC256kは0x52を渡してやることで発行されます。
今,改めて考えて見ると,コマンドを仕様書通りに渡す方法でも,別にコードサイズは変わりません。かえってわかりにくくなってしまっただけでメリットのない実装になってしまいましたが,まあ動いているからよし。
さて,コマンドを送るのは初期化の時だけで,以後はデータばかりです。データについては,アドレスが自動でインクリメントされ,データを連続で送り込むことの出来るモードもあるのですが,今回はそこまでしなくてもいいと思うので,簡略化のためにいちいちアドレスを送る関数だけで対応します。
LCD_WRITE_DATAがデータをRAMに書き込む関数ですが,これもまあ単純です。データを示すIDである101を最初に送り,続けて6ビットのアドレス,続けて4ビットのデータを送ります。
ところで,1箇所だけ工夫があります。実は,2つあるコロンのうち,右側のコロンがアドレス0x08,データ0x01にマッピングされており,2桁目のセグメントの一部とアドレスを共用しているため,2桁目にデータを書き込むと消えてしまうのです。
消えないようにするには,アドレス0x08のデータを読み込んでORを取る事になるんですが,前述のようにRD端子が出ていないため今回は出来ません。ならマイコン側に仮想VRAMを持って,あとでまとめて書き込むという方法もありますが,tiny13Aでそんなもったいないことはできません。
そこで,今回は共用されているアドレスが来たときだけ,コロンを強制的に点灯させるように1ビット目を1にする(ただインクリメントするだけ)という処理をいれています。
そして最後の関数,LCD_DISPです。これは表示させる桁と数字を渡せば0から9までの数字が出るという簡単な物です。コメントにもあるように,左から順に1桁目,2桁目・・・と続き,右端が10桁目になります。
桁の指定は表示桁ではなく,表示桁に対応するアドレスを直接指定することにしています。理由はコードサイズを減らしたかったということと,桁とアドレスが1対1で対応しているので,プログラマーが注意するだけでいいからです。
今回は,コードサイズの関係もあるし,用途として数字を表示するだけですので,他の文字や記号などのセグメントはサポートしませんし,表示する文字についても数字のみでAやbなどのアルファベットは扱いません。
このLCDのセグメントのマッピングを見てみると,2つのアドレスを使って1つの桁を表示しています。幸いないことに,1つのアドレスが複数の桁や別のシンボルに割り当てられているケースは前述のコロン以外にはなく,ややこしい処理が必要なくて助かりました。
頭のいい人なら,0から9までの数字の表示に規則性を見つけて,もっとコードを綺麗に仕上げるのだろうと思いますが,私はアホですし単純作業が好きなので,0から9までべた書きです。こうすると自由にフォントがデザイン出来るという面白さもあるのですが,ちょっといじってみたところただ読みにくくなるだけだったので,一番標準的なものに戻してしまいました。
余談ですが,7セグメントLEDのドライバというのはTTL全盛の1960年代から存在していて,7447なんかが有名どころです。ところが,同じ機能の4000シリーズのCMOSとTTLの7447とでは文字の形が大きく違っていました。例えば6は一番上の横棒があるのがCMOS,ないのがTTLです。
1960年代の,7セグメント表示器黎明期を思い出してみると,ゼロだって8の上半分がない小さいものが結構見られました(カシオミニはそうでしたよね)し,セグメントそのもののデザインだって,もっと細かったり丸みを帯びていたりと,個性的だったように思います。
これが1980年代くらいなると,現在の形に収れんしてきて,これ以外のものは見なくなりました。やはり,見やすさで淘汰されたんだなあと思います。
ということで,LCD8812K4-01が表示器として動き出しました。下側にあるキャラクタ表示については全く触っていませんが,そのうち検討してみることにします。
あとはこのLCDを使って,TCXOクロックにまとめるだけです。これがまた,コードサイズとの戦いだったのですが,それはまた後日に。