STM32F446REのI2SのFS(サンプリング周波数)は最大192kHzなので、FS=192kHzで動作させてみます。TDA1543も192kHzまで対応しているのでDACはTDA1543を使用します。ただしbit長は16bitです。
STM32CubeIDE: Version 1.5.1
Target board: Nucleo-F446RE
MXの設定 (Clock Configuration)
ClockソースにHSEを指定します。Nucleo64の場合、ST-Linkパート上の8MHzの水晶振動子を使うことになります。デフォルトのHSIはRC発振なのでクロックの精度が上がります。
HCLKはデフォルトでは84MHzになっていますが、MAXの180MHzを入力すると、PLLやプリスケーラなどを自動的に設定してくれると思います。
MXの設定 (Pinout & Configuration)
System Core
GPIO
PC5:
GPIO mode: Output Push Pull
User Label: CK_PERIOD
PC6:
GPIO mode: Output Push Pull
User Label: CK_CPLT
PC8:
GPIO mode: Output Push Pull
User Label: CK_HALF_CPLT
Multimedia
I2S2
Mode
Mode: Half-Duplex Master
Configuration
Parameter Settings
Generic Parameters
Selected Audio Frequency: 192KHz
DMA Settings
SPI2_TX
DMA Request Settings
Mode: Circular
Peripheral: Data Width: Half Word
Memory : Data Width: Half Word
処理のタイミングを計測するため、GPIOを何本か追加しています。
Selected Audio Frequencyで192KHzを選択すると、Real Audio Frequencyが195.312KHz、Error between Selected and Realが1.72%になります。正確に192KHzにするためにはClock源を192KHzの2^N倍にする必要があります。例えば12.288MHzの水晶振動子を外部クロックとして使うなど。
※設定時に何らかのエラーが出た場合はClock ConfigurationをResolve Clock Issueしたり、.iocファイルを一旦閉じて開き直すと改善する場合があります。
main.cにコードを追加
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <math.h>
/* USER CODE END Includes */
浮動小数点数演算を行うため<math.h>をインクルードします。
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define PI_F (3.141592f)
/* USER CODE END PD */
πの値を単精度で定義します。
/* USER CODE BEGIN PV */
float sampling_rate = 195312.0f;
float frequency = 1000.0f;
float phi = 0.0f;
float delta;
uint16_t tx_buffer[2] = { 0, 0 };
/* USER CODE END PV */
サンプリングレートを195.312kHzと定義しています。オーディオデータを再生するには正確に192kHzにしなければなりませんが、プログラムで波形を生成するので実際のサンプリング周波数で計算することにします。
出力波形の周波数は1kHzとしています。
phiは位相角で、サンプリング周期ごとにdeltaだけ増分します。
tx_bufferはDMA転送に使うバッファです
/* USER CODE BEGIN 1 */
delta = (2.0f * PI_F * frequency) / sampling_rate;
/* USER CODE END 1 */
増分deltaを計算します。
/* USER CODE BEGIN 2 */
HAL_I2S_Transmit_DMA(&hi2s2, tx_buffer, 2);
/* USER CODE END 2 */
DMA経由でI2Sを開始します。Ciruclarモードにしているので呼び出しは1回だけで勝手にメモリ→ペリフェラル間の転送が繰り返されます。
/* USER CODE BEGIN 4 */
void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
{
HAL_GPIO_WritePin(GPIOC, CK_HALF_CPLT_Pin, GPIO_PIN_SET);
// Generate Sine wave
float fv = sinf(phi);
int16_t v = fv * 0x7fff;
tx_buffer[0] = (uint16_t)v;
HAL_GPIO_WritePin(GPIOC, CK_HALF_CPLT_Pin, GPIO_PIN_RESET);
}
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
HAL_GPIO_WritePin(GPIOC, CK_CPLT_Pin, GPIO_PIN_SET);
// Generate Sawtooth wave
float fv = phi / PI_F;
int16_t v = fv * 0x7fff;
tx_buffer[1] = (uint16_t)v;
// Advance in phase
phi += delta;
if (phi > PI_F) {
phi -= 2.0f * PI_F;
HAL_GPIO_TogglePin(GPIOC, CK_PERIOD_Pin);
}
HAL_GPIO_WritePin(GPIOC, CK_CPLT_Pin, GPIO_PIN_RESET);
}
/* USER CODE END 4 */
割り込みハンドラで、Lchはサイン波、Rchはノコギリ波を生成しています。
Build Setting
最適化を速度優先にします。
C/C++ Build > Settings > Tool Settings > MCU GCC Compiler > Optimization
Optimization leverl: Optimize for size (-Os)
実行結果
Analog Discovery 2で出力波形とI2S信号を観測しました。
出力波形
I2S信号
Lch(サイン波) スペクトラム(500kHzレンジ)
Lch(サイン波) スペクトラム(100kHzレンジ)
FS=48KHzの場合と比べて可聴帯域内のエイリアスは少なくなっています。
最適化フラグを-Ofastとして速度優先にしましたが、デフォルトの最適化なし-O0だと割り込み中の処理が間に合わず出力波形が乱れました。
出力波形
C1:Lch C2:Rch
I2S信号
Cpltが呼び出されず、HalfCpltが二回連続していてかなり怪しいことになっています。
FPUが搭載されているので高速に浮動小数点数演算ができますが、それほど余裕があるわけでもないようです。凝った信号処理をしようと思うとDSPを使う必要があるかも知れません。
今回は16bit長ですが、24bitや32bitにするには、浮動小数点数を整数に変換する処理が16bitが32bitかの違いなので、それほど負荷はないと思います。
STM32F4では、24(32)bit/96kHzや24(32)bit/48kHzあたりを使うか、1chのみの出力にするのが現実的でしょうか。