In this video, we will learn how to create a software interface to some underlying hardware. There are many important concepts to keep in mind when writing software. A microcontroller can have thousands of registers and many different types of memory architectures. Managing all these can be a burden on the software engineer without creating some type of software architecture. Your goal is to have an efficient yet portable interface built into your software to act as an abstraction to the physical hardware. This physical layer of firmware will be architecture and platform dependent. However, this layer will also need to be efficient and allow for easy interfacing with application software layers above. There are multiple methods to creating an abstraction layer for interfacing with hardware. This firmware layer should be efficient and bug free as possible. Additionally, you all want to create a simple interface that can be easily swapped out for different platforms or architecture dependencies depending on your build target. Compile time switches may be used to make this code more portable. These include, register definition files, macro functions, and specialized C-programming functions to allow for this portability. When designing your own software interface, you will likely use a mixture of these solutions. Higher level software should not be concerned about the low level intricate details of the hardware, instead, higher level software should interface with your software abstraction like an API. ARM provides a standard interface for its different architectures called the Cortex Microcontroller Software Interface Standards or CMSIS. This set of library files contain all of the information we are about to discuss and how it abstracts to the architecture. Let's go into detail on these different solutions. The register definition file provides us with the important platform dependency information including a software map of the peripheral and core registers, bit masks, and maybe even access methods. The peripheral memory access methods likely utilize a mixture of preprocessor macros, direct memory dereferencing, and structure overlays. This mechanism provides an extremely straightforward way to reading and writing data to peripherals. Unfortunately, by using this mechanism directly in code, we are now dependent on a single platform and architecture. Preprocessors can be used to hide specific platform details away from the programmer and have them substituted in a compile time. However, by using that method, we have to pre-define all operations we are interested in performing into separate macros. This becomes hard to maintain. This leads us into how we can use macro functions and specialized C-functions to create our hardware interface layer. These two function types are predominantly used in firmware abstraction layers especially when you need to write portable and readable code. We can define these functions to be parameterized to read some flexibility in their operation. Preprocessor macros act like a search and replace in software right before compilation. Anything you write gets directly substituted into your code. Similarly, functions in header files can be coded in a way that the build system can compile and link against different source files and function definitions. First, let's look at a macro function. We've seen macro functions before in the register definition files lecture. Macro functions get directly substituted into the code. This allows us to skip calling and returning from the function, but provide a mechanism to abstract away one or more operations to a function like interface. Just like normal functions, macro functions can allow us to pass data into the macro function to be operated on and you could even define them to return some data. Here are some peripheral memory access methods to which we would pass in an address and the macro would automatically dereference a location for us without creating an intermediate variables. These macros have a parameter X in the parentheses that gets substituted in to the corresponding directive. Here's an example of a peripheral memory access method where we can pass in an address of interest and set the peripheral to a particular value. In this case, we configured the port one pin zero to be an output enabled pin. Another example could include pass again multiple parameters into a function. This function allows to set any pin in the port direction register to output mode. This macro function would take in two parameters. First, a port direction register, and second, the pin you wish to set the output of. Here's another example where we use the ARM Bit Band feature. As mentioned previously, parts of SRAM and peripheral memory have bit-banded aliases to interact with. This provides an atomic way to read and write specific bits of memory or peripherals for much faster than traditional load and store mechanisms. This macro function requires an address and a bit number to write. The macro will automatically calculate the proper address offset into the bit-banded region and then dereference that location to read and write data. Again, this is for one bit. There are many problems associated with macro functions. They do not perform any type of type checking, so you can introduce some difficult bugs into your program if you're not careful. Macros can also get very complicated with multiple layers of redirections if you define all macros to call other macros. Additionally, if every time you use a preprocessor macro, you are duplicating code. If your macro function is long or is used regularly, you could be making a significant impact to your code size. Our last option to discuss is using specialized C-functions. Calling a normal function and software can be inefficient due to the execution overhead associated with the architecture's calling conventions. Data must be copied into the stack, a branch occurs, data is allocated in the function, and so on. Using a macro function can help with this overhead but it has a side effect. Instead, we will use inline and static functions to help with this. The inline keyword tells the compiler to skip calling this function and, instead, copy the contents into the calling location and avoid the overhead associated with calling and returning. However, this is just a suggestion. The compiler may or may not inline a function unless forced to with various optimizations or attributes. The inline keyword works really well for very small functions. But not all functions can be inlined. If the function is too big, if it's a recursive function, or it's a variadic function, the compiler will skip or can skip inlining it. By using an inline function regularly, you're creating a lot of code duplication in order to increase performance. The other added benefit of inline keyword, is it allows for a built in C-programming type checking. The static keyword allows us to write functions that are private. Previously, we use the static keyword to create a local variable that has a lifetime of the program. However, the static keyword also allows global variables or functions to be only visible to the current file it is defined in or included to. This is referred to as a translation unit. This means all linking is internal to the file and that process via the linker later in compilation. You may see static in inline combined in order to prevent the compiler from both generating the function's assembly and providing privatized interactions and integrate all that code into the calling function. Any function with internal linkage can be an inline function, but when combined with the static keyword, the compiler can assume all linkage is internal. These definitions we all write are inline static functions in header files so if their definitions can be included in a translation unit. Now we can combine all of these concepts into an interface library. Here I've created some inline functions to perform some very simple IO port reading and writing. By creating these as functions, we can have a linker substitute out which platform we are going to use by calling proper reads and writes. Additionally, since these operations are relatively simple and in need of high performance, we will make them inline so that when interfacing with hardware happens it's happening as fast as possible. There are two platforms supported here. One from the MSP432 microcontroller, and the second is the KL25z microcontroller. By using a platform compile time flag, I can switch into the appropriate low level interface code and make my hard work figuration as efficient as possible. When writing embedded software, you need to be concerned with your code's performance. Unlike traditional programming on personal computers or servers, poorly written software is compensated for with advanced hardware and processing. This is not the case for embedded firmware and we need our hardware interface layer to be as fast as possible, but we need to do this not at the expense of readability or maintainability. Thus, creating a hardware abstraction layer that you can write portable but yet efficient is important.