ATtiny10 用プログラム (2)
今回は、gcc (Atmel AVR 8-bit GNU Toolchain 3.3.0.364) を使用した ATtiny10 用の C プログラムの話題です。
以下の話はバージョン 3.3.0.364 についてのもので、当然、バージョンが変われば状況も変化する可能性があります。
「米粒 AVR」こと ATtiny10 用の gcc を使った C プログラムでは、「グローバル変数」が使えない (機能しない) という噂を聞いていて、まさかと思ったのですが、本当でした。
しかもこれは、「バグ」と言うよりは、従来の AVR と ATtiny10 とでは、大きく変化しているアーキテクチャ/命令セットへの対応が十分ではないためと思われます。
「バグ」なら比較的簡単に直るはずですが、この「変化」をサポートするためには、コンパイラや関連のツール、ライブラリなどにかなり根本的な手直しが必要で、なかなか対応が進んでいないものと考えられます。
ATtiny10 の命令セットおよびアーキテクチャ上の主要な変更部分は
- 8 ビット汎用レジスタが 32 本 (R0 〜 R31) から 16 本 (R16 〜 R31) に半減された
- フラッシュ・メモリを実行中のプログラムから書き換えることができなくなった
- 外部からフラッシュ・メモリを書き換えるためのインターフェースが TPI になった
- プログラムが格納されるフラッシュ・メモリが、データ・メモリ空間の 0x4000 番地からにマッピングされ、プログラム・メモリ空間をアクセスするための専用命令 LPM/SPM は廃止された
などがあります。
最初の項目の、削減された R0 〜 R15 は、汎用レジスタといっても、即値オペランドの命令が使えないなどの制約がもともとあって、中間結果の保存などの用途が主でした。
現在のバージョンのツールチェーンでも、この項目へはきちんと対応していて、ないはずのレジスタにアクセスするコードを生成することはありません。
二番目の 2 ワードと、1 ワードの LDS/STS 命令の問題が、「グローバル変数が機能しない」ことの原因で、現在のバージョンでは、ATtiny10 には存在しない 2 ワード LDS/STS 命令を生成するため、グローバル変数へのアクセスが全くできていません。
また、これとは別に、現在のツールチェーンでは、
- .data セグメント、つまり初期値付きのグローバル変数の初期化がされない
- PROGMEM 属性でプログラム・メモリ領域に割り付けられた定数データのアクセスがうまくいかない
などの問題があります。
従来の AVR ではプログラム・メモリ空間と、データ・メモリ空間は完全に分離されており、プログラム・メモリ空間に書かれた値をデータとしてアクセスするためには専用命令の LPM を使う必要がありました。
ATtiny10 では、プログラム・メモリはデータ・メモリの空間の 0x4000 番地以降にマッピングされて、通常のデータ・アクセスと同じ命令でアクセスするようになりました。
また、通常は、初期値付き変数の初期値データは、プログラム・メモリの空間に置かれ、C の main() 関数の実行前にスタートアップ (C ランタイム) ・ルーチンによって、対応するデータ・メモリ領域にコピーされます。
現在のツールチェーンでは、このプログラム・メモリのマッピングに対応していないので、PROGMEM を正常にアクセスできませんし、変数初期化のルーチン自体も生成されません。
これらの問題を正面から解決するには、C コンパイラ、アセンブラ、C ライブラリ、バイナリ・ユーティリティーの objdump (逆アセンブラ) などの広範囲のツールについて変更を加える必要があります。
今後のツールチェーンのバージョン・アップで、どの方向へ行くのか分かりませんし、自前で広範囲のソフトウェアにパッチを当てるようなスキルもありませんから、とりあえずグローバル変数が機能するような対症療法的な対応をしたいと思います。
コンパイラが吐き出す (間違った) 2 バイト LDS/STS 命令を、ATtiny10 が実行できる (正しい) 1 バイト LDS/STS 命令に書き直せれば、グローバル変数は機能するようになります。
1 バイト → 2 バイトの変更では、バイト数が増えるので簡単には行えませんが、2 バイト → 1 バイトではバイト数が減るので、減った 1 バイトのスペースに nop 命令を埋めてやれば、効率はともかく、動作に支障はなくなります。
この考えで、出力された HEX ファイルを読み込み、2 バイト LDS/STS 命令を探し、1 バイト LDS/STS 命令に変換した結果の HEX ファイルを出力するプログラムを作りました。
この、変換ずみの HEX ファイルを ATtiny10 に書き込めば、グローバル変数が有効に機能します。
前述のように、変数の初期値データと、PROGMEM によるデータはプログラム領域に存在するので、偶然そのデータが 2 バイト LDS/STS 命令のパターンと一致して、誤変換される可能性はあります。
ただし、現状のツールチェインでは PROGMEM も初期値化も正しく機能しないので事実上使えず、誤変換の心配はありません。
しかし、アセンブラで記述したルーチンでは、「自前」でプログラム領域のデータを持つことができ、誤変換の対象にならないかどうか注意する必要があります。
Windows 上の Cygwin の gcc で書いた変換プログラムを下に示します。
特に環境には依存しないはずです。
/******************************************/ /* fix_lds : fix 2 word AVR LDS/STS */ /* to 1 word format (AVR8L inst set) */ /* */ /* 2011/12/19: Created by pcm1723 */ /******************************************/ #include <stdio.h> #include <stdint.h> #include <string.h> #define RAM_START (0x0040) #define RAM_END (0x005F) #define OP_MASK (0xFD0F) #define LDS32_STS32_PATTERN (0x9100) #define LDS16_STS16_PATTERN (0xA000) #define VERBOSE 1 typedef struct tag_rec_info_t { uint16_t adrs, len; } rec_info_t; uint8_t rom[1026]; rec_info_t rec_info[1024]; uint16_t lc = 0; // location counter uint16_t beg_adrs = 0xffff; // beginning address uint16_t end_adrs = 0x0000; // ending address uint16_t rec_len; // record length uint16_t rec_type; // record type uint16_t csum; // check sum int s_len, s_pos; int num_rec_line = 0; // input line buffer unsigned char line_buf[1 + (5 + 255)*2 + 2 + 1]; int s_eol( void ) { return( s_pos >= s_len ); } // int s_eol() unsigned char s_getc( void ) { if (!s_eol()) { return(line_buf[s_pos++]); } else { return(' '); } } // char s_getc() unsigned char skipblank( void ) { char c; c = ' '; do { c = s_getc(); } while ((!s_eol()) & (isspace(c))); return(c); } int gethex1( void ) { int i, c; c = skipblank(); i = (('A' <= c) ? (c - 'A' + 10) : (c - '0')); return(0x0f & i); } // int gethex1() int gethex2( void ) { int h2; h2 = (gethex1() << 4 ) + gethex1(); csum += h2; return(h2); } // int gethex2() int search_colon( void ) { int c; do { c = skipblank(); } while ((!s_eol()) & (':' != c)); return(c); } int read_rec( void ) { int i; s_len = strlen(line_buf); s_pos = 0; if (search_colon()) { // colon ':' found csum = 0; rec_len = gethex2(); // record length lc = 0x3ff & ((gethex2() << 8) + gethex2()); // address rec_type = gethex2(); // record type if (0 == rec_type) { // normal data record rec_info[num_rec_line].adrs = lc; rec_info[num_rec_line].len = rec_len; num_rec_line++; for (i = 0; i < rec_len; i++) { rom[lc++] = gethex2(); // read data } // for (i = 0; ... gethex2(); // read check sum byte return(0 != (0xff & csum)); // check sum error / no error } else { return(1); // unsupported record type } // if (0 == rec_type) ... } else { // colon ':' not found return(1); // invalid } // if } // int read_rec() uint16_t LDS_STS_convert(uint16_t op1, uint16_t op2) { uint16_t k6, k5, k4, k3_0, ldst; ldst = 0x01 & (op1 >> 9); // bit9 LDS/STS bit op1 &= 0x00F0; // extract r3..r0 bit k3_0 = 0x0F & op2; // k3..k0 bit k4 = 0x01 & (op2 >> 4); // k4 bit k5 = 0x01 & (op2 >> 5); // k5 bit k6 = 0x01 & (op2 >> 6); // k6 bit op1 |= LDS16_STS16_PATTERN; // LDS/STS(16) bit pattern op1 |= k3_0; // k3..k0 op1 |= (k4 << 9); // k4 bit at b9 op1 |= (k5 <<10); // k5 bit at b10 op1 |= (k6 << 8); // k6 bit at b8 op1 |= (ldst << 11); // LDS/STS bit return(op1); } // uint16_t LDS_STS_convert() void puthex1(uint16_t n) { static const char hstr[] = "0123456789ABCDEF"; fprintf(stdout,"%c", hstr[0x0f & n]); } // void puthex1() void puthex2(uint16_t n) { puthex1(n >> 4); puthex1(n); csum += (0xff & n); } // void puthex2() void puthex4(uint16_t n) { puthex2(n >> 8); puthex2(n); } // void puthex4() void write_rec(int i) { int k; fprintf(stdout,":"); // start of intel hex format rec_len = rec_info[i].len; // record length lc = rec_info[i].adrs; // location address csum = 0; // clear check sum puthex2(rec_len); // record length byte puthex4(lc); // location address byte puthex2(0x00); // record type = data for (k = 0; k < rec_len; k++) { puthex2(rom[lc++]); // data byte } // for (k = 0; ... puthex2(-csum); // check sum byte fprintf(stdout,"\r\xa"); } // void write_rec() int main() { uint16_t op1, op2; int i; // read a line from standard input until end of file while (NULL != fgets(line_buf, sizeof(line_buf), stdin)) { if (read_rec()) { // invalid record // fprintf(stdout, "%s", line_buf); // echo back to output } // if } // while (NULL != ... if (0 < num_rec_line) { // valid data line found // scan ROM contents for (lc = 0; lc < 1024; lc += 2) { // word access op1 = (rom[lc+1] << 8) + rom[lc]; // OP code word if (LDS32_STS32_PATTERN == (OP_MASK & op1)) { // LDS(32) / STS(32) found lc += 2; // advance to next word op2 = (rom[lc+1] << 8) + rom[lc]; // offset word if ((RAM_START <= op2) & (RAM_END >= op2)) { // valid offset if (VERBOSE) fprintf(stderr,"# %.4x: %.4x %.4x --> ", lc-2, op1, op2); op1 = LDS_STS_convert(op1, op2); // convert 2 word OP to 1 word if (VERBOSE) fprintf(stderr,"%.4x\r\xa", op1); rom[lc+1] = 0; rom[lc] = 0; // fill by NOP rom[lc-1] = (op1 >> 8); // upper byte of fixed LDS/STS(16) rom[lc-2] = 0xff & op1; // lower byte of fixed LDS/STS(16) } // if ((RAM_START ... } // if (LDS32_ ... } // for (lc = 0; ... for (i = 0; i < num_rec_line; i++) { // output hex record write_rec( i ); } // for (i = 0; ... fprintf(stdout,"%s\r\xa", ":00000001FF"); // end record } // if (0 < num_rec_line) ... return(0); } // int main()
このプログラムは「フィルタ」として動作し、標準入力 (stdin) から「intel HEX フォーマット」のデータを入力し、LDS/STS 命令を変換した後、同じ形式で標準出力 (stdout) に出力します。
変換した LDS/STS 命令のアドレスと変換の内容を標準エラー出力 (stderr) に出力しますが、ソースの「VERBOSE」の定義値を 0 に変更してコンパイルすれば、その出力を抑制できます。
次回は、ATtiny10 のプログラム例と、その HEX ファイルの変換結果などについて説明します。