Prerequisites
Lights Code (MSXI)
We'll be using the Lights code from MSXI as a reference, since it is one of the more straightforward boards to understand.
git clone -b lights https://github.com/uw-midsun/msxi.git
The repository should look something like this
adaria@KANGA:~/msxi (lights) $ tree . ├── config │ ├── can │ │ ├── chaos.inc │ │ ├── config.h │ │ ├── devices.inc │ │ ├── license.md │ │ ├── motor_controller.inc │ │ ├── plutus.inc │ │ └── themis.inc │ ├── config.h │ └── lights_config.c ├── LICENSE ├── license.md ├── lnk_msp430f2471.cmd ├── lnk_msp430f247.cmd ├── README.md ├── src │ ├── lib │ │ ├── drivers │ │ │ ├── adc12.c │ │ │ ├── adc12_f247_shim.h │ │ │ ├── adc12.h │ │ │ ├── can.c │ │ │ ├── can.h │ │ │ ├── gpio_map.h │ │ │ ├── io_map.c │ │ │ ├── io_map.h │ │ │ ├── led.c │ │ │ ├── led.h │ │ │ ├── mcp2515.h │ │ │ ├── relay.c │ │ │ ├── relay.h │ │ │ ├── spi.c │ │ │ ├── spi.h │ │ │ ├── themis │ │ │ │ ├── lcd.c │ │ │ │ ├── lcd.h │ │ │ │ └── st7066.h │ │ │ ├── timer.c │ │ │ └── timer.h │ │ ├── license.md │ │ └── sm │ │ ├── event_queue.c │ │ ├── event_queue.h │ │ ├── state_machine.c │ │ ├── state_machine.h │ │ ├── transition.c │ │ └── transition.h │ ├── lights.c │ ├── lights.h │ └── main.c └── targetConfigs ├── MSP430F2471.ccxml ├── MSP430F247.ccxml └── readme.txt 8 directories, 47 files
Header Files
A header file is a file with the *.h extension, which contains C function declarations and macro definitions to be shared between several source files. In a nutshell, it provides an Application Program Interface (API) that does not contain or expose any implementation details.
Example: io_map.h
Here's a sample API from io_map.h.
#pragma once #include "gpio_map.h" #include <stdint.h> #include <stdbool.h> // Provides common functions and convenient mappings for interfacing with IO pins. struct IOMap { uint8_t port; uint8_t pins; }; typedef enum { IO_LOW, IO_HIGH } IOState; typedef enum { PIN_IN, PIN_OUT } IODirection; typedef enum { RESISTOR_NONE, RESISTOR_PULLUP, RESISTOR_PULLDOWN } IOResistor; typedef enum { EDGE_RISING, EDGE_FALLING } IOInterruptEdge; void io_set_dir(const struct IOMap *map, IODirection direction); // Enables the pin's secondary function. void io_set_peripheral_dir(const struct IOMap *map, IODirection direction); // Enables the pin's pullup/down resistor. void io_set_resistor_dir(const struct IOMap *map, IODirection direction, IOResistor resistor); void io_set_state(const struct IOMap *map, const IOState state); // Set the specified port state, masked by the port's pins. void io_set_port(const struct IOMap *map, const uint8_t state); void io_toggle(const struct IOMap *map); IOState io_get_state(const struct IOMap *map); void io_configure_interrupt(const struct IOMap *map, bool enabled, IOInterruptEdge edge); // Returns whether the specified pin has its interrupt flag set and clears it. bool io_process_interrupt(const struct IOMap *map); // Flips the interrupt edge void io_toggle_interrupt_edge(const struct IOMap *map);
Notice how we pass around an IOMap struct, instead of having all the parameters in a function as arguments.
Preprocessor Directives
In C, all code goes through what is called a preprocessor, before it touches the compiler. In this stage of compilation, lines starting with a # character are interpreted by the preprocessor as preprocessor directives. These commands form a simple language which provides functionality to inline files, define macros and to conditionally omit code. The preprocessor will do some initial processing, including joining continued lines (lines ending with a \) and stripping comments.
If you are familiar with the compilation process, the preprocessor sits just before the code touches the compiler.
+--------------+ +----------+ +-----------+ +--------+ -- file -->| Preprocessor |----->| Compiler |----->| Assembler |------>| Linker |--- executable --> +--------------+ +----------+ +-----------+ +--------+
#pragma once
The #pragma once preprocessor directive is a non-standard (but widely supported) directive, designed to cause the current source file to be included only once in a single compilation.
If you've worked with C before, it's similar to the include-guard pattern
#ifndef FILE_H #define FILE_H // your code here #endif
except it does not pollute the global namespace with a preprocessor symbol.
#include
A header file can be requested for use in a program, by including it with the C preprocessor directive #include (see gpio_map.h), or one included by the compiler in the standard library (see stdint.h).
Essentially, all #include does is paste the contents of the header file into the current file that is calling the directive.
typedefs
The typedef keyword allows one to "alias" new names for types. This allows the programmer to simplify the syntax of declaring complex data structures, and instead, write things like
void io_set_dir(const struct IOMap *map, IODirection direction);
instead of having to write out
void io_set_dir(const struct IOMap *map, enum IODirection direction);
Note that the IODirection enum is anonymous.
Functions
See the Coding Standards for function naming conventions. Read the document, and follow the standards. Please.
structs
A struct is a type that is typically used to encapsulate small groups of related variables under one name in a block in memory.
struct IOMap { uint8_t port; uint8_t pins; };
The struct allows all the variables to be accessed via a single pointer.
In addition, structs also allows the user to simplify an API, and abstract away certain details that only the implementation may need to know.
Note: The size of a struct is implementation-dependent, due to alignment, and may include padding.
unions
A union is a type that stores each data member at the same location in memory.
This is a 16-bit union, that essentially acts as an array of bits.
union { struct { bool bit00 : 1; bool bit01 : 1; bool bit02 : 1; bool bit03 : 1; bool bit04 : 1; bool bit05 : 1; bool bit06 : 1; bool bit07 : 1; bool bit08 : 1; bool bit09 : 1; bool bit10 : 1; bool bit11 : 1; bool bit12 : 1; bool bit13 : 1; bool bit14 : 1; bool bit15 : 1; }; uint8_t byte[2]; uint16_t word; } msg;
Since all these values are stored at the same memory location, we can do things like
// union definition ... uint16_t some_value = 0xF801; msg.word = some_value; // we can access individual bytes msg.byte[0]; // 0xF8 msg.byte[1]; // 0x01 // and individual bits msg.bit00; msg.bit13;
Note: Unions are endianness dependent (and may vary depending on architecture). So be careful when using them.
static
The static keyword in C languages has a few differences from Java.
- A static variable or function is "seen" only in the file it's declared in (kind of like private)
- A static variable within a function maintains its value between function calls
Here's a quick example to illustrate.
#include "some.h" // this global variable can only be seen in this file static uint32_t something; // this function can only be used in this file static void prv_some_foo(void) { // do some stuff.. } // some_bar can be called in other files void some_bar(void) { // value will maintain its value for each call // (ie. it won't reset to 0) static uint8_t value = 0; value++; }
Event Loop
The event loop pattern is basically a loop that contains all the program logic.
while (true) { lights_process_message(); }
Essentially, after the board performs its initialization sequence, it enters the event loop and just processes events forever, until it loses power.
The Importance of Modular Code
This API abstracts away a lot of the internal implementation details, so that we can swap out the driver implementations, while keeping the code exactly the same. The struct contains any configuration options needed so that
Consider the io_set_dir function.
void io_set_dir(const struct IOMap *map, IODirection direction);
We might see it being used in another file (like led.c)
void led_init(const struct IOMap *map) { led_set_state(led, LED_OFF); io_set_dir(led, PIN_OUT); }
We can change the underlying implementation of io_set_dir, and everything else will work exactly the same way as before. What's important is that the user don't care about the implementation details, and they shouldn't care!
Parameter Object Pattern
This also allows behaviour and arguments to easily be added to the API without breaking backwards compatibility.
Consider the following two examples
public void some_foo(int baz, int qux, int quux, int corge); struct Grault { int baz; int qux; int quux; int corge; }; public void some_bar(struct Grault *garply);
Now suppose you decide that you need to modify the API. Maybe it needs another parameter. Maybe a parameter needs to be taken away.
In the first case, you need to modify
- The header file
- The implementation file
- Anywhere else the function is called
In the second case
- Add an additional field to the struct
- Add the additional field if we need it when initializing the struct