Lab 2: Switch Debouncing
While debouncing is a relatively simple concept, it is an important and challenging task in embedded development; it illustrates the difficulty of interfacing with physical systems that produce noisy or “dirty” signals that must be filtered through software and/or hardware means to produce reliable results. In this lab, you will implement basic switch debouncing techniques in software using mixed C and Assembly programming.
Through this lab, you will also continue to set up your development environment and learn more about how larger projects are maintained and built via separate source (implementation) and header (interface) files.
Learning Vim
Throughout this class, we will be using vim to complete our work. vim is a powerful text editor that is widely used in embedded and systems programming due to its efficiency and customizability compared to other well-known development environments. It is also a common choice for embedded work since it is able to run on relatively resource constrained hardware, such as higher-performance embedded systems such as routers, building automation systems, and so on. It has also had a surprisingly large influence on user interface design across a variety of popular software packages.
Although vim is said to have a unique learning curve, the reality is that it is simply unfamiliar–most users have years of experience with non-modal editor interfaces, so vim’s modal editor interface can feel confusing and difficult at first. However, most users find very quickly that it is far more efficient and intuitive once they begin to feel comfortable with it.
In this class, we use a modern reimplementation of vim called NeoVim (nvim), which has largely replaced vim over the past few years. To start NeoVim, execute the command $ nvim
.
Note
If you want to make it so $ vim
launches nvim, you can create a link to it, $ sudo ln -s /bin/nvim /bin/vim
If you are new to vim, there is a tutorial available that explains the interface design, and introduces many of the most commonly used commands. It takes about 15-30 minutes to complete, and is part of vim itself–simply execute the command :Tutor
in a new neovim window and work through the interactive lessons.
Getting Started with Multi-Source Projects
Most important programs are divided into many source files and header files. Source files contain implementation details, while header files define interfaces.
Within a source file, objects and functions are associated with particular identifiers (e.g. main
, i
, printf
, etc.). Identifiers can have a property called linkage. When identifiers have linkage, it means that multiple declarations of that identifier refer to the same object or function. Function local variables and parameters have no linkage–if two functions each declare a variable, x
, those declarations each refer to different objects that just happen to have the name. On the other hand, all identifiers declared at file-scope have linkage–multiple declarations of the same identifier refer to the same object (and therefore must be compatible declarations, or an error would occur).
Linkage may either be internal or external. Internally linked identifiers are not visible in other source files–in other words, two declarations of the same identifier in different source files refer to different things. Externally linked identifiers, on the other hand, are visible across the entire compilation unit (program).
By default, declarations at file scope are externally linked. For example, int main();
declares a function named “main” that has external linkage. So would a file-scope declaration such as int x;
refer to an externally linked object, named “x”. Internally linked identifiers are declared with the static
storage class specifier–e.g. static int x;
would refer to an object that is confined to the current source file. Similarly, static int main()
would result in an internally-linked “main” function; this would fail to compile into an executable, since the C runtime (which is a separate source file) needs to call an externally linked “main” function.
Typically, each source file, called a translation unit, or implementation file, will define some static
functions and file-scope static
objects which are internal implementation details of that particular source file. Any components which are meant to be exposed to the rest of the program would be declared with external linkage.
Consider a simple example,
int validate_name(char *name) {
if (!name) return -1;
return validate_name_unsafe(char *name);
}
static int validate_name_unsafe(char *name) {
for (; *name; ++name) {
if (!(isalpha(*name) || *name == ' ')) return 0;
}
return 1;
}
Here, the externally linked “validate_name” method checks that name
is not a null pointer before calling the internal, unsafe function “validate_name_unsafe”, which does not check for a null pointer. This internal function is declared static, so it’s not visible in other source files. It might be reused in other functions in the above example file, in situations where the argument is known to be non-null; this efficiently skips unnecessary null pointer checks in the internal implementation. On the other hand, “validate_name” is externally linked, and could be called in other source files.
Source files are typically associated with a header file, which describes this external interface through declarations. Often times, a single header file will represent a collection of source files, each implementing only a small part of an overall program–often as little as single function. Other source files can include this header file to expose these declarations without having to manually maintain accurate declarations for objects and functions that might change over time.
A header for the above example might be a single line, which simply declares the one externally linked method:
int validate_name(char *);
When another source file #include`s this header file (e.g. `#include "name.h"
), the declaration gives the compiler enough information to make the assembly instructions to perform the call. It doesn’t yet know where the validate_name
function is, so it just leaves a placeholder value in the call
instruction, and adds a “relocation entry” into a “fix-up” table that associates the unresolved name with that particular instruction address, for later resolution. The result is called an object file, which contains both executable binary code, as well as these symbol tables. When the name.c
file is compiled, the “validate_name” identifier is added to the symbol table of the object file name.o
.
As a final step, all of the individual object files are linked together by a program called the linker. The linker combines the executable code of many object files into a single cohesive program. It also updates and combines symbol tables to reflect the translocations of code as things are stitched together. Finally, it resolves any fix-ups using the new comprehensive symbol table and combined executable code.
Working with C and Assembly
Everything mentioned above applies equally to assembly files, typically denoted with the extension .S
–except that assembly files are not compiled, since they are already in assembly. Instead, they are directly assembled into object files, ready to be linked. Assembly files are similarly associated with header files that declare their interfaces.
In order to get assembly and C code to play nice, assembly routines must follow the application binary interface (ABI) of the compiler in question. In our case, that would be avr-gcc. The ABI specifies, among other things, which registers are used to pass function argument and return values. It also specifies which registers are callee-saved and which are caller-saved. Caller-saved registers are also called clobber registers, since the function is free to modify these registers without restoring their previous values upon returning. In contrast, callee-saved registers must be preserved by the function; this usually manifests as pushing all the callee-saved registers to the top of the stack when entering a function, and later popping them from the stack back into the registers that previously held those values.
The avr-gcc ABI specification can be found here
Read through the calling convention, and then review the following function,
.globl lpm
.type lpm, @function
lpm:
mov r31,r25
mov r30,r24
lpm r24, Z
ret
.size lpm, .-lpm
What do you notice about the above function in relation to the ABI?
A single 16-bit value is passed in r24 and r25.
A single 8-bit value is returned in r24.
No registers are saved, since those used are all caller-saved registers.
Debouncing Switches
In this lab, you will implement three different debouncing strategies in software:
- Delay Debouncing
In this strategy, after a button press is detected, a short delay is observed. After the delay, if the button remains pressed, it is considered as “debounced”. Likewise for button release events.
- Shift Register Debouncing
In this strategy, the state of the button is repetitively polled, and saved into a shift register. When the shift register is full of 1s or 0s, the switch is considered “debounced”.
- Non-blocking Debouncing
In this strategy, the state of the button is polled. If its state is unchanged since the last polling, a counter is incremented; otherwise the counter is reset. When the counter reaches a set value, the switch is considered “debounced”.
You will implement each in its own source file, with an associated header file. The function, source, and header names shall be:
delay_debounce()
,delay_debounce.c
,delay_debounce.h
shift_debounce()
,shift_debounce.c
,shift_debounce.h
async_debounce()
,async_debounce.c
,async_debounce.h
The .c
and .h
`files shall not declare any externally linked identifiers besides the corresponding debounce function.
delay_debounce
The delay_debounce
function shall have the following signature,
int delay_debounce(uint8_t volatile *port, uint8_t pin, uint16_t cycles);
It will check the state of the specified pin on the specified port, and then wait for cycles
“delay” cycles, before checking the specified pin again. If the value is unchanged on two consecutive polling events, the switch is debounced; return the unchanged value.
shift_debounce
The shift_debounce
function shall have the following signature,
int shift_debounce(uint8_t volatile *port, uint8_t pin);
It will repeatedly check the state of the specified pin on the specified port, and push that value into a local uint16_t variable. e.g.,
uint16_t shift_reg;
/* ... */
shift_reg = (shift_reg << 1) | is_pin_set(port, pin);
When the shift register is full of 1s or 0s (e.g. 0xff
or 0x00
) the switch is debounced; return the value which fills the shift register (1 or 0).
Note
Consider carefully the initial value of the shift register–it should depend on the first read value of the pin. Consider the values 0x55
and 0xaa
. Why might we initialize to either of these values?
async_debounce
The async_debounce
function shall have the following signature,
int async_debounce(uint8_t volatile *port, uint8_t pin, int8_t *counter);
If the pin is 1, and the counter is negative, it is set to 0. Otherwise, it is incremented. Likewise, if the pin is 0 and the counter is positive, it is set to 0. Otherwise it is decremented.
If the counter reaches the value -64 or +64, it will not be decremented or incremented further, respectively.
The async_debounce
function returns immediately; it does not loop. It must be called repeatedly to debounce.
Assembly Code
Along with the above files, you will also write an assembly file, delay_cycles.S
and associated header, delay_cycles.h
, containing the function delay_cycles
void delay_cycles(uint16_t cycles);
The delay_cycles
function should produce a delay of N + 8 * cycles
CPU cycles, where N
is the delay function overhead (e.g. function call and return delays, check if cycles is 0, etc.). Review the Instruction Set Summary sheet at the end of the ATMega16U4/32U4 Datasheet for instruction timings, and refer to the AVR-GCC ABI for register usage rules.
You must comment each line of the assembly function with the number of clock cycles, and derive a formula for the total cycle count based on the cycles
argument. The delay_cycles
method must be used by delay_debounce
to time its delays.
Main function
The file, main.c
will contain the main
function. The main function should be an infinite loop which debounces three of the pushbuttons on PORTD. Each time a debounced rising edge is detected, toggle the LEDs on PORTB.
Keep in mind, the PORTD pushbuttons are pull-down switches. The un-pressed state is a “high” state. The PORTD register should be set to 0xff
to pull of the pins, so that the pull-down effect of the button can be detected. The rising edge therefore refers to the button being released after pressing. Testing has shown this edge has more flutter compared to the falling edge.
To check off, show your code and board to the TA for your lab.