-
Notifications
You must be signed in to change notification settings - Fork 3
Onboarding Task
This page is not 100% finished and is not being worked on anymore, so it will not function as a complete tutorial. Some stuff here still might be useful for new members though. For more beginner and on-boarding material, take a look at the stuff in the examples folder
Not a comprehensive guide to ChibiOS, or embedded development, hopefully just a step-by-step guide that gives you a vague idea of how things work.
Take a look around the repo, and just see where things are, if it doesn't make sense, just take note of it for now.
Read through blinky code and try and understand what's going on.
(Start with blinky/src/main.c
).
Take a peek through the ChibiOS documentation, try and find some of the functions that we're using.
Peek through STM32F303 reference manual (make very clear that you don't need to read the thing cover-to-cover, just like, read GPIO or something).
Copy blinky to new folder named <yourname>_onboarding
.
Explain how to copy/set VSCode configurations (using c_cpp_properties.py script and launch.json)
Just a simple overview so people know how the code functions.
I should add code samples of main.c, instead of relying on people being able to follow along through the file.
This will be a simple exercise, maybe shows some way that we solve problems.
First include the NodeStatus message type:
#include "uavcan/protocol/NodeStatus.h"
First we create a fairly simple structure and set of variables to maintain a simple list.
// Struct to declare a node seen.
typedef struct {
uint8_t node_id;
systime_t last_seen;
} bus_node_id;
#define NUM_NODES_TO_WATCH 10
// List of nodes seen on bus (as determined by NodeStatus messages)
static bus_node_id active_nodes[NUM_NODES_TO_WATCH];
static uint8_t num_active_nodes = 0;
We want to know the node ids, and the time we saw them last, so we can keep track of which nodes we've seen, and detect when they've gone offline.
Also create an array, and a variable to track the length of this list seperately.
Explain everything is static so that it stays inside of this compilation unit, and no other portions of code can accidentally link to these variables, which could cause issues.
num_active_nodes
set to 0 so that it will be safe by default.
static void delete_active_node_entry(uint8_t index) {
for (int i = index; i < num_active_nodes - 1; i++) {
active_nodes[i] = active_nodes[i+1];
num_active_nodes -= 1;
}
}
static void watch_heartbeat(CanardInstance* ins, CanardRxTransfer* transfer) {
(void)ins;
uavcan_protocol_NodeStatus status;
int32_t rc = uavcan_protocol_NodeStatus_decode(transfer, transfer->payload_len, &status, NULL);
if (rc < 0) {
// Error! In an actual application we would want to add some logging or something
// to indicate this, but here it doesn't matter
return;
}
// Search for an existing entry, and update it if found
bool entry_exists = false;
for (int i = 0; i < num_active_nodes; i++) {
if (active_nodes[i].node_id == transfer->source_node_id) {
entry_exists = true;
active_nodes[i].last_seen = chVTGetSystemTime();
}
}
// If we haven't seen this already and our list isn't full, add it to the list
if (!entry_exists && num_active_nodes < NUM_NODES_TO_WATCH) {
active_nodes[num_active_nodes].node_id = transfer->source_node_id;
active_nodes[num_active_nodes].last_seen = chVTGetSystemTime();
num_active_nodes += 1;
}
// Clean up old nodes - we should be receiving these message types at least once
// per second for each node on the bus, so cleanup is fine to handle hear (frequent
// enough to actually catch nodes going down, and not too much to handle for frequent
// message types)
systime_t time = chVTGetSystemTime();
for (int i = 0; i < num_active_nodes; i++) {
if (TIME_I2MS(time - active_nodes[i].last_seen) > 5000) {
// Delete entry in place
delete_active_node_entry(i);
// Next entry is now in this entry's place, we need to check this spot again
i -= 1;
}
}
}
Explain libcanard stuff:
- Function signature needs to be this form to match handler type (elaborate when registering handler)
- process to decode NodeStatus message
- fairly standard C, do I need to explain much here?
- chVTGetSystemTime() and TIME_I2MS(), explain what they do and why they're used
- potential pitfalls of wrapping timestamp (need to actually figure out what happens here)
// Small extra blinker thread to indicate we are alive.
static THD_WORKING_AREA(waHeartbeatThread, 128);
static THD_FUNCTION(HeartbeatThread, arg) {
(void)arg;
while (1) {
// Blink N times for N nodes seen
for (int i = 0; i < num_active_nodes; i++) {
palSetLine(LINE_LED);
chThdSleepMilliseconds(200);
palClearLine(LINE_LED);
chThdSleepMilliseconds(200);
}
// Long delay between sets of blinks
chThdSleepMilliseconds(1000);
}
}
Fairly self explanatory, just do a quick explanation.
static void watch_heartbeat(CanardInstance* ins, CanardRxTransfer* transfer);
can_msg_handler can_broadcast_handlers[] = {
CAN_MSG_HANDLER(UAVCAN_PROTOCOL_NODESTATUS_ID, UAVCAN_PROTOCOL_NODESTATUS_SIGNATURE, watch_heartbeat),
CAN_MSG_HANDLER_END,
};
can_msg_handler can_request_handlers[] = {CAN_MSG_HANDLER_END};
Explain that we declare the prototype up here so we can put the function definition wherever.
We use these structures as convenient methods to register a message handler. Each one looks for a specific message ID and signature, and when one of those comes in, passes that to the function you give. Macros are used as convenience, and to reduce text bloat.
void (*handler)(CanardInstance* ins, CanardRxTransfer* transfer);
Function pointer to handler, you have to follow this to implement a handler. Build will fail if this is incorrect, so don't worry too much. The error message can be a bit obtuse though.
Once this is done, it should blink!
Start in cfg
.
Edit cfg/halconf.h
Change HAL_USE_ADC to TRUE
#define HAL_USE_ADC TRUE
(Explain here what this does, it enables the ADC HAL module so everything gets built)
Edit cfg/mcuconf.h
Change STM32_ADC_USE_ADC1 to TRUE
(Explain what this does, it creates the ADC1 object or whatever)
Edit src/main.c
Add the following:
static ADCConfig adc1_config = {
.difsel = 0,
};
static ADCConversionGroup measure_conversion = {
.circular = false,
.num_channels = 1,
.end_cb = NULL,
.error_cb = NULL,
.cfgr = 0,
.tr1 = 0,
.smpr = {
ADC_SMPR1_SMP_AN1(ADC_SMPR_SMP_601P5),
0
},
.sqr = {
ADC_SQR1_SQ1_N(1),
0,
0,
0},
};
ADC configuration is needed for device-specific stuff. (ChibiOS can't encapsulate everything)
Not strictly necessary here, as the default is exactly what we're doing here.
DIFSEL is all 0's so all the ADC inputs are single-ended (i.e. referenced to ground)
Maybe explain how we know what the struct looks like (alternative is clicking through the defines)
- ADCConfig: ChibiOS/os/hal/include/hal_adc.h
- adc_lld_config_fields: somewhere in ports?
- check ChibiOS/os/hal/ports/STM32
- ChibiOS/os/hal/ports/STM32/STM32F3xx for the F303
- Check platform.mk, will include different versions of ADC driver
- Specifically include $(CHIBIOS)/os/hal/ports/STM32/LLD/ADCv3/driver.mk
- Now can look in that folder, and see what adc_lld_config_fields is defined as
- This can also be done just by clicking through in a properly configured VSCode
ADCConversionGroup is similar, except here there's some common configuration to set as well
Common stuff:
- Circular mode means it will keep starting over again. We want a single reading.
- We only want to use 1 ADC channel (measure 1 pin)
- No ending callback, we're waiting for it to finish
- No error callback, assuming no errors - we aren't doing anything fancy
Peripheral-specific:
- default CFGR is fine, take a look at the RM
- No threshold value to set, this is a special feature of this ADC, see RM if curious
- Set the highest sample time for the most accurate value on channel 1
- Use channel 1 as first capture in sequence - length is set by driver, so we can ignore setting that
Add the following after starting heartbeat thread:
palSetPadMode(GPIOA, 0, PAL_MODE_INPUT_ANALOG);
adcStart(&ADCD1, &adc1_config);
Add a new thread:
static THD_WORKING_AREA(waAdcThread, 256);
static THD_FUNCTION(AdcThread, arg) {
(void)arg;
while (1) {
adcsample_t val;
msg_t rc = adcConvert(&ADCD1, &measure_conversion, &val, 1);
if (rc == MSG_OK) {
float voltage = ((float) val / 4096) * 3.3;
} else {
// Should never get here
chSysHalt("ADC Error - should be unreachable");
}
}
}
Explain that adcConvert starts a conversion and waits for it to finish. Perhaps explain async functions as well.
Then explain how voltage is calculated.
Start the thread in main after configuration:
palSetPadMode(GPIOA, 0, PAL_MODE_INPUT_ANALOG);
adcStart(&ADCD1, &adc1_config);
chThdCreateStatic(waAdcThread, sizeof(waAdcThread), NORMALPRIO,
AdcThread, NULL);
Sets pad mode to an analog input - GPIO has to be configured correctly
so that different peripherals can use them. There is a default configuration
in board.h
in the SPEEDY_CONFIG_F3 folder, but it doesn't include this one.
Then we start ADC thread, and give it access to the waAdcThread, where the stack is placed. (maybe brief overview/reminder of stack?)
Hijacking a v0 message type, situation should be improved with v1, but for now making new types is annoying and out of scope
include message type:
include "uavcan/equipment/power/CircuitStatus.h"
Now back in AdcThread:
extern CanardInstance canard_instance; // Code smell, this should be wrapped with a mutex of some sort
static THD_WORKING_AREA(waAdcThread, 256);
static THD_FUNCTION(AdcThread, arg) {
(void)arg;
uint8_t transfer_id = 0;
while (1) {
adcsample_t val;
msg_t rc = adcConvert(&ADCD1, &measure_conversion, &val, 1);
if (rc == MSG_OK) {
float voltage = ((float) val / 4096) * 3.3;
uint8_t msg_buf[UAVCAN_EQUIPMENT_POWER_CIRCUITSTATUS_MAX_SIZE];
uavcan_equipment_power_CircuitStatus status = {
.circuit_id = 0,
.voltage = voltage,
.current = 0,
.error_flags = 0,
};
uint32_t len = uavcan_equipment_power_CircuitStatus_encode(&status, (void *)msg_buf);
canardBroadcast(&canard_instance,
UAVCAN_EQUIPMENT_POWER_CIRCUITSTATUS_SIGNATURE,
UAVCAN_EQUIPMENT_POWER_CIRCUITSTATUS_ID,
&transfer_id,
0,
(const void *)msg_buf,
len);
transfer_id += 1;
} else {
// Should never get here
chSysHalt("ADC Error - should be unreachable");
}
}
}
Explain we need to get canard_instance
from somewhere, and that this is actually bad and there should be
wrappers around it.
Explain transfer_id, and how it needs to be static and monotonic.
Message buffer of maximum message size, then create the message. Use the generated encode function
to serialize into that buffer (saving the length).
Finally, broadcast message with signature and ID. Note that this just enques message onto a list,
transmission will be handled in can_handle_forever
.