Adding cellular connectivity to a headless IoT gateway usually means fumbling with physical nano-SIM cards, which is a liability in remote or high-vibration deployments. To use an eSIM on a Raspberry Pi, you need a cellular HAT equipped with an LPA (Local Profile Assistant) capable modem—specifically the Quectel RM520N-GL—and you must provision the profile via AT commands over the serial UART interface. This guide walks through the exact hardware, wiring, and Python-based LPA provisioning required to get a 5G eSIM online, along with the specific error strings you will encounter when the SM-DP+ server rejects your download request.

Build Specs & Difficulty
Target Board: Raspberry Pi 5 (8GB variant recommended for 5G baseband overhead)
OS: Raspberry Pi OS (64-bit, Bookworm)
Difficulty: 3/5 (Requires UART configuration and basic Python serial scripting)
Estimated Time: 45 minutes
Estimated Cost: $280 - $340 (Pi 5 + HAT + 5G Modem + Antennas)

Hardware Requirements and Pin Mapping

Not all M.2 cellular modems support eSIM. The standard Quectel RM500Q lacks an integrated eUICC (embedded Universal Integrated Circuit Card). You must source the Quectel RM520N-GL variant specifically populated with the eSIM chip, paired with a HAT that breaks out the necessary UART and power lines.

ComponentExact Model / VariantNotes
SBCRaspberry Pi 5 (8GB)Requires 27W USB-C PD power supply to prevent modem brownouts.
Cellular HATWaveshare SIM8200EA-M2 5G HATIncludes active cooling fan and pogo pins for Pi 5 UART.
5G ModemQuectel RM520N-GL (eSIM variant)M.2 B-Key form factor. Verify eUICC presence on the PCB.
Antennas4x 5G MIMO IPEX to SMADo not power the modem without antennas attached; RF reflection can damage the PA.

UART Pin Mapping (Pi 5 to Waveshare HAT)

The Raspberry Pi 5 uses a different UART mapping than the Pi 4. The primary UART (/dev/ttyAMA0) must be mapped to the GPIO header to communicate with the modem's AT command port. The Waveshare HAT uses pogo pins to make this connection without jumper wires.

Pi 5 GPIOHAT PinFunction
GPIO 14 (TXD)RXDPi Transmit to Modem AT Port
GPIO 15 (RXD)TXDPi Receive from Modem AT Port
GPIO 4WAKEModem Wakeup / Power Enable
5V (Pin 2/4)5VMain Power Rail (Modem peaks at 3A)
GND (Pin 6)GNDCommon Ground

UART Configuration and Physical Assembly

Before stacking the HAT, you must free up the primary UART from the Raspberry Pi's Bluetooth module and disable the serial console login, which will otherwise spam the modem with garbage data and corrupt your AT command responses.

  1. Disable Serial Console: Run sudo raspi-config, navigate to Interface Options > Serial Port. Select No for 'login shell to be accessible over serial', and Yes for 'serial port hardware to be enabled'.
  2. Configure Device Tree Overlay: Edit your boot configuration. On Pi 5 (Bookworm), this is done in /boot/firmware/config.txt. Add the following line to map UART0 to the GPIO pins:
    dtoverlay=uart0,ctsrts
  3. Physical Assembly: Screw the RM520N-GL into the M.2 B-Key slot on the Waveshare HAT using the provided 2mm screw. Warning: Do not overtighten; the PCB trace layers are fragile. Attach the 4 IPEX antennas before mounting the HAT to the Pi.
  4. Mount HAT: Align the pogo pins with the Pi 5 GPIO header and press down firmly. Secure with the brass standoffs.
  5. Reboot and Verify: After rebooting, verify the serial port exists by running ls -l /dev/ttyAMA0. You should see a character device file.
Safety Callout: RF and Power
Never transmit on the 5G modem without all four MIMO antennas attached. The return loss can damage the internal power amplifier. Furthermore, the RM520N-GL can pull transient currents exceeding 3A during tower handshakes. If your Pi 5 is powered by a standard 15W phone charger, the modem will brownout and reset the USB/UART bridge. Use the official 27W Raspberry Pi USB-C PD power supply.

