Encapsulating Siemens FB12 BSEND and FB13 BRCV in a Custom FB

David Krause12 min read
S7-300SiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

Siemens S7-300/400 communication blocks FB12 "BSEND" and FB13 "BRCV" ship from the Standard Library > System Function Blocks and form the buffered, bi-directional S7 communication pair used to exchange variable-length data records between two CPUs. Wrapping both blocks inside a single user function block (for example FB100) keeps the call surface compact, centralises the connection-ID, error handling, and status evaluation, and allows the application code to treat the S7 link as one logical channel.

The pattern works fine for scalar parameters (INT, BOOL, WORD), but breaks down when the application wants to expose a single ANY input on the wrapper and forward it to the inner SD_1 (FB12) and RD_1 (FB13) parameters. The reason is that the ANY pointer is not passed by value; STEP 7 stores it in the calling block's local stack and supplies the called FB with a cross-reference pointer to that local area. Direct symbolic assignment of an IN-OUT ANY to another IN-OUT ANY therefore fails silently: STATUS stays W#16#0000, DONE never sets, and ERROR never sets, because the inner FBs never see a valid source/destination area.

This article shows two production-quality methods for routing an ANY from the wrapper FB to the inner BSEND/BRCV instances: a TEMP-based byte copy and an AR2-relative direct read. Both rely on the multi-instance mechanism for the inner FBs.

Prerequisites

How STEP 7 Stores the ANY Parameter

When the OB1 calls FB100 with a literal ANY such as P#DB1.DBX0.0 BYTE 5, the compiler emits a 10-byte ANY constant and parks it in the calling block's TEMP area. The first 4 bytes of the called FB's local stack contain the cross-reference P#Lx.0 pointing to the 10 bytes. Inside the FB, the formal parameter DataArea is therefore accessed through that 4-byte descriptor; it is not a direct pointer to the data area.

ANY pointer layout (10 bytes)
Byte offset Length Content Example value
0 2 Syntax ID (10h for ANY) 10 02
2 2 Length in bytes 00 05
4 2 DB number (0 for non-DB) 00 01
6 4 Area + byte address (byte.bit encoded) 84 00 00 00

To pass that ANY to the inner BSEND/BRCV instances, you must copy those 10 bytes from the calling block's local stack into a TEMP ANY in FB100 and then assign that TEMP symbolically to SD_1 / RD_1. A direct wire from DataArea to SD_1 does not work because both parameters are formal IN-OUTs and the compiler cannot chain cross-references of equal depth.

The AR2 Trap in Multi-Instance FBs

STEP 7 uses AR2 to store the multi-instance offset of the currently executing FB. As soon as you touch AR2 in your own STL without saving it first, every subsequent symbolic access to instance-DB variables resolves to the wrong offset. The official Siemens FAQ on register/ACCU handling documents the full set of opcodes that overwrite AR1, AR2, and the accumulators:

Save/restore AR2 around any direct register manipulation, or stay inside the LAR1 / T W[AR1,P#x.y] idiom that the compiler can supervise. Solution 1 below uses the safer idiom.

Solution 1: TEMP ANY with Byte-by-Byte Copy

Add a single TEMP ANY in the wrapper FB and copy the 10 bytes from the formal parameter's local location to that TEMP. Then assign the TEMP symbolically to the inner instances. This is the recommended pattern because the compiler can still relocate AuxAny without breaking the code.

FB100 declaration (top portion):

FUNCTION_BLOCK FB100
TITLE = 'S7 Communication Wrapper (BSEND + BRCV)'
VERSION : '1.1'

VAR_INPUT
  DataArea : ANY;          // Source/Destination ANY from caller
  ID        : WORD;        // Connection ID from NetPro
  R_ID      : DWORD;       // Request ID, must match on both sides
  REQ       : BOOL;        // Rising edge starts BSEND
  EN_R      : BOOL;        // Enable BRCV reception
END_VAR

VAR
  BS_inst   : BSEND;       // Multi-instance: FB12
  BR_inst   : BRCV;        // Multi-instance: FB13
  DoneX     : BOOL;
  ErrorX    : BOOL;
  StatusX   : WORD;
END_VAR

VAR_TEMP
  AuxAny    : ANY;         // Working copy of the caller ANY
  SaveAR2   : DWORD;       // AR2 save slot (only for Solution 2)
END_VAR

Network 1 - Copy the 10 bytes of the caller's ANY into AuxAny:

      L     P##DataArea        // Pointer to the IN parameter descriptor (4 bytes)
      LAR1                      // AR1 now points to the 4-byte cross-reference
      L     P##AuxAny           // Pointer to the TEMP ANY (10 bytes)
      LAR2                      // AR2 -> AuxAny (touched AR2, restore later!)

      L     W [AR1,P#0.0]       // Syntax ID + length high word
      T     W [AR2,P#0.0]
      L     D [AR1,P#2.0]       // Length low word + DB number
      T     D [AR2,P#2.0]
      L     D [AR1,P#6.0]       // Area + byte address
      T     D [AR2,P#6.0]

The first 4 bytes of DataArea in the local stack are the cross-reference P#Lx.0. Indirectly reading with L W[AR1,P#0.0] therefore yields the first 2 bytes of the actual 10-byte ANY (skipping the cross-reference header). The displacements P#2.0 and P#6.0 map exactly to the ANY layout shown above.

Network 2 - Call BSEND with the working copy:

      CALL  BS_inst
        REQ   := REQ
        R     := FALSE
        ID    := ID
        R_ID  := R_ID
        DONE  := DoneX
        ERROR := ErrorX
        STATUS:= StatusX
        SD_1  := AuxAny          // symbolic assignment of the working ANY
        LEN   := 0               // 0 = use the full length from SD_1

Network 3 - Call BRCV with the same working copy:

      CALL  BR_inst
        EN_R  := EN_R
        ID    := ID
        R_ID  := R_ID
        NDR   := NDR_X
        ERROR := ErrorBR
        STATUS:= StatusBR
        RD_1  := AuxAny          // shared buffer for receive
        LEN   := LenRcv

Because both BSEND and BRCV read AuxAny symbolically, the compiler emits the correct cross-reference at call time. The previous failure (DONE=0, STATUS=0, ERROR=0) is replaced with a real status word such as W#16#0000 after the job completes or an explicit error code such as W#16#000A for an unknown connection.

Solution 2: AR2-Based Direct Forwarding (Advanced)

If you must avoid the TEMP copy (for example to keep determinism in a fast OB), forward the ANY by keeping AR2 intact and re-loading it from the instance DB after the manipulation. The block is rarely worth the maintenance burden, but is included for completeness:

      L     P##DataArea
      LAR1
      TAR2  SaveAR2              // save multi-instance offset

      L     W [AR1,P#0.0]
      T     W [AR1,P#0.0]        // no-op: shows the pattern only

      LAR2  SaveAR2              // restore multi-instance offset
      CALL  BS_inst
        SD_1 := DataArea         // now safe, AR2 restored

Any direct STL that touches AR1 with LAR1 inside a multi-instance FB must end with a matching LAR1 back to the instance offset, or the next symbolic access will corrupt the variable image. The two Siemens links above list every opcode that clobbers AR1 and AR2.

Diagnostics and Common Faults

Status / Symptom to root-cause matrix for the wrapper FB
Observed behaviour Likely root cause Remedy
DONE=0, ERROR=0, STATUS=0 indefinitely ANY not forwarded; inner FB sees an empty descriptor Apply Solution 1 (TEMP copy) or Solution 2 (AR2 restore)
STATUS = W#16#0001 / 0002 / 000A Connection issues, see BSEND/BRCV manual Verify ID from NetPro, partner CPU in RUN, R_ID match
STATUS = W#16#8085 / 8090 Length / type mismatch in the ANY (often DBX outside instance DB) Check that the caller's ANY is fully inside a real DB or M area, not a TEMP
Wrapper compiles, online shows wrong source data AR2 was clobbered; symbolic reads return shifted offsets Wrap each LAR2 in save/restore, or use Solution 1 only
Error only after stop/Run of CPU Multi-instance DB was re-initialised; AR2 not reloaded Add initialisation network at the top of FB100 that calls BS_inst with R := TRUE on first scan

For PCS 7 users, Siemens flags additional constraints: BSEND/BRCV must be called in OB1 (or a cyclic OB of the same priority class) at a defined interval to maintain the receive side, and the connection must be configured as S7 connection in NetPro, not as a TCP connection. These are detailed in the PCS 7-specific FAQ linked above.

Step-by-Step Commissioning Procedure

  1. Open NetPro and create an S7 connection between the two stations. Note the ID shown in the connection properties (hex, e.g. W#16#0001).
  2. Generate a new DB10 in the partner CPU that mirrors the data layout. Ensure DB10 length is greater than or equal to the highest DataArea length used in the application.
  3. Create FB100 in the program, declare BS_inst : BSEND and BR_inst : BRCV as STAT (multi-instance), and add the TEMP AuxAny and IN parameters as listed in the declaration above.
  4. Paste Networks 1-3 from Solution 1 into FB100 and recompile. The instance DB for FB100 will contain the BSEND/BRCV instance data automatically.
  5. Call FB100 from OB1 with DataArea := P#DB10.DBX0.0 BYTE 20, the same ID from step 1, a matching R_ID, and a 1-Hz clock on REQ to drive the send.
  6. Download both stations and place the partner CPU in RUN. Observe StatusX in VAT: a successful first send returns W#16#0000 and sets DoneX := TRUE for one cycle.
  7. Trigger a receive on the partner by writing into its DB10 from a third station or an HMI script and watch BR_inst.NDR rise on the wrapper FB. LenRcv reports the actual bytes received.

Verification

Confirm correct behaviour with the following checks in VAT or with the online monitor:

  • After REQ rising edge, BS_inst.DONE is TRUE for one OB1 cycle, BS_inst.STATUS = W#16#0000, and the partner's receive data block has been overwritten with the sent bytes.
  • After the partner writes back, BR_inst.NDR is TRUE for one cycle, BR_inst.STATUS = W#16#0000, and the wrapper's DataArea shows the new values.
  • Force a wrong ID (e.g. W#16#00FF): BS_inst.ERROR must rise and BS_inst.STATUS must read W#16#000A (unknown connection) or W#16#0001 (low-priority setup fault) - this proves the wrapper is actually executing the inner FB and not returning a frozen zero status.
  • Cross-check the DI monitor on the instance DB of FB100: the AuxAny TEMP is volatile and invisible in the instance DB; only the multi-instance data of BS_inst and BR_inst must appear.

Performance and Stack Depth Notes

Copying 10 bytes per call adds roughly 0.5 microseconds of OB1 time on an S7-315-2 PN/DP and is below the 1 ms jitter threshold for cyclic communication. The TEMP footprint is 12 bytes (10 for the ANY plus 4 for the cross-reference pointer, aligned). For a wrapper that fans out to N parallel BSEND/BRCV pairs, allocate a separate AuxAny per pair; do not reuse one TEMP across calls because BSEND and BRCV both read the ANY asynchronously between OB1 cycles.

The local stack depth of OB1 grows by the sum of TEMP usage of all FBs in the call chain. Adding 12 bytes to FB100 is negligible on S7-300 (default 256-byte local stack per priority class) but should be measured on S7-400 CPUs in heavily nested OBs.

Migrating to TIA Portal and S7-1500

On S7-1500 with TIA Portal V16 and later, BSEND and BRCV are available as BSEND / BRCV instructions in the Communication > S7 Communication palette and accept ANY parameters through their SD_1 / RD_1 inputs. The wrapper pattern translates directly: declare an InOut of type Variant on the wrapper, convert to ANY via VariantToAny (SCL) or the legacy byte copy (STL), and assign to the inner instructions. For S7-1500 firmware V2.5+ you may alternatively use the optimised TSEND_C / TRCV_C blocks, but they are TCP-based and not drop-in replacements for the S7-connection semantics of BSEND/BRCV.

The TIA Portal sample at Program example for BSEND & BRCV (S7-1500, S7-1200 G2) shows a full two-CPU project that compiles against the same wrapper contract described in this article.

Safety and Operational Caveats

BSEND and BRCV operate on configured S7 connections only. They do not provide any authentication or encryption; for untrusted networks use the S7 Communication (Secure) variants introduced with TIA Portal V18 / S7-1500 firmware V3.0, or front-end the link with a VPN / SCALANCE firewall. Treat the ANY parameter on the wrapper as a potential injection vector: validate Area byte and DB number against an allow-list before passing it to the inner blocks.

Multi-instance FBs that use AR2 for parameter passing must be compiled in the same OB priority class as their caller. Mixing priority classes (for example calling FB100 from OB35 and from OB82) is legal but each caller must use its own instance DB; the multi-instance offset is stored per instance, not per call site.

FAQ

Why does FB12 / FB13 stay at STATUS = W#16#0000 with DONE = 0 and ERROR = 0 when I assign the ANY directly?

STEP 7 stores the formal ANY in the calling block's local stack and gives the called FB only a 4-byte cross-reference. A direct symbolic wire between two IN-OUT ANY parameters cannot chain the cross-reference; the inner FB sees an empty descriptor and returns the zero status. Copy the 10 bytes into a TEMP ANY first (Solution 1) or restore AR2 and assign symbolically (Solution 2).

Can I avoid the TEMP copy and just assign the formal parameter to SD_1 in SCL?

No. The same cross-reference rule applies in SCL. Use the VariantToAny / VariantGet conversion block in TIA Portal or paste the STL byte copy from Solution 1 into an STL section of the SCL FB.

Do FB12 and FB13 need to be in the same call priority?

They can be called from any cyclic OB, but the wrapper FB must be called in the same priority class on every cycle, otherwise the receive side will time out. PCS 7 V9 calls both in OB1 by convention - see the PCS 7 FAQ for BSEND/BRCV linked above.

Why does the wrapper compile but STATUS shows W#16#8085 or W#16#8090?

The ANY length or the area / DB combination is invalid. Confirm the caller's ANY points to a real DB area, not a TEMP, and that the byte/bit encoding in bytes 6-9 of the ANY is correct (e.g. 84 00 00 00 for DB 1, byte 0). A common mistake is using the DBX address of a multi-instance DB - the inner BSEND then cannot resolve the DB number at run time.

Is the same pattern valid for PUT (FB15) and GET (FB16)?

Yes. PUT and GET also accept ANY pointers at ADDR_1..ADDR_4, and the same TEMP-copy pattern applies. The PCS 7 FAQ linked above documents the additional constraint that PUT/GET need a different connection type than BSEND/BRCV; both can coexist on the same S7 connection configured as "S7 connection" in NetPro.

Back to blog