SNSへはこちら

AVRマイコンでブートローダーを自作(3) - Flashにアクセスする

かなり足並み遅いですが、今回は 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. アセンブリ命令でラベルアドレスを格納
  2. 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