しまねソフト研究開発センター(略称 ITOC)にいます、東です。
これは mrubyファミリー Advent Calendar 2023 の12日目の記事です。
はじめに
私は、ITOCで軽量Ruby言語の1種類である、mruby/c を開発しています。
mruby や mruby/c は、C言語との連携ができると応用範囲が広がり、機器の柔軟性・カスタマイズ性もとても良いものとなりますが、なかなかその使い分けの勘所が分かりづらいようです。
一方、ITOCでは EdgeTech+2023 への出展があり、展示用デモ機がなにか欲しいので、mruby/c によってカスタマイズ可能な 7セグメントLED表示デバイスを作ろうと思いました。
この記事はその製作記であるとともに、mruby, mruby/c の使いどころはここだ! という事を示したいと思って書きます。
成果物
7セグメントLEDとダイナミック点灯について
7セグメントLEDは、棒状に光るLEDを7つ使って数字を表示するためのデバイスです。加えて小数点を表示できるので、一つのデバイスにLEDは合計8個ついています。配線は9本あり、1本がコモン端子で残り8本が各LEDに接続されます。
従って、1桁表示するのに8本のGPIO端子を出力として使用します。今回6桁ありますので、合計 8x6=48本のGPIO端子が必要ですね・・・ムリです。今回使うマイコン PIC32MX270 には、そんなに端子がありません。
通常、このような多桁の駆動には、ダイナミック点灯というやり方が使われます。仕組みは、こうです。
- 1桁目LEDのコモン端子だけに電圧をかける。
- 1桁目LEDの各LED端子に表示パターンの電圧をかけて、意図したパターンを点灯させる。
- これを、2桁目、3桁目・・・と順番に繰り返す。
このような工夫をすることによって、コモン端子以外の8本をまとめることができます。
瞬間的には1桁しか点灯していませんが、すばやく繰り返す事によって人の目の残像現象により、全桁が点灯しているように見えます。
また、コモン端子も一度に複数のビットが同時にはONしないので、ラインデコーダICを使って 6本のコモン端子を3本のGPIO端子で制御します。よって合計 8+3=11 本となり、これなら PIC32MX270 でもピン数が不足することはありません。
このようなダイナミック点灯を、ハードウェアだけでやってくれるとソフトウェア技術者はとても楽になるのですが、コストとか色々な要因によりそうも言ってられないので、いい感じのところで折り合いをつけることになります。
ハードウェア
Rboard という、mruby/c ready なマイコンボードがあり、メーカーからハードウェア設計情報などもオープンソースで公開して頂いています。
開発時間も限られるので、この設計データをそのまま使わせて頂きます。
基板構成は、メインボード(CPUボード)と、7segLED搭載ボードの2枚構成としました。
メインボードには、Rboard をそのまま使うのが簡単で良かったのですが、手持ちのプラケースに入れたかったので、そうなるとサイズがちょっとあわず、新規に作ることにしました。
メインボード(CPUボード)
回路は、オリジナルの回路をそのまま使います。ただし、オンボードLED等は不要なので省きます。
基板サイズは、約64x55mm とし、アートワークは引き直します。
せっかくなので、WiFiモジュール SiliconLabs AMW037 を直接載せられるパターンをつけておきます。
LEDボード
ケースのサイズぎりぎりまで使って、7segLED を6つ並べることができました。
この基板に、LEDドライバとなるトランジスタアレイとラインデコーダも載せます。
ファームウェア
メーカーから提供されている mruby/c ファームウェアをそのまま使います。
と、思ったのですが、回路図上はマイコンが PIC32MX 270 なのに、実際の Rboard とそのファームウェアは、PIC32MX 170 が採用されています。。。だまされた。
ボードは270で既に作ってしまいました。仕方ないので、PIC開発環境でマイコン種類を 270 に変更しビルドしなおして使います。
後述しますが、この Rboard 用のファームウェアは、ある程度ですがカスタマイズが容易にできるように色々工夫がしてあり、このような変更であれば本体への変更はわずかですみます。(とは言っても、C言語は柔軟性に乏しいので、できることはかなり限られますが)
ソフトウェア
ここからが本番です。mrubyプログラムによって 7segLEDに数字を表示します。
今回は、7segLEDに以下の通り配線しています。
segment | GPIO |
---|---|
A | B0 |
B | B1 |
C | B5 |
D | B7 |
E | B10 |
F | B11 |
G | B13 |
RDP | B14 |
よって、数字1を表示したければ、B1とB5をONすれば良いことになります。
また、コモン端子は A0,A1,A2 端子を使って、0b001から0b110 までスキャンします。
まずはテスト
まずは、ハードウェアのテストを兼ねて、あまり考えずにとりあえず表示させてみます。
プログラムは以下のとおり
sel = %w(A0 A1 A2).map {|pin|
GPIO.new(pin, GPIO::OUT)
}
dig = %w(B0 B1 B5 B7 B10 B11 B13 B14).map {|pin|
GPIO.new(pin, GPIO::OUT)
}
PATN = [ 0b0011_1111, 0b0000_0110, 0b0101_1011, 0b0100_1111, 0b0110_0110,
0b0110_1101, 0b0111_1101, 0b0010_0111, 0b0111_1111, 0b0110_1111 ]
$disp = "123456"
while true
6.times {|col|
disp1 = PATN[ $disp[col].ord - 0x30 ]
col += 1
sel[0].write( col[0] ) # 桁の選択
sel[1].write( col[1] )
sel[2].write( col[2] )
dig.each_with_index {|d,i|
d.write( disp1[i] ) # その桁のセグメントON/OFF
}
sleep_ms 1
}
end
- 表示内容を、$disp = "123456" に入れておく
- 1つのタスク(上記)で、6桁のうちの 1桁表示 > 1ms待つ > 消す > 次の桁表示を繰り返す。
- もう1つのタスク(省略)で、$disp に入っている文字列を、1秒おきにローテートする。
...表示はされていますが、汚いですね。特に左側の桁、消灯しているはずのLEDがぼんやりと点灯しているように見えます。これは俗に光漏れといわれる現象です。
現在のGPIOのAPIでは、1ビットずつしかON/OFF できないため、プログラムの書き方によってはですが、例えば 3を4にしようとした場合、一気に
0b0011 -> 0b0100
とはできず
0b0011 -> 0b0111 -> 0b0110 -> 0b0100
となる場合があります。このように意図しないパターン 0b0111, 0b0110 が混入することで、一瞬ではありますが点灯させるべきではないLEDが点灯してしまいます。これがLEDディスプレイにおける光漏れです。
もう少し考えた版
LEDの点灯ビットパターンを変更する時、一旦全消し (0b0000) を挟むことで、光漏れを無くします。
速度的に不利になりますし、ちょっと面倒だし必要以上に複雑に見えます。
while true
6.times {|col|
disp1 = PATN[ $disp[col].ord - 0x30 ]
col += 1
sel[0].write( col[0] ) # 桁の選択
sel[1].write( col[1] )
sel[2].write( col[2] )
dig.each_with_index {|d,i|
d.write( disp1[i] ) # その桁のセグメントON/OFF
}
sleep_ms 1
dig.each {|d|
d.write( 0 ) # 一旦全消し
}
}
end
そもそもそれはRubyの仕事か?
mrubyプログラムによるテストによって、光漏れを無くすアルゴリズム(というほど大げさでも無いが)を作ることができました。この仕事をこのまま引き続きmrubyにやらせるべきでしょうか? 答えは、否です。
こういうのは技術的には面白いですが、EXCEL + VBA でパックマン の開発を頑張る的な物を感じます。本来の用途とは違うけれど、そのハードルを乗り越えるのが面白いといった感覚です。
また、今は表示を変更する別タスクがとても軽い処理しかしていないから、ちゃんと表示が見ることができるレベルですが、重い処理すると、きっと表示がぱらぱらとばらつく感じになってしまうはずです。
このようにタイミングが重要 & アルゴリズムが既に定まった & 処理が単純の3拍子そろったものは、さっさと別案にゆずるべきです。
割り込み処理へ委譲
ダイナミック点灯をオフロードする選択肢としては、2つあります。
- ハードウェアで実装
- 割り込み処理で実装
それぞれメリットデメリットがあります。
ハードウェアで実装案は、最初からそのように設計をし直す必要があり、回路規模もそれなりに大きくなるためコストもかかりますが、ソフトウェアは最も簡単になり表示のジッターもほぼゼロにできます。
割り込み処理で実装案は、タイマー割り込みを使って割り込み処理ルーチンで実現する方法です。
PIC32の開発環境は割り込み処理もC言語で記述でき、ハードウェアの改造も必要ありませんので、今回はこちらを採用します。
mruby/c とのインターフェース
RAM上に、表示ビットのフレームバッファ 6桁なので6バイトを用意し、mruby/c からはフレームバッファへのアクセスメソッドを用意するようにデザインします。
segled_bit( column, 0b1111_1111 ) # column = 1..6
ついでだったので、よく使うであろう標準的な数字出力に関しては、メソッドを用意しました。
segled_digit( column, "3.14" )
タイマー割り込み処理
都合の良いことに、mruby/c (Rboard) のファームウェアでは 1ms ごとのタイマー割り込みを使ってタスクスケジューラを動かしています。この割り込みルーチンに LED点灯処理を追加してやれば目的が達成できます。LED点灯処理では、フレームバッファのビット列をそのままGPIOに出力します。
// Timer1 interrupt handler.
void __ISR(_TIMER_1_VECTOR, IPL1AUTO) timer1_isr( void )
{
segled_irq(); # 追加
mrbc_tick();
IFS0CLR = (1 << _IFS0_T1IF_POSITION);
}
void segled_irq(void)
{
static int col;
LATxCLR(2) = 0b0110110010100011; // clear Bx -> ALL OFF
LATxCLR(1) = 0b0111; // clear A0,1,2 -> ALL OFF
uint8_t src = frame_buffer[col];
uint16_t dst;
dst = (src & 0b11000000);
dst <<= 1;
dst |= (src & 0b00110000);
dst <<= 2;
dst |= (src & 0b00001000);
dst <<= 1;
dst |= (src & 0b00000100);
dst <<= 3;
dst |= (src & 0b00000011);
LATxSET(2) = dst; // set digits.
LATxSET(1) = ++col; // set row.
if( col >= N_COLUMN ) col = 0;
}
プロダクトとしての最終デザイン
最終的に、
- カスタマイズの必要が無い部分(ダイナミック点灯)は、ソフトウェア技術者が、C言語+割り込み処理できっちり作り込む。
- 表示内容、デザインに関わる部分は、デザイナーが簡易な言語 mruby, mruby/c を使って、自由に表現する。
このような使い分けが可能になり、ソフトウェア技術者にとって手離れが良く、応用範囲が広いプロダクトになりました。
デモンストレーション
EdgeTech+2023 会場用デモ用の表示です。
ソースコードは長いので省略しますが、展示会前日の午後から、約4時間で書けました。
ファームウェアの改造点
今回行ったファームウェアの改造点について、ポイントだけかいつまんで記述します。
ステップ1
Rboardのクローンを作りましたが、マイコンの型番が微妙に違いました。そのため、PICの開発環境 MPLAB X 上で、PIC32MX270F256B に変更します。
ステップ2
マイコンの種類を変更しただけだと、PIC32MX170F256B/model_dependent.h
のコンパイル時に以下のメッセージがでて叱られるので、270用の定義ファイルを書きます。
Change the project property, xc32-gcc Include directories to the MPU you want to use.
170のファイルをコピーして必要な箇所を書き換えます。ディレクトリ名は何でも良いです。
cp -r PIC32MX170F256B PIC_mAb_r2
MPLAB X のプロジェクトプロパティーで、Include directoris
を、PIC32MX170F256B
から PIC_mAb_r2
に書き換えます。
ステップ3
各ペリフェラル用のヘッダファイルを用意します。これは、マイコンの種類ごとにピンアサインなどが違うため、それを記述したファイルになります。
- adc_dependent.h
- pwm_dependent.h
- spi_dependent.h
- uart_dependent.h
たとえば、uart_dependent.h
だと、以下の通り。
/*! UART TxD pin setting table
Pin assign: DS60001168L TABLE 11-2: OUTPUT PIN SELECTION
*/
static const uint8_t UART_TXD_RPxnR[NUM_UART_UNIT] = {
0x01, // U1TX = 0001
0x02, // U2TX = 0002
};
/*! UART RxD pin settng table
Pin assign: DS60001168L TABLE 11-1: INPUT PIN SELECTION
*/
static const uint8_t UART_RXD_PINS[NUM_UART_UNIT][5] = {
// U1RX
// A2 B6 A4 B13 B2
{0x12, 0x26, 0x14, 0x2d, 0x22},
// U2RX
// A1 B , B1 B11 B8
{0x11, 0x25, 0x21, 0x2b, 0x28},
};
これも、UARTだと uart.c に、170用の記述があるので、それを雛形としてコピーし使います。
ステップ4
オンボードデバイス用の関数をオーバライドします。
- void system_init()
- void onboard_led( int num, int on_off )
- int onboard_sw( int num )
この3種類の関数は GCC拡張の weak属性をつけてもらっているのでそれを利用すると、別ファイルに書くことでそのボード用にオーバライドできます。
今回は、オンボードスイッチもオンボードLEDも無いので、空の関数にしておくと良いです。
このような感じで、Rboardのファームウェアは、最低限の変更で別プロダクトに応用できるように工夫されています。
おわりに
このボード、WiFiが載ってるんですよね。EdgeTechの会場は WiFi が使い物にならないので使っていませんでしたが、WiFiでサーバ連携できるんですよね。もうすこし、いじり甲斐がありそうです。
それでは、Happy Holidays!