rajveerchaudhari.com · open the BIOS version → · plain version

Rajveer Chaudhari

Firmware & Low-Level Systems. I write the code that runs before everything else.

rajveer@rajveerchaudhari.com · GitHub · Resume (PDF)

Open to firmware / embedded internships

About

I'm a CS undergrad who likes the layer where software meets silicon. I've written an x86 operating system from the bootloader up, designed a drone flight-controller PCB and I'm bringing it up with hand-written ARM assembly, and I've had patches accepted into the mainline Linux kernel.

I learn by building the whole stack myself: no HAL, no RTOS, no external libraries. Then I debug it with GDB over SWD, a UART console and a lot of reference-manual reading until I can explain every cycle.

MetalBird: FPV drone flight controller: custom PCB + bare-metal firmware

Feb 2026 – present · STM32F411 · ARM Thumb-2 asm · C · KiCad · OpenOCD · GDB · repo · 3D board, PCB layers and schematic

3D render of the MetalBird v1 PCB

A drone flight controller built from zero: my own 2-layer PCB, and firmware written in Cortex-M4 assembly with no dev board, no HAL and no RTOS.

MCUSTM32F411CE (QFN-48), Cortex-M4F @ 84 MHz (8 MHz HSE → PLL)
Board2-layer, 38 × 38 mm, 611 track segments, 94 vias (v1, fabricated)
PowerMP2331H buck 7.4 V → 5 V, AMS1117 3.3 V LDO, PMOS reverse-polarity protection
SensorsMPU-6050 IMU + BMP280 barometer on I2C
I/O4 ESC headers, ELRS receiver, camera + VTX, USB/UART debug, SWD
FirmwareHand-written vector table, startup, linker script; clocks, SysTick, USART, I2C

Firmware

Timeline

PenguOS: 32-bit x86 operating system from scratch

Dec 2025 – present · C · x86 asm (NASM) · QEMU · GDB · repo · boot it in the browser

A bootable 32-bit OS with no external libraries: my own two-stage bootloader, memory manager, drivers, preemptive scheduler and a software 3D renderer.

Boot path

  1. BIOS: Loads sector 0 to 0x7C00
  2. Stage 1: INT 13h extended read: 8 sectors → 0x7E00
  3. Stage 2: VBE mode → GDT → protected mode → ATA PIO kernel load to 1 MB
  4. Kernel: Serial → IDT → PIC → PMM → paging → salloc → VBE → mouse → STI → FPU
  5. Userland-ish: Shell, graphics, threads, 3D renderer

Timeline

Linux kernel

In mainline

Also sent

All my mail on lore.kernel.org · read every patch in the browser

Debug logs

The two "dead" instructions that made I2C work

MetalBird

In i2c_init there were two lines that re-read GPIOB->AFRH into R1, and R1 was overwritten three instructions later. Removing them made the firmware hang at the START-bit self-test.

I diffed the objdump output of both builds. Every instruction was identical; only the code after that point moved 6 bytes earlier. So the read itself didn't matter. Timing did.

STM32F4 flash is fetched in 16-byte lines with 2 wait states. In the working build, the instruction after the RCC_APB1ENR store started a fresh flash line and stalled for a few cycles. In the broken build it didn't.

Those few cycles were hiding the real bug: after enabling a peripheral clock, the store can still be in flight when the next instruction touches the peripheral. Writes to an unclocked APB peripheral are silently dropped, so I2C_CR2, CCR and TRISE never took effect. ST lists this in the errata ("Delay after an RCC peripheral clock enabling").

The correct fix is to read RCC_APB1ENR back (plus a DSB) before touching I2C2. That's correct by construction, not by where the code happens to land in flash.

Lesson: A fix you can't explain causally is a mask, not a fix.

Proving the IMU was dead, not my driver

MetalBird

Sensor readings were wrong, and with a hand-written I2C driver there were plenty of suspects. To take my own driver out of the picture, the diagnostic firmware bit-bangs I2C on the same pins.

It runs the factory self-test from the register map (±14 % pass limit, computed on the host), then streams the FIFO at exactly 1 kHz with a sequence counter, so a lost sample is a detected gap, not a guess.

Result, reproduced on 4 runs: accel Y stuck at 32767 (full scale) with zero noise and zero self-test response, accel X reading ≈ +1.96 g while flat, and gyro Z with a −43 °/s zero-rate offset (+271 % self-test). WHO_AM_I = 0x68, so it's a genuine part that's damaged.

Lesson: Build the tool that makes the bug impossible to argue with.

PenguOS vs. a read-only BIOS shadow

PenguOS

Stage 2 read the kernel-size sector into 0xFFE00, just below 1 MB. That's the BIOS ROM shadow region; on SeaBIOS it's read-only, so the size came back as BIOS code and the loader kept reading sectors until the ATA controller errored.

Moving the buffer to 0x90000 (free conventional memory) fixed it. The web build also pads the image to a whole number of sectors and picks the 1280×720×32 VBE mode the emulated card offers.

Lesson: Every emulator is a new board. Bring-up never really ends.

Skills

LanguagesC, ARM Thumb-2 assembly (GAS), x86 assembly (NASM), Python, Bash
EmbeddedSTM32F4 / Cortex-M4 at register level, RCC / clock tree, GPIO, USART, I2C, SysTick, Startup code & linker scripts
HardwareKiCad schematic & PCB layout, Buck + LDO power design, Datasheet-driven bring-up
Debug & ToolsGDB, OpenOCD, SWD / CMSIS-DAP, QEMU, arm-none-eabi, GCC, Make, Git
Kernelx86 protected mode, paging, interrupts, Linux IIO subsystem, Mailing-list patch workflow

Education

B.Tech CSE, IIIT Vadodara (International Campus Diu), Aug 2024 – May 2028. Coursework: Operating Systems, Computer Organization, Digital Logic & Electronics, Computer Networks.