The Raspberry Pi 5 represents a massive leap in memory bandwidth and I/O throughput, thanks to the BCM2712 SoC and the RP1 southbridge. But with higher clock speeds and faster peripherals comes a common trap for embedded developers: blowing past your available RAM when buffering high-speed sensor data. Whether you are logging vibration data from an SPI ADC, buffering frames from a CSI camera, or running local inference models, understanding how the Pi 5 manages memory is the difference between a robust data logger and a board that silently OOM-kills your Python script.
In this guide, we will break down the exact RAM specifications across Pi 5 variants, build a high-speed SPI data logger that stresses the memory bus, and walk through the exact debugging steps to fix allocation crashes when your buffers get too large.
Raspberry Pi 5 RAM Specs: Variant Breakdown and Architecture
Unlike the Pi 4, which used LPDDR4-3200, the Raspberry Pi 5 utilizes LPDDR4X-4267 memory. This yields a theoretical bandwidth of roughly 34.1 GB/s. More importantly for embedded Linux, the Pi 5 uses a dynamic Kernel Mode Setting (KMS) driver for the GPU. This means the old gpu_mem=128 trick in config.txt is obsolete; the OS dynamically allocates VRAM from the main RAM pool as needed, which is great for desktop use but requires careful monitoring when writing headless data-logging daemons.
| Variant | Capacity | Memory Type | Speed (MT/s) | Bandwidth | Ideal Use Case | Approx. Price |
|---|---|---|---|---|---|---|
| Pi 5 2GB | 2 GB | LPDDR4X | 4267 | ~34 GB/s | Headless IoT nodes, basic MQTT brokers | $40 |
| Pi 5 4GB | 4 GB | LPDDR4X | 4267 | ~34 GB/s | Home Assistant, light computer vision | $60 |
| Pi 5 8GB | 8 GB | LPDDR4X | 4267 | ~34 GB/s | High-speed SPI buffering, local LLMs, multi-stream video | $80 |
| Pi 5 16GB | 16 GB | LPDDR4X | 4267 | ~34 GB/s | Edge AI training, heavy Docker orchestration | $120 |
The Pi 5 routes GPIO through the RP1 southbridge chip. This makes the GPIO pins strictly 3.3V logic. Unlike some older microcontrollers, the RP1 pins are not 5V tolerant. When interfacing with SPI ADCs or sensors, ensure your modules operate at 3.3V or use a logic level shifter. Feeding 5V into the Pi 5 MISO line will permanently damage the RP1 chip.
Project Build: High-Speed SPI Vibration Data Logger
To demonstrate RAM management, we are building a high-frequency vibration logger. We will read an analog piezo sensor via an MCP3208 12-bit SPI ADC at roughly 20kHz, buffer the readings in a NumPy array in RAM, and flush to disk once the buffer hits 500MB. This project specifically targets the Raspberry Pi 5 8GB variant.
Parts List
- Board: Raspberry Pi 5 8GB (with official Active Cooler - the BCM2712 will thermal throttle under sustained SPI/USB loads without it)
- ADC: MCP3208 12-bit SPI ADC module (3.3V compatible)
- Sensor: Analog Piezo Vibration Sensor (e.g., SW-420 or similar analog output module)
- Storage: High-endurance A2-rated MicroSD card (or NVMe SSD via Pi 5 PCIe HAT)
- Wiring: 24 AWG silicone jumper wires
Pin Mapping Table
| Pi 5 GPIO (Physical Pin) | BCM Name | MCP3208 Pin | Function |
|---|---|---|---|
| 19 (Pin 35) | SPI0_MISO | DOUT | Master In, Slave Out (Data from ADC) |
| 21 (Pin 40) | SPI0_MOSI | DIN | Master Out, Slave In (Channel select) |
| 23 (Pin 16) | SPI0_SCLK | CLK | Serial Clock |
| 24 (Pin 18) | SPI0_CE0 | CS/SHDN | Chip Select (Active Low) |
| 1 (Pin 1) | 3V3 | VDD / VREF | 3.3V Power and Reference |
| 6 (Pin 9) | GND | AGND / DGND | Common Ground |
Complete Python Data-Logging Code
This script uses the spidev library to poll the ADC. It pre-allocates a NumPy array to avoid the massive overhead of Python list appends. Notice the explicit error handling for memory allocation failures, which is critical when pushing the Pi 5's RAM limits.
import spidev
import numpy as np
import time
import os
import sys
import gc
# --- Configuration ---
# Target Board: Raspberry Pi 5 8GB
# Buffer size: ~500MB. Using uint16 (2 bytes per sample).
# 500MB / 2 bytes = 262,144,000 samples max capacity.
BUFFER_CAPACITY = 262_144_000
SAMPLE_RATE_HZ = 20000
SPI_BUS = 0
SPI_DEVICE = 0
SPI_SPEED_HZ = 1000000 # 1MHz is safe for MCP3208 at 3.3V
def init_spi():
spi = spidev.SpiDev()
spi.open(SPI_BUS, SPI_DEVICE)
spi.max_speed_hz = SPI_SPEED_HZ
spi.mode = 0
return spi
def read_adc_channel(spi, channel):
"""Reads a 12-bit value from MCP3208 single-ended channel."""
if channel < 0 or channel > 7:
raise ValueError("Channel must be 0-7")
# MCP3208 command byte structure: Start bit, Single/Diff, D2, D1, D0
cmd = 0x06 | (channel >> 2)
cmd2 = (channel & 0x03) << 6
resp = spi.xfer2([cmd, cmd2, 0x00])
# Extract 12-bit value from response bytes
value = ((resp[1] & 0x0F) << 8) | resp[2]
return value
def main():
spi = init_spi()
print(f"Initializing {BUFFER_CAPACITY} sample buffer...")
try:
# CRITICAL: Use uint16 to save RAM. Default float64 would require 2GB!
buffer = np.zeros(BUFFER_CAPACITY, dtype=np.uint16)
except MemoryError as e:
print(f"FATAL: Failed to allocate RAM buffer.\nError: {e}")
print("Action: Reduce BUFFER_CAPACITY, check OS overhead, or add swap.")
sys.exit(1)
except OSError as e:
print(f"FATAL: OS denied memory allocation.\nError: {e}")
sys.exit(1)
print("Buffer allocated successfully. Starting acquisition...")
index = 0
start_time = time.time()
try:
while index < BUFFER_CAPACITY:
buffer[index] = read_adc_channel(spi, 0)
index += 1
# Optional: throttle to approximate sample rate
# (Real-time guarantees require C/C++ or RT kernel)
# time.sleep(1 / SAMPLE_RATE_HZ)
if index % 1000000 == 0:
elapsed = time.time() - start_time
print(f"Logged {index} samples | Effective Rate: {index/elapsed:.0f} Hz")
except KeyboardInterrupt:
print("\nAcquisition interrupted by user.")
finally:
spi.close()
# Trim buffer to actual size and save to disk
actual_data = buffer[:index]
print(f"Saving {actual_data.nbytes / (1024*1024):.2f} MB to disk...")
np.save("vibration_log.npy", actual_data)
print("Done. Memory released.")
del buffer
gc.collect()
if __name__ == "__main__":
main()
Debugging: Fixing Allocation Crashes and MemoryErrors
When you push a Raspberry Pi 5 to its memory limits, Python will not gracefully degrade; it will crash. Here are the exact error strings you will encounter and how to fix them.
Error 1: The NumPy Allocation Failure
Exact Error String: numpy.core._exceptions._ArrayMemoryError: Unable to allocate 2.00 GiB for an array with shape (262144000,) and data type float64
The Cause: You omitted the dtype argument in your NumPy array initialization. By default, NumPy uses float64, which consumes 8 bytes per element. A 250-million-element array will attempt to claim 2GB of contiguous heap space. On a Pi 5 4GB, the OS desktop environment and background services already consume ~800MB, leaving insufficient contiguous RAM for the kernel to fulfill the request.
The Fix: Always specify dtype=np.uint16 (2 bytes) or dtype=np.float32 (4 bytes) when dealing with sensor data. An MCP3208 only outputs 12-bit integers (0-4095), which fits perfectly inside a 16-bit unsigned integer.
Error 2: The OS-Level OOM Killer
Exact Error String: OSError: [Errno 12] Cannot allocate memory (Often seen when using multiprocessing.shared_memory or forking child processes).
The Cause: The Linux Out-Of-Memory (OOM) killer has stepped in, or the system has exhausted both physical RAM and the swap file. The Pi 5's Bookworm OS defaults to a relatively small swap file (usually 100MB or 256MB via dphys-swapfile), which is inadequate for heavy embedded buffering.
The First 3 Things to Check When It Fails
- Check Actual Available RAM (Not 'Free'): Run
free -min the terminal. Look at the available column, not the free column. Linux caches disk I/O in unused RAM; 'available' tells you what the kernel can actually reclaim for your Python script. - Verify Swap File Configuration: Run
swapon --show. If your swap is under 1GB, edit/etc/dphys-swapfile, changeCONF_SWAPSIZE=2048, and restart the service withsudo systemctl restart dphys-swapfile. Note: Heavy swapping will degrade your MicroSD card rapidly; use an NVMe drive via the Pi 5 PCIe lane if you rely on swap. - Hunt Zombie Processes: Run
htopand sort by MEM%. The Pi 5 Wayfire desktop environment can silently leak memory over days of uptime. If you are running headless data loggers, disable the desktop environment entirely viasudo raspi-config(System Options -> Boot/Auto Login -> Console).
Extending and Simplifying the Build
How to Simplify (For Pi 5 2GB or 4GB Boards)
If you are constrained to a lower-RAM variant, you must reduce the memory footprint per sample and increase the frequency of disk flushes. Instead of pre-allocating a massive 500MB array, allocate a 10MB uint16 buffer. Once the buffer fills, append it to a binary file on disk using np.save() or standard file I/O, then clear the buffer. This trades RAM usage for increased I/O operations, keeping your memory footprint under 50MB regardless of total logging duration.
How to Extend (For Pi 5 8GB or 16GB Boards)
To push beyond Python's limitations and achieve true 100kHz+ sampling rates without dropping packets, you need to bypass the Python Global Interpreter Lock (GIL) and the standard heap allocator.
- Hardware Extension: Attach an NVMe SSD via a Pi 5 PCIe 2.0 x1 HAT. This provides ~400MB/s sequential write speeds, allowing you to stream data directly to disk via memory-mapped files (
mmap) rather than holding it in RAM. - Software Extension: Rewrite the acquisition loop in C or C++ using the bcm2835 library (which supports the Pi 5's RP1 chip via updated forks) or direct
spidevioctl calls. Use Contiguous Memory Allocator (CMA) reservations in the device tree to guarantee a locked block of physical RAM that the OS cannot page out, ensuring zero jitter in your sensor sampling.
Managing RAM on the Raspberry Pi 5 is less about raw capacity and more about understanding how Linux handles memory allocation, data typing, and I/O caching. By selecting the right board variant, enforcing strict data types in your arrays, and monitoring the OS-level memory pools, you can build embedded data loggers that run reliably for months without a single OOM crash.






