Category Archives: Technology

3D Design: 3D Lissajous


  • 2024/01/22: published without much reflection & conclusion as research is ongoing
  • 2023/12/02: adding more examples and refining details
  • 2023/10/22: start writeup


While studying continuous fiber 3D printing and its main nature is to find ways to lay fiber without interruption. In order to refresh my memory I revisited the Lissajous forms, which until recently only knew in their 2D form, the swirling strings or lines – and now extending it into 3D as well.

The main idea is to realize how a line, string or fiber can be used to fill non-planar and circumvent a 3D structure and how angular shifting in Lissajous context affects such form.

3D Lissajous

  • angle: 0 .. 2pi or 0 .. 360°
  • p, n, m: 0 .. 1000, the amount of loops
  • phi0, phi1, phi2: the angular offsets 0 … 2pi or 0 .. 360°
  • X = sin(angle*p+phi0)*r
  • Y = sin(angle*n+phi1)*r
  • Z = sin(angle*m+phi2)*r

I did a lot of experimenting – I could post hundreds of forms – but let me focus on one a bit closer, which got my attention:

It is a very interesting transition, 8/13/21 with phi0=0° gives almost a cube-like structure, and shifting the X loop to 90° we get a tetrahedron:

Spherical Lissajous

While playing with 3D Lissajous, I thought to adapt the cyclic nature, but apply it to a circle laying in the XY plane and then rotate in X axis, and Y axis as well, and optionally cyclic translation as well:

  • d: diameter
  • angle: 0 .. 2pi or 0 .. 360°
  • p: amount of loops as in X=sin(angle*p)*d/2, Y=cos(angle*p)*d/2
  • q: amount of X rotations: rotateX(angle*q)
  • r: amount of Y rotations: rotateY(angle*r)
Spherical Lissajous 12.23 with spreading struts

The model was printed with MSLA white resin at XYZ 50um resolution with 120mm diameter, with a few support structures near the bottom:

Spherical Lissajous with Translations

Using the Spherical Lissajous and extend it slightly:

  • [A,B,C]loop/offset/radius: translate([ sin(angle*AL+AO)*AR], sin(angle*BL+BO)*BR], sin(angle*CL+CO)*CR ])

which spreads the ribbons away from the spherical surface origins.

Spherical Lissajous 5.11 AL=3, AR=5

Spherical Lissajous 5.11 AL=3, AR=5

It’s symmetric X- and Z-wise, in Y-axis it isn’t.

The model was printed with MSLA white resin at XY 35um / Z 50um, at 60mm in Z height, ~94mm width; with a some support structures:

Spherical Lissajous 11.15 AL=2, AR=5

A more elaborate form is 11.15 AL=2, AR=5:

Spherical Lissajous 11.15 AL=2, AR=5 with spreading struts

So, there is no X-, Y- or Z-wise symmetry.

The model was printed with MSLA white resin at XY 35um / Z 50um, at 60mm in Z height, ~94mm width; with a some support structures:

and printing it larger with ~200mm width with manually positioned support:

That’s it (for now).


MSLA Anycubic Photon Mono X 6Ks


  • 2023/11/18: published
  • 2023/11/11: printer ordered
  • 2023/10/24: starting writeup


Anycubic Photon Mono X 6Ks

After using Anycubic Photon Mono X2, and not that happy with it, I saw Anycubic released another resin printer:

  • build volume: 195.8 x 122.4 x 200 mm (WxDxH), same size as Mono X2:
    • reuse all its third party parts also compatible with X2: vat, FEP films etc
  • resolution: XY 34μm, Z 10-50μm
  • 9.1″ display with 6K resolution (5760×3600) display
  • monochromatic LCD (hence “Mono”), faster printer due shorter exposure
  • affordable with EUR 250-280 (2023/11)
  • no network, only USB drive printing

The Photon Mono X2 with XY 48μm has not so well performed for me in regards of precise parts, as 100μm precision was not really delivered, either it was underexposing and fail on 1mm walls, or solid walls but thicker – somehow I could not find a good balance. The Photon Mono 4K performed more reliable for me and was able to print geometrically more reliable and correct with XY 35μm; so my hope is that the X 6Ks delivers with 34μm pixel size.

I ordered the Photon Mono X 6Ks in 2023/11 for EUR ~230 incl. shipment, and it arrived a few days later from a warehouse in Germany to Switzerland.

Anycubic’s Naming Convention & Communication

The X 6Ks is kind of successor of the X2 with the same body shape and build volume, but who cares of good naming? It could have been X2 6K simply. Aside, Anycubic seems the care little to provide customers the XY resolution anymore on info material or their web-site (2023/11) but only state XY build area and XY resolution, and announces 4K, 6K or whatever – you need to calculate the XY pixel size yourself . . .

Print Settings

retrieved from Anycubic 2023/11/11 as screenshot:

For Lychee Slicer (2023/11) I recommend following settings:

  • enable Two Stage Motion Control (TSMC)
  • Bottom Layer: 1-6 (1 for small pieces, 6 for larger pieces)
  • Transition Layers: 10 (for flat pieces reduce to 1)
  • Lift Distances: [1] 4mm, [2] 8mm
  • Lift Speeds: [1] 1mm/s, [2] 3mm/s or 60mm/min, 180mm/min

Here my settings:

Flex Build Plate

I’ve got a spring steel plate 202×128 mm from Aliexpress, and as I ordered two of them for X2 but I can reuse one for the X 6Ks as well:

  • magnetic base: 2.2mm thick, slightly extends, 203x129mm
  • steel plate: 0.5mm thick, exact 202x128mm, I roughed up the exposed surface with sandpaper to increase adhesion

so one has to compensate 2.5 – 3mm in Z height and adjust, speak extend, the Z-level probe. I just glued a black strip of paper, and then of course re-level the bed again afterwards.

Glued a piece of paper extending it ~2.5mm (I used black marker to darken the paper afterwards)

Preliminary Review Mono X 6Ks


  • cost-effective for 196x122mm build area and 34μm pixel size
  • good third party market (X K6s parts are mostly compatible with X2, except UV LCD & mainboard)


  • Mechanical Quality Control (QC):
    • build-plate has too much horizontal play (2+mm), the Mono X2 build-plate which has the same size fits tighter (see below for detailed photos)
    • grey/clear upper case is/was warped, doesn’t fit seamless off by 3-4mm bent inside on left & right; likely package long stored in wrong position or bad clear plastic deforming over time (remedy below “Cover Wedges”)
  • no WiFi
  • very basic firmware
  • poor quality SD stick, it usually won’t last a couple of weeks before it becomes unwritable or unreadable, but this time I couldn’t even backup the data, the stick failed right away; visit and download the slicer, handbook and test files to calibrate UV exposure times

Anycubic is known to invest very little into the firmware which is provided by Chitubox, given they are one of the biggest resin printer sellers, it affects 100,000s of customers.

The build plate also has new a black plastic connector to the mount, an attempt to reduce cost perhaps. The Mono X2 build plate mount is much better in my opinion, more solid; the Mono X 6Ks build plate has more wiggle/play horizontally, therefore pieces printed you want definitely in the center of the UV LCD/vat, not on the side to avoid any wiggle or play while printing introducing imprecisions.

Cover Wedges

As the X K6s came with a warped cover, and with the X2 with the same case geometry it was wiggly everytime I put the cover over, I printed a few “wedges” which align the cover easily:

and I did the same with my Mono X2.

(M)SLA Value Comparison

A numeric value summarization for features I care about, the rough & simple formula:

XY Area [mm2] / Price [EUR] / XY Resolution [μm]

The bigger the XY area, the smaller the price and the smaller the resolution, the higher the value:

State 2024/07

Elegoo Saturn 42024-19μm x 24μm2)MSLA218x122x220300 EUR3.74
Elegoo Saturn 4 Ultra2024-19μm x 24μm2)MSLA218x122x220400 EUR2.81
Anycubic Photon Mono M52023-19μm x 24μm2)MSLA218x123x200410 EUR2.72
Elegoo Mars 5 Ultra2024-18μmMSLA153x78x165270 EUR2.54
Anycubic Photon Mono X 6Ks2023-34μmMSLA196x122x200280 EUR2.51
Elegoo Saturn 3 Ultra2023-19μm x 24μm2)MSLA218x123x260480 EUR2.32
Anycubic Photon Mono 4K2021-202235μmMSLA132x80x165150 EUR2.01
EMake Galaxy 1202325μm / 100μm1)SLA400x200x4001,600 EUR2.00
Anycubic Photon Mono X22023-48μmMSLA196x122x200260 EUR1.91
Anycubic Photon Mono 22023-35μmMSLA143x89x165210 EUR1.73
Elegoo Jupiter SE2023-51μmMSLA277x156x300700 EUR1.21
Anycubic Photon M3 Max2022-45μmMSLA298x164x300980 EUR1.10
Formlabs 3L2020-25μm / 100μm1)SLA335x200x3009,000 EUR0.29
Formlabs 3+2020-25μm / 100μm1)SLA146x145x1853,500 EUR0.24
Prusa SL1S2021-49μmMSLA127x80x1801,700 EUR0.12
Formlabs 42024-50μmMSLA200x125x2105,175 EUR0.09
  1. Formlabs 3’s & EMake SLA printers use a laser beam which has 100μm in diameter, but it can be positioned 25μm exact, the latter was used to calculate the value
  2. The UV pixels are non-square, the longer side was used to calculate the value

The XY area in SLA scales not as good as with MSLA, as the laser beam takes longer the more XY area (e.g. more pieces) need to be rendered per layer – so, MSLA is recommended for aiming fast parallel printing. Interestingly Formlabs 4 is now a MSLA as well (2024/04), they seem to have abandoned the SLA laser-based approach.

Anycubic Mono X 6Ks, X2 and 2x 4K

One of the main reason I choose Anycubic MSLA is the third party market for add-on’s, like flex build plate, vats, LCD replacement etc, and I also like the build plate mount with 4 screws for alignment; and I hoped to replace the firmware of the Mono 4K which did not happen as the Open Source variant is still incomplete (2023/11).


Misc: Formnext 2023


  • 2023/11/13: published
  • 2023/11/11: starting writeup


Another year, another November in Frankfurt (Germany) and Formnext – this is the main event of the year professionally for me. As I reside in Switzerland the travel is fairly easy and short and the 770 exhibitors in two halls (11 & 12) with two floors each is so overwhelming that even 4 days attending is not sufficient.

  • Day 1 (Tue, Nov 7): I spent an entire day to explore hall 12.1 alone, which turns out a good choice as it was a dense populated hall with many smaller companies
  • Day 2 (Wed, Nov 8): visiting with a client half of the day to review some of their possible competition, and then explore 12.0
  • Day 3 (Thu, Nov 9): some schedules meetings and then explore 11.0 and 11.1
  • Day 4 (Fri, Nov 10): revisiting 12.1 and 11.1 briefly, visiting with another client some selected booths to check products on display

I surely missed a few booths in 11.1 and 12.1 still; whereas 12.0 and 11.0 were more large scale industrial AM solutions, mixed with university and regional focused booths which I didn’t have time to explore in detail.

Personal Selection

I feature some companies according my personal professional interests:

Spherene (Math)

I made contact with Spherene before via LinkedIn but I realized I missed the point of what Spherene actually “invented”, at their booth Daniel Bachmann took the time to show me the features of their new class of minimal surface model and it was challenging for me to follow him despite of my own experience with Triply Periodic Minimal Surfaces (TPMS) – after apprx. 20 mins I realized the scope and some of their depth of their “invention”.

In essence, the sphere is used as a base form, and density, wall thickness and other features are processed in a localized manner, filling the space. The main result doing is optimizing a form to distribute inner/outer forces, e.g. the ends of the spheres are perpendicular to the surface providing ideal way to distribute them into a network of thin walled interconnected spheres providing isotropic (“all directions”) property.

The samples on display were printed with MSLA, SLA, FFF/FDM or SLM were indeed very strong in relation to the printed volume, e.g. the hallow rabbit printed with resin barely gave in when pushing on the thin outer perimeter – impressive.

Their approach is available as cloud-based GUI or as Grasshopper/Rhino plugin. The actual details of their procedure isn’t easily found but a patent (WO2020229692A1) by CEO Christian Waldvogel gives some idea.

Genera (DLP Resin)

There are many MSLA/SLA/DLP printer manufacturers, yet, I wasn’t aware of Genera and I was shown their system, an integrated workflow:

  • all resin vats have a lid (only applies for G1/F1 combo but not their bigger machines), which are opened only within the machine
  • the finished prints (still on the plate) are moved in a box into the washing machine (without any person touching resin or the resin coated prints)
  • once automatically cleaned and post-cured, the prints are removed from the build plate manually

In essence one does not interact with resin directly, it’s all contained within the workflow – which I like a lot. They also provide wide selection of resins: hard, soft, rubbery, opaque, transparent/clear.

My idea has been to adapt some of their approach to make my own resin printing with Photon series (4K, X2 and X 6Ks); right now I also have multiple vats, and flex-plate, but moving the printed parts and washing them are still messy.

Quantica (Resin Jetting)

Last year I already visited the booth of Quantica, and so this year again. I asked earlier for printed samples, but they declined, and again this time . . . it is bizarre to see a machine actually able to print, and they don’t hand out samples, but I was told by January 2024 I might get some. This tells me a few things, the printed pieces are very sparse or not yet at the quality they want others to experience – some samples were on display, but sealed behind a glass box unable to have in my hand. So I guess now, they are expecting or already have better and more reliable printing results where the printed pieces match other similar printing processes.

I follow their development closely since ~2 years as I consider it very innovative to print with 7 different resin-based materials at the same time and able to fine-tune material properties on the voxel-level.

Duet3D (Open Source Hardware & Community Building)

UK-based Duet3D with its Duet boards and RepRapFirmware (RRF) is, as I wrote before, a beacon within the Open Source Hardware community – it isn’t just an example for other companies, and but also a great synergy provider, aiming to bring different individuals, groups and companies together.

Brandon Builds’ Open 5X version was featured on a Voron 0, and a second machine also 5-axis setup with a tool changer.

Rapid Liquid Printing / RLP (Flexible Structures)

While roaming around a small booth of RLP caught also my attention, where a video was featured of a nozzle moving in a bed filled with silicon printing rubber, and other flexible material:

Reinforce 3D (Enhancement)

Another truly innovative approach combining and enhancing existing additive manufacturing processes was shown by Reinforce 3D:

  • using existing AM methods such as SLM, SLA, MSLA and even FFF/FDM to make models with thin walled tunnels and then
  • filling or rather pushing them with strains of carbon fibres along with resin into the tunnels
  • and thereby reinforcing free forms by keeping the result lightweight but incredible strong due the embedded carbon fibres

A very small but significant detail is, that you can print multiple parts on a smaller printer, but once you start to insert the bundles of carbon fiber those segments of pieces get combined in a strong assembly, as the aluminium skeleton shown above.

Plasmics (Inductive Heated Hotend)

INo Trident – inductive hotend by Plasmics: fast heatup and cooldown / 3s from 20C to 220C, 10s from 220C to 150C

At the booth of Plasmics I looked at the inductive hotend and saw the heating up in a few seconds from 20C to 220C and cool-off in a small demo first hand.

The hot part of the nozzle looks like a needle, with little thermal mass, hence the fast heat and cooling-off time, and then surrounded by ceramics with the inductive coil on it.

The hotend incl. the controller board priced at EUR 400 is high for DIY enthusiasts but low for an industrial setup.


