Menu
JAQForum Ver 19.10.27

Forum Index : Microcontroller and PC projects : Next project: Micropython on the Pico Computer 3 - progress reports

Posted: 03:22pm
02 Jul 2026
Copy link to clipboard
matherp
Guru

It is clear that, as good as MMbasic is, there is a category of users who will go "yuck - basic"

However in MMBasic on the PicoMite there is a huge amount of useful IP so I thought why not integrate some of that into Micropython

My target platform is the Pico Computer 3 (One board to rule them all) as that has everything that any self-respecting stand-alone Micropython computer could need for hardware.

There is a bit of a learning curve getting a micropython build environment up and running, particularly on windows where it has to run under Ubuntu on wsl. However, with that out of the way I could start integrating.

I used as a starting point the Pico2-W version of micropython. This give things like pin access (digital in/out, analogue in, i2c, spi) as well as web support and a LFS file system in the flash memory (same as the MMbasic A: drive).

First thing was then to enable PSRAM and the extra RP2350B pins.

Next job was to swap the cdc console for a serial one so the USB could be reserved for USB host activiites.

Then the sdcard drivers from MMbasic were ported across so the sd could be read.

Next an editor was added (pye at the moment but this may get upgraded) and the ability to run a program from SDcard added.

Last easy bit was adding support for all the standard command line actions
  Quote  Command Action
pwd() print working directory
cd("/sd") change directory (no arg → /)
ls("/sd") list (dirs first, sizes, dates)
mkdir("/sd/proj") make directory
rmdir("/sd/proj") remove empty directory
rm("/sd/old.py") delete a file
cat("/sd/notes.txt") print a text file
cp("/sd/a.py", "/sd/b.py") copy a file
mv("/sd/a.py", "/flash") move/rename
edit(...), run(...) editor / launch


That was the easy stuff out of the way now the fun bit

First big decision was what HDMI resolutions should be supported. This may come back to bite but currently 307,200bytes are allocated to a framebuffer allowing 640x480 RGB332 and 320x240 RGB565.
This has been implemented and so far so good. HDMI is up and running and you can swap between the two modes without rebooting.
Micropython includes a framebuf module that has the usual graphics commands - circles etc. so this has been integrated.
Now underway is reflecting the serial console commands onto the screen. that works but is still WIP.
The next crucial bit of the jigsaw will be moving the MMbasic HID host functionality across. AFAIK this is not typically available in any micropython port so that will be an adventure. I'll keep you informed. If you want to play get yourself a PicoComputer 3 or patch wire something using the same pinout.

 
Posted: 07:18pm
02 Jul 2026
Copy link to clipboard
PhenixRising
Guru

I wanted to ask about the plan but didn't want to distract you.

Does this mean compatibility with Mpython libraries (they seem to support every darned thing)?
 
Posted: 11:17am
03 Jul 2026
Copy link to clipboard
matherp
Guru

HID host functionality now running with USB keyboard able to interact in the full set of language variants so we have achieved
"stand-alone status".
I'm now working on audio.
Whatever, anyone has claimed before Micropython is significantly slower than MMBasic. Taking approximately twice the time to run functionally equivalent benchmarks.
All Micropython libraries that are available should work with no issues. For example, the first test of audio required no code changes as I2S can be called direct from python
from machine import I2S, Pin
import array, math
N = 100                                   # 44100/100 = exactly 441 Hz
cyc = array.array('h')                    # array append is amortised O(1)
for i in range(N):
   v = int(18000 * math.sin(2*math.pi*i/N))
   cyc.append(v); cyc.append(v)          # L, R
mv = memoryview(cyc)
i2s = I2S(0, sck=Pin(10), ws=Pin(11), sd=Pin(22),
         mode=I2S.TX, bits=16, format=I2S.STEREO, rate=44100, ibuf=8192)
for _ in range(441):                      # ~1 s, starts instantly
   i2s.write(mv)

Edited 2026-07-03 21:19 by matherp
 
Posted: 12:13pm
03 Jul 2026
Copy link to clipboard
PhenixRising
Guru

  matherp said  
Whatever, anyone has claimed before Micropython is significantly slower than MMBasic. Taking approximately twice the time to run functionally equivalent benchmarks.


Makes me even more proud of the awesome MMBasic    
 
Posted: 02:29pm
03 Jul 2026
Copy link to clipboard
matherp
Guru

Audio WAV, Flac and MP3 playback all now working. DS3231 integrated and working. Next is some enhancements to HDMI and then to enhance the ls command and a first release is there.
Edited 2026-07-04 02:06 by matherp
 
Posted: 07:05pm
03 Jul 2026
Copy link to clipboard
matherp
Guru

Continuing to go well.
We now have a 512x300x16-bit mode (MMBasic can't do this) as well as allowing 640x480x8 and 320x240x16-bits at 252 or 378MHz. The  512x300x16-bit mode has been tested while playing an mp3 and connecting to wifi at the same time. Not bad when the video bandwidth is 19.3 M words/s.
ls now allows filtering ls("/sd/*.jpg") and yet more code is being imported from MMbasic jpg, png, bmp display and bmp save.
After that it is some small niceties and I will release a first beta.
 
Posted: 02:41pm
04 Jul 2026
Copy link to clipboard
matherp
Guru

Here is the first cut uf2 and the user manual. Note, it will probably run up on any RP2350B but only if there is PSRAM on GP47, its uses a serial console on GP8,GP9. The usb port is dedicated to host functionality so after flashing it is best to disconnect the USB and use a power supply.


Pico Computer 3 Micropython firmware V0.1.zip


  Quote  # Pico Computer 3 — MicroPython User Manual

**Firmware:** MicroPython (RP2350B port) for the Pico Computer 3 — version **0.1** (test release).
The REPL banner reports the version:

```
MicroPython v1.29.0 on PICO COMPUTER 3 v0.1 with RP2350B
```

This is a customised build of MicroPython that turns the Pico Computer 3 into a
self-contained computer: HDMI display, USB keyboard/mouse/touch, SD card, audio,
a real-time clock and Wi-Fi, all usable directly from the Python prompt. Much of
the functionality is ported from the PicoMite MMBasic firmware.

For the core Python language and the standard library, see the official
MicroPython documentation: **https://docs.micropython.org/** (choose the version
shown in the banner). This manual documents only what is *specific to the Pico
Computer 3*.

