Newer
Older
In this project, we explore self-replication of microcontroller code. The code can jump hosts by simply streaming its own bytes on a UART port.
We picked an RP2040 microcontroller equipped with Micropython for the following reasons:
- As an interpreted language, it offers self-reflection at no extra cost
- The Python interpreter (REPL) can be made available directly on the UART port of the RP2040
- Sending a `CTRL-C` (=`\x03`) character resets the target microcontroller and gets it ready for code injection, no matter its current state
## Board
The board we built for these experiments is a xiao RP2040 with a single cell LiPo battery, a piezo buzzer and UART connectors:
<img src="img/board_v1_1.jpg" width=70%></img>
The LiPo battery is mounted in the back, in a 3D printed enclosure. Thanks to a specific charging manager IC, it can be charged directly from the USB connector's 5V.
<img src="img/parts.jpg" width=70%></img>
## Micropython firmware
For the purpose of this project, we use a version of Micropython in which the REPL can talk to the UART port, in addition to the usual USB CDC port.
You can find a `.uf2` build of this firmware [here](./firmware).
You can install Micropython on the board by resetting the xiao RP2040 and dragging the `.uf2` file onto the flash drive that shows up.
To verify that the REPL is available on the RP2040's UART port, you can connect a USB-to-serial adapter directly to it and power the board through its battery alone:
<img src="img/uart.jpg" width=70%></img>
The USB-to-serial adapter should be set to a baudrate of `115200`. If successful, you'll be greeted by the REPL as if you were directly connected to the RP2040 through its native USB port.
## Code
example codes are found in [](./code)
### Minimal self-replicating code
```py
# main.py
import machine
import time
# inject
print(f"\3f=open('main.py', 'wb')\nf.write({open('main.py', 'rb').read()})\nf.close()\nimport machine\nmachine.reset()")
# blink
p = machine.Pin(25, machine.Pin.OUT)
p.value(0)
time.sleep_ms(200)
p.value(1)
```
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
```py
# main.py
import machine
import time
import neopixel
# code injection
with open("main.py", "rb") as f:
print("\x03", end="")
print("f = open('main.py', 'wb')")
print("f.write(")
print(f.read())
print(")")
print("f.close()")
print("import machine")
print("machine.reset()")
# start of code
duty = int(0.6*65535)
pwm0 = machine.PWM(machine.Pin(3),freq=50_000,duty_u16=0)
# from Ride of the Valkyries, Richard Wagner
note_time_us = 110_000
notes = [
39, 0, 0, 34, 39, 42, 42, 42, 42, 42, 39, 39, 39, 39, 39,
42, 0, 0, 39, 42, 46, 46, 46, 46, 46, 42, 42, 42, 42, 42,
46, 0, 0, 42, 46, 49, 49, 49, 49, 49, 37, 37, 37, 37, 37,
42, 0, 0, 37, 42, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46
]
# neopixel color
machine.Pin(11, machine.Pin.OUT).value(1)
n = neopixel.NeoPixel(machine.Pin(12), 1)
n[0] = 0, 0, 24
n.write()
# C# Eb F# Ab Bb C# Eb F# Ab Bb
# C4 D4 E4 F4 G4 A4 B4 C5 D5 E5 F5 G5 A5 B5 C6
# 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
pitches = [1e5, 27.50000,29.13524,30.86771,32.70320,34.64783,36.70810,38.89087,
41.20344,43.65353,46.24930,48.99943,51.91309,55.00000,58.27047,61.73541,
65.40639,69.29566,73.41619,77.78175,82.40689,87.30706,92.49861,97.99886,
103.8262,110.0000,116.5409,123.4708,130.8128,138.5913,146.8324,155.5635,
164.8138,174.6141,184.9972,195.9977,207.6523,220.0000,233.0819,246.9417,
261.6256,277.1826,293.6648,311.1270,329.6276,349.2282,369.9944,391.9954,
415.3047,440.0000,466.1638,493.8833,523.2511,554.3653,587.3295,622.2540,
659.2551,698.4565,739.9888,783.9909,830.6094,880.0000,932.3275,987.7666,
1046.502,1108.731,1174.659,1244.508,1318.510,1396.913,1479.978,1567.982,
1661.219,1760.000,1864.655,1975.533,2093.005,2217.461,2349.318,2489.016,
2637.020,2793.826,2959.955,3135.963,3322.438,3520.000,3729.310,3951.066,
4186.009]
delays_us = [int(1e6/(2*pitch)) for pitch in pitches]
def play():
t = time.ticks_us()
for k in range(len(notes)):
tend = t+note_time_us
if notes[k] == 0:
while (t < tend):
t = time.ticks_us()
continue
delay_us = delays_us[notes[k]]
while (t < tend):
t = time.ticks_us()
pwm0.duty_u16(duty)
time.sleep_us(delay_us)
pwm0.duty_u16(0)
time.sleep_us(delay_us)
play()
machine.reset()
```
[OnShape Assembly](https://cad.onshape.com/documents/dbb1f2f6468431d768c0d460/w/ff7db2adb0811f91412017d1/e/b7272632b534832a71510b02)
## License
This project is provided under the MIT license.
Quentin Bolsée and Nikhil Lal, 2024.