Major AM players were present:

  • Formlabs: industrial SLA & SLS
  • Markforged: 3-axis Continueous Carbon Fiber (CFF)
  • Nexa3D: industrial SLA & SLS
  • Prusa Research: the usual lower-end/lower cost printer and their industrial aimed printers of the “Pro” series
  • Elegoo: low-cost resin & FFF/FDM printers, resins & filaments
  • Anycubic: low-cost resin & FFF/FDM printers, resins & filaments
  • BambuLab: cost-effective quality high-speed FFF/FDM printers
  • Creality: low-cost FFF/FDM & resin printers
  • Modix: low-cost but large scale FFF/FDM printers
  • Polymaker: filaments
  • eSun: filaments & resins
  • many smaller filament seller
  • E3D: hotends, extruders

And UltiMaker (after Ultimaker & MakerBot merger) wasn’t present again; the consensus has been that BambuLab‘s printers have taken the higher quality consumer FFF/FDM printers market segment, and the air getting thinner for UltiMaker – at the same time they are doing a great service with the Open Source Cura slicer.

Random Impressions


IoT: Milk-V Duo (RISC-V) eSBC running Linux


  • 2024/02/18: added AlpineLinux for Milk-V Duo 256M
  • 2024/02/15: adding Ubuntu-22.04 & ArchLinux disk images for Milk-V Duo 256M version, and new photos of 256M version as well
  • 2023/11/20: Ubuntu 22.04 v0.0.2/0.0.3 images released
  • 2023/11/02: ArchLinux 2023-10-09 v0.0.3 image released
  • 2023/10/31: distro update on Debian RISCV64, Fedora 38, Alpine Linux; description how to set static IP for host when using RNDIS
  • 2023/10/21: released ArchLinux disk image with RNDIS support
  • 2023/10/16: creating custom disk images, releasing my own disk image tagged “spiritdude”
  • 2023/10/11: 2nd option for swap space, OLED SSD1306 example with TinyCC
  • 2023/10/10: adding Resizing Disk, Start Script at Boot, TinyCC, GPIO, Pinpong, Software State update
  • 2023/10/05: published
  • 2023/10/03: adding printable case and guide to add multiple boards on a host
  • 2023/09/21: starting writeup


In 2023/09 I purchased a couple of Milk-V Duo boards at USD 5.00 / piece, which supposed are able to boot and run Linux – a very competitive option.

Milk-V Duo board

Within 3D printing context it can or could serve as:

  • real-time motor controller
  • tiny AI component to detect failed prints via camera (check these threads on ‘yolo’ and MilkV Duo: TPU)
  • running Klipper on it, likely requires expansion board with additional USB ports, not yet tested; ArchLinux repo provides it though
  • any kind of quick experimental setup when Linux is a requirement, and Raspberry Pi perhaps overkill already, and ESP32 not powerful enough to run Linux*)

*) as of 2023/09 a few people working on Linux for ESP32-S3

A couple of years ago I used ESP8266 with Lua, and ESP32 with Lua and MicroPython, but the functionality was very limited, although it had WiFi built-in, but barely ran anything more complex or serious – the Milk-V Duo changes this, at the same pricing of USD 5.00-9.00; and I really looking forward to have just a small device running Linux, and competing with Raspberry Pi’s which either hardly were available or sold at 2-3x the announced price.

and the newer 256MB RAM variant (released 2023/12):

Specification & Features have been partially copied from (2023/09 & 2024/02):


Milkv-DUO (64M)MILKV-DUO 256M 1)MILKV-DUO 512M 2)
ProcessorCVITEK CV1800B (C906@1GHz + C906@700MHz) + 8051@8KB SRAM + TPUSG 2002 (C906@1GHz + C906@700MHz) + (Cortex-A53@1GHz) + 8051@6KB SRAM + TPUSG 2000
MemoryDDR2 64MB DDR2 256MBDDR2 512MB
Storage1x Mirco SD slot
1x SD NAND solder pad
1x Mirco SD slot
1x SD NAND solder pad
USB1x USB-C for data and power
1x USB2 solder pad
1x USB-C for data and power
Camera1x 16P FPC connector (MIPI CSI 2-lane)1x 16P FPC connector (MIPI CSI 2-lane)
GPIOup to 26 Pins available for general purpose I/O(GPIO)up to 26 Pins available for general purpose I/O(GPIO)
Size21mm * 51mm (same as Raspberry Pi Pico)21mm * 51mm (same as Raspberry Pi Pico)
PriceEUR 5.00-7.00EUR 7.00-9.00
  1. available since 2024/01
  2. not yet announced



  • 1GHz and 700MHz RISC-V C906 processors
  • 8051with 8/6KB SRAM
  • Integrated CVITEK TPU for smart detection
  • Supports H.264/H.265 video encoding, up to 2880×1620@20fps
  • Compatible with high-definition CMOS sensors
  • Programmable frequency output for sensor clock
  • Comprehensive ISP features for image optimization
  • Partial OpenCV library support with CV hardware acceleration
  • 16-bit audio codec with built-in mic input and output functions
  • Flexible network configurations with 1 Ethernet PHY

CSI-2 (MIPI serial camera)

  • Features a 16-pin FPC interface for 2-lane MIPI camera input
  • Operates I2C, CLK, and RST signals at a 1.8V voltage level


  • Milk-V Duo includes CV1800B chip with a 100Mbps PHY
  • PHY is linked to a 5-pin solder pad
  • External transformer and RJ45 socket are needed for Ethernet use


  • USB 2.0 compliant, backward compatible with USB 1.1
  • Supports various speed modes, Host/Device functionality, and transfer protocols
  • Expandable interfaces via USB Hub (up to 127 devices)
  • Power-saving mode, supports HID devices
  • Functions as USB slave device with configurable software
  • USB Type-C for storage media access

Micro SD

  • SDIO0 is compatible with Secure Digital Memory (SD 3.0) protocol

No WiFi (but Ethernet over USB)

  • it has no WiFi (either use ESP8266 or ESP32 at WiFi gateway via UART, but this kind contradicts the entire single board concept)
  • USB(-C) connector can be used in double feature mode: as power supply and pseudo network called RNDIS (virtual Ethernet over USB)

No HDMI / Video Out

  • it has no video out, at best I2C or SPI-based LCD can be connected, and function as framebuffer (see this post as a start)


  • Up to 26 GPIO pins on the MilkV-Duo 40-pin header provide access to internal peripherals such as SDIO, I2C, PWM, SPI, J-TAG, and UART
  • Up to 3x I2C
  • Up to 5x UART
  • Up to 1x SDIO1
  • Up to 1x SPI
  • Up to 2x ADC
  • Up to 7x PWM
  • Up to 1x RUN
  • Up to 1x JTAG

Distributions for Milk-V Duo 256M

Status 2024/02

Ubuntu 22.04 RISCV64 ★★☆☆☆★★★☆☆– rndis / usb network
– package management (apt) works with 240MB available RAM
– rootfs 8GB, 600MB used
ArchLinux RISCV64★★☆☆☆★★★☆☆– package management (pacman)
– rootfs 8GB, 960MB used
– rndis / usb network
AlpineLinux RISCV64★★☆☆☆★★★☆☆– package management (apk)
– rndis / usb network (but random MAC addresses)
– rootfs 1GB, 150MB used

Distributions for Milk-V Duo (64MB RAM)

Status 2023/11

Duo BuildRoot SDK★★★☆☆★★☆☆☆– works
– compact
– rndis / usb network
– cumbersome bootstrap (make menuconfig)
– no easy install of apps on live system
ArchLinux RISCV64★★☆☆☆★★★☆☆– package management (pacman)
– rootfs 2/4/8GB, 960MB used
– rndis / usb network
AlpineLinux RISCV64★★☆☆☆★★★☆☆– package management (apk)
– rndis / usb network
– rootfs 1GB, 150MB used
– use apk [add|update] –no-check-certificate
Ubuntu 22.04 RISCV64★☆☆☆☆★★★☆☆– package management (apt)
– rootfs 8GB, 600MB used
apt is awfully slow due 50+MB RAM requirement
Debian RISCV64★☆☆☆☆★★★☆☆package management (apt/dpkg)– no rndis (no virtual ether over usb)
– very limited amount of packages
– 4GB SD card minimum
Gentoo RISCV☆☆☆☆☆★★★☆☆– no disk image yet released
Fedora RISCV Builder☆☆☆☆☆★★★☆☆package management (rpm)– boots, but then fails with systemd (coredump)
– no login possible

Linux BuildRoot Disk Image

The BuildRoot is a minimal custom Linux release which is meant for IoT developers who know what they want and need and select the features at building time and then get a disk image which contains then a set of features and applications. It’s not very user friendly, e.g. there is no package manager which allows to add new packages afterwards once the system is running.



Attach a SD card to your host, find out which device it became (Linux):

% lsblk

CAUTION: make sure /dev/sdX (replace X with proper letter) is the SD card and not your other disk(s) as copying the disk image will erase and replace the content of the device you choose with the following command:

% sudo dd if=milkv-duo-v1.0.4-2023-0908.img of=/dev/sdX bs=1M; sync

then remove SD card from the host*), and insert it to the MilkV-Duo board, and power it on via USB-C.

*) in case you have the wrong /dev/sdX, or write to it if it’s not plugged in, you might struggle to write there again, simply do sudo rm /dev/sdX and then pull out and plugin the SD card again, and you should be able to write again to it.

After ~15 seconds you should be able to login with ssh root@ into your MilkV-Duo with passwd: milkv or you attach a UART to USB bridge like that:

  • Pin 16/TX: RX/white UART-USB
  • Pin 17/RX: TX/green UART-USB
  • Pin 18/GND: GND/black UART-USB
  • don’t connect 5V/red UART-USB

and use tio /dev/ttyUSB0 (or another number) under Linux to connect via serial port; also useful in case bootup is stuck after doing changes, and ssh isn’t possible anymore.

Note: you won’t need to solder the male connectors, I usually just insert them loosely and the cable bending gives sufficient connectivity for a brief login to fix things and then remove UART-USB cable again.

Boot dmesg

