Skip to content

Puya PY32F MCU support#5106

Open
burgrp wants to merge 33 commits into
tinygo-org:devfrom
burgrp:py32
Open

Puya PY32F MCU support#5106
burgrp wants to merge 33 commits into
tinygo-org:devfrom
burgrp:py32

Conversation

@burgrp

@burgrp burgrp commented Dec 2, 2025

Copy link
Copy Markdown

Hi, this is a port of of Puya PY32F super-cheap micro-controllers.
There are two new boards and supported (available on Aliexpress) for PY32F030 and PY32002b.
PY32F030 and PY32002b are supported, PY32F002a and PY32F071 will follow.
It follows the STM32 SVD pattern - there's a small TinyGo SVD repository dependent on Rust py32-rs repo. The repository is here: https://github.com/burgrp/py32-svd . I think it would be worth to move it to tinygo-org.

@deadprogram

Copy link
Copy Markdown
Member

Hello @burgrp thank you for working on this.

Have you seen this page?
https://tinygo.org/docs/guides/contributing

I think you will find it helpful. 😸

@deadprogram deadprogram changed the base branch from release to dev December 13, 2025 18:43
Comment thread targets/embedfire-py32f002b.json Outdated
Comment thread targets/embedfire-py32f002b.json Outdated
Comment thread targets/embedfire-py32f030.json Outdated
@deadprogram

Copy link
Copy Markdown
Member

@burgrp I have switched the branch for this PR to dev branch as mentioned here:
https://tinygo.org/docs/guides/contributing/#how-to-use-our-github-repository

Can you please rebase it against the latest dev so it can run all of the latest tests?
Also I made a few comments.

Planning on ordering a few of these myself, thanks for working on it!

@b0ch3nski

Copy link
Copy Markdown
Contributor

@burgrp You are updating bdwgc submodule which is probably not something you meant to do - this breaks a lot of stuff 🔥

@burgrp

burgrp commented Jan 27, 2026

Copy link
Copy Markdown
Author

You are right... I will fix it and hopefully it will be possible to merge the PR.

@burgrp

burgrp commented Feb 13, 2026

Copy link
Copy Markdown
Author

@b0ch3nski , the sub-module issue is fixed, all checks are passing

@b0ch3nski

Copy link
Copy Markdown
Contributor

Thanks! I think now there is some unrelated change also in net submodule 😬

@deadprogram

Copy link
Copy Markdown
Member

@b0ch3nski looks that way to me as well: https://github.com/tinygo-org/tinygo/pull/5106/changes#diff-d7ea94295bc068e99e5edeacb50734849fb40d5aa39cbc68be5e55c0d90bd60a

@burgrp one other thing that is needed is to add something to the smoketests to show that at least everything can build. Someplace appropriate in here, please: https://github.com/tinygo-org/tinygo/blob/dev/GNUmakefile#L567-L989

@burgrp

burgrp commented Feb 21, 2026

Copy link
Copy Markdown
Author

@b0ch3nski , @deadprogram smoketests added, net module update reverted

return uint8(p) & 0x0F
}

func (p Pin) getPort() (*py32.GPIO_Type, uint8) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to use unsafe.Add() if you must. Probably even better to use a switch to avoid possible errors.

