…for Embedded and Low-Level Systems Development
C provides the convenience of learning one language while retaining the ability to target a variety of platforms including modern operating systems (Linux, Windows, Mac), real-time operating systems, systems-on-a-chip, and a host of microcontrollers for embedded development. And if you have to “mov” the bits around yourself (device drivers, DMA controllers), you can do that too. This is a significant efficiency over assembly languages which are essentially chip-specific control codes and therefore require understanding the architecture of the target chip.
The C Language for Systems Development
The C language can be thought of as a machine independent, “macro assembler” with a small library of useful core subroutines provided so that you don’t have to re-invent the wheel every time you write low-level code.
Why choose C (over Assembly) for Systems Development?
So you’ve tried assembly language? That’s good. It brings the pedal to the metal when it comes to understanding computing toolchains: binary files, object code, assemblers and disassemblers, libraries, linkers, stacks, function calling conventions and other complexities.
When you’re coding in assembly, what is immediately obvious is that you’re on your own: even the most basic support routines are not there. All you have are the target chip and its capabilities, and that’s it. Everything else is considered higher level and there is no built-in support for it. Files? Forget it. Serial? TCP/IP? Web? Nope. If you need the functionality you have to build it yourself.
On the one hand, that is why there was a market for Operating Systems; the operating system handles all that for you. And these were indeed originally coded in assembly. But allowing for an operating system does not reduce the complexity of working in Assembly. You would need to interface directly with the Operating System’s API, e.g. by manually placing parameters on the call stack ahead of every OS call and restoring the registers upon each return. There are literally thousands of OS API functions on a modern, fine-grained OS.
On the other, for many embedded systems applications (and certainly for operating system development itself), there is no operating system, i.e. bare metal programming.
For these situations, there are two languages — C, which we will cover here, and Forth, covered separately.
Origins of the C Language
In the 1970s, before deregulation of the telephone industry, when Bell Labs was one of the most powerful companies in the U.S., Dennis Ritchie and Ken Thompson developed the C language at Bell Labs as a way to speed up the porting of their newly programmed UNIX operating system between PDP minicomputers. This approach was wildly successful, and put the C language as the mainstream OS development language. Since then, C has become a universal standard, with a standardized library of functions and standards for compiler designers etc. In a nutshell, the C language is now a UNIVERSAL interface to assembly language for all those systems for which there exists a C compiler that targets it.
The Strengths of C
C was designed from the ground up to be not one hair less powerful than assembler. Everything you can do in assembler, you can do in C. And if you want to drop down INTO assembler, you can do that as well: inline assembly (also called embedded assembly). If you want to link in modules or functions written purely in assembly and assembled into object files, you can do that also. And if you want to write in assembler and pull in (extend) assembly with the C functions, you can do that too.
What you get is the convenience of learning one language (a significant efficiency over assembly language), while retaining the ability to target a variety of platforms — from modern operating systems (Linux, Windows, Mac) to real-time operating systems, (PC/104 stacks), systems-on-a-chip (Analog Devices Blackfin, Microchip’s 32-bit PIC chips), and a host of microcontrollers for embedded development (Microchip’s 8-bit PIC chips, Atmel AVR, Motorola (Freescale) 68k family, Intel 8051 family). And whenever you have to “mov” the bits around yourself (device drivers, DMA controllers), well, you can do that too, using inline assembly.
Bottom line: for most things that even system level programmers want to do, C is the preferred level of work. This can be seen by looking at the source code for the Linux OS and Windows OS. Both are done primarily in C.
“Noise-free” programming of Algorithms and Data Structure
There is a pragmatic advantage when coding algorithms and data structures in C vs. assembly. Working in assembly means that the essence of data structures and algorithms is often hidden in the noise of mov’s and pushes. In C, algorithmic details stand out more clearly, so the patterns can be more easily understood, and programs more easily modified and maintained.
Bare Bones Programming
Like assembly, in C you are forced to work directly with memory, pointers, and the underlying arrangement of the bits and bytes that make up your data structures and their layout (packed structures, byte ordering, data types and conversions, etc.) No garbage collection, no forgiveness for buffer overruns, no regular expressions. C is for bare bones programming — which typically require safe coding practices, tight program design, and a good understanding of the toolchain and the target environment.
But when you want the underlying elements of the data structures to be visible and your algorithms to be working with these bare bones details, C is a great choice for turning out programs that interact with hardware, networks, sensors, and peripherals, and that are small (compiled size), fast (execution time), and have a small footprint (few dependencies, low loading overhead).
With the aid of bare bones compilation modes like gcc -S %1.c -fno-exceptions -s -Os, and of working syntax converters like A2I, whenever you wish to see the kind the assembly code that C is generating for you, you can.
By using C for systems level development, one retains the power of assembly but the efficiency of a language that abstracts the specific innards of the target chip.
Three Plus Three Further Reasons to Code In C (over Assembly)
There are three good reasons why C is a more compelling choice than assembly for systems development:
- it more easily supports a myriad of simple as well as complex data structures that are the bedrock of algorithms and therefore implementations
- it comes with a wide variety of standard facilities (the C Standard Library)
- it is independent of particular computing architectures — the essential details posed by specific computing architectures are handled by the target specific compiler. (Note! In theory, C as a language is independent of Operating System, but in practice, there is usually an Operating System dependence — see Practical Matters, below.)
Even where the use of assembly is compelling, there are three reasons why C might yet be a better choice:
- C retains all of the power and fine-grained control that assembly has; C can be treated as an assembly language for assembly
- C plays well with assembly, allowing inline and embedded assembly, as well as the linking in of modules written entirely in assembly
Controversies around Optimizing Compilers
A end-to-end compiler does 4 things to go from source code to executable: pre-process, compile, assemble, and link. Here’s how you can setup GCC to see the intermediate results of each step.
- Non-optimizing compilers such as TCC can create simple/straightforward assembly code and be used as an efficient assembler (see Assembly Part III). C compilers exist that target most embedded platforms, chips, and environments.
- Modern optimizing compilers such as GCC are no longer portable, universal assemblers (their original vision). While they handle the complexities of modern processor architectures, their mission has shifted and they have become repositories of compiler optimization capabilities (e.g. pipelining, caching, etc.) making the C compiler into automatic code generator, but at the same time taking liberties with C code which now becomes a suggestion to the compiler rather than a specification for a machine.
For a more detailed discussion of the controvery, see Assembly Part I, Appendix: The Sacred and the Profane.
Practical Matters: Designing Complex Programs in C
The C standard libraries are designed to make available the facilities most commonly needed, and make them available in a manner that is independent of platform and operating system.
In practice, however, there are typically facilities that you will desire for your programs that are not part of the C standard library, and that will be supplied to you by the Operating System. At this point, your C program will need headers and object code provided by the Operating System. The form of the functions you call will then often be Operating System dependent.
Thus, one way to think of C is that while it enables hardware independence, it does NOT enable operating system independence.
Complex programs need their own, coarser-grained building blocks. When working in C, programmers typically create their own application specific libraries. This allows program designers to break down their designs into modules: abstract, general, re-usable blocks of code. These modules are then woven into applications that work within the same problem space, and thus, that share the fundamental elements of application logic and program design.
Encapsulation of implementation
The function is the fundamental unit of modular programming in C (versus the class in object oriented programming).
One of the goals of a library is to provide building blocks whose interfaces and functionality is well documented but whose implementation is a black box and hence free to be modified without affecting any user code (code using the library). For this be accomplished, it is important to be able to separate user code from library code.
C’s method of separation is by convention: .h header files containing the user interface; .c code files containing the implementation. However, this gets blurred when one-line inline definitions or macro definitions are used in the header files (bad style).
Perhaps the best way of addressing this problem is to have pre-compiled libraries of object code (.a, .dll, .o) that are linked into each new design. This requires documentation external from the object code (since object code is not human readable), and it also introduces the possibility of dll hell: the mismatch and maintenance of versions of the object code, since these typically then proliferate and are copied and re-copied, etc.
Object-Oriented C vs. Higher Level Languages
Object-oriented design is certainly possible in C — it is just that the programmer must code in a way that respects the fundamental principles of object orientation, namely information hiding, and the encapsulation of both data structures AND methods into abstract blocks — objects.
It is these, and other facilities that are addressed and improved upon in higher level languages than C: Objective-C, C++, Java, .Net along one language stream, and Perl, Python, and Ruby along another.
I’ll take up higher level languages (Objective-C, C++, Java, .Net, Perl, Python, Ruby) and their strengths for applications development in upcoming posts.
I’ll also discuss a functional open-source development environment for systems and embedded development in C.
Stay tuned.
If you enjoyed this article, subscribe to our RSS feed — don’t miss the next article in the series.
Further Reading
>> Browse Computing Toolkits.
- Reference:
- Safety:
- Style:
[…] From : http://mathscitech.org/articles/c-for-systems […]
Should coders learn to design then, maybe? Learning to code is an absolute necessity if you want to be a good web designer (key word is good). You’ll understand the limitations and loopholes and how a design works when translated into code. You can have infinite possibilities at your fingertips.
Good design comes through experience, some trial and error, and studying good designs. Unless the coding is trivial, there is usually an element of design involved in the translation from requirements to an implementation that delivers these.
I’ve changed the title of this article to Bare Bones instead of Bare Metal, as the latter was misleading.
Bare Metal Programming refers to programming on a chip without the benefit of an operating system. The best way I know for programming “bare metal” is using Forth.
For this, you may enjoy “A Three Instruction Forth”, by Frank Sergeant, which describes a systematic way to program bare metal, provided the chip has a serial communication capability.
[…] software IDE (FREE!) for programming the Nano using the C language (Tip: get the zip download for portable […]
[…] Assembly for Embedded Systems (Assembly 2), “Assembly Toolkit (Assembly 3)“, “Bare Bones (C)“, and Electronics Gateway to […]