Appendix B — Programming in C – from nothing to software

Author
Affiliation

Ben Clifford

Welsh Centre for Printing and Coatings

B.1 Introduction

This part provides a detailed worked example of writing code in C language to run on the Atmel ATmega328 microcontroller, including why each line of code is written and where any additional information comes from and how it can be found.

These pages start with a description of some desired functionality written in the form of a task to be completed.

The sections go through the thought process a programmer should follow to plan and ultimately write the software to achieve this task. The first version is focussed on using the Arduino IDE with the predefined functions such as pinMode() and digitalWrite() before looking at writing the same program in pure C.

Contents

B.2 The Task

B.2.1 Project Brief

Using the Arduino Nano board which contains an Atmel ATMega 328 Microcontroller; write a program so that every time a push button is pressed, an analogue signal (such as the output of a sensor) is read and the value of the voltage as a proportion of the maximum voltage displayed on a bank of LEDs.

This task is similar to the task defined for Experiment 3: Analogue to Digital Conversion but it is tackled in a slightly different way.

B.3 Understanding the task and first thoughts

B.3.1 Understanding the Brief

The first step in writing any program is ensuring you understand the task or brief, the hardware that will be involved and what the outputs/results should be.

B.3.2 The Task

The program written needs to detect when a push button is pressed, at each button press the voltage from an analogue signal needs to be read. Once the signal voltage has been read, the value must be processed and the value displayed visually on a bank of LEDs.

B.3.3 The Hardware

In this task, there are three pieces of external hardware involved, an analogue signal from a sensor, a push button switch and a bank of LEDs.

  • The analogue input – this can be created using a potentiometer connected between the supply voltage and ground with the centre tap connected to an input on the microcontroller1. For the built in ADC of the Atmel ATmega328 this is 5V.
  • The push button switch – this can be connected to ground on one side of the switch and to a microcontroller input on the other2.
  • The bank of LEDs (with current limiting resistors) – for simplicity it is easiest to use 8 LEDs connected to the 8 pins of a single port. Since each port is linked to an output register which is 8 bits wide (PORTx) this will make it simpler to program the output part of the task.

B.3.4 First thoughts on Implementation

B.3.4.1 Push Button

  • The pin that the push button will be connected to will need to be configured as an input.
  • The pull-up resistor for this pin will also need to be enabled.
    • This will be a digital signal with a value of 5V or HIGH when the button is not pressed and a value of 0V or LOW when the button is pressed

B.3.4.2 Analogue Input

  • The pin that the analogue input is connected to will need to be one that is also connected to the ADC.
  • The pin will also need to be configured as an input.
  • The ADC registers will need to be configured as required.

B.3.4.3 LEDs

  • The pins that the LEDs are connected to will need to be configured as outputs.
  • The default state of the LEDs should be off or LOW
  • As the task states the value shown by the LEDs should represent the value of the analogue input relative to the maximum voltage, this will be easier to Implement in software if all of the LEDs are on the same port.

B.4 Wiring up the Circuit

Referencing the pinout of the Arduino nano in Figure B.1 below, the programmer must first identify where each of the three hardware elements will be connected, ensuring that the pins/ports meet the requirements.

  • For the push button sw -ltch, this will be a digital signal (on/off) so can be connected to any GPIO pin.
    • Looking at the other hardware requirements, this should avoid the pins connected to the ADC and leave a full port of input pins for the LEDs. For this example, connection D8 of the nano board is a convenient pin to use, this is connected to PB0, i.e. Port B bit 03.
  • For the analogue input, this can be connected to any of the GPIO pins which is also connected to the ADC.
    • Looking at Figure B.1, these connections are labelled A0A7 on the nano board. Connection A0 is connected to PC0, i.e. Port C bit 0 which is also connected to ADC[0] – ADC channel 0.4
  • The LEDs can be connected to any digital GPIO pin, but ideally will be 8 pins on the same port.
    • Looking at Figure B.1 and the two pins we have used above, connections D0D7 can be used which corresponds to Port D bits 0-75.

A possible assembly is illustrated in Figure B.26

