Catto / Topic Map · Computing for Electronics I Year 4
Computing for Electronics I · 72 h · Topic 1 of 3

Basic elements of the C language

C is the language of electronics. It is used to program the microcontrollers of Years 6 and 7, industrial embedded systems and the kernel of almost every operating system. It is demanding, but it gives total control over the machine.

Programming Data types Flow control Functions Preprocessor

01Why C in an electronics technician program

There are more comfortable languages, but none combines these four things at once:

  • Direct access to the hardware. It lets you write a value into a microcontroller register or read an input port exactly as described in the datasheet.
  • Efficiency. The generated code is almost as compact and fast as code written in assembly, and that matters when you have 2 KB of memory.
  • Portability. The same program, with minimal adjustments, compiles for a PIC, an AVR, an ARM or a PC.
  • Universality. Every microcontroller manufacturer supplies its own C compiler. It is the common denominator of the industry.
C is a compiled language

The source program (.c) goes through the preprocessor, then through the compiler, which translates it to object code (.o), and finally through the linker (link editor), which joins the objects and the libraries and produces the executable. On a microcontroller that result is a .hex file that is written to flash memory. Understanding this chain is what makes it possible to interpret error messages.

02Structure of a program

#include <stdio.h>      // preprocessor directives
#define PI 3.14159    // symbolic constant

float area(float radio);   // function prototype

int main(void)             // EVERY program has a main
{
    float r = 2.5;         // declaration with initialization
    printf("Area = %.2f\n", area(r));
    return 0;                // 0 = finished without error
}

float area(float radio)    // function definition
{
    return PI * radio * radio;
}
ElementWhat it is for
#includeBrings in a header file with ready-made declarations. With <> it searches the system libraries; with "", the project directory.
#defineDefines a constant or macro that the preprocessor replaces textually before compiling.
PrototypeTells the compiler a function’s name, what it receives and what it returns, before its code appears.
main()Entry point. On a microcontroller it never ends: its body is usually a while(1).
{ }Delimit blocks. Everything declared inside a block exists only there.
;Ends each statement. Forgetting it is the number one compilation error.
C is case-sensitive

contador, Contador and CONTADOR are three different variables. By convention, lowercase is used for variables and functions, and UPPERCASE for constants defined with #define.

03Variables, types and constants

byte 1uint8_tuses1 byterange0 to 255One byte. Enough for a parts counter or a PWM value.byte 1byte 2int16_tuses2 bytesrange-32,768 to 32,767Two bytes, signed. It is the default integer on 8-bit micros.byte 1byte 2uint16_tuses2 bytesrange0 to 65,535Two bytes, unsigned: double the reach if there are never negative values.byte 1byte 2byte 3byte 4int32_tuses4 bytesrange±2,147,483,647Four bytes. For accumulated milliseconds or large counts.byte 1byte 2byte 3byte 4floatuses4 bytesrange±3.4 × 10^384-byte floating point, with about 7 significant digits. On a micro with no floating-point unit it isvery slow.Choosing the smallest type that fits saves memory, but exceeding the range gives no error: the value wraps around.A uint8_t at 255 plus one is 0, and that is one of the hardest errors to find.
Figure 1. The types and their size in memory, animated. Choosing the smallest one that fits saves memory, but exceeding the range gives no error: the value wraps around and the problem shows up later.

Every variable must be declared before it is used, stating its type. That type determines how much memory it takes up and what values it can hold.

TypeTypical size
(8-bit microcontroller)
RangeUse
char1 byte−128 to 127A character, or a small signed integer.
unsigned char1 byte0 to 255The most widely used type in microcontrollers: one port, one byte.
int2 bytes−32,768 to 32,767General-purpose integers. Careful: on a PC they are 4 bytes.
unsigned int2 bytes0 to 65,535Counters, values from a 10-bit ADC.
long4 bytes± 2,147,483,648Elapsed milliseconds, frequencies.
float4 bytes± 3.4 × 1038, 7 digitsCalculations with decimals. Expensive on a micro with no floating-point unit.
double8 bytes15 digitsDouble precision. Rarely justifiable in embedded systems.
void——“Nothing.” A function that returns no value or takes no parameters.
The size of int depends on the machine

The C standard only guarantees minimums, not exact sizes. That is why in embedded programming the types from <stdint.h> are used, which are unambiguous: uint8_t, int16_t, uint32_t. The number is the count of bits and the u means unsigned. It is the recommended professional practice and avoids errors when porting code.

Overflow

unsigned char c = 250;
c = c + 10;     // NOT 260: it gives 4, because 260 does not fit in 8 bits

