C Guide for Firmware
Introduction
This document briefly goes through some important concepts in C that are commonly used in our code. This does not cover everything in C programming and is written with the assumption that the reader has a basic knowledge of programming (eg variables, loops, conditional statements, OOP).
For a more comprehensive tutorial visit this link.
Table of Contents
#define
What is #define?
In embedded systems, we use #define
to define macros. A macro is a block of code which is given a name. An example is
#define NUM_GPIO_PINS 15
Here the macro, 15
, is given the name NUM_GPIO_PINS
. This is a preprocessor directive, meaning that before the code is compiled, the preprocessor will replace NUM_GPIO_PINS
to 15
every time it appears. Even though we could simply just hardcode 15 into our code, when we use #define
we make our code much more readable.
#define vs Variables
The alternative to using #define
is to use variables. We could alternatively do the same thing with what we did above and use the keyword const
to make sure the variable value does not change so it acts similar to #define
.
const int NUM_GPIO_PINS = 15;
However, we normally using #define since we have a limited amount of memory in MCUs and do not want to waste space.
typedef struct
If you have programmed with OOP, you should be familiar with classes and objects. If not that's okay too. In C, we have structs which act similarly. Basically, a struct is a data type used to group a list of variables under a certain name. For example, in the struct below we have a dog with variables name and age.
struct Dog {
string name;
int age;
};
To access create a variable of type Dog and access it, we can do
struct Dog d = {
.name = "Winchester",
.age = 42
};
In our code, you’ll notice that we put the keyword typedef
in front of the struct
. This is so every time we create a variable of type Dog, we don’t have to put struct
in front of it and we can access it as shown below.
Dog d = {
.name = "Winchester",
.age = 42
};
typedef enum
An enum (enumeration) in C allows us to easily assign variables with consecutive values. See the example below. Again, we use the typedef so we don’t need to write enum in front of the type name when trying to create a variable of type GpioPins.
This is a clean way of assigning variables in consecutive numbers and grouping them under one type. Now when we write GPIO_PIN_2
in our code, it will evaluate to the value 2
. This works better than manually defining each number as shown below.
See? This is okay, but it is much nicer to use enums in this case.
Decimal, Binary, and Hex
Decimal, binary, and hexadecimal are simply different ways of writing numbers. We are normally used to using decimal in our daily lives. Binary, on the other hand, is used since it more closely resembles how the computer calculates numbers. Hexadecimal is used as a more friendly way of representing binary values.
Below is a table of numbers 0 - 15 represent in decimal, binary, and hex.
Decimal
Decimal is what we are used to seeing which uses 10 as a base. I think this should be trivial since we always see this. However, to calculate this value we do the following.
Example: What is 156 in decimal?
Obviously, the answer is 156. But how can break this down to understand what “base 10” means?
Example:
6 is in the 0th column, so we multiply it by 10^0
5 is in the 1st column, so we multiply it by 10^1
1 is in the 2nd column, so we multiply it by 10^2
We then add up the sums of each of these products so we get
sum = (6 x 10^0) + (5 x 10^1) + (1 x 10^2)
= 6 + 50 + 100
= 156
Notice how we take each value x, from the nth column, and multiply it by the base to the nth exponent. This will help us with calculations for binary values as well.
Binary
In binary, numbers are represented by 1s and 0s. Binary uses base 2, let us break this down.
Example: Convert 1011 from binary to decimal
1 is in the 0th column, so we multiply it by 2^0
1 is in the 1st column, so we multiply it by 2^1
0 is in the 2nd column, so we multiply it by 2^2
1 is in the 3rd column, so we multiply it by 2^3
Then we add up all these products.
sum = (1 x 2^0) + (1 x 2^1) + (0 x 2^2) + (1 x 2^3)
= 1 + 2 + 0 + 8
= 11
Hexadecimal
In hex, numbers are represented from 0-9 and A-F. Hex uses base 16. You can see the values of A-F in the chart above. We can again, break this down and convert hexadecimal to decimal. For hex, you will also notice they generally have a prefix “0x…” before it. This just indicates that it is in hex.
Example: Convert 0x4C from hex to decimal.
C (which has a value of 12) is in the 0th column, so we multiply it by 16^0
4 is in the 1st column, so we multiply it by 16^1
Then we can find the sum.
sum = (12 x 16^0) + (4 x 16^1)
= 12 + 64
= 76
Bitwise Operations
Below is a table to summarize bitwise operators. Please view this link to see examples of how bitwise operators work and how we can manipulate numbers.
https://www.programiz.com/c-programming/bitwise-operators
Writing to Registers
When writing drivers for certain MCUs, one important skill is being able to find the correct register from a datasheet and write to it.
Suppose we want to activate the 0th bit in an 8-bit register. We can do so as shown below.
The method above helps make sure we don’t accidentally clear the other bits by accident. We could have also done this in one line when we first initialized the variable. Let’s try and activate the 1st and 0th bit. Below is recommended for when we know exactly which bits we want to be on or off.
Let’s say we realize we want to deactivate the 0th bit now.
We can check if the 0th bit is activated as well