Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/iolinki/frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ typedef struct
} iolink_frame_operate_response_t;

int iolink_frame_encode_type0(uint8_t mc, uint8_t* out, size_t out_size);
int iolink_frame_encode_type0_write(uint8_t mc, uint8_t od, uint8_t* out, size_t out_size);
int iolink_frame_encode_type1_cycle(const uint8_t* pd_out, uint8_t pd_out_len, uint8_t od_len,
uint8_t* out, size_t out_size);
int iolink_frame_decode_operate_response(const uint8_t* frame, size_t frame_len, uint8_t pd_in_len,
Expand Down
13 changes: 13 additions & 0 deletions include/iolinki/isdu.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,17 @@ int iolink_isdu_collect_byte(iolink_isdu_ctx_t* ctx, uint8_t byte);
*/
int iolink_isdu_get_response_byte(iolink_isdu_ctx_t* ctx, uint8_t* byte);

/**
* @brief Read a single Direct Parameter page 1 octet.
*
* Builds the current Direct Parameter page 1 image (Table B.1) and returns the
* octet at the given address. Used by the DLL to answer a startup Type-0 read on
* the page communication channel.
*
* @param ctx ISDU context
* @param addr Direct Parameter page address (0x00-0x0F)
* @return uint8_t Octet value, or 0 for an out-of-range address or NULL context
*/
uint8_t iolink_isdu_direct_param_page1_octet(iolink_isdu_ctx_t* ctx, uint8_t addr);

#endif // IOLINK_ISDU_H
8 changes: 6 additions & 2 deletions include/iolinki/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@

#define IOLINK_MC_TRANSITION_COMMAND 0x0FU

/* MasterCommand values written to Direct Parameter page address 0x00 (Table B.2). */
#define IOLINK_CMD_DEVICE_OPERATE 0x99U

/* ISDU Control Byte Bits */
#define IOLINK_ISDU_CTRL_START 0x80U
#define IOLINK_ISDU_CTRL_LAST 0x40U
#define IOLINK_ISDU_CTRL_SEQ_MASK 0x3FU

/* ISDU Service IDs */
#define IOLINK_ISDU_SERVICE_READ 0x08U
#define IOLINK_ISDU_SERVICE_WRITE 0x09U
/* I-Service nibble, IO-Link spec Table A.12 (16-bit Index + Subindex form). */
#define IOLINK_ISDU_SERVICE_READ 0x0BU
#define IOLINK_ISDU_SERVICE_WRITE 0x03U

/* Mandatory ISDU Indices */
#define IOLINK_IDX_DIRECT_PARAMETERS_1 0x0000U
Expand Down
64 changes: 56 additions & 8 deletions src/dll.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ static void dll_handle_preoperate(iolink_dll_ctx_t* ctx, uint8_t mc, uint8_t ck)
}
}

/* Answer a startup Type-0 read on the page communication channel (spec startup
transition T1): the master reads a Direct Parameter page octet (address in the
MC address field, e.g. 0x02 = MinCycleTime). Reply with a 2-octet Type-0 frame
carrying that octet, rather than feeding the MC into the ISDU engine. */
static void dll_handle_page_channel_read(iolink_dll_ctx_t* ctx, uint8_t mc)
{
uint8_t resp[2];
resp[0] =
iolink_isdu_direct_param_page1_octet(&ctx->isdu, (uint8_t) (mc & IOLINK_MC_ADDR_MASK));
resp[1] = iolink_checksum_ck(resp[0], 0U);
if (ctx->phy->send != NULL) {
ctx->phy->send(ctx->phy->user, resp, 2);
}
}