unsigned char i;
for (i = 0; i < 300; i++) { ... }   // INFINITE LOOP: i never reaches 300

Overflow does not generate any error: the value simply “wraps around.” It is one of the hardest causes to find in an embedded program, which is why the type must be chosen with the maximum possible value in mind, not the usual one.

Constants

#define LARGO 10            // preprocessor: textual replacement, no type
const float PI = 3.14159;   // read-only variable, WITH a type

int a = 255;      // decimal
int b = 0xFF;     // hexadecimal — the most widely used with registers
int c = 0b11111111; // binary (GCC extension, very handy for ports)
int d = 0377;     // OCTAL: the leading 0 changes the base. Classic trap

In electronics hexadecimal is the natural format: each hex digit represents exactly 4 bits, so 0xF0 reads at a glance as “the four high bits set to 1.”

04Operators

Arithmetic and assignment

+ − * / % · ++ -- · = += -= *= /=

The % is the remainder of integer division. / between integers truncates: 7/2 gives 3, not 3.5.

Relational and logical

== != < > <= >= · && || !

In C, 0 is false and any other value is true.

Bitwise operators: the essential ones

These are what make C suitable for electronics. They let you manipulate individual bits of a register without touching the others.

OperatorFunctionTypical use
&Bitwise ANDSetting bits to 0 (masking) and testing a bit.
|Bitwise ORSetting bits to 1.
^Bitwise XORInverting bits (toggle).
~ComplementInverting all the bits.
<<Shift leftBuilding masks. Equivalent to multiplying by 2.
>>Shift rightExtracting fields. Equivalent to dividing by 2.
// The four operations used all the time with registers:

PORTB |=  (1 << 3);   // set bit 3 to 1, without touching the others
PORTB &= ~(1 << 3);   // set bit 3 to 0
PORTB ^=  (1 << 3);   // toggle bit 3

if (PINB & (1 << 3)) { // is bit 3 set to 1?
    // yes: the pushbutton is pressed
}
Why you do not write PORTB = 8;

Because that would set bit 3 to 1 and all the others to 0, switching off the rest of the port’s outputs. The forms with |= and &= ~ modify only the bit you care about. It is the difference between a program that works and one that shuts off the motor every time it turns on the LED.

The most common error of all: = versus ==
if (x = 5)   // ASSIGNS 5 to x, and since 5 is not 0, it is ALWAYS true
if (x == 5)  // COMPARES x with 5. This is what was intended

It is not a compilation error: the program compiles and misbehaves. Modern compilers warn with a warning, which is why you should always compile with -Wall and read the warnings.

05Flow control

int suma = 0;for (int i = 1; i <= 4; i++) { suma = suma + i;}printf("%d", suma);i-suma0step 1 of 11suma is created and starts at zero.i1suma0step 2 of 11condition truei is created with value 1 and the condition is checked: 1 ≤ 4, so the loop is entered.i1suma1step 3 of 11condition truesuma takes the value 0 + 1 = 1.i2suma1step 4 of 11condition truei++ sets it to 2 and the check is repeated: 2 ≤ 4, so the loop is entered again.i2suma3step 5 of 11condition truesuma becomes 1 + 2 = 3.i3suma3step 6 of 11condition truei is 3, the condition still holds.i3suma6step 7 of 11condition truesuma becomes 3 + 3 = 6.i4suma6step 8 of 11condition truei is 4: the last value that satisfies 4 ≤ 4.i4suma10step 9 of 11condition truesuma becomes 6 + 4 = 10.i5suma10step 10 of 11condition falsei is 5 and no longer satisfies the condition: the loop ends.i5suma10step 11 of 1110 is printed.Tracing the program by hand, noting each variable at every step, is the best way to understand a loop.
Figure 2. A for loop executed step by step, animated: you can see the line being executed and the value of each variable at that moment.
Decision
if (temp > 30) {
    encender_ventilador();
} else if (temp < 18) {
    encender_estufa();
} else {
    apagar_todo();
}

switch (tecla) {
    case 1: subir();  break;
    case 2: bajar();  break;
    default: parar();
}
Repetition
for (i = 0; i < 8; i++) {
    encender_led(i);
}

while (!dato_listo()) {
    // wait
}

do {
    v = leer_adc();
} while (v < umbral);

while (1) { ... }  // main loop
Forgetting the break in a switch

Without break, execution keeps falling through into the next case. Sometimes this is done on purpose (to group cases), but it is almost always an oversight and produces baffling behavior.

The main loop of an embedded system
int main(void)
{
    configurar_puertos();   // runs ONCE
    configurar_adc();
    configurar_timers();

    while (1) {              // repeats FOREVER
        leer_sensores();
        procesar();
        actualizar_salidas();
    }
    return 0;   // never reached
}

This structure — configuration + infinite loop — is that of every microcontroller program, and it is literally what the setup() and loop() functions of Arduino do, which is C underneath.

06Arrays and strings

An array is a set of elements of the same type, contiguous in memory, accessed by an index that starts at 0.

int muestras[10];              // valid indices: 0 to 9
int tabla[5] = {2, 4, 6, 8, 10};

muestras[0] = 512;             // first element
muestras[9] = 128;             // LAST element
muestras[10] = 0;              // OUT OF RANGE! Overwrites memory that is not yours

// Average 10 samples from the ADC
long suma = 0;
for (int i = 0; i < 10; i++) {
    muestras[i] = leer_adc();
    suma += muestras[i];
}
int promedio = suma / 10;
C does not check the bounds of an array

Writing muestras[10] in an array of 10 elements gives no error: it writes to the next memory location, which may belong to another variable, to the stack or to a register. The program keeps running and fails later in an inexplicable way. On a PC this is a classic security hole (buffer overflow); on a microcontroller, a random hang. Checking the indices is the programmer’s responsibility.

Character strings

In C a string is simply an array of char terminated by the null character '\0'. It is not a special type.

char nombre[20] = "Electronics";   // takes 11 letters + 1 for the '\0' = 12

#include <string.h>
strlen(nombre);            // 11 — does not count the '\0'
strcpy(destino, origen);   // copy
strcmp(a, b);              // compare: returns 0 if they are equal
strcat(destino, origen);   // concatenate
You must reserve room for the '\0'

An array of 20 char holds at most a string of 19 characters. And strcmp(a, b) returns 0 when they are equal, which is counterintuitive: you write if (strcmp(a,b) == 0). Never compare strings with ==, because that compares memory addresses, not contents.

07Functions

Splitting the program into functions is not cosmetic: it is what lets you test each part separately, reuse code and understand a thousand-line program.

// return_type  name(type param1, type param2)
float celsius_a_fahrenheit(float c)
{
    return c * 9.0 / 5.0 + 32.0;
}

void parpadear(unsigned char veces)   // void: returns nothing
{
    for (unsigned char i = 0; i < veces; i++) {
        PORTB |= (1 << 0);
        retardo_ms(200);
        PORTB &= ~(1 << 0);
        retardo_ms(200);
    }
}
In C, parameters are passed by value

The function receives a copy of the argument. If it modifies it, the original does not change. For a function to be able to modify a variable of its caller you have to pass its address, with pointers — which is the central subject of the next topic.

Scope of variables

KindWhere it livesWhen to use it
LocalDeclared inside a function. Born on entry and dies on exit.Whenever possible. It is the safest and saves memory.
GlobalOutside every function. Exists during the whole program.Only for data shared by several parts, typically between the main loop and an interrupt.
Staticstatic inside a function: keeps its value between calls.Internal counters that must not be visible from outside.
Volatilevolatile: tells the compiler that it can change outside the program flow.Mandatory for variables modified by an interrupt or mapped to a hardware register.
Without volatile, the optimizer breaks the program

If a global variable is modified inside an interrupt and the main loop reads it, the compiler may “realize” that nobody changes it inside the loop and keep it in a register forever. The program ends up waiting endlessly for a change it cannot see. The keyword volatile forbids that optimization. It is an error that only shows up when compiling with optimization turned on, and it is a classic of Year 6.

08The preprocessor and the standard library

The preprocessor acts before the compiler and performs text replacements. It knows nothing about types or C syntax.

#include <stdio.h>       // paste the contents of that file here
#define LED_ON  PORTB |= 1
#define MAX(a,b) ((a) > (b) ? (a) : (b))   // macro with parameters

#ifdef DEBUG              // conditional compilation
    printf("value = %d\n", x);
#endif

#ifndef CONFIG_H          // include guard: avoids including
#define CONFIG_H          // the same file twice
    ...
#endif
Parentheses in macros are not optional

With #define DOBLE(x) x*2, the expression DOBLE(3+1) expands to 3+1*2 = 5, not 8. That is why every macro is written wrapping each parameter and the whole expression in parentheses: #define DOBLE(x) ((x)*2). When possible, an inline function is preferable, since it does respect types.

The libraries in use

HeaderContainsNote for embedded systems
<stdio.h>printf, scanf, filesprintf takes up several KB: on a small micro it is replaced by a custom function that sends over serial.
<stdlib.h>atoi, rand, mallocAvoid malloc in embedded systems: it fragments memory and can fail without warning.
<string.h>String and memory handlingmemset and memcpy are very useful with buffers.
<math.h>sin, sqrt, powVery heavy. If it is enough, use integer arithmetic or precomputed tables.
<stdint.h>uint8_t, int16_t, …Always use in microcontroller programming.
<stdbool.h>bool, true, falseImproves readability at no cost.

09In the lab

Lab 1 · First program and the compilation cycle

With GCC on the command line (not with a graphical environment, so you can see the steps):

gcc -Wall -c hola.c -o hola.o    # compile to object
gcc hola.o -o hola               # link
./hola                           # run
gcc -E hola.c > hola.i           # see the PREPROCESSOR output

Open hola.i and check that the #include turned into hundreds of lines and that the #defines disappeared, replaced by their value. It is the clearest way to understand what each stage does.

Lab 2 · Types and overflow

Write a program that declares an unsigned char at 250 and adds 1 to it in a loop, printing the value. Observe how it jumps from 255 to 0. Then print sizeof(int), sizeof(long), sizeof(float) and compare with what the course notes say: on the workshop PC int will take up 4 bytes, not 2.

Lab 3 · Bit manipulation

Simulate a port with a variable unsigned char puerto = 0; and write functions set_bit(n), clear_bit(n), toggle_bit(n) and get_bit(n). Print the value in binary after each operation. It is exactly what will be done with real registers in Digital Electronics III.

Lab 4 · Converting an ADC value

Write a function that takes the raw value of a 10-bit ADC (0 to 1023) and converts it to voltage with a 5 V reference (it helps to have the prefix converter at hand so you do not get lost between millivolts and volts). Do it in two ways: with float and with integer arithmetic in millivolts. Compare the results and think about which is better on an 8-bit microcontroller. It is the calculation that will appear in every sensor project.

10Common mistakes

MistakeConsequence
A ; is missingCompilation error, sometimes reported on the next line. Always check the line above the one the compiler points to.
= instead of == in an ifIt compiles, and the condition is always true. Compile with -Wall.
Array index out of rangeNo error: memory that is not yours is overwritten and the failure shows up much later, somewhere else.
Variable used uninitializedIt contains garbage. On a PC it often happens to be zero; on a micro, anything.
Integer division when decimals were expected7/2 gives 3. You have to write 7.0/2 or use a cast.
volatile missing on an interrupt variableThe optimizer “freezes” it and the program hangs waiting for a change it cannot see.
break missing in a switchThe following cases are executed too.
Comparing strings with ==It compares memory addresses, not contents. You have to use strcmp.
Macro without parenthesesUnexpected arithmetic results because of operator precedence.

11Self-assessment

What are the three stages a C program goes through until it is executable?

Preprocessor (resolves #include and #define), compiler (translates to object code) and linker (joins the objects with the libraries and generates the executable or the .hex).

What does unsigned char c = 200; c = c + 100; give?

44. 300 does not fit in 8 bits: 300 − 256 = 44. It is a silent overflow.

Write the statement that sets bit 5 of PORTC to 1 without altering the others.

PORTC |= (1 << 5);
To clear it to 0: PORTC &= ~(1 << 5);
To toggle it: PORTC ^= (1 << 5);

How many usable characters fit in char texto[16];?

15. The sixteenth slot is taken by the terminator '\0'.

Why does if (strcmp(a, b)) do the opposite of what it seems?

Because strcmp returns 0 when the strings are equal, and in C 0 is false. That is, that condition is true when they are different. You have to write if (strcmp(a, b) == 0).

What is the difference between a local variable and a static one inside a function?

The local one is created on entering the function and destroyed on leaving, losing its value. The static one (static) keeps its value between successive calls, although it is still invisible from outside the function.

When is it mandatory to declare a variable as volatile?

When it can change outside the normal flow of the program: variables modified by an interrupt, or mapped to a hardware register. Without volatile, the optimizer may assume nobody changes it and use a copy in a register.

Why is it recommended to use uint8_t instead of unsigned char?

Because the size of C’s basic types depends on the platform. The types in <stdint.h> explicitly declare the number of bits, so the program behaves the same when ported to another microcontroller or to a PC.

What does printf("%d", 7/2); print? And how do you get 3.5?

It prints 3: division between two integers is integer division and truncates. To get 3.5 you have to force floating point: 7.0/2 or (float)7/2, and print it with %f.

Development of the topic “Basic elements of the C language” of Computing for Electronics I (Year 4), following the “Curriculum Proposal – Second Cycle of the Technical-Professional Track, Secondary Education – Electronics,” Ministry of Education of the Province of Córdoba, DGETyFP. Back to the Topic Map · catto.ar