(Source: Victoria Kluy/stock.adobe.com)
It is increasingly rare to build electronic systems that consist only of hardware. In both professional and hobbyist markets, embedded systems are a marriage of passive and active electronic components and software or firmware. A quick aside: The term firmware originates from the late 1960s and is a combination of "firm" (suggesting something solid but not as rigid as hardware) and "ware" (from software). Firmware is "firm" in the sense that it is stored in non-volatile memory (like ROM, EPROM, or flash memory) and is essential for a device’s operation. However, it can still be updated or modified.
So, what does it take to develop code for DIY electronics, specifically microcontroller-based projects? First, let’s examine the embedded programming languages geared toward hobbyists. The most common options are Arduino (C/C++), MicroPython, CircuitPython, and their associated runtime environments.
If you have experience in desktop programming in C/C++ or Python, you should pick the embedded counterpart to reduce the learning curve. For those without programming experience, reviewing sample projects in each language can help determine which feels most natural. In general, C/C++ offers better performance, while Python-based options prioritize ease of use and readability.
Regardless of the hardware platform and programming language selected, the high-level process of writing code is essentially the same. First, we must get our logic out of our brains and into the computer. This can be achieved in a simple text editor like Notepad or a more advanced editor like Visual Studio Code. Simply write down the steps your code needs to walk through in a numbered list. We aren’t worried about coding yet; we are just documenting how we envision our code will flow. What inputs are taken in, how are we processing the data, and what outputs do we generate and when? This is called pseudocode and is written in your native language.
After we have documented the high-level requirements for our code, it’s time to write the firmware. The code we produce in human-readable format is called source code. The file extension of a source code file typically indicates the programming language used. For example, .ino and .c are indicative of Arduino and C programs, whereas .py and .mpy are the file extensions of Python-based programs. You can write source code in a simple text editor. However, novices might consider an integrated development environment (IDE).
.ino
.c
.py
.mpy
An IDE is a software application that provides a comprehensive set of tools for software development in one package. In addition to a code editor, it also includes useful tools such as a compiler/interpreter, debugger, and version control. While it is possible to manually set up the toolchain that allows one to compile source code into a file understood by a microcontroller (i.e., machine code) and then upload it to the device using specialized hardware, an IDE handles all this complexity in a single application from the user’s perspective.
Conversely, MicroPython and CircuitPython are interpreted languages. This means the source code remains in a human-readable format and is executed directly by the Python interpreter on the microcontroller. This removes the need for a compilation step but requires the microcontroller to have sufficient resources to run the interpreter in addition to the source. The CircuitPython firmware must first be flashed to the hardware. After a reboot, the development board will appear as a USB mass storage device and the user’s source code can be copied to the apparent flash drive. After another reboot, the microcontroller will begin executing the code.
Whether you use compiled code, such as Arduino, or interpreted code, like CircuitPython, it is important to remember to wire your external hardware before uploading firmware. Ensure the USB cable you use to connect your microcontroller development kit to your development board carries data. If you connect your board and onboard LEDs light up, but your IDE doesn’t see the board, you may have incorrect drivers or are using a power-only USB cable that lacks data lines. Also, consider the following when getting started with coding for your DIY projects:
Blinking an LED, reading a sensor, or controlling a motor are great beginner exercises. They help you learn how software influences hardware and vice versa. It is not always intuitive, so start small and build confidence.
Code will rarely, if ever, work as intended on the first try. Developing debugging skills is crucial. Professional engineers may rely more on hardware debugging tools, such as hardware debuggers, oscilloscopes, and logic analyzers, to inspect signals, particularly when building custom circuit boards or interfacing with many external components. However, a simple way to debug for beginners is to use the serial terminal to pass messages from the microcontroller to the host machine. The Arduino ecosystem uses the serial.print() function for this purpose. Similarly, in MicroPython, the print() function outputs messages via the read-eval-print loop (REPL) interface. That said, even low-cost development kits incorporate built-in debugging hardware so that users can insert breakpoints and peek into registers via the IDE.
serial.print()
print()
If you are considering adding functionality or external components (e.g., sensors, actuators) to your project, chances are that code already exists to provide the interfaces. Arduino libraries and MicroPython modules are great ways to get a project working fast and reliably. However, overreliance on libraries and modules may deprive you of a great learning opportunity to understand the intersection of hardware and software. Once you get a project up and running, consider forking a library that interfaces a sensor and the microcontroller and see if you can add functionality that is not native to the library. Reading the library or module code is beneficial as you will have to interact more directly with the hardware (e.g., ports and registers).
Hands-on learning and iterative improvements will enhance your understanding of microcontroller programming. Beyond the basic general-purpose input/output (GPIO), learn how to interact with communication protocols (e.g., I²C, SPI, UART), power management, task scheduling, and other advanced features your hardware supports. Pick a new feature of your microcontroller and learn how to write the code that controls that functionality.
Getting started with embedded programming has never been more accessible. Platforms like Arduino, MicroPython, and CircuitPython provide powerful options regardless of your technical background. Arduino, built on C/C++, delivers high performance and precise hardware control, while Python-based environments prioritize ease of use and rapid development. Writing firmware is a structured process: Start with pseudocode, move to an IDE to program the source code, and then test with real hardware interactions and serial output. Beginners should start with fundamental projects like blinking an LED before diving into debugging techniques, serial communication, and built-in diagnostic tools. While third-party libraries accelerate development, writing custom code deepens understanding and enhances flexibility. Exploring core microcontroller functions like I²C, SPI, and UART opens the door to more advanced designs, helping engineers push the limits of what’s possible.
Michael Parks, P.E. is the co-founder of Green Shoe Garage, a custom electronics design studio and embedded security research firm located in Western Maryland. He produces the Gears of Resistance Podcast to help raise public awareness of technical and scientific matters. Michael is also a licensed Professional Engineer in the state of Maryland and holds a Master’s degree in systems engineering from Johns Hopkins University.