Python LPA Provisioning Script

Unlike consumer phones that scan a QR code to trigger an LPA download, IoT modems require you to pass the SM-DP+ server address and the Activation Code directly via AT commands. The following Python script uses pyserial to wake the modem, verify the eUICC state, and download the eSIM profile.

Prerequisite: Install pyserial via pip install pyserial.

import serial
import time
import sys
import re

# --- Pin Definitions & Serial Config ---
# GPIO 14 (TX) -> HAT RX, GPIO 15 (RX) -> HAT TX
SERIAL_PORT = '/dev/ttyAMA0'
BAUD_RATE = 115200
TIMEOUT = 30

# Replace with your IoT provider's SM-DP+ address and Activation Code
# Example format from 1NCE, Hologram, or Twilio Super SIM eSIM
SMDP_ADDRESS = 'rsp.1nce.com'
ACTIVATION_CODE = '89012345678901234567'

def send_at_command(ser, cmd, expected='OK', timeout=10):
    """Sends AT command and waits for expected response or error."""
    ser.reset_input_buffer()
    full_cmd = cmd + '\r\n'
    ser.write(full_cmd.encode())
    print(f'TX: {cmd}')
    
    response = ''
    start_time = time.time()
    
    while (time.time() - start_time) < timeout:
        line = ser.readline().decode('utf-8', errors='ignore').strip()
        if line:
            print(f'RX: {line}')
            response += line + '\n'
            if expected in line:
                return response
            if '+CME ERROR' in line or '+QESIM: 1,1,4' in line or '+QESIM: 1,1,5' in line:
                raise RuntimeError(f'Modem returned error: {line}')
        time.sleep(0.1)
        
    raise TimeoutError(f'Timeout waiting for {expected}')

def provision_esim():
    try:
        ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1)
        print(f'Connected to {SERIAL_PORT}')
        
        # Wake up modem (Waveshare HAT handles GPIO 4 toggle via hardware circuit)
        time.sleep(2)
        
        # Basic handshake
        send_at_command(ser, 'AT', 'OK')
        send_at_command(ser, 'ATE0', 'OK') # Disable echo
        
        # Check eUICC status
        esim_status = send_at_command(ser, 'AT+QESIM?', 'OK')
        if '+QESIM: 0' in esim_status:
            print('eUICC not detected. Check M.2 seating.')
            sys.exit(1)
            
        # Initiate Profile Download
        # Syntax: AT+QESIM=0,"",""
        download_cmd = f'AT+QESIM=0,"{SMDP_ADDRESS}","{ACTIVATION_CODE}"'
        print('Initiating eSIM profile download...')
        
        # Profile downloads can take up to 60 seconds depending on RF link
        download_resp = send_at_command(ser, download_cmd, 'OK', timeout=90)
        
        if '+QESIM: 1,1,0' in download_resp:
            print('SUCCESS: Profile downloaded and installed.')
            # Enable the newly downloaded profile (AID parsing omitted for brevity)
            send_at_command(ser, 'AT+QESIM=1,1,""', 'OK')
        else:
            print('Download completed but success flag missing.')
            
    except RuntimeError as e:
        print(f'FAILED: {e}')
        print('Check debugging section for CME ERROR codes.')
    except TimeoutError as e:
        print(f'FAILED: {e}')
    except serial.SerialException as e:
        print(f'Serial port error: {e}. Is /dev/ttyAMA0 enabled in config.txt?')
    finally:
        if 'ser' in locals() and ser.is_open:
            ser.close()

if __name__ == '__main__':
    provision_esim()

Debugging eSIM Profile Failures

When the script fails, the modem will return specific error strings. Do not guess; read the exact string and cross-reference it with the ranked causes below.

Exact Error String: +QESIM: 1,1,4

This indicates a network timeout during the SM-DP+ profile download phase. The modem connected to the tower, but the secure TLS tunnel to the eSIM server dropped.

  • Cause 1 (Most Likely): No Initial Bearer APN. The modem needs an active data connection to reach the SM-DP+ server. If your physical bootstrap SIM (or default APN) isn't configured via AT+CGDCONT, the LPA download will time out. Set the APN first using the default IoT APN provided by your carrier.
  • Cause 2: Weak RF Signal. eSIM profile downloads require a stable, moderate-bandwidth connection. If AT+CSQ returns an RSSI below 10 (approx -93 dBm), the TLS handshake will fail. Relocate the antennas or check for IPEX connector damage.
  • Cause 3: Incorrect SM-DP+ Address. A typo in the SMDP_ADDRESS variable. Ensure you are using the IoT-specific LPA server, not a consumer phone provisioning server.

Exact Error String: +CME ERROR: 515

This is a generic 'SIM busy' or 'eUICC locked' error. It usually occurs when you send the AT+QESIM command while the modem's internal state machine is still processing a previous network registration or SIM reset.

  • Cause 1: Command Collision. You sent the download command before the modem finished registering on the network. Add a 10-second delay after AT+CREG? confirms registration before attempting the download.
  • Cause 2: eUICC Firmware Bug. Early batches of the RM520N-GL had a bug where the eUICC locked up after a soft reboot. Fix: Issue a cold boot by toggling the HAT's hardware reset pin, or physically remove power from the Pi for 30 seconds to drain the capacitors.
The First Three Things to Check When It Fails:
  1. Run AT+CPIN? to ensure the modem sees the eUICC hardware (should return READY or SIM PIN, not NOT INSERTED).
  2. Run AT+CGDCONT? to verify your bootstrap APN is set and active.
  3. Run AT+CSQ to verify you have an RSSI value greater than 15 before triggering the download.

Frequently Asked Questions

Can I use a standard smartphone eSIM QR code on a Raspberry Pi eSIM module?

No. Consumer smartphones use the GSMA SGP.22 standard, which relies on a graphical Local Profile Assistant (LPA) app to parse a QR code, authenticate with the carrier's consumer SM-DP+ server, and download the profile. IoT modules like the Quectel RM520N-GL use the GSMA SGP.32 IoT architecture (or a proprietary AT-command LPA). You cannot simply scan a T-Mobile or Vodafone phone QR code with a Pi camera and push it to the modem. You must obtain an IoT-specific activation code and SM-DP+ address from an IoT connectivity provider (like 1NCE, Hologram, or Twilio) and pass them via the AT+QESIM command.

Why does my Raspberry Pi eSIM module drop the profile after a reboot?

If your eSIM profile disappears or fails to attach to the network after a reboot, it is usually due to one of two reasons. First, the profile might be downloaded but not explicitly enabled in the eUICC's non-volatile memory. You must send the enable command (using the profile's AID) and verify it with AT+QESIM?. Second, the Raspberry Pi 5's boot sequence can cause voltage sags on the 5V rail. If the voltage drops below 4.8V during the kernel initialization phase, the Waveshare HAT's voltage regulator may reset the modem, corrupting the active profile session. Ensure you are using the official 27W Pi 5 power supply and check your dmesg logs for 'under-voltage detected' warnings.

How do I extend or simplify this build for remote fleet deployment?

To simplify the build for a single device, use a cloud-based eUICC manager provided by your SIM vendor. Many modern IoT eSIM providers support 'Push' provisioning, where the modem connects using a factory-installed bootstrap profile, and the vendor's cloud server pushes the final operational profile over-the-air via SGP.32, eliminating the need to hardcode activation codes in your Python script.

To extend this for a fleet of 100+ devices, integrate a hardware watchdog timer and utilize the RM520N-GL's dual-SIM capability. Insert a physical, cheap fallback nano-SIM into the secondary M.2 slot. You can then use AT+QUIMSLOT=1 or 2 to programmatically switch to the physical SIM if the eSIM profile download fails or the primary carrier experiences a regional outage, ensuring your gateway always comes online.