From 2ebf5db498266618fc0f037469309a6ea1906304 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 8 Mar 2021 12:59:08 +0000 Subject: [PATCH] Make compatible with Raspberry Pi Pico (RP2 arch). --- README.md | 16 +++---- RECEIVER.md | 4 ++ RP2_RMT.md | 94 +++++++++++++++++++++++++++++++++++++++++ TRANSMITTER.md | 16 ++++++- ir_rx/__init__.py | 2 +- ir_rx/acquire.py | 2 + ir_rx/test.py | 2 + ir_tx/__init__.py | 13 ++++-- ir_tx/mcetest.py | 5 +-- ir_tx/rp2_rmt.py | 104 ++++++++++++++++++++++++++++++++++++++++++++++ ir_tx/test.py | 28 +++++++++++-- 11 files changed, 266 insertions(+), 20 deletions(-) create mode 100644 RP2_RMT.md create mode 100644 ir_tx/rp2_rmt.py diff --git a/README.md b/README.md index 750a131..96c3fce 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ a driver for IR "blaster" apps. The device drivers are nonblocking. They do not 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 @@ -90,20 +90,22 @@ proprietary protocols and are not supported by these drivers. # 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) diff --git a/RECEIVER.md b/RECEIVER.md index d6998f1..cd84c4f 100644 --- a/RECEIVER.md +++ b/RECEIVER.md @@ -15,6 +15,10 @@ driver design ensures operation regardless of sense. 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. diff --git a/RP2_RMT.md b/RP2_RMT.md new file mode 100644 index 0000000..8f86e6d --- /dev/null +++ b/RP2_RMT.md @@ -0,0 +1,94 @@ +# 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. diff --git a/TRANSMITTER.md b/TRANSMITTER.md index 7a70cca..1c30d4f 100644 --- a/TRANSMITTER.md +++ b/TRANSMITTER.md @@ -16,6 +16,12 @@ are arbitrary: X3 and X4 are used. The driver uses timers 2 and 5. 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 @@ -80,7 +86,8 @@ Instructions will be displayed at the REPL. # 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 @@ -101,6 +108,13 @@ from ir_tx.nec import NEC 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 diff --git a/ir_rx/__init__.py b/ir_rx/__init__.py index 525a859..a8ff823 100644 --- a/ir_rx/__init__.py +++ b/ir_rx/__init__.py @@ -2,7 +2,7 @@ # 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 diff --git a/ir_rx/acquire.py b/ir_rx/acquire.py index e5162b2..ea0b2e4 100644 --- a/ir_rx/acquire.py +++ b/ir_rx/acquire.py @@ -101,6 +101,8 @@ def test(): 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() diff --git a/ir_rx/test.py b/ir_rx/test.py index 9a5daca..48f342e 100644 --- a/ir_rx/test.py +++ b/ir_rx/test.py @@ -25,6 +25,8 @@ elif platform == 'esp8266': 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): diff --git a/ir_tx/__init__.py b/ir_tx/__init__.py index d6c8429..ac9755c 100644 --- a/ir_tx/__init__.py +++ b/ir_tx/__init__.py @@ -1,14 +1,17 @@ # __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 @@ -18,7 +21,6 @@ from time import ticks_us, ticks_diff # 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 @@ -42,6 +44,8 @@ class IR: 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 @@ -93,6 +97,9 @@ class IR: 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 diff --git a/ir_tx/mcetest.py b/ir_tx/mcetest.py index 250c693..fc473d9 100644 --- a/ir_tx/mcetest.py +++ b/ir_tx/mcetest.py @@ -53,10 +53,7 @@ class Rbutton: 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 diff --git a/ir_tx/rp2_rmt.py b/ir_tx/rp2_rmt.py new file mode 100644 index 0000000..7b42fa8 --- /dev/null +++ b/ir_tx/rp2_rmt.py @@ -0,0 +1,104 @@ +# 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 diff --git a/ir_tx/test.py b/ir_tx/test.py index c1961fd..c9e3366 100644 --- a/ir_tx/test.py +++ b/ir_tx/test.py @@ -7,7 +7,9 @@ # 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 @@ -61,6 +63,8 @@ async def main(proto): # 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) @@ -69,14 +73,19 @@ async def main(proto): # 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: @@ -106,7 +115,18 @@ IR LED gate on pin 23 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)) -- 2.47.3