SNSへはこちら

RL78/G10はgccでの開発が不便だという話(長編記事)

以前から苦労して RL78 マイコンの開発を行ってきています。本記事では RL78 マイコンは、場合によっては gcc での開発が不可ということを、根拠を述べつつ記載したいと思います。今回はタイトルにもあるように長編記事になります。

注意

この記事は、RL78/G10 は gcc での開発が厳しいということを示す記事です。その他の RL78 シリーズでは不具合を確認していません。

また、ルネサス社の出している純正コンパイラ CC-RL や GNU Tools の出す gcc では動作ができるものと考えられます。あくまで gcc を本家からダウンロードし、イチからビルドしたものでは利用が厳しいことを示すだけですので、勘違いの無いようご留意ください。

確認したマイコン

僕が購入したのは2つで、1つは RL78/G10 というシリーズのもの、もう1つは RL78/G1C というものです。それぞれ秋月で購入したものを以下に示します。

電子部品,通販,販売,半導体,IC,LED,マイコン,電子工作R5F10Y16ASP使用 RL78マイコンモジュール秋月電子通商 電子部品通信販売
電子部品,通販,販売,半導体,IC,マイコン,電子工作RL78マイコン R5F10JBCAFP秋月電子通商 電子部品通信販売

で、色々とやってみた結果、数ある RL78 マイコンのシリーズにおいて、RL78/G10 シリーズは gcc でまともに開発ができないのではないかという結論に至りました。本記事ではその根拠と実情についてお話いたします。

厳しい点

何がうまく行かないのかを端的に示しますと、gcc を利用した際、ポインタが不正なアドレスを示すということです。これには RL78 特有のアドレッシング方法と、ミラー領域が大きく関わっているものだと考えます。

アドレッシングの仕様

まずこれは大切なことですが、RL78 は全メモリ空間のうち、実質上位の 0xF0000 〜 0xFFFFF までしか参照できないマイコンです。この制約は、このマイコンのビット幅は 16bit であり、比較的大容量のメモリを積むために、メモリアドレスに無理をさせている(?)ことから来ています。命令セットのマニュアルを見てもらえば分かりますが、関数コールや分岐を除いた基本的なアドレッシングは、これらの範囲外にアクセスするためにはひと手間必要になります。例えば以下の例を考えます。

movw hl, #0x10 ; hlレジスタに 0x0010(アドレス値とする)を格納
movw ax, [hl] ; 「0x0010から値を取ってくる」と思いきや...

普通に見たら、「あ〜はいはい、ax レジスタに 0x0010 番地から値を持ってくるのね」となりそうですが、実は違います。このマイコンは先程述べたように、基本的に 0xF0000 〜 0xFFFFF までしか参照できないのです。
ではこの場合はどうなるかというと、取ってくるアドレスは 0x0010 ではなく、0xF0010 になります。そう、勝手に F が追加されるのです。

では希望通りに 0x00010 から値を取ってくるにはどうするのか?それには ES レジスタを使う必要があります。ES レジスタによって、アドレス幅を 20bit (16 bit + 4bit) に拡張することができ、それを用いた専用の命令を実行することで実現されます。

movw hl, #0x10 ; hlレジスタに 0x0010(アドレス値とする)を格納
mov es, #0x00 ; esレジスタに0を入れる
movw ax, es:[hl] ; hlレジスタの先頭にesレジスタの中身をつけてアクセス

こうすると、es == 0x0 であり hl == 0x0010 なので、3つ目の命令で 0x0 0010 にアクセスすることができます。解決解決...

だと良かったんですが、これはすなわち、アドレス 0xF0000 以降とそれ未満では、アクセス処理が異なるということを示しています。
0xF0000 以降では単純に先頭の F を無視してアドレッシングを行えば良いのですが、それ以外では es レジスタに値の代入という作業が伴います。これは大問題です。なぜなら、C言語においてポインタを渡された関数側では処理が異なってしまうからです。

Cコンパイラにおける処理の違いの実装

コンパイラの方では、0xF0000 以降のメモリ領域を near、それ以前を far とし、後者の変数(定数)宣言では __far を付けることとして(far ポインタ)、処理の違いを陽に表しています。
far ポインタでは es レジスタを利用した処理を行ってくれているようで、メモリのアラインメントの都合上、そのポインタは 32bit となっています(通常の near では 16bit)。つまり sizeof(char *) は 2 を返し、sizeof(char __far *) は 4 を返します。

これに関する動作を実際に確認してみましょう。以下の2つの関数を用意し、コンパイル後の逆アセンブリを行うことで比較してみます。

void f(int *a) {
    *a = 10;
}

void g(int __far *a) {
    *a = 10;
}

f() は near ポインタ、g() は far ポインタを引数にとっています。これに対応する機械語は以下のようです。

Disassembly of section .text:

00000000 <_f>:
   0:   a8 04                           movw    ax, [sp+4]
   2:   12                              movw    bc, ax
   3:   30 0a 00                        movw    ax, #10
   6:   78 00 00                        movw    0[bc], ax
   9:   d7                              ret

0000000a <_g>:
   a:   a8 04                           movw    ax, [sp+4]
   c:   bd c8                           movw    0xffec8, ax
   e:   a8 06                           movw    ax, [sp+6]
  10:   bd ca                           movw    0xffeca, ax
  12:   ad c8                           movw    ax, 0xffec8
  14:   bd cc                           movw    0xffecc, ax
  16:   ad ca                           movw    ax, 0xffeca
  18:   60                              mov a, x
  19:   9e fd                           mov es, a
  1b:   da cc                           movw    bc, 0xffecc
  1d:   30 0a 00                        movw    ax, #10
  20:   11 78 00 00                     movw    es:0[bc], ax
  24:   d7                              ret

何やら 0xffec8 とかが謎ですが、それは置いておきましょう。f() では単純に示されたアドレスに代入をしているだけですが、g() では色々加工して、最終的に es レジスタを利用してアクセスを行っているようです

ということで、near と far で まるっきり処理が異なってしまっている 事が分かります。ここまで読んでくれた方は分かると思いますが、すべてのポインタに __far を指定しておけば near 領域でもアクセスできます。
では何故処理を分けているかというと、ズバリ メモリ資源の節約のため だと考えます。毎回アドレッシングを 32bit でやっていては ROM がいっぱいになってしまうよ、ということなのでしょう。これが大きな仇になるわけですが。。。

メモリ空間とミラー領域

で、これがどう影響するのかという本題に入っていきます。

メモリ空間の外観

まずは RL78 の基本的なメモリ空間の図を御覧ください。

0xF 0000 の上と下を見る限り、プログラムが入るコードフラッシュメモリは far であることが分かると思います。そう、ROM に書かれた定数値へのアクセスは面倒くさいのです。
もっと言うなら、ROM に書かれた定数値にアクセスするためには、__far が必須だということですね。これはまた、1度でも far 領域にある値のアドレスが渡されうる関数の仮引数には、必ず __far を指定しなければ正常に動作しないことも意味しています。

また、ミラー領域とかいう変なもののもあります。これが曲者です。

ミラー領域

この領域は、遠い領域にアクセスするのは面倒くさいから、実態にアクセスできるもう1つの窓口を near 領域に作ってやったよというものです。どうミラーされるのかはアーキテクチャの細部と、レジスタ設定によって異なるのでご覧頂きましょう。以下ではとりあえず MAA = 0 (デフォルト設定)のみを見てください。

S2コアのミラー領域について説明

不穏な S1 コアはスルーして、S2 と S3 コアを見てみると、ミラー先は例の F が付くか付かないかしか違いません。なので特に __far しなくても、勝手にコンパイラさんにアドレスを勘違いさせておけば済むんですね。以下のような感じで結果オーライになります

  1. 例として 0x0 5000 にアクセスしたい
  2. 関数にアドレス 0x0 5000 を渡す
  3. 関数側は __far が付いていないので 0xF 5000 として処理しようとする
  4. 0xF 5000 は丁度 0x0 5000 を指している
  5. 結果として、所望の 0x0 5000 にアクセスできた!結果オーライ!!

なるほどですね、良かったです。めでたしめでたし。

(毎回恒例ですが)いや、実はちょっと罠があって、この表記の下部にさらっとミラー領域となるはずのところに RAM とかデータフラッシュ領域とかあったら、その部分はミラーされないから、と書いてあります。

つまりはこういうことです。S2 コアで サイズが 0x2000 のデータフラッシュ領域が 0xF00000 から始まるとしましょう。すると 0xF0000 から 0xF2000 はデータフラッシュ領域となり、ミラーはされません。よって 0x00000 から 0x02000 はミラーされないことになります。

とするとですよ、0x00000 から 0x02000 に格納された定数値へのアクセスは __far じゃなきゃいけなくなるわけです。そんなのは面倒くさい。ということなので、思い切ってこの領域にはデータを書き込まない or 関数のコードのみを格納するようにする という処理を取るべきなわけです。gcc に付いてくるデフォルトのリンカスクリプトではこれを考慮してうまい感じに書かれています(長くなりすぎるのでこの話は別記事で)。

ということなので、S2 コアを採用している RL78/G1C では問題なく gcc で開発ができるわけです。

S0コアのミラー領域について説明(闇)

さて、続いて今回の本題である S1 コアを積んでいる RL78/G10 (闇)について見ていきましょう。先程の対応リストから情報を抜き出すと、こんな感じです↓

  • 0x000000x05EFF0xF80000xFDEFF へミラーしている

抜粋してもう一度いいますね。

  • 0x00000 を 0xF8000 へミラーしている

そう、下4桁のアドレスが、ミラー元とミラー先で一致しない のです。どうやらこれは上述のような、S2 コアとかでミラーされない領域があることへの反省を生かした作りのようですが、どうしてこうなった。
おかげで上のたまたまな対応ができません。というかたまたまで成功するほうが稀な気がする。

なので、現状このような奇妙なプログラムを書くことになります。UART での文字送信です。char * を引数に渡しています。

#include <rl78.h>

const char mes[] = "Hello"; // 0xF0000よりも下位(ROM)に配置される

void uart_send_str(char *); // これが問題の関数
int main(void){
    timer_init();
    uart_init();
    adc_init();
    while(1){
        uart_send_str(mes + 0x8000); // オフセットを無理やり作るw
        ms_wait(1000);
    }
    return 0;
}

こうすると、以下のような解釈でデータアクセスが完了しますw 仮に mes == 0x0 0000 としましょう。

  1. 関数に mes + 0x8000 == 0x08000 を渡す
  2. 呼ばれた関数がこのアドレスからデータを取ってこようとするが、これを 0xF8000 と解釈する
  3. 0xF8000 には 0x00000 がミラーされているので、問題なくデータにアクセスできる

こんな細工をすれば良いのですが、正直不便すぎるし、それ、本当にC言語で開発できていると言えるの?と思いますよね。だってC言語の規格上は 0x8000 加算なんてしなくて良いはずですから。僕はC言語で開発できているとは言えないんじゃないかな〜って思います。思います。

もっとこれを便利にするのなら、こんな関数を作るしか無いっていう話になってしまいますね...(動作未確認です)

// 脳死で関数に渡すポインタにはこれを被せればいい
// __far なんてなかった
#define ADJUST_PTR_FOR_ARGUMENT(p) \
    (sizeof(p) == 4) ? (p) : ( \
        ((p) <= 0x5eff) ? ((p) + 0x8000) : (p) \
    )

const char mes[] = "Hell";

uart_send_str(ADJUST_PTR_FOR_ARGUMENT(mes));

うーん、なんかいまいちですねえ...

そんな僕からこんな提案。

アセンブリすれば良いんじゃないかな

追記

SADDR 領域(ショート・ダイレクト・アドレッシング領域)はコンパイラが汎用レジスタ的に使うのでスタックに使わないほうが良さそうです↓
RL78のSADDR領域は使用を避けたほうが無難? - 101: RL78 - Forum - かふぇルネ - Renesas Rulz - Japan

参考サイト