SNSへはこちら

STM32でUSBをベアメタル(4) - ひとまず落ち着け

何か問題や疑問点が積んできたので、今回は色々見直してみることにします。
前回の状況だと、GET_DESCRIPTOR を受けた送信で上手く行かないという問題がありましたが、「とりあえず動けばいいじゃない」というスタンスを一度捨てて、現状の把握に全力を注ごうと思います。もはやスルーできるような状況じゃなくなってきた。

どこが分からんのかまとめると、以下の2つに大別できます。

  1. パケットバッファ書き込み時に奇妙なキャストが必要
    • 元々 g_packet_buffer[0].addr_txuint16_t で宣言していますが、直接書き込むことは出来ずに *(uint16_t *)&g_packet_buffer[0].addr_tx としないと値が変になる
    • というか値が反映されない
  2. そもそも各トランザクションが上手く行っているのか
    • エンドポイントレジスタの値どうなってる?

モヤモヤしっぱなしではできることも出来ないので、状況を可視化します。

パケットバッファ書き込み時に奇妙なキャストが必要問題

これに関しては C 言語のソースを見てもよくわかりません。関数内に雑多な記述があるし。ということで、別途関数を用意して、その挙動を逆アセンブルすることで解析することにしました。

使用ソースとコマンド

構造体 PacketBuffer を真似て、以下の構造体で検証です。

#include <stdint.h>

typedef struct __attribute__((packed)){
    uint16_t a;
    uint16_t b;
    uint16_t c;
    uint16_t d;
} St;

volatile uint32_t addr = 0x00114514;
volatile static St *st;

void f(void) {
    st = addr;
    st[0].a = 10;
    st[0].b = 20;
}

以下のように状況を揃えました。

  • とあるアドレスに構造体 St は配置されると考える
  • volatile も揃えた
  • packed 属性もお忘れなく

さて、コンパイル/逆アセンブルしてみます。コマンドは以下。

$ arm-none-eabi-gcc -mthumb -mcpu=cortex-m0 -c -Os a.c
$ arm-none-eabi-objdump -d a.o

結果

逆アセンブル結果です。

