Setting up an Embedded Development Environment: Difference between revisions

No edit summary
No edit summary
 
(2 intermediate revisions by the same user not shown)
Line 7: Line 7:
For the purposes of this page, I will be using the Make naming convention for compiler toolchain programs, namely <code>CC</code> for the compiler, <code>LD</code> for the linker, <code>OBJCOPY</code> for the object file translator. and <code>AS</code> for the assembler.</blockquote>
For the purposes of this page, I will be using the Make naming convention for compiler toolchain programs, namely <code>CC</code> for the compiler, <code>LD</code> for the linker, <code>OBJCOPY</code> for the object file translator. and <code>AS</code> for the assembler.</blockquote>


=== Compilation ===
== Compilation ==
A '''compiler''' translates code from a source language to a target language. In our case, C is the source language and Arm assembly is the target language. We don't really care about the generated assembly code though, so when we run the compiler we also have it '''assemble''' the generated assembly code into an object file ready for linking.
A '''compiler''' translates code from a source language to a target language. In our case, C is the source language and Arm assembly is the target language. We don't really care about the generated assembly code though, so when we run the compiler we also have it '''assemble''' the generated assembly code into an object file ready for linking.


=== Linking ===
== Linking ==
A '''linker''' takes multiple object files and combines them into a coherent machine code executable, resolving symbols and references in the process. This is how things like functions and function prototypes are correlated and <code>extern</code> variables are resolved.
A '''linker''' takes multiple object files and combines them into a coherent machine code executable, resolving symbols and references in the process. This is how things like functions and function prototypes are correlated and <code>extern</code> variables are resolved.


=== Architecture ===
== Architecture ==
When writing code on a PC intended to be run on a PC, the compilation-into-executable process is really straightforward. There might be some hiccups when it comes to writing software for Windows versus Linux versus macOS for example, but that is just a matter of library usage (when building for Linux your program would link to Linux system libraries, as the exercise in the previous lesson indicates; building for Windows is just a matter of linking a Windows library in its place). It gets a little more complicated when it comes to computer architecture differences however, which has become more evident recently with Apple switching their Macs to Arm processors instead of Intel x86 ones.
When writing code on a PC intended to be run on a PC, the compilation-into-executable process is really straightforward. There might be some hiccups when it comes to writing software for Windows versus Linux versus macOS for example, but that is just a matter of library usage (when building for Linux your program would link to Linux system libraries, as the exercise in the previous lesson indicates; building for Windows is just a matter of linking a Windows library in its place). It gets a little more complicated when it comes to computer architecture differences however, which has become more evident recently with Apple switching their Macs to Arm processors instead of Intel x86 ones.


==== Compiling a Desktop Program ====
=== Compiling a Desktop Program ===
Using the GNU compiler toolchain as an example, when you run <code>gcc</code> on a Linux machine with an x86_64, what is actually being run is x86_64-none-linux-gcc (in a sense, that's why it's not in the monospaced font, because this isn't ''actually'' what happens). For GNU, compiler toolchains roughly follow this naming convention: <code>target_architecture-vendor-ABI</code>, where <code>target_architecture</code> means the architecture you are compiling for (where the compiled code is going to be executed on), <code>vendor</code> is the distributor of the compiler (this part isn't super important, for our purposes this is almost always "none"), and <code>ABI</code> is the '''Application Binary Interface''' of your target.
Using the GNU compiler toolchain as an example, when you run <code>gcc</code> on a Linux machine with an x86_64, what is actually being run is x86_64-none-linux-gcc (in a sense, that's why it's not in the monospaced font, because this isn't ''actually'' what happens). For GNU, compiler toolchains roughly follow this naming convention: <code>target_architecture-vendor-ABI</code>, where <code>target_architecture</code> means the architecture you are compiling for (where the compiled code is going to be executed on), <code>vendor</code> is the distributor of the compiler (this part isn't super important, for our purposes this is almost always "none"), and <code>ABI</code> is the '''Application Binary Interface''' of your target.


==== Application Binary Interface ====
=== Application Binary Interface ===
The Application Binary Interface (ABI) is similar to an API in the sense that it provides a common executable binary interface for programs to use. One part of an ABI that you might be familiar with is the function calling convention. This is what determines the instructions that appear in a function's prologue and epilogue to ensure that no data gets lost when calling a function. The ABI also dictates how the stack behaves. Different operating systems use different ABIs, so you can imagine that it is important to specify this when generating assembly code at compile-time.
The Application Binary Interface (ABI) is similar to an API in the sense that it provides a common executable binary interface for programs to use. One part of an ABI that you might be familiar with is the function calling convention. This is what determines the instructions that appear in a function's prologue and epilogue to ensure that no data gets lost when calling a function. The ABI also dictates how the stack behaves. Different operating systems use different ABIs, so you can imagine that it is important to specify this when generating assembly code at compile-time.


==== Compiling Our Embedded Program ====
=== Compiling Our Embedded Program ===
As of time of writing, we use two MCUs: an STM32G0B1RE, and an STM32G431CB. Both of them use a 32-bit ARM CPU, so that is our target architecture. Arm and other companies use a standardized ABI for their embedded processors, called the '''Embedded ABI''' (EABI), and that is the ABI we use. Thus, the name of our compiler is <code>arm-none-eabi-gcc</code> (and now you know where the name comes from).
As of time of writing, we use two MCUs: an STM32G0B1RE, and an STM32G431CB. Both of them use a 32-bit ARM CPU, so that is our target architecture. Arm and other companies use a standardized ABI for their embedded processors, called the '''Embedded ABI''' (EABI), and that is the ABI we use. Thus, the name of our compiler is <code>arm-none-eabi-gcc</code> (and now you know where the name comes from).

=== Downloads for Our Board ===
== Downloads for Our Board ==

=== General Compilation Tools ===
We first need higher-level tools that help automate the compilation of any C or C++ program. To do this, run the command

<code>sudo apt install make cmake</code>

<code>make</code> is a program that adjudicates what files need to be compiled, determined by whether they were changed or not. If you are working on a project with thousands of files that takes maybe 10 minutes to compile, you really don't want to sit around and wait for the whole program to recompile if you just add one print statement in a random file somewhere for example. It turns out that all you have to do is recompile that source file, and then re-link everything, which might take a couple seconds to do. While we won't be working with a codebase that large, <code>make</code> has a bonus side effect of making compiling more "scriptable", meaning that rather than manually type out a bunch of flags for the compiler every time I want to recompile, I can just type it all out once in a Makefile (the file that contains the instructions for <code>make</code> to follow), and call <code>make</code>.

<code>cmake</code> is a program that builds Makefiles for <code>make</code> to call. It is simply another layer of abstraction that, in a sense, makes <code>make</code> more readable and easier to edit. We may use <code>cmake</code> later on for building some projects.

=== Binary Utilities ===
The collection of the compiler, assembler, linker, and various programs for manipulating compiled binaries make up a toolkit called the '''Binary Utilities''' (or binutils). Since the provided compiler for your desktop/laptop is very likely not going to work for our embedded devices we need the binutils for that architecture.

* For x86_64 hosts: [https://developer.arm.com/-/media/Files/downloads/gnu/11.3.rel1/binrel/arm-gnu-toolchain-11.3.rel1-x86_64-arm-none-eabi.tar.xz]
* For aarch64 hosts (ARM Macs, etc.): [https://developer.arm.com/-/media/Files/downloads/gnu/11.3.rel1/binrel/arm-gnu-toolchain-11.3.rel1-aarch64-arm-none-eabi.tar.xz?rev=82c9a3730e454ab6b8101952cd700cda&hash=A484F380E7D73DF3C5F13CA6EBB954D5]

=== Debugging Tools ===
Like programming for any other environment, a debugger is a crucial tool for developing software. The technology behind debugging an embedded application is really interesting. If you call <code>gdb</code> on your desktop for a program running on your desktop, it is relatively straightforward to picture how that works. At a high level, your operating system provides certain features to let you pause the execution of a program and analyze the state of memory or registers in your program at any given time. But our embedded device doesn't have an operating system... '''it's not even on our computer'''! How does one debug an application that is not even running on the same machine you are debugging from? The answer is that there is specialized hardware on the chip and a little bit of software provided by the manufacturer which enables the debugger to pause the hardware in execution and inspect memory or registers. The program that we use is called <code>openocd</code>, or Open On-Chip Debugger. It takes advantage of an underlying standard called JTAG, which is beyond the scope of this lesson.

To install, run the command:

<code>sudo apt install openocd gdb</code>

=== Flasher ===
TBD

Latest revision as of 19:11, 3 October 2024

A development environment is a collection of software organized to help streamline writing code, compiling it, and installing it to the correct location to be run.

For writing embedded software, it is important to recall how compilation works.

Note

For the purposes of this page, I will be using the Make naming convention for compiler toolchain programs, namely CC for the compiler, LD for the linker, OBJCOPY for the object file translator. and AS for the assembler.

CompilationEdit

A compiler translates code from a source language to a target language. In our case, C is the source language and Arm assembly is the target language. We don't really care about the generated assembly code though, so when we run the compiler we also have it assemble the generated assembly code into an object file ready for linking.

LinkingEdit

A linker takes multiple object files and combines them into a coherent machine code executable, resolving symbols and references in the process. This is how things like functions and function prototypes are correlated and extern variables are resolved.

ArchitectureEdit

When writing code on a PC intended to be run on a PC, the compilation-into-executable process is really straightforward. There might be some hiccups when it comes to writing software for Windows versus Linux versus macOS for example, but that is just a matter of library usage (when building for Linux your program would link to Linux system libraries, as the exercise in the previous lesson indicates; building for Windows is just a matter of linking a Windows library in its place). It gets a little more complicated when it comes to computer architecture differences however, which has become more evident recently with Apple switching their Macs to Arm processors instead of Intel x86 ones.

