Skip to content

File syn_ethercat.c

File List > proto > syn_ethercat.c

Go to the documentation of this file

#if __has_include("syn_config.h")
#include "syn_config.h"
#endif

#if !defined(SYN_USE_ETHERCAT) || SYN_USE_ETHERCAT

#include "../util/syn_assert.h"
#include "syn_ethercat.h"

#include <string.h>

/* ── Inline Helpers ─────────────────────────────────────────────────────── */

static inline void store16_le(uint8_t *p, uint16_t val)
{
    p[0] = (uint8_t)(val & 0xFF);
    p[1] = (uint8_t)((val >> 8) & 0xFF);
}

static inline void store32_le(uint8_t *p, uint32_t val)
{
    p[0] = (uint8_t)(val & 0xFF);
    p[1] = (uint8_t)((val >> 8) & 0xFF);
    p[2] = (uint8_t)((val >> 16) & 0xFF);
    p[3] = (uint8_t)((val >> 24) & 0xFF);
}

static inline uint16_t load16_le(const uint8_t *p)
{
    return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
}

static inline uint32_t load32_le(const uint8_t *p)
{
    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

/* ── Public API ─────────────────────────────────────────────────────────── */

void syn_ecat_frame_begin(uint8_t *buf, size_t buf_size)
{
    if (buf == NULL || buf_size < 2) {
        return;
    }
    uint16_t ecat_hdr = (uint16_t)((SYN_ETHERCAT_TYPE_DATAGRAM & 0x0F) << 12);
    store16_le(buf, ecat_hdr);
}

size_t syn_ecat_frame_add_datagram(uint8_t *buf, size_t buf_size, SYN_EcatCmd cmd, uint8_t idx,
                                   uint32_t addr, const uint8_t *data, uint16_t data_len, bool circ)
{
    if (buf == NULL || buf_size < 14) {
        return 0;
    }

    uint16_t cur_hdr = load16_le(buf);
    size_t cur_dg_bytes = (size_t)(cur_hdr & 0x07FF);
    size_t offset = 2 + cur_dg_bytes;
    size_t dg_total = 12 + (size_t)data_len;

    if (offset + dg_total > buf_size) {
        return 0;
    }

    /* If there is a preceding datagram, set its 'm' (more datagrams) bit */
    if (cur_dg_bytes > 0) {
        size_t pos = 2;
        while (pos < offset) {
            uint16_t d_len = load16_le(&buf[pos + 6]) & 0x07FF;
            size_t d_size = 12 + (size_t)d_len;
            if (pos + d_size == offset) {
                uint16_t prev_flags = load16_le(&buf[pos + 6]);
                prev_flags |= (1U << 15);
                store16_le(&buf[pos + 6], prev_flags);
                break;
            }
            pos += d_size;
        }
    }

    buf[offset + 0] = (uint8_t)cmd;
    buf[offset + 1] = idx;
    store32_le(&buf[offset + 2], addr);

    uint16_t len_flags = (uint16_t)(data_len & 0x07FF);
    if (circ) {
        len_flags |= (1U << 14);
    }
    store16_le(&buf[offset + 6], len_flags);
    store16_le(&buf[offset + 8], 0); /* irq */

    if (data_len > 0 && data != NULL) {
        memcpy(&buf[offset + 10], data, data_len);
    }

    store16_le(&buf[offset + 10 + data_len], 0); /* WKC = 0 on transmit */

    size_t new_dg_bytes = cur_dg_bytes + dg_total;
    uint16_t new_hdr = (uint16_t)(new_dg_bytes & 0x07FF);
    new_hdr |= (uint16_t)((SYN_ETHERCAT_TYPE_DATAGRAM & 0x0F) << 12);
    store16_le(buf, new_hdr);

    return 2 + new_dg_bytes;
}

size_t syn_ecat_frame_finalize(uint8_t *buf)
{
    if (buf == NULL) {
        return 0;
    }
    return 2 + (size_t)(load16_le(buf) & 0x07FF);
}

bool syn_ecat_frame_parse_next(const uint8_t *frame, size_t frame_len, size_t *offset,
                               SYN_EcatDatagramResult *out)
{
    if (frame == NULL || offset == NULL || out == NULL || frame_len < 14) {
        return false;
    }

    uint16_t ecat_hdr = load16_le(frame);
    uint16_t frame_type = (ecat_hdr >> 12) & 0x0F;
    if (frame_type != SYN_ETHERCAT_TYPE_DATAGRAM) {
        return false;
    }

    size_t total_dg_len = (size_t)(ecat_hdr & 0x07FF);
    if (frame_len < 2 + total_dg_len) {
        return false;
    }

    if (*offset < 2 || *offset >= 2 + total_dg_len) {
        return false;
    }

    if (*offset + 12 > 2 + total_dg_len) {
        return false;
    }

    out->cmd = frame[*offset + 0];
    out->idx = frame[*offset + 1];
    out->addr = load32_le(&frame[*offset + 2]);
    uint16_t len_flags = load16_le(&frame[*offset + 6]);
    out->data_len = len_flags & 0x07FF;

    if (*offset + 12 + (size_t)out->data_len > 2 + total_dg_len) {
        return false;
    }

    out->data = (out->data_len > 0) ? &frame[*offset + 10] : NULL;
    out->wkc = load16_le(&frame[*offset + 10 + (size_t)out->data_len]);

    *offset += 12 + (size_t)out->data_len;
    return true;
}

void syn_ecat_init(SYN_EcatNode *node, uint16_t station_addr, SYN_CANOpenNode *od)
{
    SYN_ASSERT(node != NULL);

    memset(node, 0, sizeof(*node));
    node->state = SYN_ECAT_STATE_INIT;
    node->target_state = SYN_ECAT_STATE_INIT;
    node->station_addr = station_addr;
    node->al_status = 0;
    node->wkc_expected = 1;
    node->od = od;
}

size_t syn_ecat_build_datagram_frame(uint8_t *buf, size_t buf_len, const SYN_EcatDatagram *dg,
                                     const uint8_t *data, uint16_t data_len)
{
    SYN_ASSERT(buf != NULL && dg != NULL);

    /* Total = 2 (EtherCAT Header) + 10 (Datagram Header) + data_len + 2 (WKC) */
    size_t total_len = 2 + 10 + data_len + 2;
    if (buf_len < total_len)
        return 0;

    /* 1. Pack 2-byte EtherCAT Header */
    uint16_t ecat_hdr = (uint16_t)((10 + data_len + 2) & 0x07FF);
    ecat_hdr |= (uint16_t)((SYN_ETHERCAT_TYPE_DATAGRAM & 0x0F) << 12);
    store16_le(buf, ecat_hdr);

    /* 2. Pack 10-byte Datagram Header */
    buf[2] = dg->cmd;
    buf[3] = dg->idx;
    store32_le(&buf[4], dg->addr);

    uint16_t len_flags = (uint16_t)(data_len & 0x07FF);
    if (dg->m)
        len_flags |= (1U << 15);
    if (dg->circ)
        len_flags |= (1U << 14);
    store16_le(&buf[8], len_flags);

    store16_le(&buf[10], dg->irq);

    /* 3. Copy Data */
    if (data_len > 0 && data != NULL) {
        memcpy(&buf[12], data, data_len);
    }

    /* 4. Pack Working Counter (WKC = 0 on transmit) */
    store16_le(&buf[12 + data_len], 0);

    return total_len;
}

SYN_Status syn_ecat_parse_frame(SYN_EcatNode *node, const uint8_t *frame, size_t len, uint16_t *wkc)
{
    SYN_ASSERT(node != NULL && frame != NULL);

    if (len < 14) /* 2 byte header + 10 byte dg header + 2 byte WKC */
        return SYN_ERROR;

    uint16_t ecat_hdr = load16_le(frame);
    uint16_t frame_type = (ecat_hdr >> 12) & 0x0F;
    if (frame_type != SYN_ETHERCAT_TYPE_DATAGRAM)
        return SYN_ERROR;

    uint16_t dg_len = ecat_hdr & 0x07FF;
    if (dg_len < 12 || len < (size_t)(2 + dg_len))
        return SYN_ERROR;

    /* Extract Datagram WKC from the end of the first datagram */
    size_t wkc_offset = 2 + dg_len - 2;
    uint16_t received_wkc = load16_le(&frame[wkc_offset]);
    if (wkc != NULL) {
        *wkc = received_wkc;
    }

    node->wkc_last = received_wkc;

    /* Check if WKC matches expected */
    if (received_wkc < node->wkc_expected) {
        return SYN_ERROR;
    }

    return SYN_OK;
}

SYN_Status syn_ecat_set_state(SYN_EcatNode *node, SYN_EcatState new_state)
{
    SYN_ASSERT(node != NULL);

    switch (new_state) {
    case SYN_ECAT_STATE_INIT:
    case SYN_ECAT_STATE_PREOP:
    case SYN_ECAT_STATE_BOOT:
    case SYN_ECAT_STATE_SAFEOP:
    case SYN_ECAT_STATE_OP:
        node->target_state = new_state;
        return SYN_OK;
    default:
        return SYN_ERROR;
    }
}

void syn_ecat_update(SYN_EcatNode *node)
{
    SYN_ASSERT(node != NULL);

    if (node->state != node->target_state) {
        /* Simple valid transition logic */
        node->state = node->target_state;
        node->al_status = 0;
    }
}

size_t syn_ecat_coe_encode_sdo_download(uint8_t *buf, size_t buf_len, uint16_t index,
                                        uint8_t subindex, const void *data, size_t data_len)
{
    SYN_ASSERT(buf != NULL);

    if (data_len > 4 || buf_len < 10) /* 2 byte Mailbox + 8 byte SDO */
        return 0;

    /* CoE Mailbox Header: Type 3 (SDO Request) */
    uint16_t coe_hdr = (uint16_t)((SYN_ECAT_COE_TYPE_SDO_REQ & 0x0F) << 12);
    store16_le(buf, coe_hdr);

    /* SDO Download Command Header: Expedited SDO Download Request (0x22 / 0x23 / etc) */
    uint8_t sdo_hdr = 0x22 | (uint8_t)(((4 - data_len) & 0x03) << 2) | 0x02; /* e=1, s=1 */
    buf[2] = sdo_hdr;
    store16_le(&buf[3], index);
    buf[5] = subindex;

    /* SDO Data */
    memset(&buf[6], 0, 4);
    if (data != NULL && data_len > 0) {
        memcpy(&buf[6], data, data_len);
    }

    return 10;
}

size_t syn_ecat_coe_encode_sdo_upload(uint8_t *buf, size_t buf_len, uint16_t index,
                                      uint8_t subindex)
{
    SYN_ASSERT(buf != NULL);

    if (buf_len < 10)
        return 0;

    /* CoE Mailbox Header: Type 3 (SDO Request) */
    uint16_t coe_hdr = (uint16_t)((SYN_ECAT_COE_TYPE_SDO_REQ & 0x0F) << 12);
    store16_le(buf, coe_hdr);

    /* SDO Upload Request Command Header: 0x40 */
    buf[2] = 0x40;
    store16_le(&buf[3], index);
    buf[5] = subindex;
    memset(&buf[6], 0, 4);

    return 10;
}

/* ── EtherCAT Master API Implementation ─────────────────────────────────── */

SYN_Status syn_ecat_master_init(SYN_EcatMaster *m, uint8_t *tx_buf, uint16_t tx_buf_size,
                                uint8_t *rx_buf, uint16_t rx_buf_size, uint8_t *output_image,
                                uint16_t output_size, uint8_t *input_image, uint16_t input_size)
{
    if (m == NULL || tx_buf == NULL || rx_buf == NULL || tx_buf_size < 64 || rx_buf_size < 64) {
        return SYN_INVALID_PARAM;
    }

    memset(m, 0, sizeof(*m));
    m->tx_buf = tx_buf;
    m->tx_buf_size = tx_buf_size;
    m->rx_buf = rx_buf;
    m->rx_buf_size = rx_buf_size;
    m->output_image = output_image;
    m->output_size = output_size;
    m->input_image = input_image;
    m->input_size = input_size;

    return SYN_OK;
}

size_t syn_ecat_encode_scan_bus(SYN_EcatMaster *m)
{
    if (m == NULL || m->tx_buf == NULL) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint8_t dummy[2] = {0, 0};
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_BRD, 0x01,
                                       SYN_ESC_REG_TYPE, dummy, 2, false);
}

uint8_t syn_ecat_decode_scan_bus(SYN_EcatMaster *m, size_t rx_len)
{
    if (m == NULL || m->rx_buf == NULL || rx_len < 14) {
        return 0;
    }

    size_t offset = 2;
    SYN_EcatDatagramResult res;
    if (syn_ecat_frame_parse_next(m->rx_buf, rx_len, &offset, &res)) {
        uint8_t count =
            (res.wkc > SYN_ECAT_MAX_SLAVES) ? (uint8_t)SYN_ECAT_MAX_SLAVES : (uint8_t)res.wkc;
        m->slave_count = count;
        return count;
    }

    return 0;
}

size_t syn_ecat_encode_assign_addr(SYN_EcatMaster *m, uint16_t position, uint16_t station_addr)
{
    if (m == NULL || m->tx_buf == NULL) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint8_t payload[2];
    store16_le(payload, station_addr);

    /* Position auto-increment addressing format: 0 - position in high 16 bits */
    uint16_t auto_inc = (uint16_t)(-(int16_t)position);
    uint32_t addr = ((uint32_t)auto_inc << 16) | SYN_ESC_REG_STATION_ADDR;

    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_APWR,
                                       (uint8_t)position, addr, payload, 2, false);
}

size_t syn_ecat_encode_read_sii(SYN_EcatMaster *m, uint16_t station_addr, uint16_t word_offset)
{
    if (m == NULL || m->tx_buf == NULL) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint32_t addr = ((uint32_t)station_addr << 16) | SYN_ESC_REG_SII_EEPROM_CTRL;
    uint8_t payload[6];
    store16_le(&payload[0], 0x0100); /* Read 4-byte command */
    store32_le(&payload[2], (uint32_t)word_offset);

    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPWR, 0x02, addr,
                                       payload, 6, false);
}

