You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的状态判断,确保格式化完成后再执行写入:

  1. 在VAR块中新增状态变量:eFormatState : (FORMAT_WAIT, FORMAT_RUNNING);
  2. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 07:18:09