Skip to content

File port_stm32f4.c

File List > port > stm32f4 > port_stm32f4.c

Go to the documentation of this file

#if defined(STM32F407xx) && !defined(ARDUINO)

#include "syntropic/common/syn_compiler.h"
#include "syntropic/common/syn_defs.h"
#include "syntropic/port/syn_port_adc.h"
#include "syntropic/port/syn_port_can.h"
#include "syntropic/port/syn_port_exti.h"
#include "syntropic/port/syn_port_i2c.h"
#include "syntropic/port/syn_port_spi.h"
#include "syntropic/system/syn_sleep.h"

#include <stdbool.h>
#include <stdint.h>
#include <string.h>

/* ═══════════════════════════════════════════════════════════════════════════
 *  Register Definitions
 * ═══════════════════════════════════════════════════════════════════════════ */

#define PERIPH_BASE 0x40000000UL
#define APB1_BASE PERIPH_BASE
#define APB2_BASE (PERIPH_BASE + 0x10000UL)
#define AHB1_BASE (PERIPH_BASE + 0x20000UL)

/* ── RCC ────────────────────────────────────────────────────────────────── */

#define RCC_BASE (AHB1_BASE + 0x3800UL)
#define RCC_AHB1ENR (*(volatile uint32_t *)(RCC_BASE + 0x30))
#define RCC_APB1ENR (*(volatile uint32_t *)(RCC_BASE + 0x40))
#define RCC_APB2ENR (*(volatile uint32_t *)(RCC_BASE + 0x44))

/* ── SysTick ────────────────────────────────────────────────────────────── */

#define SYSTICK_BASE 0xE000E010UL
#define SYSTICK_CTRL (*(volatile uint32_t *)(SYSTICK_BASE + 0x00))
#define SYSTICK_LOAD (*(volatile uint32_t *)(SYSTICK_BASE + 0x04))
#define SYSTICK_VAL (*(volatile uint32_t *)(SYSTICK_BASE + 0x08))

/* ── GPIO ───────────────────────────────────────────────────────────────── */

typedef struct {
    volatile uint32_t MODER;
    volatile uint32_t OTYPER;
    volatile uint32_t OSPEEDR;
    volatile uint32_t PUPDR;
    volatile uint32_t IDR;
    volatile uint32_t ODR;
    volatile uint32_t BSRR;
    volatile uint32_t LCKR;
    volatile uint32_t AFR[2];
} GPIO_TypeDef;

#define GPIOA ((GPIO_TypeDef *)(AHB1_BASE + 0x0000))
#define GPIOB ((GPIO_TypeDef *)(AHB1_BASE + 0x0400))
#define GPIOC ((GPIO_TypeDef *)(AHB1_BASE + 0x0800))
#define GPIOD ((GPIO_TypeDef *)(AHB1_BASE + 0x0C00))
#define GPIOE ((GPIO_TypeDef *)(AHB1_BASE + 0x1000))
#define GPIOF ((GPIO_TypeDef *)(AHB1_BASE + 0x1400))
#define GPIOG ((GPIO_TypeDef *)(AHB1_BASE + 0x1800))
#define GPIOH ((GPIO_TypeDef *)(AHB1_BASE + 0x1C00))
#define GPIOI ((GPIO_TypeDef *)(AHB1_BASE + 0x2000))

static GPIO_TypeDef *const gpio_ports[] = {GPIOA, GPIOB, GPIOC, GPIOD, GPIOE,
                                           GPIOF, GPIOG, GPIOH, GPIOI};
#define NUM_GPIO_PORTS (sizeof(gpio_ports) / sizeof(gpio_ports[0]))
#define GPIO_PORT(pin) gpio_ports[(pin) >> 4]
#define GPIO_BIT(pin) ((pin) & 0x0F)

/* ── USART ──────────────────────────────────────────────────────────────── */

typedef struct {
    volatile uint32_t SR;
    volatile uint32_t DR;
    volatile uint32_t BRR;
    volatile uint32_t CR1;
    volatile uint32_t CR2;
    volatile uint32_t CR3;
    volatile uint32_t GTPR;
} USART_TypeDef;

#define USART1 ((USART_TypeDef *)0x40011000)
#define USART2 ((USART_TypeDef *)0x40004400)
#define USART3 ((USART_TypeDef *)0x40004800)
#define UART4 ((USART_TypeDef *)0x40004C00)
#define UART5 ((USART_TypeDef *)0x40005000)

#define USART_SR_TXE (1U << 7)
#define USART_SR_RXNE (1U << 5)
#define USART_SR_TC (1U << 6)
#define USART_CR1_UE (1U << 13)
#define USART_CR1_TE (1U << 3)
#define USART_CR1_RE (1U << 2)

static USART_TypeDef *const usart_instances[] = {USART1, USART2, USART3, UART4, UART5};

/* ── SPI ────────────────────────────────────────────────────────────────── */

typedef struct {
    volatile uint32_t CR1;
    volatile uint32_t CR2;
    volatile uint32_t SR;
    volatile uint32_t DR;
    volatile uint32_t CRCPR;
    volatile uint32_t RXCRCR;
    volatile uint32_t TXCRCR;
    volatile uint32_t I2SCFGR;
    volatile uint32_t I2SPR;
} SPI_TypeDef;

#define SPI1 ((SPI_TypeDef *)0x40013000)
#define SPI2 ((SPI_TypeDef *)0x40003800)
#define SPI3 ((SPI_TypeDef *)0x40003C00)

#define SPI_CR1_SPE (1U << 6)
#define SPI_CR1_MSTR (1U << 2)
#define SPI_CR1_SSM (1U << 9)
#define SPI_CR1_SSI (1U << 8)
#define SPI_CR1_CPOL (1U << 1)
#define SPI_CR1_CPHA (1U << 0)
#define SPI_CR1_LSBFIRST (1U << 7)
#define SPI_SR_TXE (1U << 1)
#define SPI_SR_RXNE (1U << 0)
#define SPI_SR_BSY (1U << 7)

static SPI_TypeDef *const spi_instances[] = {SPI1, SPI2, SPI3};

/* ── I2C ────────────────────────────────────────────────────────────────── */

typedef struct {
    volatile uint32_t CR1;
    volatile uint32_t CR2;
    volatile uint32_t OAR1;
    volatile uint32_t OAR2;
    volatile uint32_t DR;
    volatile uint32_t SR1;
    volatile uint32_t SR2;
    volatile uint32_t CCR;
    volatile uint32_t TRISE;
    volatile uint32_t FLTR;
} I2C_TypeDef;

