Skip to content

CAN Wrapper Module Usage Guide

Arnav Gupta edited this page Oct 27, 2025 · 89 revisions

Welcome to the CAN Wrapper Module Usage Guide. This guide will walk you through how to use the CAN Wrapper Module with examples.

Table of Contents

  1. Overview
  2. Initialisation
  3. A Note on RTOS
  4. Handling Events
  5. Receiving Messages
  6. Handling Errors
  7. Advanced API
  8. Additional Examples
  9. Tips 'n' Tricks
  10. Additional Reading

Overview

The CAN Wrapper Module is designed with usability in mind, providing an easy-to-use API that makes sending and receiving messages onboard TSAT as simple as possible. It also does some extra work for you, such as detecting timeouts and errors that are then reported to you. Under the hood, CAN Wrapper Module uses CAN functions from HAL to perform its duties. For this reason, we say it wraps HAL's usual CAN interface. The API contains a complete set of functions for sending and receiving messages, and a CANMessage structure that's used to represent individual messages. This makes it so that all of our subsystems can seamlessly communicate in a standardized and effective manner.

You must configure a CAN peripheral and enable RTOS in your project to use CAN Wrapper Module. See the CAN Wrapper Module Installation Guide.

Initialisation

This module uses a flexible callback approach to handle events such as incoming messages and communication errors.

The CANWrapper_InitTypeDef lets you provide your callback functions and your subsystem's ID.

CANWrapper_InitTypeDef cw_init = {
		.node_id = NODE_ADCS,    // your subsystem's unique ID in the CAN network.
		.message_callback = &on_message_received, // called when a message is received and ready to be handled.
		.error_callback = &on_error_occured;      // called when a communication error occurs.
};

To initialise CAN Wrapper, call CANWrapper_CAN_Start and CANWrapper_Init sometime between osKernelInitialize and osKernelStart:

CANWrapper_CAN_Start(&hcan1); // starts the CAN peripheral.
CANWrapper_Init(&cw_init); // initializes CAN Wrapper and its RTOS objects.

A Note on RTOS

CAN Wrapper uses RTOS, which enables asynchronous and real-time message handling. CAN messages are placed in a message queue and later processed by a dedicated RTOS task. This allows for efficient handling of time-critical data and faster acknowledgement times.

CAN Wrapper integrates directly with your subsystem's RTOS, adding its own tasks to the RTOS scheduler. As when using RTOS regularly, it's important to consider that any running task may be interrupted by another one.

Handling Events

When an event occurs, (such as a new message arrival or a CAN error is detected) CAN Wrapper triggers your callbacks (i.e. message_callback or error_callback).

Be aware that these callbacks happen in a task context. This means that you need to avoid race conditions and other issues by handling the sharing of resources between your tasks and your callbacks carefully. It also means that the speed of your callbacks is not as crucial as in an ISR context.

Receiving Messages

Here is starter template for a message handling function. Add your specific subsystem's functionality as needed. Note that this code snippet also makes use of the DebugLogger utility to record errors when they occur. Read about it here.

#include "tuk/can_wrapper.h"
#include "tuk/debug.h"

#include <stdbool.h>

void on_message_received(CAN_HandleTypeDef *hcan, CANMessage *msg)
{
	if (*hcan != hcan1) return;

	LogBuffer debug_log;
	DebugLogger_Push_Buffer(&debug_log);

	ErrorID error = ERR_OK;

	switch (msg->cmd)
	{
	case CMD_PLD_SET_ACTIVE_ENVS: // example command.
	{
		// get the command arguments as defined in the command reference.
		uint16_t envs = GET_MSG_DATA(msg->body, 0, uint16_t); // syntax: GET_MSG_DATA(body, byte #, type)

		// perform instructed action.
		for (int i = 0; i < 16; i++)
		{
			bool is_active = envs & 1;
			
			error = TCS_Set_Well_Heating(i, is_active);
			error = LEDs_Set_Power(i, is_active);
			
			envs = envs >> 1;
		}
		break;
	}
	// ...
	default:
	{
		// unrecognized command.
		error = ERR_UNKNOWN_COMMAND;
		break;
	}
	}

	if (error)
	{
		uint8_t error_body[CAN_MAX_BODY_SIZE] = {0}; // Best Practice: always initialize message bodies with zeros.

		SET_MSG_DATA(error_body, 0, uint8_t, error);
		SET_MSG_DATA(error_body, 1, uint8_t, msg.cmd);
		SET_MSG_DATA(error_body, 2, LogBuffer, debug_log);

		CANWrapper_Transmit(&hcan1, NODE_CDH, CMD_CDH_PROCESS_COMMAND_ERROR, error_body);
	}

	DebugLogger_Pop_Buffer();
}

