ESP32 with Dual-Channel DAC Output
ตัวอย่างการเขียนโค้ดไมโครไพธอน สำหรับ ESP32 เพื่อสร้างสัญญาณเอาต์พุต โดยใช้วงจร DAC (Digital-to-Analog Converter) ที่อยู่ภายใน ESP32 จำนวน 2 ช่องสัญญาณ
การใช้งาน DAC ของ ESP32
ถ้าต้องการใช้งานวงจร DAC ภายในชิป ESP32 ก็มี 2 ช่องสัญญาณสำหรับขา I/O ให้เลือกใช้ได้ และในส่วนการเขียนโค้ดไมโครไพธอน ก็มีคำสั่งในไลบรารี machine.DAC
ไว้ใช้งานร่วมกับ machine.Pin
เมื่อแปลงข้อมูลเป็นสัญญาณไฟฟ้า จะได้แรงดันไฟฟ้าในช่วง 0V และไม่เกิน +3.3V (VCC) แต่ความละเอียดของข้อมูลเอาต์พุตสำหรับ DAC ของ ESP32 นั้น มีขนาดเพียง 8 บิต ดังนั้นจึงเขียนค่าเป็นจำนวนเต็มได้ในช่วง 0..255 เท่านั้น
การสร้างสัญญาณรูปไซน์ (Sine Wave Generation)
ถ้าเราต้องการจะสร้างเอาต์พุตให้มีลักษณะเป็นรูปคลื่นสัญญาณไซน์โดยใช้ ESP32 แต่มีแรงดันไฟฟ้าอยู่ระหว่าง 0V แต่ไม่เกิน +3.3V จะเขียนโค้ดอย่างไร ?
ถ้าสร้างสัญญาณรูปคลื่นไซน์ได้ (Sinusoidal Waveform) และมีจำนวนสองฟังก์ชันที่ขึ้นกับตัวแปรเวลา t แล้วนำมากำหนดพิกัด (x(t),y(t)) ในการวาดกราฟแบบพาราเมตริก (Parametric Plot) ก็จะเป็นการวาดเส้นโค้งให้ได้รูปที่เรียกว่า “Lissajous Figures” (“รูปลิสซาจูส์”)
กำหนดให้ฟังก์ชันทั้งสองมีความต่างเฟสกัน (เช่น ให้มีความต่างเฟสกัน 90 องศา) อาจมีความถี่เท่ากันหรือต่างกัน (แต่เป็นอัตราส่วนที่เป็นจำนวนตรรกยะ)
ลองมาดูตัวอย่างการสร้างฟังก์ชันสำหรับ x(t) และ y(t) ให้เป็นรูปแบบฟังก์ชันไซน์ ในกรณีนี้ ค่าของ x(t) และ y(t) จะอยู่ระหว่าง -A และ A ถ้า A > 0 ซึ่งเป็นค่าแอมพลิจูดของฟังก์ชันทั้งสอง
ถ้าให้ผลต่างเฟส (Phase Difference) เท่ากับ 90 องศา ก็จะเขียนใหม่ได้เป็น
แต่ถ้าจะนำไปวาดรูปกราฟด้วยคอมพิวเตอร์หรือประมวลผลเชิงตัวเลข ตัวแปร t จะถูกแทนที่ด้วยตัวแปร i ที่เป็นเลขจำนวนเต็ม (เป็น discrete-time steps) เช่น อยู่ในช่วง 0 ถึง (N-1) สำหรับลำดับของข้อมูลหรือจุดพิกัดที่มีจำนวนเท่ากับ N
ดังนั้นเราสามารถสร้างฟังก์ชันสำหรับพิกัด (x,y) ที่ขึ้นอยู่กับตัวแปร i ที่เป็นเลขจำนวนเต็ม (เป็น index แทนการใช้ตัวแปร t) ตามรูปสมการดังนี้

ถ้าให้ N เป็นเลขจำนวนเต็มบวก เราสามารถสร้างอาร์เรย์เพื่อเก็บค่าที่คำนวณไว้ล่วงหน้า ใช้เป็นตารางค่าคงที่ (Lookup Table) หรือบางทีก็เรียกว่า Waveform Table ดังนั้นเวลาจะสร้างสัญญาณเอาต์พุต ก็อ่านค่าตัวเลขดังกล่าวไปตามลำดับจนครบแล้ววนซ้ำ
ข้อสังเกต: การเลือกค่าสำหรับ N จะมีผลต่อขนาดของตารางและการใช้หน่วยความจำของไมโครคอนโทรลเลอร์ และเมื่อคำนวณค่าสำหรับ (x,y) จะต้องกำหนดให้เป็นค่าเลขจำนวนเต็ม และอยู่ในช่วง 0..255 (มีค่ากลางเท่ากับ 127) และเมื่อนำไปบวกหรือลบกับ A (แอมพลิจูด) จะต้องอยู่ในช่วงดังกล่าว เมื่อจะนำไปใช้กับ DAC ของ ESP32
อย่างไรก็ตาม เอาต์พุตที่ได้และเมื่อแปลงเป็นแรงดันไฟฟ้าแล้ว มีค่าไม่ต่อเนื่อง (Discrete Values) ในเชิงแอมพลิจูด ( ถ้าไม่มีวงจรกรองสัญญาณที่เรียกว่า Smoothing Filter )
ตัวอย่างการเขียนโค้ด
ในโค้ดตัวอย่างนี้ เราจะใช้ตัวแปร index
เริ่มนับขึ้นจาก 0 ไปจนถึง (N-1)
แล้ววนซ้ำเริ่มใหม่ ค่าของตัวแปรนี้จะถูกใช้ในการอ่านค่าจากอาร์เรย์ sin_table
และ cos_table
ที่ได้มีการคำนวณค่าเก็บไว้ก่อนแล้วหลังจากได้ทำคำสั่งในฟังก์ชัน create_wave_tables()
ค่าที่อ่านได้จากตารางตามตำแหน่งที่อ้างอิงด้วยตัวแปร index
ในแต่ละครั้ง จะถูกนำไปใช้เป็นค่าเอาต์พุต DAC จำนวน 2 ช่องสัญญาณตามลำดับ การทำงานในลูปแต่ละรอบ จะไม่มีการหน่วงเวลา และความถี่ของสัญญาณเอาต์พุตที่ได้ จะขึ้นอยู่กับความเร็วในการประมวลผลของ ESP32 ที่เขียนโค้ดด้วยไมโครไพธอน
ในโค้ดตัวอย่างนี้ ได้เลือกใช้ค่า N=200, A=100, P=2 และ Q=3
import utime as time
from machine import Pin, DAC
import math
DAC_PINS = [25,26] # ESP32 DAC pins
def dac_init():
dac_units = []
for pin in DAC_PINS:
dac = DAC( Pin(pin, Pin.OUT) )
dac.write(0)
dac_units.append( dac )
return dac_units
# global variables (constants)
P = 2
Q = 3
N = 200
A = 100
Omega = 2*math.pi/N
# global variables for wave tables
sin_table = []
cos_table = []
def create_wave_tables():
global sin_table, cos_table
for i in range(N):
arg = Omega*i
x = A*math.sin( P*arg ) + 127
y = A*math.cos( Q*arg ) + 127
sin_table.append( int(x) )
cos_table.append( int(y) )
# global variable (main-loop condition)
running = True
def btn_cb(p):
global running
running = False
BUTTON_C_PIN = const(37)
btnC = Pin( BUTTON_C_PIN, Pin.IN, Pin.PULL_UP )
btnC.irq( trigger=Pin.IRQ_FALLING, handler=btn_cb )
# global variable for indexing the wave tables
index = 0 # index range: 0..(N-1)
try:
create_wave_tables()
dac_outputs = dac_init()
while running:
x = sin_table[ index ]
y = cos_table[ index ]
index = (index+1) % N
dac_outputs[0].write( x )
dac_outputs[1].write( y )
except KeyboardInterrupt:
pass
for dac in dac_outputs:
dac.write(0)
print('Done')
การหยุดการทำงานของโค้ด ทำได้โดยการกดปุ่มที่ต่อกับขา GPIO-37 (ทำงานแบบ Active-Low) และถ้าใช้อุปกรณ์ M5Stack-Core ก็จะตรงกับปุ่ม Button C (BtnC)
ถัดไปลองเปลี่ยนมาใช้ Hardware Timer ของ ESP32 เป็นตัวกำหนดจังหวะการเขียนค่าเอาต์พุตให้ DAC จะเขียนโค้ดอย่างไร มาดูตัวอย่างกัน
ในโค้ดตัวอย่างนี้ เราใช้คำสั่งที่เกี่ยวข้องกับ machine.Timer
เพื่อเปิดใช้งาน Hardware Timer
(เลือกใช้หมายเลข 4 )
การทำงานของไทม์เมอร์ (Timer) เป็นแบบ periodic คือ จะทำให้เกิดอินเทอร์รัพท์จากไทม์เมอร์ซ้ำอีก โดยระบุคาบ (Period) เป็นตัวเลขจำนวนเต็มบวก (หน่วยเป็น มิลลิวินาที)
เมื่อเกิดอินเทอร์รัพท์ในแต่ละครั้ง และฟังก์ชัน Callback (Interrupt Handler) ที่เกี่ยวข้องจะคอยทำหน้าที่อ่านค่าจากตารางค่าคงที่ตามลำดับของ index แล้วเขียนค่าเอาต์พุตสำหรับ DAC ทั้งสองช่อง
ถ้ากำหนดให้ N=200 และคาบเวลาเท่ากับ 1 msec จะได้สัญญาณเอาต์พุต (ไซน์) ที่มีคาบเท่ากับ 200 msec หรือมีความถี่เท่ากับ 5 Hz (กำหนดให้ P=Q=1)
import utime as time
from machine import Pin, DAC, Timer
import math
DAC_PINS = [25,26] # ESP32 DAC pins
def dac_init():
dac_units = []
for pin in DAC_PINS:
dac = DAC( Pin(pin, Pin.OUT) )
dac.write(0)
dac_units.append( dac )
return dac_units
# N=200, Timer period=1ms => output freq. 5Hz (for P=Q=1)
# global variables (constants)
P = 2
Q = 3
N = 200
A = 100
Omega = 2*math.pi/N
# global variables for storing values in wave tables
sin_table = []
cos_table = []
# global variable for indexing the wave tables
index = 0 # range: 0..(N-1)
def create_wave_tables():
global sin_table, cos_table
for i in range(N):
arg = Omega*i
x = A*math.sin( P*arg ) + 127
y = A*math.cos( Q*arg ) + 127
sin_table.append( int(x) )
cos_table.append( int(y) )
# callback for hardware timer
def timer_cb(t):
global dac_outputs, index
index = (index+1) % N
x = sin_table[ index ]
y = cos_table[ index ]
dac_outputs[0].write( x )
dac_outputs[1].write( y )
# use Hardware Timer 4 in periodic mode (period=1ms)
timer = Timer(4)
timer.init(period=1, mode=Timer.PERIODIC, callback=timer_cb)
# global variable
running = True
def btn_cb(p):
global running
running = False
# constant
BUTTON_C_PIN = const(37)
# use button on GPIO-37
btnC = Pin( BUTTON_C_PIN, Pin.IN, Pin.PULL_UP )
btnC.irq( trigger=Pin.IRQ_FALLING, handler=btn_cb )
try:
dac_outputs = dac_init()
create_wave_tables()
while running:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
timer.deinit()
for dac in dac_outputs:
dac.write(0)
print('Done')
ถ้าเราลองวัดสัญญาณที่ขาเอาต์พุตของ DAC ทั้งสองช่อง ที่ขา GPIO-25 และ GPIO-26 โดยใช้เครื่องออสซิลโลสโคป และเลือกโหมดการแสดงผลเป็นแบบ X-Y แทนการใช้โหมด Y-T และเลือก Coupling Mode เป็นแบบ AC จะได้รูปตามตัวอย่างดังนี้ (ถ้ามีโอกาสได้ทดลองกับอุปกรณ์จริง ให้ทดลองเปลี่ยนค่า P และ Q)