[    0.000000] Linux version 5.10.4-tag- (ubuntu@linux) (riscv64-unknown-linux-musl-gcc (Xuantie-900 linux-5.10.4 musl gcc Toolchain V2.6.1 B-20220906) 10.2.0, GNU ld (GNU Binutils) 2.35) #1 PREEMPT Fri Sep 8 17:23:15 CST 2023
[    0.000000] earlycon: sbi0 at I/O port 0x0 (options '')
[    0.000000] printk: bootconsole [sbi0] enabled
[    0.000000] efi: UEFI not found.
[    0.000000] Ion: Ion memory setup at 0x0000000082473000 size 26 MiB
[    0.000000] OF: reserved mem: initialized node ion, compatible id ion-region
[    0.000000] Zone ranges:
[    0.000000]   DMA32    [mem 0x0000000080000000-0x0000000083f3ffff]
[    0.000000]   Normal   empty
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000080000000-0x0000000083f3ffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000080000000-0x0000000083f3ffff]
[    0.000000] On node 0 totalpages: 16192
[    0.000000]   DMA32 zone: 222 pages used for memmap
[    0.000000]   DMA32 zone: 0 pages reserved
[    0.000000]   DMA32 zone: 16192 pages, LIFO batch:3
[    0.000000] SBI specification v0.3 detected
[    0.000000] SBI implementation ID=0x1 Version=0x9
[    0.000000] SBI v0.2 TIME extension detected
[    0.000000] SBI v0.2 IPI extension detected
[    0.000000] SBI v0.2 RFENCE extension detected
[    0.000000] riscv: ISA extensions acdfimsuv
[    0.000000] riscv: ELF capabilities acdfimv
[    0.000000] pcpu-alloc: s0 r0 d32768 u32768 alloc=1*32768
[    0.000000] pcpu-alloc: [0] 0 
[    0.000000] Built 1 zonelists, mobility grouping on.  Total pages: 15970
[    0.000000] Kernel command line: root=/dev/mmcblk0p2 rootwait rw console=ttyS0,115200 earlycon=sbi loglevel=9 riscv.fwsz=0x80000
[    0.000000] Dentry cache hash table entries: 8192 (order: 4, 65536 bytes, linear)
[    0.000000] Inode-cache hash table entries: 4096 (order: 3, 32768 bytes, linear)
[    0.000000] Sorting __ex_table...
[    0.000000] mem auto-init: stack:off, heap alloc:off, heap free:off
[    0.000000] Memory: 29360K/64768K available (3671K kernel code, 457K rwdata, 1651K rodata, 144K init, 195K bss, 35408K reserved, 0K cma-reserved)
[    0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[    0.000000] rcu: Preemptible hierarchical RCU implementation.
[    0.000000] rcu: 	RCU event tracing is enabled.
[    0.000000] 	Trampoline variant of Tasks RCU enabled.
[    0.000000] rcu: RCU calculated value of scheduler-enlistment delay is 25 jiffies.
[    0.000000] NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0
[    0.000000] riscv-intc: 64 local interrupts mapped
[    0.000000] plic: interrupt-controller@70000000: mapped 101 interrupts with 1 handlers for 2 contexts.
[    0.000000] random: get_random_bytes called from start_kernel+0x2e0/0x41c with crng_init=0
[    0.000000] riscv_timer_init_dt: Registering clocksource cpuid [0] hartid [0]
[    0.000000] clocksource: riscv_clocksource: mask: 0xffffffffffffffff max_cycles: 0x5c40939b5, max_idle_ns: 440795202646 ns
[    0.000009] sched_clock: 64 bits at 25MHz, resolution 40ns, wraps every 4398046511100ns
[    0.008425] Calibrating delay loop (skipped), value calculated using timer frequency.. 50.00 BogoMIPS (lpj=100000)
[    0.019128] pid_max: default: 4096 minimum: 301
[    0.024025] Mount-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
[    0.031444] Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
[    0.041119] ASID allocator initialised with 65536 entries
[    0.046849] rcu: Hierarchical SRCU implementation.
[    0.052223] EFI services will not be available.
[    0.057313] devtmpfs: initialized
[    0.066807] early_time_log: do_initcalls: 4548979us
[    0.072472] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[    0.082528] futex hash table entries: 16 (order: -4, 384 bytes, linear)
[    0.089496] pinctrl core: initialized pinctrl subsystem
[    0.095414] NET: Registered protocol family 16
[    0.100480] DMA: preallocated 128 KiB GFP_KERNEL pool for atomic allocations
[    0.107813] DMA: preallocated 128 KiB GFP_KERNEL|GFP_DMA32 pool for atomic allocations
[    0.116713] thermal_sys: Registered thermal governor 'step_wise'
[    0.132538] OF: /gpio@03020000/gpio-controller@0: could not find phandle
[    0.145753] OF: /gpio@03021000/gpio-controller@1: could not find phandle
[    0.152748] OF: /gpio@03022000/gpio-controller@2: could not find phandle
[    0.159737] OF: /gpio@03023000/gpio-controller@3: could not find phandle
[    0.166722] OF: /gpio@05021000/gpio-controller@4: could not find phandle
[    0.175540] clk reset: nr_reset=64 resource_size=8
[    0.181130] get audio clk=24576000
[    0.184672] cvitek-i2s-subsys 4108000.i2s_subsys: Set clk_sdma_aud0~3 to 24576000
[    0.205405] dw_dmac 4330000.dma: CVITEK DMA Controller, 8 channels, probe done!
[    0.213805] SCSI subsystem initialized
[    0.218237] usbcore: registered new interface driver usbfs
[    0.224017] usbcore: registered new interface driver hub
[    0.229634] usbcore: registered new device driver usb
[    0.238395] Ion: ion_parse_dt_heap_common: id 0 type 2 name carveout align 1000
[    0.246344] Ion: rmem_ion_device_init: heap carveout base 0x0000000082473000 size 0x0000000001acd000 dev (____ptrval____)
[    0.257624] ion_carveout_heap_create, size=0x1acd000
[    0.262937] cvi_get_rtos_ion_size, rtos ion_size get:0x0
[    0.398272] platform carveout: [ion] add heap id 0, type 2, base 0x82473000, size 0x1acd000
[    0.407276] Advanced Linux Sound Architecture Driver Initialized.
[    0.414938] clocksource: Switched to clocksource riscv_clocksource
[    0.423536] NET: Registered protocol family 2
[    0.429180] tcp_listen_portaddr_hash hash table entries: 256 (order: 0, 4096 bytes, linear)
[    0.437895] TCP established hash table entries: 512 (order: 0, 4096 bytes, linear)
[    0.445680] TCP bind hash table entries: 512 (order: 0, 4096 bytes, linear)
[    0.452950] TCP: Hash tables configured (established 512 bind 512)
[    0.459557] UDP hash table entries: 128 (order: 0, 4096 bytes, linear)
[    0.466324] UDP-Lite hash table entries: 128 (order: 0, 4096 bytes, linear)
[    0.473794] NET: Registered protocol family 1
[    0.479012] RPC: Registered named UNIX socket transport module.
[    0.485127] RPC: Registered udp transport module.
[    0.490010] RPC: Registered tcp transport module.
[    0.494934] RPC: Registered tcp NFSv4.1 backchannel transport module.
[    0.504220] Initialise system trusted keyrings
[    0.509141] workingset: timestamp_bits=62 max_order=13 bucket_order=0
[    0.524041] squashfs: version 4.0 (2009/01/31) Phillip Lougher
[    0.531324] jffs2: version 2.2. (NAND) © 2001-2006 Red Hat, Inc.
[    0.538516] Key type asymmetric registered
[    0.542733] Asymmetric key parser 'x509' registered
[    0.554087] Serial: 8250/16550 driver, 5 ports, IRQ sharing disabled
[    0.562710] printk: console [ttyS0] disabled
[    0.567239] 4140000.serial: ttyS0 at MMIO 0x4140000 (irq = 15, base_baud = 1562500) is a 16550A
[    0.576276] printk: console [ttyS0] enabled
[    0.584875] printk: bootconsole [sbi0] disabled
[    0.595241] 41c0000.serial: ttyS4 at MMIO 0x41c0000 (irq = 16, base_baud = 1562500) is a 16550A
[    0.607812] cvi-spif 10000000.cvi-spif: unrecognized JEDEC id bytes: 00 00 00 00 00 00
[    0.616044] cvi-spif 10000000.cvi-spif: device scan failed
[    0.621769] cvi-spif 10000000.cvi-spif: unable to setup flash chip
[    0.635092] libphy: Fixed MDIO Bus: probed
[    0.639938] bm-dwmac 4070000.ethernet: IRQ eth_wake_irq not found
[    0.646304] bm-dwmac 4070000.ethernet: IRQ eth_lpi not found
[    0.652286] bm-dwmac 4070000.ethernet: Hash table entries set to unexpected value 0
[    0.660361] bm-dwmac 4070000.ethernet: no reset control found
[    0.666605] bm-dwmac 4070000.ethernet: User ID: 0x10, Synopsys ID: 0x37
[    0.673525] bm-dwmac 4070000.ethernet: 	DWMAC1000
[    0.678424] bm-dwmac 4070000.ethernet: DMA HW capability register supported
[    0.685641] bm-dwmac 4070000.ethernet: RX Checksum Offload Engine supported
[    0.692858] bm-dwmac 4070000.ethernet: COE Type 2
[    0.697743] bm-dwmac 4070000.ethernet: TX Checksum insertion supported
[    0.704512] bm-dwmac 4070000.ethernet: Normal descriptors
[    0.710115] bm-dwmac 4070000.ethernet: Ring mode enabled
[    0.715630] bm-dwmac 4070000.ethernet: Enable RX Mitigation via HW Watchdog Timer
[    0.723389] bm-dwmac 4070000.ethernet: device MAC address 96:8d:89:ed:5b:ca
[    0.758200] libphy: stmmac: probed
[    0.763554] bm-dwmac 4070000.ethernet: Cannot get clk_500m_eth!
[    0.769815] bm-dwmac 4070000.ethernet: Cannot get gate_clk_axi4!
[    0.777191] dwc2 4340000.usb: axi clk installed
[    0.781982] dwc2 4340000.usb: apb clk installed
[    0.786726] dwc2 4340000.usb: 125m clk installed
[    0.791530] dwc2 4340000.usb: 33k clk installed
[    0.796239] dwc2 4340000.usb: 12m clk installed
[    0.801042] dwc2 4340000.usb: EPs: 8, dedicated fifos, 3072 entries in SPRAM
[    0.808950] dwc2 4340000.usb: DWC OTG Controller
[    0.813824] dwc2 4340000.usb: new USB bus registered, assigned bus number 1
[    0.821096] dwc2 4340000.usb: irq 36, io mem 0x04340000
[    0.827545] hub 1-0:1.0: USB hub found
[    0.831570] hub 1-0:1.0: 1 port detected
[    0.837416] usbcore: registered new interface driver usb-storage
[    0.844106] i2c /dev entries driver
[    0.849865] sdhci: Secure Digital Host Controller Interface driver
[    0.856316] sdhci: Copyright(c) Pierre Ossman
[    0.860841] sdhci-pltfm: SDHCI platform and OF driver helper
[    0.867007] cvi:sdhci_cvi_probe
[    0.914963] mmc0: SDHCI controller on [] using ADMA 64-bit
[    0.923131] cvi_proc_init cvi_host 0x(____ptrval____)
[    0.929146] usbcore: registered new interface driver usbhid
[    0.938965] usbhid: USB HID core driver
[    0.944921] cvitek-i2s 4100000.i2s: cvi_i2s_probe
[    0.955198] cvitek-i2s 4130000.i2s: cvi_i2s_probe
[    0.960922] cviteka-adc sound_adc: cviteka_adc_probe, dev name=sound_adc
[    0.968044] cviteka-adc sound_adc: cviteka_adc_probe start devm_snd_soc_register_card
[    0.976650] cvitekaadc 300a100.adc: cvitekaadc_probe
[    0.987619] cviteka-dac sound_dac: cviteka_dac_probe, dev name=sound_dac
[    0.995091] cvitekadac 300a000.dac: cvitekadac_probe
[    1.000685] cvitekadac_probe gpio_is_valid mute_pin_l
[    1.006585] NET: Registered protocol family 17
[    1.011477] Loading compiled-in X.509 certificates
[    1.039015] mmc0: new SDHC card at address 0001
[    1.051483] mmcblk0: mmc0:0001 MSD20 14.6 GiB 
[    1.061126] cviteka-adc sound_adc: cviteka_adc_probe, dev name=sound_adc
[    1.068302] cviteka-adc sound_adc: cviteka_adc_probe start devm_snd_soc_register_card
[    1.080491]  mmcblk0: p1 p2 p3
[    1.088431] cviteka-dac sound_dac: cviteka_dac_probe, dev name=sound_dac
[    1.101557] cfg80211: Loading compiled-in X.509 certificates for regulatory database
[    1.112515] cfg80211: Loaded X.509 cert 'sforshee: 00b28ddf47aef9cea7'
[    1.119630] cfg80211: failed to load regulatory.db
[    1.124824] ALSA device list:
[    1.128383] dw-apb-uart 4140000.serial: forbid DMA for kernel console
[    1.148546] EXT4-fs (mmcblk0p2): mounted filesystem with ordered data mode. Opts: (null)
[    1.157087] VFS: Mounted root (ext4 filesystem) on device 179:2.
[    1.166212] devtmpfs: mounted
[    1.169547] Freeing unused kernel memory: 144K
[    1.174226] Kernel memory protection not selected by kernel config.
[    1.180757] Run /sbin/init as init process
[    1.185024]   with arguments:
[    1.188116]     /sbin/init
[    1.190917]   with environment:
[    1.194189]     HOME=/
[    1.196652]     TERM=linux
[    1.199477] early_time_log: run_init_process: 5681657us
[    1.307708] EXT4-fs (mmcblk0p2): re-mounted. Opts: errors=remount-ro
[    1.395862] random: fast init done
[    1.511578] random: dd: uninitialized urandom read (512 bytes read)
[    1.725888] random: dhcpcd: uninitialized urandom read (112 bytes read)
[    1.817530] bm-dwmac 4070000.ethernet eth0: PHY [stmmac-0:00] driver [Generic PHY] (irq=POLL)
[    1.839025] dwmac1000: Master AXI performs any burst length
[    1.845702] bm-dwmac 4070000.ethernet eth0: No Safety Features support found
[    1.853923] bm-dwmac 4070000.ethernet eth0: IEEE 1588-2002 Timestamp supported
[    1.862474] bm-dwmac 4070000.ethernet eth0: configuring for phy/rmii link mode
[    7.195361] random: dnsmasq: uninitialized urandom read (128 bytes read)
[    7.202528] random: dnsmasq: uninitialized urandom read (48 bytes read)
[    7.250085] cv180x_sys: bad vermagic: kernel tainted.
[    7.255425] Disabling lock debugging due to kernel taint
[    7.261336] cv180x_sys: loading out-of-tree module taints kernel.
[    7.294527] res-reg: start: 0xa0c8000, end: 0xa0c801f, virt-addr(ffffffd0040c9000).
[    7.303316] CVITEK CHIP ID = 22
[    7.318433] cvi_rtos_cmdqu_probe start ---
[    7.322806] name=1900000.rtos_cmdqu
[    7.327194] res-reg: start: 0x1900000, end: 0x1900fff, virt-addr(ffffffd004228000).
[    7.335254] cvi_rtos_cmdqu_probe DONE
[    7.339691] [cvi_spinlock_init] success
[    7.377509] cif a0c2000.cif: cam0 clk installed
[    7.382321] cif a0c2000.cif: cam1 clk installed
[    7.387481] cif a0c2000.cif: vip_sys_2 clk installed
[    7.392945] cif a0c2000.cif: clk_mipimpll clk installed (____ptrval____)
[    7.400203] cif a0c2000.cif: clk_disppll clk installed (____ptrval____)
[    7.407370] cif a0c2000.cif: clk_fpll clk installed (____ptrval____)
[    7.414298] cif a0c2000.cif: (0) res-reg: start: 0xa0c2000, end: 0xa0c3fff.
[    7.421797] cif a0c2000.cif:  virt-addr((____ptrval____))
[    7.427690] cif a0c2000.cif: (1) res-reg: start: 0xa0d0000, end: 0xa0d0fff.
[    7.435183] cif a0c2000.cif:  virt-addr((____ptrval____))
[    7.441074] cif a0c2000.cif: (2) res-reg: start: 0xa0c4000, end: 0xa0c5fff.
[    7.448559] cif a0c2000.cif:  virt-addr((____ptrval____))
[    7.454448] cif a0c2000.cif: (3) res-reg: start: 0x3001c30, end: 0x3001c5f.
[    7.461933] cif a0c2000.cif:  virt-addr((____ptrval____))
[    7.467824] cif a0c2000.cif: no pad_ctrl for cif
[    7.472974] cif a0c2000.cif: request irq-26 as cif-irq0
[    7.478759] cif a0c2000.cif: request irq-27 as cif-irq1
[    7.484514] cif a0c2000.cif: rst_pin = 424, pol = 1
[    7.499342] snsr_i2c snsr_i2c: i2c:-------hook 0
[    7.504362] snsr_i2c snsr_i2c: i2c:-------hook 1
[    7.509771] snsr_i2c snsr_i2c: i2c:-------hook 2
[    7.515073] snsr_i2c snsr_i2c: i2c:-------hook 3
[    7.519993] snsr_i2c snsr_i2c: i2c:-------hook 4
[    7.596322] vi_core_probe:203(): res-reg: start: 0xa000000, end: 0xa07ffff, virt-addr(ffffffd004400000).
[    7.606219] vi_core_probe:216(): irq(28) for isp get from platform driver.
[    7.614260] vi_tuning_buf_setup:253(): tuning fe_addr[0]=0x8183f490, be_addr[0]=0x81837290, post_addr[0]=0x81820000
[    7.625482] vi_tuning_buf_setup:253(): tuning fe_addr[1]=0x8193f490, be_addr[1]=0x81937290, post_addr[1]=0x81920000
[    7.636654] vi_tuning_buf_setup:253(): tuning fe_addr[2]=0x8185f490, be_addr[2]=0x81857290, post_addr[2]=0x81840000
[    7.647773] sync_task_init:177(): sync_task_init vi_pipe 0
[    7.653739] sync_task_init:177(): sync_task_init vi_pipe 1
[    7.659725] sync_task_init:177(): sync_task_init vi_pipe 2
[    7.666173] vi_core_probe:252(): isp registered as cvi-vi
[    7.750302] cvi_dwa_probe:487(): done with rc(0).
[    7.791388] cv180x-cooling cv180x_cooling: elems of dev-freqs=6
[    7.797661] cv180x-cooling cv180x_cooling: dev_freqs[0]: 850000000 500000000
[    7.805487] cv180x-cooling cv180x_cooling: dev_freqs[1]: 425000000 375000000
[    7.813089] cv180x-cooling cv180x_cooling: dev_freqs[2]: 425000000 300000000
[    7.820782] cv180x-cooling cv180x_cooling: Cooling device registered: cv180x_cooling
[    7.866285] jpu ctrl reg pa = 0xb030000, va = (____ptrval____), size = 256
[    7.873985] end jpu_init result = 0x0
[    8.065890] cvi_vc_drv_init result = 0x0
[    8.080795] sh (172): drop_caches: 3
[    8.261909] using random self ethernet address
[    8.261928] using random host ethernet address
[    8.309722] usb0: HOST MAC 6e:54:6b:61:a9:f5
[    8.312146] usb0: MAC ce:b2:db:da:82:7b
[    8.312220] dwc2 4340000.usb: bound driver configfs-gadget
[    8.346187] dwc2 4340000.usb: new device is high-speed
[    8.391171] dwc2 4340000.usb: new device is high-speed
[    8.682141] dwc2 4340000.usb: new device is high-speed
[    8.750359] dwc2 4340000.usb: new address 14
[   57.067015] random: crng init done

Note: usb0: HOST MAC and MAC are set randomly and change after each (re)boot.


[root@milkv-duo]~# more /proc/cpuinfo 
processor       : 0
hart            : 0
isa             : rv64imafdvcsu
mmu             : sv39


[root@milkv-duo]~# df
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/root               763327    157774    562136  22% /
devtmpfs                 14680         0     14680   0% /dev
tmpfs                    14752         0     14752   0% /dev/shm
tmpfs                    14752        52     14700   0% /tmp
tmpfs                    14752        28     14724   0% /run

[root@milkv-duo]~# df -h
Filesystem                Size      Used Available Use% Mounted on
/dev/root               745.4M    154.1M    549.0M  22% /
devtmpfs                 14.3M         0     14.3M   0% /dev
tmpfs                    14.4M         0     14.4M   0% /dev/shm
tmpfs                    14.4M     52.0K     14.4M   0% /tmp
tmpfs                    14.4M     28.0K     14.4M   0% /run


    1 root     init
    2 root     [kthreadd]
    3 root     [rcu_gp]
    4 root     [rcu_par_gp]
    5 root     [kworker/0:0-eve]
    7 root     [kworker/u2:0-ev]
    8 root     [mm_percpu_wq]
    9 root     [ksoftirqd/0]
   10 root     [rcu_preempt]
   11 root     [kdevtmpfs]
   12 root     [rcu_tasks_kthre]
   13 root     [oom_reaper]
   14 root     [writeback]
   15 root     [kcompactd0]
   24 root     [kblockd]
   25 root     [watchdogd]
   27 root     [kworker/0:1H-mm]
   28 root     [rpciod]
   29 root     [kworker/u3:0]
   30 root     [xprtiod]
   31 root     [cfg80211]
   32 root     [kswapd0]
   33 root     [nfsiod]
   34 root     [spi0]
   35 root     [spi1]
   36 root     [stmmac_wq]
   37 root     [kworker/u2:1]
   70 root     [irq/45-cviusb-o]
   71 root     [irq/46-cd-gpio-]
   72 root     [sdhci]
   73 root     [irq/23-mmc0]
   75 root     [ion_system_heap]
   76 root     [mmc_complete]
   81 root     [jbd2/mmcblk0p2-]
   82 root     [ext4-rsv-conver]
   97 root     /sbin/syslogd -n
  101 root     /sbin/klogd -n
  131 dhcpcd   dhcpcd: [master] [ip4]
  132 root     dhcpcd: [privileged actioneer]
  133 dhcpcd   dhcpcd: [network proxy]
  134 dhcpcd   dhcpcd: [control proxy]
  147 root     [kworker/0:3-eve]
  156 root     /usr/sbin/ntpd -g -p /var/run/
  165 root     /usr/sbin/dropbear -R
  170 nobody   /usr/sbin/dnsmasq
  180 root     [cvitask_isp_pre]
  181 root     [cvitask_isp_bla]
  182 root     [cvitask_isp_err]
  184 root     [cvitask_vpss_0]
  185 root     [cvitask_vpss_1]
  187 root     [gdc_work]
  192 root     [cvitask_tpu_wor]
  198 root     {S99user} /bin/sh /etc/init.d/S99user start
  199 root     [kworker/0:2H]
  211 root     -sh
 1297 root     /usr/sbin/dropbear -R
 1302 root     -sh
 1331 root     sleep 0.5
 1332 root     ps


[root@milkv-duo]~# ls /bin
arch           dmesg          linux64        nuke           sleep
ash            dnsdomainname  ln             pidof          stty
base32         dumpkmap       login          ping           su
base64         echo           ls             pipe_progress  sync
busybox        egrep          lsattr         printenv       tar
cat            false          mk_cmds        ps             touch
chattr         fdflush        mkdir          pwd            true
chgrp          fgrep          mknod          resume         umount
chmod          getopt         mktemp         rm             uname
chown          grep           more           rmdir          usleep
compile_et     gunzip         mount          run-parts      vi
cp             gzip           mountpoint     sed            watch
cpio           hostname       mt             setarch        zcat
date           kill           mv             setpriv
dd             link           netstat        setserial
df             linux32        nice           sh


[root@milkv-duo]~# ls /usr/bin/
[                  fold               od                 tee
[[                 free               openvt             telnet
ar                 fuser              passwd             test
ascii              gcore              paste              tftp
awk                gdb                patch              time
basename           gdb-add-index      pip                top
bc                 head               pip3               tr
bunzip2            hexdump            pip3.9             traceroute
bzcat              hexedit            printf             truncate
chrt               hostid             pyserial-miniterm  ts
chvt               htop               pyserial-ports     tty
cksum              id                 python             uniq
clear              install            python3            unix2dos
cmp                ipcrm              python3.9          unlink
crc32              ipcs               readlink           unlzma
crontab            killall            realpath           unlzop
cut                last               renice             unxz
cvi_pinmux         less               reset              unzip
dbclient           logger             resize             uptime
dc                 logname            scp                uudecode
deallocvt          lsof               seq                uuencode
diff               lspci              setfattr           vlock
dirname            lsscsi             setkeycodes        w
dos2unix           lsusb              setsid             wc
dropbearconvert    lzcat              sha1sum            wget
dropbearkey        lzma               sha256sum          which
du                 lzopcat            sha3sum            who
easy_install       md5sum             sha512sum          whoami
easy_install-3.9   mesg               shred              xargs
eject              microcom          xmlcatalog
env                mkfifo             sort               xmllint    mkpasswd           ssh                xmlwf
evtest             nl                 strace             xsltproc
expr               nohup              strace-log-merge   xxd
factor             nproc              strings            xz
fallocate          nslookup           svc                xzcat
find               ntpdate            svok               yes
flock              ntptime            tail

[root@milkv-duo]~# ls -1 /usr/bin/ | wc -l

Multiple Milk-V Duos / Alternative IPs

In order to support multiple Milk-V Duos on the same host via USB-C, you assign for each board its own network:

  • board 1:
  • board 2:
  • board 3:

Edit on each board two files:

/mnt/system/ (buildroot-based) or /etc/ (other systems):

ifconfig usb0



In order to add a new board, you login into as usual, and change it to the and so on.

Resizing Disk

By default the entire available space of the SD card is only 1GB (or 2GB in case you use another distro), but you can make the rest of the SD card available to /data for example – part of the guide was taken from a post in the forum but updated it:

% mkdir /data
% fdisk /dev/mmcblk0
n (new partition)
p (primary partition)
<RETURN> (confirm start selection)
<RETURN> (confirm end selection)

% reboot

and login again, continue with:

% mkfs.ext4 /dev/mmcblk0p4
% echo "/dev/mmcblk0p4 /data ext4 defaults 0 0" >> /etc/fstab
% reboot

once you login again, you see the new available space:

% df -h
Filesystem                Size      Used Available Use% Mounted on
/dev/root               745.4M    154.1M    549.0M  22% /
devtmpfs                 14.3M         0     14.3M   0% /dev
tmpfs                    14.4M         0     14.4M   0% /dev/shm
tmpfs                    14.4M     52.0K     14.4M   0% /tmp
tmpfs                    14.4M     28.0K     14.4M   0% /run
/dev/mmcblk0p4           13.4G     24.0K     12.7G   0% /data

Making Swap Space

pip won’t work by default, as there is too little memory to work – so you can make swap space in two ways:

mmcblk0p3: unused 256M partition

As of system image v1.0.4 there is an unused partition you can activate:

% mkswap /dev/mmcblk0p3
% swapon /dev/mmcblk0p3
% echo "/dev/mmcblk0p3 swap swap defaults 0 0" >> /etc/fstab


Or you can create a 256M swapfile to increase available memory, given we claimed the rest of the SD card as /data as previously shown:

% cd /data
% fallocate -l 256M swapfile
% chmod 600 swapfile
% mkswap swapfile
% swapon swapfile

and to make it permanent:

% echo "/data/swapfile swap swap defaults 0 0" >> /etc/fstab

ArchLinux Disk Image

I followed this guide to get ArchLinux working – thanks to Judehahh doing the main work – and added RNDIS support (Virtual Ethernet over USB) and made a disk image to use, the date e.g. “2023-10-09” references the riscv64 rootfs date which was unpacked as a base, the “x.xgb” describes the size of disk or rootfs and “vX.X.X” the actual release version. Unzip downloaded image first before writing on the SD card.

ssh root@
passwd: milkv

Note: these are very experimental disk images.

Milk-V Duo 256M

Disk ImageFeaturesIncluded
BuildRoot v1.0.4
– 240MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– minimum 8GB SD card
5.7GB free in rootfs
– persistent MAC addresses for RNDIS (internet routing ready)
– pacman (pkg mgr)
– ssh (dropbear v2022.83)
– python 3.11
– make
– tinycc/tcc
– lua/luac
– lighttpd

For the 256m-version disk image I downloaded the kernel from this thread and just copied and fip.bin into the first partition, otherwise no changes toward the milkv-duo disk-image (below) was made

Milk-V Duo (64MB)

Disk ImageFeaturesIncluded
BuildRoot v1.0.4
– 55MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– minimum 8GB SD card
5.7GB free in rootfs
– persistent MAC addresses for RNDIS (internet routing ready)
– pacman (pkg mgr)
– ssh (dropbear v2022.83)
– python 3.11
– make
– tinycc/tcc
– lua/luac
– lighttpd
BuildRoot v1.0.4
– 55MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– minimum 8GB SD card
5.7GB free in rootfs
– pacman (pkg mgr)
– ssh (dropbear v2022.83)
– python 3.11
– make
– tinycc/tcc
– lua/luac
– lighttpd
BuildRoot v1.0.4
– 55MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– minimum 8GB SD card
5.7GB free in rootfs
– pacman (pkg mgr)
– ssh (dropbear v2022.83)
– python 3.11
– make
– tinycc/tcc
– lua/luac
– lighttpd
BuildRoot v1.0.4
– 55MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– minimum 4GB SD card
– 960 apps in /usr/bin/
1GB free in rootfs
– pacman (pkg mgr)
– ssh (dropbear v2022.83)
– python 3.11
– make
– tinycc/tcc
– lua/luac
– lighttpd
BuildRoot v1.0.4
– 55MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– minimum 2GB SD card
– 960 apps in /usr/bin/
only 40MB free in rootfs (!!)
– pacman (pkg mgr)
– ssh (dropbear v2022.83)
– python 3.11
– make
– tinycc/tcc
– lua/luac
– lighttpd

You have a full Linux system with 55MB RAM available . . .


  • pacman -Fy 'term' fails for me (too much memory needed), instead run gzip -d -c /var/lib/pacman/sync/*.files | grep -ai 'term'

AlpineLinux Disk Image

As this discussion thread, Chaiwat Suttipongsakul did the disk image hosted on github, I made a mirror and added date of his ‘cwt’ handle to distinct it from future releases:

Milk-V Duo 256M

DISK IMAGEFeaturesIncluded
md5: 057d0304d958de7b7323d6d963d801e2
– package management (apk)
– rndis / usb network (but random MAC addresses)
– rootfs 1GB, 150MB used
– dropbear v2022.83

– use apk [add|update] –no-check-certificate

For the 256m-version disk image I downloaded the kernel from this thread and just copied and fip.bin into the first partition, otherwise no changes toward the milkv-duo disk-image (below) was made.

Milk-V Duo (64MB)

Disk ImageFeaturesincluded
md5: dbc23ca7b372ce7718924067ac846693
– package management (apk)
– rndis / usb network (but random MAC addresses)
– rootfs 1GB, 150MB used
– dropbear v2022.83

– use apk [add|update] –no-check-certificate


  • use apk [add|update] --no-check-certificate to install new packages, otherwise installs or update fail

Ubuntu Disk Image

I followed this guide (see discussion thread as well) – thanks to Bassusteur – and added RNDIS related services so you can login with ssh root@ (passwd milkv) via virtual Ethernet over USB; unzip disk image before you write on the SD card.


  • apt / apt-get are awfully slow on Milk-V Duo at step “Building dependency tree...“, takes 4+mins for each apt install call as apt requires 50+MB RAM to build that dependency tree, which goes hard on all available RAM + swap
  • these are very experimental disk images

Note: milkv-duo (64MB RAM) vs milkv-duo-256m (256MB RAM)

Milk-V Duo 256M

Disk ImageFeaturesIncluded
md5: 0a80c70d9a6b2e763f49b7dca7aba59f
BuildRoot v1.0.5
– 240MB RAM (no camera support)
– RNDIS (connect via usb virtual ether)
– persistent MAC addresses for RNDIS (internet routing ready)
– apt works now smoothly
– minimum 8GB SD card
6.0GB free in rootfs
– dropbear (v2020.81)
– python 3.10.12

For the 256m-version disk image I downloaded the kernel from this thread and just copied and fip.bin into the first partition, otherwise no changes toward the milkv-duo disk-image (below) was made.

Milk-V Duo (64MB)

Disk ImageFeaturesIncluded
md5: 21833b5041e4f6a1706e910c491ba2d3
BuildRoot v1.0.5
– 55MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– persistent MAC addresses for RNDIS (internet routing ready)
– minimum 8GB SD card
6.0GB free in rootfs
– dropbear (v2020.81)
– python 3.10.12
– zram enabled in kernel, and works now, but apt still very slow
md5: 9f8d3ab61a7ea328b8da5217f020c2b2
BuildRoot v1.0.5
– 55MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– persistent MAC addresses for RNDIS (internet routing ready)
– minimum 8GB SD card
6.0GB free in rootfs
– dropbear (v2020.81)
– python 3.10.12
– zram not enabled in kernel yet
md5: bb05d70a0e81169ef2af05b68ef82ced
BuildRoot v1.0.4
– 55MB RAM (no camera support)
– 250MB swap enabled
– RNDIS (connect via usb virtual ether)
– persistent MAC addresses for RNDIS (internet routing ready)
– minimum 8GB SD card
6.0GB free in rootfs
– dropbear (v2020.81)
– python 3.10.12

Changes done compared to this guide:

  • removed dhcpcd as is clashes with dnsmasq
  • added RNDIS related scripts, incl. dnsmasq and rndis.service
  • disabled systemd-resolved service due clash with dnsmasq
  • added /etc/resolv.conf.tail with default DNS servers
  • installed dropbear (lightweight sshd), removed the auto generated keys, added in /etc/default/dropbear the -R switch so new keys are generated at first boot

Tips & Examples

Most of the examples relate to the Duo BuildRoot SDK setup, but should be easily adaptable to ArchLinux or other distros. with sysfs GPIO

There is a way to control GPIO via sysfs with Python:

Note: you require more memory to run pip, use guide Make Swap Space (previous section) then proceed:

% cd gpio-1.0.0
% pip install .
% chmod +r /sys/class/gpio/export

then use this script

import time

import gpio as GPIO

pin = 440
GPIO.setup(pin, GPIO.OUT)

while True:
    GPIO.output(pin, GPIO.HIGH)
    GPIO.output(pin, GPIO.LOW)

and run it:

% python

See this table for GPIO names, pins and numbers, a copy (2023/10/10):



Follow the guide to install (my mirror), only for V1.0.4 system image, and then:

import time
from pinpong.board import Board,Pin


led = Pin(Pin.D0, Pin.OUT)

while True:

The pinpong library covers quite a lot of functionality, and useful examples:

root@milkv-duo2]~# ls /usr/lib/python3.9/site-packages/pinpong/examples/milkv-Duo/                oled2864.pyc
__init__.pyc                                  i2c.pyc                   ozone.pyc
adc.pyc                               i2c_scan.pyc              paj7620.pyc
as7341.pyc                        iic_to_serial.pyc         ph1.pyc
blink.pyc                              ir_recv.pyc               ph2.pyc
bme280.pyc                             ir_send.pyc               pwm.pyc
bme680.pyc                             irq.pyc                   rgb_panel.pyc
bmi160_acc.pyc                    lcd1602.pyc               sen0483.pyc
bmi160_step.pyc                         lis2dh.pyc                servo.pyc
bmp280.pyc                            max30103.pyc              sht31.pyc
bmp388.pyc                       mics_enable_power.pyc     speech_synthesis.pyc
button.pyc                       mics_get_adc_data.pyc     spi.pyc
buzzer.pyc         mics_get_gas_exist.pyc    sr04_urm10.pyc
ccs811_read_baseline.pyc       mics_get_gas_ppm.pyc
ccs811_read_data.pyc               st7789.pyc                    mlx90614.pyc    
dht.pyc                             tcs34725.pyc                  mp3.pyc         
dht20.pyc                      tds.pyc                 neopixel.pyc    
ds0469.pyc                          tone.pyc                 nfc.pyc         
ds1307.pyc                uart.pyc                nfc_card_info.pyc
ds18b20.pyc                    urm09.pyc                 nfc_uart.pyc    
ens160.pyc                vl53l0.pyc           nfc_uart_card.pyc

I will explore those examples.

Start Script at Boot

As of V1.0.4 system image, the /etc/init.d/S99user executes /mnt/data/ if it exists:

% mkdir /mnt/data
% vi /mnt/data/

and list each of the processes:

/path/to/my/ &
python /path/to/another/ &


According this post, tinycc has been ported as well – C compiler and C interpreter in one – download the .zip (my mirror) and run its, and then fix missing executable bit:

% chmod +x /usr/local/bin/tcc

and start playing with test.c:

#!/usr/local/bin/tcc -run

#include <stdio.h>

int main(int argc, char **argv) {
    printf("%s---\n", "hello milk-v!");
    return 0;
% tcc -o test test.c
% ./test
hello milk-v!---
% chmod +x test.c
% ./test.c
hello milk-v!---

Thanks to Yang who ported and provided the download.


I followed the example, and compiled the sources on the board itself with tcc:

tcc -r ssd1306.c -I .
tcc -r linux_i2c.c -I .
tcc -o ssd1306 main.c linux_i2c.o ssd1306.o -I .
./ssd1306 -I 128x64
./ssd1306 -c
./ssd1306 -m "Hello world!\nMilk-V Duo"
OLED SSD1306 connected to Milk-V Duo via I2C, controlled via RISC-V binary ssd1306 from ssd1306_linux

Software State

On-BoardBuildRoot v1.0.4-2023-0908BUILDROOT V1.0.4-2023-1017-spiritdudeArchLinux 2023-10-09 v0.0.1 spiritdude
available memory (RAM)48MB55MB55MB
ssh serverok (dropbear v2020.81)ok (dropbear v2020.81)ok (dropbear v2022.83)
ssh clientok (dropbear v2020.81)ok (dropbear v2020.81)ok (dropbear v2022.83)
pythonok (python 3.9.5)ok (python 3.9.5)ok (python 3.11)
py web-server (with socket)okokok
py gpio (with pinpong or gpio)okok
py spi (with built-in spidev)available, not yet testedavailable, not yet tested
py i2c (with pinpong)available, not yet tested with pinpong
ok with .c (see above section)
available, not yet tested with pinpong
ok with .c (see above section)
py pwm (with pinpong)not found, /sys/class/pwm/* is emptynot found, /sys/class/pwm/* is empty
pipfailed (hangs), enable swap space to use itbarely works, enable swap spaceworks (swap space enabled)
cc/gcc/clangnot foundnot includednot included
tinycc/tccok, see this post how to installok, see this post how to installok
rsyncnot foundincludedincluded
wgetok, but no https (only http, ftp)ok, but no https (only http, ftp)not included, installable
lua/luaclua (5.4.6): segmentation fault, luac (5.4.6): seems to work (both compiled with tinycc) lua & luac (5.3.6) workslua & luac (5.4.6) works
extrasquickjs/qjs, micropython, nano, screen, git, make (no gcc/cc, use tinycc), thttpd, nginx, lighttpd, php-cgi, file, which, sudopacman (package mgr), lighttpd, file, which, sudo, make

Internet Access for Milk-V Duo

RNDIS (Virtual Ethernet over USB)

The host has to run Ethernet over USB and also operate as transparent router and let the connected board(s) reach the internet, see this guide (use google translate to english), here the brief description:

On The Host

The outgoing_if is the outgoing interface, either eth0 or wpl0s0 or something, check with ifconfig of the proper name, and then as root perform:

% sysctl net.ipv4.ip_forward=1
% iptables -P FORWARD ACCEPT
% iptables -t nat -A POSTROUTING -o outgoing_if -j MASQUERADE

Also, find out which IP your host got (ip_of_host) from the connected board, e.g., also check with ifconfig.

On The Board

% ip r add default via ip_of_host
% echo "nameserver" >> /etc/resolv.conf

Static IP for Host with RNDIS

What may look simple actually isn’t that easy as RNDIS itself is the culprit:

  • one can limit the IP range in dnsmasq.conf to from/to be the same IP, but
  • as RNDIS assigns random MAC addresses to the RNDIS host (the board) and the RNDIS client (the host the board is connected to) treat it as new device at every boot – if you force it to have the same IP again, the host will not get a new IP via DHCP as it has the IP remembered for another MAC address . . .
  • it’s a mess

So, a working solution is:

/etc/ which generates two random MAC addresses but keeps them persistent then:



generate_random_mac() {
    printf "02:%02x:%02x:%02x:%02x:%02x\n" $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256))

# check if the MAC address file exists and has exactly two lines
if [[ -f "$MAC_FILE" ]] && [[ $(wc -l < "$MAC_FILE") -eq 2 ]]; then
    # read the two MAC addresses from the file
    IFS=$'\n' read -d '' -r -a macs < "$MAC_FILE"
    echo "using existing MAC addresses:"
    # generate two new MAC addresses and store them in the file
    echo "generating new MAC addresses:"
    echo "$dev" > "$MAC_FILE"
    echo "$host" >> "$MAC_FILE"
echo "dev_addr: $dev"
echo "host_addr: $host"
echo "$dev" > "$RNDIS_USB"/dev_addr
echo "$host" > "$RNDIS_USB"/host_addr


/etc/ device >> /tmp/rndis.log 2>&1
/etc/ probe rndis >> /tmp/rndis.log 2>&1
/etc/ >> /tmp/rndis.log 2>&1
/etc/ start rndis >> /tmp/rndis.log 2>&1
sleep 0.5
ip link set dev usb0 up
ip a add dev usb0
ip r add default via
sleep 0.5
systemctl start dnsmasq



Note: if you had once assigned to the host, choose another “static” IP in your range, as your host has remembered the IP to a particular MAC address and won’t accept it again, e.g. or so.

As of milkv-duo-archlinux-*-v0.0.3-spiritdude.img the above solution is included, see list of ArchLinux disk images.

Custom BuildRoot Milk-V Duo Disk Image

Here my brief guide – see also this guide (use google translate) – how to customize packages included in the base distribution of the image for the SD card:

% cd duo-buildroot-sdk/buildroot-2021.05
% make menuconfig

then go into the “Target packages”, and then walk through:

  • Audio and video applications
  • Compressors and decompressors
  • Debugging, profiling and benchmark
  • Development tools
  • Filesystem and flash utilities
  • Fonts, cursors, icons, sounds and themes
  • Games
  • Graphic libraries and applications (graphic/text)
  • Hardware handling
  • Interpreter languages and scripting
  • Libraries
  • Mail
  • Miscellaneous
  • Networking applications
  • Package managers
  • Real-Time
  • Security
  • Shell and utilities
  • System tools
  • Text editors and viewers

once you selected the packages you like to have included, choose “Save” and confirm as ‘.config’ and then “Exit”.

% cp .config configs/milkv_duo_musl_riscv64_defconfig
% cd ..
% ./

and after while, depending on how many packages you selected, you find in out/ folder your new disk image you can copy on the SD card.

Note: buildroot is quite a quirky package, e.g. when you select a package and make a build, later deselect a package, it will still be included – worse, if you commit a clean slate in output/, some packages might not fully build anymore – you have to go back to an earlier state of fewer packages, remake the build, and restart re-selecting new packages.

Postfixing missing .so file

As of 2023/10 v1.0.4 buildroot-2021.05 environment, there seems a problem regarding a missing shared library for some of the compiled apps (like qjs), you can fix this:

% cd /lib
% ln -s

My BuildRoot Custom Disk Images

disk ImageNotes
– lua, quickjs/qjs, micropython, nano, screen, git, make (no gcc/cc, use tinycc), thttpd2), nginx, lighttpd, php-cgi
– 55MB RAM1) available
– “fixed” image has .so lib-fix included
milkv-duo-v1.0.4-20231016-spiritdude-64mb_ram-nocam.img– lua, quickjs/qjs, micropython, nano, screen, git, make (no gcc/cc, use tinycc), lighttpd (doesn’t work yet out of the box)
– 55MB RAM1) available
  1. apprx. 55MB actual available RAM, no camera support, INO_SIZE=0
  2. thttpd, nginx and lighttpd are all http-server, only use one, see /etc/init.d/ where those are started.

Ethernet Add-On

The easier way is to get proper networking is to add a real ethernet port and properly wire it to the ethernet router. There are several options & sources (2023/09):

Printable Case

I did a small case for the bare board without Ethernet or Extension board to be 3D printed:


As the Milk-V Duo has the same width and depth (X/Y) as the Raspberry Pico but it’s a bit thicker so the some of the existing cases work only partially:

As soon my Ethernet connectors and Extension boards arrived I will provide case variants (2023/10/05).


IoT: Wemos S2-Mini (ESP32-S2)


  • 2023/10/04: finally published, added CircuitPython which should work better than MicroPython
  • 2023/09/25: retesting WiFi, still fails with latest firmware
  • 2023/02/03: starting writeup


Wemos S2-Mini is a ESP32-S2FN4R2 chip on a small module, supporting to run MicroPython and connecting to existing WiFi or operating as WiFi Access Point – it’s ideal as IoT device, or easy add-on to 3D printing infrastructure to probe sensors and make them WiFi compatible.


  • ESP32-S2FN4R2 (WIFI IC)
  • Type-C USB
  • 4MB Flash
  • 27x IO
  • Compatible with LOLIN D1 mini shields
  • Compatible with MicroPython, Arduino, CircuitPython and ESP-IDF

At the price of barely EUR 2.00 on Aliexpress (2023/02) this is a very affordable IoT device.

Note: MicroPython on ESP32-S2 seem not able to connect to an WiFi AP, and also not operate as WiFi AP itself. Before you follow any steps below, read it first through entirely and decide whether MicroPython or CircuitPython is more suitable – for me both approaches fail to provide WiFi connectivity.

Installing MicroPython (no WiFi)

My modules came without MicroPython unlike advertised, I had to upload the latest MicroPython firmware myself:

  • download latest MicroPython firmware (near bottom of the page)
  • attach module with USB-C cable to the computer (e.g. running Linux as in my case)
  • press Button “0” and hold down, press Button “RST” briefly, wait 2 secs, release Button “0”
  • use ./ --port /dev/ttyACM0 --baud 1000000 write_flash -z 0x1000 firmware-LOLIN_S2_MINI-v1.19.1-669-gd4b9df176.bin

Adadfruit ampy

In order to use the modules, one requires ampy which is installed via

% pip3 install adafruit-ampy

and provides ampy command line interface:

Usage: ampy [OPTIONS] COMMAND [ARGS]...

  ampy - Adafruit MicroPython Tool

  Ampy is a tool to control MicroPython boards over a serial connection.
  Using ampy you can manipulate files on the board's internal filesystem and
  even run scripts.

  -p, --port PORT    Name of serial port for connected board.  Can optionally
                     specify with AMPY_PORT environment variable.  [required]
  -b, --baud BAUD    Baud rate for the serial connection (default 115200).
                     Can optionally specify with AMPY_BAUD environment
  -d, --delay DELAY  Delay in seconds before entering RAW MODE (default 0).
                     Can optionally specify with AMPY_DELAY environment
  --version          Show the version and exit.
  --help             Show this message and exit.

  get    Retrieve a file from the board.
  ls     List contents of a directory on the board.
  mkdir  Create a directory on the board.
  put    Put a file or folder and its contents on the board.
  reset  Perform soft reset/reboot of the board.
  rm     Remove a file from the board.
  rmdir  Forcefully remove a folder and all its children from the board.
  run    Run a script and print its output.

so you can put (upload), run, delete files – essentially you have a small filesystem where files reside, to run/execute or read from or write to.

Booting Module

By default / is executed, where some default behavior can be defined.

No WiFi (Yet)

As first I uploaded and (see, along with default wifi.dat:


which contains the SSID / Password of the WiFi AP I wanted the module to connect to. All 3 files I uploaded ampy -p /dev/ttyACM0 put and so on, one can only upload one file at a time with ampy.

Finally one can ampy -p /dev/ttyACM0 run

As of 2023/09 with MicroPython V1.20.0 I was not able connect the module to an existing WiFi neither have it operate as Access Point (AP) itself – both cases failed; quite disappointing.

FirmwareBootREPLWiFi ClientAP
LOLIN_S2_MINI-v1.19.1-669okokfailed failed

Installing CircuitPython (no WiFi)

Alternatively you can install CircuitPython, for such you

Install once Bootloader

  • unzip bootloader

and press button “0” and hold, press button “RST” briefly, and release button “0” again, then run this code (Linux):

% python3 ../esptool/ --chip esp32s2 --port /dev/ttyACM0 --baud 1000000 write_flash -z 0x0 tinyuf2/combined.bin

then press button “RST” again, after few seconds the board appears like a USB disk, and then you copy the adafruit-circuitpython-....bin into the USB disk – once done, it does reboot itself again (or press button “RST” again).

It will again appear as USB disk named “CIRCUITPY”, that means CircuitPython has been successfully installed and is active running.

Running Code on CircuitPython

Well, you create on that USB disk a new file called which is executed as default when its content changes – so you can edit it directly. Any other files which that needs resides in the same root folder.

The lib/ folder contains all libraries, which you can copy from CircuitPython Libraries, you download a .zip file with all libraries and unzip it and then individually copy libraries you need into the lib/ which become then active right away.

The console is viewable if you start

% tio /dev/ttyACM0

I tried to run a web-server example, but it failed with ConnectionError: Unknown failure 2 – so neither MicroPython or CircuitPython are providing working WiFi connectivity.

No WiFi Yet

I’m quite surprised to see my Wemis S2-Mini fail to connect to my WiFi network, regardless of MicroPython or CircuitPython – it seems to me the libraries involved as faulty for both approaches, I cannot believe that this is a hardware problem, but is a software problem.


Misc Hardware Notes

These are just links to hardware I frequently use, and wrote my own documentation for:

Printer Controllers

  • Duet 3 Mini 5+ (2023: EUR 150-200), 1x in use and very satisfied with it
  • MKS Monster8 (2022: EUR 50), 2x in use
  • Mellow Fly Super8 V1.2 (2022: EUR 80), Duet clone running RRF, 1x in use

Single Board Computer (SBC)

  • Orange Pi Zero: I use a couple of them as Print3r/Prynt3r printhub: I connect multiple USB-based printers, and used Print3r/Prynt3r network-based remote printing capabilities

Embedded Single Board Computer (eSBC)

  • Milk-V Duo (EUR 5.00-9.50) Ethernet over USB, Linux, Python


  • ESP8266 (EUR 2.50-3.00), Wifi, Lua, MicroPython
  • ESP32 (EUR 3.50-5.00), Wifi, RTOS, Lua, MicroPython
  • Wemos S2-Mini / ESP32-S2 (EUR 3.00), Wifi, RTOS, MicroPython

MSLA Anycubic Photon Mono X2


  • 2023/07/25: published
  • 2023/06/13: adding my print settings to increase reliability
  • 2023/03/31: first prints performed
  • 2023/03/29: adding Formlabs 3/3L and Prusa SL1S as comparison
  • 2023/03/23: starting write-up


After my first steps with Anycubic Photon Mono 4K, an entry-level MSLA I also got a slightly larger Anycubic Photon Mono X2:

  • build volume: 196 x 122 x 200 mm (WxDxH)
  • resolution: XY 48μm, Z 50μm
  • 9.1″ display with 4K+ resolution (4096×2560) display
  • monochromatic LCD (hence “Mono”), faster printer due shorter exposure
  • affordable with EUR 300-350 (2023/03)
  • no network, only USB drive printing

I ordered at directly (EUR 350) 2023/03/24, and got it a week later.

Default Settings

Firmware V0.2.3
My Settings
My Settings
Bottom Layers651 .. 6 2)
Exposure Off [s]1.0
Bottom Exposure [s]
Normal Exposure [s]
Transition Layers41010
Bottom Region
Bottom Layer [0] Rising Height [mm]3.04.0
Bottom Layer [0] Rising Speed [mm/s]1.0
Bottom Layer [0] Retract Speed [mm/s]1.0
Bottom Layer [1] Rising Height [mm]4.04.0 1)
Bottom Layer [1] Rising Speed [mm/s]
Bottom Layer [1] Retract Speed [mm/s]3.0
Normal Region
Normal Layer [0] Rising Height [mm]3.04.0
Normal Layer [0] Rising Speed [mm/s]1.0
Normal Layer [0] Retract Speed [mm/s]1.0
Normal Layer [1] Rising Height [mm]4.04.0 1)
Normal Layer [1] Rising Speed [mm/s]
Normal Layer [1] Retract Speed [mm/s]3.0
UV Light (not available)
  1. in Lychee Slicer the 2nd lift distance is the sum of [0] Rising Height 4.0mm + [1] Rising Height 4mm = “4.0mm > 8.0mm”:
  2. depending on the size of the pieces, if they are small (e.g. 20x20mm foot print) then I use 1 bottom layer, for larger pieces I increase up to 6 bottom layers

After some prints I realized the 3mm/s or 180mm/min 2nd rising speed really decreased the reliability of prints I did, usually after bottom layers some models, especially fine structures, deliminated and I realized the transitioning needed to be longer and also reduce the 2nd rising speed, the retraction speed could stay as is. The slowed down the overall print time, but reliability is getting near to Mono 4K.

Various Resins

from Anycubic Web-Site (2023/04/02)

Mono 4K vs Mono X2 vs Mono M3 Max

The print area Mono 4K doubles nearly with Mono X2, and the Mono M3 Max (EUR 1000) additionally doubles the print area:

As pointed out previously, the print area with MSLA directly affects the print speed, the larger the build plate, the faster the print: more parts can be printed at the same time.

The Mono X2 is relatively new as of writing this blog-post, just released in late 2022, so the third party market has not yet taken off regarding spare vats, spring steel magnetic build plate, protective films and alike.

Update 2023/07: The Mono X2 already discontinued as M5 and M5S has been released.

Photon Mono X2 vs Creality LD-006: Details Matter

The main reason I prefered the Photon Mono X2 (EUR 350) over the Creality LD-006 (EUR 250) with the nearly same build-volume is that the LD-006 has a bad build-plate mount where resin is trapped near the screws – which makes cleaning and switching resins much harder:

Eventually either cured, dried or semi liquid resin trapped in there will contaminate your next prints – so a bad build plate design can ruin your prints eventually.

Slicer Support

Lychee Slicer 5.x and upward also supports the Mono X2 out of the box, whereas Prusa Slicer SLA slicer, as of 2.x series, does not slice to any but .sl1, and so requires a custom setting to add support for it indirectly then requires a tool like UVtools to convert to .pmx2 (Note: as of 2023/03 the output format is not working properly).

The native Photon Workshop slicer coming with the printer has no Linux support and only works via Wine “Windows” emulator, and is barely usable.

Printing 40x Triply Periodic Minimal Surfaces 20mm cubes samples at once, and it took 1h45m (with default settings) with apprx. 30mm height, as some required support.

The pieces were overexposed, the bottom layers were significantly overexposed, the clear resin as well the white resin which came as 2nd batch – the defaults as recommended by Anycubic and consequently by Lychee Slicer 5.1.8 were not as good as Photon Mono 4K.

The default settings with 3mm/s or 180mm/min for 2nd lift speed caused multiple failed prints for me such as delimination, and I reduced it to 1.5mm/s or 90mm/min and increased transitioning layers from 4 to 10 – after that fine structures like those 40mm cube lattice prints came out quite well.

Photon Mono X2 Drip Hook

I highly recommend a drip hook, you want uncured resin easily drip over a period of 5-10min back into the vat, before you wash it off in water (when using water wash resin) or isopropyl alcohol (IPA).

Photon Mono X2 Drip Hook

Third Party Market

Photon Mono X2 compatibilitySpecifications
vatPhoton Mono X, Mono X 6K, Mono M3 Plus263 x 169 x 30 mm (outside max)
238 x 162 mm (inside)
steel build plateElegoo Saturn S (same LCD size)202 x 129 mm (min. 196 x 122mm)

Spring Steel Plate

I made two prints with the original build plate, and it was difficult to remove the prints without flexing and scratched the plate right away – the adhesion is good, too good actually. So it became clear, I also want a spring steel plate for it.

I’ve got a spring steel plate 202×128 mm from Aliexpress.

  • magnetic base: 2.2mm thick, slightly extends, 203x129mm
  • steel plate: 0.5mm thick, exact 202x128mm

so one has to compensate 2.5 – 3mm in Z height and adjust, speak extend, the Z-level probe. I just glued a black strip of paper, and then of course re-level the bed again afterwards.


The motherboard has two main chips:

  • ARM GD32F407
    • driving Z stepper motor with endstop (Z=0)
    • driving USB port
    • driving both LCDs via ASIC
  • EF2L 45L G1444B (ASIC)
    • driving monochrome LCD via MIPI DSI (not sure where the image buffer resides)
    • driving UI color LCD with touchscreen
Mono X2 motherboard

Preliminary Review Mono X2


  • good value for build size, but not features
  • mid-size build volume with 196 x 122 x 200 mm (WxDxH)
  • no cover sensor (can be on or off and still prints)
  • available replacement of parts (LCD, vat, build-plate)


  • no Wi-Fi (only USB Stick)
  • surprisingly noisy with fans while printing
  • firmware doesn’t allow to change UV light intensity
  • default settings are not giving reliable prints1)
  • very simple firmware & UI
  • no flex build plate
  • no upper endstop (you can ramm the build-plate into the plate on top)
  • no sensor for running plate against cured remains in vat2), that would require more advanced firmware
  • cheap USB memory stick (breaks after a few days, data corruption)
  1. using original Anycubic Water Washable Resin +
  2. newer printers (2023) have load cell to measure counter force, and prevent build-plate to run with force against vat and LCD underneath. Update 2023/05: Anycubic released Mono M5S (2023/05) which has a load-cell sensor, but reviews are mixed.
Photon #4 (Mono X2) & Photon #1 & #2 (Mono 4K): additional handle on top, and labels on cover and body


Printing Procedure

Let me take the chance to describe my MSLA resin printing procedure – if you are newcomer you might like to read it in a concise way.

  • resin printing (as in 2023) is still a messy procedure at the consumer and prosumer level as well
  • the moment the print is finished, you need to remove the build-plate or flex steel plate, no matter which, the piece(s) still have resin on it, which likely will drip on the printer and especially around the vat is the most critical area:
    • by any means prevent any resin to come under the vat
    • don’t touch the underside of the vat with hands or gloves, unless you have a fresh piece of sheet of paper towel or equivalent
    • if you must clean the underside of the vat – and if you do you already screwed up when this happens – clean it first thoroughly, and after that the side of the vat and other part of the printer
    • prevent any resin to get on the LCD or protective layers on the LCD, wipe it away right away
    • cured resin is difficult to remove, especially from softer surfaces like plastic – so I have the habit to remove all resin outside of the vat right away
  • clean your gloves with pieces of paper towel, to prevent to further spoil your equipment you grab as next:
    • clean gloves when handling the build plate
    • I keep the same gloves for multiply prints or a 1-2 weeks as I clean them with paper towels right away (2-3 times per print: whenever I touch the 3d printer, the gloves are clean)
  • when removing the pieces from the build plate, things will become messy, no matter what
  • washing the piece(s) off in a water container as I use only water wash resin
    • 1st wash container: I dip the pieces with the build plate into the container, my gloves don’t get wet or touch resin
    • 2nd wash container: I remove the flex build plate, and dip it with the pieces still on the plate into the 2nd container – dipping in and out several times
  • then I remove the pieces from the flex build plate, by flexing the build plate or use a metal scrapper, and they drop into a small container with layers of paper towels where they dry a bit (10-30min) – I don’t touch the pieces with my gloves, I only touch the build plate
    • cleaning the build plate with paper towels dry, mount it back on the printer
    • cleaning the flex build plate with paper towels and attach it to the build plate
  • no cured remains in vat: using the plastic scrapper to push on the vat’s bottom softly, and move through the vat slowly to see if there are remains attached to the vat bottom, those needs to be removed otherwise at next print the build plate will push it unto the LCD and potentially damage it – high risk to damage the MSLA printer severely, such as puncture FEP of the vat and leak resin all over
    • if there are cured remains on the bottom FEP which I can’t remove with the plastic scrapper (large enough to pick out), then
      • either I expose the entire plate for 10-20s, and remove then a full cured layer of the entire area, this removes all small remains
      • if there are small floating pieces, then I empty the vat off the resin by pouring through a metal or paper filter (keeps back remove all cured remains) back into the bottle – this is time consuming and messy as you need to clean funnel and metal filter again
  • if I have printed pieces with small holes where water might still be captured, I take a piece of paper towel and wrap it and shake it fast in my hands – I have seen people blowing compressed air on the cured pieces, this makes sense if you produce many pieces in series frequently
  • 2nd stage curing of the pieces in UV chamber:
    • white/black/grey resin 1-3 mins
    • clear resin 2 mins max
  • further resting the piece(s) for 1-2 hours to dry further

And I leave the resin in the vat, and let the vat stay on the printer for days – no refilling back and forth to and from bottles, unless I know I have cured fragments in the vat, then I empty the vat with metal or paper filter back into the bottle and clean it with paper towels thoroughly as mentioned above already.

Keep everything around MSLA printer as clean as possible.

Consumer MSLA vs Professional SLA

When trying to compare consumer MSLA and professional SLA like from Formlabs you might just look at the print size or build volume:

but also look at other aspects:

Photon MonoPrusa SL1SFOrmlabs Form 3, 3L
PricingEUR 200-1,000EUR 2,000EUR 5,000-10,000
TechnologyMSLAMSLALow Force SLA
Exposure TechnologyUV light with monochrome LCD (4K)UV light & monochrome LCD (2K)UV laser
Automaticsresin levelresin level
resin dispenser
resin heating
level indication
Next Layer Methodpeeling with 2 stage raisingpeeling by tiltingpeeling & wiping
Removal of Partmessymessy without CW1
OK with CW1
messy without Form Wash
OK with Form Wash3)
Cleaning Partmessymessy without CW1
OK with CW1
messy without Form Wash
OK with Form Wash3)
Post Curing Partrequiredrequiredoptional
Slicing Software★★☆☆☆ Photon Workshop
★★★★☆ Lychee Slicer
★★★★☆ Prusa Slicer★★★★★ PreForm
Print Speed★★★★☆
depends only on print height
depends only on print height
depends on layer cross section &
print height
Hardware Maintainability★★★☆☆★★★★☆★★★☆☆
Original Online Resources★☆☆☆☆★★★★★★★★★★
Community Resources★★★★☆★★★★☆★★★☆☆
After Sales Support★★☆☆☆★★★★☆★★★★☆
Third Party Market★★★☆☆★★☆☆☆☆☆☆☆☆
  1. based on overall build and firmware, which can’t even properly calculate remaining print time while printing, I have a low expectation
  2. value for all is the same, because the low price for Anycubic Photon Mono series also give you moderately good solution, whereas Formlabs printers are very pricey but also giving you good value, and Prusa SL1S rather high priced for the gaining reliability and print speed
  3. Form Wash station for Form 3, Form Wash L for Form 3L, you move the build plate direct into the washing station, and before curing remove them from the build plate
  4. Prusa SL1S prints a layer in 2 seconds in total, whereas traditional MSLA takes 7-10s (exposure time + lifting/retraction)

As a follow-up, while reviewing another resin printer, I realized the “value” for me I could actually determined by the XY area which translate into parallel printing capability, which is more relevant than the height (Z), combined with the precision or voxel size – as of 2023 Anycubic doesn’t even mention the XY resolution anymore – a bad marketing move.

(M)SLA Value Comparison

A numeric value summarization for features I care about, the rough & simple formula:

XY Area [mm2] / Price [EUR] / XY Resolution [μm]

The bigger the XY area, the smaller the price and the smaller the resolution, the higher the value:

State 2024/07

Elegoo Saturn 42024-19μm x 24μm2)MSLA218x122x220300 EUR3.74
Elegoo Saturn 4 Ultra2024-19μm x 24μm2)MSLA218x122x220400 EUR2.81
Anycubic Photon Mono M52023-19μm x 24μm2)MSLA218x123x200410 EUR2.72
Elegoo Mars 5 Ultra2024-18μmMSLA153x78x165270 EUR2.54
Anycubic Photon Mono X 6Ks2023-34μmMSLA196x122x200280 EUR2.51
Elegoo Saturn 3 Ultra2023-19μm x 24μm2)MSLA218x123x260480 EUR2.32
Anycubic Photon Mono 4K2021-202235μmMSLA132x80x165150 EUR2.01
EMake Galaxy 1202325μm / 100μm1)SLA400x200x4001,600 EUR2.00
Anycubic Photon Mono X22023-48μmMSLA196x122x200260 EUR1.91
Anycubic Photon Mono 22023-35μmMSLA143x89x165210 EUR1.73
Elegoo Jupiter SE2023-51μmMSLA277x156x300700 EUR1.21
Anycubic Photon M3 Max2022-45μmMSLA298x164x300980 EUR1.10
Formlabs 3L2020-25μm / 100μm1)SLA335x200x3009,000 EUR0.29
Formlabs 3+2020-25μm / 100μm1)SLA146x145x1853,500 EUR0.24
Prusa SL1S2021-49μmMSLA127x80x1801,700 EUR0.12
Formlabs 42024-50μmMSLA200x125x2105,175 EUR0.09
  1. Formlabs 3’s & EMake SLA printers use a laser beam which has 100μm in diameter, but it can be positioned 25μm exact, the latter was used to calculate the value
  2. The UV pixels are non-square, the longer side was used to calculate the value

The XY area in SLA scales not as good as with MSLA, as the laser beam takes longer the more XY area (e.g. more pieces) need to be rendered per layer – so, MSLA is recommended for aiming fast parallel printing. Interestingly Formlabs 4 is now a MSLA as well (2024/04), they seem to have abandoned the SLA laser-based approach.

That’s it.


MSLA Anycubic Photon Mono 4K


  • 2023/07/25: moved content from blog into regular pages
  • 2023/03/15: adding more Settings for different resins, different default for different firmware version, added preliminary Review
  • 2023/03/09: finally published
  • 2023/03/05: first prints made, more info on Lychee Slicer, Prusa Slicer SLA, more on magnetic steel plate use, prints with or without support, closeup photos added
  • 2022/11/29: added Drip Hook, still preparing the utilities to make first print
  • 2022/11/26: Mono 4K arrived, preparing software pipeline (slicer, converters) and working place
  • 2022/11/07: starting write-up


The past years I focused on Filament Deposition Manufacturing (FDM) / extrusion based 3D printing, and the time came to focus on MSLA resin based process as well.

My main use case a small pieces, like custom pulleys and idlers – precise parts, such as:

Anycubic Photon Mono 4K

The Anycubic Photon Mono 4K seemed like a good choice to start with:

  • build volume: 132 x 80 x 165 mm (WxDxH)
  • resolution: XY 35μm, Z 50μm
  • 6.1″ display with 4K resolution (3840×2400) display
  • monochromatic LCD (hence “Mono”), faster printer due shorter exposure
  • affordable with EUR 170-220 (2022/11)
  • no network, only USB drive printing
Stereolithography Apparatus
– one beam/point light source
Mask Stereolithography Apparatus
– one light source, masking what does not need to be printed
Digital Light Processing
– one image, each pixel controlled by micro mirror

I ordered 2022/11/22 for EUR 170 and received it 4 days later via Anycubic Store within Amazon, along with a few utilities like water wash resins, gloves, etc – ready to MSLA print.

Update 2023/05: the Mono 4K has been discontinued, and replaced with Mono 2 with slightly larger print volume of 143 (+11mm) x 89 (+9mm) x 165 (+33mm).

FDM/FFF vs Resin

X, Y precision: 100μm
Z precision: 50μm
minimal structure: 200-600μm1)
minimal post-processing
minimal toxicity of filament
print duration based on printed volume
large scale prints affordable
1kg filament EUR 15-30
mixed materials2)
X, Y precision: 35-75μm
Z precision
: 30-50μm
minimal structure
: 30-50μm
extensive post-processing
severe toxicity of resin
print duration based on print Z height only
large scale prints require expensive printers
1kg resin EUR 30-803)
single material
  1. depends on nozzle diameter
  2. multiple printheads/hotends required, e.g. IDEX, tool changer, material changer
  3. Anycubic sold water wash resins at 22-27 EUR/kg 2023/03 (black, white, clear, grey and waterblue)

Print Settings

  • layer height: 0.05mm / 50μm
  • exposure time: 2s
  • light-off time: 0.5s
  • bottom layer count: 6
  • bottom exposure time: 40s
  • lifting distance: 6mm
  • lifting speed: 4mm/s
  • retract speed: 6mm/s
  • anti-aliasing level: 1
  • file formats: pwma (proprietary)

Photon Workshop on Linux

My development environment is Linux, and as of 2022/11 there is no Linux Photon Workshop, the name of the slicer needed to slice for Mono 4K; but you can run it via Wine (a Windows compatibility wrapper):

% wine AnycubicPhotonWorkshop_V2.2.19_x86.exe

and afterwards it is available direct at:

% wine ~/.wine/drive_c/Program\ Files\ \(x86\)/AnycubicPhotonWorkshop/AnycubicPhotonWorkshop.exe

I tried my xyzHollowCalibrationCubeV2 and used auto hollow feature which defaults to 2mm thickness:

This seems to work but is not ideal. The Photon Workshop reveals that Photon Mono 4K supports only .pmwa format, which as it turns out, is PWS format just with the extension .pmwa to avoid mixing up different PWS files for different machines, as the pixel-based slices are hardware dependent now as resolution of the display is set.

Chitubox for Linux

Chitubox Slicer is available for Linux natively and supports a variety of SLA printers, also the Mono 4K:

I also used the auto hollow feature, which defaulted to 1.2mm wall thickness.

Lychee Slicer for Linux

I ended up with the Lychee Slicer which is available for Linux as well, which contains some annoying advertising to wait for when slicing or exporting various formats, but functionality-wise it it is more intuitive than Chitubox Slicer.

Prusa SLA Slicer

As mentioned, the Photon Mono 4K has its own proprietary file-format PWS file to print with, with a particular file extension to indicate which Anycubic MSLA device it is sliced for:

MachineFile ExtensionLayer Image Encoding

Prusa Slicer slices also for MSLA, it’s own .sl1 format, just a ZIP file with a list of PNG files per slice, in order to convert .sl1 to PWS/.pwma another tool is required:

  • UVtools, it can read and write many SLA image formats, incl. PhotonWorkshop (.pw*) file-formats
  • another approach could be (as of 2022/12 not yet) to just convert sl1 to .photon with SL1toPhoton command line tool, and extend functionality to support PWS/.pwma as well
  • alternatively uv3dp supports pw0 and pws files, but struggles with new(er) PWS files like .pwma

.photon/ctb/cbddlp vs .pw*

.photon (and ctb/cbddlp) is an older file format, whereas newer Photon Workshop (PW) has PWS and PW0 encoded images as layers – so far the format seems reverse engineered and somewhat documented via UVTools: PhotonWorkshopFormat.cs

ManufacturerFile ExtensionFIle FOrmat
Anycubic Photon*PWS/PW0
Anycubic Photon.photonalike CTB
Elegoo Mars.ctbCTB/CBDDLP1)
Creality LD002.ctbCTB/CBDLP1)
  1. possibly encrypted, but the algorithm is available

PWS vs SL1

Some deeper dive into the available metadata of PWS vs SL1 format:

PWS (header)SL1 (prusaslicer.ini)
“xy_pixel”: 35.0,
“z_thickness”: 0.05000000074505806,
“exposure_time”: 2.0,
“off_time”: 0.5,
“bottom_layers_exposure_time”: 40.0,
“bottom_layers”: 6.0,
“z_lift_height”: 6.0,
“z_lift_speed”: 4.0,
“z_drop_speed”: 6.0,
“total_volume”: 4.853200912475586,
“antialiasing_grade”: 1,
“x_resolution”: 3840,
“y_resolution”: 2400,
“weight”: 0.0,
“price”: 1.067704200744629,
“resin_type”: 36,
“layers_count”: 400

“absolute_correction”: “0”,
“area_fill”: “50”,
“bed_shape”: “0x0,120×0,120×68,0x68”,
“bottle_cost”: “0”,
“bottle_volume”: “1000”,
“bottle_weight”: “1”,
“display_height”: “68”,
“display_mirror_x”: “1”,
“display_mirror_y”: “0”,
“display_orientation”: “portrait”,
“display_pixels_x”: “2560”,
“display_pixels_y”: “1440”,
“display_width”: “120”,
“elefant_foot_compensation”: “0”,
“elefant_foot_min_width”: “0.2”,
“exposure_time”: “10”,
“faded_layers”: “10”,

“fast_tilt_time”: “5”,
“gamma_correction”: “1”,
“hollowing_closing_distance”: “2”,
“hollowing_enable”: “0”,
“hollowing_min_thickness”: “3”,
“hollowing_quality”: “0.5”,
“initial_exposure_time”: “15”,
“initial_layer_height”: “0.3”,
“layer_height”: “0.3”,

“material_correction”: “1,1,1”,
“material_correction_x”: “1”,
“material_correction_y”: “1”,
“material_correction_z”: “1”,
“material_density”: “1”,
“material_print_speed”: “fast”,
“max_exposure_time”: “100”,
“max_initial_exposure_time”: “150”,
“max_print_height”: “200”,
“min_exposure_time”: “0”,
“min_initial_exposure_time”: “0”,
“output_filename_format”: “[input_filename_base].sl1”,
“pad_around_object”: “0”,
“pad_around_object_everywhere”: “0”,
“pad_brim_size”: “1.6”,
“pad_enable”: “1”,
“pad_max_merge_distance”: “50”,
“pad_object_connector_penetration”: “0.3”,
“pad_object_connector_stride”: “10”,
“pad_object_connector_width”: “0.5”,
“pad_object_gap”: “1”,
“pad_wall_height”: “0”,
“pad_wall_slope”: “90”,
“pad_wall_thickness”: “2”,
“printer_technology”: “SLA”,
“relative_correction”: “1,1”,
“relative_correction_x”: “1”,
“relative_correction_y”: “1”,
“relative_correction_z”: “1”,
“slice_closing_radius”: “0.049”,
“slicing_mode”: “regular”,
“slow_tilt_time”: “8”,
“support_base_diameter”: “4”,
“support_base_height”: “1”,
“support_base_safety_distance”: “1”,
“support_buildplate_only”: “0”,
“support_critical_angle”: “45”,
“support_head_front_diameter”: “0.4”,
“support_head_penetration”: “0.2”,
“support_head_width”: “1”,
“support_max_bridge_length”: “15”,
“support_max_bridges_on_pillar”: “3”,
“support_max_pillar_link_distance”: “10”,
“support_object_elevation”: “5”,
“support_pillar_connection_mode”: “dynamic”,
“support_pillar_diameter”: “1”,
“support_pillar_widening_factor”: “0”,
“support_points_density_relative”: “100”,
“support_points_minimal_distance”: “1”,
“support_small_pillar_diameter_percent”: “50%”,
“supports_enable”: “1”


  • PWS:
    • xy_pixel dictates square pixels
    • x_resolution & y_resolution with xy_pixel give actual build area
    • build height (Z) determined via layers_count * z_thickness, assuming equal layer heights
  • SL1:
    • does not report layer counts, but be determined from the amount of enclosed .png files
      • does not support multi-exposure printing
x_resolution [px]display_pixels_x [px]
y_resolution [px]display_pixels_y [px]
xy_pixel [μm]display_pixels_x / bedshape[x] * 1000 [μm]

Replacing Firmware

MSLA resin printers are quite closed systems without much information of the hardware, firmware, and additional having their own proprietary file formats which contain the layer images.

For the Anycubic Photon Mono 4K is an open source firmware available, Turbo Resin – which gave me a good reason to get this 3D printer (2023/07: the firmware isn’t complete yet). Along with it, the hardware has been pretty much reversed engineered.

Anycubic FirmwareTurbo Resin (2022/12)
– PW0/PWMA format– PW0/PWMA & CTB format

Custom MSLA Slicer

Pondering on a custom MSLA slicer:

  • automatic hollowing of solids at certain wall thickness
  • automatic support generation, outside and inside (after hollowing)
  • drain hole generation
  • command line interface (CLI)
  • external preview of sliced part
  • supporting sl1 and pws/pw0 as a start
FeaturePrusa SlicerLychee SlicerChituBox Slicer
command line interface (CLI)Y
automatic hollowingYYY
drain holesYYY
automatic supportYYY
Linux supportY
PWS supportYY

Open MSLA Format

Unfortunately there is no open (M)SLA format, each manufacturer kind of does its own, whereas G-code .gcode has some conformity, although G-code in general is also machine specific has it has absolute positioning, which differ from machine to machine, but at least G-code is easy to compose unlike proprietary (M)SLA file formats.

SL1 format by Prusa Engineering is a simple ZIP file which contains:

  • config.ini: irrelevant info
  • prusaslicer.ini: info about printer (bed size), pixel density, and many slicer settings
  • *.png: enumerated image files per slice in PNG format

and thereby is open enough for my taste.

Requirements of Open MSLA Format

  • simple format for controller to decode
    • simple pixel data1)
    • simple preview image format1)
  1. this is why PWS/PW0 or CTB fileformat use some simple RLE algorithm to compress pixel data

First Print

After many weeks postponing, as I wasn’t eager deal with the inherent messiness of resin printing, I gave it a shot with some of the Triply Periodic Minimal Surfaces (TPMS) (2023/03/05):

The Lychee Slicer gave 1h 15m print time, the printer itself showed 2h 15m; I used Anycubic White Water Wash Resin. I used automatic supports, and it printed 5 pieces successful, 3 pieces failed and only apprx. 4mm Z height were printed, interestingly all 3 failed pieces failed at the same Z height and broke off and stuck at the print plate.

I reprinted the 3 failed pieces at the same place, and this time they succeeded – which is strange as I suspected perhaps uneven light or some other positional inconsistency, but obviously the position did not matter, which is bad as I don’t know what caused the first failed print.

The curved bottoms (not directly printed but with diverse support pipes) already showing severe distortion while printing.

Update: It seems my office rooms aren’t warm enough, so the bed adhesion isn’t optimal as I read up in some forum posts. I tried to print a few other pieces, all failed the next day in the room with 15-19C° – the prints detached after 2-3mm height from the build plate. I moved it to another warmer room, warmed the resin on the radiator which helped.

The overall quality of the pieces is astonishing, no visible voxels or layers are seen, incredible quality for those prints which didn’t fail.

Yet the failure rate is still significant for my taste, so I need to pay close attention to room temperature, and other aspects:

The cause of the “delimination” isn’t clear yet to me, it seems the prints with proper support and elevated bottom printed better, but I need to confirm with more prints.

More photos you find at MSLA printing Triply Periodic Minimal Surfaces (TPMS).

Curing Station

As a start I assembled a simple DIY curing station with a 5m UV LED strip and placed it inside a plastic cup, with some aluminium foil at the bottom and top lid:

I only cure for 4-5mins, longer exposure changes the white resin into yellowish tint, and indicates over curing.

Keeping Resin in the Vat

After a print, the resins needs to be filtered for impurities, such as partial cured pieces not attached to the part, with a funnel and filter into a cup or bottle, and then it can be poured back into the vat ready to print again.

One can leave the resin in the vat for weeks, if you stir the resin short before you print again – stirring the resin within the vat is not ideal, as one has to avoid to scratch or FEP film; yet there is no need to pour resin back into the bottle unless one changes the resin, like the brand or color.

Drip Hook

I remixed an existing drip hook for Photon Mono to fit Photon Mono 4K, and make it easier to slide the bed on and off.


Spring Steel Build Plate

Additionally I’ve got a spring steel build plate 135x80mm with a magnetic base for EUR 10 (2022/12).

This turned out to be a good choice, the removal of the pieces is easy without additional tool.


  • the thickness of the adhesive magnet holding the plate required the optical Z endswitch to recalibrate, instead to move the sensor, I extended the light breaking piece with just a small piece of paper with a drop of glue – it was easier than 3d printing an extender for the entire sensor.
  • in my case the small handle of the plate scratches on the original vat at the last 3-4mm height, therefore the entire plate needs to be slightly misaligned (just pushing one side while fastening the build plate) so the handle doesn’t touch the vat.
    • third party Anycubic Mono 4K vat like from Mega/Kingroon have a larger space, and don’t need any fiddling around therefore
  • the spring steel is sharp, it happened several times the single-use gloves being torn/cut while handling the plate

(One of) My Use Case

After a few days I aimed for the main use case of mine: printing custom pulleys.

MSLA @ 35μm XY, 50μm Z vs FFF @ 400μm nozzle, 100μm Z

As I printed them with Anycubic Water Wash White Resin without support, the “elephant foot” comes from the first 6 layers being cured for 40s as in my case, and the UV light refracting and curing more than meant to be, but I can neglect this.

16 custom pulleys ID8 20T printed with Anycubic Photon Mono 4K in 1h 30m or 5m30s per piece

geometrical accuracy★★★★☆1)★★★☆☆
surface quality★★★★☆★★☆☆☆
mechanical sturdiness(not yet tested)★★★☆☆ (PLA)
print time per piece5m 30s2)15m
print time for 1 piece1h 30m15m
print time for 16 pieces1h 30m4h
  1. due the “elephant foot” the Z accuracy was off by 0.8mm, instead of 15.0mm it’s 14.2mm
  2. when printing 16 pieces, it took 1h 30m for printing 15mm in Z, I could have printed ~28 pulleys on 132 x 80mm build plate, bringing print time for a piece down to 3m 10s


from Anycubic Web-Site (2023/03/12)

My own experience with different resins (to be extended):

Defaults V0.0.11
Firmware V0.11
Defaults V2.0.2
Firmware V0.16
White WATER WASH RESIN (Anycubic)Clear Water Wash Resin (Resione)
Layer Thickness [μm]50505050
Exposure Time [s]32.532 .. 3
Exposure Off Time [s]2.510.50.5
Bottom Exposure [s]50304040
Bottom Layers6666
Z Lift Distance [mm]
Z Lift Speed [mm/s]
Z Retract Speed [mm/s]
UV Power [%]10010010050 .. 100
NotesDistance, Speed & Retract Speed for
– [BL] Bottom Layers
– [NL] Normal Layers
individually definable
– geometrical precise– soft with 3s exposure
– stiffer & brittle with 5s exposure
– geometrical not precise (+0.2 .. 0.8mm in XYZ)

Preliminary Review


  • good prints for the price
  • cost effective
  • power loss recovery actually works
  • lot’s of third party replacements (vat, FEP, etc)1)
  • alternatively Open Source firmware
  • nearly full reverse engineered hardware
  1. this actually is quite important: popular machine raise a secondary market for replacements: future replacement of parts even when Anycubic ends support


  • newly bought machine had outdated firmware
  • updated firmware calculates wrong total print time, this is just sloppy
  • touchscreen unreliable (wrong position) ‘print’ vs ‘delete’, use a soft pencil
  • slow prints with default settings (1mm/s Z motion), not nearly at 50mm/h height as advertised


  • slicer print settings are ignored, only settings on the machine matter
    • advantage: once sliced the .pwma can be printed with different resins and settings changed on the machine only
    • disadvantage: one has to memorize or document settings for different resins, as it not stored in the .pwma file


It’s a low-cost entry level MSLA machine, Anycubic seems to care little about the software (2023/03) as the slicer as well the firmware are Minimal Viable Product (MVP) level, but aren’t mature or reliable at all. Given they sell 500K+ machines per year at least, investing to improve in the firmware would help 500,000 users.

Small anecdote: I bought a pre-owned Mono 4K, becoming “Photon 2”, from official Anycubic store at Ebay directly – the listing said the machine likely would miss some parts – but I felt to get some more in-depth experience, and I’ve got what I asked for: the machine came dirty with resin all over, no power-supply, no build-plate, no vat, and cured resin between LCD and underlying (acryl-)glass, spent EUR 120+ for replacement parts, and 10+ hrs cleaning it to get it in working condition again. From a cost saving point of view not worth it, but experience wise it was good to get to know the machine more thoroughly.

(M)SLA Value Comparison

A numeric value summarization for features I care about, the rough & simple formula:

XY Area [mm2] / Price [EUR] / XY Resolution [μm]

The bigger the XY area, the smaller the price and the smaller the resolution, the higher the value:

State 2024/07

Elegoo Saturn 42024-19μm x 24μm2)MSLA218x122x220300 EUR3.74
Elegoo Saturn 4 Ultra2024-19μm x 24μm2)MSLA218x122x220400 EUR2.81
Anycubic Photon Mono M52023-19μm x 24μm2)MSLA218x123x200410 EUR2.72
Elegoo Mars 5 Ultra2024-18μmMSLA153x78x165270 EUR2.54
Anycubic Photon Mono X 6Ks2023-34μmMSLA196x122x200280 EUR2.51
Elegoo Saturn 3 Ultra2023-19μm x 24μm2)MSLA218x123x260480 EUR2.32
Anycubic Photon Mono 4K2021-202235μmMSLA132x80x165150 EUR2.01
EMake Galaxy 1202325μm / 100μm1)SLA400x200x4001,600 EUR2.00
Anycubic Photon Mono X22023-48μmMSLA196x122x200260 EUR1.91
Anycubic Photon Mono 22023-35μmMSLA143x89x165210 EUR1.73
Elegoo Jupiter SE2023-51μmMSLA277x156x300700 EUR1.21
Anycubic Photon M3 Max2022-45μmMSLA298x164x300980 EUR1.10
Formlabs 3L2020-25μm / 100μm1)SLA335x200x3009,000 EUR0.29
Formlabs 3+2020-25μm / 100μm1)SLA146x145x1853,500 EUR0.24
Prusa SL1S2021-49μmMSLA127x80x1801,700 EUR0.12
Formlabs 42024-50μmMSLA200x125x2105,175 EUR0.09
  1. Formlabs 3’s & EMake SLA printers use a laser beam which has 100μm in diameter, but it can be positioned 25μm exact, the latter was used to calculate the value
  2. The UV pixels are non-square, the longer side was used to calculate the value

The XY area in SLA scales not as good as with MSLA, as the laser beam takes longer the more XY area (e.g. more pieces) need to be rendered per layer – so, MSLA is recommended for aiming fast parallel printing. Interestingly Formlabs 4 is now a MSLA as well (2024/04), they seem to have abandoned the SLA laser-based approach.


Anycubic Photon Mono 4K specific:

General Photon Series MSLA:

MSLA Slicers:


3D Printing: MSLA printing Triply Periodic Minimal Surfaces (TPMS) – Gallery


  • 2023/07/25: adding 20mm/40mm TPMS photos
  • 2023/03/12: starting write-up, and published

20mm cubes of several Triply Periodic Minimal Surfaces (TPMS) as explored at Generative Parametric Infill Geometries printed with MSLA (Anycubic Photon Mono 4K) at 35 μm XY, 50 μm Z:

Most of the cubes were printed without support, the cylindrical and spherical projections required supports.


20mm and 40mm cubes of TPMS mounted on canvases:


3D Printing: Parametric Generative 3D Infill Geometries


  • 2023/02/09: finally published
  • 2023/02/08: worked on text and illustrations a lot, many sample prints, multiple visualization approaches, details on f1 + f2 vs f1 * f2 and cylindrical and spherical transformation of TMPS
  • 2023/01/05: adding mesh/voxel renderings, slicing geometry to generate G-code
  • 2022/12/11: first FDM G-code generated using 2D / contour approach
  • 2022/12/07: included many suitable periodic minimal surfaces
  • 2022/12/02: start with implicit surface focus

As I progress I will update this blog-post.


Infill geometries are geometries which are continuous, repetitive or periodic; they fill a boundary defined geometry aka outer form often defined via meshs. Let’s dive into some of the simple geometries and then looking at some more complex structures:

The Implicit Geometries

Implicit geometries are geometries defined via f(x,y,z) = 0 defining their surface, the boundary between inside and outside and they are ideal to define repetitive or periodic 3D infill geometries.


Sphere: x2 + y2 + z2 – r2 = 0

When you ever tried to compose a sphere as a mesh, you know there are many ways to do so, and all are more complex than this simple description, and as you realize, the formula is perfect, it’s not an approximation – this is the nature of implicit formula. When you try to visualize an implicit formula, then you need to discretize and there the approximation takes place, as a mesh or as voxels.

Another nifty property of the sphere, it is the minimal surface to circumvent a volume, and through this blog-post, the minimal surface will become a common theme.


Cube: max(abs(x),abs(y),abs(z)) – w/2 = 0


Plane: z = 0

As I render only -10 to 10 to each axis, it creates a small plate:

Triply Periodic Minimal Surface (TPMS)

Let’s move to the world of minimal surfaces, so called Triply Periodic Minimal Surfaces (TPMS), those can be expressed in implicit form and have some properties as sought for infill geometries.

In differential geometry, a triply periodic minimal surface (TPMS) is a minimal surface in ℝ3 that is invariant under a rank-3 lattice of translations. These surfaces have the symmetries of a crystallographic group. Numerous examples are known with cubic, tetragonal, rhombohedral, and orthorhombic symmetries. Monoclinic and triclinic examples are certain to exist, but have proven hard to parametrise.

Wikipedia: Triply Periodic Minimal Surface (TPMS), retrieved 2023/02/08

Schwarz P aka Primitive

One of the simplest yet powerful formula:

Schwarz P: cos(x) + cos(y) + cos(z) = 0

increasing the frequency or scale of the structure:

By extending the formula with +a, we can animate it:

animating a: -1..1, transits from octahedron to cuboctahedron
Schwarz P 4x animated a: -1..1

Schwarz D aka Diamond

Schwarz D: sin(x)*sin(y)*sin(z) + sin(x)*cos(y)*cos(z) +
cos(x)*sin(y)*cos(z) + cos(x)*cos(y)*sin(z) = 0


Neovius: 3*(cos(x)+cos(y)+cos(z)) + 4*cos(x)*cos(y)*cos(z) = 0

C(Y) Surface

C(Y) Surface: sin(x)*sin(y)*sin(z) + sin(2x)*sin(y) + cos(x)*sin(2y) + sin(2y)*sin(z) + sin(2z)*sin(x) + cos(x)*cos(y)*cos(z) + sin(2x)*cos(z) + cos(x)*sin(2y) + cos(y)*sin(2z) = 0

Fischer Koch

Fischer Koch: (cos(x)*cos(y)*cos(z) + cos(z)*cos(x)) –
(cos(2x)+cos(2y)+cos(2z)) = 0

S Surface

S Surface: cos(2x)*sin(y)*cos(z) + cos(2y)*sin(z)*cos(x) +
cos(2z)*sin(y)*cos(y) – 0.4 = 0


Gyroid: cos(x)*sin(y) + cos(y)*sin(z) + cos(z)*sin(x) = 0


FRD: 8 * a*cos(x)*cos(y)*cos(z) + b*(cos(2x)*cos(2y)*cos(2z)) –
c*cos(2x)*cos(2y) – d*cos(2y)*cos(2z) – e*cos(2z)*cos(2x)

Let’s explore this form more thoroughly, we animate a, b, c, d, and e and see what it does, essentially we animate -1 to 1 in sinus, 0 eliminates of the chunk of the formula:

animating a (-1..-1)
animating b (-1..1)
animating c (-1..1)
animating d (-1..1)
animating e (-1..1)

Gyroid Skeletal

Gyroid Skeletal: 10*cos(x)*sin(y)+cos(y)*sin(z)+cos(z)*sin(x)) –
0.5*(cos(2x)*cos(2y)+cos(2y)*cos(2z)+cos(2z)*cos(2x)) – 14

P Skeletal

P Skeletal: 10*(cos(x)+cos(y)+cos(z) –
5.1*(cos(x)*cos(y)+cos(y)*cos(z)+cos(z)*cos(x)) – 14.6

By changing the last substraction of 14.6 to 10 or 8, the structure get more dense – ideal to use.

P Skeletal, animating main subtraction -14.6(thin)..5.4(disconnected)

The P Skeletal connects 6 arms to each other.

IWP Skeletal

IWP Skeletal connects 8 arms to each other.

Schwarz D Skeletal

Schwarz D Skeletal connects with 4 arms to each other.

The above “skeletal” minimal surfaces are ideal for lattice structures, likely most usable in context of voxel-based 3D printing approaches, such as SLA, SLS, SLM and so forth, but less ideal for traditional FDM where the lattice is sliced Z-planar again kind of defeating the overall purpose of lattice structures.

D Surface

D Surface: cos(x)*cos(y)*cos(z) – sin(x)*sin(y)*sin(z)

As Juergen Meier created a variant, adding a, which gives these variants:

providing a structure using 4 arms to connect each other.


Using Implicit Geometries as Infill Structures

Slic3r and Prusa Slicer are providing gyroid infill pattern since early version, but beyond that it seems no to little development happened since (2022/12).

Let’s see how implicit geometry can be transformed into slices (FDM) or voxels/pixels (SLA, SLS etc)

Algorithm A: 3D Cache

  • create point cloud of surface of implicit geometry
  • create surface of implicit geometry using marching cube
  • (optional) determine x, y, z size where it repeats itself
  • slice surface for infills at certain scale
    • clip inner surface with outer perimeter of slice


  • with caching: fast lookup of infill geometry


  • many steps
  • x, y, z repeatability must be given, hard to determine programmatically from outside
  • clipping to perimeter can be computational expensive depending

Algorithm B: 2D Cache

  • create 2D point cloud of a slice of implicit geometry based on clipped 2D area / slice
  • convert 2D point cloud to polylines (FDM) or pixels (SLA)


  • reduction to 2D problem at first stage
  • fast 2D point cloud creation as only one z-level is used


  • create 2D point cloud at arbitrary resolution, loss of curves unless refitted
  • caching without knowing repeatability of the geometry makes little sense

FDM G-code

Here some early G-code for FDM 3D printer using PyImplicit tool tracking the implicit surface as 2D contour:

Meshs & Voxels

The implicit surfaces only define the surface, either:

  • inside vs outside – a solid; or
  • certain thickness of such surface

In order to create watertight meshs the volume needs to be limited with a boundary box, and Marching Cube is performed from outside to get proper mesh to post-process afterwards.

Now you may wonder, what’s the fuss with all those forms, why doing this complicate implicit form, why not just create a few forms as meshs right away and repeat them orderly – well, here it comes why:

Frequency or Scale Gradients

Changing the frequency or scale s0 and s1 can be achieved by:

znorm = (z-zmin) / (zmax-zmin)
s = (1-znorm)*s0 + znorm*s1 or
s = lerp(s0, s1, znorm)
f = surface(x*s, y*s, z*s)

This shows the power of generative geometries, we simply can define the scale or frequency of a geometry at any point, given we transit within reason and not too sharply to cause discontinuty.

Thickness Gardients

Alike changing thickness:

znorm = (z-zmin) / (zmax-zmin)
t = lerp(t0, t1, znorm)
f = abs(surface(x,y,z)) – t

Form Gradients

What looks very complex is done quite simply with:

znorm = (z-zmin) / (zmax-zmin)
f = lerp(surface1(x,y,z) , surface2(x,y,z), znorm)

This is quite powerful property, to be able to morph from one implicit form to another with such a simple formula.

Contineous Transitions:

  • Schwarz D – Schwarz P
  • Schwarz D – Neovius
  • Schwarz P – Neovius
  • thickness: IWP Skeletal – Schwarz P
  • thickness: IWP Skeletal – Schwarz D

Discontinueous Transitions:

  • IWP Skeletal – P Skeletal
  • IWP Skeletal – Neovius
  • solid: IWP Skeletal – Schwarz P
  • solid: IWP Skeletal – Schwarz D

Combining Implicit Surfaces


Algebraic addition has the effect of apply one geometry within another, alike recursion:


Algebraic multiplication has the effect of clipping, or geometrical intersection:

Mapping Implicit Surfaces

One can map the coordinates, and create a cylindrical gyroid, where former X & Y become distance and rotation angle, and Z remains as is, and so spherical projection is possible as well, or even feed coordinates through implicit formula itself:

Next blog-post(s) I will go into further details utilizing TPMS in Additive Manufacturing (AM) like FDM/FFF, SLA, MSLA, SLS, MJF or SLM – each one of them have unique features and limitation for using those Parametric Generative Infill Geometries.

Appendix: Visualization

In case you wondered of the different styled visualization through this blog-post, let me show you the different approaches to discretize implicit defined surfaces.


The code is rather simple with OpenSCAD yet rather slow: either skin is true or false, and delta determines the thickness of the skin if enable:

t = 1;
r = 20*t;
st = 1/2;
delta = 0.2;

function schwarz_p(x,y,z,s=1) = cos(x*s) + cos(y*s) + cos(z*s);

skin = true;

      for(z=[-r:st:r]) {
         f = schwarz_p(x,y,z,360/20/2);
         if(skin && abs(f)<delta)           // -- skin only
            translate([x,y,z]) cube(st);
         else if(!skin && f<delta)          // -- inside/outside
            translate([x,y,z]) cube(st);

Rendered via voxelation:


Rendered in OpenSCAD via marching cube algorithm with Level Surfaces:

Volumes & Surfaces in OpenGL GLSL

Following experiments were done with Spirula/Implicit3 within the browser, the implicit formulas are rendered in realtime at 100-500 fps using OpenGL’s GLSL (GL Shader Language):

One has to clip the formulas with a cube in order to have a limited set, otherwise you get a full screen looking at infinite X, Y & Z, here Schwarz P:

Spirula/Implict3 realtime rendered Schwarz P TPMS in the browser

Meshs with Marching Cube

In order to create a mesh, I developed PyImplicit which utilizes Numpy library to calculate the implicit formula fast, and then run a Marching Cube algorithm over the result in order to get a discrete mesh like STL, OBJ, or 3MF to process further for 3D printing.

Foreground: 1st row: 90mm cube clipped of frequency gradients on Schwarz D, Schwarz P*,
surface gradient between Schwarz D to Schwarz P (top) at certain thickness or solid,
2nd row: 2x IWP skeletal 90mm cubes at different frequency;
Background: various 30/40mm cube clipped Triply Periodic Minimal Surfaces

*) some of my larger prints I attach RFID tags, e.g. as on top of the variable frequency Schwarz P print, which I store the print UID from my Prynt3r job which logs all my prints with all settings and webcam snapshots. In future blog-post I will illustrate my NFC/RFID setup.

And Polyviuw is a small mesh viewer using Polyscope Python as backend to display it as mesh:

It is easy to create huge files when exporting an implicit generative infill geometry and one ends up with a 700MB binary STL file, which becomes hard to view at least on my system. To handle complex outer forms, with complex inner geometries I estimate reaching multiple gigabytes large files – let’s see.