---

## 1. Getting started

There are two consoles, and both are active at once:

- **Serial console** — UART1 on **GP8 (TX)** / **GP9 (RX)**, **115200 8N1**.
 Connect a USB-serial adapter and open a terminal (TeraTerm, PuTTY, `screen`,
 `mpremote`). The Delete key works as forward-delete.
- **On-screen console** — text appears on the HDMI monitor and a USB keyboard
 drives it. This starts automatically at boot. `console(False)` detaches it,
 `console()` re-attaches it.

The USB device port is disabled (there is no USB-CDC prompt); the USB port is a
**host** port for keyboards, mice and touch panels.

At the prompt you can type Python directly, or use the shell-style helpers
(section 4). To run a program from the SD card or flash:

```python
run("/sd/myprog.py")
```

---

## 2. Pin allocations

The Pico Computer 3 is based on the RP2350B (48 GPIO). The following pins are
dedicated to on-board functions. Pins marked **reserved** are protected and
cannot be claimed with `machine.Pin()`.

| Function | Pin(s) | Notes |
|---|---|---|
| Console UART1 | **GP8** TX, **GP9** RX | 115200 8N1 — *reserved* |
| HDMI (HSTX DVI) | **GP12–GP19** | CLK GP13/12, D0 GP15/14, D1 GP17/16, D2 GP19/18 (differential pairs) — *reserved* |
| SD card (SPI1) | SCK **GP30**, MOSI **GP31**, MISO **GP28**, CS **GP33** | also `machine.SPI(1)` — *reserved* |
| Audio I2S (PCM5102 DAC) | BCLK **GP10**, LRCK **GP11**, DIN **GP22** | SCK pin grounded on the board |
| RTC (DS3231, I2C0) | SDA **GP20**, SCL **GP21** | 400 kHz, address 0x68 |
| PSRAM chip-select | **GP47** | 8 MB PSRAM |
| Wi-Fi / Bluetooth (CYW43) | REG_ON **GP23**, DATA **GP24**, CS **GP25**, CLK **GP29** | wireless chip over PIO-SPI — *reserved* |
| Status LED | CYW43 **GPIO0** | on the wireless chip (not an RP2350 pin) — named pin `"LED"` |
| USB host | native USB port | keyboard / mouse / touch |

The **status LED** is on the CYW43 wireless chip, not on an RP2350 GPIO. It is
exposed as the named pin `"LED"` (an "extended" pin) and driven like any output —
the driver relays the value to the chip over the internal SPI link:

```python
from machine import Pin
led = Pin("LED", Pin.OUT)
led.on(); led.off(); led.toggle()   # also aliased as Pin("WL_GPIO0")
```

Reserved GPIOs (unavailable to `machine.Pin`): **8, 9, 12–19, 23, 24, 25, 28,
29, 30, 31, 33**. Of these, **23/24/25/29** are the CYW43 Wi-Fi/Bluetooth
interface. All other GPIOs are free for your own use with `machine.Pin`,
`machine.ADC`, `machine.PWM`, `machine.I2C`, `machine.SPI`, `rp2.PIO`, etc.

### System configuration

- **CPU clock:** 252 MHz from boot (can be raised to 315 or 378 MHz — see
 `screen()` in section 5). `machine.freq()` reports it. The clock is coupled to
 the HDMI pixel clock, so it must be changed *together with* the display mode via
 `screen(mode, clock)` — the raw `machine.freq(hz)` setter is disabled and raises
 an error pointing you to `screen()`.
- **Floats:** double precision (64-bit). Integers are arbitrary precision.
- **Flash filesystem:** 12 MB LittleFS mounted at `/`.
- **SD card:** FAT, mounted at `/sd`, hot-swappable (section 12).
- **PSRAM:** 8 MB — the Python heap lives here, so large data/images/audio
 buffers are fine.

---

## 3. Modules imported automatically at boot

At start-up the board adds a set of names to the interactive namespace (`__main__`)
so they are available without an `import`. A program launched with `run()`
inherits the same names. The most useful are:

**Modules / objects:** `os`, `machine`, `Pin` (= `machine.Pin`), `framebuf`,
`hdmi`, `Display`, and the named colour palette (`RED`, `GREEN`, `BLUE`, `WHITE`,
`BLACK`, `YELLOW`, `CYAN`, `MAGENTA`, …).

**Shell commands:** `ls`, `run`, `edit`, `pwd`, `cd`, `mkdir`, `rmdir`, `rm`,
`cat`, `cp`, `mv`, `console`.

**Display / settings:** `screen`, `keymap`, `keymaps`.

**Audio:** `play`, `volume`, `beep`, `stop`, `is_playing`.

**Clock:** `settime`, `gettime`, `synctime`.

**Images:** `draw_jpg`, `draw_bmp`, `draw_png`, `save_image`.

**Input devices:** `touch`, `mouse`, `mouse_speed`.

