2021年2月14日日曜日

STM32CubeIDE: I2S DACのTDA1543を使う - サイン波を出力

前回と同じく抵抗でI-V変換を行います。

プログラムを変更して任意の周波数のサイン波を出力してみます。今までAVRやPSoCなどマイコンでの波形生成には計算量の少ない整数演算で済むDDSを使っていましたが、STM32F446REには単精度のFPUが搭載されているので、リアルタイムに浮動小数点数演算して波形生成してみることにします。

TDA1543のPCMデータ・フォーマット


SPIのDACの場合符号なし整数のデータフォーマットが多いですが、I2SのDACはほとんど2の補数の符号付き整数のデータフォーマットです。

16bit値の2の補数の符号付き整数は以下のようになります。

16進数 符号なし10進数 符号付き10進数
0x0000 0 0
0x0001 1 1
0x0002 2 2
: :
0x7FFD 32765 32765
0x7FFE 32766 32766
0x7FFF 32767 32767
0x8000 32768 -32768
0x8001 32769 -32767
0x8002 32770 -32766
: : :
0xFFFD 65533 -3
0xFFFE 65534 -2
0xFFFF 65535 -1

ちょっとややこしそうですが、幸いに(?)C言語のsigned intは2の補数なのでプログラムは難しくありません。

STM32CubeIDE: Version 1.5.1
Target board: Nucleo-F446RE

MXの設定






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: 48KHz
    DMA Settings
      SPI2_TX
        DMA Request Settings
          Mode: Circular
          Peripheral: Data Width: Half Word
          Memory   : Data Width: Half Word

処理のタイミングを計測するため、GPIOを何本か追加しました。

配線



追加したGPIOをH/Lし、Logic Analyzerで補足します。

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.1415926f)
/* USER CODE END PD */

πの値を単精度で定義します。

/* USER CODE BEGIN PV */
uint32_t sampling_rate = 48000u;
float frequency = 1000.0f;
float phi = 0.0f;
float delta;
uint16_t tx_buffer[2] = { 0, 0 };
/* USER CODE END PV */

サンプリングレート48kHz、出力波形の周波数1kHzと定義しています。

phiは位相角で、サンプリング周期ごとにdeltaだけ増分します。

tx_bufferはDMA転送に使うバッファです。

int main(void)
{
  /* 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はノコギリ波を生成しています。

// Generate Sine wave
float fv = sinf(phi);
int16_t v = fv * 0x7fff;
tx_buffer[0] = (uint16_t)v;

sinf()の返り値は-1~1なので、0x7fffを乗算するとint16_t型(2の補数の16bit符号付き整数)の最小~最大になります。tx_bufferが符号なしのuint16_t型なので、明示的にキャストしています。キャストしても2進数としては同じ値のままでメモリに格納されます。

処理にかかった時間を計測するため、ハンドラ内の最初と最後でGPIOをH/Lしています。

Analog Discovery 2で出力波形と信号を観測


TDA1543の出力波形

C1:Lch C2:Rch

ノー・オーバーサンプリングで48kHz/16bitで1kHzの波形なので、きっちりガタガタが現れています。

周波数(MeasureのFrequency)を見ると992.7kHzとなっています。STM32CubeIDEのMXではサンプリング周波数の誤差が-0.79%と表示されていて、1,000Hz * 0.79% = 7.9Hz なので仕方ないでしょう。実際の値として見ると少し大きい感じがしますね。

計算処理時間を計測



1ch分転送後、割り込みハンドラに処理が移るまで結構タイムラグがあるようです。

処理にかかった時間は、Quick Measure機能で信号がHighになっている時間を調べると

HalfCplt: 3.51us (284.9kHz)
Cplt: 1.56us (641.0kHz)

でした。Clockが84MHz、DebugプロファイルでBuildした場合の計測なのでもう少し速くできると思います。

0 件のコメント:

コメントを投稿