Developing in C for the ATmega328P: Using Data Types and Math

6 minute read

Where I describe the impact of data types on math on an AVR microcontroller (ATmega328P) and pitfalls to avoid.

Introduction

While working on the mapping entry I ran into a problem with the mapping function. The function wasn’t returning correct values. I solved it by changing the data types and examining the results, after a couple of attempts, it was clear the mapping function requires a long data type for all of its calculations. This issue got me thinking about the impact of data types on complex math functions using an embedded microcontroller.

When writing a program for a personal computer, variable memory capacity isn’t a typical concern. Programs which have 20-30 variables, all created as double data types (8 bytes of storage each), would consume an infestimally small percentage of storage of a 2GB RAM personal computer. This issue changes dramatically on an embedded microcontroller for several reasons. The most obvious is the amount of RAM available on an embedded microcontroller could be as small as 512 bytes or 2KB on an Uno. The next issue is speed of execution, as the AVR family of processors is designed to work on single byte (1 clock cycle) or 2 byte (2 clock cycles), multi-byte data slows the microcontroller dramatically. And finally, for most situations, there isn’t a need for large data. The PWM duty cycle can be at most 2 bytes, the ADC is only 10 bits and the data paths can only be 16 bits. This means calculations which require 4 or 8 bytes are needlessly inefficient.

Understanding Data Types

In most C language books, the descriptions of data types include the following, char, int, float, double, short, long and possibly boolean. With the standard, C99, the following types were introduced to reduce the confusion as to size of a variable:

Data type C99 Data type largest value
signed char int8_t 127
unsigned char uint8_t 255
signed int int16_t 32,767
unsigned int uint16_t 65,535
signed long int int32_t 2,147,483,648
unsigned long int uint32_t 4,294,967,296
signed long long int int64_t 9.223372037E18
unsigned long long int uint64_t 1.844674407E19
float (4 bytes) float 4,294,967,296
double (8 bytes) double 1.844674407E19

When writing for the AVR family, you can use either the data type int, char or the C99 standard data type int, uint8_t, however I recommend using the latter as it will remind you of the size of the data type.

Examples

Digital I/O

When you are using commands such as digitalRead() and digitalWrite(), you are directly addressing I/O registers on the ATmega328P. Specifically, with digitalRead(), you are reading one of the two input ports and with digitalWrite(), you are writing to one of the two output ports. This makes it important to use a uint8_t for your data type. It ensures you are only using one byte and it is unsigned so you are able to use the top most bit for data. For the same reasons, pin data type must also need be a uint8_t.

This advice also applies to analogWrite(), as the function is looking for a value between 0 - 255 for the duty cycle.

Usage:

uint8_t digital_value
uint8_t digital_pin = 5;
pinMode(digital_pin, OUTPUT);
digital_value = digitalRead(digital_pin);

Analog to Digital Converter (ADC)

The ATmega328P microcontroller has 10-bit ADC registers, which means you will need to use a 16-bit data type. I prefer to use unsigned, which means I would use a uint16_t for the data, while I would need to use an uint8_t for the pin address.

Usage

uint16_t analog_value;
uint8_t analog_pin = A0;
pinMode(analog_pin, INPUT);
analog_value = analogRead(digital_pin);

Performing Calculations

Determining the data type for a specific calculation must be carefully considered. One of the most significant issues with developing code for a embedded microcontroller is that there are very few, if any, execution error messages. This means, the work must be performed up front to solve possible execution errors. For example, as mentioned above, I was not getting the correct results when I was developing the map() function. And it wasn’t until I made sure all of the calculations were 32-bits, was I able to properly execute the function.

Review the output below:

1
2
3
4
5
6
7
8
Testing multiplication and variable types, (execution time in usec) 
 8 bit test(  1):  23 *  26 =    86
16 bit test(  1):  23 *  26 =   598
16 bit test(  2):  23 *  23 *  26 *  26 =  29924
32 bit test(  1):  23 *  23 *  26 *  26 =    29924
32 bit test( 11): (u) 23 *  23 *  26 *  26 =   357604
32 bit test(  5): (u) 23 * ( 23 *  26 *  26) =   357604
 float test( 41): (f)  23 *  23 *  26 *  26 = 357604.00

The highlighted lines show incorrect values for the calculations:

  • Line 2: fairly obvious that 23 * 26 will result in a number greater than 255
  • Line 4: again, obvious the result will be larger than 65,535
  • Line 5: the maximum value of a 32-bit number can be 4,294,967,296, why did it fail? It failed because the intermediate result was only 16-bits.
  • Line 6: Correct result because I cast one of the operands as 32-bits, which increased the size of the intermediate results, resulting in the correct answer, note the execution time is 10X
  • Line 7: I was able to cut the execution time of Line 6 in half and retaining accurate results, by changing the execution order
  • Line 8: float delivers the correct result, with one of the operands cast as float, however notice the execution time has now increased to 657 ticks (41 microseconds) or almost 8 times slower than the correct integer calculation

Note: See this entry to understand more about timing program execution.

Float Inaccuracies

As slow as float can be, it may also be inaccurate. The errors aren’t significantly large, however they manifest as two issues:

  • as the numbers approach 1,000,000, the calculated values begin to degrade
  • at any point in time, comparing two floats might not provide consistent results

Here’s a table of powers of two by 32-bit integer, float using successive multiplications and float using the pow() function:

i 32-bit unsigned 32-bit signed float pow(flt,i)
1 2 2 2.0 2.0
2 4 4 4.0 4.0
3 8 8 8.0 8.0
4 16 16 16.0 16.0
5 32 32 32.0 32.0
6 64 64 64.0 64.0
7 128 128 128.0 128.0
8 256 256 256.0 256.0
9 512 512 512.0 512.0
10 1024 1024 1024.0 1024.0
11 2048 2048 2048.0 2048.0
12 4096 4096 4096.0 4096.0
13 8192 8192 8192.0 8192.0
14 16384 16384 16384.0 16384.0
15 32768 32768 32768.0 32768.0
16 65536 65536 65536.0 65536.0
17 131072 131072 131072.0 131072.0
18 262144 262144 262144.0 262143.7
19 524288 524288 524288.0 524287.3
20 1048576 1048576 1048576.0 1048574.6
21 2097152 2097152 2097152.0 2097152.0
22 4194304 4194304 4194304.0 4194298.5
23 8388608 8388608 8388608.0 8388597.0
24 16777216 16777216 16777216.0 16777194.0
25 33554432 33554432 33554432.0 33554432.0
26 67108864 67108864 67108864.0 67108864.0
27 134217728 134217728 134217730.0 134217550.0
28 268435456 268435456 268435460.0 268435100.0
29 536870912 536870912 536870910.0 536870910.0
30 1073741824 1073741824 1073741800.0 1073740400.0
31 2147483648 -2147483648 2147483600.0 2147480800.0
32 0 0 4294967300.0 4294961700.0

Note the line at 18 (2 ^ 18), the pow() function begins to have inaccuracies as well as the successive multiplications have inaccuracies starting at line 27. The good news is that in embedded development, one rarely requires numbers great than 1,000,000. It is important to note that the numbers won’t be exact, which means you do not want to make equality comparisons.

Comments powered by Talkyard.