Arduino Nano Pinout.
Figure B.1: Arduino Nano Pinout - Arduino Nano links.
A photograph of circuit assembled on breadboard.
Figure B.2: The circuit assembled on breadboard

B.5 Using the Arduino Integrated Development Environment

Initially this guide will look at programming this task using the Arduino Integrated Development Environment (IDE) with the predefined functions detailed in the Arduino Language Reference.

B.5.1 Step 1

Open the Arduino software and set up the IDE by selecting which device is being used and which communication interface it is connected to. Within the “Tools” menu option, locate the menu item “Board” and select Arduino Nano, also in the “Tools” menu select the port that the Arduino Nano is connected to (COMx) under the “Port” menu item (note the Arduino must be connected to do this)7. After setting these parameters , under the “Tools” menu the “Get Board Info” menu item can be pressed which will attempt to communicate with the Arduino board and get the board name and serial number of the USB communication chip if successful.

B.5.2 Step 2

With the IDE now setup, we can begin writing the code that will be uploaded to run on the microcontroller. By default, when a new file is started in the Arduino IDE, there are two existing function blocks as shown below and in Figure B.3.

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}
A screenshot of the Arduino IDE when first opened showing the 'setup' and 'loop' functions.
Figure B.3: A screenshot of the Arduino IDE when first opened showing the setup and loop functions.

B.5.2.1 Setup function

The first of these functions is called “setup” and takes no input arguments and has no return type indicated by the empty brackets and void data type. This function is where the programmer can put any code that only needs to be run once, examples include USB communication setup and port data direction configuration.

B.5.2.2 Loop function

The second function is called “loop” and takes no input arguments and has no return type as above. This function is where the bulk of the code is written and as its name suggests will repeatedly execute the code inside the function block.

B.6 Setting up I/O connections

B.6.1 Step 3

Within the Arduino IDE, several library files containing useful definitions and functions are automatically included. One of these files contains a map of the memory address of each port register with masks that allow access to each bit/pin individually to labels that are the same as the connection label printed on the Arduino nano circuit board. Whilst this is convenient, it is good practice to define variables with more practical names which represent what is connected to them that the programmer can use throughout the program. A variable must be declared before it is used within the program and there are three different ways to achieve this:

  1. Declare the variable at the start of the program before void setup (this is the best option)
  2. Declare the variable at the start of the function (in this case, at the top of void loop)
  3. Declare the variable inline.

As a minimum, the variable declaration must specify the data type and a name to be used, however this can also set the initial value of the variable. In the code, a variable is declared for each I/O connection of the circuit and given the value of the pin connection as follows:

B.6.1.1 Variable definitions

//Pin Definitions
const int pushButton = 8;
const int inputSignal = A0;
const int ledPin0 = 0;
const int ledPin1 = 1;
const int ledPin2 = 2;
const int ledPin3 = 3;
const int ledPin4 = 4;
const int ledPin5 = 5;
const int ledPin6 = 6;
const int ledPin7 = 7;

For example, the first of these declarations sets up the label “pushButton” in place of the value 8, this allows the programmer to use “pushButton” instead of the pin number when an operation needs to be performed.

Note: In the above variable definitions the qualifier const is added at the start of each declaration. This tells the compiler this value is a constant and essentially makes it read-only8.

B.6.2 Step 4

The next step is to set up the ports/pins that the external hardware is connected to including the data direction or mode and the default state of any outputs. 

When setting up the ports/pins, first set the data direction using the pre-defined Arduino function “pinMode”. This function takes two input arguments, the first is the connection label and the second is the mode itself. The syntax of this function is:

pinMode(pin, mode)

where mode can be INPUT, OUTPUT, or INPUT_PULLUP. More information on this function can be found in the pinMode section of the Arduino reference library.

Implementing the setup we described Section B.4 and using the names defined in Section B.6.1.1, the data direction/pinMode for the connections can be written as follows:

pinMode(pushButton, INPUT_PULLUP); // Default value is high, pressed value is low
pinMode(inputSignal, INPUT);
pinMode(ledPin0, OUTPUT);
pinMode(ledPin1, OUTPUT);
pinMode(ledPin2, OUTPUT);
pinMode(ledPin3, OUTPUT);
pinMode(ledPin4, OUTPUT);
pinMode(ledPin5, OUTPUT);
pinMode(ledPin6, OUTPUT);
pinMode(ledPin7, OUTPUT);

