Zum Inhalt

modbus_lib.tc

modbus_lib.tc — Modbus TCP helpers for TinyC (v1.6.0+)

Source on GitHub

// =====================================================================
// modbus_lib.tc — Modbus TCP helpers for TinyC (v1.6.0+)
// =====================================================================
//
// Provides clean wrappers around the v1.6.0 tcpTransact() syscall for
// the most-used Modbus function codes:
//   * mbFC03 — read holding registers (16-bit)
//   * mbFC04 — read input registers (16-bit)
//   * mbFC06 — write single holding register
//   * mbFC16 — write multiple holding registers
//
// Design goals:
//   - Caller picks the TCP slot (multi-meter setups need this)
//   - TID is auto-incremented per call; response TID is checked
//   - Modbus exception responses (high bit on FC) return -3
//   - Wrong byte-count returns -4
//   - Timeout returns -1, mismatched TID -2
//   - Single source-of-truth for response framing
//
// Usage:
//   #include "modbus_lib.tc"
//
//   void EverySecond() {
//       char regs[8];                                  // 4 regs * 2 bytes
//       int n = mbFC03(0, 1, 0x0010, 4, regs, 200);   // slot 0, slave 1
//       if (n == 4) {
//           int v0 = (regs[0] << 8) | regs[1];
//           // ...
//       }
//   }
//
// Combine with the 1.5.1 TCP tuning syscalls for robust long-running
// Modbus-TCP sessions:
//   tcpSelect(0);
//   if (tcpConnect("192.168.1.50", 502) == 0) {
//       tcpKeepalive(30, 10, 3);   // SMA Tripower idle-timeout fix
//   }
//   // periodic reads via mbFC03/04 ...
//   if (!tcpConnected()) {
//       int reason = tcpDisconnectReason();
//       // 4 = NETWORK error, back off harder
//       // 2 = PEER_CLOSED, retry sooner
//   }
//
// Implementation note (v1.6.0):
// Each FC helper used to do `tcpWriteArray(req,…,0)` + `mb_wait_for(N, ms)`
// + `tcpReadArray(resp)` — three separate syscalls plus a `delay(2)`
// busy-loop in user code. v1.6.0 collapses that to one `tcpTransact()`
// call which writes the request, waits for any response within the
// timeout (1-ms vTaskDelay between availability checks), and reads the
// reply in one shot. Tighter timeout precision, no per-FC poll loop, no
// `mb_wait_for` helper required.
// =====================================================================

// Auto-incremented transaction ID. Wraps at 0xFFFF (Modbus TCP spec
// allows 0..65535; the actual value doesn't matter as long as request
// and response match).
int mb_tid = 0;

// FC03: Read Holding Registers
//   slot         — TCP client slot (0..TC_TCP_CLI_SLOTS-1)
//   slave        — Modbus slave/unit ID
//   addr         — register start address
//   qty          — number of 16-bit registers (1..125)
//   dst          — char[] to receive raw bytes (length must be >= qty*2)
//   timeout_ms   — max wait for response
// Returns:
//   qty          — success (dst contains qty*2 bytes, MSB first per reg)
//   -1           — timeout / not connected
//   -2           — TID mismatch
//   -3           — Modbus exception response (dst[0] = exception code)
//   -4           — byte-count mismatch
int mbFC03(int slot, int slave, int addr, int qty, char dst[], int timeout_ms) {
    char req[12];
    char resp[260];                  // 9 header + 250 max payload
    tcpSelect(slot);
    if (!tcpConnected()) return -1;

    mb_tid = (mb_tid + 1) & 0xFFFF;
    req[0]  = (mb_tid >> 8) & 0xFF;
    req[1]  =  mb_tid       & 0xFF;
    req[2]  = 0; req[3]  = 0;        // protocol id = 0
    req[4]  = 0; req[5]  = 6;        // length = 6 (unit + fc + addr + qty)
    req[6]  = slave;
    req[7]  = 0x03;                  // function code 03
    req[8]  = (addr >> 8) & 0xFF;
    req[9]  =  addr       & 0xFF;
    req[10] = (qty  >> 8) & 0xFF;
    req[11] =  qty        & 0xFF;

    // Atomic write+wait+read. Returns: bytes received (>=9 expected for
    // a valid header), -1 timeout, -2 disconnect, -3 bad args.
    int got = tcpTransact(req, 12, resp, 260, timeout_ms);
    if (got < 0)  return -1;          // timeout / disconnect
    if (got < 9)  return -1;          // truncated header

    // TID match
    int rtid = (resp[0] << 8) | resp[1];
    if (rtid != mb_tid) return -2;

    // Modbus exception (high bit on FC)
    if (resp[7] & 0x80) {
        if (got > 8) dst[0] = resp[8];   // exception code
        return -3;
    }

    // Byte count check
    int bc = resp[8] & 0xFF;
    if (bc != qty * 2) return -4;
    if (got < 9 + bc) return -1;     // response truncated

    // Copy payload
    int i = 0;
    while (i < bc) {
        dst[i] = resp[9 + i];
        i++;
    }
    return qty;
}

