Developing in C for the ATmega328P: Using Data Types and Math
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:
|
|
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.