To build a reliable barcode reader for Arduino, use a UART-based CMOS module like the GM65, wire its TX/RX lines through a 3.3V logic level shifter to the Arduino's SoftwareSerial pins (D10/D11), and parse the ASCII stream at 9600 baud. Unlike USB handheld scanners that require complex HID decoding, UART modules output raw string data directly, making them ideal for embedded inventory, access control, or DIY point-of-sale projects.
This guide targets the Arduino Nano V3 (ATmega328P) running at 5V. Because the GM65 operates strictly at 3.3V, we will address the critical voltage translation step that most online tutorials skip—a mistake that routinely destroys the scanner's RX optocoupler within weeks of use.
Module Selection & Specification Comparison
Choosing the right scan engine dictates your project's physical footprint and decoding capabilities. Laser scanners (like the EM1365) are cheaper but cannot read 2D QR codes or scan from smartphone screens due to the refresh rate of digital displays. CMOS imagers are the modern standard.
| Module | Sensor Type | Interface | 1D/2D Support | Typical Price | Best Use Case |
|---|---|---|---|---|---|
| GM65 | CMOS Imager | UART / USB | Both | $12 - $16 | General DIY, Kiosks, Access Control |
| GM77 | CMOS Imager | UART / RS232 | Both | $18 - $24 | Industrial panels, Long-range scanning |
| DY1000 | CMOS Imager | UART | 1D Only | $8 - $11 | Simple retail barcodes, low budget |
| EM1365 | Laser | UART / USB | 1D Only | $10 - $14 | Printed paper labels (fails on screens) |
The GM65 is the optimal choice for Arduino projects. It auto-detects continuous vs. trigger modes via configuration barcodes (included in its datasheet) and handles poorly lit or crinkled QR codes far better than the DY1000.
Parts List & Pin Mapping
Required Components
- Microcontroller: Arduino Nano V3 (ATmega328P, 5V/16MHz variant)
- Scanner: GM65 1D/2D CMOS Barcode Module (UART interface)
- Level Shifter: BSS138 Bidirectional Logic Level Converter (4-channel)
- Power: 5V 2A USB power supply (scanners draw up to 250mA during illumination)
- Wiring: 22 AWG solid core jumper wires
Pin Mapping Table
| GM65 Pin | Level Shifter (LV Side) | Level Shifter (HV Side) | Arduino Nano V3 Pin |
|---|---|---|---|
| VCC (Red) | LV (3.3V) | HV (5V) | 5V Pin |
| GND (Black) | GND | GND | GND |
| TX (Green) | LV1 | HV1 | D10 (SoftwareSerial RX) |
| RX (White) | LV2 | HV2 | D11 (SoftwareSerial TX) |
Wiring Steps & Hardware Setup
- De-energize the circuit. Disconnect the Arduino Nano from USB power before making connections to prevent shorting the 5V rail to ground.
- Wire the Level Shifter Power. Connect the Arduino's 5V pin to the HV (High Voltage) and 3.3V pin to the LV (Low Voltage) on the BSS138 module. Tie both GND pins to the Arduino GND. Reference: For deeper understanding of MOSFET-based translation, review the SparkFun Logic Level Converter tutorial.
- Connect the GM65 Power. Wire the GM65 VCC to the LV (3.3V) side of the shifter. The GM65 requires a stable 3.3V supply capable of delivering 250mA peak. If your Nano's onboard 3.3V regulator overheats, power the LV side from an external 3.3V buck converter.
- Route the Data Lines. Connect GM65 TX to LV1, and HV1 to Nano D10. Connect GM65 RX to LV2, and HV2 to Nano D11.
- Verify Continuity. Use a multimeter in continuity mode to ensure no shorts exist between VCC and GND before applying power.
Compilable C++ Code with Error Handling
The following code targets the Arduino Nano V3 (ATmega328P). It utilizes the SoftwareSerial library, allowing us to keep the hardware UART (pins 0 and 1) free for debugging via the Serial Monitor. The code includes explicit timeout handling and buffer overflow detection.
#include <SoftwareSerial.h>
// Pin Definitions for Arduino Nano V3
#define RX_PIN 10
#define TX_PIN 11
// SoftwareSerial instance
// Note: GM65 default baud rate is 9600.
SoftwareSerial barcodeSerial(RX_PIN, TX_PIN);
// State variables
String currentBarcode = "";
unsigned long lastCharTime = 0;
const unsigned long TIMEOUT_MS = 50; // Time between characters to consider message complete
const int MAX_BARCODE_LEN = 120;
void setup() {
// Initialize Hardware Serial for debugging
Serial.begin(115200);
while (!Serial) { ; } // Wait for serial port to connect (Nano clones may not need this)
// Initialize SoftwareSerial for GM65
barcodeSerial.begin(9600);
Serial.println(F("[SYS] GM65 Barcode Reader Initialized"));
Serial.println(F("[SYS] Waiting for scan..."));
}
void loop() {
// Read data from GM65
while (barcodeSerial.available() > 0) {
char incomingChar = (char)barcodeSerial.read();
// Filter out carriage returns and newlines, but use them as end-of-message markers
if (incomingChar == '\r' || incomingChar == '\n') {
if (currentBarcode.length() > 0) {
processBarcode(currentBarcode);
currentBarcode = ""; // Clear buffer for next scan
}
} else {
// Append character, checking for buffer overflow
if (currentBarcode.length() < MAX_BARCODE_LEN) {
currentBarcode += incomingChar;
lastCharTime = millis();
} else {
Serial.println(F("[ERR] Buffer Overflow - Truncated payload > 120 chars"));
currentBarcode = ""; // Reset to prevent memory corruption
barcodeSerial.flush(); // Clear remaining garbage in UART buffer
return;
}
}
}
// Timeout fallback: If data stopped arriving but no newline was sent
if (currentBarcode.length() > 0 && (millis() - lastCharTime > TIMEOUT_MS)) {
processBarcode(currentBarcode);
currentBarcode = "";
}
}
void processBarcode(String code) {
Serial.print(F("[OK] Scanned: "));
Serial.println(code);
// Example Application Logic
if (code == "ACCESS_GRANTED_01") {
Serial.println(F("[ACT] Triggering Relay: Door Unlocked"));
// digitalWrite(RELAY_PIN, HIGH);
}
}
Debugging: First Three Things to Check
When your Serial Monitor remains blank or outputs garbage, follow this ranked decision tree. These are the three most common failure modes for UART barcode scanners.
1. Exact Error: Garbage Output (e.g., ÿÿÿÿ or ????)
- Cause A (Most Likely): Baud rate mismatch. The GM65 may have been previously configured to 115200 baud via a setup barcode. Fix: Scan the 'Restore Factory Defaults' barcode from the GM65 datasheet, then scan the '9600 Baud' configuration barcode.
- Cause B: SoftwareSerial cannot keep up with high baud rates. If you must use 115200,
SoftwareSerialwill drop bits. Fix: Use a board with multiple hardware UARTs (like the Arduino Mega or ESP32).
2. Exact Error: [ERR] UART Timeout - No data received (or completely blank monitor)
- Cause A: TX and RX lines are swapped. UART requires TX to RX, and RX to TX. Fix: Swap the wires on the LV1/LV2 pins of the level shifter.
- Cause B: The GM65 is in 'Command Mode' instead of 'Normal Scan Mode'. Fix: Scan the 'Normal Mode' configuration barcode from the manual.
- Cause C: Insufficient current on the 3.3V rail. The Nano's onboard AMS1117-3.3 regulator maxes out around 150mA, while the GM65 illumination LED pulls 250mA. Fix: Power the GM65 VCC from the 5V rail through a dedicated 3.3V buck converter (like an LM2596 module).
3. Exact Error: [ERR] Buffer Overflow - Truncated payload
- Cause: You are scanning a dense QR code (like a vCard or URL exceeding 120 characters) and the default
SoftwareSerial64-byte internal buffer is overflowing before theloop()can read it. Fix: For high-density 2D codes, abandonSoftwareSerial. Migrate to an Arduino Leonardo or Pro Micro and useSerial1(Hardware UART). See the official Arduino SoftwareSerial limitations documentation for details on interrupt conflicts.
Extending and Simplifying the Build
How to Simplify the Hardware
If you want to eliminate the BSS138 logic level shifter and the SoftwareSerial library entirely, switch your microcontroller to an ESP32 DevKit V1 or an Arduino Pro Mini 3.3V. Both operate natively at 3.3V logic, allowing you to wire the GM65 directly to the MCU pins. The ESP32 also features three hardware UARTs, completely eliminating the software buffer overflow issues associated with long QR codes.
How to Extend the Project
To turn this into an IoT inventory tracker, integrate the PubSubClient library. In the processBarcode() function, replace the local Serial print with an MQTT publish command:
// Requires ESP32 and PubSubClient library
client.publish("warehouse/inbound", code.c_str());
This pushes every scanned barcode directly to a Node-RED dashboard or Home Assistant instance over WiFi, transforming a $15 DIY scanner into a commercial-grade wireless data terminal.






