STM32F4で大量の相手にUARTを送信したい

## はじまり

電子工作用の周辺回路では、UARTでコマンドを出すものがいくつかある。そのおかげか、STM32F405RGTではUARTが6本あったりする。しかし、SPIやI2Cと違って一意のデバイスを選択する手段がない場合がほとんどなので、周辺回路1個にUART1本が必要になったりすると、6本では心もとない。

いくつかの方法で複数のUART送信を実現してみる。

大抵の場合コマンドを送信できれば十分なので、今回は受信は考慮しない。

## テスト方法

適当なデータを送信する。今回は以下のデータ列を使う。

```c++
static const uint8_t data[13][2] = {
    {0x12, 0x23},
    {0x34, 0x45},
    {0x56, 0x67},
    {0x78, 0x89},
    {0x9A, 0xAB},
    {0xBC, 0xCD},
    {0xDE, 0xEF},
    {0xF0, 0x01},
    {0x12, 0x23},
    {0x34, 0x45},
    {0x56, 0x67},
    {0x78, 0x89},
    {0x9A, 0xAB},
};
```

FreeRTOSのスタックをCCMRAMに置いているので、staticで宣言している。

今回はGPIOCの0から12までの13本をUART TXとして使用する。

## ビットバンド

ARMのCortex-M3に搭載されているビットバンドという手段を使う方式(M4やM4Fでも使用可能)。ビットバンドを使うと任意の1bitを読み書きできる。

```c++
uint32_t *bit_band_calc_address_peripheral_area(volatile uint32_t *const regi, const uint8_t bit_pos)
{
    // 計算間違ってるかも

    const uint32_t addr = reinterpret_cast<uint32_t>(regi);
    const uint32_t byte_offset = addr - 0x40000000;
    const uint32_t bit_word_offset = byte_offset * 32 + bit_pos * 4;
    uint32_t *const bit_word_addr = reinterpret_cast<uint32_t *>(bit_word_offset + 0x42000000);

    return (bit_word_addr);
}


template <uint16_t N>
void send_uart_bit_band(UART_HandleTypeDef &huart,
                        const uint8_t (&data)[N],
                        GPIO_TypeDef *const src_port,
                        const uint8_t src_pin_index,
                        GPIO_TypeDef *const dst_port,
                        const uint8_t dst_pin_index)
{
    while (!__HAL_UART_GET_FLAG(&huart, UART_FLAG_TXE))
    {
    }

    volatile const uint32_t *const copy_src = bit_band_calc_address_peripheral_area(&src_port->IDR, src_pin_index);
    volatile uint32_t *const copy_dst = bit_band_calc_address_peripheral_area(&dst_port->ODR, dst_pin_index);

    for (const uint8_t d : data)
    {
        taskENTER_CRITICAL();

        huart.Instance->DR = d;

        while (!__HAL_UART_GET_FLAG(&huart, UART_FLAG_TC))
        {
            *copy_dst = *copy_src;
        }

        taskEXIT_CRITICAL();
    }
}
```

こんな感じで使う。

```c++
for (uint8_t i = 0; i < sizeof(data) / sizeof(data[0]); i++)
{
    send_uart_bit_band(huart2, data[i], GPIOA, 2, GPIOC, i);
}
```

転送元(UARTペリフェラルTXピン)と転送先(任意のGPIO OUT)を引数に渡すと、UARTのピン状態をUARTの送信が終わるまで反映し続ける。
途中で上位のタスクや割り込みが入ると反映ができなくなるので、CRITICALで囲っている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/f48f939c-e987-47b9-d5f8-62ef54187e52.png)

115.2kbaudで送信している。紫のUART2TX(PA2)をPC0-PC12の各GPIOに転送できていることがわかる。

9.6kbaudで20バイト転送するとクリティカル区間で約20msecブロックされるので、RTOSで使う手段としては下策。自身では割り込み等は一切使わないので、数十msecブロックしても問題ないならお手軽。

## EXTI

ピンの論理変化を伝えれば十分なのに、whileで回すと論理が変化していないときでも転送を続ける必要がある。幸いなことにEXTIというピン変化割り込みが使えるので、論理が変化したときだけビットバンド転送を行う。

