STM32 を使用して PWM シリアル通信

若干ワナにハマったのでメモ。

PWMを使って通信を行うことがある。以下のような波形でシリアル通信を行う。

周波数自体は一定の周波数で、HとLの時間の比率を変えることで1と0を表現する。
クロックとデータを合わせて一本の信号線で送信できるので便利である。
ただCPUを使って高速で通信するには向かないし、STM32にこの機能があるかというと 微妙なので、TIM+DMAを使って実装する。

GPIOのON / OFF をいずれもハーフワードの書き込みで制御可能なBSRRというGPIOの レジスタを使用する。要するにこのレジスタをターゲットとしてDMA転送を行う 図に書き込んだようにTIMのUPDATEで、GPIOのビットを立てる
以上の操作は「1の時だけCC1でGPIOのビットを下げる」という動作で実装可能である。
(1の送信時にCC2でビットを下げても、既にCC1で下がっているので問題ない)。
つまり、UPDATEとCC2は固定値でよく、CC1だけ、送信しようとするデータからDMAで動作させれば良い。

ここで罠がある。
①update
②CC1
③CC2
という順序なら良いが、実際には、 ①CC1 ②CC2 ③update という順序なのである。よってupdateを先に実行すべく、タイマのカウンタ値を細工する。
  TIM->ARR = PULSE_GAP;
  TIM->EGR = TIM_EGR_UG;
  TIM->CNT = PULSE_GAP-1;

※TIMはcountup モードを想定、PULSE_GAPはパルスの周期を示す固定値とする
とすれば良さそうである。

だがここでさらに罠がある。

まず罠1、TIM->EGR = TIM_EGR_UG にて、update のトリガが効くようになるが、このときCNTが0にされてしまう。
・・・だから下の行で、TIM->CNT = PULSE_GAP-1;としていて、一見問題なさそうに見える。
ところが罠2、上記3行はこの順序で実行されるとは限らない(最適化ってコワい)。扱っているデータに(CPU目線で)関係性がないからである。
罠3、上記3行は関連性がないだけに、PipeLineが最適に実行されてしまう。
ハードウェアの機能で、CNTをリセットするのと、CNTに値を設定する処理がぶつかってしまう。

じゃどうするか
static volatile void SetCNT(){
  TIM->CNT = PULSE_GAP-1;
}
  :
  :
  TIM->ARR = PULSE_GAP;
  TIM->EGR = TIM_EGR_UG;
  SetCNT();

一見ばかばかしいが、これで正しく動作する。