The default state of the LEDs can be set using the pre-defined Arduino function “digitalWrite”. This function takes two input arguments, the first is the connection label and the second is the value to set. The syntax of this function is:

digitalWrite(pin, value)

where value can be HIGH or LOW. More information on this function can be found in the digitalWrite section of the Arduino reference library.

Implementing the setup we described in Section B.4 and using the names defined in Section B.6.1.1, the code to set the initial state of the LEDs to off can be written as follows:

//Set the initial state of the LEDs to be off
digitalWrite(ledPin0, LOW);
digitalWrite(ledPin1, LOW);
digitalWrite(ledPin2, LOW);
digitalWrite(ledPin3, LOW);
digitalWrite(ledPin4, LOW);
digitalWrite(ledPin5, LOW);
digitalWrite(ledPin6, LOW);
digitalWrite(ledPin7, LOW);

B.7 Detecting and reacting to a button press

B.7.1 Step 5 - Detecting the Button Press

Now the direction/mode for each pin and the initial state of the output pins has been set, the main program code can be written within the loop function. Looking back at the Section B.4

The program written needs to detect when a push button is pressed…“,

the first section of code within the loop function needs to check if the button has been pressed. This is achieved by reading the state of the pin and comparing against 0 or LOW, 0 is used since the pull-resistor for this pin has been enabled meaning the default value (no button press) seen at the input is 1 or HIGH.

To achieve this functionality in code, an if statement is used - The if statement allows branching within code and can be used to check if a particular condition has been met. If the condition has been met, the set of statements within the code block (parentheses) is executed, if the condition is not met then the statements within the code block are not executed and the program will branch over to the next statement outside of the if statement. The syntax of an if statement is

if (condition is met) {
  //statements to be run
}

To read the current state of the pin, the pre-defined Arduino function “digitalRead” can be used. This function takes the connection label as an argument. The syntax of this function is:

digitalRead(pin)

More information on this function can be found in the digitalRead section of the Arduino reference library.

To check if the push button has been pressed, the equal to (==) comparison operator is used. This operator compares the value on the left with the value on the right and returns true when the two operands are equal.

By putting these three components together, and using the value LOW (or 0) on the right hand side of the “equal to” comparison operator, the following code checks whether the push button connected to D8 has been pressed:

if (digitalRead(pushButton) == LOW) {
    
}

B.7.2 Step 6 - Reading the Analog Input

The next step in this program is to write the code that can read the analogue signal and store the converted value into a variable. There are 2 parts to this step, the first is to declare a variable that the result will be stored in, and second, to read the analogue signal and assign the result to the declared variable. A variable must be declared before it is used and there are three different ways to achieve this:

  1. Declare the variable at the start of the program before void setup (this is the best option)
  2. Declare the variable at the start of the function (in this case, at the top of void loop)
  3. Declare the variable inline.

As a minimum, the variable declaration must specify the data type and a name to be used, however this can also set the initial value of the variable. In code, we can declare a variable with the name sensorValue as follows:

//Variable declarations
int sensorValue = 0;

Now the variable has been declared, the programmer/code can assign values to it and/or change its value. In this example, the programmer needs to read the value of the analogue input signal every time the push button is pressed. This means the next bit of code that is added needs to be within the code block of the button press detection.

To read the current value at an analogue pin, the pre-defined Arduino function “analogRead” can be used. This function takes the connection label as an argument. The syntax of this function is:

analogRead(pin)

More information on this function can be found in the analogRead section of the Arduino reference library.

Adding this component to our existing code which checks whether the push button connected to D8 has been pressed, the code becomes:

if (digitalRead(pushButton) == LOW) {
    // read the value from the sensor and store the result in sensorValue variable
    sensorValue = analogRead(inputSignal);
}

In simple terms, each time the push button connected to D8 is pressed, the value of the input signal at A0 is read and stored in the variable sensorValue.

B.7.3 Step 7 - Outputting the value of sensorValue to the LEDs

