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) に半減された
  • 64 K バイトのアドレス空間にアクセス可能な 2 ワード LDS/STS 命令が廃止され、 128 バイト のみアクセス可能な 1 ワード LDS/STS 命令に置き換わった
  • 1 ワード LDS/STS 命令とビットパターンがカブる、ディスプレースメント付きレジスタ間接アドレシングの LD/ST 命令が廃止された
  • フラッシュ・メモリを実行中のプログラムから書き換えることができなくなった
  • 外部からフラッシュ・メモリを書き換えるためのインターフェースが 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 上の Cygwingcc で書いた変換プログラムを下に示します。
特に環境には依存しないはずです。

/******************************************/
/* 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 ファイルの変換結果などについて説明します。