#define I2C1 ((I2C_TypeDef *)0x40005400)
#define I2C2 ((I2C_TypeDef *)0x40005800)
#define I2C3 ((I2C_TypeDef *)0x40005C00)

#define I2C_CR1_PE (1U << 0)
#define I2C_CR1_START (1U << 8)
#define I2C_CR1_STOP (1U << 9)
#define I2C_CR1_ACK (1U << 10)
#define I2C_SR1_SB (1U << 0)
#define I2C_SR1_ADDR (1U << 1)
#define I2C_SR1_BTF (1U << 2)
#define I2C_SR1_TXE (1U << 7)
#define I2C_SR1_RXNE (1U << 6)
#define I2C_SR1_AF (1U << 10)

static I2C_TypeDef *const i2c_instances[] = {I2C1, I2C2, I2C3};

/* ── Flash controller ───────────────────────────────────────────────────── */

typedef struct {
    volatile uint32_t ACR;
    volatile uint32_t KEYR;
    volatile uint32_t OPTKEYR;
    volatile uint32_t SR;
    volatile uint32_t CR;
    volatile uint32_t OPTCR;
} FLASH_TypeDef;

#define FLASH ((FLASH_TypeDef *)0x40023C00)
#define FLASH_SR_BSY (1U << 16)
#define FLASH_CR_PG (1U << 0)
#define FLASH_CR_SER (1U << 1)
#define FLASH_CR_STRT (1U << 16)
#define FLASH_CR_LOCK (1U << 31)
#define FLASH_CR_PSIZE_BYTE (0U << 8)
#define FLASH_KEY1 0x45670123UL
#define FLASH_KEY2 0xCDEF89ABUL

/* ── ADC ────────────────────────────────────────────────────────────────── */

typedef struct {
    volatile uint32_t SR;
    volatile uint32_t CR1;
    volatile uint32_t CR2;
    volatile uint32_t SMPR1;
    volatile uint32_t SMPR2;
    volatile uint32_t JOFR1;
    volatile uint32_t JOFR2;
    volatile uint32_t JOFR3;
    volatile uint32_t JOFR4;
    volatile uint32_t HTR;
    volatile uint32_t LTR;
    volatile uint32_t SQR1;
    volatile uint32_t SQR2;
    volatile uint32_t SQR3;
    volatile uint32_t JSQR;
    volatile uint32_t JDR1;
    volatile uint32_t JDR2;
    volatile uint32_t JDR3;
    volatile uint32_t JDR4;
    volatile uint32_t DR;
} ADC_TypeDef;

#define ADC1 ((ADC_TypeDef *)0x40012000)

#define ADC_SR_EOC (1U << 1)
#define ADC_CR2_ADON (1U << 0)
#define ADC_CR2_SWSTART (1U << 30)

/* ── EXTI ───────────────────────────────────────────────────────────────── */

typedef struct {
    volatile uint32_t IMR;
    volatile uint32_t EMR;
    volatile uint32_t RTSR;
    volatile uint32_t FTSR;
    volatile uint32_t SWIER;
    volatile uint32_t PR;
} EXTI_TypeDef;

#define EXTI ((EXTI_TypeDef *)0x40013C00)

/* SYSCFG for EXTI line mapping */
#define SYSCFG_BASE 0x40013800UL
#define SYSCFG_EXTICR ((volatile uint32_t *)(SYSCFG_BASE + 0x08))

/* ── CAN ────────────────────────────────────────────────────────────────── */

typedef struct {
    volatile uint32_t MCR;
    volatile uint32_t MSR;
    volatile uint32_t TSR;
    volatile uint32_t RF0R;
    volatile uint32_t RF1R;
    volatile uint32_t IER;
    volatile uint32_t ESR;
    volatile uint32_t BTR;
    uint32_t _reserved0[88];
    /* TX mailboxes */
    volatile uint32_t TI0R;
    volatile uint32_t TDT0R;
    volatile uint32_t TDL0R;
    volatile uint32_t TDH0R;
    volatile uint32_t TI1R;
    volatile uint32_t TDT1R;
    volatile uint32_t TDL1R;
    volatile uint32_t TDH1R;
    volatile uint32_t TI2R;
    volatile uint32_t TDT2R;
    volatile uint32_t TDL2R;
    volatile uint32_t TDH2R;
    /* RX FIFO 0 */
    volatile uint32_t RI0R;
    volatile uint32_t RDT0R;
    volatile uint32_t RDL0R;
    volatile uint32_t RDH0R;
    /* RX FIFO 1 */
    volatile uint32_t RI1R;
    volatile uint32_t RDT1R;
    volatile uint32_t RDL1R;
    volatile uint32_t RDH1R;
    uint32_t _reserved1[12];
    /* Filter bank registers */
    volatile uint32_t FMR;
    volatile uint32_t FM1R;
    uint32_t _reserved2;
    volatile uint32_t FS1R;
    uint32_t _reserved3;
    volatile uint32_t FFA1R;
    uint32_t _reserved4;
    volatile uint32_t FA1R;
    uint32_t _reserved5[8];
    volatile uint32_t FR1[28]; /* Filter bank i register 1 */
    volatile uint32_t FR2[28]; /* Filter bank i register 2 */
} CAN_TypeDef;

#define CAN1_BASE 0x40006400UL
#define CAN1 ((CAN_TypeDef *)CAN1_BASE)

#define CAN_MCR_INRQ (1U << 0)
#define CAN_MSR_INAK (1U << 0)
#define CAN_TSR_TME0 (1U << 26)
#define CAN_TIR_TXRQ (1U << 0)
#define CAN_TIR_IDE (1U << 2)
#define CAN_RF0R_FMP0 (0x3U)
#define CAN_RF0R_RFOM0 (1U << 5)

/* ── System clock (assumed 16 MHz for Renode default) ───────────────────── */
#define SYSTEM_CLOCK_HZ 16000000UL
#define APB1_CLOCK_HZ (SYSTEM_CLOCK_HZ / 1)
#define APB2_CLOCK_HZ (SYSTEM_CLOCK_HZ / 1)

/* ═══════════════════════════════════════════════════════════════════════════
 *  System / Tick
 * ═══════════════════════════════════════════════════════════════════════════ */

static volatile uint32_t systick_ms = 0;

void SysTick_Handler(void)
{
    systick_ms++;
}

uint32_t syn_port_get_tick_ms(void)
{
    return systick_ms;
}

void syn_port_delay_ms(uint32_t ms)
{
    uint32_t start = systick_ms;
    while ((systick_ms - start) < ms) {
        __asm volatile("wfi");
    }
}

