# 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. 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 [here](./code). ### Minimal self-replicating code ```py # 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 ```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.