เราสามารถนำไปเปรียบเทียบรูปคลื่นสัญญาณที่สร้างจากเครื่องกำเนิดสัญญาณ (Function Generator) แบบสองช่องเอาต์พุต


โค้ดวาดรูปกราฟสำหรับ ESP32-M5Stack
จากการกำหนดรูปแบบของฟังก์ชันสำหรับพิกัด (x,y) เราก็มาดูตัวอย่างการเขียนโค้ดไมโครไพธอน เพื่อลองวาดรูปกราฟ และแสดงผลบนจอ LCD ขนาด 320x240 ของอุปกรณ์ M5Stack-Core (ESP32-based) โดยมีการสุ่มค่า P และ Q ให้เป็นจำนวนเต็มในช่วง 1..10 ดังนั้นรูปกราฟหรือเส้นโค้ง (Curve) ที่ได้ จะแตกต่างกันไปตามอัตราส่วน P:Q
ถ้าเราใช้ API หรือไลบรารีของ M5Stack สำหรับภาษาไมโครไพธอน ก็มีคำสั่ง lcd.drawPixel()
เพื่อวาดจุดหนึ่งพิกเซลบนสกรีน (Screen) โดยระบุตำแหน่งหรือค่า (x,y)
ถ้าต้องการเชื่อมต่อจุดเหล่านั้นที่อยู่ถัดไปตามลำดับด้วยเส้นตรง ก็สามารถใช้คำสั่ง lcd.drawLine()
พร้อมกำหนดสีของเส้นตรงนั้นได้ด้วย
ในโค้ดตัวอย่างนี้ ได้กำหนดให้ N=256 และ A=100
## Parametric plot: Lissajous figure
from m5stack import *
import utime as time
import math
from urandom import *
# define the screen size (width and height in pixels)
scr_w, scr_h = (320,240)
def draw_curve(A,N,P,Q):
lcd.clear()
last_point = None
line_color = lcd.YELLOW
omega = 2*math.pi/N
phase = math.pi/2
for i in range(N+1):
x = scr_w//2 + int( A*math.sin( P*omega*i + phase ) )
y = scr_h//2 - int( A*math.sin( Q*omega*i ) )
if last_point:
lx, ly = last_point
lcd.drawLine( lx, ly, x, y, line_color )
else:
lcd.drawPixel( x, y, line_color )
last_point = (x,y)
text = 'P={}, Q={}'.format(P,Q)
lcd.print(text, lcd.CENTER, 222, lcd.GREEN)
# global variable (for the main loop condition)
running = True
def btnC_callback():
global running
running = False
btnC.wasPressed( btnC_callback )
try:
while running:
P = randint(1,10)
Q = randint(1,10)
draw_curve( 100, 256, P, Q )
time.sleep(3.0)
except KeyboardInterrupt:
pass
lcd.clear()
print('Done')
โค้ดนี้จะทำงานต่อเนื่องไปเรื่อย ๆ ถ้าต้องการหยุดการทำงาน ให้กดปุ่มแล้วปล่อยที่ Button C (BtnC) ของ M5 Stack



กล่าวสรุป
เราได้เห็นตัวอย่างการเขียนโค้ดไมโครไพธอนสำหรับ ESP32 เพื่อสร้างสัญญาณเอาต์พุตเป็นรูปคลื่นไซน์ โดยใช้ DAC จำนวน 2 ช่อง เพื่อวาดรูปเส้นโค้ง Lissajous แล้ววัดสัญญาณด้วยเครื่องออสซิลโลสโคป เพื่อดูคลื่นสัญญาณที่ได้ในโหมด XY และได้สาธิตการเขียนโค้ดเพื่อวาดรูปเส้นโค้ง Lissajous และแสดงผลบนจอ LCD ของอุปกรณ์ M5Stack Core
Last updated
Was this helpful?