Computer Laboratory

Raspberry Pi

RPii - C Tutorial

C.1 Introductions to C Programming

This is a short and quick introduction to programming in C. This section is aimed at anybody who is interested new to programming and may wish to exploring the libRPii source files. The examples and exercise are designed to provide enough references and experience such that even complete beginners can still make the most out of the exploring libRPii tools. All of the source code examples (with prefix ExS.) can be found in the libRPii example source packages. See section GS.2 about how to get these resources.

C.1.1 The Compiler

To produce a program in C, there are two things that must be done. The programmer must write their program, but then they must also convert the code that they have written in C, into a language that the computer's hardware can understand. This language is called machine code and consists of purely 1's and 0's. If humans had to write all of their programs in machine code, then writing any complex program would take too long, and reading it would be even harder! To overcome having to program in machine code a language called assembly was conceived. The assembly language is a language of core instructions like 'move memory,' 'read memory,' 'goto memory location,' 'Add' and 'subtract', and each instruction has an associated representation in machine code call an opcode; when we want to run the program we have written we use an application call an assembler to turn the simple instructions into binary machine code which can be directly read by the computer. Every computer processor or microcontroller is designed to be able to execute these simple instructions, and most have a set of roughly 30 unique instructions (although there may be many more variations on these). The Raspberry Pi's ARM chip has 28. Because of its simplicity, Assembly is known as a low level language. Being constrained to only 30 different commands is still limiting, and is still too time consuming to code complex applications. Each line of code in assembly is (nearly always) stored as 4 bytes of information. Early computer games often used assembly code to give programmers maximum control over the available hardware, and games could easily run to hundreds of thousands of lines long! Once again, to make life easier for programmers, applications call compilers were written to transform commands that are easier for us humans to understand into many more lines of assembly code which can then easily be turned into machine code. A compiler is written in such a way that it understands a file that we give it so long as the file is written in a particular way; i.e in a particular language, following a particular grammar. Moreover, when a programmer does make mistakes (and we all do!), compilers are able to point out where in the code the errors occurred from, and the cause of the error. Therefore, to make a program using the C language, we must pass the files that we want to turn into a program, to the C compiler. The C compiler then turns our C code into assembly code, and the assembler turns this resulting assembly code into a binary file. With a binary file we can run our programs by calling them from the command line.

Throughout this tutorial, do not be put off when you get compiler error codes. While making the RPii Motion Logger application, 70% of attempts to compile failed with an error due to trivial (and sometimes not so trivial errors). Look out for these note boxes throughout the tutorial for help on the errors that are most likely to arise! Be perseverant!

C.1.2 Functions, Libraries and Headers

From now on in the tutorials may sometimes refer to function inputs as arguments!

In C, every line of code we write essentially performs a function of some type. Functions can include inputs, and can produce an output when they finish. For every function that we want a program to perform, we must define (in machine code eventually) how the computer must manipulate the electronic signals in order to produce an output, and how to interpret the output. Take a calculator as an example, when we press '1+2=' the calculator must be programmed to know how to represent the numbers 1 & 2 in such a way so that it can manipulate both of them to find out what the answer is. It must then convert this answer into a form that we understand - the image '3'. Somewhere in the calculator there will be a series of instructions saved in machine code which tell the calculator exactly how to do this. We call the inputs to a function the arguments, and the outputs the outputs; in this case for the 'add' function, if the arguments are 1 and 2, then the output is 3.

Say the calculator already 'knows' how to add. Let's explore how we could instruct it to multiply two numbers together. Noting that that '3 x 2' is the same as '2 + 2 + 2,' then as we know how to add, when the calculator has inputs '3x2' we can instruct it to add 2 and 2, and then to add 2 to the answer.

Notice how by doing this, we don't have to tell the calculator how to add again; the work that was done previously to tell the calculator how to add does not need to be redone… ever (unless we wanted to improve its computational efficiency and make it faster). This is an important concept in programming; once we have written a function to do something, we can use that function as many times as we want. In order to organise the functions which have already been written, we save collections of function definitions into documents called libraries and save them in a known location on our computer. On the Raspberry Pi, such libraries are saved in /usr/lib.

Now consider compiling a source file which calls a function we have written previously. For the compiler to turn our source file into a binary file, the compiler requires that:

  1. The functions called in the source file exist and have been defined elsewhere on the computer.
  2. The functions defined on our computer have the same types of arguments (i.e numbers rather than words) and outputs as the function of the same name we have called in our source files.
  3. We tell it where to find the binary instructions which describe how the function is implemented; the address of the library that contains the function we want to use.

We give this information to the compiler in the following ways:

  • To satisfy requirements 1 and 2 we produce what we call a 'header file.' Each header file contains, among other things, lists of many function names and defines each of their inputs and outputs. We call these function prototypes. If we use a function from another library in our code we must tell the compiler to look in a particular header file in order for it to confirm that the functions that we want to use match what exists in the computer's libraries. If a function we want to use is not found in any of the header files that we told the compiler to look in, then the compilation will fail.
  • Should the compiler be happy that all of the functions used in a program exist, it then continues to look for where the functions are defined. The compiler first looks in a number of default directories. If however we want to use libraries which are installed in different directories to the default, then when we run the compiler, along we must also give the location of the libraries which we want to include.

Summary

A computer has many saved libraries of pre-written code. When we write a program, in our source code we tell the compiler to check in header files for the existence of the functions we want to use, and when we run the compiler we tell it where to look for libraries. If the compiler can find the definitions of the functions we want to use, then it can successfully compile our program. In section C.1.3 we look into how we structure a C source file so that the compiler knows which header files to look for, and can interpret the rest of the code we have written.

C.1.3 The structure of a C source file: hellopi.c

In order to have a program successfully compile (that is be successfully converted without errors into machine code) particular features must be included in the source files to help the compiler find everything it needs. Example Source ExS.1 is for a simple program which prints "Hello Pi" to the terminal. The line by line breakdown which follows explains the role of each line in the program.

ExS.1 Example Source Code: hellopi.c

1.
2.
3.
4.
5.
6.
7.
// Comment: This is the Hello Pi Program.
#include <stdio.h>

int main(){
printf("Hello Pi");
return 0; //This is also a comment and is ignored.
}

A line by line breakdown of ExS.1 hellopi.c

Line No. Comment
1. In C the compiler doesn't read any code after '//'. This lets us write comments about the code. It is important that we leave comments about each section of our code, so that it is easier to understand which sections of code do what. Furthermore, if we would like other contributers to edit our code, then commenting is especially important as code without any comments is very time consuming to understand.
2. At the top of each file, as discussed in section C.1.2, we tell the compiler to look in the header file stdio.h.
  • #include tells the compiler to include the file which follows. In this case the file stdio header file.
  • The chevrons, < and >, tell the compiler to look for stdio.h in the default 'include directory', where most of the standard header files are saved.
  • stdio.h is the 'standard input and output' header file. It contains prototypes for functions which let us print out or read in words and numbers to or from the terminal. We need to include this header to use the function 'printf' on line 5.
3. An empty line does nothing (as we may expect). We do not need them in our code, but they make reading it easier and help separate different sections of code. Here it separates the 'include' command from the 'main' function.
4. The compiler always looks for the main function and expects our programs to start there. The parentheses, '()' always follow any function name, and are used to contain any inputs to the function. This main function has no inputs and so they are empty. The curly bracket '{', indicates the start of the main function definition, and the function definition is ended by placing a closing curly bracket '}' at the end of the function (line 7). The word 'int' is specific to the output of the function, we will discuss this more later.
5. Note the indent (using the 'tab' key) at the start of this line. Similarly to an empty line, in C this does nothing (together, spaces, tabs and returns are known as white space, and all are ignored by the compiler).Once again however, it helps to clearly show that line 5 is part of the function 'main', and makes reading the code much easier. 'printf' is a function in the stdio library. The sole purpose of this program is to call this line of code - the rest of the writing in this file is just to keep the compiler happy. The input to the function is placed between parentheses, and using double quotes, "", signifies to the function that the input is a string of characters which we want to appear in the terminal. Finally, the line ends with a semi-colon, ';'. Every line in a function must end with a semicolon as it signifies to a compiler the end of a statement.
6. We exit any function, including the main loop, by using a line with return in like this one. It is used here to signify the end of the main function, and describes the output of the function. Note that this line still needs a semicolon at the end of it, and that the comments (following the //) are ignored by the compiler.
7. The curly bracket signifies the end of the definition of the function 'main'.

C.1.4 Compiling and Makefiles

In this section we briefly discuss how to compile a program into a binary executable. The compiler is essentially just another program which runs on a computer - like the teaching resources, we can call it from the command line. For compiling C programs, we use a compiler called gcc because it comes installed on the Raspberry Pi. It has many complicated options which allow us to tailor its output to our needs, however to compile a simple program like the Hello Pi above, it is nicely simple. Just like the functions discussed in C.1.2 & C.1.3, the program takes in arguments which are passed to the program by the command when gcc is called.

Try typing gcc --help into the command line. In this case, --help is the only argument needed and in response to this input, the gcc program prints out all of its functionalities and how to use them. You do not need to read everything it outputs, this was purely a demonstration on how to pass arguments into a function.

To compile a simple source file into an executable, we must give the compiler 2 arguments - the source files which make up the program, and the name of the executable. By convention source files written in c have a '.c' suffix at the end of their filename to indicate that the file is written in C. Files written in another language have different suffixes; e.g C plus plus has the suffix '.cpp'. We signify the name of the executable output file by preceeding it with a '-o' argument (Explore the --help option again if you are interested in what the '-o' represents). The executable output file can be named anything we want, but it makes sense to name it after the source file. It does not matter which order these two inputs are passed, however we must give the location of the source file with respect to the working directory of the terminal, and the executable will be saved in the working directory. Furthermore, as the only library we wish to compile with, the stdio library, is a default library, we have no need to link the compiler to it, which keeps compiling the hellopi.c source file nicely simple.

The remaining section of the document is intended to be read but not practiced right away. Please wait until Exercise 2 at the end of this section before trying to run any of this code for yourself.

In order to use the gcc compiler to turn our hellopi.c source file into a binary executable, first we need to navigate to the directory which includes our hellopi.c source file. Once we are in the same directory, (as order of inputs does not matter) to compile our short program we can type into the command line either of:

If we now open the file manager, or just type 'ls' into the command line after compiling, we notice that the executable file 'hellopi' has been produced in the working directory.

gcc hellopi.c -o hellopi

or

gcc -o hellopi hellopi.c


The executable hellopi can be run by typing

./hellopi

We see that the line 'Hello Pi' has been printed to the terminal!

Typing 'gcc -o …' into the command line every time we want to re-compile a program may not seem too bad considering the example above, however once we want to compile many files together and link with libraries which are not default, our gcc calls become more and more complicated - to the extent that we want an easier way to call these lines.

make is another program which comes installed on the Raspberry Pi. The program searches the working directory for a file called makefile or Makefile, and uses this file to execute the necessary gcc commands. Using make can save time in the long run, as by writing a makefile once, the command gcc hellopic.c -o hellopi can be reduced to just make. A makefile for the hellopi program is shown and explained below.

ExS.1.m Example make Code for hellopi.c

1.
2.
3.
4.
#in makefiles, a hash indicates a comment instead
all: hellopi
hellopi: hellopi.c
gcc -o hellopi hellopi.c

A line by line breakdown of ExS.1.m

Line No. Comment
1. To make a comment in a makefile, instead of using // as in C, we use #. The make program ignors any text which follows a #.
2. A makefile uses what are known as targets to decide what needs to be compiled. The target is followed by a colon, and following the colon is a list of the files (or other targets) upon which the target depends. This line states that the 'all:' target depends on hellopi.
3. Make then looks at the hellopi: target to see what that depends on; in this case it depends on hellopi.c. If the file hellopi.c has been changed since the target was compiled, then make runs the line of code below the target.
4. Unlike in C source files, in a makefile the TAB key is not completely ignored. The line following a target MUST begin with (and only with) a tab. (If a space is mixed up in there then make will return a '***missing separator' error). This line gives make the command to run when the target is to be re-made. Note that this line is the same as those used to run gcc directly from the command line.

From now on, if we edit the file hellopi, we can save on having to retype 'gcc -o…' and now need only to simply type 'make'. By adding extra dependencies to all:, and writing the rules for the new target, make is able to compile multiple executables from multiple source files at once.

Exercise 2: hellopi

  1. Using the documentation in section 'GS.2 Navigating the terminal' find your way to the examplesources/RPii_C-1/ExampleSources directory. Use the ls command to find out which documents exist in this directory.
  2. Open the GUI file manager and also navigate into the hellopi folder. Move the windows so that you can see both the GUI file manager and the terminal at the same time. The two documents you see should match the names of the documents which were returned when using ls from the terminal in 1.
  3. You can choose to open the files in either of 2 ways:
    • If you want to use the gedit text editor, type into the command line:
      gedit hellopi.c &
      If you are using another editor, type editorname hellopi.c
      If you wanted to create a new file, you can type
      gedit newfile.c &
      and gedit will create a new file with the name 'newfile.c' in the current working directory.
    • Alternatively, you can double click on a file in the file manager, or right click and choose another editor of choice via the 'open with' option.
    Try both; learning the first method can help speed up your work in the future.
  4. Using the explanation in section C.1.3, spend some time thinking about what each line does. Write a comment after each line (use '//') about why each line is included in the source file and save this new file as 'myhellopi.c'.
  5. Recall from ExS.1.m that makefiles have a target for each executable file that needs compiling. Unless such a target (and relevent compiler command) are added to the makefile for myhellopi.c, this file cannot yet be made using make.
  6. The original hellopi.c file needs compiling. Use the command:
    gcc -o hellopi hellopi.c
    in the terminal to compile the program. When this is finished use the ls command again and note that a new file has been created in the directory. Alternatively, you may choose to use make; the makefile discussed in C.1.4 is in the directory already, so running make straight away will also compile the program. It will not however, compile myhellopi.c (unless we edit it).
  7. By typing ./hellopi into the command line and pressing enter, we can run the program. Our new terminal should read:
    pi@raspberry ~/examplesources/hellopi $ ./hellopi
    Hello Pi pi@raspberrypi ~/examplesources/hellopi $
  8. We are going to introduce the new line character. Including '\n' in a printf function has the same effect as pressing return/enter on the keyboard - it causes a new line. If we add this character to the end of "Hello Pi" in hellopi.c, re-compile the program and run it again, then this time our terminal should read:
    pi@raspberry ~/examplesources/hellopi $ ./hellopi
    Hello Pi
    pi@raspberry ~/examplesources/hellopi $
  9. Finally, we will add our second file, myhellopi.c to the makefile. This means that if we change either of hellopi.c or myhellopi.c, then running make will re-compile either of the programs if they have had their source files altered. First, we want to make sure that we can tell if the code has re-compiled, so in myhellopi.c, change Hello Pi, to Hello Pi version 1. So that make will compile both source files, we need to add another target to the makefile. We must add myhellopi as a dependency to 'all:', and then write some lines which tell the makefile how to make the new target. By copying the format of the hellopi target, add a few more lines to the makefile so that runningmake will re-compile both myhellopi.c and hellopi.c should they be changed. Test your makefile by changing the version number in myhellopi.c, running make and checking if this new number is printed to the terminal when the program is run. Dont forget to re-read ExS.1.m just above if you get stuck.
  10. Congratulations on getting to the end of the first exercise. Now that you can quickly compile multiple source files at once, try to draw pictures in the command line using the printf function. Each time you start writing a new file, just repeat step 8 to add it to the makefile. See if you can draw a raspberry using the characters on the keyboard! (when doing this, backslashes \, and quote marks " have other meanings in the printf function, and so it is best to avoid using them). For a complete solution, compile raspberrypi.c which will be in the same directory as the example makefile and hellopi.c files.

Continue to C.2 Basic C concepts and syntax to continue this tutorial.