1 of 27

Solar Car Team at UVA

Embedded Subteam

Ethan Mathias

Embedded Lead

C++ and FreeRTOS

Part 2

How do we make the code that controls the car?

2 of 27

Overview

  • C/C++:
    • Basics: casting
    • Memory: pointers, references, arrays
  • FreeRTOS specifics:
    • Concurrency: reacting to events, deferred work
  • CAN: sending and receiving messages
  • Code Standards: documentation, style guide, logging

3 of 27

Data Types: Casting

  • Data can be converted between different data types with casting
    • Used to satisfy C++ type checking
  • Values are ‘padded’ or truncated as appropriate
  • Avoid needless casting

4 of 27

Pointers

  • A pointer is a location in memory
  • Declared as type *varName
  • Use the & operator to get the memory address of another variable

5 of 27

Pointers

  • Use the * operator to ‘dereference’ the pointer, getting the value at that memory address
  • If a pointer points nowhere, assign it as a Null Pointer
  • Avoid pointers when possible, pointer mismanagement causes many headaches

6 of 27

Pointers in Function Calls

  • Pointers can appear in function headers, just like any other data type

7 of 27

Pointers to Pointers

8 of 27

References in Function Calls

  • Used when a parameter needs to be changed inside the function
  • A pointer can do this, but references are syntactically nicer
  • When the function is called, the normal non-pointer variable is passed in

9 of 27

Arrays

  • Arrays store an ordered list of variables of the same type
  • Can be declared like:

or

  • Arrays as a data type are extremely similar to pointers

10 of 27

Arrays

  • Array members are accessed by their index, starting at 0
  • Members of the array can be changed just as easily:
  • The sizeof(…) function can be used to get the size of an array, but it returns the size in bytes, not elements
    • To get the number of elements, divide by the size of the data type

11 of 27

Reacting to Events

  • Every input on the car arrives physically in one of two ways:

​

​

​

​

​

​

​

​

​

​

There are two ways a task can find out "is there new data yet?"

Polling: ask on a timer, over and over, whether anything's ready

Example: CAN::read() checks the FIFO every 1ms, whether or not a message is there

Simple to write, but the CPU keeps doing work even when there's nothing to do

Interrupt: the hardware itself signals the CPU the instant something actually happens

Example: HAL_UART_RxCpltCallback() fires the moment a byte arrives

The task sleeps in between, using zero CPU until there's really something to do

�

12 of 27

Polling in a Thread

13 of 27

Timeout and MessageQueue

14 of 27

CAN

  • A CAN message is a set of data that is transmitted over the CAN network
  • Signals – the individual data in a CAN message being sent
  • ‘throttle’ is a signal
  • We interact with CAN messages as a struct
  • CAN messages are transmitted in a serialized format
  • Each CAN message is defined in a dbc file, in the separate CAN-messages repo
  • Rivanna3S.dbc, BPS.dbc, MPPT.dbc and MotorController.dbc

15 of 27

CAN: Defining Messages

  • Each message type is defined in a dbc file
  • C++ structs are auto-generated from these by generate.py

16 of 27

CAN: Sending Messages

  1. A CAN struct is created and filled with data
  1. the struct is sent, using the write() function from the CAN interface

17 of 27

CAN: Sending Messages

  • 3. write() serializes the struct and puts the data on the CAN bus
  • 4. We log the struct’s signals (for debugging)

18 of 27

CAN: Receiving Messages

  • 1. Register a callback for each message ID this board cares about
  • The old pattern – subclass the CAN interface and override message_handler() – is gone

19 of 27

CAN: Receiving Messages

  • 2. When that message arrives, CanInterface’s handler thread calls your function
  • 3. Deserialize into the struct, then do whatever you want with the data

20 of 27

CAN: Two Networks

  • The Motor Board sits on two buses: the main car bus and the motor controller’s own bus
  • One CanInterface per bus, each with its own receiver and handler thread

21 of 27

Heartbeat: the Safety Net

  • Every board broadcasts a Heartbeat message every 100 ms
  • If any board goes quiet for 500 ms, your callback fires and the car can shut down safely
  • Built on Timeout and CAN callbacks – no polling anywhere

22 of 27

23 of 27

Documentation: Comments

  • Anything that isn’t apparent should be commented
  • C++ comments start with //
  • Target audience is college students who have taken 1 CS class
  • Reading comments isn’t meant to replace reading the actual code

24 of 27

Documentation: Docstrings

  • Text summary of what the function does, describing the input parameters and return value, and any other relevant info
    • Should resolve any ambiguities about the function
  • Most importantly: when you hover over a function in VS Code, the docstring is shown

25 of 27

Style Guide: Naming

  • Struct names: Camel case starting with a capital
    • Ex. DataPoint
  • Function names: snake case
    • Ex. function_that_does_stuff()
  • Variable that is set once: snake case
    • Ex. input_files
  • Variable that changes: Camel case starting with a lower
    • Ex. countValue
  • Constants: capitalized snake case
    • Ex. MAX_LENGTH

26 of 27

Style Guide: Misc.

  • Functions should have a blank line before and after declaration
  • Spaces around operators
  • Blocks of code that are logically separate should be separated by a blank line
  • Try to limit each line of code to under 80 characters
    • This allows code to be easily read when split-screening
  • #define constants should be defined in the *.cpp file if they are only meant to be used in that *.cpp file

27 of 27

Logging Hierarchy

  • Pick the level from the question the log answers, not from how urgent it feels
  • Each board sets its own threshold in app_main() – some run at INFO_LVL, some at DEBUG_LVL
  • log_fault() is the one the heartbeat and fault handlers use