Automatic Modbus RTU scanner to detect several devices with DRV_mbus library

Hello everyone,

I am developing a program to automate the Modbus RS485 RTU network of a B&R X20CP0484-1 PLC in Automation Studio 4.12.2.93. The aim of my program is to automatically scan all Modbus RTU devices connected (with DRV_mbus library) to my system via the X20CS1030 module (ADR’IF6.ST2.IF1’).

:open_book:My level
I’m just starting out with B&R PLCs and the Automation Studio environment, so I’m gradually discovering best practices and how to structure objects/files in a project.

:hammer_and_wrench: My setup

  • Automation Studio version : 4.12.2.93
  • PLC : B&R X20CP0484-1 on X20BB57
  • Communication module : X20CS1030 (configured RS485, 19200 bps, Even parity, 1 stop bit)
    Library used : DRV_mbus

The devices, which support Modbus RTU via RS485. I’m using the datasheet from the supplier of devices. To identify the devices, I’m using the following Modbus register addresses (according to Sensirion documentation):

  • 0x4103: Modbus slave address (uint16 / 1 registers) (R/W)
  • 0x6000 to 0x6009 : Product Name (string[20] / 10 registers) (R)
  • 0x600A to 0x6013: Article Code (string[20] / 10 registers) (R)
  • 0x6014 to 0x601D: Serial Number (string[20] / 10 registers) (R)

I’ve already configured the interface of X20CS1030 :

I’ve already programmed one simple program but I have still have some issue regarding the function “ConvertRegistersToString”.

PROGRAM _INIT
    (* Initialisation au démarrage *)
	ScanState := SCAN_IDLE;
	ReadState := READ_IDLE;
	FoundMFCCount := 0;
	ScanInProgress := FALSE;
	ScanComplete := FALSE;
    
	(* Effacer les tableaux *)
	FOR i := 1 TO 20 DO
		FoundMFCs[i] := 0;
		FoundMFCDetails[i].SlaveAddress := 0;
		FoundMFCDetails[i].IsValid := FALSE;
		FoundMFCDetails[i].ProductName := '';
		FoundMFCDetails[i].SerialNumber := '';
		FoundMFCDetails[i].ArticleCode := '';
	END_FOR
    
END_PROGRAM

PROGRAM _CYCLIC
    
    (* Machine d'états principale *)
	CASE ScanState OF
        
		SCAN_IDLE:
			(* Attente du démarrage *)
			IF StartScan THEN
				StartScan := FALSE;
				ScanState := SCAN_INIT;
				ScanInProgress := TRUE;
				ScanComplete := FALSE;
				FoundMFCCount := 0;
				TotalAddressesScanned := 0;
				LastErrorCode := 0;
                
				(* Effacer les résultats précédents *)
				FOR i := 1 TO 20 DO
					FoundMFCs[i] := 0;
					FoundMFCDetails[i].SlaveAddress := 0;
					FoundMFCDetails[i].IsValid := FALSE;
				END_FOR
			END_IF
            
		SCAN_INIT:
			(* Initialisation de la communication Modbus *)
			MBMOpen_0.enable := TRUE;
			MBMOpen_0.pDevice := ADR(DeviceName);
			MBMOpen_0.pMode := ADR(Mode);
			MBMOpen_0.pConfig := 0; (* Pas de data object *)
			MBMOpen_0.timeout := ScanTimeout;
			MBMOpen_0.ascii := 0; (* Mode RTU *)
			MBMOpen_0();
            
			IF MBMOpen_0.status = 0 THEN
				(* Succès - sauvegarder l'identifiant *)
				ModbusIdent := MBMOpen_0.ident;
				CurrentAddress := ScanStartAddress;
				ScanState := SCAN_START;
			ELSIF MBMOpen_0.status <> 65535 THEN
				(* Erreur *)
				LastErrorCode := MBMOpen_0.status;
				ScanState := SCAN_ERROR;
			END_IF
            
		SCAN_START:
			(* Démarrer le scan à l'adresse courante *)
			IF CurrentAddress <= ScanEndAddress THEN
				RetryCount := 0;
				ScanState := SCAN_CHECK_ADDRESS;
                
				(* Calculer la progression *)
				ScanProgress := TO_USINT(TO_REAL(CurrentAddress - ScanStartAddress) / TO_REAL(ScanEndAddress - ScanStartAddress + 1) * 100.0);
			ELSE
				ScanState := SCAN_COMPLETE;
			END_IF
            
		SCAN_CHECK_ADDRESS:
			(* Tester la présence d'un esclave à l'adresse courante *)
			(* Lecture d'un registre de test *)
			MBMCmd_0.enable := TRUE;
			MBMCmd_0.ident := ModbusIdent;
			MBMCmd_0.mfc := 3; (* Read Holding Registers *)
			MBMCmd_0.node := CurrentAddress;
			MBMCmd_0.data := ADR(TempBuffer);
			MBMCmd_0.offset := REG_TEST;
			MBMCmd_0.len := 1;
			MBMCmd_0();
            
			(* Gérer le timeout *)
			TON_Timeout(IN := MBMCmd_0.enable, PT := T#200ms);
            
			IF MBMCmd_0.status = 0 AND NOT TON_Timeout.Q THEN
				(* Succès - MFC trouvé *)
				IF FoundMFCCount < 20 THEN
					FoundMFCCount := FoundMFCCount + 1;
					FoundMFCs[FoundMFCCount] := CurrentAddress;
					FoundMFCDetails[FoundMFCCount].SlaveAddress := CurrentAddress;
					CurrentMFCIndex := FoundMFCCount;
                    
					(* Passer à la lecture des détails *)
					ReadState := READ_PRODUCT_NAME;
					ScanState := SCAN_READ_DETAILS;
					MBMCmd_0.enable := FALSE;
					TON_Timeout(IN := FALSE);
				ELSE
					(* Tableau plein *)
					ScanState := SCAN_NEXT_ADDRESS;
				END_IF
                
			ELSIF MBMCmd_0.status <> 0 AND MBMCmd_0.status <> 65535 THEN
				(* Erreur ou pas de réponse *)
				IF MBMCmd_0.status = mbERR_NODE_TOUT OR TON_Timeout.Q THEN
					(* Timeout - pas de MFC à cette adresse *)
					MBMCmd_0.enable := FALSE;
					TON_Timeout(IN := FALSE);
					ScanState := SCAN_NEXT_ADDRESS;
				ELSIF RetryCount < MaxRetries THEN
					(* Réessayer *)
					RetryCount := RetryCount + 1;
					MBMCmd_0.enable := FALSE;
                    
					(* Petit délai avant retry *)
					TON_Delay(IN := TRUE, PT := T#50ms);
					IF TON_Delay.Q THEN
						TON_Delay(IN := FALSE);
						MBMCmd_0.enable := TRUE;
					END_IF
				ELSE
					(* Erreur après retries *)
					LastErrorCode := MBMCmd_0.status;
					LastErrorAddress := CurrentAddress;
					MBMCmd_0.enable := FALSE;
					TON_Timeout(IN := FALSE);
					ScanState := SCAN_NEXT_ADDRESS;
				END_IF
			END_IF
            
		SCAN_READ_DETAILS:
			(* Lire les détails du MFC trouvé *)
			CASE ReadState OF
                
				READ_PRODUCT_NAME:
					(* Lire le nom du produit *)
					MBMCmd_0.enable := TRUE;
					MBMCmd_0.ident := ModbusIdent;
					MBMCmd_0.mfc := 3; (* Read Holding Registers *)
					MBMCmd_0.node := CurrentAddress;
					MBMCmd_0.data := ADR(TempBuffer);
					MBMCmd_0.offset := REG_PRODUCT_NAME;
					MBMCmd_0.len := 16; (* 32 bytes = 16 registres *)
					MBMCmd_0();
                    
					IF MBMCmd_0.status = 0 THEN
						(* Convertir les données Big Endian en chaîne *)
						ConvertRegistersToString(ADR(TempBuffer), ADR(FoundMFCDetails[CurrentMFCIndex].ProductName), 16);
						MBMCmd_0.enable := FALSE;
                        
						(* Délai entre lectures *)
						TON_Delay(IN := TRUE, PT := T#50ms);
						IF TON_Delay.Q THEN
							TON_Delay(IN := FALSE);
							ReadState := READ_ARTICLE_CODE;
						END_IF
                        
					ELSIF MBMCmd_0.status <> 0 AND MBMCmd_0.status <> 65535 THEN
						(* Erreur - passer au suivant *)
						MBMCmd_0.enable := FALSE;
						ReadState := READ_ARTICLE_CODE;
					END_IF
                    
				READ_ARTICLE_CODE:
					(* Lire le code article *)
					MBMCmd_0.enable := TRUE;
					MBMCmd_0.ident := ModbusIdent;
					MBMCmd_0.mfc := 3;
					MBMCmd_0.node := CurrentAddress;
					MBMCmd_0.data := ADR(TempBuffer);
					MBMCmd_0.offset := REG_ARTICLE_CODE;
					MBMCmd_0.len := 16;
					MBMCmd_0();
                    
					IF MBMCmd_0.status = 0 THEN
						ConvertRegistersToString(ADR(TempBuffer), ADR(FoundMFCDetails[CurrentMFCIndex].ArticleCode), 16);
						MBMCmd_0.enable := FALSE;
                        
						TON_Delay(IN := TRUE, PT := T#50ms);
						IF TON_Delay.Q THEN
							TON_Delay(IN := FALSE);
							ReadState := READ_SERIAL_NUMBER;
						END_IF
                        
					ELSIF MBMCmd_0.status <> 0 AND MBMCmd_0.status <> 65535 THEN
						MBMCmd_0.enable := FALSE;
						ReadState := READ_SERIAL_NUMBER;
					END_IF
                    
				READ_SERIAL_NUMBER:
					(* Lire le numéro de série *)
					MBMCmd_0.enable := TRUE;
					MBMCmd_0.ident := ModbusIdent;
					MBMCmd_0.mfc := 3;
					MBMCmd_0.node := CurrentAddress;
					MBMCmd_0.data := ADR(TempBuffer);
					MBMCmd_0.offset := REG_SERIAL_NUMBER;
					MBMCmd_0.len := 8; (* 16 bytes = 8 registres *)
					MBMCmd_0();
                    
					IF MBMCmd_0.status = 0 THEN
						ConvertRegistersToString(ADR(TempBuffer), ADR(FoundMFCDetails[CurrentMFCIndex].SerialNumber), 8);
						FoundMFCDetails[CurrentMFCIndex].IsValid := TRUE;
						MBMCmd_0.enable := FALSE;
						ReadState := READ_COMPLETE;
                        
					ELSIF MBMCmd_0.status <> 0 AND MBMCmd_0.status <> 65535 THEN
						FoundMFCDetails[CurrentMFCIndex].IsValid := TRUE;
						MBMCmd_0.enable := FALSE;
						ReadState := READ_COMPLETE;
					END_IF
                    
				READ_COMPLETE:
					(* Lecture terminée pour ce MFC *)
					ReadState := READ_IDLE;
					ScanState := SCAN_NEXT_ADDRESS;
                    
			END_CASE
            
		SCAN_NEXT_ADDRESS:
			(* Passer à l'adresse suivante *)
			CurrentAddress := CurrentAddress + 1;
			TotalAddressesScanned := TotalAddressesScanned + 1;
            
			(* Vérifier si on doit arrêter *)
			IF StopScan THEN
				StopScan := FALSE;
				ScanState := SCAN_COMPLETE;
			ELSE
				(* Délai entre adresses *)
				TON_Delay(IN := TRUE, PT := T#100ms);
				IF TON_Delay.Q THEN
					TON_Delay(IN := FALSE);
					ScanState := SCAN_START;
				END_IF
			END_IF
            
		SCAN_COMPLETE:
			(* Scan terminé *)
			ScanInProgress := FALSE;
			ScanComplete := TRUE;
			ScanProgress := 100;
            
			(* Fermer la communication Modbus *)
			MBMClose_0.enable := TRUE;
			MBMClose_0.ident := ModbusIdent;
			MBMClose_0();
            
			IF MBMClose_0.status = 0 THEN
				MBMClose_0.enable := FALSE;
				ScanState := SCAN_IDLE;
			END_IF
            
		SCAN_ERROR:
			(* Gestion d'erreur *)
			ScanInProgress := FALSE;
            
			(* Essayer de fermer la communication *)
			IF ModbusIdent <> 0 THEN
				MBMClose_0.enable := TRUE;
				MBMClose_0.ident := ModbusIdent;
				MBMClose_0();
                
				IF MBMClose_0.status = 0 THEN
					MBMClose_0.enable := FALSE;
					ModbusIdent := 0;
					ScanState := SCAN_IDLE;
				END_IF
			ELSE
				ScanState := SCAN_IDLE;
			END_IF
            
	END_CASE
    
END_PROGRAM

PROGRAM _EXIT
    (* Fermeture propre *)
	IF ModbusIdent <> 0 THEN
		MBMClose_0.enable := TRUE;
		MBMClose_0.ident := ModbusIdent;
		MBMClose_0();
	END_IF
    
END_PROGRAM

(* Fonction de conversion Big Endian vers chaîne *)
FUNCTION ConvertRegistersToString
	VAR_INPUT
	pRegisters : POINTER TO UINT;
	pString : POINTER TO STRING;
	NumRegisters : UINT;
		END_VAR
		VAR
	i, j : UINT;
	pBytes : POINTER TO USINT;
	pDest : POINTER TO USINT;
	TempByte : USINT;
		END_VAR
    
		pBytes := pRegisters;
		pDest := pString;
    
		(* Convertir chaque registre (Big Endian) *)
		FOR i := 0 TO (NumRegisters * 2 - 1) BY 2 DO
			(* Swap bytes pour chaque registre *)
			TempByte := pBytes[i];
			pDest[i] := pBytes[i + 1];
			pDest[i + 1] := TempByte;
		END_FOR
    
		(* S'assurer que la chaîne se termine par NULL *)
		pDest[NumRegisters * 2] := 0;
    
END_FUNCTION

I don’t know why it doesn’t work. Someone can help me to finish this program, please.

Hello!

Are you getting any errors from the function blocks? You can check by opening a watch widow and looking at the status variable.

I would start by disabling the mobus communications at the bottom of the config below. That is only for ACOPOSInverters. The rest of your config should be okay as long as it matches your slave device.

Did you setup a data module with all your reads and writes? I typically create a blank “dummy” data module for this and then use MBMCmd() to read/write entries from an initialized array of a struct containing the node, data, and address.

Regardless of whether you are using the data module, you still have to point to at least a blank one, otherwise you’ll get an error.

I write all my code in ANSI C, but below is an example of how I setup the MBMOpen FUB-