それ以外にも、チップの外につけるパーツを減らせるようになっている。
参照したドキュメントはSoC-Peripherals.pdfで、6節にGPIOの説明がある。
GPIOなソースコードはこちら: https://github.com/mgwsuzuki/v6pi/tree/master/base
SoCに内蔵される機能は多いほど応用が広がるわけだが、それはつまり入力または出力のピン数の増加を意味する。
とは言っても、チップの外に出せるピンの数は有限であり、またユーザがすべての機能を使うわけでもないので、機能ごとに専用のピンを割り当てるのは非効率である。
そんなわけでRasPiでは、1つのピンに対して複数の機能をマルチプレクスし、ユーザにそれを選択してもらうようになっている。
vp_gpio.hでは以下のようなコードでピンの機能選択ができる。
// function select
typedef enum {
VP_GPIO_FSEL_INPUT = 0,
VP_GPIO_FSEL_OUTPUT = 1,
VP_GPIO_FSEL_ALT0 = 4,
VP_GPIO_FSEL_ALT1 = 5,
VP_GPIO_FSEL_ALT2 = 6,
VP_GPIO_FSEL_ALT3 = 7,
VP_GPIO_FSEL_ALT4 = 3,
VP_GPIO_FSEL_ALT5 = 2
} vp_gpio_fsel_t;
// 指定された番号のGPIOピンのfunctionをセットする
void vp_gpio_set_fsel(i32_t portnum, vp_gpio_fsel_t fsel);
vp_gpio_fsel_tの意味はSoC-Peripherals.pdfをみれば分かると思う。
GPIOを出力ピンとしたとき、出力したい値を設定するために以下の2つの関数を用意した。
// 指定された番号のGPIOピンの出力を1にする
void vp_gpio_set_output(i32_t portnum);
// 指定された番号のGPIOピンの出力を0にする
void vp_gpio_clear_output(i32_t portnum);
これはGPSETnまたはGPCLRnレジスタを叩くだけの関数である。
上記のように、RasPiではピンの出力を1にするか0にするかの2つしか持っていないようだが、マイコンによっては設定したい値そのものを指定できるレジスタや、ピン出力を反転するためのレジスタを持つものもある。
特に設定したい値そのものを指定できるレジスタは必須と思っていたけど、RasPiはそうではないようだ。ということで、それが出来る関数を用意してみた。
// 指定された番号のGPIOピンのレベルをセットする
void vp_gpio_set_level(i32_t portnum, u32_t level);
GPIOを入力ピンとしたとき、ピンの状態を知るために以下の関数を用意した。
// 指定された番号のGPIOピンのレベルを返す
u32_t vp_gpio_get_level(i32_t portnum);
指定されたピンがHなら1、Lなら0を返す。
RasPiでは、各ピンごとにプルアップ、プルダウン、プルアップ/ダウンなしを指定できる。ただ、その指定方法は変わっていて、
という手順になっている。一見複雑に見えるが、LSI設計している人間にとってその狙いと回路構成が目に浮かぶのである。
150clk待つというのは正確には150clk以上待てばいいはずで、実装もそうしている。
ここで忘れていけないのは、最後のステップのGPPUDCLKnに0を書き込むところである。
GPPUDに書き込んだ値を記憶させるのに重要なことは、GPPUDCLKnを0から1に変化させることである。いわゆる立ち上がりエッジと言われるものだが、これを発生させるためにはGPPUDCLKnに1を書き込む前にその値が0になっていないといけない。
もし0を書き込むのを忘れて、別のピンのプルアップ/ダウンを設定しようとして1を書き込んでも、1から1への変化(というか変化していない)じゃ立ち上がりエッジは発生しない。
その結果、正しく設定できないということになる。
vp_gpio.hでは以下のコード及び関数でプルアップ/プルダウンを指定できる。
// pull-up/down
typedef enum {
VP_GPIO_PUD_OFF = 0,
VP_GPIO_PUD_DOWN = 1,
VP_GPIO_PUD_UP = 2
} vp_gpio_pud_t;
// 指定された番号のGPIOピンのpull-up/downをセットする
void vp_gpio_set_pud(i32_t portnum, vp_gpio_pud_t pud);
そもそもプルアップとかプルダウンってなんのために使うのか、というのを理解しないといけない。
よくデジタル回路では0を示すのに0V、1を示すのに3.3Vとか2.5Vの電圧をかける、みたいに言われる。
がしかし、デジタル回路といってももとはアナログな振る舞いに支配されるわけで、そこには0でも1でもない状態が存在する。
1つの信号線を誰かが0Vや3.3Vに駆動していればいいのだが、実は誰も駆動していない、という状態が普通にある。例えば、USBなどのコネクタに何も挿さっていない場合を考えてみるとよい。
そのような、だれも駆動していない信号を入力したら、それは0になるのか、1になるのか?
答えは「分からない」とか「0や1にふらつく」である。
この「ふらつく」というのがかなり厄介で、例えばUARTなら1->0の変化でデータが到着したと判断するので、ふらつきはすなわち誤受信になる。割り込み信号ならふらつくたびに意図しない割り込みが発生する。
これは困るので、それを回避するためにプルアップ/プルダウンをする。
プルアップにしたら、誰も信号を駆動していないときに1になり、プルダウンにしたら、誰も信号を駆動していないときに0になる。もし誰かが信号を駆動したらプルアップ/プルダウンの指定に関係なく、駆動されたレベルになる。
つまり、主に入力ピンのための機能だと思えばよい。
出力ピンなら自らが0か1に駆動するから基本的にいらない。が、自ら「駆動しない」という使い方をするなら別だが、ちょっと割愛。
プルアップにするかプルダウンにするかは、そのピンの機能によりけりであり、誤動作しないように設定すればよい。
]]>“OSなしのCPUで”などと書いたけど、OSの立ち上げだって同じ話だったりする。ブートローダーというキーワードで片付けられたりするけど。
ということで、電源投入からそれなりに正しい手順でmain関数を実行するまでのコードを作った。
https://github.com/mgwsuzuki/v6pi/tree/master/base
それなりに正しい手順だけど、テスト目的程度のレベルである。
CPUは命令を実行している間に割り込みが入ったとき、ある特定のアドレスにジャンプしたり、特定のアドレスを読み出してそこにジャンプしたりする。このような動作を例外処理と呼び、割り込みは例外処理の1つである。
CPUの例外要因は1つではない。そのため、例外要因ごとに所望の処理ルーチンにジャンプするのを効率化するために、例外要因ごとに番号をつけ、それをインデックスとしたテーブルを作る。これをベクタテーブルなどと呼ぶ。
テーブルの中身はRasPiに使われているARMでは命令そのものである。ただ、CPUごとに実装はそれぞれでジャンプ先のアドレスだったりするものもある。
このテーブルのベースアドレスは0x0か、0xffff_0000を選べる。0xffff_0000はMMUを使ったOSで使用することを想定している。初期状態は0x0である。
ARMでは以下のようなテーブル定義となっている。
開始アドレス | 例外/割り込み名 |
---|---|
0x0000_0000 | リセット |
0x0000_0004 | 未定義命令 |
0x0000_0008 | ソフトウェア割り込み |
0x0000_000c | プリフェッチアボート |
0x0000_0010 | データアボート |
0x0000_0014 | 予約 |
0x0000_0018 | 割り込み要求 |
0x0000_001c | 高速割り込み要求 |
このテーブルには命令を記述する必要があり、要するにアセンブラの命令を記述すればよい。ということでvector.sには次のようなコードを記述している。1命令あたり4byteなので、ぴったり8つの要素があるベクタテーブルとなる。
_start:
ldr pc, htbl_reset
ldr pc, htbl_undef
ldr pc, htbl_swi
ldr pc, htbl_pabt
ldr pc, htbl_dabt
ldr pc, htbl_unused
ldr pc, htbl_irq
ldr pc, htbl_fiq
“htblなんとか”から始まるラベルは次のようになっている。
htbl_reset:
.word reset_handler
htbl_undef:
.word undef_handler
htbl_swi:
.word swi_handler
htbl_pabt:
.word pabt_handler
htbl_dabt:
.word dabt_handler
htbl_unused:
.word unused_handler
htbl_irq:
.word irq_handler
htbl_fiq:
.word fiq_handler
“なんとかhandler”は例外処理ハンドラの開始アドレスである。つまり、ldr命令で”なんとかhandler”のアドレスをプログラムカウンタに読み込ませてジャンプする。
ちなみに、ldr命令ではなくてb命令でジャンプさせる方法もある。が、32MBより離れたアドレスに分岐できないし、後で説明する回避できない副作用があるため用いない。
リセットハンドラの処理は以下のようになっている。
名前の通り、電源投入直後やCPUがリセットされたときに実行されるコードである。vector.sでは次のとおり。
mov r0, #0x8000
mov r1, #0x0000
ldmia r0!, {r2, r3, r4, r5, r6, r7, r8, r9}
stmia r1!, {r2, r3, r4, r5, r6, r7, r8, r9}
ldmia r0!, {r2, r3, r4, r5, r6, r7, r8, r9}
stmia r1!, {r2, r3, r4, r5, r6, r7, r8, r9}
やっていることは、0x8000から32byte分のデータを0x0にコピーしている。
実は、_startラベルから始まるベクタテーブルは、アドレス0x8000に配置されている。0x8000は何かというと、RasPiのARMが起動したときに実行が開始されるアドレスなのである。0x0でない。
これはRasPiに搭載されているSoC(及びメーカー配布のブートローダー)がそうなっているため、どうしようもない。だからベクタテーブルを0x8000に配置すれば電源投入直後だけはしのげる。
かといって、ARMのベクタテーブルの開始アドレスはやはり0x0である。なので、0x8000にあるままでは正しく動作しない。ということで、上記のコードで0x8000にあるベクタテーブルを0x0へコピーしている。
ここにベクタテーブルをb命令で記述できない理由がある。b命令はプログラムカウンタをベースとした相対アドレスで分岐先アドレスを決定するため、命令を別のアドレスに移動すると分岐先アドレスが狂ってしまうのである。
ldr命令もプログラムカウンタをベースとした相対アドレスで読み出しアドレスを決定するけど、読み出し先データも矛盾なくコピーするので問題ない。そして、読み出し先に書いてあるジャンプ先アドレスは絶対アドレスなので問題なくジャンプできる。
ARMはCPUの動作モードして以下のようなものがある。
これらはcpsr(レジスタ)の[4:0]に値を書き込むことで変更できる。といっても、書き込み権限のないモードになってしまうと書き込めない。
もうひとつ大切なことは、各モードごとにいくつかのレジスタが切り替わる、バンクレジスタとなっていて、スタックポインタもそれに含まれる。
現状のv6piではCPUモードを切り替えるようなレベルではないため、スーパバイザのみ設定しているが、例えば割り込みを使うようになったらそのモードにおけるスタックポインタを正しく設定する必要がある。
mov r0, #(0x13|0xc0)
msr cpsr_c, r0
mov sp, #0x10000000
なお、上のコードでは割り込みを禁止している。
ブートローダーによって、SDカードから実行したいプログラムはすでにメモリに読み込まれているが、これを実行する前の準備が必要である。
1つは初期化データのコピー、もうひとつはBSS領域をゼロクリアすることである。
初期化データはプログラムが実行される前にメモリにセットされている値だが、これは.textの後ろに配置されていて、これを.dataの領域にコピーする必要がある。
ldr r1, =__text_end__
ldr r2, =__data_start__
ldr r3, =__data_end__
1: cmp r2, r3
ldrlo r0, [r1], #4
strlo r0, [r2], #4
blo 1b
このおかげで初期化データをプログラムが変更しても、リセットすることでもとの値へ復活させることができる。
bss領域は、初期値がゼロでなければいけない。ということでゼロクリアする。
mov r0, #0
ldr r1, =__bss_start__
ldr r2, =__bss_end__
2: cmp r1, r2
strlo r0, [r1], #4
blo 2b
なお、”__“で始まるシンボルはmemmapに記述されており、リンカの処理で値が確定する。
ここでようやくmain関数をコールすることができる。
bl main
書きながら気が付いたが、main関数から戻ってきたときの処理が入っていない。後で無限ループを追加しておこう。
リセット以外のハンドラを記述するためのテンプレートである。現在は何も使っていないのですべて無限ループとなっているが、必要に応じて記述する。
undef_handler:
b undef_handler
swi_handler:
b swi_handler
pabt_handler:
b pabt_handler
dabt_handler:
b dabt_handler
unused_handler:
b unused_handler
irq_handler:
b irq_handler
fiq_handler:
b fiq_handler
]]>mrubyをRaspberry Pi(bare metal)で動かす: Windows編の通りにJTAGを使えるように作業すると、SDカードに以下のファイルを用意することになる。
これらのファイルが何か、というのを調べたところ、次のような情報があった。
http://raspberrypi.stackexchange.com/questions/10489/how-does-raspberry-pi-boot
訳すとこんな感じだろうか。
loader.binは大してなにもしない。これは.elfファイルを扱うことができ、start.elfをメモリの最初に読み出すのに必要である(ARMはSDRAMをアドレス0番地から使用する)。
bootcode.binに.elf読み出し機能を追加する予定があり、そうなるとloader.binは不要になる。ただ、作業の優先度は低い。
「電源投入直後、ARMコアがoffでGPUコアがonになっており」ってほんと? という気がして調べてみたけど、寄り道な気がしたので深追いしないことにした。
loader.binというのはSDカードにコピーしていないので、bootcode.binに取り込まれたのだろう。
読み込む実行イメージのファイル名はkernel.imgに固定されているようだ。
SDカードにcmdline.txtは用意していないけど、フォーマットは以下のようだ(自分は試していない)。
kernel.imgを読み出した後は書かれていないけど、プログラムカウンタが0x8000にセットされる。なので、自分で作った実行イメージは必ず0x8000にリセットベクタ相当のコードがないといけない。
SDカードにコピーされるkernel.imgはarmjtagと呼ばれているもので、以下から取得できる。
https://github.com/dwelch67/raspberrypi/tree/master/armjtag
ちなみにこのリポジトリにはbare metalでRasPiを使うための情報がかなり蓄積されていて、自分もちょこちょこ確認していたりする。
このarmjtagは何をやっているかというと、次のとおり。
GPIOは有限のピンに多数の機能をマッピングできるように、またボード設計におけるピン配置の柔軟性をあげるために、ピンごとにそれをどのように使いたいかを設定することができる。
単なる入力、出力ピンにすることもできるし、alternate functionとして6つ設定できるようになっている。ここらあたりはSoC-Peripherals.pdfの”6. General Purpose I/O”に書かれている。
というわけで、以下のようにGPIOにJTAGのピンを割り当てている。
GPIO | JTAG | func |
---|---|---|
GPIO4 | ARM_TDI | ALT5 |
GPIO22 | ARM_TRST | ALT4 |
GPIO24 | ARM_TDO | ALT4 |
GPIO25 | ARM_TCK | ALT4 |
GPIO27 | ARM_TMS | ALT4 |
また、これらのピンのpull-up/downを無効にしている。でも、入力ピンは無接続に備えてpull-upかdownにしたほうが良い気がする。
後はLEDを点滅させるためにタイマを使っている。といっても割り込みを使っているわけじゃなく、動作させてカウンタ値を無限ループで確認し、所定の値になったらLEDをon/offしている。
なお、事前にLEDにつながっているGPIO16をoutputモードにしている。このピンをLにするとLEDが点灯し、HにするとLEDが消灯する。
]]>gdbはもっぱらeclipseから使っているけど、コマンドラインから使う方法を一応調べてみた。
mrubyをRaspberry Pi(bare metal)で動かす: Windows編にあった通りに環境を構築していて、主なツールは以下のとおり。
それぞれインストールしたディレクトリを$(OPENOCD), $(YAGARTO)とする。
ARM用のgdbはYAGARTOに含まれていて、これとJTAGツールを接続するためにOpenOCDがある。関係を図にすると以下のような感じ。
RasPiのSDカードにはmruby(略)のサイトにあったとおり、JTAGデバッグに必要なファイルをコピーしておく。
コマンドは以下の通り。
$(OPENOCD)/bin/openocd-0.7.0.exe -f $(OPENOCD)/scripts/interface/olimex-arm-usb-tiny-h.cfg -f ./raspi.cfg
なお、OpenOCDを起動する前にRasPiの電源を投入し、JTAGツールをUSBで接続しておく。
raspi.cfgはmruby(略)のサイトの手順通りに作業するとどこかのリポジトリから取得される。raspiプロジェクトを作成するところだったかな。
無事起動すると以下のようなメッセージがでる。
Open On-Chip Debugger 0.7.0 (2013-05-05-10:41)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.sourceforge.net/doc/doxygen/bugs.html
Info : only one transport option; autoselect 'jtag'
adapter speed: 1000 kHz
none separate
raspi.arm
Info : max TCK change to: 30000 kHz
Info : clock speed 1000 kHz
Info : JTAG tap: raspi.arm tap/device found: 0x07b7617f (mfg: 0x0bf, part: 0x7b76, ver: 0x0)
Info : found ARM1176
Info : raspi.arm: hardware has 6 breakpoints, 2 watchpoints
OpenOCDは自分自身の制御のために4444ポートにtelnetで接続できる。
ほとんど使うことは無いと思ったが、実はgdbを接続したときにARMが動作しているとgdbが何もできないという状態になる。
ということで、telnetなり、他のツールを使ってlocalhost:4444に接続し、haltコマンドを実行しておく。
起動の前に、実行したいプログラムのelfファイルをtarget.elfとして作業ディレクトリにコピーしておく。ここではv6piのgpioのそれをコピーした。うまく実行できればACT LEDが点滅する。
gdbの起動コマンドは以下の通り。
$(YAGARTO)/bin/arm-none-eabi-gdb.exe target.elf
続いて以下のコマンドを入力する。
target remote localhost:3333
load
後はnextとかstepとかlistとかでステップ実行できる。適当にこれらコマンドを実行したログは以下の通り。なお、nはnext, lはlistの略コマンド。
(gdb) target remote localhost:3333
Remote debugging using localhost:3333
vp_gpio_set_fsel (portnum=743516865, fsel=VP_GPIO_FSEL_INPUT) at vp_gpio.c:45
45 }else if (portnum < 54){
(gdb) load
Loading section .text, size 0x2f8 lma 0x8000
Start address 0x8000, load size 760
Transfer rate: 6080 bits in <1 sec, 760 bytes/write.
(gdb) n
Single stepping until exit from function _start,
which has no line number information.
stepi ignored. GDB will now fetch the register state from the target.
Program received signal SIGINT, Interrupt.
0x00008000 in _start ()
(gdb) n
Single stepping until exit from function _start,
which has no line number information.
0x00008040 in reset_handler ()
(gdb) n
Single stepping until exit from function reset_handler,
which has no line number information.
main () at main.c:21
21 int main(){
(gdb) n
24 vp_gpio_set_fsel(16, VP_GPIO_FSEL_OUTPUT);
(gdb) l
19 }
20
21 int main(){
22 u32_t n;
23 // ACT LEDのピンをoutputにする
24 vp_gpio_set_fsel(16, VP_GPIO_FSEL_OUTPUT);
25
26 while(1){
27 vp_gpio_clear_output(16);
28 delay();
(gdb) n
27 vp_gpio_clear_output(16);
(gdb) n
28 delay();
(gdb) n
29 vp_gpio_set_output(16);
(gdb)
ステップ実行じゃなくて、ブレークポイントに達するまで実行するのはcontinueコマンドである。ブレークポイントを指定してなかったら永遠に実行しつづける。
continueから抜ける方法を調べようとしたところで疲れたので終了。
]]>いわゆる組み込み系ではJTAGを使って実行イメージをターゲットに書き込んだりデバッグするのが普通である。ということで、RasPiに使えるJTAGツールを通販で購入した。OlimexのARM-USB-TINY-Hというものである。
これをRasPiのピンヘッダに挿すわけだが、信号配置が違うのでそのまま挿せるわけではない。なのでブレッドボードで使うようなジャンパーコードでつなぐわけだが、抜けてしまったときに間違ったピンに接続するケースが間違いなく起こるので、手元にあるパーツを使って専用のケーブルを作った。
こうするとまず間違えないから安心して使うことができる。
余談だが、とある作業でRasPiのピン接続を間違って1台お釈迦にしたことがある。間違いに電源ピンが絡むと壊れる可能性がぐっと高くなるので注意である。
ちなみに右下のボードはUSBシリアルである。この手の開発には必須なので、あらかじめ用意してある。
母艦のソフトウェアはeclipseを使うことにした。といっても、それでコーディングすることはなく、JTAGを経由した実行イメージのロードとデバッグがメインである。
その方法が非常によくまとめられていたサイトがあって、参考にさせて頂いた。
mrubyをRaspberry Pi(bare metal)で動かす: Windows編
RasPiとJTAGツールの結線情報も書かれている。
たぶんeclipseを使わずにgdbをベタで使っても同じことが出来るのだろう。ストイックで自分には出来ないけど。
]]>とりあえずgithubにリポジトリを作ってみた。
https://github.com/mgwsuzuki/v6pi
V6 UNIXを移植するわけだけど、別にV6 UNIXが使いたいからじゃない。ただもう少しV6 UNIXを知りたい。
また、V6 UNIXが動いていたPDP11に興味があるわけじゃないから、そのハードを作ったり、エミュレータを作るのもあまり興味がない。それよりも現在よく使われているARMのほうがよっぽど興味がある。
そんなわけで、V6 UNIXとARMの勉強をいっぺんにやってしまおうというのが目的だったりする。
今のところV6 UNIXどころじゃなくて、CPUのブートからCのmain関数がコールできるまでのコードを整備したり、GPIOのピン設定の簡易ライブラリが出来上がってる程度である。
これからの予定として、V6 UNIXをすぐに移植するというより、ARMのMMUの使い方を調べたりするのがしばらく続くと思う。
自分が理解することが最優先なので、進捗はとても遅いはずである。
]]>