The initial task asked for a program that outputs the value of the analogue input signal as a proportion of the maximum value on a series of LEDs. In this application, 8 LEDs are connected to PORT D and variable names ledPin0ledPin7 have been declared. At this stage, the programmer needs to work out the relationship between the analogue input signal and each LED. The analogue input signal is being processed by the built in Analog to Digital Converter or ADC of the ATmega 328 microcontroller which has a 10 bit resolution meaning the digital output will be an integer value in the range of 0:1023 (\(2^{10}-1\)). Dividing the maximum digital value by the number of LEDs:

\[\mathrm{EachLED} = \frac{1023}{8} = 127.875\]

Now that the relationship between the output of the ADC and the LEDs is known the programmer can create the flow control statements to implement the output. This can be achieved by using an if, else-if statement with 8 test conditions as follows9:

if (sensorValue <= 127) {

} else if (sensorValue <= 255) {

} else if (sensorValue <= 383) {

} else if (sensorValue <= 511) {

} else if (sensorValue <= 639) {

} else if (sensorValue <= 767) {

} else if (sensorValue <= 895) {

} else if (sensorValue <= 1023) {

}

Finally, the programmer must populate each of these if statement blocks with the code to turn the correct LEDs on/off. Using the Arduino pre-defined functions there isn’t an elegant way to manipulate the state of the whole port meaning each of the above test cases will need 8 digitalWrite statements. The first test condition, should only switch the first LED on and as such is written as:

if (sensorValue <= 127) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, LOW);
      digitalWrite(ledPin2, LOW);
      digitalWrite(ledPin3, LOW);
      digitalWrite(ledPin4, LOW);
      digitalWrite(ledPin5, LOW);
      digitalWrite(ledPin6, LOW);
      digitalWrite(ledPin7, LOW);
}

Once each of the test conditions has been populated with digitalWrite statements the code is complete and can uploaded to the Arduino Nano board.

B.8 Code Listing

The complete code listing for the example program is given in Listing B.1.

Listing B.1: An Example C Program
//Pin Defintions
const int pushButton = 8;
const int inputSignal = A0;
const int ledPin0 = 0;
const int ledPin1 = 1;
const int ledPin2 = 2;
const int ledPin3 = 3;
const int ledPin4 = 4;
const int ledPin5 = 5;
const int ledPin6 = 6;
const int ledPin7 = 7;

//Variable declarations
int sensorValue = 0;

void setup() {

  // put your setup code here, to run once:
  pinMode(pushButton, INPUT_PULLUP);  // Default value is high, pressed value is low
  pinMode(inputSignal, INPUT);
  pinMode(ledPin0, OUTPUT);
  pinMode(ledPin1, OUTPUT);
  pinMode(ledPin2, OUTPUT);
  pinMode(ledPin3, OUTPUT);
  pinMode(ledPin4, OUTPUT);
  pinMode(ledPin5, OUTPUT);
  pinMode(ledPin6, OUTPUT);
  pinMode(ledPin7, OUTPUT);

  //Set the initial state of the LEDs to be off
  digitalWrite(ledPin0, LOW);
  digitalWrite(ledPin1, LOW);
  digitalWrite(ledPin2, LOW);
  digitalWrite(ledPin3, LOW);
  digitalWrite(ledPin4, LOW);
  digitalWrite(ledPin5, LOW);
  digitalWrite(ledPin6, LOW);
  digitalWrite(ledPin7, LOW);
}


