前回と同じく抵抗で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を何本か追加しました。
配線
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 wavefloat 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 件のコメント:
コメントを投稿