Community project

Smart Thermal Printing Calculator

ESP32
Photo of Smart Thermal Printing Calculator
Generated with AI

Muhammad Anas

Published August 28, 2026

This project builds a portable calculator with a thermal printer and a large color touchscreen display, powered by an ESP32 microcontroller. The device combines a 35-key calculator keypad, a 4.58-inch RGB IPS display, and a CSN-A2 thermal printer to create a compact computing tool that can print receipts and calculations on demand.

The guide provides a complete wiring diagram, detailed parts list, and step-by-step assembly instructions to integrate the power management system (including Li-Po battery, BMS protection, fuel gauge, and USB-C charging), dual voltage rails for the printer and logic circuits, and all peripheral connections. Firmware code is included to handle the display rendering, keypad scanning, printer communication, and battery monitoring through LVGL graphics and the MCP23017 I/O expander.

Wiring diagram

Wiring diagram for Smart Thermal Printing Calculator

Gather all the parts

QtyComponent
1

CSN-A2 Thermal Printer

58 mm / 5–9 V TTL

Mini thermal receipt printer (CSN-A2 / CSN-A2-T) controlled over TTL serial — default 19200 baud — using the Adafruit Thermal Printer library. The on-printer connector exposes a POWER side (1=GND, 2=NC, 3=VH 5-9V) and a TTL side (4=GND, 5=printer RX, 6=printer TX). Wire the printer's RX (TTL pin 5) to an MCU TX GPIO; the printer's TX (TTL pin 6) is only needed if you want to read printer status back. Power VH from a dedicated 5-9V supply rated for ~1.5-2A peaks during printing — never draw VH from the MCU's onboard 3.3V or 5V regulator. Tie POWER GND (pin 1) and TTL GND (pin 4) to the same common ground as the MCU.

1

MP1584 Buck Converter

MP1584EN mini buck module, adjusted to 5.0 V

Adjustable buck (step-down) DC-DC converter, 4.5-28 V in -> 0.8-20 V out, ~3 A. Configured to 5 V to step a 9 V / 12 V supply or battery pack down to the board's 5 V rail.

1

2S 7.4 V 4000 mAh Li-Po battery pack

2 × 606090, 7.4 V nominal, 4000 mAh

Two 606090 lithium-polymer cells wired in series to provide portable power for the calculator and printer.

1

2S Li-Po BMS protection board

2S Li-Po BMS, ≥5 A continuous

A protection board that prevents unsafe overcharge, over-discharge, and over-current conditions in the two-cell battery pack.

1

MAX17049 2S Li-Po fuel gauge breakout

MAX17049, I2C address 0x36

A small I2C module that estimates the remaining charge in the protected two-cell battery pack.

1

MCP23017 I/O Expander

I2C address 0x20

Microchip MCP23017 16-bit GPIO expander controlled over I2C. Provides two 8-bit ports for external digital inputs or outputs.

1

35-key 6×7 tactile-switch calculator keypad

35 × normally-open tactile switches, 6×7 matrix

Thirty-five momentary calculator keys wired as a diode-isolated six-row by seven-column matrix.

35

1N4148 Signal Diode

1N4148, one per key

Fast small-signal switching diode in DO-35 glass package. Forward voltage ~0.6–0.7 V at low current, reverse breakdown 75 V, max forward current 300 mA, reverse recovery time 4 ns. Used here in an anti-parallel pair across the TL072 op-amp feedback loop (D1: anode→OPAMP_OUT, cathode→OPAMP_IN_NEG; D2: anode→OPAMP_IN_NEG, cathode→OPAMP_OUT) to provide soft audio clipping.

1

4.58-inch 320×960 ST7701S RGB IPS TFT module

320 × 960, RGB565, ST7701S

The wide color screen that shows the calculator interface and receipt QR code.

1

74LVC2T45 dual-direction UART level shifter

74LVC2T45, dual-channel

A dual-channel direction-controlled chip that safely translates the printer UART between 3.3 V and 5 V.

1

Inline automotive blade fuse holder

5 A fuse, ≥32 V