SYN_Status syn_ecat_decode_read_sii(SYN_EcatMaster *m, size_t rx_len, uint32_t *out_val)
{
    if (m == NULL || m->rx_buf == NULL || out_val == NULL || rx_len < 14) {
        return SYN_INVALID_PARAM;
    }

    size_t offset = 2;
    SYN_EcatDatagramResult res;
    if (syn_ecat_frame_parse_next(m->rx_buf, rx_len, &offset, &res)) {
        if (res.data_len >= 4) {
            *out_val = load32_le(res.data);
            return SYN_OK;
        }
    }

    return SYN_ERROR;
}

size_t syn_ecat_encode_write_sm(SYN_EcatMaster *m, uint16_t station_addr, uint8_t sm_index,
                                const SYN_EcatSMConfig *cfg)
{
    if (m == NULL || m->tx_buf == NULL || cfg == NULL || sm_index >= 4) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint32_t addr = ((uint32_t)station_addr << 16) | (SYN_ESC_REG_SM0 + (uint32_t)sm_index * 8U);
    uint8_t payload[8] = {0};
    store16_le(&payload[0], cfg->start_addr);
    store16_le(&payload[2], cfg->length);
    payload[4] = cfg->control;
    payload[5] = 0;
    payload[6] = cfg->enable;
    payload[7] = 0;

    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPWR, sm_index, addr,
                                       payload, 8, false);
}

size_t syn_ecat_encode_write_fmmu(SYN_EcatMaster *m, uint16_t station_addr, uint8_t fmmu_index,
                                  const SYN_EcatFMMUConfig *cfg)
{
    if (m == NULL || m->tx_buf == NULL || cfg == NULL || fmmu_index >= 4) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint32_t addr =
        ((uint32_t)station_addr << 16) | (SYN_ESC_REG_FMMU0 + (uint32_t)fmmu_index * 16U);
    uint8_t payload[16] = {0};
    store32_le(&payload[0], cfg->logical_start);
    store16_le(&payload[4], cfg->length);
    payload[6] = cfg->logical_start_bit;
    payload[7] = cfg->logical_end_bit;
    store16_le(&payload[8], cfg->phys_start_addr);
    payload[10] = cfg->phys_start_bit;
    payload[11] = cfg->type;
    payload[12] = cfg->enable ? 1U : 0U;

    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPWR, fmmu_index,
                                       addr, payload, 16, false);
}

size_t syn_ecat_encode_read_al_status(SYN_EcatMaster *m, uint16_t station_addr)
{
    if (m == NULL || m->tx_buf == NULL) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint32_t addr = ((uint32_t)station_addr << 16) | SYN_ESC_REG_AL_STATUS;
    uint8_t dummy[4] = {0};
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPRD, 0x05, addr,
                                       dummy, 4, false);
}

SYN_Status syn_ecat_decode_read_al_status(SYN_EcatMaster *m, size_t rx_len, SYN_EcatState *state,
                                          uint16_t *status_code)
{
    if (m == NULL || m->rx_buf == NULL || state == NULL || status_code == NULL || rx_len < 14) {
        return SYN_INVALID_PARAM;
    }

    size_t offset = 2;
    SYN_EcatDatagramResult res;
    if (syn_ecat_frame_parse_next(m->rx_buf, rx_len, &offset, &res)) {
        if (res.data_len >= 4) {
            *state = (SYN_EcatState)(res.data[0] & 0x0F);
            *status_code = load16_le(&res.data[2]);
            return SYN_OK;
        }
    }

    return SYN_ERROR;
}

/* ── Phase 3: Mailbox & CoE SDO API Implementation ─────────────────────── */

size_t syn_ecat_encode_coe_sdo_write(SYN_EcatMaster *m, uint16_t station_addr, uint16_t index,
                                     uint8_t subindex, const void *data, size_t data_len)
{
    if (m == NULL || m->tx_buf == NULL || data_len > 4) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);

    /* 6B Mailbox Header + 2B CoE Header + 8B SDO = 16B payload */
    uint8_t payload[16] = {0};

    /* Mailbox Header */
    store16_le(&payload[0], 10);           /* Mailbox payload length (CoE 2B + SDO 8B) */
    store16_le(&payload[2], station_addr); /* Target station address */
    payload[4] = 0x00;                     /* Channel 0, Priority 0 */
    payload[5] = 0x03;                     /* Mailbox Type 3 = CoE */

    /* CoE Header */
    uint16_t coe_hdr = (uint16_t)((SYN_ECAT_COE_TYPE_SDO_REQ & 0x0F) << 12);
    store16_le(&payload[6], coe_hdr);

    /* SDO Download Command Header (expedited) */
    uint8_t sdo_hdr = 0x22 | (uint8_t)(((4 - data_len) & 0x03) << 2) | 0x02;
    payload[8] = sdo_hdr;
    store16_le(&payload[9], index);
    payload[11] = subindex;

    if (data != NULL && data_len > 0) {
        memcpy(&payload[12], data, data_len);
    }

    uint32_t addr = ((uint32_t)station_addr << 16) | 0x1000U; /* Default Mailbox Out DPRAM */
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPWR, 0x06, addr,
                                       payload, 16, false);
}

size_t syn_ecat_encode_coe_sdo_read(SYN_EcatMaster *m, uint16_t station_addr, uint16_t index,
                                    uint8_t subindex)
{
    if (m == NULL || m->tx_buf == NULL) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);

    uint8_t payload[16] = {0};

    /* Mailbox Header */
    store16_le(&payload[0], 10);
    store16_le(&payload[2], station_addr);
    payload[4] = 0x00;
    payload[5] = 0x03; /* CoE */

    /* CoE Header */
    uint16_t coe_hdr = (uint16_t)((SYN_ECAT_COE_TYPE_SDO_REQ & 0x0F) << 12);
    store16_le(&payload[6], coe_hdr);

    /* SDO Upload Command Header (0x40) */
    payload[8] = 0x40;
    store16_le(&payload[9], index);
    payload[11] = subindex;

    uint32_t addr = ((uint32_t)station_addr << 16) | 0x1000U;
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPWR, 0x07, addr,
                                       payload, 16, false);
}

SYN_Status syn_ecat_decode_coe_sdo_response(SYN_EcatMaster *m, size_t rx_len, void *out_data,
                                            size_t max_len, size_t *out_len)
{
    if (m == NULL || m->rx_buf == NULL || out_data == NULL || out_len == NULL || rx_len < 14) {
        return SYN_INVALID_PARAM;
    }

    size_t offset = 2;
    SYN_EcatDatagramResult res;
    if (syn_ecat_frame_parse_next(m->rx_buf, rx_len, &offset, &res)) {
        if (res.data_len >= 16) {
            /* Check Mailbox Type (byte 5 & 0x0F == 3) */
            uint8_t mbox_type = res.data[5] & 0x0F;
            if (mbox_type != 3) {
                return SYN_ERROR;
            }

            /* SDO Header at offset 8 */
            uint8_t sdo_hdr = res.data[8];
            /* Expedited upload response header matches 0x40 (0x4F, 0x4B, 0x47, 0x43) */
            if ((sdo_hdr & 0xE0) == 0x40) {
                uint8_t n = (sdo_hdr >> 2) & 0x03;
                size_t payload_len = 4 - n;
                if (payload_len > max_len) {
                    payload_len = max_len;
                }
                memcpy(out_data, &res.data[12], payload_len);
                *out_len = payload_len;
                return SYN_OK;
            }
        }
    }

    return SYN_ERROR;
}

/* ── Phase 4: Cyclic Process Data Exchange Implementation ───────────────── */

size_t syn_ecat_encode_cyclic(SYN_EcatMaster *m)
{
    if (m == NULL || m->tx_buf == NULL || m->output_size == 0) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_LRW, 0x10,
                                       m->logical_addr, m->output_image, m->output_size, false);
}

SYN_Status syn_ecat_decode_cyclic(SYN_EcatMaster *m, size_t rx_len)
{
    if (m == NULL || m->rx_buf == NULL || rx_len < 14) {
        return SYN_INVALID_PARAM;
    }

    size_t offset = 2;
    SYN_EcatDatagramResult res;
    if (syn_ecat_frame_parse_next(m->rx_buf, rx_len, &offset, &res)) {
        m->wkc_last = res.wkc;
        if (res.wkc >= m->wkc_expected) {
            if (m->input_image != NULL && m->input_size > 0 && res.data_len >= m->input_size) {
                memcpy(m->input_image, res.data, m->input_size);
            }
            m->cycle_count++;
            return SYN_OK;
        } else {
            m->wkc_errors++;
            return SYN_ERROR;
        }
    }

    return SYN_ERROR;
}

/* ── Phase 5: ESM State Transitions Implementation ──────────────────────── */

size_t syn_ecat_encode_set_al_control(SYN_EcatMaster *m, uint16_t station_addr, SYN_EcatState state)
{
    if (m == NULL || m->tx_buf == NULL) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint32_t addr = ((uint32_t)station_addr << 16) | SYN_ESC_REG_AL_CONTROL;
    uint8_t payload[2] = {(uint8_t)(state & 0x0F), 0x00};
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPWR, 0x08, addr,
                                       payload, 2, false);
}

/* ── Phase 6: Distributed Clocks (DC) Implementation ───────────────────── */

size_t syn_ecat_encode_dc_read_system_time(SYN_EcatMaster *m)
{
    if (m == NULL || m->tx_buf == NULL) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint8_t dummy[8] = {0};
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_ARMW, 0x09,
                                       SYN_ESC_REG_DC_SYSTEM_TIME, dummy, 8, false);
}

size_t syn_ecat_encode_dc_configure(SYN_EcatMaster *m, uint16_t station_addr,
                                    uint32_t sync0_cycle_ns, int32_t sync0_shift_ns)
{
    if (m == NULL || m->tx_buf == NULL) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint32_t addr = ((uint32_t)station_addr << 16) | SYN_ESC_REG_DC_SYNC0_CYCLE;
    uint8_t payload[8];
    store32_le(&payload[0], sync0_cycle_ns);
    store32_le(&payload[4], (uint32_t)sync0_shift_ns);

    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPWR, 0x0A, addr,
                                       payload, 8, false);
}

/* ── EtherCAT Master Transport & Protothread Tasks ─────────────────────── */

SYN_Status syn_ecat_master_set_rx_frame(SYN_EcatMaster *m, const uint8_t *frame, size_t len)
{
    if (m == NULL || frame == NULL || len < 14 || len > m->rx_buf_size) {
        return SYN_INVALID_PARAM;
    }

    memcpy(m->rx_buf, frame, len);
    m->rx_frame_len = (uint16_t)len;
    m->frame_rx_ready = true;
    return SYN_OK;
}

bool syn_ecat_master_pop_tx_frame(SYN_EcatMaster *m, const uint8_t **out_frame, size_t *out_len)
{
    if (m == NULL || out_frame == NULL || out_len == NULL || !m->frame_tx_pending) {
        return false;
    }

    *out_frame = m->tx_buf;
    *out_len = (size_t)m->tx_frame_len;
    m->frame_tx_pending = false;
    return true;
}

SYN_PT_Status syn_ecat_master_scan_task(SYN_PT *pt, SYN_EcatMaster *m)
{
    if (pt == NULL || m == NULL) {
        return PT_ENDED;
    }

    PT_BEGIN(pt);

    /* 1. Send scan bus BRD */
    m->tx_frame_len = (uint16_t)syn_ecat_encode_scan_bus(m);
    m->frame_tx_pending = true;
    m->frame_rx_ready = false;
    PT_WAIT_UNTIL(pt, m->frame_rx_ready);
    syn_ecat_decode_scan_bus(m, m->rx_frame_len);

    /* 2. Assign station addresses for each slave */
    for (m->current_slave_idx = 0;
         m->current_slave_idx < m->slave_count && m->current_slave_idx < SYN_ECAT_MAX_SLAVES;
         m->current_slave_idx++) {
        m->slaves[m->current_slave_idx].station_addr = (uint16_t)(0x1001 + m->current_slave_idx);
        m->tx_frame_len = (uint16_t)syn_ecat_encode_assign_addr(
            m, m->current_slave_idx, m->slaves[m->current_slave_idx].station_addr);
        m->frame_tx_pending = true;
        m->frame_rx_ready = false;
        PT_WAIT_UNTIL(pt, m->frame_rx_ready);
        m->slaves[m->current_slave_idx].auto_inc_addr = m->current_slave_idx;
        m->slaves[m->current_slave_idx].state = SYN_ECAT_STATE_INIT;
    }

    m->master_state = SYN_ECAT_STATE_INIT;
    PT_END(pt);
}