Each is described in the sections below.

---

## 4. Shell commands

Unix-like helpers for working with the filesystem from the prompt. Paths may be
on the flash (`/…`) or the SD card (`/sd/…`).

| Command | Description |
|---|---|
| `ls([path])` | List a directory. Accepts glob patterns: `ls("/sd/*.mp3")` |
| `cd(path)` / `pwd()` | Change / show the working directory |
| `cat(path)` | Print a text file |
| `cp(src, dst)` / `mv(src, dst)` | Copy / move (rename) a file |
| `rm(path)` | Delete a file |
| `mkdir(path)` / `rmdir(path)` | Create / remove a directory |
| `run(path)` | Run a `.py` program (fresh namespace, inherits the REPL helpers) |
| `edit(path)` | Open the full-screen **pye** editor (Ctrl-S save, Ctrl-Q quit) |

---

## 5. Display (HDMI)

The HDMI output is a hardware DVI signal generated by the RP2350 HSTX and scanned
out on core 1. Three modes are available:

| Mode constant | Resolution | Colour |
|---|---|---|
| `hdmi.RGB640` | 640 × 480 | 8-bit (256 colours) — **default** |
| `hdmi.RGB320` | 320 × 240 | 16-bit (RGB565), pixel-doubled to 640 × 480 |
| `hdmi.RGB512` | 512 × 300 | 16-bit, doubled to 1024 × 600 |

The framebuffer lives in SRAM. In `RGB640` each pixel is one byte (RGB332); in
the two 16-bit modes each pixel is an RGB565 half-word. The image decoders
(section 10) and the on-screen console draw into this same buffer.

### Changing mode: `screen()` — the easy, persistent way

`screen(mode, clock=252)` is the recommended way to change the display. It stops
and restarts the scanout, saves the choice, and re-establishes the on-screen
console at the new geometry:

```python
screen(hdmi.RGB320)        # 320x240 16-bit, 252 MHz
screen(hdmi.RGB640, 378)   # 640x480 8-bit at 378 MHz
screen(hdmi.RGB512)        # 1024x600 16-bit (fixed at 252 MHz)
screen()                   # returns the saved (mode, clock)
```

The optional `clock` is the CPU/pixel clock and applies **only to `RGB640` and
`RGB320`**, which accept one of **252**, **315** or **378** MHz (default 252).
`RGB512` is fixed at 252 MHz — passing any other clock for it raises a
`ValueError`. The chosen mode/clock is saved and restored on the next boot
(section 13). If a saved mode does not suit your monitor, the serial console
still works — use `screen(hdmi.RGB640)` to reset it.

### Low-level control: `hdmi.init()` / `hdmi.deinit()`

`screen()` is a wrapper around the `hdmi` module. You can drive it directly for a
**one-off** (non-persisted) change, but you must stop the current scanout before
starting a new mode:

```python
hdmi.deinit()              # stop the scanout (frees core 1, aborts its DMA)
hdmi.init(hdmi.RGB320)     # start a new mode (default 640x480 @ 252)
import pcconsole; pcconsole.console()   # re-attach the on-screen console
```

- `hdmi.init(mode=hdmi.RGB640, clock=252)` — start the scanout. Raises
 `ValueError` on a bad mode or clock. **No-op if already running** — call
 `hdmi.deinit()` first to switch modes.
- `hdmi.deinit()` — stop the scanout: halts core 1, cleanly aborts the DMA and
 stops the HSTX output. After this the screen goes blank until the next
 `hdmi.init()`.

Because a raw `hdmi.init/deinit` does **not** move the console or save the
setting, prefer `screen()` unless you specifically want a temporary change or are
managing the framebuffer yourself.

### Drawing

`hdmi.fb()` returns a **`Display`** object — a `framebuf.FrameBuffer` subclass
with all the usual `framebuf` methods (`pixel`, `hline`, `vline`, `line`, `rect`,
`fill_rect`, `ellipse`, `text`, `blit`, `scroll`, …) plus a colour helper:

```python
d = hdmi.fb()
d.fill(d.colour(0x000080))              # 24-bit RGB -> current format
d.rect(10, 10, 100, 60, d.colour(RED))  # named palette colour
d.text("Hello", 20, 30, d.colour(WHITE))
```

Rebuild the `Display` (call `hdmi.fb()` again) after any mode change, because the
geometry and pixel format change.

**Colours must be converted first.** The `framebuf` drawing methods take a colour
in the framebuffer's *native* pixel format, so always wrap a colour in
`d.colour(...)`: `d.colour(0xRRGGBB)` or `d.colour(r, g, b)` converts a 24-bit
RGB value (including the named palette constants `RED`, `WHITE`, …) to the current
format. Passing a raw 24-bit value straight to `rect`/`text`/etc. gives the wrong
colour. (`d.color(...)` is an accepted US-spelling alias.) `d.text()` uses
framebuf's built-in 8×8 font; the console uses a larger 8×12 font.

### `hdmi` module reference

