Skip to content
Snippets Groups Projects
Commit 05d75b03 authored by Quentin Bolsee's avatar Quentin Bolsee
Browse files

doc and code

parent 226997c1
Branches
No related tags found
No related merge requests found
Showing
with 370 additions and 4 deletions
# hexcell
# Microquine: self-replicating microcontroller code
<img src="img/board_transfer.jpg" width=70%></img>
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.
"i hear biomimicry is the most sincere form of flattery."
- somebody
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.
## 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)
```
## Song and dance: using the buzzer and neopixel
```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()
```
## Files
[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.
# 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 Invention No. 8, J.S. Bach
note_time_us = 140_000
notes = notes = [
0,0,45,45,49,49,45,45,52,52,45,45,
57,57,56,54,52,54,52,50,49,50,49,47,
45,45,49,49,52,52,49,49,57,57,52,52,
61,64,62,64,61,64,62,64,61,64,62,64,
57,61,59,61,57,61,59,61,57,61,59,61,
54,57,56,57,54,57,56,57,54,57,56,57,
51,51,47,47,54,54,51,51,57,57,54,54,
59,61,59,57,56,57,56,54,52,54,52,50,
49,49,54,52,51,52,51,49,47,49,47,45,
44,45,44,42,40,40,52,51,52,52,44,44,
45,45,52,52,44,44,52,52,42,42,51,51,
52,52,52,52,0,0
]
# neopixel color
machine.Pin(11, machine.Pin.OUT).value(1)
n = neopixel.NeoPixel(machine.Pin(12, machine.Pin.OUT), 1)
n[0] = 0, 10, 0
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()
# 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)
# 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)
# morse code S.O.S.
note_time_us = 80_000
notes = notes = [
50,0,50,0,50,
0,0,0,0,0,
50,50,50,0,50,50,50,0,50,50,50,
0,0,0,0,0,
50,0,50,0,50,
0,0,0,0,0,
]
# neopixel color
machine.Pin(11, machine.Pin.OUT).value(1)
n = neopixel.NeoPixel(machine.Pin(12, machine.Pin.OUT), 1)
n[0] = 10, 0, 0
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()
# 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()
doc/PXL_20240404_011325939.jpg

146 KiB

doc/PXL_20240404_011335413.jpg

179 KiB

doc/PXL_20240404_011405820.jpg

181 KiB

doc/PXL_20240404_012448719.jpg

236 KiB

doc/PXL_20240404_013801518.jpg

354 KiB

doc/PXL_20240404_013816159.jpg

347 KiB

File moved
File moved
File moved
img/board_transfer.jpg

366 KiB

img/board_transfer_chain.jpg

363 KiB

img/board_v1.jpg

194 KiB

img/board_v1_1.jpg

191 KiB

File moved
File moved
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment