Learning A New Microcontroller
- The Peripherals
- System Complexity
- Support Software
- Do It Like Phil
- The Programs
- WET And DRY Code
- Hardware Setup
- Reference Learning Platform
- Keep An Engineering Notebook
This is a situation you'll find yourself in repeatedly throughout your career as an embedded systems developer: you need to learn a new (to you) microcontroller (MCU) and its associated development and runtime environments in order to be able to write the software for it.
If you're a beginner just learning embedded systems, that's a big initial hurdle. Even if you're experienced, you may have only worked with certain functionality in MCU's and never explored the rest.
Regardless of your experience level, in this post I'll outline a process for building up that knowledge in a structured, repeatable manner. The first time will be challenging. By the third or fourth one, it will be a familiar skill, and you'll be able to nail a new MCU in just a few days.
There's a basic set of peripherals that are common to virtually every MCU, providing progressively more capability. By taking a stepwise process to learning to work with these at increasing levels of complexity, you can build your competency with the MCU. That provides you with a basic set of software development capabilities to go along with the set of peripherals.
Armed with a development board and a set of development tools, you can write a set of simple programs that exercise the various capabilities. Then you can add various levels of complexity to explore related modes and usages. That develops your competency and facility with the board and the MCU, preparing you to work on your actual target embedded system.
What makes this process repeatable is that you can develop a generic set of programs, with the hardware details abstracted and segregated out. Then for a given MCU and board, what's left is to write the set of hardware-specific functions. That's a design strategy that allows you to focus on the hardware details, and compare and contrast them with other MCU's you're familiar with.
What makes using peripherals complex is their setup and control. This is generally done through memory-mapped registers. Depending on processor architecture, there may be multiple levels of dependent registers that need to be configured. These are the details you need to learn and become familiar with.
What can be overwhelming in all this is the enormous volume of information provided by the MCU vendor. Datasheet PDF's may be thousands of pages long, and library and middleware software support packages may have hundreds of files.
The key to getting a grip on it is to focus on the basic set of peripherals. Start simple, and build from there. Then you can apply the experience you've gained to even more complex cases.
This is not a new methodology. You can find various examples of it in videos and tutorials; I've linked to some below. I'm simply presenting it as a structured approach that you can use as a habit.
You can also use an analogous approach to learn how to work with a new language on a familiar MCU, for instance, using Rust where previously you've used C.
Background For The Beginner
What makes embedded systems useful is not just their basic compute capability, but their interaction with the external world. Interactions take place via peripherals, devices that provide input and output capabilities.
A microcontroller unit (MCU) is a combination of:
- A central processing unit (CPU) that provides the basic compute capability.
- The necessary components to make the CPU a fully functional standalone unit.
- A number of built-in peripherals and associated functional blocks.
At its simplest, the hardware design for an embedded system is then a matter of placing the MCU on a board that provides power and the devices that the MCU interacts with in order to fulfill its embedded application, along with any required support and interface circuitry.
In order to help developers learn and evaluate their products, MCU vendors typically sell development boards (also known as developer kits, dev kits, edu kits, eval boards, demonstration kits, discovery boards, etc.). A development board typically has an MCU, power, and a number of external devices on it such as LED's and switches, along with breakout headers that allow you to connect other devices. They may also include sensors and displays.
MCU vendors typically provide detailed datasheets and a number of software development tools, libraries, middleware, and example programs. Some of these components may be absolutely required to do real work, some are purely for reference; there are various arguments for and against using them in a real project. Regardless, they're all useful learning tools.
The peripherals, in roughly increasing order of complexity:
- GPIO (General-Purpose Input/Output)
- Serial interfaces:
- UART (Universal Asynchronous Receiver/Transmitter)
- SPI (Serial Peripheral Interface)
- I2C (Inter-Integrated Circuit)
- PWM (Pulse-Width Modulation) output
- ADC (Analog-to-Digital Converter) input
The serial interfaces can be used to connect to external devices, such as sensors, flash memory, and SD cards. These are some of the other devices that may be present on development boards, or you can purchase separate breakout modules that contain them and hook them up to the board breakout pins.
Some MCU's have additional peripherals and specialized components. The way to learn about them is to figure out where they sit in the complexity order above, find similarities to other peripherals, and apply a similar learning process to them. Your experience with the basic peripherals will prepare you for working with them.
System complexity comes in three dimensions. First, peripherals can generally be used in two modes:
- Polled/loop-driven: a software loop controls the peripheral, polling it for status.
- Interrupt-driven: the peripheral generates an interrupt on specified conditions.
Second, peripherals can generally access data in two modes:
- Programmatically: software moves data in and out of peripheral registers.
- DMA (Direct Memory Access): the peripheral moves data directly to and from memory without involving software.
The simplest usage is the first mode in both dimensions above. This is software-driven, using the simplest peripheral setup. It places the largest processing burden on the CPU.
The second mode in both dimensions requires more complex peripheral setup. This is hardware-driven, offloading the processing burden to the peripheral hardware and associated elements (interrupt and DMA controllers). That frees up the CPU for other work; it also increases system complexity by providing parallelism, where multiple hardware elements are operating concurrently.
For the third dimension of complexity, the software can operate in two runtime environments:
- Bare-metal: a single "superloop" runs continuously, cycling through the processing to be done.
- RTOS (Real-Time Operating System): multiple schedulable tasks or threads share the CPU, cooperating with each other to get the work done.
Those three dimensions form a conceptual cube of complexity space, divided into 8 distinct octants. That provides great flexibility in how you implement a system, with various tradeoffs. To take advantage of that flexibility, you need to learn how to operate a given MCU's peripherals in all those combinations.
If you're new to RTOS's and designing for concurrency, see my post Review: Hands-On RTOS with Microcontrollers for an excellent book by Brian Amos, as well as the book Real-Time Design Patterns: Robust Scalable Architecture for Real-Time Systems by Bruce Powel Douglass.
There are a number of vendor and open-source software tools to assist in this process. First, look at the software package that the MCU vendor supplies. This may be in the form of an SDK (Software Development Kit) that includes:
- Runtime libraries for accessing peripherals at various levels of abstraction and in various modes, from bare memory-mapped registers through full-function components.
- Middleware libraries that provide additional services on top of the basic libraries.
- Example programs illustrating the setup and usage of the peripherals and libraries.
- Some form of free development environment and toolchain (compiler, linker, flash programmer, debugger, build system).
- Support files for various third-party commercial development environments such as IAR, Keil, Segger, etc.
- RTOS bindings for various open-source and commercial RTOS's.
- BSP's (Board Support Packages) for various board and runtime environment configurations.
Second, look at the RTOS's that support the MCU (known as a port of the system to the MCU). This is just a sampling of some current common options:
Some commercial products offer free evaluation or non-commercial-use versions, ideal for this kind of learning.
Exercising an MCU with any of the above RTOS's will provide a good foundation. Just be aware that different operating systems offer different models and isolation for managing hardware. Linux tends to be the most different from the others. Some use third-party middleware for drivers and system components.
Opinions on the vendor-provided software and RTOS's vary widely. The most common complaints:
- Vendor-supplied code or an RTOS port is of poor quality, consumes excessive resources, or exhibits poor performance.
- An RTOS port only provides limited support for some peripherals, limiting their usage.
- Examples show only the most trivial usage, or don't show a particular usage you're interested in.
Regardless of their quality or thoroughness, the vendor- and RTOS-provided examples are the best starting point for learning about and exercising the MCU. Start there, especially reviewing how they setup and initialize registers and components. Sometimes register setup can be very finicky, with ordering dependencies and obscure or poorly-documented settings, and the examples can be very helpful for that.
Do It Like Phil
Philip Salmony's YouTube channel Phil's Lab is an excellent example of this general process. He also clearly knows what he's doing, so his videos are great tutorials. But notice how he works with a variety of peripherals, with DMA, with and without an RTOS, while using datasheets and vendor code.
He has a number of videos, but these are a great starter set:
- STM32 Programming Tutorial for Custom Hardware | SWD, PWM, USB, SPI - Phil's Lab #13
- STM32 DMA and FreeRTOS Tutorial - Phil's Lab #14
- How To Write A Driver (STM32, I2C, Datasheet) - Phil's Lab #30
These are the suggested programs to develop, getting progressively more complex. Typically, vendor examples will provide some of these, so use them as starting points for your own versions.
Once you have basic features implemented for the various peripherals, mix and match them. That ensures you know how to combine them and use them concurrently in a real system.
The goal is to implement code for each peripheral, varying each of the three dimensions of complexity listed above:
- Polled vs. interrupt-driven.
- Programmatic vs. DMA data access.
- Bare-metal vs. RTOS (possibly with multiple different RTOS's).
That's potentially at least 8 different programs per peripheral. By doing it in a stepwise fashion from the simplest program, you can build progressive knowledge that you apply to the other programs.
I've combined several features in the lists below to incrementally build up functionality. An easy way to vary the third dimension is to implement the program as bare-metal, then mostly the same code as a task in an RTOS. Some usages lend themselves naturally to multiple RTOS tasks, such as an input task and an output task, which then requires some concurrency mechanism to pass data between tasks in a thread-safe manner.
How far you go with all this depends on your learning goals, but once you've done a good cross-section of these, you'll have a good understanding of how to work with the MCU.
GPIO And Timers
- Loop-driven GPIO output toggle: blink an LED. This is the classic blinky program. It serves as a basic sign of life for the board and your ability to work on it.
- Polled GPIO input: blink an LED in response to button pushes.
- Interrupt-driven GPIO input: blink an LED in response to button interrupts.
- Timer-driven GPIO output toggle: blink an LED at a fixed frequency in response to timer interrupts.
- Timer-driven GPIO output toggle with button: blink an LED at a fixed frequency in response to timer interrupts; change the frequency in response to button interrupts.
UART And Timers
- Loop-driven UART output: print a fixed string to a terminal emulator. This is almost as classic as blinky, and is the general-purpose logging method.
- Polled UART input: read a character in from the terminal emulator and print it back out (echo it). This is the first step in implementing a CLI (Command Line Interface).
- Interrupt-driven UART input: echo based on input interrupt.
- Interrupt-driven UART CLI: accumulate input characters into a string; on receipt of a CR (Carriage Return), interpret the string as a command in non-interrupt context (for instance, a "version" command that prints a version string). This is the first command in a CLI, and the first major separation of interrupt and non-interrupt code sharing data. This is also be the first step in creating an interface that communicates with another device over serial port, such as a GPS module.
- DMA: send and receive chunks of data via DMA rather than programmatic character-at-a-time register access.
- Timer-driven UART output: print a periodic message in response to timer interrupts.
- Timer-driven UART output with input: print a periodic message in response to timer interrupts; change the frequency in response to input commands in the CLI.
This requires connection to an external device that communicates over the serial bus, such as a temperature/humidity sensor or an RTC (Real-Time Clock) module.
Some of the things below may depend on the particular vendor interface and what level of capabilities it provides.
- Loop-driven bus output and input: send a control message to the device and receive the response.
- Interrupt-driven bus output and input.
- DMA data access instead of programmatic.
- Timer-driven bus exchange.
PWM output is used to control a number of different devices, such as LED brightness, motor speed, or servo motor position. The programs are mostly about how to change the PWM signal in response to various inputs to control variable output.
- Loop-driven PWM that controls the PWM signal, running it up and down between low and high values.
- Interrupt-driven control that changes the PWM signal in response to button pushes or CLI commands.
- Timer-driven control that steps the PWM up and down at a specific period. An LED "breathing" fade in and out is an example of this.
A number of sensors provide an analog output signal voltage that needs to be converted to a digital value, such as moisture and audio sensors. ADC conversion (sampling the signal and converting it to a digital value) is not instantaneous; it generally requires a start step and wait for completion. It may be possible to setup continuous conversion that collects a stream of input samples, such as audio input, as in this tutorial: Getting Started with STM32 - Working with ADC and DMA by Shawn Hymel (who is another excellent resource for this learning process).
- Loop-driven ADC sampling. You can do various things with the digital values, such as print them out via UART, or use them to vary a PWM output.
- Interrupt-driven completion.
- DMA to transfer multiple samples.
Go Crazy And Combine Them All
This is where you can play with all the things above and roll them into a larger, more complex program that does everything. This is essentially a full-blown embedded system at this point, and is useful for sorting out the issues that might crop up interoperating everything together.
WET And DRY Code
As you write the programs, keep an eye out for where you can apply the DRY principle: Don't Repeat Yourself. Vendor examples are meant to be standalone, so duplicate a lot of code. Meanwhile, as you work with different MCU's, you'll find yourself writing the same programs over and over, just with different MCU-specific details.
DRY means you find the commonality among the programs and separate it out so it can be reused in multiple places. The goal is to factor out and abstract the generic high-level parts from the specific low-level parts (often referred to as platform-independent (PI) and platform-specific (PS) code, or some variation of that terminology). The platform-independent code is common reusable code that you can use in multiple programs, across multiple MCU's.
You can also do that on multiple layered levels, creating intermediate middleware components between the main program level and the peripheral access level. Each of those layers provides a level of abstraction to help manage the complexity.
Ultimately, you'd like to end up with a library of platform-independent program skeletons and middleware components that can be tailored with platform-specific code. Then 90% of the code is ready to go when you want to work on a new MCU, and you just need to work on the 10% of PS code.
But how do you know what parts to abstract out? Often, control code is generic, platform-independent, and hardware access is platform-specific. For example, you might have a PI loop that starts an operation on a peripheral and polls it for completion status, using PS functions that access the appropriate MCU registers.
But it's hard to know up front, because it's all hypothetical. While some things may be fairly obvious, some aren't, and it can be hard to know where the draw the dividing lines. You may agonize over how to do it.
That's where the WET principle comes in: Write Everything Twice. Don't agonize over it. The first time you write something, just write it without worrying what might be PI and what might be PS.
Then, the second time, when you find yourself about to duplicate some code you've already written (which would be repeating yourself), don't. Instead, look at the two usages and see where they are common and where they are different.
Go back to the original code and write it a second time, but now split into PI and PS portions, and add another PS portion for the new MCU. Now you have a clearer view with two concrete examples, so you can see where to draw the lines. Now you've turned it into DRY code, with different PS parts for the two MCU's.
What you end up with is a HAL (Hardware Abstraction Layer) that allows you to think of the PS capabilities in an abstract manner, with multiple implementations for different MCU's.
You might need to write it a third time. Maybe the DRY code doesn't quite work for a third MCU, or you see additional opportunities to DRY it. With three different usages, you can step back and see the patterns of commonality in the code, much more clearly distinguishable from the platform-specific parts.
You might even need a fourth iteration. But by the time you've refined it this many times, you really have a good idea of what it needs to look like. You'll end up with really nice tight code.
What about when you need to work on a real project? You can take that nice tight code you've refined over multiple iterations and put it in the project. Need to write a driver for a peripheral that uses DMA? Bam, just a day's work to implement the PS components that tailor your PI components. That's the real benefit of abstraction and splitting things up this way.
Similar to a HAL, you can apply the WET/DRY principles to create an OSAL (Operating System Abstraction Layer) that allows you to think of RTOS capabilities in an abstract manner, with multiple implementations for different RTOS's. HAL's and OSAL's are types of API's (Application Programming Interfaces) that define abstract ways of working with things.
DRY and API are two of Six Software Design Tools to help you write good code. Jacob Beningo's book Reusable Firmware Development: A Practical Approach to APIs, HALs and Drivers discusses how to break up code into separate reusable elements.
I recommend the following:
- A host development system that can run the vendor software tools. A low- to medium-range laptop is generally sufficient, but vendor tools may require specific operating systems.
- A development board with the MCU you want to learn, along with any required external power supply and cables. Many dev boards require only a USB cable for power and communications with the host.
- If required, an external debug probe and cables. Many dev boards have this built into the USB interface.
- A breadboard and jumper wires.
- An assortment of external devices on breakout boards that connect via the different serial interfaces.
- An inexpensive logic analyzer.
I recommend using the vendor tools because those will have the most support from the vendors, and some offer advanced capabilities such as hardware configuration utilities. Most vendors provide free or trial versions of their tools. Don't forget to look for their examples first thing!
There may be other third-party tools that are easier to work with, for instance PlatformIO. This is a double-edged sword. They may allow you to do things faster, but they may also gloss over details that you might later need to learn.
I recommend using a logic analyzer because it can be an indispensable tool for seeing what's going on in the hardware connections. It's a bit of an investment in money and learning, but will pay itself back many times over. It allows you to monitor, capture, and analyze the signals to and from the external devices, providing an independent view of them. When running any of your programs, have it setup to monitor and analyze the specific peripheral I/O. Compare the analyzer traces with the datasheets for the MCU and external devices; that's a common debugging method.
See the "Equipment, Books, and Supplies" section of my Video-Based STEM Embedded Systems Curriculum, Part 1 for specific hardware suggestions, including pre-packaged kits that provide an assortment of parts and external devices.
Reference Learning Platform
This is where having a reference learning platform is invaluable. The idea is to have a simple, reliable, known-good setup to compare against.
Arduino is ideal for this, because it's inexpensive, very easy to use, and provides all the peripherals listed above. You can buy pre-packaged kits that combine an Arduino-compatible board with breadboard, parts, and breakout modules.
Arduino glosses over many details, taking care of many things for you. We can take advantage of that to get to rapid success with a given peripheral. It's a stepping stone to more complex systems.
Use the Arduino and its simple development environment to prototype a given hardware setup and code, connecting things on the breadboard. That includes logic analyzer setup, capturing reference signal traces for comparison with other MCU's.
That shows you how things are supposed to work externally. It also validates your hardware setup, to show that it's working.
Once you have a setup working and understand the concepts involved, replace the Arduino with your development board; you might be able to have both connected to the breadboard, only powering up one or the other at a time. Then you can work on reproducing the behavior and logic analyzer traces with your dev board.
Keep An Engineering Notebook
Keep track of everything you do in an electronic version of an engineering notebook. Some useful platforms for this are GitHub or GitLab, where you can write up information in markdown and keep the documentation along with source code in a repo. Another is Google Docs, where you can dump all your information into a document.
You can also keep logs, screen captures, photos, and diagrams. Cellphone photos of hand-drawn paper and whiteboard diagrams, along with photos of hardware setups, are a great way to record information quickly; scale them down to reasonable size for storage.
The reason for doing this is that there are a lot of details, and it's easy to forget things. Some of them are a lot of work to tease out of documents and code. Once you've invested the time and effort to find them, you don't want to lose them.
Treat your engineering notebook as a tutorial to your future self, so that 6 months from now when you want to use the information, you can pick right back up. You can also use it to teach other team members. It provides the raw material for creating more streamlined documentation.
The act of writing and recording things also helps organize your thoughts and lock the information in your mind. Having good records is a lot better than struggling to remember what you did.
Very well structured synopsis of the topic I had to learn by "hard knocks" across 40+ years.
A "must read" for any nooby to get a leg up. Good stuff <<<)))
To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.
Please login (on the right) if you already have an account on this platform.
Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: