STM32F4のADCはあまり高速ではない(それでも結構な速度だが)。ある程度の速さで変化する信号を記録したい場合、少し困ることになる。簡単な方法では最大で7.2Mspsでサンプリングできるが、今回はそれでは足りなかったので、少し工夫して9Mspsでサンプリングする方法を考えてみた。
簡単に動作確認して、一応狙い通りに動いている雰囲気は得ているけど、リファレンスマニュアル(RM0090)で腑に落ちていない部分もあるので、盛大に間違いをやらかしている可能性もあります。以下を参考にする際は各自の自己責任で……
ハードウェア
STBee F4miniを使った。STM32F405RGTが乗ってる。
ペリフェラル
何はさておきADCが必要。ADC1, 2, 3すべて使う。
他に、ADCを駆動するためのタイマが必要。今回は他のペリフェラルとの兼ね合いでTIM2を使った。
GPIOも2本(or3本)必要。ADC123全てからアクセスできるピンを割り当てる必要がある。
PA0はプッシュスイッチに結線されているので、PA2, PA3をADCの入力として使った。
ADCの転送にDMA2-0も必要。
あと、ADCの動作確認用にDACから適当な波形を出すので、DACch1とそれを駆動するTIM7も使った(DAC周りはの解説は今回は行わない)。
タイミング
今回のADCは最大で内部クロック36MHzで駆動できる。分周器は2^nで設定するので、コアクロックを144MHzに設定し、2分周されたペリフェラルクロックを、更に2分周して36MHzとしてADCに入力した。
コアは168MHzまで設定できるが、ADCで高速サンプリングしようとするとコアの計算リソースを最大限使うことはできない。
ADCのサンプリングのタイミングは、以下の図のようになる。
pin1とpin2は、ADCがサンプリングしているタイミングを表している。緑はADCの変換処理の期間とは異なる点に注意(終了位置は正しい)。
9Mspsでサンプリングするので、36MHz/9Msps=4クロック毎にADCを行う必要がある。実際には3本で順番に処理するので、それぞれのADCは4*3=12クロック毎に変換を行う。
ADCの最短サンプリング時間は3サイクルなので、12クロック以内に変換を終了するにはビット深度が9bit以下である必要がある。ビット深度は6/8/10/12bitから選べるが、10bitではタイミングが満たせず、6bitでは分解能が足りないので、8bitを選択した。
サンプリング時間は3サイクルだが、サンプリング時間+2サイクルはそのADCが排他的に利用するので、他のADCがサンプリングすることができない。3+2=5サイクルを各ADCが交互に専有するが、4サイクルごとに変換を行うから、1サイクル足りない。そのため、2本のピンを交互に利用していく。(こういう理由があってか、STM32F4のマルチADCの遅延時間は最小が5になっている。3や4に設定できたら苦労せずに12Mspsや9Mspsでサンプリングできるのに。。。)
プログラム
トリガ
ADCをタイマでトリガするので、その設定を行う。
htim2.Init.Prescaler = 0;
htim2.Init.Period = 24 - 1;
HAL_TIM_Base_Init(&htim2);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 1 + 8 * 0);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, 1 + 8 * 1);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, 1 + 8 * 2);
TIM_CCxChannelCmd(htim2.Instance, TIM_CHANNEL_2, TIM_CCx_ENABLE);
TIM_CCxChannelCmd(htim2.Instance, TIM_CHANNEL_3, TIM_CCx_ENABLE);
TIM_CCxChannelCmd(htim2.Instance, TIM_CHANNEL_4, TIM_CCx_ENABLE);
タイマの1サイクルでADC3本にトリガするので、タイマは9Msps/3=3MHzで動くように設定する。タイマのクロックは72MHzなので、1サイクル24クロックにすれば3MHzになる。
ADCは立ち下がりでトリガさせるので、PWMの立ち下がりエッジが均等な間隔で並ぶようなタイミングに設定する。
STM32でTIMのOCをトリガに使う場合、基本的にはChannelCmdでENABLEにしなくても使えるのだが、ADCは例外で、ENABLEに設定しないと動作しない。
ADC
hadc1.Init.Resolution = ADC_RESOLUTION_8B;
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.NbrOfConversion = 2;
hadc1.Init.DiscontinuousConvMode = ENABLE;
hadc1.Init.NbrOfDiscConversion = 1;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_CC2;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_FALLING;
hadc2.Init.Resolution = ADC_RESOLUTION_8B;
hadc2.Init.ScanConvMode = ENABLE;
hadc2.Init.NbrOfConversion = 2;
hadc2.Init.DiscontinuousConvMode = ENABLE;
hadc2.Init.NbrOfDiscConversion = 1;
hadc2.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_CC3;
hadc2.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_FALLING;
hadc3.Init.Resolution = ADC_RESOLUTION_8B;
hadc3.Init.ScanConvMode = ENABLE;
hadc3.Init.NbrOfConversion = 2;
hadc3.Init.DiscontinuousConvMode = ENABLE;
hadc3.Init.NbrOfDiscConversion = 1;
hadc3.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_CC4;
hadc3.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_FALLING;
HAL_ADC_Init(&hadc1);
HAL_ADC_Init(&hadc2);
HAL_ADC_Init(&hadc3);
//***
ADC_ChannelConfTypeDef sConfig = {};
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = 1;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = 2;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = 1;
HAL_ADC_ConfigChannel(&hadc2, &sConfig);
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = 2;
HAL_ADC_ConfigChannel(&hadc2, &sConfig);
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = 1;
HAL_ADC_ConfigChannel(&hadc3, &sConfig);
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = 2;
HAL_ADC_ConfigChannel(&hadc3, &sConfig);
//***
ADC_MultiModeTypeDef multimode = {};
multimode.DMAAccessMode = ADC_DMAACCESSMODE_3;
multimode.Mode = ADC_TRIPLEMODE_ALTERTRIG;
HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode);
__HAL_ADC_ENABLE(&hadc1);
__HAL_ADC_ENABLE(&hadc2);
__HAL_ADC_ENABLE(&hadc3);
ADC3本で設定も3組なのでコード行数はかなり多い。
交互にピンをサンプリングするために、レギュラ変換は2chを登録し、スキャンモードを有効にしつつ、不連続モードも有効にして、トリガ1回で1回のサンプリングを行うようにする。あとそれぞれのADCに合わせてトリガの選択をしている。
通常のADC_StartはADCの有効化を行ってくれるが、MultiModeStartではスレーブまで有効化してくれないので、すべてのADCを明示的に有効化している。
変換開始
static constexpr uint32_t buff_len = 90'000;
static uint8_t buff[buff_len];
HAL_ADCEx_MultiModeStart_DMA(&hadc1, reinterpret_cast<uint32_t *>(buff), buff_len / 2);
HAL_TIM_Base_Start(&htim2);
osDelay(20);
printf("\n");
for (const uint8_t a : buff)
{
printf("%hhu\n", a);
}
printf("\n");
適当なバッファを確保して、MultimModeStart_DMAでADCを開始し、TIMを開始させれば変換が始まる。バッファは8bitで宣言するが、転送は16bit単位で行われるので、バッファ長の半分の長さを与える。
転送終了待ちは、いろいろ工夫するのが面倒だったので今回は固定時間で待った。
結果
適当なタイミングを切り出してグラフ化するとこんな感じ。
1kHzの正弦波を9Mspsで読み出してるので、9kポイントで1周期になる。
FFTを通すとこんな感じ。
1kHzとその高調波が出ている。高調波はDACとADCを直結しているからだと思う。
ADCは単電源シングルエンドなのでDC成分が入る。ちょうど127くらいのピークになっている。
1kHzの信号はDACから振幅が±112.5くらいで出していて、実部のみなのでその半分の56くらいの高さのピークが見えている(振幅が整数でないのは、DACがADCより高分解能で出しているため)。
その他
今回は8bit分解能が必要だったので9Mspsの設定にしたが、6bitでいいなら12Mspsでサンプリングできる。