昨日の投稿で、DAC搭載STM32が少ないので自分でつけるべくアキバへ行った件の続報.
秋月でPT8211を買いました.
↓weactのSTM32F401基板に取り付けました.動きました.
少してこづったので要点を書いときます.
まずclk構成です.
STM32F401のclkは84MHz
I2Sの内部で分周してサンプリングclk 8kHzをつくります(Master Mode)
I2SはLRCK=8kHzを出力します
DATAは16bitです(LRだから32bit)
BCKは8x32=256kHz
ここで、サンプリングclk(Fs)がI2Sによって生成されてしまうことを留意しておきましょう.後で気分が悪い事態に直面します.
気分が悪いことの1つを先に書いときますと、I2Sが生成するFsの選択肢はこれだけです.中間値は選択できません.
ならばSlave Modeを使えばいいと思うでしょうけど、SlaveだとBCKとLRCK=BCK/32を与えなくてはいけないので、外部clkでI2Sを動かしたければ外部にFPGAでも搭載して÷32までせにゃいけません.楽したいからSTM32を使うのにわざわざFPGAを載せたりするかフツー?
こうゆうclk系の不都合って出鼻を挫かれるのですごく嫌いなんです、わたしは.
一番遅い8kHzで動かすことにします.
ーーーー
それではSTM32CubeMXの設定をば.
↓キモの部分は赤線のところでしょう.
とりわけ
MSB First(Left Justified)、16bit on 16bit は重要で、PT8211のI2S timingにマッチさせるにはこれ以外の選択肢はないと思います.
clock pol.は効いてるのか効いてないのかよくわかりません.というか何が変わったのか分かりません.
ーーーー
↓8kHzに間に合わせてI2Sを動かすにはDMAが必須です.I2SにDMAを紐づけときます.
bufferはCircularです.16bitなのでHalf Wordです.
↓DMAのcallback()を利用するので割込みも必要です.
↓STM32F401のピン配置.この3本をPT8211へ接続します.
↓PT8211のピン配置.WSはLRCKのことです.
ーーーー
codeへ.
まずデータ準備.
PT8211は2の補数の16bitです.実験ですので何も考えずにベタ並べしときます.
偶数番目がLch、奇数番目がRchということ.(逆かもw)
DACbuf[0] = -30000; DACbuf[1] = 30000;
DACbuf[2] = -25000; DACbuf[3] = 25000;
DACbuf[4] = -20000; DACbuf[5] = 20000;
DACbuf[6] = -15000; DACbuf[7] = 15000;
DACbuf[8] = -10000; DACbuf[9] = 10000;
DACbuf[10] = -5000; DACbuf[11] = 5000;
DACbuf[12] = 0; DACbuf[13] = 0;
DACbuf[14] = 5000; DACbuf[15] = -5000;
DACbuf[16] = 10000; DACbuf[17] = -10000;
DACbuf[18] = 15000; DACbuf[19] = -15000;
DACbuf[20] = 20000; DACbuf[21] = -20000;
DACbuf[22] = 25000; DACbuf[23] = -25000;
DACbuf[24] = 30000; DACbuf[25] = -30000;
DMA関連
main()の始まりの方でDMAを起動.26wordを送信させるおまじない.
HAL_I2S_Transmit_DMA( &hi2s3, DACbuf, 26);
DMAのcallback関数、何処かで_weak宣言されているので自作関数で上書きする.DMA送信完了したらすぐさま次のDMAを起動している.
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s){
HAL_I2S_Transmit_DMA( &hi2s3, DACbuf, 26);
}追記:bufferがcircularなので↑このDMA都度起動はしなくても良いかもしれない.
以上で、こんな波形がでるというわけ.
注意:PT8211はフルスイングさせても2.7Vppぐらいしか出ないみたいです.「5V電源だったらフルスイング5V」とかではないので注意してください.
続きます.
続きはADCとの連携動作.
ーーーーーーーー
DACはI2Sが生成するFsで動きました.
でも、これでおしまいじゃないです.
最終的には、ADC→信号処理→DAC のように信号を流したいわけです.つまり、ADCとDACが同じclkで動かなくちゃいけません.
でも、I2Sは勝手に動きます.
ADCはI2Sとは別の動きをします. ←こいつら同期しないじゃん! fuckでしょこんなの
ADCとI2Sをどうやって同期させるか?
↓無理やりですがこうすれば同期できると思います.
I2Sの分周器の分周比と、TIM1の分周比を同じにすればADC系列とI2S系列を同期できるという理屈です.
84MHz→8kHzを作るのですから分周比=10500だと予想できます.
しかし、この分周比が10500じゃないんですよ.
精密に実測したところ、分周比は10495でした.厳密に8kHzじゃないんです.8003Hzとかそんななの.I2Sの内部回路なにやってんの? イミフすぎて腰が抜けるんだが....
ともあれ10495というmagic numberが判明したのですから、TIM1の分周比も同じくします.この操作によって、全処理系のclkが同期しました.
続きます.
ーーーー
ADCを動かせました.上がADC入力波形、下がDAC出力波形.33Hz.
ADCの設定画面.
Left alignmentは、12bit ADCを16bitに左詰めにする意味.
Timer1 capture Compare 1 eventは、このADCがTIM1でトリガされる意味.TIM1は8kHzを生成します.TIM1設定は割愛します.
↓ADCのデータはDMAでメモリに積んでもらいますので、half word(16bit)にしとく.
codeについて.
DACバッファは2000個、ADCバッファは1000個です.なぜかというと、ADCは1chですが、DACはI2Sの仕様によって2ch分だからです.
int16_t DACbuf[2000];
uint16_t ADCbuf[1000];
1000とか2000に特別な意味はないです.
main()の最初の方でADCをDMAモードで起動します.
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)var.ADCbuf, 1000);
uint32でcastしてるのはwarningを避けるためです.実際には16bit幅左詰めでメモリに積まれます.
ADCが1000回サンプルし終えたら呼ばれるcallback関数._weak宣言されてる関数名を上書きします.
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc){
for(int i=0; i<ADCBUFSIZE; i++) {
int16_t a = var.ADCbuf[i];
var.DACbuf[i*2] = a + 32768;
}
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)var.ADCbuf, ADCBUFSIZE);
}
やってることは、①ADCメモリ→DACメモリへのコピー、②次のDMA起動.
①コピーの際に、リニアから2の補数に変換してるつもりだけどなんか変だけど、とりあえずこれで動いています.
フィルタなど重い処理を追加すると時間がかかってグリッジが生じてしまいます.
それを避けるために、リングバッファの半分毎に処理するのが正統です.
HAL_ADC_ConvHalfCpltCallback()というのがそれです.割愛します.
以上でPT8211 DACをSTM32内蔵ADCと同時に動かすことができました.おしまい
ーーーー
翌日、STM32G030F6P6に移植しました.
I2S,I2C,UART,ADC,PROG,GPIOを盛ったらキチキチになっちまった.RAMも8kBと小さいし.
かしこ