diff --git a/README.md b/README.md index 8777b1dd7b579749d770346e12420c1df3c076fa..305bf421fc04f96ea369f98cd0c9c667795662f2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,138 @@ -# 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) +[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. diff --git a/code/main_bach.py b/code/main_bach.py new file mode 100644 index 0000000000000000000000000000000000000000..48c583eee39f1416d8fbc51a4aec57e340ad376e --- /dev/null +++ b/code/main_bach.py @@ -0,0 +1,80 @@ +# 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() diff --git a/code/main_blink.py b/code/main_blink.py new file mode 100644 index 0000000000000000000000000000000000000000..b54e4d77f7d9517eb66e5300ad81991307e29bb4 --- /dev/null +++ b/code/main_blink.py @@ -0,0 +1,12 @@ +# 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) diff --git a/code/main_sos.py b/code/main_sos.py new file mode 100644 index 0000000000000000000000000000000000000000..1153a0e638f6bf30c81de2f9fa8c3b168e9b675a --- /dev/null +++ b/code/main_sos.py @@ -0,0 +1,74 @@ +# 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() diff --git a/code/main_wagner.py b/code/main_wagner.py new file mode 100644 index 0000000000000000000000000000000000000000..813583e3e468453af1fed72658ac6b3b968b0d24 --- /dev/null +++ b/code/main_wagner.py @@ -0,0 +1,72 @@ +# 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() diff --git a/doc/PXL_20240404_011325939.jpg b/doc/PXL_20240404_011325939.jpg deleted file mode 100644 index 9ac5b9dd3233ce920e1fd417851e0f182e4ae52b..0000000000000000000000000000000000000000 Binary files a/doc/PXL_20240404_011325939.jpg and /dev/null differ diff --git a/doc/PXL_20240404_011335413.jpg b/doc/PXL_20240404_011335413.jpg deleted file mode 100644 index 8c9c05c6eb5a8f1f0618964878f6fee7e1e8997c..0000000000000000000000000000000000000000 Binary files a/doc/PXL_20240404_011335413.jpg and /dev/null differ diff --git a/doc/PXL_20240404_011405820.jpg b/doc/PXL_20240404_011405820.jpg deleted file mode 100644 index 4e934cc885fd72e4020650a74860359539ec1dde..0000000000000000000000000000000000000000 Binary files a/doc/PXL_20240404_011405820.jpg and /dev/null differ diff --git a/doc/PXL_20240404_012448719.jpg b/doc/PXL_20240404_012448719.jpg deleted file mode 100644 index b9b7e06eb1cebc4a8e3c5655f5fb64208b225e17..0000000000000000000000000000000000000000 Binary files a/doc/PXL_20240404_012448719.jpg and /dev/null differ diff --git a/doc/PXL_20240404_013801518.jpg b/doc/PXL_20240404_013801518.jpg deleted file mode 100644 index 736a59710bc5911a4e87643acf8bdd68c54e90e5..0000000000000000000000000000000000000000 Binary files a/doc/PXL_20240404_013801518.jpg and /dev/null differ diff --git a/doc/PXL_20240404_013816159.jpg b/doc/PXL_20240404_013816159.jpg deleted file mode 100644 index 9357c1ea388a8aa960d108579132e4118a92292d..0000000000000000000000000000000000000000 Binary files a/doc/PXL_20240404_013816159.jpg and /dev/null differ diff --git a/firmware/firmware.uf2 b/firmware/micropython_rp2_uart.uf2 similarity index 100% rename from firmware/firmware.uf2 rename to firmware/micropython_rp2_uart.uf2 diff --git a/doc/PXL_20240401_050930159.jpg b/img/board_backlight.jpg similarity index 100% rename from doc/PXL_20240401_050930159.jpg rename to img/board_backlight.jpg diff --git a/doc/PXL_20240403_014834720.jpg b/img/board_milled.jpg similarity index 100% rename from doc/PXL_20240403_014834720.jpg rename to img/board_milled.jpg diff --git a/img/board_transfer.jpg b/img/board_transfer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..979ac719fd9958223afd1940d515e39923be97f7 Binary files /dev/null and b/img/board_transfer.jpg differ diff --git a/img/board_transfer_chain.jpg b/img/board_transfer_chain.jpg new file mode 100644 index 0000000000000000000000000000000000000000..baa104ebb3131d24136672211194efb23449edef Binary files /dev/null and b/img/board_transfer_chain.jpg differ diff --git a/img/board_v1.jpg b/img/board_v1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe6410c103a6e286489b7ddbd564a27c60726ddd Binary files /dev/null and b/img/board_v1.jpg differ diff --git a/img/board_v1_1.jpg b/img/board_v1_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..746db929fbf92c79b7a27ef5e2312626d3183208 Binary files /dev/null and b/img/board_v1_1.jpg differ diff --git a/doc/PXL_20240404_011413203.jpg b/img/board_v1_1_back.jpg similarity index 100% rename from doc/PXL_20240404_011413203.jpg rename to img/board_v1_1_back.jpg diff --git a/doc/PXL_20240403_014819343.jpg b/img/milled_all.jpg similarity index 100% rename from doc/PXL_20240403_014819343.jpg rename to img/milled_all.jpg diff --git a/doc/PXL_20240403_013226992.jpg b/img/milled_traces.jpg similarity index 100% rename from doc/PXL_20240403_013226992.jpg rename to img/milled_traces.jpg diff --git a/doc/milling_interface.png b/img/milling_interface.png similarity index 100% rename from doc/milling_interface.png rename to img/milling_interface.png diff --git a/img/parts.jpg b/img/parts.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34097e1ed199ed240ef1cf02067b68c2932b65ab Binary files /dev/null and b/img/parts.jpg differ diff --git a/doc/PXL_20240403_011606789.jpg b/img/probing.jpg similarity index 100% rename from doc/PXL_20240403_011606789.jpg rename to img/probing.jpg diff --git a/doc/probe.png b/img/probing_result.png similarity index 100% rename from doc/probe.png rename to img/probing_result.png diff --git a/doc/PXL_20240403_032451975.jpg b/img/soldering1.jpg similarity index 100% rename from doc/PXL_20240403_032451975.jpg rename to img/soldering1.jpg diff --git a/doc/PXL_20240402_024455802.jpg b/img/soldering2.jpg similarity index 100% rename from doc/PXL_20240402_024455802.jpg rename to img/soldering2.jpg diff --git a/doc/PXL_20240403_022504258.jpg b/img/two_boards_milled.jpg similarity index 100% rename from doc/PXL_20240403_022504258.jpg rename to img/two_boards_milled.jpg diff --git a/doc/PXL_20240404_013315167.jpg b/img/uart.jpg similarity index 100% rename from doc/PXL_20240404_013315167.jpg rename to img/uart.jpg diff --git a/calculator/calculator.py b/pcb/calculator/calculator.py similarity index 100% rename from calculator/calculator.py rename to pcb/calculator/calculator.py