From: Peter Hinch Date: Wed, 11 Mar 2020 11:39:58 +0000 (+0000) Subject: RMT tested. X-Git-Url: https://vault307.fbx.one/gitweb/micorpython_ir.git/commitdiff_plain/d8d6758c3370f4adc636aed7b935a492754f6062 RMT tested. --- diff --git a/TRANSMITTER.md b/TRANSMITTER.md index 88c3ed7..8a83314 100644 --- a/TRANSMITTER.md +++ b/TRANSMITTER.md @@ -5,7 +5,7 @@ # 1. Hardware Requirements The transmitter requires a Pyboard 1.x (not Lite), a Pyboard D or an ESP32. -Output is via an IR LED which will need a transistor to provide sufficient +Output is via an IR LED which needs a simple circuit to provide sufficient current. Typically these need 50-100mA of drive to achieve reasonable range and data integrity. A suitable 940nm LED is [this one](https://www.adafruit.com/product/387). @@ -13,10 +13,10 @@ On the Pyboard the transmitter test script assumes pin X1 for IR output. It can be changed, but it must support Timer 2 channel 1. Pins for pushbutton inputs are arbitrary: X3 and X4 are used. The driver uses timers 2 and 5. -On ESP32 pin 23 is used for IR output and pins 18 and 19 for pushbuttons. The -ESP32 solution has limitations discussed in [section 5.2](./TRANSMITTER.md#52-esp32). +On ESP32 the demo uses pins 21 and 23 for IR output and pins 18 and 19 for +pushbuttons. These pins may be changed. -## 1.1 Wiring +## 1.1 Pyboard Wiring I use the following circuit which delivers just under 40mA to the diode. R2 may be reduced for higher current. @@ -33,20 +33,40 @@ The driver assumes circuits as shown. Here the carrier "off" state is 0V, which is the driver default. If using a circuit where "off" is required to be 3.3V, the constant `_SPACE` in `ir_tx.__init__.py` should be changed to 100. -# 2. Installation +## 1.2 ESP32 Wiring -The transmitter is a Python package. This minimises RAM usage: applications -only import the device driver for the protocol in use. +The ESP32 RMT device does not currently support the carrier option. A simple +hardware gate is required to turn the IR LED on when both the carrier pin and +the RMT pin are high. A suitable circuit is below. +![Image](images/gate.png) -Copy the following to the target filesystem: - 1. `ir_tx` Directory and contents. +The transistor type is not critical. A gate could be built with two similarly +connected N-channel MOSFETS. The 1KΩ resistors would not be required. The +MOSFETS would require a low RDSon at Vgs == 3.3V. A ZVN4210A seems suitable +but I haven't tried it. + +# 2. Dependencies and installation + +## 2.1 Dependencies The device driver has no dependencies. +On ESP32 a firmware version >= V1.12 is required. The Loboris port is not +supported owing to the need for the RMT device. + The demo program requires `uasyncio` from the official library and `aswitch.py` -from [this repo](https://github.com/peterhinch/micropython-async). The demo is -of a 2-button remote controller with auto-repeat. It may be run by issuing: +from [this repo](https://github.com/peterhinch/micropython-async). + +## 2.2 Installation + +The transmitter is a Python package. This minimises RAM usage: applications +only import the device driver for the protocol in use. +Copy the following to the target filesystem: + 1. `ir_tx` Directory and contents. + +The demo is of a 2-button remote controller with auto-repeat. It may be run by +issuing: ```python from ir_tx.test import test ``` @@ -61,10 +81,6 @@ It implements a class for each supported protocol, namely `NEC`, `SONY_12`, common abstract base class in `__init__.py`. The application instantiates the appropriate class and calls the `transmit` method to send data. -The ESP32 platform is marginal in this application because of imprecision in -its timing. The Philips protocols are unsupported as they require unachievable -levels of precision. Test results are discussed [here](./TRANSMITTER.md#52-esp32). - #### Common to all classes Constructor args: @@ -112,9 +128,6 @@ value. #### Philips classes -These are only supported on Pyboard hosts. An `RuntimeError` will be thrown on -an attempt to instantiate a Philips class on an ESP32. - The RC-5 protocol supports a 5 bit address and 6 or 7 bit (RC5X) data. The driver uses the appropriate mode depending on the `data` value provided. @@ -124,17 +137,15 @@ Both send a `toggle` bit which remains constant if a button is held down, but changes when the button is released. The application should implement this behaviour, setting the `toggle` arg of `.transmit` to 0 or 1 as required. -# 4. Test results - -# 5. Principle of operation +# 4. Principle of operation -## 5.1 Pyboard +## 4.1 Pyboard The classes inherit from the abstract base class `IR`. This has an array `.arr` to contain the duration (in μs) of each carrier on or off period. The `transmit` method calls a `tx` method of the subclass which populates this array. This is done by two methods of the base class, `.append` and `.add`. The -former takes a list of times (in μs) and appends them to the array. A bound +former takes a list of times (in ) and appends them to the array. A bound variable `.carrier` keeps track of the notional on/off state of the carrier: this is required for bi-phase (manchester) codings. @@ -155,32 +166,17 @@ The duty ratio is changed by the Timer 5 callback `._cb`. This retrieves the next duration from the array. If it is not `STOP` it toggles the duty cycle and re-initialises T5 for the new duration. -## 5.2 ESP32 - -This is something of a hack because my drivers work with standard firmware. - -A much better solution will be possible when the `esp32.RMT` class supports the -`carrier` option. A fork supporting this is -[here](https://github.com/mattytrentini/micropython). You may want to adapt the -base class to use this fork: it should be easy and would produce a solution -capable of handling all protocols. - -A consequence of this hack is that timing is imprecise. In testing NEC -protocols were reliable. Sony delivered some erroneous bitsreams but may be -usable. Philips protocols require timing precision which is unachievable; these -are unsupported. +## 4.2 ESP32 -The ABC stores durations in Hz rather than in μs. This is because the `period` -arg of `Timer.init` expects an integer number of ms. Passing a `freq` value -enables slightly higher resolution timing. In practice timing lacks precision -with the code having a hack which subtracts a nominal amount from each value to -compensate for the typical level of overrun. +The carrier is output continuously at the specified duty ratio. A pulse train +generated by the RMT instance drives a hardware gate such that the IR LED is +lit only when both carrier and RMT are high. -The carrier is generated by PWM instance `.pwm` with its duty cycle controlled -by software timer `._tim` in a similar way to the Pyboard Timer 5 described -above. The ESP32 duty value is in range 0-1023 as against 0-100 on the Pyboard. +The carrier is generated by PWM instance `.pwm` running continuously. The ABC +constructor converts the 0-100 duty ratio specified by the subclass to the +0-1023 range used by ESP32. -# 6. References +# 5. References [General information about IR](https://www.sbprojects.net/knowledge/ir/) diff --git a/images/circuits.fzz b/images/circuits.fzz index 9ad4333..ad48376 100644 Binary files a/images/circuits.fzz and b/images/circuits.fzz differ diff --git a/images/gate.png b/images/gate.png new file mode 100644 index 0000000..31b11f2 Binary files /dev/null and b/images/gate.png differ diff --git a/ir_rx/test.py b/ir_rx/test.py index c3c7160..112db2e 100644 --- a/ir_rx/test.py +++ b/ir_rx/test.py @@ -32,7 +32,7 @@ def cb(data, addr, ctrl): else: print('Data {:02x} Addr {:04x} Ctrl {:02x}'.format(data, addr, ctrl)) -def run(proto=0): +def test(proto=0): classes = (NEC_8, NEC_16, SONY_12, SONY_15, SONY_20, RC5_IR, RC6_M0) ir = classes[proto](p, cb) # Instantiate receiver ir.error_function(print_error) # Show debug information diff --git a/ir_tx/__init__.py b/ir_tx/__init__.py index ef52e84..6fe7710 100644 --- a/ir_tx/__init__.py +++ b/ir_tx/__init__.py @@ -5,9 +5,10 @@ # Copyright (c) 2020 Peter Hinch from sys import platform -ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' +ESP32 = platform == 'esp32' # Loboris not supported owing to RMT if ESP32: - from machine import Pin, Timer, PWM, freq + from machine import Pin, PWM + from esp32 import RMT else: from pyb import Pin, Timer # Pyboard does not support machine.PWM @@ -17,10 +18,12 @@ import micropython # micropython.alloc_emergency_exception_buf(100) -# ABC only +# ABC and Pyboard only: ESP32 ignores this value. +# Duty ratio in carrier off state: if driver is such that 3.3V turns the LED +# off, set _SPACE = 100 _SPACE = const(0) -# If the wiring is such that 3.3V turns the LED off, set _SPACE as follows -# On Pyboard 100, on ESP32 1023 +# On ESP32 gate hardware design is led_on = rmt and carrier + # Shared by NEC STOP = const(0) # End of data @@ -31,15 +34,11 @@ class IR: def __init__(self, pin, cfreq, asize, duty, verbose): if ESP32: - freq(240000000) - self._pwm = PWM(pin) # Produces 36/38/40KHz carrier + self._pwm = PWM(pin[0]) # Continuous 36/38/40KHz carrier self._pwm.deinit() - self._pwm.init(freq=cfreq, duty=_SPACE) # ESP32: 0 <= duty <= 1023 - self._duty = round((duty if not _SPACE else (100 - duty)) * 10.23) - self._tim = Timer(-1) # Controls carrier on/off times - self._off = self.esp_off # Turn IR LED off - self._onoff = self.esp_onoff # Set IR LED state and refresh timer + self._pwm.init(freq=cfreq, duty=round(duty * 10.23)) + self._rmt = RMT(0, pin=pin[1], clock_div=80) # 1μs resolution else: # Pyboard tim = Timer(2, freq=cfreq) # Timer 2/pin produces 36/38/40KHz carrier self._ch = tim.channel(1, Timer.PWM, pin=pin) @@ -47,67 +46,51 @@ class IR: # Pyboard: 0 <= pulse_width_percent <= 100 self._duty = duty if not _SPACE else (100 - duty) self._tim = Timer(5) # Timer 5 controls carrier on/off times - self._off = self.pb_off - self._onoff = self.pb_onoff - self._tcb = self.cb # Pre-allocate + self._tcb = self._cb # Pre-allocate + self._arr = array('H', 0 for _ in range(asize)) # on/off times (μs) + self._mva = memoryview(self._arr) + # Subclass interface self.verbose = verbose - self.arr = array('H', 0 for _ in range(asize)) # on/off times (μs) self.carrier = False # Notional carrier state while encoding biphase self.aptr = 0 # Index into array - # Before populating array, zero pointer, set notional carrier state (off). - def transmit(self, addr, data, toggle=0): # NEC: toggle is unused - self.aptr = 0 # Inital conditions for tx: index into array - self.carrier = False - self.tx(addr, data, toggle) - self.append(STOP) - self.aptr = 0 # Reset pointer - self.cb(self._tim) # Initiate physical transmission. - - # Turn IR LED off (pyboard and ESP32 variants) - def pb_off(self): - self._ch.pulse_width_percent(_SPACE) - - def esp_off(self): - self._pwm.duty(_SPACE) - - # Turn IR LED on or off and re-initialise timer (pyboard and ESP32 variants) - @micropython.native - def pb_onoff(self, p, v): - self._ch.pulse_width_percent(_SPACE if p & 1 else self._duty) - self._tim.init(prescaler=84, period=v, callback=self._tcb) - - @micropython.native - def esp_onoff(self, p, v): - self._pwm.duty(_SPACE if p & 1 else self._duty) - self._tim.init(mode=Timer.ONE_SHOT, freq=v, callback=self.cb) - - def cb(self, t): # T5 callback, generate a carrier mark or space + def _cb(self, t): # T5 callback, generate a carrier mark or space t.deinit() p = self.aptr - v = self.arr[p] + v = self._arr[p] if v == STOP: - self._off() # Turn off IR LED. + self._ch.pulse_width_percent(_SPACE) # Turn off IR LED. return - self._onoff(p, v) + self._ch.pulse_width_percent(_SPACE if p & 1 else self._duty) + self._tim.init(prescaler=84, period=v, callback=self._tcb) self.aptr += 1 - def append(self, *times): # Append one or more time peiods to .arr + # Public interface + # Before populating array, zero pointer, set notional carrier state (off). + def transmit(self, addr, data, toggle=0): # NEC: toggle is unused + self.aptr = 0 # Inital conditions for tx: index into array + self.carrier = False + self.tx(addr, data, toggle) # Subclass populates ._arr + self.trigger() # Initiate transmission + + # Subclass interface + def trigger(self): # Used by NEC to initiate a repeat frame + if ESP32: + self._rmt.write_pulses(tuple(self._mva[0 : self.aptr]), start = 1) + else: + self.append(STOP) + self.aptr = 0 # Reset pointer + self._cb(self._tim) # Initiate physical transmission. + + def append(self, *times): # Append one or more time peiods to ._arr for t in times: - if ESP32 and t: - t -= 350 # ESP32 sluggishness - t = round(1_000_000 / t) # Store in Hz - self.arr[self.aptr] = t + self._arr[self.aptr] = t self.aptr += 1 self.carrier = not self.carrier # Keep track of carrier state self.verbose and print('append', t, 'carrier', self.carrier) - def add(self, t): # Increase last time value + def add(self, t): # Increase last time value (for biphase) assert t > 0 self.verbose and print('add', t) # .carrier unaffected - if ESP32: - t -= 350 - self.arr[self.aptr - 1] = round((self.arr[self.aptr - 1] / 1_000_000 + t) / 1_000_000) - else: - self.arr[self.aptr - 1] += t + self._arr[self.aptr - 1] += t diff --git a/ir_tx/nec.py b/ir_tx/nec.py index 8cf39e7..1e02c9f 100644 --- a/ir_tx/nec.py +++ b/ir_tx/nec.py @@ -33,6 +33,5 @@ class NEC(IR): def repeat(self): self.aptr = 0 - self.append(9000, 2250, _TBURST, STOP) - self.aptr = 0 # Reset pointer - self.cb(self._tim) # Initiate physical transmission. + self.append(9000, 2250, _TBURST) + self.trigger() # Initiate physical transmission. diff --git a/ir_tx/philips.py b/ir_tx/philips.py index b069361..a37dd5b 100644 --- a/ir_tx/philips.py +++ b/ir_tx/philips.py @@ -6,7 +6,6 @@ from micropython import const from sys import platform -ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' from ir_tx import IR # Philips RC5 protocol @@ -15,8 +14,6 @@ ermsg = 'ESP32 does not support Philips protocols' class RC5(IR): def __init__(self, pin, freq=36000, verbose=False): - if ESP32: - raise RuntimeError(ermsg) super().__init__(pin, freq, 28, 30, verbose) def tx(self, addr, data, toggle): @@ -42,8 +39,6 @@ _T2_RC6 = const(889) class RC6_M0(IR): def __init__(self, pin, freq=36000, verbose=False): - if ESP32: - raise RuntimeError(ermsg) super().__init__(pin, freq, 44, 30, verbose) def tx(self, addr, data, toggle): diff --git a/ir_tx/test.py b/ir_tx/test.py index bb54329..b175dcd 100644 --- a/ir_tx/test.py +++ b/ir_tx/test.py @@ -6,7 +6,7 @@ # Implements a 2-button remote control on a Pyboard with auto repeat. from sys import platform -ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' +ESP32 = platform == 'esp32' if ESP32: from machine import Pin else: @@ -56,7 +56,10 @@ async def main(proto): # If button is held down normal behaviour is to retransmit # but most NEC models send a REPEAT code rep_code = proto == 0 # Rbutton constructor requires False for RC-X. NEC protocol only. - pin = Pin(23, Pin.OUT) if ESP32 else Pin('X1') + if ESP32: # Pins for IR LED gate + pin = (Pin(23, Pin.OUT, value = 0), Pin(21, Pin.OUT, value = 0)) + else: + pin = Pin('X1') classes = (NEC, SONY_12, SONY_15, SONY_20, RC5, RC6_M0) irb = classes[proto](pin, 38000) # My decoder chip is 38KHz @@ -75,30 +78,30 @@ async def main(proto): await asyncio.sleep_ms(500) # Obligatory flashing LED. led.toggle() +# Greeting strings. Common: s = '''Test for IR transmitter. Run: from ir_tx_test import test test() for NEC protocol test(1) for Sony SIRC 12 bit test(2) for Sony SIRC 15 bit -test(3) for Sony SIRC 20 bit''' -spb = ''' -test(5) for Philips RC-5 protocol -test(6) for Philips RC-6 mode 0. +test(3) for Sony SIRC 20 bit +test(4) for Philips RC-5 protocol +test(5) for Philips RC-6 mode 0. +''' +# Pyboard: +spb = ''' IR LED on pin X1 Ground pin X3 to send addr 1 data 7 Ground pin X4 to send addr 0x10 data 0x0b.''' -sesp = ''' -IR LED on pin 23 +# ESP32 +sesp = ''' +IR LED gate on pins 23, 21 Ground pin 18 to send addr 1 data 7 Ground pin 19 to send addr 0x10 data 0x0b.''' -if ESP32: - s = ''.join((s, sesp)) -else: - s = ''.join((s, spb)) -print(s) +print(''.join((s, sesp)) if ESP32 else ''.join((s, spb))) def test(proto=0): loop.run_until_complete(main(proto))