Saturday, 21 March 2026

Getting an ESP32-S3-Mini to talk to an HX711 load cell amplifier and the Bojke laser distance sensor

It's all very well using that large DIN Rail-mounted ESP32-S3-POE-ETH-8DI-8DO to talk to the laser sensor and load cell but it feels a bit over the top. There's also a problem with this product when you want to connect to an HX711. This device uses I2C to connect up its 8 digital outputs, rather than go directly from the ESP32's GPIO. The HX711 uses a proprietary 2 wire serial protocol (not I2C or SPI), just clock and data, so requires "proper" GPIO digital outputs to work.

How about simply running both the HX711 and the Bojke sensor on one device, namely the ESP32-S3-Mini? I have a couple of these (and some other flavours eg RP2040) that I acquired for the MIG torch driver ("Motorhead"). That would allow me to drive the HX711 using the GPIO and talk to the Bojke sensor over RS485.

Something like this. I asked Claude Code to draw it up as a simple schematic but TBH it's not very good at that kind of task. It actually created a Python sketch that it then ran to create the graphic:


I said I wasn't quite there with its effort, so asked it to come up with a table in JPG format. I suppose it's a bit better in some respects:

That's better but I'd rather work from some sort of schematic when it comes to wiring the thing up. So it then came up with this:

...and this:


Not sure that's much of an improvement from where I stand. It offered me the schematic in Eagle (Fusion 360), so in the interests of science I then gave that a go. That caused my GMKtec NUCbox9 to hang and stutter badly (Ryzen graphics) and the resulting schematic was pretty shit. It suggested replacing the blocks with pukka Eagle symbols but that turned out to be a fool's errand that finally finished me off. The original block diagram is the best I can get from it, alongside the connection table as a tabular version.

After that rather fruitless diversion, perhaps it's time to get on with wiring the thing up. I think I'll use a breadboard for this to start with, otherwise I'll end up with several boards all hanging together with a spider's web of wiring.

Bojack to the rescue! No idea when I got this one but it saves stripping out the other boards I used for the Motorhead.


So without much more pointless buggerage, let's try and lash this thing up, using the table and block schematic.

Before I forget, here's the Arduino code for the combined sensors:
It seems to compile and upload to the ESP32-S3-Mini without issues - but of course it won't do anything without the HX711 and Bojke sensor connected up.

/**
 * ============================================================
 *  Waveshare ESP32-S3-Zero
 *  HX711 load cell + BOJKE BL-30NZ-485 laser — stiffness logger
 * ============================================================
 *
 *  WIRING
 *  ──────
 *  HX711
 *    VCC  → 3.3V
 *    GND  → GND
 *    DT   → GPIO2
 *    SCK  → GPIO3
 *    RATE → 3.3V for 80Hz, GND/float for 10Hz
 *
 *  MAX485 module
 *    VCC  → 5V
 *    GND  → GND
 *    DI   → GPIO1  (UART1 TX)
 *    RO   → GPIO0  (UART1 RX)
 *    DE+RE tied together → GPIO4
 *    A/B screw terminals → BOJKE green(A+) / white(B-)
 *
 *  BOJKE BL-30NZ-485
 *    brown  → +12-24V DC
 *    blue   → GND
 *    green  → A(+) on MAX485
 *    white  → B(-) on MAX485
 *
 *  CALIBRATION
 *  ───────────
 *  Set CALIBRATION_FACTOR to 0.0 on first flash.
 *  Follow Serial Monitor prompts (115200 baud).
 *  Paste the printed factor back in and re-flash.
 *
 *  CSV OUTPUT (USB Serial, 115200)
 *  ───────────────────────────────
 *  # timestamp_ms, distance_mm, force_g
 *  1000, 28.450, 0.0
 *  1050, 28.451, 12.3
 *  ...
 *
 *  POLL RATE
 *  ─────────
 *  POLL_INTERVAL_MS 50  → 20 Hz (safe default)
 *  POLL_INTERVAL_MS 12  → ~80 Hz (requires RATE pin HIGH on HX711)
 * ============================================================
 */

#include <Arduino.h>
#include <HX711.h>

// ── HX711 ────────────────────────────────────────────────────
#define HX711_DT_PIN    2
#define HX711_SCK_PIN   3
#define HX711_READINGS  3     // averages per sample (lower = faster)

// ── MAX485 / RS485 ───────────────────────────────────────────
#define RS485_TX_PIN    1
#define RS485_RX_PIN    0
#define RS485_DE_PIN    4

// ── BOJKE Modbus settings ────────────────────────────────────
#define MODBUS_BAUD              115200
#define LASER_SLAVE_ID           0x01
#define LASER_REG_ADDR           0x0000
#define LASER_REG_COUNT          0x0002
#define LASER_RESPONSE_TIMEOUT   50    // ms
#define LASER_EXPECTED_LEN       9     // bytes

// ── Poll rate ─────────────────────────────────────────────────
#define POLL_INTERVAL_MS   50

// ── Calibration ──────────────────────────────────────────────
// Set to 0.0 to run calibration on boot.
// After calibration paste the printed value here and re-flash.
#define CALIBRATION_FACTOR   0.0f

// Known weight used during calibration (grams)
#define CALIBRATION_WEIGHT_G  500.0f

// ─────────────────────────────────────────────────────────────

HX711 scale;
HardwareSerial rs485(1);

// ─────────────────────────────────────────────────────────────
//  CRC-16/IBM (Modbus)
// ─────────────────────────────────────────────────────────────
uint16_t modbusCRC(const uint8_t *buf, uint8_t len) {
    uint16_t crc = 0xFFFF;
    for (uint8_t i = 0; i < len; i++) {
        crc ^= (uint16_t)buf[i];
        for (uint8_t j = 0; j < 8; j++)
            crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
    }
    return crc;
}

