Skip to content

Commit d436ecd

Browse files
committed
Ported to Raspberry Pi Pico.
1 parent b54d37f commit d436ecd

File tree

5 files changed

+164
-21
lines changed

5 files changed

+164
-21
lines changed

README.md

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ electrical equipment.
88
They are cheap, reliable and consume negligible power. However they lack
99
flexibility: they can only be controlled by the matching remote. This library
1010
provides a means of incorporating them into an IOT (internet of things)
11-
solution, or simply enabling the construction of a remote with a better antenna
12-
and longer range than the stock item.
11+
solution, or building a remote capable of controlling more devices with a
12+
better antenna and longer range than the stock item.
13+
14+
For the constructor a key benefit is that no high voltage wiring is required.
1315

1416
The approach relies on the fact that most units use a common frequency of
1517
433.92MHz. Transmitter and receiver modules are available for this frequency at
1618
low cost, e.g. [from Seeed](https://www.seeedstudio.com/433Mhz-RF-link-kit-p-127.html).
19+
The Seeed units:
1720

1821
![Image](images/seeed.png)
1922

@@ -25,15 +28,15 @@ requiring the remote to be held very close to it to achieve results.
2528
The signal from the supplied remote is captured by a simple utility and stored
2629
in a file. Multiple signals - optionally from multiple remotes - may be stored
2730
in a file. The utility is used interactively at the REPL. Supported targets are
28-
Pyboard D, Pyboard 1.x, Pyboard Lite and ESP32.
31+
Pyboard D, Pyboard 1.x, Pyboard Lite, ESP32 and Raspberry Pi Pico.
2932

3033
#### Transmitter
3134

3235
This module is intended to be used by applications. The application loads the
3336
file created by the receiver and transmits captured codes on demand.
34-
Transmission is nonblocking. Supported targets are Pyboard D, Pyboard 1.x and
35-
ESP32. Pyboard Lite works in my testing, but only in blocking mode. The module
36-
does not use `uasyncio` but is compatible with it.
37+
Transmission is nonblocking. Supported targets are Pyboard D, Pyboard 1.x,
38+
ESP32 and Raspberry Pi Pico. Pyboard Lite works in my testing, but only in
39+
blocking mode. The module does not use `uasyncio` but is compatible with it.
3740

3841
#### Warning
3942

@@ -45,6 +48,14 @@ to improve accuracy.
4548

4649
See [section 6](./README.md#6-background) for the reasons for this approach.
4750

51+
#### Raspberry Pi Pico note
52+
53+
Early firmware has [this issue](https://github.com/micropython/micropython/issues/6866)
54+
affecting USB communication with some PC's. It particularly affects code which
55+
issues `print()` only occasionally: the application appears to have failed. The
56+
missing messages appear when you press a key. Hopefully this will be fixed soon
57+
(note dated 8th March 2021).
58+
4859
# 1. Installation
4960

5061
## 1.1 Code
@@ -53,8 +64,8 @@ Receiver: copy the `rx` directory and contents to the target's filesystem.
5364
Transmitter: copy the `tx` directory and contents to the target's filesystem.
5465

5566
In each directory there is a file `get_pin.py`. This provides a convenient way
56-
to instantiate a `Pin` on Pyboard or ESP32. This may be modified for your own
57-
needs or ignored and replaced with your own code.
67+
to instantiate a `Pin` on Pyboard, ESP32 or Raspberry Pi Pico. This may be
68+
modified for your own needs or ignored and replaced with your own code.
5869

5970
There are no dependencies.
6071

@@ -64,26 +75,33 @@ It is difficult to generalise as there are multiple sources for 433MHz
6475
transceivers. Check the data for your modules.
6576

6677
My transmitter and receiver need a 5V supply. The receiver produces a 0-5V
67-
signal: this is compatible with Pyboards but the ESP32 requires a circuit to
68-
ensure 0-3.3V levels. The receiver code is polarity agnostic so an inverting
69-
buffer as shown below will suffice.
78+
signal: this is compatible with Pyboards but the ESP32 and Raspberry Pi Pico
79+
require a circuit to ensure 0-3.3V levels. The receiver code is polarity
80+
agnostic so an inverting buffer as shown below will suffice.
7081

71-
By default pin X3 is used on the Pyboard and pin 27 on ESP32, but any pins may
72-
be used.
82+
Receiver defaults are pin X3 on Pyboard, pin 27 on ESP32 and pin 17 on Pico,
83+
but any pins may be used.
7384

7485
![Image](images/buffer.png)
7586

7687
The transmitter can be directly connected as 5V devices are normally compatible
77-
with 3.3V logic levels. Default pins are X3 on Pyboard, 23 on ESP32. Any pins
78-
may be substituted. The
88+
with 3.3V logic levels. Transmitter defaults are X3 on Pyboard, 23 on ESP32 and
89+
16 on the Pico. Any pins may be substituted. The
7990
[data for the Seeed transmitter](https://www.seeedstudio.com/433Mhz-RF-link-kit-p-127.html)
8091
states that a supply of up to 12V may be used to increase power. Whether this
8192
applies to other versions is moot: try at your own risk. I haven't.
8293

8394
## 1.3 Hardware usage
8495

8596
Pyboards: Timer 5.
86-
ESP32: RMT channel 0.
97+
ESP32: RMT channel 0.
98+
Pico: PIO state machine 0, IRQ 0, PIO 0.
99+
100+
The Pico uses `tx/rp2_rmt.py` which uses the PIO for nonblocking modulation in
101+
a similar way to the ESP32 RMT device. To use this library there is no need to
102+
study the code, but documentation is available
103+
[here](https://github.com/peterhinch/micropython_ir/blob/master/RP2_RMT.md) for
104+
those interested.
87105

88106
# 2. Acquiring data
89107

@@ -165,9 +183,10 @@ from tx.get_pin import pin
165183
transmit = TX(pin(), 'remotes')
166184
transmit('TV on') # Immediate return
167185
```
168-
The transmit method is nonblocking, both on Pyboard and on ESP32. There is an
169-
alternative blocking method for use on Pyboard only. This offers more precise
170-
timing, and I found it necessary on the Pyboard Lite only. This is accessed as:
186+
The transmit method is nonblocking, on Pyboard, ESP32 and Raspberry Pi Pico.
187+
There is an alternative blocking method for use on Pyboard only. This offers
188+
more precise timing, and I found it necessary on the Pyboard Lite only. This is
189+
accessed as:
171190
```python
172191
transmit.send('TV on') # Blocks
173192
```
@@ -230,6 +249,8 @@ async def send_queued():
230249
transmit(to_send)
231250
await asyncio.sleep_ms(delay)
232251
```
252+
In the absence of an official `Queue` class, an unofficial version is available
253+
documented [here](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#35-queue).
233254

234255
# 4. File maintenance
235256

rx/get_pin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ def pin():
1616
#pin = Pin(13, Pin.IN)
1717
elif platform == 'esp32' or platform == 'esp32_LoBo':
1818
pin = Pin(27, Pin.IN)
19+
elif platform == 'rp2': # Raspberry Pi Pico
20+
pin = Pin(17, Pin.IN)
1921
return pin

tx/__init__.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
# __init__.py Nonblocking 433MHz transmitter
2-
# Runs on Pyboard D, Pyboard 1.x, Pyboard Lite and ESP32
2+
# Runs on Pyboard D, Pyboard 1.x, Pyboard Lite, ESP32 and Raspberry Pi Pico
33

44
# Released under the MIT License (MIT). See LICENSE.
5-
# Copyright (c) 2020 Peter Hinch
5+
# Copyright (c) 2020-2021 Peter Hinch
6+
67
from sys import platform
78
ESP32 = platform == 'esp32' # Loboris not supported owing to RMT
9+
RP2 = platform == 'rp2'
810
if ESP32:
911
from esp32 import RMT
12+
elif RP2:
13+
from .rp2_rmt import RP2_RMT
1014
else:
1115
from pyb import Timer
1216

@@ -40,6 +44,11 @@ def __init__(self, pin, fname, reps=5):
4044
gc.collect()
4145
if ESP32:
4246
self._rmt = RMT(0, pin=pin, clock_div=80) # 1μs resolution
47+
elif RP2: # PIO-based RMT-like device
48+
self._rmt = RP2_RMT(pin_pulse=pin) # 1μs resolution
49+
# Array size: length of longest entry + 1 for STOP
50+
asize = max([len(x) for x in self._data.values()]) + 1
51+
self._arr = array('H', (0 for _ in range(asize))) # on/off times (μs)
4352
else: # Pyboard
4453
self._tim = Timer(5) # Timer 5 controls carrier on/off times
4554
self._tcb = self._cb # Pre-allocate
@@ -83,6 +92,11 @@ def __call__(self, key):
8392
# This would save RAM. RMT.loop() is currently broken:
8493
# https://github.com/micropython/micropython/issues/5787
8594
self._rmt.write_pulses(lst * self._reps, start = 1) # Active high
95+
elif RP2:
96+
for x, t in enumerate(lst):
97+
self._arr[x] = t
98+
self._arr[x + 1] = STOP
99+
self._rmt.send(self._arr, self._reps)
86100
else:
87101
x = 0
88102
for _ in range(self._reps):

tx/get_pin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def pin(state=0):
1212
pin = Pin('X3', Pin.OUT)
1313
elif platform == 'esp32':
1414
pin = Pin(23, Pin.OUT)
15+
elif platform == 'rp2': # Raspberry Pi Pico
16+
pin = Pin(16, Pin.OUT)
1517
elif platform == 'esp8266':
1618
raise OSError('Transmitter does not support ESP8266')
1719
elif platform == 'esp32_LoBo':

tx/rp2_rmt.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# rp2_rmt.py A RMT-like class for the RP2.
2+
3+
# Released under the MIT License (MIT). See LICENSE.
4+
5+
# Copyright (c) 2021 Peter Hinch
6+
7+
from machine import Pin, PWM
8+
import rp2
9+
10+
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, autopull=True, pull_thresh=32)
11+
def pulsetrain():
12+
wrap_target()
13+
out(x, 32) # No of 1MHz ticks. Block if FIFO MT at end.
14+
irq(rel(0))
15+
set(pins, 1) # Set pin high
16+
label('loop')
17+
jmp(x_dec,'loop')
18+
irq(rel(0))
19+
set(pins, 0) # Set pin low
20+
out(y, 32) # Low time.
21+
label('loop_lo')
22+
jmp(y_dec,'loop_lo')
23+
wrap()
24+
25+
@rp2.asm_pio(autopull=True, pull_thresh=32)
26+
def irqtrain():
27+
wrap_target()
28+
out(x, 32) # No of 1MHz ticks. Block if FIFO MT at end.
29+
irq(rel(0))
30+
label('loop')
31+
jmp(x_dec,'loop')
32+
wrap()
33+
34+
class DummyPWM:
35+
def duty_u16(self, _):
36+
pass
37+
38+
class RP2_RMT:
39+
40+
def __init__(self, pin_pulse=None, carrier=None, sm_no=0, sm_freq=1_000_000):
41+
if carrier is None:
42+
self.pwm = DummyPWM()
43+
self.duty = (0, 0)
44+
else:
45+
pin_car, freq, duty = carrier
46+
self.pwm = PWM(pin_car) # Set up PWM with carrier off.
47+
self.pwm.freq(freq)
48+
self.pwm.duty_u16(0)
49+
self.duty = (int(0xffff * duty // 100), 0)
50+
if pin_pulse is None:
51+
self.sm = rp2.StateMachine(sm_no, irqtrain, freq=sm_freq)
52+
else:
53+
self.sm = rp2.StateMachine(sm_no, pulsetrain, freq=sm_freq, set_base=pin_pulse)
54+
self.apt = 0 # Array index
55+
self.arr = None # Array
56+
self.ict = None # Current IRQ count
57+
self.icm = 0 # End IRQ count
58+
self.reps = 0 # 0 == forever n == no. of reps
59+
rp2.PIO(0).irq(self._cb)
60+
61+
# IRQ callback. Because of FIFO IRQ's keep arriving after STOP.
62+
def _cb(self, pio):
63+
self.pwm.duty_u16(self.duty[self.ict & 1])
64+
self.ict += 1
65+
if d := self.arr[self.apt]: # If data available feed FIFO
66+
self.sm.put(d)
67+
self.apt += 1
68+
else:
69+
if r := self.reps != 1: # All done if reps == 1
70+
if r: # 0 == run forever
71+
self.reps -= 1
72+
self.sm.put(self.arr[0])
73+
self.apt = 1 # Set pointer and count to state
74+
self.ict = 1 # after 1st IRQ
75+
76+
# Arg is an array of times in μs terminated by 0.
77+
def send(self, ar, reps=1, check=True):
78+
self.sm.active(0)
79+
self.reps = reps
80+
ar[-1] = 0 # Ensure at least one STOP
81+
for x, d in enumerate(ar): # Find 1st STOP
82+
if d == 0:
83+
break
84+
if check:
85+
# Discard any trailing mark which would leave carrier on.
86+
if (x & 1):
87+
x -= 1
88+
ar[x] = 0
89+
self.icm = x # index of 1st STOP
90+
mv = memoryview(ar)
91+
n = min(x, 4) # Fill FIFO if there are enough data points.
92+
self.sm.put(mv[0 : n])
93+
self.arr = ar # Initial conditions for ISR
94+
self.apt = n # Point to next data value
95+
self.ict = 0 # IRQ count
96+
self.sm.active(1)
97+
98+
def busy(self):
99+
if self.ict is None:
100+
return False # Just instantiated
101+
return self.ict < self.icm
102+
103+
def cancel(self):
104+
self.reps = 1

0 commit comments

Comments
 (0)