Add native PY32F071 Firestarter target - #48
Conversation
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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);
}| 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; | ||
| } |
There was a problem hiding this comment.
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;
}| __HAL_RCC_TIM3_CLK_ENABLE(); | ||
|
|
||
| microsecond_timer.Instance = TIM3; | ||
| microsecond_timer.Init.Prescaler = (HAL_RCC_GetPCLK1Freq() / 1000000U) - 1U; |
There was a problem hiding this comment.
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.
| 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; |
| 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); | ||
| } |
There was a problem hiding this comment.
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);
}| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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;
}
}| extern "C" void rurp_save_config(rurp_configuration_t *value) | ||
| { | ||
| rurp_validate_config(value); | ||
| configuration = *value; | ||
| } |
There was a problem hiding this comment.
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.
| 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; | |
| } |
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
BSRRwrites and one-snapshotIDRreadsProvisional pin map
A clearly marked, replaceable example mapping is included so the target can compile before the final PCB wiring is available:
/OE/CEPA4 / 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-macroswith no divergence. An actual GNU Arm firmware build and hardware validation are still required. Configuration storage is currently runtime-only rather than flash-persistent.