// ─────────────────────────────────────────────────────────────
//  Read laser distance (mm) via Modbus RTU FC04
// ─────────────────────────────────────────────────────────────
float readLaserMM() {
    // Flush any stale bytes
    while (rs485.available()) rs485.read();

    // Build FC04 request
    uint8_t req[8];
    req[0] = LASER_SLAVE_ID;
    req[1] = 0x04;
    req[2] = LASER_REG_ADDR >> 8;
    req[3] = LASER_REG_ADDR & 0xFF;
    req[4] = LASER_REG_COUNT >> 8;
    req[5] = LASER_REG_COUNT & 0xFF;
    uint16_t crc = modbusCRC(req, 6);
    req[6] = crc & 0xFF;
    req[7] = crc >> 8;

    digitalWrite(RS485_DE_PIN, HIGH);
    delayMicroseconds(100);
    rs485.write(req, sizeof(req));
    rs485.flush();
    delayMicroseconds(100);
    digitalWrite(RS485_DE_PIN, LOW);

    // Wait for response
    uint8_t resp[LASER_EXPECTED_LEN];
    uint8_t n = 0;
    uint32_t deadline = millis() + LASER_RESPONSE_TIMEOUT;
    while (millis() < deadline && n < LASER_EXPECTED_LEN) {
        if (rs485.available())
            resp[n++] = rs485.read();
    }

    if (n < LASER_EXPECTED_LEN)                   return NAN;
    if (resp[0] != LASER_SLAVE_ID)                return NAN;
    if (resp[1] != 0x04)                          return NAN;

    uint16_t rxCRC   = (uint16_t)resp[7] | ((uint16_t)resp[8] << 8);
    uint16_t calcCRC = modbusCRC(resp, 7);
    if (rxCRC != calcCRC)                         return NAN;

    uint16_t raw = ((uint16_t)resp[3] << 8) | resp[4];
    return raw * 0.001f;
}

// ─────────────────────────────────────────────────────────────
//  Calibration routine
// ─────────────────────────────────────────────────────────────
void runCalibration() {
    Serial.println("\n========================================");
    Serial.println("  HX711 CALIBRATION");
    Serial.println("========================================");
    Serial.println("Step 1: Remove ALL load from the cell.");
    Serial.println("        Send any character when ready...");
    while (!Serial.available()) delay(100);
    while (Serial.available()) Serial.read();

    scale.set_scale();
    scale.tare(20);
    Serial.println("Tared.");

    Serial.println("\nStep 2: Place known weight on the cell.");
    Serial.print("        Known weight: ");
    Serial.print(CALIBRATION_WEIGHT_G, 1);
    Serial.println(" g");
    Serial.println("        Send any character when stable...");
    while (!Serial.available()) delay(100);
    while (Serial.available()) Serial.read();

    float raw    = scale.get_value(20);
    float factor = raw / CALIBRATION_WEIGHT_G;

    Serial.println("\n========================================");
    Serial.print("  CALIBRATION_FACTOR = ");
    Serial.println(factor, 4);
    Serial.println("  Paste this into the sketch and re-flash.");
    Serial.println("========================================\n");

    // Apply immediately so it works this session without re-flashing
    scale.set_scale(factor);
    scale.tare(10);
}

// ─────────────────────────────────────────────────────────────
//  setup()
// ─────────────────────────────────────────────────────────────
void setup() {
    // USB CDC serial — required for S3-Zero
    // In Arduino IDE: Tools → USB CDC On Boot → Enabled
    Serial.begin(115200);
    delay(2000);
    Serial.println("# Waveshare ESP32-S3-Zero — stiffness logger");

    // RS485 direction pin
    pinMode(RS485_DE_PIN, OUTPUT);
    digitalWrite(RS485_DE_PIN, LOW);

    // RS485 UART
    rs485.begin(MODBUS_BAUD, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN);

    // HX711
    scale.begin(HX711_DT_PIN, HX711_SCK_PIN);
    Serial.print("Waiting for HX711");
    while (!scale.is_ready()) { Serial.print("."); delay(100); }
    Serial.println(" ready.");

    if (CALIBRATION_FACTOR == 0.0f) {
        runCalibration();
    } else {
        scale.set_scale(CALIBRATION_FACTOR);
        Serial.println("Remove all load — taring in 2s...");
        delay(2000);
        scale.tare(20);
        Serial.println("Tared.");
    }

    Serial.println("# timestamp_ms, distance_mm, force_g");
}

// ─────────────────────────────────────────────────────────────
//  loop()
// ─────────────────────────────────────────────────────────────
void loop() {
    static uint32_t lastPoll = 0;
    if (millis() - lastPoll < POLL_INTERVAL_MS) return;
    lastPoll = millis();

    uint32_t ts    = millis();
    float distMM   = readLaserMM();
    float forceG   = scale.is_ready() ? scale.get_units(HX711_READINGS) : NAN;

    Serial.print(ts);
    Serial.print(',');
    if (!isnan(distMM)) Serial.print(distMM, 3); else Serial.print("ERR");
    Serial.print(',');
    if (!isnan(forceG)) Serial.print(forceG, 1); else Serial.print("ERR");
    Serial.println();
}

No comments:

Post a Comment

Getting an ESP32-S3-Mini to talk to an HX711 load cell amplifier and the Bojke laser distance sensor

It's all very well using that large DIN Rail-mounted ESP32-S3-POE-ETH-8DI-8DO to talk to the laser sensor and load cell but it feels a b...