require `uasyncio` but are compatible with it, and are designed for standard
firmware builds.
-The receiver is cross platform and has been tested on Pyboard, ESP8266 and
-ESP32.
+The receiver is cross platform and has been tested on Pyboard, ESP8266, ESP32
+and Raspberry Pi Pico.
In a typical use case the receiver is employed at the REPL to sniff the address
and data values associated with buttons on a remote control. The transmitter is
# 4. References
-Sources of information about IR protocols.
+Sources of information about IR protocols. The `sbprojects.net` site is an
+excellent resource.
[General information about IR](https://www.sbprojects.net/knowledge/ir/)
The NEC protocol:
[altium](http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol)
[circuitvalley](http://www.circuitvalley.com/2013/09/nec-protocol-ir-infrared-remote-control.html)
+[sbprojects.net](https://www.sbprojects.net/knowledge/ir/nec.php)
Philips protocols:
-[RC5](https://en.wikipedia.org/wiki/RC-5)
-[RC5](https://www.sbprojects.net/knowledge/ir/rc5.php)
-[RC6](https://www.sbprojects.net/knowledge/ir/rc6.php)
+[RC5 Wikipedia](https://en.wikipedia.org/wiki/RC-5)
+[RC5 sbprojects.net](https://www.sbprojects.net/knowledge/ir/rc5.php)
+[RC6 sbprojects.net](https://www.sbprojects.net/knowledge/ir/rc6.php)
Sony protocol:
-[SIRC](https://www.sbprojects.net/knowledge/ir/sirc.php)
+[SIRC sbprojects.net](https://www.sbprojects.net/knowledge/ir/sirc.php)
MCE protocol:
[OrtekMCE](http://www.hifi-remote.com/johnsfine/DecodeIR.html#OrtekMCE)
In my testing a 38KHz demodulator worked with 36KHz and 40KHz remotes, but this
is obviously neither guaranteed nor optimal.
+The TSOP4838 can run from 3.3V or 5V supplies. The former should be used on
+non-5V compliant hosts such as ESP32 and Raspberry Pi Pico and is fine on 5V
+compliant hosts too.
+
The pin used to connect the decoder chip to the target is arbitrary. The test
program assumes pin X3 on the Pyboard, pin 23 on ESP32 and pin 13 on ESP8266.
On the WeMos D1 Mini the equivalent pin is D7.
--- /dev/null
+# 1. Pulse train ouput on RP2
+
+The `RP2_RMT` class provides functionality similar to that of the ESP32 `RMT`
+class. It enables pulse trains to be output using a non-blocking driver. By
+default the train occurs once. Alternatively it can repeat a defned number of
+times, or can be repeated continuously.
+
+The class was designed for my [IR blaster](https://github.com/peterhinch/micropython_ir)
+and [433MHz remote](https://github.com/peterhinch/micropython_remote)
+libraries. It supports an optional carrier frequency, where each high pulse can
+appear as a burst of a defined carrier frequency. The class can support both
+forms concurrently on a pair of pins: one pin produces pulses while a second
+produces carrier bursts.
+
+Pulse trains are specified as arrays with each element being a duration in μs.
+Arrays may be of integers or half-words depending on the range of times to be
+covered. The duration of a "tick" is 1μs by default, but this can be changed.
+
+# 2. The RP2_RMT class
+
+## 2.1 Constructor
+
+This takes the following args:
+ 1. `pin_pulse=None` If an ouput `Pin` instance is provided, pulses will be
+ output on it.
+ 2. `carrier=None` To output a carrier, a 3-tuple should be provided comprising
+ `(pin, freq, duty)` where `pin` is an output pin instance, `freq` is the
+ carrier frequency in Hz and `duty` is the duty ratio in %.
+ 3. `sm_no=0` State machine no.
+ 4. `sm_freq=1_000_000` Clock frequency for SM. Defines the unit for pulse
+ durations.
+
+## 2.2 Methods
+
+### 2.2.1 send
+
+This returns "immediately" with a pulse train being emitted as a background
+process. Args:
+ 1. `ar` A zero terminated array of pulse durations in μs. See notes below.
+ 2. `reps=1` No. of repetions. 0 indicates continuous output.
+ 3. `check=True` By default ensures that the pulse train ends in the inactive
+ state.
+
+In normal operation, between pulse trains, the pulse pin is low and the carrier
+is off. A pulse train ends when a 0 pulse width is encountered: this allows
+pulse trains to be shorter than the array length, for example where a
+pre-allocated array stores pulse trains of varying lengths. In RF transmitter
+applications ensuring the carrier is off between pulse trains may be a legal
+requirement, so by default the `send` method enforces this.
+
+The first element of the array defines the duration of the first high going
+pulse, with the second being the duration of the first `off` period. If there
+are an even number of elements prior to the terminating 0, the signal will end
+in the `off` state. If the `check` arg is `True`, `.send()` will check for an
+odd number of elements; in this case it will overwrite the last element with 0
+to enforce a final `off` state.
+
+This check may be skipped by setting `check=False`. This provides a means of
+inverting the normal sense of the driver: if the first pulse train has an odd
+number of elements and `check=False` the pin will be left high (and the carrier
+on). Subsequent normal pulsetrains will start and end in the high state.
+
+### 2.2.2 busy
+
+No args. Returns `True` if a pulse train is being emitted.
+
+### 2.2.3 cancel
+
+No args. If a pulse train is being emitted it will continue to the end but no
+further repetitions will take place.
+
+# 3. Design
+
+The class constructor installs one of two PIO scripts depending on whether a
+`pin_pulse` is specified. If it is, the `pulsetrain` script is loaded which
+drives the pin directly from the PIO. If no `pin_pulse` is required, the
+`irqtrain` script is loaded. Both scripts cause an IRQ to be raised at times
+when a pulse would start or end.
+
+The `send` method loads the transmit FIFO with initial pulse durations and
+starts the state machine. The `._cb` ISR keeps the FIFO loaded with data until
+a 0 entry is encountered. It also turns the carrier on and off (using a PWM
+instance). This means that there is some latency between the pulse and the
+carrier. However latencies at start and end are effectively identical, so the
+duration of a carrier burst is correct.
+
+# 4. Limitations
+
+While the tick interval can be reduced to provide timing precision better than
+1μs, the design of this class will not support very high pulse repetition
+frequencies. This is because each pulse causes an interrupt: MicroPython is
+unable to support high IRQ rates.
+[This library](https://github.com/robert-hh/RP2040-Examples/tree/master/pulses)
+is more capable in this regard.
On ESP32 the demo uses pin 23 for IR output and pins 18 and 19 for pushbuttons.
These pins may be changed. The only device resource used is `RMT(0)`.
+On Raspberry Pi Pico the demo uses pin 17 for IR output and pins 18 and 19 for
+pushbuttons. These pins may be changed. The driver uses the PIO to emulate a
+device similar to the ESP32 RMT. The device driver is
+[documented here](./RP2_RMT.md); this is for experimenters and those wanting to
+use the library in conjunction with their own PIO assembler code.
+
## 1.1 Pyboard Wiring
I use the following circuit which delivers just under 40mA to the diode. R2 may
# 3. The driver
-This is specific to Pyboard D, Pyboard 1.x (not Lite) and ESP32.
+This is specific to Pyboard D, Pyboard 1.x (not Lite), ESP32 and Raspberry Pi
+Pico (RP2 architecture chip).
It implements a class for each supported protocol, namely `NEC`, `SONY_12`,
`SONY_15`, `SONY_20`, `RC5` and `RC6_M0`. Each class is subclassed from a
nec = NEC(Pin(23, Pin.OUT, value = 0))
nec.transmit(1, 2) # address == 1, data == 2
```
+Basic usage on Pico:
+```python
+from machine import Pin
+from ir_tx.nec import NEC
+nec = NEC(Pin(17, Pin.OUT, value = 0))
+nec.transmit(1, 2) # address == 1, data == 2
+```
#### Common to all classes
# IR_RX abstract base class for IR receivers.
# Author: Peter Hinch
-# Copyright Peter Hinch 2020 Released under the MIT license
+# Copyright Peter Hinch 2020-2021 Released under the MIT license
from machine import Timer, Pin
from array import array
pin = Pin(13, Pin.IN)
elif platform == 'esp32' or platform == 'esp32_LoBo':
pin = Pin(23, Pin.IN)
+ elif platform == 'rp2':
+ pin = Pin(16, Pin.IN)
irg = IR_GET(pin)
print('Waiting for IR data...')
return irg.acquire()
p = Pin(13, Pin.IN)
elif platform == 'esp32' or platform == 'esp32_LoBo':
p = Pin(23, Pin.IN)
+elif platform == 'rp2':
+ p = Pin(16, Pin.IN)
# User callback
def cb(data, addr, ctrl):
# __init__.py Nonblocking IR blaster
-# Runs on Pyboard D or Pyboard 1.x (not Pyboard Lite) and ESP32
+# Runs on Pyboard D or Pyboard 1.x (not Pyboard Lite), ESP32 and RP2
# Released under the MIT License (MIT). See LICENSE.
-# Copyright (c) 2020 Peter Hinch
+# Copyright (c) 2020-2021 Peter Hinch
from sys import platform
ESP32 = platform == 'esp32' # Loboris not supported owing to RMT
+RP2 = platform == 'rp2'
if ESP32:
from machine import Pin, PWM
from esp32 import RMT
+elif RP2:
+ from .rp2_rmt import RP2_RMT
else:
from pyb import Pin, Timer # Pyboard does not support machine.PWM
# import micropython
# micropython.alloc_emergency_exception_buf(100)
-# On ESP32 gate hardware design is led_on = rmt and carrier
# Shared by NEC
STOP = const(0) # End of data
if ESP32:
self._rmt = RMT(0, pin=pin, clock_div=80, carrier_freq=cfreq,
carrier_duty_percent=duty) # 1μs resolution
+ elif RP2: # PIO-based RMT-like device
+ self._rmt = RP2_RMT(pin_pulse=None, carrier=(pin, cfreq, duty)) # 1μs resolution
else: # Pyboard
if not IR._active_high:
duty = 100 - duty
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)
+ elif RP2:
+ self.append(STOP)
+ self._rmt.send(self._arr)
else:
self.append(STOP)
self.aptr = 0 # Reset pointer
self.irb.transmit(self.addr, self.data, _REP, True)
async def main():
- if ESP32: # Pins for IR LED gate
- pin = (Pin(23, Pin.OUT, value = 0), Pin(21, Pin.OUT, value = 0))
- else:
- pin = Pin('X1')
+ pin = Pin(23, Pin.OUT, value = 0) if ESP32 else Pin('X1')
irb = MCE(pin) # verbose=True)
# Uncomment the following to print transmit timing
# irb.timeit = True
--- /dev/null
+# rp2_rmt.py A RMT-like class for the RP2.
+
+# Released under the MIT License (MIT). See LICENSE.
+
+# Copyright (c) 2021 Peter Hinch
+
+from machine import Pin, PWM
+import rp2
+
+@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, autopull=True, pull_thresh=32)
+def pulsetrain():
+ wrap_target()
+ out(x, 32) # No of 1MHz ticks. Block if FIFO MT at end.
+ irq(rel(0))
+ set(pins, 1) # Set pin high
+ label('loop')
+ jmp(x_dec,'loop')
+ irq(rel(0))
+ set(pins, 0) # Set pin low
+ out(y, 32) # Low time.
+ label('loop_lo')
+ jmp(y_dec,'loop_lo')
+ wrap()
+
+@rp2.asm_pio(autopull=True, pull_thresh=32)
+def irqtrain():
+ wrap_target()
+ out(x, 32) # No of 1MHz ticks. Block if FIFO MT at end.
+ irq(rel(0))
+ label('loop')
+ jmp(x_dec,'loop')
+ wrap()
+
+class DummyPWM:
+ def duty_u16(self, _):
+ pass
+
+class RP2_RMT:
+
+ def __init__(self, pin_pulse=None, carrier=None, sm_no=0, sm_freq=1_000_000):
+ if carrier is None:
+ self.pwm = DummyPWM()
+ self.duty = (0, 0)
+ else:
+ pin_car, freq, duty = carrier
+ self.pwm = PWM(pin_car) # Set up PWM with carrier off.
+ self.pwm.freq(freq)
+ self.pwm.duty_u16(0)
+ self.duty = (int(0xffff * duty // 100), 0)
+ if pin_pulse is None:
+ self.sm = rp2.StateMachine(sm_no, irqtrain, freq=sm_freq)
+ else:
+ self.sm = rp2.StateMachine(sm_no, pulsetrain, freq=sm_freq, set_base=pin_pulse)
+ self.apt = 0 # Array index
+ self.arr = None # Array
+ self.ict = None # Current IRQ count
+ self.icm = 0 # End IRQ count
+ self.reps = 0 # 0 == forever n == no. of reps
+ rp2.PIO(0).irq(self._cb)
+
+ # IRQ callback. Because of FIFO IRQ's keep arriving after STOP.
+ def _cb(self, pio):
+ self.pwm.duty_u16(self.duty[self.ict & 1])
+ self.ict += 1
+ if d := self.arr[self.apt]: # If data available feed FIFO
+ self.sm.put(d)
+ self.apt += 1
+ else:
+ if r := self.reps != 1: # All done if reps == 1
+ if r: # 0 == run forever
+ self.reps -= 1
+ self.sm.put(self.arr[0])
+ self.apt = 1 # Set pointer and count to state
+ self.ict = 1 # after 1st IRQ
+
+ # Arg is an array of times in μs terminated by 0.
+ def send(self, ar, reps=1, check=True):
+ self.sm.active(0)
+ self.reps = reps
+ ar[-1] = 0 # Ensure at least one STOP
+ for x, d in enumerate(ar): # Find 1st STOP
+ if d == 0:
+ break
+ if check:
+ # Discard any trailing mark which would leave carrier on.
+ if (x & 1):
+ x -= 1
+ ar[x] = 0
+ self.icm = x # index of 1st STOP
+ mv = memoryview(ar)
+ n = min(x, 4) # Fill FIFO if there are enough data points.
+ self.sm.put(mv[0 : n])
+ self.arr = ar # Initial conditions for ISR
+ self.apt = n # Point to next data value
+ self.ict = 0 # IRQ count
+ self.sm.active(1)
+
+ def busy(self):
+ if self.ict is None:
+ return False # Just instantiated
+ return self.ict < self.icm
+
+ def cancel(self):
+ self.reps = 1
# Implements a 2-button remote control on a Pyboard with auto repeat.
from sys import platform
ESP32 = platform == 'esp32'
-if ESP32:
+RP2 = platform == 'rp2'
+PYBOARD = platform == 'pyboard'
+if ESP32 or RP2:
from machine import Pin
else:
from pyb import Pin, LED
# Test uses a 38KHz carrier.
if ESP32: # Pins for IR LED gate
pin = Pin(23, Pin.OUT, value = 0)
+ elif RP2:
+ pin = Pin(17, Pin.OUT, value = 0)
else:
pin = Pin('X1')
classes = (NEC, SONY_12, SONY_15, SONY_20, RC5, RC6_M0)
# irb.timeit = True
b = [] # Rbutton instances
- px3 = Pin(18, Pin.IN, Pin.PULL_UP) if ESP32 else Pin('X3', Pin.IN, Pin.PULL_UP)
- px4 = Pin(19, Pin.IN, Pin.PULL_UP) if ESP32 else Pin('X4', Pin.IN, Pin.PULL_UP)
+ px3 = Pin('X3', Pin.IN, Pin.PULL_UP) if PYBOARD else Pin(18, Pin.IN, Pin.PULL_UP)
+ px4 = Pin('X4', Pin.IN, Pin.PULL_UP) if PYBOARD else Pin(19, Pin.IN, Pin.PULL_UP)
b.append(Rbutton(irb, px3, 0x1, 0x7, proto))
b.append(Rbutton(irb, px4, 0x10, 0xb, proto))
if ESP32:
while True:
print('Running')
await asyncio.sleep(5)
+ elif RP2:
+ led = Pin(25, Pin.OUT)
+ while True:
+ await asyncio.sleep_ms(500) # Obligatory flashing LED.
+ led(not led())
else:
led = LED(1)
while True:
Ground pin 18 to send addr 1 data 7
Ground pin 19 to send addr 0x10 data 0x0b.'''
-print(''.join((s, sesp)) if ESP32 else ''.join((s, spb)))
+# RP2
+srp2 = '''
+IR LED gate on pin 17
+Ground pin 18 to send addr 1 data 7
+Ground pin 19 to send addr 0x10 data 0x0b.'''
+
+if ESP32:
+ print(''.join((s, sesp)))
+elif RP2:
+ print(''.join((s, srp2)))
+else:
+ print(''.join((s, spb)))
def test(proto=0):
loop.run_until_complete(main(proto))