STM32の中でも、特にアナログに強い(=オペアンプとか乗ってる)STM32F3を使って、アナログ回路の勉強をしています。その備忘録的な感じでメモって行きます。
最終的にはオペアンプでローパスフィルタを作り、周波数特性を計測するところまでを目指します。
ハードウェア
STM32F303REが乗ったNucleo64を使っています。
STM32 Nucleo Board STM32F303RE: マイコン関連 秋月電子通商-電子部品・ネット通販
ジャンパワイヤでピン間を接続したり、キャパシタを外付けしてフィルタにしたりするので、多少の部品も必要になります。
ノイズを出す
まずはDACの動作確認を行います。
DACとTIMを使いつつ、DMAを使わずに変化のある信号を出す、最低限のプログラムです。
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio_init = {
.Pin = GPIO_PIN_4,
.Mode = GPIO_MODE_ANALOG,
};
HAL_GPIO_Init(GPIOA, &gpio_init);
__HAL_RCC_DAC1_CLK_ENABLE();
DAC_HandleTypeDef hdac = {
.Instance = DAC1,
};
HAL_DAC_Init(&hdac);
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 1023);
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
DAC_ChannelConfTypeDef channel_config = {
.DAC_Trigger = DAC_TRIGGER_T6_TRGO,
};
HAL_DAC_ConfigChannel(&hdac, &channel_config, DAC_CHANNEL_1);
__HAL_RCC_TIM6_CLK_ENABLE();
TIM_HandleTypeDef htim = {
.Instance = TIM6,
.Init = {},
};
HAL_TIM_Base_Init(&htim);
htim.Instance->PSC = 72 - 1;
htim.Instance->ARR = 1000 - 1;
htim.Instance->EGR = TIM_EGR_UG;
TIM_MasterConfigTypeDef master_config = {
.MasterOutputTrigger = TIM_TRGO_UPDATE,
};
HAL_TIMEx_MasterConfigSynchronization(&htim, &master_config);
HAL_DACEx_NoiseWaveGenerate(&hdac, DAC_CHANNEL_1, DAC_LFSRUNMASK_BITS11_0);
HAL_TIM_Base_Start(&htim);
システムクロックを72MHzに設定しているので、プリスケーラで72分周、タイマで1000分周し、1kHzのタイミングをDACに与えています。
オシロで観測すると、1ミリ秒ごとに電圧が変化し、周波数空間で見ると1kHz毎にノッチのあるSincのように見えます。
正弦波を出す
ノイズを出してDACとTIMが動いてることが確認できたので、DMAを追加してみます。とりあえず正弦波を出してみました。
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio_init = {
.Pin = GPIO_PIN_4,
.Mode = GPIO_MODE_ANALOG,
};
HAL_GPIO_Init(GPIOA, &gpio_init);
__HAL_RCC_DAC1_CLK_ENABLE();
DAC_HandleTypeDef hdac = {
.Instance = DAC1,
};
HAL_DAC_Init(&hdac);
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 1023);
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
__HAL_RCC_DMA1_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();
SYSCFG->CFGR1 |= SYSCFG_CFGR1_TIM6DAC1Ch1_DMA_RMP;
DMA_HandleTypeDef hdma = {
.Instance = DMA1_Channel3,
.Init = {
.Direction = DMA_MEMORY_TO_PERIPH,
.PeriphInc = DMA_PINC_DISABLE,
.MemInc = DMA_MINC_ENABLE,
.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD,
.MemDataAlignment = DMA_MDATAALIGN_HALFWORD,
.Mode = DMA_CIRCULAR,
.Priority = DMA_PRIORITY_MEDIUM,
},
};
HAL_DMA_Init(&hdma);
hdac.DMA_Handle1 = &hdma;
__HAL_RCC_TIM6_CLK_ENABLE();
TIM_HandleTypeDef htim = {
.Instance = TIM6,
.Init = {},
};
HAL_TIM_Base_Init(&htim);
htim.Instance->PSC = 0;
htim.Instance->ARR = 72 - 1;
htim.Instance->EGR = TIM_EGR_UG;
DAC_ChannelConfTypeDef channel_config = {
.DAC_Trigger = DAC_TRIGGER_T6_TRGO,
};
HAL_DAC_ConfigChannel(&hdac, &channel_config, DAC_CHANNEL_1);
TIM_MasterConfigTypeDef master_config = {
.MasterOutputTrigger = TIM_TRGO_UPDATE,
};
HAL_TIMEx_MasterConfigSynchronization(&htim, &master_config);
constexpr uint16_t DAC_buff_len = 4096;
static int16_t DAC_buff[DAC_buff_len] = {};
for (uint32_t i = 0; i < DAC_buff_len; i++)
{
DAC_buff[i] = static_cast<int16_t>(2047 + 1800 * sinf(i * 3.1415 * 2 / DAC_buff_len));
}
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, reinterpret_cast<uint32_t *>(DAC_buff), DAC_buff_len, DAC_ALIGN_12B_R);
HAL_TIM_Base_Start(&htim);
タイマは1MHzに設定し、サンプル数は4096に設定したので、244.14Hzくらいの正弦波が出てくるはずです。
いい感じに出ていますね。
Sincを出す
forで正弦波を作っている部分で、Sinc関数を作ってみます
for (uint32_t i = 0; i < DAC_buff_len; i++)
{
const float a = (static_cast<int32_t>(i) - DAC_buff_len / 2) * 3.1415 * 2 * 0.02;
const float b = a == 0 ? 1 : sinf(a) / a;
DAC_buff[i] = static_cast<int16_t>(1024 + 2048 * b);
}
1Msps*0.02=20kHzのパルスが出てくるはずです。
きれいな周波数特性で出てきていますね。
(厳密には、1Msps*0.04=40kHzで、負の周波数を含めて帯域幅40kHz、FFTで見えるのは0Hzから上の20kHz、という感じです)
ADCでサンプリングする
DAC, TIM, DMAと動作確認ができたので、続けてADCも接続します。
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio_init = {
.Pin = GPIO_PIN_1 | GPIO_PIN_4,
.Mode = GPIO_MODE_ANALOG,
};
HAL_GPIO_Init(GPIOA, &gpio_init);
__HAL_RCC_DAC1_CLK_ENABLE();
DAC_HandleTypeDef hdac = {
.Instance = DAC1,
};
HAL_DAC_Init(&hdac);
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 1023);
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
__HAL_RCC_DMA1_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();
SYSCFG->CFGR1 |= SYSCFG_CFGR1_TIM6DAC1Ch1_DMA_RMP;
DMA_HandleTypeDef hdma_dac = {
.Instance = DMA1_Channel3,
.Init = {
.Direction = DMA_MEMORY_TO_PERIPH,
.PeriphInc = DMA_PINC_DISABLE,
.MemInc = DMA_MINC_ENABLE,
.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD,
.MemDataAlignment = DMA_MDATAALIGN_HALFWORD,
.Mode = DMA_NORMAL,
.Priority = DMA_PRIORITY_MEDIUM,
},
};
HAL_DMA_Init(&hdma_dac);
hdac.DMA_Handle1 = &hdma_dac;
__HAL_RCC_ADC1_CLK_ENABLE();
ADC_HandleTypeDef hadc = {
.Instance = ADC1,
.Init = {
.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1,
.Resolution = ADC_RESOLUTION_12B,
.DataAlign = ADC_DATAALIGN_RIGHT,
.ScanConvMode = ADC_SCAN_ENABLE,
.EOCSelection = ADC_EOC_SINGLE_CONV,
.LowPowerAutoWait = DISABLE,
.ContinuousConvMode = DISABLE,
.NbrOfConversion = 1,
.DiscontinuousConvMode = DISABLE,
.NbrOfDiscConversion = 0,
.ExternalTrigConv = ADC1_2_EXTERNALTRIG_T6_TRGO,
.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING,
.DMAContinuousRequests = DISABLE,
.Overrun = ADC_OVR_DATA_OVERWRITTEN,
},
};
HAL_ADC_Init(&hadc);
HAL_ADCEx_Calibration_Start(&hadc, ADC_SINGLE_ENDED);
ADC_ChannelConfTypeDef adc_channel_config = {
.Channel = ADC_CHANNEL_2,
.Rank = ADC_REGULAR_RANK_1,
.SamplingTime = ADC_SAMPLETIME_19CYCLES_5,
.SingleDiff = ADC_SINGLE_ENDED,
.OffsetNumber = ADC_OFFSET_1,
.Offset = 1024,
};
HAL_ADC_ConfigChannel(&hadc, &adc_channel_config);
DMA_HandleTypeDef hdma_adc = {
.Instance = DMA1_Channel1,
.Init = {
.Direction = DMA_PERIPH_TO_MEMORY,
.PeriphInc = DMA_PINC_DISABLE,
.MemInc = DMA_MINC_ENABLE,
.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD,
.MemDataAlignment = DMA_MDATAALIGN_HALFWORD,
.Mode = DMA_NORMAL,
},
};
HAL_DMA_Init(&hdma_adc);
hadc.DMA_Handle = &hdma_adc;
__HAL_RCC_TIM6_CLK_ENABLE();
TIM_HandleTypeDef htim = {
.Instance = TIM6,
.Init = {},
};
HAL_TIM_Base_Init(&htim);
htim.Instance->PSC = 0;
htim.Instance->ARR = 72 - 1;
htim.Instance->EGR = TIM_EGR_UG;
DAC_ChannelConfTypeDef dac_channel_config = {
.DAC_Trigger = DAC_TRIGGER_T6_TRGO,
};
HAL_DAC_ConfigChannel(&hdac, &dac_channel_config, DAC_CHANNEL_1);
TIM_MasterConfigTypeDef master_config = {
.MasterOutputTrigger = TIM_TRGO_UPDATE,
};
HAL_TIMEx_MasterConfigSynchronization(&htim, &master_config);
constexpr uint16_t DAC_buff_len = 4096;
static int16_t DAC_buff[DAC_buff_len] = {};
for (uint32_t i = 0; i < DAC_buff_len; i++)
{
const float a = (static_cast<int32_t>(i) - DAC_buff_len / 2) * 3.1415 * 2 * 0.02;
const float b = a == 0 ? 1 : sinf(a) / a;
DAC_buff[i] = static_cast<int16_t>(1024 + 2048 * b);
}
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, reinterpret_cast<uint32_t *>(DAC_buff), DAC_buff_len, DAC_ALIGN_12B_R);
constexpr uint16_t ADC_buff_len = DAC_buff_len;
static int16_t ADC_buff[ADC_buff_len] = {};
HAL_ADC_Start_DMA(&hadc, reinterpret_cast<uint32_t *>(ADC_buff), ADC_buff_len);
HAL_TIM_Base_Start(&htim);
HAL_Delay(10);
HAL_TIM_Base_Stop(&htim);
printf("\n ADC\n");
for (uint32_t i = 0; i < ADC_buff_len; i++)
{
printf("%lu %hd\n", i, ADC_buff[i]);
}
printf("\n");
DACの出力はPA4、ADCの入力はPA2に設定したので、ジャンパワイヤでDACとADCを接続します。NucleoボードであればA1とA2を短絡させます。
シリアルポート経由で出てきたデータをグラフ化すると、以下のようになります。
いい感じにSincが見えてますね。ADC内でオフセットの計算も行っているので、0を中心に計測できます。
FFTに通す
せっかくSinc波形を使うので、周波数解析も行ってみます。都合のいいことにCubeMXで生成したコードにはFFTライブラリも含まれているので、これを利用します。
Drivers/CMSIS/Lib/GCC/libarm_cortexM4lf_math.a
をライブラリに追加し、ARM_MATH_CM4
を定義したあとでarm_math.h
をインクルードすると使用できるようになります。
// HAL_TIMEx_MasterConfigSynchronizationまでは同じなので省略
constexpr float bandwidth = 2 * 0.02;
constexpr float magn = 2048;
constexpr uint16_t DAC_buff_len = 4096;
static int16_t DAC_buff[DAC_buff_len] = {};
for (uint32_t i = 0; i < DAC_buff_len; i++)
{
const float a = (static_cast<int32_t>(i) - DAC_buff_len / 2) * PI * bandwidth;
const float b = a == 0 ? 1 : sinf(a) / a;
DAC_buff[i] = static_cast<int16_t>(1024 + magn * b);
}
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, reinterpret_cast<uint32_t *>(DAC_buff), DAC_buff_len, DAC_ALIGN_12B_R);
constexpr uint16_t ADC_buff_len = DAC_buff_len;
static int16_t ADC_buff[ADC_buff_len] = {};
HAL_ADC_Start_DMA(&hadc, reinterpret_cast<uint32_t *>(ADC_buff), ADC_buff_len);
HAL_TIM_Base_Start(&htim);
HAL_Delay(10);
HAL_TIM_Base_Stop(&htim);
printf("\n raw\n");
for (uint32_t i = 0; i < ADC_buff_len; i++)
{
printf("%lu %hd\n", i, ADC_buff[i]);
}
float FFT_in[ADC_buff_len] = {};
for (uint32_t i = 0; i < ADC_buff_len / 2; i++)
{
FFT_in[i + ADC_buff_len / 2] = ADC_buff[i] / magn;
FFT_in[i] = ADC_buff[i + ADC_buff_len / 2] / magn;
}
float FFT_out[ADC_buff_len / 2][2] = {};
arm_rfft_fast_instance_f32 FFT_instance = {};
arm_rfft_fast_init_f32(&FFT_instance, ADC_buff_len);
arm_rfft_fast_f32(&FFT_instance, FFT_in, &FFT_out[0][0], 0);
constexpr float sps = 1e6;
printf("\n Power Degree\n");
printf("0 %.3f\n", sqrtf(FFT_out[0][0] * FFT_out[0][0] + FFT_out[0][1] * FFT_out[0][1]) * magn / ADC_buff_len);
for (uint32_t i = 1; i < ADC_buff_len * bandwidth * 0.55; i++)
{
const float freq = i * sps / ADC_buff_len / 1000; // kHz
const float re = FFT_out[i][0];
const float im = FFT_out[i][1];
const float power = sqrtf(re * re + im * im) * bandwidth;
char tmp[20] = "#N/A";
if (0.8 < power && power < 1.2)
{
const float degree = atan2f(im, re) / PI * 180;
sprintf(tmp, "%.1f", degree);
}
printf("%.3f %.3f %s\n", freq, power, tmp);
}
実行するとADCの計測値とFFTを通したあとのいくつかのサンプルがシリアルポートを経由して出てきます。FFT結果だけグラフ化すると、以下のようになります。
横軸はkHzです。20kHzまでは1倍でフラットな、オシロのFFTで見たのと同じ形になります。0Hzはアナログ値のDCオフセットを示していますが、これは単純な平均値が出力されており、Sinc関数は正負非対称な(単純な平均では正に偏る)ため、数値としてはあまり意味を持ちません。
Degreesは周波数ごとの遅延量を示しています。たとえば5kHzは-2度、10kHzで-3.7度、15kHzで-5.7度、くらいの線形ですが、平均すると1kHzあたり-0.38度くらいで、周波数に依存しない遅延があると考えられます。0.38deg/360deg/1k=1.055usec
となり、DACとADCの間で約1マイクロ秒の遅延がある、と考えられます。DACとADCは同じタイミングで変換を開始するので、DACが出したサンプルは、ADCは次の変換でサンプリングしますから、1マイクロ秒の遅延が発生することになり、計測結果を説明できます。
オペアンプでフィルタを作ってみる
STM32F3にはオペアンプが乗っているので、使ってみたいと思います。
PGA(プログラマブルゲインアンプ)で2倍に設定し、0.01uFのコンデンサを使用してLPFとします。
いくつかパラメータを変更したりしています。ピンアサイン(ジャンパワイヤの接続)も変更になります。
constexpr float sinc_bandwidth = 2 * 0.05;
constexpr float sinc_offset = 512;
constexpr float sinc_magn = 1024;
constexpr uint32_t ADC_offset = 1024;
constexpr uint32_t TIM_ARR = 180 - 1;
constexpr uint16_t DAC_buff_len = 4096;
constexpr uint16_t ADC_buff_len = DAC_buff_len;
constexpr uint32_t TIM_clock = 72'000'000;
constexpr float sps = TIM_clock / (TIM_ARR + 1);
// PA1 ADC1 ch2
// PA4 DAC1 ch1 / OPA4 IN+
// PB10 OPA4 IN-
// PB12 OPA4 OUT
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef gpio_init = {
.Mode = GPIO_MODE_ANALOG,
};
gpio_init.Pin = GPIO_PIN_1 | GPIO_PIN_4;
HAL_GPIO_Init(GPIOA, &gpio_init);
gpio_init.Pin = GPIO_PIN_10 | GPIO_PIN_12;
HAL_GPIO_Init(GPIOB, &gpio_init);
__HAL_RCC_DAC1_CLK_ENABLE();
DAC_HandleTypeDef hdac = {
.Instance = DAC1,
};
HAL_DAC_Init(&hdac);
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, static_cast<uint16_t>(sinc_offset));
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
__HAL_RCC_DMA1_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();
SYSCFG->CFGR1 |= SYSCFG_CFGR1_TIM6DAC1Ch1_DMA_RMP;
DMA_HandleTypeDef hdma_dac = {
.Instance = DMA1_Channel3,
.Init = {
.Direction = DMA_MEMORY_TO_PERIPH,
.PeriphInc = DMA_PINC_DISABLE,
.MemInc = DMA_MINC_ENABLE,
.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD,
.MemDataAlignment = DMA_MDATAALIGN_HALFWORD,
.Mode = DMA_NORMAL,
.Priority = DMA_PRIORITY_MEDIUM,
},
};
HAL_DMA_Init(&hdma_dac);
hdac.DMA_Handle1 = &hdma_dac;
__HAL_RCC_ADC1_CLK_ENABLE();
ADC_HandleTypeDef hadc = {
.Instance = ADC1,
.Init = {
.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1,
.Resolution = ADC_RESOLUTION_12B,
.DataAlign = ADC_DATAALIGN_RIGHT,
.ScanConvMode = ADC_SCAN_ENABLE,
.EOCSelection = ADC_EOC_SINGLE_CONV,
.LowPowerAutoWait = DISABLE,
.ContinuousConvMode = DISABLE,
.NbrOfConversion = 1,
.DiscontinuousConvMode = DISABLE,
.NbrOfDiscConversion = 0,
.ExternalTrigConv = ADC1_2_EXTERNALTRIG_T6_TRGO,
.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING,
.DMAContinuousRequests = DISABLE,
.Overrun = ADC_OVR_DATA_OVERWRITTEN,
},
};
HAL_ADC_Init(&hadc);
HAL_ADCEx_Calibration_Start(&hadc, ADC_SINGLE_ENDED);
ADC_ChannelConfTypeDef adc_channel_config = {
.Channel = ADC_CHANNEL_2,
.Rank = ADC_REGULAR_RANK_1,
.SamplingTime = ADC_SAMPLETIME_19CYCLES_5,
.SingleDiff = ADC_SINGLE_ENDED,
.OffsetNumber = ADC_OFFSET_1,
.Offset = ADC_offset,
};
HAL_ADC_ConfigChannel(&hadc, &adc_channel_config);
DMA_HandleTypeDef hdma_adc = {
.Instance = DMA1_Channel1,
.Init = {
.Direction = DMA_PERIPH_TO_MEMORY,
.PeriphInc = DMA_PINC_DISABLE,
.MemInc = DMA_MINC_ENABLE,
.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD,
.MemDataAlignment = DMA_MDATAALIGN_HALFWORD,
.Mode = DMA_NORMAL,
},
};
HAL_DMA_Init(&hdma_adc);
hadc.DMA_Handle = &hdma_adc;
__HAL_RCC_TIM6_CLK_ENABLE();
TIM_HandleTypeDef htim = {
.Instance = TIM6,
.Init = {},
};
HAL_TIM_Base_Init(&htim);
htim.Instance->PSC = 0;
htim.Instance->ARR = TIM_ARR;
htim.Instance->EGR = TIM_EGR_UG;
DAC_ChannelConfTypeDef dac_channel_config = {
.DAC_Trigger = DAC_TRIGGER_T6_TRGO,
};
HAL_DAC_ConfigChannel(&hdac, &dac_channel_config, DAC_CHANNEL_1);
TIM_MasterConfigTypeDef master_config = {
.MasterOutputTrigger = TIM_TRGO_UPDATE,
};
HAL_TIMEx_MasterConfigSynchronization(&htim, &master_config);
static int16_t DAC_buff[DAC_buff_len] = {};
for (uint32_t i = 0; i < DAC_buff_len; i++)
{
const float a = (static_cast<int32_t>(i) - DAC_buff_len / 2) * PI * sinc_bandwidth;
const float b = a == 0 ? 1 : sinf(a) / a;
DAC_buff[i] = static_cast<int16_t>(sinc_offset + sinc_magn * b);
}
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, reinterpret_cast<uint32_t *>(DAC_buff), DAC_buff_len, DAC_ALIGN_12B_R);
static int16_t ADC_buff[ADC_buff_len] = {};
HAL_ADC_Start_DMA(&hadc, reinterpret_cast<uint32_t *>(ADC_buff), ADC_buff_len);
OPAMP_HandleTypeDef hopamp = {
.Instance = OPAMP4,
.Init = {},
};
hopamp.Init.Mode = OPAMP_PGA_MODE;
hopamp.Init.NonInvertingInput = OPAMP_NONINVERTINGINPUT_IO2;
hopamp.Init.PgaConnect = OPAMP_PGA_CONNECT_INVERTINGINPUT_IO0;
hopamp.Init.PgaGain = OPAMP_PGA_GAIN_2;
HAL_OPAMP_Init(&hopamp);
HAL_OPAMP_Start(&hopamp);
HAL_Delay(5);
HAL_TIM_Base_Start(&htim);
HAL_Delay(100);
HAL_TIM_Base_Stop(&htim);
float FFT_in[ADC_buff_len] = {};
for (uint32_t i = 0; i < ADC_buff_len / 2; i++)
{
FFT_in[i + ADC_buff_len / 2] = ADC_buff[i] / sinc_magn;
FFT_in[i] = ADC_buff[i + ADC_buff_len / 2] / sinc_magn;
}
float FFT_out[ADC_buff_len / 2][2] = {};
arm_rfft_fast_instance_f32 FFT_instance = {};
arm_rfft_fast_init_f32(&FFT_instance, ADC_buff_len);
arm_rfft_fast_f32(&FFT_instance, FFT_in, &FFT_out[0][0], 0);
printf("\n Power Degree\n");
printf("0 %.3f\n", sqrtf(FFT_out[0][0] * FFT_out[0][0] + FFT_out[0][1] * FFT_out[0][1]) *
sinc_magn / ADC_buff_len);
for (uint32_t i = 1; i < ADC_buff_len * sinc_bandwidth * 0.55; i++)
{
const float freq = i * sps / ADC_buff_len / 1000; // kHz
const float re = FFT_out[i][0];
const float im = FFT_out[i][1];
const float power = sqrtf(re * re + im * im) * sinc_bandwidth;
char tmp[20] = "#N/A";
if (0.5 < power)
{
const float degree = atan2f(im, re) / PI * 180;
sprintf(tmp, "%.1f", degree);
}
printf("%.3f %.3f %s\n", freq, power, tmp);
}
かなり長くなってきましたね。。。
オペアンプの伝達関数計算ツールで計算したところによると、ゲイン2倍のPGAである5.4k/5.4kと0.01uFの組み合わせでは、利得6dB、約2kHzで4dBまで下がり、約6kHzで2dBまで下がる、という感じになるようです。位相は1kHzで-10度、3kHzで-20度、20kHz弱で-10度まで戻る、という感じでしょうか。
実際に計測してみると、このようになりました。
400kSPSに合わせて、グラフ描画時に1kHzあたり0.9degを足してADCの遅れを補正してあります。
1kHz程度までは6dBでほぼフラット、3kHzで4dB、6kHzで2dB、位相は1kHzで-10度、3kHzで-20度、20kHzで-10度、と、シミュレーション通りの特性が得られています。
ちなみに、周波数軸も対数なのでかなり急激に利得が下がっているように見えますが、線形で見ると以下のようになります。
ローパスフィルタとしてはとてもゆったりした特性ですね。ゲインが低いせいもありそうですが。
まとめ
かなり長くなりましたが、STM32F303RE Nucleoボード1枚でDACやADCを使用し、アナログ回路の周波数特性を計測してボード線図のようなグラフを作ったりできるようになりました。Nucleoはデバッガも付属して、USB1本でPCに接続できるので、手軽に使うことができます。マイコンボードですが、アナログ回路の学習にも最適なボードだと思います。