Beta 1 ships from main at 4fa38d6 today. One commit landed since the
2026-09-29 re-freeze:
Two unmodified Adafruit libraries ran through this layer on a real Arduino Uno
with the beta 1 compiler: adafruit_ssd1306 (with adafruit_framebuf and
adafruit_bus_device, over busio.I2C) and adafruit_hcsr04 (its own simpletest).
Every other Adafruit library figure for this release comes from compilation and
the AVR emulator, not from a board.
- corpus: re-baselined every program's size after
pymcu-compiler's RFC 0013 phase 0 fix (every object of static duration is now zeroed at boot, matching what a real ATmega328P needs and an emulator that always started fresh state at zero used to hide). A program that keeps static state in SRAM pays the clear loop once, about +24 bytes (02_button_pullup178 to 204,21_digitalio_context170 to 194), plus 2 bytes per register that homes static state (03_pwm_fade,04_servo_sweepup to +38 bytes). A handful of programs moved down instead, from unrelated compiler work landed since the last baseline.
Beta 1 ships from main at f8677cc today. Three commits landed since the
2026-09-26 re-freeze:
- corpus:
42_except_as_e_args(try: raise RuntimeError("Timed out") except RuntimeError as e: print(e.args[0])) is re-baselined from 384 to 460 bytes. The growth is real and accepted, not a regression, and not related to floats: this program prints no float. Beforepymcu-compiler's fix (79c7d411/3a0623c7/23135d73),e.args[0]on a raise with no message printed garbage, the interrupt vector table's own bytes, instead of raisingIndexError: tuple index out of rangethe way CPython does. The fix adds that check, the "tuple index out of range" string, and anE:IndexErrordispatcher entry. This program's ownraise RuntimeError("Timed out")always carries a message, so itsargs[0]never actually raises, but the compiler cannot prove that no reachable raise in the program reaches this handler without one, so every handler readinge.args[0]pays for the check regardless. A follow-up fix in the same window (print(e.args[0])no longer asking twice whether the raise carried a message) trimmed 6 bytes back off (466 to 460). - corpus: every program that prints a
floatis re-baselined afterpymcu-compiler's float print policy fix (two fixed decimals, silently wrong, replaced by MicroPython's real algorithm): 4 corpus programs grow by roughly 4 KB each, the cost of the first_f32_repr/_f32_scalecall in a program that did not already pay for a string-formatting runtime.
- corpus:
pwmio's variable-frequency PWM idiom (32_pwm_variable_frequency, the CircuitPython buzzer pattern of retuning an exact Timer1 channel at run time while keeping its duty cycle) now builds instead of being pinned as a refusal, and a sibling program pins the still-refused case (two exact-frequency channels sharing Timer1'sICR1). Both measured againstpymcu-compilermain's stdlib, both front ends.
Beta 1 was first frozen on 2026-09-15 and the freeze was re-taken from main on 2026-09-26. These entries and the section below it are one release, split only by when the work landed.
- busio:
I2C.writeto,readfrom_intoandwriteto_then_readfromignored the HAL's return values, so a NACKed transaction looked like a successful one and atry/except Exceptionaround the call never ran — the dark-display field report. They now raiseOSErrorwith upstream's messages —[Errno 19] No such devicefor a NACKed address,[Errno 5] Input/output errorfor a failed START or a NACKed data byte — after sending STOP. That is the contractadafruit_bus_device.I2CDeviceexpects, so an absent device surfaces as itsValueError("No I2C device at address: 0x.."). The checks live in shared_i2c_writeto/_i2c_readfrombodies rather than expanding at every call site, and the slice bounds reach them as compile-time constants, so the raise costs the display driver a fraction of what a per-site expansion did. - bitbangio:
I2Cfollows the same contract on the software bus, reading the ACK bit the HAL bit-bang returns (0 = ACK).
Beta 1: this layer moves out of alpha alongside the frontend
(pymcu-compiler/pymcu-stdlib 0.1.0b1) and the AVR backend
(pymcu-avr 0.1.0b1), since AVR is the only chip family with real
CircuitPython hardware to compare against. Coverage on RP2040/RP2350
tracks the ARM backend, which stays alpha; keypad.KeyMatrix (#13)
and the storage/os modules (#16) are out of beta 1 by decision.
- boards: adafruit_trinket, and pico as an alias of raspberry_pi_pico
- microcontroller: disable_interrupts and enable_interrupts
- a micropython module, providing const and nothing else
- a sys module, providing implementation.name and nothing else
- boards: board.I2C(), board.SPI() and board.UART() on the AVR Arduino boards (#7)
- pulseio: PulseIn and PulseOut (#9)
- servo: adafruit_motor.servo, so an angle lands on its pulse width (#8)
- bitbangio: the same buses as busio, driven in software on any pins (#10)
- rainbowio: colorwheel(pos), the colour at a position on the wheel (#15)
- countio: Counter, the edges that arrived on a pin (#11)
- keypad: Keys and Event, which button changed and which way (#13)
- boards: board.I2C(), board.SPI() and board.UART() are back (#7)
- rotaryio: IncrementalEncoder, where a two-track knob has turned to (#12)
- boards: the bare 8-pin ATtinys answer to board.Dn
- neopixel_write: the low-level one-wire write, as CircuitPython spells it
- analogio,countio,digitalio,keypad,pwmio,rotaryio: exit matches upstream's (self, *args) arity
- busio: start/end and configure() become keyword-only, matching upstream
- bitbangio: start/end/configure() become keyword-only; write()'s buffer is spelled buf, and configure()'s baudrate default matches upstream's 100000
- pulseio: PulseIn.bool answers len(pulses) > 0; exit matches upstream's arity
- watchdog: WatchDogTimer is exported as the type of microcontroller.watchdog
- microcontroller: implement RunMode and on_next_reset
- keypad: Event gains timestamp, EventQueue is public, bool added
- microcontroller: cpu.frequency returns the clock instead of failing to compile
- remove two MicroPython spellings and move WatchDogMode where upstream keeps it
- alarm: the tail is written as the else it already logically is
- net: refuse on a chip with no radio, and say that is the reason
- pwmio: PWMOut.frequency reprograms the timer, and is refused without variable_frequency (#5)
- pwmio: deinit() releases the pin as an input (PyMCU#296)
- pwmio: the constructor leaves the start to the HAL (PyMCU#296)
- pwmio: duty_cycle goes to the HAL as the 16-bit value it is (#30)
- digitalio: deinit() releases the pin without pull, and switch_to_output sets the level first (PyMCU#309)
- analogio: the value covers the whole range, the reference is the chip's, and AnalogOut is refused where there is no DAC (#21)
- busio: every parameter reaches the hardware or is refused, and a read has a deadline (#22, #23, #24)
- pwmio: frequency reports what the pin emits, and the first parameter is pin (#18)
- time, supervisor, nvm, watchdog: four values that were not the part's (#26, #25, #16, #19)
- microcontroller: uid refused rather than eight zeros, cpus has a length (#27)
- alarm: wait on several alarms, and say which one fired (#20)
- time: sleep() folds to one calibrated delay for a literal duration (#26)
- microcontroller: len(nvm) is a literal again, because a slice of nvm needs one (#16)
- digitalio: switch_to_output's value default is False, as upstream spells it
- micropython: const()'s parameter is named expr, as upstream spells it
- rainbowio: colorwheel()'s parameter is named n, as upstream spells it
- watchdog: move WatchDogTimer into watchdog.py, breaking the import cycle
- time: sleep() written with plain locals now that a constant survives one (PyMCU#327)
- the README describes the modules and the spellings that exist (#29)
- neopixel_write joins the supported module table
- corpus: triage the refused programs
- readme: document the API surface this class of AVR chip will not provide
- parity: regenerate the parity report
- the conftest stubs CHIP as an object, pymcu.hal.irq, FREQ and the layer's own module names (#6)
- the conftest binds TIMEBASE like FREQ (PyMCU#295)
- the analogio mocks follow the HAL's ADC and DAC, and the tests measure what changed (#21)
- the busio mocks follow the HAL, and the tests measure what reaches the hardware (#22, #23, #24, #7)
- pulse mocks that behave like the HAL, and refuse like it (#9)
- the PWM mock reports the frequency the pin emits (#18, #8)
- soft I2C and SPI mocks that log what they clocked out (#10)
- an edge-counter mock that refuses a single edge where the pin cannot tell (#11)
- the four values, and an EEPROM mock that has a size (#26, #25, #16, #19)
- the mock clock advances, because alarm waits on it (#20)
- add a corpus of user-style programs with expected build outcomes and size gates
- add CircuitPython API parity suite
- parity: resolve symbolic stub defaults and accept exit(self, *args)
- parity: allowlist real gaps tracked as GitHub issues
- corpus: create the template's src directory, which git does not keep empty
- corpus: resolve the pymcu driver from PyMCU's own repo venv, not a user project
- corpus: flip four fixed programs to build, refresh all sizes
- add API parity and corpus GitHub Actions workflows
- board.I2C(), board.SPI() and board.UART(), which miscompile busio (#7)
Everything the PyPI 0.1.0a1 layer (June) predates. With the 0.1.0a10
compiler, the canonical CircuitPython nvm pattern works end-to-end:
microcontroller.nvm[0:4] = b'...' and print(microcontroller.nvm[0:4])
print the real CPython bytearray repr, verified against EEPROM on a real
Arduino Uno.
microcontroller.nvm: EEPROM-backed persistent byte storage.microcontroller.watchdog(WatchDogTimer) andmicrocontroller.cpu.reset_reason/ResetReason.- RP2040 support: board/digitalio/busio dispatch by architecture.
- NeoPixel moved to its own library (pymcu-lib-neopixel).
- pymcu-compiler >= 0.1.0a10 for the nvm slice forms; single-byte
nvm[i]access works with earlier compilers.