-
Notifications
You must be signed in to change notification settings - Fork 0
CAN Wrapper Module Usage Guide
Welcome to the CAN Wrapper Module Usage Guide. This guide will walk you through how to use the CAN Wrapper Module with examples.
- Overview
- Initialisation
- Polling Events
- Immediate Mode
- Receiving Messages
- Handling Errors
- Additional Examples
- Tips 'n' Tricks
- Additional Reading
The CAN Wrapper Module is designed with useability 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 in your project to use CAN Wrapper Module. See the CAN Wrapper Module Installation Guide.
This module uses a flexible callback approach to handle events such as incoming messages and communication errors.
The CANWrapper_InitTypeDef structure contains many settings that tune the module to your needs.
CANWrapper_InitTypeDef cw_init = {
.node_id = NODE_ADCS, // your subsystem's unique ID in the CAN network.
.notify_of_acks = false, // whether to notify you of incoming ACK's.
.hcan = &hcan1, // pointer to the CAN peripheral handle.
.htim = &htim16, // pointer to the timer handle.
.message_callback = &on_message_received, // called when a new message is polled.
.error_callback = &on_error_occured; // called when a communication error occurs.
};To initialise CAN Wrapper, call CANWrapper_Init sometime after MX_CAN1_Init with your chosen settings:
CANWrapper_Init(cw_init);When an event occurs, (such as a new message arrival or a CAN error is detected) CAN Wrapper puts it into a queue for deferred processing. This was a design decision which arose from the need to keep interrupt service routines (ISRs) as short as possible.
Because of this design choice, CAN Wrapper will not trigger your callbacks (i.e. message_callback or error_callback) until you've explicitly instructed it to process events.
Call CANWrapper_Poll_Events to process incoming messages and errors. In most cases, putting this function call in your main loop will work fine.
If instead you wish to have full control over the processing of incoming messages, CAN Wrapper offers an Immediate Mode.
In Immediate Mode, messages are directly forwarded to the message_callback on arrival. In this mode, it will be illegal to call CANWrapper_Poll_Messages as there will be no queue to poll.
If you are using RTOS and wish to process messages in an asynchronous task, the recommended approach is to enable Immediate Mode and use the message_callback as a "loading zone" where messages are placed into an RTOS queue to be processed in a dedicated task.
⚠️ Caution: Remember that when you are using Immediate Mode, your message callback is called from within an ISR. This means all contemporary wisdom about ISR-safety applies. Keep processing short, and be weary that your code could be executing during any state of your program (valid or invalid).
To enable Immediate Mode, open your STM32 project, and navigate to Project > Properties > C/C++ Build > Settings > MCU GCC Compiler > Preprocessor. On the right hand panel, click the "Add" button to add the symbol CWM_IMMEDIATE_MODE. This will cause the CAN Wrapper Module to compile in Immediate Mode.
Keep in mind, Immediate Mode only affects the processing of messages, not errors. You must still make regular calls to CANWrapper_Poll_Errors to ensure errors are detected by your program. If you are using RTOS, it's recommended that you create a dedicated task for this purpose that executes at regular time intervals.
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(CANMessage msg)
{
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_ARG(msg, 0, uint16_t); // syntax: GET_ARG(msg, 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)
{
CANMessage error_report = {0}; // Best Practice: always initialize messages with zeros.
error_report.cmd = CMD_CDH_PROCESS_COMMAND_ERROR;
SET_ARG(error_report, 0, uint8_t, error);
SET_ARG(error_report, 1, uint8_t, msg.cmd);
SET_ARG(error_report, 2, LogBuffer, debug_log);
CANWrapper_Transmit(NODE_CDH, &error_report);
}
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!
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(error_info.msg.recipient, &error_info.msg);
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.
#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 for a report.
CANMessage msg = {0};
msg.cmd = CMD_CDH_PROCESS_TELEMETRY_REPORT;
uint8_t tel_key = CREATE_TELEMETRY_KEY(TEL_PCB_TEMP, NODE_ADCS);
SET_ARG(msg, 0, uint8_t, tel_key);
SET_ARG(msg, 1, uint8_t, s_sequence_num);
SET_ARG(msg, 2, uint8_t, 0); // packet #
SET_ARG(msg, 3, uint16_t, temp);
// send the message.
CANWrapper_Transmit(NODE_CDH, &msg);
s_sequence_num++;
}
else
{
// failed to read temperature.
// send an error report.
CANMessage msg = {0};
msg.cmd = CMD_CDH_PROCESS_RUNTIME_ERROR;
SET_ARG(msg, 0, uint8_t, error);
SET_ARG(msg, 1, uint8_t, CONTEXT_REPORTING_PCB_TEMP);
CANWrapper_Transmit(NODE_CDH, &msg);
}
return success;
}🛟 Best Practice: Unless your best judgement says otherwise, favour hard-coding the recipient of a message you are about to send. This reduces the variability of behaviour in your code.
Here's the full definition of the CANMessage type:
typedef struct
{
CmdID cmd;
uint8_t body[CAN_MAX_BODY_SIZE];
uint8_t priority;
NodeID sender;
NodeID recipient;
uint8_t is_ack;
} CANMessage;Note that when transmitting a message, the only two fields that matter are .cmd and .body. All other fields will be ignored.
Here are some usage examples of the CANMessage type:
CANMessage msg = {0}; // Best Practice: always initialize messages with zeros.
msg.cmd = CMD_CDH_PROCESS_HEARTBEAT; // set command ID.
msg.body[0] = 'A'; // set first byte in message body. (ie. the byte after command ID)
SET_ARG(msg, 0, char, 'A'); // equivalent.
// SET_ARG 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_ARG(msg, 0, uint32_t, large_number);
// you can access arguments in a similar way
uint8_t single_byte = GET_ARG(msg, 0, uint8_t, single_byte); // retrieves byte 0 in the message body.
uint32_t four_bytes = GET_ARG(msg, 0, uint32_t, four_bytes); // retrieves bytes 0-3 in the message body.Please use the SET_ARG and GET_ARG macros instead of manually accessing data from the message buffer. This will make your code easier to work with and provides flexibility for the developers of TUK to change underlying implementations in the future.
- 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, orCMD_CDH) and pressCtrl + Space. You'll then be greeted with a list of matching commands. (assuming you've includedtuk/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.
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