SNSへはこちら

STM32でUSBをベアメタル(2) - SET_ADDRESSが...

こんにちは。早速絶不調です。一応タイトル通りに微妙に実装は出来ましたが、正直訳がわかっていないところが多すぎます。まあとりあえず細かいところはちょっと置いておいて、動かすことに専念しましょう。

以下に現状のコードを置きます。やりながらの記事執筆ですので、間違いもあるでしょうが、それは回を重ねるごとに直っていくことでしょう。

やるぞ

ひとまず、直感的にコードを書き書きしていきます。ペリフェラルの初期化コード。

void usb_init(void) {
    RCC->APB1ENR |= RCC_APB1ENR_USBEN;

    USB->CNTR &= ~USB_CNTR_PDWN;
    for(volatile int i=0; i<100; i++); // wait for voltage to be stable
    USB->CNTR &= ~USB_CNTR_FRES;
    USB->BCDR |= USB_BCDR_DPPU; // pull-up DP so as to notify the connection to the host

    USB->CNTR = USB_CNTR_RESETM; // enable reset interrupt
    NVIC_SetPriority(USB_IRQn, 0); // Highest Priority
    NVIC_EnableIRQ(USB_IRQn);
}

リファレンスマニュアルによると、 PDWN を無効化した後は一定時間置いてほしいとのことなので、ごく適当ですが無駄ループで時間稼ぎをしています。事実これで動いてるっぽいのでいいでしょう。
あとは忘れずに割り込みの有効化。コメントにもある通り、優先度は最大にしました。
リセット以外に割り込み要因あるだろ!」って思う方もいると思いますが、デバイスはリセット前にはいかなる要求にも応答してはならない(リセット要求以外)とされているので、現状この設定で合っているものと思われます。これ以外の割り込みは、リセット割り込みが入った後の割り込みハンドラで行うつもりです。

あ、そうそう。事前に HSI48 を有効化し、CPU 自体もそれで動かしています。まずはこの関数を呼ぶ必要がありますね。

