Saturday, September 4, 2021

AVR 328P Frequency Counter--Embedded C--Using the Handy ICP1 Pin

Hello Again Fellow Bitheads.  If you've been following the blog for the past few months I've been coding in "Pure C" or "Bare Metal C" or "Embedded C." Whatever you want to call it, it "ain't Arduino Sketch".  

This time, I was thinking about adding a beats per measure (BPM) to frequency feature to the improved Dirty Digital LFO. Last post about the current Embedded DDLFO module's design/code/build is here.

To make this work, I will first need a squarewave frequency counter, so we can feed in 4 or 8 pulses per beat and get BPM out, so this time, let's work on the counter component of the design.

The counter works from about 2hz to about 300khz!

To develop this on the bench, what hardware to use? I have a few Arduino Uno R3 development boards in my junk box, with a PDIP Atmel 328P IC on board, so let's use that, but we will code with Atmel Studio 7 and upload our code to the MCU with an Atmel Ice Programmer. With all due respect to the Arduino community: no Arduino Sketch code will be used for this project.

Next let's go steal some open source Embedded C code. There should be lots of Atmel 328 counters coded in C to be found online, right?

Well, no. Some posts I found use assembly language for AVR, not C, see the project here for instance--assembly is at the bottom. Overall I couldn't find as many C based AVR frequency counters  as I expected.

However I found the very useful post here (ElectronicWings--lots of good stuff at that site) with code for a counter based on Atmega32 processors--a good start.

I am not using a Atmega16 nor a 32, and the timers/registers in the Atmega 328P are somewhat different than the 16/32, so some code porting was needed.

Fortunately porting this code to Atmega 328P was not that difficult, taking me maybe a morning to figure out. I primarly studied the Atmega 328P datasheet, pages 96-99, and beyond changing the names of some registers most of the ElectronicWings code worked without a whole lot of modification.

The code uses a feature called an input capture pin, or ICP; something I've not used before--on the 328P that's a dedicated Pin: PB0. On an Arduino Uno R3, that's "Digital pin 8". Details about ICP can be found in the datasheet and is also covered here. The basic idea: capture the rising or falling edge of your square wave and put a timer value into a 16 bit register; move that register's content to a variable in your C code, then repeat the process: capture the incremented timer value a different variable. 

Finally, subtract the two variables to determine the frequency.  

I also learned of an interesting associated peripheral--the 328P has a pretty sophisticated analog comparator built in you can use to capture input timing in the same manner.  I didn't use that feature this time, but I can see a lot of uses for it for what we do. More detail about the 328's comparator peripheral is here.  

I needed a display for this week's project; I used an SSD1306 OLED cheap display I had in my junk box. 

I used my prewritten I2C libary as well as the 1306 library found here to complete the C code for the counter. 






Wire me up Scotty! The "works on my bench" build is pretty accurate from 2hz to about 300khz. Not too shabby? I am manually setting the range 2-500hz and 500-300Khz) by recoding what is below then recompiling/uploading to the MCU--using the--you guessed it--"range" variable. Value 0 is for high frequences, while 1 is for lower frequencies. This could be a front panel switch, or the code could be rewritten to autorange, but for now just typing in a value and recompiling/uploading is good enough.

Wiring on the bench is very simple--I2C, power, and a feed from my "freq gen"--i.e., my bench squarewave generator (A Siglent SDG1025)

How accurate is this quick counter?  Again: good enough. It tracks pretty well for frequencies between about 700hz and 200khz for range 0. 

For range 1 2hz to about 300hz is good and it's usable to about 800hz. 

I will probably need to use a slower prescaler for the BPM clock capture, but that's for another day.  

OK, here's the main.c code.  Again you'll need my I2C libary .c and .h files and this 1306 driver added to your project before you can compile this....upload and have fun. If you come up with anything good please let me know in the comments.


