STM32F303K8 ミリ秒遅延の実装(mdelay)

# はじめに
STM32 マイコンで他デバイスと通信を行い, 結果を取得するまでに遅延処理が必要であったため Basic Timer (TIM6 / TIM7) を用いて遅延処理を実装.
`nop` 命令を使用して遅延を実装した方法 [stm32:wait,delay処理 - Qiita](https://qiita.com/takoke/items/9e53fe12d48caa49cd40) や `HAL_Delay` (SysTick割り込み)を使用した方法 [HAL_Delay 関数の注意点](https://lawn-tech.jp/hal_delay.html) などが非常に参考になる.

## 開発環境
HAL ライブラリや STM32CubeMX などの環境の使用は想定していないため, 使用するレジスタのアドレス及び値を事前に定義する必要がある.

| | 名称 | 備考 |
| :--- | :--- | :--- |
| コンパイラ | arm-none-eabi-gcc | 11.2.0 |
| 評価ボード | NUCLEO-F303K8 | STM32F303K8 |

# 遅延処理の実装

## RCC レジスタ設定
### システムクロック設定と TIM6 の有効化
はじめに, STM32F303K8 のシステムクロックを HSI(8MHz) に設定. 以下, この値を順にしてプリスケーラ, クロック数等の値を設定.

```rcc.h
#define RCC_ADDR (0x40021000)
#define RCC ((volatile struct rcc_t *)(RCC_ADDR))
```

```main.c
  // 内部クロック 8Mhz を使用
  if((RCC->CFGR & RCC_CFGR_SWS_HSI) != RCC_CFGR_SWS_HSI) {
    RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_HSI;

    while((RCC->CFGR & RCC_CFGR_SWS_HSI) == 0);
  }
  // TIM6 タイマクロックの有効化
  RCC->APB1ENR |= RCC_APB1ENR_TIM6EN | RCC_APB1ENR_TIM6EN;
```

## IRQ 割り込みの有効化と優先順位の指定
上記の `HAL_Delay 関数の注意点` で記載されているのと同様に, この実装方法でも割り込みの優先順位に注意する必要がある.

```main.c
  // TIM6, DAC2 割り込み
  NVIC_enable_irq(TIM6DAC1_IRQn);

  // 割り込みの優先順位を指定
  NVIC_set_priority(TIM6DAC1_IRQn, 0x00);
```

## 遅延処理
遅延処理は, 指定時間が経過するまでポーリングすることで実現している.
`TIM6->RSC = 7999` このプリスケーラの値で, 1クロック辺りに必要とする時間を指定. 今回の場合 システムクロックを 8[MHz] としているため, カウンタクロック周波数 $f_{\rm{CK\\_CNT}}$ は,

```math
f_{\rm{CK\_CNT}} = \frac{f_{\rm{CK\_PSC}}}{7999 + 1} = \frac{8.0 \times 10^6}{8.0 \times 10^3} = 1.0 \times 10^3 [\rm{Hz}] = 1.0 [\rm{kHz}]
```

であり, その逆数である周期 $T$ は

```math
T = \frac{1}{f} = \frac{1}{1.0 \times 10^6} = 1.0 \times 10^{-3} [\rm{s}] = 1.0 [\rm{ms}]
```

となる. 周期 $T$ とカウントするクロックは同じであるため, 自動再ロードレジスタ `TIM6->ARR` に遅延させたい秒数 [ms] を代入すれば指定時間後に割り込みが発生する.

```mdelay.c
volatile static uint8_t _f_timeout;

void TIM6_DAC1_handler(void) {
  uint32_t sr = TIM6->SR;

  if((sr & TIM67_SR_UIF) == TIM67_SR_UIF) {
    // TIM6 割り込み
    TIM6->SR = 0;
    _f_timeout = 1;
  }
}

void mdelay(uint16_t msec) {
  if(msec == 0) {
    return;
  }

  _f_timeout = 0;

  // ms
  TIM6->PSC  = 7999;
  TIM6->ARR  = msec;
  TIM6->CNT  = 0;
  TIM6->CR1  =
    TIM67_CR1_OPM |
    TIM67_CR1_URS;

  NVIC_enable_irq(TIM6DAC1_IRQn);
  NVIC_set_priority(TIM6DAC1_IRQn, 0);

  TIM6->DIER = TIM67_DIER_UIE;
  TIM6->CR1 |= TIM67_CR1_CEN;

  while(! _f_timeout) {
    NOP();
  }

  NVIC_disable_irq(TIM6DAC1_IRQn);

  TIM6->CR1 &= TIM67_CR1_CEN;
  TIM6->DIER = 0;
}
```

# 実行結果
実行したコード(PB3 の LED 点滅)と`Hantek DSO5072P` で計測したデータを下図に示す.

```main.c
  uint8_t _f_switch = 0;

  while(1) {
    // 20[ms] の遅延. 1周期当たり 40[ms]
    mdelay(20);

    if(_f_switch) {
      GPIOB->BRR |= (1 << 3);
    } else {
      GPIOB->ODR |= (1 << 3);
    }
    _f_switch ^= 1;
```

![pic_156_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33399/c711f2f7-5851-af7a-8dcb-4c3530b5fc70.png)

10[ms], 20[ms] 以上であれば十分使用できる結果ではないだろうか.
1[ms] のように極端に小さい値にすると, `if` 分岐やレジスタへの代入等の影響で誤差が大きくなる.
また, プリスケーラの値を変更して, $10^{-6}$ [s] で `udelay` の実装を行ったが, こちらの場合は設定や呼び出しなどで 30[us] 程かかってしまうため, システムクロック数を上げたり処理を最小限に最適化する必要がある.

# Appendix
補足コード等

## NVIC 有効化, 無効化, 優先順位指定
```nvic.c
void NVIC_enable_irq(irq_type_t type) {
  if(type >= 0) {
    uint8_t offset = type / 32,
            shift_size = type % 32;

    *(NVIC->ISER + offset) = 1 << shift_size;
  }
}

void NVIC_disable_irq(irq_type_t type) {
  if(type >= 0) {
    uint8_t offset = type / 32,
            shift_size = type % 32;

    *(NVIC->ICER + offset) = 1 << shift_size;
  }
}

void NVIC_set_priority(
    irq_type_t type,
    uint8_t priority) {
  if(type >= 0) {
    uint8_t offset = type / 4,
            shift_size = type % 4;

    *(NVIC->IPR + offset) |= ((uint32_t)priority << (8 * shift_size + 4));
  } else {
    // TODO SysTick などの割り込み優先順位を設定
  }
}
```