AVR Project – Second Semester

Introduction

In this project we must make a remote-controlled fan. For this we will be using an AVR processor, the ATMAGE328p. This will manage the speed of the fan with PWM. The command for how fast the fan shall run is being controlled by a master controller, a Raspberry Pi. This communication will be over USART. The AVR will get a command with a value from 0-255, based on how fast the fan should be. The AVR should then response is a fitting response.

In this post, we will go though the main points of the code and look at some of the techniques and methods used in the code. The code is written in C/C++. It is required that you have a basic knowledge of the C/C++ programming language.

Control Flow

The AVR is used to control a fan, aka, a motor, and the speed of the motor. For this to be possible with a digital signal, we can use pulse width modulation or PWM. PWM is in short just turning on and off the signal very fast. The on/off period is called the duty cycle. This duty cycle should be controlled based on a signal, that can be based on a temperature sensor or a potentiometer. This control signal will however not be originating from the AVR, as it will most likely be located close to the motor, where it could be hard to reach or a bad place for a sensor. So, we need to be able to send the duty cycle to the AVR from a master controller. This can be done a lot of ways, but for this we will use a serial connection or USART. And to make sure that it got the duty cycle, the AVR should response to the message. This message should also be a number and a number that will fit to use for the duty cycle.

So, in short:

  • Get a duty cycle from a master controller over USART.
  • Convert the USART message into a number and check if the number is correct.
  • Response to the message if the number is good over USART.
  • Change the duty cycle if the number is right.

Compiler Math

To begin with, we will define some constants. These will be used to set the baud rate for the USART communication:

#define F_CPU 16000000
#define BAUD 38400
#define BAUDRATE F_CPU/16/BAUD - 1

But I hear you ask; “You should do math in the program if possible, especially on a low power processor as the Atmage328p.” To that you are correct, you should not. But compilers are smart. They will see our math and just calculate it for the AVR. It is like; if you got an apple in both hands, you would not say you got 1 + 1 apples, you would say you got 2 apples. The compiler does the same. It will not write in the program that the “BAUDRATE” is 16000000/16/38400 – 1, it will write that the “BAUDRATE” is 25. By placing the calculation on the compiler, we have made the coding more intuitive. So instead of saying the baud rate is some magic number, we write our desired baud rate and let the compiler do the mathematics for us.

This does not work if the compiler does not know the numbers involved in the calculation, like if it is based on a user input. For this we can apply other ways of removing the calculation off the AVR, like lookup tables.

Declaring Main Stope

After this we include some libraries that the program needs:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdlib.h>

And set some variables for later use:

bool uartFlag = false;
char RXdata[8];
volatile unit8_t RXidx = 0;

These variables will be used later in the code, but should be accessible everywhere in the program, so we place them in the global scope.

At the end we declare some functions for later use in the main program:

void usartSend(const char *data);
int changeDuty(const char *data);

These both take the same input, a pointer to a character array. A pointer is a memory address to where we can find the value. In C/C++ it is not smart to copy the content of a variable into a function as that will take more memory that should be needed. So instead we will tell the function where in memory it can find the variable and read is from there.

Bitwise Operation and Bit Shifting

Now we can begin our main program. And first, we will again set some variables that will be used a bit later:

int main(void){
	char dataBuf[64];
	uint8_t local_RXinx = 0;
	uint8_t data_idx = 0;

After this, we will enable the PWM on the AVR:

	OCR0A = 255;		// Duty Cycle: 100%
	TCCR0A |= (1 << COM0A1);// None-Inverted Mode
	TCCR0A |= (1 << WGM00);	// PWM Phase-corrected - Top: 0xFF
	TCCR0B |= (1 << CS01);	// Pre-scaler: Clock / 8

In this section, we are two coding techniques: Bitwise operations and Bit shifting. A bitwise operation and a bit shift are just doing a logical operation with the bits. There are four bitwise operations and two bit-shifting operations:

  • Logical AND (&)
  • Logical OR (|)
  • Logical XOR (^)
  • Logical NOT (~)
  • Shift Right (>>)
  • Shift Left (<<)

In the code above, we do a bitwise OR operation and a bit shift to the left. First, lets go over the OR operation.

A OR operations compares to inputs. If one of them is a one, the result will be a one. But if both are zero, the result will be a zero. For example:

0x8 | 0x4 = 0xC
0xC = 1100
0x4 = 0100
0xC = 1100

This we use to save the previous set bits in our registers. Otherwise, we will be righting over previous set bits with zeroes, potentially changing the intended outcome. In our code, if we have not used the OR operation, our TCCR0A register would not have the inverted/none-inverted mode bit set, as we did that in another line.

We next technique is the bit shifting. With bit shifting, we can push bits to the left or right of a byte. The bits that goes over the ends of the byte will be discarded. Remember, that a byte contains 8 bits. So, if we shift the byte ones to the right, the least significant bit (the first bit) will fall over the end and be replaced by the second bit and the rest of the bits follows. The most significant bit (the last bit) will be empty as it has taken the 7’th place, so it will be a zero at the end. We can keep doing this how many times we want. If we shift to the left, the opposite will happen: The most significant bit will be pushed over the edge and get removed with the 7’th bit replaces it and so forth down the bits. At the end, the least significant bit is empty and a zero will take its place. For example:

0xC >> 1 = 0x6
0xC = 1100
1100 >> 1 = 0110 = 0x6

0x1 << 3 = 0x8
0x1 = 0001
0001 << 3 = 1000 = 0x8

Bit shifting is useful when programming the AVR. In the library for the AVR, the programmers have defined the offsets for all the bits in the registers. For example: the COM0A1 bit in the TCCR0A register is the 7’th bit (remember, that the byte starts with the 0’th bit), so the offset is 7. So, if we shift a single bit to the left seven times, we can set the COM0A1 bit.

We can even see this in the library itself by going to the COM0A1 definition:

#define TCCR0A _SFR_IO8(0x24)
#define WGM00 0
#define WGM01 1
#define COM0B0 4
#define COM0B1 5
#define COM0A0 6
#define COM0A1 7

In the next part of the code, we will enable and configure the USART communication:

	cli();
	UBRR0H = ((BAUDRATE) >> (8));		// Baudrate - Higher bits
	UBRR0L = (BAUDRATE);			// Baudrate - Lower bits
	UCSR0B |= (1 << RXEN0) | (1 << TXEN0);	// Enable receiver and transmitter
	UCSR0B |= (1 << RXCIE0);		// Enable receiver interrupt
	USCR0C |= (1 << UCSZ01) | (1 << UCSZ00);// Set frame: 8bit, 1stop
	sei();

In here we do much of the same. But here is an example on where to use a shift right operation. Our baud rate calculation can potentially be bigger than 255 or bigger than what a single byte can contain. And when we set our registers, it will only save the 8 lowest bits in it, as it is an 8-bit register. The rest will be discarded. So, for our higher bits, we shift them right eight times so they can be in a single byte. We can then save the lower in another register.

At the end, we set our GPIO pins and we are done with the setup:

	DDRD |= (1 << DDD6) | (1 << DDD7);

Bit Mask

To make sense of the program flow, lets go outside the main function and look at the interrupt routine. This routine is not a part of the main program, the main program does not even know it exists. This routine will be called by the processor when the interrupt flag gets set by the hardware. In our code, we have enabled the interrupt on received USART messages, so we create the routine:

ISR(USART_RX_vect){
	RXdata[RXidx++] = UDR0;
	RXidx &= 7;
	PORTD ^= (1 << PIND7);
}

The first part of the routine saves the message in a data buffer, which we will go through a bit later. While we save the message in the buffer, we also increment the index of the buffer by one. Then, we are bitwise ANDing the index with 7. This concept is normally used as a bit mask. A bit mask allows us to select a part of a bit string. In this code, we use it to limit the height if the index.

While we can increment our index to any number, out buffer is only so big. 8 characters big in this case. So, to make sure we do not point to an index outside our buffer, we will use the bit mask to limit the index. For example, if our index is at number 3, it is fine, as that number is within our buffer. But the number 8 is not:

0x3 & 0x7 = 0x3
0011 = 0x3
0111 = 0x7
0011 = 0x3

0x8 & 0x7 = 0x0
1000 = 0x8
0111 = 0x7
0000 = 0x0

This way, we can limit our index. However, this is not flexible solution. As we are using a bit mask, we can only do this with buffer sizes that is a power of two (2^n). Any other will roll back to zero before we intend to. For example, we cannot limit our index to 13 (0xD):

0x2 & 0xD = 0x0
0010 = 0x2
1101 = 0xD
0000 = 0x0

But a power of two is not that uncommon a number to use in programming, so it does not impair the code.

Data Buffers

Now that we have look at the receive interrupt routine, lets go back into the main loop and see what we do to our messages from the USART connection.

Since we have completed the setup of the AVR, we can let it run forever in a continuous loop. In this loop we will save our USART message in a bigger buffer:

	while(1){
		if(local_RXidx != RXidx){
			char ch = RXdata[local_RXidx];
			if(ch){
				data_buf[data_idx++] = ch;
			}
			if(ch == ‘\r’){
				uartFlag = true;
				data_buf[data_idx] = 0;
			}
			local_RXidx++;	
			local_RXidx &= 7;
		}

So, while we are inside the loop, we would like to save our message in a better place. Right now, the messages are in a small buffer, only 8 characters long, as we have set in our global variable RXdata. For our current application, this is plenty space, since we are only expecting a command from the master controller to be at maximum 6 characters long, with all the invisible characters included. But if we would want to evolve the system, it is best to plan for this, instead of redoing a lot of code.

The first our loop does is checking if any new message have been received. We do this by having two indexes. One for the interrupt routine and one for the main loop. These two indexes; “RXidx” and “local_RXidx” should always be the same, if “RXidx” is head, that means we have more messages to read.

We read this message into a temporary variable “ch”. We then check if it is not a NULL byte. This is because of how character arrays are structured in C/C++. When we declare the array, the program cannot know when a string in the character array ends. So, it uses a NULL byte (0x0) as an indicator that the string ends here. If we allow this NULL byte into our array, the string will be end prematurely and our command will be interpreted wrong. So, if the message is not a NULL byte, save the message in our bigger buffer. This buffer is 64 bytes long, so it can contain 63 characters and a NULL byte for ending the string.

To indicate that the command we send have been send in its entirety, we will send a carriage return command. When the message is this ascii character, we set a flag to true, telling the program that it has received a command and can process it now. And we set the last character to a NULL byte to indicate that the string ends here.

We then increment our “local_RXidx” and use the bit mask to limit the size the index to 8 numbers, just like we did in interrupt routine.

IF Shorthand

After we have check if the big data buffer and our small USART buffer is the same, we then check if the command is ready to be processed:

		if(uartFlag){
			int change = changeDuty(data_buf);
			const char* TXdata = (change ? “OK\r\n” : “ER\r\n”);
			uartSend(TXdata);
			data_idx = 0;
			uartFlag = false;
		}
	}
}

When the full command is received and the “uartFlag” is true, we can then change our duty cycle for our PWM signal. This function returns either a true or false, aka, one or zero, depending on if the command is valid. Our code in the beginning of the loop only makes sure that it is a command we get; it does not check if it is a valid command. This we handle in the “changeDuty” function.

When the command is processed, we generate a response message to the master controller. For this, we use a shorthand to write our if statement. The shorthand is structured like this:

(variable ? <if true> : <if false>);

if(variable){
	<if true>
}
else{
	<if false>
}

These two codes do the same. This is a quick way to make an if/else statement if the code inside the if/else is only one line long. If you want more than one line, you will have to use the second method.

Circular Buffers and Buffer Reset

After we have generated our response, we send it back to the master controller with the “uartSend” function. We then reset our data index and toggle the “uartFlag” off. We do not need to clear the buffers. In the big data buffer, we handle the end of the command with a NULL byte, so everything will be handled by the NULL byte. If a shorter command is sent, the old one will be overwritten by the new one and the NULL byte will indicate the new end of the command.

The smaller buffers do not need to be reset as well. We do not care about the previous messages; we only care that we have read everything in it. So, we never look back in these buffers, so clearing them is a waste of time. And we use the bit mask to make sure that they both stay inside the limits.

Conclusion

At the end, we write the content of our missing functions: “uartSend” and “changeDuty”. There is not much to go though, so I will let you read it in the source code.

The next part of the project is to program the master controller. But this post has gone on long enough.


Link to the source code on Gitlab: https://gitlab.com/alex8684/system_development/-/blob/master/day_7/day_7/main.cpp

AVR Project – Second Semester
Scroll to top