quasiyoke

Programming Arduino Nano with AVRDUDE to drive a shift register

Arduino Nano is a pretty cheap single-board microcontroller kit. It’s very useful for a tasks like generating specific signal, programming EEPROM, etc. For example Ben Eater uses it to program EEPROM. If you want to build a great device from scratch I still recommend you STM32 but if you want to solve some problem very quickly (and perhaps dirty), you can use AVR microcontroller inside the Arduino Nano. In this article I’ll show you how to program Arduino Nano on Ubuntu to drive 74HC595 shift register.

The problem

Using shift registers is a common practice in the cases when you need a lot of low-frequency digital signals or connecting serial communication line (like USB) to a bus with a parallel interface. In this tutorial I’ll obtain 8-bit signal out of Arduino Nano using only 3 output pins. To do that I’ll use 74HC595 shift register and a breadboard.

First of all lets connect shift register to the push-buttons to take an intuition of working with it:

Here’s my breadboard:

A chip on the left is 74HC595 shift register. You see a switch and push buttons in the middle. Arduino on the right is only for obtaining the power (GND and +5V). You’re able to look at Ben Eater manually controlling his shift register here.

Using Arduino Nano to control shift register

Lets connect our microcontroller kit to the shift register. Arduino Nano's scheme (PDF, 79 KiB) may be very helpful. We’re using just two Arduino’s pins now: D12 and D13. D13 is also connected to the Arduino’s LED so we’re able to look at serial data transmission.

Install some packages to program AVR microcontrollers (if you’re using APT):

sudo apt install avr-libc avrdude binutils-avr gcc-avr srecord

You’re able to take the source code from my GitHub repo:

git clone https://github.com/quasiyoke/shift-register-arduino-nano.git
git checkout shift-register-demo

To compile your source code you’ll need to run several console commands. I’ve found a great Makefile helping in that here. I’ve customized it in several ways:

  1. changed the programming language from C++ to C (there’re some considerations for doing that),
  2. removed fuses flashing (their default configuration is OK usually),
  3. changed BAUD rate, clock frequency and MCU type according to our hardware (ATMega328P).

Please note that Makefile should be indented using tabs instead of spaces – this is very important requirement. Here’s the Makefile contents:

baud=57600
src=src/main
build=build
asset=project
avrType=atmega328
avrFreq=16000000
programmerDev=/dev/ttyUSB0
programmerType=arduino

cflags=-std=c99 -g -DF_CPU=$(avrFreq) -Wall -Os -Werror -Wextra

memoryTypes=calibration eeprom efuse flash fuse hfuse lfuse lock signature application apptable boot prodsig usersig

.PHONY: backup clean disassemble dumpelf eeprom elf flash help hex makefile object program

help:
@echo 'backup Read all known memory types from controller and write it into a file. Available memory types: $(memoryTypes)'
@echo 'clean Delete automatically created files.'
@echo 'disassemble Compile source code, then disassemble object file to mnemonics.'
@echo 'dumpelf Dump the contents of the .elf file. Useful for information purposes only.'
@echo 'eeprom Extract EEPROM data from .elf file and program the device with it.'
@echo 'elf Create $(build)/$(asset).elf'
@echo 'flash Program $(build)/$(asset).hex to controller flash memory.'
@echo 'help Show this text.'
@echo 'hex Create all hex files for flash, eeprom.'
@echo 'object Create $(build)/$(asset).o'
@echo 'program Do all programming to controller.'

#all: object elf hex

clean:
rm -f $(build)/$(asset).elf $(build)/$(asset).eeprom.hex $(build)/$(asset).flash.hex $(build)/$(asset).o $(build)/$(asset).lst
rm -df $(build)
@date

$(build):
mkdir $(build)

object: $(build)
avr-gcc $(cflags) -mmcu=$(avrType) -Wa,-ahlmns=$(build)/$(asset).lst -c -o $(build)/$(asset).o $(src).c

elf: object
avr-gcc $(cflags) -mmcu=$(avrType) -o $(build)/$(asset).elf $(build)/$(asset).o
chmod a-x $(build)/$(asset).elf 2>&1

hex: elf
avr-objcopy -j .text -j .data -O ihex $(build)/$(asset).elf $(build)/$(asset).flash.hex
avr-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" --change-section-lma .eeprom=0 -O ihex $(build)/$(asset).elf $(build)/$(asset).eeprom.hex

disassemble: elf
avr-objdump -s -j .fuse $(build)/$(asset).elf
avr-objdump -C -d $(build)/$(asset).elf 2>&1

eeprom: hex
#avrdude -p$(avrType) -c$(programmerType) -P$(programmerDev) -b$(baud) -v -U eeprom:w:$(build)/$(asset).eeprom.hex
@date

dumpelf: elf
avr-objdump -s -h $(build)/$(asset).elf

