MicroPython Programming for ESP32 [14]

<rawat.s>
5 min readMay 8, 2020

--

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

บทความนี้นำเสนอตัวอย่างการเขียนโค้ด MicroPython สำหรับ ESP32 และกล่าวถึง การรับและแปลงรหัสจากสัญญาณแสงอินฟราเรด (Infrared: IR) จากรีโมทคอนโทรล (IR Remote Control) โดยใช้เซนเซอร์แสง ร่วมกับบอร์ด ESP32 และเขียนโค้ดด้วย MicroPython

อุปกรณ์ตัวรับแสงอินฟราเรด: 38kHz IR Receiver

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

ระบบการสื่อสารข้อมูลด้วยแสงอินฟราเรด (ความยาวคลื่นประมาณ 940 nm) ประกอบด้วย ตัวส่ง (Transmitter) และตัวรับ (Receiver) ผ่านตัวกลางที่เป็นอากาศ เป็นการสื่อสารแบบไร้สายระยะใกล้ ตัวส่งก็มีวงจรไดโอดที่ให้แสงอินฟราเรด (IR LED) แต่ต้องมีการมอดูเลตสัญญาณด้วยความถี่ เช่น 36kHz, 38kHz หรือ 40kHz (Carrier Frequency) เพื่อส่งข้อมูลทีละบิต ตามโพรโตคอลที่กำหนด (เช่น NEC Protocol, Sony SIRC หรือ Philips RC5/RC6 Protocol เป็นต้น)

ตัวรับประกอบด้วยไอซีที่ได้ออกแบบมาสำหรับรับแสงอินฟราเรด (IR Receiver) ที่มีการมอดูเลตสัญญาณด้วยความถี่ประมาณ 38kHz เช่น TSOP34438, TSOP33538 หรือ VS1838B เป็นต้น (ลองดูตัวอย่างรายการสินค้าประเภทนี้ได้จาก Mouser)

ถ้าไม่ได้รับแสงอินฟราเรดจากตัวส่ง เอาต์พุตของตัวรับจะเป็น High (Logic 1) แต่ถ้าได้รับแสงอินฟราเรดความถี่ 38kHz จะได้เอาต์พุตเป็น Low (Logic 0)

ตัวอย่างอุปกรณ์ตัวรับ VS1838B (ซ้าย) และโมดูล KY-022 (ขวา)

ในบทความนี้ได้เลือกโมดูล KY-022 ซึ่งประกอบด้วย VS1838B และมีวงจร LED (SMD) + ตัวต้านทาน (1kΩ) ต่ออยู่ระหว่างขา VCC (+) กับขา S (Signal) สำหรับสัญญาณเอาต์พุต ถ้าได้รับสัญญาณอินฟราเรด 38kHz ตัว LED จะอยู่ในสถานะ ON

ข้อสังเกต: โมดูล KY-022 มี 3 ขา เป็น Pin Header คือ GND (-), VCC(+) และ S ตามลำดับ และสามารถใช้แรงดันไฟเลี้ยง +3.3V หรือ +5V ได้

การโมดูเลตสัญญาณสำหรับรีโมท-อินฟราเรด

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

ถ้าตัวส่งได้สร้างสัญญาณไฟฟ้าแบบมีคาบ เป็นรูปคลื่นสี่เหลี่ยม (ด้วยความถี่ 38kHz, 33% Duty Cycle สำหรับ NEC Protocol) แล้วส่งเป็นสัญญาณอินฟราเรดออกไป (Pulse Burst) ช่วงนี้เรียกว่า Mark และเมื่อตัวรับได้รับสัญญาณดังกล่าวในช่วงเวลานั้น จะได้เอาต์พุตเป็น 0 ถัดไปให้หยุดส่ง ช่วงนี้เรียกว่า Space ไม่มีสัญญาณอินฟราเรด ดังนั้นที่เอาต์พุตของตัวรับจะเป็น 1

การส่งข้อมูลจะเริ่มด้วยสัญลักษณ์ที่เรียกว่า Mark และ Space สำหรับ Head ซึ่งจะมีช่วงความกว้าง (Pulse Width) ตามที่โพรโตคอลสื่อสารกำหนดไว้ จากนั้นจะตามด้วย Mark และ Space สำหรับข้อมูลบิต และใช้ความกว้างเป็นตัวกำหนดว่า บิตใดมีค่าเป็น 0 หรือ 1

ช่วง Mark สำหรับบิตข้อมูล 0 และ 1 จะเท่ากัน แต่ช่วง Space ของบิต 1 จะกว้างกว่าของบิต 0

สัญญาณตัวส่ง (บน) และสัญญาณที่ตัวรับแต่กลับลอจิก (ล่าง) [Source: Adafruit]