// FC04: Read Input Registers — identical wire format to FC03 but
// the slave returns its INPUT register table instead of holding.
// SMA Tripower exposes most live values via FC04.
int mbFC04(int slot, int slave, int addr, int qty, char dst[], int timeout_ms) {
    char req[12];
    char resp[260];
    tcpSelect(slot);
    if (!tcpConnected()) return -1;

    mb_tid = (mb_tid + 1) & 0xFFFF;
    req[0]  = (mb_tid >> 8) & 0xFF;
    req[1]  =  mb_tid       & 0xFF;
    req[2]  = 0; req[3]  = 0;
    req[4]  = 0; req[5]  = 6;
    req[6]  = slave;
    req[7]  = 0x04;                  // FC04
    req[8]  = (addr >> 8) & 0xFF;
    req[9]  =  addr       & 0xFF;
    req[10] = (qty  >> 8) & 0xFF;
    req[11] =  qty        & 0xFF;

    int got = tcpTransact(req, 12, resp, 260, timeout_ms);
    if (got < 9) return -1;          // timeout / disconnect / truncated

    int rtid = (resp[0] << 8) | resp[1];
    if (rtid != mb_tid) return -2;
    if (resp[7] & 0x80) {
        if (got > 8) dst[0] = resp[8];
        return -3;
    }
    int bc = resp[8] & 0xFF;
    if (bc != qty * 2) return -4;
    if (got < 9 + bc) return -1;
    int i = 0;
    while (i < bc) { dst[i] = resp[9 + i]; i++; }
    return qty;
}

// FC06: Write Single Register
//   value — 16-bit value to write to addr
// Returns:
//   1            — success
//   -1/-2/-3     — same as FC03
int mbFC06(int slot, int slave, int addr, int value, int timeout_ms) {
    char req[12];
    char resp[12];
    tcpSelect(slot);
    if (!tcpConnected()) return -1;

    mb_tid = (mb_tid + 1) & 0xFFFF;
    req[0]  = (mb_tid >> 8) & 0xFF;
    req[1]  =  mb_tid       & 0xFF;
    req[2]  = 0; req[3]  = 0;
    req[4]  = 0; req[5]  = 6;
    req[6]  = slave;
    req[7]  = 0x06;                  // FC06
    req[8]  = (addr  >> 8) & 0xFF;
    req[9]  =  addr        & 0xFF;
    req[10] = (value >> 8) & 0xFF;
    req[11] =  value       & 0xFF;

    int got = tcpTransact(req, 12, resp, 12, timeout_ms);
    if (got < 12) return -1;         // timeout / disconnect / truncated echo

    int rtid = (resp[0] << 8) | resp[1];
    if (rtid != mb_tid) return -2;
    if (resp[7] & 0x80) return -3;   // exception
    return 1;
}

// FC16: Write Multiple Registers
//   src     — char[] containing qty*2 bytes (MSB first per reg)
//   qty     — number of 16-bit registers (1..123)
// Returns:
//   qty     — success (number of registers written, echoed by slave)
//   -1/-2/-3
int mbFC16(int slot, int slave, int addr, int qty, char src[], int timeout_ms) {
    char req[260];                   // 13 header + 246 max payload
    char resp[12];
    tcpSelect(slot);
    if (!tcpConnected()) return -1;

    mb_tid = (mb_tid + 1) & 0xFFFF;
    int byte_cnt = qty * 2;
    int frame_len = 7 + byte_cnt;    // unit + fc + addr + qty + bc + payload
    req[0]  = (mb_tid >> 8) & 0xFF;
    req[1]  =  mb_tid       & 0xFF;
    req[2]  = 0; req[3]  = 0;
    req[4]  = (frame_len >> 8) & 0xFF;
    req[5]  =  frame_len       & 0xFF;
    req[6]  = slave;
    req[7]  = 0x10;                  // FC16
    req[8]  = (addr >> 8) & 0xFF;
    req[9]  =  addr       & 0xFF;
    req[10] = (qty  >> 8) & 0xFF;
    req[11] =  qty        & 0xFF;
    req[12] = byte_cnt;
    int i = 0;
    while (i < byte_cnt) { req[13 + i] = src[i]; i++; }

    int got = tcpTransact(req, 13 + byte_cnt, resp, 12, timeout_ms);
    if (got < 12) return -1;         // timeout / disconnect / truncated echo

    int rtid = (resp[0] << 8) | resp[1];
    if (rtid != mb_tid) return -2;
    if (resp[7] & 0x80) return -3;   // exception
    int echo_qty = (resp[10] << 8) | resp[11];
    if (echo_qty != qty) return -4;
    return qty;
}

// Convenience: parse an N×u16 (big-endian) byte buffer into an int[].
// Useful right after mbFC03/04 to convert raw bytes to register values.
//   src  — char[] with qty*2 bytes (output of mbFC03/04)
//   dst  — int[] to receive qty register values (signed 16-bit, sign-extended)
//   qty  — number of registers
void mbParseU16(char src[], int dst[], int qty) {
    int i = 0;
    while (i < qty) {
        int v = ((src[i*2] & 0xFF) << 8) | (src[i*2 + 1] & 0xFF);
        if (v & 0x8000) v = v - 65536;     // sign-extend
        dst[i] = v;
        i++;
    }
}