program: flash eeprom

flash: hex
avrdude -p$(avrType) -c$(programmerType) -P$(programmerDev) -b$(baud) -v -U flash:w:$(build)/$(asset).flash.hex
@date

backup:
@for memory in $(memoryTypes); do \
avrdude -p $(avrType) -c$(programmerType) -P$(programmerDev) -b$(baud) -v -U $$memory:r:./$(avrType).$$memory.hex:i; \
done

Here’s the source code from src/main.c:

#include <avr/io.h>
#include <util/delay.h>

#define BYTE_LENGTH (8)
#define LED_PIN (PB5)
#define SERIAL_OUTPUT_PIN (LED_PIN)
#define CLK_PIN (PB4)

const char MESSAGE[] = {
0b00000000,
0b10101010,
0b11111111,
0b00110011,
};

int main(void) {
DDRB = _BV(SERIAL_OUTPUT_PIN)
| _BV(CLK_PIN);

for (int i = 0; ; i = (i + 1) % sizeof(MESSAGE)) {
char valueByte = MESSAGE[i];

for (char j = BYTE_LENGTH - 1; j >= 0; --j) {
char valueBit = (valueByte >> j) & 1;
// Set serial output to `0`
PORTB &= ~(_BV(SERIAL_OUTPUT_PIN));
// Send current bit value to serial output
PORTB |= valueBit << SERIAL_OUTPUT_PIN;
// Make a rising edge of clock signal
PORTB |= _BV(CLK_PIN);
// Make a falling edge of clock signal
PORTB &= ~(_BV(CLK_PIN));
_delay_ms(250);
}
}
}

To compile and flash the program, execute:

sudo make flash

Here’s a video of this program working:

Bit manipulation

Note that the line:

PORTB |= _BV(SHIFT_REGISTER_CLK_PIN);

compiles to single assembly language instruction:

sbi 0x05, 4

This instruction does exactly what we want: sets fourth bit (PB4) of the fith I/O port (PORTB) to 1. You’re able to look at ATMega328P datasheet (PDF, 5.1 MiB, page 433) to check that.

This line:

PORTB &= ~(_BV(SHIFT_REGISTER_CLK_PIN));

compiles to single assembly instruction too:

cbi 0x05, 4

and sets PB4 to 0. That’s all thanks to AVR Libc input/output optimizations.

_BV macro refers to “bit value” and was defined in AVR Libc as follows:

#define _BV(bit) \
(1 << (bit))

Controlling 74HC595’s storage register

Usually there’s no need to see how does shift register fills in: we need ready-to-use bytes out of it. For that purpose 74HC595 has internal storage register. To control it we need to use third Arduino’s pin DD11:

Lets change our program to control storage register. Checkout shift-register-with-storage-register GIT tag or change the source code manually:

#include <avr/io.h>
#include <util/delay.h>

#define BYTE_LENGTH (8)
#define LED_PIN (PB5)
#define SERIAL_OUTPUT_PIN (LED_PIN)
#define SHIFT_REGISTER_CLK_PIN (PB4)
#define REGISTER_CLK_PIN (PB3)

const char MESSAGE[] = {
0b00000000,
0b10101010,
0b11111111,
0b00110011,
};

int main(void) {
DDRB = _BV(SERIAL_OUTPUT_PIN)
| _BV(SHIFT_REGISTER_CLK_PIN)
| _BV(REGISTER_CLK_PIN);

for (int i = 0; ; i = (i + 1) % sizeof(MESSAGE)) {
char valueByte = MESSAGE[i];
// Make a falling edge of storage register clock signal
PORTB &= ~(_BV(REGISTER_CLK_PIN));

for (char j = BYTE_LENGTH - 1; j >= 0; --j) {
char valueBit = (valueByte >> j) & 1;
// Set serial output to `0`
PORTB &= ~(_BV(SERIAL_OUTPUT_PIN));
// Send current bit value to serial output
PORTB |= valueBit << SERIAL_OUTPUT_PIN;
// Make a rising edge of shift register clock signal
PORTB |= _BV(SHIFT_REGISTER_CLK_PIN);
// Make a falling edge of shift register clock signal
PORTB &= ~(_BV(SHIFT_REGISTER_CLK_PIN));
}

// Make a rising edge of storage register clock signal
PORTB |= _BV(REGISTER_CLK_PIN);
_delay_ms(250);
}
}

This program works very well. It shows information on the LEDs byte-by-byte hiding shift register operation inside the chip. It looks more boring than a previous setup but it’s more useful in practice:

I’ve captured signals coming out of Arduino Nano using my oscilloscope:

On a channel 1 (blue) you see the storage register clock signal. On a channel 2 (yellow) you see shift register clock signal. Microcontroller pushes a byte of data into the shift register bit-by-bit with a shift register clock and outputs the whole byte on a positive edge of the storage register clock signal.

