LoginSignup
16
7

SDCCを使ってCH559の開発をした際のハマりポイント

Last updated at Posted at 2021-05-30

概要

USBデバイス、ホストどちらにもなれる$1程度で入手可能なCH559が便利そうなので色々と実験してます。これは実験過程でハマったポイントのメモ。今後も増えていくかも。

メリット

今のところ感じているメリットは以下のような感じ。

  • オシレータは内蔵しており、単体で48MHzで動作可能。PLL等の設定でもう少し上げられるけど、USB扱う時に都合良いのは48MHz
  • 8051 (MCS51) 互換のコアで比較的資料も豊富
  • USBデバイス、または2ポートまで接続可能なUSBホストになれる
  • UART1がRS485もサポート(ただしUSBホスト機能と同時には使えない)
  • Flashの一部にデータを保存してEEPROMのように使える
  • 英語の資料が入手可能(中国語だけよりはマシ、という意味でメリット)
  • 5V電源、3.3V動作、5Vトレラント(出力は3.3Vです、5V入れたら5Vで動いて欲しかった)
  • GPIOの数がかなり豊富(電源ピンが少ないので同じLQFP48でも他のマイコンより多め)

ハマりポイント

リセット

極性が一般的な負極性ではない。RSTピンは正論理で内部ではpull-downされている。リセット回路を組まないならそのままで問題ないが、組む場合はリセットボタンでHIGHになるように組む必要がある

Bootloader

マニュアルでは一切記載が見当たらないが、P4.6/XI/SCS_ピンをGNDに落として電源を入れるとBootloaderが起動してUSB/SPIからのファームウェアダウンロードモードになる。通常の開発フローでBootloaderが消えることはないので安心して自前ファームをダウンロードして大丈夫。
起動時にはBootloaderが走りP4.6が押されてなければユーザープログラムの実行を開始する。リセット時にはユーザープログラムが直接走るので、必要なら二回目の起動時には0xF400にジャンプすればBootloaderに処理が渡せて便利。RESET_KEEPというPON時に0でリセット時は保持されるレジスタがあるので、これを使って起動回数を判定するコードが組める。

追記:P4.6ピンを使う事に関しては公式ツールのUIでも確認できた。チップの設定を書き換えることでP5.1ピンに切り替えることもできるようだ。

ファームウェア書き込み

公式GUIツールのみを使っていれば問題ないが、開発を円滑にしようとCUIツールを探すなら注意が必要。chflasher.pyが一番最初に見つかるけどバグってます。ある程度大きなサイズになってくると正しく書き込めません。verifyも同時にバグってるので書き込み失敗にすら気づけない。
これをベースに改良されたCH55x_python_flasherというのがあるので、こちらがオススメ。
BTVER:02.31、BTVER:02.40にてCH55x_python_flasherで書き込んだ物が公式ツールのverifyで終盤チェックサムエラーを出している。不安があったら公式ツールで確認した方が良いかも。ただ、少なくとも自分はずっとその状態で作業してて実害が出ていないので、たぶん末尾のパディングか何かで問題が出てるだけで実質影響ない気がしている。
追記:29,736バイト以上になると書き込み失敗するみたいなので、それより大きな転送は現状では公式ツールしかないかも。
追記:直してpull requestを送りました。60KBまで正しく書き込めるバージョンがこちらから利用できます。 => マージされました!

各種コンパイルモード

MCS51互換CPUはそれぞれ実装してるメモリサイズとか異なるため、コンパイル・リンク時にはそれらの情報を渡してやる必要がある。基本的には以下のフラグで良いと思う。
-V -mmcs51 --model-large --xram-size 0x1800 --xram-loc 0x0000 --code-size 0xf000 --stack-auto
--model-*はいくつか種類があるが、実際に使うのは--model-small--model-largeだと思う。smallの場合はxRAMを使わない。つまり0x00-0xFFまでの256Bだけでレジスタバンク、ヒープ、スタックを確保することになる。Lチカみたいな1ファイルで完結する程度の小さいプログラムのみに留めるべき。効率は一番良いはずだけど、48MHzでそこまで速度が問題になる事もないと思うし、そういう時はそこだけアセンブリでモード無視して書いたほうが良いと思う。largeはヒープにxRAMを使う。CH559なら6KBとかあるのでかなり安心。基本はこっちだと思う。
--stack-autoは関数ポインタ使わないと違いに気づかずに落とし穴になりそう。このフラグを使わないと関数がリエントラントにならないので注意を要する。ようはauto変数がstackに配置されない。一方で関数ポインタを定義した場合はstack-auto時のABIが適用されるため、--stack-autoなしで関数ポインタを使うとsignature不一致でハマります。でも本当にハマるのは再入不能って事に気づかずに使い続けた場合な気はする。自分はOOPする関係で速攻--stack-autoを入れたのでそっちではハマってない。