static void dll_handle_operate_type0(iolink_dll_ctx_t* ctx, uint8_t mc, uint8_t cks)
{
(void) cks;
Expand Down Expand Up @@ -383,7 +398,16 @@ void iolink_dll_process(iolink_dll_ctx_t* ctx)
ctx->frame_index = 1U;
ctx->last_frame_us = now_us;

if (ctx->baudrate == IOLINK_BAUDRATE_COM1) {
/* DeviceOperate MC = WRITE (RW=0) to Direct Parameter address 0x00 on
the page channel = 0x20. It is the only 3-octet Type-0 request the
device expects in PREOPERATE (MC + OD + CK); every other PREOPERATE
Type-0 frame is a 2-octet ISDU exchange. */
bool is_preop_device_operate =
(ctx->state == IOLINK_DLL_STATE_PREOPERATE) && (byte == 0x20U);
if (is_preop_device_operate) {
ctx->req_len = 3U;
}
else if (ctx->baudrate == IOLINK_BAUDRATE_COM1) {
ctx->req_len = 2U;
}
else {
Expand Down Expand Up @@ -437,17 +461,41 @@ void iolink_dll_process(iolink_dll_ctx_t* ctx)
}

if (crc_ok) {
if ((ctx->state == IOLINK_DLL_STATE_AWAITING_COMM) ||
(ctx->state == IOLINK_DLL_STATE_STARTUP)) {
bool was_establishing = (ctx->state == IOLINK_DLL_STATE_AWAITING_COMM) ||
(ctx->state == IOLINK_DLL_STATE_STARTUP);
if (was_establishing) {
dll_set_state(ctx, IOLINK_DLL_STATE_PREOPERATE);
}

if (ctx->state == IOLINK_DLL_STATE_PREOPERATE) {
if (ctx->req_len == 2U) {
if (ctx->frame_buf[0] == IOLINK_MC_TRANSITION_COMMAND)
dll_handle_preoperate(ctx, ctx->frame_buf[0], ctx->frame_buf[1]);
else
dll_handle_operate_type0(ctx, ctx->frame_buf[0], ctx->frame_buf[1]);
uint8_t mc = ctx->frame_buf[0];
bool page_read = ((mc & IOLINK_MC_RW_MASK) != 0U) &&
((mc & IOLINK_MC_COMM_CHANNEL_MASK) == 0x20U);
if (was_establishing && (ctx->req_len == 2U) && page_read) {
/* First message after wake-up is the spec startup probe
(transition T1): a Type-0 READ of a Direct Parameter page
octet on the page channel. Answer from the direct-parameter
source rather than feeding the MC into ISDU. Later PREOPERATE
Type-0 frames are ISDU traffic and are not intercepted. */
dll_handle_page_channel_read(ctx, mc);
}
else if (ctx->req_len == 2U) {
if (mc == IOLINK_MC_TRANSITION_COMMAND) {
dll_handle_preoperate(ctx, mc, ctx->frame_buf[1]);
}
else {
dll_handle_operate_type0(ctx, mc, ctx->frame_buf[1]);
}
}
else if ((ctx->req_len == 3U) && ((mc & IOLINK_MC_RW_MASK) == 0U) &&
((mc & IOLINK_MC_COMM_CHANNEL_MASK) == 0x20U) &&
((mc & IOLINK_MC_ADDR_MASK) == 0x00U) &&
(ctx->frame_buf[1] == IOLINK_CMD_DEVICE_OPERATE)) {
/* Spec DeviceOperate: page-channel WRITE of MasterCommand
0x99 to Direct Parameter address 0x00 establishes
communication (no response per spec). */
dll_set_state(ctx, IOLINK_DLL_STATE_ESTAB_COM);
ctx->fallback_count = 0U;
}
}
else if (ctx->state == IOLINK_DLL_STATE_ESTAB_COM) {
Expand Down
16 changes: 16 additions & 0 deletions src/frame.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ int iolink_frame_encode_type0(uint8_t mc, uint8_t* out, size_t out_size)
return (int) IOLINK_M_SEQ_TYPE0_LEN;
}

int iolink_frame_encode_type0_write(uint8_t mc, uint8_t od, uint8_t* out, size_t out_size)
{
if ((out == NULL) || (out_size < IOLINK_M_SEQ_MIN_LEN)) {
return -1;
}

/* Type-0 write frame (MC + one OD data octet + CK). The trailing checksum is
the 6-bit M-sequence CRC over the preceding octets, matching how the
device DLL verifies any request longer than the 2-octet Type-0 read. */
out[0] = mc;
out[1] = od;
out[2] = iolink_crc6(out, 2U);

return (int) IOLINK_M_SEQ_MIN_LEN;
}

int iolink_frame_encode_type1_cycle(const uint8_t* pd_out, uint8_t pd_out_len, uint8_t od_len,
uint8_t* out, size_t out_size)
{
Expand Down
36 changes: 32 additions & 4 deletions src/isdu.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ static int isdu_params_set(iolink_isdu_ctx_t* ctx, uint16_t index, uint8_t subin
return iolink_params_set(index, subindex, data, len, persist);
}

static void isdu_params_factory_reset(iolink_isdu_ctx_t* ctx)
{
/* Reset the device's own parameter context when one is bound (as device.c
does via ctx->params_ctx); otherwise fall back to the legacy global set.
Using the bound context is required, else a per-device app tag survives a
Restore Factory Settings command. */
if ((ctx != NULL) && (ctx->params_ctx != NULL)) {
iolink_params_ctx_factory_reset(ctx->params_ctx);
}
else {
iolink_params_factory_reset();
}
}

static int isdu_handle_idle(iolink_isdu_ctx_t* ctx, uint8_t byte)
{
bool start = ((byte & IOLINK_ISDU_CTRL_START) != 0U);
Expand Down Expand Up @@ -150,12 +164,14 @@ int iolink_isdu_collect_byte(iolink_isdu_ctx_t* ctx, uint8_t byte)
uint8_t service = (uint8_t) ((byte >> 4) & 0x0FU);
uint8_t length = (uint8_t) (byte & 0x0FU);

if (service == 0x08U) {
/* I-Service nibble per spec Table A.12 (16-bit Index + Subindex form,
* which is what the master emits): read = 0x0B, write = 0x03. */
if (service == IOLINK_ISDU_SERVICE_READ) {
ctx->header.type = IOLINK_ISDU_SERVICE_TYPE_READ;
ctx->header.length = 0U;
ctx->next_state = ISDU_STATE_HEADER_INDEX_HIGH;
}
else if (service == 0x09U) {
else if (service == IOLINK_ISDU_SERVICE_WRITE) {
ctx->header.type = IOLINK_ISDU_SERVICE_TYPE_WRITE;
if (length == 15U) {
ctx->next_state = ISDU_STATE_HEADER_EXT_LEN;
Expand Down Expand Up @@ -468,12 +484,12 @@ static void handle_system_command(iolink_isdu_ctx_t* ctx, uint8_t cmd)

case IOLINK_CMD_RESTORE_FACTORY_SETTINGS: /* 0x82 */
/* Reset all parameters to factory defaults */
iolink_params_factory_reset();
isdu_params_factory_reset(ctx);
break;

case IOLINK_CMD_RESTORE_APP_DEFAULTS: /* 0x83 */
/* Reset application-specific parameters (currently same as factory) */
iolink_params_factory_reset();
isdu_params_factory_reset(ctx);
break;

case IOLINK_CMD_SET_COMM_MODE: /* 0x84 */
Expand Down Expand Up @@ -757,6 +773,18 @@ static void build_direct_param_page1(iolink_isdu_ctx_t* ctx, uint8_t* page)
/* 0x0C-0x0E reserved (0); 0x0F SystemCommand (W) reads 0. */
}

uint8_t iolink_isdu_direct_param_page1_octet(iolink_isdu_ctx_t* ctx, uint8_t addr)
{
uint8_t page[16];

if ((ctx == NULL) || (addr >= sizeof(page))) {
return 0U;
}

build_direct_param_page1(ctx, page);
return page[addr];
}

static void handle_direct_parameters(iolink_isdu_ctx_t* ctx)
{
bool page2 = (ctx->header.index == IOLINK_IDX_DIRECT_PARAMETERS_2);
Expand Down
9 changes: 5 additions & 4 deletions tests/test_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,8 @@ int isdu_send_read_request(iolink_isdu_ctx_t* ctx, uint16_t index, uint8_t subin
ret = iolink_isdu_collect_byte(ctx, 0x80);
if (ret != 0) return ret;

/* Data: Read service (0x80 = Read, Length=0) */
ret = iolink_isdu_collect_byte(ctx, 0x80);
/* Data: Read service (0xB0 = I-Service READ nibble 0x0B per Table A.12, Length=0) */
ret = iolink_isdu_collect_byte(ctx, 0xB0);
if (ret != 0) return ret;

/* Control: Seq=1 */
Expand Down Expand Up @@ -313,10 +313,11 @@ int isdu_send_write_request(iolink_isdu_ctx_t* ctx, uint16_t index, uint8_t subi
/* Data: Write service */
uint8_t service_byte;
if (data_len <= 15) {
service_byte = 0x90 | data_len; /* Write, embedded length */
service_byte =
0x30 | data_len; /* I-Service WRITE nibble 0x03 (Table A.12), embedded length */
}
else {
service_byte = 0x9F; /* Write, extended length */
service_byte = 0x3F; /* I-Service WRITE nibble 0x03, extended length */
}
ret = iolink_isdu_collect_byte(ctx, service_byte);
if (ret != 0) return ret;
Expand Down
6 changes: 3 additions & 3 deletions tests/test_isdu_segmented.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ static void test_isdu_segmented_write_corrected(void** state)
/* Write Index 0x18, 2 bytes */
/* Write Index 0x18, 2 bytes */
iolink_isdu_collect_byte(&ctx, 0x80); /* Start, Seq=0 */
iolink_isdu_collect_byte(&ctx, 0x92);
iolink_isdu_collect_byte(&ctx, 0x32); /* Write (I-Service nibble 0x03), Len=2 */

iolink_isdu_collect_byte(&ctx, 0x01); /* Seq=1 */
iolink_isdu_collect_byte(&ctx, 0x00);
Expand Down Expand Up @@ -90,7 +90,7 @@ static void test_isdu_busy_response(void** state)

/* 1. Start a write request */
iolink_isdu_collect_byte(&ctx, 0x81); /* Start, Seq=1 */
iolink_isdu_collect_byte(&ctx, 0x92); /* Write, Len=2 */
iolink_isdu_collect_byte(&ctx, 0x32); /* Write (I-Service nibble 0x03), Len=2 */

/* 2. Before finishing, send another Start bit (Concurrent request) */
/* iolink_isdu_collect_byte should return 1 to indicate a response is now ready (the error
Expand Down Expand Up @@ -133,7 +133,7 @@ static void test_isdu_segmentation_error(void** state)

/* 1. Start a segmented write */
iolink_isdu_collect_byte(&ctx, 0x81); /* Start, Seq=1, !Last */
iolink_isdu_collect_byte(&ctx, 0x91); /* Write, Len=1 */
iolink_isdu_collect_byte(&ctx, 0x31); /* Write (I-Service nibble 0x03), Len=1 */

/* 2. Send wrong sequence number (Expected 0x02, send 0x03) */
assert_int_equal(iolink_isdu_collect_byte(&ctx, 0x03), -1);
Expand Down
7 changes: 4 additions & 3 deletions tests/test_isdu_stress.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ static void test_rapid_concurrent_requests(void** state)

/* 1. Start a write request */
iolink_isdu_collect_byte(&ctx, 0x81); /* Start, Seq=1 */
iolink_isdu_collect_byte(&ctx, 0x92); /* Write, Len=2 */
iolink_isdu_collect_byte(&ctx, 0x32); /* Write (I-Service nibble 0x03), Len=2 */

/* 2. Send another Start bit immediately (Collision/Concurrency) */
iolink_isdu_collect_byte(&ctx, 0x82);
Expand Down Expand Up @@ -82,7 +82,8 @@ static void test_maximum_segmented_transfer(void** state)
/* Write 16 bytes to Index 0x0018 (App Tag) using segmentation */
/* Header: [RW+Len] [ExtLen] [IndexH] [IndexL] [Subindex] */
iolink_isdu_collect_byte(&ctx, 0x81); /* Start, Seq=1, !Last */
iolink_isdu_collect_byte(&ctx, 0x9F); /* Write, Len=15 (extended length follows) */
iolink_isdu_collect_byte(
&ctx, 0x3F); /* Write (I-Service nibble 0x03), Len=15 (extended length follows) */

/* Extended length: 16 bytes total */
iolink_isdu_collect_byte(&ctx, 0x02); /* Seq=2, !Last */
Expand Down Expand Up @@ -130,7 +131,7 @@ static void test_sequence_number_wraparound(void** state)
/* Test sequence number wraparound (0-63) */
/* Start with seq 61 */
iolink_isdu_collect_byte(&ctx, 0x80 | 61); /* Start, Seq=61, !Last */
iolink_isdu_collect_byte(&ctx, 0x80); /* Read, Len=0 */
iolink_isdu_collect_byte(&ctx, 0xB0); /* Read (I-Service nibble 0x0B), Len=0 */

iolink_isdu_collect_byte(&ctx, 62); /* Seq=62, !Last */
iolink_isdu_collect_byte(&ctx, 0x00); /* Index MSB */
Expand Down
2 changes: 1 addition & 1 deletion tools/virtual_master/test_conformance_error_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def test_04_boundary_condition_max_isdu_size(self):
time.sleep(0.5)

self.master.run_startup_sequence()
# Transition to OPERATE to avoid Type 0 MC collision (0x0F)
# Transition to OPERATE via DeviceOperate (MC=0x20, OD=0x99)
self.master.m_seq_type = 2
self.master.pd_out_len = 2
self.master.pd_in_len = 2
Expand Down
4 changes: 2 additions & 2 deletions tools/virtual_master/test_conformance_state_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def test_07_estab_com_to_operate_transition(self):
Requirement: IO-Link V1.1.5 Section 7.3 - DLL State Machine

Validates:
- Device enters ESTAB_COM state on transition command (MC=0x0F)
- Device enters ESTAB_COM state on DeviceOperate (MC=0x20, OD=0x99)
- First valid PD frame triggers ESTAB_COM → OPERATE transition
- Subsequent PD frames are processed in OPERATE state
- Invalid frames in ESTAB_COM don't cause premature transition
Expand All @@ -256,7 +256,7 @@ def test_07_estab_com_to_operate_transition(self):

self.master.go_to_operate()
time.sleep(0.05)
print("[INFO] Sent transition command (MC=0x0F), device should be in ESTAB_COM")
print("[INFO] Sent DeviceOperate (MC=0x20, OD=0x99), device should be in ESTAB_COM")

resp1 = self.master.run_cycle(pd_out=b"\x12\x34")
self.assertIsNotNone(resp1, "First PD frame should get response")
Expand Down
16 changes: 10 additions & 6 deletions tools/virtual_master/virtual_master/master.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def read_isdu(self, index: int, subindex: int = 0) -> Optional[bytes]:
)

bytes_to_send = self.generator.generate_isdu_read_v11(
index, subindex, service_id=0x80
index, subindex, service_id=0xB0
)

def send_and_recv(byte_to_send: int):
Expand Down Expand Up @@ -316,9 +316,13 @@ def run_startup_sequence(self, send_wakeup: bool = True) -> bool:
return False

def go_to_operate(self) -> bool:
"""Send transition command to Device."""
print("[Master] Sending OPERATE transition command (MC=0x0F)")
frame = self.generator.generate_type0(0x0F) # Custom transition MC
"""Send the spec DeviceOperate transition to the Device.

Type-0 WRITE: MC 0x20 (page channel, addr 0x00) + OD 0x99
(DeviceOperate) + CRC6. No response per spec.
"""
print("[Master] Sending DeviceOperate transition (MC=0x20, OD=0x99)")
frame = self.generator.generate_device_operate()
self.uart.send_bytes(frame)
time.sleep(0.05) # Give device time to switch
self.state = MasterState.OPERATE
Expand Down Expand Up @@ -420,7 +424,7 @@ def write_isdu(self, index: int, subindex: int, data: bytes) -> bool:

data_len = len(data)
if data_len > 15:
service_id = 0x9F
service_id = 0x3F # I-Service WRITE nibble 0x03 (Table A.12), extended length
request_data = [
service_id,
data_len,
Expand All @@ -430,7 +434,7 @@ def write_isdu(self, index: int, subindex: int, data: bytes) -> bool:
] + list(data)
else:
request_data = [
0x90 | (data_len & 0x0F),
0x30 | (data_len & 0x0F), # I-Service WRITE nibble 0x03, embedded length
(index >> 8) & 0xFF,
index & 0xFF,
subindex,
Expand Down
Loading
Loading