Skip to content

Instantly share code, notes, and snippets.

@cGandom
Last active April 24, 2025 16:48
Show Gist options
  • Save cGandom/23764ad5517c8ec1d7cd904b923ad863 to your computer and use it in GitHub Desktop.
Save cGandom/23764ad5517c8ec1d7cd904b923ad863 to your computer and use it in GitHub Desktop.
Emulating Raspberry Pi 4 with Qemu

Emulating Raspberry Pi 4 with Qemu

Just a quick update before we dive in: what we're actually doing here is running Raspberry Pi OS (64-bit) on a QEMU virtual ARM setup. This isn't full-blown hardware emulation of the Raspberry Pi 4, but more about creating a virtual environment for the OS. It doesn't mimic all the specific hardware features of the Pi 4, but it's pretty useful and great for general testing. I turned to this solution mainly to extract a modified sysroot from the Raspberry Pi OS, something not readily available in other resources. For those looking into detailed emulation of the actual Raspberry Pi 4's hardware in QEMU, check out this link for the latest updates: https://gitlab.com/qemu-project/qemu/-/issues/1208.

Hope it helps! :D

Shortcomings: No GUI yet, only console.

Steps

  1. Download Raspberry Pi OS (64-bit) from Raspberry Pi operating system images.
    Here we downloaded Raspberry Pi OS (64-bit) with desktop, Kernel version: 6.1, Debian version: 11 (bullseye), Release date: May 3rd 2023, named 2023-05-03-raspios-bullseye-arm64.img. We put it in /home/mydir.
  2. Install the required packages on your host system:
    $ # Cross compilers for arm64
    $ sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
    
    $ # Qemu itself
    $ sudo apt install qemu qemubuilder qemu-system-gui qemu-system-arm qemu-utils \
        qemu-system-data qemu-system
  3. Build the Linux kernel for qemu arm64 (You can download the kernel from Kernel.org):
    $ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.34.tar.xz
    $ tar xvJf linux-6.1.34.tar.xz
    $ cd linux-6.1.34
    
    $ # create a .config file
    $ ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make defconfig
    $ # Use the kvm_guest config as the base defconfig, which is suitable for qemu
    $ ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make kvm_guest.config
    $ # Build the kernel
    $ ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make -j8
    
    $ cp arch/arm64/boot/Image /home/mydir
  4. Mount the image for enabling ssh and configuring username and password:
    1. Get the correct offset value with the help of fdisk utility:
      $ fdisk -l 2023-05-03-raspios-bullseye-arm64.img
      Disk 2023-05-03-raspios-bullseye-arm64.img: 4.11 GiB, 4412407808 bytes, 8617984 sectors
      Units: sectors of 1 * 512 = 512 bytes
      Sector size (logical/physical): 512 bytes / 512 bytes
      I/O size (minimum/optimal): 512 bytes / 512 bytes
      Disklabel type: dos
      Disk identifier: 0x3e247b30
      Device                                 Boot  Start     End Sectors  Size Id Type
      2023-05-03-raspios-bullseye-arm64.img1        8192  532479  524288  256M  c W95 FAT32 (LBA)
      2023-05-03-raspios-bullseye-arm64.img2      532480 8617983 8085504  3.9G 83 Linux
      As we can see, we have two partitions inside the downloaded image. The first device (partition) is the bootable partition, and the second one is the root filesystem. The first partition is what will be mounted as /boot in Raspberry Pi, and this is where we'll need to create some files.
      Obtain the correct offset of the first device by multiplying the start of the first partition (here 8192) by the sector size (here 512). Here it will be calculated as 8192 * 512 = 4194304
    2. Mount the image in /mnt/rpi directory:
      $ sudo mkdir /mnt/rpi
      $ sudo mount -o loop,offset=4194304 2023-05-03-raspios-bullseye-arm64.img /mnt/rpi
    3. Create a file named ssh to enable ssh:
      $ cd /mnt/rpi
      $ sudo touch ssh
    4. Additionally, create a file named userconf.txt in the same directory and put your desired username and password there, like <username>:<hashed-password> (might be better to leave the username as pi). This will be your default credentials:
      $ openssl passwd -6                                     # Generate the <hashed-password>
      $ echo 'pi:<hashed-password>' | sudo tee userconf.txt   # Put them inside `userconf.txt`
    5. Finally, unmount the image:
      $ sudo umount /mnt/rpi
  5. Run qemu emulator:
    $ cd /home/mydir
    $ qemu-system-aarch64 -machine virt -cpu cortex-a72 -smp 6 -m 4G \
        -kernel Image -append "root=/dev/vda2 rootfstype=ext4 rw panic=0 console=ttyAMA0" \
        -drive format=raw,file=2023-05-03-raspios-bullseye-arm64.img,if=none,id=hd0,cache=writeback \
        -device virtio-blk,drive=hd0,bootindex=0 \
        -netdev user,id=mynet,hostfwd=tcp::2222-:22 \
        -device virtio-net-pci,netdev=mynet \
        -monitor telnet:127.0.0.1:5555,server,nowait
    This machine will be able to access the internet.
  6. After the machine is completely booted up, you can login to it from your computer by using ssh and the username and password you specified:
    $ ssh -l pi localhost -p 2222
  7. Done!

Troubleshooting

  • If you had any problem with connecting to internet, it might be because of bad DNS configurations, and you should consider adding nameserver 8.8.8.8 to top of the file /etc/resolv.conf in the machine.
  • You can access the qemu monitor console with:
    $ telnet localhost 5555
@aidigital
Copy link

Emulating raspi3b is a bit easier than the generic virt because we can simply extract the kernel8.8.img from the raspberry image, without having to compile the linux kernel manually.

qemu-system-aarch64 \
    -machine raspi3b \
    -cpu cortex-a72 \
    -m 1G  \
    -smp 4 \
    -sd ${IMAGE_FILE} \
    -kernel ${KERNEL} \
    -dtb ${DEVICE_TREE_BLOB} \
    -append "rw earlyprintk loglevel=8 console=ttyAMA0,115200 dwc_otg.lpm_enable=0 root=/dev/mmcblk0p2 rootdelay=1" \
    -netdev user,id=net0,hostfwd=tcp::2264-:22 \
    -device usb-net,netdev=net0 \
    -device usb-mouse -device usb-kbd \
    -msg timestamp=on

The above command will also give you graphics if:

  • you use the latest qemu, or at least above v6.2.0, which is what I am getting from apt on Debian.
    That version has a bug which prevents it from creating /dev/fb0 framebuffer, and without that bad boy you have no graphics.
    I went on a wild goose chase trying to compile the kernel with CONFIG_DRM_FBDEV_EMULATION=y and similar params, but that wasn't the problem; the defaults are ok.

With qemu v6.2.0 we see this error during boot:

pi@raspberrypi:/dev$ dmesg | grep -i fb
[    1.501182] bcm2708_fb soc:fb: Unable to determine number of FBs. Disabling driver.
[    1.501286] bcm2708_fb: probe of soc:fb failed with error -2
  • grab raspios buster.
    2021-05-07-raspios-buster-arm64.img - graphics work
    2024-07-04-raspios-bullseye-arm64.img - the logo appears, but after that the graphics don't. don't know why
    2024-07-04-raspios-bookworm-arm64.img - bookworm is even more tricky to get to work than bullseye, even the console won't work

Both of these points are also mentioned here.

If you want to use -machine virt, the advantage is you can give it more cores and ram. But you will need to find the right flags to attach graphics, because otherwise you will get just the console, and you'll see that the /dev/fb0 is not there.

@TheBrick2
Copy link

Works great for me but I have a question. I was initially trying to use this guide and precompiled kernel image and that guide uses a .dtb file and you do not. What's the difference / advantage / disadvantage?

@AIDIGIT
Copy link

AIDIGIT commented Mar 5, 2025

Hey @TheBrick2, think about it like this.
The raspberry .img that we download is not designed for qemu specifically (it's designed for the physical Raspberry),
so its kernel (the kernel8.img inside it) is probably not compiled with kvm_guest config, it's more "generic".

However, '-machine raspi3b' flag (which I used above) works around this, because even though the kernel is generic, it knows the specific machine type: raspi3b.

But when the machine type is also generic (e.g. '-machine virt', or -M versatilepb, used by the guide you linked) it doesn't work, and we need the kernel to be specific.

So that's why in that case, we need to compile the kernel with kvm_guest, or get it pre-compiled from somewhere, e.g. that repo you mentiond.

PS: new account, but I'm the guy who posted the message you quoted.

@Chandler-Kluser
Copy link

Thank you a lot, dear friend!!

@jamesy0ung
Copy link

I got it working under the Apple Hypervisor with virtio graphics. The M4 is fast enough to run llvmpipe. Benchmark: https://browser.geekbench.com/v6/cpu/11278261

https://gist.github.com/jamesy0ung/b411143c15d3c8296684c4987e7c9894

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment