ディスプレイ
この節ではI/Oボードに搭載されている8x8ドットマトリックスLEDをディスプレイとして使って, 文字や画像を表示する. ドットマトリックスLEDは64個のLEDで構成されているが制御用のGPIOポートは16個しか使っておらず, 前節で使った単体のLEDよりも高度な制御が必要になる.
LED
まず,第3部1章で扱った緑色のLEDがGPIOのポート10の操作で制御できる理由を説明しよう. LEDには2本の端子があり,それぞれアノード(陽極),カソード(陰極)と言う. アノードの電位がカソードの電位よりも十分高いときにLEDは発光する. I/Oボードでは,LEDのアノードはGPIOのポート10に,カソードはGND(常に低電位)に接続されている.そして,Raspberry PiではGPIOに1を出力するとそのピンの出力が高電位になり, 0を出力すると低電位になる(1 = 高電位,0 = 低電位という対応づけを正論理という). したがって,ポート10の出力を1にするとLEDが点灯し,0にすると消灯する.
8x8ドットマトリックスLED
8x8ドットマトリックスLEDは,LEDが8行8列に並んだ電子部品である(下図). I/Oボード上では,緑色の単体LEDの近くがマトリックスLEDの第1行第1列に当たり,上から順に第1〜8行 (ROW),左から順に第1〜8列 (COL) と名付けられている.各行を制御する信号線と各列を制御する信号線の合計16本の信号線を使って,マトリックス上の各LEDを点灯させるか消灯させるか制御する.
8x8ドットマトリックスLEDの回路を下図に示す.網目状になった信号線の交点にLEDが配置されている.各LEDは列の信号線にアノードが,行の信号線にカソードが接続されている.(図中の“PIN NO.”はこのマトリックスLED装置の端子番号.自分で配線する場合には必要な情報だが本科目では無視してよい.)
したがって,あるLEDを点灯させるには,そのLEDの 列 が接続されたGPIOポートに 1 を, 行 が接続されたGPIOポートに 0 を出力する. それ以外の組み合わせでは点灯しない(下表).
列(COL) | 行(ROW) | 結果 |
---|---|---|
0 | 0 | 消灯 |
0 | 1 | 消灯 |
1 | 0 | 点灯 |
1 | 1 | 消灯 |
8x8ドットマトリックスLEDの各信号線は下表のGPIOポートに接続されている.
ピン名 | GPIO ポート番号 | ピン名 | GPIO ポート番号 | |
---|---|---|---|---|
COL1 | 27 | ROW1 | 14 | |
COL2 | 8 | ROW2 | 15 | |
COL3 | 25 | ROW3 | 21 | |
COL4 | 23 | ROW4 | 18 | |
COL5 | 24 | ROW5 | 12 | |
COL6 | 22 | ROW6 | 20 | |
COL7 | 17 | ROW7 | 7 | |
COL8 | 4 | ROW8 | 16 |
1点を点灯させるプログラム
第 r 行第 c 列を (r, c) と書くことにする. (2, 3)のLEDだけを点灯させるプログラムを作ろう.
準備
まずディスプレイを制御するIOポートの用途を「出力」に設定する. ついでに,単体LEDを制御する GPIO #10 も出力に設定しておく. ディスプレイを制御するIOポートのうち GPIO #4, #7, #8 は,GPFSEL0 番地でまとめて設定する.
- GPIO #4 はビット 12 (= 4 × 3) から 14 (= 4 × 3 + 2) に 001 を書くことで出力に設定する.
- GPIO #7 はビット 21 (= 7 × 3) から 23 (= 7 × 3 + 2) に 001 を書くことで出力に設定する.
- GPIO #8 はビット 24 (= 8 × 3) から 26 (= 8 × 3 + 2) に 001 を書くことで出力に設定する.
- それ以外のポートは入力に設定するために 000 を書き込む.
これらを同時に行うために, GPFSEL0 番地に 0x01201000 を書き込む(下図).
同様に,ディスプレイを制御する他のGPIOポートも出力に設定する.
- GPFSEL0 + 4 番地 (GPFSEL1) で GPIO #10, #12, #14, #15, #16, #17, #18 を出力に設定する.
- GPFSEL0 + 8 番地 (GPFSEL2) で GPIO #20, #21, #22, #23, #24, #25, #27 を出力に設定する.
点灯
(2,3)のLEDを点灯させるには,2行目を制御するGPIOポートに0を,3列目を制御するGPIOポートに1を出力すればよい. それ以外の列を制御するGPIOポートには1を,行を制御するGPIOポートには0を出力する(下図の右端と下端の 0, 1 を参照).
(補足: もし5行目にも0を出力すると (5,3) も点灯してしまうし,もし8列目にも1を出力すると (2,8) も点灯してしまう.(2,3) だけ点灯させるには,上記の組み合わせで出力する以外にない.)
演習3.2-1 chap2 ディレクトリを作って,その中に 23.s というファイルを作って次のプログラムを書き,実行して動作を調べなさい.ただし,GPFSEL2の設定値 (GPFSEL_VEC2) は隠してあるので,各自で計算して求めること.
.equ GPIO_BASE, 0x3f200000 @ GPIOベースアドレス
.equ GPFSEL0, 0x00 @ GPIOポートの機能を選択する番地のオフセット
.equ GPSET0, 0x1C @ GPIOポートの出力値を1にするための番地のオフセット
.equ GPCLR0, 0x28 @ GPIOボートの出力値を0にするための番地のオフセット
.equ GPFSEL_VEC0, 0x01201000 @ GPFSEL0 に設定する値 (GPIO #4, #7, #8 を出力用に設定)
.equ GPFSEL_VEC1, 0x01249041 @ GPFSEL1 に設定する値 (GPIO #10, #12, #14, #15, #16, #17, #18 を出力用に設定)
.equ GPFSEL_VEC2, 0x???????? @ GPFSEL2 に設定する値 (GPIO #20, #21, #22, #23, #24, #25, #27 を出力用に設定)
.equ COL1_PORT, 27
.equ COL2_PORT, 8
.equ COL3_PORT, 25
.equ COL4_PORT, 23
.equ COL5_PORT, 24
.equ COL6_PORT, 22
.equ COL7_PORT, 17
.equ COL8_PORT, 4
.equ ROW1_PORT, 14
.equ ROW2_PORT, 15
.equ ROW3_PORT, 21
.equ ROW4_PORT, 18
.equ ROW5_PORT, 12
.equ ROW6_PORT, 20
.equ ROW7_PORT, 7
.equ ROW8_PORT, 16
.section .init
.global _start
_start:
@ LEDとディスプレイ用のIOポートを出力に設定する
ldr r0, =GPIO_BASE
ldr r1, =GPFSEL_VEC0
str r1, [r0, #GPFSEL0 + 0]
ldr r1, =GPFSEL_VEC1
str r1, [r0, #GPFSEL0 + 4]
ldr r1, =GPFSEL_VEC2
str r1, [r0, #GPFSEL0 + 8]
@ (2,3) を点灯
@ 第3列だけ点灯
mov r1, #(1 << COL1_PORT)
str r1, [r0, #GPCLR0]
mov r1, #(1 << COL2_PORT)
str r1, [r0, #GPCLR0]
mov r1, #(1 << COL3_PORT)
str r1, [r0, #GPSET0] @ 点灯
mov r1, #(1 << COL4_PORT)
str r1, [r0, #GPCLR0]
mov r1, #(1 << COL5_PORT)
str r1, [r0, #GPCLR0]
mov r1, #(1 << COL6_PORT)
str r1, [r0, #GPCLR0]
mov r1, #(1 << COL7_PORT)
str r1, [r0, #GPCLR0]
mov r1, #(1 << COL8_PORT)
str r1, [r0, #GPCLR0]
@ 第2行だけ点灯
mov r1, #(1 << ROW1_PORT)
str r1, [r0, #GPSET0]
mov r1, #(1 << ROW2_PORT)
str r1, [r0, #GPCLR0] @ 点灯
mov r1, #(1 << ROW3_PORT)
str r1, [r0, #GPSET0]
mov r1, #(1 << ROW4_PORT)
str r1, [r0, #GPSET0]
mov r1, #(1 << ROW5_PORT)
str r1, [r0, #GPSET0]
mov r1, #(1 << ROW6_PORT)
str r1, [r0, #GPSET0]
mov r1, #(1 << ROW7_PORT)
str r1, [r0, #GPSET0]
mov r1, #(1 << ROW8_PORT)
str r1, [r0, #GPSET0]
loop:
b loop
2点の点灯
次に,(2,3) と (4,7) のちょうど2点だけを点灯させる方法を考える.
間違った方法
8x8ドットマトリックスLEDは,点灯させようとした行と列が交わる全てのLEDが点灯する. そのため,(2,3)と(4,7)のLEDを点灯させようと思って,2行と4行を制御するGPIOポートに0を, 3列と7列を制御するGPIOポートに1を出力すると,(2,7)と(4,3)のLEDも点灯してしまう.
演習3.2-2 演習3.2-1で作った23.sを23x47.sという名前にコピーしたうえで,23x47.sを編集して, 2行と4行のGPIOポートを0に,3列と7列のGPIOポートに1を出力するようにしなさい. このプログラムを実行して,4点が点灯することを確認しなさい.
正しい方法
(2,3)と(4,7)のLEDが 点灯しているように見せる ためには, (2,3)と(4,7)を交互に短時間ずつ繰り返し点灯させればよい.
←高速に切り替える→
つまり,
- (2,3)を点灯する.
- 少し待つ.
- (2,3)を消灯する.
- (4,7)を点灯する.
- 少し待つ.
- (4,7)を消灯する.
- 1.に戻る.
とする.
「少し待つ」部分は,演習3.1-3のblink.sと同じように,何もせずにループするプログラム片を使って実現すればよい. ただし,点灯する点の切り替えが人の目ではわからない程度に速い必要があるので,blink.sの待ち時間よりはかなり短くする必要がある. 1/500秒〜1/1000秒ごとに切り替えれば十分高速であろう(2点だけならそこまで高速にしなくてよいが,後の1画面分の描画をする演習問題では8つの行を順に切り替えることになるので,それを見込んだ値にしている.1行が1/480秒なら1画面8行の描画に1/60秒かかり,TV等の画面リフレッシュレートと同程度になる). blink.sのように空ループで時間つぶしをする場合,ループ回数と待ち時間がほぼ比例するので,必要な待ち時間に合わせてループ回数を調節すればよい.
演習3.2-3
(2,3)と(4,7)だけが点灯するプログラムを作りなさい.
この演習で作るプログラムのファイル名は23-47.sにしなさい.
23-47.img
を作るMakefileと,chap2ディレクトリ中の各ソースファイルの説明を書いたREADME.mdもpushすること.
図形の表示
任意の図形を表示するためには,点灯させるLEDを1個ずつ短時間ずつ点灯させる方法もあるが, 1行ずつまとめて表示する方がよい. 点灯させる行を1つに決めれば, その行にある8個のLEDは列を制御する8つのGPIOポートで個別に点灯と消灯を制御できる. しかも,LEDを1個ずつ点灯させた場合は1個のLEDを点灯させる時間が最小で1/64になってしまうのに対して, 1行ずつ表示すれば1個のLEDを点灯させる時間は1/8になるので明るくなる. なお,1列ずつでも良さそうに思うかもしれないが, 実験用I/Oボードの回路の特性で,1列ずつ表示させたときは同じ列で点灯させるLEDの数によって明るさが変わってしまう.
例えば,下図のような渦巻き図形を表示するためには,
- 1行目を表示する.
- 少し待つ.
- 2行目を表示する.
- 少し待つ.
- ...
とする.
なお,同じ行にあるLEDは,明るさにムラができないように,8点が同時に点灯・消灯するようプログラムすべきである. そのためには以下のようにするとよい.
- 該当する行に1を出力してどの点も点灯しないようにする.
- 各列の出力を更新する(行が1なのでまだどれも点灯しない).
- 該当する行に0を出力する(列が1である点が同時に点灯する).
- 少し待ち,該当する行に1を出力する(すべての点が同時に消灯する).
上の2.と3.の順序が逆だと,列に1を出力した時点でそのLEDが点灯してしまう.すると,先に点灯したLEDの方が長く点灯するので,明るく点灯しているように見えてしまう.
演習3.2-4
ディスプレイに,上に示した渦巻きを表示するプログラム uzu.s を作り, chap2 ディレクトリの中に置きなさい.uzu.img
を作るMakefileと,chap2ディレクトリ中の各ソースファイルの説明を書いたREADME.mdもpushすること.
なお,各LEDが同じ明るさで点灯するようにすること.
動画とフレームバッファ
ディスプレイに表示する画像(静止画)を定期的に切り替えることで,動画を表現することができる. しかし,各静止画を表示するプログラム片を静止画の枚数だけ作っていたら,プログラムが膨大になってしまう.さらに,ゲーム中の画面のように,表示する画像を予め決められない(プレイヤーがどう操作するか等によって画面が自由に変わる)こともある.
そこで,表示したい画像を格納しておく主記憶領域(バッファ領域)を用意し,そこから画像データを読み出して表示するテクニックが一般によく用いられる.表示内容を変えるときはバッファ領域を書き換えるだけでよい.このような用途の主記憶領域をフレームバッファと言う.動画を表示したいときは,
- フレームバッファから画像データを読み出して表示するプログラム
- フレームバッファを書き換えるプログラム
の2つを同時に実行する. ゲーム等の場合も同様に,画面に変化があるたびにフレームバッファを書き換えればよい.
(補足: この章では,上記の2タスク「フレームバッファ中の画像の表示」「フレームバッファの書き換え」の「同時実行」を,「前者を一定回数実行するたびに後者を1回行う」という方法で実現する. 第III部4章「マルチタスク」では,各タスクの実行間隔を個別に設定できるような,本来の姿に近い並行処理方法を説明する.)
フレームバッファを用いた静止画の表示
動画やゲームを作る準備として,まず,静止画をフレームバッファを使って表示するプログラムを作る. 静止画なので「フレームバッファを書き換える処理」はなく,「フレームバッファから画像を読み出して表示する処理」のみ必要である.
ディスプレイの1行は8つのドットで構成されているので,1行分のデータは1バイトで表現できる. それが8行あるので,フレームバッファは8バイトあればよい.
フレームバッファの1バイト目が1行目,2バイト目が2行目のドットパタンを格納するものとする.各バイトは,最上位ビット (MSB) がその行の1列目,最下位ビット (LSB) が8列目に対応するとし,そのビットが1であれば点灯を,0であれば消灯を意味するものとする. このように決めると,渦の画像の1行目は2進数で00011110と表現できる.これは16進数では0x1eになる.
フレームバッファ中の画像を表示するには,次の動作を繰り返す.
- フレームバッファの1バイト目を読み出して,対応するドットパタンを1行目に表示する.
- 少し待つ
- 2バイト目を読み出して,対応するドットパタンを2行目に表示する.
- 少し待つ
- ...
- 8バイト目を読み出して,対応するドットパタンを8行目に表示する.
- 少し待つ.
- 1に戻る.
もちろん,以下の疑似コードのように,8行分の処理をループにした方が洗練されている(オプション項目)
(frame_buffer[row]
は「frame_buffer
の先頭からrowバイト目」の意味).
「frame_buffer[row]が表す図形をrow行目に表示」も,列1から列8までの処理をループ化すると,プログラムが簡潔になる.
while (true) { // 無限ループ
for (row = 0; row < 8; row++) {
frame_buffer[row]が表すドットパタンをrow行目に表示;
少し待つ;
row行目を消灯;
}
}
演習3.2-5
あらかじめ渦の画像のデータを書き込んだフレームバッファ(次のプログラム片の??を正しいデータに置き換えなさい)を用意し,それをディスプレイに表示するプログラム frame.s を作りなさい.frame.img
を作るMakefileと,chap2中の各ソースファイルの説明を書いたREADME.mdもpushすること.
.global frame_buffer
frame_buffer:
.byte 0x1e, 0x??, 0x??, 0x??, 0x??, 0x??, 0x??, 0x??
補足1: この演習問題では簡単のため,最初からフレームバッファ中に画像データが書き込まれた状態でプログラムを開始する.以降の演習問題では,さらに,適切なタイミングでフレームバッファを書き換える処理が加わる.が,フレームバッファの内容を表示する部分はほとんど変更が要らないはず(変更しなくて良いように作るべき)である.
補足2:
frame_buffer
を.global
宣言しているのは,「フレームバッファの内容を表示する処理」と「フレームバッファを書き換える処理」を別のソースファイルに記述できるようにするためである.
つまり,データ領域 frame_buffer
及び「フレームバッファの内容を表示する処理」を行うサブルーチンを独立したソースファイルに記述して,複数の課題でそのソースファイルを共有することを想定している.
補足1で述べたように「フレームバッファの内容を表示する処理」はアプリケーションに依らない(ディスプレイを使うどの課題でも同じである)ので,独立したソースファイルに記述して共有するのは良いやり方である.
frame_buffer
が.global
宣言されていれば,別のソースファイルからも frame_buffer
にアクセスでき,「フレームバッファを書き換える処理」を行える.
(参考: 2-a ライブラリのすすめ)
演習3.2-6 この演習問題では,以下の実演動画に示すような, スイッチ1 (SW1) を押している間だけディスプレイ上の点が左上から順に点灯していくプログラム swframe.s を作る.
(スイッチを押していない間は表示内容が変化しない. 全点が点灯したら今度は左上から順に消灯していく. 全点が消灯したら再び左上から順に点灯していく.)
スイッチを押していない間も,「1行目を表示して少し待つ → 2行目を表示して少し待つ → …」という処理はずっと動き続けないといけないことに注意.
フレームバッファを使えば,このようなプログラムも見通しよく実現できる. 一定時間間隔で「スイッチ1が押されていたらフレームバッファ中の『次のビット』を反転する」処理を実行すればよいだけである. 「一定時間間隔」の実現は,例えば「8行分の表示を10回繰り返すたび(または1行の表示を80回繰り返すたび)」などとすればよい(10回でなくてもよい.点灯する点が増える速さに影響する).
以下の疑似コードに示すようなアルゴリズムにすればよいだろう.「frame_buffer
に従って1〜8行目を順に表示」する処理は,前問 演習3.2-5のプログラムと同じでよいはず.「frame_buffer
に従って1〜8行目を順に表示」する処理を10回程度実行するたびに「SW1が押されていたらframe_buffer
中のビットを反転」する処理が実行されるようにすればよい(実行回数の比が10対1程度になるなら下記の疑似コードと異なるアルゴリズムでもよい).
x = 0;
while (true) { // 無限ループ
if (SW1が押されている) {
左上から x 番目の点(0 ≦ x ≦ 63)に対応するframe_buffer中のビットを反転;
x++;
if (x >= 64) { x = 0; }
}
for (i = 0; i < 10; i++) {
frame_bufferに従って1〜8行目を順に表示
}
}
上記の説明に従って,swframe.s を作成しなさい. 上記のように,必ず「フレームバッファの内容を表示」する処理と「SW1が押されていたらフレームバッファを書き換える」処理とで実現すること.
swframe.img
を作るMakefileと,chap2中の各ソースファイルの説明を書いたREADME.mdもpushすること.
演習3.2-7
フレームバッファ内の書き換えを一定周期ごとに行うことで, 00 → 01 → ... → 09 → 00 → 01 → ... と, 繰り返し00から09までを順にディスプレイに表示するプログラム count.s を作りなさい. 表示の切り替えが速すぎると数字が見えないので,適切な時間同じ数字を表示し続けるようにすること.(演習3.2-6と同様に,1〜8行目を順に表示することを一定回数繰り返したら,「次の数字の画像をフレームバッファに書き込む」処理を行えばよい).
x = 0;
while (true) { // 無限ループ
数字 x の画像をframe_bufferに書き込む;
x++;
if (x > 9) { x = 0; }
for (i = 0; i < 適当な繰り返し回数; i++) {
frame_bufferに従って1〜8行目を順に表示
}
}
1桁の数字は4x8ドットで次のように表示することにし,それを並べて2桁の数にしなさい.
表示 | 表示 | 表示 | 表示 | ||||
---|---|---|---|---|---|---|---|
0 | ![]() |
1 | ![]() |
2 | ![]() |
3 | ![]() |
4 | ![]() |
5 | ![]() |
6 | ![]() |
7 | ![]() |
8 | ![]() |
9 | ![]() |
count.img
を作るMakefileと,chap2中の各ソースファイルの説明を書いたREADME.mdもpushすること.
補足:
数字画像のビットパタンをデータとして定義するのは構わない(むしろ良い)が,それはフォントデータであって,フレームバッファではない.
この演習で想定するプログラムは,(1)「フレームバッファの内容をディスプレイに表示する処理」は常に配列 frame_buffer
からデータを読み出してLEDマトリックスを制御する,(2)「フレームバッファの内容を書き換える処理」は一定周期ごとにフレームバッファ内を書き換える,というものである.
フォントデータを使う場合は,(2)においてフォントデータをフレームバッファにコピーすればよい.
上記の(1), (2)のように処理を分けることで,(1)の部分は,演習3.2-5以降すべて同じになる.
(第III部4章「マルチタスク」以降は,「(1)を一定回数行うごとに(2)を行う」といった記述も不要になり,(1)と(2)を全く独立した処理として記述可能になる.)
演習3.2-8
前問と同様に,フレームバッファ内の書き換えを一定周期ごとに行うことで, 00 → 01 → ... → 99 → 00 → 01 → ... と, 繰り返し00から99までを順にディスプレイに表示するプログラム count99.s を作りなさい.
count99.img
を作るMakefileと,chap2中の各ソースファイルの説明を書いたREADME.mdもpushすること.
補足1:
100通りの図形それぞれに対するコードを記述したり,100通りのフォントデータを作ったりするのは避けて,なるべく少ないコード量でできないか工夫すべきである.
プログラムが長すぎると ldr =
命令がエラーで使えなくなることにも注意.
補足2: フレームバッファの内容をディスプレイに表示する処理や,1行表示したあと少し待つ処理などは,独立した関数として本体プログラムから切り離しておくと,見通しの良いプログラムになるだろう. フレームバッファに数字画像を書き込む処理も,表示したい数を引数とする関数にするなど工夫すると,さらに見通しの良いプログラムになる.
参考: 2-a ライブラリのすすめ
(なお,サブルーチンを作るためにスタックポインタやリンクレジスタを使うと,それら(r13
, r14
)は当然ほかの用途に使えなくなる.うっかり他の用途に使わないように注意(わりとよくある誤り).)