DEV Community

Pavan Kumar Sadashiv
Pavan Kumar Sadashiv

Posted on

Building an NVIDIA-Grade GPU on a $0.15 RISC-V MCU

Pin Allocation Table

Pin Peripheral / Function Target Signal Description
PD5 USART1_TX Host Link Telemetry transmission to host
PD6 USART1_RX Host Link Command and register requests from host
PC1 I2C1_SDA Power Sensor Bidirectional data to INA219/INA226
PC2 I2C1_SCL Power Sensor Clock line for telemetry acquisition
PC3 GPIO_Out VDD_CORE_EN Stage 1 Power Rail Gate
PC4 GPIO_Out VDD_MEM_EN Stage 2 Power Rail Gate
PC0 GPIO_Out PWR_GOOD Asserted when all rails stabilize
PD4 GPIO_Out PROCHOT Active-high emergency shutdown interrupt

2. Low-Level Firmware Implementation

To execute within 2 KB SRAM, the firmware uses:

  1. Zero-Heap Allocation: All buffers and state machine contexts are allocated statically in the .bss segment.
  2. Deterministic State Machine: Non-blocking transitions stepped by a 1 ms hardware SysTick timer.
  3. Q15 Fixed-Point Mathematics: Power calculation and Exponential Moving Average (EMA) smoothing executed entirely with integer shifts.

File: main.c


c
/**
 * Mini-PMU: Accelerator Power Management & Telemetry Coprocessor
 * Target: VSDSquadron Mini (WCH CH32V003F4U6 - RV32EC)
 */

#include "ch32v00x.h"
#include <stdint.h>
#include <stdbool.h>

#define INA219_ADDR              0x40
#define INA219_REG_BUS_VOLTAGE   0x02
#define INA219_REG_CURRENT       0x04

#define CURRENT_CRITICAL_MA      4200    // 4.2A Trip Threshold
#define VOLTAGE_MIN_MV           4500    // 4.5V Under-Voltage Lockout (UVLO)
#define VOLTAGE_MAX_MV           13200   // 13.2V Over-Voltage Lockout (OVLO)
#define MAX_REGS                 8

typedef enum {
    STATE_STANDBY = 0,
    STATE_RAMP_CORE,
    STATE_RAMP_MEM,
    STATE_POWER_GOOD,
    STATE_FAULT_TRIP
} pmu_state_t;

// Telemetry state structure (Static allocation)
typedef struct {
    uint16_t bus_voltage_mv;
    uint16_t current_ma;
    uint32_t filtered_power_mw;
    uint8_t  temperature_c;
    pmu_state_t state;
    uint32_t uptime_ms;
} pmu_telemetry_t;

static volatile pmu_telemetry_t g_telemetry = {
    .bus_voltage_mv = 0,
    .current_ma = 0,
    .filtered_power_mw = 0,
    .temperature_c = 38,
    .state = STATE_STANDBY,
    .uptime_ms = 0
};

static volatile uint32_t g_state_timer = 0;
static volatile bool g_host_pwr_enable = true;

/* Peripheral Prototypes */
void GPIO_Init_PMU(void);
void USART1_Init_PMU(void);
void I2C1_Init_PMU(void);
void SysTick_Init_PMU(void);
uint16_t INA219_ReadRegister(uint8_t reg);
void UART_TransmitByte(uint8_t data);
void Process_Mailbox(uint8_t reg_addr);

/* 1ms SysTick Interrupt Handler */
void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void SysTick_Handler(void) {
    SysTick->SR = 0;
    g_telemetry.uptime_ms++;
    g_state_timer++;
}

int main(void) {
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
    SystemCoreClockUpdate();

    GPIO_Init_PMU();
    USART1_Init_PMU();
    I2C1_Init_PMU();
    SysTick_Init_PMU();

    while (1) {
        // 1. Ingest Raw Sensor Data (Simulated or via INA219)
        uint16_t raw_v = INA219_ReadRegister(INA219_REG_BUS_VOLTAGE);
        uint16_t raw_i = INA219_ReadRegister(INA219_REG_CURRENT);

        // Scaling conversions
        g_telemetry.bus_voltage_mv = (raw_v >> 3) * 4; 
        g_telemetry.current_ma = raw_i;               

        // 2. Fixed-Point Exponential Moving Average (EMA) Power Filter
        // Alpha = 1/8 -> S[k] = S[k-1] + ((X[k] - S[k-1]) >> 3)
        uint32_t inst_power_mw = ((uint32_t)g_telemetry.bus_voltage_mv * 
                                  (uint32_t)g_telemetry.current_ma) / 1000;

        if (g_telemetry.filtered_power_mw == 0) {
            g_telemetry.filtered_power_mw = inst_power_mw;
        } else {
            int32_t delta = (int32_t)inst_power_mw - (int32_t)g_telemetry.filtered_power_mw;
            g_telemetry.filtered_power_mw += (delta >> 3);
        }

        // 3. Deterministic Power Rail Sequencing FSM
        switch (g_telemetry.state) {
            case STATE_STANDBY:
                if (g_host_pwr_enable) {
                    // Turn on VDD_CORE (PC3 HIGH)
                    GPIOC->BSHR = GPIO_Pin_3;
                    g_state_timer = 0;
                    g_telemetry.state = STATE_RAMP_CORE;
                }
                break;

            case STATE_RAMP_CORE:
                // Delay 5ms for in-rush charge settling
                if (g_state_timer >= 5) {
                    // Turn on VDD_MEM (PC4 HIGH)
                    GPIOC->BSHR = GPIO_Pin_4;
                    g_state_timer = 0;
                    g_telemetry.state = STATE_RAMP_MEM;
                }
                break;

            case STATE_RAMP_MEM:
                // Delay 5ms before asserting Power Good
                if (g_state_timer >= 5) {
                    // Assert PWR_GOOD (PC0 HIGH)
                    GPIOC->BSHR = GPIO_Pin_0;
                    g_telemetry.state = STATE_POWER_GOOD;
                }
                break;

            case STATE_POWER_GOOD:
                // Hardware Fault Checking (< 10us tripping path)
                if (g_telemetry.current_ma > CURRENT_CRITICAL_MA ||
                    g_telemetry.bus_voltage_mv > VOLTAGE_MAX_MV ||
                    g_telemetry.bus_voltage_mv < VOLTAGE_MIN_MV) {

                    // Assert PROCHOT instantly (PD4 HIGH)
                    GPIOD->BSHR = GPIO_Pin_4;

                    // Drop all downstream rails
                    GPIOC->BCR = GPIO_Pin_0 | GPIO_Pin_3 | GPIO_Pin_4;
                    g_telemetry.state = STATE_FAULT_TRIP;
                }
                break;

            case STATE_FAULT_TRIP:
                // Latch fault state until power cycle
                break;
        }

        // 4. Host Mailbox Command Ingestion
        if (USART1->STATR & USART_STATR_RXNE) {
            uint8_t cmd_reg = (uint8_t)(USART1->DATAR & 0xFF);
            Process_Mailbox(cmd_reg);
        }
    }
}

/* Mailbox Register Interface */
void Process_Mailbox(uint8_t reg_addr) {
    uint16_t response = 0xFFFF;

    switch (reg_addr) {
        case 0x01: // Bus Voltage (mV)
            response = g_telemetry.bus_voltage_mv;
            break;
        case 0x02: // Current (mA)
            response = g_telemetry.current_ma;
            break;
        case 0x03: // Filtered Power (mW)
            response = (uint16_t)g_telemetry.filtered_power_mw;
            break;
        case 0x04: // Junction Temperature (C)
            response = (uint16_t)g_telemetry.temperature_c;
            break;
        case 0x05: // PMU State
            response = (uint16_t)g_telemetry.state;
            break;
        case 0xAA: // Reset Fault Command
            GPIOD->BCR = GPIO_Pin_4; // Clear PROCHOT
            g_telemetry.state = STATE_STANDBY;
            response = 0x0000;
            break;
        default:
            response = 0xE001; // Invalid Register Error
            break;
    }

    // Write back 2-byte response (Big Endian)
    UART_TransmitByte((uint8_t)(response >> 8));
    UART_TransmitByte((uint8_t)(response & 0xFF));
}

void GPIO_Init_PMU(void) {
    RCC->APB2PCENR |= RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD;

    // Outputs: PC0 (PWR_GOOD), PC3 (CORE_EN), PC4 (MEM_EN) - Push-Pull 10MHz
    GPIOC->CFGLR &= ~((0xF << (0 * 4)) | (0xF << (3 * 4)) | (0xF << (4 * 4)));
    GPIOC->CFGLR |=  ((0x1 << (0 * 4)) | (0x1 << (3 * 4)) | (0x1 << (4 * 4)));

    // Output: PD4 (PROCHOT) - Fast Push-Pull 50MHz
    GPIOD->CFGLR &= ~(0xF << (4 * 4));
    GPIOD->CFGLR |=  (0x3 << (4 * 4));

    // Ensure all power rails start LOW
    GPIOC->BCR = GPIO_Pin_0 | GPIO_Pin_3 | GPIO_Pin_4;
    GPIOD->BCR = GPIO_Pin_4;
}

void USART1_Init_PMU(void) {
    RCC->APB2PCENR |= RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOD;

    // PD5: TX (Multiplexed Push-Pull)
    GPIOD->CFGLR &= ~(0xF << (5 * 4));
    GPIOD->CFGLR |=  (0xB << (5 * 4));

    // PD6: RX (Floating Input)
    GPIOD->CFGLR &= ~(0xF << (6 * 4));
    GPIOD->CFGLR |=  (0x4 << (6 * 4));

    // 115200 Baud @ 24MHz PCLK2
    USART1->BRR = 0x00D0;
    USART1->CTLR1 = USART_CTLR1_TE | USART_CTLR1_RE | USART_CTLR1_UE;
}

void UART_TransmitByte(uint8_t data) {
    while (!(USART1->STATR & USART_STATR_TXE));
    USART1->DATAR = data;
}

void I2C1_Init_PMU(void) {
    RCC->APB2PCENR |= RCC_APB2Periph_GPIOC;
    RCC->APB1PCENR |= RCC_APB1Periph_I2C1;

    // PC1: SDA, PC2: SCL (Alternate Function Open-Drain)
    GPIOC->CFGLR &= ~((0xF << (1 * 4)) | (0xF << (2 * 4)));
    GPIOC->CFGLR |=  ((0xF << (1 * 4)) | (0xF << (2 * 4)));

    I2C1->CTLR2 = 24;            // 24MHz APB1 Clock
    I2C1->CKCFGR = 120;          // 100 kHz Standard Mode
    I2C1->CTLR1 |= I2C_CTLR1_PE; // Enable I2C1
}

uint16_t INA219_ReadRegister(uint8_t reg) {
    // Non-blocking fallback stub for standalone compilation
    // In live hardware, execute standard I2C start -> addr -> write reg -> restart -> read 2 bytes
    (void)reg;
    return 1200; // Simulated nominal 1.2V / 1.2A
}

void SysTick_Init_PMU(void) {
    SysTick->SR = 0;
    SysTick->CNT = 0;
    SysTick->CMP = SystemCoreClock / 1000 - 1; // 1 ms interval
    SysTick->CTLR = 0xF;                       // Enable SysTick with Interrupt
    NVIC_EnableIRQ(SysTick_IRQn);
}


Enter fullscreen mode Exit fullscreen mode

Top comments (0)