```c++
// bit_band_calc_address_peripheral_areaの実装は前述と同じなので省略

// 割り込み有効、イベント無効、両エッジでトリガ
void EXTI_enable(GPIO_TypeDef *const port, const uint8_t pin_index)
{
    const uint32_t iocurrent = 1 << pin_index;

    {
        uint32_t temp = SYSCFG->EXTICR[pin_index >> 2U];
        temp &= ~(0x0FU << (4U * (pin_index & 0x03U)));
        temp |= GPIO_GET_INDEX(port) << (4U * (pin_index & 0x03U));
        SYSCFG->EXTICR[pin_index >> 2U] = temp;
    }

    EXTI->IMR |= iocurrent;  // interrupt enable
    EXTI->EMR &= ~iocurrent; // event disable
    EXTI->RTSR |= iocurrent; // rising enable
    EXTI->FTSR |= iocurrent; // faling enable
}

volatile const uint32_t *EXTI2_copy_src = 0;
volatile uint32_t *EXTI2_copy_dst = 0;

extern "C" void EXTI2_IRQHandler(void)
{
    if (__HAL_GPIO_EXTI_GET_IT(EXTI_LINE_2))
    {
        if (EXTI2_copy_src && EXTI2_copy_dst)
        {
            *EXTI2_copy_dst = *EXTI2_copy_src;
        }

        __HAL_GPIO_EXTI_CLEAR_IT(EXTI_LINE_2);
    }
}

template <uint16_t N>
void send_uart_EXTI(UART_HandleTypeDef &huart,
                    const uint8_t (&data)[N],
                    GPIO_TypeDef *const src_port,
                    const uint8_t src_pin_index,
                    GPIO_TypeDef *const dst_port,
                    const uint8_t dst_pin_index,
                    const IRQn_Type EXTI_IRQn)
{
    while (!__HAL_UART_GET_FLAG(&huart, UART_FLAG_TXE))
    {
    }

    EXTI2_copy_src = bit_band_calc_address_peripheral_area(&src_port->IDR, src_pin_index);
    EXTI2_copy_dst = bit_band_calc_address_peripheral_area(&dst_port->ODR, dst_pin_index);

    EXTI_enable(src_port, src_pin_index);
    HAL_NVIC_EnableIRQ(EXTI_IRQn);

    HAL_UART_Transmit_DMA(&huart, const_cast<uint8_t *>(data), N);

    while (!__HAL_UART_GET_FLAG(&huart, UART_FLAG_TC))
    {
        // osDelay(1);
    }

    HAL_NVIC_DisableIRQ(EXTI_IRQn);

    EXTI2_copy_src = 0;
    EXTI2_copy_dst = 0;
}
```

こんな感じで使う。

```c++
for (uint8_t i = 0; i < sizeof(data) / sizeof(data[0]); i++)
{
    send_uart_EXTI(huart2, data[i], GPIOA, 2, GPIOC, i, EXTI2_IRQn);
}
```

STM32F4のEXTIはオルタネートと同時使用が可能だが、HAL_GPIO_Initでは同時使用の初期化ができないので、自前で初期化する必要がある。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/3b055459-1a4a-34ef-4a74-d32edca5a6fd.png)

これだけ見るとビットバンドでゴリ押しとあまり変わらない。

ループ内のosDelay(1)のコメントアウトを解除するとこんな感じになる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/f9daedac-6ecb-749f-4a0f-43fd431fc626.png)

1msec毎にポーリングするようになるのでデータ転送に隙間が空くが、転送中はcore_busy(一番下の灰色)がLOWに落ちており、コアの計算リソースがあまり使用されていないことがわかる。転送中に時々core_busyがHIGHになっているが、これはEXTI割り込みの処理を行っている部分。UART2TX(PA2)が変化したタイミングでEXTI割り込みが発生しており、PC0-PC3へ正常に転送できていることがわかる。キャプチャ期間の関係でPC3までしか表示されていないが、もちろんPC12まで正常に転送できている。

EXTIを扱う必要があるので若干面倒だが、比較的使いやすい方法。
実際に使う場合は、EXTIの優先度を高く設定しておく必要がある。

## タイマ

UARTを送信するだけなら、適切なタイミングでビット操作を行うだけなので比較的簡単。いわゆるソフトウェアシリアルポート。
タイミングを作るのにはタイマを使う。チープなマイコンだとタイマ割り込みを使ったりするが、今回使うマイコンにはDMAが乗っているので、データを与えてしまえばハードウェアで転送を行える。

注意点として、STM32F1等ではすべてのDMAが使用できるが、STM32F4等ではDMA1は使用できず、DMA2を使用する必要がある。DMA2を使えるのはTIM1とTIM8だけなので、このどちらかを使用する。

データハンドリングがちょっと面倒なので、クラスを作る。