スタック

伸びる向きが通常と反対。プッシュするとインクリメント。通常は気にしたりハマったりする事はないけど、動作が不安定になった時にコンパイル設定とか色々と見直す際に知らないと困る。
デフォルト設定だとスタックは0x21から始まり0xFFで終わり。そして初期値が0x21。うっかり0xFFを指定するといきなり0x00に戻ってレジスタ破壊して(終)。

SFRとSBIT

MCS51はI/Oがメモリマップかつアドレッシングモードによって多重化されているため、特殊なマクロを使ってI/Oレジスタを定義してコンパイラに使うべきアドレッシングモードを教えてあげる必要がある。その時に使うのがSFRマクロ。またMCS51では一部の領域のレジスタに関してのみビット単位のアドレッシングモードを持っている。これを定義するのがSBIT。詳しく知らずにビットアクセス便利だーってSBITで定義しまくると対応してない領域でもコンパイルが通ってしまい実際のアクセスは失敗するので頭を抱えることになるので注意。
CH559ではさらにxSFRと呼んでいるレジスタ郡が存在しているが、こちらは--mode-largeなら通常のメモリ空間同様にアクセスすれば良い。

エンディアン

Keil C51が公式サンプルが想定するコンパイラらしいが、このコンパイラはBig Endianでコンパイルする。一方でSDCCはLittle Endian。そしてCH559のレジスタ群で16bitのものはBig Endianだったりする。この問題があるため、16bitレジスタには上位と下位を分けて8bitアクセスで値を設定する必要がある。例えばUEP0_DMA(Endpoint0が使うDMA転送先にメモリアドレス)はuint16_t volatileとしてアクセスしてはいけない。UEP0_DMA_HとUEP0_DMA_Lに分けて設定する。

メモリアライメント

DMAアドレスは偶数アドレスである必要があるのだが、どうやらSDCCではalignを指定して変数を配置する事ができない。自分の解決策としては、とりあえず1B大きい空間を確保しておき、そこへのポインタを取得、最下位ビットが立っていたらアドレスをインクリメントする、という方法を取った。

static uint8_t _ep0_buffer[10 + 1];
static uint8_t* ep0_buffer = _ep0_buffer;

void init (void) {
  if ((uint16_t)ep0_buffer & 1)
    ep0_buffer++;
}

割り込みハンドラ

void interrupt(void) __interrupt(割り込み番号) __using(レジスタバンク番号) {
  ...
}

といった形で割り込みハンドラを記述できる。__usingは省略可能で省略時には0が使われる。レジスタバンクがなんなのかはMCS51のアーキテクチャを知る必要があるので、アセンブリには関わらずCより上の世界で暮らしたい人は、小さなハンドラなら0、大きめのハンドラなら1を選べば効率的なコードが吐かれる、くらいの理解で良いと思う。
割り込みハンドラはどこに記述しても良いのだが、mainを記述したソースファイル内にプロトタイプ宣言が存在しないと割り込みベクタが正しく生成されないので注意が必要。コンパイル・リンク時にワーニングの類は一切出ないので地雷度が高い。割り込み有効で一撃必殺。

キャスト

なんでかわからないけど、以下の2通りのキャストで挙動に違いが発生する。厳密な発生条件は精査する必要があるかも。

uint8_t buffer[1024];
struct Foo* foo1 = (struct Foo*)&buffer[8];
struct Foo* foo2 = (struct Foo*)(buffer + 8);

foo2は期待通りなんだけど、foo1は謎の場所を指す。もしかしたらCの仕様がそれを許してるのかな?とか思ったけど最初の1バイトからして値が違うので「これはbuffer[8]というuint8_tへのポインタであって、配列など知らん」っていう解釈違いでもなさそう。

コンパイルエラーにすらならず化けた値が見えるので、既存のコードを持ち込む際には注意が必要。

また、関数に渡す際にキャストができない。

Foo((struct Bar*)buffer);

とか

struct Bar* bar = (struct Bar*)buffer;
Foo(bar);

ってやると構造体の型が違うと怒られてコンパイルが通らない。---model-large--stack-autoと関係してそうなんだけど、ちょっと理解しきれてない。しかたないので引数を呼び出し元の型に変更して、呼ばれる関数内でキャストしてやると通る。けど辛い。

void Foo(uint8_t* buffer) {
  struct Bar* bar = (struct Bar*)buffer;
  bar->...
}
Foo(buffer);

Flash

メモリ領域内にData Flashと呼ばれている空間があり、ここに永続化させたいデータを保存できる。ブロック単位で消して書く時はbitを0に落とすことしかできないっていう通常の手順で利用するが、データ領域は1ブロックしかないため、安全のために新しいデータ書いてから古いのを消してってやろうとすると少し用が足りない。ただ、プログラム用の領域がわりと潤沢にあるので、プログラム後半の余った領域を使ってやれば良いと思う。うっかりはみ出さないように--code-sizeでプログラム用の領域を制限した上で使うのが安全。

書き込み手順はデータシートの通りなので良いとして、読み込む際には注意が必要。MCS51はハーバードアーキテクチャだが、Data Flashもコード用Flashと同様にコード側の空間に位置しているため、通常のポインタで一生懸命読もうとしても別のとこを読んでしまう。

__code uint8_t* flash_data = (__code uint8_t*)0xf000;

などとしてコード領域だと思って読むべし。プログラム後半の領域も当然同じコード領域。

static

関数内でstaticで宣言してもスタック内に変数が確保されてしまう気がする。
少なくとも、値が壊れまくる現象に遭遇してるので、素直に外で確保した方が無難。

Errataと思われるもの

UART1 pin mode 2

SER1_IERからpin mode 2を設定して、RXD1/TXD1にP2.6/P2.7をアサインしようとした場合、おそらく正しく動作しない。RXD1側しか試していないが、ゴミを数十バイト読みだした後に何も読まなくなる。P4.0では正しく動くコードをこの設定だけ変えてP2.6で試して駄目だったのでプログラム的な問題ではないと思う。自作ボードでも評価ボードでも同様の問題が発生しているので、基板設計の問題やチップの壊れでもないはず。

追記:解決しました。データシートでも説明されてたんですがP4.0だと動いてたので見落とし。本来は RS485EN = bUH1_DISABLE & ~ (bXBUS_CS_OE & ~ bXBUS_AL_OE | bALE_CLK_EN) でRS485ENを0にするように何かしらの設定を入れておかないとRS485モードになってしまう。自分は普段USBを2ポートとも使ってたのでこの式はfalseだったんですが、使わないなら他の設定でRS485を殺さなければいけなかった。この式自体がワークアラウンド的な空気があるのですが、とりあえずbALE_CLK_ENを関係ねーじゃんと言いながら立てるのが大人の対応っぽいです……GPIOの割り込みでRXD1を使う時にも同じ問題が起きます。

USB割り込み要因のリセット

全てのレジスタに言える事かわからないけど。少なくともUSB_INT_FGに関してはDirect bit address clear or write 1 to clear. と説明されてるけど、ビットアクセスで0クリアした方が良い。1を書いた場合、後続の割り込みが飛ばされる事がある気がする。自分の場合、USB_GET_STATUSを受けた後のUIF_TRANSFER=1では次のUSB_SET_ADDRESSで割り込みが発生したけど、ここでもう一度UIF_TRANSFER=1をやると、その直後に発生するはずのINパケットで割り込みが発生しなかった。もしかしたら1を書いた場合はキューに入ってる要因全てがクリアされちゃう仕様かも。サンプルが初期化時には1を書いてリセット、動作中の割り込み要因クリアでは0を書いてるので、その説明なら辻褄が合う。

