さくらのジャンク箱ロゴ

Sakura87がほぼ月刊でお届けするPCや電子工作と写真のブログ @なんと20周年

Raspberry Pi 2/4で実用的な電光掲示板を作る

この記事は7年ほど前に投稿されました。内容が古くなっている可能性がありますので更新日時にご注意ください。

ご無沙汰しておりますSakura87です。

ここ最近更新が滞っていた理由としては主にこいつでして。基本的にはプログラムがなぜか思い通りに動かないことにハマっておりました。

ということで今回は実用的な電光掲示板を作ります。

お知らせ(2020/04/07追記)

文字コード読み込み部分のコードが間違っていたので修正しました。(なんでこれで動いていたのかは不明。)

Raspberry Pi4での動作を確認しました。現在ページにあるコードでそのまま動きます。
以前のコードでも以下の部分を修正すれば動きます。

        // 文字コード判定
        // 1バイト目が0x80以上なら2バイト文字として認識。
        // 半角カナは使えない仕様
        if (*s >= 0x80) {y+=2;cd=((*s++)<<8)|(*++s);
        }else{y++;cd=(*s++);}

前回のコード183行目です。ここの最後にある*++sを*s++と修正します。
元々のコードはここが問題で2バイト文字が化けていたはずですが、なぜか初出の段階で動いていました。(多分コード修正前のものが誤って後悔されていたものと思われます)

		// 文字コード判定
		// 1バイト目が0x80以上なら2バイト文字として認識。
		// 半角カナは使えない仕様
		if (*s >= 0x80) {
			y += 2; cd = ((*s++)<< 8) | (*s++);
		}
		else { y++; cd = (*s++); }

※コードの行数が違うのは現在のコードは先の作品と使用するフォント等を統一したことによるものです。

きっかけとかそういうの

すこし前までアンプづくりにハマっておりまして。アンプばっかり作っていたので、そろそろデジタルなアイテムを作ってみたく。実用的な電光掲示板を作ってみようと思い作りました。

仕様とかそういうの

まず初めに決めたのは描画領域のサイズですね。英語ならば前回の8×16の電光掲示板でも十分実用的ですが。日本語を扱うのであれば縦幅8pxでは少々きつい。少し前のWEBサイトが10pxの小さな文字で作られていたことを考えると、最低10pxはほしい。

しかし10pxとなるとそんなドットマトリクスLED売ってないし、制御するにしても微妙。そしてaitendoのホームページでよさそうなLEDを見つけたので今回は縦幅は16pxで行くことにしました。

次に横幅を決めますが。1文字でも使えないことはないし、コストも安くていいのだけど。実用的となると2文字は一度に表示したいところ。例えばソニックの車内の電光掲示板とかは8文字くらい表示できますのですがこれを実現しようとすると128ドット必要になるので配線作業が十分死ねる量です。絶対やりたくはありません。

ということで2~8文字の間となると、せめて日本人の標準的な名前くらいは漢字で一度に表示したいということで4文字 64pxで作ることにしました。

色は単色でいいということで。2色とかそれ以上でもいいのだけど制御が大変なので。あと2色以上のドットマトリクスLEDって高いし。

そんなことで作る電光掲示板の仕様はこんな感じに決まりました。

  • 制御はRaspberryPi 2を使う。持っているので。
  • 画面サイズは64×16ドット
  • 前回の回路を応用するので長辺を一度に制御する
  • スクロールさせる
  • 赤色単色1bitカラー
  • ラズパイなので3.3V仕様

部品選定

まずメインとなるドットマトリクスLEDですが、aitendoにある以下のものにしました。

正確にはこのLEDを見つけたからこれを作ろうと思ったので、順序が逆ですが。これは小型で16LED四方。ピン配列もドットと一致しているうえ、ピッチも2.54mmと非常に扱いやすいのでこいつを採用しました。

裏面。

今回も前回と同じく、ラッチ付きのシフトレジスタで制御します。

部品も前回と同じSN74HC595Nです。秋月電子で40円の安いほうですね。縦横どちらの制御にも使いますので今回はこれが10個必要になります。

縦ドットをダイナミック点灯で切り替えて使いますので、トランジスタアレイは16ピンあればよいので2つ使います。今回はカソードコモンでつかうのでNPN型のトランジスタアレイを使います。型番はTBD62083APGです。今回は新型であるTBDシリーズを使いました。

正直今回の用途であればどちらを使ってもよいですしAの前が3か4かもあまり関係ないです。とりあえずNPN型というところだけあっていてLEDの消費電流が許容量を超えていなければOK。(といってもRaspberryPiだとこいつ以外に選択肢ないでしょうけど。)

今回は5mA×64ということで320mA流せれば問題ない。こいつは1chあたり500mA流せるので問題なかろう。できればもうちょっと余裕を持たせたいので実際は4.24mAを1つのLEDに流す。これで271.36mAとなり54%となりなかなか余裕ができている。

基盤はLEDユニットを4つ取り付けられなければならないので大きめを。秋月電子のガラスエポキシユニバーサル基盤の厚めのタイプを使いました。まぁ正直これじゃなくてもいいです。

それと、今回は基板上に実装しましたが。これだけの大きさとなると、基板実装よりなんらかの方法でユニット自体を連結させて空中配線する方法になるか、そもそも基盤を特注する必要が出てきそうです。

そのほかに必要なものは抵抗器ですが、今回は64ドットを一度に制御しますので64本最低必要になります。秋月の100本パックでも半分なくなりますのでまとめ買いができるところで買うのがよいでしょう。

特に難しいことはしませんので何の変哲もないカーボン抵抗でよいです。抵抗値は5mA付近を目指して330Ωを使うことにしました(4.24mA)なお、特に画像はありません。

ちなみに今回は部品の種類も大したことないので簡単に材料を説明しますと。

  • ドットマトリクスLED 16×16ドット
    M1571616C aitendoで税別900円 4個
  • ラッチ付き8bitシフトレジスタ
    SN74HC595N 秋月で税込み40円 10個
  • NPNトランジスタアレイ
    TBD62083APG 秋月で税込み60円 2個
  • 何の変哲もないカーボン抵抗
    330Ω1/4W 秋月で100本100円 64本
  • 大きな基板
    適当なもの 今回は秋月で580円の基板
  • 合計 5088円+送料

という感じ。

それでは材料もそろいましたので組み立てていきます。

製作

なお、回路図については前回のものを延長したにすぎませんので新たに作っておりません。前回の回路図を参考にしてください。

ちがうところとしては、LEDのピン番号が違うのと、LED GND切り替えが2つになっているところくらいで、あとは今回はCOMMONはどこにもつなげていないところくらいですね。

それでは作っていきましょう。

aitendoのデータシートは信頼できないことが多いので現物でどのように光るかを確認します。まぁどこの店で買っても動作テストというのは必要でしょうけどね。

まずはLEDユニットを並べます。だいたいこんな感じでできればよいですね。

部品の位置決めをしてはんだ付けをします。

今回は裏面に部品を配置したほうが都合がよかったのでこんなアクロバティックな形になっています。はんだ付けをする際の注意点としては、LEDユニットが斜めについたりしないようにすることです。定規などを挟んで傾きを調整した後、四隅をはんだ付けしてから残りの作業をしましょう。そうしないと私みたいに残念なことになります。

基盤をカットします。ちなみに基盤ははんだ付け前にカットしたほうが絶対にいいです。

配線完成。死ぬかと思った。

なお、上の写真ではトランジスタアレイ側にも抵抗がついていますが、これでは十分な光量が確保できなかったのでこちらの抵抗は外しています。

完成した基盤表面。見てください、この不揃いなLEDをw

というわけで完成。

ソフトウェアの作成

ハードウェアができましたので次はソフトウェアを作ります。

ソフトウェアの仕様は以下の通りとします。

  • 長辺を一度に制御する
  • SPIポートを使って制御
  • このソフトをドライバとして使えばいいようにしたい
  • というわけでテキストファイルを読み込んで表示させる
  • フォントはFONTX形式のフォントを使う
  • 横書き。縦書きでもいいけどめんどくさい。
  • スクロールできるようにする

以上のような仕様になります。

FONTXファイルの読み込み

FONTXファイルの読み込みはArduinoで作成しましたFONTXライブラリを使いたいと思います。

詳しいFONTXファイルの解説だとかは上の記事を読んでください。

今回はこいつをRaspberryPi用に移植することにします。RaspberryPiであればLinuxなのでほかのフォントなどを使うこともできると思いますが。TrueTypeフォントはめんどくさいし、日本語の仕様書がほぼない。最悪Imagickなんかで画像にしたら早いかなと思ったけど。それならもういっそFONTX使ったほうがいいや。すでに作ってるライブラリがあるし。っていうことでFONTXを採用。

FONTXだと文字コードはShift-JISになる。今のご時世UTF-8が標準だがそんなこと知ったことではない。とりあえず日本語が表示できればそれでいいので。

というわけでRaspberryPi用のFONTXライブラリです。

詳しい使い方は中にある取説を読んでね。

スクロール

今回は横スクロールをさせるわけなのですが、単純にビットシフトをしてもあふれた分は破棄されてしまいます。そこで大したものではないですが変数をまたいでビットシフトを実現させる関数を作りました。

char ARShift(unsigned char *dat,unsigned char p,unsigned char sbit){
	char i=0;for (i=0;i<p;i++) dat[i]=(dat[i]<<sbit)|(dat[i+(i<p)]>>((sizeof(dat[0])<<3)-sbit));
}

特に難しいことはしていないですので詳しい説明は省きますがビットシフトで捨てられる頭をくっつけて変数に格納しているだけです。これで左に流れる横スクロールを実現しています。

ただし、このやり方だと、型が保持できる以上のビットシフトを行うと値が破棄されてしまいますので、char型では最大8bitまでのシフトになります。

フォントの読み込み

FONTXライブラリからフォントを読み込んで連結させるための関数です。

int FontRead(unsigned char* s, unsigned char sp) {
	unsigned int y = 0, z = 0, cd = 0; // ちゃんと0で初期化しないと動作不良
	unsigned char str[FontSizeB] = {};
	unsigned long p;

	while (*s) {
		// 文字コード判定
		// 1バイト目が0x80以上なら2バイト文字として認識。
		// 半角カナは使えない仕様
		if (*s >= 0x80) {
			y += 2; cd = ((*s++)<< 8) | (*s++);
		}
		else { y++; cd = (*s++); }

		// 実際の読み込み
		p = KanjiReadX(cd, str) & 0xffff; z = p;

		// デバッグ用
		printf("[Code] %04X\n",cd);
		printf("Read %d\n",p);

		// 表示用バッファに転送
		if (p == FontSizeB)while (--z)LED_BUF[(z >> 1)][(z & 1) + y + (sp >> 3)] = str[z];
		if (p == FontSizeB >> 1)while (--z)LED_BUF[(z)][1 + y + (sp >> 3)] = str[z];
	}

	return y; //処理した文字数(バイト)を返す
}

まず初めに文字コードを判定します。

単にASCII文字かどうかを判別すればいいので初めに読み込んだ文字コード(1バイト目)が0x80(128)より前か後かで判別します。0x80より前であればASCII文字。後であれはShift-JIS領域という感じで判断します。

Shift-JISであれば2バイト目を読み込んで連結させ、ASCIIであれば今読み込んだものをそのままFONTXライブラリに渡します。

そのあとFONTXライブラリからの戻り値のバイト数を取得します。バイト数がフォント1文字のバイト数と一致していれば全角フォント、その半分と一致していれば半角フォントとして処理をします。今回は16pxが前提となっているのでもう少し雑な処理でもよいですが。将来性を考えてこうしました。

テキストファイルの読み込みおよびデータ転送は特に何の変哲もない標準的な項目ですので割愛します。

動作確認

0xFF(255)を10個転送して全点灯させてみたのだけど。どうやら傾き修正や基盤加工しているときに1ライン死んでしまったようです。なので個人的に何かの完成品として仕上げる際はここから作り直すことにします。

いったんスクロールさせずに正しく制御できているかを確認します。

というわけで電光掲示板の作成が完了しました。以下に実際に動作している動画を置いておきます。

最後に

というわけで今回は実用的なものを作りました。

使用用途としては、例えばイベントなんかで何かしらのメッセージを表示しておくとか、ニュースやツイッターの情報を取得して表示させるとか。いろいろとやれることはありそうです。

言ってしまえばLEDディスプレイですからね。自由度は高いです。

それでは最後に上の動画を動かしているプログラム全体を載せておきます。

ソースコード

#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <pthread.h> // マルチスレッド化

#include <string.h>
#include <stdlib.h>

#define FONT_FILEA "./PAW16A.FNT"   // 全角文字用フォントを指定
#define FONT_FILEJ "./PAW16K.FNT"   // 全角文字用フォントを指定


#include "FONTX_PI/fontxpi.h"

#define ARRAY_SIZE(X) (sizeof(X)/(sizeof(X[0])))
// 周波数用
#define kHz  * 1000
#define MHz  * 1000000

#define SclollDelay 30 //スクロールの待ち時間。(ms)
#define ScrollPixel 1 //1回に何pxスクロールするか。1-8px
//            Char型は1バイトなのでその範囲内で。

#define MaxChar 127 //最大文字数
#define  FontSizeH 16 // フォントサイズ 縦
#define  FontSizeW 16 // フォントサイズ 横
#define  FontSizeB 32 // フォントサイズ バイト数((横÷8bit) ×縦)端数繰り上げ)
#define  LineSizeP 64 // 1行のサイズ
#define  LineSizeN 16 // 行数

// 確保する正味バッファサイズ
// 実際はスムーズなスクロールのために前後2行分ずつ余白を取る
#define  BUF_X ((FontSizeW + 7) >>3 )*MaxChar 
// 今回の規定値ならば
// ((横フォントサイズ +7)÷8)×最大文字数
// =((16+7)÷8)×127
// =(23÷8)×127
// =2.875×127
// =254 となる。(int型なので0.875は切り捨てられる)

// 読み込むテキストファイルのパス。
// フルパスで書いておくと安心
#define TXT_File_Path "./MESG.TXT"


static uint8_t SPIMode = 0, SPIbit = 8; //SPIモード0 bit数8
static uint32_t SPISpeed = 10 MHz;    // SPI転送速度設定

// SPIデバイス定義
// # ls -l /dev/spidev* で確認できる。
// Pi2の場合CE1に接続した場合は0.0
//          CE2に接続した場合は0.1 だった。
static char* SPIDevice = "/dev/spidev0.0";

// LEDバッファ用のグローバル変数
// 今回のLEDマトリックスサイズは、横1行を想定しているので
// バッファサイズ+(LED1ラインサイズ×2) が横サイズ
// 横×1行のサイズ
// =(254+(64÷4))×16
// =(254+16)×16
// =270×16=4320バイト 確保されます。
unsigned char LED_BUF[FontSizeH][BUF_X + (LineSizeP >> 2)] = {};

// プロトタイプ宣言
char ARShift(unsigned char* dat, unsigned char p, unsigned char sbit);
int DataSend(int fd, unsigned char* TrData);
int FontRead(unsigned char* s, unsigned char sp);
unsigned char TxtRead(unsigned char* rtxt);
void* LED_SND(void* args);

// エラー処理
void ErrBreak(char* msg) {
	puts("******** FATAL ERROR ********");
	puts(msg); puts("*****************************"); exit(-1);
}

int main(void) {
	unsigned char st[1024] = {}, i = 0;
	unsigned int x = 1, z = ARRAY_SIZE(st), c, d;;

	// 初期化中はメッセージ表示(ほとんど一瞬だが一応。)
	FontRead("初期化中", 0);

	// LED制御変数をマルチスレッドとして初期化
	pthread_t ledt;
	pthread_create(&ledt, NULL, LED_SND, (void*)NULL);

	// 初回の読み込み
	TxtRead(st); z = FontRead(st, LineSizeP);
	FontRead("    ", 0);


	// 表示用ループ
	while (1) {

		// スクロール
		c = FontSizeW;
		while (c--)ARShift(LED_BUF[c], BUF_X, ScrollPixel);


		//スクロールディレイ
		usleep(SclollDelay * 1000);

		//スクロール回数カウント
		d++;

		// 文字が流れきったらバッファの初期化
		if (d >= (8 * z) + (LineSizeP << 1)) { d = 0; usleep(1000); TxtRead(st); z = FontRead(st, LineSizeP); }

	}
}



// SPIデータ転送関数。
int DataSend(int fd, unsigned char* TrData) {
	// 転送するデータの準備
	struct spi_ioc_transfer tr = {
		.tx_buf = (unsigned long)TrData,
		.len = 10,
		.delay_usecs = 1,
		.speed_hz = SPISpeed,
		.bits_per_word = SPIbit,
	};


	// 転送
	if (!(ioctl(fd, SPI_IOC_MESSAGE(1), &tr)))ErrBreak("SPI TrError(Data)");
	return 0; // 何もなければ0を返す
}

// LED制御用関数
void* LED_SND(void* args) {

	// SPIデバイスの初期化
	// 実はここでしかSPIを使わないのでここで初期化する。
	unsigned char LED_Line, i, t[(16 + LineSizeP) >> 3];
	int x, fd;

	if ((fd = open(SPIDevice, O_RDWR)) < 0) ErrBreak("SPI Device Init error");
	if ((ioctl(fd, SPI_IOC_WR_MODE, &SPIMode)) < 0) ErrBreak("SPI Mode Setting Error");
	if ((ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &SPIbit)) < 0) ErrBreak("SPI bit/Word Setting Error");
	if ((ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &SPISpeed)) < 0) ErrBreak("SPI Speed Setting Error\n");

	// LED制御ループ
	while (1) {
		LED_Line = 10; x = 1 << i;
		t[0] = (unsigned char)(x >> 8); t[1] = (unsigned char)(x & 0xff);
		while (LED_Line--) t[2 + LED_Line] = LED_BUF[i][LED_Line + 2];
		DataSend(fd, t); i++; if (i == 16)i = 0;
	}
}

// 配列間ビットシフト関数
// ARShift(目的の配列変数,変数の配列数,シフトするビット数);
// 例:ARShift(Array,ARRAY_SIZE(Array),1);
// [       0][       1]
//  00000001  10000001
//          ↓        
//  00000011  00000010
// ↑     ↑1番目の変数の最上位ビットは0番目の変数の最下位ビットに代入
// └0番目の変数の最上位ビットは捨てられる
// ただし基本的には1つ隣までしか面倒を見ないので
// 8bit以上シフトすると内容は保持されない。

char ARShift(unsigned char* dat, unsigned char p, unsigned char sbit) {
	char i = 0; for (i = 0; i < p; i++) dat[i] = (dat[i] << sbit) | (dat[i + (i < p)] >> ((sizeof(dat[0]) << 3) - sbit));
}

// フォント読み込み関数。
// 与えられた文字列変数を読み込んで適当なフォントパターンを
// バッファに送る。
// FontRead(文字列,オフセット);
// オフセットはビット単位 8ビット未満切り捨て
int FontRead(unsigned char* s, unsigned char sp) {
	unsigned int y = 0, z = 0, cd = 0; // ちゃんと0で初期化しないと動作不良
	unsigned char str[FontSizeB] = {};
	unsigned long p;

	while (*s) {
		// 文字コード判定
		// 1バイト目が0x80以上なら2バイト文字として認識。
		// 半角カナは使えない仕様
		if (*s >= 0x80) {
			y += 2; cd = ((*s++)<< 8) | (*s++);
		}
		else { y++; cd = (*s++); }

		// 実際の読み込み
		p = KanjiReadX(cd, str) & 0xffff; z = p;

		// デバッグ用
		printf("[Code] %04X\n",cd);
		printf("Read %d\n",p);

		// 表示用バッファに転送
		if (p == FontSizeB)while (--z)LED_BUF[(z >> 1)][(z & 1) + y + (sp >> 3)] = str[z];
		if (p == FontSizeB >> 1)while (--z)LED_BUF[(z)][1 + y + (sp >> 3)] = str[z];
	}

	return y; //処理した文字数(バイト)を返す
}

// テキストファイル読み込み関数
// テキストファイルの最初の一行を文字数制限まで読み込み
// 戻り値は文字数じゃなくてバイト単位なので注意
// 「あいう」は6 「ABC」は3 「あBC」は4がそれぞれ返ってくる。
unsigned char TxtRead(unsigned char* rtxt) {
	unsigned int x = 0, n = 0;
	FILE* txt;
	if ((txt = fopen(TXT_File_Path, "r")) == NULL)ErrBreak("File Open Error");
	fgets(rtxt, MaxChar << 1, txt);
	fclose(txt);
	return strlen(rtxt);

}

 

総閲覧数:520 PV

関連記事

“Raspberry Pi 2/4で実用的な電光掲示板を作る” への1件のコメント

Raspberry Pi 2/4で実用的な電光掲示板を作る - NGO 気候危機アクション藤沢 へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください