概要
タイマーを使ったスレッドの実装の前に,動作を確認するための便利な関数を作っておきたい.具体的には,'print'に相当する関数を作りたい.ここまでの実装では,シリアルを使ったputc
を実装したものの,この関数は一文字出力することしかできなかった.そこで,文字列を出力できるputs
の実装を試みる.
ところが,puts
を正しく機能させるには,メモリマップと,それを指定するためのリンカスクリプトを書く必要がある.かつ,静的変数を読み書きするため,ROMからRAMへのデータコピーも必要になる.
失敗例
シリアル通信で作成したputc
を利用し,main.cにputs
を実装し,mainから呼び出してみる.
int puts(char *str) {
while (*str) {
putc(*(str++));
}
return 0;
}
int main (void)
{
serial_init();
char str1[] = {'a','b','c', '\0'};
char *s = "hello world\n";
puts(str1);
puts(s);
return 0;
}
この結果をシリアルモニタで確認すると,以下のようになる.
よく見ると,スタックに確保したstr1はちゃんと出力できているのに,静的変数である"hello world\n"がおかしいことに気づく.つまり,puts
とかputc
の実装が悪いのではなく,データを格納しているアドレスを正しく参照できないことによる不具合,という結論にたどり着いた.
P.S. とはいえ,静的変数だとしても,書き換えをしなければ正しくアクセスできるはずで,こんな不具合起こらないと思うんだけど...謎
メモリマップ
上記の不具合に対応するため,メモリマップを設定し,変数それに対応する初期値を正しくメモリに配置する.
具体的には,
- リンカスクリプトの修正
- 起動時に初期値のある変数のデータのコピー
を行う.
リンカスクリプトの修正
今回対象とするatmega328pは,32KBのROMと2KBのRAMを持っているので,それに合わせてリンカスクリプト(ld.scr)を修正する.
OUTPUT_FORMAT("elf32-avr")
OUTPUT_ARCH(avr)
ENTRY("start")
MEMORY
{
romall(rx) : o = 0x000000, l = 0x008000 // ROM全体で32K(0x8000)
vectors(r) : o = 0x000000, l = 0x000100 // 先頭には割り込みベクタを配置.256Bは少し多め
rom(rx) : o = 0x000100, l = 0x007800 - 0x000100 // 残りのROM.offsetは0x100
bootloader(r) : o = 0x007800, l = 0x008000 - 0x007800 // (1)
ramall(rwx) : o = 0x800000, l = 0x000800 // RAM全体で2K(0x800)
registers(rw) : o = 0x800000, l = 0x000100 // 先頭はレジスタやIOレジスタが配置されている
ram(rwx) : o = 0x800100, l = 0x800600 - 0x800100 // 残りのRAM全体
bootstack(rw) : o = 0x8007fc, l = 0x000000 // (2)
}
SECTIONS
{
.vectors : {
vector.o(.text)
} > vectors // (3)
.text : {
_text_start = . ;
*(.text)
_etext = . ;
} > rom // (3)
.rodata : {
_rodata_start = . ;
*(.strings)
*(.rodata)
*(.rodata.*)
_erodata = . ;
} >ram AT> rom // (4)
.data : {
_data_start = . ;
*(.data)
_edata = . ;
} > ram AT> rom // (5)
.bss : {
_bss_start = . ;
*(.bss)
*(COMMON)
_ebss = . ;
} > ram AT> rom // (5)
. = ALIGN(4);
_end = . ;
.bootstack : {
_bootstack = .;
} > bootstack // (6)
}
- Arduino Unoのatmega328pは,ヒューズビットの設定によってブートローダが0x7800から書き込まれているので,そこを潰さないように保護する.
- これまでのスタック領域を踏襲して,bootstack領域を作っておく.ここには単にシンボルを配置するのみ.
- ここは割り込みベクタを配置する.割り込みベクタはアセンブリ言語で定義され(vector.s),アセンブル後それらはプログラムとして保存されている(データじゃない!).よって,vector.oのテキストセグメント(
.text
)を,vectorsという領域に配置する,という意味になる.その後,textセクションを生成し,残りすべてのファイル(vectors.o)のテキストセグメントをROMに配置する. - READONLYデータをRAMに配置する(
>ram
).その上で,.rodataセクションの物理アドレスはROMに配置される(AT>rom
).これは,VA != PA処理に当たる(※1). - 初期値のあるデータ(RW可能)をRAMに配置する(
ram
).それ以外はREADONLYと同じく,.dataセクションの物理アドレスをROMに配置する..bss(初期値のないデータの変数の値のための領域)も同様 - スタック領域にシンボルを配置(
_bootstack
)
※1: そもそもRODATAは変更しないので,ROMに配置して置けばいい気がするが,ROMに配置するとシリアルで出力するときに失敗例のように文字化けする.謎
謎が解けた.AVRはROMとRAMが別のメモリになっており,ROMを読む命令とRAMを読む命令が異なっている.そして,gccはRAMのデータを読むようなコードを出力するので,RAMにデータをコピーしておかないと文字化けする.
データのコピー
上記の4で,VA!=PAにしているため,で.rodate, .data. .bssはプログラムがアクセスするときはRAMのアドレスにアクセスするようにリンクされるが,物理的にはROMにデータが配置される.よって,このまま実行すると,例えばdataを読みにいってもRAMにはそのデータがない,ということになる.
そのため,OSが起動したらすぐにROMからRAMの適切な位置にデータをコピーしておかないといけない.
どこからどこに,コピーするかといえば,シンボルで書くと,_etext
から_rodata_start
にコピーする.そして,そのサイズは_edata
- _rodata_start
となる.何故かというと,rodata, dataはtextが配置された直後から配置されるため,開始アドレスは_etext
,コピー先はRAMの先頭に配置した_rodata_start
となるため.
また,BSSに関しても0で初期化する.
start:
ldi r28, lo8(_bootstack) // スタックポインタの設定. ldiは即値を読み込む命令
ldi r29, hi8(_bootstack) // 0x3d, 0x3eはSPのアドレス
out 0x3d, r28
out 0x3e, r29
data_copy_set:
eor r1, r1 // r1を0にクリア
/* length */
ldi r18, lo8(_rodata_start) // _rodata_startのアドレスを読み込む
ldi r19, hi8(_rodata_start)
ldi r20, lo8(_edata) // _edataを読み込む
ldi r21, hi8(_edata)
sub r20, r18 // _edata - _rodata_startの実行
sbc r21, r19 // コピーするデータサイズがr20, r21に入る
/* src */
ldi r30, lo8(_etext) // コピー元のアドレスをr30, r31に設定
ldi r31, hi8(_etext)
/* dst */
ldi r26, lo8(_rodata_start) // コピー先のアドレスをr26, r27に設定
ldi r27, hi8(_rodata_start)
data_copy:
cp r1, r20
cpc r1, r21
brge bss_clear_set // コピーサイズが0になったらbss_clear_setにジャンプ
lpm r24, Z+ // Zレジスタからr24にデータを読みインクリメント.Zはr30, r31を連結したレジスタ
st X+, r24 // r24の値をXレジスタにコピーし,Xをインクリメント.Xはr26, r27を連結したレジスタ
subi r20, 0x01 // コピーサイズを1減らす
sbci r21, 0x00
rjmp data_copy
(略)
call_main:
call main // mainにジャンプ
アセンブリ言語でコピーしているが,C言語で記述した関数でコピーすると何故かうまくいかない(文字化け).
void cpdata() {
extern long _rodata_start, _edata, _etext;
extern long _bss_start, _ebss;
memcpy(&_rodata_start, &_etext, (unsigned long)&_edata - (unsigned long)&_rodata_start);
memset(&_bss_start, 0, (unsigned long)&_ebss - (unsigned long)&_bss_start);
}
謎..
これも解決で,C言語で書くとRAMからデータを読むようなコードを出力するため.よって,ここはアセンブリ言語で書くしかない
実行
再度,以下のmainを実行してみる
volatile int value = 10;
int main (void)
{
int i;
extern char _bootstack;
static char *ts = &_bootstack;
// cpdata();
serial_init();
char str1[] = {'a','b','\n', '\0'};
char *s = "hello world\n";
puts(s);
puts(str1);
putxval(value);
putxval((unsigned long)ts);
for (i = 0; i < 5; ++i) {
puts(s);
}
return 0;
}
成功してる!
この実装は,以下のコマンドで試すことができる.
>git clone https://github.com/hiro4669/iosv.git
>cd iosv
>git branch memmap_ver1 origin/memmap_ver1
>git checkout memmap_ver1
>cd mammap
>make
>make write