🦺 Best Practice: As usual, make sure to have sanity checks in place in all your functions, especially if a function is meant to directly handle data from the CAN bus. You cannot assume the data you are receiving is valid and correct!

Handling Errors

Here is starter template for an error handling function. Currently, the only type of error that is reported is a timeout event.

#include "tuk/can_wrapper.h"

void on_error_occured(CANWrapper_ErrorInfo *error_info)
{
	switch (error_info->error)
	{
	case CAN_WRAPPER_ERROR_TIMEOUT:
	{
		// your call to CANWrapper_Transmit failed to invoke an Acknowledge message
		// in your target recipient. Or, the ACK message simply didn't reach you.
		// This was detected as a timeout event.
		// Here you can resolve the issue as appropriate.

		// You may want to run a check to see if it's still a good idea to resend
		// your message.

		// If all is well, you can re-send the message to the intended recipient like so:
		CANWrapper_Transmit(&hcan1, error_info->msg.recipient, error_info->msg.cmd, error_info->msg.body);

		break;
	}
	}
}

🗒️ Note: The error handling functionality is quite bare in this version. It only notifies of timeouts, but there a plenty of other things that can go wrong. Expect improvements in the future.

Advanced API

CAN Wrapper has an optional API which provides some advanced features, which is useful for hardware-in-the-loop devices but should not be used for flight code. To enable the advanced API, define the symbol CAN_WRAPPER_ADVANCED_API in your preprocessor settings.

The changes in the advanced API are as follows:

The CANWrapper_Transmit_Raw Function

CANWrapper_Transmit is replaced with CANWrapper_Transmit_Raw. This function allows you to send completely custom, even nonsensical, messages. Additionally, the node_id field is removed from CANWrapper_InitTypeDef, allowing the sender to masquerade as any subsystem.

For (bad) example:

CANMessage msg = {
	.cmd = CMD_CDH_DEPLOY_ANTENNA;
	.body = { 0 },
	.body_size = 3,
	.priority = 12,
	.sender = NODE_PAYLOAD,
	.recipient = NODE_POWER,
	.is_ack = false
}
bool strict_timeout = false; // CAN Wrapper will ignore an ACK timeout error
CANWrapper_Transmit_Raw(&hcan1, &msg, strict_timeout);

TX and RX Callbacks

Two callbacks are added to CANWrapper_InitTypeDef to provide greater control over message handling.

rx_callback is called when a CAN message is received, and lets you selectively choose what CAN Wrapper should do with the message:

void rx_callback(const CAN_HandleTypeDef *hcan, const CANMessage *msg, uint8_t *rx_behaviour) {
	if (msg->recipient == NODE_ADCS) {
		*rx_behaviour |= RX_HANDLE; // tells CAN Wrapper to trigger message_callback
	}
	if (msg->cmd == CMD_ADCS_SET_MAGNETORQUER_DIRECTION) {
		*rx_behaviour |= RX_ACK; // tells CAN Wrapper to send an ACK
	}
	if (msg->sender == NODE_ADCS && msg->is_ack) {
		*rx_behaviour |= RX_CLEAR_TX_STORE; // tells CAN Wrapper that an ACK has been received for a sent message
	}
}

🦺 Note: rx_callback happens in an ISR context.

tx_callback is called once a CAN message has been sent. You can implement any handling you want:

void tx_callback(const CAN_HandleTypeDef *hcan, const CANMessage *msg) {
	if (*hcan == &hcan1) {
		// release a TX mutex, allowing another message to be sent
		osMutexRelease(txMutexHandle);
	}
	else if (*hcan == &hcan2) {
		// send confirmation of TX over UART
		uint8_t serializedData[12];
		serializeCANMessage(msg, serializedData);
		serializedData[11] = TX_CONFIRMATION;
		HAL_UART_Transmit(&huart1, serializedData, sizeof(serializedData), HAL_MAX_DELAY);
	}
}

