From 1fab254fa9072a7071fc2f87975da852b9049398 Mon Sep 17 00:00:00 2001 From: w1ne <14119286+w1ne@users.noreply.github.com> Date: Sun, 5 Jul 2026 00:48:29 +0200 Subject: [PATCH 1/4] wire(isdu): I-Service nibble per spec Table A.12 [slice 1/3] protocol.h IOLINK_ISDU_SERVICE_READ 0x08->0x0B, WRITE 0x09->0x03 (16-bit Index+Subindex form). isdu.c decoder accepts the spec nibbles. Pairs with the iolinki-master encode change; verified via the master's test_master_real_iolinki_device. Device-repo C tests + virtual master + conformance suite still need updating (follow-up). --- include/iolinki/protocol.h | 5 +++-- src/isdu.c | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/include/iolinki/protocol.h b/include/iolinki/protocol.h index 38eb9e8..e282088 100644 --- a/include/iolinki/protocol.h +++ b/include/iolinki/protocol.h @@ -32,8 +32,9 @@ #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 diff --git a/src/isdu.c b/src/isdu.c index 79c6cc1..9e81ec3 100644 --- a/src/isdu.c +++ b/src/isdu.c @@ -150,12 +150,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; From b7ff6286e5c6d25511bf66add425c5b62692c29b Mon Sep 17 00:00:00 2001 From: w1ne <14119286+w1ne@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:04:18 +0200 Subject: [PATCH 2/4] feat: spec-conformant startup probe and OPERATE transition (device) Make the device DLL agree with the master on the V1.1.5 startup wire. - Startup probe (transition T1): in STARTUP/AWAITING_COMM, answer the first page-channel Type-0 READ (MC 0xA2) with the requested Direct Parameter page 1 octet (address 0x02 -> MinCycleTime) instead of feeding the MC into ISDU, then advance to PREOPERATE. Only the first post-wake frame is treated as the probe, so later PREOPERATE ISDU traffic is unaffected. - OPERATE transition: accept a Type-0 page-channel WRITE of MasterCommand DeviceOperate (0x99) to Direct Parameter address 0x00 (MC 0x20, 3-octet frame) to establish communication. The legacy 0x0F transition path is retained. - Add iolink_frame_encode_type0_write() for the 3-octet write frame and iolink_isdu_direct_param_page1_octet() to source the probe reply. - Add IOLINK_CMD_DEVICE_OPERATE (0x99). --- include/iolinki/frame.h | 1 + include/iolinki/isdu.h | 13 ++++++++ include/iolinki/protocol.h | 3 ++ src/dll.c | 63 +++++++++++++++++++++++++++++++++----- src/frame.c | 16 ++++++++++ src/isdu.c | 12 ++++++++ 6 files changed, 100 insertions(+), 8 deletions(-) diff --git a/include/iolinki/frame.h b/include/iolinki/frame.h index 76cc973..b423869 100644 --- a/include/iolinki/frame.h +++ b/include/iolinki/frame.h @@ -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, diff --git a/include/iolinki/isdu.h b/include/iolinki/isdu.h index d00f7b4..fe2f143 100644 --- a/include/iolinki/isdu.h +++ b/include/iolinki/isdu.h @@ -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 diff --git a/include/iolinki/protocol.h b/include/iolinki/protocol.h index e282088..42baf3f 100644 --- a/include/iolinki/protocol.h +++ b/include/iolinki/protocol.h @@ -26,6 +26,9 @@ #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 diff --git a/src/dll.c b/src/dll.c index cddd9f4..17f2a70 100644 --- a/src/dll.c +++ b/src/dll.c @@ -131,6 +131,20 @@ 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; @@ -383,7 +397,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 { @@ -437,17 +460,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) { diff --git a/src/frame.c b/src/frame.c index 7b42844..a5aec2d 100644 --- a/src/frame.c +++ b/src/frame.c @@ -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) { diff --git a/src/isdu.c b/src/isdu.c index 9e81ec3..96b51af 100644 --- a/src/isdu.c +++ b/src/isdu.c @@ -759,6 +759,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); From 865afb93f9159a51b7e348e232a187b70fa119ae Mon Sep 17 00:00:00 2001 From: w1ne <14119286+w1ne@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:22:11 +0200 Subject: [PATCH 3/4] test: drive spec-conformant ISDU/startup wire across device test infra The device stack now speaks the V1.1.5-conformant wire, but the test infrastructure still emitted the old co-designed octets. Update all of it to the conformant framing so the C suite and the 49-test conformance suite pass end to end. - C tests: ISDU request headers now use the I-Service nibbles from Table A.12 (read 0xB0, write 0x3X / 0x3F extended) in test_helpers, test_isdu_segmented and test_isdu_stress. - Virtual master: startup probe is a Type-0 page-channel read (MC 0xA2, MinCycleTime); DeviceOperate transition is a Type-0 write (MC 0x20 + OD 0x99 + CRC6); ISDU read/write use the conformant service nibbles. - Conformance suite: correct the state-machine and error-injection notes to the DeviceOperate octets. Also fix a latent device defect exposed once ISDU writes complete on the conformant wire: the Restore Factory Settings / Restore Application Defaults commands reset the legacy global parameter set instead of the device's bound context, so a per-device application tag survived a factory restore. Route the reset through the bound params context. --- src/isdu.c | 18 ++++++- tests/test_helpers.c | 8 +-- tests/test_isdu_segmented.c | 6 +-- tests/test_isdu_stress.c | 6 +-- .../test_conformance_error_injection.py | 2 +- .../test_conformance_state_machine.py | 4 +- tools/virtual_master/virtual_master/master.py | 16 +++--- .../virtual_master/virtual_master/protocol.py | 49 ++++++++++++++++--- 8 files changed, 82 insertions(+), 27 deletions(-) diff --git a/src/isdu.c b/src/isdu.c index 96b51af..47c4644 100644 --- a/src/isdu.c +++ b/src/isdu.c @@ -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); @@ -470,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 */ diff --git a/tests/test_helpers.c b/tests/test_helpers.c index 5fedc58..391a282 100644 --- a/tests/test_helpers.c +++ b/tests/test_helpers.c @@ -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 */ @@ -313,10 +313,10 @@ 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; diff --git a/tests/test_isdu_segmented.c b/tests/test_isdu_segmented.c index e2659b6..475774d 100644 --- a/tests/test_isdu_segmented.c +++ b/tests/test_isdu_segmented.c @@ -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); @@ -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 @@ -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); diff --git a/tests/test_isdu_stress.c b/tests/test_isdu_stress.c index f8919c2..1711c42 100644 --- a/tests/test_isdu_stress.c +++ b/tests/test_isdu_stress.c @@ -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); @@ -82,7 +82,7 @@ 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 */ @@ -130,7 +130,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 */ diff --git a/tools/virtual_master/test_conformance_error_injection.py b/tools/virtual_master/test_conformance_error_injection.py index 40aed36..e6857c2 100755 --- a/tools/virtual_master/test_conformance_error_injection.py +++ b/tools/virtual_master/test_conformance_error_injection.py @@ -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 diff --git a/tools/virtual_master/test_conformance_state_machine.py b/tools/virtual_master/test_conformance_state_machine.py index 97c4d78..e40fad4 100755 --- a/tools/virtual_master/test_conformance_state_machine.py +++ b/tools/virtual_master/test_conformance_state_machine.py @@ -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 @@ -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") diff --git a/tools/virtual_master/virtual_master/master.py b/tools/virtual_master/virtual_master/master.py index cb328ba..bbe8e59 100644 --- a/tools/virtual_master/virtual_master/master.py +++ b/tools/virtual_master/virtual_master/master.py @@ -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): @@ -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 @@ -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, @@ -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, diff --git a/tools/virtual_master/virtual_master/protocol.py b/tools/virtual_master/virtual_master/protocol.py index 3f68c8c..0aff309 100644 --- a/tools/virtual_master/virtual_master/protocol.py +++ b/tools/virtual_master/virtual_master/protocol.py @@ -7,7 +7,12 @@ """ from enum import IntEnum -from .crc import calculate_checksum_type0, calculate_checksum_type1, verify_checksum +from .crc import ( + calculate_checksum_type0, + calculate_checksum_type1, + calculate_crc6, + verify_checksum, +) """ IO-Link protocol implementation - M-sequence generation and parsing. @@ -43,6 +48,16 @@ class MasterCommand: MC_EVENT_REQ = 0xA2 + # Spec-conformant startup probe (transition T1): Type-0 READ (RW=1, 0x80) + # on the page communication channel (0x20) of Direct Parameter address + # 0x02 = MinCycleTime. 0x80 | 0x20 | 0x02 = 0xA2. + MC_STARTUP_PROBE = 0xA2 + + # Spec DeviceOperate: page-channel WRITE (RW=0) at Direct Parameter + # address 0x00 (0x20 | 0x00) carrying MasterCommand 0x99 (DeviceOperate). + MC_DEVICE_OPERATE = 0x20 + OD_DEVICE_OPERATE = 0x99 + @staticmethod def is_isdu_command(mc: int) -> bool: """Check if MC is an ISDU command.""" @@ -98,29 +113,51 @@ def generate_wakeup(self) -> bytes: return self.generate_type0(MasterCommand.MC_WAKEUP) def generate_idle(self) -> bytes: - """Generate idle sequence.""" - return self.generate_type0(MasterCommand.MC_IDLE) + """Generate the spec startup probe frame. + + The device establishes communication on a Type-0 READ of the page + communication channel (MC=0xA2, MinCycleTime). It replies with the + MinCycleTime octet, which the master uses to confirm the link. + """ + return self.generate_type0(MasterCommand.MC_STARTUP_PROBE) + + def generate_device_operate(self) -> bytes: + """Generate the spec DeviceOperate transition frame. + + Type-0 WRITE: MC 0x20 (page channel, address 0x00) + OD 0x99 + (MasterCommand DeviceOperate) + 6-bit CRC over [MC, OD]. + """ + return self.generate_type0_write( + MasterCommand.MC_DEVICE_OPERATE, MasterCommand.OD_DEVICE_OPERATE + ) + + def generate_type0_write(self, mc: int, od: int) -> bytes: + """Generate a 3-octet Type-0 WRITE frame: [MC, OD, CRC6(MC,OD)]. + + Mirrors iolink_frame_encode_type0_write() in the device DLL. + """ + return bytes([mc, od, calculate_crc6(bytes([mc, od]))]) def generate_isdu_read(self, index: int, subindex: int = 0) -> list[bytes]: """ Generate ISDU Read request frames (Old Type 0 / Type 1 legacy). """ frames = [] - frames.append(self.generate_type0(0x90)) + frames.append(self.generate_type0(0xB0)) frames.append(self.generate_type0((index >> 8) & 0xFF)) frames.append(self.generate_type0(index & 0xFF)) frames.append(self.generate_type0(subindex)) return frames def generate_isdu_read_v11( - self, index: int, subindex: int = 0, service_id: int = 0x80 + self, index: int, subindex: int = 0, service_id: int = 0xB0 ) -> list[int]: """ Generate ISDU Read request BYTES (excluding M-seq framing) for V1.1.5. Interleaves Control Bytes. """ data = [ - service_id, # Read Service, Len 0 (Standard 0x80) + service_id, # I-Service READ nibble 0x0B (Table A.12) -> 0xB0, Len 0 (index >> 8) & 0xFF, index & 0xFF, subindex, From cac724ad00de91b4516102b1de977aed32559b36 Mon Sep 17 00:00:00 2001 From: w1ne <14119286+w1ne@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:38:34 +0200 Subject: [PATCH 4/4] style: clang-format the spec-conformant wire changes --- src/dll.c | 3 ++- tests/test_helpers.c | 3 ++- tests/test_isdu_stress.c | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dll.c b/src/dll.c index 17f2a70..341710c 100644 --- a/src/dll.c +++ b/src/dll.c @@ -138,7 +138,8 @@ static void dll_handle_preoperate(iolink_dll_ctx_t* ctx, uint8_t mc, uint8_t ck) 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[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); diff --git a/tests/test_helpers.c b/tests/test_helpers.c index 391a282..55480f6 100644 --- a/tests/test_helpers.c +++ b/tests/test_helpers.c @@ -313,7 +313,8 @@ 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 = 0x30 | data_len; /* I-Service WRITE nibble 0x03 (Table A.12), embedded length */ + service_byte = + 0x30 | data_len; /* I-Service WRITE nibble 0x03 (Table A.12), embedded length */ } else { service_byte = 0x3F; /* I-Service WRITE nibble 0x03, extended length */ diff --git a/tests/test_isdu_stress.c b/tests/test_isdu_stress.c index 1711c42..3adbcbd 100644 --- a/tests/test_isdu_stress.c +++ b/tests/test_isdu_stress.c @@ -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, 0x3F); /* Write (I-Service nibble 0x03), 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 */