# 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).
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.
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.
+
-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
```
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:
#### 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.
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.
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/)
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
# 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
# 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
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)
# 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
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.
from micropython import const
from sys import platform
-ESP32 = platform == 'esp32' or platform == 'esp32_LoBo'
from ir_tx import IR
# Philips RC5 protocol
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):
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):
# 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:
# 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
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))