Additional Examples

Reporting PCB Temperature to CDH

#include "tuk/can_wrapper.h"

#include <stdbool.h>

bool Report_PCB_Temp()
{
	// measure the temperature.
	uint16_t temp;
	ErrorID error = TMP235_Read_Temp(&temp);
	if (error == ERR_OK)
	{
		// now create a message body for a report.
		uint8_t msg_body[CAN_MAX_BODY_SIZE] = {0};
		uint8_t tel_key = CREATE_TELEMETRY_KEY(TEL_PCB_TEMP, NODE_ADCS);
		SET_MSG_DATA(msg_body, 0, uint8_t, tel_key);
		SET_MSG_DATA(msg_body, 1, uint8_t, s_sequence_num);
		SET_MSG_DATA(msg_body, 2, uint8_t, 0); // packet #
		SET_MSG_DATA(msg_body, 3, uint16_t, temp);

		// send the message.
		CANWrapper_Transmit(&hcan1, NODE_CDH, CMD_CDH_PROCESS_TELEMETRY_REPORT, msg_body);

		s_sequence_num++;
	}
	else
	{
		// failed to read temperature.
		// send an error report.
		uint8_t msg_body[CAN_MAX_BODY_SIZE] = {0};
		SET_MSG_DATA(msg_body, 0, uint8_t, error);
		SET_MSG_DATA(msg_body, 1, uint8_t, CONTEXT_REPORTING_PCB_TEMP);
		CANWrapper_Transmit(&hcan1, NODE_CDH, CMD_CDH_PROCESS_RUNTIME_ERROR, msg_body);
	}

	return success;
}

🦺 Best Practice: Unless your best judgement says otherwise, favour hard-coding the recipient of a message you are about to send. This keeps your code more predictable.

The CANMessage Type

When receiving a message, you will have to read the fields of the CANMessage type:

typedef struct
{
	CmdID cmd;
	uint8_t body[CAN_MAX_BODY_SIZE];
	uint8_t body_size;
	uint8_t priority;
	NodeID sender;
	NodeID recipient;
	uint8_t is_ack;
} CANMessage;

Note that you should avoid creating or modifying messages directly. Instead, use the SET_MSG_DATA macro to create a message body to be passed to CANWrapper_Transmit. Similarly, use the GET_MSG_DATA macro to parse the body of a received message.

Using these macros will make your code easier to work with and provide flexibility for the developers of TUK to change underlying implementations in the future.

Here are some usage examples of the SET_MSG_DATA and GET_MSG_DATA macros:

uint8_t msg_body[CAN_MAX_BODY_SIZE] = {0}; // Best Practice: always initialize messages with zeros.

// SET_MSG_DATA allows you to assign larger types to the message body very easily.
// Warning: make sure your data is no more than 7 bytes! (56 bits)
uint32_t large_number = 4294967295;
SET_MSG_DATA(msg_body, 0, uint32_t, large_number);

// you can access arguments in a similar way
uint8_t single_byte = GET_MSG_DATA(msg, 0, uint8_t); // retrieves byte 0 in the message body.
uint32_t four_bytes = GET_MSG_DATA(msg, 0, uint32_t);  // retrieves bytes 0-3 in the message body.

Tips 'n' Tricks

  • To quickly search for commands in the code editor, type the name of a prefix (e.g. one of CMD_PLD, CMD_ACDS, CMD_PWR, or CMD_CDH) and press Ctrl + Space. You'll then be greeted with a list of matching commands. (assuming you've included tuk/can_wrapper.h)
  • If you haven't already, check out the Command Reference for TSAT-7 to read up on the expected format of each command. Ensuring the format you send is correct is important, as otherwise the data received may be incorrect or it might be cut off.

Additional Reading

The below link is old, so I don't recommend reading it, but I will leave it here in case you want to read the documentation for the old version of this interface or you want to understand the high level concepts of how CAN works. Definitely not a required read to use this module.

https://drive.google.com/file/d/1HHNWpN6vo-JKY5VvzY14uecxMsGIISU7/view?usp=share_link