มาลองดูตัวอย่างรูปแบบส่งข้อมูล (โค้ด) มีทั้งหมด 32 บิต โดยใช้โพรโตคอลของ NEC จากรูปต่อไปนี้ เป็นคลื่นสัญญาณที่ตัวรับแต่กลับลอจิก (Logical Inverse) จะเห็นได้ว่า ช่วง Mark และ Space สำหรับ Head จะมีความกว้าง 9 msec และ 4.5 msec (โดยประมาณ) ตามลำดับ ช่วง Mark สำหรับข้อมูลบิต มีความกว้าง 562.5 μsec ช่วง Space สำหรับบิต 0 มีความกว้าง 562.5 μsec และสำหรับบิต 1 มีความกว้างประมาณ 1687.5 μsec

ถ้าเรากดรีโมทค้างไว้ จะมีการส่งโค้ดซ้ำ (Repeat Code) และมีการเว้นระยะเวลา

ความกว้าง (โดยประมาณ) ของบิต 0 และบิต 1 สำหรับ NEC Protocol

การสร้างคลาส MicroPython สำหรับถอดรหัสข้อมูลจากตัวรับอินฟราเรด

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

ในเชิงเทคนิคการเขียนโปรแกรมสำหรับสมองกลฝังตัว เราจะเปิดใช้อินเทอร์รัพท์จากภายนอก (External Interrupt หรือ Pin Change Interrupt) ที่ขาอินพุต โดยตรงสอบทั้งขอบขาขึ้นและขาลง (Rising Edge & Falling Edge)

เมื่อเกิดเหตุการณ์จากภายนอกในแต่ละครั้ง ฟังก์ชันที่ทำหน้าที่เป็น Callback (หรือ Interrupt Handler) จะทำงานโดยอัตโนมัติ และอ่านเวลาของระบบในขณะนั้นแล้วบันทึกเก็บไว้ในอาร์เรย์ (หน่วยเป็นไมโครวินาที)

เมื่อเกิดเหตุการณ์ครั้งแรก เราจะเปิดใช้ Software Timer ในโหมด One Shot และตั้งค่าไว้ เช่น 80 msec (Timeout) เมื่อเวลาผ่านไปตามที่กำหนด จะถือว่า การตรวจจับสัญญาณสิ้นสุดลงสำหรับข้อมูลหนึ่งเฟรม (Message Frame) จากนั้นจึงตรวจสอบข้อมูลที่ได้บันทึกเก็บไว้ แล้วนำมาแปลงหรือถอดรหัส (Decode) ให้เป็นโค้ด 32 บิต เก็บไว้เป็นโค้ดล่าสุดที่รับได้ (เป็นข้อความเลขฐานสิบหก)

ถ้าเรานำโค้ดเหล่านี้ ไปจับคู่กับปุ่มกดบนอุปกรณ์รีโมท เราก็จะทราบได้ว่า ปุ่มใดถูกกด และได้โค้ดตรงกับที่เป็นเลขฐานสิบหก

