ラジオの製作(IC制御)その5

正直あまり進んでいません。いろいろあって、3Dプリンターを買おうかと考えたりして、ラジオの製作に回す時間がほとんどない状況になっています。3Dプリンターの件はいずれブログに書こうと思っていますが、とりあえず、今回は表示部分に使う予定のOLEDを動かしてみました。

OLEDを動かしてみる。

前回、ようやくM6951というチップを使ったDSPモジュールでAM受信までこぎつけました。
alasixosaka.hatenablog.com
出来たといってもブレッドボード上にバラックで組んだ状態だし、プログラムもテスト用のものだったので、ここからは、実際のラジオの形にしていく作業を進めることにします。
まず、表示装置をどうするかということですが、冒頭に結論を書いてしまっていますが、今回はOLEDを使うことにしました。
この手のポータブル機器では電池を使うということで一般的には液晶が選ばれることが多いと思います。市販の機器(ラジオ、リモコンなどのたぐい)も大概は液晶がついています。
なぜ液晶かというと圧倒的に消費電力が小さいからで、セグメントタイプの液晶素子を使えば、電流値が1桁のμAレベルというのも普通です。昔、時計を作ろうと思って実験をした時に確かめていますが、圧倒的な低消費電力に驚いた覚えがあります。ただし、液晶を表示させるには、専用のドライバーないし、ドライバーを搭載したマイコンが必要になりますので、それらの消費電流を考慮しないといけません。
これも、昔の実験の時に思い知ったのですが、専用の液晶ドライバーだからと言って低消費電力とは限らないのが注意する点です。その時は何も考えずに、液晶だから低消費電力だろうと安易に実験を始めたものの、グラフィック液晶では10mAレベルの消費電流でこりゃいかんということで、セグメント液晶に変えてみたものの、mAレベルの消費電流だったので、なぜだろうかと悩んだことがありました。この時は、専用のドライバーを使っているモジュールを使ったのでドライバーが電流を消費していたのが原因でした。PICで液晶ドライブできるものを試してみたところ、ようやくμAの低消費電力が達成できました。
前置きが長くなってしまいましたが、今回はいろいろ考えてOLEDを使うことにしました。決め手は夜間の視認性です。目的が山で非常事態に陥った時にも使えるということなので、夜間に使いやすいことが重要だと考えました。液晶だと、昼間の視認性はすごくいいんですが、夜はバックライトを使うか、外から光を当ててやらないと見ることができません。その点OLEDは自発光なので、夜間に関しては余分なものが不要というメリットがあります。その分消費電流は多くなりますが、不要な時は消えるようにプログラムすれば良いと考えました。それに、液晶を使うのであれば市販品を買った方が安いし、簡単というのもあって、普通に売ってないものを作ってやろうというのもあります。
簡単に表示装置の比較を書くと、例えばセグメント液晶と前回テストに使ったAQM1602などの液晶モジュールそれに今回使用する予定のOLEDの比較をすると、消費電流はセグメント液晶≪液晶モジュール≦OLEDとなります。
夜間の視認性はOLED≫液晶モジュール≧セグメント液晶の順になります。また、プログラムの簡便さは、液晶モジュール>OLED>セグメント液晶の順になります。それぞれ一長一短なんですが、今回は夜間の視認性を重視しました。小型のものが良いと思い、128×32ドットのものを例によってAliexpressで購入。ブログの記事なんかでは128×64のものを使っている人が多いですが、基本的な使い方は同じです。
ja.aliexpress.com

どうやって使うか

OLDEモジュールはSPI接続のものもありますが、今回はI2C接続のものを使いました。なぜなら、DSPモジュールがI2C接続だからです。マイコンはPICを使うことを考えていますが、PICではMSPPというのでI2Cのインターフェイスを行いますが、こいつはSPI接続の時にも使うようになっていて共用しています。つまり、MSPPをI2Cに使ってしまうとSPIには使えなくなってしまいます。中にはMSPPを複数持っていてI2CとSPIの両方を使えるチップもありますが、選択肢が狭くなるし、わざわざSPIを使う必要性もないのでI2C接続としました。
I2C接続なので、前回テストに使ったAQM1602と同様に接続線はVCC、GND、SDL、SCKの4本だけです。ただ、AQM1602がコマンドとキャラクタコードを送ってやればそのキャラクターを表示してくれたのに対し、OLEDではグラフィックメモリに値を書き込んでドット表示をすることになるので、そう簡単にはいきません。もっとも簡単にOLEDを使うには、ArduinoでU8GLIBというライブラリを使ってやればその辺の面倒なことをライブラリがやってくれますので簡単に済ますことができます。しかし、今回は低消費電力のためあえてPICで行こうと思います。
OLEDモジュールのメモリの書き方は結構独特で、縦に8bitずつのSegmentとよばれる列が128個並んでいて、これを1ページとし、このページが縦に4つあるという構成になっています。細かく書くととっても長くなるので、詳細を知りたい人は参考サイトを見て下さい。
グラフィックメモリになんか書かないと表示されないということは、そのためのデータを自前で用意しないといけないということになります。OLEDの縦は32ドットなので、最大で縦32ドットの文字が表示することができますが、そこまで大きくすると横の文字数が少なくなってしまいます。横も32ドットとすると4文字しか表示できないことになります。ただ、実物のOLEDモジュールは思いのほか小さく、縦半分の16ドットではいささか見づらい印象なので縦は24ドットにしたいと思います。24ドットだと横に5文字表示できますし、数字だと8文字くらいは表示できそうなので、なんとか使えそうな気がします。表示方法として考えているのは、プリセットで主要な放送局はプログラムで持っておいてその放送局名を漢字などで表示。例えば「ラジオ大阪」とか。Seekで局を探したときは前回のテストみたいに数字で周波数を表示。1314kZHzとか。を考えています。余った8ドットの一行は受信感度なんかを表示させようかと思っています。この辺はまだアバウトで作りながら考えていこうと思っています。

フォントはどうするか

自前でフォントを用意しないといけないですが、すべてを自分で作るのはさすがにしんどいので、フリーのフォントを探します。使うのは最近はあまり使われなくなったビットマップフォントです。今のパソコンはTruTypeというフォントを使って滑らかに表示するのでビットマップフォントはあまり使われなくなっています。しかも、24ドットとなるとあまり種類がありません。その中で今回は日比谷24というのを使いました。ところが、どこからダウンロードしたのか全然わからない状態になってしまいました。なんてこったい。もう一度検索したら一応次のサイトからダウンロードできます。
jikasei.me
しかし、ダウンロードして解凍すると拡張子がttfすなわちTruTypeのフォント。それも、一応FontForgeというソフトを使えばビットマップに変換できる。
higambana.blog.shinobi.jp
ここに出てくるBDFというファイルはAdobeが策定したフォントのフォーマットということらしい。
BDFファイルはASCIIコードで記述されているのでメモ帳でもなんでも簡単に読出しできる。しかし、FontForgeで変換してみても、自分が使った日比谷フォントのBDFファイルとは中身が違っている、ファイル名も違う。
色々調べたが結局わからずじまい。何週間か前にダウンロードしたのに、もうダウンロードサイトがなくなってしまったのだろうか?
これ以上調べても時間の無駄なのでとりあえず、やったことを書いておくことにする。実は、BDFファイルから目的のフォントを取り出しても、そのままではOLEDで表示ができません。なぜなら、上に書いたようにOLEDのメモリの書き方が結構変わっているから。そこで、OLEDにそのまま書き込めるように変換する必要がある。

BDFをOLED用に変換する

これが結構大変だった。BDFの説明はここのサイトが参考になりました。
hp.vector.co.jp

つまり左上を起点に24ドットなら、24ビット(つまり3byte)を左から右にドットを表示するなら1、表示しないなら0という具合に並んでいます。例えば数字の”0”なら次のようになっています。

000000
000000
000000
003c00
00c300
018180
018180
0300c0
0300c0
0300c0
0300c0
0300c0
0300c0
0300c0
0300c0
0300c0
0300c0
0300c0
018180
018180
00c300
003c00
000000
000000
それをドット表示に直すとこんな感じでたしかに数字の”0”になります。

f:id:alasixOsaka:20200809130437j:plain
数字の”0”をビットマップ表示

データの順番は左上から右方向に並んでいます。これをOLED用に縦8ビットずつのデータに直す必要があります。縦の並びが下MSBで上がLSBです。なので例えば、最初の方だけ書くと、0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xE0となります。これを右端までいって終わると次の8ビットは(OLEDの2ページ目で)0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFFというようになります。
数字だけなら10個変換すれば終わりですが、漢字になると種類が多くていちいち手で変換するのも面倒なので、Pythonでプログラムを書きました。その前に、まず元になるデータを用意します。
例えば数字の"0"を変換する場合、次のようになります。
まず、使うBDFファイルを開いて、そこから目的のコードの値を抽出し、CSVファイルとして保存します。
BDFファイルはメモ帳などで開くことができるので、開いておいて、JSIコードから目的のキャラクタのコードを探します。数字の”0”はJISコードで0x2330ですので、ファイルのSTAETCHAEが2330になっているところを検索機能を使って探します。こんな感じにコードが並んでいます。

ENDCHAR
STARTCHAR 2330
ENCODING 9008
SWIDTH 144 0
DWIDTH 24 0
BBX 24 24 0 -2
BITMAP
000000
000000
000000
003c00
00c300



そして、BITMAP以下のところの数字を使います。こいつをコピペでエクセルかなんかに貼り付けて、CSVファイルとして保存します。
ファイル名は何でもいいですが"zero.csv"としました。そして、Pythonで変換します。スクリプトはこんな感じです。

import csv

import numpy as np

import pprint
x = []
d = np.array([0]*24)
p = np.array([0]*72)
fn = "d:font\zero.csv"
a = np.loadtxt(fn,dtype="str",delimiter="/")
mask = 1

#print (len(a))
#for j in range(8):
for i in range(len(a)):
        d[i] = int(a[i],16)
        print (d[i])

