Arduino Unoで電光掲示板をつくる
作成日時:2016年03月02日 00時56分11秒
更新日時:2016年03月10日 00時08分11秒
⚠
この記事は1年以上前に投稿された記事です。 この警告表示について
マイコンを使って何かを表示する場合に、一番多用されるのは安価に入手できるLEDや7セグLED、キャラクタ液晶といったものだと思いますが。今回はそのLEDのうちドットマトリクスLEDというものを制御して電光掲示板を作ってみたいと思います。
はじめに
まずドットマトリクスLEDとは何かというと、1つのユニットに複数のLEDランプを二次元的に規則正しく並べたものです。例えば今回使用しているLEDユニットでは縦8×横8個、合計で64個のLEDランプが使われています。これを制御することで文字やイラストといったものが表示できるようになります。
用意するもの
- 前回作成したドットマトリクスLED表示器
ただし、これは長辺を一度に制御するように設計しているので、今回の使用方法だと輝度にばらつきが出ます。これを出ないようにするには、前回の図面では各ユニットのカソードをまとめているのですが、これをアノードでまとめ、抵抗をつけている部分にシンクドライバタイプのトランジスタアレイを追加し、アノード側に抵抗を付けシフトレジスタとつなぐ(部品の仕様位置をシフトレジスタ以外真逆にする)ことで置き換え可能だと思われます。 - FONTX形式の8×8ドットフォント
今回は恵梨沙フォントのX-Windows版を使い、前回FONTXファイルを扱った時に利用した変換ソフトでFONTXファイルに変換しました。今回美咲から変更した理由は。美咲は画面での閲覧に最適化されており、フォントは7×7以内に収まっていますが。今回は別に8ドットフルで使って構わないため。今までたくさん使用してきた美咲フォントは最初からFONTXファイルが用意されているので、勿論そちらを利用しても問題無いです。
制御方法
ドットマトリクスLEDは、縦横のLEDの縦1列のアノード・横1行のカソード若しくはその逆を一つにまとめたものです。よって制御方法は基本的にはダイナミック点灯方式の7セグLED制御と変わりません。単純に制御した結果が数字の形ではなく1行で表示されるだけです。
今回はビットを回転させるのが面倒くさいので……電光掲示板っぽく文字を流すので縦長で使います。
文字パターンの作成
Raspberry Piで制御する場合は、色々とやりようがありますが。Arduinoでは制約が多いのと、できれば表示時はSDカード無しのドットマトリクス単体で使えるようにしたいため、1つのIOピンを使用して電光掲示板モードにするのか、それとも表示する文字を入力するモードにするのかを切り替えられるようにします。
そこで今回は
- ボタンを押しながら起動・リセットを行うと記録モード
- 文字転送を待機する
- 文字が入力されたら文字コードを格納
この時半角英数字の文字コードなら全角に変換 - 転送した文字コードよりフォントから文字パターンを作成
- 作成したパターンを内蔵EEPROMに格納する
- リセットされるのを待つ
という手順にしたいと思います。
文字パターンを作成するのは前回利用した関数を使います。全角半角変換用関数も作りますが。ShiftJISは全角記号が散乱しており、変換コードが長くなってしまうので。今回はライブラリとして抜き出しました。
このライブラリには前回作成したFONTX形式のフォントファイルを読み込む関数と、半角→全角変換の関数の2つが格納されています。使用方法は当ライブラリとSD・SPIのライブラリをインクルードしてSDカードを読み込んでやれば使えるようになります。(詳しいことはReadMeファイルに書いてありますのでそちらを読んでください。)
作成したパターンの格納
作成したドットマトリクスパターンの格納法ですが。考えられる方法としては3つ程度あると思います。
ひとつはSDカードなどにファイルとして置く方法。2つ目は内外のEEPROMを利用してデータを格納する方法。3つ目は配列に格納する方法です。
SDカードにファイルを置くとPCからでも操作でき便利ですが。今回は文字を設定してしまったあとはSDカードが無くても動作するようにしたいためこの方法は見送りました。
また、配列に格納する方法は電源を切ると初期化されるので。これも今回の趣旨とは違うものなのでやめました。
最後に残ったのはEEPROMに格納する方法ですが。Arduino Unoに使われているAVRマイコンには1024バイトのEEPROMが内蔵されていますので。今回はこれを使うことにします。1024バイトあれば今回利用する8×8ピクセルのフォントデータで127文字(先頭1バイトを文字数格納として利用するため)、16×16のフォントデータで31文字分のデータが格納できます。(ただし今回はシリアル通信のバッファサイズが64バイトなので32文字を上限として作成します。)
しかし、動作プログラム自体が900バイト近くメモリを使っているので実際は512バイト(8×8 64文字 16×16 16文字)程度が限界でしょう。SRAMを増設するという手もありますが。今回はハードウェアが8×16ドットなので内蔵メモリだけで何とかさせます。
というか、本格的な電光掲示板を作るのならArduinoならM0みたいなARM系コアのメモリサイズの大きなものを使うか。それこそRaspberry Piを使ったほうが絶対にいいと思います。
表示機構
実際にパターンを読み込んで表示させる部分ですが。
EEPROMに格納されたデータを一旦RAMへ読み込んでしまい、それをSPI転送でシフトレジスタに転送してやろうと思います。表示データは一定回数書き換え後に配列を1つずらすことで文字を流します。なお、綺麗に流れているように見せるためには、前後に空白が必要になるので実際は表示文字数+4文字分の変数を確保するようにします。
回路説明
まず表示器を作りますが。
前回と全く同じものを作っても一応は使うことが出来ますので、前回の回路図を載せておきます。
ただし、これだと表示ドット数によって明るさが変わってしまいますので前述のとおり、LED GND切り替え部分と桁切り替えの部分を入れ替えたほうがいいかもしれません。その場合の回路は以下のとおりになると思いますが。この回路図の動作は未検証なので作成する場合は十分検証してから使ってください。
なお、接続についてはSPIで接続するのでSDカードと共通のところに接続します。(SS用のIOだけ分けてください。)
接続位置については既に何度も書いてあるので今回は載せません。
わからない場合は以下の記事を参考にしてください。使用しているものは出力先が液晶モジュールになっている以外は同じです。
なお、モード切り替え用のswitchは以下の回路図のような回路になります。
心配な人はIOピンとスイッチの間にも抵抗を入れてください。無くても動きます。
スイッチとGNDの間にある抵抗の抵抗値は10kΩ前後で好きなものでいいです。
コーディング
今回は多くを説明すると長くなりますので。要所だけ説明をして全体のコードを貼り付けておきます。
まずシフトレジスタへデータを転送する部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
////// シフトレジスタ側の転送命令 ////// // long型1つをchar型4つに切り離して転送する命令。 void data_tr_long(int tr_byte, unsigned long data) { unsigned char d[tr_byte]; int i = tr_byte, r = 0; while (i--) d[r++] = (data >> (i * 8)) & 0xff; data_tr(tr_byte, d); } // データの転送用命令 // 配列を後ろから転送する void data_tr(int tr_byte, unsigned char *data) { digitalWrite(SPI_SSel, 0); while (tr_byte--) SPI.transfer(*(data + tr_byte)); digitalWrite(SPI_SSel, 1); } |
今回は3バイトで制御できますので転送データをunsigned longで扱っています。
これはRaspberry Piのコードを流用したことによるもので。ArduinoのSPIライブラリは一度に1バイトごとの転送にしか対応していないので、unsigned longだと一度に転送することが出来ません。
そのために一度unsigned longの値をunsigned charに切り分けて転送しています。
次にシリアル通信の受信データを取り込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// データ受信待ち while (!Serial.available()) { // 受信待機中は先頭行のドットマトリクス1ドットを点滅させてお知らせする data_tr_long(3, 0x010001); delay(250); data_tr_long(3, 0x010000); delay(250); }; // 受信データを取り込む while (Serial.available()) { unsigned int zen = 0; r_kanji[r] = Serial.read(); if (!(r & 1) && r_kanji[r] <= 0x80) { zen = AsciiZen(r_kanji[r]); r_kanji[r] = zen >> 8; r_kanji[++r] = zen & 0xFF; } |
まずデータの到着を待ちます。
到着を待っている間は、そのことを示すためにドットマトリクス表示器の端1ドットを点滅させておきます。
データの到着後はデータを配列に入れ込みますが。受け取ったデータの受信バイト数が偶数でなく、数値が0x80以上であれば2バイト文字だと判断し次のデータを取り込みに行きます。もしそれ以外の数値であれば半角→全角変換に通して全角の文字コードを取得した後、配列に入れ込み、次のデータを受信します。
EEPROMの操作
EEPROMを操作するにはArduinoの標準ライブラリを使う方法もありますが。
実はArduino標準ライブラリも単純にAVRの標準ライブラリに投げているだけなので、ここではAVRの標準ライブラリを使いたいと思います。
- #include <avr/eeprom.h>
標準ライブラリのEEPROMライブラリはこれで呼び出せます。
EEPROMを書き込む処理は以下の2行で行うことが出来ます。
1 2 3 4 5 |
// EEPROM処理待ち eeprom_busy_wait(); // EEPROMに書き込み eeprom_write_byte((uint8_t *)ROMアドレス, (uint8_t)書き込みデータ); |
eeprom_busy_waitはEEPROMが書き込み可能になるまでループする関数で。
EEPROMが書き込み可能になったら抜けます。
eeprom_write_byteが書き込み用関数で。1番目の引数に書き込み対象のROMアドレス。
2番目に書き込む1バイトのデータを指定することで書き込みが行われます。
ちなみにこの書き込み命令はbyteの部分をword・dwordと変えることで2バイトごと、4バイトごとの書き込みができるようです。また、floatにすると実数値も書き込めるようです。
読み込みの場合は以下のとおりです。
1 2 3 |
eeprom_busy_wait(); unsigned int data = eeprom_read_byte(ROMアドレス(uint8_t *)); |
1行目は書き込みと同じです。
eeprom_read_byteが読み込み関数で。()の中にアドレスを指定することて指定されたアドレスのデータが読み込まれます。これもbyteの部分を変更することで2バイトごと、4バイトごとと読み込むことが可能です。
なお、今回はloop関数は使用しません。表示用と書き込み用で関数を分けて、setup関数の中で分岐させてループします。
表示部のループ
1 2 3 4 5 6 7 8 9 10 |
// 表示用ループ // 今回は縦1列を書き換えて行く。 while (1) { data_tr_long(3, ((unsigned long)matrix_pat[(i % DM_LEN) + po] << DM_LEN) | ((unsigned long)1 << (i % DM_LEN))); delayMicroseconds(MAT_LDL); if (++i > (DM_LEN * MAT_DEL)) i = 0; if (i == 0) po++; if (po >= r) po = 0; } |
表示用のループはこのようにwhile(1)で無限ループを作り、unsigned long の値に表示パターンと桁切り替えビットをくっつけて転送しています。文字を流す動作については、文字を表示させるためのループカウンタである変数 i の数値を監視し、変数 i が規定の表示回数になって0に戻ったらオフセット用の変数 po をインクリメントします。
poの値が変数分を超えたらpoも0に戻します。
この数値を元に転送する配列番号を決定しています。
全体的な動作(書き込み)
- シリアルからデータが転送されてくるのを待つ
- 受信したデータを読み込む
- 受信データを元に漢字パターンをフォントファイルから抜き出す
- 全部抜き出し終えたら配列をEEPROMに書き込む
- 書き込み完了を知らせてリセットされるまで待つ
全体的な動作(電光掲示板表示時)
電光掲示板の場合は、EEPROMに格納されているデータを配列に読み出し、それを上のループを使って表示しているだけです。特に特別な動作はしていません。
動作結果
以下にある全体コードを実行すると以下の様な感じになるはずです。
動画
Arduino UnoでドットマトリックスLEDを制御してみました。 pic.twitter.com/Avvqt9X9f2
— Sakura87@多摩提督 (@Sakura87_net) 2016年2月24日
終わりに
というわけでArduino Unoで電光掲示板を作ってみました。
と言ってもハードウェアはRaspberry Pi2向けに作ったものの流用ですけどね。
ArduinoやRaspberry Pi等のマイコンボードを使って何か物を作る場合、程よく複雑な回路が必要で、かつプログラムも比較的大きくなる電光掲示板というものはぜひ作っておきたいアイテムだと思います。
しかも自由な文字を表示することが出来、本格的に作れば実用性も高いです。
今回は実験ということで8×16の小さい電光掲示板だったので余り実用性は高くありませんが。
16×32、16×48くらいの大きさのドットマトリックス表示器を作れば中々実用性が高いものが出来ると思います。
なお、写真ではブレッドボードにLEDが刺さっていますが。これは製作時に動作確認に使用したLEDですので今回は使いません。
なお、今回はunsigned long型に入れ込みましたが。unsigned char型で同様のビットシフト処理を行う場合は、以下のようにすればいいらしいです。
1 2 3 4 5 6 7 8 9 |
int i=32; while(i--){ a[0] =( 1 << i - 23) >>1; a[1] =( 1 << i - 15) >>1; a[2] =( 1 << i - 7) >>1; a[3] =( 1 << i - 0) ; } |
これはたとえばunsigned longと同じ4バイト32bit分ビットシフトで桁を切り替える場合のコードですが。
最初の桁はそのまま1をビットシフトしていけばよく、2番目以降は i からbit数-(8*バイト数)-1をした数引きます。
例えば32bitで右から2バイト目だった場合はa[2]=( 1 << i – 7) >>1;となります。
右から2バイト目以降、最後に1bitシフトしているものは、これを行わないと2バイト目以降に常に1が残ってしまうからです。
ただし、これは例えば桁数が56bit以上(先頭をパターンとした場合のunsigned long longの最大値)になってしまった場合にこうすれば同じことが出来ますよ。というものであり。特に桁数が4バイト(32bit)以内に収まるのであれば、今回のように、最初はunsigned long変数を作ってビットシフトを行い、その後8ビット毎に分割して配列に押しこむ方法のほうがめんどくさくないと思います。
全体のソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
#include <avr/eeprom.h> // 内蔵EEPROMを使うライブラリ #include <SD.h> #include <SPI.h> #include "fontx.h" // 漢字パターン変換はヘッダファイルに移動しました。 #define SD_SELECT 4 ///// ドットマトリックスLED側の設定 ///// // シフトレジスタピンの設定 #define SPI_SSel 8 // 流れる文字の余白 #define DM_MARG 2 // 長辺のドット数 #define DM_LEN 16 // 1行あたりの表示ディレイ(マイクロ秒) // 大きいほど明るくなる #define MAT_LDL 10 // 1ドットずらすまでの表示回数(回) // 文字が流れる速さを決定する // (MAT_LDL × DM_LEN)× 回数 #define MAT_DEL 60 /***** フレームレートについて ***** 長辺のドット数 16ドット 1行あたりのディレイ 100μs 表示回数 60回 とした場合。 100×16 = 1600 μs 8x16pxのLED表示器を1回表示してしまうまでに 1600μsかかり。これを60回なので 1600×60=96000μs つまり合計で96000μs=9.6msのディレイが 発生し104fps程度となる。 ※実際は書き換え自体に時間がかかるので 104fpsよりは少なくなる。 ***********************************/ ///// 全体で使う設定 ///// // ROM書き込みor表示切り替え用ピン #define SEL_PIN 2 // ROM書き込みモード表示用ピン #define WRL_PIN 7 // 最大表示文字数 シリアル通信が64バイトのバッファなので32文字まで #define MAX_LEN 32 ///// EEPROMの構造 ///// // ※初期設定の場合 // 0x0000 表示文字数格納(これを8倍する。) // 0x0001 // | 表示パターン格納 // // パターン格納用変数の設定 unsigned char matrix_pat[(MAX_LEN * 9) + (DM_MARG << 4)] = {0,}; void setup() { // モード切替に使うピンの初期化 pinMode(SEL_PIN, INPUT); // シフトレジスタに使用するピンの定義 pinMode(SPI_SSel, OUTPUT); digitalWrite(WRL_PIN, 0); SPI.begin(); // ピンの値によりモード切替 // L=電光掲示板モード // H=ROM書き換えモード switch (digitalRead(SEL_PIN)) { case 1: Serial_read(); break; case 0: matrix(); break; } } ////// ロム書き換えモード ////// void Serial_read() { unsigned char r = 0, r_kanji[MAX_LEN * 2], pat[8]; int i, p; data_tr_long(3, 0x000000); // SerialPortの初期化 Serial.begin(9600); while (!Serial); // SDカードの初期化 if (!SD.begin(SD_SELECT)) { Serial.println(F("SD card error !")); return; } Serial.print(F("Ready! Max ")); Serial.print(MAX_LEN, DEC); Serial.println(F(" characters.")); // データ受信待ち while (!Serial.available()) { // 受信待機中は先頭行のドットマトリクス1ドットを点滅させてお知らせする data_tr_long(3, 0x010001); delay(250); data_tr_long(3, 0x010000); delay(250); }; // 受信データを取り込む while (Serial.available()) { unsigned int zen = 0; r_kanji[r] = Serial.read(); if (!(r & 1) && r_kanji[r] <= 0x80) { zen = AsciiZen(r_kanji[r]); r_kanji[r] = zen >> 8; r_kanji[++r] = zen & 0xFF; } r++; //char x[10];sprintf(x,"%02d 0x%04X 0x%02X %02X",r>>1 ,zen | (r_kanji[r - 2]<<8 | r_kanji[r - 1]),r_kanji[r - 2],r_kanji[r - 1]);if(~r&1)Serial.println(x); // ↑デバッグ用 文字コード表示したいときはコメント外す。 } // 2バイト文字なので2で割る r /= 2; i = 0; Serial.print(F("Receive ")); Serial.print(r, DEC); Serial.println(F(" characters.")); // 最大文字数より大きい場合は最大文字数でカットする if (MAX_LEN <= r) r = MAX_LEN; // 文字パターン取得ループ Serial.println(F("Kanji pattern Reading.... ")); while (i < r) { // 文字コードを1文字ずつ取り出す unsigned int kanji = (unsigned int)r_kanji[(i << 1)] << 8 | r_kanji[(i << 1) + 1]; // パターンの取得 unsigned long n = KanjiReadX(kanji, pat); // 受信した文字コードと文字をフィードバックする Serial.println(kanji, HEX); Serial.write(kanji >> 8); Serial.write(kanji & 0xff); Serial.println(); // 取得したパターンを配列に書き込む p = 8; while (p--)matrix_pat[(i << 3) + p] = pat[p]; i++; data_tr_long(3, 0xC00000 | (0xFFFF >> (int)((DM_LEN * ((double)i / (double)r))) )); } Serial.print(F("EEPROM Writing... ")); // パターンを格納した配列を内蔵EEPROMに書き込む Serial.print(p = i * 8, DEC); Serial.println(" byte."); while (p--) { // EEPROM処理待ち eeprom_busy_wait(); // EEPROMのアドレス0x01以降に書き込み eeprom_write_byte((uint8_t *)p + 1, (uint8_t)matrix_pat[p]); } // 最大文字数を内蔵EEPROMのアドレス0x00に書き込む // EEPROM処理待ち eeprom_busy_wait(); // EEPROMに書き込み eeprom_write_byte((uint8_t *)0x00, (uint8_t)r); Serial.println(F("EEPROM Written. [ OK ]")); Serial.println(F("Please RESET !")); // 書き込みが完了したら先頭1行を点滅させて書き込み完了を通知 while (1) { data_tr_long(3, 0xFF0001); delay(250); data_tr_long(3, 0x010000); delay(250); }; } ////// 電光掲示板表示部分 ////// void matrix() { // アドレス0x00にある文字数データを読み込んで8倍する。 eeprom_busy_wait(); unsigned int p = eeprom_read_byte(0x00) , r = p + DM_MARG + (p >> 3), i, po, all; r *= 8; p *= 8; all = p; // EEPROMに格納されている表示パターンを読み込む // 読み込みの際にドットマトリクスにプログレスバーを出す。 while (p--) { eeprom_busy_wait(); matrix_pat[p + (DM_MARG * 8) + (p >> 3)] = eeprom_read_byte((uint8_t *)p + 1); data_tr_long(3, 0x030000 | (0xFFFF >> (int)((DM_LEN * ((double)p / (double)all))) )); delay(1); } delay(100); // 表示用ループ // 今回は縦1列を書き換えて行く。 while (1) { data_tr_long(3, ((unsigned long)matrix_pat[(i % DM_LEN) + po] << DM_LEN) | ((unsigned long)1 << (i % DM_LEN))); delayMicroseconds(MAT_LDL); if (++i > (DM_LEN * MAT_DEL)) i = 0; if (i == 0) po++; if (po >= r) po = 0; } } ////// シフトレジスタ側の転送命令 ////// // long型1つをchar型4つに切り離して転送する命令。 void data_tr_long(int tr_byte, unsigned long data) { unsigned char d[tr_byte]; int i = tr_byte, r = 0; while (i--) d[r++] = (data >> (i * 8)) & 0xff; data_tr(tr_byte, d); } // データの転送用命令 // 配列を後ろから転送する void data_tr(int tr_byte, unsigned char *data) { digitalWrite(SPI_SSel, 0); while (tr_byte--) SPI.transfer(*(data + tr_byte)); digitalWrite(SPI_SSel, 1); } // 今回使わないので隅っこに追いやっておく。 void loop() { // put your main code here, to run repeatedly: } |
前後に投稿された記事
前後に投稿された記事(カテゴリー『 Arduino 』内)
関連記事