// 上のusb_initを呼ぶ前に、main関数で呼んでおく
void sysclk_HSI48_init(void) {
    RCC->CR2 |= RCC_CR2_HSI48ON;
    while( !(RCC->CR2 & RCC_CR2_HSI48RDY) );
    RCC->CFGR |= RCC_CFGR_SW_HSI48;
    while( (RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_HSI48 );
}

ユーティリティの準備

本格的な実装をする前に、PMA への領域確保等の関数を実装しておく必要があります。

データ配置

malloc / free に対応するかんたんな関数です。非常にミニマルであんまり安全ではない実装ですが、とりあえずこれで行きましょう。

static uint16_t alloc(int size) {
    uint16_t ret = pma_sp;
    pma_sp += size;
    return ret;
}
static void dealloc(int size) {
    pma_sp -= size;
}

pma_sp というのは PMA のためのスタックポインタです。別にスタックのような使い方をしているわけではないのですが、感覚的にそう命名しました。
特に領域確保時に初期化をするわけでもなく、とりあえず pma_spsize 分だけずらすだけです。

データのコピー

前の記事で書いたとおり、PMA は 16bit アクセスしか出来ません。なので 8bit アクセス等をすることが出来ず、非常に不便なわけです。
よって、通常の RAM と PMA とでデータのやり取りを行う関数をまたもや雑に実装してみました。

static void pma_out(void *dest, uintptr_t src, int size) {
    volatile uint16_t *src_p = (void *)src + USB_PMAADDR;
    volatile uint16_t *dest_p = dest;
    if( size & 1 ) size++;
    size /= 2;

    for(int i = 0; i < size; i++) {
        *(dest_p + i) = *(src_p + i);
    }
}
static void pma_in(uintptr_t dest, void *src, int size) {
    volatile uint16_t *src_p = src;
    volatile uint16_t *dest_p = (void *)dest + USB_PMAADDR;
    if( size & 1 ) size++;
    size /= 2;

    for(int i = 0; i < size; i++) {
        *(dest_p + i) = *(src_p + i);
    }
}

もし size が奇数だったら、インクリメントして偶数に丸めます。これで希望のデータをコピーできます。
そういえば上の alloc / dealloc にはこの操作をしてませんね。やっておけば良かった。でもまあ実装しちゃったので... 後に修正する予定とします。

バッファテーブル関連

USB ペリフェラルでは PMA 上に バッファテーブルなるものを置いて、そこで送受信のためのデータを書き込みます。そうすることで、アプリケーションはペリフェラルにどのデータをどの長さで通信するかを通知することができるのです。それ用の関数です。

static void usb_endpoint_set_status(int endp, uint32_t status, uint32_t mask) {
    const uint32_t reg = USB_EPnR(endp);
    USB_EPnR(endp) = (reg & (USB_EPREG_MASK | status)) ^ status;
}

static void usb_ep0_send(uint16_t addr, int size) {
    *(uint16_t *)&g_packet_buffer[0].addr_tx = addr;
    *(uint16_t *)&g_packet_buffer[0].count_tx = size;
    usb_endpoint_set_status(0, USB_EP_TX_VALID, USB_EPTX_STAT);
}
static void usb_ep0_receive(uint16_t addr, int size) {
    *(uint16_t *)&g_packet_buffer[0].addr_rx = addr;
    if( size > 62 ) {
        *(uint16_t *)&g_packet_buffer[0].count_rx = (size / 32) << 10;
        *(uint16_t *)&g_packet_buffer[0].count_rx |= 1 << 15;
    }else{
        *(uint16_t *)&g_packet_buffer[0].count_rx = size << 10;
    }
    usb_endpoint_set_status(0, USB_EP_RX_VALID, USB_EPRX_STAT);
}

操作は割と単純。送信/受信のどれかに対応するバッファテーブルに対してデータの書き込みを行い、そうしてエンドポイントレジスタのステータスを書き換えるだけ。このステータスが1を書き込むことでトグルするという若干厄介なものになっているので、既存のデータを一度読みだした上で、設定値と XOR を取ることによって所望の値に設定することが可能になっています。

*(uint16_t *)&

この奇妙な書き方が目につくと思います。実はよくわかりません。よく分からないのですが、こう書かないとデータの書き込みが反映されないのです。読み込みは OK なんだけどね...
でもこれ、非常に気持ち悪いですよね。今度になってしまいますが、この謎をアセンブリレベルで解読していきたいと思います。

割り込み関数

リセット割り込みから始まる割り込み関数です。

void USB_IRQHandler(void) {
    uint32_t flag = USB->ISTR;

    if( flag & USB_ISTR_RESET ) {
        iprintf("\nRESET\n");
        USB->ISTR &= ~USB_ISTR_RESET;
        USB->CNTR |= USB_CNTR_CTRM | USB_CNTR_RESETM;
        pma_sp = 0;
        USB->BTABLE = alloc(sizeof(PacketBuffer) * 1);
        g_packet_buffer = (PacketBuffer *)((uintptr_t)USB->BTABLE + USB_PMAADDR);
        USB->DADDR = USB_DADDR_EF; // enable the functionality

        // initialize EP
        USB->EP0R = USB_EP_CONTROL;
        pma_packet_buffer_offset = alloc(sizeof(USBSetupPacket));
        usb_ep0_receive(pma_packet_buffer_offset, sizeof(USBSetupPacket));
    }

    while( (flag = USB->ISTR) & USB_ISTR_CTR ) {
        const int ep_num = flag & USB_ISTR_EP_ID;
        const uint32_t epreg = USB_EPnR(ep_num);

        if( ep_num == 0 ) {
            if( flag & USB_ISTR_DIR ) {
                // DIR == 1 : IRQ by SETUP transaction. CTR_RX is set.
                USB_EPnR(0) = (epreg & ~USB_EP_CTR_RX & USB_EPREG_MASK);
                if( epreg & USB_EP_SETUP ) {
                    pma_out((void *)&current_setup_packet, pma_packet_buffer_offset, sizeof(USBSetupPacket));
                    iprintf("** SETUP\n");
                    iprintf("bmResuestType: %d\n", current_setup_packet.bmRequestType);
                    iprintf("bRequest: %d\n", current_setup_packet.bRequest);
                    iprintf("wLength: 0x%x\n", current_setup_packet.wLength);
                    iprintf("wValue: 0x%x\n", current_setup_packet.wValue);
                    usb_ep0_handle_setup();
                }else{
                    // IRQ by OUT transaction
                }
            }else{
                // DIR = 0 : IRQ by IN transaction. CTR_TX is set.
                USB_EPnR(0) = (epreg & ~USB_EP_CTR_TX & USB_EPREG_MASK);
            }
        }

    }
}

長いですが、最初のリセット割り込みで各種パラメータ等を初期化しています。

CTR 割り込み(正しいデータ転送割り込み)が本体です。何か複数データ受信に備えて while ループを形成しておくのが流儀なようです。
内部では禁断の割り込み関数内部で printf をしていますが、許して。現状これで動いているのでいいでしょ、と思ってください。

コード全景

こちらの stm32f042_usb_no2.c をどうぞ。一部この記事に記載していない部分がありますのでぜひ一度ご覧ください。

ちなみに usb_packet.h の中身は以下です。

typedef struct
__attribute__((packed)) {
    uint8_t bmRequestType;
    uint8_t bRequest;
    uint16_t wValue;
    uint16_t wIndex;
    uint16_t wLength;
} USBSetupPacket;

動作結果

iprintf は USART ペリフェラルで値を出力することとしています。とりあえずターミナルでシリアル通信結果をウォッチしました。

RESET

RESET
** SETUP
bmResuestType: 0
bRequest: 5
wLength: 0x0
wValue: 0x2b

RESET
** SETUP
bmResuestType: 0
bRequest: 5
wLength: 0x0
wValue: 0x2a

RESET
** SETUP
bmResuestType: 0
bRequest: 5
wLength: 0x0
wValue: 0x28

RESET
** SETUP
bmResuestType: 0
bRequest: 5
wLength: 0x0
wValue: 0x2d

RESET
** SETUP
bmResuestType: 0
bRequest: 5
wLength: 0x0
wValue: 0x2c

RESET
** SETUP
bmResuestType: 0
bRequest: 5
wLength: 0x0
wValue: 0x30

RESET
** SETUP
bmResuestType: 0
bRequest: 5
wLength: 0x0
wValue: 0x2f

RESET
** SETUP
bmResuestType: 0
bRequest: 5
wLength: 0x0
wValue: 0x2e

ん?? SET_ADDRESS が成功していません。というのも、ホストから bRequest == 5 の SETUP パケットが何度も送出されているからです。アドレス値自体はどうやら受け取れているっぽいので、多分ステータスステージの0バイトデータ送信がうまく行ってない説ありますねこりゃ。次回までになんとかします。