A replaceable fuse holder that protects the battery positive wire from a dangerous short circuit.

1

USB-C PD 2S Li-Po power-path charger module

USB-C PD 9 V input, 2S 8.4 V charge, SYS/LOAD ≥5 A

A USB-C charging module that makes a protected 9 V system supply now and charges a two-cell battery pack when it is added later.

1

Adafruit USB Type C Power Delivery Dummy - I2C or Switchable - HUSB238

HUSB238, select 9 V PD

HUSB238-based USB-C PD trigger breakout with I2C control or on-board DIP-switch voltage selection, STEMMA QT/Qwiic connector, USB-C input, and screw-terminal power output. Negotiates a fixed PD voltage from a USB-C PD supply.

Assemble it in 6 steps

1. Install the named power modules

Mount the Adafruit HUSB238 USB-C PD Dummy board (usb_c_pd_sink), the selected 2S USB-C power-path charger board (charger_2s_powerpath), and the MP1584EN mini buck module (buck_5v) close to the case power opening. Connect HUSB238 `+` to the charger `VBUS` terminal (USB-C input power) and HUSB238 `GND` to charger `GND` (ground). Set the HUSB238 hardware selection for the voltage accepted by the exact charger board; this design presently uses 9 V.

  • The exact label to look for on the small buck board is `MP1584EN`.
  • The charger board must explicitly provide a battery-free `SYS`, `LOAD`, or `OUT` terminal rated for at least 5 A.
  • The charger board has not been assigned a model number because its manufacturer and terminal layout were not supplied; buy only one matching the stated SYS/LOAD requirement.
  • Do not connect the wall socket to any internal wire. Only connect a certified USB-C PD adapter to the HUSB238 USB-C socket.
  • Do not connect the calculator load to `VBUS`; `VBUS` is the charger input, while `SYS` or `LOAD` powers the calculator.

2. Create the 9 V printer rail and 5 V logic rail

Connect the charger module `SYS` / `LOAD` output to the CSN-A2 printer `VH` terminal (printer power) and the MP1584EN `VIN` terminal (power). Connect charger `GND`, printer `GND`, and MP1584EN `GND` together (ground). With only the power modules connected, adjust the MP1584EN trimmer until `VOUT` measures exactly 5.0 V relative to GND. Connect its `VOUT` to ESP32-S3 `5V/VIN`, the ST7701S display `VCC`, and the 74LVC2T45 `VCCB` (5 V power).

  • Use short 18–20 AWG paired wires between SYS/LOAD and the CSN-A2 printer because the print head takes brief high-current pulses.
  • The small module used here is specifically the MP1584EN, replacing the larger LM2596 module.
  • Make sure the MP1584EN output is 5.0 V before connecting the ESP32-S3. A higher setting can permanently damage the board and display.
  • Make sure every ground named here is connected together; a missing ground makes signals unreliable.

3. Connect the exact logic parts

Power the MCP23017 keypad expander and MAX17049 fuel-gauge board from ESP32-S3 `3V3` (power) and common ground (ground). Connect each board's `SDA` to GPIO8 (data) and `SCL` to GPIO9 (clock). Connect the 74LVC2T45 level translator: `VCCA` and `DIR1` to 3V3 (power), `VCCB` to 5 V (power), `GND` and `DIR2` to GND (ground), `A1` to GPIO5 (printer data out), `B1` to CSN-A2 `RX` (printer data), CSN-A2 `TX` to `B2` (printer data), and `A2` to GPIO4 (printer data in).

  • Model labels in this section are MAX17049, MCP23017, and 74LVC2T45.
  • The 74LVC2T45 prevents the CSN-A2 printer's possible 5 V TX signal from reaching the ESP32-S3 directly.
  • Use a MAX17049 breakout with 3.3 V-safe I2C pins. A board that drives SDA or SCL at 5 V can damage the ESP32-S3.
  • Do not omit the 74LVC2T45 between the printer and ESP32-S3.

4. Wire the display and keypad

Connect the ST7701S 4.58-inch 320×960 RGB display setup pins: `SPI_CS` to GPIO41 (data), `SPI_SCK` to GPIO42 (clock), and `SPI_MOSI` to GPIO47 (data). Connect its timing pins `DE` to GPIO38 (screen-area signal), `VSYNC` to GPIO40 (frame signal), `HSYNC` to GPIO39 (line signal), and `PCLK` to GPIO21 (pixel timing). Connect RGB data pins exactly as shown in the wiring diagram. Connect the 35 tactile switches as a 6-by-7 matrix to MCP23017 `GPA0–GPA5` for rows and `GPB0–GPB6` for columns, with one 1N4148 diode in series with every switch; put each diode's striped end toward its row wire.

  • The display driver is ST7701S; obtain its panel-specific initialization timing from the seller because separate 4.58-inch ST7701S panels can have different timing values.
  • The diode marking band is the striped end.
  • Make sure the ST7701S module accepts 3.3 V RGB and setup signals. Do not connect a 5 V logic-output display module directly to the ESP32-S3.
  • Keep the display ribbon and RGB wires away from the printer power wires to reduce picture noise.

5. Leave the future battery parts disconnected

For the present USB-C-only build, leave the 2 × 606090 2S pack, HW-391 2S BMS board, and 5 A blade fuse holder disconnected. Later, connect battery `PACK+` through the 5 A fuse to HW-391 `B+` (battery positive), battery `MID` to HW-391 `B1` (middle-cell sense), and battery `PACK−` to HW-391 `B−` (battery negative). Connect HW-391 `P+` to charger `VBAT` (battery power) and HW-391 `P−` to common ground (ground). Connect MAX17049 `CELL` to HW-391 `P+` (battery measurement).

  • HW-391 is the protection-board label supplied by you; verify its printed terminals before attaching cells.
  • The MAX17049 cannot report a useful battery value while the battery is absent.
  • Insulate every unused future battery wire so it cannot touch another wire or the metal enclosure.
  • Never reverse a Li-Po connection. A reversed or shorted pack can overheat or catch fire.

6. Check voltages and power the calculator

Check for loose wire strands. Plug a USB-C PD adapter into the Adafruit HUSB238 board. Measure about 9 V at the charger's `SYS` / `LOAD` terminal and exactly 5.0 V at MP1584EN `VOUT`. Only then connect the ESP32-S3 through its USB programming connector, load paper into the CSN-A2 printer, and press Deploy in Schematik.

  • If printing causes resets, first inspect the SYS-to-CSN-A2 wire thickness and confirm that the adapter truly supplies the requested PD voltage.
  • The model labels in the final power path are HUSB238 → selected 2S power-path charger → MP1584EN.
  • Unplug immediately if a connector, wire, printer, or module becomes hot.
  • Do not install batteries until the battery wiring has been checked against the printing on the actual HW-391 and charger boards.

Review all connections

1. Connections between "battery_pack_2s" and "ESP32"

Functionbattery_pack_2sESP32
powerPACK+Inline automotive blade fuse holder INEXT
powerMID2S Li-Po BMS protection board B1EXT
groundPACK-2S Li-Po BMS protection board B-EXT

2. Connections between "battery_fuse" and "ESP32"

Functionbattery_fuseESP32
powerOUT2S Li-Po BMS protection board B+EXT

3. Connections between "printer_58mm" and "ESP32"

Functionprinter_58mmESP32
groundGNDGND
powerVHVIN

4. Connections between "fuel_gauge" and "ESP32"

Functionfuel_gaugeESP32
groundGNDGND
powerVDD3V3
i2cSDAGPIO 8
i2cSCLGPIO 9
powerCELL2S Li-Po BMS protection board P+EXT

5. Connections between "keypad_expander" and "ESP32"

Functionkeypad_expanderESP32
powerVDD3V3
groundVSSGND
powerRESET3V3
i2cSDAGPIO 8
i2cSCLGPIO 9
dataGPA0-GPA735-key 6×7 tactile-switch calculator keypad ROW0-ROW5EXT

6. Connections between "keypad_6x7" and "ESP32"

Functionkeypad_6x7ESP32
digitalCOL0-COL61N4148 Signal Diode CATHODEEXT

7. Connections between "key_diodes" and "ESP32"

Functionkey_diodesESP32
dataANODEMCP23017 I/O Expander GPB0-GPB7EXT

8. Connections between "display_st7701s" and "ESP32"

Functiondisplay_st7701sESP32
groundGNDGND
spiSPI_CSGPIO 41
spiSPI_SCKGPIO 42
spiSPI_MOSIGPIO 47
digitalDEGPIO 38
digitalVSYNCGPIO 40
digitalHSYNCGPIO 39
digitalPCLKGPIO 21
dataR0GPIO 1
dataR1GPIO 2
dataR2GPIO 3
dataR3GPIO 6
dataR4GPIO 7
dataG0GPIO 10
dataG1GPIO 11
dataG2GPIO 12
dataG3GPIO 13
dataG4GPIO 14
dataG5GPIO 15
dataB0GPIO 16
dataB1GPIO 17
dataB2GPIO 18
dataB3GPIO 19
dataB4GPIO 20
powerVCC5V

9. Connections between "uart_level_shifter" and "ESP32"

Functionuart_level_shifterESP32
powerVCCA3V3
groundGNDGND
uartA1GPIO 5
uartB1CSN-A2 Thermal Printer RXEXT
uartA2GPIO 4
uartB2CSN-A2 Thermal Printer TXEXT
powerDIR13V3
groundDIR2GND
powerVCCB5V

10. Connections between "charger_2s_powerpath" and "ESP32"

Functioncharger_2s_powerpathESP32
groundGNDGND
powerSYSVIN

11. Connections between "bms_2s" and "ESP32"

Functionbms_2sESP32
powerP+SC8815 Bidirectional Buck-Boost Charger VBATEXT
groundP-GND

12. Connections between "usb_c_pd_sink" and "ESP32"

Functionusb_c_pd_sinkESP32
powerV+SC8815 Bidirectional Buck-Boost Charger VBUSEXT
groundGNDGND

13. Connections between "buck_5v" and "ESP32"

Functionbuck_5vESP32
powerVINVIN
powerVOUT5V
groundGNDGND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <NimBLEDevice.h>
#include <Adafruit_MCP23X17.h>
#include <Adafruit_Thermal.h>
#include <Arduino_GFX_Library.h>
#include <lvgl.h>

#define LCD_B1 17
#define LCD_B2 18
#define LCD_B3 19
#define LCD_B4 20
#define LCD_G1 11
#define LCD_G2 12
#define LCD_G3 13
#define LCD_G4 14
#define LCD_G5 15
#define LCD_R1 2
#define LCD_R2 3
#define LCD_R3 6
#define LCD_R4 7
#define LCD_SPI_SCK 42
#define LCD_SPI_MOSI 47


// Forward declarations
void flushDisplay(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorMap);
void initDisplay();
void initKeypad();
void printMockReceipt();
void onKeyPressed(uint8_t row, uint8_t col);
void scanKeypad();
void readFuelGauge();
void initConnectivityStubs();

constexpr int I2C_SDA = 8;
constexpr int I2C_SCL = 9;
constexpr int PRINTER_RX = 4;  // ESP32 input, connected after the 5 V-to-3.3 V divider.
constexpr int PRINTER_TX = 5;  // ESP32 output, connected to printer RX.
constexpr int LCD_DE = 38;
constexpr int LCD_VSYNC = 40;
constexpr int LCD_HSYNC = 39;
constexpr int LCD_PCLK = 21;
constexpr int LCD_R0 = 1, LCD_R1 = 2, LCD_R2 = 3, LCD_R3 = 6, LCD_R4 = 7;
constexpr int LCD_G0 = 10, LCD_G1 = 11, LCD_G2 = 12, LCD_G3 = 13, LCD_G4 = 14, LCD_G5 = 15;
constexpr int LCD_B0 = 16, LCD_B1 = 17, LCD_B2 = 18, LCD_B3 = 19, LCD_B4 = 20;
constexpr int LCD_SPI_CS = 41, LCD_SPI_SCK = 42, LCD_SPI_MOSI = 47;
constexpr uint16_t LCD_WIDTH = 320;
constexpr uint16_t LCD_HEIGHT = 960;
constexpr uint8_t MCP_ADDRESS = 0x20;
constexpr uint8_t MAX17049_ADDRESS = 0x36;
constexpr uint32_t PRINTER_BAUD = 19200;
constexpr uint32_t DEBOUNCE_MS = 30;