static uint16_t syn_ecat_get_slave_station_addr(const SYN_EcatMaster *m, uint8_t idx)
{
    /* LCOV_EXCL_START: Static helper defensive bounds guard */
    if (m == NULL || idx >= SYN_ECAT_MAX_SLAVES) {
        return 0x1001;
    }
    /* LCOV_EXCL_STOP */
    uint16_t addr = m->slaves[idx].station_addr;
    return (addr != 0) ? addr : (uint16_t)(0x1001 + idx);
}

SYN_PT_Status syn_ecat_master_transition_task(SYN_PT *pt, SYN_EcatMaster *m,
                                              SYN_EcatState target_state)
{
    if (pt == NULL || m == NULL) {
        return PT_ENDED;
    }

    PT_BEGIN(pt);

    for (m->current_slave_idx = 0;
         m->current_slave_idx < m->slave_count && m->current_slave_idx < SYN_ECAT_MAX_SLAVES;
         m->current_slave_idx++) {
        uint16_t st_addr = syn_ecat_get_slave_station_addr(m, m->current_slave_idx);

        /* If moving to PREOP, write SyncManager 0 & 1 default mailbox settings */
        if (target_state == SYN_ECAT_STATE_PREOP) {
            static SYN_EcatSMConfig sm0 = {
                .start_addr = 0x1000, .length = 128, .control = 0x26, .enable = 0x01};
            static SYN_EcatSMConfig sm1 = {
                .start_addr = 0x1080, .length = 128, .control = 0x22, .enable = 0x01};

            m->tx_frame_len = (uint16_t)syn_ecat_encode_write_sm(m, st_addr, 0, &sm0);
            m->frame_tx_pending = true;
            m->frame_rx_ready = false;
            PT_WAIT_UNTIL(pt, m->frame_rx_ready);

            st_addr = syn_ecat_get_slave_station_addr(m, m->current_slave_idx);
            m->tx_frame_len = (uint16_t)syn_ecat_encode_write_sm(m, st_addr, 1, &sm1);
            m->frame_tx_pending = true;
            m->frame_rx_ready = false;
            PT_WAIT_UNTIL(pt, m->frame_rx_ready);
        }

        /* Request AL Control state change */
        st_addr = syn_ecat_get_slave_station_addr(m, m->current_slave_idx);
        m->tx_frame_len = (uint16_t)syn_ecat_encode_set_al_control(m, st_addr, target_state);
        m->frame_tx_pending = true;
        m->frame_rx_ready = false;
        PT_WAIT_UNTIL(pt, m->frame_rx_ready);

        /* Read AL Status */
        st_addr = syn_ecat_get_slave_station_addr(m, m->current_slave_idx);
        m->tx_frame_len = (uint16_t)syn_ecat_encode_read_al_status(m, st_addr);
        m->frame_tx_pending = true;
        m->frame_rx_ready = false;
        PT_WAIT_UNTIL(pt, m->frame_rx_ready);
        syn_ecat_decode_read_al_status(m, m->rx_frame_len, &m->slaves[m->current_slave_idx].state,
                                       &m->slaves[m->current_slave_idx].al_status_code);
    }

    m->master_state = target_state;
    PT_END(pt);
}

SYN_PT_Status syn_ecat_master_cyclic_task(SYN_PT *pt, SYN_EcatMaster *m)
{
    if (pt == NULL || m == NULL) {
        return PT_ENDED;
    }

    PT_BEGIN(pt);

    m->tx_frame_len = (uint16_t)syn_ecat_encode_cyclic(m);
    m->frame_tx_pending = true;
    m->frame_rx_ready = false;
    PT_WAIT_UNTIL(pt, m->frame_rx_ready);

    syn_ecat_decode_cyclic(m, m->rx_frame_len);

    PT_END(pt);
}

size_t syn_ecat_encode_read_reg(SYN_EcatMaster *m, uint16_t station_addr, uint16_t reg_addr,
                                uint16_t len)
{
    if (m == NULL || m->tx_buf == NULL || len == 0) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint32_t addr = ((uint32_t)station_addr << 16) | (uint32_t)reg_addr;
    uint8_t dummy[16] = {0};
    uint16_t datagram_len = (len > sizeof(dummy)) ? (uint16_t)sizeof(dummy) : len;
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPRD, 0x01, addr,
                                       dummy, datagram_len, false);
}

size_t syn_ecat_encode_write_reg(SYN_EcatMaster *m, uint16_t station_addr, uint16_t reg_addr,
                                 const void *data, uint16_t len)
{
    if (m == NULL || m->tx_buf == NULL || data == NULL || len == 0) {
        return 0;
    }

    syn_ecat_frame_begin(m->tx_buf, m->tx_buf_size);
    uint32_t addr = ((uint32_t)station_addr << 16) | (uint32_t)reg_addr;
    return syn_ecat_frame_add_datagram(m->tx_buf, m->tx_buf_size, SYN_ECAT_CMD_FPWR, 0x01, addr,
                                       (const uint8_t *)data, len, false);
}

