Achieving reliable serial thermal printing with Raspberry Pi requires bypassing the Linux serial console, wiring the printer's TTL RX/TX to the Pi's primary UART0 (GPIO 14/15), and driving it via the pyserial library at 19200 baud. While USB printers rely on CUPS and heavy drivers, a raw TTL serial thermal mechanism gives you deterministic, low-latency control over ESC/POS byte commands—ideal for kiosk builds, point-of-sale systems, and automated logging stations.
This guide walks through the exact hardware, GPIO mapping, and Python implementation required to get text and raster graphics printing, alongside the specific debugging steps for the UART permission errors that trap most builders.
Hardware Spec Sheet & Parts List
The most common failure point in embedded thermal printing is underestimating the current draw of the thermal head. The printer below requires a dedicated power supply; attempting to pull 2A+ from the Raspberry Pi's 5V GPIO rail will trigger a brownout and crash the board.
| Component | Exact Variant / Model | Key Specification | 2026 Est. Price |
|---|---|---|---|
| Microcontroller | Raspberry Pi 4 Model B (4GB) or Pi 5 (4GB) | BCM2711/2712, 3.3V UART Logic | $55 - $60 |
| Thermal Printer | Adafruit Mini Thermal Receipt Printer (PID 597) | TTL Serial, 19200 Baud default, 5V-9V Power | $49.95 |
| Printer Power | Mean Well LRS-35-5 or 5V 2A Brick | 5V 2A minimum (dedicated to printer) | $15 - $22 |
| Logic Protection | NXP PCA9306 I2C Level Shifter (Optional) | 3.3V to 5V bidirectional translation | $3.50 |
| Wiring | 28 AWG Silicone Jumper Wires | Female-to-Female, 20cm length | $6.00 |
Note on Logic Levels: The Raspberry Pi's GPIO UART operates strictly at 3.3V. The Adafruit PID 597 printer is 3.3V-5V tolerant on its RX pin, meaning you can safely connect the Pi's 3.3V TX directly to the printer's RX. However, if you are using a generic, unbranded eBay thermal mechanism, verify its TX output voltage. If it outputs 5V on the TX line, you must use a level shifter or a simple voltage divider to step it down to 3.3V before it hits the Pi's GPIO 15 (RXD), or you risk frying the Pi's southbridge.
GPIO Pin Mapping & Wiring Procedure
Before connecting any wires, you must configure the Raspberry Pi OS to expose the hardware UART to the GPIO header. By default, the Pi routes the primary UART to the Bluetooth module and leaves the GPIO pins on the lower-performance mini-UART.
Pin Mapping Table
| Pi GPIO (Physical Pin) | Pi Function | Printer Wire Color | Printer Pin Function |
|---|---|---|---|
| Pin 8 (GPIO 14 / TXD) | UART0 TX | Yellow (or RX) | RX |
| Pin 10 (GPIO 15 / RXD) | UART0 RX | Green (or TX) | TX |
| Pin 6 (GND) | Ground | Black | GND |
Configuration & Wiring Steps
- Disable the Serial Console: Open a terminal and run
sudo raspi-config. Navigate to Interface Options > Serial Port. Select No when asked if you want a login shell over serial, and Yes when asked if you want the serial port hardware enabled. - Force Hardware UART (PL011): Edit your boot config. On Raspberry Pi OS Bookworm (and newer), this file moved. Run
sudo nano /boot/firmware/config.txt(use/boot/config.txton older Bullseye builds). Add the linedtoverlay=disable-btat the bottom. This disables Bluetooth and maps the full-featured PL011 UART to GPIO 14/15. - Reboot: Run
sudo rebootto apply the device tree overlay. - Wire the Data Lines: Connect Pi Pin 8 (TX) to the Printer RX. Connect Pi Pin 10 (RX) to the Printer TX. Connect Pi Pin 6 (GND) to the Printer GND. Notice the crossover: TX always connects to RX.
- Wire the Power: Connect your dedicated 5V 2A power supply to the printer's VH (Heater) and GND pins. Do not connect the printer's VH pin to the Raspberry Pi.
ls -l /dev/serial0. It should symlink to /dev/ttyAMA0 (the hardware UART). If it symlinks to /dev/ttyS0, the mini-UART is still active, and your baud rate will drift under CPU load, resulting in garbled text.
Python UART Implementation (Target: Pi 4 & Pi 5)
The following code targets the Raspberry Pi 4 Model B and Raspberry Pi 5 running a 64-bit Raspberry Pi OS. It utilizes the pyserial library to send raw ESC/POS byte sequences. Install it via pip3 install pyserial.
import serial
import time
import sys
# Target Board: Raspberry Pi 4 / Pi 5
# Port: /dev/serial0 (Symlink to hardware UART /dev/ttyAMA0)
# Baud Rate: 19200 (Default for Adafruit PID 597. Change to 9600 for generic clones)
PRINTER_PORT = '/dev/serial0'
BAUD_RATE = 19200
def init_printer():
try:
ser = serial.Serial(
port=PRINTER_PORT,
baudrate=BAUD_RATE,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=1
)
return ser
except serial.SerialException as e:
print(f"Failed to open port: {e}")
sys.exit(1)
def print_receipt(ser, text_lines):
# ESC @ : Initialize printer
ser.write(b'\x1B\x40')
time.sleep(0.1)
for line in text_lines:
# Encode string to bytes and add newline
ser.write(line.encode('utf-8') + b'\n')
# Thermal printers need time to process and heat the head
time.sleep(0.2)
# Feed 3 blank lines before cutting
ser.write(b'\n\n\n')
time.sleep(0.5)
# GS V 0 : Full cut command (ESC/POS standard)
ser.write(b'\x1D\x56\x00')
ser.close()
if __name__ == '__main__':
receipt_data = [
"--- ELECTRICAL FLUX ---",
"Item: 10 AWG THHN Wire",
"Qty: 50 ft",
"Total: $14.50",
"-----------------------"
]
printer = init_printer()
print_receipt(printer, receipt_data)
print("Print job sent successfully.")
The time.sleep() delays are not optional. Thermal printers buffer data, but if you push bytes faster than the physical head can heat and advance the paper, the internal buffer overflows and the printer will drop characters or lock up until power-cycled.
Debugging: Permission Denied & UART Failures
When working with raw GPIO UART, you will inevitably hit OS-level roadblocks. If your script fails, check these first three things in order:
- Verify User Groups: Is your user in the
dialoutgroup? Rungroups. Ifdialoutis missing, runsudo usermod -a -G dialout $USERand reboot. - Verify Console Disabled: Run
cat /proc/cmdline. If you seeconsole=serial0,115200, the Linux kernel is still hogging the UART for boot logs. Re-runraspi-configand disable the serial login shell. - Verify TX/RX Crossover: If the script runs without errors but the printer does nothing, swap the Yellow and Green wires. It is incredibly common to misread "TX" on a datasheet as "Connect to TX" rather than "This is the TX output".
Common Error Strings & Ranked Causes
Error 1: serial.serialutil.SerialException: [Errno 13] could not open port /dev/serial0: [Errno 13] Permission denied: '/dev/serial0'
- Cause A (Most Likely): Your user lacks
dialoutgroup permissions. Fix:sudo usermod -a -G dialout $USERthen reboot. - Cause B: You are running the script via a cron job or systemd service as the
rootuser, but the udev rules haven't applied to the serial device yet. Fix: Add aSUBSYSTEM=="tty", KERNEL=="ttyAMA0", MODE="0666"rule to/etc/udev/rules.d/99-serial.rules.
Error 2: serial.serialutil.SerialException: [Errno 2] could not open port /dev/ttyS0: [Errno 2] No such file or directory: '/dev/ttyS0'
- Cause A: You hardcoded
/dev/ttyS0in your Python script instead of/dev/serial0. On the Pi 4 and Pi 5,/dev/ttyS0is the mini-UART and may not exist if Bluetooth is disabled. Always use the/dev/serial0symlink, which the OS automatically maps to the correct active hardware UART.
Error 3: Printer prints garbage characters (e.g., "ÿÿÿ") instead of text.
- Cause A: Baud rate mismatch. The Adafruit printer defaults to 19200 baud. Generic clones often default to 9600. Change the
BAUD_RATEvariable in the script to match your hardware. - Cause B: You are using the mini-UART (
/dev/ttyS0) without disabling Bluetooth. The mini-UART's baud rate is tied to the core CPU clock, which scales dynamically. Under load, the baud rate drifts, corrupting the serial data. Force the PL011 hardware UART viadtoverlay=disable-bt.
Extending and Simplifying the Build
How to Simplify: The USB-to-TTL Bypass
If you do not want to deal with raspi-config, device tree overlays, or GPIO wiring, you can completely bypass the Pi's internal UART. Purchase a USB-to-TTL Serial Cable (e.g., an FTDI FT232RL-based adapter, ~$12). Plug the USB end into the Pi, and wire the adapter's TX/RX/GND to the printer. The OS will mount it as /dev/ttyUSB0. You simply change the PRINTER_PORT variable in the Python script to /dev/ttyUSB0. This sacrifices a USB port but eliminates 90% of OS-level UART debugging.
How to Extend: Bitmap & QR Code Printing
The ESC/POS protocol supports raster graphics, allowing you to print dynamic QR codes or logos. To extend this build, install pillow and qrcode via pip. You can generate a QR code image in memory, resize it to the printer's native 384-pixel width, convert it to a 1-bit black-and-white bitmap, and translate the pixel array into the ESC/POS GS v 0 raster image byte sequence. This transforms the kiosk from a simple text logger into a modern interface capable of printing WiFi credentials, payment links, or dynamic URL redirects.
For deeper ESC/POS command references and byte-level formatting, consult the Epson ESC/POS specification or the Adafruit Thermal Printer Guide. For Raspberry Pi UART configuration specifics, refer to the official Raspberry Pi UART documentation.






