Skip to content
Snippets Groups Projects
Quentin Bolsee's avatar
Quentin Bolsee authored
77246501
History
Name Last commit Last update
code
firmware
img
pcb
.DS_Store
.gitignore
README.md

Microquine: self-replicating microcontroller code

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:

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.

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.

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:

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 here.

Minimal self-replicating code

# main.py
import machine
import time

# inject
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()")

# 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

# 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

License

This project is provided under the MIT license.

Quentin Bolsée and Nikhil Lal, 2024.