中華STM32基板のBlackpillを味見しています.
前回に続きまして、今回もADCを動かします.前回は1chでしたが、今回は2chに拡張します.clock系とかNVICとかUSBとかTIM3の設定は前回を読んでください.以下ではADC+DMAにfocusします.
ADCを動かすと言ってもいろんな動かし方の作法があるわけでして、、、ここでやったのは次の仕組みです.
・サンプリング周波数は500Hz
・連続する500ptsをメモリにストア (1秒分)
・同500ptsをCDC(USB COM)でPCへ送信する
・サンプリング周期はTIM3の出力する2mSecで決める (つまりhardware自走)
・ADC入力はIN1とIN9の2本 (2ch scan)
・ADCは1トリガで2サンプル (2ch scan)
・DMAを使う
・DMAはIN1サンプル→DMA→IN9サンプル→DMAという動作をする
・すなわち、TIM3→ADC(IN1)→DMA→ADC(IN9)→DMAがhardwareで自走する
・ADC完了をIRQで知る
・同IRQで500pts x 2ch分のbufferに積む
・同IRQでbuffer fullになったらUSB送信を起動 (USB送信はブロックモード)
・ADCは12bitなので、1サンプルあたりASCIIの3bytesをbufferに積む
・ゆえに1USB送信あたり500x2x3=3000bytes
・USB FULL SPEED(12Mbps)で3000bytes送信するのにover head込みで20mSecぐらいかかるのでその間はADC変換動作は滞るが気にしない
ポエム的な表現では、こんなイメージ.
1)ADC変換のTRIGはTIM3にやらせる (2mSec毎)
2)2ch scanのAD dataはDMAでmemoryにコピーさせる
3)2が終わったら割り込みで知らせてもらう
4)1へもどる
5)1234をhardwareで自走させる
6)3の割り込みでUSB buffer(3000bytes)に積むのとUSB転送はprogramで面倒をみる
ーーーー
だがしかし、いろいろな疑問があります.
Q:ADC終了割り込みはどれを使えばいいんだ?
A:HAL_ADC_ConvCpltCallback()
ADC_IRQHandler()
ADC割り込み.これはお役御免でしょう.なぜかというと、ADC→DMAと自走するのでADC変換終了割り込みの意味がない.
DMA2_Stream0_IRQHandler()
DMA割り込み.変換終了をDMA割り込みで受領するのは合理的なのですが、、、うまく動かない.
2ch scanする時を考えると、2ch scan終了でDMA割り込みしてもらいたいが、実際の挙動は1ch目変換→DMA割り込み→2ch目変換→DMA割り込みと2度割り込んでくる.これは困る.DMAのflugをチェックすれば回避できるのだろうが、使い心地が悪い.
追記:後日この2度割り込みが再現しなくなって戸惑っています
HAL_ADC_ConvCpltCallback()
これは期待通りの割り込みをしてくれる.(2ch scan→割り込みということ)
weak宣言されているので、同名でmain()の近所に宣言して上書きして使う.
Q:DMAのdata widthは8/16/32bitのどれにしようか?
A:32bitにした
まず背景を書くと、
・ADCは12bitであるが、DR registerは32bitであり、右詰めで納まる
・DMAは8/16/32bitの選択が可能
・DMA起動関数の第2引数が格納bufferであり32bitで定義されている
HAL_ADC_Start_DMA(&hadc, uint32_t* pData, Length)
この背景から32bit=wordで統一しました.
netの事例では16bit=half wordで実装しているケースもありますが.
Q:多ch scan時のADC出力形態はどうなっている?
A:DRレジスタは1つしかないのでDMA推奨
ADCは最大16chをscanできる仕様です.つまり、TRIG1発で16chをシーケンシャルに変換0変換1・・・変換16まで全自動で行えます.
ならば16ch ADC dataはDR0,DR1,DR2,,,,DR15へパラレル出力されるのでしょうか?
datasheetによるとSTM32のDRはそうなっていません.DRは1つだけです.(32bit幅)
これは何を意味するのかというと、いくら全自動16ch変換できるとはいえ、ADC dataは各ch毎に逐次採取しないとどんどん上書きされちゃうという、片手落ちな仕様であります.いちいちprogramでそんなものを面倒見ていたら他の仕事ができなくなってしまいます.
そんな悩みを解決してくれるのがADC+DMAの組み合わせです.DMAに16個のbufferを割り当てて、ADC dataをbufferに自動転送させます.そして、16個の変換が終わったら割り込みが発生します.
(ただし此処で実装するのは2ch scanですので)
Q:TIM3+ADC+DMAを起動するやりかた
A:HAL_TIM_Base_Start_IT(&htim3);
HAL_ADC_Start_DMA(&hadc1, &buffer[0], 2);
main()の最初の方で1回だけこれをやると、あとはhardwareが自走してくれます.(あらかじめの設定は必要、後述)
DMAの最後の引数2は「buffer2つを埋めろ」の意味です.2ch scanなので、
TIM3→ADC(IN1)→DMA→ADC(IN9)→DMA→割り込み
が実行されます.
これを4にしたらどうなるか?
「buffer4つを埋めろ」なので、
TIM3→ADC(IN1)→DMA→ADC(IN9)→DMA
TIM3→ADC(IN1)→DMA→ADC(IN9)→DMA→割り込み
という動作になりました.ゆえに8とか16とかも出来るのでしょう.便利よね.
Q:TIM3とADCとDMAをバインドするにはどうするの?
A:STM32CubeMXをつかう
下記の設定にする.
Data Alignment:32bit DRの下12bitにADC dataを格納する.Scan Conversion Mode:2ch scanする.
DMA Continuous Requests:DMAが一仕事終わったら、自動的に次の仕事待ち.
End of Conversion Sel.:2ch scanが終わったら割り込み.
Number of Conversion:2chだから2にする.
External Trigger Conversion Source:ADCがTIM3でTRIGされる.
Channel:ch1とch9を設定する.
Sampling Time:この用途にはdon't careかもしれない.よく判ってない.
ADCのDMA設定.Mode:Circularにするのは必須.Wordは上述したbit幅が理由.
ーーーー
STM32CubeMXのCDC雛形source codeへ追加するもの.
main()の上とかにADC変換完了callbackを記す.
uint32_t buffer[4];
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc){
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
static N = 0;
static uint8_t usbbuf[3*500*2+100]; 3byte x 500pts x 2ch
if(N==0) usbbuf[0]=0;
char c[15];
sprintf(c,"%03X",buffer[0]); IN1
strcat(usbbuf,c); ※
sprintf(c,"%03X",buffer[1]); IN9
strcat(usbbuf,c);
if(N++>=499){
int l = strlen(usbbuf);
usbbuf[l] = '\n';
CDC_Transmit_FS(&usbbuf[0],l+1);
N=0;
}
}
※追記:strcat()は文字数が増えると遅くなるので使わない方がいいです.usbbuf[i]=c[k]みたく自分で積んだ方がはるかに速いです.
main()のInitの下に追加.TIM3とADCとDMAを起動する.以降はhardwareが自走する.やっぱhardware orientedは気持ちいい.
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim3);
HAL_ADC_Start_DMA(&hadc1, &buffer[0], 2);
/* USER CODE END 2 */
ーーーー
以上により、USB COMにこのような文字列が3000文字+CRだけ毎秒送信されます.
"50F7FB5077F15197FF.......62B2281EB0D81DADC8\n"
USB COMを受信したPCではこれを3文字づつに切り分け、さらに偶数番目と奇数番目で分けてHEX→DEC変換するとこのような2chグラフを描けます.
50F,507,519 → 1295,1287,1305
7FB,7F1,7FF → 2043,2033,2047
かしこ
0 件のコメント:
コメントを投稿