Ditching libraries and working directly with hardware can be a rewarding way to deepen your understanding of microcontrollers. In this guide, we’ll explore how to interface the HC-SR04 ultrasonic distance sensor with an Arduino Nano using baremetal programming, no external libraries, just pure register-level control.
If you are a usual reader of our baremetal tutorials we made a small library to help you get started with Pins and UART control. If you don’t care about that, you will have a link to jump to the part of the tutorial where we get the sensor working only with the main file after setting up your workspace.
To follow this article you only need a HC-SR04 distance sensor, an Arduino Nano and a computer with Ubuntu (VSCode is optional but recommended).
To code for the Arduino without the IDE, we will need some libraries. Go to your terminal and install the following AVR packages:
sudo apt install avr-libc avrdude binutils-avr gcc-avr build-essential
With those packages installed, create a folder for your project with the following structure:
projectfolder
├── Makefile
├── .vscode
│ └── c_cpp_properties.json
└── src
└── main.cpp
Let’s start with the file inside ‘.vscode’, this file is usefull for those of you who like VSCode’s intellisense. This file will tell VSCode where the libraries header files are located at. Create the ‘.vscode’ folder with a file named ‘c_cpp_properties.json’ and, in it, paste the following json:
{
"configurations": [
{
"name": "AVR",
"includePath": [
"${workspaceFolder}/**",
"/usr/lib/avr/include/**"
],
"defines": [
"__AVR_ATmega328P__"
],
"compilerPath": "/usr/bin/avr-gcc",
"cStandard": "c11",
"cppStandard": "gnu++11",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
Now we need a Makefile, this file will let us compile and upload our code to the Arduino by running commands like ‘make build’ and ‘make upload’:
# arduino nano
ARDUINO_PORT=/dev/ttyUSB0
ARDUINO_BAUD=115200
ARDUINO_F_CPU=16000000UL
ARDUINO_MMCU=atmega328p # atmega2560
ARDUINO_PROGRAMMER_ID=arduino # wiring
# ====================================================================================================
# directories
DIR_SRC=src
DIR_OUT=out
OUT_NAME=program
# ====================================================================================================
# files
SOURCES := $(shell find $(DIR_SRC) -name '*.cpp')
ARDUINO_ELF := $(DIR_OUT)/$(OUT_NAME)_arduino.elf
ARDUINO_HEX := $(DIR_OUT)/$(OUT_NAME)_arduino.hex
ARDUINO_OBJECTS := $(patsubst $(DIR_SRC)/%.cpp, $(DIR_OUT)/arduino/%.o, $(SOURCES))
# ====================================================================================================
# compiler flags
ARDUINO_CC= avr-gcc
ARDUINO_OCP= avr-objcopy
ARDUINO_UPL= sudo avrdude
ARDUINO_CC_FLAGS= -Os -Wall -DF_CPU=$(ARDUINO_F_CPU) -mmcu=$(ARDUINO_MMCU)
ARDUINO_CC_HEADERS=
# ====================================================================================================
# functions
.PHONY: build, upload, clean
# Compile for arduino
build: $(ARDUINO_HEX)
$()
# Upload code to arduino
upload:
$(ARDUINO_UPL) -V -c $(ARDUINO_PROGRAMMER_ID) -p $(ARDUINO_MMCU) -P $(ARDUINO_PORT) -b $(ARDUINO_BAUD) -D -U flash:w:$(ARDUINO_HEX):i
# Remove out folder
clean:
rm -rf $(DIR_OUT)
# ====================================================================================================
# compile
$(DIR_OUT)/arduino/%.o: $(DIR_SRC)/%.cpp
mkdir -p $(dir $@)
$(ARDUINO_CC) $(ARDUINO_CC_FLAGS) -c -o $@ $<
$(ARDUINO_ELF): $(ARDUINO_OBJECTS)
$(ARDUINO_CC) $(ARDUINO_CC_FLAGS) -o $@ $(ARDUINO_OBJECTS)
$(ARDUINO_HEX): $(ARDUINO_ELF)
$(ARDUINO_OCP) $(ARDUINO_ELF) -O ihex $(ARDUINO_HEX)
Now that we have everything ready to code, let’s make our Arduino’s built in LED blink to test everything we have done until now. Keep in mind that this code is for the Arduino Nano and you might need to change the registers while using another one.
In this code we start by importing both ‘avr/io’ and ‘util/delay’, these two files contain everything we will need for this small test. In the main function we start by setting the ‘PB5’ bit in the ‘DDRB’ register, this sets pin D13 (connected to the LED) as output, then we create an infinite while loop where we turn on the LED by setting ‘PB5’ in the ‘PORTB’ register, wait a little, clear the bit to turn off the LED and finally wait a little more.
#include <avr/io.h> // Used for registers and pins
#include <util/delay.h> // Used for _delay_ms
int main()
{
DDRB |= _BV(PB5); // Set LED Pin as output
while (1) // Loop forever
{
PORTB |= _BV(PB5); // Set pin as HIGH
_delay_ms(100); // 100 ms LED On
PORTB &= ~_BV(PB5); // Set pin as LOW
_delay_ms(900); // 900 ms LED Off
}
return 0;
}
To compile and upload your code run:
make build
make upload
If you want to get your sensor working as fast as possible all inside the main file click here to jump to that part of the article. If instead, you want to make your code modular and easier to use in future projects, continue reading.
With all the Baremetal Arduino tutorials we write, one thing that is common in almost every single one of them is that we use pins and UART, because of that, I made a small ‘library’ that handles UART and pin modes and levels as well as adds the names of the pins like in Arduino (Ex: you can access pin D13 by writting D13, SPI_SCK or LED_BUILTIN).
Inside the ‘src’ directory, create a folder named ‘tavr’, that folder will have the following structure:
tavr
├── nano
│ ├── pins.cpp
│ ├── pins.h
│ ├── uart.cpp
│ └── uart.h
├── mega
│ └── # files for mega
├── uno
│ └── # files for uno
├── utils
│ ├── ringbuf.cpp
│ └── ringbuf.h
├── pins.h
└── uart.h
The first things we will create are two virtual classes, ‘tavr/pins.h’ and ‘tavr/uart.h’, these files will be like the blueprint for the micro controller specific implementation. Lets start with ‘pins.h’.
This header file will have a class and two enums. The first enum is the modes that the pin can be in, the second one is the state of the pin, or it’s level, which can either be high or low. The ‘TPin’ class has four public virtual methods, one for changing the pin mode, one for reading the pin state and the other two to set it’s state, one using the enum and the other using an integer or bool:
#ifndef _tavr_pins_h_
#define _tavr_pins_h_
#include <inttypes.h>
enum EPinMode : uint8_t
{
INPUT = 0,
OUTPUT = 1,
INPUT_PULLUP = 2,
INPUT_PULLDOWN = 3
};
enum EPinLevel : uint8_t
{
LOW = 0,
HIGH = 1
};
class TPin
{
public:
virtual void mode(EPinMode m) = 0;
virtual void set(EPinLevel level) = 0;
virtual void set(uint8_t level) = 0;
virtual uint8_t read() = 0;
};
#endif // _pins_h_
Now go to the ‘nano’ folder inside ‘tavr’ and create another header file named ‘pins.h’ and a cpp file named ‘pins.cpp’.
The ‘pins.h’ inside this folder will be the implementation specific to the Arduino Nano. This file will have a class that is a child of ‘TPin’ and will override it’s four virtual methods but, besides that, it will also have four private variables, three registers and a bit, this is all the information we need to control the pins. Below the class we also create global variables for each of the pins to make it easier to use in our code.
#ifndef _tavr_nano_pins_h_
#define _tavr_nano_pins_h_
#include <avr/io.h>
#include "../pins.h"
class TPin_Nano : public TPin
{
private:
volatile uint8_t* ddr;
volatile uint8_t* port;
volatile uint8_t* pin;
uint8_t bit;
public:
TPin_Nano(volatile uint8_t* ddr, volatile uint8_t* port, volatile uint8_t* pin, uint8_t bit);
void mode(EPinMode mode) override;
void set(EPinLevel v) override;
void set(uint8_t v) override;
uint8_t read() override;
};
#define LED_BUILTIN D13
#define PIN_OC2A D10
extern TPin_Nano D2;
extern TPin_Nano D3;
extern TPin_Nano D4;
extern TPin_Nano D5;
extern TPin_Nano D6;
extern TPin_Nano D7;
extern TPin_Nano D8;
extern TPin_Nano D9;
extern TPin_Nano D10;
extern TPin_Nano D11;
extern TPin_Nano D12;
extern TPin_Nano D13;
extern TPin_Nano D14;
extern TPin_Nano D15;
extern TPin_Nano D16;
extern TPin_Nano D17;
extern TPin_Nano D18;
extern TPin_Nano D19;
#define A0 D14
#define A1 D15
#define A2 D16
#define A3 D17
#define A4 D18
#define A5 D19
#define LED_BUILTIN D13
#define SPI_SCK D13
#define SPI_MISO D12
#define SPI_MOSI D11
#define SPI_CS D10
#define I2C_SCL A5
#define I2C_SDA A4
#define P_INT0 D2
#define P_INT1 D3
#endif // _tavr_nano_pins_h_
The ‘pins.cpp’ file is very simple, we start by creating the constructor that simply assigns the variables, then the ‘mode’ function sets the pins as either output or input (in case of input pullup it also needs to set the pin as high), the set functions either set or clear the bit in ‘PORTx’ turning the pin high or low and finally the read function reads from ‘PINx’ if the pin is set as input and reads from ‘PORTx’ if the pin is set as output.
Below the functions we also need to assign the global variables created in the header file.
#include "pins.h"
TPin_Nano::TPin_Nano(volatile uint8_t* ddr, volatile uint8_t* port, volatile uint8_t* pin, uint8_t bit) :
ddr(ddr), port(port), pin(pin), bit(bit) {}
void TPin_Nano::mode(EPinMode mode)
{
switch (mode)
{
case EPinMode::OUTPUT: *(this->ddr) |= _BV(this->bit); return;
case EPinMode::INPUT_PULLUP:
*(this->ddr) &= ~_BV(this->bit);
*(this->port) |= _BV(this->bit);
return;
case EPinMode::INPUT:
case EPinMode::INPUT_PULLDOWN:
default:
*(this->ddr) &= ~_BV(this->bit);
*(this->port) &= ~_BV(this->bit);
return;
}
}
void TPin_Nano::set(EPinLevel level)
{
switch (level)
{
case EPinLevel::HIGH: *(this->port) |= _BV(this->bit); return;
case EPinLevel::LOW:
default:
*(this->port) &= ~_BV(this->bit);
return;
}
}
void TPin_Nano::set(uint8_t level)
{
if (level)
*(this->port) |= _BV(this->bit);
else
*(this->port) &= ~_BV(this->bit);
}
uint8_t TPin_Nano::read()
{
if (*(this->ddr) & _BV(this->bit))
return (*(this->port) & _BV(this->bit)) ? 1 : 0;
else
return (*(this->pin) & _BV(this->bit)) ? 1 : 0;
}
TPin_Nano D2 = TPin_Nano(&DDRD, &PORTD, &PIND, PD2);
TPin_Nano D3 = TPin_Nano(&DDRD, &PORTD, &PIND, PD3);
TPin_Nano D4 = TPin_Nano(&DDRD, &PORTD, &PIND, PD4);
TPin_Nano D5 = TPin_Nano(&DDRD, &PORTD, &PIND, PD5);
TPin_Nano D6 = TPin_Nano(&DDRD, &PORTD, &PIND, PD6);
TPin_Nano D7 = TPin_Nano(&DDRD, &PORTD, &PIND, PD7);
TPin_Nano D8 = TPin_Nano(&DDRB, &PORTB, &PINB, PB0);
TPin_Nano D9 = TPin_Nano(&DDRB, &PORTB, &PINB, PB1);
TPin_Nano D10 = TPin_Nano(&DDRB, &PORTB, &PINB, PB2);
TPin_Nano D11 = TPin_Nano(&DDRB, &PORTB, &PINB, PB3);
TPin_Nano D12 = TPin_Nano(&DDRB, &PORTB, &PINB, PB4);
TPin_Nano D13 = TPin_Nano(&DDRB, &PORTB, &PINB, PB5);
TPin_Nano D14 = TPin_Nano(&DDRC, &PORTC, &PINC, PC0);
TPin_Nano D15 = TPin_Nano(&DDRC, &PORTC, &PINC, PC1);
TPin_Nano D16 = TPin_Nano(&DDRC, &PORTC, &PINC, PC2);
TPin_Nano D17 = TPin_Nano(&DDRC, &PORTC, &PINC, PC3);
TPin_Nano D18 = TPin_Nano(&DDRC, &PORTC, &PINC, PC4);
TPin_Nano D19 = TPin_Nano(&DDRC, &PORTC, &PINC, PC5);
With some small changes to our main file, we can now use our ‘pins.h’ file to control the LED state:
#include <avr/io.h>
#include <util/delay.h>
#include "tavr/nano/pins.h"
int main()
{
LED_BUILTIN.mode(EPinMode::OUTPUT);
while (1)
{
LED_BUILTIN.set(EPinLevel::HIGH);
_delay_ms(100);
LED_BUILTIN.set(EPinLevel::LOW);
_delay_ms(900);
}
return 0;
}
To compile and upload your code run:
make build
make upload
Inside ‘tavr/utils’ create a file named ‘ringbuf.h’, this will be a data structure that will help us once we start implementing the UART.
This data structure will have a pointer to an array, the size of the array and then two indices, head and tail, besides that, it will also have four functions, one to add data, one to remove, one to calculate the length and a final one that marks the buffer as empty:
#ifndef _tavr_utils_ringbuf_h_
#define _tavr_utils_ringbuf_h_
#include <inttypes.h>
/* Buffer with 'no end' as long as you
* read it faster than you write to it */
class TRingbuf
{
uint8_t* data; /* internal buffer */
uint16_t maxlen; /* internal buf len */
uint16_t head; /* write pointer */
uint16_t tail; /* read pointer */
public:
/* @brief Creates a ring buffer with the given array and size
* @param internal_buf internal buffer
* @param maxlen internal buffer size */
TRingbuf(uint8_t* internal_buf, uint16_t maxlen);
/* @brief Saves a value to the buffer
* @param val value to store in the buffer */
uint8_t push(uint8_t val);
/* @brief Reads a value from the buffer
* @param out pointer to store the value read */
uint8_t pop(uint8_t* out);
/* @brief Calculates the ammount of data in the buffer */
uint16_t length();
/* @brief Empties the buffer. Sets head and tail to 0 */
void clear();
};
#endif
On the ‘ringbuf.cpp’ file, implement the constructor by simply assigning the pointer to the buffer and it’s size and settings both head and tail to zero. The ‘push’ function will check if the buffer as space and if it does, it will add the data to it. The ‘pop’ function checks if the buffer has data in it and if it does, returns it to the ‘out’ variables in it’s aguments. The ‘length’ function calculates the ammount of items inside the array and the clear function sets both the head and tail to zero:
#include "ringbuf.h"
TRingbuf::TRingbuf(uint8_t* internal_buf, uint16_t maxlen)
{
this->data = internal_buf;
this->maxlen = maxlen;
this->head = 0;
this->tail = 0;
}
uint8_t TRingbuf::push(uint8_t val)
{
uint16_t n = this->head + 1; // next pos
if (n >= this->maxlen) // reached end of buffer
n = 0;
if (n == this->tail) // buffer full
return 0;
this->data[this->head] = val; // save data
this->head = n; // update head
return 1;
}
uint8_t TRingbuf::pop(uint8_t* out)
{
if (this->head == this->tail) // empty
return 0;
uint16_t n = this->tail + 1; // next pos
if (n >= this->maxlen) // reached end of buffer
n = 0;
*out = this->data[this->tail]; // read data
this->tail = n; // update tail pos
return 1;
}
uint16_t TRingbuf::length()
{
if (this->head == this->tail) // empty buf
return 0;
else if (this->tail < this->head) // tail behind head
return (this->maxlen - this->head + this->tail);
else return this->tail - this->head; // tail after head
}
void TRingbuf::clear()
{
this->head = 0;
this->tail = 0;
}
Now that we have the Ring Buffer data structure, lets create the virtual class for the UART. Create a file named ‘uart.h’ inside the ‘tavr’ folder, in it, create a class with virtual functions like, ‘read’, ‘send’ string, character and byte array and then a function to send an integer and one to send a new line (Either ‘\n‘ or ‘\r\n‘).
#ifndef _tavr_uart_h_
#define _tavr_uart_h_
#include <inttypes.h>
class TUart
{
public:
/* @brief Read a value from the uart
* @param out pointer to store the value read */
virtual uint8_t read(uint8_t* out) = 0;
/* @brief Send a string via uart
* @param str string to send */
virtual void send(const char* str) = 0;
/* @brief Send a string via uart
* @param c char to send */
virtual void send(uint8_t c) = 0;
/* @brief Send bytes via uart
* @param data data to send
* @param len data size */
virtual void send(const uint8_t* buf, uint16_t len) = 0;
/* @brief Send a string via uart
* @param v number to send */
virtual void sendNumber(uint32_t n, uint16_t base) = 0;
/* @brief Sends a new line char */
virtual void newline() = 0;
};
#endif
Then we move onto ‘tavr/nano/uart.h’, in this file we create a class that inherits ‘TUart’ from ‘tavr/uart.h’, besides overriding the functions in the virtual class, we also need to add some variables, like the buffers, a flag to know if the Uart is currently sending anything and a static pointer to an instance of this class so that it can be accessed from the ISRs (interrupts), besides the variables we will also create a static function to get the current instance of this class and two non-static functions that will be called inside the ISRs:
#ifndef _tavr_nano_uart_h_
#define _tavr_nano_uart_h_
#include "../uart.h"
#include "../utils/ringbuf.h"
#ifndef UART_NANO_BUFSIZE
/* Buffer size of UARTs for the Arduino Mega (two buffers of this size are created) */
#define UART_NANO_BUFSIZE 128
#endif
class TUart_Nano : TUart
{
private:
uint8_t m_txflag; // true if the uart is currently sending
uint8_t m_txbuf_[UART_NANO_BUFSIZE]; // send buffer (internal buffer for the ring buffer)
uint8_t m_rxbuf_[UART_NANO_BUFSIZE]; // receive buffer (internal buffer for the ring buffer)
TRingbuf m_txbuf; // send ringbuf
TRingbuf m_rxbuf; // receive ringbuf
static TUart_Nano* instance;
public:
TUart_Nano(uint32_t baud, uint8_t doubleSpeed = 1);
/* @brief Read a value from the uart
* @param out pointer to store the value read */
uint8_t read(uint8_t* out) override;
/* @brief Send a string via uart
* @param str string to send */
void send(const char* str) override;
/* @brief Send a string via uart
* @param c char to send */
void send(uint8_t c) override;
/* @brief Send bytes via uart
* @param data data to send
* @param len data size */
void send(const uint8_t* buf, uint16_t len) override;
/* @brief Send a string via uart
* @param v number to send */
void sendNumber(uint32_t n, uint16_t base) override;
/* @brief Sends a new line char */
void newline() override;
static TUart_Nano* getUART();
void isr_send();
void isr_receive(uint8_t v);
};
#endif
In the cpp file, before implementing the functions we declared in the header file, we will also create some new ones, one of them being the ‘uart_ubrrn_from_baud’ function. This function takes a baudrate and whether it should use double speed and, using a table in the datasheet, returns the value for the ‘UBRRn’ register.
The next new function is ‘uart_init’ this function’s job is to enable the UART’s transmitter and receiver along with their interrupts, set the baudrate, double speed and to set the format as 8bit data and 2bit stop.
Finally the last helper function will be called ‘uart_str_fromint’ and it converts a 32bit unsigned integer into a string.
The constructor of the ‘TUart_Nano’ class will take the baud and double speed, intialize the ring buffers and tx flag, set the static instance to this class and then call ‘uart_init’.
The function ‘isr_send’ will check if there is any data to send, and if there is, write it to ‘UDR0’ while the ‘isr_receive’ function simply pushes the data received to the receive buffer.
The ‘read’ function tries to pop data from the receive buffer and the ‘send’ functions simply add data to the buffer and start sending data if nothing is being sent.
The instance static variable can be initialized to ‘nullptr’ and ‘getUART’ will return that static variable.
Finally, on the ISRs, get the UART instance and, if it isn’t null, call either ‘isr_send’ or ‘isr_receive’ inside that instance.
#include "uart.h"
#include <avr/io.h>
#include <avr/interrupt.h>
uint16_t uart_ubrrn_from_baud(uint32_t baud, uint8_t u2xn)
{
if (u2xn) // If using double speed
{
switch (baud)
{
case 2400: return 832;
case 4800: return 416;
case 9600: return 207;
case 14400: return 138;
case 19200: return 103;
case 28800: return 68;
case 38400: return 51;
case 57600: return 34;
case 76800: return 25;
case 115200: return 16;
case 230400: return 8;
case 250000: return 7;
case 500000: return 3;
case 1000000: return 1;
default: return 16; // 115200
}
}
switch (baud)
{
case 2400: return 416;
case 4800: return 207;
case 9600: return 103;
case 14400: return 68;
case 19200: return 51;
case 28800: return 34;
case 38400: return 25;
case 57600: return 16;
case 76800: return 12;
case 115200: return 8;
case 230400: return 3;
case 250000: return 3;
case 500000: return 1;
case 1000000: return 0;
default: return 8; // 115200
}
}
void uart_init(uint32_t baud, uint8_t doubleSpeed)
{
// set baud rate
UBRR0 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR0B |= _BV(RXEN0) | _BV(TXEN0);
// enable interrupts
UCSR0B |= _BV(RXCIE0) | _BV(TXCIE0);
// double speed
if (doubleSpeed)
UCSR0A |= _BV(U2X0);
// 8bit, 2bit stop
UCSR0C |= _BV(USBS0) | _BV(UCSZ02);
}
uint16_t uart_str_fromint(char* buf, uint16_t maxlen, uint32_t v, uint32_t base)
{
if (maxlen <= 1) // return if buffer has no space
return 0;
if (v == 0) // if number is 0, add 0 and the null char
{
buf[0] = '0'; // add 0
buf[1] = '\0'; // add null char
return 1;
}
char temp[11]; // 11 digits is the max an uint32 base 10 can have
uint8_t i = 0;
while (v > 0) // while the number is greater than 0
{
uint8_t digit = v % base; // get the digit
temp[i++] = (digit < 10) ? (digit + '0') : (digit - 10 + 'A'); // add digit
v /= base; // divide by base
}
uint8_t j = 0;
while (i > 0 && j < maxlen - 1) // add string to buffer (reversed)
buf[j++] = temp[--i];
buf[j] = '\0'; // null char
return j; // return size
}
TUart_Nano::TUart_Nano(uint32_t baud, uint8_t doubleSpeed) :
m_txflag(0),
m_txbuf(m_txbuf_, UART_NANO_BUFSIZE),
m_rxbuf(m_rxbuf_, UART_NANO_BUFSIZE)
{
this->instance = this;
uart_init(baud, doubleSpeed);
}
void TUart_Nano::isr_send()
{
uint8_t c;
if (m_txbuf.pop(&c))
{
m_txflag = 1;
UDR0 = c;
}
else m_txflag = 0;
}
void TUart_Nano::isr_receive(uint8_t v) { m_rxbuf.push(v); }
uint8_t TUart_Nano::read(uint8_t* out) { return m_rxbuf.pop(out); }
void TUart_Nano::send(const char* str)
{
while (*str != '\0')
m_txbuf.push(*str++);
if (!m_txflag)
isr_send();
}
void TUart_Nano::send(uint8_t c)
{
m_txbuf.push(c);
if (!m_txflag)
isr_send();
}
void TUart_Nano::send(const uint8_t* buf, uint16_t len)
{
for (uint8_t i = 0; i < len; i++)
m_txbuf.push(buf[i]);
if (!m_txflag)
isr_send();
}
void TUart_Nano::sendNumber(uint32_t n, uint16_t base)
{
char buf[16];
uart_str_fromint(buf, 15, n, base);
send(buf);
}
void TUart_Nano::newline() { send("\r\n"); }
TUart_Nano* TUart_Nano::instance = nullptr;
TUart_Nano* TUart_Nano::getUART() { return instance; }
// ISR
// ========== ========== ========== ========== ========== ==========
ISR(USART_TX_vect)
{
TUart_Nano* u = TUart_Nano::getUART();
if (u) u->isr_send();
}
ISR(USART_RX_vect)
{
TUart_Nano* u = TUart_Nano::getUART();
if (u) u->isr_receive(UDR0);
}
With some small changes to our main file, we can now use our ‘pins.h’ file to control the LED state and ‘uart.h’ to send and receive data via UART:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "tavr/nano/pins.h"
#include "tavr/nano/uart.h"
int main()
{
LED_BUILTIN.mode(EPinMode::OUTPUT);
TUart_Nano uart(115200, 1);
sei(); // Enable interrupts
uart.send("Hello\n");
while (1)
{
LED_BUILTIN.set(EPinLevel::HIGH);
_delay_ms(100);
LED_BUILTIN.set(EPinLevel::LOW);
_delay_ms(900);
uint8_t data; // Echo received
if (uart.read(&data))
uart.send(data);
}
return 0;
}
To compile and upload your code run:
make build
make upload
With the ‘library’ to control the pins and uart, lets now make a driver for our distance sensor. Start by creating a file named ‘hc_sr04.h’ and, in it, create the class below. It should have two private variables that are the pointers to the pins we will use, a constructor where we pass in those pins and a function to make a measurement.
#ifndef _HC_SR04_h_
#define _HC_SR04_h_
#include "tavr/pins.h"
class HC_SR04
{
TPin *trig, *echo;
public:
HC_SR04(TPin* trig, TPin* echo);
float dist_cm();
};
#endif
On the ‘hc_sr04.cpp’ file, on the constructor, assign the pins and set the trigger as output and the echo as input. On the ‘dist_cm’ function we will send a 10 microsecond pulse in the ‘trig’ pin, then with the help of Timer1 (16bit precision vs 8bit of Timer0 and 2), we measure the time it takes the ‘echo’ to go from high to low, finally we calculate the distance in centimeters and return it.
In the code below I also disabled interrupts by calling ‘cli’ while we are measuring to avoid inacuracies caused by, for example, a uart interrupt. At the end of the measurement we need to call ‘sei’ to re-enable the interrupts.
#include "hc_sr04.h"
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
HC_SR04::HC_SR04(TPin* trig, TPin* echo)
{
// Save pointer to pins
this->trig = trig;
this->echo = echo;
// Set pins as output and input
this->trig->mode(EPinMode::OUTPUT);
this->echo->mode(EPinMode::INPUT);
}
float HC_SR04::dist_cm()
{
// Send trigger
trig->set(0); // Set trigger as low
_delay_us(2); // Wait 2 micro seconds
cli(); // Disable interrupts
trig->set(1); // Set trigger as high
_delay_us(10); // Wait 10 micro seconds
trig->set(0); // Set trigger as low
TCNT1 = 0; // Reset Timer1
TCCR1B = (1 << CS11); // Start Timer1 with prescaler 8
while (!echo->read()); // Wait for Echo HIGH
TCNT1 = 0; // Reset Timer1 again
while (echo->read()); // Wait for Echo LOW
TCCR1B = 0; // Stop Timer1
sei(); // Enable interrupts
return ((float)TCNT1 / 2.0) * 0.0343 / 2.0;
}
On ‘main.cpp’, include the headers for the pins, uart and the HC-SR04 sensor and on the main function, set the led pin as output, create the UART and HC-SR04 sensor, enable interrupts and send a hello message, on an infinite loop, turn the led on, send the reading via uart, turn the led off and wait some 500 milliseconds.
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "tavr/nano/pins.h"
#include "tavr/nano/uart.h"
#include "hc_sr04.h"
int main()
{
// Set led pin as output
LED_BUILTIN.mode(EPinMode::OUTPUT);
// Create UART and distance sensor
TUart_Nano uart(115200);
HC_SR04 sensor(&D4, &D5);
// Enable interrupts
sei();
// Send hello
uart.send("Hello\r\n");
// Loop forever
while (1)
{
// Turn led on
LED_BUILTIN.set(EPinLevel::HIGH);
// Send measurement via uart
uart.send("Distance: ");
uart.sendNumber((uint32_t)(sensor.dist_cm()), 10);
uart.newline();
// Turn led off and wait 500ms
LED_BUILTIN.set(EPinLevel::LOW);
_delay_ms(500);
}
return 0;
}
And that’s it, you now have a driver for your HC-SR04 distance sensor as well as a base ‘library’ for your future projects. If you don’t want to use the pins and UART classes from ‘TAVR’ you can continue reading as we will implement the same functionality all in the ‘main.cpp’ file, no external libraries or files.
All of the following code blocks will be on the same ‘main.cpp’ file. If you want to download the complete main file, you can get it here. You can also test this code out in the Wokwi emulator.
Start by including ‘avr/io’ and ‘util/delay’:
#include <avr/io.h>
#include <util/delay.h>
Then define which pins we will use, in this case, PD2 is D2 and PD3 is D3:
#define TRIG_PIN PD2
#define ECHO_PIN PD3
To be able to send messages via UART, we will need the following three functions: ‘uart_init’ which will initialize the uart transmitter at a baudrate of 115200 and with 8bits of data, ‘uart_tx’ will wait until the send buffer is clear and then write a character to it and finally the ‘uart_print’ function will call ‘uart_tx’ for every character in the string:
/* Intialize the uart */
void uart_init(void)
{
UBRR0H = 0;
UBRR0L = 16; // Baud rate 115200 at 16MHz
UCSR0B = (1 << TXEN0); // Enable TX
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // 8-bit data
}
/* Send character via uart */
void uart_tx(char c)
{
while (!(UCSR0A & (1 << UDRE0))); // Wait for buffer
UDR0 = c; // Send character
}
/* Send string via uart */
void uart_print(const char* str)
{
while (*str)
uart_tx(*str++);
}
Moving on to the ‘measure’ function, this will be the function that actually communicates with the sensor. The first thing we need to do is to set the trigger pin as high for ten microseconds and then turn it off, then enable the Timer1 and wait for the echo pin to become high, reset the counter and wait for it to become low, stop the timer and then calculate the distance in centimeters from the time it took the pin to go from high to low:
/* Measure distance with sensor */
float measure()
{
PORTD |= (1 << TRIG_PIN);
_delay_us(10);
PORTD &= ~(1 << TRIG_PIN);
TCNT1 = 0; // Reset Timer1
TCCR1B = (1 << CS11); // Start Timer1 with prescaler 8
while (!(PIND & (1 << ECHO_PIN))); // Wait for Echo HIGH
TCNT1 = 0; // Reset Timer1 again
while (PIND & (1 << ECHO_PIN)); // Wait for Echo LOW
TCCR1B = 0; // Stop Timer1
return ((float)TCNT1 / 2.0) * 0.0343 / 2.0;;
}
On ‘main’, create a small buffer where we can write our messages, initialize UART, set the ‘trig’ pin as output and ‘echo’ as input, in an infinite loop, take a measurement, use ‘snprintf’ to print a message with the measurement, send the message and wait 500 milliseconds:
int main()
{
char buf[32]; // Buffer for the message to send
uart_init(); // Initialize uart
DDRD |= (1 << TRIG_PIN); // Trig as output
DDRD &= ~(1 << ECHO_PIN); // Echo as input
// Loop forever
while (1)
{
// Get measurement
float cm = measure();
// Convert to string
sprintf(buf, "Dist: %d cm\r\n", (uint16_t)cm);
// Send message via uart
uart_print(buf);
// Wait 500ms
_delay_ms(500);
}
}
And that’s it. Thanks for reading and stay tuned for more tech insights and tutorials. Until next time, keep exploring the world of tech!