08000220 <f>:
 8000220:   4b06        ldr r3, [pc, #24]   ; (800023c <f+0x1c>)
 8000222:   210a        movs    r1, #10
 8000224:   681b        ldr     r3, [r3, #0]
 8000226:   781a        ldrb    r2, [r3, #0]
 8000228:   2200        movs    r2, #0
 800022a:   7019        strb    r1, [r3, #0]
 800022c:   7859        ldrb    r1, [r3, #1]
 800022e:   705a        strb    r2, [r3, #1]
 8000230:   7899        ldrb    r1, [r3, #2]
 8000232:   2114        movs    r1, #20
 8000234:   7099        strb    r1, [r3, #2]
 8000236:   78d9        ldrb    r1, [r3, #3]
 8000238:   70da        strb    r2, [r3, #3]
 800023a:   4770        bx  lr
 800023c:   20000000    .word   0x20000000

何がなんやら、という方も多いと思いますが、よく見てください。命令の末尾に b と付いているものが多いですよね??つまり、このソースでは 8bit アクセスをしているアセンブリコードが生成されます。
ところで、ユーザーマニュアルによると、どうやら PMA はバイトアクセスとハーフワードアクセスができるらしい。

上のコードはバイトアクセスですね。コードを見る限り、ちゃんと命令は構成されています。一応以下で挙動を説明します。

0: r3 <= 0x2000 0000 # addrのアドレス (&addr)
2: r1 <= 10
4: r3 <= *(uint32_t *)r3 # r3は*(uint32_t *)&addr, つまり変数addrの値 == 構造体のベースアドレス
6: r2 <= *(uint8_t *)r3 # 構造体の最初の要素にバイトアクセスし、値を退避
8: r2 <= 0 # このプログラムでは r2 == 0 で固定
a: *(uint8_t *)r3 <= r1 # 構造体の最初の要素low byteにバイトアクセスし、10を書き込む
c: r1 <= *((uint8_t *)r3 + 1) # 構造体の最初の要素high byteにバイトアクセスし、値を退避
e: *((uint8_t *)r3 + 1) <= r2 # 構造体の最初の要素high byteにバイトアクセスし、0を書き込む
~~~ この時点で構造体内のデータは {10, 00, xx, xx} (xxは不定)
0: r1 <= *((uint8_t *)r3 + 2) # 構造体の2番目の要素high byteにバイトアクセスし、値を退避
2: r1 <= 20
4: *((uint8_t *)r3 + 2) <= r1 # 構造体の2番目の要素high byteにバイトアクセスし、20を書き込む
6: r1 <= *((uint8_t *)r3 + 3) # 構造体の2番目の要素low byteにバイトアクセスし、値を退避
8: *((uint8_t *)r3 + 3) <= r2 # 構造体の最初の要素high byteにバイトアクセスし、0を書き込む
a: return;
~~~ ここまでで構造体内のデータは {10, 00, 20, 00}

この冗長な挙動は、packed 属性に依るものです。この属性は構造体内の各メンバを詰めて配置するというものであり、つまりある意味で変数のアラインメントを無視するように指示するものになります。よって無理やり 16bit 変数を構成するために、アラインメントの不整合が起きぬよう8bitアクセスのみで命令が構成されるというわけです。ちなみにですが、この挙動は addrconst とすればなくなってハーフワードアクセスで動作するようになりました。アドレスが定数になり、またそのアドレス値が2の倍数になったので、16bit 変数のアラインメントに整合します。なのでこのような考慮は不要になったのでしょうね。

ではここで、__attribute__((packed)) を外してみます。すると、以下のようにコードが短くなりました。命令の末尾に h がついていますね。コレがハーフワードアクセス(16bit)です。

08000220 <f>:
 8000220:   220a        movs    r2, #10
 8000222:   4b03        ldr r3, [pc, #12]   ; (8000230 <f+0x10>)
 8000224:   681b        ldr r3, [r3, #0]
 8000226:   801a        strh    r2, [r3, #0]
 8000228:   1892        adds    r2, r2, r2
 800022a:   805a        strh    r2, [r3, #2]
 800022c:   4770        bx  lr
 800022e:   46c0        nop         ; (mov r8, r8)
 8000230:   20000000    .word   0x20000000

それでは USB のコードの構造体、PacketBufferpacked も同様に外してみます。すると上と同じように、ハーフワードアクセスになりました。
すると、、、なんと、変な記述をしなくても送受信に成功しました

そもそも各トランザクションが上手く行っているのかの確認

このペリフェラルは PMA オーバーフロー等のメモリ関連や通信フレーム関連のエラーは捕捉してくれますが、STALL 等のトランザクション単位でのエラー発生時は状況がよくわかりません。なんだそれ。なので逐一エンドポイントレジスタを監視する感じで状況を把握します。

以下にその結果を示しましょう。

SET_ADDRESS時

タイミング EP0R (hex) DTOG_RX DTOG_TX STATUS_RX STATUS_TX
SETUP待機 3200 0 0 VALID DISABLED
SETUP受信 6260 1 1 NAK NAK
ステータス送信準備 6270 1 1 NAK VALID

ユーザーマニュアルには SETUP 受信時には {DTOG_RX, DTOG_TX} = {0, 1} になるよって書かれているのに、異なっている。でもまあ普通に考えて、両方とも 1 のほうが都合いいよね。
なお、STATUS 値はマニュアルどおり、どちらも NAK になっていて良き良き。

GET_DESCRIPTOR (8Bytes)時

タイミング EP0R (hex) DTOG_RX DTOG_TX STATUS_RX STATUS_TX
SETUP待機 fa60 1 1 VALID NAK
SETUP受信 7260 1 1 VALID NAK
送信準備 7270 1 1 VALID VALID

...あれ?STATUS_OUT の受信どこ行った??
何らかの理由で、ここで止まっているのかも...

GET_DESCRIPTOR (16Bytes)時

タイミング EP0R (hex) DTOG_RX DTOG_TX STATUS_RX STATUS_TX
SETUP待機 fa60 1 1 VALID NAK
SETUP受信 7260 1 1 VALID NAK
SEND受信 7270 1 1 VALID VALID

う〜ん。。。上と同じ状況。。。

つまり...?

以上を鑑みて、何かしらの通信が欠損していることが考えられます。その後も貧相な print デバッグで色々と見てみましたが、どうやら SETUP パケットの受信に失敗するっぽい事が分かりました。もう何もかもわからん。

ここまでのソースコードはこちら

なんとかします。