Merge pull request #8218 from jepler/esp32-camera

Update the camera board to use ESP32-S3 microcontroller
This commit is contained in:
Jeff Epler 2023-07-31 14:48:50 -05:00 committed by GitHub
commit 272a2dcdb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 165 additions and 133 deletions

View File

@ -148,50 +148,21 @@ def get_board_mapping():
return boards
def read_mpconfig():
"""Open 'circuitpy_mpconfig.mk' and return the contents."""
configs = []
cpy_mpcfg = get_circuitpython_root_dir() / "py" / "circuitpy_mpconfig.mk"
with open(cpy_mpcfg) as mpconfig:
configs = mpconfig.read()
return configs
def build_module_map():
"""Establish the base of the JSON file, based on the contents from
`configs`. Base will contain module names, if they're part of
the `FULL_BUILD`, or their default value (0, 1, or a list of
modules that determine default [see audiocore, audiomixer, etc.]).
`configs`. Base contains the module name and the controlling C macro name.
"""
base = dict()
modules = get_bindings()
configs = read_mpconfig()
full_build = False
for module in modules:
full_name = module
if module in ADDITIONAL_MODULES:
search_identifier = ADDITIONAL_MODULES[module]
else:
search_identifier = "CIRCUITPY_" + module.lstrip("_").upper()
re_pattern = f"{re.escape(search_identifier)}\s*\??=\s*(.+)"
find_config = re.findall(re_pattern, configs)
if not find_config:
continue
find_config = ", ".join([x.strip("$()") for x in find_config])
full_build = int("CIRCUITPY_FULL_BUILD" in find_config)
if not full_build:
default_val = find_config
else:
default_val = "None"
base[module] = {
"name": full_name,
"full_build": str(full_build),
"default_value": default_val,
"excluded": {},
"key": search_identifier,
}
@ -199,15 +170,14 @@ def build_module_map():
def get_settings_from_makefile(port_dir, board_name):
"""Invoke make in a mode which prints the database, then parse it for
settings.
"""Invoke make to print the value of critical build settings
This means that the effect of all Makefile directives is taken
into account, without having to re-encode the logic that sets them
in this script, something that has proved error-prone
"""
contents = subprocess.run(
["make", "-C", port_dir, f"BOARD={board_name}", "-qp", "print-CC"],
["make", "-C", port_dir, "-f", "Makefile", f"BOARD={board_name}", "print-CFLAGS", "print-CIRCUITPY_BUILD_EXTENSIONS", "print-FROZEN_MPY_DIRS"],
encoding="utf-8",
errors="replace",
stdout=subprocess.PIPE,
@ -223,9 +193,10 @@ def get_settings_from_makefile(port_dir, board_name):
settings = {}
for line in contents.stdout.split("\n"):
# Handle both = and := definitions.
m = re.match(r"^([A-Z][A-Z0-9_]*) :?= (.*)$", line)
if m:
if line.startswith('CFLAGS ='):
for m in re.findall('-D([A-Z][A-Z0-9_]*)=(\d+)', line):
settings[m[0]] = m[1]
elif m := re.match(r"^([A-Z][A-Z0-9_]*) = (.*)$", line):
settings[m.group(1)] = m.group(2)
return settings
@ -268,6 +239,10 @@ def get_repository_url(directory):
repository_urls[directory] = path
return path
def remove_prefix(s, prefix):
if not s.startswith(prefix):
raise ValueError(f"{s=} does not start with {prefix=}")
return s.removeprefix(prefix)
def frozen_modules_from_dirs(frozen_mpy_dirs, withurl):
"""
@ -280,7 +255,8 @@ def frozen_modules_from_dirs(frozen_mpy_dirs, withurl):
"""
frozen_modules = []
for frozen_path in filter(lambda x: x, frozen_mpy_dirs.split(" ")):
source_dir = get_circuitpython_root_dir() / frozen_path[7:]
frozen_path = remove_prefix(frozen_path, '../../')
source_dir = get_circuitpython_root_dir() / frozen_path
url_repository = get_repository_url(source_dir)
for sub in source_dir.glob("*"):
if sub.name in FROZEN_EXCLUDES:

View File

@ -330,13 +330,13 @@ $(BUILD)/esp-idf:
TARGET_SDKCONFIG = esp-idf-config/sdkconfig-$(IDF_TARGET).defaults
ifeq ($(CIRCUITPY_ESP_FLASH_SIZE), 2MB)
FLASH_SDKCONFIG = esp-idf-config/sdkconfig-$(CIRCUITPY_ESP_FLASH_SIZE)-no-ota-no-uf2.defaults
FLASH_SDKCONFIG ?= esp-idf-config/sdkconfig-$(CIRCUITPY_ESP_FLASH_SIZE)-no-ota-no-uf2.defaults
else
UF2_BOOTLOADER ?= $(CIRCUITPY_USB)
ifeq ($(UF2_BOOTLOADER), 1)
FLASH_SDKCONFIG = esp-idf-config/sdkconfig-$(CIRCUITPY_ESP_FLASH_SIZE).defaults
FLASH_SDKCONFIG ?= esp-idf-config/sdkconfig-$(CIRCUITPY_ESP_FLASH_SIZE).defaults
else
FLASH_SDKCONFIG = esp-idf-config/sdkconfig-$(CIRCUITPY_ESP_FLASH_SIZE)-no-uf2.defaults
FLASH_SDKCONFIG ?= esp-idf-config/sdkconfig-$(CIRCUITPY_ESP_FLASH_SIZE)-no-uf2.defaults
endif
endif

View File

@ -1,10 +0,0 @@
USB_VID = 0x239A
USB_PID = 0x8118
USB_PRODUCT = "Camera"
USB_MANUFACTURER = "Adafruit"
IDF_TARGET = esp32s2
CIRCUITPY_ESP_FLASH_MODE = dio
CIRCUITPY_ESP_FLASH_FREQ = 40m
CIRCUITPY_ESP_FLASH_SIZE = 4MB

View File

@ -1,33 +0,0 @@
CONFIG_ESP32S2_SPIRAM_SUPPORT=y
#
# SPI RAM config
#
# CONFIG_SPIRAM_TYPE_AUTO is not set
CONFIG_SPIRAM_TYPE_ESPPSRAM16=y
# CONFIG_SPIRAM_TYPE_ESPPSRAM32 is not set
# CONFIG_SPIRAM_TYPE_ESPPSRAM64 is not set
CONFIG_SPIRAM_SIZE=2097152
#
# PSRAM clock and cs IO for ESP32S2
#
CONFIG_DEFAULT_PSRAM_CLK_IO=30
CONFIG_DEFAULT_PSRAM_CS_IO=26
# end of PSRAM clock and cs IO for ESP32S2
# CONFIG_SPIRAM_FETCH_INSTRUCTIONS is not set
# CONFIG_SPIRAM_RODATA is not set
# CONFIG_SPIRAM_SPEED_80M is not set
CONFIG_SPIRAM_SPEED_40M=y
# CONFIG_SPIRAM_SPEED_26M is not set
# CONFIG_SPIRAM_SPEED_20M is not set
CONFIG_SPIRAM=y
CONFIG_SPIRAM_BOOT_INIT=y
# CONFIG_SPIRAM_IGNORE_NOTFOUND is not set
CONFIG_SPIRAM_USE_MEMMAP=y
# CONFIG_SPIRAM_USE_CAPS_ALLOC is not set
# CONFIG_SPIRAM_USE_MALLOC is not set
CONFIG_SPIRAM_MEMTEST=y
# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY is not set
# end of SPI RAM config

View File

@ -84,7 +84,7 @@ void board_init(void) {
MIPI_COMMAND_WRITE_MEMORY_START, // Write memory command
display_init_sequence,
sizeof(display_init_sequence),
&pin_GPIO41, // backlight pin
NULL, // backlight pin
NO_BRIGHTNESS_COMMAND,
1.0f, // brightness
false, // single_byte_bounds

View File

@ -27,13 +27,11 @@
// Micropython setup
#define MICROPY_HW_BOARD_NAME "Adafruit Camera"
#define MICROPY_HW_MCU_NAME "ESP32S2"
#define MICROPY_HW_MCU_NAME "ESP32S3"
#define MICROPY_HW_NEOPIXEL (&pin_GPIO21)
#define MICROPY_HW_NEOPIXEL (&pin_GPIO1)
#define MICROPY_HW_NEOPIXEL_COUNT (1)
#define MICROPY_HW_LED_STATUS (&pin_GPIO1)
#define DEFAULT_I2C_BUS_SDA (&pin_GPIO33)
#define DEFAULT_I2C_BUS_SCL (&pin_GPIO34)

View File

@ -0,0 +1,21 @@
USB_VID = 0x239A
USB_PID = 0x8118
USB_PRODUCT = "Camera"
USB_MANUFACTURER = "Adafruit"
IDF_TARGET = esp32s3
CIRCUITPY_ESP_FLASH_MODE = dio
CIRCUITPY_ESP_FLASH_FREQ = 40m
CIRCUITPY_ESP_FLASH_SIZE = 4MB
FLASH_SDKCONFIG = esp-idf-config/sdkconfig-4MB-1ota.defaults
CIRCUITPY_AUDIOBUSIO = 0
CIRCUITPY_CANIO = 0
CIRCUITPY_ESPCAMERA = 1
CIRCUITPY_FRAMEBUFFERIO = 0
CIRCUITPY_KEYPAD = 0
CIRCUITPY_ONEWIREIO = 0
CIRCUITPY_PARALLELDISPLAY = 0
CIRCUITPY_RGBMATRIX = 0
CIRCUITPY_ROTARYIO = 0

View File

@ -24,21 +24,21 @@ STATIC const mp_rom_map_elem_t board_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_MOSI), MP_ROM_PTR(&pin_GPIO35) },
{ MP_ROM_QSTR(MP_QSTR_SCK), MP_ROM_PTR(&pin_GPIO36) },
{ MP_ROM_QSTR(MP_QSTR_MISO), MP_ROM_PTR(&pin_GPIO37) },
{ MP_ROM_QSTR(MP_QSTR_TFT_BACKLIGHT), MP_ROM_PTR(&pin_GPIO41) },
{ MP_ROM_QSTR(MP_QSTR_TFT_DC), MP_ROM_PTR(&pin_GPIO40) },
{ MP_ROM_QSTR(MP_QSTR_TFT_CS), MP_ROM_PTR(&pin_GPIO39) },
{ MP_ROM_QSTR(MP_QSTR_TFT_RESET), MP_ROM_PTR(&pin_GPIO38) },
{ MP_ROM_QSTR(MP_QSTR_TFT_CS), MP_ROM_PTR(&pin_GPIO39) },
{ MP_ROM_QSTR(MP_QSTR_TFT_DC), MP_ROM_PTR(&pin_GPIO40) },
{ MP_ROM_QSTR(MP_QSTR_CARD_CS), MP_ROM_PTR(&pin_GPIO2) },
{ MP_ROM_QSTR(MP_QSTR_CARD_CS), MP_ROM_PTR(&pin_GPIO48) },
{ MP_ROM_QSTR(MP_QSTR_IRQ), MP_ROM_PTR(&pin_GPIO3) },
{ MP_ROM_QSTR(MP_QSTR_NEOPIXEL), MP_ROM_PTR(&pin_GPIO21) },
{ MP_ROM_QSTR(MP_QSTR_NEOPIXEL), MP_ROM_PTR(&pin_GPIO1) },
{ MP_ROM_QSTR(MP_QSTR_SPEAKER), MP_ROM_PTR(&pin_GPIO45) },
{ MP_ROM_QSTR(MP_QSTR_SPEAKER), MP_ROM_PTR(&pin_GPIO41) },
{ MP_ROM_QSTR(MP_QSTR_BUTTON), MP_ROM_PTR(&pin_GPIO0) },
{ MP_ROM_QSTR(MP_QSTR_LED), MP_ROM_PTR(&pin_GPIO1) },
{ MP_ROM_QSTR(MP_QSTR_BATTERY_MONITOR), MP_ROM_PTR(&pin_GPIO4) },
{ MP_ROM_QSTR(MP_QSTR_A0), MP_ROM_PTR(&pin_GPIO17) },
{ MP_ROM_QSTR(MP_QSTR_A1), MP_ROM_PTR(&pin_GPIO18) },

View File

@ -0,0 +1,62 @@
# Component config
#
#
# ESP32S3-Specific
#
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
#
# SPI RAM config
#
CONFIG_SPIRAM_MODE_QUAD=y
# CONFIG_SPIRAM_MODE_OCT is not set
CONFIG_SPIRAM_TYPE_AUTO=y
# CONFIG_SPIRAM_TYPE_ESPPSRAM16 is not set
# CONFIG_SPIRAM_TYPE_ESPPSRAM32 is not set
# CONFIG_SPIRAM_TYPE_ESPPSRAM64 is not set
CONFIG_SPIRAM_SIZE=2097152
#
# PSRAM Clock and CS IO for ESP32S3
#
CONFIG_DEFAULT_PSRAM_CLK_IO=30
CONFIG_DEFAULT_PSRAM_CS_IO=26
# end of PSRAM Clock and CS IO for ESP32S3
# CONFIG_SPIRAM_FETCH_INSTRUCTIONS is not set
# CONFIG_SPIRAM_RODATA is not set
# CONFIG_SPIRAM_SPEED_80M is not set
CONFIG_SPIRAM_SPEED_80M=y
# CONFIG_SPIRAM_SPEED_26M is not set
# CONFIG_SPIRAM_SPEED_20M is not set
CONFIG_SPIRAM=y
CONFIG_SPIRAM_BOOT_INIT=y
# CONFIG_SPIRAM_IGNORE_NOTFOUND is not set
CONFIG_SPIRAM_USE_MEMMAP=y
# CONFIG_SPIRAM_USE_CAPS_ALLOC is not set
# CONFIG_SPIRAM_USE_MALLOC is not set
CONFIG_SPIRAM_MEMTEST=y
# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY is not set
# end of SPI RAM config
# end of ESP32S3-Specific
#
# LWIP
#
CONFIG_LWIP_LOCAL_HOSTNAME="espressif-esp32s3"
# end of LWIP
# CONFIG_OV7670_SUPPORT is not set
# CONFIG_NT99141_SUPPORT is not set
CONFIG_OV3360_SUPPORT=n
# CONFIG_OV2640_SUPPORT is not set
CONFIG_OV5640_SUPPORT=y
# CONFIG_GC2145_SUPPORT is not set
# CONFIG_GC032A_SUPPORT is not set
# CONFIG_GC0308_SUPPORT is not set
# CONFIG_BF3005_SUPPORT is not set
# CONFIG_BF20A6_SUPPORT is not set
# CONFIG_SC101IOT_SUPPORT is not set
# CONFIG_SC030IOT_SUPPORT is not set
# end of Component config

View File

@ -0,0 +1,10 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
# bootloader.bin,, 0x1000, 32K
# partition table, 0x8000, 4K
nvs, data, nvs, 0x9000, 20K,
otadata, data, ota, 0xe000, 8K,
ota_0, app, ota_0, 0x10000, 2816K,
uf2, app, factory,0x2d0000, 256K,
ffat, data, fat, 0x310000, 960K,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 # bootloader.bin,, 0x1000, 32K
4 # partition table, 0x8000, 4K
5 nvs, data, nvs, 0x9000, 20K,
6 otadata, data, ota, 0xe000, 8K,
7 ota_0, app, ota_0, 0x10000, 2816K,
8 uf2, app, factory,0x2d0000, 256K,
9 ffat, data, fat, 0x310000, 960K,

View File

@ -0,0 +1,18 @@
#
# Serial flasher config
#
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=y
# end of Serial flasher config
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="esp-idf-config/partitions-4MB-1ota.csv"
#
# Partition Table
#
CONFIG_PARTITION_TABLE_FILENAME="esp-idf-config/partitions-4MB-1ota.csv"
# end of Partition Table

@ -1 +1 @@
Subproject commit 4ff7f348d0713ea8eca022f73a059b0fe0934531
Subproject commit 2cd2a6d69faaba9ebb6105814a50039d82e2f899

View File

@ -14,11 +14,8 @@ CIRCUITPY_FULL_BUILD ?= 1
CIRCUITPY_ALARM ?= 1
CIRCUITPY_ANALOGBUFIO ?= 1
CIRCUITPY_AUDIOBUSIO ?= 1
CIRCUITPY_AUDIOBUSIO_I2SOUT ?= 1
CIRCUITPY_AUDIOBUSIO_PDMIN ?= 0
CIRCUITPY_AUDIOCORE ?= 1
CIRCUITPY_AUDIOIO ?= 0
CIRCUITPY_AUDIOMIXER ?= 1
CIRCUITPY_AUDIOMP3 ?= 0
CIRCUITPY_BLEIO ?= 1
CIRCUITPY_BLEIO_HCI = 0

View File

@ -73,7 +73,9 @@ with open(sys.argv[2], "r") as f:
ota = None
app = None
for partition in csv.reader(f):
if partition[0][0] == "#":
if not partition: # empty row
continue
if partition[0].startswith("#"):
continue
subtype = partition[2].strip()
if subtype == "factory":

View File

@ -26,6 +26,22 @@
# Boards default to all modules enabled (with exceptions)
# Manually disable by overriding in #mpconfigboard.mk
# These Makefile variables are used to implement the "any" and "all" functions.
# Note that these only work when the arguments expand to "0" and/or "1" but not
# if they expand to other values like "yes", "/bin/sh", or "false".
#
# Make's "sort" will transform a mixed sequence of 0s and 1s to "0 1" (because
# it also eliminates duplicates), or a non-mixed sequence of "0" or "1" to just
# itself. Thus, if all the inputs are 1 then the first word will be 1; if any
# of the inputs are 1, then the last word will be 1.
enable-if-any=$(lastword $(sort $(1) 0))
enable-if-all=$(firstword $(sort $(1) 1))
# To use any/all, you "$(call)" it, with the values to test after a comma.
# Usually the values are other $(CIRCUITPY_foo) variables. The definition
# of CIRCUITPY_AUDIOCORE and CIRCUITPY_AUDIOMP3 below are typical of how
# any/all are expected to be used.
# Always on. Present here to help generate documentation module support matrix for "builtins".
CIRCUITPY = 1
CFLAGS += -DCIRCUITPY=$(CIRCUITPY)
@ -95,16 +111,10 @@ CFLAGS += -DCIRCUITPY_AUDIOIO=$(CIRCUITPY_AUDIOIO)
CIRCUITPY_AUDIOPWMIO ?= 0
CFLAGS += -DCIRCUITPY_AUDIOPWMIO=$(CIRCUITPY_AUDIOPWMIO)
ifndef CIRCUITPY_AUDIOCORE
ifeq ($(CIRCUITPY_AUDIOPWMIO),1)
CIRCUITPY_AUDIOCORE = $(CIRCUITPY_AUDIOPWMIO)
else
CIRCUITPY_AUDIOCORE = $(CIRCUITPY_AUDIOIO)
endif
endif
CIRCUITPY_AUDIOCORE ?= $(call enable-if-any,$(CIRCUITPY_AUDIOPWMIO) $(CIRCUITPY_AUDIOIO) $(CIRCUITPY_AUDIOBUSIO))
CFLAGS += -DCIRCUITPY_AUDIOCORE=$(CIRCUITPY_AUDIOCORE)
CIRCUITPY_AUDIOMIXER ?= $(CIRCUITPY_AUDIOIO)
CIRCUITPY_AUDIOMIXER ?= $(CIRCUITPY_AUDIOCORE)
CFLAGS += -DCIRCUITPY_AUDIOMIXER=$(CIRCUITPY_AUDIOMIXER)
ifndef CIRCUITPY_AUDIOCORE_DEBUG
@ -112,13 +122,7 @@ CIRCUITPY_AUDIOCORE_DEBUG ?= 0
endif
CFLAGS += -DCIRCUITPY_AUDIOCORE_DEBUG=$(CIRCUITPY_AUDIOCORE_DEBUG)
ifndef CIRCUITPY_AUDIOMP3
ifeq ($(CIRCUITPY_FULL_BUILD),1)
CIRCUITPY_AUDIOMP3 = $(CIRCUITPY_AUDIOCORE)
else
CIRCUITPY_AUDIOMP3 = 0
endif
endif
CIRCUITPY_AUDIOMP3 ?= $(call enable-if-all,$(CIRCUITPY_FULL_BUILD) $(CIRCUITPY_AUDIOCORE))
CFLAGS += -DCIRCUITPY_AUDIOMP3=$(CIRCUITPY_AUDIOMP3)
CIRCUITPY_BINASCII ?= $(CIRCUITPY_FULL_BUILD)
@ -200,15 +204,9 @@ endif
CFLAGS += -DCIRCUITPY_PARALLELDISPLAY=$(CIRCUITPY_PARALLELDISPLAY)
# bitmaptools and framebufferio rely on displayio
ifeq ($(CIRCUITPY_DISPLAYIO),1)
CIRCUITPY_BITMAPTOOLS ?= $(CIRCUITPY_FULL_BUILD)
CIRCUITPY_FRAMEBUFFERIO ?= $(CIRCUITPY_FULL_BUILD)
CIRCUITPY_VECTORIO ?= 1
else
CIRCUITPY_BITMAPTOOLS ?= 0
CIRCUITPY_FRAMEBUFFERIO ?= 0
CIRCUITPY_VECTORIO ?= 0
endif
CIRCUITPY_BITMAPTOOLS ?= $(call enable-if-all,$(CIRCUITPY_FULL_BUILD) $(CIRCUITPY_DISPLAYIO))
CIRCUITPY_FRAMEBUFFERIO ?= $(call enable-if-all,$(CIRCUITPY_FULL_BUILD) $(CIRCUITPY_DISPLAYIO))
CIRCUITPY_VECTORIO ?= $(CIRCUITPY_DISPLAYIO)
CFLAGS += -DCIRCUITPY_BITMAPTOOLS=$(CIRCUITPY_BITMAPTOOLS)
CFLAGS += -DCIRCUITPY_FRAMEBUFFERIO=$(CIRCUITPY_FRAMEBUFFERIO)
CFLAGS += -DCIRCUITPY_VECTORIO=$(CIRCUITPY_VECTORIO)
@ -255,12 +253,7 @@ CFLAGS += -DCIRCUITPY_FUTURE=$(CIRCUITPY_FUTURE)
CIRCUITPY_GETPASS ?= $(CIRCUITPY_FULL_BUILD)
CFLAGS += -DCIRCUITPY_GETPASS=$(CIRCUITPY_GETPASS)
ifeq ($(CIRCUITPY_DISPLAYIO),1)
#CIRCUITPY_GIFIO ?= $(CIRCUITPY_CAMERA)
CIRCUITPY_GIFIO ?= 1
else
CIRCUITPY_GIFIO ?= 0
endif
CIRCUITPY_GIFIO ?= $(CIRCUITPY_DISPLAYIO)
CFLAGS += -DCIRCUITPY_GIFIO=$(CIRCUITPY_GIFIO)
CIRCUITPY_GNSS ?= 0
@ -468,9 +461,7 @@ CFLAGS += -DCIRCUITPY_SYS=$(CIRCUITPY_SYS)
CIRCUITPY_TERMINALIO ?= $(CIRCUITPY_DISPLAYIO)
CFLAGS += -DCIRCUITPY_TERMINALIO=$(CIRCUITPY_TERMINALIO)
ifeq ($(CIRCUITPY_DISPLAYIO),1)
CIRCUITPY_FONTIO ?= $(CIRCUITPY_TERMINALIO)
endif
CIRCUITPY_FONTIO ?= $(call enable-if-all,$(CIRCUITPY_DISPLAYIO) $(CIRCUITPY_TERMINALIO))
CFLAGS += -DCIRCUITPY_FONTIO=$(CIRCUITPY_FONTIO)
CIRCUITPY_TIME ?= 1