マイコンのプログラミングをしてるときに、ペリフェラルのレジスタとかを見たいことがありますよね。デバッガやprintfを使う手もありますが、往々にして「printfを入れると動く」という事態になりかねません。これはprintfの処理を行っている期間が適当なディレイとして機能して「動いてしまう」例です。リアルタイム性が重要な状況の場合、この影響を可能な限り小さくしたいわけです。
ところで、STM32には大変便利なSPIという機能があります! これを使うと極めて少ない処理で32bitの値を出力できます!! 早速使ってみましょう!!!
デバッグ用に一時的に使いたい、という場合を想定して、最低限必要な初期化コードだけを書いていきます。
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_SPI1_CLK_ENABLE();
GPIO_InitTypeDef gpio = {
.Pin = GPIO_PIN_3 | GPIO_PIN_5,
.Mode = GPIO_MODE_AF_PP,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_HIGH,
.Alternate = GPIO_AF5_SPI1,
};
HAL_GPIO_Init(GPIOB, &gpio);
SPI_HandleTypeDef hspi = {
.Instance = SPI1,
.Init = SPI_InitTypeDef{
.Mode = SPI_MODE_MASTER,
.Direction = SPI_DIRECTION_2LINES,
.DataSize = SPI_DATASIZE_16BIT,
.CLKPolarity = SPI_POLARITY_LOW,
.CLKPhase = SPI_PHASE_1EDGE,
.NSS = SPI_NSS_SOFT,
.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2,
},
};
HAL_SPI_Init(&hspi);
__HAL_SPI_ENABLE(&hspi);
この設定により、コアクロック168MHzであればSCLKが42MHzで駆動され、1回毎に16bitのデータが出てきます。
あとは、必要なときに以下のように出力を行います。
extern "C" void I2C1_EV_IRQHandler(void)
{
SPI1->DR = I2C1_EV_IRQn;
__NOP();
__NOP();
__NOP();
__NOP();
SPI1->DR = i2c.SR1 & 0xFFFF;
// 以下、割り込み内での処理
SPIの通信が行われていないとき(BSYがクリアされているとき)にDRへ書き込んだ場合、ほぼ即時に送信バッファへの転送が行われ、その時点でTXEがセットされます。ただ、さすがにノーウェイトで連続して書き込むと間に合わないことがあるので、NOPで待っています。とりあえず4サイクルくらい待てば足りそうな感じですが、このあたりは必要に応じて調整してください。
whileでTXEがセットされるのを待つのが確実ですが、ビットテストとループはそれなりのサイクル数が必要なので、必要以上の待ち時間が発生してしまいます(それでも数百ナノ秒程度ですが)。
今回はプリスケーラを2に設定していますが、この値を大きくする場合(ビットレートを下げた場合)はDRの転送により長い時間が必要なため、その場合はwhileで監視したほうが確実です。
前述の例では、1回目にIRQnを出力し、2回目にステータスレジスタの下位16bitを出力しています。これにより「場所」と「値」を出力できます。ただし、Cortexコアに関わるIRQn(SysTick_IRQn
等)は負数が指定されているため、これを使う場合は注意が必要です。
他にも、SPI1->DR = __LINE__ & 0xFFFF;
のようにすればソースコードの通過位置を出力することもできます(あまり高頻度にやると出力が間に合わないので注意が必要ですが)。
あとは、SPIをロジックアナライザで監視して、仮想スレーブセレクト、ビット長32、といった設定でデコードすれば、4オクテットを見ることができます。
雰囲気としては、4オクテットの送信に40コアクロック未満で足りそうな感じです。それすらも避けたい場合は、2オクテットに限定すればさらに少ないコアクロックで転送できます。printfのような高コストな方法と比べると、ほぼ無負荷に近いレベルで値の出力が行なえます。
割り込みハンドラ内で使うと、割り込みの発生したタイミングとステータスレジスタの中身を同時に出力できるので便利です。GPIOのトグルでもタイミングは確認できますが、あくまでも「ここで割り込みが発生した」ということしかわかりません。SPIで4オクテット転送を使った場合は「どの割り込みが発生したか」「ステータスレジスタの値は何か」という情報の出力が行えるようになります。
もちろん、16bitのステータスを2本、あるいは32bitのステータスを1本出力する、といった使い方もできます。しかし、この場合はどこで発生したイベントか特定するのが難しいという欠点があります。
また、この方法は簡易的なデバッグ手段の提供が目的なので、イベントレートが高くなると正常に動作しなくなります。特に複数のペリフェラルの割り込みを出力する場合、それぞれの割り込みタイミングが接近してSPIの転送が間に合わない、といったことも考えられます。不必要に多用しないようにしましょう。