for i in range(24):
    pmask = 1
    for j in range(8):
        y=d[j] & mask
        if (y!=0):
            p[(23-i)] = p[(23-i)] + pmask
            #print (23-i)
            #print (format(p[23-i],'02x'))

        pmask = pmask << 1


    mask = mask << 1
    #print (format(mask,'02x'))
mask = 1
for i in range(24):
    pmask = 1
    for j in range(8):
        y=d[j+8] & mask
        if (y!=0):
            p[(47-i)] = p[(47-i)] + pmask
           
        pmask = pmask << 1


    mask = mask << 1
mask = 1
for i in range(24):
    pmask = 1
    for j in range(8):
        y=d[j+16] & mask
        if (y!=0):
            p[(71-i)] = p[(71-i)] + pmask
            
        pmask = pmask << 1

    mask = mask << 1

for i in range(72):
    print(format(p[i],'02x'))

l = p.tolist()
print (type(l))
with open(fn[:-4] + "oled.csv", 'w') as f:
    writer = csv.writer(f)
    writer.writerow(l)
#np.savetxt(fn[:-4]+"oled.csv",p,delimiter=',',fmt='%02x')

まず、CSVファイルから a = np.loadtxt(fn,dtype="str",delimiter="/")でaに読み込んでいます。文字列で読み込まれますので、それを、
for i in range(len(a)):
d[i] = int(a[i],16)
print (d[i])
として、16進数に変換し、配列dに格納します。このビットマップデータは24×24ドットのものですので、各行24ビットが24行分あることになります。つまり、d[0]は一番上の行の24ビット分のビットデータを表します。

そして、ここからが変換の部分です。例えば配列dの最初のデータはビットマップの左上から右に24bitのデータですから、そのLSBすなわち最下位ビットは、OLEDのデータでいうと24個目のデータのLSBとなります。下の図でいうと、SEG23のLSBがd[0]のLSBということになります。SEG23の2ビット目はd[1]のLSB、SEG23のMSBはd[7]のLSBになります。そして、SEG0のLSBはというと、d[0]の24ビット目つまりMSBということになります。

f:id:alasixOsaka:20200920165452j:plain
OLEDのメモリーマップ。縦に8ビットずつ並んでいます。

そこで、マスクとしてmaskを定義し、まず1代入、d[0]にANDして、最下位ビット(LSB)を取り出し、ビットが立っていれば、24個目のデータすなわちSEG23、スクリプト中ではp[23]に値をpmaskの値を加算します。pmaskも最初は1です。そして、pmaskを左に1ビットシフトします。ループ変数jをインクリメントして2ビット目がどうなっているかを、d[1]とmask(ここでは1)をANDしてビットが立っていれば2ビット目が1ですので、SEG23すなわちp[23]にpmaskを加算します。これを8回繰り返して、SEG23すなわちp[23]のデータが完成します。できたら、今度はmaskを左に1ビットシフトし、同じことを24回繰り返したら1ページ目のデータ出来上がります。
for i in range(24):
pmask = 1
for j in range(8):
y=d[j] & mask
if (y!=0):
p[(23-i)] = p[(23-i)] + pmask

pmask = pmask << 1


mask = mask << 1

の部分がそうです。ややこしいですが、紙に書きだしたりしてゆっくり考えるとわかりやすいと思います。私も、これを考えているときは頭がぐちゃぐちゃになりました。

ここからはベタに書いていますが、p[(23-i)] = p[(23-i)] + pmaskとなっているところを p[(47-i)] = p[(47-i)] + pmask、 p[(71-i)] = p[(71-i)] + pmaskとして2ページ目と3ページ目のデータを作っています。ループにした方がスマートなんですが、面倒くさいのでベタな書き方で済ましています。(お恥ずかしいですが)
そして、l = p.tolist()として、numpy配列をlist型に変換して、
with open(fn[:-4] + "oled.csv", 'w') as f:
writer = csv.writer(f)
writer.writerow(l)
として元のファイル名に"oled”を加えたCSVファイルとして出力しています。
このスクリプトだと、一回に一文字ずつ変換する形になるのでその辺は面倒ですが、必要なデータだけを変換するので、全部を変換する必要がないので、まあいいやという感じでやってしまいました。
ただ、数字の場合横幅を24ビットにすると、文字間隔が空いてしまってちょっと見づらいのと、表示できる文字数が減るので、左右4ビットずつを削って幅を16ビットにしました。
先ほどのPythonスクリプトに下の部分を追加して、左右4ビットを削って、配列n[]に格納して、これを出力するようにしました。
for i in range(72):

mod = i % 24
if (mod > 3) and (mod < 20):
n[j] = p[i]
j = j + 1

これでOLEDに表示するビットマップデータが用意できました。まだまだ先は長いですね。


参考にしたサイト
OLED
Analogic Intelligence: MycroPythonとSSD1306 ~ 描画まわり ~
OLEDディスプレイモジュールを試す。(その1) : 趣味の覚書(Junk_Audio)
OLED 0.96インチで遊ぶ (5) - PICから表示 - LSI Jiu-Jitsu