ESP32 多核多工執行應用實例 - 定頻取樣
0 |
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 多核平行處理能力的優勢,實作方式意外簡單,腦中又浮出許多有趣的點子,就等有閒有緣再一一實現吧。
【參考資料】
- ESP32類比讀取(analogRead):小夜燈 by 夜市小霸王
- ESP32 特殊應用:多核心執行 by 夜市小霸王
- ESP32 ADC – Read Analog Values with Arduino IDE by Random Nerd Tutorials
- How to use ESP32 Dual Core with Arduino IDE by Random Nerd Tutorials
Example of how to implement multitasking in ESP32 Arduino on fixed-frequence sampling.
Comments
Be the first to post a comment