HardwareSerial printerSerial(1);
Adafruit_Thermal printer(&printerSerial);
Adafruit_MCP23X17 mcp;

// Replace these porch values with the timing table supplied for the exact panel glass.
Arduino_ESP32RGBPanel *rgbBus = new Arduino_ESP32RGBPanel(
  LCD_DE, LCD_VSYNC, LCD_HSYNC, LCD_PCLK,
  LCD_R0, LCD_R1, LCD_R2, LCD_R3, LCD_R4,
  LCD_G0, LCD_G1, LCD_G2, LCD_G3, LCD_G4, LCD_G5,
  LCD_B0, LCD_B1, LCD_B2, LCD_B3, LCD_B4,
  1, 40, 48, 40, 1, 13, 3, 29, 1, 16000000UL);
Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel(
  rgbBus, GFX_NOT_DEFINED, 0, true, LCD_WIDTH, LCD_HEIGHT,
  st7701_type1_init_operations, sizeof(st7701_type1_init_operations), true,
  40, 48, 40, 13, 3, 29);

static lv_disp_draw_buf_t drawBuffer;
static lv_color_t *lvBuffer = nullptr;
static lv_disp_drv_t displayDriver;
static uint32_t lastTickMs = 0;
static uint8_t stableKeys[6][7] = {};
static uint8_t pendingKeys[6][7] = {};
static uint32_t changeTime[6][7] = {};

const char *const keyNames[6][7] = {
  {"C", "±", "%", "÷", "M+", "M-", "MC"},
  {"7", "8", "9", "×", "Tax", "Up", "Down"},
  {"4", "5", "6", "−", "Print", "WiFi", "BLE"},
  {"1", "2", "3", "+", "(", ")", "Back"},
  {"0", "00", ".", "=", "Subtotal", "Enter", "Mode"},
  {"F1", "F2", "F3", "F4", "F5", "F6", "F7"}
};

void flushDisplay(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorMap) {
  const uint32_t width = area->x2 - area->x1 + 1;
  const uint32_t height = area->y2 - area->y1 + 1;
  gfx->draw16bitRGBBitmap(area->x1, area->y1, reinterpret_cast<uint16_t *>(colorMap), width, height);
  lv_disp_flush_ready(disp);
}

void initDisplay() {
  gfx->begin();
  gfx->fillScreen(BLACK);
  lv_init();
  const size_t pixels = LCD_WIDTH * 40;
  lvBuffer = static_cast<lv_color_t *>(heap_caps_malloc(pixels * sizeof(lv_color_t), MALLOC_CAP_SPIRAM));
  if (lvBuffer == nullptr) {
    Serial.println("Display buffer allocation failed");
    while (true) delay(1000);
  }
  lv_disp_draw_buf_init(&drawBuffer, lvBuffer, nullptr, pixels);
  lv_disp_drv_init(&displayDriver);
  displayDriver.hor_res = LCD_WIDTH;
  displayDriver.ver_res = LCD_HEIGHT;
  displayDriver.flush_cb = flushDisplay;
  displayDriver.draw_buf = &drawBuffer;
  lv_disp_drv_register(&displayDriver);

  lv_obj_t *title = lv_label_create(lv_scr_act());
  lv_label_set_text(title, "SMART DESKTOP CALCULATOR");
  lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
  lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
  lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 28);
  lv_obj_t *total = lv_label_create(lv_scr_act());
  lv_label_set_text(total, "TOTAL  $123.45");
  lv_obj_set_style_text_color(total, lv_color_hex(0x44E3A3), 0);
  lv_obj_set_style_text_font(total, &lv_font_montserrat_28, 0);
  lv_obj_align(total, LV_ALIGN_TOP_MID, 0, 92);
  lv_obj_t *qr = lv_qrcode_create(lv_scr_act(), 210, lv_color_hex(0x111111), lv_color_white());
  lv_qrcode_update(qr, "CALC-RECEIPT:TOTAL=123.45", 25);
  lv_obj_align(qr, LV_ALIGN_TOP_MID, 0, 175);
  lv_obj_t *hint = lv_label_create(lv_scr_act());
  lv_label_set_text(hint, "Use keypad; press Print for a mock receipt");
  lv_obj_set_style_text_color(hint, lv_color_hex(0xB0B8C8), 0);
  lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 430);
}