switch config.Mode {

case PinInputFloating:
port.MODER.ReplaceBits(gpioModeInput, gpioModeMask, pos)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not installed/run the gen-device-py32 yet, but I assume there are probably some functions like SetMODERInput() that are being generated. Using these are usually preferable, to avoid possible bugs from setting unexpected bits.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there is no SetMODERInput() generated. These helper functions are generated in form SetMODER_MODE0()...SetMODER_MODE15(), which are quite useless for parametric access.
I've fixed the machine constants to use the svd pin0 generated constants e.g.:
gpioModeOutput = py32.GPIO_MODER_MODE0_Output
so there is at least some binding to svd values.

Comment thread src/machine/machine_py32_pin.go Outdated
const PinInput PinMode = PinInputFloating

const (
gpioModeInput = 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are any of these being generated from the SVD file already? See comment below.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment above.

Comment thread src/machine/machine_py32_uart.go Outdated
// peripheral clock frequency.
func InitSerialWithClock(clockHz uint32) {
py32UARTClockHz = clockHz
//Serial.ConfigureWithClock(UARTConfig{}, clockHz)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented line?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

py32.RCC.SetICSCR_HSI_FS(py32.RCC_ICSCR_HSI_FS_Freq24MHz)

ConfigureSystemTimer(24_000_000)
machine.InitSerial()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This perhaps needs to be moved into an init() function?

See #5200 (comment)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread targets/embedfire-py32f002b.json Outdated
],
"build-tags": [
"embedfire_py32f002b",
"default_uart_pins"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting, please.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@deadprogram

Copy link
Copy Markdown
Member

This is getting exciting @burgrp thank you so much for working on it! We were discussing during our TinyGo monthly meeting last night ❤️

I made some comments, hopefully @soypat and others can also take a look.

@soypat

soypat commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

I've taken a look and have some reservations on exported API chosen. I'd like to better understand how we define APIs in machine package to understand if the exported functions implemented here match with what the rest of TinyGo does. Basically working on getting something like go doc -tags=rp2040 machine to work (which it does not currently)

@soypat

soypat commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

I vibe coded a tool to help us understand the tinygo APIs better. I'll invite you all to check it out to understand what our APIs look like in TinyGo #5224

@soypat soypat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running tgdoc I've found what look like discrepancies with usual TinyGo API.

You can browse our APIs by running go run ./cmd/tgdoc -http=:18080 ../tinygo/targets ../tinygo/src/machine given tinygodoc shares tinygo parent directory
https://github.com/tinygo-org/tinygodoc

Comment thread src/machine/machine_py32_uart.go Outdated

// ConfigureWithClock initializes the UART using the provided peripheral clock
// frequency (in Hz). This avoids assuming a fixed MCU clock.
func (uart *UART) ConfigureWithClock(config UARTConfig, clockHz uint32) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we replace clockHz with CPUFrequency() internal call to Configure?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread src/machine/machine_py32_uart.go Outdated

// InitSerialWithClock configures the default Serial using the supplied
// peripheral clock frequency.
func InitSerialWithClock(clockHz uint32) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, use CPUFrequency(). It is standard tinygo API, present on 160 targets.

Image

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread src/machine/machine_py32_uart.go Outdated
}

// Configure pin for use by UART
func ConfigureUARTPin(pin Pin, af uint8) {

@soypat soypat Mar 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be Pin.Configure followed by an SetAltFunc? (is it really necessary to export this?)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or unexport it if it is expected to only be called internally for UART configuration

@burgrp burgrp Mar 3, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, cleaned up to just Pin.Configure+SetAltFunc.
This needs to be exported, because other boards may need explicit call for non-default UART pins.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non default pins can be configured with the UARTConfig struct. Please unexport this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it can't be configured using UARTConfig on PY32F. We need to pass the AF parameter somehow.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I will try to fix it somehow...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function removed.

Comment thread src/machine/machine_py32_clock.go Outdated
Comment on lines +16 to +18
func SetCPUFrequency(frequency uint32) {
CPUFrequencyHz = frequency
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very confusing API- it does not actually change the frequency. Prefer unexporting it and either linking with //go:linkname compiler directive from runtime package or setting as constant at build time.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - the function is removed. The variable CPUFrequencyHz still needs to be exported to allow clock change from user code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you want to allow changing CPU frequency from user code?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API does not exist yet in TinyGo, we'd likely need to discuss the best design going forward if we are to add it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For two simple reasons (correct me if I'm not seeing something):

  1. TinyGo comes with a finite set of supported dev boards. In practice, final product boards differ, so they may need other setting.
  2. There may be application which need to set clock dynamically, e.g. to save energy.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I need that for one of my projects. Thanks for being open to discuss it!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd hate for this to stay in a stale state while we discuss the CPUFrequency API. Could you implement CPU frequency API on your side and test things and let us know how your experimentation goes?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is still very unintuitive because it does exactly none of what it says it does. Calling this function does not actually do anything yet documentation and the name state otherwise. Either remove the CPU setting frequency API or figure out how to include it in an intuitive way.

Consider we want new users of the PY32 with TinyGo to have a smooth experience and not be confused as to why the CPU frequency has not changed after calling the function that says it changes CPU frequency. If need be have this function do all the reinitialization of all peripherals to fulfill the objective.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function was removed - no SetCPUFrequency anymore.

@burgrp

burgrp commented Apr 28, 2026

Copy link
Copy Markdown
Author

@soypat I'm using this PY32F port already for few firmware projects. No change was needed for a month or two, so the code seems to be stable. What do you think about merging it?

Comment thread src/machine/machine_py32_clock.go Outdated
Comment on lines +16 to +18
func SetCPUFrequency(frequency uint32) {
CPUFrequencyHz = frequency
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is still very unintuitive because it does exactly none of what it says it does. Calling this function does not actually do anything yet documentation and the name state otherwise. Either remove the CPU setting frequency API or figure out how to include it in an intuitive way.

Consider we want new users of the PY32 with TinyGo to have a smooth experience and not be confused as to why the CPU frequency has not changed after calling the function that says it changes CPU frequency. If need be have this function do all the reinitialization of all peripherals to fulfill the objective.

Comment thread src/machine/machine_py32_uart.go Outdated
}

// Configure pin for use by UART
func ConfigureUARTPin(pin Pin, af uint8) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non default pins can be configured with the UARTConfig struct. Please unexport this.

Comment thread src/machine/machine_py32_uart.go Outdated
Comment on lines +59 to +60
for uart.Bus.SR.Get()&py32.USART_SR_TXE == 0 {
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed these hot loops. can we take measures to prevent runaway spinning?

https://tinygo.org/docs/guides/driver-design/#hot-loops

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread src/machine/machine_py32_uart.go Outdated
Comment on lines +66 to +67
for uart.Bus.SR.Get()&py32.USART_SR_TC == 0 {
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this hot loop too

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines +5 to +12
func (p Pin) SetAltFunc(af uint8) {
port, pin := p.getPort()
if pin >= 8 {
port.AFRH.ReplaceBits(uint32(af), 0xF, (pin%8)*4)
} else {
port.AFRL.ReplaceBits(uint32(af), 0xF, (pin%8)*4)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still very confused as to why these APIs are here. I am not familiar with the py32. Maybe documenting them here would clarify to me and future users how to use Altfuncs on the py32 and also maybe give me clarity to suggest alternative APIs?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how you configure a pin for an alternative function, such as UART RX or ADC IN.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation added.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think we can approach the UART config of pins like RP2040 does it:

  • type pinFunc: https://github.com/tinygo-org/tinygo/blob/dev/src/machine/machine_rp2_gpio.go#L59
  • setPinFunc: https://github.com/tinygo-org/tinygo/blob/dev/src/machine/machine_rp2_gpio.go#L145-L152
  • pinfunc definitions for RP2040:
    fnJTAG pinFunc = 0
    fnSPI pinFunc = 1 // Connect one of the internal PL022 SPI peripherals to GPIO
    fnUART pinFunc = 2
    fnI2C pinFunc = 3
    // Connect a PWM slice to GPIO. There are eight PWM slices,
    // each with two outputchannels (A/B). The B pin can also be used as an input,
    // for frequency and duty cyclemeasurement
    fnPWM pinFunc = 4
    // Software control of GPIO, from the single-cycle IO (SIO) block.
    // The SIO function (F5)must be selected for the processors to drive a GPIO,
    // but the input is always connected,so software can check the state of GPIOs at any time.
    fnSIO pinFunc = 5
    // Connect one of the programmable IO blocks (PIO) to GPIO. PIO can implement a widevariety of interfaces,
    // and has its own internal pin mapping hardware, allowing flexibleplacement of digital interfaces on bank 0 GPIOs.
    // The PIO function (F6, F7) must beselected for PIO to drive a GPIO, but the input is always connected,
    // so the PIOs canalways see the state of all pins.
    fnPIO0, fnPIO1 pinFunc = 6, 7
    // General purpose clock inputs/outputs. Can be routed to a number of internal clock domains onRP2040,
    // e.g. Input: to provide a 1 Hz clock for the RTC, or can be connected to an internalfrequency counter.
    // e.g. Output: optional integer divide
    fnGPCK pinFunc = 8
    // USB power control signals to/from the internal USB controller
    fnUSB pinFunc = 9
    fnNULL pinFunc = 0x1f
    fnXIP pinFunc = 0
  • uart config of pins: https://github.com/tinygo-org/tinygo/blob/dev/src/machine/machine_rp2_uart.go#L53-L57

what do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: The exported API is PinMode:

PinOutput PinMode = iota
PinInput
PinInputPulldown
PinInputPullup
PinAnalog
PinUART
PinPWM
PinI2C
PinSPI
PinPIO0
PinPIO1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants