Programming ATmega328P in C

This page contains info on how to write c code for microcontroller atmega328P(arduino uno or nano) and similar and compile it on the command line using avr-gcc.
All you need is the compiler called avr-gcc; the compiler that loads the .hex file onto the microcontroller, called avrdude; and a txt editor to write the code. And if you want to use a serial port then you also need picocom or other serial port monitor.

Note: arduino ide also uses avr-gcc and avrdude so if you already have that ide installed you dont need to reinstall the compiler, you just need to make sure that you set the right path to it in the Makefile. You might also need to add it to the search path. To find out what path:
 in linux if its already in the search path then in the shell just type which avr-gcc, else do like in windows
 in windows open the ide, under file->preferences check the show verbose output during compilation and upload. Then recompile ur code.

Also, to compile the comand make is used which linux automatically has but on windows u need to install it. Here is a link to download it: http://tuxgraphics.org/~guido/windows-survival-kit/

You write your c code in a .c file(can also have .c and .h header files) then you write a makefile to tell the compiler how to make a .hex file which is the file that the microcontroller will get.

How to Compile and Run

Makefile


#Makefile example #----------first declare variables(all strings/text) used later ------- #set path(need to do this even if its already in the search path) #(if dont know path then write in linux shell: which avr-gcc) AVRGCCDIR="/home/jessica/arduino-install/arduino-1.8.19/hardware/tools/avr" #select chip #difference between MCU and DUDECPUTYPE: one is used by avr-gcc the other by avrdude(here they are same but for other chips this might not be the case) MCU=atmega328p DUDECPUTYPE=atmega328p #select port #for nano: #for linux: #DUDEPORT=/dev/ttyUSB0 # for uno: #for linux: DUDEPORT=/dev/ttyACM0 #set compiler and things like that PROGRAMER=arduino LOADCMD=$(AVRGCCDIR)/bin/avrdude CC=$(AVRGCCDIR)/bin/avr-gcc OBJCOPY=$(AVRGCCDIR)/bin/avr-objcopy DUDECONF=$(AVRGCCDIR)/etc/avrdude.conf #set speed of data transmission(if have wrong speed then cant talk to chip) # for nano its 57600 while for uno its 11560 which is the default #BAUTRATE=-b57600 #(for uno board) its default so no need to set it #the -b is put here instead of in avrdude because if you dont set baudrate then you give avrdude nothing rather then -b nothing. and avrdude will complain if it gets -b nothing #set a varibale called LOADARG with other variables($ means its a variable) LOADARG=-C $(DUDECONF) -P $(DUDEPORT) $(BAUTRATE) -p $(DUDECPUTYPE) -c $(PROGRAMER) -e -U flash:w: #set variable called CFLAGS CFLAGS=-g -mmcu=$(MCU) -Wall -Wstrict-prototypes -Os -mcall-prologues #------------- #this specifies what the make or make all shell command should do #you tell it what file you want as the default all: blinkled.hex #-------------- #this specifies what the make help shell command should do #echo is a command that prints text to screen, the @ hides the command so that you dont see echo text help: @echo "Usage: make all|load|rdfuses|clean" @echo " make or make all: compile " @echo " make load: use avrdude to load the code " @echo " make rdfuses: use avrdude to read fuses and test connection" @echo " make clean: delete all generated compilation files" #------------------- #here we say how to make the file we specified in all #here we say if we want .hex then we need the .out file and if we want .out we need .o and to get .o we need .c blinkled.hex : blinkled.out $(AVRGCCDIR)/bin/avr-size blinkled.out $(OBJCOPY) -R .eeprom -O ihex blinkled.out blinkled.hex blinkled.out : blinkled.o $(CC) $(CFLAGS) -o blinkled.out -Wl,-Map,blinkled.map blinkled.o blinkled.o : blinkled.c $(CC) $(CFLAGS) -Os -c blinkled.c #--------------- #this specifies what the make load shell command should do load: blinkled.hex $(LOADCMD) $(LOADARG)blinkled.hex #------------------- #this specifies what the make rdfuses shell command should do rdfuses: $(AVRGCCDIR)/bin/avrdude -C $(DUDECONF) -P $(DUDEPORT) $(BAUTRATE) -p $(DUDECPUTYPE) -c $(PROGRAMER) -v -q #------------------- #this specifies what the make clean shell command should do clean: #for linux: rm -f *.o *.map *.out *.hex #for windows: # del *.o *.map *.out *.hex #-------------------

If you have .h files then its like this:

blinkled.hex : blinkled.out $(AVRGCCDIR)/bin/avr-size blinkled.out $(OBJCOPY) -R .eeprom -O ihex blinkled.out blinkled.hex blinkled.out : blinkled.o myhfile.o $(CC) $(CFLAGS) -o blinkled.out -Wl,-Map,blinkled.map blinkled.o myhfile.o blinkled.o : blinkled.c myhfile.h $(CC) $(CFLAGS) -Os -c blinkled.c myhfile.o : myhfile.c myhfile.h $(CC) $(CFLAGS) -Os -c myhfile.c

Shell Commands

Here is a description of the make commands we defined in the makefile(see above):

C Code

Turn on an LED(in arduino its digitalWrite)

Here we turn on the led on the arduino board(arduino pin 13, also called pin PB5)(its same pin for both uno and nano) so no wiring is needed but this code can turn on any led attached(with a resistor) to an IO pin.

//this file is called blinkled.c but you can name it what you want as long as the makefile contains the same name #include <avr/io.h>//this library is needed to use IO pins(input/output pins) //_delay_ms(x) fct uses this to know how many cycles a second is //_delay_ms(x) does not use prescaler //for uno and nano use 16MHz #define F_CPU 16000000UL // 16 MHz //this library contains _delay_ms(x); #include <util/delay.h>//this line needs to be after #define F_CPU because delay needs F_CPU //led on nano or uno board goes to ground(so 1 is on) #define LEDOUT PORTB5 //pin (bit) #define LEDPORT PORTB //port(this is a register) #define LEDDDR DDRB //register needed to set pin to input or output #define LEDDDRPIN DDB5 //register digit(aka bit) needed to set pin to input or output int main(void){ //enable pin as output LEDDDR|= (1<<LEDDDRPIN);//if instead wanted to set to input do: LEDDDR&= ~(1<<LEDDDRPIN); while (1) { //this fct is a busy wait delay _delay_ms(500);//this fct is in C library included at top //led off LEDPORT &= ~(1<<LEDOUT);//write 0 for off (because negation(~) we do &)(doing LEDPORT&=(0<<LEDOUT); does the same thing _delay_ms(500); //led on LEDPORT|= (1<<LEDOUT); //write 1 for on(because not negation(~) we do |) } return(0); }

Serial Port

To send things like sensor readings between chip and computer we use a serial port.

UART(universal asynchronous receiver / transmitter) defines a protocol, or set of rules, for exchanging serial data between two devices. It uses 2 pins, TX:transmit data, and RX:receive data

Use picocom to see the transmitted data.(see shell commands above for details)

//serial port example #define F_CPU 16000000UL//16MHz #include <avr/io.h>//this library is needed to use IO pins(input/output pins) #include <avr/interrupt.h>//needed to handle interrupts #include <stdlib.h>//standard library/general utilities #include <string.h>//to send strings we need this library #include <util/delay.h>//this library contains _delay_ms(x); //define variables to set speed of serial comunication #define BAUD 9600UL #define UBRR ((F_CPU)/((BAUD)*(16UL))-1)//UBRR(UART Baude Rate Register) //to set flags volatile struct { uint8_t TX_finished:1; uint8_t sample:1; } flags; static const char CRLF[3]={13, 10};//Carriage Return (ASCII 13, \r) Line Feed (ASCII 10, \n) static volatile char *msg, TX_buffer1[10];//needs to be volatile because gets modified by interrupt //===================== //only transmission void uart_tx_init(void) { // Set the UART speed as defined by UBRR UBRR0H = (uint8_t)((UBRR)>>8); //load upper 8 bits UBRR0L = (uint8_t)UBRR;//load lower 8bits UCSR0B|=(1<<TXCIE0)|(1<<TXEN0); //(1<<UDRIE0) Enable TX and TX IRQ. UCSR0C=(3<<UCSZ00); // Asynchronous UART, 8-N-1 } //both transmitting and reciving void uart_init(void) { //TX and RX init with IRQ(interrupt request) // Set the UART speed as defined by UBRR UBRR0H = (uint8_t)((UBRR)>>8); //load upper 8 bits UBRR0L = (uint8_t)UBRR;//load lower 8bits UCSR0B|=(1<<TXCIE0)|(1<<TXEN0); // Enable TX and TX IRQ. UCSR0B|=(1<<RXCIE0)|(1<<RXEN0); // Enable RX and RX IRQ UCSR0C=(3<<UCSZ00); // Asynchronous UART, 8-N-1 } //====================== // UART TX ISR(interrupt service routine) // it loads UDR as long as the "msg" contains non-zero characters // once it finds a zero character, it sets TX_finished flag, so a new process can start transmission //look up interrupt vector names https://ece-classes.usc.edu/ee459/library/documents/avr_intr_vectors/ ISR (USART_TX_vect) { msg++; if (*msg) UDR0=*msg; else flags.TX_finished=1; } //========================= void send_int(int16_t data, uint8_t base, uint8_t crlf){//ex:15, 10, 1 here data=15, base =10, 1 to send new line while (!flags.TX_finished); //waiting for other transmission to complete flags.TX_finished=0; itoa(data, (char*) &TX_buffer1[0], base); if (crlf) strcat((char*)TX_buffer1, CRLF); msg=TX_buffer1; UDR0=*msg; } void send_string (char *str) { while (!flags.TX_finished); //waiting for other transmission to complete msg=str; UDR0=*msg; flags.TX_finished=0; } //============================ int main(void){ uart_tx_init(); flags.TX_finished=1; sei(); // enable interrupt flags.sample=1; while (1) { //infinie loop send_string("hi\r\n"); send_int(15,10,1); _delay_ms(1000); send_string("shark\r\n"); send_string("yellow\r\n"); _delay_ms(1000); } return(0); }

Dimming led(PWM)(in arduino its analogWrite)

We could just take our blinkled.c code and change the first delay from ms(milli) to us(micro) and have the second delay also much smaller but not as small. Then we vary the length of the first delay to change the brightness(because the delays are so fast, the longer its on the brighter it looks). This is a manual way of doing PWM, but it is easier and better to do it using a timer in the chip.

PWM(pulse width modulation) is a technique where you generate a square wave(pulse) for a certain time(width). and change(modulate) the pulse width to do what you want to do(height of pulse is fixed because pulse is only on or off). You need a timer to control how long the pulse is high in a certain interval of time(called duty cycle).
To slowly increase the light of an led we slowly increase the time the pulse is on.

The Atmega328P has 3 timer/counters. Timer/counter 0 and 2 are 8bit(max decimal is 255) and timer/counter 2 has 16 bits.
Timer/counter 0 and 2 are very similar and for the example below either one could be use. The main difference between the two is that timer/counter2 can be used asynchronously.

//dimming led pwm example #include <avr/io.h>//this library is needed to use IO pins(input/output pins) //_delay_ms(x) fct uses this to know how many cycles a second is //_delay_ms(x) does not use prescaler //for uno and nano use 16MHz #define F_CPU 16000000UL // 16 MHz //this library contains _delay_ms(x); #include <util/delay.h>//this line needs to be after #define F_CPU because delay needs F_CPU //led on nano or uno board goes to ground(so 1 is on) //using arduino pin11, aka PB3, aka OC2A #define LEDOUT PORTB3 //pin (bit) #define LEDPORT PORTB //port(this is a register) #define LEDDDR DDRB //register needed to set pin to input or output #define LEDDDRPIN DDB3 //register digit(aka bit) needed to set pin to input or output uint8_t cnt=0;//this variable has value of intensity. initially led off, cnt stands for count //initialize pwm(pulse width modulation) void init_pwm(void) { //pin PB3 is connected to OC2A pin(which is the output pin of OCR2A) //TCNT2 is timercounter2 register. TCNT2=0;// set counter to zero // led intensity(OCR2A)=0 -> led =off // led intensity(OCR2A)=0xFF -> led =fully on OCR2A=0;//OCR2A(Output Compare Register A) constantly compares itself to TCNT2 to know when to generate output at OC2A pin //TCCR2A(Timer/Counter Control Register A) contains bits(COM2A1,COM2A0,WGM21,WGM20, and others) // COM2A1 COM2A0 // 0 0 Normal port operation, OC0A disconnected. // 0 1 Toggle OC2A on Compare Match // 1 0 Clear OC2A on Compare Match(Set output to low level) // 1 1 Set OC2A on Compare Match(Set output to high level) // Fast PWM, Phase Correct, 8-bit (always count up, wrap counter at top) TCCR2A=(1<<COM2A1)|(0<<COM2A0)|(1<<WGM21)|(1<<WGM20); //we need TCCR2B(Timer/Counter Control Register B) to set the prescaler TCCR2B=(0<<CS21)|(1<<CS20); // here we set prescaler to no prescaler } int main(void){ //enable pin as output LEDDDR|= (1<<LEDDDRPIN); //initialize pwm init_pwm(); while (1) { cnt+=20; // count up and wrap at 254(wrap happens automatically) OCR2A=cnt; //set register to wanted intensity, led intensity 0 to 254, 254 = full on, 0 = off _delay_ms(500); } return(0); }

Servo (using timer/counter2)

Servos also use pwm. However, they are designed to have a duty cycle of 20ms=1/20E-3=50Hz and to have a pulse width between 1ms and 2ms. Need to have a pulse width of a least 1ms, if no pulse then it assumes its not connected(it will jitter or simply not move) 1ms=0degrees, 1.5=9degrees, and 2ms=180degrees

The big gap between 2ms and 20ms is so that many signals can be multiplexed, so that many servos can be controlled by a single radio controller.
The duty cycle is 20ms because it is long enough to alow multiplexing but short enough to have quick response time.

So, all we need to do is take our dimming led code and modify it a little, mainly the prescaler.
For the led the prescaler did not really matter that much but here it does since we want approximately exactly 50Hz. A prescaler takes the clock speed of the chip(for Atmega328P this is 16MHz) and every x milliseconds it sends a signal to the pwm. So in other words, it slows down the clock speed input.

//servo controlled with timercounter2 example #include <avr/io.h>//this library is needed to use IO pins(input/output pins) //_delay_ms(x) fct uses this to know how many cycles a second is //_delay_ms(x) does not use prescaler //for uno and nano use 16MHz #define F_CPU 16000000UL // 16 MHz //this library contains _delay_ms(x); #include <util/delay.h>//this line needs to be after #define F_CPU because delay needs F_CPU //led on nano or uno board goes to ground(so 1 is on) //using arduino pin11, aka PB3, aka OC2A #define LEDOUT PORTB3 //pin (bit) #define LEDPORT PORTB //port(this is a register) #define LEDDDR DDRB //register needed to set pin to input or output #define LEDDDRPIN DDB3 //register digit(aka bit) needed to set pin to input or output uint8_t cnt=7;//this variable has value of intensity. 7 not 0 initially because 0 is no pulse and servo want to always get a signal //initialize pwm(pulse width modulation) void init_pwm(void) { //pin PB3 is connected to OC2A pin(which is the output pin of OCR2A) //TCNT2 is timercounter 2 register. TCNT2=0;// set counter to zero // led intensity(OCR2A)=0 -> led =off // led intensity(OCR2A)=0xFF -> led =fully on OCR2A=0;//OCR2A(Output Compare Register A) constantly compares itself to TCNT2 to know when to generate output at OC2A pin //TCCR2A(Timer/Counter Control Register A) contains bits(COM2A1,COM2A0,WGM21,WGM20, and others) // COM2A1 COM2A0 // 0 0 Normal port operation, OC0A disconnected. // 0 1 Toggle OC2A on Compare Match // 1 0 Clear OC2A on Compare Match(Set output to low level) // 1 1 Set OC2A on Compare Match(Set output to high level) // Fast PWM, Phase Correct, 8-bit (always count up, wrap counter at top) TCCR2A=(1<<COM2A1)|(0<<COM2A0)|(1<<WGM21)|(1<<WGM20); //we need TCCR2B(Timer/Counter Control Register B) to set the prescaler //servo needs a very slow pulse //servo needs 20ms duty cycle so to get from 16E6Hz(clock speed) to 1/20E-3=50Hz we need 16E6/50/255=1254 so closest prescaler is 1024 //(we devide by 8bit(255) because 255 is the max period of a 8 bit register and we want to convert this to max period being 20ms.) TCCR2B=(1<<CS22)|(1<<CS21)|(1<<CS20); // 1/1024 of clock speed //CS22 CS21 CS20 //0 0 0 No clock source (Timer/Counter stopped). //0 0 1 clk T2S /(No prescaling) //0 1 0 clk T2S /8 (From prescaler) //0 1 1 clk T2S /32 (From prescaler) //1 0 0 clk T2S /64 (From prescaler) //1 0 1 clk T2S /128 (From prescaler) //1 1 0 clk T2S /256 (From prescaler) //1 1 1 clk T2S /1024 (From prescaler) } int main(void){ //enable pin as output LEDDDR|= (1<<LEDDDRPIN); //initialize pwm init_pwm(); while (1) { //cnt is 8 bit so can take any value between 0 and 254. 20ms=254 so to get 1ms we need 254/20=13, for 90deg:1.5ms*254/20=19, for 180deg:2ms*254/20=26 //however,I think the exact numbers depend on the specific servo. //using the one from school(HS-422) min is 7(0degrees) and max is 38(180degrees) and 20 is 90degrees and if out of range it gitters. //however using MG90s(black) where 3 is lowest value(overshots to like -30 degrees) 7=0degrees, 25=90degrees, 40 does 180degrees and at 41 does 360 degrees //(this servo is special and can do not just 180 but full 360) and 42 and above or below 3 it just does not turn(so stays at previous pos) //while using SG90(blue): below 5 it refuses to turn,23=90deg,38=180deg,above 40 it does not move //(also interesting SG90(blue) needs exactly 5v which is the standard servo voltage. MG90s(black) is able to run with both 5v and 3.3v). _delay_ms(1000); cnt=7;//0 degrees relative to reference point of servo OCR2A=cnt; //set register to wanted pulse length _delay_ms(1000); cnt=25;//90degrees relative to reference point of servo OCR2A=cnt; _delay_ms(1000); cnt=40;//180degrees relative to reference point of servo OCR2A=cnt; _delay_ms(1000); } return(0); }