void initKeypad() {
  if (!mcp.begin_I2C(MCP_ADDRESS, &Wire)) {
    Serial.println("MCP23017 not found at 0x20");
    return;
  }
  for (uint8_t row = 0; row < 6; ++row) {
    mcp.pinMode(row, OUTPUT);
    mcp.digitalWrite(row, HIGH);
  }
  for (uint8_t col = 0; col < 7; ++col) mcp.pinMode(8 + col, INPUT_PULLUP);
}

void printMockReceipt() {
  printer.wake();
  printer.setDefault();
  printer.justify('C');
  printer.boldOn();
  printer.println("SMART CALCULATOR");
  printer.boldOff();
  printer.println("------------------------------");
  printer.justify('L');
  printer.println("Mock transaction");
  printer.println("Items                 $123.45");
  printer.println("------------------------------");
  printer.boldOn();
  printer.println("TOTAL                 $123.45");
  printer.boldOff();
  printer.feed(3);
  printer.sleep();
}

void onKeyPressed(uint8_t row, uint8_t col) {
  const char *key = keyNames[row][col];
  Serial.printf("Key pressed: %s (row %u, column %u)\n", key, row, col);
  if (strcmp(key, "Print") == 0) printMockReceipt();
}

void scanKeypad() {
  const uint32_t now = millis();
  for (uint8_t row = 0; row < 6; ++row) {
    mcp.digitalWrite(row, LOW);
    delayMicroseconds(100);
    for (uint8_t col = 0; col < 7; ++col) {
      const bool pressed = (mcp.digitalRead(8 + col) == LOW);
      if (pressed != pendingKeys[row][col]) {
        pendingKeys[row][col] = pressed;
        changeTime[row][col] = now;
      }
      if ((now - changeTime[row][col] >= DEBOUNCE_MS) && stableKeys[row][col] != pendingKeys[row][col]) {
        stableKeys[row][col] = pendingKeys[row][col];
        if (stableKeys[row][col]) onKeyPressed(row, col);
      }
    }
    mcp.digitalWrite(row, HIGH);
  }
}

void readFuelGauge() {
  Wire.beginTransmission(MAX17049_ADDRESS);
  Wire.write(0x02);
  if (Wire.endTransmission(false) != 0 || Wire.requestFrom(MAX17049_ADDRESS, (uint8_t)2) != 2) return;
  const uint16_t raw = (Wire.read() << 8) | Wire.read();
  Serial.printf("Battery: %.2f V\n", raw * 78.125e-6f);
}

void initConnectivityStubs() {
  WiFi.mode(WIFI_STA);
  NimBLEDevice::init("SmartCalculator");
  NimBLEDevice::setPower(ESP_PWR_LVL_P3);
  Serial.println("Wi-Fi station and BLE stubs initialized");
}

void setup() {
  Serial.begin(115200);
  delay(300);
  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(400000);
  printerSerial.begin(PRINTER_BAUD, SERIAL_8N1, PRINTER_RX, PRINTER_TX);
  initKeypad();
  initDisplay();
  initConnectivityStubs();
  readFuelGauge();
  lastTickMs = millis();
  Serial.println("Calculator hardware stack ready");
}

void loop() {
  const uint32_t now = millis();
  if (now - lastTickMs >= 5) {
    lv_tick_inc(now - lastTickMs);
    lastTickMs = now;
  }
  lv_timer_handler();
  scanKeypad();
  delay(2);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik