汎用I/Oポート
OSを使わないRaspberry Piプログラミング
第III部では,Raspberry Piとそれに接続された装置を制御するプログラムを製作する. 周辺装置の制御自体はOSを経由して行うことも可能だが, 本科目の目的はCPUを直接操作したり観察したりしてCPUの動作を体感することであるので, 第III部ではOSの力を借りずにCPUから周辺装置の制御回路に直接指令を出すようなプログラムを製作する. それにより,コンピュータシステムの仕組みとその中でのCPUの動作をより深く理解することを目指す. 第III部の最終目標は,OS無しで動作する,本科目用I/Oボード上の機器を使ったアプリケーションを製作することである.
第III部のためのGitリポジトリ
第III部で使うリポジトリをcloneする.第III部では二つのリポジトリを使う.
- 毎回の課題を提出するための2〜3人のチームのリポジトリ
- 最終課題を提出するための,4〜5人のグループのリポジトリ
まず,しばらくは1.のリポジトリを使うので,そのリポジトリをcloneしよう.
注意 第I部や第II部のリポジトリの中にはcloneしないこと.
$ cd ~/git -- ローカルリポジトリ置き場に移動
$ ls -F -- 場所が間違っていないか確認
q1-i386-team135/ q2-raspbian-team135/
$ git clone https://github.com/kut-info-pl2/q3-rasppielement-team135.git -- team135 は例
-- 略 --
$ ls -F
q1-i386-team135/ q2-raspbian-team135/ q3-rasppielement-team135/
$ cd q3-rasppielement-team135 -- 作業コピーに移動する
詳細はグループ用GitHubリポジトリの作成の手順2を参照.
最初のプログラム
最初に,I/Oボード上の緑色の発光ダイオード (LED) を光らせるプログラムを作ろう.
演習3.1-1 chap1 ディレクトリを作り,その中に light.s というファイルを作って次のプログラムを書きなさい.
.section .init
.global _start
_start:
ldr r0, =0x3f200000 @ GPIO 制御用の番地
@ GPIO #10 を出力用に,GPIO #11〜#19を入力用に設定
mov r1, #(1 << 0)
str r1, [r0, #0x04]
@ GPIO #10 に 1 を出力
mov r1, #(1 << 10)
str r1, [r0, #0x1C]
loop: b loop
ビルド
このプログラムの説明は後回しにして,まずはビルドして実行してみよう. 第II部ではRaspbian(Rapberry Pi用Linux)の上で動くプログラムを作ってきたが,第III部ではRaspbianを使わずに実行する. そのため,ビルドや実行の方法が異なる.
ビルドは次のコマンドで行う.
$ arm-none-eabi-as light.s -o light.o
$ arm-none-eabi-ld -m armelf light.o -o light.elf
$ arm-none-eabi-objcopy light.elf -O binary light.img
これによってlight.imgというファイルが作られる.リンカ arm-none-eabi-ld のコマンドライン引数が変わっていることに注意しよう.
arm-none-eabi-objcopyはファイルの形式を変換するためのコマンドである.
-O binary
(ハイフン 大文字オー バイナリ)を指定することで,ファイルの内容をそのまま主記憶にコピーすれば実行できる形式(メモリイメージ)に変換される.
実行
Raspberry Pi 3は,電源が入ると,SDカードに書かれたkernel7.img
を読み出して実行する.
普通はOS本体を kernel7.img
としてSDカードに置き,電源を入れるとOSが起動するようにしておくが,本科目では自分の作ったプログラムを kernel7.img
としてSDカードに置くことで,そのプログラムをRaspberry Piに実行させる.
詳細はkernel7.imgの正体を参照.
SDカードへの書き込みはSDカードリーダを使って行う.手順は以下の通り.
SDカードへの書き込みと実行
- Raspberry Piの電源ケーブルを抜いて作業する.
- Raspberry PiからSDカードを抜く.
- SDカードリーダにSDカードを挿し,カードリーダをPCに接続する.
2〜3秒待つとカードリーダのランプが点滅し始める(いつまでもランプが点灯しないときは,一度PCからカードリーダを外して再度接続する). - SDカードが
/media/ユーザ名/boot
にマウントされる.マウントされていることは,ls /media/ユーザ名/boot
などのコマンドで確認できる.(もしWindowsにSDカードが接続されてしまった場合は,Windows側で接続を解除した上で,一度カードリーダを抜いて挿しなおす.) /media/ユーザ名/boot
の中にlight.imgをkernel7.imgという名前で書き込む(すでにあるkernel7.imgを上書きする).$ cp light.img /media/ユーザ名/boot/kernel7.img
- SDカードの接続を解除するために,ファイルマネージャ上でデバイス
boot
の「取り出し」ボタンを押す. - SDカードをRaspberry Piに挿す.
- Raspberry Piに電源ケーブルを接続する.
電源ケーブルを接続すると,1〜2秒後にプログラムが読み出されて実行される. light.sはLEDを点灯させるプログラムだったので,LEDが点灯するはずである.
停止させるには電源ケーブルを抜く.
最初のプログラムの説明
light.sのプログラムを詳しく見てみよう.
.section .init
.global _start
_start:
ldr r0, =0x3f200000 @ GPIO 制御用の番地
@ GPIO #10 を出力用に,GPIO #11〜#19を入力用に設定
mov r1, #0x00000001
str r1, [r0, #0x04]
@ GPIO #10 に 1 を出力
mov r1, #(1 << 10)
str r1, [r0, #0x1C]
loop: b loop
.initセクション
上記のプログラムの1行目は .section .init
となっている.
これは「以降のプログラムコードを .init セクションという領域に置く」という指示だ.
第I部・第II部では .init セクションではなく .text セクションに置くように記述していた.
.init と .text はどちらも機械語命令用の領域で,.init は「.textのコードを実行する前に実行する初期設定用のコード」を置くためのものである.
以降,実験指導書のコード例は「実行開始ルーチン(_start
から始まるルーチン)は .init セクションに記述する」という規則で記述するが,実際は,.init でも .text でもどちらでもよく,「実行開始点がimgファイルの先頭になる」ようにさえすればよい.詳しくは「2-a ライブラリのすすめ」の「実行開始位置」を参照.
なお,第I部・第II部ではラベル_start
がプログラム実行開始点として扱われたが,第III部の開発手順ではそのようには扱われない._start
とは無関係に,「2-a ライブラリのすすめ」の「実行開始位置」で述べている規則で実行開始位置が決まる.
ただし,_start
を記述していないと,途中の light.elf を生成するステップで警告が出る(その後のimgファイル生成時に_start
の情報は捨てられるのだが,arm-none-eabi-ld はそのことを知らないので,_start
がないと文句を言う).ということで,_start
も書いておいた方が無難である.
GPIO
このプログラムは,Raspberry Pi上の汎用I/O (GPIO; General Purpose Input/Output) という装置を操作している. 汎用I/Oは,他の機器と接続するための最も単純なインタフェースである. Raspberry Pi本体上に汎用I/Oのための接続端子があって,プログラムによる操作で自由なタイミングで端子への出力を0(低電位)にしたり1(高電位)にしたりできる.また,接続機器から出力された電位の高低を1または0の2値で読み取ることもできる.
Raspberry Piは多数のGPIO端子(ポート)を持っており,それぞれ番号で識別される. I/Oボード上のLEDは,GPIO #10(10番のGPIOポート)を経由して接続されており,GPIO #10から1が出力されると点灯,0が出力されると消灯するようになっている.
メモリマップI/O
GPIOに限らず,CPUから制御される周辺装置(ペリフェラルとも言う)は,CPUからの制御コマンドを受け取るためのレジスタを内部に持っている.これらのレジスタに値を書き込むことで,ペリフェラルの動作を制御する.また,レジスタの値を読み取ることで,ペリフェラルから値を受け取ることもできる.(もちろん,CPU内部のr0, r1といったレジスタではない.各ペリフェラルが独自に内部に持つレジスタである.)
ARMでは,ペリフェラル制御用のレジスタは特定のメモリ番地にマップ(配置)される.つまり,そのメモリ番地に動作コマンドを書き込めば,ペリフェラル制御用レジスタに書き込まれ,ペリフェラルを操作できる.このような仕組みをメモリマップI/O (memory-mapped I/O) という.
GPIOの操作に使うのは,0x3f200000番地以降のいくつかの番地である.
0x3f200000番地をGPIOベースアドレスと呼ぶことにしよう.
まず準備として,r0
レジスタにGPIOベースアドレスをロードしておく.
以降,この番地からの相対番地で各GPIO制御用レジスタにアクセスする.
ldr r0, =0x3f200000 @ GPIOベースアドレス
GPIOの制御(初期設定)
GPIOは,汎用という名前の通り特定の目的は定められておらず,様々な用途に使うことができる. 例えば,同じ端子ピンを,入力にも出力にも使うことができる(ただし,同じピンを同時に入力用と出力用の両方に使うことはできない). 通常は,システム起動時に,各ピンを入力に使うか出力に使うか,プログラムから設定する.入力用と出力用以外にも,CPUの代わりに専用の制御回路(PWMやUSBコントローラ)に制御を任せるように設定することもできる.この機能は次章以降で使う.
上述のLEDを点灯させるプログラムでは,GPIO #10にLEDを点灯させるための電位を出力したいので,プログラムの開始時にGPIO #10の用途を「出力用」に設定する. 入出力の設定には,(GPIOベースアドレス) + 0 から始まる数ワード中の,GPIOポート番号に対応した3ビットを使う. どの番地のどのビットを使うかは,ポート番号から次のように決まる.
- GPIO #0から#9 → (GPIOベースアドレス) + 0 から始まるワード(4バイト)
- 下位ビットから3ビットずつ,GPIO #0,#1,...と対応している.
- GPIO #10から#19 → 次のワード.同様に下位ビットから3ビットずつ対応.
- GPIO #20から#29 → その次のワード
- ...
ここでは,GPIO #10が対象なので,(GPIOベースアドス) + 4 から始まるワードのビット0〜2(最下位の3ビット)を使って設定する. 出力用に設定するには,2進法で001を書き込めばよい.
mov r1, #0x00000001 @ GPIO #10を出力に設定
str r1, [r0, #0x04]
1行目でGPIO制御用レジスタに書き込む値を準備し,2行目でその値を書き込んでいる.
ただし,ペリフェラルのレジスタには1ワードまとめて書き込まれるので,同じワードを共有するポートは同時に設定されてしまうことに注意.例えば,ここではポート10を出力用に設定しているが,同時にポート11からポート19は(対応する3ビットが000なので)「入力用」に設定される.使わないポートは入力用にしておけば壊れる心配がないので,上記のプログラムは問題ない.
しかし,例えばGPIO #10を出力用に,GPIO #13を入力用に設定しようとして次のようなプログラムを書いてしまうと,正しく動作しない.
mov r1, #0x00000001 @ GPIO #10を出力に設定
str r1, [r0, #0x04]
mov r1, #0x00000000 @ GPIO #13を入力に設定
str r1, [r0, #0x04]
これは,GPIO #13を入力用に設定すると同時にGPIO #10も入力用に再設定してしまっているからである.
GPIOの制御(データ出力)
次に,GPIO #10に1を出力してLEDを点灯させる. GPIOに1を出力するには,(GPIOベースアドレス) + 0x1C から始まる数ワードの,ポートに対応するビットを1にすればよい.こちらは,1ワードに32ポートずつ割り当てられている.
@ GPIO #10 の出力を 1 にする.
mov r1, #(1 << 10)
str r1, [r0, #0x1C]
なお,同じビットに0を書き込んでも出力は0にならない.0は「現状維持」を表す. この機能によって,他のポートの出力を変化させずに目的のポートの出力だけ1にできる.
出力を0にするには,次のように,(GPIOベースアドレス) + 0x28 から始まる数ワード中の対応するビットを1にする.
@ GPIO #10 の出力を 0 にする.
mov r1, #(1 << 10)
str r1, [r0, #0x28]
[重要] プログラムの終端
最後に暴走を防ぐために無限ループする.
loop: b loop
これがなければ,CPUは諸君が書いたプログラムの後ろにあるバイト列を命令だと思って実行して暴走してしまう. 最悪の場合は壊れることもある.適切に無限ループを作るなどして,CPUが自分のプログラムの外を実行しないように(fall-offしないように)すること.
(第I部・第II部のようにOSの保護下でプログラムを実行する場合は,未定義領域にfall-offしても,OSがプログラムを強制終了するので安全だった.第III部では,そのような保護機構がないので,fall-offするとそのまま暴走状態になる.)
定数
light.s には沢山の定数があり,上記のような説明なしでプログラムだけ見ても理解が難しい.つまり,後で修正や再利用をしようとしたときに困ることが予想される. 次のように定数に名前を付けておいたり,計算結果の定数ではなく計算式を残しておけば,後で読み返したときに理解しやすい.
.equ GPIO_BASE, 0x3f200000 @ GPIOベースアドレス
.equ GPFSEL0, 0x00 @ GPIO #0〜9の機能を選択する番地のオフセット
.equ GPFSEL1, 0x04 @ GPIO #10〜19の機能を選択する番地のオフセット
.equ GPFSEL2, 0x08 @ GPIO #20〜29の機能を選択する番地のオフセット
.equ GPSET0, 0x1C @ GPIOポートの出力値を1にするための番地のオフセット
.equ GPCLR0, 0x28 @ GPIOボートの出力値を0にするための番地のオフセット
.equ GPFSEL_VEC1, 0x00000001 @ ポート10 → 出力,それ以外 → 入力
.equ LED_PORT, 10 @ LEDが接続されたGPIOのポート番号
.section .init
.global _start
_start:
ldr r0, =GPIO_BASE
@ GPIO #10 を出力用に,GPIO #11〜#19を入力用に設定
ldr r1, =GPFSEL_VEC1
str r1, [r0, #GPFSEL1]
@ GPIO #10 に 1 を出力
mov r1, #(1 << LED_PORT)
str r1, [r0, #GPSET0]
loop:
b loop
演習3.1-2
light.s を上記のように定数に名前を付けるように書き変え,リポジトリにpushしなさい.
make light.img
で実行ファイルが作られるようなMakefileも作りなさい.このプログラムはだれが作ってもよい.
データシートの読み方
Raspberry Piに搭載されているSoCのデータシートはBCM2835 ARM Peripherals からダウンロードできる.データシートに現れるregisterは,前述したようにペリフェラルの制御レジスタのことであって,r0やr1といったCPUのレジスタではないことに注意.
GPIOについては,データシートの6章(89ページ)から記載がある. 6.1節には,制御に用いるメモリ番地とその機能を説明した表が掲載されている. ただし,この表はGPIOベースアドレスを0x7e200000として書かれているので,読み替える必要がある.
6.1節の表を見ると,(ベースアドレス) + 0 である0x7e200000番地にはGPFSEL0,"GPIO Function Select 0" という記載がある.これがGPIOのポートの用途を選択するレジスタであり,GPFSEL0という名前が付いていることを表している.(なお,0x7e200000番地の記載が2つあるのは誤植.) GPFSEL0の詳細は91〜92ページに記載がある.特に91ページから92ページにまたがる表を見ると, 1つのGPIOポートに3ビットが割り当てられ,000が入力 (input),001が出力 (output) であり, それ以外に6つの選択肢があることが分かる.6つの選択肢の内容はGPIOポートによって異なり, 102ページにまとめた表がある.
GPFSEL0〜GPFSEL5以外に,この実験ではGPSET0〜1,GPCLR0〜1,GPLEV0〜1を使う.
[重要] OSのサポートがない世界のプログラミング
OSによるサポートがない世界のプログラミングは,OSのサポートがある世界のプログラミングよりも気を配らないといけない点がある.本科目の範囲では,特に次のことに気を付けなければならない. これらを考慮しないプログラムは第III部では動作しない(CPUが停止する,または暴走する)ので注意!
補足:GNUアセンブラのローカルラベル
下記の演習3.1-3では,LEDを点滅させるプログラムを作る. このとき,点灯と消灯の切り替えが速すぎると点滅しているように見えない(点灯し続けているように見える)ので,例えば点灯と消灯の間に以下のようなプログラム片を書いて時間調整をする.
mov r1, #0x1f0000
1: subs r1, r1, #1 @ 何もしないまま 0x1f0000 から 0 までカウントダウン
bne 1b
このコードの2行目のラベル 1
はローカルラベルであり,3行目の 1b
は「ここより前で最も近いラベル 1
」という意味である.
第II部・第III部で使っているGNUアセンブラでは,0以上の整数をラベルとして使うことができ,かつ同じ整数のラベルを2箇所以上に付けてもよい.ジャンプ先として使う際は「ここより前で最も近い1
(= 1b
)」「ここより後で最も近い1
(= 1f
)」のように指定する.
例(GNUアセンブラのマニュアルより):
1: b 1f
2: b 1b
1: b 2f
2: b 1b
@ 上記は以下と同じ意味:
L1: b L3
L2: b L1
L3: b L4
L4: b L3
別の例:
1: b 1b
@ 上記は以下と同じ意味:
loop: b loop
なお,b
やf
を付け忘れて bne 1
と書いてしまうと,「1番地へのジャンプ」と解釈され,全く違う意味になるので注意!
@ 誤った例
mov r1, #0x1f0000
1: subs r1, r1, #1
bne 1 @ 1番地にジャンプ(そして暴走)
練習問題
演習3.1-3 LEDを点滅させるプログラム blink.s を作り,chap1 ディレクトリの中に置きなさい. ただし,高速に点灯と消灯を繰り返すと点灯し続けているようにしか見えないので,上述のような時間つぶしコードを点灯と消灯の間に書くなどして,時間調整しなさい.
なお,点灯と消灯が分かれば,どのようなパタンで点滅してもよい.余裕があれば,点滅パタンを工夫するとなお良い.
さらに, README.md に,次の例に従ってプログラムの説明を書きなさい.ソースコードとblink.img
を作るMakefile,README.mdをpushすること.
README.md の例:
* light.s -- LEDを点灯させるプログラム
* blink.s -- LEDを○○のパタンで点滅させるプログラム
演習3.1-4 SoCのデータシートおよび実験用I/Oボードの仕様を読んで,押しボタンスイッチの使い方を調べなさい.
- BCM2835 ARM Peripherals (Raspberry Piで使われているSoCのマニュアル)
- (正誤表)
- 実験用I/Oボードの仕様
chap1 ディレクトリの中に switch.s を作り,スイッチ1 (SW1) を押している間だけLEDが点灯するプログラムを書きなさい.switch.img
を作るMakefileもpushすること.また,README.md にも switch.s の説明を追記すること.
演習3.1-5 (オプション課題) 4つのスイッチでLEDの光り方を制御するプログラムを作りなさい. どんな光り方でもよいが,次の条件を満たすこと.
- LED が点灯している時間と消灯している時間があること.(光りっぱなしはダメ)
- スイッチの操作をしたかどうかが LED を見て判断できること.(スイッチを操作しても光り方に変化が生じないのはダメ)
- スイッチ1〜4をそれぞれ単独で操作したとき,どのスイッチを操作したかが LED を見て判断できること.(どのスイッチを押しても同じなのはダメ)
さらに,複数のスイッチを同時に押すことやスイッチを押す順序を変えることによって光り方が変わるようにしてもよい(オプション項目).
swled.img
を作るMakefileもpushしなさい.ソースファイルは複数に分割して作ってもよい(ライブラリに関する補足説明を参照).ソースファイル全ての一覧をREADME.mdに書くこと.