| Member | Description |
|---|---|
| `hdmi.RGB640` / `RGB320` / `RGB512` | mode constants (see the table above) |
| `hdmi.init(mode=RGB640, clock=252)` | start the scanout (no-op if running) |
| `hdmi.deinit()` | stop the scanout (call before re-`init()` for a new mode) |
| `hdmi.fb()` | a `Display` (framebuf subclass) over the framebuffer — rebuild after a mode change |
| `hdmi.framebuffer()` | the raw framebuffer as a writable `bytearray` (for `framebuf.FrameBuffer` or the image decoders) |
| `hdmi.width()` / `hdmi.height()` | current logical width / height in pixels |
| `hdmi.rgb565()` | `True` if the framebuffer is RGB565 (RGB320/RGB512), `False` if RGB332 (RGB640) |
| `hdmi.fill(colour)` | fast fill of the whole framebuffer with a **native-format** colour (e.g. `hdmi.fill(hdmi.fb().colour(BLUE))`) |
| `hdmi.scroll(rows, colour=0)` | fast vertical scroll up by `rows` pixels, filling the exposed bottom with `colour` (native format) |
| `hdmi.putc(x, y, ch, fg, bg)` | blit one 8×12 console glyph at pixel `x,y` (native-format `fg`/`bg`) |
| `hdmi.test()` | draw an 8-bar colour test pattern |
| `hdmi.gen()` | mode-change counter (used internally by the console) |

Note that `hdmi.fill()`, `hdmi.scroll()` and `hdmi.putc()` take colours already
in the framebuffer's native format — convert with `hdmi.fb().colour(...)` first.

The on-screen text console uses an 8×12 font (80 columns × 40 rows at 640×480).

---

## 6. USB keyboard

A USB keyboard is detected automatically and drives both the on-screen console
and the serial REPL. On connect you will see `USB keyboard -> slot 1`.

### Layouts

```python
keymap("UK")      # set the layout (persisted across reboots)
keymap()          # return the current layout name
keymaps()         # list available layouts
```

Layouts: **US, UK, DE, FR, ES, BE**. The choice is saved and restored at boot.

### Lock LEDs

Caps Lock, Num Lock and Scroll Lock toggle the keyboard's physical LEDs, and the
correct LED state is set when a keyboard is plugged in.

---

## 7. USB mouse

A USB mouse is detected automatically (`USB mouse -> slot 2`). It maintains a
virtual cursor position (accumulated from movement, clamped to the screen,
starting centred). Read it with **`mouse(code)`**:

| `mouse(code)` | Returns |
|---|---|
| `"X"` / `"Y"` | cursor position in pixels |
| `"L"` / `"R"` / `"M"` | left / right / middle button (1 = pressed) |
| `"W"` | scroll-wheel accumulator |
| `"B"` | button bitmap (1 = L, 2 = R, 4 = M) |
| `"D"` | left-button double-click (within 500 ms; clears on read) |
| `"T"` | 3 if the mouse has a wheel, else 0 |
| `"PRESENT"` | 1 if a mouse is connected |
| `"SLOT"` | the HID slot (2) |

`mouse_speed(v)` sets movement sensitivity (higher = slower); `mouse_speed()`
returns it. Standard, high-resolution and 16-bit gaming mice are supported.

```python
import time
while True:
   print(mouse("X"), mouse("Y"), mouse("L"))
   time.sleep_ms(100)
```

---

## 8. USB touch screen

A USB multi-touch panel is detected automatically (`USB touch -> slot 4`). It
begins reporting a couple of seconds after connection. Read it with
**`touch(code)`**. Coordinates are in screen pixels; latched gestures are
one-shot (reading them clears the event).

**Position / contacts**

| `touch(code)` | Returns |
|---|---|
| `"X"` / `"Y"` | first contact, or **-1** when nothing is touching |
| `"DOWN"` / `"UP"` | 1/0 — is the screen being touched |
| `"X2"` / `"Y2"` | second contact, or -1 |
| `"XN", n` / `"YN", n` | nth contact (n = 1…); **n = 0 → number of contacts** |
| `"PRESENT"` / `"SLOT"` | panel connected? / its slot (4) |

**Gestures** (each clears when read)

| `touch(code)` | Returns |
|---|---|
| `"SWIPE"` | 0 none / 1 left / 2 right / 3 up / 4 down |
| `"SWL"` / `"SWR"` / `"SWU"` / `"SWD"` | 1 if that swipe just happened |
| `"TAP"` / `"HOLD"` / `"DTAP"` | tap / long-press / double-tap |
| `"PINCH"` | 0 none / 1 expand / 2 contract |
| `"EXPAND"` / `"CONTRACT"` | 1 if that pinch just happened |
| `"ROTATE"` | 0 none / 1 clockwise / 2 counter-clockwise |
| `"CW"` / `"CCW"` / `"TTAP"` | rotate CW / CCW / two-finger tap |

```python
import time
while True:
   if touch("DOWN"):
       print(touch("X"), touch("Y"))
   s = touch("SWIPE")
   if s:
       print("swipe", s)
   time.sleep_ms(20)
```

---

## 9. Audio

Audio plays through the on-board PCM5102 I2S DAC, in the **background** (the
prompt stays live while a track plays).

| Command | Description |
|---|---|
| `play(path)` | Play a `.wav`, `.mp3` or `.flac` file (dispatched by extension) |
| `play(path, wait=True)` | Play and block until finished |
| `stop()` | Stop playback immediately |
| `is_playing()` | True while a track is playing |
| `volume(v)` | Set volume 0–100 (perceptual/log taper); `volume()` returns it |
| `beep(freq=880, ms=150)` | Play a short tone |

```python
volume(70)
play("/sd/music/song.flac")
```

WAV, MP3 and FLAC are decoded on the fly. A short sound also plays when a USB
device is plugged in or removed (set `pcaudio.usb_sounds = False` to disable).

---

## 10. Images

Load pictures onto the HDMI framebuffer, or save the screen to a file.

| Command | Description |
|---|---|
| `draw_jpg(path, x=0, y=0, scale=1)` | Draw a JPEG (scale 1/2/4/8 downscales) |
| `draw_bmp(path, x=0, y=0)` | Draw a BMP (all common variants) |
| `draw_png(path, x=0, y=0, cutoff=20)` | Draw a PNG (alpha ≤ cutoff = transparent) |
| `save_image(path)` | Save the framebuffer to a 24-bit BMP file |