Let’s see writing a single bit into the shift register in a more detail:

On a channel 1 (blue) you see the serial data signal now. On a channel 2 (yellow) you see shift register clock signal.

Reducing I/O operations count

Input/output operations (writing to the PORTB) are pretty slow and one should reduce their count if possible. For someone who’re used to do usual programming it looks like we’re able to optimize inner cycle this way (GIT tag optimization):

for (char j = BYTE_LENGTH - 1; j >= 0; --j) {
char valueBit = (valueByte >> j) & 1;
/*
* Send current bit value to serial output and
* make rising edge of shift register clock signal
* with only one I/O operation
*/
PORTB = valueBit << SERIAL_OUTPUT_PIN
| _BV(SHIFT_REGISTER_CLK_PIN);
// Make a falling edge of shift register clock
PORTB &= ~(_BV(SHIFT_REGISTER_CLK_PIN));
}

Unfortunatelly, this won’t work. Such a program makes shift register to decode serial signal incorrectly: delayed by one bit! You won’t find any troubles inside the program if you disassemble it. The key of solving the problem lays in an oscillogram:

Natural result of joining several I/O operations into one is that serial data signal and shift register clock rising edges are simultaneous now. But SNx4HC595 datasheet (PDF, 2.1 MiB) says (on a page 7) that set-up time between serial data signal edge (SER) and shift register clock edge (SRCLK) should be at least 20 ns (30 ns in a worst case). This fact is a good reason to make one more I/O operation (checkout GIT branch master):

for (char j = BYTE_LENGTH - 1; j >= 0; --j) {
char valueBit = (valueByte >> j) & 1;
// Send current bit value to serial output
PORTB = valueBit << SERIAL_OUTPUT_PIN;
// Make rising edge of shift register clock signal
PORTB |= _BV(SHIFT_REGISTER_CLK_PIN);
// Make a falling edge of shift register clock
PORTB &= ~(_BV(SHIFT_REGISTER_CLK_PIN));
}

This time our program works correctly:

Notice that last listing differs from the first variant of the program by omitting the stage of “Setting serial output to 0“. This way we’ve reduced amount of I/O operations from 4 to 3 with keeping our setup functional.

Disassembling your object files

There’s a couple of situations when you need to look at the compiled program’s instructions to have a complete understanding of its work. You’re able to do that with AVR GCC toolkit.

First of all change permissions of build directory (because it was set to root during the flashing):

sudo chown -R $USER build

Now you’re able to disassemble your object file:

avr-objdump --source build/project.elf > build/listing.lst

The --source option intersperses initial C code in the output.

Here’s the listing’s fragment of the last version of the program:

a8: 47 e0         ldi r20, 0x07 ; 7
aa: 50 e0 ldi r21, 0x00 ; 0

for (char j = BYTE_LENGTH - 1; j >= 0; --j) {
char valueBit = (valueByte >> j) & 1;
ac: 06 2e mov r0, r22
ae: 00 0c add r0, r0
b0: 77 0b sbc r23, r23
b2: 9b 01 movw r18, r22
b4: 04 2e mov r0, r20
b6: 02 c0 rjmp .+4 ; 0xbc <main+0x26>
b8: 35 95 asr r19
ba: 27 95 ror r18
bc: 0a 94 dec r0
be: e2 f7 brpl .-8 ; 0xb8 <main+0x22>
c0: 21 70 andi r18, 0x01 ; 1
PORTB = valueBit << SERIAL_OUTPUT_PIN;
c2: 22 95 swap r18
c4: 22 0f add r18, r18
c6: 20 7e andi r18, 0xE0 ; 224
c8: 25 b9 out 0x05, r18 ; 5
PORTB |= _BV(SHIFT_REGISTER_CLK_PIN);
ca: 2c 9a sbi 0x05, 4 ; 5
PORTB &= ~(_BV(SHIFT_REGISTER_CLK_PIN));
cc: 2c 98 cbi 0x05, 4 ; 5
ce: 41 50 subi r20, 0x01 ; 1
d0: 51 09 sbc r21, r1
d2: 78 f7 brcc .-34 ; 0xb2 <main+0x1c>
}

PORTB |= _BV(REGISTER_CLK_PIN);
d4: 2b 9a sbi 0x05, 3 ; 5

You see a weird way of doing bit shift (offsets b4be) and as compiler optimizes I/O operations (offsets ca, cc, d4). Such listing is obviously may be very useful.

Conclusion

Perhaps, you’ve noticed at the oscillogram that microcontroller pushes each subsequent bit of a single byte faster and faster. The program sends most significant bits slower than less significant because of bit shifting operation. To make microcontroller shift bits for a constant time will be a good hometask for you.

I hope this post will help you to start working with AVR microcontrollers on Ubuntu.