SYN_Status syn_ecat_decode_read_reg(SYN_EcatMaster *m, size_t rx_len, void *out_data, uint16_t len)
{
    if (m == NULL || m->rx_buf == NULL || out_data == NULL || rx_len < 14) {
        return SYN_INVALID_PARAM;
    }

    size_t offset = 2;
    SYN_EcatDatagramResult res;
    if (syn_ecat_frame_parse_next(m->rx_buf, rx_len, &offset, &res)) {
        if (res.wkc >= 1 && res.data_len >= len) {
            memcpy(out_data, res.data, len);
            return SYN_OK;
        }
    }
    return SYN_ERROR;
}

SYN_PT_Status syn_ecat_master_read_sii_task(SYN_PT *pt, SYN_EcatMaster *m, uint16_t station_addr,
                                            uint16_t word_offset, uint32_t *out_data)
{
    if (pt == NULL || m == NULL || out_data == NULL) {
        return PT_ENDED;
    }

    PT_BEGIN(pt);

    /* 1. Write word offset to SII Address reg 0x0504 */
    uint8_t addr_bytes[4];
    store32_le(addr_bytes, (uint32_t)word_offset);
    m->tx_frame_len = (uint16_t)syn_ecat_encode_write_reg(m, station_addr, 0x0504, addr_bytes, 4);
    m->frame_tx_pending = true;
    m->frame_rx_ready = false;
    PT_WAIT_UNTIL(pt, m->frame_rx_ready);

    /* 2. Write Read Command (0x0100) to SII Control reg 0x0502 */
    uint8_t cmd_bytes[2];
    store16_le(cmd_bytes, 0x0100U);
    m->tx_frame_len = (uint16_t)syn_ecat_encode_write_reg(m, station_addr, 0x0502, cmd_bytes, 2);
    m->frame_tx_pending = true;
    m->frame_rx_ready = false;
    PT_WAIT_UNTIL(pt, m->frame_rx_ready);

    /* 3. Poll Busy bit (bit 15 of 0x0502) with timeout counter */
    m->poll_retries = 1000U;
    do {
        m->tx_frame_len = (uint16_t)syn_ecat_encode_read_reg(m, station_addr, 0x0502, 2);
        m->frame_tx_pending = true;
        m->frame_rx_ready = false;
        PT_WAIT_UNTIL(pt, m->frame_rx_ready);

        uint8_t status_buf[2] = {0};
        if (syn_ecat_decode_read_reg(m, m->rx_frame_len, status_buf, 2) == SYN_OK) {
            uint16_t ctrl = load16_le(status_buf);
            if ((ctrl & 0x8000U) == 0) {
                break; /* EEPROM operation complete */
            }
        }
        if (m->poll_retries > 0) {
            m->poll_retries--;
        }
        if (m->poll_retries == 0) {
            PT_EXIT(pt);
        }
    } while (m->poll_retries > 0);

    /* 4. Read 32-bit EEPROM data from reg 0x0508 */
    m->tx_frame_len = (uint16_t)syn_ecat_encode_read_reg(m, station_addr, 0x0508, 4);
    m->frame_tx_pending = true;
    m->frame_rx_ready = false;
    PT_WAIT_UNTIL(pt, m->frame_rx_ready);

    uint8_t data_buf[4] = {0};
    if (syn_ecat_decode_read_reg(m, m->rx_frame_len, data_buf, 4) == SYN_OK) {
        *out_data = load32_le(data_buf);
    }

    PT_END(pt);
}

SYN_PT_Status syn_ecat_master_sdo_read_task(SYN_PT *pt, SYN_EcatMaster *m, uint16_t station_addr,
                                            uint16_t index, uint8_t subindex, void *out_data,
                                            size_t max_len, size_t *out_len)
{
    if (pt == NULL || m == NULL || out_data == NULL || out_len == NULL) {
        return PT_ENDED;
    }

    PT_BEGIN(pt);

    /* 1. Send SDO Read request to SM0 Mailbox */
    m->tx_frame_len = (uint16_t)syn_ecat_encode_coe_sdo_read(m, station_addr, index, subindex);
    m->frame_tx_pending = true;
    m->frame_rx_ready = false;
    PT_WAIT_UNTIL(pt, m->frame_rx_ready);

    /* 2. Poll SM1 Status (reg 0x080D - bit 3: mailbox full) with timeout counter */
    m->poll_retries = 1000U;
    do {
        m->tx_frame_len = (uint16_t)syn_ecat_encode_read_reg(m, station_addr, 0x080D, 1);
        m->frame_tx_pending = true;
        m->frame_rx_ready = false;
        PT_WAIT_UNTIL(pt, m->frame_rx_ready);

        uint8_t sm1_status = 0;
        if (syn_ecat_decode_read_reg(m, m->rx_frame_len, &sm1_status, 1) == SYN_OK) {
            if ((sm1_status & 0x08U) != 0) {
                break; /* Mailbox response ready */
            }
        }
        if (m->poll_retries > 0) {
            m->poll_retries--;
        }
        if (m->poll_retries == 0) {
            PT_EXIT(pt);
        }
    } while (m->poll_retries > 0);

    /* 3. Read SDO response from SM1 Mailbox buffer (address 0x1080) */
    m->tx_frame_len = (uint16_t)syn_ecat_encode_read_reg(m, station_addr, 0x1080, 16);
    m->frame_tx_pending = true;
    m->frame_rx_ready = false;
    PT_WAIT_UNTIL(pt, m->frame_rx_ready);

    uint32_t val = 0;
    if (syn_ecat_decode_coe_sdo_response(m, m->rx_frame_len, &val, max_len, out_len) == SYN_OK) {
        if (max_len >= 4) {
            store32_le((uint8_t *)out_data, val);
        } else if (max_len >= 2) {
            store16_le((uint8_t *)out_data, (uint16_t)val);
        } else if (max_len >= 1) {
            ((uint8_t *)out_data)[0] = (uint8_t)val;
        }
    }

    PT_END(pt);
}

