Developing in C for the ATmega328P: AVR_C Library Functions
Where I describe how to use the Library functions in AVR_C.
Introduction
This page describes where to find specific functions for programming an AVR microcontroller in Standard C using functions similar to the Arduino Language Reference. In this case, the AVR_C framework consists of:
- The Standard C Library also known as avr-libc
- The AVR_C Library, a GitHub repository developed to mirror the functionality of the Arduino Language
Standard C Library
The Standard C Library is provided by AVR Libc. This library is incredibly important as it provides the ability to develop code in Standard C for the AVR family of microcontrollers. It has all of the functionality you would expect if your were using the a Standard C book such as The C Programming Language Kernighan and Ritchie (available in PDF and paper copy). I recommend having a link to the online manual open while developing code.
This library provides important functionality missing from the Arduino Language such as printf and scanf. It also provides functions specific to the AVR such as bit setting (_BV), interacting with program memory (progmem) and the watchdog timer (wdt).
AVR C Library
The code in this repository is the code required to program the Uno using similar routines as in the Arduino Language such as analogWrite, pinMode, and digitalWrite. It also provides additional functionality not found in the standard Arduino Language such as input line parsing (readline).
The repository contains both the Library and examples for each of the functions in the Library. I strongly recommend reviewing the example for the function if the usage is unclear.
Arduino Framework Functions
Each function used requires an #include in order to be used (example):
#include "functionname.h" /* format of include */
#include "analogRead.h" /* for example to use analogRead() */
#include "unolib.h" /* add this file for general definitions */
This keeps the code smaller than with a large file containing all of the functions available.
Arduino Framework Functions
- analogRead(pin): read one of the 6 Analog pins (A0-A5). Returns a 10-bit value in reference to AREF see analogReference(). In this case, it only DEFAULT value of VCC or 5V. To convert reading to voltage, multiply by 0.0048 (for a reference voltage of 5V).
- analogWrite(pin, n): setup the Timer/Counters to provide a PWM signal. Keep in mind, PWM using the Timer/Counters, see this AVR Datasheet Note: PWM as to which pin, a Timer/Counters is assigned. The examples such as button (T/C 2) and micros (T/C 1) also use the same Timer/Counters, so the conflict might be an issue.
- pin = Arduino UNO Pin Number, must have a “~” in its name (3, 5, 6, 9, 10, 11)
- n = n/255 Duty Cycle, i.e; n=127, 127/255 ~= 50% duty cycle
- Pin PWM Frequencies
- UNO pin 3/PD3, 488.3Hz
- UNO pin 5/PD5, 976.6Hz
- UNO pin 6/PD6, 976.6Hz
- UNO pin 9/PB1, 976.6Hz
- UNO pin 10/PB2, 976.6Hz
- UNO pin 11/PB3, 488.3Hz
- delay(ms): Blocking delay uses Standard C built-in _delay_ms, however allows for a variable to be used as an argument.
- digitalRead(pin): returns value (1 or 0) of Uno pin (pins 0-13 only). If using serial I/O (printf/puts/getchar) then Uno pins 0 and 1 are not usable. digitalRead() is not configured to use A0-A5.
- digitalWrite(pin, level): set an UNO pin to HIGH, LOW or TOG (pins 0-13 only). If using serial I/O (printf/puts/getchar) then Uno pins 0 and 1 are not usable. This version also adds TOG, which toggles the level. Much easier than checking the level and setting it to be the opposite level and requires less code. digitalWrite() is not configured to use A0-A5.
- map(value, fromLow, fromHigh, toLow, toHigh) Re-maps a number from one range to another.
- millis(): Returns a long int containing the current millisecond tick count. Review the millis example to understand how to use it. millis() uses sys_clock_2, which is a system clock configured using Timer/Counter 2.
- pinMode(pin, mode): define INPUT, OUTPUT, INPUT_PULLUP for an UNO pin (pins 0-13 only). Is not configured to use A0-A5.
- servo() Control up to 6 servos.
- tone (pin, note, duration) - play a musical frequency on a pin for a duration
- tone_on (pin, note)- play a musical frequency on a pin
Standard C I/O functions adapted for the ATmega328P
The Arduino uses the Serial class to interact with the serial port. I have found it to be confusing, and difficult to use. The Standard C functions such as printf are far more easy to use and there is significant documentation as to how to use them. See the examples starting with serialio_ for implementation. All of them will require the following lines in the file:
# in the include section at the top of the file
#include "uart.h"
#include <stdio.h>
# at the top of the main function, prior to using I/O functions
init_serial();
For more information, see this page:
Standard functions
- getchar(): reads the next character from the standard input stream and returns it as type int
- putchar(char): writes a character to the standard output stream
- puts(string): print a null-terminated string to the standard output stream, ending with a newline character
- printf(format, variable list): writes to the standard output stream, the list of variables using the formatting string specified. There is considerable detail as to how to use printf below.
- scanf(format, buffer): reads a line of text, somewhat problematic
- fgets(buffer, len, stream) reads a number of characters, also problematic
Additional AVR_C functions
- readline - illustrates how to read a line of text from the serial port and parse into delimited tokens.
- enhanced printf - illustrates how to enhance or alter printf to meet very specific needs. Also explains how to add a variable parameter function in Standard C.
Added functions beyond Arduino Framework
-
buttons[i] - provides a debounced button response. Each button must attach to an Uno pin
- Requires sysclock_2(), see examples/button as to how to implement
- buttons[i].uno are the Uno pins attached to a button and like digitalRead, function will translate Uno pin to port/pin
- buttons[i].pressed indicates if the button has been pressed (true or non-zero)
- depending on the application, you might need to set buttons[i].pressed to zero, following a successful press, if you depend on a second press to change state. Otherwise, you’ll have a race condition where one press is counted as two presses (its not a bounce, its a fast read in a state machine)
-
user-defined button RESET - as debugWIRE uses the ~RESET pin for communication, it is valuable to define another pin to use as a RESET pin. It is performed using this method. There is a environmental variable SOFT_RESET in env.make which needs to be set (=1) to enable a user-defined button to reset the board. It was done this way because the ATmega328PB XPLAINED MINI board has an on-board user defined push button on PB7. The reset routine will debounce the button. To use the reset, the routine requires an include of sysclock.h and an init_sysclock_2(). Three examples already have reset enabled, button, **millis, and analogRead. Additional variables to set are in unolib.h: (I recommend not changing the mask, unless you are experiencing significant debounce issues. The button pin needs to be expressed as pin on port B and with a pin number as shown.)
#if SOFT_RESET
#define RESET_BUTTON PB7
#define RESET_MASK 0b11000111
#endif
- Random number generation - using Mersenne Twister, TinyMT32, 32-bit unsigned integers can be created.. There is are two test routines, tinymt, which demonstrates how to setup and use it as well as rand_test, which compares the execution time of tinymt to random(). It appears that rand() is 4 times faster than tinyMT, however, I haven’t checked the “randomness” of the two routines.
Multi-tasking
There are six examples of multi-tasking in the examples folder. Two are 3rd party code which I added for consideration as multitasking models. And the remaining four are a development, which I document in greater detail here.
In a nutshell:
- multifunction is the first iteration, simply a proof of concept that the “single-line scheduler” works.
- multi_Ard takes the previous code example, which is fast and simplifies it using Arduino-type calls (pinMode and digitialWrite) for easier integration
- multi-array moves away from a separate function framework to a common function using an array to multitask
- multi_struct uses a similar approach as the previous code, however uses a struct to provide fully overlapping multi-tasking
Examples
analogRead:
Demo file for using analogRead(), requires a pot to be setup with outerpins to GND and 5V. Then connect center pin to one of A0-A5, adjust pot to see value chance in a serial monitor.
analogWrite:
Demo file for using analogWrite(), requires a scope (Labrador used) to see the output of the PWM signal
button:
Demo file for using debounced buttons, requires a button attached to a Uno digital pin with INPUT_PULLUP. buttons[i].pressed provides a indication of the button pressed.
blink:
Minimal blink sketch. Intended as a minimal test program while working on code, it doesn’t use the AVR_C Library.
digitalRead:
Uses loops to go through each digital pin (2-13) and print out level on pin. Uses INPUT_PULLUP, so pin needs to be grounded to show 0, otherwise it will be a 1.
durationTest:
An inline test of playing a melody using tone(). This version is easier to test and debug than melody. See note on ISR below.
four states:
A four state finite state machine which uses 2 pushbuttons, 2 red LEDs and 1 blue LED to move through states and indicate state status. One push button is UP, which moves through the states on being pressed, and the other push button is ENTER, which enters the state and in this case, lights a blue LED with varying intensity. The LEDs indicate the state in a binary fashion.
melody:
Fundamentally, the same as the melody sketch on the Arduino website. The changes made are those required for standard C vs. the Arduino framework. See note on ISR below.
micros:
Shows an example of using micros() to demonstrate how to measure time. Micros are a form of the system time-keeping mechanism ticks. A tick is 62.5us, which means 16 ticks = 1us. Calling micros will provide a number in microseconds, however, it will rollover every 4milliseconds, so it can’t be used for measuring an event longer than 4 milliseconds without accounting for the rollover.
millis:
Shows an example of using millis() to demonstrate the effectiveness of the delay command. Prints the time delta based on using a delay(1000).
serialio:
Simple character I/O test using the UART. The USB cable is the only cable required. See note in main.c, as program won’t work with specific combinations of a board and serial monitor. Adafruit Metro 328 and minicom for example.
To work with serial communications, I recommend CoolTerm for Windows/macOS/Linux. Once I standardized on CoolTerm, all of my serial issues went away. Use it.
Note on ISR: ISR(TIMER0_OVF_vect)
The Timer0 overflow interrupt is used by tone() and sysclock_0. Each one has the ISR commented out using a #define 0. Changing the #define value from 0 to 1 will allow the ISR to be compiled into the specific routine. Be sure to change it in only one of the three routines, otherwise there will be compilation/linker errors.
Multitasking
There are four multitasking examples in the examples folder. Only one of them will be incorporated into the Library. The goal of each example is to explore the possible approaches for multitasking.
- multi_struct Based on oneline, this version uses a struct to contain the details as to the tasks to be performed. This version will be ultimately integrated into the AVR_C Library. I will continue to evolve multi_struct as I have several specific projects which require a particular version of multitasking.
- multi_Ard Based on oneline, this version incorporates digitalWrite() from the AVR_C Library.
- multi_array Based on multi_Ard, this version incorporates digitalWrite() and uses an array of tasks to perform multitasking.
- multifunction Based on oneline, this is the original version to test the limits as to how well the concept worked.
- oneline A Multitasking Kernal in One Line of code The simplest example of round robin multitasking. Only recommended as an simple illustration as to how to multitask using pointers to functions. Highest speed, smallest footprint 466 bytes, minimal scheduling.
Next Steps
Developing in C for the ATmega328P: Using a Serial Monitor
Sources
I also write about C, MicroPython and Forth programming on microcontrollers at Wellys.
Other sources of information which were helpful:
Comments powered by Talkyard.