Skip to content

Circuitpython

joric edited this page May 21, 2023 · 312 revisions

With Circuitpython it's possible to write software without compiling, simply by editing text files on the USB drive. nRF52840 has 256K RAM and 1M flash so you get about 300K free for your source code, even more with frozen modules. So you do NOT really need an external flash chip, the main issue is that Python is super slow, booting takes about 5-10 seconds, depending on your code, considering wake up after sleep is basically rebooting that may be very annoying.

Downloads

Download nRFMicro .uf2 firmware here (I forked the repository but haven't submitted the PR just yet):

To flash .uf2 firmware to nRFMicro just press reset twice within 500 ms and copy the file to the USB drive.

  • After flashing the Python firmware, just copy your Python code to the code.py file in the USB root, it runs automatically.
  • To debug Python code, connect to virtual COM port at 115200, use Ctrl+C/Ctrl+D to break/reload and read the messages.

Debugging

Put the main code into code.py file on the USB drive root. All changes apply instantly after saving the file, no reset needed. To debug you just connect to virtual COM port at 115200, and use Ctrl+C to break and/or Ctrl+D to reload and read the messages. See virtual COM port number in windows Device Manager. I use Kitty (http://www.9bis.net/kitty) as a terminal.

debugging

Examples

Copy to USB root as code.py.

Blink

import time
import board
from digitalio import DigitalInOut, Direction, Pull

led = DigitalInOut(board.P1_10) # blue led
led.direction = Direction.OUTPUT

while True:
    led.value = True
    time.sleep(0.5)
    led.value = False
    time.sleep(0.5)

OLED

You will need the font5x8.bin font file for OLED (put it to the root directory).

import adafruit_ssd1306
import board
import busio

i2c = busio.I2C(board.SCL, board.SDA)
oled = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)

oled.fill(0)
oled.text("hello world", 0, 0, 1)
oled.show()

RGB

import neopixel
import time

leds = 6
pixels = neopixel.NeoPixel(board.P0_06, leds, auto_write=False)
for i in range(leds):
    pixels[i] = (0, 0, 32)
    time.sleep(0.1)
    pixels.show()
for i in range(leds):
    pixels[i] = (0, 0, 0)
    time.sleep(0.1)
    pixels.show()

Building

ONLY if you want to build your own custom python core. Use stock fimware above if you just want to run python.

nRFMicro

Normally you don't need to build anything, just download the .uf2 binary above, but if you want to build from scratch:

git clone https://github.com/joric/circuitpython
cd circuitpython
git checkout nrfmicro
git submodule sync
git submodule update --init
sudo apt install gettext
sudo pip install mpy-cross
make -C mpy-cross
cd ports/nrf
make BOARD=nrfmicro

Binary is saved to ports/nrf/build-nrfmicro/firmware.uf2. Configuration is in ports/nrf/boards/nrfmicro (I added more pins and removed XTAL requirement). This binary is basically chip-agnostic for all nRF52840-based boards, all pins are available. It contains frozen modules (see below).

Adafruit CircuitPython 6.0.0-beta.2-dirty on 2020-10-14; nrfmicro with nRF52840
>>> import board
>>> dir(board)
['__class__', 'AIN0', 'AIN2', 'AIN5', 'AIN7', 'I2C', 'LED', 'MISO', 'MOSI', 'NFC1', 'NFC2', 'P0_01', 'P0_02',
'P0_03', 'P0_04', 'P0_05', 'P0_06', 'P0_07', 'P0_08', 'P0_09', 'P0_10', 'P0_11', 'P0_12', 'P0_13', 'P0_14',
'P0_15', 'P0_16', 'P0_17', 'P0_18', 'P0_19', 'P0_20', 'P0_21', 'P0_22', 'P0_23', 'P0_24', 'P0_25', 'P0_26',
'P0_28', 'P0_29', 'P0_30', 'P0_31', 'P1_00', 'P1_01', 'P1_02', 'P1_03', 'P1_04', 'P1_05', 'P1_06', 'P1_07',
'P1_08', 'P1_09', 'P1_10', 'P1_11', 'P1_12', 'P1_13', 'P1_14', 'P1_15', 'RX', 'SCK', 'SCL', 'SDA', 'SPI',
'TX', 'UART', 'VCC_OFF', 'VOLTAGE_MONITOR']

nice!nano

Note that nice!nano already has its own binary hosted here: https://circuitpython.org/board/nice_nano (see PR #3017). The pinout is slightly different (see Pinout) e.g. SCL and SDA pins are 17/20 not 15/17 as on nRFMicro and LED is not 1.10 but 0.15. You may compare configurations side by side in pins.c files: nice_nano/pins.c, nrfmicro/pins.c.

Adafruit CircuitPython 6.0.0-beta.2 on 2020-10-05; nice!nano with nRF52840
>>> import board
>>> dir(board)
['__class__', 'AIN0', 'AIN2', 'AIN5', 'AIN7', 'BAT_VOLT', 'I2C', 'LED', 'MISO', 'MOSI', 'NFC1', 'NFC2',
'P0_02', 'P0_04', 'P0_06', 'P0_08', 'P0_09', 'P0_10', 'P0_11', 'P0_12', 'P0_13', 'P0_15', 'P0_17', 'P0_20',
'P0_22', 'P0_24', 'P0_26', 'P0_29', 'P0_31', 'P1_00', 'P1_01', 'P1_02', 'P1_04', 'P1_06', 'P1_07', 'P1_11',
'P1_13', 'P1_15', 'RX', 'SCK', 'SCL', 'SDA', 'SPI', 'TX', 'UART', 'VCC_OFF']

Frozen modules

It's much faster to run modules from the Circuitpython binary rather than from the filesystem. They also need less RAM.

Basically what I did is modified mpconfigboard.mk:

USB_VID = 0x239A
USB_PID = 0x80B4
USB_PRODUCT = "nrfmicro"
USB_MANUFACTURER = "joric"

MCU_CHIP = nrf52840

INTERNAL_FLASH_FILESYSTEM = 1

FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_BLE
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_HID
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_BusDevice
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_NeoPixel
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_SSD1306
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_framebuf

And added a couple of missing repositories into the /frozen folder (use git submodule add):

cd ~/circuitpython/frozen
git submodule add https://github.com/adafruit/Adafruit_CircuitPython_SSD1306
git submodule add https://github.com/adafruit/Adafruit_CircuitPython_framebuf

Then build as usual and you would have a binary that doesn't need external modules.

You can also precompile your custom keyboard-related modules this way (KMK firmware does that already).

External modules

You can use help('modules') in the python prompt to list all the available modules.

To add external modules, download 6.x bundle from https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases and copy files to internal USB drive (you can find font5x8.bin in the examples folder). You could also use Circup.

(nRFMicro build already includes those modules, no need to add them.)

  • /lib/adafruit_ble
  • /lib/adafruit_hid
  • /lib/adafruit_bus_device
  • /lib/adafruit_framebuf.mpy
  • /lib/adafruit_ssd1306.mpy
  • /lib/neopixel.mpy
  • /font5x8.bin

disk structure

Keyboard firmware

TL;DR

A minimal code that runs OLED, RGB, BLE, USB and key matrix and doesn't weight a ton (copy to the Python USB drive):

python-keyboard firmware

There's a lightweight firmware that boots relatively fast:

I've made a branch that runs on nrfmicro, working on it (you can copypaste pieces from there):

KMK firmware

There's keyboard firmware (in progress) that uses Circuitpython (no BLE support at master, just USB and UART via TRRS):

There's an experimental branch that runs wireless split keyboards:

You can just copy .py files along with the KMK folder to disk but It's better to compile KMK to save some RAM. Use make compile to build it (you will need circuitpython compiler from circuitpython package, built in the make -C mpy-cross stage). If you see maximum recursion depth exceeded that means you ran out of RAM. Try editing boot.py or something (e.g 4096+2048).

You can also try the following (you will need OLED libraries in your /lib directory, see examples above).

  • kmk - put (compiled) kmk firmware files from the repository to the kmk folder
  • main.py - put to the root folder
  • jorne.py - put to the root folder

The result looks like this (matrix works, keymap works):

Misc

Status LED and Safe Mode

Looks like Circuitpython boot time is affected by the startup LED that just stalls the entire startup for 700 ms to blink.

Use this build to boot faster (I've just commented out wait_for_safe_mode_reset call in main.c), it's near instant now:

Also make sure your python has no gc.collect() calls and modules are either compiled or built (frozen) into the .uf2 file.

Hiding USB drive on boot

Looks like it's still a feature in progress. Micropython has pyb module that you can use in boot.py to hide MSC:

import pyb
#pyb.usb_mode('VCP+MSC') # act as a serial and a storage device
pyb.usb_mode('VCP+HID') # act as a serial device and a mouse

Circuitpython has no pyb or machine modules. This feature while useful is not critical because wireless keyboards are usually not connected to USB.

References