← all projects

Digicam

A bare-metal digital camera on an STM32L432KC with SD card storage

CSTM32L432KCPlatformIOOV2640ST7789FatFsTJpgDec

A working digital camera built on a Nucleo-L432KC: press a button, the OV2640 sensor captures a JPEG, the microcontroller decodes it, paints it on a 240×320 ST7789 panel, and writes it to an SD card. A second mode reads the card back and pages through the photos.

Everything below the application talks to hardware directly — SPI, I²C, GPIO, and the clock tree are configured by writing registers.

Built with Jack Geraghty, Andrew Schretzmayer, and Taiyr Ashkenov for EE14 at Tufts. My work was the SPI and I²C layers, the OV2640 bring-up, and the colour-format and orientation fixes described below.

Video of the working device can be found on the GitHub page.

Two SPI buses

The obvious wiring is a single SPI bus with three chip selects for camera, display, and SD card. We built that first, and it was unreliable: transactions to one device would corrupt another’s state, and no data could move through the whole pipeline.

The camera and display were kept on SPI1; the SD card moved to SPI3. Two peripherals cost more GPIO and a second initialisation path, and they guarantee that a filesystem write can’t be interleaved with a display refresh.

Colour format and the byte swap

TJpgDec emits RGB565 pixels; the ST7789 expects RGB565 with the opposite byte order. Every decoded block therefore gets swapped in the decoder’s output callback before it reaches the panel:

for (uint32_t i = 0; i < npix; i++) {
    uint16_t v = px[i];
    px[i] = (v << 8) | (v >> 8);
}

Memory budget

The L432KC has 64 KB of SRAM, and the two buffers that matter are a 12 KB JPEG staging buffer and an 8 KB TJpgDec working buffer.

We decided to fix the camera at 320×240 JPEG rather than the OV2640’s full 2 MP. load_fifo reads the sensor’s FIFO length first and refuses the frame if it won’t fit, rather than overrunning the buffer:

uint32_t length = camera_fifo_length();
if (length == 0 || length > sizeof(jpeg_buf) - 4) return -1;

Decoding streams through TJpgDec block-by-block straight to the display, so a full framebuffer never has to exist in memory.

Working around the FIFO

Capture is wrapped in a retry loop: a FIFO read that doesn’t produce a valid JPEG header triggers a recapture instead of handing a corrupt frame to the decoder. A missing SD card is likewise non-fatal; the camera boots, reports NO SD, and still captures and previews.

Credit to Outside Works

FatFs and TJpgDec are ChaN’s, the SD block driver is ST’s Adafruit-shield BSP, and the ST7789 driver started from Floyd-Fish’s and was adapted. Written for this project: the SPI1/SPI3 drivers, the I²C layer and OV2640 register bring-up, the ArduCAM FIFO protocol, the clock configuration, the 5×7 bitmap font and UI drawing, and the application state machine.

Limitations

  • No DMA. Every SPI transfer is a polled loop at roughly 2.5 MHz, so the CPU blocks for the entire duration of a frame read and a display paint.
  • Fixed at 320×240. Larger captures are a buffer-size and display problem, not a sensor limitation.
  • The library caps at 64 photos and pages are forward only