Basic working version generates sounds and graphics
This commit is contained in:
parent
6459fe2f02
commit
8a3abafd84
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
*.o
|
||||||
|
*.elf
|
||||||
|
*.img
|
||||||
|
bootcode.bin
|
||||||
|
start.elf
|
||||||
|
kernel.map
|
||||||
34
DESIGN.md
Normal file
34
DESIGN.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Stupid Synth Design Document
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Stupid Synth is a minimalist, 4-voice polyphonic synthesizer designed for the RP2040. It prioritizes zero external component count (other than the sound transducer itself) and code simplicity.
|
||||||
|
|
||||||
|
## Hardware Design
|
||||||
|
* **MCU**: Raspberry Pi Pico (RP2040).
|
||||||
|
* **Audio Output**:
|
||||||
|
* **Method**: High-speed PWM (Pulse Width Modulation).
|
||||||
|
* **Pin**: GP28.
|
||||||
|
* **Circuit**: Connect a piezo buzzer directly between GP28 and GND, or a speaker/headphones in series with a 220Ω resistor.
|
||||||
|
* **Input**: USB Serial Console (no physical buttons/pots required).
|
||||||
|
|
||||||
|
## Software Architecture
|
||||||
|
### Audio Engine
|
||||||
|
* **Carrier Frequency**: ~490 kHz (125MHz sys clock / 255 wrap). This pushes PWM noise far above human hearing, minimizing the need for an analog filter.
|
||||||
|
* **Sample Rate**: 22,050 Hz.
|
||||||
|
* **Bit Depth**: 8-bit.
|
||||||
|
|
||||||
|
### Synthesis Engine
|
||||||
|
* **Waveform**: Sawtooth (generated via simple phase accumulation).
|
||||||
|
* **Polyphony**: 4 simultaneous voices.
|
||||||
|
* **Envelope**: Simple Decay (percussive/plucked sound).
|
||||||
|
* **Mixing**: Tanh saturation (soft clipping) to prevent digital distortion when voices sum.
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
* **Control**: Keyboard characters sent via Serial Monitor trigger notes.
|
||||||
|
* Keys: `a`, `s`, `d`, `f`, `g`, `h`, `j`, `k` (Scale C4 - C5).
|
||||||
|
|
||||||
|
## Execution Plan
|
||||||
|
1. Configure RP2040 Hardware PWM on GP28.
|
||||||
|
2. Setup a repeating timer interrupt at 22.05kHz.
|
||||||
|
3. Implement the mixing loop and voice management in the interrupt handler.
|
||||||
|
4. Process Serial input in the main loop.
|
||||||
12
Makefile
Normal file
12
Makefile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
TOOLCHAIN = arm-none-eabi
|
||||||
|
CC = $(TOOLCHAIN)-gcc
|
||||||
|
OBJCOPY = $(TOOLCHAIN)-objcopy
|
||||||
|
|
||||||
|
all: kernel.img
|
||||||
|
|
||||||
|
kernel.img: boot.o kernel.o
|
||||||
|
$(CC) -T link.ld -o kernel.elf -ffreestanding -O2 -nostdlib boot.o kernel.o
|
||||||
|
$(OBJCOPY) kernel.elf -O binary kernel.img
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f *.o *.elf *.img
|
||||||
74
README.md
74
README.md
@ -22,70 +22,50 @@ It boots in under two seconds and produces sound and graphics with minimal exter
|
|||||||
|
|
||||||
### Software
|
### Software
|
||||||
|
|
||||||
- **ARM Cross-Compiler**: The `arm-none-eabi-gcc` toolchain is required to build the kernel.
|
- **Distrobox**: Required for the automated setup script. It works on most Linux distributions and is pre-installed on systems like Fedora Silverblue/Kinoite and Bazzite.
|
||||||
- **Raspberry Pi Firmware**: You need the `bootcode.bin` and `start.elf` files from the official Raspberry Pi firmware repository.
|
- The provided scripts handle all other dependencies.
|
||||||
|
|
||||||
## Quick Start with Scripts
|
## Quick Start (Recommended)
|
||||||
|
|
||||||
For a fast and easy setup, you can use the provided automation scripts.
|
This project includes scripts to automate the entire setup and deployment process.
|
||||||
|
|
||||||
1. **Build the kernel**:
|
### 1. Set Up the Development Environment (One-Time Setup)
|
||||||
```sh
|
|
||||||
./build.sh
|
|
||||||
```
|
|
||||||
This will compile the C and Assembly code and create the `kernel.img` file.
|
|
||||||
|
|
||||||
2. **Deploy to SD Card**:
|
The `setup-dev-env.sh` script creates a self-contained environment with all the necessary build tools, without altering your host operating system.
|
||||||
Insert your FAT32-formatted SD card. Find its device name (e.g., `/dev/sdX` or `/dev/mmcblkX`) and mount point.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Example: ./deploy.sh /media/user/RASPIBOOT
|
|
||||||
./deploy.sh <path_to_sd_card_mount_point>
|
|
||||||
```
|
|
||||||
This script copies `kernel.img`, `bootcode.bin`, and `start.elf` to the SD card and then safely unmounts it.
|
|
||||||
|
|
||||||
## Manual Setup and Deployment
|
|
||||||
|
|
||||||
### 1. Install Toolchain
|
|
||||||
|
|
||||||
On Debian/Ubuntu-based systems, you can install the cross-compiler with:
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt-get update
|
./setup-dev-env.sh
|
||||||
sudo apt-get install gcc-arm-none-eabi binutils-arm-none-eabi
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Get Raspberry Pi Firmware
|
### 2. Build and Deploy to SD Card
|
||||||
|
|
||||||
The Pi's GPU first loads firmware from the SD card before handing control to the ARM core. You need two files:
|
Insert a FAT32-formatted SD card so that it is mounted by your system. The `deploy.sh` script will then build the kernel, download the necessary firmware, and interactively ask you which device to deploy to.
|
||||||
|
|
||||||
- `bootcode.bin`: The second-stage bootloader.
|
Run the script with `sudo` because it needs permission to unmount the drive when it's finished.
|
||||||
- `start.elf`: The GPU firmware.
|
|
||||||
|
|
||||||
You can download them from the official Raspberry Pi firmware repository. Make sure to grab the files from the `boot` directory. For this project, you only need these two files.
|
|
||||||
|
|
||||||
Place them in the root of this project directory so the `deploy.sh` script can find them.
|
|
||||||
|
|
||||||
### 3. Build the Kernel
|
|
||||||
|
|
||||||
With the toolchain installed, compile the synthesizer by running `make`:
|
|
||||||
```sh
|
```sh
|
||||||
make
|
sudo ./deploy.sh
|
||||||
```
|
```
|
||||||
This command uses the `Makefile` to compile `boot.S` and `kernel.c`, linking them into a final binary image named `kernel.img`.
|
Follow the on-screen prompts to select your SD card. The script handles the rest.
|
||||||
|
|
||||||
### 4. Prepare the SD Card
|
## How the Automation Works
|
||||||
|
|
||||||
Your SD card must be formatted with a **FAT32** filesystem. Most new SD cards are already formatted this way. The partition should also have the "boot" or "lba" flag set, which is standard for bootable Raspberry Pi cards.
|
- **`setup-dev-env.sh`**: This script uses Distrobox to create a lightweight Ubuntu container named `pi-synth-dev`. Inside this container, it installs the `make` and `gcc-arm-none-eabi` toolchain required for cross-compiling. This isolates dependencies and keeps your host system clean.
|
||||||
|
|
||||||
### 5. Deploy to SD Card
|
- **`deploy.sh`**: This script orchestrates the entire build and deployment process from your host machine:
|
||||||
|
1. **Builds the Kernel**: It automatically enters the `pi-synth-dev` container to run `build.sh`, which compiles the source code into `kernel.img`.
|
||||||
|
2. **Fetches Firmware**: It checks for `bootcode.bin` and `start.elf`. If they are missing, it downloads them from the official Raspberry Pi firmware repository.
|
||||||
|
3. **Finds the SD Card**: It scans for *mounted* removable drives and presents an interactive menu for you to choose the correct SD card partition.
|
||||||
|
4. **Copies Files**: It copies `kernel.img`, `bootcode.bin`, and `start.elf` to the selected SD card.
|
||||||
|
5. **Unmounts**: It safely unmounts the SD card, making it ready for use in the Raspberry Pi.
|
||||||
|
|
||||||
Mount the SD card and copy the three essential files to its root directory:
|
## Manual Alternative
|
||||||
|
|
||||||
- `bootcode.bin` (from the firmware repo)
|
If you prefer not to use the scripts, you can perform the steps manually.
|
||||||
- `start.elf` (from the firmware repo)
|
|
||||||
- `kernel.img` (generated by the `make` command)
|
1. **Install Toolchain**: Install `gcc-arm-none-eabi` and `make` on your system.
|
||||||
|
2. **Get Firmware**: Download `bootcode.bin` and `start.elf` from the official Raspberry Pi firmware repository.
|
||||||
|
3. **Build**: Run `make` to compile `kernel.img`.
|
||||||
|
4. **Deploy**: Copy `kernel.img`, `bootcode.bin`, and `start.elf` to the root of a FAT32-formatted SD card.
|
||||||
|
|
||||||
After copying, unmount the SD card safely.
|
|
||||||
|
|
||||||
## Hardware Connections
|
## Hardware Connections
|
||||||
|
|
||||||
|
|||||||
21
boot.S
Normal file
21
boot.S
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.cpu arm1176jzf-s
|
||||||
|
.section ".text.boot"
|
||||||
|
|
||||||
|
.global _start
|
||||||
|
|
||||||
|
_start:
|
||||||
|
// 1. Set up the stack pointer
|
||||||
|
// The Pi 1 has 512MB RAM (usually), stack grows down from a safe high address.
|
||||||
|
// We'll set it to 0x8000 (where the kernel starts) and let it grow down.
|
||||||
|
mov sp, #0x8000
|
||||||
|
|
||||||
|
// 2. Clear BSS (Uninitialized variables) - Skipped for minimal "Stupid" version
|
||||||
|
// but good practice usually.
|
||||||
|
|
||||||
|
// 3. Jump to C code
|
||||||
|
bl kernel_main
|
||||||
|
|
||||||
|
// 4. Halt if main returns
|
||||||
|
halt:
|
||||||
|
wfe
|
||||||
|
b halt
|
||||||
6
build.sh
Executable file
6
build.sh
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Building kernel.img..."
|
||||||
|
make
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Build successful."
|
||||||
|
fi
|
||||||
82
deploy.sh
Executable file
82
deploy.sh
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
MOUNT_POINT=$1
|
||||||
|
|
||||||
|
# Function to fetch firmware dependencies
|
||||||
|
fetch_dependencies() {
|
||||||
|
FIRMWARE_URL="https://github.com/raspberrypi/firmware/raw/master/boot"
|
||||||
|
REQUIRED_FILES="bootcode.bin start.elf"
|
||||||
|
|
||||||
|
for f in $REQUIRED_FILES; do
|
||||||
|
if [ ! -f "$f" ]; then
|
||||||
|
echo "File '$f' not found. Downloading from official repo..."
|
||||||
|
if command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -q --show-progress "$FIRMWARE_URL/$f" -O "$f"
|
||||||
|
elif command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -L -o "$f" "$FIRMWARE_URL/$f"
|
||||||
|
else
|
||||||
|
echo "Error: Neither wget nor curl found. Cannot download $f."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "$MOUNT_POINT" ]; then
|
||||||
|
if command -v lsblk >/dev/null 2>&1; then
|
||||||
|
echo "Scanning for mounted removable storage..."
|
||||||
|
OPTIONS=()
|
||||||
|
MOUNTPOINTS=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
eval "$line"
|
||||||
|
if [ "$RM" == "1" ] && [ -n "$MOUNTPOINT" ]; then
|
||||||
|
OPTIONS+=("$NAME ($SIZE) mounted at $MOUNTPOINT")
|
||||||
|
MOUNTPOINTS+=("$MOUNTPOINT")
|
||||||
|
fi
|
||||||
|
done < <(lsblk -P -o NAME,SIZE,MOUNTPOINT,RM)
|
||||||
|
|
||||||
|
if [ ${#OPTIONS[@]} -eq 0 ]; then
|
||||||
|
echo "No mounted removable devices found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Select the SD card partition to deploy to:"
|
||||||
|
select opt in "${OPTIONS[@]}"; do
|
||||||
|
if [ -n "$opt" ]; then
|
||||||
|
MOUNT_POINT="${MOUNTPOINTS[$((REPLY-1))]}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Invalid selection."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Usage: $0 <path_to_sd_card_mount_point>"
|
||||||
|
echo "Example: $0 /media/user/RASPIBOOT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$MOUNT_POINT" ]; then
|
||||||
|
echo "Error: Mount point '$MOUNT_POINT' not found or is not a directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
fetch_dependencies || exit 1
|
||||||
|
|
||||||
|
distrobox enter "pi-synth-dev" -- ./build.sh || exit 1
|
||||||
|
|
||||||
|
echo "Deploying to $MOUNT_POINT..."
|
||||||
|
|
||||||
|
for f in kernel.img bootcode.bin start.elf; do
|
||||||
|
if [ ! -f "$f" ]; then
|
||||||
|
echo "Error: Required file '$f' not found. Make sure you have run ./build.sh and downloaded the firmware."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Copying $f..."
|
||||||
|
cp "$f" "$MOUNT_POINT/"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deployment complete. Unmounting..."
|
||||||
|
sync # Ensure all writes are finished
|
||||||
|
umount "$MOUNT_POINT"
|
||||||
|
echo "SD card is safe to remove."
|
||||||
294
kernel.c
Normal file
294
kernel.c
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// --- BCM2835 PERIPHERAL ADDRESSES ---
|
||||||
|
// Base address for Pi 1 is 0x20000000
|
||||||
|
#define PERIPHERAL_BASE 0x20000000
|
||||||
|
#define GPIO_BASE (PERIPHERAL_BASE + 0x200000)
|
||||||
|
#define PWM_BASE (PERIPHERAL_BASE + 0x20C000)
|
||||||
|
#define CLK_BASE (PERIPHERAL_BASE + 0x101000)
|
||||||
|
#define UART0_BASE (PERIPHERAL_BASE + 0x201000) // PL011 UART
|
||||||
|
|
||||||
|
// --- MAILBOX REGISTERS (VIDEO) ---
|
||||||
|
#define MBOX_READ (PERIPHERAL_BASE + 0xB880)
|
||||||
|
#define MBOX_STATUS (PERIPHERAL_BASE + 0xB898)
|
||||||
|
#define MBOX_WRITE (PERIPHERAL_BASE + 0xB8A0)
|
||||||
|
|
||||||
|
// --- REGISTER ACCESS MACROS ---
|
||||||
|
#define MMIO32(addr) (*(volatile uint32_t *)(addr))
|
||||||
|
|
||||||
|
// --- GPIO REGISTERS ---
|
||||||
|
#define GPFSEL0 (GPIO_BASE + 0x00)
|
||||||
|
#define GPFSEL1 (GPIO_BASE + 0x04)
|
||||||
|
#define GPFSEL4 (GPIO_BASE + 0x10) // For Pin 40, 45
|
||||||
|
|
||||||
|
// --- PWM REGISTERS ---
|
||||||
|
#define PWM_CTL (PWM_BASE + 0x00)
|
||||||
|
#define PWM_STA (PWM_BASE + 0x04)
|
||||||
|
#define PWM_RNG1 (PWM_BASE + 0x10)
|
||||||
|
#define PWM_DAT1 (PWM_BASE + 0x14)
|
||||||
|
#define PWM_RNG2 (PWM_BASE + 0x20)
|
||||||
|
#define PWM_DAT2 (PWM_BASE + 0x24)
|
||||||
|
#define PWM_FIFO (PWM_BASE + 0x18)
|
||||||
|
|
||||||
|
#define PWM_CTL_PWEN1 (1 << 0)
|
||||||
|
#define PWM_CTL_USEF1 (1 << 5) // Use FIFO for channel 1
|
||||||
|
#define PWM_CTL_PWEN2 (1 << 8)
|
||||||
|
#define PWM_CTL_USEF2 (1 << 13) // Use FIFO for channel 2
|
||||||
|
#define PWM_CTL_CLRF (1 << 6)
|
||||||
|
#define PWM_STA_FULL1 (1 << 0)
|
||||||
|
|
||||||
|
// --- CLOCK MANAGER ---
|
||||||
|
#define CM_PWMCTL (CLK_BASE + 0xA0)
|
||||||
|
#define CM_PWMDIV (CLK_BASE + 0xA4)
|
||||||
|
#define CM_PASSWD (0x5A << 24)
|
||||||
|
|
||||||
|
// --- UART REGISTERS ---
|
||||||
|
#define UART0_DR (UART0_BASE + 0x00)
|
||||||
|
#define UART0_FR (UART0_BASE + 0x18)
|
||||||
|
#define UART0_IBRD (UART0_BASE + 0x24)
|
||||||
|
#define UART0_FBRD (UART0_BASE + 0x28)
|
||||||
|
#define UART0_LCRH (UART0_BASE + 0x2C)
|
||||||
|
#define UART0_CR (UART0_BASE + 0x30)
|
||||||
|
|
||||||
|
// --- UTILS ---
|
||||||
|
void delay(int32_t count) {
|
||||||
|
while (count--) asm volatile("nop");
|
||||||
|
}
|
||||||
|
|
||||||
|
void gpio_set_func(uint32_t pin, uint32_t func) {
|
||||||
|
uint32_t offset = (pin / 10) * 4;
|
||||||
|
uint32_t shift = (pin % 10) * 3;
|
||||||
|
uint32_t addr = GPIO_BASE + offset;
|
||||||
|
|
||||||
|
uint32_t val = MMIO32(addr);
|
||||||
|
val &= ~(7 << shift);
|
||||||
|
val |= (func << shift);
|
||||||
|
MMIO32(addr) = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uart_init() {
|
||||||
|
// Disable UART
|
||||||
|
MMIO32(UART0_CR) = 0;
|
||||||
|
|
||||||
|
// Setup GPIO 14/15 as Alt0 (UART0 TX/RX)
|
||||||
|
gpio_set_func(14, 4); // Alt0
|
||||||
|
gpio_set_func(15, 4); // Alt0
|
||||||
|
|
||||||
|
// Baud rate setup (assuming 3MHz default UART clock)
|
||||||
|
// For 115200: 3000000 / (16 * 115200) = 1.627
|
||||||
|
// IBRD = 1
|
||||||
|
// FBRD = 0.627 * 64 = 40
|
||||||
|
MMIO32(UART0_IBRD) = 1;
|
||||||
|
MMIO32(UART0_FBRD) = 40;
|
||||||
|
|
||||||
|
// 8 bit, no parity, FIFO enabled
|
||||||
|
MMIO32(UART0_LCRH) = (1 << 4) | (1 << 5) | (1 << 6);
|
||||||
|
|
||||||
|
// Enable UART, TX, RX
|
||||||
|
MMIO32(UART0_CR) = (1 << 0) | (1 << 8) | (1 << 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
char uart_read_byte() {
|
||||||
|
// Check RX FIFO empty flag
|
||||||
|
if (MMIO32(UART0_FR) & (1 << 4)) return 0;
|
||||||
|
return (char)(MMIO32(UART0_DR) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pwm_audio_init() {
|
||||||
|
// 1. Setup GPIO 40 and 45 to Alt0 (PWM)
|
||||||
|
// Pi 1 Model B uses 40/45. Model B+ uses 40/41.
|
||||||
|
// We enable all of them to cover both revisions.
|
||||||
|
gpio_set_func(40, 4); // PWM0
|
||||||
|
gpio_set_func(41, 4); // PWM1
|
||||||
|
gpio_set_func(45, 4); // PWM1
|
||||||
|
|
||||||
|
// 2. Setup PWM Clock
|
||||||
|
// Kill the clock completely first
|
||||||
|
MMIO32(CM_PWMCTL) = CM_PASSWD | 0;
|
||||||
|
// Wait for busy flag to clear
|
||||||
|
while (MMIO32(CM_PWMCTL) & 0x80) delay(1);
|
||||||
|
|
||||||
|
// Set Divider. 19.2MHz / 2 = 9.6MHz.
|
||||||
|
// Using divisor 2 is safer for the clock generator.
|
||||||
|
MMIO32(CM_PWMDIV) = CM_PASSWD | (2 << 12);
|
||||||
|
|
||||||
|
// Start Clock (Source 1 = Oscillator 19.2MHz)
|
||||||
|
MMIO32(CM_PWMCTL) = CM_PASSWD | 0x11; // Enable + Source 1
|
||||||
|
|
||||||
|
// 3. Configure PWM
|
||||||
|
// Clock is 9.6MHz. Target ~44.1kHz.
|
||||||
|
// 9600000 / 44100 = ~218
|
||||||
|
MMIO32(PWM_RNG1) = 218;
|
||||||
|
MMIO32(PWM_RNG2) = 218;
|
||||||
|
|
||||||
|
// Enable PWM0 and PWM1, Use FIFO for both
|
||||||
|
MMIO32(PWM_CTL) = PWM_CTL_CLRF; // Clear FIFO
|
||||||
|
delay(100);
|
||||||
|
MMIO32(PWM_CTL) = PWM_CTL_PWEN1 | PWM_CTL_USEF1 | PWM_CTL_PWEN2 | PWM_CTL_USEF2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- VIDEO / FRAMEBUFFER DRIVER ---
|
||||||
|
// The GPU handles composite/HDMI. We just ask for a memory buffer to draw in.
|
||||||
|
|
||||||
|
volatile uint32_t __attribute__((aligned(16))) mailbox[36];
|
||||||
|
|
||||||
|
uint32_t fb_width, fb_height, fb_pitch;
|
||||||
|
uint16_t *fb_ptr = 0; // Pointer to the screen pixels (RGB565)
|
||||||
|
|
||||||
|
void mbox_write(uint8_t channel, uint32_t value) {
|
||||||
|
while (MMIO32(MBOX_STATUS) & 0x80000000); // Wait while full
|
||||||
|
MMIO32(MBOX_WRITE) = (value & ~0xF) | (channel & 0xF);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t mbox_read(uint8_t channel) {
|
||||||
|
while (1) {
|
||||||
|
while (MMIO32(MBOX_STATUS) & 0x40000000); // Wait while empty
|
||||||
|
uint32_t data = MMIO32(MBOX_READ);
|
||||||
|
if ((data & 0xF) == channel) return data & ~0xF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void video_init() {
|
||||||
|
// Property Tag Interface to GPU
|
||||||
|
mailbox[0] = 35 * 4; // Total size
|
||||||
|
mailbox[1] = 0; // Request code
|
||||||
|
|
||||||
|
mailbox[2] = 0x00048003; // Set Physical Size
|
||||||
|
mailbox[3] = 8; mailbox[4] = 8; mailbox[5] = 640; mailbox[6] = 480;
|
||||||
|
|
||||||
|
mailbox[7] = 0x00048004; // Set Virtual Size
|
||||||
|
mailbox[8] = 8; mailbox[9] = 8; mailbox[10] = 640; mailbox[11] = 480;
|
||||||
|
|
||||||
|
mailbox[12] = 0x00048005; // Set Depth
|
||||||
|
mailbox[13] = 4; mailbox[14] = 4; mailbox[15] = 16; // 16-bit (RGB565)
|
||||||
|
|
||||||
|
mailbox[16] = 0x00040001; // Allocate Buffer
|
||||||
|
mailbox[17] = 8; mailbox[18] = 8; mailbox[19] = 4096; mailbox[20] = 0;
|
||||||
|
|
||||||
|
mailbox[21] = 0x00040008; // Get Pitch
|
||||||
|
mailbox[22] = 4; mailbox[23] = 4; mailbox[24] = 0;
|
||||||
|
|
||||||
|
mailbox[25] = 0; // End Tag
|
||||||
|
|
||||||
|
// Send to Mailbox Channel 8 (Tags)
|
||||||
|
// Add 0x40000000 to address to tell GPU to bypass L2 cache (ensure coherency)
|
||||||
|
mbox_write(8, (uint32_t)&mailbox | 0x40000000);
|
||||||
|
mbox_read(8);
|
||||||
|
|
||||||
|
if (mailbox[1] == 0x80000000) { // Success
|
||||||
|
// Convert GPU bus address (0x4XXXXXXX) to ARM physical address (0x0XXXXXXX)
|
||||||
|
fb_ptr = (uint16_t*)(mailbox[19] & 0x3FFFFFFF);
|
||||||
|
fb_width = mailbox[5];
|
||||||
|
fb_height = mailbox[6];
|
||||||
|
fb_pitch = mailbox[24];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_rect(int x, int y, int w, int h, uint16_t color) {
|
||||||
|
if (!fb_ptr) return;
|
||||||
|
for (int j = y; j < y + h; j++) {
|
||||||
|
if (j < 0 || j >= fb_height) continue;
|
||||||
|
// Calculate row start: fb_ptr + (j * pitch / 2) because pitch is bytes
|
||||||
|
uint16_t *row = (uint16_t*)((uint8_t*)fb_ptr + j * fb_pitch);
|
||||||
|
for (int i = x; i < x + w; i++) {
|
||||||
|
if (i >= 0 && i < fb_width) {
|
||||||
|
row[i] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MAIN KERNEL ---
|
||||||
|
void kernel_main(void) {
|
||||||
|
uart_init();
|
||||||
|
pwm_audio_init();
|
||||||
|
video_init();
|
||||||
|
|
||||||
|
// Draw a background
|
||||||
|
draw_rect(0, 0, 640, 480, 0x0000); // Black
|
||||||
|
draw_rect(10, 10, 620, 460, 0x001F); // Blue border box
|
||||||
|
|
||||||
|
// USB Note
|
||||||
|
// (USB requires a massive host stack, sticking to UART for input)
|
||||||
|
|
||||||
|
// Synth State
|
||||||
|
uint32_t phase = 0;
|
||||||
|
uint32_t increment = 0;
|
||||||
|
int amplitude = 0; // Use signed for decay
|
||||||
|
|
||||||
|
// Frequencies (scaled for 44.1kHz sample rate and 32-bit phase accumulator)
|
||||||
|
// Inc = (Freq * 2^32) / SampleRate
|
||||||
|
// C4 (261.63) -> ~25466000
|
||||||
|
const uint32_t note_C4 = 25466000;
|
||||||
|
const uint32_t note_E4 = 32090000;
|
||||||
|
const uint32_t note_G4 = 38160000;
|
||||||
|
const uint32_t note_A4 = 42837000;
|
||||||
|
const uint32_t note_C5 = 50932000;
|
||||||
|
const uint32_t notes[] = { note_C4, note_E4, note_G4, note_A4, note_C5 };
|
||||||
|
|
||||||
|
int bar_height = 0;
|
||||||
|
uint32_t sample_count = 0;
|
||||||
|
uint32_t next_event = 0;
|
||||||
|
uint32_t rng = 0x12345678;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
// 1. Sequencer and Envelope
|
||||||
|
if (sample_count >= next_event) {
|
||||||
|
rng = rng * 1664525 + 1013904223; // Simple LCG
|
||||||
|
int note_idx = (rng >> 24) % 5;
|
||||||
|
increment = notes[note_idx];
|
||||||
|
amplitude = 200; // Start amplitude (max height for visualizer)
|
||||||
|
|
||||||
|
// Duration: ~0.1s to 0.5s (4410 to 22050 samples)
|
||||||
|
next_event = sample_count + 4410 + ((rng >> 10) % 17640);
|
||||||
|
} else {
|
||||||
|
// Simple amplitude decay
|
||||||
|
// Decay every 200 samples (~4.5ms) to prevent clicking
|
||||||
|
if (sample_count % 200 == 0) {
|
||||||
|
if (amplitude > 0) {
|
||||||
|
amplitude--;
|
||||||
|
} else {
|
||||||
|
increment = 0; // Stop note when amplitude is zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sample_count++;
|
||||||
|
|
||||||
|
// 2. Update Graphics (Simple visualizer)
|
||||||
|
// Only update every 1000 samples (~22ms / 44fps) to keep audio smooth
|
||||||
|
if (sample_count % 1000 == 0 && amplitude != bar_height) {
|
||||||
|
if (amplitude > bar_height) {
|
||||||
|
// Growing: Draw red on top
|
||||||
|
draw_rect(300, 240 - amplitude, 40, amplitude - bar_height, 0xF800);
|
||||||
|
} else {
|
||||||
|
// Shrinking: Draw black on top
|
||||||
|
draw_rect(300, 240 - bar_height, 40, bar_height - amplitude, 0x0000);
|
||||||
|
}
|
||||||
|
bar_height = amplitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Wait for FIFO space
|
||||||
|
// The hardware consumes data at 44.1kHz.
|
||||||
|
// By waiting for space, we automatically sync to that speed.
|
||||||
|
// Added timeout to prevent hang if clock fails
|
||||||
|
int timeout = 2000;
|
||||||
|
while ((MMIO32(PWM_STA) & PWM_STA_FULL1) && timeout--) {
|
||||||
|
asm volatile("nop");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Generate Sample
|
||||||
|
phase += increment;
|
||||||
|
|
||||||
|
// Signed 8-bit sawtooth
|
||||||
|
int8_t s_saw = (phase >> 24) - 128;
|
||||||
|
|
||||||
|
// Apply volume and convert to unsigned for PWM
|
||||||
|
int32_t mixed = (s_saw * amplitude) / 255;
|
||||||
|
uint32_t sample = 128 + mixed;
|
||||||
|
|
||||||
|
// 4. Write to FIFO
|
||||||
|
// Write same sample to Left (PWM1) and Right (PWM2)
|
||||||
|
MMIO32(PWM_FIFO) = sample; // Channel 1
|
||||||
|
MMIO32(PWM_FIFO) = sample; // Channel 2
|
||||||
|
}
|
||||||
|
}
|
||||||
12
link.ld
Normal file
12
link.ld
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
SECTIONS
|
||||||
|
{
|
||||||
|
. = 0x8000;
|
||||||
|
.text : { *(.text.boot) *(.text) }
|
||||||
|
.rodata : { *(.rodata) }
|
||||||
|
.data : { *(.data) }
|
||||||
|
.bss : {
|
||||||
|
__bss_start = .;
|
||||||
|
*(.bss)
|
||||||
|
__bss_end = .;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
setup-dev-env.sh
Executable file
78
setup-dev-env.sh
Executable file
@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Self-documenting setup script for the Raspberry Pi 1 Bare-Metal Synth
|
||||||
|
# development environment.
|
||||||
|
#
|
||||||
|
# This script is tailored for immutable operating systems like Bazzite or
|
||||||
|
# Fedora Atomic, but will work on any system with Distrobox installed.
|
||||||
|
# It creates a clean, containerized Ubuntu 24.04 environment
|
||||||
|
# without altering your host system.
|
||||||
|
#
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
CONTAINER_NAME="pi-synth-dev"
|
||||||
|
CONTAINER_IMAGE="ubuntu:24.04"
|
||||||
|
BOLD=$(tput bold)
|
||||||
|
GREEN=$(tput setaf 2)
|
||||||
|
NORMAL=$(tput sgr0)
|
||||||
|
|
||||||
|
# --- Helper Functions ---
|
||||||
|
print_header() {
|
||||||
|
echo -e "\n${BOLD}${GREEN}=======================================================${NORMAL}"
|
||||||
|
echo -e "${BOLD}${GREEN} $1 ${NORMAL}"
|
||||||
|
echo -e "${BOLD}${GREEN}=======================================================${NORMAL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Start of Script ---
|
||||||
|
clear
|
||||||
|
print_header "Pi 1 Bare-Metal Synth Dev Environment Setup"
|
||||||
|
echo "This script will set up a containerized development environment"
|
||||||
|
echo "to build the bare-metal kernel.img for the Raspberry Pi 1."
|
||||||
|
echo
|
||||||
|
echo "It uses Distrobox to create an Ubuntu 24.04 container named '${BOLD}$CONTAINER_NAME${NORMAL}'."
|
||||||
|
echo "This keeps your host system clean and is ideal for immutable OSes."
|
||||||
|
|
||||||
|
# --- 1. Check for Distrobox ---
|
||||||
|
print_header "Step 1: Checking for Distrobox"
|
||||||
|
if ! command -v distrobox &> /dev/null; then
|
||||||
|
echo "Error: 'distrobox' command not found." >&2
|
||||||
|
echo "Please install Distrobox to continue." >&2
|
||||||
|
echo "See: https://github.com/89luca89/distrobox" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Distrobox found."
|
||||||
|
|
||||||
|
# --- 2. Check if Container Already Exists ---
|
||||||
|
print_header "Step 2: Checking for existing container"
|
||||||
|
if distrobox list | grep -q " ${CONTAINER_NAME} "; then
|
||||||
|
echo "Container '${BOLD}$CONTAINER_NAME${NORMAL}' already exists. Skipping creation."
|
||||||
|
else
|
||||||
|
echo "Container not found. Creating a new one from image '${BOLD}$CONTAINER_IMAGE${NORMAL}'..."
|
||||||
|
distrobox create --name "$CONTAINER_NAME" --image "$CONTAINER_IMAGE"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to create Distrobox container." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Container '${BOLD}$CONTAINER_NAME${NORMAL}' created successfully."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 3. Install Dependencies in Container ---
|
||||||
|
print_header "Step 3: Installing Build Tools inside the Container"
|
||||||
|
echo "Entering the container to install 'make', 'gcc-arm-none-eabi', and 'binutils-arm-none-eabi'..."
|
||||||
|
echo "You may be prompted for your password for 'sudo'."
|
||||||
|
|
||||||
|
distrobox enter "$CONTAINER_NAME" -- sudo apt-get update && sudo apt-get install -y make gcc-arm-none-eabi binutils-arm-none-eabi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to install dependencies inside the container." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Build tools installed successfully."
|
||||||
|
|
||||||
|
# --- 4. Final Instructions ---
|
||||||
|
print_header "Setup Complete!"
|
||||||
|
echo "Your development environment is ready."
|
||||||
|
echo -e "\nTo build the project, run the following commands:"
|
||||||
|
echo -e "1. Enter the container: ${GREEN}distrobox enter $CONTAINER_NAME${NORMAL}"
|
||||||
|
echo -e "2. Navigate to the project directory: ${GREEN}cd '$(pwd)'${NORMAL}"
|
||||||
|
echo -e "3. Run the build script: ${GREEN}./build.sh${NORMAL}"
|
||||||
Loading…
Reference in New Issue
Block a user