TwinCAT 3写入CSV文件时意外跳行的问题排查求助
TwinCAT3 CSV写入丢失数据问题排查
问题背景
使用Beckhoff PLC在TwinCAT3环境开发试验数据记录功能:试验过程中将数据存入内存缓冲区aTrialDataBuffer,试验完成后批量写入CSV文件。目前内存缓冲区数据完整,但生成的CSV文件仅能记录约每4-6行数据中的一行。调试确认FB_FilePuts未跳过迭代,怀疑bExecute标志或状态机逻辑存在问题。
相关代码
数据记录功能块代码
FUNCTION_BLOCK CalibrationBlockLogger VAR_INPUT sFileName : STRING; END_VAR VAR_OUTPUT bBusy : BOOL := FALSE; bError : BOOL := FALSE; END_VAR VAR CONSTANT nMaxBufferSize : UDINT := 6000000; END_VAR VAR cDefaultHeader : T_MaxString := 'TimeLow, TimeHigh, EncoderDegrees, CommandVoltage, CommandTorque, GripForce, GripThreshold, SequenceState, CurrentTestIndex, TestIsLive $n'; sDefaultDirectory : T_MaxString := 'C:\Users\njanne\Documents\DDTestLogs\'; bWriteToDisk : BOOL := FALSE; eFileWriteState : (IDLE, OPEN_FILE, WAIT_FOR_OPEN, WRITE_TO_FILE, WAIT_FOR_WRITE, CLOSE_FILE, WAIT_FOR_CLOSE, ERROR); fbFileOpen : FB_FileOpen; fbFilePuts : FB_FilePuts; fbFileClose : FB_FileClose; fbFormatString : FB_FormatString2; nFileHandle : UINT; bHeaderWriteComplete : BOOL := FALSE; nWriteIndex : UDINT := 0; // For formatting data into a string. sDataFormatStr : STRING := '%d, %d, %f, %f, %f, %f, %f, %s, %d, %d $n'; sTempString : STRING(600); aTrialDataBuffer : ARRAY[0..nMaxBufferSize] OF ST_CalibTrialStep; nStepsSoFar : UDINT := 0; // Current number of steps in the database. END_VAR -------------------------------------- IF bWriteToDisk THEN // externally set to TRUE once the test is complete, and is set FALSE once the file closes in this state machine. // If it's time to write to disk, we need to empty the buffer. CASE eFileWriteState OF IDLE: // Get everything ready. nFileHandle := 0; bBusy := TRUE; eFileWriteState := OPEN_FILE; OPEN_FILE: // Opens a file for writing at the end of the file (append). // If the file does not exist, a new file is created. fbFileOpen(bExecute := FALSE); fbFileOpen( bExecute := TRUE, sNetID := '', sPathName := CONCAT(sDefaultDirectory, sFilename), nMode := FOPEN_MODEAPPEND OR FOPEN_MODETEXT); eFileWriteState := WAIT_FOR_OPEN; WAIT_FOR_OPEN: fbFileOpen(bExecute := FALSE); IF fbFileOpen.bError THEN eFileWriteState := ERROR; ELSIF NOT fbFileOpen.bBusy THEN nFileHandle := fbFileOpen.hFile; eFileWriteState := WRITE_TO_FILE; END_IF WRITE_TO_FILE: // First check if we need to write the header. IF NOT bHeaderWriteComplete THEN fbFilePuts(bExecute := FALSE); fbFilePuts( bExecute := TRUE, sNetId := '', hFile := nFileHandle, sLine := cDefaultHeader); eFileWriteState := WAIT_FOR_WRITE; bHeaderWriteComplete := TRUE; ELSE // If the header is already written, then we're going for data. // We need to first format the string. fbFormatString( pFormatString := ADR(sDataFormatStr), arg1 := F_UDINT(aTrialDataBuffer[nWriteIndex].nTimestepLow), arg2 := F_UDINT(aTrialDataBuffer[nWriteIndex].nTimeStepHigh), arg3 := F_REAL(aTrialDataBuffer[nWriteIndex].rEncoderDegrees), arg4 := F_REAL(aTrialDataBuffer[nWriteIndex].rCommandVoltage), arg5 := F_REAL(aTrialDataBuffer[nWriteIndex].rCommandTorque), arg6 := F_REAL(aTrialDataBuffer[nWriteIndex].rGripForce), arg7 := F_REAL(aTrialDataBuffer[nWriteIndex].rCurrentGripThreshold), arg8 := F_STRING(aTrialDataBuffer[nWriteIndex].sSequenceStateString), arg9 := F_INT(aTrialDataBuffer[nWriteIndex].nCurrentTestIndex), //arg10 := F_INT(aTrialDataBuffer[nWriteIndex].nTestIsLive), arg10 := F_UDINT(nWriteIndex), // TODO: DELETE pDstString := ADR(sTempString), nDstSize := SIZEOF(sTempString)); fbFilePuts(bExecute := FALSE); fbFilePuts( bExecute := TRUE, sNetId := '', hFile := nFileHandle, sLine := sTempString); // Update the next iteration if we need to write more. nWriteIndex := nWriteIndex + 1; eFileWriteState := WAIT_FOR_WRITE; END_IF WAIT_FOR_WRITE: fbFilePuts(bExecute := FALSE); IF fbFilePuts.bError THEN eFileWriteState := ERROR; ELSIF NOT fbFileClose.bBusy THEN // Success, data was written. // Check if we need to write anything more. IF nWriteIndex = nStepsSoFar THEN eFileWriteState := CLOSE_FILE; ELSE eFileWriteState := WRITE_TO_FILE; // Go back to writer. END_IF END_IF CLOSE_FILE: fbFileClose(bExecute := FALSE); fbFileClose( bExecute := TRUE, sNetId := '', hFile := nFileHandle); eFileWriteState := WAIT_FOR_CLOSE; WAIT_FOR_CLOSE: fbFileClose(bExecute := FALSE); IF fbFileClose.bError THEN eFileWriteState := ERROR; ELSIF NOT fbFileClose.bBusy THEN nFileHandle := 0; eFileWriteState := IDLE; // Also reset everything so that a new test can overwrite. nStepsSoFar := 0; nWriteIndex := 0; bHeaderWriteComplete := FALSE; bBusy := FALSE; bWriteToDisk := FALSE; END_IF ERROR: // Error, clear the handle and go back to idle. nFileHandle := 0; eFileWriteState := IDLE; bError := TRUE; END_CASE END_IF
缓冲区数据添加方法
METHOD addStep : BOOL VAR_INPUT stNewStep : ST_CalibTrialStep; // The incoming step to be added to memory END_VAR ------------------------- // We need to simply copy over all the fields into the new position. // And then, update the counter. MEMCPY( destAddr := ADR(aTrialDataBuffer[nStepsSoFar]), srcAddr := ADR(stNewStep), n := SIZEOF(ST_CalibTrialStep)); nStepsSoFar := nStepsSoFar + 1;
问题根源与修复方案
核心问题1:WAIT_FOR_WRITE状态判断错误
在WAIT_FOR_WRITE分支中,代码错误地检查fbFileClose.bBusy状态,而非当前等待的fbFilePuts.bBusy。由于fbFileClose此时处于未激活状态,bBusy始终为FALSE,导致状态机在FB_FilePuts还未完成写入时就直接跳转回WRITE_TO_FILE,nWriteIndex被快速递增,大量数据行还没来得及写入就被跳过。
修复代码:
将WAIT_FOR_WRITE中的判断逻辑修改为:
WAIT_FOR_WRITE: fbFilePuts(bExecute := FALSE); IF fbFilePuts.bError THEN eFileWriteState := ERROR; ELSIF NOT fbFilePuts.bBusy THEN // Success, data was written. // Check if we need to write anything more. IF nWriteIndex = nStepsSoFar THEN eFileWriteState := CLOSE_FILE; ELSE eFileWriteState := WRITE_TO_FILE; // Go back to writer. END_IF END_IF
核心问题2:字符串格式化未等待完成
当前代码中调用fbFormatString后直接使用sTempString,未等待格式化操作完成。TwinCAT的FB_FormatString2是异步执行的功能块,若未等待其完成就调用FB_FilePuts,可能导致写入旧的字符串数据,甚至空数据。
修复方案:
在WRITE_TO_FILE的数据写入分支中,新增对fbFormatString的状态判断,确保格式化完成后再执行写入:
- 在VAR块中新增状态变量:
eFormatState : (FORMAT_WAIT, FORMAT_RUNNING); - 修改
WRITE_TO_FILE分支的写入逻辑:
WRITE_TO_FILE: IF NOT bHeaderWriteComplete THEN // 原有头部写入逻辑不变 ELSE CASE eFormatState OF FORMAT_WAIT: fbFormatString(bExecute := TRUE, pFormatString := ADR(sDataFormatStr), arg1 := F_UDINT(aTrialDataBuffer[nWriteIndex].nTimestepLow), arg2 := F_UDINT(aTrialDataBuffer[nWriteIndex].nTimeStepHigh), arg3 := F_REAL(aTrialDataBuffer[nWriteIndex].rEncoderDegrees), arg4 := F_REAL(aTrialDataBuffer[nWriteIndex].rCommandVoltage), arg5 := F_REAL(aTrialDataBuffer[nWriteIndex].rCommandTorque), arg6 := F_REAL(aTrialDataBuffer[nWriteIndex].rGripForce), arg7 := F_REAL(aTrialDataBuffer[nWriteIndex].rCurrentGripThreshold), arg8 := F_STRING(aTrialDataBuffer[nWriteIndex].sSequenceStateString), arg9 := F_INT(aTrialDataBuffer[nWriteIndex].nCurrentTestIndex), arg10 := F_UDINT(nWriteIndex), pDstString := ADR(sTempString), nDstSize := SIZEOF(sTempString)); eFormatState := FORMAT_RUNNING; FORMAT_RUNNING: fbFormatString(bExecute := FALSE); IF NOT fbFormatString.bBusy THEN // 格式化完成,执行写入 fbFilePuts(bExecute := FALSE); fbFilePuts( bExecute := TRUE, sNetId := '', hFile := nFileHandle, sLine := sTempString); nWriteIndex := nWriteIndex + 1; eFileWriteState := WAIT_FOR_WRITE; eFormatState := FORMAT_WAIT; END_IF END_CASE END_IF
额外优化建议
- 确保
bExecute标志仅在触发功能块时置为TRUE一个周期,之后保持FALSE,避免重复触发; - 可考虑在写入完成后调用
FB_FileSync确保数据完全刷入磁盘,防止缓存导致的数据丢失; - 增加对
nWriteIndex的边界检查,避免超出nStepsSoFar范围。
内容的提问来源于stack exchange,提问作者Nick
相关产品推荐
相关产品推荐

