1. はじめに
ESP32は、逐次比較型(SAR)アナログデジタル変換器(ADC)を2つ持っており、多くのピンがそれらに接続可能です。IoT向けアナログセンサにも活用できます。しかし、マニュアルにある150kSa/sという高速な読み込みを行うにはi2sを使う必要がある模様です。その方法について記載します。
2. I2Sの設定
まず、I2Sを使うには次をincludeします。
#include "driver/i2s.h"
肝である初期設定は下記です。サンプルコードHiFreq_ADC.ino
とソースコードなどを参考にしました。
.sample_rate
は150kSa/sを設定する場合、その半分の値を設定します。(2021.1/5追記) この理由ははっきりしませんが、.channel_format
にI2S_CHANNEL_FMT_ALL_LEFT
を設定したためと考えています。I2Sはクロックの立ち上がりと立ち下がりでそれぞれ右と左のチャネルとして扱うのですが、全て左チャネルとしたことで、クロックの立ち上がりと立ち下がりの両方でADCから値を取得しているの可能性があります。 .communication_format
はI2S_COMM_FORMAT_I2S_MSB
だけでも良いようですが、念のためI2S_COMM_FORMAT_I2S
も付けています。.dma_buf_count
と.dma_buf_len
は、ソフトの設計から決めます。今回は、合計で64サンプルとなるように、(16/2)x(8)としました。
ADC1の設定はデフォルト値ですので省略しても良いです。ADC1_CHANNEL_5は33番ピンです。
(2021.1/5修正: .sample_rate = 150000/2にしました)
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
.sample_rate = 150000/2, // The format of the signal using ADC_BUILT_IN
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
.channel_format = I2S_CHANNEL_FMT_ALL_LEFT,
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 16,
.dma_buf_len = 8,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
adc1_config_channel_atten(ADC_INPUT, ADC_ATTEN_11db);
adc1_config_width(ADC_WIDTH_12Bit);
esp_err_t erReturns;
erReturns = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
erReturns = i2s_set_adc_mode(ADC_UNIT_1, ADC1_CHANNEL_5);
i2s_adc_enable(I2S_NUM_0);
これでほぼできたも同然です。
3. 読み込み
初期設定でdma_buf_count=16 [Byte]で、dma_buf_len=8 [word]なので、合計128Byte(64word)がDMAで読み込まれます。次のコードでは、これを4回分、256wordを読み込むように設定しています。
ADCは12bitで、上位4bitに別の情報が含まれるため、下位12bitのみ取り出します。
#define NUM_READ_LEN 256
uint16_t u16Buf[NUM_READ_LEN];
size_t uiGotLen=0;
i2s_read(I2S_NUM_0, (char *)u16Buf, NUM_READ_LEN*sizeof(uint16_t), &uiGotLen, portMAX_DELAY);
for (int i=0;i<NUM_READ_LEN;i++)
Serial.println( u16Buf[i]&0xFFF ) ;
初期化はややこしいですが、読み込み自体はシンプルです。
4. 実際のコード
高速で読み込む分、長い時間読むには、メモリが必要です。ここでは、ESP32-wroverを前提に、PSRAMに格納していきます。
i2s_read
は処理中にブロッキングしますので、並列化するためxTaskCreate
を使います。このあたりは、サンプルコードのHiFreq_ADC.ino
と同じ手法です。メモリを2面にして、連続でデータを取得します。
入力電圧が、THRESH_SCALEで定義している値を超えるとデータ保存が開始されます。オシロでいうところのトリガレベルです。
一通り取り終えると、シリアルに出力します。入力は33番ピンです。
esp32_i2sAdc150ksps.ino
#include "driver/i2s.h" //https://github.com/espressif/arduino-esp32/blob/master/tools/sdk/include/driver/driver/i2s.h
#define PIN_MIC_IN 33
#define ADC_INPUT ADC1_CHANNEL_5 //pin 33
#define THRESH_SCALE (1<<10) //trigger level
#define MAX_MEM_SIDE (2)
#define NUM_MEM_SECT 128*2
#define NUM_READ_ITTER (120) //0.4ms at 150kSa/s
#define MAX_STORAGE_SIZE NUM_READ_ITTER*NUM_MEM_SECT*MAX_MEM_SIDE
uint16_t guiStream[MAX_MEM_SIDE][NUM_MEM_SECT];
uint16_t *guiStorage;
volatile uint8_t gu8Side=0;
uint8_t gu8SidePrev=0;
volatile uint32_t guiCountReadAdc=0;
uint32_t guiPreRead=0;
uint32_t guiCntStorage=0;
float gfValAve=0;
volatile uint8_t gui8flagReadDone=0;
bool gbFalgStartStorage=false;
xTaskHandle gxHandle;
#define I2S_SAMPLE_RATE 38000*2
//#define I2S_SAMPLE_RATE 78125*2
//#define I2S_SAMPLE_RATE 44100*1
void i2sInit()//ref to samplecode HiFreq_ADC.ino
{
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
.sample_rate = I2S_SAMPLE_RATE, // The format of the signal using ADC_BUILT_IN
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
//.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
//.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.channel_format = I2S_CHANNEL_FMT_ALL_LEFT,
//.communication_format = I2S_COMM_FORMAT_I2S_MSB,
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 16, //num of Bytes, from 2 upto 128
.dma_buf_len = 8, //num of sample not Bytes, from 8 upto 1024 //https://github.com/espressif/esp-idf/blob/master/components/driver/i2s.c#L922
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
adc1_config_channel_atten(ADC_INPUT, ADC_ATTEN_11db);
adc1_config_width(ADC_WIDTH_12Bit);
esp_err_t erReturns;
erReturns = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
Serial.print("i2s_driver_install : ");Serial.println(erReturns);
erReturns = i2s_set_adc_mode(ADC_UNIT_1, ADC_INPUT);
Serial.print("i2s_set_adc_mode : ");Serial.println(erReturns);
i2s_adc_enable(I2S_NUM_0);
}
void taskI2sReading(void *arg){
size_t uiGotLen=0;
while(1){
esp_err_t erReturns = i2s_read(I2S_NUM_0, (char *)guiStream[gu8Side], NUM_MEM_SECT*sizeof(uint16_t), &uiGotLen, portMAX_DELAY);
gu8Side = (gu8Side+1)&1;
gui8flagReadDone =1;
}
}
bool isRiseEdge(uint16_t *uiStrm ,uint32_t uiThresh ,uint32_t uiLen){
for(uint32_t i =0;i<uiLen;i++){
if((uiStrm[i]&0xFFF)>uiThresh) return(true);
}
return(false);
}
void copyStorage(uint16_t *uiSrc,uint16_t *uiDist ,uint32_t uiLen){
for(uint32_t i =0;i<uiLen;i++){
uiDist[i]=uiSrc[i]&0xFFF;
}
}
void streamPrint(uint16_t *uiStrm ,uint32_t uiLen){
for(uint32_t uilp =0;uilp<uiLen;uilp++){
Serial.println(uiStrm[uilp]);
}//end for
}
void testloop(void)
{
while(guiCntStorage < MAX_STORAGE_SIZE){
uint8_t u8Side=gu8Side;
uint8_t u8flagReadDone=gui8flagReadDone;
if ( u8flagReadDone==1){
gui8flagReadDone = 0;
copyStorage( &guiStream[(u8Side+1)&1][0], &guiStorage[guiCntStorage] , NUM_MEM_SECT);
guiCntStorage += NUM_MEM_SECT;
gu8SidePrev = u8Side;
}
}
streamPrint(&guiStorage[0] , (guiCntStorage-NUM_MEM_SECT) );
}
void setup() {
Serial.begin(115200);
pinMode(PIN_MIC_IN, INPUT);
guiCountReadAdc=0;
guiPreRead=0;
gfValAve=0;
gu8Side =0;
gu8SidePrev =0;
gbFalgStartStorage=false;
guiCntStorage=0;
guiStorage = (uint16_t *)ps_malloc(MAX_STORAGE_SIZE*sizeof(uint16_t));
Serial.println("done initialize");
i2sInit();
xTaskCreate(taskI2sReading, "taskI2sReading", 2048, NULL, 1, &gxHandle);
Serial.println("done setup");
unsigned int uiFlag=0;
while(uiFlag==0)
{
if(gui8flagReadDone==1){
gui8flagReadDone=0;
gu8SidePrev = gu8Side;
uiFlag=1;
}
}
Serial.println("done initial sampling");
//testloop();
}
void loop()
{
uint8_t u8Side=gu8Side;
uint8_t u8flagReadDone=gui8flagReadDone;
if(gbFalgStartStorage==false){
if ( u8flagReadDone==1){
gbFalgStartStorage = isRiseEdge( &guiStream[(u8Side+1)&1][0], THRESH_SCALE , NUM_MEM_SECT);
}
}
if(gbFalgStartStorage==true){
if ( u8flagReadDone==1){
gui8flagReadDone = 0;
copyStorage( &guiStream[(u8Side+1)&1][0], &guiStorage[guiCntStorage] , NUM_MEM_SECT);
guiCntStorage += NUM_MEM_SECT;
gu8SidePrev = u8Side;
}
}
if ( guiCntStorage > MAX_STORAGE_SIZE){
streamPrint(&guiStorage[0] , (guiCntStorage-NUM_MEM_SECT) );
guiCountReadAdc =0;
guiCntStorage =0;
gbFalgStartStorage=false;
}//endif counter
}
5. おわりに
ESP32でロジアナやオシロみたいな役割を持たせたいと思い、割り込み処理とanalogReadでサンプリングを行っていたところ、20kSa/sを越えたあたりから、リブートの嵐となりました。どうやら、analogReadではスピードが限界のため、次の割り込みに間に合わないためだろうと予想しています。調べていくうちに、i2sでなければ難しいという情報があり、試した次第です。
参考資料
ESP32 Technical Reference
ESP-IDF API Reference I2S
Reading ADC using I2S
adc i2s mode with multiple channel pattern (IDFGH-803) #1991
zekageri/ESP32_I2S-ADC-DMA-Plotting-to-WEB
I2S: dma_buf_count and dma_buf_len values for receive?
i2s.h
i2s.c