How to log data to CSV on CC100 SD Card

This simple example makes use of Coesys 3.5 Libraries, SysFile, SysTypes and SysTimeRtc to datalog every second to a CSV file on an SD Card. This will create a new file when the xRunning variable becomes TRUE. The format of the file used is: Recipe_Name_YYYY-MM-DD_HH-MM-SS.csv

PROGRAM Datalog
VAR
   xRunning : BOOL;
   tTimer : TON; // Timer for 1-second interval
   hFile : RTS_IEC_HANDLE; // File handle
   sFileName : STRING(255); // Dynamic file path
   bFileOpen : BOOL := FALSE; //File open status
   bWriteHeader : BOOL := TRUE; //Flag to write CSV header
   bSetFileName : BOOL := TRUE; //Flag to set filename on xRunning rising edge
   dtTimestamp : DATE_AND_TIME; //RTC timestamp
   sBuffer : STRING(1024); //Buffer for CSV line
   iResultOpen : DINT; //RTS_IEC_RESULT
   iResultWrite : DINT; //RTS_IEC_RESULT
   iResultClose : DINT; //RTS_IEC_RESULT

   // Timestamp variables
   diRtcSec : DWORD; 
   Year : STRING;
   Month : STRING;
   Day : STRING;
   Hour : STRING;
   Minute : STRING;
   Second : STRING; 
   dtStr : STRING(30);
   pos : INT;

   // Example Process Variables 
   sRecipe_Name : STRING := 'MyTest';
   wRPM : WORD := 250; 
   rDegreesF : REAL := 23.4; 
   rPressurePSI : REAL := 100.4;  

   // Install SysFile (3.5.17.0+), SysTypes (3.5.17.0+) & SysTimeRtc (3.5.17.0+) 
   // in Library Manager. 
   // Tested on CC100 FW 28 Codesys 3.5.17.0 on Oct 1 2025
END_VAR
(* Initialize timer *)
tTimer(IN := xRunning AND NOT tTimer.Q , PT := T#1S);

(* Detect rising edge of xRunning to set filename *)
IF xRunning AND bSetFileName THEN
   IF sRecipe_Name <> '' THEN 		
      sFileName := CONCAT('/media/sd/', sRecipe_Name); 	
      sFileName := CONCAT(sFileName, '_'); 
   ELSE 		
      sFileName := CONCAT('/media/sd/', 'NoRecipe_'); 
   END_IF  

   diRtcSec := SysTimeRtcGet(pResult := Result); 
   dtTimestamp := UDINT_TO_DT(diRtcSec); 
   dtStr := DT_TO_STRING(dtTimestamp);  // Convert DT to string 
   pos := FIND(dtStr, '#') + 1; // Position after 'DT#': 4 
   Year := MID(dtStr,4, pos); // 'DT#2025-10-01-16:48:00' 
   Month := MID(dtStr,2, pos + 5); // '10' 
   Day := MID(dtStr,2, pos + 8); // '01' 
   pos := FIND(dtStr, '-') + 1;// Extract (HH:MM:SS part, after the '-') 
   Hour := MID(dtStr,2, pos); // '16' 
   Minute := MID(dtStr,2, pos + 3); // '48' 
   Second := MID(dtStr,2, pos + 6); // '00'  	
   sFileName :=   CONCAT(sFileName, Year); 	
   sFileName := CONCAT(sFileName, '-'); 	
   sFileName := CONCAT(sFileName, Month); 	
   sFileName := CONCAT(sFileName, '-'); 	
   sFileName := CONCAT(sFileName, Day); 	
   sFileName := CONCAT(sFileName, '_'); 	
   sFileName := CONCAT(sFileName, Hour); 	
   sFileName := CONCAT(sFileName, '-'); 	
   sFileName := CONCAT(sFileName, Minute); 	
   sFileName := CONCAT(sFileName, '-'); 	
   sFileName := CONCAT(sFileName, Second); 	
   sFileName := CONCAT(sFileName, '.csv');  
   // Format filename: /media/sd/RecipeName_YYYY-MM-DD_HH-MM-SS.csv 	
   bSetFileName := FALSE; // Prevent resetting filename 
END_IF

(* Reset filename flag when xRunning goes FALSE *)
IF NOT xRunning THEN
   bSetFileName := TRUE;
   bWriteHeader := TRUE;
END_IF

IF xRunning AND tTimer.Q THEN

   (* Open file if not already open *)
   IF NOT bFileOpen THEN
      hFile := SysFileOpen(sFileName, 1, ADR(iResultOpen));
      IF iResultOpen = 0 THEN
         bFileOpen := TRUE;
      END_IF
   END_IF

   IF bFileOpen THEN     

      (* Write header if needed *)     
      IF bWriteHeader THEN         	
         sBuffer := 'Timestamp,RPM,Degrees,Pressure'; 	// EDIT THIS SECTION
         sBuffer := CONCAT(sBuffer, '$R$L'); // CR LF         
         SysFileWrite(hFile, ADR(sBuffer), LEN(sBuffer), ADR(iResultWrite));         
         bWriteHeader := FALSE;     
      END_IF         

      (* Format data row *)     
      sBuffer := CONCAT(DT_TO_STRING(dtTimestamp), ',');   // EDIT THIS SECTION  
      sBuffer := CONCAT(sBuffer, WORD_TO_STRING(wRPM)); 
      sBuffer := CONCAT(sBuffer, ',');     
      sBuffer := CONCAT(sBuffer, REAL_TO_STRING(rDegreesF)); 
      sBuffer := CONCAT(sBuffer, ',');     
      sBuffer := CONCAT(sBuffer, REAL_TO_STRING(rPressurePSI)); 
      sBuffer := CONCAT(sBuffer, '$R$L'); // CR LF          

      (* Write to file *)     
      SysFileWrite(hFile, ADR(sBuffer), LEN(sBuffer), ADR(iResultWrite)); 
   END_IF 
END_IF 

(* Close file when not running )
IF NOT xRunning AND bFileOpen THEN
   SysFileClose(hFile);
   bFileOpen := FALSE;
   bWriteHeader := TRUE; // Reset header flag for next file open 
END_IF 

I wanted to test this program on my PFC 200. But I get an error message on this line:

diRtcSec := SysTimeRtcGet(pResult := Result);

Apparently Result is not defined. What needs to be done?

Hi,

Not tested, but try:

add to declaration part

iResultTime : DINT; //RTS_IEC_RESULT

change to in programm part

diRtcSec := SysTimeRtcGet(pResult := adr(iResultTime));

But also check WagoAppDatalogger library. There is also an example available.

I don`t fully understand what this command is doing? I have changed according to your instructions, but got the following error:
VAR_IN_OUT- beziehungsweise REFERENCE-Parameter ‘pResult’ von ‘SysTimeRtcGet’ benötigt eine Variable mit Schreibzugriff als Eingang

OK, pResult is differently handled here.than in the sysfile function.

udiResultTime : RTS_IEC_RESULT;
diRtcSec := SysTimeRtcGet(pResult := udiResultTime);

I like a lot to use the PLC as a datalogger.

But CSV is quite heavy => 123;45.7458 means 12 octets.

Is it possible to generate directly a binary file that translate the word 123 and the real 45.7458 to 6 octets only?

First in english we don’t use octets but bytes.

Next to Word to string should only convert the available data:
“123” should become 16#31,16#32,16#33 (3 byte)
The problem come to the real value,
If you only want 3 byte you will have “45.” (In that case only 2 byte is ok) or you need a 4th byte “45.7”

You can test this function:

FUNCTION FuLreal_to_nString : STRING
(* This function convert a LREAL into a STRING.*)
VAR_INPUT
	lrValue		: LREAL;
	usiPres		: USINT := 3;
END_VAR
VAR
	_lrValue	: LREAL;
	_uliHelp	: ULINT;
	_sHelp		: STRING;
END_VAR
(* LIMIT usiPres between 0 and 7 *)
usiPres := LIMIT(0, usiPres, 7);
_lrValue := lrValue;
IF (_lrValue = 0) THEN
	_sHelp := '0';
	IF (usiPres > 0) THEN
		// _sHelp := Standard.CONCAT(_sHelp, '.');
		WagoAppString.StrAppend(sBuffer := _sHelp, udiSize := SIZEOF(_sHelp), sAppendee := '.');
		FOR _uliHelp := 1 TO usiPres BY 1 DO
			// _sHelp := Standard.CONCAT(_sHelp, '0');
			WagoAppString.StrAppend(sBuffer := _sHelp, udiSize := SIZEOF(_sHelp), sAppendee := '0');
		END_FOR;
	END_IF;
ELSE
	(* round the rVar to bPres digits *)
	_uliHelp := TO_ULINT(ABS(_lrValue) * EXP(usiPres * LN(10)));
	(* convert _uliHelp to string *)
	_sHelp := TO_STRING(_uliHelp);
	(* add missing "0"s *)
	WHILE (Standard.LEN(_sHelp) < usiPres) DO
		// _sHelp := Standard.CONCAT('0', _sHelp);
		WagoAppString.StrAppend(sBuffer := _sHelp, udiSize := SIZEOF(_sHelp), sAppendee := '0');
	END_WHILE
	(* add the separator *)
	IF (usiPres > 0) THEN
		//_sHelp := Standard.INSERT(_sHelp, '.', Standard.LEN(_sHelp) - bPres);
		WagoAppString.StrInsert(sBuffer := _sHelp, udiSize := SIZEOF(_sHelp), udiPos := TO_UDINT(Standard.LEN(_sHelp) - usiPres) + 1, sInsert := '.');
	END_IF;
	(* add leading 0 if needed *)
	IF (-1 < _lrValue) AND (_lrValue < 1) THEN
		// _sHelp := Standard.CONCAT('0', _sHelp);
		WagoAppString.StrAppend(sBuffer := _sHelp, udiSize := SIZEOF(_sHelp), sAppendee := '0');
	END_IF;
	(* add a minus sign if rVar is negative *)
	IF (_lrValue < 0) THEN
		// _sHelp := Standard.CONCAT('-', _sHelp);
		WagoAppString.StrAppend(sBuffer := _sHelp, udiSize := SIZEOF(_sHelp), sAppendee := '-');
	END_IF;
END_IF;
(* we copy the _sHelp in the output *)
FuLreal_to_nString := _sHelp;

usiPres is the number of data after the . (can be from 0: 46 to 7: 45.7458000)
The function should return the closest data possible or the full value.

Sorry for the octet :slight_smile:

Thanks for the code. I’m going to do more about this subject.