Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 20 of 56

TWAI: CAN 2.0, with the bus-off trap

Standard and extended frames as separate types so a 29-bit identifier cannot reach an 11-bit frame — and an error state that a single-node bench setup walks straight into.

One controller, three modes

TWAI — Two-Wire Automotive Interface — is CAN 2.0, on an SJA1000-compatible controller. A real bus needs an external transceiver on the TX/RX pins.

type Bus_Mode is (Normal, Listen_Only, Self_Test);

Identifiers that cannot be mixed up

subtype Standard_Id is Interfaces.Unsigned_32 range 0 .. 16#7FF#;        --  11-bit
subtype Extended_Id is Interfaces.Unsigned_32 range 0 .. 16#1FFF_FFFF#;  --  29-bit

type Standard_Frame is record
   Id     : Standard_Id  := 0;
   Remote : Boolean      := False;
   Length : Data_Length  := 0;        --  0 .. 8
   Data   : Data_Bytes   := (others => 0);
end record;
--  Extended_Frame is the same shape with an Extended_Id.

Each frame type carries its own identifier subtype, so the identifier is range-checked against the standard it belongs to, and Send is overloaded on the frame type. You cannot put a 29-bit identifier in a standard frame — it is a compile-time or constraint error, not a malformed frame on the wire.

Remote => True makes it a remote-transmission request: it carries the identifier and the requested length but no data, and a node owning that identifier is expected to answer with a data frame.

Receiving, two ways

Polled, where the sender chooses the width so you must ask:

function  Available   (S : Session) return Boolean;
function  Is_Extended (S : Session) return Boolean;   --  ask BEFORE choosing an overload
procedure Receive (S : Session; F : out Standard_Frame; Got : out Boolean);
procedure Receive (S : Session; F : out Extended_Frame; Got : out Boolean);

Or interrupt-driven, which is the shape you want for a real bus:

procedure Enable_Rx_Interrupt (S : Session);   --  needs the Session
procedure Get (F : out Queued_Frame);          --  does NOT -- call it from another task
function  Rx_Overruns return Natural;

A Queued_Frame carries its own width, so the consumer can tell a standard frame from an extended one without asking the controller. Note the asymmetry: Enable_Rx_Interrupt touches the controller and so needs the held session, but Get deliberately does not — it is meant to be called from a different task, typically a decode task separate from the one that owns the port. Rx_Overruns counts what the queue dropped, which is the number to watch when you suspect your decoder is too slow.

The bus-off trap

type Bus_State is (Active, Warning, Bus_Off);

function  Health  (S : Session) return Bus_State;
procedure Recover (S : Session);

A single node on a bench goes bus-off. Active is normal and Warning means an error counter has passed the warning limit, but Bus_Off means the node took itself off the bus after too many transmit errors — and the classic way to cause that is transmitting in Normal mode with no other node present to acknowledge. A bus-off node neither sends nor receives until Recover rejoins it. If your first CAN experiment goes deaf after a few frames, check Health before suspecting wiring, and use Self_Test mode when there is nothing else on the bus.

TWAI: CAN 2.0, with the bus-off trap · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 20 of 56

TWAI: CAN 2.0, with the bus-off trap

Standard and extended frames as separate types so a 29-bit identifier cannot reach an 11-bit frame — and an error state that a single-node bench setup walks straight into.

One controller, three modes

TWAI — Two-Wire Automotive Interface — is CAN 2.0, on an SJA1000-compatible controller. A real bus needs an external transceiver on the TX/RX pins.

type Bus_Mode is (Normal, Listen_Only, Self_Test);

Identifiers that cannot be mixed up

subtype Standard_Id is Interfaces.Unsigned_32 range 0 .. 16#7FF#;        --  11-bit
subtype Extended_Id is Interfaces.Unsigned_32 range 0 .. 16#1FFF_FFFF#;  --  29-bit

type Standard_Frame is record
   Id     : Standard_Id  := 0;
   Remote : Boolean      := False;
   Length : Data_Length  := 0;        --  0 .. 8
   Data   : Data_Bytes   := (others => 0);
end record;
--  Extended_Frame is the same shape with an Extended_Id.

Each frame type carries its own identifier subtype, so the identifier is range-checked against the standard it belongs to, and Send is overloaded on the frame type. You cannot put a 29-bit identifier in a standard frame — it is a compile-time or constraint error, not a malformed frame on the wire.

Remote => True makes it a remote-transmission request: it carries the identifier and the requested length but no data, and a node owning that identifier is expected to answer with a data frame.

Receiving, two ways

Polled, where the sender chooses the width so you must ask:

function  Available   (S : Session) return Boolean;
function  Is_Extended (S : Session) return Boolean;   --  ask BEFORE choosing an overload
procedure Receive (S : Session; F : out Standard_Frame; Got : out Boolean);
procedure Receive (S : Session; F : out Extended_Frame; Got : out Boolean);

Or interrupt-driven, which is the shape you want for a real bus:

procedure Enable_Rx_Interrupt (S : Session);   --  needs the Session
procedure Get (F : out Queued_Frame);          --  does NOT -- call it from another task
function  Rx_Overruns return Natural;