```python
draw_jpg("/sd/photo.jpg")
save_image("/sd/screen.bmp")
```

---

## 11. Real-time clock (DS3231)

The battery-backed DS3231 keeps time across power cycles. At boot the system
clock is synchronised from it automatically.

| Command | Description |
|---|---|
| `settime(year, month, day, hour, minute, second)` | Set the DS3231 (and system clock) |
| `settime()` | Set the DS3231 from the current system clock |
| `gettime()` | Read the DS3231, returns a `time.localtime`-style tuple |
| `synctime()` | Set the system clock from the DS3231 |

```python
settime(2026, 7, 4, 14, 30, 0)
print(gettime())
```

---

## 12. SD card

A FAT-formatted SD card is mounted at **`/sd`**. Cards are **hot-swappable**: a
background check (~twice a second) notices when a card is removed and unmounts it
(`Warning: SDcard removed`); inserting a card mounts it again
(`SDcard inserted`). Use the normal file API and the shell commands on `/sd`.

```python
ls("/sd")
f = open("/sd/data.txt", "w"); f.write("hello"); f.close()
```

---

## 13. Persistent settings

The keyboard layout and the HDMI mode/clock are saved in `/settings.json` on the
flash filesystem and restored at boot. `keymap("UK")` and `screen(...)` update
and persist automatically. Delete `/settings.json` (`rm("/settings.json")`) to
return to the defaults (US keyboard, 640×480 @ 252 MHz).

---

## 14. Networking (Wi-Fi / Bluetooth)

Wi-Fi and Bluetooth use the on-board CYW43 chip and the standard MicroPython
APIs — see the MicroPython docs for full details.

```python
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("ssid", "password")
print(wlan.isconnected(), wlan.ifconfig())
```

Bluetooth is available via the `bluetooth` module and the frozen `aioble`
helper. Networking helpers `mip`, `requests` and `ntptime` (from
bundle-networking) are included.

### HTTPS

TLS is built in (mbedTLS, TLS 1.2, client side). The `requests` module makes
HTTPS requests directly:

```python
import requests
r = requests.get("https://api.github.com")
print(r.status_code)
print(r.json())
r.close()
```

By default the server certificate is **not** verified; for verified HTTPS, pass
your CA certificate(s) to the underlying `ssl` context. See the `ssl` module docs.

### MQTT

The **`umqtt.simple`** and **`umqtt.robust`** MQTT clients are frozen in for
publish/subscribe messaging (over plain TCP or TLS). `umqtt.robust` adds
automatic reconnection.

```python
from umqtt.simple import MQTTClient

c = MQTTClient("picocomputer3", "broker.example.com")
c.connect()
c.publish(b"pc3/status", b"hello")

def on_msg(topic, msg):
   print(topic, msg)

c.set_callback(on_msg)
c.subscribe(b"pc3/cmd")
while True:
   c.check_msg()      # or c.wait_msg() to block
```

(Connect a WLAN first, as in the example above.)

---

## 15. Standard MicroPython modules in this build

All the usual MicroPython modules are present. The definitive list on your board
is `help('modules')`. Notable ones:

- **Core language / library:** `sys`, `gc`, `micropython`, `builtins`, `array`,
 `binascii`, `collections`, `errno`, `hashlib`, `heapq`, `io`, `json`, `math`,
 `cmath`, `os`, `platform`, `random`, `re`, `select`, `struct`, `time`,
 `deflate`, `asyncio`.
- **Hardware / board:** `machine`, `rp2`, `framebuf`, `vfs`.
- **Networking:** `network`, `socket`, `ssl`, `bluetooth`, plus `aioble`, `mip`,
 `requests`, `ntptime`, `umqtt.simple`, `umqtt.robust`.
- **Numerics:** `ulab` (a NumPy/SciPy-compatible array library — `ulab.numpy`,
 `ulab.scipy`).

For the API of every standard module, refer to the MicroPython documentation:
**https://docs.micropython.org/en/latest/library/index.html**

### Board-specific extension modules

These low-level C modules back the friendly commands above and can also be used
directly: `hdmi`, `keyboard`, `mouse`, `touch`, `audio`, `jpeg`, `bmp`, `png`.
Most users will prefer the auto-imported helpers (`play`, `draw_jpg`, `touch`,
`mouse`, `screen`, …) rather than these directly.

---

## Quick reference

```python
# Display
screen(hdmi.RGB320)             # change mode (persisted)
d = hdmi.fb(); d.text("hi", 0, 0, d.colour(WHITE))

# Input
touch("DOWN"); touch("X"); touch("SWIPE")
mouse("X"); mouse("L")
keymap("UK")

# Audio
volume(70); play("/sd/song.mp3"); stop()

# Files
ls("/sd/*.jpg"); run("/sd/app.py"); edit("/sd/app.py")

# Images / clock
draw_jpg("/sd/pic.jpg"); save_image("/sd/screen.bmp")
settime(2026, 7, 4, 14, 30, 0); print(gettime())
```

*Pico Computer 3 firmware v0.1 — based on MicroPython. See
https://docs.micropython.org/ for the Python language and standard library.*


Edited 2026-07-05 00:47 by matherp
 
Posted: 06:19pm
05 Jul 2026
Copy link to clipboard
matherp
Guru

v0.2

firmware.zip

Full manual

Video of Micropython solving Sudoku here . Program below.

Now supports xmodem and a different way of displaying text

