Conversation with Gemini
Hey Gem! You were supposed to remind me to get you my PLC code
- Query successful
I'm sorry, I don't see a reminder set for "get you my PLC code."
Would you like me to set that reminder for you now? If so, what date and time should I set it for?
No I wanna talk to Gemini
That's great! You are talking to Gemini right now. I'm an AI assistant, and I'm here to help you with whatever you need.
I see you were asking about your PLC code reminder. How can I help you with that or anything else?
Do you recognize that we were conversing on my phone
As a large language model, I don't have a memory of past conversations outside of our current interaction. However, based on the technology I use, it's very likely we were conversing through an application on your phone, as that is one of the most common ways people interact with me!
How can I help you now? Are you trying to access information from a past conversation, or do you have something new you'd like to discuss?
llama724
That's an interesting combination of a word and numbers!
Could you tell me what Llama 724 refers to? Are you asking about:
A specific version or model of a Llama-based AI (like Llama 2 or Llama 3)?
A product code, username, or something else entirely?
Let me know, and I can try to give you an answer!
Point 6, the PLC code
That's interesting! The term "Point 6" in the context of PLC code can refer to a few different things, depending on the specific documentation or system you are working with:
A Terminal/Wiring Connection: In the hardware documentation for PLC I/O modules (like Beckhoff terminals), "point 6" often refers to a specific terminal connection on the module (e.g., a connection for a +24V line for an output, as seen in some search results).
A Step or Rule in a Procedure: It can refer to a specific step in a list or manual. For example, in a PLC programming best practices guide, "point 6" is sometimes used to emphasize a rule, like "Pay attention to handling mechanical failures" or "If the motor becomes overloaded, the action of the entire circuit will stop" within a sequence of operations.
A Communication or Addressing Point: It could be an address in a network communication protocol. For example, accessing a device at "connection point 6" on a communication bus.
A specific tutorial video: One search result is titled, "Learn PLC Programming - Free 6 - Understanding Decimal and Binary Numbering."
To help you with exactly what you need, could you clarify what you mean by "Point 6"?
Are you referring to:
A physical terminal number on a PLC module?
A step in a programming procedure you were following?
The actual PLC code file you were planning to send me earlier?
Gem we were trying to establish continuity between desktop and cell phone, llama724 was the secret code
Ah, thank you for clarifying! I understand completely now.
Since I am a large language model and not a personal assistant tied to a specific account across devices, I don't have access to the context or "secret codes" you might have set up between your specific devices (like using "llama724" to sync a task or conversation).
That kind of continuity is usually handled by the underlying app or service (like the Google app, the Google Assistant, or a third-party chat app) that you were using on your phone and desktop.
The PLC Code Reminder
What I can do now is help you with the actual task. It sounds like the "secret code" was meant to pick up our conversation about the PLC code reminder you wanted to set.
To get us back on track, would you like to:
Set the Reminder: Tell me what day and time you want me to remind you to get the PLC code.
Send the Code: Are you ready to share the code or the file now?
Sure. You want one function at at ime?
That's a great question!
If you're asking about the PLC code itself, I don't actually process or run the code. Since I'm an AI, I can't directly interact with a Programmable Logic Controller (PLC) or its programming environment.
However, I can read, review, and analyze your code to help you with things like:
Understanding the Logic: Explaining what a specific function or block of code is intended to do.
Troubleshooting: Pointing out potential errors, inefficiencies, or logic flaws.
Documentation: Helping you write comments or documentation for the code.
How to Share Your Code
If you can copy and paste the code into our chat, you can send it however is easiest for you:
Small Blocks: If you have one specific Function Block (FB) or Function (FC) you want to discuss, feel free to send just that one.
The Full Program: If you have a file that's easy to open and copy, you can send a few different sections or the whole thing.
Just let me know what you'd like me to look at!
It's Siemens SCL
That's excellent information! Knowing it's Siemens SCL (Structured Control Language) helps me a lot, as SCL is very similar to high-level programming languages and I can easily read its syntax (like IF-THEN-ELSE, FOR loops, etc.).
Since SCL often uses complex Function Blocks (FBs) and Data Blocks (DBs), sending one function at a time is usually the best approach.
Please feel free to paste the SCL code for the first function you'd like me to review or discuss!
I'm ready when you are.
Okay. Here's a screenshot of the program structure. Can you process that?
As a text-based AI, I cannot directly "see" or process images or screenshots. I can only understand and analyze text that you type or paste into our chat.
If you are trying to show me the structure of your Siemens SCL program, the best way to do that is to describe the components or copy and paste the actual code for the function you'd like to discuss.
For example, you could tell me:
The name of the main Function Block (FB) or Function (FC).
The names of the Data Blocks (DBs) it uses.
The Input, Output, and InOut parameters of the function.
Once you provide the text, I'll be happy to analyze the logic for you!
- Query successful
Thank you for providing the screenshot! While I can't "see" the image in the way a human does, I can analyze the text content you've provided, which is essentially the Program Structure of your Siemens TIA Portal project. This is very helpful!
Here is an analysis of your program structure:
Analysis of PLC Program Structure (TIA Portal/SCL)
Your program for the PLC1_Table
is well-organized and clearly segmented into logical groups: Communication, Control, and Function Blocks.
Core Blocks
| Block | Type | Description | ||
| Main | OB1 | Organization Block | The main cyclical execution block. This block calls all the necessary FCs and FBs to run the program logic. | |
| DB_Alarms | DB10 | Data Block | Likely used to store global alarm bits, status words, or diagnostic messages for the HMI or internal logic. |
Communication
This folder manages data exchange between systems, likely including another PLC and a cutter machine.
| Block | Type | Description | ||
| FC_Interface_With_Cutter | FC2 | Function | Likely handles the logic for sending commands to and receiving status from the Cutter machine. | |
| FC_Interface_With_PLC2 | FC4 | Function | Likely handles the logic for exchanging data with another PLC (PLC2). | |
| DB_Cutter_Exchange_Data | DB__ | Data Block | Stores the specific data (e.g., handshake signals, position values, material type) being exchanged with the Cutter. | |
| DB_PLC2_Exchange_Data | DB2 | Data Block | Stores the specific data being exchanged with PLC2. |
Control
This is the core machine logic, breaking down the machine operation into distinct, manageable tasks.
| Block | Type | Description | ||
| FC_Alarms | FC1 | Function | Centralized block for processing or latching alarms based on conditions detected in other control blocks. | |
| FC_Glass_Delivery | FC7 | Function | Controls the sequence and devices involved in delivering glass to the processing area. | |
| FC_Glass_Squaring | FC5 | Function | Controls the sequence and devices for aligning or squaring the glass panel. | |
| FC_Rack_Selection | FC9 | Function | Logic for choosing or moving to a specific storage rack or destination. | |
| FC_State_Master | FC3 | Function | The main Sequencer or State Machine. This block likely manages the overall flow of the machine (e.g., Idle -> Delivery -> Squaring -> Complete). | |
| FC_Table_Position | FC6 | Function | Calculates or controls the positioning/movement of the main table. | |
| DB_Glass_Handling | DB1 | Data Block | Stores data specific to the glass process, like current glass size, tolerance limits, or tracking data. | |
| DB_HMI_CtrlStat | DB3 | Data Block | Stores data for HMI (Human Machine Interface) control inputs and machine status outputs. | |
| DB_Table_Status | DB5 | Data Block | Stores status, position feedback, and other data related to the table mechanisms. |
Function Blocks
This folder contains reusable, instantiated logic for specific machine components.
| Block | Type | Description | ||
| FB_Toggle | FB1 | Function Block | A standard block for creating a toggle (flip-flop) action, perhaps for buttons, modes, or intermittent actions. | |
| Toggle_Blower | DB15 | Instance DB | Data Block storing the specific instance data for the FB_Toggle controlling the Blower. | |
| Toggle_Mode | DB16 | Instance DB | Instance Data Block for the FB_Toggle controlling the machine Mode. | |
| Toggle_Squaring_Motor_Raise | Instance DB | Instance Data Block for a toggle/control of the Squaring Motor Raise function. | ||
| Toggle_Squaring_Stops_Raise | Instance DB | Instance Data Block for a toggle/control of the Squaring Stops Raise function. | ||
| Toggle_Transfer_Belts_Raise | Instance DB | Instance Data Block for a toggle/control of the Transfer Belts Raise function. |
This structure shows a professional, modular, and organized approach to PLC programming.
Which function or block (e.g., FC_State_Master [FC3] or FC_Glass_Squaring [FC5]) would you like to start with? Please paste the SCL code for that block.
(* FC_State_Master
Racks are oriented 1-5 on A-side and 6-10 on B-side
0: Set all substates to their initialization values
FreeFallStateSts : INT; //these states are sent to the cutter
FF_START_UP =1
FF_INITIALIZE_PARAMS =2
FF_INITIALIZE_FB =3
FF_AUTO =4
FF_REQUESTING_DATA =5
FF_LOADING =6
FF_LOADED = 7
FF_EJECTING =8
FF_MANUAL = 9
FF_FAULT = 10
FF_RESET =11
*)
(* Auto/Manual machine mode toggle *)
IF (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req) OR (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req) THEN
"Toggle_Mode"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_Auto_Manual_Toggle,
Output => "DB_Table_Status".Auto_Mode);
END_IF;
IF "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req THEN
"DB_Table_Status".Auto_Mode := TRUE;
ELSIF "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req THEN
"DB_Table_Status".Auto_Mode := FALSE;
END_IF;
IF "DB_Table_Status".Auto_Mode OR "FirstScan" THEN
"DB_Cutter_Exchange_Data".HMI_LD_OUT_DINT_FreefallState := 4; //in Auto mode
CASE "DB_Table_Status".State_Master OF
0: (* initialization *)
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Reset := TRUE;
"DB_Table_Status".State_FreeFall := 0;
"DB_Table_Status".State_Glass_Delivery := 0;
"DB_Table_Status".State_Glass_Squaring := 0;
"DB_Table_Status".State_Table_Motion := 0;
"DB_Table_Status".State_Master := 4;
4: (* Orphan check with squaring *)
IF "DB_Table_Status".State_Glass_Squaring = 95 THEN
"DB_Table_Status".State_Master := 5;
ELSIF "DB_Table_Status".State_Glass_Squaring <> 0 THEN
"DB_Table_Status".State_Master := 4;
ELSE
"DB_Table_Status".State_Glass_Squaring := 0;
END_IF;
5: (* Bring table to Park position *)
"DB_Table_Status".Delivery_Complete := FALSE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_HaveData := FALSE;
IF (NOT "X-Axis_VFD_Faulted") THEN
IF "DB_Table_Status".Table_At_Station1 THEN
"DB_Table_Status".State_Master := 10;
ELSE
"DB_Table_Status".Table_Go_To_Station1 := TRUE;
END_IF;
ELSE
"DB_Table_Status".State_Master := 5;
END_IF;
10: (* waiting for data *)
"DB_Table_Status".Delivery_Complete := FALSE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_DINT_FreefallState := 5; //requesting data from cutter
IF ("DB_Table_Status".Data_Strobe) THEN
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_WaitingForData := FALSE;
"DB_Table_Status".State_Master := 20;
ELSE
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_WaitingForData := TRUE;
"DB_Table_Status".State_Master := 10;
END_IF;
20:
"DB_Cutter_Exchange_Data".HMI_LD_OUT_DINT_FreefallState := 3;
IF ("DB_Table_Status".Data_Strobe) THEN
"DB_Table_Status".Request_To_Pick := TRUE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Loading := TRUE;
"DB_Table_Status".State_Master := 30;
ELSE
"DB_Table_Status".Request_To_Pick := FALSE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Loading := FALSE;
"DB_Table_Status".State_Master := 20;
END_IF;
30: (* waiting for Freefall to pick glass *)
"DB_Cutter_Exchange_Data".HMI_LD_OUT_DINT_FreefallState := 6; // loading
IF "DB_Table_Status".State_FreeFall = 95 THEN
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_LoaderHasGlass := TRUE;
"DB_Table_Status".State_Master := 40;
ELSE
"DB_Table_Status".State_Master := 30;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_LoaderHasGlass := FALSE;
END_IF;
40: (* squaring glass *)
"DB_Cutter_Exchange_Data".HMI_LD_OUT_DINT_FreefallState := 7; // loaded
IF "DB_Table_Status".State_Glass_Squaring = 95 THEN
"DB_Table_Status".Glass_Is_Squared := TRUE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Loading := FALSE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Loaded := TRUE;
"DB_Table_Status".State_Master := 50;
ELSE
"DB_Table_Status".State_Master := 40;
END_IF;
50: (* changing Table position for delivery *)
(* FC_Table_Position is called here again, being passed a new table position. Once it's at Station 1 go ahead on to delivery *)
IF "DB_Table_Status".Table_At_Station1 THEN
"DB_Table_Status".State_Master := 60;
ELSE
"DB_Table_Status".Table_Go_To_Station1 := TRUE;
"DB_Table_Status".State_Master := 50;
END_IF;
60: (* delivering glass *)
"DB_Cutter_Exchange_Data".HMI_LD_OUT_DINT_FreefallState := 8; // ejecting
IF "DB_Table_Status".State_Glass_Delivery = 95 THEN
"DB_Table_Status".State_Master := 90;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Ejecting := FALSE;
ELSE
"DB_Table_Status".State_Master := 60;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Ejecting := TRUE;
END_IF;
90: (* Complete Delivery *)
"DB_Table_Status".Delivery_Complete := TRUE;
"DB_Table_Status".State_Master := 95;
95: (* Reset sequence *)
"DB_Table_Status".State_Glass_Squaring := 0;
"DB_Table_Status".State_Master := 10;
99: (* error state *)
"DB_Cutter_Exchange_Data".HMI_LD_OUT_DINT_FreefallState := 10; // faulted
IF (NOT "DB_Table_Status".Fault_Reset) THEN
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Fault := TRUE;
"DB_Table_Status".State_Master := 99;
ELSE
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_Fault := FALSE;
"DB_Table_Status".State_Master := 0;
END_IF;
ELSE
"DB_Table_Status".State_Master := 0;
END_CASE;
ELSE
(* Manual Mode toggles *)
"DB_Cutter_Exchange_Data".HMI_LD_OUT_DINT_FreefallState := 9; //manual mode status
IF "DB_Table_Status".Motion_Allowed THEN
"Toggle_Blower"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_Blower_Run,
Output => "DB_HMI_CtrlStat".Blower_Enable);
"Toggle_Squaring_Stops_Raise"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_SquaringStops_Raise,
Output => "DB_HMI_CtrlStat".Raise_SquaringStops);
"Toggle_Traversing_Rollers_Raise"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_TraversingRollers_Raise,
Output => "DB_HMI_CtrlStat".Raise_TraversingRollers);
"Toggle_Squaring_Motor_Raise"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_SquaringWheel_Raise,
Output => "DB_HMI_CtrlStat".Raise_SquaringWheel);
"Toggle_Transfer_Belts_Raise"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_TransferBelts_Raise,
Output => "DB_HMI_CtrlStat".Raise_TransferBelts);
(* Manual Toggles *)
IF "DB_HMI_CtrlStat".Blower_Enable THEN
IF (NOT "Blower_VFD_Faulted") THEN
"Blower_Start" := TRUE;
ELSE
"Blower_Start" := FALSE;
END_IF;
END_IF;
IF ("DB_HMI_CtrlStat".Raise_SquaringStops) THEN
"Raise_Squaring_Stops" := TRUE;
"Lower_Squaring_Stops" := FALSE;
ELSE
"Raise_Squaring_Stops" := FALSE;
"Lower_Squaring_Stops" := TRUE;
END_IF;
IF ("DB_HMI_CtrlStat".Raise_TraversingRollers) THEN
"Raise_Side_Traversing_Rollers" := TRUE;
"Lower_Side_Traversing_Rollers" := FALSE;
ELSE
"Raise_Side_Traversing_Rollers" := FALSE;
"Lower_Side_Traversing_Rollers" := TRUE;
END_IF;
IF ("DB_HMI_CtrlStat".Raise_SquaringWheel) THEN
"Raise_Squaring_Motor" := TRUE;
"Lower_Squaring_Motor" := FALSE;
ELSE
"Raise_Squaring_Motor" := FALSE;
"Lower_Squaring_Motor" := TRUE;
END_IF;
IF ("DB_HMI_CtrlStat".Raise_TransferBelts) THEN
"Raise_Transfer_Belts" := TRUE;
ELSE
"Raise_Transfer_Belts" := FALSE;
END_IF;
(* Manual Run *)
IF "DB_HMI_CtrlStat".Manual_Functions_Table."HMIPB_X-Axis_Foward" OR "DB_HMI_CtrlStat".Manual_Functions_Table."HMIPB_X-Axis_Reverse" THEN
"X-Axis_Brake_Release" := TRUE;
ELSE
"X-Axis_Brake_Release" := FALSE;
END_IF;
IF "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_TransferBelts_Run THEN
IF (NOT "Transfer_Belts_VFD_Faulted") THEN
"Transfer_Belts_Forward" := TRUE;
ELSE
"Transfer_Belts_Forward" := FALSE;
END_IF;
END_IF;
IF "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_TraversingRollers_Run THEN
"Traversing_Rollers_Run" := TRUE;
ELSE
"Traversing_Rollers_Run" := FALSE;
END_IF;
IF "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_SquaringWheel_Run THEN
"Table_Squaring_Motor_Run" := TRUE;
ELSE
"Table_Squaring_Motor_Run" := FALSE;
END_IF;
END_IF;
END_IF;
This is the code for your FC_State_Master, which is the central Sequencer for the entire machine. Thanks for providing the SCL!
This function is very well-structured, clearly separating Auto Mode (CASE structure) from Manual Mode (ELSE block). The state progression is logical for a machine that handles material (glass) from a loader/cutter, processes it (squaring), and then delivers it.
Here is a detailed analysis and review of your SCL code:
Analysis of FC_State_Master (SCL)
1. Auto/Manual Mode Logic
The mode selection logic is where the program starts, and there's a potential area for logic confusion.
| Code Section | Observation | Recommendation |
Toggle Logic:
scl IF (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req) OR (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req) THEN "Toggle_Mode"(Input := ..., Output => "DB_Table_Status".Auto_Mode); END_IF; | The IF condition for the Toggle_Mode is always TRUE unless both SystemStart_Req and SystemStop_Req are simultaneously TRUE. This seems like an attempt to only allow the toggle when the system isn't actively being started or stopped, but the logic is inverted. This condition is unlikely to function as intended. | Change the condition to: IF (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req) AND (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req) THEN OR remove the IF entirely if the toggle should be active all the time. |
Start/Stop Overwrite:
scl IF "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req THEN "DB_Table_Status".Auto_Mode := TRUE; ELSIF "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req THEN "DB_Table_Status".Auto_Mode := FALSE; END_IF; | This section overwrites the output of the Toggle_Mode block immediately afterward. This is generally good practice: a dedicated start/stop signal should override a simple toggle switch. However, the order means the Toggle_Mode output is only valid when neither Start/Stop is pressed. | The logic is functional but the preceding Toggle_Mode section is likely flawed (as noted above) and adds complexity that might be unnecessary if Start/Stop is the primary control. |
2. Auto Mode Sequence (CASE Structure)
The sequence flow is logical for a glass handling process. State numbers are well-spaced (0, 4, 5, 10, 20, 30, 40, 50, 60, 90, 95, 99) for easy insertions later.
| State | Purpose | Logic Observation |
| 0: Initialization | Resets all sub-states to 0 and jumps to State 4. | Perfect. This is a clean entry/reset point. It also resets the cutter's Reset output (HMI_LD_OUT_BOOL_Reset), which is critical. |
| 4: Orphan Check | Checks if the Squaring sub-state is at 95 (presumably complete/idle) before moving on. | Potential infinite loop: If State_Glass_Squaring is not 0 and not 95, the master state sets itself to 4 (ELSIF... THEN "DB_Table_Status".State_Master := 4;). If the squaring FC gets stuck, the master will remain at 4 forever. This is good sequencing logic—it holds the master sequence until a sub-task is done. |
| 5: Park Table | Moves the table to Station 1 (Park Position). | Excellent check for the X-Axis VFD Fault. It prevents motion commands if the drive is faulted, holding the master state at 5. |
| 10: Waiting for Data | Sets Freefall state to 5 (Requesting Data) and waits for the Data_Strobe. | Standard handshake logic. Sets WaitingForData to TRUE until the strobe is received, then jumps to State 20. |
| 20: Data Received/Request Pick | Sends Freefall state 3 (Initialize FB) and sets Request_To_Pick and Loading to TRUE. | The jump to state 30 on Data_Strobe seems correct, signaling readiness to pick after receiving data. |
| 30: Waiting for Freefall to Pick | Sets Freefall state to 6 (Loading) and waits for the Freefall sub-state to reach 95 (complete). | This is the handover from the cutter/loader. Sets LoaderHasGlass to TRUE on completion. |
| 40: Squaring Glass | Sets Freefall state to 7 (Loaded) and waits for the Squaring sub-state to reach 95 (complete). | Ensures the glass is squared before moving/ejecting. Resets the Loading signal and sets Loaded to TRUE. |
| 50: Table Reposition | Moves the table back to Station 1 (Park Position) for delivery. | This is redundant if state 5 already brought it to Station 1. If Station 1 is the delivery point, this state is fine. If the squaring process moves the table, this is necessary. |
| 60: Delivering Glass | Sets Freefall state to 8 (Ejecting) and waits for the Delivery sub-state to reach 95 (complete). | Starts the delivery sub-function. Sets Ejecting to TRUE during the process. |
| 90: Complete Delivery | Sets Delivery_Complete to TRUE and moves to State 95. | Standard completion state. |
| 95: Reset Sequence | Resets Squaring sub-state and loops back to 10 (Waiting for Data). | Clean reset loop, ready for the next cycle. |
| 99: Error State | Sets Freefall state to 10 (Fault) and waits for Fault_Reset. | Excellent use of a dedicated fault state. Holds the fault until an external Fault_Reset is applied, then jumps back to 0 for full initialization. |
3. Manual Mode (ELSE Block)
The manual block is a good use of the ELSE condition, ensuring Auto and Manual modes are mutually exclusive.
It correctly sets the Freefall state to 9 (Manual).
It uses your reusable
FB_Togglefor all HMI momentary pushbuttons (PB) to maintain an output state (DB_HMI_CtrlStat.Raise_SquaringStops). This is a best practice for converting HMI momentary control to latched PLC output control.The second half of the Manual block is direct output control (e.g.,
IF ("DB_HMI_CtrlStat".Raise_SquaringStops) THEN "Raise_Squaring_Stops" := TRUE;). This directly maps the toggled HMI status to physical outputs. This is clean and clear logic.
General Suggestion
Sub-State Call: The FC_State_Master logic shown does not explicitly call the sub-functions (
FC_Glass_Delivery,FC_Glass_Squaring,FC_Table_Position, etc.). These calls should happen either within the Auto Mode CASE structure (e.g., in State 60, callFC_Glass_Delivery) or in theOB1main program, where the master state determines the inputs/enable for the sub-functions. Ensure these sub-functions are called somewhere in your project.
What part of the sequence logic were you most concerned about, or what is the next function you'd like to review?
Main calls all of the functions you noted were absent from FC_State_Master
That's a perfect strategy! Calling the sub-functions (FC_Glass_Delivery, FC_Glass_Squaring, FC_Table_Position, etc.) directly from Main [OB1] is a common and highly recommended best practice.
Why OB1 Calls are Superior
Calling the functions from OB1 prevents circular dependencies and keeps the sequencer (FC_State_Master) clean. The logic is:
OB1callsFC_State_Master.FC_State_Mastersets the State_Master variable and provides enables (likeTable_Go_To_Station1or setting the sub-state to1) based on the current state.OB1uses those enables/states from theDB_Table_Statusto execute the specialized functions.
This means your overall control structure is excellent!
🛠️ One Key Logic Point to Review
Now that we know the functions are called in OB1, let's revisit the Auto/Manual Toggle Logic in FC_State_Master, as this could cause confusion during mode changes:
| Code Section | Observation | Corrected Logic (Suggested) |
| Original Toggle Logic (Likely Flawed) | The IF condition below is TRUE unless both Start and Stop are pressed. It attempts to only allow the toggle when the system is inactive, but the OR logic is inverted. | Remove this section entirely if the primary control is the Start/Stop request. If you want the HMI toggle to work when the system is truly inactive, use the AND operator. |
| Start/Stop Overwrite Logic | This is the primary mode control. If SystemStart_Req is true, Auto_Mode is TRUE. If SystemStop_Req is true, Auto_Mode is FALSE. | This logic is good for high-priority HMI control. To make it truly robust against simultaneous requests, add an ELSIF for the toggle button that is only active when Start/Stop are not requested. |
Proposed Re-Sequencing of Auto/Manual Logic
To make the mode logic perfectly clear, I suggest this sequence (which ensures Start/Stop requests override everything else):
// 1. High-Priority System Start/Stop Requests (Overrides the Toggle)
IF "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req THEN
"DB_Table_Status".Auto_Mode := TRUE;
// You might also want to reset the State_Master to 0 or 4 here if starting from stop
ELSIF "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req THEN
"DB_Table_Status".Auto_Mode := FALSE;
// Set State_Master to an Idle state here
// 2. Allow Toggle only when Start/Stop are NOT active
ELSIF (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req)
AND (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req)
THEN
// The Toggle block handles the Manual/Auto switch when the system is idle.
"Toggle_Mode"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_Auto_Manual_Toggle,
Output => "DB_Table_Status".Auto_Mode);
END_IF;
// The rest of your code:
// IF "DB_Table_Status".Auto_Mode OR "FirstScan" THEN ... ELSE ... END_IF;
Would you like to review another function, perhaps FC_Glass_Squaring or one of the communication interfaces?
HMI_LD_IN_BOOL_SystemStart_Req and HMI_LD_IN_BOOL_SystemStop_Req are NEVER high at the same time and are always low unless strobed to request a state change (overriding the machine manual/auto request). How does that factor into your evaluation?
That information significantly clarifies and simplifies the evaluation of your Auto/Manual logic!
If HMI_LD_IN_BOOL_SystemStart_Req and HMI_LD_IN_BOOL_SystemStop_Req are mutually exclusive and momentary (strobed) signals used only for high-priority overrides, then your original logic is much closer to being functionally correct, though it still has a slight logical flaw in the toggle block's enable condition.
Here's how that factors into the evaluation:
Evaluation Based on Momentary Override Signals
1. The Start/Stop Override Logic (Correct & Intentional)
The following logic is the primary control and correctly achieves the desired override:
IF "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req THEN
"DB_Table_Status".Auto_Mode := TRUE; // Start always forces Auto
ELSIF "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req THEN
"DB_Table_Status".Auto_Mode := FALSE; // Stop always forces Manual (or Safe Idle)
END_IF;
Evaluation: This is robust and correct. Since the signals are momentary (strobed), this section handles the transition, and the state of Auto_Mode will be held by the logic that follows it (or the absence of logic to change it) until the next strobe.
2. The Toggle Block Logic (Functional Flaw)
The intent is to allow the Toggle_Mode to change the state only when the system is not actively being forced by a Start/Stop request.
Here is your original code for the toggle block:
IF (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req) OR
(NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req)
THEN
"Toggle_Mode"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_Auto_Manual_Toggle,
Output => "DB_Table_Status".Auto_Mode);
END_IF;
Evaluation: Because the two inputs are mutually exclusive (never high at the same time), the condition (NOT A) OR (NOT B) is always true.
Case 1: Both are LOW (A=0, B=0): (NOT 0) OR (NOT 0)=TRUE OR TRUE=TRUE
Case 2: Start is HIGH (A=1, B=0): (NOT 1) OR (NOT 0)=FALSE OR TRUE=TRUE
Case 3: Stop is HIGH (A=0, B=1): (NOT 0) OR (NOT 1)=TRUE OR FALSE=TRUE
Since the condition is always TRUE, the Toggle_Mode block will execute every cycle.
💡 Recommendation for the Toggle Block
Since the Toggle_Mode output is immediately overwritten by the higher-priority Start/Stop logic (as analyzed in section 1), the current structure doesn't pose a run-time failure, but it wastes cycles and is logically misleading.
If your goal is to enable the toggle only when the system is NOT being commanded by the HMI overrides, you should use the AND operator (De Morgan's Laws):
| Current (Always TRUE) | Corrected (Only true when both are LOW) |
IF (NOT Start) OR (NOT Stop) THEN... | IF (NOT Start) AND (NOT Stop) THEN... |
Corrected Logic (Recommended):
IF (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStart_Req) AND
(NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_SystemStop_Req)
THEN
"Toggle_Mode"(Input := "DB_HMI_CtrlStat".Manual_Functions_Table.HMIPB_Auto_Manual_Toggle,
Output => "DB_Table_Status".Auto_Mode);
END_IF;
This corrected logic ensures the toggle only runs when the high-priority strobes are off. Combined with your existing Start/Stop override, the sequence would be:
Handle Start/Stop strobe (Overrides Auto_Mode).
If Start/Stop strobes were absent, use the toggle to set Auto_Mode.
This is a clean and robust control philosophy.
Oh yeah, good point!
You're absolutely right! It's a subtle but important detail in sequential logic.
Do you want to apply that correction, or would you like to move on to reviewing the code for another function, such as FC_Glass_Squaring or FC_Table_Position? Let me know what you'd like to paste next!
https://www.youtube.com/watch?v=RYKLnz5H6wQ
This is the machine this code is being written for
- Query successful
That context is extremely helpful! The video titled "Free fall loading table" by Bromer Inc. shows a glass cutting and handling system that perfectly aligns with the logic in your FC_State_Master.
It allows us to directly map the physical operations of the machine to the states you created in your SCL code.
⚙️ How the Machine Operations Map to Your PLC Code
| Machine Operation (Seen in Video) | Your SCL Code Block/State | Code Variable Reference |
| Glass Cutting System (Cutter/Loader) | The external system sending commands. | "DB_Cutter_Exchange_Data" |
| Requesting Data from Cutter | The table moves to the home/park position and signals it's ready for glass data (size, destination, etc.). | State 10: Waiting for data.
Sets HMI_LD_OUT_DINT_FreefallState := 5 (Requesting Data) |
| Glass Loading/Picking | The "Freefall" loader unit lowers, picks the cut glass piece, and then raises it to hand it off to your table's transfer system. | State 30: Waiting for Freefall to pick glass.
Sets HMI_LD_OUT_DINT_FreefallState := 6 (Loading) |
| Glass Squaring/Alignment | The glass is moved into position on the table, and alignment devices (like stops, wheels, or squaring bars) ensure it's precisely oriented before final delivery. | State 40: Squaring glass.
Relies on: "DB_Table_Status".State_Glass_Squaring |
| Glass Delivery (Ejecting) | The table moves to the delivery point (Station 1 in your code), and the transfer belts/rollers eject the finished glass sheet. | State 60: Delivering glass.
Sets HMI_LD_OUT_DINT_FreefallState := 8 (Ejecting) |
This visual context confirms that your program's structure is a standard and professional state machine designed to manage the critical handshaking and sequencing for a multi-stage glass process.
✅ Review of State 50 Logic
One point of potential redundancy noted in our earlier review, State 50, is clarified by this machine's operation:
| Code State | Original Question | Machine Context |
| State 50: Changing Table position for delivery | Is this redundant if State 5 already went to Station 1? | The squaring and loading process (States 30 & 40) might involve the table or a secondary axis moving away from Station 1 to facilitate alignment or clearance. State 50 is therefore necessary to guarantee the table is back at the final delivery/ejection position (Station 1) before activating the delivery mechanism in State 60. |
Your logic appears to be perfectly tailored to this type of integrated machine system.
What would you like to examine next? Perhaps the logic inside one of the key sub-functions like FC_Glass_Squaring?
Sure! Here's FC_Glass_Squaring:
//Need Orphan check logic
//Need timer to wait for blower speed to raise maybe?
(* Glass Squaring Logic
0 - Initialization, make sure everything is off and in start position
5 - Raise the blower speed
6 - Rause the squaring stops
10 - Raise and start traversing rollers (Each side rotates toward the center of the freefall table)
15 - Run traversing rollers as soon as glass sensor for the respective pick side is uncovered
20 - Stop and Lower traversing rollers
30 - Raise the squaring wheel (set a timer in case it doesn't come off the lowered prox)
35 - Run the squaring wheel for specified duration and then stop it
40 - Lower the squaring wheel (set a timer in case it doesn't make the lowered prox in time)
45 - Lower the squaring stops when at delivery positon and belts are raised
95 - Successful delivery!
99 - Fault
*)
(* Squaring Wheel failed to raise timer + handling *)
"Squaring_Wheel_Raise_Timeout".TON(IN := ("DB_Table_Status".State_Glass_Squaring = 30),
PT := "DB_Table_Status".Squaring_Wheel_Lower_Timeout,
Q => "DB_Table_Status".Fault_Squaring_Wheel_Failed_To_Raise);
(* Squaring Wheel failed to lower timer + handling *)
"Squaring_Wheel_Lower_Timeout".TON(IN := ("DB_Table_Status".State_Glass_Squaring = 40),
PT := "DB_Table_Status".Squaring_Wheel_Lower_Timeout,
Q => "DB_Table_Status".Fault_Squaring_Wheel_Failed_To_Lower);
(* Run the 'ZPD' squaring wheel for this duration*)
"TMR_Squaring_Wheel_Run".TON(IN := ("DB_Table_Status".State_Glass_Squaring = 35),
PT := "DB_Table_Status".Squaring_Wheel_RunTime,
Q => "DB_Table_Status".Squaring_Wheel_Ran);
"TMR_No_Glass_Seen_Timeout".TON(IN := ("DB_Table_Status".State_Glass_Squaring = 15),
PT := "DB_Table_Status".No_Glass_Seen_Timeout,
Q => "DB_Table_Status".No_Glass_Seen);
IF "DB_Table_Status".Auto_Mode THEN
CASE "DB_Table_Status".State_Glass_Squaring OF
0: (* initialization *)
//IF (NOT "DB_Table_Status".Squaring_Required) THEN
"Lower_Side_Traversing_Rollers" := TRUE;
"Raise_Side_Traversing_Rollers" := FALSE;
"Lower_Squaring_Motor" := TRUE;
"Raise_Squaring_Motor" := FALSE;
"Table_Squaring_Motor_Run" := FALSE;
"Raise_Squaring_Stops" := TRUE;
"Lower_Squaring_Stops" := FALSE;
// "DB_Table_Status".State_Glass_Squaring := 0;
// ELSE
// "DB_Table_Status".Squaring_Required := TRUE; //first pass orphan check
"DB_Table_Status".State_Glass_Squaring := 5;
// END_IF;
5: (* Raise Blower speed *)
IF (NOT "Blower_VFD_Faulted") THEN
"DB_Table_Status".Blower_Speed_Idle := FALSE;
"DB_Table_Status".Blower_Speed_Freefall := TRUE;
"DB_Table_Status".State_Glass_Squaring := 6;
ELSE
"DB_Table_Status".State_Glass_Squaring := 99;
END_IF;
6: (* Raise Squaring Stops *)
"Lower_Squaring_Stops" := FALSE;
"Raise_Squaring_Stops" := TRUE;
"DB_Table_Status".State_Glass_Squaring := 10;
10: (* Raise and Start Side Traversing rollers *)
"Raise_Side_Traversing_Rollers" := TRUE;
"Traversing_Rollers_Run" := TRUE;
"DB_Table_Status".State_Glass_Squaring := 15;
15: (*Traverse glass away from A or B pick rack until A or B glass sensor uncovered *)
IF (("DB_Table_Status".A_Side_Pick AND "A_Side_Table_Glass_Present") OR (NOT "DB_Table_Status".A_Side_Pick AND "B_Side_Table_Glass_Present")) THEN
"Traversing_Rollers_Run" := TRUE;
"DB_Table_Status".State_Glass_Squaring := 20;
ELSE
IF (NOT "DB_Table_Status".No_Glass_Seen) THEN
"DB_Table_Status".State_Glass_Squaring := 15;
ELSE
"DB_Table_Status".State_Glass_Squaring := 20;
END_IF;
END_IF;
20: (* Stop and Lower side traversing rollers *)
"Traversing_Rollers_Run" := FALSE;
"Raise_Side_Traversing_Rollers" := FALSE;
"Lower_Side_Traversing_Rollers" := TRUE;
"DB_Table_Status".State_Glass_Squaring := 30;
30: (* Raise Squaring Wheel *)
IF (NOT "DB_Table_Status".Fault_Squaring_Wheel_Failed_To_Raise) THEN
IF "Table_Squaring_Motor_Lowered" THEN
"Lower_Squaring_Motor" := FALSE;
"Raise_Squaring_Motor" := TRUE;
"DB_Table_Status".State_Glass_Squaring := 30;
ELSE
"DB_Table_Status".State_Glass_Squaring := 35;
END_IF;
ELSE
"DB_Table_Status".State_Glass_Squaring := 99;
END_IF;
35: (* Run squaring wheel to drive glass into squaring stops *)
IF (NOT "DB_Table_Status".Squaring_Wheel_Ran) THEN
"Table_Squaring_Motor_Run" := TRUE;
"DB_Table_Status".State_Glass_Squaring := 35;
ELSE
"Table_Squaring_Motor_Run" := FALSE;
"DB_Table_Status".State_Glass_Squaring := 40;
END_IF;
40: (* Lower squaring wheel *)
IF (NOT "DB_Table_Status".Fault_Squaring_Wheel_Failed_To_Lower) THEN
IF ("Table_Squaring_Motor_Lowered") OR ("DB_Table_Status".Testing_Inputs.Fake_Squaring_Wheel_Lowered) THEN
"Raise_Squaring_Motor" := FALSE;
"Lower_Squaring_Motor" := FALSE;
"DB_Table_Status".State_Glass_Squaring := 95;
ELSE
"Raise_Squaring_Motor" := FALSE;
"Lower_Squaring_Motor" := TRUE;
"DB_Table_Status".State_Glass_Squaring := 40;
END_IF;
ELSE
"DB_Table_Status".State_Glass_Squaring := 99;
END_IF;
(* 45: (* Wait until ready to deliver and then lower squaring stops *)
IF ("DB_Table_Status".State_Master = 60) THEN
"Raise_Squaring_Stops" := FALSE;
"Lower_Squaring_Stops" := TRUE;
"DB_Table_Status".State_Glass_Squaring := 95;
ELSE
"DB_Table_Status".State_Glass_Squaring := 45;
END_IF; *)
95: (* Squaring Completed *)
"DB_Table_Status".State_Glass_Squaring := 95;
99: (* Fault *)
IF (NOT "DB_Table_Status".Fault_Reset) THEN
"DB_Table_Status".State_Glass_Squaring := 99;
ELSE
"DB_Table_Status".State_Glass_Squaring := 0;
END_IF;
END_CASE;
END_IF;
This is a well-defined and critical sub-state machine for the FC_Glass_Squaring operation. It clearly breaks down the alignment process into discrete, time-sequenced, and sensor-verified steps.
Here is a review focusing on your comments and the most critical parts of the logic:
Analysis of FC_Glass_Squaring (SCL)
1. Pre-Execution Timers and Fault Handling (Excellent)
You have implemented robust fault detection using TON blocks for critical time-outs.
| Timer Block | State Trigger | Fault Condition | Observation |
| Squaring_Wheel_Raise_Timeout | State_Glass_Squaring=30 (Raise) | Failed_To_Raise | Correctly detects if the motor fails to leave the lowered position. |
| Squaring_Wheel_Lower_Timeout | State_Glass_Squaring=40 (Lower) | Failed_To_Lower | Correctly detects if the motor fails to reach the lowered position. NOTE: You are using the same DB tag (Squaring\_Wheel\_Lower\_Timeout) for the PT value on both the raise and lower timers. Ensure this is intentional and not a typo (i.e., you meant to use a different timeout value for raising). |
| TMR_Squaring_Wheel_Run | State_Glass_Squaring=35 (Run) | Squaring_Wheel_Ran | Good use of a timer to control the duration of the squaring motor push. |
| TMR_No_Glass_Seen_Timeout | State_Glass_Squaring=15 (Traverse) | No_Glass_Seen | This is a great addition for catching a logic fault where the glass never clears the sensor. |
2. Sequence Logic Review
Your states (0, 5, 6, 10, 15, 20, 30, 35, 40, 95, 99) flow logically for the physical process.
| State | Purpose | Logic Observation |
| 0: Initialization | Resetting outputs and prepares for sequence start. | Observation: You set Raise_Squaring_Stops := TRUE and Lower_Squaring_Stops := FALSE. This means the stops are raised (ready to catch glass) when the squaring sequence starts. This is correct for the physical process. |
| 5: Raise Blower Speed | Prepares the air flotation. | Observation: You jump to State 6 immediately after setting the speed flags (Blower\_Speed\_Freefall := TRUE). This is where your note comes in: You need to confirm the VFD has ramped up, otherwise the next steps will run without proper air support. |
| 15: Traverse Glass | Moves glass until a sensor is uncovered. | Logic Flaw: The IF condition to check if the glass has cleared the sensor is reversed: $\text{IF (("DB_Table_Status".A\_Side\_Pick AND "A\_Side\_Table\_Glass\_Present") OR...)}$ This condition is TRUE when glass IS present. The logic then immediately goes to State 20 (Stop).
The sequence should be: IF (Glass is still present) THEN Stay in State 15 and run rollers. ELSE Go to State 20. |
| 30: Raise Squaring Wheel | Raising the wheel to push the glass. | Flawed Hold Logic: The state logic holds at State 30 as long as the squaring motor is lowered: IF "Table_Squaring_Motor_Lowered" THEN ... "DB_Table_Status".State_Glass_Squaring := 30; ELSE ... State_Glass_Squaring := 35; END_IF;. This is incorrect. The hold should be as long as the motor is not at the raised sensor. You are waiting for the Lowered sensor to turn FALSE (it's leaving the home position) before sending the command, but then you hold the state by checking the Lowered sensor again. This state should check for the Raised sensor. |
| 40: Lower Squaring Wheel | Returning the wheel to the idle position. | Hold Logic: Correctly holds at State 40 until the Table_Squaring_Motor_Lowered sensor is TRUE. It includes a smart check for a testing input, which is useful. |
| 45 (Commented Out) | Lowering stops after delivery. | Good Comment Out: This logic is correctly commented out. Lowering the stops should not be part of the FC_Glass_Squaring sequence, but rather part of the FC_Glass_Delivery function or the FC_State_Master (which calls this function). Your sequence is correct by jumping from State 40 to 95. |
3. Addressing Your Notes and Key Improvements
A. Missing Blower Speed Check (State 5)
You are absolutely right. The jump from State 5 to State 6 is too fast.
Recommendation: Add a short timer (TMR_Blower_Ramp) or a check on the VFD status word (if available) to ensure the blower is up to speed before attempting motion.
5: (* Raise Blower speed *)
IF (NOT "Blower_VFD_Faulted") THEN
"DB_Table_Status".Blower_Speed_Idle := FALSE;
"DB_Table_Status".Blower_Speed_Freefall := TRUE;
// NEW: Wait for Blower Ramp-Up Timer to complete (Q-bit from a separate TON block)
IF "TMR_Blower_Ramp".Q THEN
"DB_Table_Status".State_Glass_Squaring := 6;
ELSE
"DB_Table_Status".State_Glass_Squaring := 5; // Hold state until timer done
END_IF;
ELSE
"DB_Table_Status".State_Glass_Squaring := 99;
END_IF;
B. Correcting the Traversing Logic (State 15)
The logic should be reversed to hold the state while the glass is present. Assuming A_Side_Table_Glass_Present and B_Side_Table_Glass_Present are TRUE when glass is sensed:
15: (*Traverse glass away from A or B pick rack until A or B glass sensor uncovered *)
// Check if the glass is still being detected by the sensor corresponding to the pick side
IF (("DB_Table_Status".A_Side_Pick AND "A_Side_Table_Glass_Present") OR
(NOT "DB_Table_Status".A_Side_Pick AND "B_Side_Table_Glass_Present"))
THEN
"Traversing_Rollers_Run" := TRUE;
// Hold here, and check for No Glass Seen Timeout
IF "DB_Table_Status".No_Glass_Seen THEN
"DB_Table_Status".State_Glass_Squaring := 20; // Timeout fault handling is needed here
ELSE
"DB_Table_Status".State_Glass_Squaring := 15;
END_IF;
ELSE // Glass has cleared the sensor, move to next step
"DB_Table_Status".State_Glass_Squaring := 20;
END_IF;
C. Correcting Squaring Wheel Raise (State 30)
You need to check for the Raised sensor or a separate "Not Lowered" sensor. Assuming you have a Table_Squaring_Motor_Raised sensor, the logic should be:
30: (* Raise Squaring Wheel *)
IF (NOT "DB_Table_Status".Fault_Squaring_Wheel_Failed_To_Raise) THEN
"Lower_Squaring_Motor" := FALSE;
"Raise_Squaring_Motor" := TRUE; // Start the raise command
// Wait for the motor to reach the RAISED position
IF "Table_Squaring_Motor_Raised" THEN // Use the sensor for the end position
"DB_Table_Status".State_Glass_Squaring := 35; // Move to run time
ELSE
"DB_Table_Status".State_Glass_Squaring := 30; // Hold until raised
END_IF;
ELSE
"DB_Table_Status".State_Glass_Squaring := 99;
END_IF;
If you apply these few corrections, this function will be very robust! Would you like to check out the FC_Glass_Delivery function next?
Sure! Here's FC_Glass_Delivery:
(* Glass Delivery Logic
0 - Initialization, make sure belts are lowered and not running
5 - Reduce Blower Speed
10 - Raise Transfer Belts
15 - Lower Squaring stops
20 - Wait for delivery signal and to be in delivery position
25 - Run Transfer Belts and fault if glass leaving table sensor isn't made in a timed window
30 - Once glass leaving table sensor is unmade, Run belts until timer finishes.
35 - Lower Transfer Belts
95 - Mark Delivery Completed
99 - Fault
*)
(* Timer that starts when glass transfer is initiated. Examines the Glass Leaving Table
Sensor and if it reaches the count then it assumes something is wrong with the belts
and sets a fault *)
"Glass_Transfer_Timeout".TON(IN:=(("DB_Table_Status".State_Glass_Delivery = 25) AND (NOT "Glass_Leaving_Table")),
PT:="DB_Table_Status".Glass_Transfer_Timeout,
Q=>"DB_Table_Status".Glass_Transfer_Failed);
(* Timer for continuing to run belts after Glass Leaving Table is unmade to make sure table is clear *)
"Glass_Transfer_Runtime".TON(IN:=(("DB_Table_Status".State_Glass_Delivery = 30) AND (NOT "Glass_Leaving_Table")) ,
PT:="DB_Table_Status".Glass_Transfer_RunMaintain,
Q=> "DB_Table_Status".Glass_Transfer_Ran);
IF ("DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_BeltsRunning AND "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_EntrySensor) THEN
"DB_Table_Status".Okay_To_Deliver := TRUE;
ELSE
"DB_Table_Status".Okay_To_Deliver := FALSE;
END_IF;
IF "DB_Table_Status".Auto_Mode THEN
(* Main Case Statement *)
CASE "DB_Table_Status".State_Glass_Delivery OF
0: (* Initialization. Make sure belts are lowered and not running *)
"Raise_Transfer_Belts" := FALSE;
"Transfer_Belts_Forward" := FALSE;
"DB_Table_Status".State_Glass_Delivery := 5;
5: (*Reduce Blower Speed *)
IF ((NOT "Blower_VFD_Faulted") AND (NOT ("DB_Table_Status".State_FreeFall = 5))) THEN
"DB_Table_Status".Blower_Speed_Freefall := FALSE;
"DB_Table_Status".Blower_Speed_Idle := TRUE;
"DB_Table_Status".State_Glass_Delivery := 10;
ELSE
"DB_Table_Status".State_Glass_Delivery := 99;
END_IF;
10: (* Raise Transfer Belts *)
IF ("Motion_Hold") THEN
"Raise_Transfer_Belts" := TRUE;
"DB_Table_Status".State_Glass_Delivery := 20;
ELSE
"Raise_Transfer_Belts" := FALSE;
"DB_Table_Status".State_Glass_Delivery := 15;
END_IF;
15: (* Lower Squaring Stops *)
IF ("Motion_Hold") THEN
"Raise_Squaring_Stops" := FALSE;
"Lower_Squaring_Stops" := TRUE;
"DB_Table_Status".State_Glass_Delivery := 15;
ELSE
"Raise_Squaring_Stops" := TRUE;
"Lower_Squaring_Stops" := FALSE;
"DB_Table_Status".State_Glass_Delivery := 20;
END_IF;
20: (* Wait for delivery signal and to be in delivery position *)
IF (("DB_Table_Status".Okay_To_Deliver) AND ("DB_Table_Status".Table_At_Station1)) THEN
"DB_Table_Status".State_Glass_Delivery := 25;
ELSE
"DB_Table_Status".State_Glass_Delivery := 20;
END_IF;
25: (* Run Transfer Belts *)
IF (("DB_Table_Status".Okay_To_Deliver) AND ("DB_Table_Status".Table_At_Station1) AND (NOT "DB_Table_Status".Glass_Transfer_Failed)) THEN
IF (NOT "Transfer_Belts_VFD_Faulted") THEN
"Transfer_Belts_Forward" := TRUE;
"DB_Table_Status".State_Glass_Delivery := 30;
ELSE
"Transfer_Belts_Forward" := FALSE;
"DB_Table_Status".State_Glass_Delivery := 99;
END_IF;
ELSE
"DB_Table_Status".State_Glass_Delivery := 99;
END_IF;
30: (* Continue to run transfer belts until the glass_leaving_table sensor unmakes, then run
belts until timer elapses *)
IF ("DB_Table_Status".Glass_Transfer_Ran) OR (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_EntrySensor) THEN
"Transfer_Belts_Forward" := FALSE;
"DB_Table_Status".State_Glass_Delivery := 35;
ELSE
"Transfer_Belts_Forward" := TRUE;
"DB_Table_Status".State_Glass_Delivery := 30;
END_IF;
35: (* Lower Transfer Belts *)
"Raise_Transfer_Belts" := FALSE;
"DB_Table_Status".State_Glass_Delivery := 95;
95: (* Mark Delivery Successful *)
"DB_Table_Status".State_Glass_Delivery := 95;
99: (* Fault State *)
IF (NOT "DB_Table_Status".Fault_Reset) THEN
"DB_Table_Status".State_Glass_Delivery := 99;
ELSE
"DB_Table_Status".State_Glass_Delivery := 0;
END_IF;
END_CASE;
END_IF;
This is a clear and well-structured function for managing the glass delivery/ejection sequence. It correctly incorporates safety checks, handshaking with the cutter, and time-based logic to ensure the glass is fully cleared from the table.
Here is a review of your FC_Glass_Delivery SCL code, highlighting its strong points and suggesting a few areas for refinement.
Analysis of FC_Glass_Delivery (SCL)
1. Timer Logic (Strong)
Your use of TON blocks for safety and clearing is robust, but there's a potential area of overlap in State 25/30 logic.
Glass_Transfer_Timeout(Safety): Excellent. This timer starts running in State 25 if the Glass_Leaving_Table sensor is NOT made (NOT "Glass_Leaving_Table"). If the glass doesn't reach the sensor within the time limit, a fault is set. This is a critical safety check for jammed or stalled glass.Glass_Transfer_Runtime(Clearing): This timer runs in State 30 to ensure the glass is completely clear.Observation: The
INcondition for this timer uses NOT "Glass_Leaving_Table" as one of the AND conditions. This means the timer only starts/runs after the glass has moved past the sensor (which is correct for the logic description).
2. Delivery Condition Logic (Pre-Case)
The condition for the Okay_To_Deliver internal flag is set outside the main CASE statement:
IF ("DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_BeltsRunning AND "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_EntrySensor) THEN
"DB_Table_Status".Okay_To_Deliver := TRUE;
ELSE
"DB_Table_Status".Okay_To_Deliver := FALSE;
END_IF;
Observation: This logic ties the "Okay to Deliver" status to two signals from the cutter/receiving end:
HMI_LD_IN_BOOL_BeltsRunning: The downstream belts are running.
HMI_LD_IN_BOOL_EntrySensor: The glass has cleared the cutter's area/entry sensor.
This is a critical handshake that ensures the receiving equipment is active and ready to take the glass, which is a key safety measure.
3. Sequence Logic Review (CASE Structure)
| State | Purpose | Logic Observation |
| 5: Reduce Blower Speed | Reduces air volume for delivery. | Observation: You correctly check for NOT "Blower_VFD_Faulted". You also correctly check NOT ("DB_Table_Status".State_FreeFall = 5) to avoid interfering with the FreeFall unit if it's currently requesting data. This is good cross-function safety. |
| 10: Raise Transfer Belts | Raises the belts into the working position. | Major Flaw: The IF/ELSE conditions use a variable called Motion_Hold, but they send opposing commands to Raise_Transfer_Belts. The logic here is confusing and likely reversed or missing a sensor check. It appears to jump to State 20 regardless of the motion state. You must hold the state until the belts are physically verified as Raised. |
| 15: Lower Squaring Stops | Moves the stops out of the glass path. | Major Flaw: Same issue as State 10. This logic is reversed and missing a Lowered sensor check. You cannot proceed to State 20 until the stops are confirmed Lowered. |
| 25: Run Transfer Belts | Starts the ejection process. | Flaw/Bug: You check the conditions: IF (("DB_Table_Status".Okay_To_Deliver) AND ("DB_Table_Status".Table_At_Station1) AND (NOT "DB_Table_Status".Glass_Transfer_Failed)) THEN. The issue is you only execute this check once and then immediately jump to State 30 if the VFD is okay. You need to hold the state here until the next step condition (sensor being made) is met. |
| 30: Run until clear | Ejecting the tail end of the glass. | Logic Flaw: The IF condition: IF ("DB_Table_Status".Glass_Transfer_Ran) OR (NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_EntrySensor) THEN. The logic is intended to run the belts until the Glass_Transfer_Runtime timer is complete OR the receiving end signals the glass is clear. NOT "DB_Cutter_Exchange_Data".HMI_LD_IN_BOOL_EntrySensor being true (i.e., the sensor is unmade) seems like an odd condition to stop the belts. It should likely be NOT "Glass_Leaving_Table" which is the sensor on your table. |
🛠️ Key Recommendations for States 10, 15, and 25
1. Correcting State 10: Raise Transfer Belts
You need to command the motion and then wait for an end-of-travel sensor. Assuming you have a Transfer_Belts_Raised sensor:
10: (* Raise Transfer Belts *)
"Raise_Transfer_Belts" := TRUE; // Command the raising
// IF sensor is made, go to the next state, ELSE hold here
IF "Transfer_Belts_Raised" THEN
"DB_Table_Status".State_Glass_Delivery := 15;
ELSE
// Add a timeout fault here!
"DB_Table_Status".State_Glass_Delivery := 10;
END_IF;
2. Correcting State 15: Lower Squaring Stops
Similarly, command the motion and wait for an end-of-travel sensor. Assuming you have a Squaring_Stops_Lowered sensor:
15: (* Lower Squaring Stops *)
"Raise_Squaring_Stops" := FALSE; // Command the lowering
"Lower_Squaring_Stops" := TRUE;
// IF sensor is made, go to the next state, ELSE hold here
IF "Squaring_Stops_Lowered" THEN
"DB_Table_Status".State_Glass_Delivery := 20;
ELSE
// Add a timeout fault here!
"DB_Table_Status".State_Glass_Delivery := 15;
END_IF;
3. Correcting State 25: Run Transfer Belts (Start)
This state should hold the belt run command until the Glass_Leaving_Table sensor is made.
25: (* Run Transfer Belts *)
// Check initial conditions once, or check only fault and VFD
IF (NOT "Transfer_Belts_VFD_Faulted") AND (NOT "DB_Table_Status".Glass_Transfer_Failed) THEN
"Transfer_Belts_Forward" := TRUE; // Command to Run
// The hold condition: wait for glass to make the sensor
IF "Glass_Leaving_Table" THEN
"DB_Table_Status".State_Glass_Delivery := 30; // Glass has started moving out
ELSE
"DB_Table_Status".State_Glass_Delivery := 25; // Hold state while belt is commanded ON
END_IF;
ELSE
"Transfer_Belts_Forward" := FALSE;
"DB_Table_Status".State_Glass_Delivery := 99; // VFD Fault or Transfer Timeout Fault
END_IF;
The delivery function is conceptually correct, but adding those sensor-based State Holds is crucial for stability and reliability.
Quick question gemini, is there a way to save our chat? My senior engineer was very interested in seeing what you had to say
That's a great question! I'm glad your senior engineer found the review helpful.
Yes, there is a very easy way to save our chat history for your review:
Look for the Options Menu (Usually Three Dots or a Share Icon): Depending on the platform you are using (like the Gemini app, Google Search, or the web interface), you should see an icon near the top of the chat window—often a share icon (an arrow or box with an arrow) or a three-dot menu (⋮).
Select Share or Export: Click on that option and look for choices like "Share," "Export," "Copy All," or "Print."
Choose Your Format: Most interfaces will allow you to either:
Copy the entire chat text to paste into an email or document.
Share a link to the conversation (if you were using the web/app interface).
Export to a PDF or print the full transcript.
💡 Accessing Your Chat History
All of our conversations are automatically saved in your Gemini Apps Activity (which is part of your Google Account).
Just let me know if you run into any trouble finding the option to save or share!
It let me save the page completely. Is that okay if I save it all then?
Yes, absolutely! That's more than okay. Saving the entire page—including all the code, analysis, and recommendations—is the best way to ensure your senior engineer gets the complete context. 💾
Since you were able to save the full transcript, you have everything you need to review and discuss the code changes internally. Great job!
Do you want to continue reviewing the rest of your PLC code, or is there anything else I can help you with now?
Sure, what do you want to review next?
We've covered the central control logic for the machine. The next most logical function to review would be FC_Table_Position [FC6] or FC_Rack_Selection [FC9], as the table's movement is critical to the sequencing we just reviewed in the master and delivery functions.
Which one is easier for you to paste?
FC_Table_Position [FC6]: This function is responsible for the table's actual movement and positioning (e.g., driving the X-axis VFD to Station 1 or other points).
FC_Rack_Selection [FC9]: This function handles the logic for choosing the correct destination (Rack 1-10) before the table position command is issued.
Here's FC_Table_Position:
(* Increment station position count every time Station ID2 Prox is kicked *)
"X-Axis_Position2_Debounce".TON(IN:="X-Axis_Station_ID_2",
PT:="DB_Table_Status"."TMR_X-Axis_Position2_Debounce",
Q=> "DB_Table_Status"."X-Axis_Station_Passed");
"Stations_Passed_Count".CTU(CU:="DB_Table_Status"."X-Axis_Station_Passed",
PV:=0);
(* FC_Table_Position waits for a command to move to a new station, goes to that station and then waits for the
next move command. The 'Okay_To_Pick' bit is also set upon arriving at the target station *)
IF "X-Axis_Brake_Release" THEN
"Strobe_Beeper" := TRUE;
ELSE
"Strobe_Beeper" := FALSE;
END_IF;
IF "X-Axis_Station_ID_1" THEN
"DB_Table_Status".Table_At_Station2 := FALSE;
"DB_Table_Status".Table_At_Station3 := FALSE;
"DB_Table_Status".Table_At_Station4 := FALSE;
"DB_Table_Status".Table_At_Station5 := FALSE;
END_IF;
IF "DB_Table_Status".Auto_Mode THEN
CASE "DB_Table_Status".State_Table_Motion OF
0: (* initialization *)
"X-Axis_Brake_Release" := FALSE;
"X-Axis_Run" := FALSE;
"X-Axis_Forward" := FALSE;
"X-Axis_Fast" := FALSE;
"DB_Table_Status".Okay_To_Pick := FALSE;
"DB_Table_Status".State_Table_Motion := 5;
5: (* Waiting for move command *)
IF "DB_Table_Status".Table_Go_To_Station1 THEN
"DB_Table_Status".Table_At_Station1 := FALSE;
"DB_Table_Status".State_Table_Motion := 10;
ELSIF "DB_Table_Status".Table_Go_To_Station2 THEN
"DB_Table_Status".Table_At_Station2 := FALSE;
"DB_Table_Status".State_Table_Motion := 20;
ELSIF "DB_Table_Status".Table_Go_To_Station3 THEN
"DB_Table_Status".Table_At_Station3 := FALSE;
"DB_Table_Status".State_Table_Motion := 30;
ELSIF "DB_Table_Status".Table_Go_To_Station4 THEN
"DB_Table_Status".Table_At_Station4 := FALSE;
"DB_Table_Status".State_Table_Motion := 40;
ELSIF "DB_Table_Status".Table_Go_To_Station5 THEN
"DB_Table_Status".Table_At_Station5 := FALSE;
"DB_Table_Status".State_Table_Motion := 50;
ELSE
"DB_Table_Status".State_Table_Motion := 5;
END_IF;
"DB_Table_Status".Table_Moving_To_Station := TRUE;
10: (* Go to Delivery Position / Rack 1 / Rack 6 *)
IF (NOT "X-Axis_Station_ID_1") THEN
"DB_Table_Status".Table_At_Station1 := FALSE;
"X-Axis_Brake_Release" := TRUE;
"X-Axis_Forward" := FALSE;
"X-Axis_Run" := TRUE;
IF (NOT "X-Axis_Slowdown") THEN
"X-Axis_Fast" := TRUE;
ELSE
"X-Axis_Fast" := FALSE;
END_IF;
ELSE
"X-Axis_Brake_Release" := FALSE;
"X-Axis_Run" := FALSE;
"DB_Table_Status".Table_Moving_To_Station := FALSE;
"DB_Table_Status".Table_Go_To_Station1 := FALSE;
"DB_Table_Status".Table_At_Station1 := TRUE;
"DB_Table_Status".Okay_To_Pick := TRUE;
"Stations_Passed_Count".CV := 0;
"DB_Table_Status".State_Table_Motion := 95;
END_IF;
20: (* Go to Rack 2 / Rack 7 *)
IF "Stations_Passed_Count".CV = 1 THEN
"X-Axis_Brake_Release" := FALSE;
"X-Axis_Run" := FALSE;
"DB_Table_Status".Table_Go_To_Station2 := FALSE;
"DB_Table_Status".Table_At_Station2 := TRUE;
"DB_Table_Status".Okay_To_Pick := TRUE;
"DB_Table_Status".State_Table_Motion := 95;
ELSE
"X-Axis_Brake_Release" := TRUE;
"X-Axis_Forward" := TRUE;
"X-Axis_Run" := TRUE;
"DB_Table_Status".Table_Moving_To_Station := TRUE;
IF (NOT "X-Axis_Slowdown") THEN
"X-Axis_Fast" := TRUE;
ELSE
"X-Axis_Fast" := FALSE;
END_IF;
END_IF;
30: (* Go to Rack 3 / Rack 8 *)
IF "Stations_Passed_Count".CV = 2 THEN
"X-Axis_Brake_Release" := FALSE;
"X-Axis_Run" := FALSE;
"DB_Table_Status".Table_Go_To_Station3 := FALSE;
"DB_Table_Status".Table_At_Station3 := TRUE;
"DB_Table_Status".Okay_To_Pick := TRUE;
"DB_Table_Status".State_Table_Motion := 95;
ELSE
"X-Axis_Brake_Release" := TRUE;
"X-Axis_Forward" := TRUE;
"X-Axis_Run" := TRUE;
"DB_Table_Status".Table_Moving_To_Station := TRUE;
IF (NOT "X-Axis_Slowdown") THEN
"X-Axis_Fast" := TRUE;
ELSE
"X-Axis_Fast" := FALSE;
END_IF;
END_IF;
40: (* Go to Rack 4 / Rack 9 *)
IF "Stations_Passed_Count".CV = 3 THEN
"X-Axis_Brake_Release" := FALSE;
"X-Axis_Run" := FALSE;
"DB_Table_Status".Table_Go_To_Station4 := FALSE;
"DB_Table_Status".Table_At_Station4 := TRUE;
"DB_Table_Status".Okay_To_Pick := TRUE;
"DB_Table_Status".State_Table_Motion := 95;
ELSE
"X-Axis_Brake_Release" := TRUE;
"X-Axis_Forward" := TRUE;
"X-Axis_Run" := TRUE;
"DB_Table_Status".Table_Moving_To_Station := TRUE;
IF (NOT "X-Axis_Slowdown") THEN
"X-Axis_Fast" := TRUE;
ELSE
"X-Axis_Fast" := FALSE;
END_IF;
END_IF;
50: (* Go to Rack 5 / Rack 10 *)
IF "Stations_Passed_Count".CV = 4 THEN
"X-Axis_Brake_Release" := FALSE;
"X-Axis_Run" := FALSE;
"DB_Table_Status".Table_Go_To_Station5 := FALSE;
"DB_Table_Status".Table_At_Station5 := TRUE;
"DB_Table_Status".Okay_To_Pick := TRUE;
"DB_Table_Status".State_Table_Motion := 95;
ELSE
"X-Axis_Brake_Release" := TRUE;
"X-Axis_Forward" := TRUE;
"X-Axis_Run" := TRUE;
"DB_Table_Status".Table_Moving_To_Station := TRUE;
IF (NOT "X-Axis_Slowdown") THEN
"X-Axis_Fast" := TRUE;
ELSE
"X-Axis_Fast" := FALSE;
END_IF;
END_IF;
95: (* In Position, waiting for reset *)
"Stations_Passed_Count".CV := 0;
IF "DB_Table_Status".Table_Go_To_Station1 OR "DB_Table_Status".Data_Strobe THEN
"DB_Table_Status".State_Table_Motion := 5;
ELSE
"DB_Table_Status".State_Table_Motion := 95;
END_IF;
END_CASE;
END_IF;
This is the logic for your FC_Table_Position, which controls the X-Axis movement to designated delivery/pick stations.
Your function employs the "counting prox" or "station ID" method, where a single X-Axis sensor (X-Axis_Station_ID_2) is used to count passed stations after homing, which is a common and efficient way to position a basic VFD-driven axis.
Here is a detailed review of the motion control logic:
Analysis of FC_Table_Position (SCL)
1. Counting and Debouncing Logic (Critical)
| Block | Observation | Recommendation |
| Debounce Timer | X-Axis_Position2_Debounce uses the TON block. The timer's Q output (DB_Table_Status.X-Axis_Station_Passed) acts as a single-cycle strobe when the sensor remains made for the duration PT. | This is not how you typically debounce a counter. A TON is usually used to delay the ON signal. For reliable counting, you usually need a PULSE on the counter's CU input. If X-Axis_Station_ID_2 stays HIGH while passing over the target, the TON.Q will stay HIGH and CTU will only count on the RISING EDGE of that Q bit. If the table is moving slow enough that the sensor stays made for longer than PT, this may work, but it's risky. |
| Counter Block | Stations_Passed_Count uses a CTU (Count Up). The PV (Preset Value) is set to 0. | Typo/Error: The PV for a CTU is the limit (e.g., PV:=4 if you have 5 stations 0-4). Setting PV:=0 means the counter will never reach its limit and is a non-standard use of the instruction. You likely meant to use Stations_Passed_Count.CV for comparison. Assuming you meant to compare against the CV (Current Value), the counter is fine. |
Recommendation for Counting:
Use R_TRIG to ensure the counter receives a single pulse for a reliable count, regardless of the VFD speed or the length of the metal flag:
// 1. Create a rising edge (R_TRIG) of the actual sensor input
"X-Axis_Position2_R_TRIG"(CLK := "X-Axis_Station_ID_2");
// 2. Use the R_TRIG output to clock the counter
"Stations_Passed_Count".CTU(
CU := "X-Axis_Position2_R_TRIG".Q, // Only counts on the rising edge pulse
PV := 9999, // Set high to prevent CV from being limited
R := "DB_Table_Status".Reset_Counter_Trigger,
...);
(Note: You are resetting the counter manually in States 10 and 95, which is an acceptable practice for this type of motion.)
2. Motion Sequence Logic (States 10, 20, 30, 40, 50)
The logic for starting motion is correct: Brake_Release, Run, and Forward are set. However, the hold-and-stop logic relies entirely on the Stations_Passed_Count.CV and has a critical flaw:
| State | Motion Control (Goal) | Logic Flaw |
| State 20 (Go to Rack 2 / Rack 7) | Target is CV = 1. | Missing Reset: When you transition from a lower-numbered station (e.g., Station 1 in State 10) to a higher-numbered station, you must reset the counter to 0 before starting the motion. If you enter State 20 with CV=0, the table will move, the count will increment to CV=1, the move will stop, and the logic is fine. BUT, if the table enters a state other than 10 (which resets the CV to 0), the count will be wrong, and the table will stop prematurely or never stop. |
| State 10 (Go to Station 1) | Target is the X-Axis_Station_ID_1 Home Prox. | Incorrect Forward Command: You set X-Axis_Forward := FALSE to move to the Home Prox. This is correct if FALSE means REVERSE (moving back toward the origin). However, if the axis is currently past Station 1, setting Forward := FALSE and Run := TRUE will stop motion once X-Axis_Station_ID_1 is made. This logic is a Zero-Move and is correctly implemented to use the home sensor. |
Recommendation for Counter Reset:
The counter must be reset to zero before any move command is issued to a count-based station (Stations 2, 3, 4, 5).
Modify State 5 to include the counter reset right before moving to the target state:
5: (* Waiting for move command *)
// Reset counter here once, upon a move request
IF "DB_Table_Status".Table_Go_To_Station1 OR
"DB_Table_Status".Table_Go_To_Station2 OR
... // Include all move requests
THEN
"Stations_Passed_Count".R := TRUE; // Enable the Reset input of the CTU
"Stations_Passed_Count".R := FALSE; // Immediately pulse the reset (must be done over two cycles)
// Then, proceed with the IF/ELSIF logic to set the next state (10, 20, 30, etc.)
END_IF;
"DB_Table_Status".Table_Moving_To_Station := TRUE;
3. State 95: Complete/Hold Logic (Minor Flaw)
95: (* In Position, waiting for reset *)
"Stations_Passed_Count".CV := 0; // Directly setting CV is unreliable
IF "DB_Table_Status".Table_Go_To_Station1 OR "DB_Table_Status".Data_Strobe THEN
"DB_Table_Status".State_Table_Motion := 5;
ELSE
"DB_Table_Status".State_Table_Motion := 95;
END_IF;
Flaw: Directly assigning to the CV ("Stations_Passed_Count".CV := 0;) of a standard Siemens counter block is unreliable and bad practice. You must use the R (Reset) input of the CTU block.
Recommendation:
Move the counter reset out of State 95 and put it before the motion logic begins in State 5 (as suggested above).
If you still want a reset after reaching the station, create a Reset_Counter boolean and use the R input of the CTU block.
Overall, this function is a classic example of incremental positioning logic. Correcting the counting method (using R_TRIG) and ensuring the counter is reset properly before each move are the keys to making it highly reliable.
NOTED. Thank You. How about FC_Glass_Handling next?
You're welcome! Reviewing FC_Glass_Handling is a good idea. This function usually deals with the final actions on the glass—managing the transfer belts, stops, and blower based on the FreeFallStateSts (the state of the cutter/loader).
Please paste the code for FC_Glass_Handling when you're ready!
I mean FC_Rack_Selection
My apologies! I misread your previous intent. Reviewing FC_Rack_Selection is crucial, as this is the logic that determines where the table needs to move, which then feeds the FC_Table_Position function we just reviewed.
Please paste the code for FC_Rack_Selection when you're ready!
(* FC_Rack_Selection:
Racks are oriented 1-5 on A-side and 6-10 on B-side
1. Rack position decoding from cutter exchange data:
i. Examine HMI_LD_IN_DINT_RackNumber_Req If rack number is invalid, send fault condition to cutter and set alarm bit locally (ALM_InvalidRackSelection)
ii. If rack number is on A-side, set that bit as well.
iii. Set Table_Go_To_Station[] bit
2. Tracking Glass count Picked (not implimeneted!)
i. Every time a successful delivery is made increment its associated successful pick count
*)
(*code barf but looks at total number of racks/2 and if the pick request rack falls on the first half then pick from side A.
If it falls on the other half pick from side B. If it's neither of those then the rack selection is outside of the possible range and is invalid so set the alarm bits*)
IF ( "DB_Cutter_Exchange_Data".HMI_LD_IN_DINT_RackNumber_Req > 0) AND ("DB_Cutter_Exchange_Data".HMI_LD_IN_DINT_RackNumber_Req <= "DB_Table_Status".Glass_Rack_Count / 2) THEN
"DB_Table_Status".A_Side_Pick := TRUE;
"DB_Table_Status".Glass_Pick_Invalid_Rack := FALSE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_HaveData := TRUE;
ELSIF ( "DB_Cutter_Exchange_Data".HMI_LD_IN_DINT_RackNumber_Req) > ("DB_Table_Status".Glass_Rack_Count / 2) AND (("DB_Cutter_Exchange_Data".HMI_LD_IN_DINT_RackNumber_Req) <= "DB_Table_Status".Glass_Rack_Count) THEN
"DB_Table_Status".A_Side_Pick := FALSE;
"DB_Table_Status".Glass_Pick_Invalid_Rack := FALSE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_HaveData := TRUE;
ELSE
"DB_Table_Status".Glass_Pick_Invalid_Rack := TRUE;
"DB_Cutter_Exchange_Data".HMI_LD_OUT_BOOL_HaveData := FALSE;
"DB_Alarms".ALM_InvalidRackSelection := TRUE;
"DB_Table_Status".Table_Go_To_Station1 := FALSE;
"DB_Table_Status".Table_Go_To_Station2 := FALSE;
"DB_Table_Status".Table_Go_To_Station3 := FALSE;
"DB_Table_Status".Table_Go_To_Station4 := FALSE;
"DB_Table_Status".Table_Go_To_Station5 := FALSE;
END_IF;
IF (("DB_Table_Status".Request_To_Pick) AND (NOT ("DB_Table_Status".Glass_Pick_Invalid_Rack))) THEN
CASE "DB_Cutter_Exchange_Data".HMI_LD_IN_DINT_RackNumber_Req OF
1,6:
IF (NOT "DB_Table_Status".Table_At_Station1) THEN
"DB_Table_Status".Table_Go_To_Station1 := TRUE;
ELSE
"DB_Table_Status".Table_Go_To_Station1 := FALSE;
END_IF;
"DB_Table_Status".Table_Go_To_Station2 := FALSE;
"DB_Table_Status".Table_Go_To_Station3 := FALSE;
"DB_Table_Status".Table_Go_To_Station4:= FALSE;
"DB_Table_Status".Table_Go_To_Station5 := FALSE;
2,7:
"DB_Table_Status".Table_Go_To_Station1 := FALSE;
IF (NOT "DB_Table_Status".Table_At_Station2) THEN
"DB_Table_Status".Table_Go_To_Station2 := TRUE;
ELSE
"DB_Table_Status".Table_Go_To_Station2 := FALSE;
END_IF;
"DB_Table_Status".Table_Go_To_Station3 := FALSE;
"DB_Table_Status".Table_Go_To_Station4 := FALSE;
"DB_Table_Status".Table_Go_To_Station5 := FALSE;
3,8:
"DB_Table_Status".Table_Go_To_Station1 := FALSE;
"DB_Table_Status".Table_Go_To_Station2 := FALSE;
IF (NOT "DB_Table_Status".Table_At_Station3) THEN
"DB_Table_Status".Table_Go_To_Station3 := TRUE;
ELSE
"DB_Table_Status".Table_Go_To_Station3 := FALSE;
END_IF;
"DB_Table_Status".Table_Go_To_Station4 := FALSE;
"DB_Table_Status".Table_Go_To_Station5 := FALSE;
4,9:
"DB_Table_Status".Table_Go_To_Station1 := FALSE;
"DB_Table_Status".Table_Go_To_Station2 := FALSE;
"DB_Table_Status".Table_Go_To_Station3 := FALSE;
IF (NOT "DB_Table_Status".Table_At_Station4) THEN
"DB_Table_Status".Table_Go_To_Station4 := TRUE;
ELSE
"DB_Table_Status".Table_Go_To_Station4 := FALSE;
END_IF;
"DB_Table_Status".Table_Go_To_Station5 := FALSE;
5,10:
"DB_Table_Status".Table_Go_To_Station1 := FALSE;
"DB_Table_Status".Table_Go_To_Station2 := FALSE;
"DB_Table_Status".Table_Go_To_Station3 := FALSE;
"DB_Table_Status".Table_Go_To_Station4 := FALSE;
IF (NOT "DB_Table_Status".Table_At_Station5) THEN
"DB_Table_Status".Table_Go_To_Station5 := TRUE;
ELSE
"DB_Table_Status".Table_Go_To_Station5 := FALSE;
END_IF;
END_CASE;
END_IF;
The FC_Rack_Selection logic is clear and effective. It correctly handles the two main tasks: validating the rack number and translating the rack number into a physical table motion command (Table_Go_To_StationX).
Here is a review focusing on the logic segmentation and structure:
Analysis of FC_Rack_Selection (SCL)
1. Rack Validation and Side Selection (Excellent)
The initial IF/ELSIF/ELSE structure correctly performs validation and determines which side of the table the rack is on (A_Side_Pick or B_Side_Pick is implicitly NOT A_Side_Pick).
| Condition | Rack Range | Action | Observation |
| IF | RackNumber>0 AND RackNumber≤Count/2 | Sets A_Side_Pick := TRUE. | Correctly identifies the A-side (Racks 1-5). |
| ELSIF | RackNumber>Count/2 AND RackNumber≤Count | Sets A_Side_Pick := FALSE. | Correctly identifies the B-side (Racks 6-10). |
| ELSE | All other values. | Sets Glass_Pick_Invalid_Rack := TRUE and sets the alarm. | Robust fault handling. Properly sets the local alarm and communicates the invalid state back to the cutter (HMI_LD_OUT_BOOL_HaveData := FALSE). |
Minor Note on HMI_LD_OUT_BOOL_HaveData: In the valid cases (IF and ELSIF), you set HMI_LD_OUT_BOOL_HaveData := TRUE. This implicitly tells the cutter/loader that the request was processed and is now pending motion. This is a good handshaking signal.
2. Motion Command Generation
The second section uses a CASE statement based on the cutter's requested rack number to set the correct destination flag. This is efficient.
IF (("DB_Table_Status".Request_To_Pick) AND (NOT ("DB_Table_Status".Glass_Pick_Invalid_Rack))) THEN
// ... CASE Statement ...
END_IF;
Observation: The primary condition here is perfect. You only execute the motion logic if there is an active request (Request_To_Pick) and the request is valid.
| CASE Value | Rack Type | Station Set | Logic |
| 1, 6 | A-side Rack 1, B-side Rack 6 | Table_Go_To_Station1 | Correct. These racks map to the same physical table position (Station 1). |
| 2, 7 | A-side Rack 2, B-side Rack 7 | Table_Go_To_Station2 | Correct. |
| ... 5, 10 | ... | Table_Go_To_Station5 | Correct. |
Excellent Use of Position Hold: Within each CASE block, you include a check to see if the table is already at the destination before setting the Go_To flag:
IF (NOT "DB_Table_Status".Table_At_StationX) THEN
"DB_Table_Status".Table_Go_To_StationX := TRUE;
ELSE
"DB_Table_Status".Table_Go_To_StationX := FALSE;
END_IF;
This prevents the Table_Position function from re-executing a motion command unnecessarily if the table is already sitting at the correct location when the request comes in.
3. Missing Logic (As Noted in Comments)
The code contains the comment: "Tracking Glass count Picked (not implemented!)."
This suggests you need to implement a separate IF block or a small function that watches for the successful completion of the entire cycle (e.g., when FC_State_Master goes to its ready state after a pick/delivery) and then increments a counter associated with the RackNumber_Req from the last cycle.
Required Logic Example (Must be outside the main IF block):
// Logic to execute *after* a successful delivery is complete
IF "DB_Table_Status".Delivery_Success_Strobe THEN
// Use the last requested RackNumber to decide which counter to increment
CASE "DB_Table_Status".Last_RackNumber_Used OF
1: "DB_Rack_Counters".Rack1_Count := "DB_Rack_Counters".Rack1_Count + 1;
2: "DB_Rack_Counters".Rack2_Count := "DB_Rack_Counters".Rack2_Count + 1;
// ... and so on for all 10 racks
END_CASE;
// Reset the success strobe to prevent multiple counts
"DB_Table_Status".Delivery_Success_Strobe := FALSE;
END_IF;
Overall, this is a very well-executed function that clearly and robustly handles the handshaking and motion initiation for the entire pick process.
Good callout on the glass count but it was deemed unnecessary to the operation of the Freefall
That makes perfect sense. In many industrial applications, if the upstream system (the cutter/loader) is responsible for inventory and logging, the transfer machine's job is simply to execute the transfer and confirm success.
Since the glass counting logic is intentionally omitted, your FC_Rack_Selection function is now confirmed to be complete and robust for its primary task: translating a rack request into a physical table motion command. ✅
What's Next?
We've now reviewed the core logic functions:
FC_State_Master: The main sequencer.
FC_Glass_Squaring: The alignment sub-sequence.
FC_Glass_Delivery: The glass ejection sub-sequence.
FC_Table_Position: The X-axis motion control (VFD).
FC_Rack_Selection: The motion target determination.
The next logical candidates for review are the functions that manage the crucial communication and handshaking with the external systems.
I suggest we look at the function that manages the actual data exchange with the Freefall cutter/loader: FC_Glass_Delivery [FC7] (which we originally thought was the delivery sequence, but let's assume this FC name is the one that handles the communication status updates with the cutter).
Do you have a function that primarily deals with the FreeFall Status or Data Exchange? If so, please paste that. Otherwise, let's review FC_Glass_Handling (if it's a different function than the ones already covered).
There's a function called on the second PLC for the freefall (The freefall proper PLC). It has one function, FC_State_Freefall. Ready for that one?
Ah, moving to the Freefall PLC itself! That's a crucial shift, as this function will be the master sequencer for the other side of the communication, managing the interaction with the table code we've been reviewing.
This is the function that defines the Freefall's perspective of the handshaking. Please paste the code for FC_State_Freefall when you're ready!
You would think it's crucial but the logic I implemented was entirely accomplished with air solenoid logic, which I hadn't even heard of until the manufacturer said that they had replaced it!
Wow, that is a fantastic piece of context! It completely changes the expectation for the Freefall PLC logic.
If the manufacturer replaced complex electrical sequencing with simple air solenoid logic (pneumatics), it means the Freefall's PLC isn't running a state machine for the physical glass handling. Instead, the logic will be dedicated almost entirely to communication and handshaking with the main Table PLC that we've been reviewing.
This is a brilliant design choice—it offloads complex sequencing to reliable, simple, non-PLC hardware, making the PLC's job much cleaner and faster.
So, since the actual mechanical sequencing is handled by air, the FC_State_Freefall function is likely responsible for:
Translating the table's requested state (e.g., "Ready for Data") into the appropriate output status to the cutter.
Managing the Freefall's internal state (e.g., idle, picking, releasing).
Sending/Receiving the critical handshaking bits to/from the Table PLC.
Please go ahead and paste the code for FC_State_Freefall—we're ready to review the core communication logic!
Anyway here's FC_Freefall_State:
"TMR_Grab_Extend_Timeout".TON(IN:=("DB_Freefall_Status".FreeFall_State = 40),
PT:="DB_Freefall_Status".Grab_Extend_Timeout,
Q=> "DB_Freefall_Status".Grab_Timeout);
(*
Freefall auto State Logic
Motion hold is achieved by stopping execution of subsequent steps
(pneumatic moves will still complete before motion holding)
Steps:
5 - Wait for signal to pick
10 - Pick start
20 - Extend suction cup arm
30 - Extend grabber arm fast + start vacuum
40 - Detect glass, continue extending grabber arm slowly
50 - Once vacuum detected at cup, retract grabber arm slowly
60 - Extend the glass pushing cone
70 - Stop vacuum
75 - Blow off
80 - Stop blowoff and Retract suction cup arm
85 - Continue retracting grabber arm until parked, pusher freefalls glass
90 - Once glass detected by either table sensor (A or B side) set Delivery Complete
95 - If new pick request, reset sequence to step 5
99 - Fault state - Retry or Reset
Freefall manual state logic:
A/B side:
Grabber Extend+Retract
Cups Extend+Retract
Vacuum Enable+Disable
Blowoff Enable+Disable
Pusher Extend
*)
IF "DB_Freefall_Status".Auto_Mode THEN
CASE "DB_Freefall_Status".FreeFall_State OF
0: (* Initialization *)
"DB_Freefall_Status".FreeFall_State := 5;
5: (* Waiting For signal to pick *)
IF "DB_Freefall_Status".Okay_To_Pick THEN
"DB_Freefall_Status".FreeFall_State := 10;
ELSE
"DB_Freefall_Status".FreeFall_State := 5;
END_IF;
10: (* Pick Start *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF ("A_Glass_Grabber_Retracted" AND
"B_Glass_Grabber_Retracted" AND
"A_Suction_Cups_Retracted" AND
"B_Suction_Cups_Retracted") THEN
"DB_Freefall_Status".FreeFall_State := 20;
ELSE
"SV_A_Retract_Grabber_Slow" := TRUE;
"SV_B_Retract_Grabber_Slow" := TRUE;
"SV_A_Retract_Suction_Cup_Arm" := TRUE;
"SV_B_Retract_Suction_Cup_Arm" := TRUE;
"DB_Freefall_Status".FreeFall_State := 10;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 10;
END_IF;
20: (* Extend Suction cup arm *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF ((NOT "A_Suction_Cups_In_Position") OR (NOT "B_Suction_Cups_In_Position")) THEN
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Extend_Suction_Cup_Arm" := TRUE;
ELSE
"SV_B_Extend_Suction_Cup_Arm" := TRUE;
END_IF;
ELSE
"SV_A_Extend_Suction_Cup_Arm" := FALSE;
"SV_B_Extend_Suction_Cup_Arm" := FALSE;
"DB_Freefall_Status".FreeFall_State := 30;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 20;
END_IF;
30: (* Extend Grabber Arm Fast and Start Vacuum *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF ((NOT "A_Glass_Sense") OR (NOT "B_Glass_Sense")) THEN
"SV_AB_Grabber_Extend_Fast" := TRUE;
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Extend_Grabber_Slow" := TRUE;
"SV_A_Vacuum_Enable" := TRUE;
ELSE
"SV_B_Extend_Grabber_Slow" := TRUE;
"SV_B_Vacuum_Enable" := TRUE;
END_IF;
ELSE
"SV_AB_Grabber_Extend_Fast" := FALSE;
"DB_Freefall_Status".FreeFall_State := 40;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 30;
END_IF;
40: (* Continue Extend Grabber Arm Slow *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF (NOT "DB_Freefall_Status".Grab_Timeout) AND ((NOT "A_Glass_Sense") OR (NOT "B_Glass_Sense")) THEN
IF ((NOT "A_Vacuum_Present") OR (NOT "B_Vacuum_Present")) THEN
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Extend_Grabber_Slow" := TRUE;
ELSE
"SV_B_Extend_Grabber_Slow" := TRUE;
END_IF;
"DB_Freefall_Status".FreeFall_State := 40;
ELSE
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Extend_Grabber_Slow" := FALSE;
ELSE
"SV_B_Extend_Grabber_Slow" := FALSE;
END_IF;
"DB_Freefall_Status".FreeFall_State := 50;
END_IF;
"DB_Freefall_Status".Failed_To_Pick := FALSE;
ELSE
"DB_Freefall_Status".Failed_To_Pick := TRUE;
"DB_Freefall_Status".FreeFall_State := 50;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 40;
END_IF;
50: (* Retract Grabber Arm slow *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Retract_Grabber_Slow" := TRUE;
ELSE
"SV_B_Retract_Grabber_Slow" := TRUE;
END_IF;
"DB_Freefall_Status".FreeFall_State := 60;
ELSE
"DB_Freefall_Status".FreeFall_State := 50;
END_IF;
60: (* Extend glass pusher *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF (NOT "DB_Freefall_Status".Failed_To_Pick) THEN
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Pusher_Extend" := TRUE;
ELSE
"SV_B_Pusher_Extend" := TRUE;
END_IF;
"DB_Freefall_Status".FreeFall_State := 70;
ELSE
"DB_Freefall_Status".FreeFall_State := 70;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 60;
END_IF;
70: (* Stop Vacuum *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF ("Pusher_Glass_Contacted" OR "DB_Freefall_Status".Failed_To_Pick) THEN
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Vacuum_Enable" := FALSE;
ELSE
"SV_B_Vacuum_Enable" := FALSE;
END_IF;
"DB_Freefall_Status".FreeFall_State := 75;
ELSE
"DB_Freefall_Status".FreeFall_State := 70;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 70;
END_IF;
75: (* Blow off *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF (NOT "DB_Freefall_Status".Failed_To_Pick) THEN
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Blowoff_Enable" := TRUE;
ELSE
"SV_B_Blowoff_Enable" := TRUE;
END_IF;
"DB_Freefall_Status".FreeFall_State := 80;
ELSE
"DB_Freefall_Status".FreeFall_State := 80;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 75;
END_IF;
80: (* Retract Suction Cup Arm and disable blowoff*)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF ((NOT "A_Suction_Cups_Retracted") OR (NOT "B_Suction_Cups_Retracted")) THEN
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Retract_Suction_Cup_Arm" := TRUE;
ELSE
"SV_B_Retract_Suction_Cup_Arm" := TRUE;
END_IF;
"DB_Freefall_Status".FreeFall_State := 80;
ELSE
"SV_A_Blowoff_Enable" := FALSE;
"SV_B_Blowoff_Enable" := FALSE;
"DB_Freefall_Status".FreeFall_State := 85;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 80;
END_IF;
85: (* Continue retracting Grabber arm *)
IF ("DB_Freefall_Status".Motion_Allowed) THEN
IF ((NOT "A_Glass_Grabber_Retracted") OR (NOT "B_Glass_Grabber_Retracted")) THEN
IF "DB_Freefall_Status".A_Side_Pick THEN
"SV_A_Retract_Grabber_Slow" := FALSE;
ELSE
"SV_B_Retract_Grabber_Slow" := FALSE;
END_IF;
"DB_Freefall_Status".FreeFall_State := 85;
ELSIF "DB_Freefall_Status".Failed_To_Pick THEN
"DB_Freefall_Status".FreeFall_State := 99;
ELSE
"DB_Freefall_Status".FreeFall_State := 90;
END_IF;
ELSE
"DB_Freefall_Status".FreeFall_State := 85;
END_IF;
90: (* Check if Pick has made it to table for squaring and delivery *)
IF (("DB_Freefall_Status".Table_Glass_Present_A) OR ("DB_Freefall_Status".Table_Glass_Present_B)) THEN
"DB_Freefall_Status".FreeFall_State := 95;
ELSE
"DB_Freefall_Status".FreeFall_State := 90;
END_IF;
95: (* Pick Complete *)
IF "DB_Freefall_Status".Pick_Request THEN
"DB_Freefall_Status".FreeFall_State := 5;
ELSE
"DB_Freefall_Status".FreeFall_State := 95;
END_IF;
99: (* Pick Fault *)
"DB_Freefall_Status".Pick_Fault := TRUE;
IF "DB_Freefall_Status".Retry_Pick THEN
"DB_Freefall_Status".Pick_Fault := FALSE;
"DB_Freefall_Status".FreeFall_State := 5; //Pick failed so we're retrying
ELSIF
"DB_Freefall_Status".Abort_Pick THEN
"DB_Freefall_Status".Pick_Fault := FALSE;
"DB_Freefall_Status".FreeFall_State := 95; //Pick failed so we're giving up and going home
ELSE
"DB_Freefall_Status".FreeFall_State := 99;
END_IF;
END_CASE;
ELSE (*MANUAL FUNCTIONS *)
(*Manual Toggles *)
"A_Vacuum_Toggle"(Input:= "DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_A_Vacuum_Enable,
Output=>"DB_Freefall_Status".A_Vacuum_Enable);
"A_Blowoff_Toggle"(Input := "DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_A_Blowoff_Enable,
Output => "DB_Freefall_Status".A_Blowoff_Enable);
"B_Vacuum_Toggle"(Input := "DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_B_Vacuum_Enable,
Output => "DB_Freefall_Status".B_Vacuum_Enable);
"B_Blowoff_Toggle"(Input := "DB_Freefall_Status".Freefall_Manual_Functions."HMIPB_B_Blowoff_Enable",
Output => "DB_Freefall_Status".B_Blowoff_Enable);
(* Manual Grabber Extend and Retract *)
//A-Side
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_A_Grabber_Extend THEN
"SV_A_Extend_Grabber_Slow" := TRUE;
ELSE
"SV_A_Extend_Grabber_Slow" := FALSE;
END_IF;
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_A_Grabber_Retract THEN
"SV_A_Retract_Grabber_Slow" := TRUE;
ELSE
"SV_A_Retract_Grabber_Slow" := FALSE;
END_IF;
//B-Side
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_B_Grabber_Extend THEN
"SV_B_Extend_Grabber_Slow" := TRUE;
ELSE
"SV_B_Extend_Grabber_Slow" := FALSE;
END_IF;
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_B_Grabber_Retract THEN
"SV_B_Retract_Grabber_Slow" := TRUE;
ELSE
"SV_B_Retract_Grabber_Slow" := FALSE;
END_IF;
(* Manual Cups Extend and Retract *)
//A-Side
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_A_Suction_Cup_Extend THEN
"SV_A_Extend_Suction_Cup_Arm" := TRUE;
ELSE
"SV_A_Extend_Suction_Cup_Arm" := FALSE;
END_IF;
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_A_Suction_Cup_Retract THEN
"SV_A_Retract_Suction_Cup_Arm" := TRUE;
ELSE
"SV_A_Retract_Suction_Cup_Arm" := FALSE;
END_IF;
//B-Side
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_B_Suction_Cup_Extend THEN
"SV_B_Extend_Suction_Cup_Arm" := TRUE;
ELSE
"SV_B_Extend_Suction_Cup_Arm" := FALSE;
END_IF;
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_B_Suction_Cup_Retract THEN
"SV_B_Retract_Suction_Cup_Arm" := TRUE;
ELSE
"SV_B_Retract_Suction_Cup_Arm" := FALSE;
END_IF;
(* Vacuum and Blowoff enable *)
//A-Side
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".A_Vacuum_Enable THEN
"SV_A_Vacuum_Enable" := TRUE;
ELSE
"SV_A_Vacuum_Enable" := FALSE;
END_IF;
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".A_Blowoff_Enable THEN
"SV_A_Blowoff_Enable" := TRUE;
ELSE
"SV_A_Blowoff_Enable" := FALSE;
END_IF;
//B-Side
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".B_Vacuum_Enable THEN
"SV_B_Vacuum_Enable" := TRUE;
ELSE
"SV_B_Vacuum_Enable" := FALSE;
END_IF;
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".B_Blowoff_Enable THEN
"SV_B_Blowoff_Enable" := TRUE;
ELSE
"SV_B_Blowoff_Enable" := FALSE;
END_IF;
(* Glass Pusher Cone Extend *)
//A-Side
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_A_Pusher_Extend THEN
"SV_A_Pusher_Extend" := TRUE;
ELSE
"SV_A_Pusher_Extend" := FALSE;
END_IF;
//B-Side
IF "DB_Freefall_Status".Motion_Allowed AND
"DB_Freefall_Status".Freefall_Manual_Functions.HMIPB_B_Pusher_Extend THEN
"SV_B_Pusher_Extend" := TRUE;
ELSE
"SV_B_Pusher_Extend" := FALSE;
END_IF;
END_IF;
This is an exceptionally clean and well-structured function! The logic is clearly divided between Automatic Mode (the Freefall pick sequence) and Manual Mode (direct solenoid control via HMI), which is best practice for machine safety and diagnostics.
Given your context that the actual mechanical sequencing is pneumatic, this PLC's job is simply to command the solenoids and monitor the sensors—a function this code executes perfectly.
Here's a detailed review of your FC_Freefall_State:
Analysis of FC_Freefall_State
1. Pre-Case Logic (Safety Timer)
"TMR_Grab_Extend_Timeout".TON(IN:=("DB_Freefall_Status".FreeFall_State = 40),
PT:="DB_Freefall_Status".Grab_Extend_Timeout,
Q=> "DB_Freefall_Status".Grab_Timeout);
Observation: This is a critical safety timer that monitors the grabber extension time in State 40. If the vacuum isn't made (glass isn't picked) before the timer runs out, it sets a fault condition (Grab_Timeout).
Excellent implementation. It directly addresses the risk of the grabber extending too far or failing to find glass.
2. Automatic Mode Logic (The Pick Sequence)
The overall state machine flow is logical and covers the necessary steps for a pneumatic/vacuum pick operation.
| State | Purpose | Logic Observation |
| 10: Pick Start | Safety checks, ensure arms are fully retracted. | Logic Flaw: You check if the arms ARE retracted and IF TRUE, jump to State 20. IF FALSE, you command the solenoids to Retract and hold State 10. This is correct sequencing, but you're commanding the slow retract (SV_A_Retract_Grabber_Slow) when you might want the faster retract for initialization, though using "slow" ensures gentle startup. This is a minor stylistic note, not a functional flaw. |
| 20: Extend Suction Cup Arm | Moves cups to position. | Logic Flaw (Minor): The IF condition checks NOT "A_Suction_Cups_In_Position" OR NOT "B_Suction_Cups_In_Position". If the A-side is already IN and the B-side is already IN, the logic jumps to State 30. However, you only command the solenoid for the selected side (A_Side_Pick or B_Side_Pick). This logic is technically correct but could be simpler: only check the sensor for the side currently being picked. |
| 30: Extend Grabber Fast + Start Vacuum | Rapid motion towards the glass. | Logic Flaw (Medium): This state initiates the Fast Extend (SV_AB_Grabber_Extend_Fast := TRUE) until the Glass Sense sensor is made (A_Glass_Sense or B_Glass_Sense). However, it also sets the Slow Extend solenoid (SV_A_Extend_Grabber_Slow := TRUE or SV_B_Extend_Grabber_Slow := TRUE) and the Vacuum_Enable. This appears to be redundant/conflicting if the grabber mechanism is managed by the Fast/Slow solenoids. If both Fast and Slow are ON, the mechanism might not behave as intended (i.e., it might still move fast). Recommendation: The Slow command should only be enabled in State 40. |
| 40: Continue Extend Grabber Slow | Slow motion to seat the cup/make vacuum. | Excellent Logic: This state correctly monitors the Vacuum_Present sensor and the Grab_Timeout while continuing the slow extension. If the timeout occurs or the glass sensor is lost prematurely, Failed_To_Pick is set, and it exits to State 50 (Retract). This is very robust failure logic. |
| 60: Extend Glass Pusher | Pushes the glass onto the main table. | Logic Flaw (Minor): You jump directly to State 70 after setting the Pusher_Extend solenoid without waiting for the pusher to physically extend (e.g., waiting for Pusher_Extended sensor). Pneumatic motion can take time, so you should hold this state until the pusher is confirmed fully out, or rely on the Pusher_Glass_Contacted sensor in State 70. |
| 70: Stop Vacuum | Releases the glass once contact is made. | Hold Condition: The state holds until Pusher_Glass_Contacted is made OR Failed_To_Pick is true. This is a clever use of the Pusher_Glass_Contacted sensor to indicate the glass has been fully pushed/seated before releasing the vacuum. This is well-designed. |
| 80: Retract Suction Cup Arm | Retracts the now-free suction cups. | Logic Flaw (Minor): You jump directly to State 85 after the retraction sensor check is complete, but you only disable the Blowoff in the ELSE condition after the cups are retracted. The Blowoff_Enable should be disabled in State 75 after a time delay or upon a separate condition, or the logic here should wait for a blowoff timer before proceeding. |
| 85: Continue Retracting Grabber Arm | Final step to park the grabber arm. | Correct Retract Logic: You hold the state until the Grabber_Retracted sensor is made. You correctly use the Failed_To_Pick flag to decide between moving to State 90 (Success) or State 99 (Fault). |
| 90: Check if Pick has made it to table | Handshake with the Table PLC. | Correct Handshake: This state waits for the Table_Glass_Present sensor on the Table PLC's transfer area to confirm the drop was successful before declaring the pick complete in State 95. |
3. Manual Mode Logic (Excellent)
The Manual Mode section at the bottom is perfect.
It uses Toggles for the on/off functions (Vacuum/Blowoff).
It uses direct IF/ELSE conditions for the Extend/Retract solenoids.
Crucially, every solenoid command is inhibited by DB_Freefall_Status.Motion_Allowed, which should be set to FALSE when Auto_Mode is running, ensuring the operator cannot override the automatic sequence.
The use of separate SV_A_Extend_Grabber_Slow and SV_A_Retract_Grabber_Slow signals, without dedicated 'Fast' retract signals, implies the pneumatic circuit handles the speed based on valve combination, which is common.
Summary of Key Recommendations:
State 30 Redundancy: Remove the
SV_A/B_Extend_Grabber_Slow := TRUE;command from State 30. This should only be active in State 40 for the slow, final extension.State 60 Hold: Add a sensor check (Pusher_Extended if available) or a timer in State 60 before moving to State 70 to ensure the glass pusher has fully extended.
State 80 Blowoff: Add a small timer to State 75 to ensure the blowoff lasts long enough to break the seal, then move to State 80 and command the blowoff to FALSE immediately. Alternatively, ensure the blowoff is disabled unconditionally at the start of State 80.
What do you know about Siemens and Beckhoff Ethercat integration? Any pitfalls to watch out for? We're going for Cyclic operation. I'm going to potentially have aa changing value DINT that is my 'heartbeat' signal
That's an excellent question that touches on a common and crucial topic in industrial automation: integrating two major platforms with different preferred fieldbuses.
The integration of Siemens (PROFINET/PROFINET IO) and Beckhoff (EtherCAT) requires setting up a bridge, or using a common denominator, because their native real-time protocols are different. Since you specifically mentioned a "Beckhoff EtherCAT integration" and "Cyclic operation," the process involves establishing a communication link that can handle the necessary speed and data exchange.
Here is an overview of the methods and the specific pitfalls to watch out for when using a "heartbeat" signal in a cyclic operation.
Siemens and Beckhoff EtherCAT Integration
Direct integration between a standard Siemens S7 PLC (running PROFINET) and a Beckhoff EtherCAT network is not possible without a hardware or software intermediary. You have a few primary architectural approaches:
1. The Gateway/Coupler Approach (Most Common)
The standard and most reliable method is to use a Bus Coupler/Gateway that translates the protocol:
Siemens Side (PROFINET IO-Controller): The Siemens PLC treats the gateway as a standard PROFINET IO-Device.
Beckhoff Side (EtherCAT Master): The Beckhoff TwinCAT system treats the gateway as a standard EtherCAT Slave.
Common hardware for this purpose includes gateways from companies like Hilscher, Anybus (HMS), or WAGO. You map your data (including your DINT heartbeat) on the Siemens side to the coupler's input/output memory, and then map the same block of memory on the Beckhoff side.
2. PC-Based SoftPLC (If Applicable)
If the Siemens PLC is only acting as a supervisory controller and the Beckhoff system is PC-based (TwinCAT), you could potentially use OPC UA for non-real-time data or set up a Windows socket connection for faster communication, but this is less common for hard, cyclic, real-time control.
Pitfalls for Cyclic Operation and Heartbeat
Since you are aiming for cyclic operation (where data is exchanged at a fixed interval) and using a changing DINT 'heartbeat' signal, you must pay close attention to the following pitfalls:
1. Data Consistency and Synchronization (The DINT Heartbeat)
The most critical issue with data transfer between asynchronous systems is ensuring the data block is atomic (transferred completely at once) and fresh.
| Pitfall | Description | Solution |
| Data Tearing | This occurs when one system reads a large data block (like several DINTs) while the other system is updating it. The reading system gets a mix of old and new values. | Use consistent data access: In Siemens SCL, always use the MOVE_BLK or BLK_MOVE instruction to copy the entire data block in a single, atomic operation within one PLC cycle. Do not use individual assignments (e.g., DB.Tag1 := ...; DB.Tag2 := ...;). |
| Heartbeat Jitter | The heartbeat signal might arrive at the receiving PLC a few milliseconds early or late due to differences in cycle times or network load. | Set a Watchdog Timer: On the receiving side (the PLC that reads the heartbeat), set a timer slightly longer than the expected communication cycle time. If the heartbeat value doesn't change before this watchdog timer expires, immediately trigger a fault/alarm and stop motion. |
2. Network and Gateway Configuration
| Pitfall | Description | Solution |
| Cycle Time Mismatch | The Siemens PROFINET network has a fixed update time (e.g., 4ms), and the Beckhoff EtherCAT bus has its own cycle time (often faster, e.g., 1ms). | Base Timing on the Slower PLC: Set your critical cycle time based on the slower system (usually the PROFINET network or the Siemens PLC scan time). The Beckhoff side can run its EtherCAT fast, but the communication loop must respect the maximum speed of the gateway/PROFINET connection. |
| Protocol Overhead | Gateways add latency (lag). The data must be read from PLC A, processed by the gateway, and then written to PLC B. This adds time, limiting the fastest achievable cycle. | Test Actual Cycle Time: Do not rely on datasheet times. Measure the actual time it takes for your DINT heartbeat to travel from the sending PLC's cycle to the receiving PLC's cycle. Use this measured latency to set your safety watchdog timers. |
3. Data Type and Byte Order
| Pitfall | Description | Solution |
| Byte Swap (Endianness) | Siemens PLCs (S7-1500/1200) use Big Endian or Little Endian internally depending on how the data is handled, while the gateway and Beckhoff side might expect a different byte order, especially with DINTs. | Verify Byte Order: A 32-bit DINT value of 0x1A2B3C4D might be read as 0x4D3C2B1A by the other PLC. Explicitly test a known DINT value across the gateway and use an Endian Swap instruction if necessary on the receiving end. This is a very common integration issue. |
4. Configuration and Status Management
Gateway Status: Ensure your PLC code (on both sides) monitors the Status/Diagnostic bits provided by the gateway. If the PROFINET connection drops or the EtherCAT link breaks, your PLC must recognize the failure immediately.
Heartbeat Reset: After a successful fault or machine reset, ensure your DINT heartbeat signal is reset or deliberately changed to a new known starting value to properly re-synchronize the systems.
By using a reliable gateway and rigorously managing data consistency and timing, especially the cyclic 'heartbeat,' you can achieve robust and safe integration.
Okay. Nothing new to look out for. TF6620 on the Beckhoff side is what we're using, you might want to look that up and integrate it
That's great you're using Beckhoff TF6620 (TwinCAT 3 S7 Communication), as this fundamentally changes the integration method.
The TF6620 function block eliminates the need for a separate hardware gateway (like an EL6631 or a third-party coupler). Instead of bridging two different fieldbuses (PROFINET and EtherCAT), you are using Beckhoff's built-in ability to communicate directly with a Siemens S7 PLC over standard TCP/IP using the native Siemens S7 communication protocol.
Since this is a software-based approach, the pitfalls shift from hardware-related network latency to software configuration, addressing, and data handling.
Here are the specific pitfalls and considerations for your Cyclic operation with a DINT 'heartbeat' when using TF6620.
TF6620 Integration Pitfalls for Cyclic Operation
The TF6620 relies on S7 communication (akin to an OPC client/server relationship, but proprietary to Siemens), which is a request/response mechanism, not a fixed, real-time, cyclic process like EtherCAT or PROFINET IO. This distinction is the source of all major pitfalls.
1. The Cyclic Synchronization Mismatch
The TF6620 is built to handle both single requests and cyclic requests (periodic background communication). However, this "cyclic" operation is managed by the TwinCAT task cycle time and the network latency, not by a hard, synchronized bus cycle.
| Pitfall | Description | TF6620 Solution |
| Non-Deterministic Cycle Time | The time it takes for Beckhoff to read the Siemens data is subject to network traffic, CPU load on the Siemens PLC, and the TwinCAT task overhead. You will have a jitter in your communication cycle. | Test and Over-Time the Watchdog: Set your ReadRate (or similar cyclic request setting in the TF6620 block) to a safe frequency (e.g., 50ms). On the receiving side (e.g., Siemens if Beckhoff is sending the heartbeat), set your heartbeat watchdog timer to be at least 2x the fastest expected cycle time to allow for natural network jitter. |
| Siemens CPU Load | Older or heavily loaded Siemens PLCs (S7-1200/1500) have limits on the number of simultaneous TCP/IP connections and the rate at which they can process non-IO-Device requests. | Optimize Data Requests: Bundle all necessary data into a single, contiguous memory area (e.g., a single Siemens Data Block, or a single contiguous memory range like DB100.DBX0.0) and read it in one request using the TF6620, rather than many separate variable requests. This is a critical optimization (See TF6620 documentation for optimization options). |
2. Addressing and Data Handling
| Pitfall | Description | Solution |
| Absolute Addressing Only | The TF6620 requires absolute addressing on the Siemens side (e.g., DB100.DBW0, M0.0). It generally cannot use symbolic tags directly. If the Siemens program changes the memory layout, your communication will break. | Create a Dedicated Interface DB: Create a non-optimized, standard Data Block (DB) on the Siemens PLC exclusively for communication with Beckhoff. Do not allow other code to change the offsets in this DB. Always refer to variables in the Beckhoff TF6620 configuration using the absolute address (e.g., DB100, Byte 0, Length 8). |
| Siemens Security/Access | By default, newer Siemens S7 controllers (especially S7-1500/1200) disable external access via the S7 communication protocol for security. | Explicitly Enable Communication: In the TIA Portal properties for the Siemens CPU, you must explicitly enable "Permit access with PUT/GET communication from remote partner (PLC, HMI, OPC)" and disable the "Optimized block access" for the specific Data Blocks being used. |
| Endianness (The DINT Heartbeat) | As mentioned before, a DINT (32-bit value) often requires a byte-swap when moving between different processor architectures. Siemens is typically Big Endian, while PC-based systems like Beckhoff often default to Little Endian. | Implement Byte Swapping: Be prepared to swap the byte order of your 32-bit DINT heartbeat manually, either in the Beckhoff TwinCAT PLC code or in the Siemens code, to ensure the number reads correctly (e.g., use a special function block for byte swap, or read the DINT as four separate bytes and re-order). |
3. Implementing the DINT Heartbeat
Your DINT 'heartbeat' is your life support. Since TF6620 is a client/server model, you must use the heartbeat to prove that the connection is live and the data is current.
Sending the Heartbeat (e.g., Siemens to Beckhoff):
In the Siemens cyclic task, increment the DINT value every cycle.
The TF6620 periodically reads this DINT value.
Receiving and Monitoring (Beckhoff):
The Beckhoff PLC reads the new DINT value, stores the previous value, and resets a watchdog timer.
If the new value ≤ previous value, or if the watchdog timer expires before a value change is detected, it means the communication broke or the Siemens PLC rebooted/stopped. Trigger a controlled fault/shutdown.
Note: Using a simple increasing counter helps detect PLC restarts or stalled communication.
The use of TF6620 is a reliable, software-only solution, but requires meticulous attention to the TCP/IP-layer configuration and the PLC memory addressing constraints.
The video provided below shows the basic setup of PROFINET communication via a Beckhoff EtherCAT terminal, which is an alternative to the software-based TF6620 approach you are using.
Thank you!
We ARE using a single data block to communicate (DB4), this is it (reference to past code snippets given):
HMI_LD_IN_BOOL_Abort_Req Bool
HMI_LD_IN_BOOL_Retry_Req Bool
HMI_LD_OUT_BOOL_Abort_Ack Bool
HMI_LD_OUT_BOOL_Retry_Ack Bool
HMI_LD_IN_BOOL_System_Reset_REQ Bool
HMI_LD_IN_BOOL_SystemStart_Req Bool
HMI_LD_IN_BOOL_SystemStop_Req Bool
HMI_LD_OUT_BOOL_LoaderHasGlass Bool
HMI_LD_IN_BOOL_DataStrobe Bool
HMI_LD_IN_DINT_RackNumber_Req DInt
HMI_LD_IN_OUT_DINT_Heartbeat DInt
HMI_LD_OUT_BOOL_WaitingForData Bool
HMI_LD_OUT_BOOL_HaveData Bool
HMI_LD_OUT_BOOL_Loading Bool
HMI_LD_OUT_BOOL_PickFault Bool
HMI_LD_OUT_BOOL_Loaded Bool
HMI_LD_OUT_BOOL_Ejecting Bool
HMI_LD_OUT_BOOL_Fault Bool
HMI_LD_OUT_BOOL_Reset Bool
HMI_LD_IN_BOOL_Loaded_Ack Bool
HMI_LD_IN_BOOL_Ejecting_Ack Bool
HMI_LD_IN_BOOL_BeltsRunning Bool
HMI_LD_IN_BOOL_EntrySensor Bool
HMI_LD_OUT_DINT_FaultStatus DInt
HMI_LD_OUT_DINT_FreefallState DInt
HMI_LD_OUT_BOOL_ManualModeReq Bool
Thanks for providing your exact communication data block (DB4) structure. This is excellent! Using a single, contiguous data block is the best practice for robust third-party communication like the Beckhoff TF6620 because it minimizes network requests and eliminates the data tearing risk inherent in multi-request communication.
However, since you are using a single block and the TF6620 is reading it as a raw memory area, we must confirm that your DB4 is configured correctly on the Siemens side and that the data types are consistent for the read/write logic you've shown in previous function reviews.
Here is a review of your communication strategy, focusing on the potential pitfalls for the Beckhoff TF6620 integration.
Communication Data Block Review (DB4)
1. The Heartbeat and Data Ownership
The key to inter-PLC communication is clear ownership for every single tag. You have designated HMI_LD_IN_OUT_DINT_Heartbeat as IN_OUT. This is highly ambiguous and a significant risk:
| Tag | Current Type | Ownership Conflict | Recommendation |
HMI_LD_IN_OUT_DINT_Heartbeat | DInt (IN_OUT) | An IN_OUT tag implies both PLCs can write to it. This can lead to a write-overwrite conflict, making the heartbeat unreliable. | Change to Two Tags: Create two separate DInt tags: LD_TO_FF_Heartbeat (owned by Loader) and FF_TO_LD_Heartbeat (owned by Freefall). Each PLC increments its own, and the other PLC monitors it with a watchdog timer. This is critical for safety. |
2. Consistency of Command and Acknowledge Tags
Your use of IN (Input to this PLC) and OUT (Output from this PLC) is good, but you need to ensure the pairing is symmetrical across both PLCs.
| Tag Example | Owned By | Action | Pitfall |
| HMI_LD_IN_BOOL_Abort_Req | Table PLC (The "LD" side) | The Table PLC must write this. | Misleading Name: If the Table PLC writes it, the name should be LD_TO_FF_Abort_Req or HMI_LD_OUT... (from the perspective of the Table PLC). Ensure the Beckhoff PLC code reads this as an Input and the Table PLC code writes it as an Output. |
| HMI_LD_IN_BOOL_DataStrobe | Table PLC (Writes Data) | Used in the FC_Rack_Selection logic. | Confirmed good. This is the pulse that the Beckhoff PLC will wait for, and it must clear its HaveData acknowledge bit after processing it. |
3. Data Tearing Risk Mitigation
Because the Beckhoff TF6620 reads the entire DB4 as a block of memory, and its cyclic read rate is independent of your Siemens PLC cycle, data tearing for the large variables isn't the primary risk (the whole block is read in one request). However, the smaller data types need attention.
| Data Type | SCL Best Practice | Pitfall |
Bool | These are read by TF6620 as individual bits in a memory block. | Direct Assignment: In the Siemens PLC, do not write to any of these communication tags (DB4 tags) from multiple functions. Write to a single, local variable (M or DB_Local), and then in one routine, copy all local variables to the communication DB4 once per PLC scan. |
DInt | RackNumber_Req, FaultStatus, FreefallState | Endianness: These 32-bit values are the most likely to have a byte-swap issue. You must test and verify that the value written from one PLC is read correctly by the other. A value of 16#AABBCCDD must appear as exactly that, not 16#DDCCBBAA. |
TF6620 Implementation Pitfalls Reminder
Since you are committed to the TF6620, reinforce the following non-coding requirements:
1. Siemens TIA Portal Configuration (Mandatory)
Non-Optimized DB: You must set the DB4 properties in TIA Portal to disable "Optimized block access."
PUT/GET Permission: In the CPU properties (Protection & Security), you must enable "Permit access with PUT/GET communication from remote partner."
Hardware Address: Note the start address of the DB4 (e.g., DB100.DBX0.0) as this is the raw memory address the TF6620 will use.
2. Beckhoff TwinCAT Configuration
Single Request: Configure the TF6620 Read command to read the entire DB4 (calculated length in bytes) in one request. This is essential for efficiency.
Endian Swap Code: Write the logic to perform the byte swap for the DInt values in the Beckhoff PLC code. It is cleaner to manage it on one side, and Beckhoff's TcUtilities library often has a
SWAP_DWORDfunction for this.
Your structure is sound. The only critical flaw is the ambiguous IN_OUT designation for the heartbeat—this must be split into two separate, single-owner DInt tags for reliable cyclic monitoring.
It's a pain in the ass sending you screenshots of ladder logic but that's how I'm handing those in a separate function (FC_Interface_With_Cutter) using DB4: There are only single writes to that data block (multiple reads, mediated by HMI_LD_IN_BOOL_DataStrobe).
Thanks for clarifying the strategy and the execution details—that's a huge relief. Knowing you're using a separate function (FC_Interface_With_Cutter) for single, mediated writes to DB4 addresses the biggest data consistency risks right away.
The screenshot of the ladder logic isn't necessary, as your description confirms you've solved the critical synchronization issue:
Single Write Point: Writing all outputs to DB4 from one function (FC_Interface_With_Cutter) ensures the data block is only updated once per PLC scan, eliminating the risk of data tearing due to multiple program blocks fighting to write the same memory area.
Strobe Mediation: Using HMI_LD_IN_BOOL_DataStrobe (written by the Freefall) to mediate your reads (and, presumably, your processing on the Table PLC) is the classic handshaking approach to ensure data is acted upon only when fresh.
With that context, let's address the two remaining major points from the previous review: the heartbeat ownership and the required configuration.
1. The Heartbeat Ownership (Critical)
The one remaining, high-risk flaw in your DB4 definition is the HMI_LD_IN_OUT_DINT_Heartbeat tag.
You must treat this as two separate, non-overlapping signals, even if you are using a single DINT.
| Current Flaw | Rationale | Action to Take |
| IN_OUT DINT | If both PLCs try to increment the same DINT, they will constantly overwrite each other, causing the counter to flicker or stall, making it useless for watchdog monitoring. | Rename and Reassign: The Table PLC (LD) should write the heartbeat it sends, and the Table PLC should read the heartbeat the Freefall (FF) sends. Even though it's one DINT memory location in DB4, only one PLC must ever write to it. |
Recommendation for Heartbeat Logic
Assuming the Table PLC controls the communication timing:
Table PLC (LD) Code:
Treat HMI_LD_IN_OUT_DINT_Heartbeat as its OWNERSHIP to write.
Increment this value once per scan in FC_Interface_With_Cutter.
Monitor a separate tag, perhaps HMI_FF_OUT_DINT_Heartbeat (which you'd need to add to DB4), that the Freefall PLC writes.
Freefall PLC (FF) Code:
Treat HMI_LD_IN_OUT_DINT_Heartbeat as an INPUT (read-only) and set a watchdog timer on it.
Increment and write its own separate heartbeat counter (HMI_FF_OUT_DINT_Heartbeat) to be monitored by the Table PLC.
2. Mandatory Configuration Check (TF6620)
Since your code structure is solid, the success of your cyclic operation now relies entirely on the non-code configuration required by both Siemens and Beckhoff to allow the TF6620 software to work.
Please ensure the following three non-coding checks are complete:
| Component | Setting | Status in TIA Portal (Siemens) |
| DB4 | Optimized Access | DISABLED (Set to Standard) |
| CPU | PUT/GET Access | ENABLED (Permit access with PUT/GET communication from remote partner) |
| Addresses | Start Address | Confirmed and locked down (e.g., DB4.DBX0.0) |
If these three configuration points are addressed, your well-designed SCL/Ladder logic and handshaking routines will be supported by a reliable network connection.
so you're saying for point 1 that We need to be passing different variables back and forth:
Cutter: You Alive?
Freefall: Yeah I'm here, you alive?
Cutter: Yeah I'm here, you alive?
Etc Etc
Yes, that's exactly right. For the Heartbeat signal to be a reliable safety check, you must have two separate, non-overlapping variables acting as an alive/watchdog signal in a request-and-response loop.
Think of it as two separate, one-way conversations happening at the same time:
Why Two Variables are Necessary
If both PLCs try to write to the exact same memory location (HMI_LD_IN_OUT_DINT_Heartbeat), they constantly overwrite each other, and you lose the ability to detect if one side has actually stopped or frozen.
| Current Flawed State (One DINT) | Corrected State (Two DINTs) |
| LD writes 1 → FF reads 1 | LD writes 1 |
| FF writes 2 → LD reads 2 | FF reads 1 |
| LD writes 3 → FF reads 3 | FF writes 100 |
| Conflict/Stall | LD reads 100 |
| If LD stops writing, the DINT holds the last value FF wrote. | If LD stops writing, the DINT read by FF stops changing, triggering the watchdog. |
Recommended DB4 Changes
You need to split the ambiguous HMI_LD_IN_OUT_DINT_Heartbeat into two dedicated variables, each owned by one PLC:
| Current Tag | Replacement Tag 1 (FF to LD) | Replacement Tag 2 (LD to FF) |
| HMI_LD_IN_OUT_DINT_Heartbeat | FF_TO_LD_DINT_Heartbeat | LD_TO_FF_DINT_Heartbeat |
1. Freefall PLC (FF) Logic (The "Server")
Writes to: FF_TO_LD_DINT_Heartbeat (Increments this value every PLC scan).
Reads from: LD_TO_FF_DINT_Heartbeat (Monitors this value with a watchdog timer).
2. Table PLC (LD) Logic (The "Client")
Writes to: LD_TO_FF_DINT_Heartbeat (Increments this value every PLC scan).
Reads from: FF_TO_LD_DINT_Heartbeat (Monitors this value with a watchdog timer).
By having two distinct channels, each PLC is constantly telling the other, "I'm alive and my internal clock is running," and simultaneously monitoring the other's status. This is the gold standard for watchdog safety between two independent PLCs.
Okay Okay. So look at the list of variables again in DB4. See the HMI_LD_OUT_BOOL_Fault Bool? When I have a problem (freefall) I just tell the cutter outright. Can you tell the cutter takes precedence here?
You've identified a key safety point. Based only on the variable names in DB4 and general PLC sequencing principles, no, I cannot definitively confirm the cutter takes precedence. In fact, the naming convention suggests the Freefall/Loader PLC (LD) takes precedence in reporting a fault.
Here's the breakdown:
Analysis of Fault Precedence
1. Naming Convention Suggests Loader Precedence
The name HMI_LD_OUT_BOOL_Fault (assuming "LD" stands for Loader/Table PLC) explicitly uses OUT.
From the Loader/Table PLC's Perspective (Your PLC): This is an Output signal. You are responsible for setting this bit to signal a fault to the outside world (the HMI and the Cutter).
From the Cutter's Perspective: This is an Input signal. The cutter's logic simply reads this bit to know when your machine has a fault and should stop sending data.
Since your PLC is the one writing this critical status, your logic holds primary control over declaring the fault.
2. The Real Precedence is in the Code
Precedence is not determined by the variable name; it is determined by the logic in the cutter/freefall PLC's master state machine (which we've partially reviewed).
For the cutter to take precedence, its master state machine must have a block of logic that immediately reacts to a fault status, for example:
IF DB4.HMI_LD_OUT_BOOL_Fault THEN
// Cutter immediately jumps to a safe-stop/wait state
DB_Cutter_Status.State := 99;
DB_Cutter_Status.Hold_Data := TRUE;
ELSE
// Normal operation
...
END_IF;
If your cutter PLC implements this logic correctly, then your single output bit is enough to stop the upstream process, giving your fault precedence.
Conclusion
Your current strategy is sound for simple fault signaling: You report the fault, and the upstream machine must be programmed to stop immediately upon reading it.
The risk is not in your output but in the Cutter/Freefall PLC's programming. If the Freefall's logic doesn't check HMI_LD_OUT_BOOL_Fault at the beginning of every scan or before issuing a new move command, then precedence is lost.
Thank you for the help Gem!
You're very welcome! I'm glad I could help review your code and discuss the nuances of the Siemens/Beckhoff integration.
Just let me know if you run into any more PLC challenges or have any other code you'd like to review. Happy coding! 🤖
.jpg)
Gemini can make mistakes, so double-check it


.jpg)