A Queued_Frame carries its own width, so the consumer can tell a standard frame from an extended one without asking the controller. Note the asymmetry: Enable_Rx_Interrupt touches the controller and so needs the held session, but Get deliberately does not — it is meant to be called from a different task, typically a decode task separate from the one that owns the port. Rx_Overruns counts what the queue dropped, which is the number to watch when you suspect your decoder is too slow.

The bus-off trap

type Bus_State is (Active, Warning, Bus_Off);

function  Health  (S : Session) return Bus_State;
procedure Recover (S : Session);

A single node on a bench goes bus-off. Active is normal and Warning means an error counter has passed the warning limit, but Bus_Off means the node took itself off the bus after too many transmit errors — and the classic way to cause that is transmitting in Normal mode with no other node present to acknowledge. A bus-off node neither sends nor receives until Recover rejoins it. If your first CAN experiment goes deaf after a few frames, check Health before suspecting wiring, and use Self_Test mode when there is nothing else on the bus.

TWAI: CAN 2.0, with the bus-off trap · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 20 of 56

TWAI: CAN 2.0, with the bus-off trap

Standard and extended frames as separate types so a 29-bit identifier cannot reach an 11-bit frame — and an error state that a single-node bench setup walks straight into.

One controller, three modes

TWAI — Two-Wire Automotive Interface — is CAN 2.0, on an SJA1000-compatible controller. A real bus needs an external transceiver on the TX/RX pins.

type Bus_Mode is (Normal, Listen_Only, Self_Test);

Identifiers that cannot be mixed up

subtype Standard_Id is Interfaces.Unsigned_32 range 0 .. 16#7FF#;        --  11-bit
subtype Extended_Id is Interfaces.Unsigned_32 range 0 .. 16#1FFF_FFFF#;  --  29-bit

type Standard_Frame is record
   Id     : Standard_Id  := 0;
   Remote : Boolean      := False;
   Length : Data_Length  := 0;        --  0 .. 8
   Data   : Data_Bytes   := (others => 0);
end record;
--  Extended_Frame is the same shape with an Extended_Id.

Each frame type carries its own identifier subtype, so the identifier is range-checked against the standard it belongs to, and Send is overloaded on the frame type. You cannot put a 29-bit identifier in a standard frame — it is a compile-time or constraint error, not a malformed frame on the wire.

Remote => True makes it a remote-transmission request: it carries the identifier and the requested length but no data, and a node owning that identifier is expected to answer with a data frame.

Receiving, two ways

Polled, where the sender chooses the width so you must ask:

function  Available   (S : Session) return Boolean;
function  Is_Extended (S : Session) return Boolean;   --  ask BEFORE choosing an overload
procedure Receive (S : Session; F : out Standard_Frame; Got : out Boolean);
procedure Receive (S : Session; F : out Extended_Frame; Got : out Boolean);

Or interrupt-driven, which is the shape you want for a real bus:

procedure Enable_Rx_Interrupt (S : Session);   --  needs the Session
procedure Get (F : out Queued_Frame);          --  does NOT -- call it from another task
function  Rx_Overruns return Natural;

A Queued_Frame carries its own width, so the consumer can tell a standard frame from an extended one without asking the controller. Note the asymmetry: Enable_Rx_Interrupt touches the controller and so needs the held session, but Get deliberately does not — it is meant to be called from a different task, typically a decode task separate from the one that owns the port. Rx_Overruns counts what the queue dropped, which is the number to watch when you suspect your decoder is too slow.

The bus-off trap

type Bus_State is (Active, Warning, Bus_Off);

function  Health  (S : Session) return Bus_State;
procedure Recover (S : Session);

A single node on a bench goes bus-off. Active is normal and Warning means an error counter has passed the warning limit, but Bus_Off means the node took itself off the bus after too many transmit errors — and the classic way to cause that is transmitting in Normal mode with no other node present to acknowledge. A bus-off node neither sends nor receives until Recover rejoins it. If your first CAN experiment goes deaf after a few frames, check Health before suspecting wiring, and use Self_Test mode when there is nothing else on the bus.

TWAI: CAN 2.0, with the bus-off trap · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 20 of 56

TWAI: CAN 2.0, with the bus-off trap

Standard and extended frames as separate types so a 29-bit identifier cannot reach an 11-bit frame — and an error state that a single-node bench setup walks straight into.

One controller, three modes

TWAI — Two-Wire Automotive Interface — is CAN 2.0, on an SJA1000-compatible controller. A real bus needs an external transceiver on the TX/RX pins.

type Bus_Mode is (Normal, Listen_Only, Self_Test);

Identifiers that cannot be mixed up

subtype Standard_Id is Interfaces.Unsigned_32 range 0 .. 16#7FF#;        --  11-bit
subtype Extended_Id is Interfaces.Unsigned_32 range 0 .. 16#1FFF_FFFF#;  --  29-bit

type Standard_Frame is record
   Id     : Standard_Id  := 0;
   Remote : Boolean      := False;
   Length : Data_Length  := 0;        --  0 .. 8
   Data   : Data_Bytes   := (others => 0);
end record;
--  Extended_Frame is the same shape with an Extended_Id.

Each frame type carries its own identifier subtype, so the identifier is range-checked against the standard it belongs to, and Send is overloaded on the frame type. You cannot put a 29-bit identifier in a standard frame — it is a compile-time or constraint error, not a malformed frame on the wire.

Remote => True makes it a remote-transmission request: it carries the identifier and the requested length but no data, and a node owning that identifier is expected to answer with a data frame.

Receiving, two ways

Polled, where the sender chooses the width so you must ask:

function  Available   (S : Session) return Boolean;
function  Is_Extended (S : Session) return Boolean;   --  ask BEFORE choosing an overload
procedure Receive (S : Session; F : out Standard_Frame; Got : out Boolean);
procedure Receive (S : Session; F : out Extended_Frame; Got : out Boolean);

Or interrupt-driven, which is the shape you want for a real bus:

procedure Enable_Rx_Interrupt (S : Session);   --  needs the Session
procedure Get (F : out Queued_Frame);          --  does NOT -- call it from another task
function  Rx_Overruns return Natural;

A Queued_Frame carries its own width, so the consumer can tell a standard frame from an extended one without asking the controller. Note the asymmetry: Enable_Rx_Interrupt touches the controller and so needs the held session, but Get deliberately does not — it is meant to be called from a different task, typically a decode task separate from the one that owns the port. Rx_Overruns counts what the queue dropped, which is the number to watch when you suspect your decoder is too slow.

The bus-off trap

type Bus_State is (Active, Warning, Bus_Off);

function  Health  (S : Session) return Bus_State;
procedure Recover (S : Session);

A single node on a bench goes bus-off. Active is normal and Warning means an error counter has passed the warning limit, but Bus_Off means the node took itself off the bus after too many transmit errors — and the classic way to cause that is transmitting in Normal mode with no other node present to acknowledge. A bus-off node neither sends nor receives until Recover rejoins it. If your first CAN experiment goes deaf after a few frames, check Health before suspecting wiring, and use Self_Test mode when there is nothing else on the bus.

TWAI: CAN 2.0, with the bus-off trap · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 20 of 56

TWAI: CAN 2.0, with the bus-off trap

Standard and extended frames as separate types so a 29-bit identifier cannot reach an 11-bit frame — and an error state that a single-node bench setup walks straight into.

One controller, three modes

TWAI — Two-Wire Automotive Interface — is CAN 2.0, on an SJA1000-compatible controller. A real bus needs an external transceiver on the TX/RX pins.

type Bus_Mode is (Normal, Listen_Only, Self_Test);

Identifiers that cannot be mixed up

subtype Standard_Id is Interfaces.Unsigned_32 range 0 .. 16#7FF#;        --  11-bit
subtype Extended_Id is Interfaces.Unsigned_32 range 0 .. 16#1FFF_FFFF#;  --  29-bit

type Standard_Frame is record
   Id     : Standard_Id  := 0;
   Remote : Boolean      := False;
   Length : Data_Length  := 0;        --  0 .. 8
   Data   : Data_Bytes   := (others => 0);
end record;
--  Extended_Frame is the same shape with an Extended_Id.

Each frame type carries its own identifier subtype, so the identifier is range-checked against the standard it belongs to, and Send is overloaded on the frame type. You cannot put a 29-bit identifier in a standard frame — it is a compile-time or constraint error, not a malformed frame on the wire.

Remote => True makes it a remote-transmission request: it carries the identifier and the requested length but no data, and a node owning that identifier is expected to answer with a data frame.

Receiving, two ways

Polled, where the sender chooses the width so you must ask:

function  Available   (S : Session) return Boolean;
function  Is_Extended (S : Session) return Boolean;   --  ask BEFORE choosing an overload
procedure Receive (S : Session; F : out Standard_Frame; Got : out Boolean);
procedure Receive (S : Session; F : out Extended_Frame; Got : out Boolean);

Or interrupt-driven, which is the shape you want for a real bus:

procedure Enable_Rx_Interrupt (S : Session);   --  needs the Session
procedure Get (F : out Queued_Frame);          --  does NOT -- call it from another task
function  Rx_Overruns return Natural;

A Queued_Frame carries its own width, so the consumer can tell a standard frame from an extended one without asking the controller. Note the asymmetry: Enable_Rx_Interrupt touches the controller and so needs the held session, but Get deliberately does not — it is meant to be called from a different task, typically a decode task separate from the one that owns the port. Rx_Overruns counts what the queue dropped, which is the number to watch when you suspect your decoder is too slow.

The bus-off trap

type Bus_State is (Active, Warning, Bus_Off);

function  Health  (S : Session) return Bus_State;
procedure Recover (S : Session);

A single node on a bench goes bus-off. Active is normal and Warning means an error counter has passed the warning limit, but Bus_Off means the node took itself off the bus after too many transmit errors — and the classic way to cause that is transmitting in Normal mode with no other node present to acknowledge. A bus-off node neither sends nor receives until Recover rejoins it. If your first CAN experiment goes deaf after a few frames, check Health before suspecting wiring, and use Self_Test mode when there is nothing else on the bus.