static volatile uint32_t critical_nesting = 0;

void syn_port_enter_critical(void)
{
    __asm volatile("cpsid i");
    critical_nesting++;
}

void syn_port_exit_critical(void)
{
    if (--critical_nesting == 0) {
        __asm volatile("cpsie i");
    }
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  GPIO
 * ═══════════════════════════════════════════════════════════════════════════ */

SYN_Status syn_port_gpio_init(SYN_GPIO_Pin pin, SYN_GPIO_Mode mode)
{
    uint8_t port_idx = pin >> 4;
    uint8_t bit = GPIO_BIT(pin);
    if (port_idx >= NUM_GPIO_PORTS)
        return SYN_INVALID_PARAM;

    GPIO_TypeDef *gpio = gpio_ports[port_idx];

    /* Enable port clock */
    RCC_AHB1ENR |= (1U << port_idx);

    /* Clear and set mode */
    gpio->MODER &= ~(3U << (bit * 2));
    gpio->PUPDR &= ~(3U << (bit * 2));

    switch (mode) {
    case SYN_GPIO_OUTPUT:
        gpio->MODER |= (1U << (bit * 2));   /* 01 = output */
        gpio->OSPEEDR |= (2U << (bit * 2)); /* High speed */
        break;
    case SYN_GPIO_INPUT:
        /* 00 = input, no pull */
        break;
    case SYN_GPIO_INPUT_PULLUP:
        gpio->PUPDR |= (1U << (bit * 2)); /* 01 = pull-up */
        break;
    case SYN_GPIO_INPUT_PULLDOWN:
        gpio->PUPDR |= (2U << (bit * 2)); /* 10 = pull-down */
        break;
    default:
        return SYN_INVALID_PARAM;
    }

    return SYN_OK;
}

SYN_Status syn_port_gpio_deinit(SYN_GPIO_Pin pin)
{
    uint8_t port_idx = pin >> 4;
    uint8_t bit = GPIO_BIT(pin);
    if (port_idx >= NUM_GPIO_PORTS)
        return SYN_INVALID_PARAM;

    GPIO_TypeDef *gpio = gpio_ports[port_idx];
    gpio->MODER &= ~(3U << (bit * 2)); /* Reset to input */
    gpio->PUPDR &= ~(3U << (bit * 2)); /* No pull */
    return SYN_OK;
}

SYN_Status syn_port_gpio_write(SYN_GPIO_Pin pin, SYN_GPIO_State state)
{
    uint8_t port_idx = pin >> 4;
    if (port_idx >= NUM_GPIO_PORTS)
        return SYN_INVALID_PARAM;
    GPIO_TypeDef *gpio = gpio_ports[port_idx];
    uint8_t bit = GPIO_BIT(pin);

    if (state == SYN_GPIO_HIGH) {
        gpio->BSRR = (1U << bit); /* Set bit */
    } else {
        gpio->BSRR = (1U << (bit + 16)); /* Reset bit */
    }
    return SYN_OK;
}

SYN_GPIO_State syn_port_gpio_read(SYN_GPIO_Pin pin)
{
    uint8_t port_idx = pin >> 4;
    if (port_idx >= NUM_GPIO_PORTS)
        return SYN_GPIO_LOW;
    GPIO_TypeDef *gpio = gpio_ports[port_idx];
    uint8_t bit = GPIO_BIT(pin);
    return (gpio->IDR & (1U << bit)) ? SYN_GPIO_HIGH : SYN_GPIO_LOW;
}

SYN_Status syn_port_gpio_toggle(SYN_GPIO_Pin pin)
{
    uint8_t port_idx = pin >> 4;
    if (port_idx >= NUM_GPIO_PORTS)
        return SYN_INVALID_PARAM;
    GPIO_TypeDef *gpio = gpio_ports[port_idx];
    uint8_t bit = GPIO_BIT(pin);
    gpio->ODR ^= (1U << bit);
    return SYN_OK;
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  UART
 * ═══════════════════════════════════════════════════════════════════════════ */

SYN_Status syn_port_uart_init(SYN_UARTInstance inst, uint32_t baud)
{
    if (inst >= 5)
        return SYN_INVALID_PARAM;
    USART_TypeDef *uart = usart_instances[inst];

    /* Enable clock */
    if (inst == 0) {
        RCC_APB2ENR |= (1U << 4); /* USART1 on APB2 */
    } else {
        RCC_APB1ENR |= (1U << (16 + inst)); /* USART2..UART5 on APB1 */
    }

    /* Disable while configuring */
    uart->CR1 = 0;
    uart->CR2 = 0;
    uart->CR3 = 0;

    /* Set baud rate */
    uint32_t pclk = (inst == 0) ? APB2_CLOCK_HZ : APB1_CLOCK_HZ;
    uart->BRR = pclk / baud;

    /* 8N1, enable TX/RX/UART */
    uart->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;

    return SYN_OK;
}

SYN_Status syn_port_uart_deinit(SYN_UARTInstance inst)
{
    if (inst >= 5)
        return SYN_INVALID_PARAM;
    usart_instances[inst]->CR1 = 0;
    return SYN_OK;
}

SYN_Status syn_port_uart_transmit_byte(SYN_UARTInstance inst, uint8_t byte)
{
    if (inst >= 5)
        return SYN_INVALID_PARAM;
    USART_TypeDef *uart = usart_instances[inst];
    volatile uint32_t timeout = 100000;
    while (!(uart->SR & USART_SR_TXE) && --timeout) { /* spin */
    }
    if (!timeout)
        return SYN_TIMEOUT;
    uart->DR = byte;
    return SYN_OK;
}

SYN_Status syn_port_uart_receive_byte(SYN_UARTInstance inst, uint8_t *byte, uint32_t timeout_ms)
{
    if (inst >= 5)
        return SYN_INVALID_PARAM;
    USART_TypeDef *uart = usart_instances[inst];

    if (timeout_ms == 0) {
        if (!(uart->SR & USART_SR_RXNE)) {
            return SYN_TIMEOUT;
        }
        *byte = (uint8_t)(uart->DR & 0xFF);
        return SYN_OK;
    }

    uint32_t start = systick_ms;
    while (!(uart->SR & USART_SR_RXNE)) {
        if ((systick_ms - start) >= timeout_ms) {
            return SYN_TIMEOUT;
        }
    }
    *byte = (uint8_t)(uart->DR & 0xFF);
    return SYN_OK;
}

SYN_Status syn_port_uart_transmit(SYN_UARTInstance inst, const uint8_t *data, size_t len,
                                  uint32_t timeout_ms)
{
    (void)timeout_ms;
    for (size_t i = 0; i < len; i++) {
        SYN_Status s = syn_port_uart_transmit_byte(inst, data[i]);
        if (s != SYN_OK)
            return s;
    }
    /* Wait for last byte to finish transmitting */
    USART_TypeDef *uart = usart_instances[inst];
    volatile uint32_t timeout = 100000;
    while (!(uart->SR & USART_SR_TC) && --timeout) { /* spin */
    }
    if (!timeout)
        return SYN_TIMEOUT;
    return SYN_OK;
}

SYN_Status syn_port_uart_receive(SYN_UARTInstance inst, uint8_t *data, size_t len, size_t *received,
                                 uint32_t timeout_ms)
{
    size_t count = 0;
    for (size_t i = 0; i < len; i++) {
        SYN_Status s = syn_port_uart_receive_byte(inst, &data[i], timeout_ms);
        if (s == SYN_TIMEOUT)
            break;
        if (s != SYN_OK) {
            if (received)
                *received = count;
            return s;
        }
        count++;
    }
    if (received)
        *received = count;
    return SYN_OK;
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  SPI (master mode, full-duplex, polling)
 * ═══════════════════════════════════════════════════════════════════════════ */

SYN_Status syn_port_spi_init(const SYN_SPI_Config *cfg)
{
    if (!cfg || cfg->bus >= 3)
        return SYN_INVALID_PARAM;
    SPI_TypeDef *spi = spi_instances[cfg->bus];

    /* Enable clock */
    if (cfg->bus == 0) {
        RCC_APB2ENR |= (1U << 12); /* SPI1 on APB2 */
    } else {
        RCC_APB1ENR |= (1U << (13 + cfg->bus)); /* SPI2=bit14, SPI3=bit15 */
    }

    /* Build CR1 */
    uint32_t cr1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI;

    /* CPOL / CPHA */
    if (cfg->mode == SYN_SPI_MODE_2 || cfg->mode == SYN_SPI_MODE_3) {
        cr1 |= SPI_CR1_CPOL;
    }
    if (cfg->mode == SYN_SPI_MODE_1 || cfg->mode == SYN_SPI_MODE_3) {
        cr1 |= SPI_CR1_CPHA;
    }

    /* Bit order */
    if (cfg->bit_order == 1) {
        cr1 |= SPI_CR1_LSBFIRST;
    }

    /* Baud rate prescaler — find closest prescaler for requested clock.
     * BR[2:0] in bits 5:3, prescaler = 2^(BR+1) */
    uint32_t pclk = (cfg->bus == 0) ? APB2_CLOCK_HZ : APB1_CLOCK_HZ;
    uint32_t br = 0;
    while (br < 7 && (pclk / (2U << br)) > cfg->clock_hz) {
        br++;
    }
    cr1 |= (br << 3);

    spi->CR1 = cr1;
    spi->CR2 = 0;
    spi->CR1 |= SPI_CR1_SPE; /* Enable */

    return SYN_OK;
}

SYN_Status syn_port_spi_deinit(uint8_t bus)
{
    if (bus >= 3)
        return SYN_INVALID_PARAM;
    spi_instances[bus]->CR1 = 0;
    return SYN_OK;
}

SYN_Status syn_port_spi_transfer(uint8_t bus, const uint8_t *tx_buf, uint8_t *rx_buf, size_t len)
{
    if (bus >= 3)
        return SYN_INVALID_PARAM;
    SPI_TypeDef *spi = spi_instances[bus];

    for (size_t i = 0; i < len; i++) {
        /* Wait for TX empty with timeout */
        volatile uint32_t timeout = 100000;
        while (!(spi->SR & SPI_SR_TXE) && --timeout) { /* spin */
        }
        if (!timeout)
            return SYN_TIMEOUT;

        /* Send byte (or 0xFF if no TX buffer) */
        spi->DR = tx_buf ? tx_buf[i] : 0xFF;

        /* Wait for RX not empty with timeout */
        timeout = 100000;
        while (!(spi->SR & SPI_SR_RXNE) && --timeout) { /* spin */
        }
        if (!timeout)
            return SYN_TIMEOUT;

        /* Read received byte */
        uint8_t rx = (uint8_t)(spi->DR & 0xFF);
        if (rx_buf)
            rx_buf[i] = rx;
    }

    /* Wait until not busy with timeout */
    volatile uint32_t timeout = 100000;
    while ((spi->SR & SPI_SR_BSY) && --timeout) { /* spin */
    }
    if (!timeout)
        return SYN_TIMEOUT;

    return SYN_OK;
}

SYN_Status syn_port_spi_cs_assert(uint8_t bus, SYN_GPIO_Pin cs_pin)
{
    (void)bus;
    return syn_port_gpio_write(cs_pin, SYN_GPIO_LOW);
}

SYN_Status syn_port_spi_cs_deassert(uint8_t bus, SYN_GPIO_Pin cs_pin)
{
    (void)bus;
    return syn_port_gpio_write(cs_pin, SYN_GPIO_HIGH);
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  I2C (master mode, 7-bit addressing, polling)
 * ═══════════════════════════════════════════════════════════════════════════ */

SYN_Status syn_port_i2c_init(const SYN_I2C_Config *cfg)
{
    if (!cfg || cfg->bus >= 3)
        return SYN_INVALID_PARAM;
    I2C_TypeDef *i2c = i2c_instances[cfg->bus];

    /* Enable clock (I2C1=bit21, I2C2=bit22, I2C3=bit23) */
    RCC_APB1ENR |= (1U << (21 + cfg->bus));

    /* Disable peripheral for configuration */
    i2c->CR1 = 0;

    /* Set APB clock frequency in CR2 (in MHz) */
    uint32_t freq_mhz = APB1_CLOCK_HZ / 1000000;
    i2c->CR2 = freq_mhz & 0x3F;

    /* Set CCR for standard/fast mode */
    if (cfg->clock_hz <= 100000) {
        /* Standard mode: T_high = T_low = CCR * T_pclk */
        i2c->CCR = APB1_CLOCK_HZ / (2 * cfg->clock_hz);
    } else {
        /* Fast mode (duty=0): T_high = CCR * T_pclk, T_low = 2 * CCR * T_pclk */
        i2c->CCR = (1U << 15) | (APB1_CLOCK_HZ / (3 * cfg->clock_hz));
    }

    /* TRISE = (max_rise_time_ns / T_pclk_ns) + 1 */
    i2c->TRISE = freq_mhz + 1;

    /* Enable */
    i2c->CR1 = I2C_CR1_PE;

    return SYN_OK;
}

SYN_Status syn_port_i2c_deinit(uint8_t bus)
{
    if (bus >= 3)
        return SYN_INVALID_PARAM;
    i2c_instances[bus]->CR1 = 0;
    return SYN_OK;
}

static SYN_Status i2c_start(I2C_TypeDef *i2c, uint8_t addr, bool read)
{
    /* Generate START */
    i2c->CR1 |= I2C_CR1_START;

    /* Wait for SB (start bit generated) */
    uint32_t timeout = 100000;
    while (!(i2c->SR1 & I2C_SR1_SB) && timeout--) { /* spin */
    }
    if (!timeout)
        return SYN_TIMEOUT;

    /* Send address */
    i2c->DR = (addr << 1) | (read ? 1 : 0);

    /* Wait for ADDR (address acknowledged) */
    timeout = 100000;
    while (!(i2c->SR1 & I2C_SR1_ADDR) && timeout--) {
        if (i2c->SR1 & I2C_SR1_AF) {
            /* NACK — no device at this address */
            i2c->SR1 &= ~I2C_SR1_AF;
            i2c->CR1 |= I2C_CR1_STOP;
            return SYN_ERROR;
        }
    }
    if (!timeout)
        return SYN_TIMEOUT;

    /* Clear ADDR by reading SR1 then SR2 */
    (void)i2c->SR1;
    (void)i2c->SR2;

    return SYN_OK;
}

SYN_Status syn_port_i2c_write(uint8_t bus, uint8_t addr, const uint8_t *data, size_t len)
{
    if (bus >= 3)
        return SYN_INVALID_PARAM;
    I2C_TypeDef *i2c = i2c_instances[bus];

    SYN_Status s = i2c_start(i2c, addr, false);
    if (s != SYN_OK)
        return s;

    for (size_t i = 0; i < len; i++) {
        /* Wait for TXE */
        uint32_t timeout = 100000;
        while (!(i2c->SR1 & I2C_SR1_TXE) && timeout--) { /* spin */
        }
        if (!timeout)
            return SYN_TIMEOUT;
        i2c->DR = data[i];
    }

    /* Wait for BTF (byte transfer finished) */
    uint32_t timeout = 100000;
    while (!(i2c->SR1 & I2C_SR1_BTF) && timeout--) { /* spin */
    }

    /* Generate STOP */
    i2c->CR1 |= I2C_CR1_STOP;

    return SYN_OK;
}

SYN_Status syn_port_i2c_read(uint8_t bus, uint8_t addr, uint8_t *data, size_t len)
{
    if (bus >= 3 || len == 0)
        return SYN_INVALID_PARAM;
    I2C_TypeDef *i2c = i2c_instances[bus];

    /* Enable ACK for multi-byte reads */
    if (len > 1) {
        i2c->CR1 |= I2C_CR1_ACK;
    }

    SYN_Status s = i2c_start(i2c, addr, true);
    if (s != SYN_OK)
        return s;

    for (size_t i = 0; i < len; i++) {
        if (i == len - 1) {
            /* Last byte: NACK + STOP */
            i2c->CR1 &= ~I2C_CR1_ACK;
            i2c->CR1 |= I2C_CR1_STOP;
        }

        /* Wait for RXNE */
        uint32_t timeout = 100000;
        while (!(i2c->SR1 & I2C_SR1_RXNE) && timeout--) { /* spin */
        }
        if (!timeout)
            return SYN_TIMEOUT;

        data[i] = (uint8_t)(i2c->DR & 0xFF);
    }

    return SYN_OK;
}

SYN_Status syn_port_i2c_write_read(uint8_t bus, uint8_t addr, const uint8_t *tx_data, size_t tx_len,
                                   uint8_t *rx_data, size_t rx_len)
{
    if (bus >= 3)
        return SYN_INVALID_PARAM;
    I2C_TypeDef *i2c = i2c_instances[bus];

    /* Write phase (no STOP — repeated start) */
    SYN_Status s = i2c_start(i2c, addr, false);
    if (s != SYN_OK)
        return s;

    for (size_t i = 0; i < tx_len; i++) {
        uint32_t timeout = 100000;
        while (!(i2c->SR1 & I2C_SR1_TXE) && timeout--) { /* spin */
        }
        if (!timeout)
            return SYN_TIMEOUT;
        i2c->DR = tx_data[i];
    }

    /* Wait for BTF before repeated start */
    uint32_t timeout = 100000;
    while (!(i2c->SR1 & I2C_SR1_BTF) && timeout--) { /* spin */
    }

    /* Read phase (with STOP at end) */
    return syn_port_i2c_read(bus, addr, rx_data, rx_len);
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  Flash (STM32F4 internal flash controller)
 * ═══════════════════════════════════════════════════════════════════════════ */

static void flash_unlock(void)
{
    if (FLASH->CR & FLASH_CR_LOCK) {
        FLASH->KEYR = FLASH_KEY1;
        FLASH->KEYR = FLASH_KEY2;
    }
}

static void flash_lock(void)
{
    FLASH->CR |= FLASH_CR_LOCK;
}

static SYN_Status flash_wait_bsy(void)
{
    volatile uint32_t timeout = 1000000;
    while ((FLASH->SR & FLASH_SR_BSY) && --timeout) { /* spin */
    }
    return (timeout > 0) ? SYN_OK : SYN_TIMEOUT;
}

/* STM32F4 sector map (first 5 sectors of bank 1) */
static const struct {
    uint32_t addr;
    uint32_t size;
} flash_sectors[] = {
    {0x08000000, 16 * 1024},  /* Sector 0 */
    {0x08004000, 16 * 1024},  /* Sector 1 */
    {0x08008000, 16 * 1024},  /* Sector 2 */
    {0x0800C000, 16 * 1024},  /* Sector 3 */
    {0x08010000, 64 * 1024},  /* Sector 4 */
    {0x08020000, 128 * 1024}, /* Sector 5 */
    {0x08040000, 128 * 1024}, /* Sector 6 */
    {0x08060000, 128 * 1024}, /* Sector 7 */
};
#define NUM_FLASH_SECTORS (sizeof(flash_sectors) / sizeof(flash_sectors[0]))

static int flash_find_sector(uint32_t addr)
{
    for (int i = 0; i < (int)NUM_FLASH_SECTORS; i++) {
        if (addr >= flash_sectors[i].addr && addr < flash_sectors[i].addr + flash_sectors[i].size) {
            return i;
        }
    }
    return -1;
}

SYN_Status syn_port_flash_erase(uint32_t addr)
{
    int sector = flash_find_sector(addr);
    if (sector < 0)
        return SYN_INVALID_PARAM;

    flash_unlock();
    SYN_Status s = flash_wait_bsy();
    if (s != SYN_OK) {
        flash_lock();
        return s;
    }

    /* Sector erase, byte-size programming */
    FLASH->CR = FLASH_CR_SER | ((uint32_t)sector << 3) | FLASH_CR_PSIZE_BYTE;
    FLASH->CR |= FLASH_CR_STRT;

    s = flash_wait_bsy();
    FLASH->CR = 0;
    flash_lock();

    return s;
}

SYN_Status syn_port_flash_read(uint32_t addr, void *buf, size_t len)
{
    /* Flash is memory-mapped — just read directly */
    memcpy(buf, (const void *)addr, len);
    return SYN_OK;
}

SYN_Status syn_port_flash_write(uint32_t addr, const void *buf, size_t len)
{
    const uint8_t *src = (const uint8_t *)buf;

    flash_unlock();
    SYN_Status s = flash_wait_bsy();
    if (s != SYN_OK) {
        flash_lock();
        return s;
    }

    /* Byte programming */
    FLASH->CR = FLASH_CR_PG | FLASH_CR_PSIZE_BYTE;

    for (size_t i = 0; i < len; i++) {
        *(volatile uint8_t *)(addr + i) = src[i];
        s = flash_wait_bsy();
        if (s != SYN_OK) {
            FLASH->CR = 0;
            flash_lock();
            return s;
        }
    }

    FLASH->CR = 0;
    flash_lock();

    return SYN_OK;
}

uint32_t syn_port_flash_sector_size(uint32_t addr)
{
    int sector = flash_find_sector(addr);
    if (sector < 0)
        return 0;
    return flash_sectors[sector].size;
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  ADC (ADC1, single-channel software-triggered)
 * ═══════════════════════════════════════════════════════════════════════════ */

SYN_Status syn_port_adc_init(uint8_t ch)
{
    (void)ch;
    /* Enable ADC1 clock */
    RCC_APB2ENR |= (1U << 8);

    /* Turn on ADC */
    ADC1->CR2 = ADC_CR2_ADON;
    /* Small settling delay */
    for (volatile int i = 0; i < 1000; i++) {
    }

    return SYN_OK;
}

uint16_t syn_port_adc_read(uint8_t ch)
{
    /* Set channel in SQR3 (regular sequence, 1 conversion) */
    ADC1->SQR3 = ch & 0x1F;
    ADC1->SQR1 = 0; /* 1 conversion */

    /* Clear EOC */
    ADC1->SR &= ~ADC_SR_EOC;

    /* Start conversion */
    ADC1->CR2 |= ADC_CR2_SWSTART;

    /* Wait for EOC with timeout to prevent hangs in simulation */
    volatile uint32_t timeout = 10000;
    while (!(ADC1->SR & ADC_SR_EOC) && --timeout) { /* spin */
    }

    return (uint16_t)(ADC1->DR & 0x0FFF);
}

uint8_t syn_port_adc_resolution(void)
{
    return 12;
}

uint16_t syn_port_adc_reference_mv(void)
{
    return 3300;
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  EXTI (external interrupt configuration)
 * ═══════════════════════════════════════════════════════════════════════════ */

SYN_Status syn_port_exti_configure(SYN_GPIO_Pin pin, SYN_EXTI_Edge edge)
{
    uint8_t port_idx = pin >> 4;
    uint8_t bit = GPIO_BIT(pin);

    if (port_idx >= NUM_GPIO_PORTS || bit >= 16)
        return SYN_INVALID_PARAM;

    /* Enable SYSCFG clock */
    RCC_APB2ENR |= (1U << 14);

    /* Map EXTI line to GPIO port via SYSCFG_EXTICR */
    uint8_t reg_idx = bit / 4;
    uint8_t shift = (bit % 4) * 4;
    SYSCFG_EXTICR[reg_idx] &= ~(0xFU << shift);
    SYSCFG_EXTICR[reg_idx] |= ((uint32_t)port_idx << shift);

    /* Configure edge */
    if (edge == SYN_EXTI_RISING || edge == SYN_EXTI_BOTH) {
        EXTI->RTSR |= (1U << bit);
    } else {
        EXTI->RTSR &= ~(1U << bit);
    }
    if (edge == SYN_EXTI_FALLING || edge == SYN_EXTI_BOTH) {
        EXTI->FTSR |= (1U << bit);
    } else {
        EXTI->FTSR &= ~(1U << bit);
    }

    return SYN_OK;
}

void syn_port_exti_enable(SYN_GPIO_Pin pin)
{
    uint8_t bit = GPIO_BIT(pin);
    EXTI->IMR |= (1U << bit);
}

void syn_port_exti_disable(SYN_GPIO_Pin pin)
{
    uint8_t bit = GPIO_BIT(pin);
    EXTI->IMR &= ~(1U << bit);
}

void syn_port_exti_clear_pending(SYN_GPIO_Pin pin)
{
    uint8_t bit = GPIO_BIT(pin);
    EXTI->PR = (1U << bit); /* Write 1 to clear */
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  CAN (CAN1, basic TX/RX with filter support)
 * ═══════════════════════════════════════════════════════════════════════════ */

bool syn_port_can_init(uint8_t port, uint32_t bitrate)
{
    (void)port;

    /* Enable CAN1 clock */
    RCC_APB1ENR |= (1U << 25);

    /* Enter init mode */
    CAN1->MCR |= CAN_MCR_INRQ;
    uint32_t timeout = 100000;
    while (!(CAN1->MSR & CAN_MSR_INAK) && timeout--) { /* spin */
    }
    if (!timeout)
        return false;

    /* Set bit timing (rough: 16MHz / (1+BS1+BS2) / prescaler = bitrate)
     * For 500kbps at 16MHz: prescaler=2, BS1=13, BS2=2 => 16/(1+13+2)/2 = 500k */
    uint32_t prescaler = SYSTEM_CLOCK_HZ / (16 * bitrate);
    if (prescaler < 1)
        prescaler = 1;
    CAN1->BTR = ((2 - 1) << 20) |  /* SJW = 2 */
                ((13 - 1) << 16) | /* BS1 = 13 */
                ((2 - 1) << 0);    /* Prescaler */
    (void)prescaler;               /* Use computed prescaler in production */

    /* Leave init mode */
    CAN1->MCR &= ~CAN_MCR_INRQ;
    timeout = 100000;
    while ((CAN1->MSR & CAN_MSR_INAK) && timeout--) { /* spin */
    }

    return true;
}

bool syn_port_can_send(uint8_t port, uint32_t id, bool ext, const uint8_t *data, uint8_t dlc)
{
    (void)port;

    /* Wait for empty TX mailbox 0 */
    if (!(CAN1->TSR & CAN_TSR_TME0))
        return false;

    /* Set ID */
    if (ext) {
        CAN1->TI0R = (id << 3) | CAN_TIR_IDE;
    } else {
        CAN1->TI0R = (id << 21);
    }

    /* Set DLC */
    CAN1->TDT0R = dlc & 0x0F;

    /* Load data bytes */
    uint32_t lo = 0, hi = 0;
    for (int i = 0; i < dlc && i < 4; i++) {
        lo |= ((uint32_t)data[i]) << (i * 8);
    }
    for (int i = 4; i < dlc && i < 8; i++) {
        hi |= ((uint32_t)data[i]) << ((i - 4) * 8);
    }
    CAN1->TDL0R = lo;
    CAN1->TDH0R = hi;

    /* Request transmission */
    CAN1->TI0R |= CAN_TIR_TXRQ;

    return true;
}

bool syn_port_can_receive(uint8_t port, uint32_t *id, bool *ext, uint8_t *data, uint8_t *dlc)
{
    (void)port;

    /* Check FIFO 0 for pending messages */
    if ((CAN1->RF0R & CAN_RF0R_FMP0) == 0)
        return false;

    /* Read ID */
    if (CAN1->RI0R & CAN_TIR_IDE) {
        *id = CAN1->RI0R >> 3;
        *ext = true;
    } else {
        *id = CAN1->RI0R >> 21;
        *ext = false;
    }

    /* Read DLC */
    *dlc = CAN1->RDT0R & 0x0F;

    /* Read data */
    uint32_t lo = CAN1->RDL0R;
    uint32_t hi = CAN1->RDH0R;
    for (int i = 0; i < *dlc && i < 4; i++) {
        data[i] = (uint8_t)(lo >> (i * 8));
    }
    for (int i = 4; i < *dlc && i < 8; i++) {
        data[i] = (uint8_t)(hi >> ((i - 4) * 8));
    }

    /* Release FIFO 0 */
    CAN1->RF0R |= CAN_RF0R_RFOM0;

    return true;
}

void syn_port_can_set_filter(uint8_t port, uint32_t id, uint32_t mask)
{
    (void)port;

    /* Enter filter init mode */
    CAN1->FMR |= 1;

    /* Configure filter 0: mask mode, 32-bit scale */
    CAN1->FA1R &= ~1U;  /* Deactivate filter 0 */
    CAN1->FM1R &= ~1U;  /* Mask mode */
    CAN1->FS1R |= 1U;   /* 32-bit scale */
    CAN1->FFA1R &= ~1U; /* Assign to FIFO 0 */

    CAN1->FR1[0] = id << 21;   /* Filter ID */
    CAN1->FR2[0] = mask << 21; /* Filter mask */

    CAN1->FA1R |= 1U; /* Activate filter 0 */

    /* Leave filter init mode */
    CAN1->FMR &= ~1U;
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  Sleep
 * ═══════════════════════════════════════════════════════════════════════════ */

void syn_port_sleep(SYN_SleepMode mode)
{
    (void)mode;
    __asm volatile("wfi");
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  Assert handler
 * ═══════════════════════════════════════════════════════════════════════════ */

void syn_assert_failed(const char *file, int line)
{
    (void)file;
    (void)line;
    /* Hard fault loop — Renode will catch this */
    for (;;) {
        __asm volatile("bkpt #0");
    }
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  PWM — TIM2, channels 0–3 → CCR1–CCR4 (PA0–PA3, AF1)
 *
 *  The implementation uses TIM2 which is on APB1.
 *  SYSTEM_CLOCK_HZ / (prescaler+1) drives the timer counter.
 *  ARR (auto-reload register) sets the PWM period; CCRx sets the duty.
 *
 *  Channel mapping:
 *    channel 0 → TIM2 CH1 → PA0  (AF1)
 *    channel 1 → TIM2 CH2 → PA1  (AF1)
 *    channel 2 → TIM2 CH3 → PA2  (AF1)
 *    channel 3 → TIM2 CH4 → PA3  (AF1)
 * ═══════════════════════════════════════════════════════════════════════════ */

#include "syntropic/port/syn_port_pwm.h"

typedef struct {
    volatile uint32_t CR1;
    volatile uint32_t CR2;
    volatile uint32_t SMCR;
    volatile uint32_t DIER;
    volatile uint32_t SR;
    volatile uint32_t EGR;
    volatile uint32_t CCMR1;
    volatile uint32_t CCMR2;
    volatile uint32_t CCER;
    volatile uint32_t CNT;
    volatile uint32_t PSC;
    volatile uint32_t ARR;
    volatile uint32_t RCR;
    volatile uint32_t CCR1;
    volatile uint32_t CCR2;
    volatile uint32_t CCR3;
    volatile uint32_t CCR4;
} TIM_TypeDef;

#define TIM2 ((TIM_TypeDef *)0x40000000)

#define TIM_CR1_CEN (1U << 0)
#define TIM_CR1_ARPE (1U << 7)

/* PWM mode 1: active when CNT < CCRx */
#define TIM_CCMR_PWM1 0x68U /* OC1M = 110, OC1PE = 1 */

/* CCER: CCxE enables the output */
#define TIM2_APB1ENR_BIT (1U << 0)

/* GPIO AF1 for PA0–PA3 = TIM2 CH1–CH4 */
#define GPIO_MODER_AF 2U
#define GPIO_AF1 1U

static uint32_t s_pwm_arr = 999U; /* default: 1 kHz at 16 MHz with PSC=15 */

static void pwm_gpio_init_pin(uint8_t bit)
{
    /* PA[bit]: MODER = alternate function (10b) */
    GPIOA->MODER &= ~(3U << (bit * 2));
    GPIOA->MODER |= (GPIO_MODER_AF << (bit * 2));

    /* OSPEEDR: high speed */
    GPIOA->OSPEEDR |= (3U << (bit * 2));

    /* AFR[0] (AFRL, covers PA0–PA7): set AF1 for the pin */
    GPIOA->AFR[0] &= ~(0xFU << (bit * 4));
    GPIOA->AFR[0] |= (GPIO_AF1 << (bit * 4));
}

SYN_Status syn_port_pwm_init(uint8_t channel, uint32_t freq_hz)
{
    if (channel > 3 || freq_hz == 0)
        return SYN_INVALID_PARAM;

    /* Enable GPIOA and TIM2 clocks */
    RCC_AHB1ENR |= (1U << 0); /* GPIOAEN */
    RCC_APB1ENR |= TIM2_APB1ENR_BIT;

    /* Configure the GPIO pin for this channel as TIM2 AF output */
    pwm_gpio_init_pin(channel); /* PA0..PA3 */

    /*
     * Timer base: PSC=15, ARR derived from freq_hz.
     * Timer clock = 16 MHz (SYSTEM_CLOCK_HZ).
     * f_pwm = 16MHz / (PSC+1) / (ARR+1) = 1MHz / (ARR+1)
     */
    uint32_t arr = (SYSTEM_CLOCK_HZ / 16U / freq_hz);
    if (arr == 0)
        arr = 1;
    s_pwm_arr = arr - 1U;

    TIM2->PSC = 15U;
    TIM2->ARR = s_pwm_arr;
    TIM2->CR1 |= TIM_CR1_ARPE;

    /* Configure PWM mode 1 on the selected channel */
    switch (channel) {
    case 0:
        TIM2->CCMR1 = (TIM2->CCMR1 & ~0x00FFU) | ((uint32_t)TIM_CCMR_PWM1);
        TIM2->CCR1 = 0;
        TIM2->CCER |= (1U << 0); /* CC1E */
        break;
    case 1:
        TIM2->CCMR1 = (TIM2->CCMR1 & ~0xFF00U) | ((uint32_t)TIM_CCMR_PWM1 << 8);
        TIM2->CCR2 = 0;
        TIM2->CCER |= (1U << 4); /* CC2E */
        break;
    case 2:
        TIM2->CCMR2 = (TIM2->CCMR2 & ~0x00FFU) | ((uint32_t)TIM_CCMR_PWM1);
        TIM2->CCR3 = 0;
        TIM2->CCER |= (1U << 8); /* CC3E */
        break;
    case 3:
        TIM2->CCMR2 = (TIM2->CCMR2 & ~0xFF00U) | ((uint32_t)TIM_CCMR_PWM1 << 8);
        TIM2->CCR4 = 0;
        TIM2->CCER |= (1U << 12); /* CC4E */
        break;
    }

    /* Generate an update event to load the shadow registers */
    TIM2->EGR = 1U;

    /* Start the counter */
    TIM2->CR1 |= TIM_CR1_CEN;

    return SYN_OK;
}

void syn_port_pwm_set_duty(uint8_t channel, uint8_t duty_pct)
{
    if (channel > 3)
        return;
    if (duty_pct > 100)
        duty_pct = 100;

    uint32_t ccr = ((uint32_t)duty_pct * (s_pwm_arr + 1U)) / 100U;

    switch (channel) {
    case 0:
        TIM2->CCR1 = ccr;
        break;
    case 1:
        TIM2->CCR2 = ccr;
        break;
    case 2:
        TIM2->CCR3 = ccr;
        break;
    case 3:
        TIM2->CCR4 = ccr;
        break;
    }
}

void syn_port_pwm_set_duty_raw(uint8_t channel, uint16_t duty_u16)
{
    if (channel > 3)
        return;

    /* Map 0–65535 to 0–ARR */
    uint32_t ccr = ((uint32_t)duty_u16 * (s_pwm_arr + 1U)) / 65536U;

    switch (channel) {
    case 0:
        TIM2->CCR1 = ccr;
        break;
    case 1:
        TIM2->CCR2 = ccr;
        break;
    case 2:
        TIM2->CCR3 = ccr;
        break;
    case 3:
        TIM2->CCR4 = ccr;
        break;
    }
}

void syn_port_pwm_enable(uint8_t channel, bool enable)
{
    if (channel > 3)
        return;

    static const uint32_t ccer_bits[] = {1U << 0, 1U << 4, 1U << 8, 1U << 12};
    if (enable) {
        TIM2->CCER |= ccer_bits[channel];
    } else {
        TIM2->CCER &= ~ccer_bits[channel];
    }
}

void syn_port_pwm_set_freq(uint8_t channel, uint32_t freq_hz)
{
    (void)channel; /* All channels share TIM2 — frequency change affects all */

    if (freq_hz == 0)
        return;

    uint32_t arr = (SYSTEM_CLOCK_HZ / 16U / freq_hz);
    if (arr == 0)
        arr = 1;
    s_pwm_arr = arr - 1U;

    TIM2->ARR = s_pwm_arr;
    TIM2->EGR = 1U; /* reload */
}

/* ═══════════════════════════════════════════════════════════════════════════
 *  System init (called before main)
 * ═══════════════════════════════════════════════════════════════════════════ */

void syn_port_system_init(void)
{
    /* Configure SysTick for 1ms interrupt at 16MHz */
    SYSTICK_LOAD = (SYSTEM_CLOCK_HZ / 1000) - 1;
    SYSTICK_VAL = 0;
    SYSTICK_CTRL = 0x07; /* Enable, interrupt, use processor clock */
}

/* Redirect standard library output (printf/putchar) to USART2 for Renode/Unity */
int _write(int file, char *ptr, int len)
{
    (void)file;
    for (int i = 0; i < len; i++) {
        if (ptr[i] == '\n') {
            syn_port_uart_transmit_byte(1 /* USART2 */, '\r');
        }
        syn_port_uart_transmit_byte(1 /* USART2 */, (uint8_t)ptr[i]);
    }
    return len;
}

/* ── Console serial port (backed by USART2) ────────────────────────────── */

#include "syntropic/port/syn_port_serial.h"

#ifndef SYN_SERIAL_UART_INSTANCE
#define SYN_SERIAL_UART_INSTANCE 1 /* USART2 on STM32F407 Discovery */
#endif

SYN_WEAK SYN_Status syn_port_serial_init(uint32_t baudrate)
{
    if (baudrate == 0)
        baudrate = 115200;
    return syn_port_uart_init(SYN_SERIAL_UART_INSTANCE, baudrate);
}

SYN_WEAK int syn_port_serial_write(const uint8_t *data, size_t len)
{
    SYN_Status s = syn_port_uart_transmit(SYN_SERIAL_UART_INSTANCE, data, len, 100);
    return (s == SYN_OK) ? (int)len : -1;
}

SYN_WEAK int syn_port_serial_read(uint8_t *buf, size_t max_len)
{
    size_t received = 0;
    SYN_Status s = syn_port_uart_receive(SYN_SERIAL_UART_INSTANCE, buf, max_len, &received, 0);
    if (s == SYN_TIMEOUT)
        return (int)received;
    if (s != SYN_OK)
        return -1;
    return (int)received;
}

#endif /* STM32F407xx && !ARDUINO */