> For the complete documentation index, see [llms.txt](https://think-embedded.gitbook.io/micropython/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://think-embedded.gitbook.io/micropython/micropython-for-esp32/esp32-with-dual-channel-dac-output.md).

# ESP32 with Dual-Channel DAC Output

## **การใช้งาน DAC ของ ESP32** <a href="#id-16b9" id="id-16b9"></a>

ถ้าต้องการใช้งานวงจร **DAC** ภายในชิป **ESP32** ก็มี 2 ช่องสัญญาณสำหรับขา **I/O** ให้เลือกใช้ได้ และในส่วนการเขียนโค้ดไมโครไพธอน ก็มีคำสั่งในไลบรารี **`machine.DAC`** ไว้ใช้งานร่วมกับ **`machine.Pin`**

เมื่อแปลงข้อมูลเป็นสัญญาณไฟฟ้า จะได้แรงดันไฟฟ้าในช่วง **0V** และไม่เกิน **+3.3V (VCC)** แต่ความละเอียดของข้อมูลเอาต์พุตสำหรับ **DAC** ของ **ESP32** นั้น มีขนาดเพียง 8 บิต ดังนั้นจึงเขียนค่าเป็นจำนวนเต็มได้ในช่วง **0..255** เท่านั้น

## **การสร้างสัญญาณรูปไซน์ (Sine Wave Generation)** <a href="#id-3859" id="id-3859"></a>

ถ้าเราต้องการจะสร้างเอาต์พุตให้มีลักษณะเป็นรูปคลื่นสัญญาณไซน์โดยใช้ **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** ซึ่งเป็นค่าแอมพลิจูดของฟังก์ชันทั้งสอง

$$
x(t) = A, sin(\omega\_0 t),;; y(t) = A, sin(\omega\_0 t + \phi) \\
\phi = \mbox{Phase Difference}
$$

ถ้าให้ผลต่างเฟส (**Phase Difference**) เท่ากับ **90** องศา ก็จะเขียนใหม่ได้เป็น

$$
x(t) = A, sin(\omega\_0 t),;; y(t) = A, sin(\omega\_0 t + \frac{\pi}{2}) = A,cos(\omega\_0 t)
$$

แต่ถ้าจะนำไปวาดรูปกราฟด้วยคอมพิวเตอร์หรือประมวลผลเชิงตัวเลข ตัวแปร **t** จะถูกแทนที่ด้วยตัวแปร **i** ที่เป็นเลขจำนวนเต็ม (เป็น **discrete-time steps**) เช่น อยู่ในช่วง **0** ถึง **(N-1)** สำหรับลำดับของข้อมูลหรือจุดพิกัดที่มีจำนวนเท่ากับ **N**

ดังนั้นเราสามารถสร้างฟังก์ชันสำหรับพิกัด (**x,y)** ที่ขึ้นอยู่กับตัวแปร **i** ที่เป็นเลขจำนวนเต็ม (เป็น **index** แทนการใช้ตัวแปร **t**) ตามรูปสมการดังนี้

![](/files/-MKFzymPl7xRsA3foOWc)

ถ้าให้ **N** เป็นเลขจำนวนเต็มบวก เราสามารถสร้างอาร์เรย์เพื่อเก็บค่าที่คำนวณไว้ล่วงหน้า ใช้เป็นตารางค่าคงที่ (**Lookup Table**) หรือบางทีก็เรียกว่า **Waveform Table** ดังนั้นเวลาจะสร้างสัญญาณเอาต์พุต ก็อ่านค่าตัวเลขดังกล่าวไปตามลำดับจนครบแล้ววนซ้ำ

ข้อสังเกต: การเลือกค่าสำหรับ **N** จะมีผลต่อขนาดของตารางและการใช้หน่วยความจำของไมโครคอนโทรลเลอร์ และเมื่อคำนวณค่าสำหรับ **(x,y)** จะต้องกำหนดให้เป็นค่าเลขจำนวนเต็ม และอยู่ในช่วง **0..255** (มีค่ากลางเท่ากับ 127) และเมื่อนำไปบวกหรือลบกับ **A** (แอมพลิจูด) จะต้องอยู่ในช่วงดังกล่าว เมื่อจะนำไปใช้กับ **DAC** ของ **ESP32**

อย่างไรก็ตาม เอาต์พุตที่ได้และเมื่อแปลงเป็นแรงดันไฟฟ้าแล้ว มีค่าไม่ต่อเนื่อง (**Discrete Values**) ในเชิงแอมพลิจูด ( ถ้าไม่มีวงจรกรองสัญญาณที่เรียกว่า **Smoothing Filter** )

## **ตัวอย่างการเขียนโค้ด** <a href="#id-47a9" id="id-47a9"></a>

ในโค้ดตัวอย่างนี้ เราจะใช้ตัวแปร **`index`** เริ่มนับขึ้นจาก **0** ไปจนถึง **`(N-1)`** แล้ววนซ้ำเริ่มใหม่ ค่าของตัวแปรนี้จะถูกใช้ในการอ่านค่าจากอาร์เรย์ **`sin_table`** และ **`cos_table`** ที่ได้มีการคำนวณค่าเก็บไว้ก่อนแล้วหลังจากได้ทำคำสั่งในฟังก์ชัน **`create_wave_tables()`**

ค่าที่อ่านได้จากตารางตามตำแหน่งที่อ้างอิงด้วยตัวแปร **`index`** ในแต่ละครั้ง จะถูกนำไปใช้เป็นค่าเอาต์พุต **DAC** จำนวน 2 ช่องสัญญาณตามลำดับ การทำงานในลูปแต่ละรอบ จะไม่มีการหน่วงเวลา และความถี่ของสัญญาณเอาต์พุตที่ได้ จะขึ้นอยู่กับความเร็วในการประมวลผลของ **ESP32** ที่เขียนโค้ดด้วยไมโครไพธอน

ในโค้ดตัวอย่างนี้ ได้เลือกใช้ค่า **N=200**, **A=100**, **P=2** และ **Q=3**

```python
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**](https://m5stack.com/collections/m5-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**)

```python
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**)

![](/files/-MKG2Qqhf4265CPbxhhs)

![รูปภาพ: ตัวอย่างรูปคลื่นสัญญาณที่วัดได้ (ขึ้นอยู่กับค่า P และ Q ที่เลือกใช้)](/files/-MKG2JcovhOG-TyPpLuX)

![รูปภาพ: ตัวอย่างรูปคลื่นสัญญาณที่วัดได้ (P=Q=1)](/files/-MKG31ccA_HX5ptZpdny)

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

![รูปภาพ: ตัวอย่างเครื่องมือวัดที่ได้นำมาใช้งาน](/files/-MKG5A95MtiR-17ELN7x)

![รูปภาพ: รูปสัญญาณที่ได้จาก Function Generator](/files/-MKG4mLlVnHdgZTVuzjE)

## **โค้ดวาดรูปกราฟสำหรับ ESP32-M5Stack** <a href="#id-7810" id="id-7810"></a>

จากการกำหนดรูปแบบของฟังก์ชันสำหรับพิกัด **(x,y)** เราก็มาดูตัวอย่างการเขียนโค้ดไมโครไพธอน เพื่อลองวาดรูปกราฟ และแสดงผลบนจอ LCD ขนาด **320x240** ของอุปกรณ์ [**M5Stack-Core (ESP32-based)**](https://m5stack.com/collections/m5-core) โดยมีการสุ่มค่า **P** และ **Q** ให้เป็นจำนวนเต็มในช่วง **1..10** ดังนั้นรูปกราฟหรือเส้นโค้ง (**Curve**) ที่ได้ จะแตกต่างกันไปตามอัตราส่วน **P:Q**

ถ้าเราใช้ **API** หรือไลบรารีของ [**M5Stack**](https://github.com/m5stack/M5Stack) สำหรับภาษาไมโครไพธอน ก็มีคำสั่ง **`lcd.drawPixel()`** เพื่อวาดจุดหนึ่งพิกเซลบนสกรีน (**Screen**) โดยระบุตำแหน่งหรือค่า **(x,y)**

ถ้าต้องการเชื่อมต่อจุดเหล่านั้นที่อยู่ถัดไปตามลำดับด้วยเส้นตรง ก็สามารถใช้คำสั่ง **`lcd.drawLine()`** พร้อมกำหนดสีของเส้นตรงนั้นได้ด้วย

ในโค้ดตัวอย่างนี้ ได้กำหนดให้ **N=256** และ **A=100**

```python
## 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**

![รูปภาพ: การแสดงรูปเส้นโค้ง Lissajous Curve (P=8, Q=7)](/files/-MKIH9SKKzNHBXK9nSzm)

![รูปภาพ: การแสดงรูปเส้นโค้ง Lissajous Curve (P=7, Q=5)](/files/-MKIHN8uyqKSSvBDpSrP)

![รูปภาพ: การแสดงรูปเส้นโค้ง Lissajous Curve (P=1, Q=1)](/files/-MKIHhQZ_gii8h0SLLFO)

## กล่าวสรุป

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

{% hint style="info" %}
**เผยแพร่ภายใต้ลิขสิทธิ์**\
**Attribution-ShareAlike 4.0 International (**[**CC BY-SA 4.0**](https://creativecommons.org/licenses/by-sa/4.0/)**)**
{% endhint %}
