I/O Port Expander
ในการออกแบบระบบสมองกลฝังตัวที่ใช้ไมโครคอนโทรลเลอร์ ปัญหาหนึ่งที่อาจพบได้ คือ ถ้าเลือกใช้ไมโครคอนโทรลเลอร์ที่มีจำนวนขา I/O จำกัด จะมีวิธีอย่างไรบ้างที่จะช่วยเพิ่มขา I/O
เทคนิคหนึ่งคือ การใช้ไอซีมาต่อเพิ่ม และสามารถสื่อสารข้อมูลกับไมโครคอนโทรลเลอร์โดยใช้สัญญาณเพียงไม่กี่เส้น อาจจะเป็น I2C หรือ SPI เป็นต้น
การใช้งานบัส I2C มักพบเห็นได้บ่อย เนื่องจากสามารถใช้สัญญาณสื่อสารเพียง 2 เส้น คือ SCL และ SDA อุปกรณ์ที่สามารถสื่อสารแบบนี้ได้ เช่น ไอซีเซนเซอร์ประเภทต่าง ๆ โมดูลแสดงผลกราฟิกขนาดเล็กแบบ OLED เป็นต้น
ไอซีประเภท I/O Port Expander ก็เป็นอีกหนึ่งประเภทที่มีให้เลือกใช้งานและใช้รูปแบบการสื่อสารข้อมูลด้วย I2C และ SPI ยกตัวอย่างเช่น
- NXP PCF8574/PCF8574A (I2C, 8-bit I/O)
- TI PCF8574 (I2C, 8-bit I/O)
- Microchip MCP23008 (I2C, 8-bit I/O)
- Microchip MCP23016 (I2C, 16-bit I/O)
- Microchip MCP23017 (I2C, 16-bit I/O)
- Microchip MCP23S08 (SPI, 8-bit I/O)
- Microchip MCP23S17 (SPI, 16-bit I/O)
ถ้าใช้แบบ 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)
ในปัจจุบัน ก็มีโมดูล PCF8574 หลายรูปแบบให้เลือกใช้ ทำให้ช่วยลดเวลาในการต่อวงจรบนเบรดบอร์ด
PCF8574 จะทำงานเป็นอุปกรณ์สเลฟในระบบบัส I2C (Slave Device) ซึ่งจะต้องคอยตอบสนองจากคำสั่งที่ถูกส่งมาจากอุปกรณ์ที่เป็นมาสเตอร์ (Master Device) เช่น ไมโครคอนโทรลเลอร์ และจะต้องมีการกำหนดแอดเดรส (Address) ขนาด 7 บิต ที่ไม่ซ้ำ ให้กับอุปกรณ์ที่เป็นสเลฟ ในบัสเดียวกัน
ขา 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)
การสื่อสารข้อมูลด้วย 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) จากฝ่ายผู้รับข้อมูลไบต์ เป็นต้น
ข้อสังเกต:
- การใช้งานขา 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')
ตัวอย่างการอ่านค่าอินพุตจากวงจรปุ่มกด 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')
ตัวอย่างการใช้ 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, I2CROT_LSHIFT = lambda x: ((x << 1) | (x >> 7)) & 0xff
ROT_RSHIFT = lambda x: ((x >> 1) | (x << 7)) & 0xffclass 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 resultsclass 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 ในเบื้องต้น และได้เห็นตัวอย่างการเขียนโค้ด MicroPython และตัวอย่างการสร้างคลาส เพื่อควบคุมการทำงานของไอซีดังกล่าว เช่น นำมาใช้อ่านค่าอินพุตจากวงจรปุ่มกด หรือกำหนดสถานะเอาต์พุตให้วงจร LED เป็นต้น