This article discusses the basic characteristics of C, a straightforward language that is still widely used for programming microcontrollers.

Supporting Information

 

By the standards of modern technology, C is a rather old language. The original development took place in the early 70s, followed by revisions in the late 70s and standardization in the 80s. Nevertheless, in my opinion it has lost none of its vigor. It’s still a great language for embedded applications, and in my experience it is a suitable programming environment for everything from simple microcontroller-based devices to sophisticated digital signal processing.

 

The Need for C

I have no doubt that there are at least a few electrical engineers who do not know how to write a program in C and never will need to write a program in C. If you’re the sort of person who prefers hardware to software, you might consider these individuals “the lucky ones.”

Whether we like it or not, though, programming is an increasingly important part of electrical engineering, and actually I have found much satisfaction in being able to not only design circuit boards but also write the firmware for those boards. These two aspects of system development are closely related, and I suspect that the end result is often superior when board design and firmware development are carried out by the same person.

 

C vs. Assembly

In theory, I am a proponent of assembly language. In reality, I have reached a point in my life at which assembly language is a threat to both my financial security and my sanity. Writing firmware in assembly is slow and error-prone, and maintaining an adequate level of organization in long, complex programs is hopelessly difficult.

However, I will certainly insist that you cannot really understand high-level languages if you don’t understand assembly. If you’ve never had the opportunity to gain some solid experience with assembly language, you should at least familiarize yourself with some of the basic concepts before you dive into C. The articles listed above in the Supporting Information section are a good place to start.

 

 

What Do Processors Understand?

Only machine language. Ones and zeros. All of the “programmer-friendly” aspects of the C language must eventually be translated into the low-level reality of the processor’s digital hardware—i.e., binary arithmetic, logical operations, data transfer, registers, and memory locations.

It certainly is possible to successfully write a program in C while knowing nothing about the actual hardware, but in the context of embedded systems development, it’s helpful and sometimes necessary to understand both your hardware and how your C code interacts with that hardware.

 

 

The Basics

C programs range from those that are quite simple to those that are very complex. In the embedded world, many programs will tend toward the simple side of the spectrum, and the basic programming elements described below provide a good foundation for further study of C-language firmware development.

 

Include Statements

An embedded C program will begin with at least one #include statement. These statements are used to introduce the contents of a separate file into your source file. This is a handy way to keep your code organized, and it also allows you to use library functionality, hardware-configuration routines, and register definitions provided by the manufacturer.

The code excerpt below shows the include statements that I used in one of my microcontroller projects. Note that the “Project_DefsVarsFuncs.h” file is a custom header file created by the programmer (i.e., me). I was using it as a convenient way to incorporate preprocessor definitions, variables, and function prototypes into multiple source files.

 

                    //-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include                   // SFR declarations
#include "Project_DefsVarsFuncs.h"
#include "InitDevice.h"
#include "cslib_config.h"
#include "cslib.h"
                  

Preprocessor Definitions

You can use a #define statement to create a string that will be replaced by a number. Preprocessor definitions are not necessary, but in some situations they are extremely helpful because they allow you to easily modify a value that appears in various different portions of your program.

For example, let’s say that you’re using the microcontroller’s ADC and that your code uses the ADC’s sample rate in several separate calculations. A preprocessor definition allows you to use an intuitive string (such as SAMPLE_RATE) instead of the number itself in the calculation code, and if you’re experimenting with different sample rates, you only need to change the one numerical value in the preprocessor definition.

 

                    #define SAMPLE_RATE 100000
                  

You can change 100000 to any other number, and this new number will be used to replace all instances of the string SAMPLE_RATE.

Preprocessor definitions are also a great way to make code more readable. The following is a list of handy #define statements that I incorporate into all of my firmware projects.

 

                    #define BIT7 0x80
#define BIT6 0x40
#define BIT5 0x20
#define BIT4 0x10
#define BIT3 0x08
#define BIT2 0x04
#define BIT1 0x02
#define BIT0 0x01

#define HIGH 1
#define LOW 0

#define TRUE 1
#define FALSE 0

#define SET 1
#define CLEARED 0

#define LOWBYTE(v)   ((unsigned char) (v))
#define HIGHBYTE(v)  ((unsigned char) (((unsigned int) (v)) >> 8))
                  

It’s important to understand that preprocessor definitions have no direct relationship to hardware. You’re just telling the preprocessor to replace one string of characters with another string of characters before the program is compiled.

 

Variables

Processors store data in registers and memory locations. There really is no such thing as a variable as far as the hardware is concerned. For the programmer, though, writing code is much easier when we can use intuitively named variables instead of memory addresses or register numbers.

Compilers can manage the low-level details associated with variables without much input from the programmer, but if you want to optimize your use of variables you'll need to know something about the device’s memory configuration and the way in which it handles data of different bit widths.

The following code excerpt gives an example of variable definition. This was written for the Keil Cx51 compiler, which reserves one byte of memory for an “unsigned char” definition, two bytes for an “unsigned int” definition, and four bytes for an “unsigned long” definition.

 

                    unsigned long Accumulated_Capacitance_Sensor1;
unsigned long Accumulated_Capacitance_Sensor2;

unsigned int Sensor1_Unpressed;
unsigned int Sensor2_Unpressed;

unsigned int Sensor1_Measurement;
unsigned int Sensor2_Measurement;

unsigned int AngularPosition;

unsigned int TouchDuration;

unsigned char CurrentDigit;
unsigned int CharacterEntry;
unsigned char DisplayDivider;
                  

Operators, Conditional Statements, and Loops

The core of computational functionality consists of moving data, performing mathematical computations and logical operations with data, and making programmatic decisions based on the value of stored or generated data.

Mathematical operations and bit manipulation are accomplished by means of operators. C has quite a few operators: equals (=), addition (+), subtraction (-), multiplication (*), division (/), bitwise AND (&), bitwise OR (|), and so forth. The “inputs” to an operator statement are variables or constants, and the result is stored in a variable.

Conditional statements allow you to perform or not perform an action based on whether a given condition is true or false. These statements use the words “if” and “else”; for example:

 

                    if(Sensor1 < Sensor2 && Sensor1 < Sensor3)
	return SENSOR_1;

else if(Sensor2 < Sensor1 && Sensor2 < Sensor3)
	return SENSOR_2;

else if(Sensor3 < Sensor2 && Sensor3 < Sensor1)
	return SENSOR_3;

else
	return 0;

                  

For loops and while loops provide a convenient means of repeatedly executing a block of code. These types of tasks arise very frequently in embedded applications. For loops are more oriented toward situations in which a block of code must be executed a specific number of times, and while loops are handy when the processor should continue repeating the same block of code until a condition changes from true to false. Here are examples of both types.

 

                    for (n = 0; n < 16; n++)
{
	Accumulated_Capacitance_Sensor1 += Measure_Capacitance(SENSOR_1);
	Delay_us(50);
	Accumulated_Capacitance_Sensor2 += Measure_Capacitance(SENSOR_2);
	Delay_us(50);
} 
                  

                    while(CONVERSION_DONE == FALSE);
{
	LED_STATE = !LED_STATE;
	Delay_ms(100);
}
                  

Functions

Good C code is vastly superior to assembly code in terms of organization and readability, and this is due in large part to the use of functions.

Functions are blocks of code that can be easily incorporated into other portions of code. Causing the processor to execute the instructions contained in the function is referred to as “calling” the function. A function can accept one or multiple inputs, and it can provide one output, called a return value.

 

C’s functions, conditional statements, and loops make it fairly easy to translate a flowchart into working code. This example is from a project in which I used SPI communication to control an LCD.

 

The use of functions does involve some overhead, so we have to be careful to not burden the processor with an excessive number of function calls, but in general the benefits of functions far outweigh the costs.

Here is an example of a function that has three numerical inputs and uses these inputs to generate a true-or-false return value.

 

                    bit Is_In_Range(int input, int LowerBound, int UpperBound)
{
	if(input >= LowerBound && input <= UpperBound)
		return TRUE;

	else
		return FALSE;
}
                  

Conclusion

A thorough discussion of the C language could go on almost indefinitely, and this article has only scratched the surface. I hesitate even to publish something that omits so much important information, but we have to start somewhere. We plan to publish quite a few more articles on the use of the C language in embedded applications, and we’ll fill in this introductory article with links as other resources become available.

If you have any C-related topics that you’d like to learn more about, feel free to let us know in the comments section below.

 

C Language Series

 

Comments

9 Comments