🛠Technology Stack
- Target Hardware: ZX-8080 (32-bit RISC-V Architecture)
- Clock Speed: 80 MHz System Frequency
- Language: Bare-Metal C
- Toolchain: RISC-V GCC (
riscv64-unknown-elf-gcc) - Simulation: Renode (Antmicro)
- Key Peripherals: 12-bit SAR ADC, General Purpose Timer (TIM1), GPIO
The Project Overview
This repository contains a bare-metal C implementation for the ZX-8080, a custom 32-bit RISC-V based microcontroller. The project demonstrates low-level hardware abstraction by interfacing with an onboard 12-bit SAR ADC for thermal monitoring and utilizing a General Purpose Timer for precise LED toggling. Furthermore, the entire hardware environment is successfully virtualized and tested using the Renode simulation framework.
System Logic & Implementation
The application performs two concurrent tasks within a robust super-loop:
1. Precision Timing (LED Toggle)
To achieve a human-readable 0.5s blink rate from an 80 MHz system clock, I implemented a two-stage clock division using Timer 1:
Prescaler (PSC): Set to
7999. This reduces the timer clock to 10 kHz.f_clk = 80,000,000 / (7999 + 1) = 10,000 HzAuto-Reload Register (ARR): Set to
4999. The timer counts 5000 ticks before setting the Update Interrupt Flag (UIF).Delay = (PSC + 1) * (ARR + 1) / f_sys = (8000 * 5000) / 80,000,000 = 0.5s
2. Thermal Monitoring (ADC Polling)
The code interacts with a 12-bit SAR ADC (Scale: 0–4095) for real-time environmental monitoring.
- Trigger: Hardware conversion is initiated directly via software register writes.
- Synchronization: The CPU polls the
START_CONVbit, waiting for hardware to clear it upon completion to ensure data integrity. - Hysteresis Logic: *
current_temp > 3000: Activate Alarm (GPIO Pin 0 HIGH).current_temp < 1500: Deactivate Alarm (GPIO Pin 0 LOW).
Bare-Metal Compilation & Linker Script
Because this project runs without an Operating System, the standard C library is excluded using the -nostdlib flag. To tell the compiler exactly how to map the code to the ZX-8080’s physical memory, a custom linker script (link.ld) was authored:
- Flash Memory (
0x0): Mapped for the executable code (.textsections) and read-only data (.rodata). - SRAM (
0x20000000): Mapped for initialized variables (.data) and uninitialized variables (.bss).
The resulting output is a raw .elf binary that can be loaded directly onto the silicon.
Virtual Hardware Simulation (Renode)
To test the firmware without physical silicon, the ZX-8080 environment was built from scratch using Renode.
- Platform Definition (
zx8080.repl): Defines the memory map, attaching the CPU, Flash, SRAM, and Memory-Mapped I/O (MMIO) to the system bus at their exact datasheet addresses. - Hardware Injection (
gpio.py&timer.py): Custom Python peripherals act as the virtual silicon. They intercept the CPU’s memory reads/writes to inject mock ADC thermal values and assert Timer Interrupt flags in real-time. - Execution Script (
zx8080.resc): Initializes the machine, loads the.elffile, sets the Stack Pointer (SP) and Program Counter (PC), and logs GPIO outputs.
How to Run the Simulation
1. Compile the Firmware:
2. Launch and Execute in Renode:
Key Learnings
- The
volatileKeyword is Critical: A major debugging breakthrough involved compiler optimization. Without thevolatilekeyword on the hardware structs, the RISC-V GCC compiler optimized away the hardware polling loops. Addingvolatileforced the CPU to fetch fresh data from the memory bus on every cycle, allowing successful hardware synchronization. - Hardware Abstraction via Structs: Instead of using error-prone pointer arithmetic, I utilized Memory-Mapped Structures. This approach maps a C
structdirectly to the peripheral’s register offsets, ensuring type-safe access. - Register-Level Synchronization: Practiced robust peripheral handshaking by implementing polling methods—checking hardware status flags (like
UIFinTIM_STAT) and manually clearing them to acknowledge events. - Clock Tree Management: Mastered the relationship between system oscillators and peripheral clock domains to derive accurate timing intervals.