ESP 開發板目前有 ESP8266 與 ESP32 兩個世代,新一代的 ESP32 標榜雙核心、CPU 頻率翻倍、SRAM、Flash、GPIO/I2C/SPI/UART 都加倍,還內建藍牙、觸控電容、溫度感測器、霍爾(磁力)感測器,規格上完全輾壓 ESP8266。但 ESP8266 也是有個強大優勢 - 便宜,一片開發板百元有找,這半年要製作電子鐘這類會長期使用的小裝置,我都選擇交給它扛。

最近想接耳機線輸出訊號來玩,需要 analogRead() 測量輸入電壓,ESP8266 的類比輸入只有一組,固定為 A0 腳,但聲音分左右聲道需要兩組類比輸入,ESP8266 只能黯然退場,改由 ESP32 登板救援。

ESP32 有 18 組測量頻道,但只有 15 組支援類比輸入(PIN 腳參考),先 pinMode(PIN_NO, INPUT) 設為讀取模式,之後以固定頻率讀取 analogRead(PIN_NO) 就能得到聲音波形。而為了方便即時觀察,我打算將結果輸出到 SPI OLED 顯示器,SPI 傳輸速度夠快,想螢幕上即時顯示波形資訊。

程式邏輯不難,我很快寫出第一個版本:

#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_I2CDevice.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for SSD1306 display connected using software SPI (default case):
#define OLED_MOSI  23 //D1
#define OLED_CLK   18 //D0
#define OLED_DC    16
#define OLED_CS    5
#define OLED_RESET 17
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

#define AIO_PIN1  34
#define AIO_PIN2  35

#define DATA_LEN  100
byte data1[DATA_LEN];
byte data2[DATA_LEN];
int dataIdx = 0;

void setup() {
  Serial.begin(115200);
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  // Clear the buffer
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.display();

  pinMode(AIO_PIN1, INPUT);
  pinMode(AIO_PIN2, INPUT);

  for (int i = 0; i < DATA_LEN; i++) data1[i] = data2[i] = 0;
}

int v1, v2;
int divFact = 4096 / 32;
int timerCount = 0;
#define V1_BASE     31
#define V2_BASE     63

void loop() {
  v1 = analogRead(AIO_PIN1);
  v2 = analogRead(AIO_PIN2);
  data1[dataIdx] = v1 / divFact;
  data2[dataIdx] = v2 / divFact;
  auto startTime = micros();
  display.drawLine(dataIdx, 0, dataIdx, SCREEN_HEIGHT, SSD1306_BLACK);
  display.drawLine(dataIdx, V1_BASE, dataIdx, V1_BASE - data1[dataIdx], SSD1306_WHITE);
  display.drawLine(dataIdx, V2_BASE, dataIdx, V2_BASE - data2[dataIdx], SSD1306_WHITE);
  display.display();
  auto elapsed = micros() - startTime;
  Serial.println(String(elapsed));
  dataIdx++;
  if (dataIdx > DATA_LEN - 1) dataIdx = 0;
  delay(1);
}

取樣邏輯寫在 loop(),用 delay(1) 延遲 1ms 取樣一次並即時顯示波形圖。

我沒實際接耳機孔,而是用手指接觸取樣 PIN 腳,理論上會得到市電的 60Hz 正弦波,但實際結果則是多個波峰垂直切片交疊,導致同一區間包含多個波峰。推敲原因,繪圖過程會消耗時間(Serial.print 實測結果約 3.16ms,還要加上 Serial.println() 時間),導致取樣頻率非 1KHz,且因繪圖複雜動作耗時難以預測,無法精確控制取樣間隔。

ESP32 有個先進武器 - 雙核多工,現在有機會派上用場了。

用 xTaskCreatePinnedToCore() 建立一個獨立 Task,在 Task 跑無窮迴圈 delay(1) 1ms analogRead() 取樣一次,循環記錄 100 個點。而繪圖部分留在 loop() 函式中,因不會干擾取樣,故不必要求最短時間跑完,故改為每次重繪 100 個點的完整波形:

#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_I2CDevice.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for SSD1306 display connected using software SPI (default case):
#define OLED_MOSI 23 // D1
#define OLED_CLK 18  // D0
#define OLED_DC 16
#define OLED_CS 5
#define OLED_RESET 17
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT,
                         OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

#define AIO_PIN1 34
#define AIO_PIN2 35

#define DATA_LEN 100
byte data1[DATA_LEN];
byte data2[DATA_LEN];
int dataIdx = 0;
int v1, v2;
int divFact = 4096 / 32;
#define V1_BASE 31
#define V2_BASE 63
TaskHandle_t SamplingTask;

void Task1code(void *pvParameters)
{
  for (;;)
  {
    v1 = analogRead(AIO_PIN1);
    v2 = analogRead(AIO_PIN2);
    data1[dataIdx] = v1 / divFact;
    data2[dataIdx] = v2 / divFact;
    dataIdx++;
    if (dataIdx > DATA_LEN - 1)
      dataIdx = 0;
    delay(1);
  }
}

void setup()
{
  Serial.begin(115200);
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC))
  {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ; // Don't proceed, loop forever
  }

  // Clear the buffer
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.display();

  pinMode(AIO_PIN1, INPUT);
  pinMode(AIO_PIN2, INPUT);

  for (int i = 0; i < DATA_LEN; i++)
    data1[i] = data2[i] = 0;

  xTaskCreatePinnedToCore(
      Task1code,     /* Task function. */
      "Task1",       /* name of task. */
      10000,         /* Stack size of task */
      NULL,          /* parameter of the task */
      1,             /* priority of the task */
      &SamplingTask, /* Task handle to keep track of created task */
      0);            /* pin task to core 0 */
}

void loop()
{
  display.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SSD1306_BLACK);
  for (int drawIdx = 0; drawIdx < DATA_LEN; drawIdx++) {
    display.drawLine(drawIdx, V1_BASE, drawIdx, V1_BASE - data1[drawIdx], SSD1306_WHITE);
    display.drawLine(drawIdx, V2_BASE, drawIdx, V2_BASE - data2[drawIdx], SSD1306_WHITE);
  }
  display.display();
}

結果出奇成功,100 個點相當於 0.1 秒,圖形剛好出現 6 個波峰不多不少,60Hz * 0.1s = 6,完美。

xTaskCreatePinnedToCore() 時還可以指定特定 CPU Core 執行,若是更複雜更吃重的作業,開發者可精準分配 CPU 資源,像是指定取樣用某一核,繪圖用另一核之類,但我的簡單案例把取樣拉出來就夠了。

這次的案例讓我體驗到 ESP32 多核平行處理能力的優勢,實作方式意外簡單,腦中又浮出許多有趣的點子,就等有閒有緣再一一實現吧。

【參考資料】

Example of how to implement multitasking in ESP32 Arduino on fixed-frequence sampling.


Comments

Be the first to post a comment

Post a comment