MicroPython Programming for ESP32 [11]

<rawat.s>
6 min readMay 4, 2020

--

‍‍‍‍‍‍ ‍‍ ‍‍‍‍‍‍ ‍‍‍‍‍‍ ‍‍

บทความนี้นำเสนอตัวอย่างการเขียนโค้ด MicroPython สำหรับ ESP32 และกล่าวถึง การใช้งานโมดูล PCF8574 ซึ่งเป็นตัวช่วยขยายหรือเพิ่มจำนวนขา I/O แบบดิจิทัลให้กับไมโครคอนโทรลเลอร์ และเชื่อมต่อด้วยบัส I2C

I/O Port Expander

‍‍‍‍‍‍ ‍‍ ‍‍‍‍‍‍ ‍‍‍‍‍‍ ‍‍

ในการออกแบบระบบสมองกลฝังตัวที่ใช้ไมโครคอนโทรลเลอร์ ปัญหาหนึ่งที่อาจพบได้ คือ ถ้าเลือกใช้ไมโครคอนโทรลเลอร์ที่มีจำนวนขา I/O จำกัด จะมีวิธีอย่างไรบ้างที่จะช่วยเพิ่มขา I/O

เทคนิคหนึ่งคือ การใช้ไอซีมาต่อเพิ่ม และสามารถสื่อสารข้อมูลกับไมโครคอนโทรลเลอร์โดยใช้สัญญาณเพียงไม่กี่เส้น อาจจะเป็น I2C หรือ SPI เป็นต้น

การใช้งานบัส I2C มักพบเห็นได้บ่อย เนื่องจากสามารถใช้สัญญาณสื่อสารเพียง 2 เส้น คือ SCL และ SDA อุปกรณ์ที่สามารถสื่อสารแบบนี้ได้ เช่น ไอซีเซนเซอร์ประเภทต่าง ๆ โมดูลแสดงผลกราฟิกขนาดเล็กแบบ OLED เป็นต้น

ไอซีประเภท I/O Port Expander ก็เป็นอีกหนึ่งประเภทที่มีให้เลือกใช้งานและใช้รูปแบบการสื่อสารข้อมูลด้วย I2C และ SPI ยกตัวอย่างเช่น

ถ้าใช้แบบ I2C ไอซีหลายรุ่น ก็สามารถรองรับความเร็วสำหรับ I2C ได้ตั้งแต่

  • 100kHz (Standard-mode)
  • 400kHz (Fast-mode)
  • >1MHz (High speed mode)

แต่ถ้าเลือกใช้ SPI ก็จะสามารถใช้ความเร็วได้สูงกว่านั้น เช่น ได้ถึง 10MHz สำหรับไอซีบางรุ่น

ในบทความนี้ เราจะมาลองใช้โมดูล PCF8574 มาลองต่อวงจรร่วมกับ LED และปุ่มกดหลายชุด

PCF8574: I2C 8-Bit I/O Port Expander

‍‍‍‍‍‍ ‍‍ ‍‍‍‍‍‍ ‍‍‍‍‍‍ ‍‍

ไอซี PCF8574 และ PCF8574A มีให้เลือกใช้ได้ แตกต่างกันไปตามตัวถัง (IC Package)

แผงผังองค์ประกอบภายใน (Block Diagram) ของ PCF8574/A (อ้างอิง: NXP Datasheet)
ตำแหน่งขาของไอซี PCF8574/A ตัวถังแบบ PDIP16 และ SOP-16 (อ้างอิง: NXP Datasheet)

ในปัจจุบัน ก็มีโมดูล PCF8574 หลายรูปแบบให้เลือกใช้ ทำให้ช่วยลดเวลาในการต่อวงจรบนเบรดบอร์ด

ตัวอย่างโมดูล PCF8574
ตัวอย่างโมดูล PCF8574

PCF8574 จะทำงานเป็นอุปกรณ์สเลฟในระบบบัส I2C (Slave Device) ซึ่งจะต้องคอยตอบสนองจากคำสั่งที่ถูกส่งมาจากอุปกรณ์ที่เป็นมาสเตอร์ (Master Device) เช่น ไมโครคอนโทรลเลอร์ และจะต้องมีการกำหนดแอดเดรส (Address) ขนาด 7 บิต ที่ไม่ซ้ำ ให้กับอุปกรณ์ที่เป็นสเลฟ ในบัสเดียวกัน

ตัวอย่างระบบบัส I2C ที่มีอุปกรณ์ Master และ Slave หลายตัว (อ้างอิง: NXP Datasheet)
ตัวอย่างรูปแบบการต่อวงจร PCF8574 กับอุปกรณ์อื่น (อ้างอิง: TI Datasheet)

ขา SCL และ SDA เป็นขาสัญญาณ Clock และสัญญาณ Data สำหรับสื่อสารตามโพรโตคอล I2C และจะต้องมีการต่อตัวต้านทานแบบ Pullup ที่ขา SCL และ SDA อย่างละตัวด้วย

ขา P0..P7 เป็นขา I/O (bi-directional) ขนาด 8 บิต สามารถเลือกใช้เป็นขาอินพุตหรือเอาต์พุตได้โดยอิสระ ซึ่งอยู่กับการเขียนข้อมูลไบต์ไปยังรีจิสเตอร์ของไอซี

ขา /INT เป็นขาเอาต์พุต (Open-Collector/Open-Drain Output) ทำงานแบบ Active-Low เมื่อต่อกับตัวต้านทาน Pullup สถานะปรกติจะเป็น High แต่เมื่อมีการเปลี่ยนแปลงสถานะลอจิกที่ขา P0..P7 อย่างน้อยหนึ่งขา (Pin Change: เปลี่ยนจาก Low เป็น High และเปลี่ยนจาก High เป็น Low) จะทำให้เปลี่ยนสถานะเป็น Low และสามารถนำไปใช้สร้างสัญญาณอินเทอร์รัพท์ให้กับไมโครคอนโทรลเลอร์ได้

VDD และ VSS เป็นขาสำหรับป้อนแรงดันไฟเลี้ยงและ GND โดยทั่วไปก็สามารถใช้ได้ทั้ง 5V และ 3.3V

ขา A2 A1 A0 เป็นขาดิจิทัล-อินพุต ใช้สำหรับกำหนดบิต 3 ล่าง ของแอดเดรสที่มีทั้งหมด 7 บิต (A6 .. A0)

7-Bit Address + R/W Bit สำหรับ PCF8574 และ PCF8574A
ตารางแสดงค่าแอดเดรสของ PCF8574 เมื่อมีการกำหนดค่าอินพุตให้ขา A0 A1 A2
ตารางแสดงค่าแอดเดรสของ PCF8574A เมื่อมีการกำหนดค่าอินพุตให้ขา A0 A1 A2

การสื่อสารข้อมูลด้วย I2C กับไอซี PCF8574 ก็ทำได้ง่ายคือ เป็นการเขียนหรืออ่านข้อมูลเพียงหนึ่งไบต์เท่านั้น

  • การเขียนข้อมูล (Write To): ไบต์แรกที่ถูกส่งไปยัง PCF8574 คือ 7-Bit Address + R/W Bit (=0) แล้วตามด้วยข้อมูลไบต์ จำนวน 1 ไบต์ ในกรณีนี้มาสเตอร์เป็นผู้ส่งข้อมูลไบต์ (Transmitter)
  • การอ่านข้อมูล (Read From): ไบต์แรกที่ถูกส่งไปยัง PCF8574 คือ 7-Bit Address + R/W Bit (=1) แล้วตามด้วยการอ่านข้อมูลกลับมา 1 ไบต์ ในกรณีนี้ มาสเตอร์เป็นผู้ที่คอยรับข้อมูลไบต์ (Receiver)

ถ้ากล่าวถึงการทำงานของ I2C จะมีรายละเอียดเพิ่มเติม เช่น การเริ่มด้วยด้วย Start Condition (S) โดยอุปกรณ์ที่ทำหน้าที่เป็นมาสเตอร์ และการจบการสื่อสารข้อมูลด้วย Stop Condition (P) บิตที่แสดงการตอบกลับ (Acknowledge: ACK) หรือ ไม่ตอบกลับ (No Acknowledge: NACK) จากฝ่ายผู้รับข้อมูลไบต์ เป็นต้น

ตัวอย่าง I2C Bus Transaction: Write Operation (อ้างอิง: NXP Datasheet)
ตัวอย่าง I2C Bus Transaction: Read Operation (อ้างอิง: NXP Datasheet)

ข้อสังเกต:

  • การใช้งานขา P0..P7 ในทิศทางเอาต์พุต (Output Direction) สามารถรับกระแส (Current Sink) ได้ไม่เกิน 25mA แต่ไม่เหมาะกับการจ่ายกระแสให้โหลด (Current Source) ซึ่งได้ไม่เกิน 300uA หรือ 0.3mA
  • ถ้าต้องการใช้ขาใดของ P0..P7 ในทิศทางอินพุต (Input Direction) จะต้องเขียนข้อมูลไบต์ไปยังไอซี เพื่อให้ตำแหน่งของบิตที่เกี่ยวข้องมีสถานะเป็น 1 (High)

ตัวอย่างการกำหนดสถานะเอาต์พุตของ PCF8574 ให้วงจร LED 8 บิต

‍‍‍‍‍‍ ‍‍ ‍‍‍‍‍‍ ‍‍‍‍‍‍ ‍‍

ตัวอย่างสาธิตการใช้งานแรกนี้คือ การต่อวงจร LED ร่วมกับ PCF8574 สำหรับเอาต์พุตเท่านั้น (Output Pin Only) โดยมีรูปแบบการต่อวงจรดังนี้

  • ขา P0 .. P7 จะถูกใช้เป็นขาเอาต์พุต แต่ละขาจะต่อเข้ากับวงจร LED พร้อมตัวต้านจำกัดกระแส มีทั้งหมด 8 ชุด การต่อวงจร LED จะต้องต่อแบบ Active-Low โดยใช้ขา P0..P7 เป็นขาเอาต์พุต
  • เมื่อเอาต์พุตมีค่าบิตเป็น 0 จะทำให้มีกระแสไหลจาก VCC (3.3V) ผ่าน LED จากขาแอโนด (Anode) ไปยังขาแคโทด (Cathode) ผ่านตัวต้านทานจำกัดกระแส ไปที่ขาของ PCF8547 ก่อนที่จะไหลเข้าไปข้างในไอซีแล้วไปยัง GND ของวงจร

โค้ดตัวอย่างต่อไปนี้ สาธิตการเขียนข้อมูลขนาดหนึ่งไบต์ที่เป็นค่าของตัวแปร cnt (มีค่าในช่วง 0..255) แต่มีการกลับค่าบิต (Bit Inversion) ก่อนถูกเขียนไปยัง PCF8574 และนำไปแสดงผลกับ LED (8 บิต) ซึ่งทำงานแบบ Active-Low (ลอจิก 0 จะทำให้ LED อยู่ในสถานะ ON)

คำสั่งของ machine.I2C ที่ใช้เขียนข้อมูลในตัวอย่างนี้คือ writeto() โดยมีอาร์กิวเมนต์เป็นข้อมูลแบบ bytearray

ข้อสังเกต: แอดเดรสของ PCF8574 สำหรับตัวอย่างนี้ มีค่าเท่ากับ 0x21 (ขา A0=1, A1=0, A2=0) และในการต่อวงจร ได้เลือกใช้ขา GPIO21 และ GPIO22 สำหรับขา SDA และ SCL ตามลำดับ

# file: pcf8574_demo-1.py
from machine import Pin,I2C
import utime as time
# Use GPIO22=SCL, GPIO21=SDA
i2c = I2C( freq=100000,scl=Pin(22),sda=Pin(21) )
addr = 0x21
dev_found = addr in i2c.scan()
try:
cnt = 0 # counter variable, set to 0
data = bytearray(1) # one-byte data buffer
while dev_found:
data[0] = cnt ^ 0xff # invert bits
# write to PCF8574
i2c.writeto( addr, data )
# increment counter by 1
cnt = (cnt+1) % 256
time.sleep_ms(100)
except KeyboardInterrupt:
pass
finally:
print('Done')
ตัวอย่างการวางแผนต่อวงจรบนเบรดบอร์ด (Fritzing): ESP32 + PCF8574 + 8x LEDs

ตัวอย่างการอ่านค่าอินพุตจากวงจรปุ่มกด 8 บิตโดยใช้ PCF8574

‍‍‍‍‍‍ ‍‍ ‍‍‍‍‍‍ ‍‍‍‍‍‍ ‍‍

โค้ดตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อคอยอ่านข้อมูลขนาดหนึ่งไบต์จาก PCF8574 ในโหมดอินพุตเท่านั้น (Input Pin Only) และใช้ขา P0..P2 รับสัญญาณอินพุตจากวงจรปุ่มกด จำนวน 3 ชุด

ถ้ายังไม่มีการกดปุ่มใด ๆ ค่าของข้อมูลไบต์ที่อ่านได้ จะเป็น 0xFF (ทุกบิตมีค่าเป็น 1) แต่ถ้ามีปุ่มใดปุ่มหนึ่งถูกกดในขณะที่อ่านข้อมูล จะทำให้มีบิตที่เป็น 0 และเมื่อตรวจสอบพบว่า ปุ่มใดถูกกดอยู่ในขณะนั้น ให้แสดงเป็นข้อความเอาต์พุต

คำสั่งของ machine.I2C ที่ใช้อ่านข้อมูลในตัวอย่างนี้คือ readfrom() และได้ค่าที่มีชนิดข้อมูลเป็น bytes

ข้อสังเกต: แอดเดรสของ PCF8574 สำหรับตัวอย่างนี้ มีค่าเท่ากับ 0x20 (ขา A0=0, A1=0, A2=0)

# file: pcf8574_demo-2.py
from machine import Pin,I2C
import utime as time
# Use GPIO22=SCL, GPIO21=SDA
i2c = I2C( freq=100000,scl=Pin(22),sda=Pin(21) )
addr = 0x20
dev_found = addr in i2c.scan()
try:
# write 0xff to PCF8574 for input direction
i2c.writeto( addr, bytes([0xff]) )
while dev_found:
# read 1 byte from PCF8574
data = i2c.readfrom( addr, 1 )
data = data[0]
for i in range(3):
if (data >> i) & 1 == 0:
print('Button at P{} is pressed.'.format(i))
time.sleep_ms(100)
except KeyboardInterrupt:
pass
finally:
print('Done')
ตัวอย่างการวางแผนต่อวงจรบนเบรดบอร์ด (Fritzing): ESP32 + PCF8574 + 3x Push Buttons

ตัวอย่างการใช้ PCF8574 จำนวน 2 ชุด สำหรับ LED Bar และ Push Buttons

‍‍‍‍‍‍ ‍‍ ‍‍‍‍‍‍ ‍‍‍‍‍‍ ‍‍

จากสองตัวอย่างแรก ในตัวอย่างนี้ เราจะใช้ไอซี PCF8574 จำนวน 2 ตัว โดยให้ตัวแรก มีแอดเดรสเป็น 0x21 สำหรับโหมดเอาต์พุตและต่อกับโมดูล LED Bar (8-bit) ตัวที่สองมีแอดเดรสเป็น 0x20 และนำไปต่อกับวงจรปุ่มกดจำนวน 3 ปุ่ม ตั้งชื่อปุ่มเป็น A, B, C และต่อกับขา P0, P1, P2 ตามลำดับ

พฤติกรรมการทำงานโดยรวมมีดังนี้ ให้แสดงสถานะด้วย LED Bar โดยมีหนึ่งดวงเท่านั้นที่อยู่ในสถานะ ON เมื่อรอให้เวลาผ่านไป เช่น 150 msec ให้เลื่อนตำแหน่งของ LED ที่อยู่ในสถานะ ON ไปยังตำแหน่งถัดไป ทิศทางการเลื่อนมี 2 กรณี คือ เลื่อนวนไปทางซ้าย หรือ เลื่อนวนไปทางขวา

ถ้ากดปุ่ม A ให้จบการทำงานของโปรแกรม ถ้ากดปุ่ม B ให้เปลี่ยนโหมดทิศทางการเลื่อนบิต (ไปทางซ้าย หรือ ไปทางขวา) และถ้ากดปุ่ม C ให้หยุดการเลื่อนบิตชั่วคราว แต่ถ้ากดอีกครั้งให้ทำงานต่อ

วงจรที่ใช้สำหรับการสาธิต ประกอบด้วยบอร์ด ESP32 โมดูล PCF8574 จำนวน 2 ชุด ซึ่งจะใช้สำหรับวงจร LEDs 8 ชุด (เอาต์พุต) และวงจรปุ่มกด 3 ชุด (อินพุต)

ในการเขียนโค้ด ได้สร้างคลาส PCF8574 และมีอีก 2 คลาส ได้แก่ PCF8574_LED_BAR และ PCF8574_BUTTONS ที่สืบทอดคุณสมบัติจากคลาสแรก และใช้แยกกันสำหรับเป็นเอาต์พุตหรืออินพุต ตามลำดับ

ข้อสังเกต: ในการอ่านค่าอินพุตจากวงจรปุ่มกด จะมีการเปิดใช้งานอินเทอร์รัพท์ที่รับสัญญาณมาจาก /INT ของ PCF8574 เมื่อเกิดอินเทอร์รัพท์ในแต่ละครั้ง จะอ่านค่าที่เป็นข้อมูลหนึ่งไบต์จากไอซีดังกล่าว แล้วมาตรวจสอบดูว่า มีการเปลี่ยนแปลงค่าหรือสถานะลอจิกหรือไม่ ในตำแหน่งใด เพื่อระบุว่า มีการกดปุ่มหรือไม่

# file: pcf8574.py
from machine import Pin, I2C
ROT_LSHIFT = lambda x: ((x << 1) | (x >> 7)) & 0xff
ROT_RSHIFT = lambda x: ((x >> 1) | (x << 7)) & 0xff
class PCF8574():
def __init__(self,i2c,addr,pin_irq=None):
self._i2c = i2c
self._addr = addr
self._int = pin_irq
self._changes = 0
# initial state: P0..P7 input direction
self.write_byte( 0xff )
if self._int != None:
self._int.init(mode=Pin.IN, pull=Pin.PULL_UP)
self._int.irq(handler=self._callback,
trigger=Pin.IRQ_FALLING)

def _callback(self,p):
self._changes += 1

def set_callback(self,cb=None):
if self._int != None:
self._int.irq(handler=cb,
trigger=Pin.IRQ_FALLING)

def write_byte(self, value):
self._i2c.writeto(self._addr, bytes([value]))

def pin_changed(self):
return (self._changes != 0)

def read_byte(self):
self._changes = 0
value = self._i2c.readfrom(self._addr, 1)[0]
return value

def deinit(self):
self.write_byte( 0xff )
if self._int != None:
self._int.irq(handler=None)
class PCF8574_BUTTONS(PCF8574):
def __init__(self,i2c,addr,pin_irq=None):
super().__init__(i2c,addr,pin_irq)
self._saved_inputs = 0xff

def button_clicked(self):
inputs = self.read_byte()
change = (inputs ^ self._saved_inputs) != 0
self._saved_inputs = inputs
results = []
for i in range(8):
state = change and (((inputs>>i)&1)==0)
results.append(state)
return results
class PCF8574_LED_BAR(PCF8574):
def __init__(self,i2c,addr,init_value=0xff):
super().__init__(i2c,addr)
self._value = init_value
self.update()

def set_value(self,value):
self._value = value

def rotate_left(self):
self._value = ROT_LSHIFT(self._value)

def rotate_right(self):
self._value = ROT_RSHIFT(self._value)

def update(self):
self.write_byte(self._value)

โค้ดสำหรับสาธิตการทำงานที่ใช้คลาสตามที่ได้สร้างไว้

# file: pcf8574_demo-3.py
from machine import Pin,I2C
from pcf8574 import PCF8574_BUTTONS,PCF8574_LED_BAR
import utime as time
# Use GPIO22=SCL, GPIO21=SDA
i2c = I2C( freq=400000,
scl=Pin(22), sda=Pin(21) )
for addr in i2c.scan():
print( hex(addr ) )
pcf_buttons = PCF8574_BUTTONS( i2c,0x20, Pin(23) )
pcf_leds = PCF8574_LED_BAR( i2c,0x21, 0xfe )
direction = 0
running = True
ts = time.ticks_ms()
try:
while True:
# read input from buttons (A,B,C)
if pcf_buttons.pin_changed():
clicked = pcf_buttons.button_clicked()
btn_a = clicked[0]
btn_b = clicked[1]
btn_c = clicked[2]
if btn_a: # exit the loop
break
elif btn_b and running:
# change shift direction (left/right)
direction = int(not direction)
elif btn_c: # toggle running/paused
running = not running

# update output every 150 msec
if time.ticks_diff(time.ticks_ms(), ts) >= 150:
ts = time.ticks_ms()
pcf_leds.update()
if running and direction==0:
pcf_leds.rotate_left()
elif running and direction==1:
pcf_leds.rotate_right()

except KeyboardInterrupt:
pass
finally:
pcf_buttons.set_callback(None)
pcf_buttons.deinit()
pcf_leds.deinit()
print('Done')
ตัวอย่างการต่อวงจรโดยใช้โมดูล PCF8574 และโมดูล LED Bar

โดยสรุป เราได้เรียนรู้หลักการทำงานของไอซี PCF8574 ในเบื้องต้น และได้เห็นตัวอย่างการเขียนโค้ด MicroPython และตัวอย่างการสร้างคลาส เพื่อควบคุมการทำงานของไอซีดังกล่าว เช่น นำมาใช้อ่านค่าอินพุตจากวงจรปุ่มกด หรือกำหนดสถานะเอาต์พุตให้วงจร LED เป็นต้น

--

--

<rawat.s>
<rawat.s>

Written by <rawat.s>

I'm Thai and working in Bangkok/Thailand.

No responses yet