SYN_PT_Status syn_ecat_master_discover_pdo_mapping_task(SYN_PT *pt, SYN_EcatMaster *m,
                                                        uint16_t station_addr,
                                                        uint16_t *out_rx_bytes,
                                                        uint16_t *out_tx_bytes)
{
    if (pt == NULL || m == NULL || out_rx_bytes == NULL || out_tx_bytes == NULL) {
        return PT_ENDED;
    }

    PT_BEGIN(pt);

    m->total_rx_bits = 0;
    m->total_tx_bits = 0;

    /* Read 0x1C12:00 (RxPDO Assign Count) */
    m->pdo_count = 0;
    m->pdo_sdo_val = 0;
    PT_SPAWN(pt, &m->sub_pt,
             syn_ecat_master_sdo_read_task(&m->sub_pt, m, station_addr, 0x1C12, 0, &m->pdo_sdo_val,
                                           1, &m->pdo_read_len));
    m->pdo_count = (uint8_t)m->pdo_sdo_val;

    if (m->pdo_count > 0) {
        /* Read first RxPDO assignment (0x1C12:01) */
        m->pdo_sdo_val = 0;
        PT_SPAWN(pt, &m->sub_pt,
                 syn_ecat_master_sdo_read_task(&m->sub_pt, m, station_addr, 0x1C12, 1,
                                               &m->pdo_sdo_val, 2, &m->pdo_read_len));
        m->pdo_map_idx = (uint16_t)m->pdo_sdo_val;
        if (m->pdo_map_idx != 0) {
            /* Read RxPDO mapping entry count (e.g. 0x1600:00) */
            m->pdo_sdo_val = 0;
            PT_SPAWN(pt, &m->sub_pt,
                     syn_ecat_master_sdo_read_task(&m->sub_pt, m, station_addr, m->pdo_map_idx, 0,
                                                   &m->pdo_sdo_val, 1, &m->pdo_read_len));
            m->pdo_count = (uint8_t)m->pdo_sdo_val;
            for (m->pdo_entry_idx = 1; m->pdo_entry_idx <= m->pdo_count; m->pdo_entry_idx++) {
                m->pdo_sdo_val = 0;
                PT_SPAWN(pt, &m->sub_pt,
                         syn_ecat_master_sdo_read_task(&m->sub_pt, m, station_addr, m->pdo_map_idx,
                                                       m->pdo_entry_idx, &m->pdo_sdo_val, 4,
                                                       &m->pdo_read_len));
                m->total_rx_bits += (m->pdo_sdo_val & 0xFFU);
            }
        }
    }

    /* Read 0x1C13:00 (TxPDO Assign Count) */
    m->pdo_count = 0;
    m->pdo_sdo_val = 0;
    PT_SPAWN(pt, &m->sub_pt,
             syn_ecat_master_sdo_read_task(&m->sub_pt, m, station_addr, 0x1C13, 0, &m->pdo_sdo_val,
                                           1, &m->pdo_read_len));
    m->pdo_count = (uint8_t)m->pdo_sdo_val;

    if (m->pdo_count > 0) {
        /* Read first TxPDO assignment (0x1C13:01) */
        m->pdo_sdo_val = 0;
        PT_SPAWN(pt, &m->sub_pt,
                 syn_ecat_master_sdo_read_task(&m->sub_pt, m, station_addr, 0x1C13, 1,
                                               &m->pdo_sdo_val, 2, &m->pdo_read_len));
        m->pdo_map_idx = (uint16_t)m->pdo_sdo_val;
        if (m->pdo_map_idx != 0) {
            /* Read TxPDO mapping entry count (e.g. 0x1A00:00) */
            m->pdo_sdo_val = 0;
            PT_SPAWN(pt, &m->sub_pt,
                     syn_ecat_master_sdo_read_task(&m->sub_pt, m, station_addr, m->pdo_map_idx, 0,
                                                   &m->pdo_sdo_val, 1, &m->pdo_read_len));
            m->pdo_count = (uint8_t)m->pdo_sdo_val;
            for (m->pdo_entry_idx = 1; m->pdo_entry_idx <= m->pdo_count; m->pdo_entry_idx++) {
                m->pdo_sdo_val = 0;
                PT_SPAWN(pt, &m->sub_pt,
                         syn_ecat_master_sdo_read_task(&m->sub_pt, m, station_addr, m->pdo_map_idx,
                                                       m->pdo_entry_idx, &m->pdo_sdo_val, 4,
                                                       &m->pdo_read_len));
                m->total_tx_bits += (m->pdo_sdo_val & 0xFFU);
            }
        }
    }

    *out_rx_bytes = (uint16_t)((m->total_rx_bits + 7U) / 8U);
    *out_tx_bytes = (uint16_t)((m->total_tx_bits + 7U) / 8U);

    PT_END(pt);
}

#endif /* SYN_USE_ETHERCAT */