Smart Espresso Scale
U4)Table
| Function | ESP32-S3 GPIO | Net | Connected To | Notes |
|---|---|---|---|---|
| I2C SDA | IO9 | I2C_SDA | NAU7802 U5, BQ24295 U2, OLED DS1 | 3.3 V pull-up present |
| I2C SCL | IO10 | I2C_SCL | NAU7802 U5, BQ24295 U2, OLED DS1 | 3.3 V pull-up present |
| Load cell DRDY | IO8 | LOAD_DRDY | NAU7802 U5 | Interrupt input |
| microSD CS | IO14 | SPI_SD_CS | microSD J5 | Pull-up present |
| microSD SCK | IO13 | SPI_SD_SCK | microSD J5 | SPI clock |
| microSD MOSI | IO11 | SPI_SD_MOSI | microSD J5 | SPI MOSI |
| microSD MISO | IO12 | SPI_SD_MISO | microSD J5 | SPI MISO |
| microSD detect | IO15 | SD_CARD_DETECT | microSD J5 | Card detect input |
| microSD power enable | IO39 | SD_PWR_EN | TPS22919 U6 | Drives SD switched 3.3 V |
| USB D- | IO19 | USB_D- | USB-C J1 via ESD | Native USB |
| USB D+ | IO20 | USB_D+ | USB-C J1 via ESD | Native USB |
| Tare button | IO4 | BTN_TARE | SW1 | Active-low button |
| Start/stop button | IO5 | BTN_START_STOP | SW2 | Active-low button |
| Status LED | IO6 | STATUS_LED | LED path | Active-high assumed |
| Charger interrupt | IO7 | CHG_INT | BQ24295 U2 | Active-low interrupt |
platformio.ini:Ini
[env:esp32-s3-devkitc-1] platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 board_build.filesystem = littlefs lib_deps = bogde/HX711@^0.7.5 adafruit/Adafruit SSD1306@^2.5.13 adafruit/Adafruit GFX Library@^1.11.11 h2zero/NimBLE-Arduino@^1.4.3
bogde/HX711 dependency is not used by the NAU7802 code below; remove it if unused. It is listed only as a common scale-calibration utility dependency. The NAU7802 is accessed directly over I2C for a minimal starter.src/main.cpp:Cpp
#include <Arduino.h> #include <Wire.h> #include <SPI.h> #include <SD.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <NimBLEDevice.h> // Pin definitions from schematic constexpr int PIN_I2C_SDA = 9; constexpr int PIN_I2C_SCL = 10; constexpr int PIN_LOAD_DRDY = 8; constexpr int PIN_SD_CS = 14; constexpr int PIN_SD_SCK = 13; constexpr int PIN_SD_MOSI = 11; constexpr int PIN_SD_MISO = 12; constexpr int PIN_SD_DETECT = 15; constexpr int PIN_SD_PWR_EN = 39; constexpr int PIN_BTN_TARE = 4; constexpr int PIN_BTN_START_STOP = 5; constexpr int PIN_STATUS_LED = 6; constexpr int PIN_CHG_INT = 7; // I2C addresses constexpr uint8_t NAU7802_ADDR = 0x2A; constexpr uint8_t BQ24295_ADDR = 0x6B; constexpr uint8_t OLED_ADDR = 0x3C; // NAU7802 registers used by this starter constexpr uint8_t NAU_PU_CTRL = 0x00; constexpr uint8_t NAU_CTRL1 = 0x01; constexpr uint8_t NAU_CTRL2 = 0x02; constexpr uint8_t NAU_ADCO_B2 = 0x12; constexpr uint8_t NAU_ADCO_B1 = 0x13; constexpr uint8_t NAU_ADCO_B0 = 0x14; Adafruit_SSD1306 display(128, 64, &Wire, -1); SPIClass sdSPI(FSPI); NimBLECharacteristic *weightCharacteristic = nullptr; long tareOffset = 0; float scaleCountsPerGram = 1000.0f; // Calibrate with known weight. bool loggingEnabled = true; unsigned long lastSampleMs = 0; bool i2cWrite8(uint8_t addr, uint8_t reg, uint8_t value) { Wire.beginTransmission(addr); Wire.write(reg); Wire.write(value); return Wire.endTransmission() == 0; } bool i2cRead8(uint8_t addr, uint8_t reg, uint8_t &value) { Wire.beginTransmission(addr); Wire.write(reg); if (Wire.endTransmission(false) != 0) return false; if (Wire.requestFrom(addr, (uint8_t)1) != 1) return false; value = Wire.read(); return true; } bool initNAU7802() { uint8_t v = 0; // Power up digital and analog sections, then wait for power-ready. if (!i2cWrite8(NAU7802_ADDR, NAU_PU_CTRL, 0x06)) return false; delay(10); if (!i2cRead8(NAU7802_ADDR, NAU_PU_CTRL, v)) return false; i2cWrite8(NAU7802_ADDR, NAU_PU_CTRL, v | 0x20); // Start conversions. // Gain/rate defaults are usable for first bring-up; tune during calibration. i2cWrite8(NAU7802_ADDR, NAU_CTRL1, 0x27); // PGA gain and LDO defaults; verify for final sensor. i2cWrite8(NAU7802_ADDR, NAU_CTRL2, 0x30); // Conversion-rate default pattern. return true; } bool readNAU7802Raw(long &raw) { if (digitalRead(PIN_LOAD_DRDY) == HIGH) return false; Wire.beginTransmission(NAU7802_ADDR); Wire.write(NAU_ADCO_B2); if (Wire.endTransmission(false) != 0) return false; if (Wire.requestFrom(NAU7802_ADDR, (uint8_t)3) != 3) return false; uint32_t value = ((uint32_t)Wire.read() << 16) | ((uint32_t)Wire.read() << 8) | Wire.read(); if (value & 0x800000UL) value |= 0xFF000000UL; // Sign extend 24-bit value. raw = (int32_t)value; return true; } void configureBQ24295SafeDefaults() { // The charger reset default can be too high for an unknown small Li-ion cell. // Set input/charge behavior conservatively in firmware after I2C starts. // Exact register bitfields should be reviewed against the TI BQ24295 datasheet // before production calibration; this starter intentionally avoids aggressive charging. i2cWrite8(BQ24295_ADDR, 0x00, 0x10); // conservative input current limit pattern i2cWrite8(BQ24295_ADDR, 0x02, 0x20); // conservative charge-current pattern } bool initSD() { digitalWrite(PIN_SD_PWR_EN, HIGH); delay(20); sdSPI.begin(PIN_SD_SCK, PIN_SD_MISO, PIN_SD_MOSI, PIN_SD_CS); return SD.begin(PIN_SD_CS, sdSPI, 10000000); } void appendLog(float grams, long raw) { if (!loggingEnabled) return; File f = SD.open("/shots.csv", FILE_APPEND); if (!f) return; f.printf("%lu,%.2f,%ld\n", millis(), grams, raw); f.close(); } void initBLE() { NimBLEDevice::init("Smart Espresso Scale"); NimBLEServer *server = NimBLEDevice::createServer(); NimBLEService *service = server->createService("181D"); // Weight Scale service UUID weightCharacteristic = service->createCharacteristic("2A9D", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); weightCharacteristic->setValue("0.00 g"); service->start(); NimBLEAdvertising *adv = NimBLEDevice::getAdvertising(); adv->addServiceUUID("181D"); adv->start(); } void updateDisplay(float grams, bool sdOK) { display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.printf("%6.1f g", grams); display.setTextSize(1); display.setCursor(0, 32); display.printf("SD:%s BLE:ON", sdOK ? "OK" : "NO"); display.display(); } void setup() { Serial.begin(115200); pinMode(PIN_LOAD_DRDY, INPUT); pinMode(PIN_SD_DETECT, INPUT_PULLUP); pinMode(PIN_SD_PWR_EN, OUTPUT); pinMode(PIN_BTN_TARE, INPUT_PULLUP); pinMode(PIN_BTN_START_STOP, INPUT_PULLUP); pinMode(PIN_STATUS_LED, OUTPUT); pinMode(PIN_CHG_INT, INPUT_PULLUP); digitalWrite(PIN_STATUS_LED, HIGH); Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, 400000); bool oledOK = display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR); if (oledOK) { display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println("Scale booting..."); display.display(); } bool nauOK = initNAU7802(); configureBQ24295SafeDefaults(); bool sdOK = initSD(); initBLE(); Serial.printf("NAU7802:%s SD:%s\n", nauOK ? "OK" : "FAIL", sdOK ? "OK" : "FAIL"); if (sdOK) { File f = SD.open("/shots.csv", FILE_APPEND); if (f && f.size() == 0) f.println("ms,grams,raw"); if (f) f.close(); } } void loop() { static bool sdOK = SD.cardType() != CARD_NONE; if (digitalRead(PIN_BTN_TARE) == LOW) { long raw; if (readNAU7802Raw(raw)) tareOffset = raw; delay(250); } if (digitalRead(PIN_BTN_START_STOP) == LOW) { loggingEnabled = !loggingEnabled; delay(250); } if (millis() - lastSampleMs >= 100) { lastSampleMs = millis(); long raw; if (readNAU7802Raw(raw)) { float grams = (raw - tareOffset) / scaleCountsPerGram; Serial.printf("raw=%ld grams=%.2f\n", raw, grams); appendLog(grams, raw); if (weightCharacteristic) { char buf[24]; snprintf(buf, sizeof(buf), "%.2f g", grams); weightCharacteristic->setValue(buf); weightCharacteristic->notify(); } updateDisplay(grams, sdOK); } } }
pio runpio run -t uploadpio device monitor -b 115200scaleCountsPerGram = (raw - tareOffset) / known_grams.Platform & Toolchain
Pin Mapping
Dependencies & Project Setup
Complete Firmware Source
Build & Flash
Calibration Notes