かなり足並み遅いですが、今回は Flash 領域にアクセスするやや特殊な方法をやっていきます。
この記事中で表記する Flash と プログラムメモリは同義です。
追記(2021/8/16):コードを整理しました。ラベルやコード長を短くしてよりスマートになったんじゃないと思います
今回のブートローダープログラム
前回からアップデートしまして、AVR マイコンでは Flash 領域からデータを持ってくる場合は専用の命令を記述する必要があるということを知ったので一旦記事にしたまでです。
とりあえずプログラム全体を示しておきます。
.include "../m328pdef.inc"
.device atmega328p
.def tmp = r16
.cseg
.org 0x0000
jmp main
main:
sbi DDRB, 1
sbi PORTB, 1
jmp main
.org 0x3800
jmp bootloader_main
uart_init: ; {{{
ldi tmp, 0
sts UBRR0H, tmp
ldi tmp, 51
sts UBRR0L, tmp ; set baud rate to 9600 bps
ldi tmp, (1<<RXEN0) + (1<<TXEN0)
sts UCSR0B, tmp ; enable tx and rx
ldi tmp, (0b11<<UCSZ00)
sts UCSR0C, tmp ; Set frame format: 8data, 1stop bit
ret
; }}}
.def data = r18
USART_Transmit: ; {{{
; Wait for empty transmit buffer
lds tmp, UCSR0A
sbrs tmp, UDRE0
rjmp USART_Transmit
; Put data into buffer, sends the data
sts UDR0, data
ret
; }}}
USART_Receive: ; {{{
lds tmp, UCSR0A
sbrs tmp, RXC0
rjmp USART_Receive
lds data, UDR0
ret
; }}}
UART_print_hex: ; 8bit {{{
mov r19, data
; * The upper 4bits
mov data, r19
swap data
andi data, 0x0F
subi data, 10
sbrs data, 7
subi data, -('A' - '9' - 1)
subi data, -('0' + 10)
rcall USART_Transmit
; * The lower 4bits
mov data, r19
andi data, 0x0F
subi data, 10
sbrs data, 7
subi data, -('A' - '9' - 1)
subi data, -('0' + 10)
rcall USART_Transmit
; * Restore data reg
mov data, r19
ret
; }}}
; Prints program flash (0x00 - 0xFF)
bootloader_main:
rcall uart_init
blmain_loop1:
rcall USART_Receive ; wait
ldi r30, 0x00 ; the lower Z
ldi r31, 0x00 ; the upper Z
ldi r25, 0xFF
blmain_loop2: ; === Print START ===
lpm data, Z+
rcall UART_print_hex
; Print flash
ldi data, ' ' ; white space
rcall USART_Transmit
cpse r30, r25
rjmp blmain_loop2 ; === Print END ===
ldi data, 0x0d ; CR
rcall USART_Transmit
ldi data, 0x0a ; LF
rcall USART_Transmit
rjmp blmain_loop1
以下は今回のブートローダーに付随した説明です。
新概念の説明
超主観的ですが、今回プログラムを書くに当たって「これは知らなかった」「へぇ、こういう構成なのか」と思った内容をまず述べます。
プログラム解説はその後です。
Zレジスタ
まず、今回使用する Z レジスタに関する説明をします。
この「レジスタ」は2つのレジスタをペアで用いることで表現するものになっています。なんか Intel 8051 みたいだね。
その詳細は以下に示すとおりです。
レジスタ番号 | 説明 |
---|---|
r30 | Zレジスタ下位 |
r31 | Zレジスタ上位 |
...というように、r31:r30
というレジスタペアで「Z レジスタ」というものが表現されます。プログラムメモリのメモリアドレスは 16bit 長だからこのような方法が取られています。どうやら、実際に Z レジスタへの値の代入は上下 8bit ずつ ldi
命令等を用いて行うようです。
ldi r30, 0x34
ldi r31, 0x12
これで、Z レジスタに 0x1234 が代入されたことになります。
Z レジスタ自体のインクリメントは各命令実行時に行うことができて、例えば ld r16, Z+
と書くと Z レジスタに記載のアドレスに格納されている SRAM データを r16 にコピーしつつもその後 Z レジスタをインクリメントすることができます。
個人的にふと思ったことですがこれを利用してダミーコピーをすることで Z レジスタをインクリメントさせる、と言う使い方も可能ですね。
ld r16, Z+ ; ここでr16にロードされた値は使わず、Z+をインクリメントする
Flashの値をレジスタに読み込む命令
AVR マイコンは現代のマイコンとは異なり、Flash 領域と SRAM 領域などと、メモリ空間が独立しています。
ですので、通常のレジスタ間接アドレッシングではアクセスできません。
このマイコンでは Flash 領域アクセス用に LPM 命令, SPM 命令が用意されています。
LPM は Load Program Memory の略で、Z レジスタの指すアドレスに格納されるプログラムメモリの値を指定レジスタにコピーします。詳しくは AVR マイコンのマニュアルでもご覧になってください。
一方 SPM は Store Program Memory の略で、Z レジスタの指すアドレスに格納されるプログラムメモリの値を指定レジスタにコピーします。
例えばこう書くと 0x0000 の上位バイトをコピーします。AVR マイコンは Flash がリトルエンディアンで、1つのアドレスに格納されるデータは 16bit 長であることに注意してください。ついでに Z レジスタをポストインクリメントしています。
ldi r30, 1
ldi r31, 0
lpm r16, Z+
今回のブートローダーの説明
各所に分けて行っていきましょう。
UART_print_hex 関数
8bit の数値を UART で出力する関数です。USART_Transmit
にドップリ依存しています。
ここの動作は至って簡単です。要は 4bit ごとに区切って ASCII 文字列に変換しているだけです。フローは以下に示すとおり。
data
レジスタ (r18
の別名を上部で定義している) を引数とする- まず上位 4bits を出力し、その後に下位 4bits を出力する
- その際にビットシフト・ビットマスクを使用
- 桁の値に
0x30
を加算することで、簡易的にitoa
をすることができる - ASCII コードの関係上、桁の値が 10 を超える場合(0xA)、更に7を足している
- 1バイト出力後にはホワイトスペースを出力
NO_ADD1
とかラベル名を定義してあるんだけど、この AVRA ってローカルラベル使えないのかなぁ...?
Zレジスタを介するFlashへのアクセス
先程ちょろっと言いました LPM 命令を用います。
lpm data, Z+
この記述では data レジスタに Z レジスタで表現されるアドレスであって、該当する Flash 領域からデータを持ってきます。
ここで注意なのは、AVR 上の Flash アドレスを表すのは若干特殊だということです。
そもそも(これも上で述べましたが重要なので何度も述べます) AVR の Flash は特殊で、例えば「0x0000 番地」というとそのアドレスに格納されている値は 16bit の幅を持ったものとなるわけです。これはおそらくアセンブリ記述の簡易さを求めた結果なのではないかと思います。
記述の簡易さ、ですが、例えばとあるラベルのアドレスを Z レジスタに格納した後に、その1つ前の命令を実行したいとします。すると
- アセンブリ命令でラベルアドレスを格納
- 1だけ減算
だけで済みます。AVR は基本的に1命令が 16bit で構成されているので、「1命令前を実行したい」として 1 を減算するのはすごく直感的だと思います。実際のところは 16bit の即値を入れる必要のある JMP 命令等に関してはこの通りではありませんが...
...話が長くなったのでもとに戻します。ズバリ、AVR では Z レジスタの 1 〜 15bit 目がアドレスを示します。0ビット目がアドレスを示さないのって不思議ですよね。しかし 0bit 目はそのアドレスの中で上位 or 下位を指定するビットとして機能するのです。
Zレジスタ値の例
話がややこしいですよね。僕もうまく説明できるかわかりません。とりあえず具体例を見ていきたいと思います。
例えば 0x0001 の上位バイトをゲットしたい場合です。この場合以下のような記述になります。
ldi r30, 0x03
ldi r31, 0x00
lpm r16, Z
上の2つの ldi
命令では Z レジスタに値を格納しています。ここで下位バイトである r30 を見ると、0x03 == 0b0000 0011
を代入しています。これは一体どういうことかというと、0000 001 で下位アドレスを指定し、最後の 1 で「上位だよ」と指定しています。さっきから上位だの下位だの出てきて非常にややこしいですがこちらはご自身で何度も見返してみてください(丸投げ)。
...でもよく見てみると、これは AVR の Flash 空間を1アドレスにつき1バイトとみなしたものに等しいですよね。つまり「1アドレス16bit と見たときの、0x0001 にある上位バイトの値」は「1アドレス 8bit と見たときの、0x0003 にある値」に等しいわけです。ですから巷にあるコンパイラで AVR のフレーバーを追加しても問題なく動くということなんですね。頭いいけどややこしいよ。
そんなわけで、このプログラムでは各アドレスの下位→上位といった順で、合計 256 Bytes の値を読み出しているものになります。
動作
コードを読めばわかると思いますが、ブートローダー起動後に UART で 9600bps にて接続した後に、任意の1文字を送信すると Flash 領域の0番地下位から128番上位までの値をすべて出力します。
その出力結果がこちら(見やすいように、値取得後手動にて改行しています)。
0C 94 02 00 21 9A 29 9A 0C 94 02 00 FF FF FF FF
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
〜 以下略 〜
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
この値はまさに...0x0000 に記述した LED 点灯プログラムそのものでした!!
$ avr-objdump -b ihex -m avr -D test.hex
test.hex: file format ihex
Disassembly of section .sec1:
00000000 <.sec1>:
0: 0c 94 02 00 jmp 0x4 ; 0x4
4: 21 9a sbi 0x04, 1 ; 4
6: 29 9a sbi 0x05, 1 ; 5
8: 0c 94 02 00 jmp 0x4 ; 0x4