Developing in C for the ATmega328P: AVR_C Library Functions

11 minute read

Where I describe how to use the Library functions in AVR_C.

Introduction

For the most part, the Library functions in AVR_C will mirror those in the Arduino Framework. The README on the GitHub page for AVR_C also contains this same material.

Arduino Framework and standard C Replacement Routines

Much of the Standard C Library is provided by AVR Libc. I recommend having a link to the online manual open while developing code. The code in this repository is the code required to program the Uno using similar routines as in the Arduino Framework.

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
  • 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.
  • pinMode(pin, mode): define INPUT, OUTPUT, INPUT_PULLUP for an UNO pin (pins 0-13 only). Is not configured to use A0-A5.
  • delay(ms): Blocking delay uses Standard C built-in _delay_ms, however allows for a variable to be used as an argument.
  • 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.

Standard C I/O functions adapted for the ATmega328P

Use these standard C I/O functions instead of the Arduino Serial class. See example serialio for an example implementation. Requires the following 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();
  • 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.

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.

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 and moserial for Linux. Both make it easy to connect and re-connect and have an excellent interface.

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.

Makefile

The examples make use of a great Makefile courtesy of Elliot William’s in his book Make: AVR Programming. I highly recommend the book and used it extensively to understand how to program the ATmega328P (Arduino UNO) from scratch.

Makefile

Make Commands for Examples

# simple command to check syntax, similar to Verify in the Arduino IDE
make
# command to compile/link/load, similar to Upload in the Arduino IDE
make flash
# command to show the size of the code
make size
# command to clear out all the cruft created in compiling/linking/loading
make all_clean
# command to clear out the Library object files *file.o*, sometimes required if changes to Library files aren't appearing to work, uses LIBDIR folder as the folder to clean
make LIB_clean

To install the proper toolchain required to compile the code.

Serial Solutions

In use

In review to determine how to use due to its interrupts

  • Peter Fleury AVR Software Works, not integrated into avr-gcc library, so not native. It uses interrupts and buffering so it is fast and non-blocking.

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.

Configuring env.make

As stated above, instead of local enviromental variables, I have found it easier to maintain a top-level file called env.make, which contains all of the local customizable options. This file is added to the make process by an include at the top of file.

The file, env.make is not tracked by git and it looks like this: (*macOS SERIAL parameter)

## Microchip 328PB Xplained Mini environmental variables
MCU = atmega328pb
SERIAL = /dev/cu.usbmodem3101
F_CPU = 16000000UL  
BAUD  = 9600UL
LIBDIR = ../../Library
PROGRAMMER_TYPE = xplainedmini
PROGRAMMER_ARGS = 

As shown, this one is for the 328PB Xplained Mini board and on a Mac. For Make to work, you need to perform the following:

  1. Copy the contents above and paste them into a file called env.make
  2. The file needs to sit at the top level, the same level as this README, bloom.json and the programming folders Library and examples.
  3. Change the parameters to suit your board, for example, the Uno would need to look like this: (*macOS SERIAL parameter)
# Arduino UNO environmental variables
MCU = atmega328p
SERIAL = /dev/cu.usbmodem3101
F_CPU = 16000000UL  
BAUD  = 9600UL
LIBDIR = ../../Library
PROGRAMMER_TYPE = Arduino
PROGRAMMER_ARGS = -F -V -P $(SERIAL) -b 115200

I’ve found it best to include full sections per board, then comment/uncomment a section based on the board I’m using. A full version of the env.make file I’m using is below.

Note: This repository has the new version of Makefiles which uses this file, so no other changes are needed.

The nice part about this change, is once the variables have been updated for your system, you no longer have to do special programmer types such as make flash_snap or make flash_xplain, as make flash will be automatically updated for your specific programmer. (Provided you give it the right parameters.)

Here is an env.make with 3 sections, one for each board to be used. Notice that only one section is active at a time, the other two have been commented out.

Full version of the env.make file I am using:

# This file contains the environmental variables to compile/link/load AVR_C
# Only one section may be used at a time, each section describes a specific board
# Comment out the sections which won't be used

## Microchip 328PB Xplained Mini environmental variables
# MCU = atmega328pb
# SERIAL = /dev/cu.usbmodem3101
# F_CPU = 16000000UL  
# BAUD  = 9600UL
# LIBDIR = ../../Library
# PROGRAMMER_TYPE = xplainedmini
# PROGRAMMER_ARGS = 

## Microchip 168PB Xplained Mini environmental variables
MCU = atmega168pb
SERIAL = /dev/cu.usbmodem3101
F_CPU = 16000000UL  
BAUD  = 9600UL
LIBDIR = ../../Library
PROGRAMMER_TYPE = xplainedmini
PROGRAMMER_ARGS = 

# Arduino UNO environmental variables
# MCU = atmega328p
# SERIAL = /dev/cu.usbmodem3101
# F_CPU = 16000000UL  
# BAUD  = 9600UL
# LIBDIR = ../../Library
# PROGRAMMER_TYPE = Arduino
# PROGRAMMER_ARGS = -F -V -P $(SERIAL) -b 115200

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.