Overview
Reading a single bit from an Omron CP1E or CP1L PLC using Visual Basic 6 requires three building blocks: the Host Link serial protocol (ASCII), the MSComm control for COM-port I/O, and the Frame Check Sequence (FCS) integrity byte. This reference documents the wire-level frame, the VB6 implementation, the address map, and the troubleshooting matrix needed to exchange single-bit data with a CP-series PLC over RS-232C or RS-422A.
The same technique applies to any Omron SYSMAC CS/CJ/CP-series PLC running Host Link, and the DLL-free approach documented below keeps the SCADA program fully open and auditable.
Prerequisites
- PLC hardware. CP1E-N14/N20/N30/N40/N60 (built-in RS-232C port), CP1L-L14/L20 (built-in RS-232C port), or any CP1E/CP1L with the CP1W-CIF01 (RS-232C option board) or CP1W-CIF11 (RS-422A/485 option board) installed in option slot 1. CP1L-M and CP1L-EL require the option board; CP1L-EM/EL have an Ethernet port that can be used instead of serial.
- Serial cable. RS-232C null-modem crossover: SD(2)↔RD(3), RD(3)↔SD(2), RTS(4)↔CTS(5), CTS(5)↔RTS(4), SG(9)↔SG(9). For RS-422A use a twisted pair with 120 Ω termination at the receiver end.
- PLC serial port DIP switches. Set the PLC serial port to Host Link mode (CP1L-L: switch 4 OFF, switch 5 ON; CP1W-CIF01: switch 1 OFF, switch 2 ON). Confirm the node number (default 00, range 00–31) and the baud rate (default 9600).
- Development environment. VB6 SP6 with Microsoft Comm Control 6.0 referenced (Project → Components → Microsoft Comm Control 6.0).
- Reference documentation. CP1E/CP1L Getting Started Guide (W461-IT) for hardware pinout and DIP switch configuration.
Protocol Selection
Four Omron protocols are commonly available on a CP-series PLC. Choose Host Link for simple SCADA bit polling, FINS for high-speed Ethernet applications, and Toolbus only for the programming console.
| Protocol | Transport | Bit-Level Read Command | Typical Use |
|---|---|---|---|
| Host Link (SYSWAY) | RS-232C / RS-422A / RS-485 | R@ (Read Contact) | SCADA, HMI, simple polling |
| FINS/UDP | Ethernet (CP1L-EM/EL) | Memory Area Read bit (0101 hex) | High-speed, multi-node |
| Toolbus | RS-232C / USB | Vendor proprietary | CX-Programmer, Sysmac Studio |
| EtherNet/IP | Ethernet | CIP Assembly tag | ODVA networks, AB Logix |
Host Link Frame Structure
Host Link is an ASCII master-slave protocol. Every command is a single line terminated by CR (0x0D). The PLC replies with the same framing plus a two-character command code.
| Field | Bytes | Content |
|---|---|---|
| @ | 1 | Start-of-frame marker, 0x40 |
| Node | 2 | PLC node number, ASCII decimal 00–31 |
| Command | 2 | Two ASCII letters (R@, W@, RD, WR, …) |
| Parameters | Variable | ASCII decimal or hex, defined per command |
| FCS | 2 | Frame Check Sequence (hex) |
| * | 1 | End-of-frame marker, 0x2A |
| CR | 1 | Carriage return, 0x0D |
FCS is the two's complement of the lowest eight bits of the arithmetic sum of every ASCII byte from the node number through the last parameter byte (exclusive of @ and *).
For reading a single bit use the R@ command. For writing a single bit use the W@ command. The RD / WR commands operate on full 16-bit words and are used as a fallback for areas where bit commands are not supported.
Read-bit parameters:
| Parameter | Length | Format | Example |
|---|---|---|---|
| Word address | 4 | ASCII decimal, 0–32767 | "0100" = CIO 100 |
| Bit position | 2 | ASCII hex, 00–0F | "05" = bit 5 |
Worked example — read CIO 100 bit 05 from node 00:
- Body bytes:
00R@010005 - Sum: 0x30+0x30+0x52+0x40+0x30+0x31+0x30+0x30+0x30+0x35 = 0x218
- Lower 8 bits: 0x18
- Two's complement: 0x100 − 0x18 = 0xE8
- FCS:
"E8" - Full command:
@00R@010005E8*<CR>
Response on success: @00R@1E8*<CR> (bit ON) or @00R@0xx*<CR> (bit OFF) where xx is the recomputed FCS over the response body. Response on error: @00FAee xx*<CR> where ee is the end code.
Host Link end codes
| Code | Meaning | Typical Cause |
|---|---|---|
| 00 | Normal completion | — |
| 13 | FCS error | Bug in CalcFCS or parity mismatch |
| 14 | Format error | Frame length or command letter invalid |
| 15 | Entry number data error | Word number out of range |
| 16 | Command not supported | PLC in C-mode, command is CJ-only |
| 21 | Memory area designation error | Area prefix missing |
| 26 / 27 | Area not readable / not writable | Bit lies in a read-only or system area |
Memory Area Mapping for Single-Bit Access
Omron CP-series memory areas use distinct address prefixes when addressed by FINS. Under Host Link R@/W@ the area is selected by the word address block used in the parameter. The CIO area is the default and is referenced directly. To read a Work (W) or Holding (H) bit, the corresponding word number is used without a prefix, but the PLC must be told which area via the command letter — for CP-series with R@/W@ the CIO area is implicit and other areas must use the dedicated commands listed below.
| Area | Word Prefix | Host Link Command | Range |
|---|---|---|---|
| CIO (Core I/O) | 0–32767 | R@ (read), W@ (write) | CIO 0.00–32767.15 |
| Work (W) | 0–511 | Use RD/WR and parse bit, or CJ-mode dedicated bit command | W0.00–W511.15 |
| Holding (H) | 0–511 | Same as W | H0.00–H511.15 |
| Auxiliary (A) | 0–959 | CJ-mode dedicated bit command | A0.00–A959.15 |
| Data Memory (D) | 0–32767 | RD/WR then parse bit (D bits are valid in CJ mode) | D0.00–D32767.15 |
CP1E vs CP1L Serial Port Hardware
The CP1E-N family has a single built-in RS-232C port on the lower terminal block labelled "PERIPHERAL" / "RS-232C" depending on CPU type. The CP1L-L14/L20 also has a built-in port. The CP1L-M and CP1L-EL do not have a built-in RS-232C port and require the CP1W-CIF01 option board in slot 1; slot 2 may carry a CP1W-CIF11 for RS-422A/485. Each option board has its own DIP switch bank that sets the Host Link / Toolbus mode, node number low nibble, and termination.
| CPU | Built-in Port | Option Slot 1 | Option Slot 2 | Ethernet |
|---|---|---|---|---|
| CP1E-N14/N20 | RS-232C | — | — | No |
| CP1E-N30/N40/N60 | RS-232C | CP1W-CIF01/11 | — | No |
| CP1L-L14/L20 | RS-232C | — | — | No |
| CP1L-M30/M40 | USB only | CP1W-CIF01/11 | CP1W-CIF01/11 | No |
| CP1L-EL20 | USB only | CP1W-CIF01/11 | CP1W-CIF01/11 | No |
| CP1L-EM30/EM40 | USB + Ethernet | CP1W-CIF01/11 | CP1W-CIF01/11 | Yes (FINS/UDP) |
MSComm Configuration for VB6
Configure MSComm once in Form_Load. Default Host Link parameters are 9600 bps, 7 data bits, even parity, 2 stop bits. Set the RThreshold to 1 so OnComm fires as soon as a byte arrives; this lets you read the entire line before the 100 ms inter-character timeout elapses.
' Form_Load
Private Sub Form_Load()
With MSComm1
.CommPort = 1 ' COM1; change if your USB-serial adapter is COM3, COM4…
.Settings = "9600,E,7,2" ' Host Link default
.Handshaking = comNone ' Hardware RTS/CTS handled by the cable
.RThreshold = 1 ' OnComm on first received byte
.InputLen = 0 ' Read whatever is in the buffer
.InBufferSize = 1024
.OutBufferSize = 1024
.PortOpen = True
End With
End Sub
' Form_Unload
Private Sub Form_Unload(Cancel As Integer)
If MSComm1.PortOpen Then MSComm1.PortOpen = False
End Sub
If you are using a USB-to-RS-232C adapter (FTDI FT232, Silicon Labs CP2102, WCH CH340), confirm the virtual COM port in Windows Device Manager and assign that number to .CommPort. Driver-driven adapters are fully compatible with Host Link at 9600 bps.
Step-by-Step VB6 Implementation
1. FCS helper function
' Returns the two-character hex FCS for any ASCII string
Public Function CalcFCS(ByVal sBody As String) As String
Dim i As Long
Dim bytSum As Long
bytSum = 0
For i = 1 To Len(sBody)
bytSum = (bytSum + Asc(Mid$(sBody, i, 1))) And &HFF
Next i
CalcFCS = Hex$((256 - bytSum) And &HFF)
If Len(CalcFCS) = 1 Then CalcFCS = "0" & CalcFCS
End Function
2. Build and send a Host Link R@ command
' ReadBitPLC: returns True if bit is ON, False if OFF, raises error on PLC error
Public Function ReadBitPLC(ByVal lngNode As Long, _
ByVal lngWord As Long, _
ByVal bytBit As Byte) As Boolean
Dim sBody As String
Dim sFrame As String
Dim sResponse As String
Dim sBit As String
Dim t As Single
' Build body (everything between @ and *)
sBody = Format$(lngNode, "00") & "R@" & Format$(lngWord, "0000") & _
Right$("0" & Hex$(bytBit), 2)
' Append FCS, terminator and carriage return
sFrame = "@" & sBody & CalcFCS(sBody) & "*" & vbCr
' Clear receive buffer
MSComm1.InBufferCount = 0
' Send frame
MSComm1.Output = sFrame
' Wait for response up to 1 second
t = Timer
Do
DoEvents
If MSComm1.InBufferCount >= 1 Then
If InStr(MSComm1.Input, vbCr) > 0 Then Exit Do
End If
If Timer - t > 1 Then Err.Raise vbObjectError + 1001, , "PLC timeout"
Loop
sResponse = MSComm1.Input
' Validate response starts with @ and contains R@ command echo
If Left$(sResponse, 1) <> "@" Then
Err.Raise vbObjectError + 1002, , "Bad start character: " & sResponse
End If
If Mid$(sResponse, 4, 2) <> "R@" Then
Err.Raise vbObjectError + 1003, , "Bad command echo: " & sResponse
End If
' Extract bit status byte at position 6
sBit = Mid$(sResponse, 6, 1)
Select Case sBit
Case "0": ReadBitPLC = False
Case "1": ReadBitPLC = True
Case "F"
' Error response: positions 6-7 are the 2-hex end code
Err.Raise vbObjectError + 1004, , _
"PLC end code 0x" & Mid$(sResponse, 6, 2)
Case Else
Err.Raise vbObjectError + 1005, , "Unknown bit status: " & sBit
End Select
End Function
3. Reading a single bit from CIO 100.05
Private Sub cmdRead_Click()
Dim bState As Boolean
On Error GoTo PLCError
bState = ReadBitPLC(0, 100, 5)
lblState.Caption = IIf(bState, "ON", "OFF")
lblState.ForeColor = IIf(bState, vbGreen, vbRed)
Exit Sub
PLCError:
MsgBox "ReadBitPLC failed: " & Err.Description, vbCritical
End Sub
4. Writing a single bit (W@ command)
The W@ command sets or resets one bit. The parameter block is identical to R@ plus a single status character (1 = set, 0 = reset) appended before the FCS.
Public Sub WriteBitPLC(ByVal lngNode As Long, _
ByVal lngWord As Long, _
ByVal bytBit As Byte, _
ByVal bState As Boolean)
Dim sBody As String
Dim sFrame As String
Dim sResponse As String
Dim t As Single
sBody = Format$(lngNode, "00") & "W@" & Format$(lngWord, "0000") & _
Right$("0" & Hex$(bytBit), 2) & IIf(bState, "1", "0")
sFrame = "@" & sBody & CalcFCS(sBody) & "*" & vbCr
MSComm1.InBufferCount = 0
MSComm1.Output = sFrame
t = Timer
Do
DoEvents
If MSComm1.InBufferCount >= 1 Then
If InStr(MSComm1.Input, vbCr) > 0 Then Exit Do
End If
If Timer - t > 1 Then Err.Raise vbObjectError + 2001, , "PLC timeout"
Loop
sResponse = MSComm1.Input
If InStr(sResponse, "W@") = 0 Then
Err.Raise vbObjectError + 2002, , "Unexpected write response: " & sResponse
End If
If Mid$(sResponse, 6, 1) <> IIf(bState, "1", "0") Then
Err.Raise vbObjectError + 2003, , "Write echo mismatch: " & sResponse
End If
End Sub
5. Calling WriteBitPLC
Private Sub cmdSet_Click()
On Error GoTo PLCError
WriteBitPLC 0, 200, 0, True ' Set CIO 200.00 ON
Exit Sub
PLCError:
MsgBox "WriteBitPLC failed: " & Err.Description, vbCritical
End Sub
Private Sub cmdReset_Click()
On Error GoTo PLCError
WriteBitPLC 0, 200, 0, False ' Reset CIO 200.00 OFF
Exit Sub
PLCError:
MsgBox "WriteBitPLC failed: " & Err.Description, vbCritical
End Sub
Working with Non-CIO Areas (D-bits, W-bits)
If the target bit lives in D, W, or H, the cleanest cross-version technique is to read the enclosing word with the RD command and mask the desired bit. RD returns the binary word value as four hex characters.
' ReadDWord: returns Long containing the value at D area word
Public Function ReadDWord(ByVal lngNode As Long, _
ByVal lngWord As Long) As Long
Dim sBody As String
Dim sFrame As String
Dim sResponse As String
Dim t As Single
sBody = Format$(lngNode, "00") & "RD" & Format$(lngWord, "0000") & "0001"
sFrame = "@" & sBody & CalcFCS(sBody) & "*" & vbCr
MSComm1.InBufferCount = 0
MSComm1.Output = sFrame
t = Timer
Do
DoEvents
If MSComm1.InBufferCount >= 1 Then
If InStr(MSComm1.Input, vbCr) > 0 Then Exit Do
End If
If Timer - t > 1 Then Err.Raise vbObjectError + 3001, , "PLC timeout"
Loop
sResponse = MSComm1.Input
' Parse four hex chars between "RD" and FCS (positions 6-9)
ReadDWord = CLng("&H" & Mid$(sResponse, 6, 4))
End Function
' ReadDBit: returns True if the bit is set
Public Function ReadDBit(ByVal lngWord As Long, ByVal bytBit As Byte) As Boolean
ReadDBit = (ReadDWord(0, lngWord) And (2 ^ bytBit)) <> 0
End Function
This method is portable across all CP-series firmware revisions and across CS/CJ-series, since RD is a baseline Host Link word command. The drawback is payload size: 11 bytes of body for RD versus 10 bytes for R@, but RD always works where R@ may not.
Multi-Drop RS-485 with Multiple PLCs
When using a CP1W-CIF11 (RS-422A/485) option board with multiple PLCs on the same twisted pair, each PLC must have a unique node number (00–31). The VB6 application addresses individual PLCs by passing the corresponding node number as the first argument to ReadBitPLC or WriteBitPLC. The Host Link protocol handles the arbitration through command-response pairing — only the addressed PLC responds.
- Termination. Install a 120 Ω resistor across SDA/RDA at the electrically farthest PLC, and another 120 Ω at the host. CP1W-CIF11 has a built-in termination resistor selectable via DIP switch 6.
-
Direction control. RS-485 is half-duplex. If your serial port does not auto-control transmit enable, gate the driver via RTS (MSComm property
RTSEnable = Truesets RTS high while transmitting). CP1W-CIF11 handles direction automatically. - Polling discipline. Wait for the previous PLC's response (or its 1-second timeout) before sending the next command. A round-robin scheduler can poll 8 PLCs in under 500 ms at 9600 bps.
Migrating to FINS/UDP for Ethernet Models
CP1L-EM and CP1L-EL CPUs include a 100Base-TX Ethernet port that supports FINS/UDP. Single-bit reads use FINS command code 01 01 (Memory Area Read, bit). Frame layout: FINS header (10 bytes) + command (2 bytes) + area code (1 byte) + word address (2 bytes) + bit position (1 byte) + bit count (1 byte). UDP transport eliminates the FCS complexity of Host Link.
| Parameter | Value |
|---|---|
| Destination IP | PLC IP address (e.g. 192.168.250.1) |
| Destination UDP port | 9600 (default FINS/UDP) |
| Local UDP port | Any unused port ≥ 1024 |
| FINS ICF | 0x80 (command, response required) |
| FINS SID | 0x00–0xFF (any free session ID) |
FINS/UDP round-trip on a CP1L-EM is typically below 5 ms, so a VB6 Winsock control polling 50 bits at 10 Hz is feasible without driver-level tuning.
Verification Procedure
- Loopback test. Before connecting the PLC, wire SD↔RD on the COM port with a 0 Ω resistor or by shorting pins 2 and 3 on the DB9. Run ReadBitPLC(0, 100, 5). The PLC is not connected, so the function should time out within 1 second and raise error 1001. A timeout confirms the FCS, frame format, and MSComm transmit path are correct.
-
Echo test. Send
@00TS00FCS*CR(Test command) — the PLC echoes the same frame back. Verify the response matches what was sent (except for trailing CR). -
Live status bit. In CX-Programmer, force CIO 100.05 ON with
Force Setin the Watch window. Call ReadBitPLC(0, 100, 5) from VB6 — the label must display "ON". Force-reset the bit and re-call — label must display "OFF". -
Round-trip write. Call
WriteBitPLC 0, 200, 0, True, then read CIO 200.00 with CX-Programmer. The Watch window must show the bit as ON. Reset and verify OFF. - FCS validation. For ten randomly generated word/bit combinations, compare CalcFCS output to the value computed by hand. A single mismatch indicates a parity or string-handling bug.
- Stress test. Wrap ReadBitPLC in a 1000-iteration loop with a Debug.Print of each result. All 1000 calls must succeed. Any PLC-side error code indicates wiring or termination issues.
Complete Working Application: Bit Monitor Form
Drop one MSComm control, one Timer (Timer1, Interval = 200), one TextBox for the bit address (txtAddress, text "100.05"), and one Label (lblState) on a VB6 Form. The form continuously polls the configured bit and displays its state.
Private Sub Form_Load()
With MSComm1
.CommPort = 1
.Settings = "9600,E,7,2"
.RThreshold = 1
.InputLen = 0
.PortOpen = True
End With
Timer1.Interval = 200
Timer1.Enabled = True
End Sub
Private Sub Timer1_Timer()
Dim parts() As String
Dim lngWord As Long, bytBit As Byte, bState As Boolean
On Error GoTo Skip
parts = Split(txtAddress.Text, ".")
If UBound(parts) <> 1 Then Exit Sub
lngWord = CLng(parts(0))
bytBit = CByte(parts(1))
bState = ReadBitPLC(0, lngWord, bytBit)
lblState.Caption = txtAddress.Text & " = " & IIf(bState, "ON", "OFF")
lblState.ForeColor = IIf(bState, vbGreen, vbRed)
Exit Sub
Skip:
lblState.Caption = "Error: " & Err.Description
lblState.ForeColor = vbYellow
End Sub
Private Sub Form_Unload(Cancel As Integer)
Timer1.Enabled = False
If MSComm1.PortOpen Then MSComm1.PortOpen = False
End Sub
Typing "100.05" in the text box starts live polling of CIO 100.05 at 5 Hz. Typing "200.00" switches to CIO 200.00. Any malformed address is silently skipped.
Troubleshooting Matrix
| Symptom | Likely Cause | Diagnostic Step | Fix |
|---|---|---|---|
| PLC never responds (timeout) | Wrong cable pinout or PLC not in Host Link mode | Check DIP switch 4/5 on CP1L-L; verify SD/RD crossover with continuity tester | Rewire cable; set DIP switches to Host Link |
| Error 1001 timeout on every call | Baud rate mismatch | Read PLC serial settings via CX-Programmer → PLC → Serial Port | Match .Settings to PLC baud (default 9600,7,E,2) |
| Response starts with 0x15 (NAK) | Wrong node number | Print Mid$(sResponse, 2, 2) and compare to DIP switch node | Match Format$(lngNode, "00") to PLC node |
| PLC returns "FA13" (FCS error) | Bug in CalcFCS | Compare your CalcFCS output to a known-good frame from CX-Protocol trace | Ensure Asc() returns the low byte and Hex$ pads to two chars |
| PLC returns "FA14" (format error) | Parameter length wrong | Count the bytes between command and FCS | Word must be 4 ASCII decimal digits, bit 2 ASCII hex digits |
| PLC returns "FA16" (unsupported command) | CPU in C-mode, not CJ-mode | Verify CPU model and firmware; check CX-Programmer mode | Switch to RD/WR word commands and mask the bit in VB6 |
| PLC returns "FA21" (area designation) | Area prefix missing | Confirm memory area code in command | Use RD for D area instead of R@ |
| MSComm "Port already open" error | Form_Load re-opens the port on each form load | Check if Form_Load runs twice | Guard with If Not MSComm1.PortOpen Then MSComm1.PortOpen = True
|
| Bits read ON when CX-Programmer shows OFF | Inverted logic — VB6 treats 0 as ON | Add Debug.Print sBit and compare with CX-Programmer Watch | Invert the Select Case branch |
| First read OK, second read timeout | InBufferCount not cleared between calls | Verify MSComm1.InBufferCount = 0 runs before every Output |
Always clear the buffer at the start of the read routine |
| RS-422A communication fails above 5 m cable | Missing termination resistor | Measure voltage on SDA/RDA while idle | Install 120 Ω between SDA and SDB at the receiver; enable DIP switch 6 on CP1W-CIF11 |
| Garbled characters in response | Parity mismatch | Verify PLC parity setting in CX-Programmer | Match .Settings to "E" (even) for Host Link, never 8N1 |
| VB6 crashes with "Invalid use of Null" | MSComm1.Input returned Empty when buffer empty | Always check InBufferCount before reading | Wrap MSComm1.Input in If InBufferCount > 0 Then ... |
Performance and Timing
At 9600 bps a typical R@ command and reply is 17 characters (10 send + 7 receive) — about 17 ms of airtime plus PLC scan delay. The CP1L typical response time for Host Link is 5–25 ms depending on the cycle time setting. Round-trip latency for one ReadBitPLC call is therefore 20–60 ms. Polling ten bits in a single loop takes roughly 200–600 ms, which is adequate for SCADA refresh rates of 1 Hz. For faster polling, switch to 19200 or 38400 bps (set PLC DIP switches and .Settings to match) or migrate to FINS/UDP on a CP1L-EM/EL Ethernet port, where round-trip drops below 5 ms.
Safety Considerations
Bit-level writes via Host Link W@ bypass the PLC's normal safety logic and force the addressed bit immediately. Do not write to bits that control safety outputs (typically CIO 100.00 in a CP1L safety circuit) from a SCADA system without a hardware safety relay upstream. Always confirm the target bit's function in the PLC's I/O comment table before issuing a W@ command. For safety-rated communications, use a dedicated safety protocol such as CIP Safety over EtherNet/IP rather than Host Link.
Frequently Asked Questions
What is the default Host Link serial configuration for a CP1L/CP1E PLC?
9600 bps, 7 data bits, even parity, 2 stop bits (9600,E,7,2). Node number is 00. These settings can be changed via CX-Programmer under PLC → Serial Port Settings, or via DIP switches on CP1L-L.
How is the Host Link FCS calculated?
The FCS is the two's complement of the lowest 8 bits of the sum of every ASCII byte from the node number through the last parameter byte (exclusive of @ and *). Express the result as two uppercase hex characters.
Can I read a single bit from the D area with R@?
On CP1E/CP1L with CJ-compatible Host Link enabled, R@/W@ support D-area bit access by specifying the D word number directly. On legacy C-mode firmware, use the RD word command and mask the bit in VB6 instead.
Can I use a USB-to-serial adapter instead of a native RS-232C port?
Yes. FTDI FT232, Silicon Labs CP2102, and WCH CH340 adapters are fully compatible with Host Link at 9600 bps. Confirm the virtual COM port number in Windows Device Manager and assign it to MSComm.CommPort.
What does an "FA" response from the PLC mean?
"FA" is the Host Link error response marker. The next two hex characters are the end code: 13 = FCS error, 14 = format error, 15 = entry number data error, 16 = command not supported, 21 = memory area designation error, 26 = area not readable, 27 = area not writable. Match the code against your command and serial settings to isolate the fault.