]> vault307.fbx.one Git - micorpython_ir.git/commitdiff
Make compatible with Raspberry Pi Pico (RP2 arch).
authorPeter Hinch <peter@hinch.me.uk>
Mon, 8 Mar 2021 12:59:08 +0000 (12:59 +0000)
committerPeter Hinch <peter@hinch.me.uk>
Mon, 8 Mar 2021 12:59:08 +0000 (12:59 +0000)
README.md
RECEIVER.md
RP2_RMT.md [new file with mode: 0644]
TRANSMITTER.md
ir_rx/__init__.py
ir_rx/acquire.py
ir_rx/test.py
ir_tx/__init__.py
ir_tx/mcetest.py
ir_tx/rp2_rmt.py [new file with mode: 0644]
ir_tx/test.py

index 750a131bbe8fe01615e263c01dba58033ce0cf44..96c3fce5d185b4005d4b9f57e94c87cc4da88fd4 100644 (file)
--- 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)
index d6998f1cb25c1acd077f369576148ef12d2237d3..cd84c4f33f3257faa399c510cc257169aaed80d9 100644 (file)
@@ -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 (file)
index 0000000..8f86e6d
--- /dev/null
@@ -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.
index 7a70ccae6fc687121b30496f3179ceb27e5defef..1c30d4f7709ce316a8750ef822433211624c6cd1 100644 (file)
@@ -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
 
index 525a859353b43488f062b275efcf96e156819c01..a8ff82314f7d4cf2c30ba1df37fac656b8243fb8 100644 (file)
@@ -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
index e5162b21250b0aae398dc5fc52c81237f3774337..ea0b2e4eafd35499fa3c7ca2d86dcec57739762c 100644 (file)
@@ -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()
index 9a5dacaa42ef2cbe392c900566368fc886c7c383..48f342e2f4633a1ef70a6cde8fe815fb4dcca4c2 100644 (file)
@@ -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):
index d6c8429e5447cc8a1519ef9cb9cda06074a3326e..ac9755c3788aa2e496ed113ea1f42dc87e3a8ab9 100644 (file)
@@ -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
index 250c693c260b5bcd62633c991b0e8ad4bf4f4f7a..fc473d9c71abfa51db11c50d2893dce4b00931e2 100644 (file)
@@ -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 (file)
index 0000000..7b42fa8
--- /dev/null
@@ -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
index c1961fdc4b9e1f24247e4cd8021b5f285d79dca3..c9e3366e35482f57d76e87c6935018770f728fb1 100644 (file)
@@ -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))