Compiling a Desktop ProgramEdit

Using the GNU compiler toolchain as an example, when you run gcc on a Linux machine with an x86_64, what is actually being run is x86_64-none-linux-gcc (in a sense, that's why it's not in the monospaced font, because this isn't actually what happens). For GNU, compiler toolchains roughly follow this naming convention: target_architecture-vendor-ABI, where target_architecture means the architecture you are compiling for (where the compiled code is going to be executed on), vendor is the distributor of the compiler (this part isn't super important, for our purposes this is almost always "none"), and ABI is the Application Binary Interface of your target.

Application Binary InterfaceEdit

The Application Binary Interface (ABI) is similar to an API in the sense that it provides a common executable binary interface for programs to use. One part of an ABI that you might be familiar with is the function calling convention. This is what determines the instructions that appear in a function's prologue and epilogue to ensure that no data gets lost when calling a function. The ABI also dictates how the stack behaves. Different operating systems use different ABIs, so you can imagine that it is important to specify this when generating assembly code at compile-time.

Compiling Our Embedded ProgramEdit

As of time of writing, we use two MCUs: an STM32G0B1RE, and an STM32G431CB. Both of them use a 32-bit ARM CPU, so that is our target architecture. Arm and other companies use a standardized ABI for their embedded processors, called the Embedded ABI (EABI), and that is the ABI we use. Thus, the name of our compiler is arm-none-eabi-gcc (and now you know where the name comes from).

Downloads for Our BoardEdit

General Compilation ToolsEdit

We first need higher-level tools that help automate the compilation of any C or C++ program. To do this, run the command

sudo apt install make cmake

make is a program that adjudicates what files need to be compiled, determined by whether they were changed or not. If you are working on a project with thousands of files that takes maybe 10 minutes to compile, you really don't want to sit around and wait for the whole program to recompile if you just add one print statement in a random file somewhere for example. It turns out that all you have to do is recompile that source file, and then re-link everything, which might take a couple seconds to do. While we won't be working with a codebase that large, make has a bonus side effect of making compiling more "scriptable", meaning that rather than manually type out a bunch of flags for the compiler every time I want to recompile, I can just type it all out once in a Makefile (the file that contains the instructions for make to follow), and call make.

cmake is a program that builds Makefiles for make to call. It is simply another layer of abstraction that, in a sense, makes make more readable and easier to edit. We may use cmake later on for building some projects.

Binary UtilitiesEdit

The collection of the compiler, assembler, linker, and various programs for manipulating compiled binaries make up a toolkit called the Binary Utilities (or binutils). Since the provided compiler for your desktop/laptop is very likely not going to work for our embedded devices we need the binutils for that architecture.

  • For x86_64 hosts: [1]
  • For aarch64 hosts (ARM Macs, etc.): [2]

Debugging ToolsEdit

Like programming for any other environment, a debugger is a crucial tool for developing software. The technology behind debugging an embedded application is really interesting. If you call gdb on your desktop for a program running on your desktop, it is relatively straightforward to picture how that works. At a high level, your operating system provides certain features to let you pause the execution of a program and analyze the state of memory or registers in your program at any given time. But our embedded device doesn't have an operating system... it's not even on our computer! How does one debug an application that is not even running on the same machine you are debugging from? The answer is that there is specialized hardware on the chip and a little bit of software provided by the manufacturer which enables the debugger to pause the hardware in execution and inspect memory or registers. The program that we use is called openocd, or Open On-Chip Debugger. It takes advantage of an underlying standard called JTAG, which is beyond the scope of this lesson.

To install, run the command:

sudo apt install openocd gdb

FlasherEdit

TBD