Skip to content

Onboarding Task

Jack Opgenorth edited this page Sep 21, 2022 · 9 revisions

2022 NOTE

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

Onboarding task

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.

Get your bearings.

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 project

Copy blinky to new folder named <yourname>_onboarding.

Explain how to copy/set VSCode configurations (using c_cpp_properties.py script and launch.json)

Explanation of how blinky works

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.

Change out blinky

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"

Node tracking structure

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.

Node tracking function

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)

Modify blinky thread

// 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.

Register message handler

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!

ADC Reading Exercise

Configure

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

Now configure hardware:

Add the following after starting heartbeat thread:

  palSetPadMode(GPIOA, 0, PAL_MODE_INPUT_ANALOG);
  adcStart(&ADCD1, &adc1_config);

Reading ADC value

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?)

Send value over CAN

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.