C Coding Standards

This page aims to highlight code structure and architecture practices used throughout all C applications. Please note that anything on this page is just choices that we maintain- they are not objective rules , nor are they the only “correct” way.

 

This is not where syntax/style standards are discussed. For that, visit this page

Any code snippets below are probably closer to pseudocode, they are meant to depict a concept not to be copy and pasted

 

Jump to:

General Philosophy

These standards were developed with the following goals and realities in mind:

  1. Replicability

We want these standards to work for all applications, regardless of purpose, hardware, or other specifics.

  1. User-Friendly

Given the high turnover within the club, standards should be easy to follow and maintain a logical flow

  1. Memory-Consciousness

Running into memory issues sucks, and our standards will ensure efficiency in that regard

  1. Industry-Standardization

We want users to gain technical skills applicable to industry, these rules should be common methods. The Linux Kernel is always a good starting point, and will be the foundation of most practices (though we can veer away as needed)

  1. Being Realistic

Whether in support of or as an exception to anything above, we want to keep in mind that our hardware peripherals and high-level architecture are not changing too much on a year-to-year basis, and we will take advantage of that fact to promote simplicity

 

Standards

Object Handling

all hardware abstractions should belong to an object struct for that peripheral. There should be no floating global variables that belong to a piece of hardware.

 

This does not mean every variable needs to belong to an object, just that any variable associated with hardware should be encapsulated this way.

 

typedef struct { CAN_HandleTypeDef *hcan; const uint32_t *id_list; uint8_t id_list_len; } can_t;

 

here is an example of how to do this well. The can_t object takes STM’s hardware peripheral object, along with our own details like the CAN ID whitelist, and encapsulates this together.

 

Other examples could include:

  • creating a fan object to contain the PWM pins and controls, the duty cycle, etc.

  • creating a battery data object to store every piece of information associated with out accumulator, as is done with the acc_data_t in Shepherd

 

Movement of Objects

Whenever possible, the objects above should be used locally by utilizing the extern keyword. There are excpetions (like acc_data in Shepherd), but in general, we should prefer this over passing references to every function.

 

Instead of this:

uint8_t can_init(CAN_HandleTypeDef* hcan, can_t* canline, uint8_t id_list) { canline->hcan = &hcan; canline->id_list = id_list ... } (main.c) local_id_list1 = 0b01010101; local_id_list2 = 0b10101010; compute_init(&hcan1, &can1, local_id_list1); compute_init(&hcan2, &can2, local_id_list2);

 

 

Do this:

extern CAN_HandleTypeDef hcan1; extern CAN_HandleTypeDef hcan2; can_t can1; can_t can2; uint8_t compute_init() { can1.hcan = &hcan1; local_id_list = 0b01010101; can1.id_list = local_id_list; can2.hcan = &hcan2; local_id_list = 0b1010101; can2.id_list = local_id_list;

 

This choice is definitely one that you will see both ways in embedded, and is arguable the most debatable one here. In the end, we this choice was made because:

  1. using the same object is more memory efficient (even if barely)

  2. More simple, easier to keep track of objects

The main drawback to this is it is not as scalable, ie, adding a third CAN line would be much more challenging in this method than by using references. But, see philosophy rule #5 for why this is an acceptable drawback.

 

Data Types

Miscellaneous individual guidelines here:

 

  • Use unsigned ints for anything that does not correlate to a signed, real world value or flag

    • Our applications deal with so much data that it can be hard to remember what values can actually be negative. To simplify this, anything made signed (int, int8, int16…) should be done so deliberately/ because it needs to be negative at times

  • Bools should only be used for logical true or false (ex: device_connected)

    • use uint8s for anything logical high or low (ex: pin state)

 

  • Use ints multiplied by factors of ten in place of floats for long-term data storage

    • Floats take up more space than int8 and int16. For any variable that is stored long term, meaning it is not a local intermediate that will be popped from the stack quickly, we should store is as an int multiplied by 10, 100, 10000, etc to maintain the decimal precision.

    • ex: acc_data in Shepherd stores voltages as a factor of 10,000, so a stored value of 37650 = 3.7650V

 

Driver Generalization

This is mainly applicable for drivers found in Embedded Base

Also, this rule is almost a direct contradiction to the rule described in “Movement of Objects”, but since these are not being treated as objects, I’m allowing it.

 

All embedded base drivers should be made independent of any HAL or other driver. Instead of relying on a specific HAL, function pointers should be used to allow any HAL to work with the driver

 

Instead of this:

 

 

Which relies on STM’s HAL i2c driver,

 

Do this:

 

While this is obviously more work, it means that these drivers should succeed for the foreseeable future and for all different chip sets in use right now, without having to change.

 

Also note the method of updating these values does utilize extern. For devices where more than one of these devices may be present on the bus, have the driver and its APIs have a device select parameter to dynamically update to a different device, do not make multiple instances of the device struct. In the above example, you may notice the first parameter is “device address”. This would allow you to have multiple lsm6dso’s on the bus (as they’d have the different i2c addresses), but they both use the same, local lsm6dso object, which works because this object only contains function pointers to i2c read and write, which will be the same no matter which instance of the hardware you are refencing.

 

Note that this requires function signatures that match those of the function pointers defined in the driver. Make these as general as possible, and keep in mind wrappers may need to be made around the HAL in use to ensure their signature is the same