Skip to content

CAN Bus

On nXPlc, CAN is used with the standard CODESYS tools, as on any other PLC: no neXo libraries, and the code you write is not tied to this board. The MCP2515 controller driver registers itself at runtime startup, on network 0.

There are two ways:

WhenHow
CANbus and CANopen ManagerCANopen devices: remote I/O, drivesnodes in the device tree, plus the CiA405 blocks for SDOs
CAA CanL2proprietary protocols, raw framesa library you drive from ST

Use one way per application: both talk to the same controller.

  1. Right-click nXPlc → Add Device… → CANbus. Set network 0 and the same bit rate as the other nodes.
  2. Right-click the CANbus → Add Device… → CANopen Manager.
  3. Under the Manager add the slaves from their EDS files, with the node-ID set on the device.
  4. Build and download: the Manager sends the NMT reset, waits for the slaves’ boot-up, configures them via SDO and puts them in Operational. The PDOs appear in the slaves’ I/O mapping.

To read or write any object of a node, use the SDO blocks of the CAA CiA405 library. For writing, DATA is an ARRAY[1..4] OF BYTE; for reading, DATA and DATALENGTH are outputs of the block.

fbWrite(NETWORK := 1, (* CAN network 0 = NETWORK 1 in CiA405 *)
ENABLE := xWrite, (* the block starts on the rising edge *)
TIMEOUT := 500, (* ms *)
DEVICE := 1, (* node-ID *)
CHANNEL := 1,
INDEX := 16#6300,
SUBINDEX := 0,
DATA := abOut,
DATALENGTH := 2);

The bus only accepts the bit rates listed below: 1 Mbit/s is not achievable. A device that only runs at 1 Mbit/s or 500 kbit/s must be set to 500.

The library is CAA CanL2 — CAA Can Low Level, from the CAA Technical Workgroup. It is not a neXo library: it is the one CODESYS uses on any target with a CAN controller.

Three entries are needed in the Library Manager for it to work:

LibraryVersionNamespaceWhat it brings
CAA Types Extern3.5.17.0CAAHANDLE, ERROR, SIZE, COUNT
CAA CanL2 Extern3.5.17.0CL2the function blocks and BUSSTATE
CAA Can Low Level Imp Extern3.5.17.0CL2ICOBID and the frame structures

Adding the first pulls in its dependencies on its own. Leave them out and the compiler does not know the types, and the project fails with a row of Unknown type.

On the runtime side, nXPlc implements the CanL2 component and a driver for the on-board MCP2515 controller — the same layer the CANopen Manager works on. You never see it: you talk to the library, and the library talks to the chip.

This is the thing to understand before writing a line, because it is where people go wrong.

CanL2 has no “send these bytes” function. It has a pool of message blocks, built at open time inside a memory area you hand it. Every frame you send or receive is one of those blocks, and what you exchange with the library are handles and ownership.

There are no copies: when you write the data, you write it straight into the block the controller will read to put it on the wire. That is precisely why ownership has to be explicit — the library needs to know when those bytes can be reused.

Three steps, once.

(* 1. how much memory NMSG blocks need *)
szMem := CL2.DriverGetSize(NET, FALSE, NMSG, ADR(eError));
(* 2. open: the memory is yours, the pool lives inside it *)
hDriver := CL2.DriverOpenP(NET, BAUD, FALSE, szMem, ADR(abMem), ADR(eError));
(* 3. a permanent subscription to the identifier you care about *)
hRx := CL2.CreateSingleIdReceiver(hDriver, ID_RX,
FALSE, (* xRTR *)
FALSE, (* x29BitId *)
FALSE, (* xTransmit: this one receives *)
TRUE, (* xAlwaysNewest *)
0, (* eEvent *)
FALSE, (* xEnableSyncWindow *)
ADR(eError));

NET is the network number the driver registered itself on at startup: on this board it is 0. BAUD is the bit rate in kbit/s, and this is the only place it is set. abMem is an array of yours: after DriverOpenP you never touch it again, the library lives inside it.

The receiver is permanent: you create it once and it stays for the life of the application. To listen to more identifiers, create more receivers.

Four steps, for every frame:

(* 1. take a block from the pool: 0 = pool empty *)
hTx := CL2.CreateMessage(hDriver, ID_TX, 8, FALSE, FALSE, ADR(eError));
IF hTx <> 0 THEN
(* 2. get the address of the 8 bytes *)
pTx := CL2.GetMessageDataPointer(hTx, ADR(eError));
IF pTx <> 0 THEN
(* 3. fill it, zeros included *)
pTx^[0] := WORD_TO_BYTE(wOutputs);
pTx^[1] := WORD_TO_BYTE(SHR(wOutputs, 8));
FOR i := 2 TO 7 DO pTx^[i] := 0; END_FOR
(* 4. hand it over *)
eErrTx := CL2.Write(hDriver, hTx, 1, FALSE);
IF eErrTx <> 0 THEN
eErrRx := CL2.FreeMessage(hTx); (* it did not go: still yours *)
END_IF
END_IF
END_IF

There is no SetData: you write through the pointer, straight into the block. And the bytes you do not write keep whatever the previous user of that block left there — the pool cleans nothing, so zeroing the tail is not fussiness.

The mirror image, with the difference that here the block is yours and must be freed:

hMsg := CL2.Read(hRx, ADR(ctLeft), ADR(eError));
IF hMsg <> 0 THEN
pRx := CL2.GetMessageDataPointer(hMsg, ADR(eError));
IF pRx <> 0 THEN
wInputs := BYTE_TO_WORD(pRx^[0]) OR SHL(BYTE_TO_WORD(pRx^[1]), 8);
END_IF
eErrRx := CL2.FreeMessage(hMsg);
END_IF

Copy the bytes into your variables before FreeMessage. After the free, the pointer dangles and the block can be reassigned to the next frame. Do not keep it between cycles: read it again every time.

ctLeft tells you how many messages are still queued. On a busy bus it pays to drain more than one per cycle, with a WHILE until the handle comes back zero.

AfterThe block isYou must
CreateMessageyoursfill it and hand it over
Write → 0the library’snothing — the handle is dead
Write → errorstill yoursFreeMessage
Read → handleyoursFreeMessage after copying

The symptom of a lost block is not a crash: the pool drains, CreateMessage starts returning 0, and transmission stops silently. You recognise it by a transmit counter that stops rising while the receive counter keeps running.

PROGRAM PLC_PRG
VAR CONSTANT
NET : USINT := 0; (* network number: 0, the board's only CAN *)
BAUD : UINT := 500; (* kbit/s *)
NMSG : CAA.COUNT := 16;
ID_TX : CL2I.COBID := 16#401;
ID_RX : CL2I.COBID := 16#381;
END_VAR
VAR
nStep : INT;
eError : CAA.ERROR; (* for the peError parameters *)
eErrTx : CL2.ERROR; (* result of Write *)
eErrRx : CL2.ERROR; (* result of FreeMessage *)
szMem : CAA.SIZE;
abMem : ARRAY[0..1023] OF DWORD; (* driver memory, 4-byte aligned *)
hDriver : CAA.HANDLE;
hRx : CAA.HANDLE;
hTx : CAA.HANDLE;
hMsg : CAA.HANDLE;
pTx : POINTER TO ARRAY[0..7] OF BYTE;
pRx : POINTER TO ARRAY[0..7] OF BYTE;
ctLeft : CAA.COUNT;
eState : CL2.BUSSTATE;
wOutputs : WORD;
wInputs : WORD;
udiTx : UDINT;
udiRx : UDINT;
i : INT;
END_VAR
CASE nStep OF
0: szMem := CL2.DriverGetSize(NET, FALSE, NMSG, ADR(eError));
IF eError = 0 AND szMem <= SIZEOF(abMem) THEN
nStep := 1;
ELSE
nStep := 99;
END_IF
1: hDriver := CL2.DriverOpenP(NET, BAUD, FALSE, szMem, ADR(abMem), ADR(eError));
IF hDriver <> 0 THEN nStep := 2; ELSE nStep := 99; END_IF
2: hRx := CL2.CreateSingleIdReceiver(hDriver, ID_RX,
FALSE, FALSE, FALSE, TRUE, 0, FALSE,
ADR(eError));
IF hRx <> 0 THEN nStep := 3; ELSE nStep := 99; END_IF
3: (* --- steady state: one task cycle = one frame --- *)
hTx := CL2.CreateMessage(hDriver, ID_TX, 8, FALSE, FALSE, ADR(eError));
IF hTx <> 0 THEN
pTx := CL2.GetMessageDataPointer(hTx, ADR(eError));
IF pTx <> 0 THEN
pTx^[0] := WORD_TO_BYTE(wOutputs);
pTx^[1] := WORD_TO_BYTE(SHR(wOutputs, 8));
FOR i := 2 TO 7 DO pTx^[i] := 0; END_FOR
eErrTx := CL2.Write(hDriver, hTx, 1, FALSE);
IF eErrTx = 0 THEN
udiTx := udiTx + 1;
ELSE
eErrRx := CL2.FreeMessage(hTx);
END_IF
END_IF
END_IF
hMsg := CL2.Read(hRx, ADR(ctLeft), ADR(eError));
IF hMsg <> 0 THEN
udiRx := udiRx + 1;
pRx := CL2.GetMessageDataPointer(hMsg, ADR(eError));
IF pRx <> 0 THEN
wInputs := BYTE_TO_WORD(pRx^[0]) OR SHL(BYTE_TO_WORD(pRx^[1]), 8);
END_IF
eErrRx := CL2.FreeMessage(hMsg);
END_IF
eState := CL2.GetBusState(hDriver, ADR(eError));
99: ; (* stopped: read eError and szMem in the monitor *)
END_CASE

udiTx and udiRx rising together is the proof that the bus works in both directions.

The controller’s crystal is 8 MHz, and not every bit rate is reachable:

Supported10, 20, 50, 100, 125, 250, 500, 800 kbit/s
Not reachable1 Mbit/s

The timing is conformant: 250 kbit/s has the 87.5% sample point required by ISO 11783, 500 kbit/s 75%, which is the highest the datasheet allows at that speed.

Asking for an unsupported bit rate does not degrade silently: the driver refuses to open, DriverOpenP returns 0 with error 10210, and the device Log shows the line CAN: bit rate … kbps non supportato, apertura rifiutata (ammessi 10/20/50/100/125/250/500/800).

CL2.GetBusState(hDriver, ADR(eError)) returns a CL2.BUSSTATE:

ValueMeaning
UNKNOWNcontroller not open yet: may show up in the first moments after DriverOpenP
ACTIVEactive, error counters below 96 — the normal state
WARNINGerror counters above 96
PASSIVEerror-passive: transmits under restrictions
BUSOFFoff the bus, recovering automatically

ERR_FREE is never returned: with no errors the state is ACTIVE.

The controller error counters are read with CL2.GetReceiveErrorCounter(hDriver, ADR(eError)) and CL2.GetTransmitErrorCounter(hDriver, ADR(eError)). Above 96 the bus goes to WARNING: if the first one rises, the problem is on reception (termination, wiring, ground); if the second one rises, the transmitted frames are not acknowledged.

The error codes you will meet most often on open:

CodeCause
10203the network is already open with a different bit rate
10204not enough memory — grow abMem or lower NMSG
10205network number does not exist — on this board the network is 0
10210no driver registered on that network, or unsupported bit rate (see the Log)

No hardware cyclic transmission. The CAA mechanisms for automatic periodic sending are not available: the rhythm is set by your task cycle, one frame per cycle.

29-bit extended identifiers: the parameter is present in every call, but testing was done with 11-bit identifiers.