Appendix B — Programming in C – from nothing to software
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.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 toPB0
, i.e. Port B bit 03.
- 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
- 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
A0
–A7
on the nano board. ConnectionA0
is connected toPC0
, i.e. Port C bit 0 which is also connected to ADC[0] – ADC channel 0.4
- Looking at Figure B.1, these connections are labelled
- 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
D0
–D7
can be used which corresponds to Port D bits 0-75.
- Looking at Figure B.1 and the two pins we have used above, connections
A possible assembly is illustrated in Figure B.26
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:
}
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:
- Declare the variable at the start of the program before
void setup
(this is the best option) - Declare the variable at the start of the function (in this case, at the top of
void loop
) - 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:
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:
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:
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
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:
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:
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:
- Declare the variable at the start of the program before void setup (this is the best option)
- Declare the variable at the start of the function (in this case, at the top of void loop)
- 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:
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:
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 ledPin0
– ledPin7
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.
The complete code listing can be downloaded as a GitHub gist example.ino.
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.↩︎
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).↩︎
the programmer could also use
D9
,D10
,D11
,D12
etc.↩︎the programmer could also use
A1
,A2
,A3
,A4
, etc.)↩︎pins across multiple ports can be used but this will make the programming more complicated.↩︎
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.↩︎
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↩︎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.↩︎
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.