2018年04月28日

自作キーボード (ツール編)

先に Windows でテストプログラムを作る話を書いた。今回は、ライタとかデバッグ環境をどうするかについて。

長らく AVR の電子工作から離れていたが、いろいろ状況が変わっている。

ちょっと調査。

ATtiny45を使ったUSB-I2Cブリッジ

    ここを見ると、
    USB-TINY-I2Cプロジェクト
    というのがあり、USBtiny に I2C マスター機能をくっつけたもののようだ。

    USBtiny には、ISP ライタ の機能があり 、avrdude でサポートされている。... となると、ファームウェアを 書き込んで そのまま I2C キーボード のテストまでできる 可能性が。

    その上、水晶を使わない V-USB ...
    EasyLogger
    というプロジェクトがあり、 合わせることも出来るという話。

    水晶を使わない V-USB というのは、相当に不安があるが、全部出来たあとに試してみるのも良いかも知れない。水晶なしで安定して動くのであれば、ATtiny88 版などにはありがたい。それはともかく、USB-TINY-I2C をツールとして採用してみようかと。

FabISP

    usbtinyisp.jpg

    実は、aliexpress で 見つけて これを購入してある ($1.62:ATtiny44 で検索) 。なんかコンパクトで、良さそうな感じ。micro usb だし、設計が最近のようだ。良く調べないで買ったのだが、調べてみると... FabISP というものであった。

    ファームウェアのソースコードもある。まずはこれを USB-TINY-I2C と合わせることを検討したい。

    FabUSB-arduino.jpg
    A000092, Arduino ISP - Development Kit is a Tiny AVR-ISP based on David Mellis Project FabISP
    Reference Design using part ATtiny44A-MUR by Arduino Corporation

    aliexpress のものは、USB コネクタを変えた派生版だが、ほかの派生版には Arduino のリファレンスデザインと言うものがある。ちょっと驚いた。



I2Cキーボードコントローラの tool 化

    FabISP は使えるようにしておくだけで、本命はこちら。FabISP は 5V I/O なので、5V のデバイスを扱うときは都合が良いが、それ以外ではあまり使いたくない。コントローラの基板が余るのだからデバッグツールにもしようと思う。

    基板を重ねて、ひとつはデバッグツールとして使う。t88 タイプは、それを意識して作ってある。最初に作ったものは、そうでもないのだが、少しの改修でなんとかなるだろう。

ISP-TINY-I2C (github)

    ちょっと見てみた。ファームウェアは、ATmega8 にも対応していて、I2C は USI や TWI を使うものではなく、PORT で実装してあるようだ。そこは良い。今は 水晶なしの Digispark にも正式に対応しているようだ。だが、プロトコル的には、ライタの機能とうまく合わないような印象。

      digispark.jpg
      Disispark

    USB Tiny プログラマは、こんな定義

    // Generic requests
    USBTINY_ECHO, // 0x00: echo test
    USBTINY_READ, // 0x01: read byte (wIndex:address)
    USBTINY_WRITE, // 0x02: write byte (wIndex:address, wValue:value)
    USBTINY_CLR, // 0x03: clear bit (wIndex:address, wValue:bitno)
    USBTINY_SET, // 0x04: set bit (wIndex:address, wValue:bitno)

    // Programming requests
    o USBTINY_POWERUP, // 0x05: apply power (wValue:SCK-period, wIndex:RESET)
    o USBTINY_POWERDOWN, // 0x06: remove power from chip
    o USBTINY_SPI, // 0x07: issue SPI command (wValue:c1c0, wIndex:c3c2)
    o USBTINY_POLL_BYTES, // 0x08: set poll bytes for write (wValue:p1p2)
    o USBTINY_FLASH_READ, // 0x09: read flash (wIndex:address)
    o USBTINY_FLASH_WRITE, // 0x0A: write flash (wIndex:address, wValue:timeout)
    o USBTINY_EEPROM_READ, // 0x0B: read eeprom (wIndex:address)
    o USBTINY_EEPROM_WRITE, // 0x0C: write eeprom (wIndex:address, wValue:timeout)

    o が付いたのは avrdude が使うもの。付いてないものは、avrdude では触らない。Generic requests というのは、LCD とかちょっとした装置の操作用で、CLR/SET は PORT 操作用(たぶん)。

    USB-TINY-I2C は、というと

    #define CMD_ECHO 0
    #define CMD_GET_FUNC 1
    #define CMD_SET_DELAY 2
    #define CMD_GET_STATUS 3

    #define CMD_I2C_IO 4
    #define CMD_I2C_BEGIN 1 // flag fo I2C_IO
    #define CMD_I2C_END 2 // flag fo I2C_IO

    USBtiny のプロトコル無視 -- CMD_I2C_IO が 4 〜 7 まで使用する。

    USBasp も一応確認しておこう。

USBasp

    #define USBASP_FUNC_CONNECT 1
    #define USBASP_FUNC_DISCONNECT 2
    #define USBASP_FUNC_TRANSMIT 3
    #define USBASP_FUNC_READFLASH 4
    #define USBASP_FUNC_ENABLEPROG 5
    #define USBASP_FUNC_WRITEFLASH 6
    #define USBASP_FUNC_READEEPROM 7
    #define USBASP_FUNC_WRITEEEPROM 8
    #define USBASP_FUNC_SETLONGADDRESS 9
    #define USBASP_FUNC_SETISPSCK 10
    #define USBASP_FUNC_TPI_CONNECT 11
    #define USBASP_FUNC_TPI_DISCONNECT 12
    #define USBASP_FUNC_TPI_RAWREAD 13
    #define USBASP_FUNC_TPI_RAWWRITE 14
    #define USBASP_FUNC_TPI_READBLOCK 15
    #define USBASP_FUNC_TPI_WRITEBLOCK 16
    #define USBASP_FUNC_GETCAPABILITIES 127

    しばらく見ない間に TPI に対応している。I2C などは拡張されていないようだ。
    それはともかく、プロトコルは、USBasp に組み入れた方が、作りやすい。コードを読んだことがあり何が出来るか分かってるし。

    I2C の機能は、WRITE パケットを出す、READ パケットを出して 読み取る。これで良いのである。そして、USBasp では、 200バイト までなら、ホストとやり取りできる。I2C では、デスクリプタのRead で 37 バイトの READ パケットを出せる必要がある。WRITE の方は 6 バイト?

    まぁ USBTinyも似たようなものではある。結局は usbFunctionRead , usnFunctionWrite で ブロック転送を行う。usbFunctionSetup のコマンドは、なにをどのように ブロック転送するかの指示のみ。