# file: ir_receiver.py
from machine import Pin, Timer
import utime as time
from micropython import const
HEAD_INIT_MARK_MIN = const(8000)
HEAD_INIT_SPACE_MIN = const(4000)
HEAD_REPEAT_MARK_MIN = const(HEAD_INIT_MARK_MIN)
HEAD_REPEAT_SPACE_MIN = const(HEAD_INIT_SPACE_MIN//2)
BIT_MARK_MIN = const(400)
BIT_MARK_MAX = const(700)
BIT0_SPACE_MIN = const(BIT_MARK_MIN)
BIT0_SPACE_MAX = const(BIT_MARK_MAX)
BIT1_SPACE_MIN = const(1500)
BIT1_SPACE_MAX = const(1800)

class IR_RECV():
"""This class implements an IR decoder for NEC protocol."""

def __init__(self, pin, timer_id=-1,timeout=100):
self._ts = 128*[0] # array for timestamp values (usec)
self._saved_ts = 0
self._index = -1
self._max_cnt = 0
self._timeout = timeout
self._pin = pin # IR input pin
self._pin.init( mode=Pin.IN, pull=Pin.PULL_UP )
trig_mode = (Pin.IRQ_FALLING | Pin.IRQ_RISING)
self._pin.irq( trigger=trig_mode, handler=self._pin_cb )
self._tim = Timer( timer_id ) # software timer
self._codes = []

def _decode(self):
mark, space = self._ts[0], self._ts[1]
if mark < HEAD_INIT_MARK_MIN:
return
if space > HEAD_INIT_SPACE_MIN:
# found start sequence
bits = []
for i in range(2,self._max_cnt,2):
mark, space = self._ts[i], self._ts[i+1]
if BIT_MARK_MIN < mark < BIT_MARK_MAX:
if BIT0_SPACE_MIN < space < BIT0_SPACE_MAX:
bits.append('0')
elif BIT1_SPACE_MIN < space < BIT1_SPACE_MAX:
bits.append('1')
else: # error
return
bin_str = ''.join(bits)
try:
if len(bin_str) > 0:
# only 32-bit value
code = (int(bin_str,2)) & 0xffffffff
hex_str = hex(code)[2:]
self._codes.append( hex_str )
except Exception as ex:
pass
elif space > HEAD_REPEAT_SPACE_MIN:
if self._max_cnt == 3:
self._codes.append( '0' ) # repeat code

def _timer_cb(self, t):
self._tim.init(callback=None)
self._max_cnt = self._index
self._index = -1
if self._max_cnt > 1:
self._decode() # decode bits

def read(self):
_codes = self._codes[:]
self._codes = []
return _codes

def _pin_cb(self, p):
# read current timestamp
ts = time.ticks_us()
if self._index == -1:
# detected the first event (transition)
# start timer in one-shot mode
self._tim.init( period=self._timeout,
mode=Timer.ONE_SHOT,
callback=self._timer_cb )
elif self._index < len(self._ts):
t_width = time.ticks_diff(ts, self._saved_ts)
self._ts[ self._index ] = t_width
self._index += 1
self._saved_ts = ts

def deinit(self):
self._pin.irq(handler=None)
self._tim.deinit()

โค้ดทดสอบการทำงานเบื้องต้น โดยเลือกใช้ขา GPIO12 สำหรับขาสัญญาณจากโมดูล KY-022

# file: ir_receiver_demo-1.py
from machine import Pin
import utime as time
from ir_receiver import IR_RECV
ir = IR_RECV( Pin(12), timeout=80 )
try:
while True:
codes = ir.read()
for code in codes:
print("Code: '{}'".format(code) )
time.sleep_ms(300)
except KeyboardInterrupt:
pass
finally:
ir.deinit()
print('Done')

โค้ดสาธิตปรับระดับแสงหลอด RGB LED (NeoPixel)

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

ในการสาธิตเราจะใช้โมดูล RGB LED (WS2812) จำนวนหนึ่งชุด (ต่อกับขา GPIO19 ของ ESP32) และมีการใช้ปุ่มของอุปกรณ์รีโมท ดังนี้

  • ปุ่ม Home คือ ตั้งค่า R,G,B เป็น (0,0,0) ทั้งหมด (OFF)
  • ปุ่ม 0,1,2 ใช้สำหรับเลือกสี R,G,B ตามลำดับ
  • ปุ่ม Up, Down เพิ่มหรือลดระดับของสีตามที่ได้เลือกไว้ในขณะนั้น การเพิ่มหรือลงค่าตัวเลข จะอยู่ในช่วง 0..255

ข้อสังเกต: ถ้าใช้กับอุปกรณ์รีโมทที่แตกต่างไป โค้ดที่ได้อาจไม่เหมือนกัน และอาจมีขนาดยาวกว่า 32 บิต

# file: ir_receiver_demo-2.py
from machine import Pin, Timer
import utime as time
import neopixel
from ir_receiver import IR_RECV
keys = {
'0': 'REPEAT',
'ffa25d': 'POWER',
'ff629d': 'UP',
'ffa857': 'DOWN',
'ff22dd': 'LEFT',
'ffc23d': 'RIGHT',
'ffe01f': 'HOME',
'ffe21d': 'BACK',
'ff02fd': 'PLAY',
'ff906f': 'TEXT',
'ff6897': '0',
'ff30cf': '1',
'ff18e7': '2',
'ff7a85': '3',
'ff10ef': '4',
'ff38c7': '5',
'ff5aa5': '6',
'ff42bd': '7',
'ff4ab5': '8',
'ff52ad': '9',
'ff9867': '-',
'ffb04f': '+', }
ir = IR_RECV( Pin(12), timeout=80 )
np = neopixel.NeoPixel(Pin(19), 1)
rgb = 3*[0]
color = 0
np[0] = tuple(rgb) # rgb color = (0,0,0)
np.write()
try:
prev_key = None
while True:
codes = ir.read() # read IR code
if len(codes) > 0 and codes[0] in keys:
code = codes[0]
print( code, keys[code] )
if code != '0':
prev_key = keys[code]
key = prev_key
if key == 'UP':
value = rgb[color]
rgb[color] = min(value+8,255)
elif key == 'DOWN':
value = rgb[color]
rgb[color] = max(value-8,0)
elif key == 'HOME':
rgb = 3*[0]
color = 0
elif key in ['0','1','2']:
color = int(key)
elif key == 'POWER':
break
np[0] = tuple(rgb)
np.write()
time.sleep_ms(100)
except KeyboardInterrupt:
pass
finally:
ir.deinit()
np[0] = (0,0,0)
np.write()
print('Done')
ตัวอย่างการวางแผนต่อวงจรบนบนเบรดบอร์ด (Fritzing)
ตัวอย่างอุปกรณ์ที่ได้นำมามทดลองจริง

โดยสรุป เราได้เรียนรู้หลักการทำงานของตัวรับสัญญาณอินฟราเรดที่มีการโมดูเลตสัญญาณด้วยความถี่ 38kHz เราได้เห็นตัวอย่างการสร้างคลาสในภาษา MicroPython เพื่อใช้ในการถอดรหัสจากสัญญาณอินพุต และสุดท้ายได้ทดลองรับค่าจากรีโมทอินฟราเรด เพื่อมาปรับระดับความสว่างของ RGB LED

--

--

<rawat.s>
<rawat.s>

Written by <rawat.s>

I'm Thai and working in Bangkok/Thailand.

Responses (2)