```c++
template <uint16_t max_octets>
class Timer_UART
{
public:
    Timer_UART(TIM_HandleTypeDef &htim,
               GPIO_TypeDef *const port,
               const uint16_t use_pins_mask)
        : htim(&htim),
          port(port),
          mask(use_pins_mask)
    {
        clear_buffer();
    }

    void clear_buffer(void)
    {
        const uint32_t bsrr_reset_val = mask | mask << 16;

        for (uint32_t &a : buff)
        {
            a = bsrr_reset_val;
        }

        use_buff_len = 0;
    }

    template <uint16_t N>
    void set_data(const uint8_t (&data)[N], const uint8_t pin_index)
    {
        if (N <= max_octets)
        {
            const uint32_t pin = 1 << pin_index;

            for (uint16_t i = 0; i < N; i++)
            {
                const uint8_t d = data[i];

                buff[i * 10 + 0] &= ~pin;
                buff[i * 10 + 1] &= ~(d & 0x01 ? 0 : pin);
                buff[i * 10 + 2] &= ~(d & 0x02 ? 0 : pin);
                buff[i * 10 + 3] &= ~(d & 0x04 ? 0 : pin);
                buff[i * 10 + 4] &= ~(d & 0x08 ? 0 : pin);
                buff[i * 10 + 5] &= ~(d & 0x10 ? 0 : pin);
                buff[i * 10 + 6] &= ~(d & 0x20 ? 0 : pin);
                buff[i * 10 + 7] &= ~(d & 0x40 ? 0 : pin);
                buff[i * 10 + 8] &= ~(d & 0x80 ? 0 : pin);
            }

            const uint16_t use_len = N * 10;
            if (use_buff_len < use_len)
            {
                use_buff_len = use_len;
            }
        }
    }

    void transmit(const uint16_t PSC, const uint16_t ARR)
    {
        if (use_buff_len > 0)
        {
            htim->Instance->PSC = PSC;
            htim->Instance->ARR = ARR;
            HAL_TIM_GenerateEvent(htim, TIM_EVENTSOURCE_UPDATE);

            htim->State = HAL_TIM_STATE_BUSY;
            htim->hdma[TIM_DMA_ID_UPDATE]->XferCpltCallback = 0;
            htim->hdma[TIM_DMA_ID_UPDATE]->XferHalfCpltCallback = 0;
            htim->hdma[TIM_DMA_ID_UPDATE]->XferErrorCallback = 0;

            HAL_DMA_Start_IT(htim->hdma[TIM_DMA_ID_UPDATE],
                             reinterpret_cast<uint32_t>(buff),
                             reinterpret_cast<uint32_t>(&port->BSRR),
                             use_buff_len);

            __HAL_TIM_ENABLE_DMA(htim, TIM_DMA_UPDATE);
            __HAL_TIM_ENABLE(htim);

            while (0 < __HAL_DMA_GET_COUNTER(htim->hdma[TIM_DMA_ID_UPDATE]))
            {
                osDelay(1);
            }

            __HAL_TIM_DISABLE_DMA(htim, TIM_DMA_UPDATE);
            __HAL_TIM_DISABLE(htim);
            htim->State = HAL_TIM_STATE_READY;
        }

        clear_buffer();
    }

protected:
private:
    TIM_HandleTypeDef *const htim;
    GPIO_TypeDef *const port;
    const uint16_t mask;

    uint32_t buff[max_octets * 10];
    uint16_t use_buff_len;
};
```

こんな感じで使う。

```c++
const uint16_t use_pins_mask = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
                                GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 |
                                GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 |
                                GPIO_PIN_12;

static Timer_UART<10> timer_uart(htim1, GPIOC, use_pins_mask);

for (uint8_t i = 0; i < sizeof(data) / sizeof(data[0]); i++)
{
    timer_uart.set_data(data[i], i);
}

timer_uart.transmit(0, 168'000'000 / 115'200 - 1);
```

クラス内の配列をDMAに渡すので、やはりstaticで宣言する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/44d6dcb9-3090-d596-42cc-dcfec76df4a8.png)

タイマとDMAで転送するので、転送中はコアリソースが消費されていない。
転送前にデータ変換で多少リソースを喰っている。転送完了時もDMA完了割り込みが入る。1msec毎にポーリングしているので、処理開始(Aバー)の1msec後に、終了処理が行われている(Bバーの位置)。

この方法では、マイコンのUARTペリフェラルに関係なく、任意の本数(およそ16本以下)を同時に転送できる。ピン数の多いマイコンとかなら、最大で80本くらい同時に出せそうな気がする(未確認)。

今回は任意のピンのみを使うためにBSRRを使ったが、16本丸ごとUARTに割り当てるならODRを使ったほうがお得。

## まとめ

### ビットバンド無限ループ

利点

* とにかくお手軽(割り込みとか不要)

欠点

* 他の処理ができない(コアリソースを専有させる必要がある)

### EXTI割り込み

利点

* それなりにバランスが取れている
* 優先度に注意すれば、他のタスクや割り込みと共存可能

欠点

* 割り込みが若干タイミングクリティカル(上位優先度で少し長めの処理をされると辛い)

### タイマ転送

利点

* UARTペリフェラル自体が不要
* DMAで転送するのでタイミングクリティカルな処理が不要
* 複数の相手に同時に送信できる

欠点

* メモリがそれなりに必要(最大転送量の4倍のメモリが必要)
* ハードウェアリソース要求が大きい(TIM1/TIM8のどちらかを専有)
* ソースコードが比較的大規模

### 総評

個人的にはタイマ転送が有利だと思うが、タイマ+DMA+GPIOの組み合わせは他にもいろいろ使えるので、状況に合わせてEXTIと使い分け、という感じか。