Servo (using timer/counter0)

Timer/counter0 and timer counter 2 are almost the same. If you want to use a different pin then you might need a different timer. In that case all you need to do is replace the 2 in the register names with 0. HOWEVER, double check the data sheet before you do that because there are very tiny differences. In the servo code for example the prescaler settings are a tiny bit different(with timer/counter2 you can do prescale of 1/64 and 128 while with timer/counter0 you instead can set it to external clock source).

//servo controlled with timercounter0 example #include <avr/io.h>//this library is needed to use IO pins(input/output pins) //for uno and nano use 16MHz #define F_CPU 16000000UL // 16 MHz //this library contains _delay_ms(x); #include <util/delay.h>//this line needs to be after #define F_CPU because delay needs F_CPU //using arduino pin6, aka PD6, aka OC0A (on concordia board its port p2) #define SERVOOUT PORTD6 //pin (bit) #define SERVOPORT PORTD //port(this is a register) #define SERVODDR DDRD //register needed to set pin to input or output #define SERVODDRPIN DDD6 //register digit(aka bit) needed to set pin to input or output uint8_t cnt=7;//this variable has value of intensity. 7 not 0 initially because 0 is no pulse and servo wants to always get a signal //initialize pwm(pulse width modulation) void init_pwm(void) { TCNT0=0;// set timercounter 2 register to zero OCR0A=0;//OCR2A(Output Compare Register A) constantly compares itself to TCNT0 to know when to generate output at OC0A pin //TCCR0A(Timer/Counter Control Register A) contains bits(COM0A1,COM0A0,WGM01,WGM00, and others) // COM0A1 COM0A0 // 0 0 Normal port operation, OC0A disconnected. // 0 1 Toggle OC2A on Compare Match // 1 0 Clear OC2A on Compare Match(Set output to low level) // 1 1 Set OC2A on Compare Match(Set output to high level) // Fast PWM, Phase Correct, 8-bit (always count up, wrap counter at top) TCCR0A=(1<<COM0A1)|(0<<COM0A0)|(1<<WGM01)|(1<<WGM00); //we need TCCR2B(Timer/Counter Control Register B) to set the prescaler //servo needs 20ms duty cycle so to get from 16E6Hz(clock speed) to 1/20E-3=50Hz we need 16E6/50/255=1254 so closest prescaler is 1024 //(we devide by 255 because that is the max count of a 8 bit register and we want to convert this to max period being 20ms.) TCCR0B=(1<<CS02)|(0<<CS01)|(1<<CS00); // 1/1024 of clock speed //CS02 CS01 CS00 //0 0 0 No clock source (Timer/Counter stopped). //0 0 1 clk T2S /(No prescaling) //0 1 0 clk T2S /8 (From prescaler) //0 1 1 clk T2S /32 (From prescaler) //1 0 0 clk T2S /256 (From prescaler) //1 0 1 clk T2S /1024 (From prescaler) //1 1 0 External clock source on T0 pin. Clock on falling edge. //1 1 1 External clock source on T0 pin. Clock on rising edge. } int main(void){ //enable pin as output SERVODDR|= (1<<SERVODDRPIN); //initialize pwm init_pwm(); while (1) { //the exact value of cnt depend on the specific servo _delay_ms(1000); cnt=7;//0degrees relative to reference point of servo OCR0A=cnt; //set register to wanted pulse length _delay_ms(1000); cnt=20;//90degrees relative to reference point of servo OCR0A=cnt; _delay_ms(1000); cnt=38;//180degrees relative to reference point of servo OCR0A=cnt; _delay_ms(1000); cnt=20;//90degrees relative to reference point of servo OCR0A=cnt; _delay_ms(1000); } return(0); }

Further Documentation



Back to building blog

Copyright © 2024 Jessica Socher ()