The Embedded Software Engineer’s Primer: Introduction

Marcus Quettan
9 min readAug 7, 2020

--

I want to write a series of articles for the primary purpose of simply documenting all of the topics which I have, at multiple points in my career, just wanted to explain to someone. In a professional setting, there isn’t always an abundance of time with which I could dive deep into a topic with an individual. So, these articles are for future me. I am writing this all to give myself a place to point someone to when they’ve asked me a question on a topic I’ve already written about extensively. And, by extension, I hope that you as the reader will find value as well! If you have any suggestions on future topics I should discuss, let me know by commenting below!

Please note, I want these articles and the subsequent discussions to be about enduring truths with regard to the experience of being a Software Engineer. I don’t want to dive all too deeply into any particular tool, because tools change. However, I want to relentlessly drive home the core point. And that point is that understanding the problem you’re attempting to solve is FAR more important than finding the solution. This may not make sense now, but I am a firm believer that Software is merely a painfully precise description of a problem. That description, when executed, produces a solution. If you cannot appropriately describe the problem in English (Or whatever your native language is), I would argue that you should not begin to describe your problem in C++ (Or whatever your chosen programming language is). English is easier. Trust me. I’ve seen plenty of engineers fall into the trap of thinking that they just haven’t found the right solution yet. When in reality, their real issue is that they don’t fully understand the problem they’re trying to solve yet. My advice? Try to use Google and read documents more to research understanding instead of simply searching for a prepackaged solution!

This collection of articles aims to improve your understanding. Or perhaps, at the very least, shine a light on topic areas that may be blind spots to you. I do not intend to make you an expert on any particular topic. However, I want you to be aware of the fundamentals. Each topic is incredibly complex and cannot be entirely described in a single article. Hopefully, the awareness I can provide you inspires you to research a topic more deeply than I can convey here.

So, let us begin with the first topic!

What does it mean to write Software?

The first topic I want to discuss is “What does it mean to write Software?” A simple, yet complex question to answer. When you type up your first “Hello, World!” program. What happens, exactly?! Well, in short, what you are doing is writing a list of instructions for your processor to execute. For the simplest program to write in any language, the only instruction for your processor to execute is to print out the phrase “Hello, World!”

There’s a history lesson to be had here on why the simplest program in any language is widely represented as a program which prints out the phrase “Hello, World!” I believe it must have something to do with our community’s incessant desire to personify computer programs. Thus, the first thing any program should do is the same thing any person should do, right?! Say hi! At any rate, I digress, sorry for the tangent. Moving forward, I’ll be using these quote sections for even more tangents. Feel free to ignore them.

Let’s, for example, examine the following C++ implementation of a “Hello, World!” program.

#include <iostream>int main(int argc, char** argv) {
std::cout << "Hello, World!" << std::endl;
return 0;
}

Ok, just now… I realized that explaining this in C++ is a bad idea. Ignore that! Let’s go with Python instead!

print("Hello, World!")

Much better! (We’ll tackle what the differences are between programming languages later…) So, this simple one-line program instructs the processor to print the phrase “Hello, World!”. But that’s a bit too surface-level for this conversation. Let’s dive a bit deeper into what’s actually happening here. However, to do so I need to explain what a processor is.

What is a processor?

A processor can be thought of as nothing more than a gigantic collection of transistors that have been cleverly arranged. A transistor can be thought of as nothing more than a simple switch. It can be on, or off. When a transistor is in the “On” state, electricity is allowed to flow through it. When it is in the “Off” state, electricity is not allowed to flow through it. You can think of it much like the light switch in your house! This is why computers always use binary 1's and 0's all over the place. The building blocks of a computer are only capable of being in one of two states. On, or off. 1 or 0. What’s special about a transistor when compared to your house’s light switch is that a transistor can switch between states using an electrical input, as opposed to your finger. If the input is high, the transistor is on and electricity is allowed to flow through. If the input is low, the transistor is off, and electricity cannot flow through! Easy enough. It’s a lot more complicated than that but that’s practically all you’ll ever really need to know.

This electrical input signal required to change a transistor’s state is usually relatively low when compared to the amount of electricity which will flow through the transistor once it’s turned on. This is why transistors are often also used as amplifiers, but that’s a whole different discussion for another time.

Now, we can arrange these transistors to perform a bit more complicated behavior. Let’s examine the simple circuit diagram below. Please excuse my terrible drawing…

Simple AND Gate Circuit Diagram

Imagine switches A and B are connected in series with each other on a circuit where there is a constant source of electricity. Imagine the source of electricity like it is a battery that is always capable of producing electricity as long as the circuit isn’t broken. You can see that the circuit is only unbroken when BOTH A and B are switched “ON”, right? If there’s a break in the circuit, then electricity cannot flow from the source to the output. Thus, if input A is on but input B is off. The overall output is 0 (no electricity detected at the output) because the circuit is not completely connected! Only when BOTH A and B are on will the output be 1 (electricity detected at the output!). So, what we’ve created here is an AND logic gate. An electrical circuit that can inform whether or not BOTH signals A and B are on. I hope that makes sense.

The point I’m trying to get across is that if you arrange transistors in a particular way, then you can express a more complex idea. These higher-level ideas we create from transistors are called logic gates. An AND gate is a logic gate. There are OR gates, XOR gates, NAND gates, and a few more! These logic gates can then be arranged to represent even more complex ideas. You can arrange these logic gates to create circuits that can add, subtract, divide, and multiply numbers! Then you can add some more electrical components (Other than only transistors) and do even more complex things like store memory or keep track of time.

A processor is a collection of millions upon millions of these types of electric circuits (For example, a Core i7–8700K has ~3 billion transistors in it!). All squeezed into a small ~2cm by ~2cm square frame. Much of the advancement in processors over the years has been due to our ability to create increasingly small transistors. These days, transistors are literally microscopically small!

Now that you’ve got a processor that’s capable of performing complex behavior, how do you tell the processor what to do and when?! That’s where the Software Engineers come in!

When a processor is created by a manufacturer, say Intel or AMD, they create an associated language that their processor can read directly to perform instructions. This language which is directly understood by a processor is called assembly! Here’s what a “Hello, World!” program looks like in assembly:

section	.text
global _start ;must be declared for linker (ld)

_start: ;tells linker entry point
mov edx,len ;message length
mov ecx,msg ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel

mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel

section .data
msg db 'Hello, World!', 0xa ;string to be printed
len equ $ - msg ;length of the string

It looks incredibly complicated, I know. Each one of these lines of assembly code performs an incredibly simple task. That is why there are so many lines! For example, “mov edx, len” moves the value stored in register named “len” into the register named “edx”. A similar line “add esp, 8” would add the value 8 to whatever value is currently stored in the register named “esp”.

Remember earlier when we discussed that a processor is a collection of millions of electronic circuits? Well, each line of assembly code instructs the processor to fire off the corresponding electric circuit with the provided input values and store the result in the provided location. All software running on your computer is eventually translated into assembly language. The clock speed of your processor informs how fast that processor can read and execute lines of assembly. A modern 3.6 GHz processor is capable of performing 3.6 BILLION lines of assembly per second! (Some lines of assembly require more than 1 clock cycle to complete but you get the idea. Processors are amazingly fast!)

Assembly language is optimized to be as easy for a processor read as possible. Usually, it’s not even stored as human-readable text. It’s just a long list of numbers. All of this makes it incredibly painful for humans to write by hand! To solve this problem, we created higher-level programming languages. Let’s take C for example. When you write a C program you must compile that source code before it can be executed. Compiling the source code means to translate the human-readable C source code into the machine-readable assembly language! Executing the resulting executable file directly instructs the processor to begin performing the assembly language instructions stored in that file.

Generally speaking, your executable isn’t immediately “directly” run on the processor. An operating system, like Windows, often sits between your executable and the processor you’re attempting to execute on. Windows will allocate resources and schedule your commands prior to running them on the processor. However, the full behavior of what an operating system does is outside the scope of this discussion.

The executable program which performs this translation from C source code to assembly language is called a compiler! The most popular C compiler, the GNU Compiler Collection (gcc), is a Linux program that was originally written by Richard Stallman in ~1984. However, the first C compiler was created at Bell Labs by Dennis Ritchie in ~1972. Imagine writing by hand, in assembly, a program that is capable of reading the entirety of the C programming language and translating all of the rules and grammar of the C language into the corresponding assembly code. That’s what Dennis Ritchie did! Luckily, we don’t have to do that ever again because gcc exists for free!

The team at Bell Labs throughout the late 1900s was remarkable. They are the creators of the first transistor, the Unix Operating System, the C/C++ languages, and so much more. Much of what is the fabric of Software today was originally created by a few people at Bell Labs.

Because operating systems and processors are all different, there are many different C compilers. The native C compiler for Windows for instance is not gcc. It’s some Microsoft proprietary plugin to Visual Studio. However, the grammar of the C language itself is always the same. Each new compiler translates the standardized C code into the corresponding assembly capable of being understood by its associated processor.

So, what does a Software Engineer do?

A Software Engineer’s job, in short, is to coordinate all the functionality of a processor to perform a task or calculation which solves the problem the engineer wants to solve. Software only exists to orchestrate the hardware it runs on. You are the conductor, guiding the flow electrons in precisely the manner which you require. For the vast majority of Software Engineers. Those who write in interpreted languages like Javascript, Python, or another similar language— the hardware is abstracted away from you. However, for the Embedded engineer writing usually in C, C++, or some other compiled language; understanding how the hardware operates is directly applicable.

Our next topic will be a discussion on the differences between interpreted and compiled languages! That will dive a bit deeper into the relationship between source code and the hardware it runs on.

--

--

Marcus Quettan
Marcus Quettan

Written by Marcus Quettan

Research Engineer — Bored Millennial

No responses yet