追記:それでも割り込みが飛ばされる事があった。USB_CTRLにbUC_INT_BUSYを設定した場合、UIF_TRANSFERが1の間に次のリクエストが来たら、NAKでBUSY応答してくれるはずなのだが、どうもNAK応答せずにキューに積んでる気がする。そしてUIF_TRANSFERのクリアと同時にキューの割り込みもクリア。割り込み直後すぐにUIF_TRANSFERをクリアする事でクリアせずに積まれた割り込みを消化できるが、その場合のUSB_INT_STが壊れないかは怪しい。ので、割り込みハンドラではUSB_INT_STを退避して即UIF_TRANSFERをクリアするのが一番信頼できる方法に思える。(これも動作が怪しくなるので処理が終わってからクリアするのが一番無難らしい)

その他の情報

Bootloaderのバージョン

手元で確認できているバージョンは、評価ボードに載っていたBTVER:02.31と、単体で購入したチップに載っていたBTVER:02.40。ファームウェアの書き込み方法については差異はなさそう。BTVER:02.31はVerifyの成約が甘いせいで工夫をすると総当りを使ったファームウェア読み出しが現実的な時間でできてしまうので注意。

SDCCのバージョンによる動作の差異

変更のあった厳密なバージョンは確認していないが、しばらく前のバージョンと執筆時での最新バージョン(4.3)は以下の点で動作が変更されてるようです。

  • 引数なしの関数でvoidが省略できなくなった(略すとプロトタイプ宣言がされていない、と怒られる)
  • 割り込みハンドラ記述の初期が変わり、割り込み番号とレジスタバンク番号は括弧で囲むようになった

他にもあるのかもしれませんが、自分のコードはこれらが原因で通らなくなって修正が必要でした。

chlib

自分で使う範囲で各種ペリフェラルをライブラリ化したchlibがGitHubにあげてあります。
すでに自作のいくつかのプロジェクトで使っているので、私のGitHubを漁ると具体的な利用例が見れるかと想います。USBデバイス、USBホスト、タイマー、Flash書き込み、PWM、UART、RS485、LEDアニメーションの管理など。

CH559Flasher.js

Chrome系のブラウザからファームウェアの書き込みを行うライブラリをCH559Flasher.jsとして公開しています。

ch559flasher

新たにRustでフラッシュ書き込みツールを書いてみました。開発時のファームウェアの書き込みにどうぞ。

命令ごとの消費サイクル数

高速なプロトコルをGPIOで実装する必要に迫られたため、実効サイクルを数えるようなインラインアセンブリによる最適化が必要になったのですが、どうにも言い値より遅い。クロック設定が間違ってるんじゃないかと疑うくらい。マニュアルには79%の命令は単一バイト単一サイクルって書いてあるんだけど79%のカウントの仕方が胡散臭いかも。

間違ってるところもあるかと思うけど、計測した命令のサイクルは以下のような感じ。前後の命令で実行サイクルが伸びる命令もあるみたいなのであくまでも参考に。他の命令についても、この表からだいたい傾向がわかるんじゃないかと思います。元の8051が12クロック×Nサイクルで動いてた事を考えると十分速いのかもしれないけど、Cortex M系のクロック数の感覚でいると若干驚きます。特に条件分岐はきつくて最適化し甲斐があります。

命令 消費サイクル数
NOP 1
INC 1
RR(RL) A 1
RRC(RLC) A 1
SETB bit 2
CLR bit 2
MOV direct, Rn 2
MOV A, Rn 2
MOV Rn, A 2
MOV A, @Ri 2
MOV(ADD) Rn, #data 4
SJMP rel 4
JNB AAC.bit, rel 8 (taken) / 6 (not taken)
JC(JNC) rel 4 (taken) / 2 (not taken)
DJNZ Rn, rel 6 (taken) / 4 (not taken)

まとめ

以上、どれもこれも初心者には一撃必殺の致命傷になりかねない地雷原の数々でした。

16
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
7