/*

 * freqcounter main.c

 *

 * Created: 9/2/2021 10:42:24 AM

 * Author : audiodiWHY

 */ 

#include <avr/io.h>

#include <util/delay.h>

#include <avr/interrupt.h>

#include <stdlib.h>

#include "SSD1306.h"

#include "i2c.h" 

 //////// range = 0  measure freqs 500-300K with pretty good //accuracy--audio and bench stuff

 //////// range = 1  freq 2hz to about 700hz with pretty good //accuracy -- LFO etc.

 //************SET INITIAL RANGE HERE************

 uint8_t range = 0;

//***************************************

int main ( )

{

OLED_Init();  //initialize the OLED

OLED_Clear(); //clear the display  

unsigned int a,c,period;

char frequency[14];

          DDRB = 0x03;

PORTB = 0b00000001;

/* 

Turn ON pull-up resistor 

NOTE! ICP1 is pin PB0 on AVR328 you knew that?

*/


//1306 lib, set the cursor position to (0, 0)

OLED_SetCursor(0, 0);        

//SSD1306 library to print text to OLED

 OLED_Printf("AudioDiWhy Freq ctr");  

 OLED_SetCursor(2, 0);

 OLED_Printf("FREQUENCY:"); 

while(1)

{

        // set counter parameters

TCCR1A = 0;

TCNT1=0;

TIFR1 = (1<<ICF1); 

/* 

Clear ICF (Input Capture flag)                                     flag; writing 1 clears

*/                

if (range == 0)

{

                   // Rising edge, no prescaler 0x41  

TCCR1B = 0b10000001;  

}     

if (range == 1)

{

TCCR1B = 0b10000100;  // Rising edge, 256                                                    //prescaler

}

while ((TIFR1 & (1<<ICF1)) == 0)

               {

/* next lines for                                                    debug.              

OLED_SetCursor(4, 0);

OLED_Printf("waiting for cptr a"); */                    

   }

/* put capture register in a */

a = ICR1; 

TIFR1 = (1<<ICF1);  /* Clear ICF flag again */

        if (range == 0)

       {

    TCCR1B = 0b10000001;  /* Rising edge, no                                                   /prescaler  0x41 */

         }

if (range == 1)

{

TCCR1B = 0b10000100; 

                /* Rising edge, 256 prescaler */

}

while ((TIFR1 & (1<<ICF1)) == 0)

              {   

     /*    OLED_SetCursor(4, 0);

       OLED_Printf("waiting for cptr c"); */

                              } 

c = ICR1;  /* capture 2nd up edge */

TIFR1 = (1<<ICF1);  /* Clear ICF flag */

TCCR1B = 0;  /* Stop the timer */

if(a < c)   

            /* Check for valid condition, to avoid                                  timer overflow reading */

{  

period=c-a;

long freq= F_CPU/period;

                         /* Calculate                                                          frequency */

if (range == 0)

{

/*do zilch*/

}

if (range == 1)

{

freq = freq/256;  

                         /*accommodate prescaler                                                  /value.*/

}

ltoa(freq,frequency,10);  

OLED_SetCursor(4,0);

OLED_Printf("           ");

                        OLED_SetCursor(4,0);

OLED_Printf(frequency);

OLED_SetCursor(4, 60);

OLED_Printf("hz");

_delay_ms(500);

      

}  // end if

else

    {

        OLED_SetCursor(4,0);

OLED_Printf("ReadErr"); //Counter overflow

    }

_delay_ms(50);

}


} // end main

Code-ah? When I get more of the BPM features coded I will post the whole project to github. In the meantime: Hey, a new batch of boards and stencils are here from the blog's sponsor, PCBWAY, so some soldering is right around the corner. See ya next time!


No comments:

Post a Comment

A guy OK with C tries to learn C++. Bjane me Up, Stroustruppy!

Why no posts so far for 3-2024?  I have been woodshedding, brushing up on C++ skills.  What got me to finally start digging into C++? I was ...