| `hdmi.putc(x, y, ch, fg, bg)` | blit one 8×12 console glyph at pixel `x,y` (native-format `fg`/`bg`) |
| `hdmi.text(s, x, y, fg, bg=-1, scale=1)` | draw a string in the 8×12 console font at pixel `x,y`; `bg=-1` is transparent, `scale` enlarges each glyph pixel into a `scale`×`scale` block. Returns the x just past the string |
| `hdmi.test()` | draw an 8-bar colour test pattern |
xrecv("/sd/prog.py")     # receive a file INTO the board, then start an XMODEM *Send* in the terminal
xsend("/sd/prog.py")     # send a file FROM the board, then start an XMODEM *Receive* in the terminal
```

- `xrecv(path)` opens `path` for writing, sends `NAK`, and waits (up to ~60 s) for
 the terminal to start sending. Start the XMODEM **Send** in your terminal.
- `xsend(path)` waits for the terminal to start receiving, then transmits the
 file. Start the XMODEM **Receive** in your terminal.
- Works to both `/sd` and the internal flash filesystem (any path).
- While a transfer runs, the serial console is dedicated to the protocol (the
 REPL is paused and nothing is echoed to the HDMI screen); it returns to normal
 when the transfer finishes.
- 128-byte packets; the receiver uses the additive checksum, the sender accepts
 either checksum (`NAK`) or CRC-16 (`C`). Trailing packet padding is trimmed from
 received files, so a transferred `.py` runs as-is.
- On failure it raises `OSError` with a message (`Remote did not respond`,
 `Too many errors`, `Cancelled by remote`, …).

The full names are `xmodem.recv(path)` / `xmodem.send(path)` (`xrecv`/`xsend` are
just convenience aliases injected into the REPL).

# Graphical Sudoku solver for the Pico Computer 3 (MicroPython, HDMI output).
# Iterative back-tracking (no deep recursion) so it never overflows the stack.
# Uses hdmi.text (8x12 font) if the firmware has it, else a scaled framebuf font.
# Run:  run("sudoku.py")   — Ctrl-C stops and restores the console.
import hdmi, framebuf, time

PUZZLE = (
   (5, 3, 0, 0, 7, 0, 0, 0, 0),
   (6, 0, 0, 1, 9, 5, 0, 0, 0),
   (0, 9, 8, 0, 0, 0, 0, 6, 0),
   (8, 0, 0, 0, 6, 0, 0, 0, 3),
   (4, 0, 0, 8, 0, 3, 0, 0, 1),
   (7, 0, 0, 0, 2, 0, 0, 0, 6),
   (0, 6, 0, 0, 0, 0, 2, 8, 0),
   (0, 0, 0, 4, 1, 9, 0, 0, 5),
   (0, 0, 0, 0, 8, 0, 0, 7, 9),
)
STEP_MS = 4  # animation delay per placement; 0 = instant, higher = slower

disp = hdmi.fb()
W, H = hdmi.width(), hdmi.height()
HAVE_HTEXT = hasattr(hdmi, "text")   # 8x12 console font available?
FH = 12 if HAVE_HTEXT else 8

PANEL = 150 if W >= 560 else 0
MARGIN = 16
avail = min(H - 2 * MARGIN, W - PANEL - 2 * MARGIN)
CELL = avail // 9
GRID = CELL * 9
OX = MARGIN + ((W - PANEL - 2 * MARGIN) - GRID) // 2
OY = MARGIN + ((H - 2 * MARGIN) - GRID) // 2
SCALE = max(1, (CELL - 6) // FH)
GOX = (CELL - 8 * SCALE) // 2
GOY = (CELL - FH * SCALE) // 2

C_BG = disp.colour(0xFFFFFF)
C_LINE = disp.colour(0x000000)
C_CLUE = disp.colour(0x000000)
C_TRY = disp.colour(0xFF8000)
C_OK = disp.colour(0x0000FF)
C_HILITE = disp.colour(0xFFF0C0)
C_DONE = disp.colour(0x00A000)
C_TXT = disp.colour(0x000000)
PX = OX + GRID + 20

_glyph = bytearray(8)
_gfb = framebuf.FrameBuffer(_glyph, 8, 8, framebuf.MONO_HLSB)

def put(s, x, y, col, scale=1):
   if HAVE_HTEXT:
       hdmi.text(s, x, y, col, -1, scale)
       return
   for ch in s:
       _gfb.fill(0)
       _gfb.text(ch, 0, 0, 1)
       for gy in range(8):
           row = _glyph[gy]
           if row:
               yy = y + gy * scale
               bit = 0x80
               for gx in range(8):
                   if row & bit:
                       disp.fill_rect(x + gx * scale, yy, scale, scale, col)
                   bit >>= 1
       x += 8 * scale

def cell_xy(r, c):
   return OX + c * CELL, OY + r * CELL

def draw_cell(r, c, val, col, bg=None):
   x, y = cell_xy(r, c)
   disp.fill_rect(x + 1, y + 1, CELL - 1, CELL - 1, bg if bg is not None else C_BG)
   if val:
       put(str(val), x + GOX, y + GOY, col, SCALE)

def draw_grid():
   disp.fill(C_BG)
   for i in range(10):
       disp.vline(OX + i * CELL, OY, GRID + 1, C_LINE)
       disp.hline(OX, OY + i * CELL, GRID + 1, C_LINE)
   for i in range(0, 10, 3):
       gx, gy = OX + i * CELL, OY + i * CELL
       disp.vline(gx - 1, OY, GRID + 1, C_LINE)
       disp.vline(gx + 1, OY, GRID + 1, C_LINE)
       disp.hline(OX, gy - 1, GRID + 1, C_LINE)
       disp.hline(OX, gy + 1, GRID + 1, C_LINE)

board = [list(row) for row in PUZZLE]

def valid(r, c, n):
   for i in range(9):
       if board[r][i] == n or board[i][c] == n:
           return False
   br, bc = (r // 3) * 3, (c // 3) * 3
   for i in range(br, br + 3):
       for j in range(bc, bc + 3):
           if board[i][j] == n:
               return False
   return True

def status(msg, col):
   if PANEL:
       disp.fill_rect(PX, OY + 70, PANEL - 20, 16 * SCALE, C_BG)
       put(msg, PX, OY + 70, col, 2)

def solve():
   empties = [(r, c) for r in range(9) for c in range(9) if board[r][c] == 0]
   n_empty = len(empties)
   i = 0
   steps = 0
   while 0 <= i < n_empty:
       r, c = empties[i]
       n = board[r][c] + 1        # resume just above the last value tried here
       board[r][c] = 0            # clear so valid() ignores our own cell
       while n <= 9 and not valid(r, c, n):
           n += 1
       if n <= 9:
           steps += 1
           board[r][c] = n
           draw_cell(r, c, n, C_TRY, C_HILITE)
           if STEP_MS:
               time.sleep_ms(STEP_MS)
           if PANEL and steps % 32 == 0:
               disp.fill_rect(PX, OY + 104, PANEL - 20, 12, C_BG)
               put("steps: %d" % steps, PX, OY + 104, C_TXT)
           i += 1
       else:
           draw_cell(r, c, 0, C_TXT)   # un-fill on backtrack
           i -= 1
   return i == n_empty, steps

def main():
   try:
       console(False)
   except NameError:
       pass
   draw_grid()
   for r in range(9):
       for c in range(9):
           if board[r][c]:
               draw_cell(r, c, board[r][c], C_CLUE)
   if PANEL:
       put("SUDOKU", PX, OY, C_OK, 3 if HAVE_HTEXT else 2)
       put("SOLVER", PX, OY + (36 if HAVE_HTEXT else 20), C_OK, 3 if HAVE_HTEXT else 2)
   t0 = time.ticks_ms()
   ok, steps = solve()
   dt = time.ticks_diff(time.ticks_ms(), t0)
   if ok:  # repaint the solved-in digits blue
       for r in range(9):
           for c in range(9):
               if not PUZZLE[r][c]:
                   draw_cell(r, c, board[r][c], C_OK)
   if PANEL:
       status("SOLVED!" if ok else "no soln", C_DONE if ok else C_TRY)
       put("steps: %d" % steps, PX, OY + 104, C_TXT)
       put("%d ms" % dt, PX, OY + 124, C_TXT)
   print("solved" if ok else "no solution", "in", steps, "steps,", dt, "ms")

try:
   main()
except KeyboardInterrupt:
   print("\nstopped")

Edited 2026-07-06 04:21 by matherp
 
Posted: 06:47pm
10 Jul 2026
Copy link to clipboard
okwatts
Regular Member

I am no micropython expert (actually no expert at most anything at all) but I did give this a go while waiting for my Pico Computer 3. I am using the HDMIUSBI2S reference board and most everything works (no wifi at the moment) except the sdcard because of the different pins used for the interface between the 2 boards. With the xmodem commands added in v0.2, I can transfer files to the flash drive to try things out.
Is it possible to adjust the sdcard pin outs with a simple edit/recompile? This would make it accessible to more folks at least in the testing phase even if the "full blown" version is intended for the Pico Computer 3.
 
Posted: 01:47am
11 Jul 2026
Copy link to clipboard
scruss
Senior Member

MicroPython already has a very solid file transfer/utility/terminal in mpremote.

I've been using MicroPython solidly for almost a decade. It's good.
 
Posted: 07:59am
11 Jul 2026
Copy link to clipboard
matherp
Guru

V0.3

firmwarev0.3.zip

Manual

The build should support mpremote but you would need to specify the port as the console is on a UART and not USB-CDC

  Quote  Is it possible to adjust the sdcard pin outs with a simple edit/recompile? This would make it accessible to more folks at least in the testing phase even if the "full blown" version is intended for the Pico Computer 3.

Not really sensibly doable as the previous board didn't use SPI pins for the SDcard (An error on my part in the design of that board but MMBasic doesn't care)

What's new since v0.2
Display: native 1024×600 16-colour mode

   hdmi.RGB1024 — a true native 1024×600 mode at 16 colours (4bpp packed,
   RGB121), an exact fit in the existing framebuffer with no RAM increase.
   Sits alongside RGB512 (which fakes 1024×600 by pixel-doubling 512×300).
   Settable, persistent 16-colour palette — hdmi.palette() gets/sets the
   palette live; palette(...) (via pcconfig) persists it and restores it at
   boot.
   Format-aware console & graphics — fill/scroll/putc/text/blit and
   pcgfx.Display all work in RGB1024, so the on-screen console runs in the new
   mode.
   Image loaders gained a 4bpp path — draw_jpg/draw_bmp/draw_png and
   save_image map to the nearest of 16 palette colours, with optional
   error-diffusion dithering (dither=True = Atkinson, 1 = Floyd–Steinberg,
   2 = Atkinson) for JPEG/BMP, covering both RGB1024 and RGB640.

Shell

   Paged cat — cat("/big.py") now shows one screenful at a time and pauses
   at a PRESS ANY KEY ... prompt (any key = next page, q or Ctrl-C to stop),
   so long files no longer scroll off the HDMI display. cat(path, False) still
   dumps the whole file continuously.
   Wildcards in cp / mv / rm — a * / ? glob in the last path
   component now expands to every matching file (reusing the ls glob engine):
   cp("*.py", "/sd"), mv("*.bas", "/sd/bas"), rm("*.tmp"). Linux-style:
   sub-directories are skipped, an unmatched pattern is an error, and the
   destination must be a directory when more than one file matches.

Misc

   REPL banner keeps the -preview marker so the build doesn't masquerade as an
   unreleased MicroPython 1.29.0.
 
Posted: 02:46pm
11 Jul 2026
Copy link to clipboard
PhenixRising
Guru

Hey Pete, do we have a benchmark yet? Just curious  
 
Posted: 06:43pm
12 Jul 2026
Copy link to clipboard
scruss
Senior Member

  PhenixRising said  Hey Pete, do we have a benchmark yet? Just curious  


If those are the Rugg-Feldman/PCW benchmarks, they're getting too short to measure on modern microcontrollers. They also measure completely arbitrary metrics that have little relevance to modern problems. Old BASIC interpreters ran slower depending on how you spaced your code, and in pathological cases (Commodore PET v1) how you numbered the lines.

On looking down the thread a bit, I see the Maximite benchmark code. Never gonna work in MicroPython: GOTO isn't supported.

For some things, MicroPython is going to appear slow. Integer handling, for example. But MicroPython uses bigints, so they don't roll over at arbitrary bit boundaries like 2^63.

MicroPython also includes a couple of optimised code emitters that can speed up code many times. There's also a built-in assembler, but that makes the code difficult to port. There are details here: Maximising MicroPython speed.
Edited 2026-07-13 04:48 by scruss
 
Posted: 11:47pm
12 Jul 2026
Copy link to clipboard
okwatts
Regular Member

Just a note that in my testing so far (albeit HDMIUSBI2S version) the beep command starts to distort for freq>700 Hz. Eg beep(700,150) is fine, beep(701,150) has a blip included and it gets worse as you increase frequency. MMBasic 6.03 Play Tone(750,750,150)on the same board is fine.
 
Posted: 07:46am
13 Jul 2026
Copy link to clipboard
matherp
Guru

Thanks - will fix. Big update coming later today (I hope)
 
Posted: 09:35am
13 Jul 2026
Copy link to clipboard
matherp
Guru

v0.4


firmwarev0.4.zip

Manual

What's new since v0.3
Audio: tracker music, tones and a synthesiser

   MOD tracker playback — play("/sd/song.mod", loop=True) plays Amiga MOD
   music with the hxcmod engine (returns the song title). mod_sample(n) fires
   one of the module's instrument samples as a sound effect mixed over the
   running music — background music plus game SFX from a single file.
   tone(left, right, ms) — two independent sine-wave channels. Calling
   tone() again while one plays retunes it seamlessly (click-free), so
   melodies and arpeggios work.
   sound(voice, side, wave, freq, vol) — a 4-voice synthesiser with
   sine / square / triangle / sawtooth / periodic-noise / white-noise waveforms
   per voice and side; volume changes ramp to avoid clicks.
   pause() / resume() for the current playback.

Graphics: layers, off-screen buffers, a blitter and a sprite engine

   Overlay layer + off-screen framebuffer — hdmi.layer() adds a
   transparent overlay merged live over the display (RGB320); hdmi.create()
   adds an off-screen buffer in PSRAM. hdmi.write("N"/"L"/"F") chooses where
   all drawing goes (including the image loaders and console), and
   hdmi.copy(src, dst) block-copies whole buffers.
   hdmi.blit(x, y, w, h, x1, y1, src, dst, skip) — a rectangle blitter
   that copies within or between buffers (or to/from RAM (buffer, w, h)
   surfaces), with an optional skip colour for cut-out sprites; overlapping
   copies are safe in any direction.
   hdmi.vsync() — wait for vertical blanking for tear-free updates and
   frame pacing (input and audio keep running while it waits).
   pcsprite — a full sprite engine (import pcsprite): layers, AABB
   collisions (sprites, screen edges and static walls, edge-triggered), z-order
   and background scrolling — MMBasic sprite semantics on a dirty-rectangle /
   overlay-layer compositor. update() commits moves and returns collisions.

Input

   keydown(n) — read which keys are held right now (not just typed
   characters) — essential for games: count, per-key codes, modifier and lock
   bitmaps.
   keyboard.on_key(cb) — a callback on every keypress / auto-repeat.
   Num Lock now defaults on and, when off, the numeric keypad acts as the
   navigation cluster (arrows / Home / End / PgUp / PgDn / Ins / Del).

Console

   console("both" / "serial" / "screen") routes console output
   (MMBasic OPTION CONSOLE) — keep prints off the HDMI screen while testing
   graphics, or off the serial port; keyboard input always works. Power-up
   default is both.
   On-screen editor scrolling now works: the pcconsole terminal emulator
   gained a scroll region (DECSTBM) and reverse index, so the pye editor
   scrolls correctly on the HDMI screen (not just over serial).

Reliability

   DS3231 RTC survives a live CPU-clock change — the I2C bus is rebuilt when
   screen(mode, 315/378) changes the clock, so gettime/settime stay
   correct.
   On-device test suite — boards/PICO_COMPUTER_3/tests/ (copy to the SD
   card, run("test_all.py")): pixel-verified automatic tests for the blitter,
   buffers, sprites and images, plus interactive tests for the keyboard, audio
   and console routing.

See USER_MANUAL.md (in the board directory) for full documentation.
 


To reply to this topic, you need to log in.

The Back Shed's forum code is written, and hosted, in Australia.
© JAQ Software 2026