void loop() {
  //if statement to test if pushButton has been pressed
  //Default pushButton state is high as the pull-up resitor is enabled
  if (digitalRead(pushButton) == LOW) {
    // read the value from the sensor ans store the result in sensorValue variable
    sensorValue = analogRead(inputSignal);

    // Write to the LEDS
    if (sensorValue <= 127) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, LOW);
      digitalWrite(ledPin2, LOW);
      digitalWrite(ledPin3, LOW);
      digitalWrite(ledPin4, LOW);
      digitalWrite(ledPin5, LOW);
      digitalWrite(ledPin6, LOW);
      digitalWrite(ledPin7, LOW);
    } else if (sensorValue <= 255) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, HIGH);
      digitalWrite(ledPin2, LOW);
      digitalWrite(ledPin3, LOW);
      digitalWrite(ledPin4, LOW);
      digitalWrite(ledPin5, LOW);
      digitalWrite(ledPin6, LOW);
      digitalWrite(ledPin7, LOW);
    } else if (sensorValue <= 383) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, HIGH);
      digitalWrite(ledPin2, HIGH);
      digitalWrite(ledPin3, LOW);
      digitalWrite(ledPin4, LOW);
      digitalWrite(ledPin5, LOW);
      digitalWrite(ledPin6, LOW);
      digitalWrite(ledPin7, LOW);
    } else if (sensorValue <= 511) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, HIGH);
      digitalWrite(ledPin2, HIGH);
      digitalWrite(ledPin3, HIGH);
      digitalWrite(ledPin4, LOW);
      digitalWrite(ledPin5, LOW);
      digitalWrite(ledPin6, LOW);
      digitalWrite(ledPin7, LOW);
    } else if (sensorValue <= 639) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, HIGH);
      digitalWrite(ledPin2, HIGH);
      digitalWrite(ledPin3, HIGH);
      digitalWrite(ledPin4, HIGH);
      digitalWrite(ledPin5, LOW);
      digitalWrite(ledPin6, LOW);
      digitalWrite(ledPin7, LOW);
    } else if (sensorValue <= 767) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, HIGH);
      digitalWrite(ledPin2, HIGH);
      digitalWrite(ledPin3, HIGH);
      digitalWrite(ledPin4, HIGH);
      digitalWrite(ledPin5, HIGH);
      digitalWrite(ledPin6, LOW);
      digitalWrite(ledPin7, LOW);
    } else if (sensorValue <= 895) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, HIGH);
      digitalWrite(ledPin2, HIGH);
      digitalWrite(ledPin3, HIGH);
      digitalWrite(ledPin4, HIGH);
      digitalWrite(ledPin5, HIGH);
      digitalWrite(ledPin6, HIGH);
      digitalWrite(ledPin7, LOW);
    } else if (sensorValue <= 1023) {
      digitalWrite(ledPin0, HIGH);
      digitalWrite(ledPin1, HIGH);
      digitalWrite(ledPin2, HIGH);
      digitalWrite(ledPin3, HIGH);
      digitalWrite(ledPin4, HIGH);
      digitalWrite(ledPin5, HIGH);
      digitalWrite(ledPin6, HIGH);
      digitalWrite(ledPin7, HIGH);
    }
  }
}

The complete code listing can be downloaded as a GitHub gist example.ino.


  1. When considering the physical set up of this the programmer/engineer must ensure that the maximum possible value seen at the microcontroller input is less than or equal to the reference voltage of our Analogue to Digital Converter (ADC) to prevent an overvoltage.↩︎

  2. In order to clearly detect the button press and avoid floating voltages the programmer needs to ensure the pull up resistor for the specific pin is enabled (or that this has been implemented separately in hardware).↩︎

  3. the programmer could also use D9, D10, D11, D12 etc.↩︎

  4. the programmer could also use A1, A2, A3, A4, etc.)↩︎

  5. pins across multiple ports can be used but this will make the programming more complicated.↩︎

  6. When building this circuit, it is advised that the red wire from the 5V output of the Arduino nano board to the bottom rail of the breadboard is replaced with a 220 Ohm resistor, this will protect the USB circuitry within the PC in the event of any wiring faults/short circuits between the rails.↩︎

  7. The IDE set-up instructions given here are for windows. Additional hardware specific intructions are given in {ref}set_up_arduino and in the Platform specific guides. :w↩︎

  8. If a value will not change, it can be stored as part of the program in read-only-memory. This will release a small amount of space in working memory which is often limited in a microcontroller.↩︎

  9. To avoid using floating point aritmetic, we have rounded \(127.875\) to \(127\). If you wanted to be really accurate, you could add the full value and round afterwards. For example \(2\times 127.875 = 256\); \(3\times 127.875 = 384\). Indeed it is arguable that \(127.875\) is closer to \(128\) than \(127\)!↩︎

Copyright © 2021-2024 Swansea University. All rights reserved.