方針決定

    USB-TINY-I2C は I2C 操作だけをコピーするか参考にするかに留める。プロトコルを ライタと競合しない方法で独自に作る。せっかくだから、FabUSB のファームウェアをベースに、USBtiny に組み込むことを考えてみる。

    usbFunctionRead , usnFunctionWrite で ブロック転送をするとして、他に必要なのは ...

      USBTINY_I2C_CTRL, // 0x0D
      USBTINY_I2C_TRANSMIT, // 0x0E

    これだけの追加でやれないか? 
    TRANSMIT は、装置アドレス+RW と 転送バイト数(上限あり) 。
    CTRL は、I2C モードの開始(+転送速度) と終了。+ステータスREAD ... みたいな。

    Setup パケットの復習

      +---------------------------+
      | | Request |
      +---------------------------+
      | ValueL | ValueH |
      +---------------------------+
      | IndexL | IndexH |
      +---------------------------+
      | LengthL | LengthH |
      +---------------------------+

    USBasp や USBtiny は、Request でコマンドを切り分ける。 パラメータとして 6 バイトが自由に使え実際にそうしている。レスポンスについても (確か) 8 バイトまでは、usbFunctionSetup の戻り値で返せる。 FLASH の Read/Write には、usbFunctionRead/Write を使う。で、オリジナルの USBtiny は、独自の API で V-USB とは別物。FabISP では、そこを V-USB に変更してある。USB-TINY-I2C はというと、USBtiny と V-USB の両対応で #ifdef で切り分けている。

      追記)
      The USB spec requires that the low byte of wIndex be set to the interface number .
      ということで、WinUSB では、IndexL を自由に使えないとかなんとか。検討しなおす必要がある。(未検討)
      追記 -- ここまで)

      ちょっと不明瞭なところが。
      ホスト側では (例えば)WinUsb_ControlTransfer() でリプライを受け取ることが出来るが、IN エンドポイントは使わないような気が。 一方 usbFunctionRead は、IN エンドポイントであるから、ホスト側ではWinUsb_ReadPipe()を使う。

    これをどう使うかだが、I2C_TRANSMIT では、
    IndexL : I2C アドレス +RW
    LengthL : 転送長
    なにかオプションがあったら、ValueL に入れることにしよう。

    I2C_CTRL では、
    IndexL : サブコマンド
    ValueL,H : パラメータ
    とか。レスポンスは、内部の制御変数を構造体にして使って、それを見せるということで。

    ところで V-USB だが、一定期間毎に usbPoll() を call する必要がある。 ( 50ms 以内の間隔だそうだ ) 。I2C_TRANSMIT に続く usbFunctionWrite/Read であまり時間をかけてはならない。パケットは 8 バイトで、一旦 usbFunctionWrite/Read をリターンして またパケットを受け取る処理になる。一見問題なさそうに見えるが、I2C デバイスは、SCL の L 期間をいくらでも引き延ばすことが出来る。

    USB-TINY-I2C での例:

    static void i2c_io_set_scl(uchar hi) {
    #ifdef ENABLE_SCL_EXPAND
    _delay_loop_2(clock_delay2);
    if(hi) {
    I2C_DDR &= ~I2C_SCL; // port is input
    I2C_PORT |= I2C_SCL; // enable pullup

    // wait while pin is pulled low by client
    while(!(I2C_PIN & I2C_SCL));
    } else {
    I2C_DDR |= I2C_SCL; // port is output
    I2C_PORT &= ~I2C_SCL; // drive it low
    }
    _delay_loop_2(clock_delay);
    #else
    _delay_loop_2(clock_delay2);
    if(hi) I2C_PORT |= I2C_SCL; // port is high
    else I2C_PORT &= ~I2C_SCL; // port is low
    _delay_loop_2(clock_delay);
    #endif

    ENABLE_SCL_EXPAND の方が正しい処理だが、I2C デバイス側に問題があったりすると、無限ループになる可能性がある。

      ループの上限をもうけるようにするのも加えて、パラメータで方式を設定できるのが良さそうに思う。最低クロックが 100kHz だとすると 9クロック x8 バイトで 720us 。これ自体は問題ないが、1パケットトータルの ビジーループ時間の上限を例えば 1ms とする ... みたいな。ビジーループ1回に 10us の delay を入れて、100 回まで... というような処理にするのだ。

      なお、TRANSMIT がエラーになったかどうかは、CTRL で確認しないといけない。

    なお、同様の問題が USI での I2C デバイス側にもある。クロックが遅いのは問題ないが、ストップ条件の判定でビジーループがあり、割り込み処理で無限ループの可能性がある。Watch Dog Timer を入れるとしても、安易に時間設定してはならない。

以上のように、ライタ+I2C の機能を持たせることは決めた。ライタは avrdude を使えば良いが、I2C のテストは、自製のツールということに。


ブートローダ編

Interrupt free V-USB

    なんのことだろう?と思ったら、V-USB で割り込みを使わない改造だった。これがあると、ATtiny で ブートローダが使えるようになる。いや、その前から ATtiny の V-USB ブートローダ はあったのだ。割り込みベクタにパッチするというもの。これも入れたい。調べてみよう。ちょっと見たところ ATtiny85 USB Boot Loader を元に改造したもの。割り込みは別にあっても良いから、まずオリジナルから。

ATtiny85 USB Boot Loader( github )

    これは、ATtiny-85 用というより Digispark 用。外部クロックなしである。内蔵 RC を 16.5 MHz にまでクロックアップして 4.5V 以上で使う建前。Digispark は、レギュレータが付いているから、多分 3.3V -- 普通はこれでいけるはず。

    他に Experimental version of USBasp bootloader for AVR なんてのもあった。

    Differences from original USBaspLoader で気になるところを見ると
    ・Works even when watchdog timer is fused on.
    ・Acknowledges USBASP_FUNC_SETISPSCK command to avoid spurious warning from avrdude.
    ・Uses software-based protection from overwriting bootloader
    ・Verifies CRC of received USB data before writing to flash.
    上の3つは、いいね という感じだが、最後のは何?知らない。CRC は もともと V-USB でチェックしているのでは? 気になる。

    また、こういうのは完全に理解して使いたい。また、ATtiny85 USB Boot Loader は、ちょっとコードが気に入らない。自作の AT90USB162 (ATmega16u2) 用 USBaspLoader を持っているので、これをベースに作りたくなってきた。

    対象は、ATtiny861, ATtiny88 である。特定のキーを押して USB につなぐと ブートローダーが起動する仕組みにしたい。ブートローダーのサイズは、2.8KB ぐらいだそうだが、アプリケーションで ブートローダーのコードを利用する方法も入れてみたい。そうすれば、ATTiny44 の FabUSB でも使えるようになるはずだ。

V-USB のブートローダーと アプリケーションの共有

どれぐらい大変なことなのか?検討してみよう。

ブートローダが使う変数

    ブートローダーを置くプログラムエリアのアドレスは、リンクの指定によって後ろに位置をずらす。変数も同じように ずらせる。最後に置くとすれば、スタックのアドレスも前にずらさないといけない。

      APP with V-USB BootLoader
      +----------------------+ +----------------------+
      | APP .data | | BootLoader .data |
      +----------------------+ +----------------------+
      | APP .bss | | BootLoader .bss |
      +----------------------+ +----------------------+
      | APP .heap | | BootLoader .heap |
      | | | |
      | (stack) | | (stack) |
      +----------------------+ __stack +----------------------+ __stack
      | BootLoader .api | | BootLoader .api | (0x100+0x60 - 0x40)
      +----------------------+ +----------------------+ __stack(default)
      (0x100+0x60)

    RAM の メモリマップはこんな風になる。

      -Wl,--section-start=.text=0xXXXX,\
      --section-start=.data=0xXXXX,\
      --section-start=.api=0xXXXX,\
      --defsym=__stack=0xXXXX

    セクションを移動して、__stack のアドレスを変更。ブートローダーのビルドでは、こういう指定をする。

      -Wl,--section-start=.api=0xXXXX,\
      --defsym=__stack=0xXXXX

    アプリケーションで、ブートローダーの V-USB 機能を利用する場合は、まずスタックをずらす。そうしないと、V-USB が使用する変数を壊してしまう。( 機能を利用しない場合は、当然不要である。)

.api セクション

    当然の話だが、アプリケーションが、V-USB が使っている変数を壊してしまうと、V-USB が動かない。(その他のブートローダーの変数は壊してしまっても良い)。また、callback 関数の登録など、API に関する部分もある。

    そのため、V-USB 変数に対し section を作り --section-start を使い、RAM の最後 -- stack の上に移動してしまう。
    microchip ! のドキュメント /AVRLibcReferenceManual に説明がある。

    section 名は .api としよう。ここに、アプリケーションでも使う変数を 割り当てる。ローカルなものは、.data/.bss で良い。
    この.api セクションを定義したオブジェクトファイルを ブートローダとアプリケーションの両方で使えば良い。

    ただし、.api セクションは初期値不定である。ブートローダとアプリケーションそれぞれで、どこで初期化するかルールを決めないといけない。

usbInit()

    usbFunctionSetup() をはじめとする callback 関数は、アプリケーションが .api 変数に設定してから、usbInit() を call するようにしよう。usbPoll() など V-USB 関数も .api 変数として関数ポインタを作り usbInit() で、設定することも出来る。

    では usbInit() はどうやって call するか? アドレスが決まっていれば良い。usbPoll() なども 関数ポインタではなく、アドレス固定の方が良さそうだ。他に、ブートローダの機能として割り込みベクタをフックする必要があり、元のアプリケーションの割り込みベクタを 覚えておく領域が必要だ。

プログラムエリア・メモリマップ

      APP with V-USB .text
      +----------------------+
      | APP .vectors |
      +----------------------+
      | APP .progmem |
      +----------------------+
      | APP .init0 |
      | __init: |
      +----------------------+
      | . |
      .
      | . |
      +----------------------+ (BootLoader .text)
      | BootLoader .vectors |
      +----------------------+
      | BootLoader .progmem |
      +----------------------+
      | BootLoader .init0 |
      | __init: |
      +----------------------+
      | . |
      | . |
      +----------------------+

    こんな風に2つのプログラムが置かれる。ブートローダは、.text をずらしただけなので、無駄にベクタテーブルが作られる。本物のベクタテーブルは、別に作ってやらないといけない。

    また、固定のアドレスなのは、先頭のベクタテーブルだけである。ベクタテーブル自体の大きさが AVR によって違うし、次に PROGMEM の領域がありサイズ不定。その後にようやく __init というスタートアドレス。

ベクタテーブル

    サイズ RESET INT0 INT1
    ATtiny88 20 0, 1, 2
    ATtiny861 19 0, 1, 13
    ATtiny44 17 0, 1, --
    ATtiny2313 19 0, 1, 2
    ATmega88 26 0, 1, 2


    ベクタテーブルは、8KB までの AVR では、RJMP 命令が置かれる。最初のエントリは、常に RESET 。次は INIT0 ここまでは変わらない。INT1 はチップによって 2 ではない場合や、そもそもない場合も。
    8KB を超えると RJMP が届かない。JMP 命令が入るように 4 バイトエントリになる。
    またサイズはチップによって様々である。

    ブートローダのベクタテーブル領域は、すべてがダミーである。好きに定義して使える。

    このダミーのベクタテーブルに まずフックエントリを置く。
    最初は、スタートアップ(__init)で、

      SBIS GPIOR2,7
      RJMP __init (ブートローダ)
      RJMP __init (アプリケーション)

    こんな風に 3 命令エントリにしようかと。
    次は、ブートローダ V-USB 割り込み(INT0) 。

    後からは、1 命令エントリで、usbInit() 、usbPoll() ... への RJMP 命令を置く。

    フックエントリには、どこか IO レジスタの 1bit が専用に必要である。アプリケーションでは これを変更してはならない。仮に GPIOR2 の bit7 と bit6 いうことにする。

    bit7 が 0 だとブートローダにジャンブする。1 で アプリケーション。
    bit6 が 0 だと INI0 がブートローダの V-USB に飛ぶ。1 にするとアプリケーションで定義して INI0 を使う。(デフォルト)、usbInit() が成功すると、1 に設定される。


      BOOTLOADER_ADDR:
      +0x00 SBIS GPIOR2,7
      +0x02 RJMP __init (ブートローダ)
      +0x04 RJMP __init (アプリケーション ※)
      +0x06 SBIS GPIOR2,6
      +0x08 RJMP __int0 (ブートローダ)
      +0x0A RJMP __int0 (アプリケーション ※)
      +0x0C RJMP usbInit ()
      +0x0E RJMP usbPoll ()


ベクタテーブルの書き換え

    ブートローダでファームウェアを書き込む場合、RESET , ブートローダ割り込み(INT0) を フックエントリに退避し、フックエントリのアドレスに書き換える。これを自動で行う。


      APPLICATION_ADDR:
      +0x00 RJMP BOOTLOADER_ADDR
      +0x02 RJMP BOOTLOADER_ADDR+6

    ブートローダを最初に書き込む場合は、ベクタテーブルを別に用意する。といっても RJMP 2 エントリで 飛び先も固定。INT0 を使う場合は、こんな風になる。

検討してみたが、作れそうな気がしてきた。

以上までが、I2C_KEYBOARD の範囲での全体の構想である。随分と大風呂敷になったようにも思うが、これ以上は広げない。だいたい、FPGA があって、謎PC もあるのである。ブートローダはやりすぎという気もするのだが、これとて以前に取り組んだネタである。興が乗ったら一気に作ってしまうかも。


V-USB のシンボル

    FabUSB のコードを作っているのだが、elf ファイルに avr-nm をかけて、シンボルの調査。


    PROGMEM (デスクリプタ)
    00000022 T usbDescriptorString0
    00000026 T usbDescriptorStringDevice
    00000034 T usbDescriptorDevice
    00000046 T usbDescriptorConfiguration

    V-USB 関数
    00000090 T usbInit
    0000009e T usbPoll

    000002b8 T usbCrc16
    000002e2 T usbCrc16Append
    (割り込み)
    000002ea T __vector_1

    (callback)
    000006fa T usbFunctionSetup
    000008cc T usbFunctionRead
    0000095a T usbFunctionWrite

    (V-USB グローバル 変数)
    00800060 D usbTxLen
    0080007c B usbTxBuf
    00800087 B usbDeviceAddr
    00800088 B usbRxToken
    00800089 B usbInputBufOffset
    0080008a B usbMsgPtr
    0080008c B usbRxLen
    0080008d B usbNewDeviceAddr
    0080008e B usbCurrentTok
    0080008f B usbConfiguration
    00800090 B usbRxBuf

    全部書いてもリストアップできる量。

    このうち callback は、次のように定義する。

    usbMsgLen_t (* _usbFunctionDescriptor)(struct usbRequest *rq);
    usbMsgLen_t (* _usbFunctionSetup)(uchar data[8]);
    uchar (* _usbFunctionWrite)(uchar *data, uchar len);
    uchar (* _usbFunctionRead)(uchar *data, uchar len);

    上でも書いた 3 つに加えて、デスクリプタも callback として定義する。
    これの値を usbInit() の前に設定する。

    _usbFunctionDescriptor = usbFunctionDescriptor;
    _usbFunctionSetup = usbFunctionSetup;
    _usbFunctionWrite = usbFunctionWrite;
    _usbFunctionRead = usbFunctionRead;

    V-USB 変数 をすべて .api セクションに移動。

    #define APP_SECTION __attribute__ ((section (".api")))

    として、すべての変数定義につける。

    $ avr-objdump -h usbdrv\usbdrv.o
    Sections:
    Idx Name Size VMA LMA File off Algn
    0 .text 00000216 00000000 00000000 00000034 2**0
    CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
    1 .data 00000000 00000000 00000000 0000024a 2**0
    CONTENTS, ALLOC, LOAD, DATA
    2 .bss 00000000 00000000 00000000 0000024a 2**0
    ALLOC
    3 .api 00000035 00000000 00000000 0000024a 2**0
    CONTENTS, ALLOC, LOAD, DATA

    うまくいった。初期値ありの変数は、2つあるが、usbInit() の中で初期化する。

    次、ベクタ

    $ avr-objdump -h -S main.elf
    Disassembly of section .text:

    00000000 <__vectors>:
    0: 39 c0 rjmp .+114 ; 0x74 <__ctors_end<
    2: 6d c1 rjmp .+730 ; 0x2de <__vector_1<
    4: 47 c0 rjmp .+142 ; 0x94 <__bad_interrupt<
    6: 46 c0 rjmp .+140 ; 0x94 <__bad_interrupt<
    :

    普通はこんな風に割り当てられる。
    これを書き換えたい。

    -Wl,--defsym=__vector_4=__vector_1 \
    -Wl,--defsym=__vector_6=usbInit \
    -Wl,--defsym=__vector_7=usbPoll

    ちょっと無理だった。ただ、空いているところに関数のアドレスを埋めることは出来た。
    こうしておくと、書き換えツールの作成が楽にはなる。

    スタックと .api の移動

    -Wl,--defsym=__stack=0x0080011f \
    -Wl,--section-start=.api=0x00800120

    実際のアドレスに 0x0080_0000 が加算されているが、リンカのアドレス管理の都合なので気にしなくて良い。この例は、RAM 256B の ATtiny44 。RAM のアドレスは 0x60 〜 0x15f である。スタックを 0x40(64) 下にずらし、空いたところに .app セクションを持って行く。

    Sections:
    Idx Name Size VMA LMA File off Algn
    0 .api 00000035 00800120 00800120 00000b94 2**0
    CONTENTS, ALLOC, LOAD, DATA
    1 .text 00000b00 00000000 00000000 00000094 2**1
    CONTENTS, ALLOC, LOAD, READONLY, CODE
    2 .bss 0000001f 00800060 00800060 00000b94 2**0
    ALLOC

    ここまでの改造は、V-USB として動くことが期待できる。しかし、.text を移動するには、ベクタ書き換えのツールを作らないと無理。

    ツールは、hex ファイルを編集することを考えている。

    ところで、これらの改造でどれぐらいサイズが増えたのだろう?

    avr-size main.elf
    text data bss dec hex filename
    2116 2 67 2185 889 main.elf

    text data bss dec hex filename
    2256 53 25 2334 91e main.elf

    data は、69 バイト → 88 バイト。11 バイトの増加である。このうち callback 関数ポインタが 8 バイト。
    あと、ディスクリプタ切り替えのための usbconfig.h 変更によるもの。-- 定数が良かったものが、変数になるところが出てくる -- と思う。

.text をずらし、hex ファイルを編集

    -Wl,--section-start=.text=0x0600

    仮に 1.5KB 後ろにずらしてみよう。アプリケーションが 1.5KB ということだが、これは ATtiny44 のビル環境を流用しているため。ターゲットは 8KB の AVR で 一応 5.5KB 使えるようにするのが目標。

    hex ファイルを編集するのはベクタ領域の頭なので見てみよう。

    :1000000039C06DC147C046C06AC144C044C050C079
    :1000100041C040C03FC03EC03DC03CC03BC03AC0F4

    :1006000039C06DC147C046C06AC144C044C050C073
    :1006100041C040C03FC03EC03DC03CC03BC03AC0EE

    どこが違う? というと 置かれるアドレスと チェックサム。RJMP を使っているので、全体をずらすだけでは内容は変わらない。

    :100000 00 39C0 6DC1 47C0 46C0 6AC1 44C0 44C0 50C0 79
    :100010 00 41C0 40C0 3FC0 3EC0 3DC0 3CC0 3BC0 3AC0 F4

    :100600 00 39C0 6DC1 47C0 46C0 6AC1 44C0 44C0 50C0 73
    :100610 00 41C0 40C0 3FC0 3EC0 3DC0 3CC0 3BC0 3AC0 EE

    分かりやすいように区切ると、こう。C0 で終わる2バイトデータが多いが 全部 RJMP 命令である。
    これをどうするか?
     ・ 0x600 にずらした、ベクタテーブルを 0x0 にコピーする。
      ・ そのとき、2バイト目( C0 等)を +3 (0x600/2/0x100) する。
      ・チェックサムの再計算。

     ・ 0x600 にある、ベクタテーブルの書き換え。
      ・#0 を #1 に移動、RJMP のアドレスを -2 (1バイト目を -1)
      ・#0 と #3 に SBIC 命令。GPIOR2 のアドレスはチップによって違うので注意。
     ・チェックサムの再計算。

    RJMP
    0xc0000 | ((addr/2) & 0xfff)
    addr は相対 (-4096 - +4092)
    SBIS
    0x9B00 | (io_addr <<3) & 0xf8 | bit & 0x7

    GPIOR0 GPIOR1 GPIOR2
    ATtiny44 0x13 0x14 0x15
    ATtiny88 0x1B 0x2A 0x2B
    ATtiny861 0x0A 0x0B 0x0C

    SBIS が使えるのは、0x1F まで。... ということは、ATtiny88 では、GPIOR0 しか使えない。

再度割り込みベクタ

    割り込みベクタを思ったように変更できなかったのは、スタートアップの gcrt1.S を 変更するのを諦めたからである。が、以前 AT90USB162 のブートローダーを作ったときは、それもやっていたのである。再検討してみよう。

    Disassembly of section .text:

    00000600 <__vectors>:
    600: df 9b sbis 0x1b, 7 ; 27
    602: 31 c0 rjmp .+98 ; 0x666 <__ctors_end>
    604: 30 c0 rjmp .+96 ; 0x666 <__ctors_end>
    606: de 9b sbis 0x1b, 6 ; 27
    608: 59 c1 rjmp .+690 ; 0x8bc <__vector_1>
    60a: 58 c1 rjmp .+688 ; 0x8bc <__vector_1>
    60c: 33 c0 rjmp .+102 ; 0x674 <usbInit>
    60e: 3f c0 rjmp .+126 ; 0x68e <usbPoll>
    610: 3c c1 rjmp .+632 ; 0x88a <usbCrc16>
    612: 50 c1 rjmp .+672 ; 0x8b4 <usbCrc16Append>

    出来た! これが作れると、hex の編集はいらない。

    0 番地からのベクターテーブルも、0x600, 0x606 に飛ばすようにするものなので、固定のファイルでいい。

    0x600

    :10000000FFC201C300C3FFC2FEC2FDC2FCC2FBC2ED
    :10001000FAC2F9C2F8C2F7C2F6C2F5C2F4C2F3C21C
    :00000001FF


    0x1600

    :10000000FFCA01CB00CBFFCAFECAFDCAFCCAFBCAAD
    :10001000FACAF9CAF8CAF7CAF6CAF5CAF4CAF3CADC
    :00000001FF

    これを使いまわす。

    ブートローダーが占有する領域は

      プログラム: 後ろから 2.8KB
      I/O レジスタ: GPIOR0 bit6,bit7
      RAM : 後ろから 64B (0x40) V-USB をランタイムとして使う場合のみ必要

    これで決めた。

ブートローダでのベクタ書き換え

    ブートローダでどんな処理をしないといけないか?メモ

    読み込みで、書いたデータと同じものが読める。

      違うとベリファイエラーになってしまうから、まずこれをクリアすることを考えよう。
      割り込みベクタ RESET(#0) と INT0(#1) は、ブートローダへの RJMP が格納されていて、これは書き換えない。
      そこに書いたデータは、ブートローダのベクタ領域の #2 と #5 に RJMP 命令として格納されている。

      #0(#1) を READ した場合、ブートローダ#2(#5) の RJMP を変換したものが読めることにしよう。
      他については、ブートローダ領域を読み込もうとした場合 0xff が読めることにする。
      それ以外は、変換しない。

      変換は、次のようにする。

      割り込みベクタ → ブートローダベクタ
      r = data - ((BOOTLOADER_ADDRESS+4 -0)>>1);
      (data & 0xf000 | r & 0x0fff )

      r = data - ((BOOTLOADER_ADDRESS+10 -BOOTLOADER_INT)>>1);
      (data & 0xf000 | r & 0x0fff )

      ブートローダベクタ → 割り込みベクタ
      data = pgm_read_word_near(BOOTLOADER_ADDRESS+4);
      r = data + ((BOOTLOADER_ADDRESS+4 -0)>>1);
      (data & 0xf000 | r & 0x0fff )

      data = pgm_read_word_near(BOOTLOADER_ADDRESS+10);
      r = data + ((BOOTLOADER_ADDRESS+10 -BOOTLOADER_INT)>>1);
      (data & 0xf000 | r & 0x0fff )


    ページ WRITE

      ブートローダでは、ページイレーズして 書き込むようなことをしている。
      上で書いたように、
      #0(#1) を WRITE した場合、そこは前の値を残し、ブートローダ#2(#5) に RJMP を変換したものを書き込む。

      これ相当にめんどくさい。

      AVR は、一時バッファというのを持っている。ここに 新規に書くデータを書いて、変更のない部分は LPM で読み込んだ値を埋める。次に ページイレーズ → 一時バッファの書き込みという技ができる。

      ブートローダ#2(#5)の書き換えは、そういう風にして行うが、後でやらないといけない。

      また、一時バッファに一度書いたら、同じ所へ 2度書いても、2回目は無視されて先に書いた方が残る。
       ・ 1ページ分 全部 LPM でデータを読んで 一時バッファに書く
      という操作の順番で 前の値を残すとか、一部書き換えるという場合に便利なようになっている。が、不安なので、最初は重複しないよう制御しようと思う。

さて、ディスクリプタ切り替えだが、これがまた七面倒くさい。
かと言って、切り替えできないのは論外。アプリケーションへの制限が大きすぎる。
今回は HID がターゲットなのだから、それぐらいは作れないと。

    というより、変更できるのは、ディスクリプタだけである。

    #define USB_CFG_HAVE_INTRIN_ENDPOINT 0
    #define USB_CFG_HAVE_INTRIN_ENDPOINT3 0
    #define USB_CFG_IMPLEMENT_FN_WRITE 1
    #define USB_CFG_IMPLEMENT_FN_READ 1
    #define USB_CFG_IMPLEMENT_FN_WRITEOUT 0
    #define USB_CFG_HAVE_FLOWCONTROL 0

    このへんのコンフィグは変更できない。CDC など ENDPOINT3 が必要であるが、サポートしないということ。

    ディスクリプタについては次のようになっている。

    #define USB_CFG_DESCR_PROPS_DEVICE USB_PROP_IS_DYNAMIC
    #define USB_CFG_DESCR_PROPS_CONFIGURATION USB_PROP_IS_DYNAMIC
    #define USB_CFG_DESCR_PROPS_STRINGS USB_PROP_IS_DYNAMIC
    #define USB_CFG_DESCR_PROPS_STRING_0 USB_PROP_IS_DYNAMIC
    #define USB_CFG_DESCR_PROPS_STRING_VENDOR USB_PROP_IS_DYNAMIC
    #define USB_CFG_DESCR_PROPS_STRING_PRODUCT USB_PROP_IS_DYNAMIC
    #define USB_CFG_DESCR_PROPS_STRING_SERIAL_NUMBER USB_PROP_IS_DYNAMIC
    #define USB_CFG_DESCR_PROPS_HID 0
    #define USB_CFG_DESCR_PROPS_HID_REPORT 0
    #define USB_CFG_DESCR_PROPS_UNKNOWN 0

    USB_PROP_IS_DYNAMIC とすると、(_usbFunctionDescriptor)() がいちいち callback される。USB_CFG_DESCR_PROPS_HID については意味が違う。HID ディスクリプタを埋め込むかどうかであり、callback する以上関係ない。

    めんどくさいのは、自前でディスクリプタを用意するからである。

ディスクリプタの定義

    ちょっと定義をみてみよう。テストツールを作るときに必要な情報だし。

    USBasp:
    #define USB_CFG_VENDOR_ID 0xc0, 0x16 /* = 0x16c0 = 5824 = voti.nl */
    #define USB_CFG_DEVICE_ID 0xdc, 0x05 /* = 0x05dc = 1500 */
    #define USB_CFG_DEVICE_VERSION 0x02, 0x01
    #define USB_CFG_VENDOR_NAME 'w','w','w','.','f','i','s','c','h','l','.','d','e'
    #define USB_CFG_VENDOR_NAME_LEN 13
    #define USB_CFG_DEVICE_NAME 'U', 'S', 'B', 'a', 's', 'p'
    #define USB_CFG_INTERFACE_CLASS 0
    FabISP:
    #define USB_CFG_DEVICE_ID 0x9f, 0x0c /* = 0x0c9f = usbtinyisp */
    #define USB_CFG_VENDOR_ID 0x81, 0x17 /* = 0x1781 = usbtinyisp */
    #define USB_CFG_DEVICE_VERSION 0x04, 0x01

    #define USB_CFG_DEVICE_NAME 'F', 'a', 'b', 'I', 'S', 'P'
    #define USB_CFG_INTERFACE_CLASS 0xff

    こんな風に差分のみ定義すればよかったのが、全部を定義しないといけなくなる。自由度は高くなるが、間違いも入りやすい。



さて、そろそろである。部品が ようやく揃った。基板も tiny2313版、tiny861 版が到着している。tiny88 版もあと数日。そろそろ組みだそうと思う。今、最初にテストしたいのは、ブートローダになっている。なんとか作ってしまいたい。

最初のビルド

    いままで書いてきたのをコードにして、ビルドは通るようになった。

    text data bss dec hex filename
    2744 53 10 2807 af7 asploader.elf

    まだ初期化の一部が入っていない。にもかかわらず、2560 バイトに収めるのが目標。--- 無理そうだ。
    無理ならば、2816 バイト、それも無理なら 3072 バイト。-- ここまで来ると 4KB tiny のアプリケーション・プログラム が 1KB まで減らされる。そうなると、何を作っているのか? ということに。

      8KB あれば、別にV-USB をランタイムとして使う必要はないのである。4KB のチップでは、ブートローダと V-USB アプリは絶対に両立しない。それで、面白いから 挑戦してみることにしたのだ。 目標は I2C 拡張した FabUSB が入ること。

    現状はたいへん厳しい。

    text data bss dec hex filename
    2790 53 10 2853 b25 asploader.elf
    1076 53 25 1154 482 fabisp.elf (original)

    1636 53 31 1720 6b8 fabisp.elf (I2C_EXTEND)

    オリジナルなら入る。が I2C 拡張は無理。1280 バイトに収めるのはちょっと厳しすぎ。
    ブートローダを 2560 バイトに収められれば可能性が出てくるが ... これから先は一回動かしてからになる。

      オリジナルの FabISP のサイズは、2118 バイト。これが、1076 バイトになる。1KB は節約できるのだ。頑張った成果はあるのだ。また、ブートローダの機能をぎりぎりまで減らすと 2570 バイトとかサイズを減らせる。あと少しではある。

    asploader-tiny-0.0.zip

    とりあえず、ここまでをまとめた。usbdrv の変更元を FabISP のものとしたが、ちょっと適当すぎかも。
    一応 変更点を diff にして入れてある。

asploader-tiny-0.0 API (訂正版)

    USBasp プロトコルのブートローダで、tiny 専用。V-USB の機能を ランタイムとして使用できるようにしてある。

    アプリケーションの制限 (V-USB ランタイム を使用しない場合)
     ・ GPIOR0 の bit6,bit7 を変更してはならない。
     ・ プログラムサイズ 制限(ブートローダ 占有分は使用できない)

    アプリケーションの制限 (V-USB ランタイム を使用する場合)
     ・ GPIOR0 の bit6,bit7 を変更してはならない。
     ・ プログラムサイズ 制限(ブートローダ 占有分は使用できない)
     ・ RAM 使用量制限 (後ろ 66B を使用してはならない。)
      - スタックポインタの移動も必要
     ・ V-USB 機能制限 (ブートローダ と同じ機能範囲)

    V-USB ランタイムの使い方 (API)

    #define USBDDR DDRA
    #define USBMINUS PA1
    #include "usbruntime.h"

    usbruntime.h を usnconfig.h , usbdrv/usbdrv.h の代わりに使用する。
    USBDDR,USBMINUS の define は、usbDeviceConnect(),usbDeviceDisconnect() マクロでのみ使用。正しいものを define するか、usbDeviceConnect(),usbDeviceDisconnect() を自前のものにする。

    main関数:

    _usbFunctionDescriptor = usbFunctionDescriptor;
    _usbFunctionSetup = usbFunctionSetup;
    _usbFunctionRead = usbFunctionRead;
    _usbFunctionWrite = usbFunctionWrite;
    usbInit();
    usbDeviceDisconnect();
    i = 0;
    while(--i){ /* fake USB disconnect for > 250 ms */
    wdt_reset();
    _delay_ms(1);
    }
    usbDeviceConnect();
    sei();
    for(;;){ /* main event loop */
    wdt_reset();
    usbPoll();
    }

    callback 関数を 登録してから、usbInit() を call する。
    _usbFunctionDescriptor() が必要になるが、テンプレートを aspdesc.c , tinydesc.c として用意してある。
    リンク:

    BOOTLOADER_ADDRESS = 0x0500 # program 4K model
    API_ADDRESS = 0x0080011e # RAM 256B model
    USBINIT=0x050c
    USBPOLL=0x050e
    USBSETINT=0x0510

    #BOOTLOADER_ADDRESS = 0x1500 # program 8K model
    #API_ADDRESS = 0x0080031e # RAM 512B model
    #USBINIT=0x150c
    #USBPOLL=0x150e
    #USBSETINT=0x01510
    LDFLAGS_APP = -Wl,--defsym=__stack=0x0080011d
    LDFLAGS_APP += -Wl,--section-start=.api=$(API_ADDRESS)
    LDFLAGS_APP += -Wl,--defsym=usbInit=$(USBINIT)
    LDFLAGS_APP += -Wl,--defsym=usbPoll=$(USBPOLL)
    LDFLAGS_APP += -Wl,--defsym=usbSetInterrupt=$(USBSETINT)

    usbruntime.o を追加し、-Wl での 4つの指定が必要。
      ・__stack は、API_ADDRESS -1 の値、
      ・usbInit は、BOOTLOADER_ADDRESS + 0xc の値
      ・usbPoll は、BOOTLOADER_ADDRESS + 0xe の値

    Makefile 注意点:
    busybox rm とか使っているが、WinAVR のコマンドが エラーになったり不安定なためである。最新の avr-toolchain + mingw+msys を使用する場合は、rm 等で良い(はず)。

asploader-tiny-0.0 ブートローダまとめ

    ブートローダ自体を作成する 場合 main のプログラムの作り方はアプリケーションと同じである。リンクは次のようにする。

    LDFLAGS = --defsym=__stack=0x0080011d
    LDFLAGS += -section-start=.text=$(BOOTLOADER_ADDRESS)
    LDFLAGS += -section-start=.api=$(API_ADDRESS)

    ・スタートアップ gcrt1.o を追加し、avr-gcc ではなく avr-ld を使用する。(avr-ld を使用するときは、-Wl, を外す。)
    ・usbdrv は、添付した版を 普通にコンパイル リンク (usbInit,usbPoll の defsym はいらない )
    ・usbruntime.o をアプリケーション同様追加する。

今のコードサイズ:

text data bss dec hex filename
146 0 0 146 92 aspdesc.o
1202 0 6 1208 4b8 aspfunc.o
30 0 0 30 1e gcrt1.o
0 53 0 53 35 usbruntime.o
536 0 0 536 218 usbdrv\usbdrv.o
652 0 0 652 28c usbdrv\usbdrvasm.o

現状こんな感じで、これ以上 サイズを減らすのは厳しい。usbdrv や usbdrvasm をほんの僅かでも減らせるような気がしない。コードを減らす努力は、また別の機会にしよう。

そんなことより、HID のサンプルコード動かした方が有益な気がする。終わりにするまえに、ちょっと見てみる。

    ちょっと HIDKeys.2012-12-08 をコンパイルできるか試したところ ... オプションが足りなかった!

    #define USB_CFG_HAVE_INTRIN_ENDPOINT 1

    としなければならない。そうすると

      void usbSetInterrupt(uchar *data, uchar len);
      usbTxStatus_t usbTxStatus1;

    が増える。usbTxStatus1 はサイズが大きく、.api セクションが 65 バイトになってしまった。

    いろいろと変更しないとならなくなった。 ザイズも当然増える。しかし、HID も作れないようではダメである。

    コードを少し見てみたが、
     ・Interrpt 転送 のバッファが空いていれば( usbInterruptIsReady())
        定期的に usbSetInterrupt() でレポートを送る。

     ・SETUP で、USBRQ_HID_GET_REPORT が来たら、リプライで レポートを送る。
     ・SETUP で、USBRQ_HID_SET_IDLE が来たら、値を覚える。
     ・SETUP で、USBRQ_HID_GET_IDLE が来たら、覚えた値を返す。

    これだけのようだ。なお、レポートのサイズは 2 バイトにしている。これでも良いらしいが、8 バイトの定義を採用したい。ここら辺は レポートディスクリプタで定義するようなのだが ...

    0x05, 0x01, // USAGE_PAGE (Generic Desktop)
    0x09, 0x06, // USAGE (Keyboard)
    0xa1, 0x01, // COLLECTION (Application)
    /* modifier E0 - E7 */
    0x05, 0x07, // USAGE_PAGE (Keyboard)
    0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
    0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0x01, // LOGICAL_MAXIMUM (1)
    0x75, 0x01, // REPORT_SIZE (1)
    0x95, 0x08, // REPORT_COUNT (8)
    0x81, 0x02, // INPUT (Data,Var,Abs)
    /* Reserved Byte */
    0x95, 0x01, // REPORT_COUNT (1)
    0x75, 0x08, // REPORT_SIZE (8)
    0x81, 0x01, // INPUT (Constant); Reserved Byte
    /* KeyCode Array [6] */
    0x95, 0x06, // REPORT_COUNT (6)
    0x75, 0x08, // REPORT_SIZE (8)
    0x25, 0x9A, // LOGICAL_MAXIMUM (0x9A)
    0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
    0x29, 0x9A, // USAGE_MAXIMUM (Keyboard SysRq)
    0x81, 0x00, // INPUT (Data,Ary,Abs)
    0xc0 // END_COLLECTION

    どうやら、この 41 バイトで良いようだ。一般的なものから、LED の定義だけ抜いたものになる。



WinUSB の制限

     ・https://github.com/libusb/libusb/wiki/Windows

    ここを見て知ったのだが、
      The USB spec requires that the low byte of wIndex be set to the interface number
    ということらしい。仕様と言われればそれまで。問題は、USBasp も USBtiny もダメということ。

    USBasp の問題に対して、
     Zadig で libusbK にドライバを変更せよ

    というのが解らしい。

    問題は、自分でビルドする場合。
    libusbK は、libusb0.dll の互換 dll を持っている。一方 avrdude は、libusb-0 もしくは libusb-1 に対応していて、libusbK に直接は対応していない。

    とりあえず、tcc を使う計画は棚上げ。すなおに mingw をインストールして avrdude-6.0 をビルドしてみる。

    混乱しているのだが、libusb-1 は使わずにビルドすれば良いということだろうか?

    とりあえず、libusb-win32-bin-1.2.6.0 から lusb0_usb.h と libusb.a を持ってくると

    Configuration summary:
    ----------------------
    DON'T HAVE libelf
    DO HAVE libusb
    DON'T HAVE libusb_1_0
    DON'T HAVE libftdi1
    DON'T HAVE libftdi
    DO HAVE libhid
    DO HAVE pthread
    DISABLED doc
    ENABLED parport
    DISABLED linuxgpio

    こうなった。

    さて、libftdi は? とか思って調べてみると ... ft245.c というのがあるではないか! 中を見ると見おぼえがあるコードが。私のコードがマージされている! 何年もたって気が付くとは!

    ただ、意地でも ftd2xx は使わない方針のようで、libftdi に置き換えられている。私のコードは、libftdi 向きではなかったのだが、pthread を使って 対応までしている。... これは有効にしなくては!

    しかし、libftdi は、libusb_1_0 に対応しているようなのである。これはどうしたら良いのだろうか?
    ... libftdi1 は、libusb_1_0 に対応で、libftdi は、libusb に対応らしい。

    libftdi-0.20 のソースコードだけを引っ張ってきて、ビルドしてみる。

    λ sh \tcc\ldd.sh avrdude.exe
    avrdude.exe
    +- libusb0.dll
    D:\MinGW\bin\objdump.exe: libusb0.dll: File format not recognized
    +- hid.dll
    +- KERNEL32.dll
    +- msvcrt.dll
    +- msvcrt.dll
    +- pthreadGC-3.dll
    | +- libgcc_s_dw2-1.dll
    | | +- KERNEL32.dll
    | | +- msvcrt.dll
    | +- KERNEL32.dll
    | +- msvcrt.dll
    +- setupapi.dll
    +- WS2_32.dll

    mingw の pthread をインストールすると、いくつか DLL が必要になった。ちょっと面白くないので、
    pthread も ソースコードを引っ張ってきてみる。


    λ sh \tcc\ldd.sh avrdude.exe
    avrdude.exe
    +- libusb0.dll
    D:\MinGW\bin\objdump.exe: libusb0.dll: File format not recognized
    +- hid.dll
    +- KERNEL32.dll
    +- msvcrt.dll
    +- msvcrt.dll
    +- setupapi.dll
    +- WS2_32.dll

    作ることは出来たが ...


    D:\avr-prj\avrdude-6.3
    λ avrdude -c ft232r -p t2313 -P ft0
    can't open ftdi device 0. (device not found)

    avrdude done. Thank you.

    λ avrdude -c ft232r -p t2313 -P ft0

    avrdude: Device is not responding to program enable. Check connection.
    avrdude: initialization failed, rc=-1
    Double check connections and try again, or use -F to override
    this check.

    USB 3.0 のポートでは全然認識しない。USB 2.0 の方では、認識はした。ただ、この後 フォルトを起こす。適当にやりすぎた。それはともかく、libftdi+libusb を使うと シリアルとして使えない。ドライバの入れ替えが必要で、シリアルとして使いたい人には煩雑という問題が。


posted by すz at 22:22| Comment(0) | TrackBack(0) | I2CKEYBOARD
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/183077301
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック