CODESYS communicatioins with IO-Link master 765-45x

We are using the 765-4501 as an IO-Link master, connected by EtherNet/IP to a PFC200 (750-8217). Logic is programmed in CODESYS (V3.5 SP19 Patch 7). We know the set of IO-Link slave devices that will be connected to the IO-Link master, but not how many of each type of device will be connected or which ports each will use. We have been unable to find a CODESYS library to facilitate communications between the PFC and the 765-4501 (unlike e.g. the 750-657) and consequently have two questions:

  1. How can we identify IO-Link device details for each connected port?
  2. How can we write to selected settings of IO-Link devices for each connected port?

Hi Justin,

You can do this with ISDU read/writes via an EthernetIP Explicit Message Function block.

I am working on a function block that can configure the ports of a WAGO 765-1703 DIO Hub (IOL Device). Here is what I have so far in this FB:

  1. Check that the EIP Connection is OK

  2. Check that the device that is connected to the specified port is a “765-1703”. In this case I perform an ISDU read for Index 19.

  3. I can then read and write to Index 64 (16#40) to set the each port as an input or output.
    image

You could all adapt this FB to read Index 19 for each port to get data about what device is connected. I believe that this is an IO-Link standard Index, but would need to check the manual of you IOL devices.

For this case, this is what the instance of my function block looks like. I pass it the EIP device name, a port number and can trigger reads/writes to the DIO Hub.

I added the development logic to a GitHub link for now. Its not finalized but should get you started.

:GitHub - mpsaltis/dev_EIP_IOLData_FB: This is a development project to read IOL data from a WAGO 765-4501 master connected to a 765-1703 DIO Hub.

I also forgot to mention that you can use the Web Based Management of the 765-4501 to do ISDU R/Ws. This was handy when testing.

Hi Mike, This is really useful. I didn’t respond immediately, as I have been focussed on putting together a broadly similar routine. Like you, I started with the EtherNetIP Services library and provide my current working here in case it might be of some use:

FUNCTION_BLOCK FB_IOL_Index_Comms
VAR_INPUT
	x_trigger :			BOOL;
	x_execute :			BOOL;
	port_number : 		BYTE;
	io_index : 			WORD;
	io_subindex : 		BYTE;
	io_length : 		WORD;
	read_flag : 		BOOL;
	t_timeout : 		TIME := T#500S;
	t_timegap :			TIME := T#0MS;
END_VAR
VAR_OUTPUT
	x_status : 			EVAL_STATUS;
	s_state : 			STRING;
	s_iol_call_data :	STRING(257);
END_VAR
VAR_IN_OUT
	data : 				ARRAY[0..255] OF BYTE;
END_VAR
VAR
	cip_class :			ENIP.CIPClass := 16#83;		//ISDU service of the IO-Field device
	cip_attribute :		WORD := 0; 					//to avoid crash for unknown reason

	gs_done : 			BOOL;
	gs_busy : 			BOOL;
	gs_error : 			BOOL;

	state :				INT;
	s_state_m1 : 		STRING;
	a_state :			ARRAY[0..log_size] OF STRING;
	ii :				INT;
	generic_service : 	ENIP.Generic_Service;
	usint_bytes : 		USINT_TO_BYTES;
	bytes_dint : 		BYTES_TO_DINT;
	cip_service :		ENIP.CIPCommonService;
	cip_instance :		DWORD;
	rec_data_size :		UDINT;
	write_data :		ARRAY[0..255] OF BYTE;
	
	o_timeout :			TON;
	o_timegap :			TON;
	flag_gap :			BOOL;
END_VAR
VAR CONSTANT
	log_size :			INT := 20;
END_VAR

IF x_trigger AND (o_timegap.Q OR x_status = EVAL_STATUS.rest) THEN

	//initialise
	flag_gap := FALSE;
	o_timegap(IN := flag_gap, PT := t_timegap);
	x_status := EVAL_STATUS.busy;
	generic_service(
		xDone => gs_done,
		xBusy => gs_busy,
		xError => gs_error,
		udiReceivedDataSize => rec_data_size,
		itfEtherNetIPDevice := IO_Link_Master,
		dwInstance := cip_instance,
		eClass := cip_class,
		wAttribute := cip_attribute
	);
	
	CASE state OF
		//STATE OF MACHINE
		
		0 :	//initialisation
			state := 1;
			s_state := 'IOL-CALL - initialised';
			
		1 : //wait for execute
			IF x_execute THEN
				(* ERROR CHECKING *)
				IF port_number < 0 OR port_number > 8 THEN
					state := 999;
					s_state := 'IOL-CALL - port error';
				ELSE
					cip_instance := port_number;
					state := 10;
					s_state := 'IOL-CALL - initialised';
				END_IF
			ELSE
				s_state := 'IOL-CALL - waiting for execution command';
			END_IF
			
		10 : //IOL-CALL to read or write
			IF read_flag THEN
				state := 20;
				s_state := 'IOL-CALL - read selected';
			ELSE
				state := 30;
				s_state := 'IOL-CALL - write selected';
			END_IF
			
		20 : //IOL-CALL to read
			cip_service := 75;											//75 (0x4B) ISDU read service
			generic_service.eService := cip_service;
			generic_service.pReadData := ADR(data);						//external buffer
			generic_service.udiReadDataSize := SIZEOF(data);			//external buffer size

			//set indices			
			write_data[0] := TO_BYTE(io_index);							//index
			write_data[1] := TO_BYTE(SHR(io_index,8));
			write_data[2] := io_subindex;								//subindex
			generic_service.pWriteData := ADR(write_data);				//internal buffer
			generic_service.udiWriteDataSize := 3;						//internal buffer size
			
			generic_service.xExecute := TRUE;							//start read
			state := 21;
			s_state := 'IOL-CALL - read start';
			
		21 : //wait for read response
			IF gs_busy AND o_timeout.Q THEN
				state := 999;
				s_state := 'IOL-CALL - read timeout';
			ELSIF gs_error THEN
				state := 999;
				s_state := 'IOL-CALL - read error';
			ELSIF gs_done THEN
				state := 40;
				s_state := 'IOL-CALL - read done';
			END_IF
			
		30 : //IOL-CALL to write
			cip_service := 76;											//76 (0x4C) ISDU write service
			generic_service.pReadData := ADR(data);						//external buffer
			generic_service.udiReadDataSize := SIZEOF(data);			//external buffer size
			
			//set indices			
			write_data[0] := TO_BYTE(io_index);							//index
			write_data[1] := TO_BYTE(SHR(io_index,8));
			write_data[2] := io_subindex;								//subindex

			//append write data to internal buffer
			WagoSysPlainMem.MemCopySecure(	
				pDest := ADR(write_data)+3, 							//copy to internal buffer 
				udiDestSize := SIZEOF(write_data)-3, 
				pSource := ADR(data), 									//copy source ist the external buffer
				udisourceSize := io_length, 							//length to copy
				bPadding :=0);
			generic_service.pWriteData := ADR(write_data);				//internal buffer pointer
			generic_service.udiWriteDataSize := io_length + 3;			//internal buffer size
			
			generic_service.xExecute := TRUE;							//start write
			state := 31;
			s_state := 'IOL-CALL - write start';
			
		31 : //wait for write response
			IF gs_busy AND o_timeout.Q THEN
				state := 999;
				s_state := 'IOL-CALL - write timeout';
			ELSIF gs_error THEN
				state := 999;
				s_state := 'IOL-CALL - write error';
			ELSIF gs_done THEN
				state := 40;
				s_state := 'IOL-CALL - write done';
			END_IF
			
		40 : //copy data
			WagoSysPlainMem.MemCopySecure(	
				pDest := ADR(s_iol_call_data), 
				udiDestSize := SIZEOF(s_iol_call_data), 
				pSource := ADR(data), 
				udisourceSize := rec_data_size, 
				bPadding :=0
			);					
			state := 41;
			s_state := 'IOL-CAll - processed response';
		
		41 : //done
			Reset( m_status := EVAL_STATUS.done, m_state := 'IOL-CALL - done');
		
		999 : //error
			Reset( m_status := EVAL_STATUS.error, m_state := WagoAppString.Concat3('error = ','', TO_STRING(generic_service.eError)));
			
		ELSE
			Reset(m_status := EVAL_STATUS.busy, m_state := 'IOL-CALL - state not recognised');
	END_CASE

	//state machine log
	IF s_state <> s_state_m1 THEN
		FOR ii := log_size TO 0 BY -1 DO
			IF ii > 0 THEN
				a_state[ii] := a_state[ii-1];
			ELSE
				a_state[ii] := s_state;
			END_IF
		END_FOR
	END_IF
	s_state_m1 := s_state;

	//timeout TON
	o_timeout(IN := (x_status=EVAL_STATUS.busy), PT := t_timeout);
ELSE
	IF x_status <> EVAL_STATUS.busy THEN
		// OTHERWISE ASSUME TIME GAP TO AVOID COMMS CONFLICT
		state := 0;
		x_status := EVAL_STATUS.rest;
		x_execute := FALSE;
		generic_service.xExecute := FALSE;
	END_IF
END_IF
flag_gap := TRUE;
o_timegap(IN := flag_gap, PT := t_timegap);

/////////////////////////////////////////////////////

METHOD PRIVATE reset
(* METHOD TO RESET THE FB TO AN INITIALISED STATE *) 
VAR_INPUT
	m_status : 		EVAL_STATUS;
	m_state :		STRING;
END_VAR

x_status := m_status;
s_state := m_state;
x_execute := FALSE;
generic_service.xExecute := FALSE;
state := 1;


Hi again, Your example is brilliant - many thanks for sharing. I have worked it into my project, but have a quick follow-up question: how can I check if a port is active? Given documentation for 765-4501, I thought that I could access this information from the event log (Object 65, 0x41), attribute 2 (current state; 1=stopped, 2=empty, 3=present, 4=full/overwrite, 5=full/stop). The parameters that I set for this query are:

	EIP_read_data_size := 64;							// 	Size of receive buffer.  Can't leave this at 0, or no data will be read
	EIP_eService := 16#0E;								//	Get Attribute Single
	EIP_class := 16#41;									//	Object 65 (0x41) - IO-Link Event Log
	EIP_instance := port_number;						//	This is the physical port number (1-8) of the 765-450X IO-Link Master
	EIP_attribute := 2;									//	2 returns current state of instance (1=stopped, 2=empty, 3=present, 4=full/overwrite, 5=full/stop)
	EIP_trigger := TRUE;								//	Setting this variable TRUE will execute the EIP function block

Unfortunately, this didn’t work as I had hoped. Grateful for any additional guidance.

Great, thanks for sharing your code here!

You might be able to use the PQI data from the IO-Link master. This is part of the cyclic process image for the master.

Yes, managed to work out integration with the PQI data today. Revised code that integrates this functionality providerd here:

FUNCTION_BLOCK PUBLIC FB_IOL_Comms
(*	Authors		:	MWP, JV
	Last Edit	:	30 May 2025 (JV)	

	This FB manages communications with IO-Link devices via an IO-Link master
*)
VAR_INPUT
	port_number			: DWORD := 1;					// IO-Link master physical port number (e.g. 1-8)
	io_index  			: WORD := 19;					// Index (default to product id)
	io_subindex  		: BYTE := 0;
	data_write			: ARRAY[0..60] OF BYTE;			// Data to write to IO-Link slave
	io_write_length		: WORD;							// Byte length of data to write to slave
	t_timeout			: TIME := T#100S;
END_VAR
VAR_IN_OUT
	x_read_cmd			: BOOL;
	x_write_cmd			: BOOL;
	x_port_query		: BOOL;
END_VAR
VAR_OUTPUT
	x_done				: BOOL;
	x_busy 				: BOOL;
	x_error				: BOOL;
	s_error 			: STRING;
	data_read			: ARRAY[0..63] OF BYTE;
	io_read_length		: UDINT;
END_VAR
VAR
	EIP					: ENIP.Generic_Service;
	EIP_trigger			: BOOL;
	EIP_done			: BOOL;
	EIP_busy			: BOOL;
	EIP_err				: BOOL;
	
	EIP_class			: ENIP.CIPClass;
	EIP_instance		: DWORD;
	EIP_attribute		: WORD;
	EIP_eService		: ENIP.CIPCommonService;
	EIP_eError			: ENIP.ERROR;
	
	EIP_write_data		: ARRAY[0..63] OF BYTE;		// Write buffer 
	EIP_write_data_size	: UDINT;
	EIP_read_data		: ARRAY[0..63] OF BYTE;		// Read buffer
	EIP_read_data_size	: UDINT;
	EIP_rcvd_size		: UDINT;

	x_read				: BOOL;
	x_write				: BOOL;
	x_query				: BOOL;
	
	i_step				: INT := 0;

	tonWatchDog			: TON;

	(*========================================================
		
		LIST OF RELATED VARIABLES DESCRIBED BY DEFINITION OF IO-LINK MASTER
		
	========================================================
		EIP_DI_status:	DIGITAL INPUT STATUS
		Reference: WAGO I/O System Field documentation, Table 19 
		BYTES: 1
		Each bit representing one port 0 = port is not a digital input; 1 = port is a digital input
	========================================================
		EIP_DI_data: DIGITAL INPUT DATA
		Reference: WAGO I/O System Field documentation, Tables 20 and 21
		BYTES: 2
		Not sure about use of these data
	========================================================
		EIP_x1_pqi: PORT QUALIFIER INFORMATION
		Reference: WAGO I/O System Field documentation, Table 37 of documentation
		BYTES: 1
		Bit 0: reserved
			1: reserved
			2: reserved
			3: event: 		0 = port has no IO-Link event; 	1 = port has IO-Link event
			4: reserved
			5: DevCom: 		0 = no IO-Link device present; 	1 = IO-Link device present
			6: DevErr: 		0 = no error/warning; 			1 = error/warning
			7: PQ:			0 = port qualfier data invalid;	1 = port qualifier data valid
	========================================================
		EIP_DO_status: DIGITAL OUTPUT STATUS
		Reference: WAGO I/O System Field documentation, Table 28 of documentation
		BYTES: 1
		Each bit representing one port 0 = port is not a digital output; 1 = port is a digital output
	========================================================
		EIP_DO_data: DIGITAL OUTPUT DATA
		Reference: WAGO I/O System Field documentation, Table 28 of documentation
		BYTES: 2
		Not sure about use of these data
	========================================================
		EIP_x1_enable_out: DIGITAL OUTPUT DATA
		Reference: WAGO I/O System Field documentation, Table 28 of documentation
		BYTES: 1
		Bit 0: 		0 = disable; 1 = enable
		Bit 1-7:	reserved
	========================================================*)
END_VAR




(* =======================================*)
(* ===== [ Set Function Block Busy ] =====*)
(* =======================================*)

IF i_step > 0 THEN
	x_busy := TRUE;
ELSE
	x_busy := FALSE;
END_IF


(* =============================*)
(* ===== [ State Machine ] =====*)
(* =============================*)

CASE i_step OF
	
	0 : // Idle	
		IF x_read_cmd OR x_write_cmd OR x_port_query THEN 
			i_step 	:= 5;
			s_error := 'None';
		END_IF
	
	5 : // PLC will begin executing before the EIP connection is established.	
		// Check for an EIP connection before trying to access to the IO-Link device.
		IF IO_Link_Master.eState <> IoDrvEthernetIp.AdapterState.RUNNING THEN
			i_step 	:= 999;
			x_error := TRUE;
			s_error := 'EIP Master Not Running';
		ELSIF NOT FN_Port_Active(port_number) THEN
			i_step 	:= 999;
			x_error := TRUE;
			s_error := 'EIP Port not active';
		ELSIF port_number > GVL.max_iol_ports THEN
			i_step 	:= 999;
			x_error := TRUE;
			s_error := 'Requested IOL port number exceeds project maximum';
		ELSE 
			i_step := 10;			
		END_IF

	10 : // determine if read or write call
		IF x_write_cmd THEN
			i_step := 100;		// Steps 100 to 199 write to device
		ELSIF x_read_cmd THEN
			i_step := 200;		// Steps 200 to 299 read from device
		ELSIF x_port_query THEN
			i_step := 300;		// Steps 300 to 399 read from device
		END_IF
			

	(* ===== [ Write to device ] =====*)

	100 : // initialise write 
		x_write := TRUE;
		i_step 	:= 110;

	110 : // check if complete
		IF NOT x_write AND EIP_done THEN
			x_done := TRUE;
			i_step := 999;
		END_IF
	

	(* ===== [ Read from device ] =====*)

	200 : // initialise read
		x_read := TRUE;
		i_step := 210;
		
	210 : // check if complete
		IF NOT x_read AND EIP_done THEN
			x_done 			:= TRUE;
			data_read 		:= EIP_read_data;
			io_read_length 	:= EIP_read_data_size;
			i_step 			:= 999;
		END_IF
	

	(* ===== [ Port query ] =====*)

	300 : // initialise query
		//x_query := TRUE;
		//i_step := 310;
		i_step 	:= 999;
		x_error := TRUE;
		s_error := 'Port query not currently working';
		
	310 : // check if complete
		IF NOT x_query AND EIP_done THEN
			x_done 			:= TRUE;
			data_read 		:= EIP_read_data;
			io_read_length 	:= EIP_read_data_size;
			i_step 			:= 999;
		END_IF


	(* ===== [ Step 999 resets the fuction block] =====*)
	
	999 : // Reset
		x_done 		:= FALSE;
		x_error 	:= FALSE;
		s_error 	:= '';
		x_busy 		:= FALSE;
		x_read_cmd 	:= FALSE;
		x_read 		:= FALSE;
		x_write_cmd := FALSE;
		x_write 	:= FALSE;
		EIP_trigger := FALSE;
		i_step 		:= 0;
END_CASE


(* ============================================*)
(* ===== [ Process EIP service requests ] =====*)
(* ============================================*)

// if x_query TRUE, then initialise query of the service
IF x_query THEN
	
	EIP_read_data_size 	:= 64;					// 	Size of receive buffer.  Can't leave this at 0, or no data will be read
	EIP_eService 		:= 16#0E;				//	Get Attribute Single
	EIP_class 			:= 16#41;				//	Object 65 (0x41) - IO-Link Event Log
	EIP_instance 		:= port_number;			//	This is the physical port number (1-8) of the 765-450X IO-Link Master
	EIP_attribute 		:= 2;					//	2 returns current state of instance (1=stopped, 2=empty, 3=present, 4=full/overwrite, 5=full/stop)
	EIP_trigger 		:= TRUE;				//	Setting this variable TRUE will execute the EIP function block
	x_query 			:= FALSE;
END_IF

// if x_read TRUE, then initialise read parameters and execute the read service
IF x_read THEN
	
	EIP_write_data[0] 		:= TO_BYTE(io_index);  			//	Index (LSB)
	EIP_write_data[1] 		:= TO_BYTE(SHR(io_index,8));	//	Index (MSB)
	EIP_write_data[2] 		:= io_subindex;					//	Subindex
	EIP_write_data_size 	:= 3;							//	3 bytes for index/subindex
	EIP_read_data_size 		:= 64;							// 	Size of receive buffer.  Can't leave this at 0, or no data will be read
	EIP_eService 			:= 16#4B;						//	Read is 0x4B, Write is 0x4C
	EIP_class 				:= 16#83;						//	Object 131 (0x83) - IO-Link Device Parameters
	EIP_instance 			:= port_number;					//	This is the physical port number (1-8) of the 765-450X IO-Link Master
	EIP_attribute 			:= 0;							//	Always 0
	EIP_trigger 			:= TRUE;						//	Setting this variable TRUE will execute the EIP function block
	x_read 					:= FALSE;
END_IF

// if x_write TRUE, then initialise write parameters and execute the write service
IF x_write THEN
	
	EIP_write_data[0] := TO_BYTE(io_index);  			// Index (LSB)	
	EIP_write_data[1] := TO_BYTE(SHR(io_index,8));		// Index (MSB)	
	EIP_write_data[2] := io_subindex;					// Subindex
	WagoSysPlainMem.MemCopySecure(						// copy write data to internal buffer	
		pDest 			:= ADR(EIP_write_data)+3, 
		udiDestSize 	:= SIZEOF(EIP_write_data)-3, 		
		pSource 		:= ADR(data_write),
		udisourceSize 	:= io_write_length,
		bPadding 		:=0
	);
	EIP_write_data_size 	:= 3 + io_write_length;		// 3 bytes for index/subindex, plus byte length of data to write
	EIP_read_data_size 		:= 0;						// Can be set to 0 for write service
	EIP_eService 			:= 16#4C;					// Read is 0x4B, Write is 0x4C
	EIP_class 				:= 16#83;					// Object 131 (0x83) - IO-Link Device Parameters
	EIP_instance 			:= port_number;				// This is the physical port number (1-8) of the 765-450X IO-Link Master
	EIP_attribute 			:= 0;						// Always 0
	EIP_trigger 			:= TRUE;					// Setting this variable TRUE will execute the EIP function block
	x_write 				:= FALSE;
END_IF

//	Instance of ENIP.Generic_Service function block
//	This function block performs a generic service at an EtherNet/IP Adapter
//	The message will be sent as an unconnected explicit message request
EIP(
	xExecute				:= EIP_trigger,
	xDone					=> EIP_done, 
	xBusy					=> EIP_busy, 
	xError					=> EIP_err, 
	itfEtherNetIPDevice		:= IO_Link_Master, 
	eClass					:= EIP_class, 
	dwInstance				:= EIP_instance, 
	wAttribute				:= EIP_attribute,  
	eError					=> EIP_eError, 
	eService				:= EIP_eService, 
	pWriteData				:= ADR(EIP_write_data), 
	udiWriteDataSize		:= EIP_write_data_size, 
	pReadData				:= ADR(EIP_read_data), 
	udiReadDataSize			:= EIP_read_data_size, 
	udiReceivedDataSize		=> EIP_rcvd_size
);


(* =============================================*)
(* ===== [ Process EIP service responses ] =====*)
(* =============================================*)

IF EIP_done THEN 
	EIP_trigger := FALSE;	 
END_IF

IF EIP_err THEN
	x_error := TRUE;
	s_error := WagoAppString.Concat3('EIP error = ','', TO_STRING(EIP_eError));
	i_step 	:= 999;
END_IF


(* ==============================*)
(* ===== [ Watchdog Timer ] =====*)
(* ==============================*)

tonWatchDog( IN := i_step <> 0, PT := t_timeout);
IF tonWatchdog.Q THEN
	x_error := TRUE;
	s_error := 'Timeout';
	i_step 	:= 999;
END_IF


(* ==============================*)
(* ===== [ FN_Port_active ] =====*)
(* ==============================*)
FUNCTION FN_Port_Active : BOOL
VAR_INPUT
	port_number : DWORD := 1;
END_VAR
VAR
	byte_value		: BYTE;
	byte_bit		: BYTE_AS_BIT;
END_VAR
byte_value := FN_Port_PQI(port_number);
byte_bit(B:=byte_value, B5=>FN_Port_Active);



(* ===========================*)
(* ===== [ FN_Port_PQI ] =====*)
(* ===========================*)
FUNCTION FN_Port_PQI : Byte
VAR_INPUT
	port_number : DWORD := 1;
END_VAR
VAR
END_VAR

IF port_number = 1 THEN
	FN_Port_PQI := EIP_x1_pqi;
ELSIF port_number = 2 THEN
	FN_Port_PQI := EIP_x2_pqi;
ELSIF port_number = 3 THEN
	FN_Port_PQI := EIP_x3_pqi;
ELSIF port_number = 4 THEN
	FN_Port_PQI := EIP_x4_pqi;
ELSIF port_number = 5 THEN
	FN_Port_PQI := EIP_x5_pqi;
ELSIF port_number = 6 THEN
	FN_Port_PQI := EIP_x6_pqi;
ELSIF port_number = 7 THEN
	FN_Port_PQI := EIP_x7_pqi;
ELSIF port_number = 8 THEN
	FN_Port_PQI := EIP_x8_pqi;
END_IF

1 Like