だいぶ出来てきたので、ブートローダ関係についてまとめておこうと思う。(USBaspのプロトコルについては前記事でまとめる)
ブートローダの基本動作
ブートローダ領域をサポートしたAVR で、ブートローダを有効にすると、リセットベクタが、ブートローダ領域 に移動する。
USBaspLoader では、PORTD の BIT7 を pull-up した後、チェックして H レベル bit_is_set(PIND,JUMPER_BIT) なら、0 番地にジャンプして アプリケーションを実行するようになっている。また、リセットボタンでリセットしたとき以外 -- bit_is_clear(MCUCSR,EXTRF) のときも アプリケーションを実行する。
ブートローダを実行するときは、set_bit(MCUCR,IVSEL) することで、割り込みベクタ全部をブートローダ領域 に移動する。こういう機能があるおかげで、VUSB のような割り込みを使う機能をブートローダでも使えるわけだ。
bit_is_set(PIND,JUMPER_BIT)の状態になれば、ブートローダを終了して、アプリケーションを実行するようになっている。
bootloaderconfig.h で、BOOTLOADER_CAN_EXIT を 1 にしておくと、USBasp への USBASP_FUNC_DISCONNECT コマンドで ブートローダを終了することはできる。
- Atmel DFU ブートローダとの違い
リセットボタンでリセットしたときに、PORTD の BIT7 が Lだったときのみ ブートローダが動作するのは同じ。ただし、DFU ブートローダは、H になったからといって ただちに抜けるわけではない。
BOOTLOADER_CAN_EXIT を 1 にして ブートローダの実行条件bootLoaderCondition()を 開始用と終了用の 2 つに分ければ Atmel DFU ブートローダに近くなるのかも。ただ、avrdude を 実行するたびに リセットボタンでリセットしないといけないかも。
-- 実験してみて仕様を決めよう。 - アプリケーションからのブートローダ実行
USBaspLoader では、アプリケーションからのブートローダ実行も考慮しているようだ。
実行すること自体は簡単で ブートローダ領域の先頭アドレスに jump すれば良い。
ただし、ブートローダの実行条件をクリアしないとリセットと同じになってしまう。
具体的には、JUMPER をセットした後、
bit_set(MCUCSR,EXTRF) ;
して jump 。JUMPER_BIT を 出力にして L レベルにする手もあるが、そうすると今度はブートローダからリセットなしに抜けられなくなる。
ブートローダのビルドについて
ブートローダは、開始アドレスがベクター領域ごとずれる。そういうものをどうやってビルドするかについて いろんなやり方があって、参考にしたものはみんな違う。
- USBaspLoader の方法
普通にコンパイルして、リンクのときに 以下のオプションでアドレスを指定。
-Wl,--section-start=.text=3800
または、
-Ttext 0x3800
hex ファイルは、まったく同じになるようだ。なら昔からある -Ttext 0x3800 で良さそうなものだが、なぜか誰も使っていない。
AT90USB162 では、--section-start がエラーになる。..と書いたが間違い。ちゃんと使えた。 - BOOTLOADER_SECTION を使う方法
ソースコードで関数の宣言のときに例えば、
void bootloader(uint8_t sync) BOOTLOADER_SECTION
__attribute__((noreturn));
という風に BOOTLOADER_SECTION を付ける。関数定義自体にも付ける必要があるようだ。
リンクでのアドレス指定は次のようにする。
-Wl,--section-start=.bootloader=3800
そういうやりかたが出来るというのは分ったのだが、こうしてしまうとライブラリが使えないように思えるし、メリットは良くわからない。
ブートローダ エミュレーション
ブートローダ領域をサポートしない AVR で、ブートローダを作るには、どうすれば良いのだろう?
実用上はともかく、これができるとデバッグするのが楽。なぜなら、本物のブートローダが使えるから。
www.mikrocontroller.net の "Bootloader" for ATTiny2313に 置いてある ATTiny2313_bootloader が 代表的なもの。ちなみに、上のリンクには、”48-instruction one-wire bootloader for ATTiny13/24/45/85” なんて話題もある。
まず、違うアドレスから実行するようにしないといけない。これは上で説明した。
次にリセットで、ブートローダに制御を渡すように 0 番地に本物のベクタを置かないといけない。
ブートローダ実行で、0 番地にアプリケーションのベクタが書かれた場合、元のデータを変更しないようにして、別のところに置くようにする。READ も対応して透過的にしないと ベリファイでエラーになる。
あと、自分自身を書き換えないように保護する必要もある。
ちなみに、AT90USB162 は割込みを一切使わなくとも USB の処理が出来る。リセットベクタだけ保護すれば良いので大分楽。また、ベクタは、2 Word なので、相対アドレスのRJMPではなく、絶対アドレスの JMP 命令を使う。これも処理が楽になる。
もっとも重要なことを見逃していた。ブートローダー領域でプログラム実行中でないと、SPM 命令でのフラッシュの書き込みが禁止される。アプリケーションから ブートローダー領域への call はできるようなので、いんちきは出来るかもしれない。ただ、そのいんちきをするためには、自作ブートローダーが動作していないといけないので、デバッグにはあまり役立たない。
ついでに書いておくと、AT90USB162では、後ろの 4KB が ブートローダー領域のサイズにかかわらず NRWW 領域となってて、ここに書き込む場合は、CPU が停止する。AT90USB162はあまり関係ないのだが、3,4 ms 停止するので、応答性が重要な VUSB では問題が出るかも知れない。
ATmega88p , ATmega88pa のデータシートを見てみたが、88/168 は、NRWW = 最大ブートローダ領域は、2KB 。328 はそれが 4KB になっているようだ。VUSB では 2KB でも厳しいし普通は NRWW = ブートローダ領域と決めてしまえば良いのだろう。
この問題、解決するかも。↓で記載。
おまけ:gawk で作る 0 番地に置くベクタを作るツール
0 番地のベクタを ビルドしたプログラムから作りたいのだが、どうしたら良いのだろう?
MinGW を前提にすれば gcc も使えるので簡単なのだが..
WinAVR の utils\bin には、いろんな UNIX 系コマンドがある。perl はないが、gawk はあるので、これでなんとかしてみることにした。ちょっと長いがスクリプトだし、参考になる時もあるかも ... ということで載せておく。
BEGIN{
hex[0] = 0; hex[1] = 1; hex[2] = 2; hex[3] = 3;
hex[4] = 4; hex[5] = 5; hex[6] = 6; hex[7] = 7;
hex[8] = 8; hex[9] = 9; hex["A"] = 10; hex["B"] = 11;
hex["C"] = 12; hex["D"] = 13; hex["E"] = 14; hex["F"] = 15;
}
{
sum = 0;
len = hex2dec($1, 2);
sum += len;
x = hex2dec($1, 4);
sum += x;
addr = hex2dec($1, 6);
sum += addr;
addr += x * 256;
rtype = hex2dec($1, 8);
sum += rtype;
for (i=0; i<len; i++) {
data[i] = hex2dec($1, 10+i*2);
sum += data[i];
}
isum = hex2dec($1, 10+len*2);
sum += isum;
sum = sum % 256;
if (sum != 0) {
printf("line %d: ihex format error!\n",NR);
exit(1);
}
if (NR == 1) {
if (data[0] == 12 && data[1] == 9 * 16 + 4) {
addr = 0;
} else {
printf("line %d: data error!\n",NR);
exit(1);
}
}
if (NR == 1 || rtype == 1) {
print_ihex(rtype, addr, data, len);
}
}
function hex2dec(str, i, x) {
x = hex[substr(str,i,1)] * 16 + hex[substr(str,i+1,1)];
return x;
}
function print_ihex(rtype, addr, data, len, sum) {
sum = 0;
printf(":%02X%04X%02X",len, addr, rtype);
sum += len;
sum += addr;
sum += int(addr/256);
sum += rtype;
for (i=0; i<len; i++) {
printf("%02X", data[i]);
sum += data[i];
}
sum = sum % 256;
printf("%02X\r\n", 256 - sum);
return sum;
}
おまけ:DFU ブートローダ 1.05 の サブルーチン
逆アセンブルしたら、SPM を使う関数一式が見つかった。
このアドレスに直接 CALL することで DEBUG できそう。
で、自作するときは、最後の方の固定のアドレスに置いておく。
... というか、最後に JUMP テーブルがあるから、なにか公開された API かも。ただ、gcc 規約ではないので注意。
gccの規約は、ELM: アセンブラ関数の書き方 (avr-gcc)が分りやすい。引数のレジスタが違うし、r16,r17 を使っているので、ラッパーで push/pop しないといけない。あと、0 にしておく r1 を壊している。0 に再設定しないと。
ところで、rww_set_enable() 相当らしき関数が、よくわからない。11 = 1011b を書いているが、データシートには記載なし。
あと、erase してwrite というのは、何? 先に fill しておいて、atomic にやるということ?
それはともかく、erase と write は、アクセスできるようになったら戻るようになっている。アプリーケーションから使っても問題なさそう。
1FF2 : 940C 1E70 JMP L1E70 ; erase and write
1FF4 : 940C 1E94 JMP L1E94 ; LPM なので略
1FF6 : 940C 1E9C JMP L1E9C ; LPM なので略
1FF8 : 940C 1EA4 JMP L1EA4 ; page_fill(addr, data);
1FFA : 940C 1E75 JMP L1E75 ; page_write(addr);
1FFC : 940C 1E86 JMP L1E86 ; page_erase(addr);
1FFE : 940C 1EAC JMP L1EAC ; lock_bits_set(lock_bits)
;page_write(addr);
1E75 : D03C L1E75: RCALL L1EB2 ; spm_busy_wait();
1E76 : 2FF1 MOV ZH,R17 ;
1E77 : 2FE0 MOV ZL,R16 ;
1E78 : E045 LDI R20,$05 ; WRITE
1E79 : BF47 OUT SPMCSR,R20 ;
1E7A : 95E8 SPM ;
1E7B : D036 RCALL L1EB2 ; spm_busy_wait();
1E7C : D012 RCALL L1E8F ; rww_set_enable();
1E7D : 9508 RET ;
; page_erase(addr);
1E86 : D02B L1E86: RCALL L1EB2 ;spm_busy_wait();
1E87 : 2FF1 MOV ZH,R17 ;
1E88 : 2FE0 MOV ZL,R16 ;
1E89 : E043 LDI R20,$03 ; ERASE
1E8A : BF47 OUT SPMCSR,R20 ;
1E8B : 95E8 SPM ;
1E8C : D025 RCALL L1EB2 ;spm_busy_wait();
1E8D : D001 RCALL L1E8F ;rww_set_enable();
1E8E : 9508 RET ;
;rww_set_enable();
1E8F : D022 L1E8F: RCALL L1EB2 ;spm_busy_wait();
1E90 : E141 LDI R20,$11 ;
1E91 : BF47 OUT SPMCSR,R20 ;
1E92 : 95E8 SPM ;
1E93 : C01E RJMP L1EB2 ;spm_busy_wait();
; page_fill(addr, data);
1EA4 : 2FF3 L1EA4: MOV ZH,R19 ;
1EA5 : 2FE2 MOV ZL,R18 ;
1EA6 : 2E01 MOV R0,R17 ;
1EA7 : 2E10 MOV R1,R16 ;
1EA8 : E041 LDI R20,$01 ; FILL
1EA9 : BF47 OUT SPMCSR,R20 ;
1EAA : 95E8 SPM ;
1EAB : C006 RJMP L1EB2 ;
; lock_bits_set(lock_bits)
1EAC : D005 L1EAC: RCALL L1EB2 ;
1EAD : 2E00 MOV R0,R16 ;
1EAE : E029 LDI R18,$09 ;BLBSET | 1
1EAF : BF27 OUT SPMCSR,R18 ;
1EB0 : 95E8 SPM ;
1EB1 : C000 RJMP L1EB2 ;
; spm_busy_wait();
1EB2 : 2E02 L1EB2: MOV R0,R18 ;
1EB3 : B727 IN R18,SPMCSR ;
1EB4 : FD20 SBRC R18,0 ;SPMEN
1EB5 : CFFC RJMP L1EB2 ;
1EB6 : 2D20 MOV R18,R0 ;
1EB7 : 9508 RET ;
; rww_busy_wait();
1EB8 : 2E02 L1EB8: MOV R0,R18 ;
1EB9 : B727 IN R18,SPMCSR ;
1EBA : FD26 SBRC R18,6 ;RWWSB
1EBB : CFFC RJMP L1EB8 ;
1EBC : 2D20 MOV R18,R0 ;
1EBD : 9508 RET ;