Skip to content

Add native PY32F071 Firestarter target - #48

Draft
henols wants to merge 47 commits into
agent/portability-macrosfrom
agent/py32f071-toolchain
Draft

Add native PY32F071 Firestarter target#48
henols wants to merge 47 commits into
agent/portability-macrosfrom
agent/py32f071-toolchain

Conversation

@henols

@henols henols commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a native PY32F071 firmware target on top of agent/portability-macros, using the current shared Firestarter command processor, packet framing, PROM algorithms and register abstraction.

PY32F071 platform support

  • native CMake/Ninja GNU Arm build
  • pinned official OpenPuya PY32F071 SDK revision
  • PY32F071 startup, linker script and C/C++ runtime initialization
  • 48 MHz clock configuration for native USB
  • CherryUSB CDC transport
  • SysTick millisecond timing and TIM3 microsecond delays
  • common platform timing and compatibility macros
  • contiguous eight-bit GPIO bus with atomic BSRR writes and one-snapshot IDR reads
  • logical Firestarter control signals mapped to physical GPIOs
  • ADC voltage measurement with VREFINT compensation
  • CI workflow producing ELF, BIN, HEX, map, size report and checksums

Provisional pin map

A clearly marked, replaceable example mapping is included so the target can compile before the final PCB wiring is available:

Signal Provisional pin
PROM D0-D7 PB0-PB7
LSB latch PA0
MSB latch PA1
/OE PA2
Control latch PA3
VPP ADC PA4 / ADC channel 4
/CE PA5
User button Not fitted

PA4 / ADC channel 4 follows the official Puya ADC example. The remaining assignments are explicitly placeholders and must be replaced and hardware-validated against the final schematic before a PROM is connected or programming voltage is enabled.

Validation status

The branch is 27 commits ahead of agent/portability-macros with no divergence. An actual GNU Arm firmware build and hardware validation are still required. Configuration storage is currently runtime-only rather than flash-persistent.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request adds native support for the PY32F071 microcontroller platform, introducing board-specific pin mappings, a CMake build system, Arduino compatibility layers, a USB CDC stack using CherryUSB, and hardware-specific implementations for GPIO, ADC, and timing. The review feedback highlights several critical areas for improvement, including potential race conditions in USB transmission and microsecond timing, a potential integer underflow in the timer prescaler setup, a division-by-zero risk in ADC averaging, and missing null pointer checks in the configuration management functions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +91 to +107
static void usb_start_next_transmit(void)
{
if (tx_busy || tx_head == tx_tail || !usb_device_is_configured())
{
return;
}

size_t count = 0U;
while (count < sizeof(cdc_in_packet) && tx_tail != tx_head)
{
cdc_in_packet[count++] = tx_buffer[tx_tail];
tx_tail = ring_next(tx_tail, FIRESTARTER_USB_TX_CAPACITY);
}

tx_busy = true;
usbd_ep_start_write(FIRESTARTER_CDC_IN_EP, cdc_in_packet, count);
}

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.

high

There is a potential race condition in usb_start_next_transmit because it is called from both the main thread (via py32_usb_write and py32_usb_flush) and the USB interrupt handler (usbd_configure_done_callback and firestarter_cdc_bulk_in) without any synchronization. Since it modifies and reads shared state like tx_tail and tx_busy, concurrent execution can lead to data corruption or transmission lockups. Wrap calls to usb_start_next_transmit from the main thread in a critical section or temporarily disable the USB interrupt.

static void usb_start_next_transmit(void)
{
    if (tx_busy || tx_head == tx_tail || !usb_device_is_configured())
    {
        return;
    }

    size_t count = 0U;
    while (count < sizeof(cdc_in_packet) && tx_tail != tx_head)
    {
        cdc_in_packet[count++] = tx_buffer[tx_tail];
        tx_tail = ring_next(tx_tail, FIRESTARTER_USB_TX_CAPACITY);
    }

    tx_busy = true;
    usbd_ep_start_write(FIRESTARTER_CDC_IN_EP, cdc_in_packet, count);
}

static void usb_start_next_transmit_safe(void)
{
    NVIC_DisableIRQ(USBD_IRQn);
    usb_start_next_transmit();
    NVIC_EnableIRQ(USBD_IRQn);
}

Comment on lines +40 to +60
extern "C" uint32_t rurp_micros(void)
{
uint32_t milliseconds_before;
uint32_t milliseconds_after;
uint32_t counter;
uint32_t reload;

do
{
milliseconds_before = HAL_GetTick();
counter = SysTick->VAL;
reload = SysTick->LOAD + 1U;
milliseconds_after = HAL_GetTick();
} while (milliseconds_before != milliseconds_after);

const uint32_t elapsed_ticks = reload - counter;
const uint32_t elapsed_microseconds =
(elapsed_ticks * 1000U) / reload;

return (milliseconds_before * 1000U) + elapsed_microseconds;
}

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.

medium

The rurp_micros implementation has a classic race condition when the SysTick timer wraps around but the SysTick interrupt has not yet executed to increment the millisecond tick count. This can cause rurp_micros to occasionally jump backward by 1 millisecond. To prevent this, check if the SysTick interrupt is pending and adjust the millisecond count accordingly.

extern "C" uint32_t rurp_micros(void)
{
    uint32_t milliseconds_before;
    uint32_t milliseconds_after;
    uint32_t counter;
    uint32_t reload;

    do
    {
        milliseconds_before = HAL_GetTick();
        counter = SysTick->VAL;
        reload = SysTick->LOAD + 1U;
        milliseconds_after = HAL_GetTick();
    } while (milliseconds_before != milliseconds_after);

    if ((SCB->ICSR & SCB_ICSR_PENDSTSET_Msk) && (counter > reload / 2U))
    {
        milliseconds_before++;
    }

    const uint32_t elapsed_ticks = reload - counter;
    const uint32_t elapsed_microseconds =
        (elapsed_ticks * 1000U) / reload;

    return (milliseconds_before * 1000U) + elapsed_microseconds;
}

Comment thread platform/py32f071/src/timing.cpp Outdated
__HAL_RCC_TIM3_CLK_ENABLE();

microsecond_timer.Instance = TIM3;
microsecond_timer.Init.Prescaler = (HAL_RCC_GetPCLK1Freq() / 1000000U) - 1U;

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.

medium

If HAL_RCC_GetPCLK1Freq() returns a frequency less than 1,000,000 Hz (e.g., during low-power modes or early initialization), the division will result in 0, and subtracting 1U will cause an integer underflow to 0xFFFFFFFFU. This will set an invalid prescaler value. Add a check to prevent underflow.

Suggested change
microsecond_timer.Init.Prescaler = (HAL_RCC_GetPCLK1Freq() / 1000000U) - 1U;
const uint32_t pclk = HAL_RCC_GetPCLK1Freq();
microsecond_timer.Init.Prescaler = (pclk >= 1000000U) ? (pclk / 1000000U) - 1U : 0U;

Comment on lines +135 to +150
uint16_t read_adc_average(uint32_t channel, uint8_t sample_count)
{
uint32_t sum = 0U;

for (uint8_t sample = 0U; sample < sample_count; ++sample)
{
const uint16_t value = read_adc_channel(channel);
if (value == 0U)
{
return 0U;
}
sum += value;
}

return static_cast<uint16_t>(sum / sample_count);
}

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.

medium

If sample_count is passed as 0, the division sum / sample_count will cause a division-by-zero exception. Add a defensive check to handle sample_count == 0 safely.

uint16_t read_adc_average(uint32_t channel, uint8_t sample_count)
{
    if (sample_count == 0U)
    {
        return 0U;
    }

    uint32_t sum = 0U;

    for (uint8_t sample = 0U; sample < sample_count; ++sample)
    {
        const uint16_t value = read_adc_channel(channel);
        if (value == 0U)
        {
            return 0U;
        }
        sum += value;
    }

    return static_cast<uint16_t>(sum / sample_count);
}

Comment on lines +15 to +25
extern "C" void rurp_validate_config(rurp_configuration_t *value)
{
if (strcmp(value->version, CONFIG_VERSION) != 0 || value->r2 == 0)
{
memset(value, 0, sizeof(*value));
strcpy(value->version, CONFIG_VERSION);
value->r1 = VALUE_R1;
value->r2 = VALUE_R2;
value->hardware_revision = 0xFFU;
}
}

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.

medium

The rurp_validate_config function dereferences value without checking if it is nullptr. If a null pointer is passed, it will cause a crash. Add a defensive null check.

extern "C" void rurp_validate_config(rurp_configuration_t *value)
{
    if (value == nullptr)
    {
        return;
    }

    if (strcmp(value->version, CONFIG_VERSION) != 0 || value->r2 == 0)
    {
        memset(value, 0, sizeof(*value));
        strcpy(value->version, CONFIG_VERSION);
        value->r1 = VALUE_R1;
        value->r2 = VALUE_R2;
        value->hardware_revision = 0xFFU;
    }
}

Comment on lines +33 to +37
extern "C" void rurp_save_config(rurp_configuration_t *value)
{
rurp_validate_config(value);
configuration = *value;
}

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.

medium

The rurp_save_config function dereferences value after validation. If value is nullptr, it will crash when copying the structure. Add a defensive null check.

Suggested change
extern "C" void rurp_save_config(rurp_configuration_t *value)
{
rurp_validate_config(value);
configuration = *value;
}
extern "C" void rurp_save_config(rurp_configuration_t *value)
{
if (value == nullptr)
{
return;
}
rurp_validate_config(value);
configuration = *value;
}

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.

1 participant