QEMU ARM64 Emulation on Linux: Running Raspberry Pi OS and Android for IoT Development

Virtualization tutorial - IT technology blog
Virtualization tutorial - IT technology blog

Running ARM64 on an x86 Machine in 5 Minutes

I run into this situation all the time: a Raspberry Pi is sitting right there, but I need to test firmware on ARM64 right now — or I’m doing a cross-compile and want to verify the output before flashing it to a real board. QEMU solves both problems — no hardware required, no extra board to buy.

My homelab runs Proxmox VE managing 12 VMs and containers — it’s my playground for testing everything before pushing to production. The most-used VM? QEMU ARM64 running Raspberry Pi OS for IoT code testing. Each iteration saves me 10–15 minutes compared to the flash-boot-debug cycle on real hardware.

Install QEMU on Ubuntu/Debian:

sudo apt update
sudo apt install -y qemu-system-arm qemu-utils qemu-efi-aarch64 wget

Verify QEMU is ready:

qemu-system-aarch64 --version
# QEMU emulator version 8.x.x

Download Raspberry Pi OS Lite (ARM64):

wget https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz
unxz 2024-11-19-raspios-bookworm-arm64-lite.img.xz

# Expand the disk (default ~2GB, resize to 8GB)
qemu-img resize 2024-11-19-raspios-bookworm-arm64-lite.img 8G

ARM64 requires UEFI firmware to boot — without it, the VM simply won’t start:

cp /usr/share/qemu-efi-aarch64/QEMU_EFI.fd .
# Create flash drive for UEFI vars
dd if=/dev/zero of=flash1.img bs=1M count=64
dd if=/dev/zero of=flash0.img bs=1M count=64
dd if=QEMU_EFI.fd of=flash0.img conv=notrunc

Boot the ARM64 virtual machine:

qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a72 \
  -m 2G \
  -smp 4 \
  -drive if=pflash,format=raw,file=flash0.img,readonly=on \
  -drive if=pflash,format=raw,file=flash1.img \
  -drive if=virtio,format=raw,file=2024-11-19-raspios-bookworm-arm64-lite.img \
  -netdev user,id=net0,hostfwd=tcp::2222-:22 \
  -device virtio-net-pci,netdev=net0 \
  -nographic

After about 30–60 seconds you’ll have a shell. SSH in via port 2222:

ssh -p 2222 pi@localhost
# Default password: raspberry

How QEMU ARM64 Works

The key distinction here: KVM uses hardware virtualization and runs at near-native speed. QEMU ARM64 on an x86 host is different — it has to translate every instruction, making it 5–10x slower than a real Pi. That’s fine for IoT development, but don’t expect performance on par with real hardware.

I primarily use QEMU ARM64 for these tasks:

  • Testing Python/Go code on ARM64 before deploying to a Pi
  • Building and testing ARM64 Docker images (using buildx)
  • Debugging firmware logic without needing a physical board
  • CI/CD pipelines: testing ARM64 code in GitHub Actions

Key parameters worth understanding:

  • -machine virt: Generic virtual ARM board, best-supported by QEMU
  • -cpu cortex-a72: The CPU used in Raspberry Pi 4 — swap to cortex-a53 for Pi 3
  • -smp 4: 4 cores, equivalent to Pi 4
  • hostfwd=tcp::2222-:22: Forwards the SSH port to the host so you can connect

Running Android for IoT Development

In IoT, Android shows up in many forms: from Android Things (discontinued by Google in 2022) to custom AOSP builds for embedded devices. With QEMU, there are two popular options: Android-x86 for quick experimentation, or Cuttlefish — Google’s official Android Virtual Device — for more serious development.

Option 1: Android-x86 (Simplest Approach)

# Download Android-x86 ISO
wget https://sourceforge.net/projects/android-x86/files/Release%209.0/android-x86_64-9.0-r2.iso

# Create disk image
qemu-img create -f qcow2 android.img 16G

# Boot from ISO to install
qemu-system-x86_64 \
  -m 4G \
  -smp 4 \
  -enable-kvm \
  -cpu host \
  -drive file=android.img,if=virtio \
  -cdrom android-x86_64-9.0-r2.iso \
  -boot d \
  -vga virtio \
  -display gtk \
  -netdev user,id=net0,hostfwd=tcp::5555-:5555 \
  -device virtio-net-pci,netdev=net0

After installation, reboot without the cdrom and connect via ADB:

adb connect localhost:5555
adb devices
adb shell

Option 2: Cuttlefish — Official Android Virtual Device

Cuttlefish runs a full AOSP stack and is better suited for IoT app development than Android-x86:

# Install dependencies
sudo apt install -y qemu-kvm libvirt-daemon-system unzip

# Download Cuttlefish host packages from Google CI
# Find the latest build at: https://ci.android.com/builds/branches/aosp-main/grid
# Download: cvd-host_package.tar.gz + aosp_cf_x86_64_phone-img-*.zip

# Extract and run
mkdir cuttlefish && cd cuttlefish
tar xzvf ../cvd-host_package.tar.gz
unzip ../aosp_cf_x86_64_phone-img-*.zip

# Start
./bin/launch_cvd \
  -daemon \
  -memory_mb 4096 \
  -num_instances 1

Practical IoT Development Workflow

Cross-Compile and Test Directly on QEMU

Here’s a workflow I use regularly: writing a Go service that runs on a Raspberry Pi, tested on QEMU first:

# Build Go binary for ARM64
GOOS=linux GOARCH=arm64 go build -o myservice ./cmd/myservice

# Copy to QEMU ARM64 via SSH
scp -P 2222 myservice pi@localhost:~

# SSH in and run
ssh -p 2222 pi@localhost './myservice --config /etc/myservice.yaml'

Docker buildx with QEMU

This combination is incredibly powerful for CI/CD — build ARM64 Docker images directly on an x86 machine:

# Register QEMU binfmt handlers with the kernel
docker run --privileged --rm tonistiigi/binfmt --install all

# Create a builder with multi-arch support
docker buildx create --name mybuilder --use
docker buildx inspect --bootstrap

# Build and push ARM64 image
docker buildx build \
  --platform linux/arm64 \
  --tag myregistry/iot-app:latest \
  --push .

# Test ARM64 image directly on x86 (QEMU emulates automatically)
docker run --rm --platform linux/arm64 myregistry/iot-app:latest --version

Quick-Start Script

Remembering all the QEMU flags is tedious. I wrap them into a script for faster invocation:

#!/bin/bash
# start-rpi-qemu.sh

DISK="${1:-rpi-arm64.img}"
RAM="${2:-2G}"
SMP="${3:-4}"
SSH_PORT="${4:-2222}"

qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a72 \
  -m "$RAM" \
  -smp "$SMP" \
  -drive if=pflash,format=raw,file=flash0.img,readonly=on \
  -drive if=pflash,format=raw,file=flash1.img \
  -drive if=virtio,format=raw,file="$DISK" \
  -netdev user,id=net0,hostfwd=tcp::"${SSH_PORT}"-:22 \
  -device virtio-net-pci,netdev=net0 \
  -nographic \
  -pidfile qemu-arm64.pid

echo "QEMU ARM64 running. SSH: ssh -p ${SSH_PORT} pi@localhost"

Practical Tips

If your host machine is ARM64 — AWS Graviton, Mac M-series via UTM — add -enable-kvm to your QEMU command and you’ll get near-native speed immediately. On x86, KVM won’t work when the CPU architectures differ.

Snapshots for fast rollback: Raw images don’t support snapshots. Convert to qcow2 to get this feature:

# Convert to qcow2
qemu-img convert -f raw -O qcow2 rpi.img rpi.qcow2

# Take a snapshot before testing
qemu-img snapshot -c clean-state rpi.qcow2

# Roll back to the clean snapshot
qemu-img snapshot -a clean-state rpi.qcow2

To mount a host directory inside the guest, use the 9P virtio filesystem — far more convenient than copying files with scp one by one:

# Add to the QEMU command:
-fsdev local,security_model=passthrough,id=fsdev0,path=/home/user/shared \
-device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare

# Inside the ARM64 guest:
mount -t 9p -o trans=virtio hostshare /mnt/shared

Headless on a server: -nographic lets QEMU run entirely through the terminal. Add -daemonize to run it in the background, and use -pidfile so you can kill it later:

# Run in the background
qemu-system-aarch64 [parameters...] -daemonize -pidfile qemu.pid

# Stop it
kill $(cat qemu.pid)

QEMU also has its own monitor console — useful when you need to inspect VM state or manage snapshots without logging into the guest:

# Add this parameter:
-monitor unix:qemu-monitor.sock,server,nowait

# Connect to the monitor:
socat - UNIX-CONNECT:qemu-monitor.sock
# Commands: info status, savevm snapshot1, loadvm snapshot1, quit

QEMU ARM64 can’t replace real hardware in every situation — GPIO, hardware interrupts, and timing-sensitive code still need to be tested on actual boards. But for business logic, API layers, and the majority of application code, QEMU eliminates a huge number of debug-flash-test cycles.

Share: