External Links

Creative Science Centre

 

Frequency Meter

Input Capture

Part 1

There are many ways to measure the period of a waveform and presented here is just one of them. A period is defined here as the time it takes for a waveform to get from one rising edge to the next. This part one deals with only square wave inputs, although because of the Schmitt action of the standard digital input it will also measure symmetrical sign waves.

Hardware

The hardware consists of an LCD display and  A BV500 (PIC32 MX1 IC). The input goes directly into an input pin on the BV500 so this part is more of a demonstration rather then an actual project.

The LCD runs on 5V and so a 3V3 regulator is required, although if using one of the USB to serial this will output 3V3 so the regulator can be eliminated if it is always going to be connected to the PC.

The connection to the LCD is as follows as it may not be clear in the diagram:

LCD Pin BV500 Pin
1 (GROUND)  
2 (+5V)  
3 (GROUND)  
4 RA2 (9)
5 (GROUND)  
6 RA4 (12)
11 RB5 (14)
12 RB6 (15)
13 RB7 (16)
14 RB8 (17)
15 RB15 (Back lt)
16 (GROUND)  

The pins are the same as those used in the library. When the LCD display is wired, this code will drive the display.

http://byvac.com/mBlib/flb/Library/Display/LCD/LCD_character_1.bas

// GPIO for driving a standard LCD character display
// This is set for the MX1 Device

#option only on   // only load functions that don't already exists
#include "http://byvac.com/mBlib/flb/Library/PIC32MX1_Family/BRegisters_short/MX1_reg_ports.bas"
// Display connection
// 	LCDpin				port:bit	MX1 Pin
//	1		GND			GND			8
//	2		VCC			+5V		<on USB>
//	3		Contrast	GND
//	4		RS			RA2			9
//  5		RW			GND
//	6		E			RA4			12
//	11		D4			RB5*		14
//	12		D5			RB6*		15
//	13		D6			RB7*		16
//	14		D7			RB8*		17
//
// * Theae pins have been chosen becasue they are all 5V tollerant, when
// reading the disply it will o/p 5V. This is not necessary if the RW pin is
// tied to ground and a small delay is introduced rather then reading the
// LCD status.

// Regitsers specific to MX1, if other device then change these
constant LCDIN		PORTB
constant LCDOUT		LATB
constant LCDcSET	LATA+SET  // for control lines
constant LCDcCLR	LATA+CLR
constant LCDdSET	LATB+SET  // data lines
constant LCDdCLR	LATB+CLR
constant LCDRS  	1 << 2
constant LCDE   	1 << 4
constant LCDPINS	0x1e0

// *******************************************************************
// Sets ports I/O direction
// *******************************************************************
function lcd_initPorts()
dim temp
	temp = LCDRS | LCDE   // default W,RS & E low
	poke(TRISA+CLR,temp) // bits 4:2 as o/p
	poke(TRISB+CLR,LCDPINS) // bits 8:5 as o/P
	poke(ANSELA+CLR,temp)
	poke(ANSELB+CLR,LCDPINS)
	@LCDdCLR = LCDPINS // set data pins low
	@LCDcCLR = temp // set control pins low
endf

// *******************************************************************
// writes nibble to the  4 bits of the lcd port
// *******************************************************************
function lcd_write4(nibble)
dim n
	n = (nibble & 0xf) << 5 // because d8:d5
	@LCDcSET = LCDE // set E high
	@LCDdCLR = LCDPINS  // clear the port bits first
	@LCDdSET = n		// set the 1's
	@LCDcCLR = LCDE // latch it in
endf

// *******************************************************************
// sends data (8 bytes) to the 4 bit lcd port
// *******************************************************************
function lcd_data(d)
	lcd_write4(d >> 4) // high nibble first
	lcd_write4(d)
	@LCDcSET = LCDRS   // default high (cmd sets low)
	wait(1) // no LCD WR line do delay needed - may vary
endf

// *******************************************************************
// lcd command data is sent with RS low
// *******************************************************************
function lcd_cmd(d)
	@LCDcCLR = LCDRS
	lcd_data(d)
endf

// *******************************************************************
// initiaalise display
// *******************************************************************
function lcd_init()
	lcd_initPorts()
	wait(20)
	lcd_write4(3)	// still in 8 bit mode
	wait(20)
	lcd_write4(3)	// again
	wait(20)
	lcd_write4(2)	// 4 bit mode
	wait(20)
	@LCDcSET = LCDRS // default now high
	wait(10)
	lcd_cmd(0x28);
	wait(10);
	lcd_cmd(0x0e);	// display control
	wait(10);
	lcd_cmd(0x06);	// entry mode
	wait(10);
	lcd_cmd(0x01);	// cursor home
	wait(30);
endf

// *******************************************************************
// sends a string
// *******************************************************************
function lcd_puts(s$[128])
dim j, k=strlen(s$)
	 for j = 0 to k-1
	 	lcd_data(asc(s$,j))
	 next
endf

Input Capture

The most obvious bit of hardware for the meter function is to use the input capture module. As with all of the hardware modules they are general purpose and are thus difficult to understand (for me anyway). It is easier to understand if used for a single purpose and so this serves as a good introduction.

An overview is that a timer is used to time two events, the events in this case are two rising edges of an input signal. The number of counts the timer has managed between one and the other is a measure of the period. Setting it up is not as straightforward as there are some options to consider.

The drawing is probably not much easier to understand than the one in the data sheet but taking it from left to right and going into detail of what needs to be set up.

The Input

Because of the PPS system we can choose a variety of pins for the input to IC1 (IC1 is the input capture module 1 we will be using).  Table 11-1 on the data sheet will give the mapping vaules for the pins and for this device we have the following choices. RA2, RB6, RA4, RB16, and RB2. For no reason RB2 has been chosen. To map I2C and RB2 simply write 4 to the IC1R register. This is done as part of the initialisation.

Capture Trigger

There is a choice of events to capture, for this project we are interested only in the rising edge, however there are three choices for that: every 16th rising edge, very forth rising edge and every rising edge. For lower frequencies it makes sense to use say every edge and for higher frequencies every 16th edge and so this choice will be deferred until a measurement is carried out so that some form of auto ranging can take place.

Timer 2

When a capture event takes place, the value of a timer is placed into a buffer. It is possible to use a 32bit timer 2 and timer 3 combination or just a 16bit timer 2. A 16bit single timer 2 has been chosen, so when a capture event takes place the contents of timer 2 are placed in a buffer. The buffer is a FIFO 4 deep called IC1BUF. Because the timer is incrementing all the time, the buffer will contain incrementing values from timer 2 and so the actual time measurement is the difference between one value and the next and not the actual value itself.

Interrupts

We are not going to use interrupts, but are going to use the flags generated for the interrupts. The first interrupt flag to consider is the one from the input capture. This will happen on every 4th capture event (as set by ICI), so basically when the 4 buffers have been filled. The next interrupt is the one from timer 2: if timer 2 overflows (as detected by the interrupt flag) whilst capturing it means that the incoming frequency is too high for the settings so either reduce the timer prescaler or the capture prescaler.

Before going to the full code it may be worthwhile to see what kind of results are being obtained, for this you will need some form of variable frequency input. Skip this if you like and download the next program for the meter proper.

http://byvac.com/mBlib/flb/Projects/Frequency/fm_cap_test.bas

// fm_cap.bas
// This uses the capture and compare module and also timer 2 to measure the
// period (frequency) of a square wave signal. The main functions are:
// fm_init() to set up the capture and
// fm_auto() that will autorange ad return the period in Hz
//
// The period is measured form rising edge to rising edge of the signal. The
// internal circuitry of the input pin (RB2) has a schmitt input and so is
// tollerent of non-square waves provided they cross the 1.5V mark.
//
constant CLR	4
constant SET	8
constant INV	12

// Capture
constant T2CON  0xBF800800
constant TMR2   0xBF800810
constant PR2    0xBF800820
constant IRT2	1 << 9
constant IC1CON	0xBF802000
constant IC1BUF 0xBF802010
constant IC1R   0xBF80FA28
constant IFS0   0xBF881030
//
constant IRCAP 1 << 6
constant TMR2ON 1 << 15
//
constant ANSELB 0xBF886100
constant TRISB  0xBF886110
//
// These calculate a best value
constant MAXR	5	// maximum different readings
dim raCount(MAXR+1), rav(MAXR+1)


// *****************************************************************************
// Set up
// *****************************************************************************
function fm_init()
	// timer 2 is set each capture event
	@PR2 = 0xffff // but this isnt
	// pps for capture input pin on RB2 pin 6
	poke(ANSELB+CLR, 1 << 2)
	poke(TRISB+SET, 1 << 2)
	@IC1R = 4  // pps register
endf

// *****************************************************************************
// Capture
// The technique for capturing a period is to look for the rising edge and load
// the time base given by timer 2 into a capture buffer. There are options for
// doing this:
// pre determins if the input signam is prescaled by 16, 4 or none
// pre = 5 for capture every 16 rising edge
// pre = 4 for every forth rising edge
// pre = 3 for every rising edge
// Timer also has 7 options for prescaling
// timer 0 to 7 where 0 is 1:1 and 7 is 1:256
// If there is no signal then it could wait in the interrupt forever so there
// is a time out.
// *****************************************************************************
function fm_cap(pre, valid, timer)
dim j, b(5), timeout= 10000
	// calculate timer value to put into timer 2 register
	timer = (timer << 4) | TMR2ON
	// turn off capture, reset timer and clear interrupt flags
	@IC1CON = 0 // turn off capture
	@T2CON=0 // turn off timer
	@TMR2 = 0 // clear timer
	poke(IFS0+CLR,IRT2) // tests if timer overfolowed
	poke(IFS0+CLR,IRCAP) // clear interrupt
	poke(T2CON+SET,timer) // reset & start timer
	// Input capture is set to that it will give an interrupt on the 3rd event
	// there is a 4 deap buffer IC1BUFF
	@IC1CON = 0x80C0+pre // start cap
	// wait here until there has been 4 capture events or time out
	while ((@IFS0 & IRCAP) = 0) && (timeout > 0)
		timeout = timeout - 1
	wend
	if timeout >= 1 then
		for j = 0 to 3
			 b(j) = @IC1BUF
		next
		if (@IFS0 & IRT2) = 0 then
			@valid = 1
		else
			@valid = 0
		endif
		// calculate the capture time base using the 2nd and 3rd events
		return b(2)-b(1)
	else
		@valid = 0
		return 0
	endif

endf

// *****************************************************************************
// This calculates the frequency (period) based on the capture prescale value
// the timer prescale value and the system peripheral clock
// *****************************************************************************
function fm_freq(pre, timer, value)
dim divid=10, div=10 // don't default to 0
dim f
	// pre is the capture prescaler that can be 16,4 or 0
	select(pre)
		case 5
			divid = 160000000; break
		case 4
			divid = 40000000; break
		case 3
			divid = 10000000; break
	endselect
	// timer is the timer 2 prescale value that can be from 0 to 256
	// 0 is 9 and 7 is 256
	select(timer)
		case 0
			div = 25; break
		case 1
			div = 50; break
		case 2
			div = 100; break
		case 3
			div = 200; break
		case 4
			div = 400; break
		case 5
			div = 800; break
		case 6
			div = 1600; break
		case 7
			div = 6400; break
	endselect
	// this does the calculation, there is no floating point and the maximum
	// integer value is about 200,000,000 so the dividend is kept below this
	// by dividing the divisor
	// Returns -1 if the value is so low that the divisor would come out as 0
	if (value > 5) then  // minimum div is 25 so this is just > 100
		value = (value*div) / 100
		f = divid / (value)
	else
		f = -1 // not enough value
	endif
	return f
endf

// *****************************************************************************
// This is the autoranger that starts with capture prescale at 0 but the timer
// prescale at max. It is in 3 groups and returns as soon as there is a valid
// reading
// Returns the value in HZ or -1 if out of range
// *****************************************************************************
function fm_auto_a()
dim pre, timer, v, valid, f
	pre = 3
	timer = 0
	while timer < 8
		v = fm_cap(pre,?valid, timer)
		f = fm_freq(pre,timer,v)
		print "\n",valid, v, f
		timer = timer + 1
	wend
	pre = 4
	timer = 0
	while timer < 8
		v = fm_cap(pre,?valid, timer)
		f = fm_freq(pre,timer,v)
		print "\n",valid, v, f
		timer = timer + 1
	wend
	pre = 5
	timer = 0
	while timer < 8
		v = fm_cap(pre,?valid, timer)
		f = fm_freq(pre,timer,v)
		print "\n",valid, v, f
		timer = timer + 1
	wend
	return -1 // out of range
endf

function fm_auto_b()
dim pre, timer, v, valid, f
	pre = 3
	timer = 7
	while timer > 0
		v = fm_cap(pre,?valid, timer)
		f = fm_freq(pre,timer,v)
		print "\n",valid, v, f
		timer = timer - 1
	wend
	pre = 4
	timer = 7
	while timer > 0
		v = fm_cap(pre,?valid, timer)
		f = fm_freq(pre,timer,v)
		print "\n",valid, v, f
		timer = timer - 1
	wend
	pre = 5
	timer = 7
	while timer > 0
		v = fm_cap(pre,?valid, timer)
		f = fm_freq(pre,timer,v)
		print "\n",valid, v, f
		timer = timer - 1
	wend
	return -1 // out of range
endf

function xa()
dim a
    a = fm_auto_a()
endf

function xb()
dim a
    a = fm_auto_b()
endf

If you use the above run fm_init() first and then use either xa() or xb() to get a list of results using all of the possible combinations for pre and timer

The full frequency meter capture code is here (excludes display)

http://byvac.com/mBlib/flb/Projects/Frequency/fmx.bas

//
#option only on   // only load functions that don't already exists
#include "http://byvac.com/mBlib/flb/Library/Display/LCD/LCD_character_1.bas"
#include "http://byvac.com/mBlib/flb/Projects/Frequency/fm_cap.bas"
// this lcd has a back light
constant    BL 1 << 15  // connected to RB15
constant ANSELB          0xBF886100
constant TRISB           0xBF886110
constant LATB            0xBF886130

// *****************************************************************************
// lcd utilities
// *****************************************************************************
function lcd_cls()
	lcd_cmd(1)
endf
// top row is 1
function lcd_rc(r,c)
	if r = 1 then
		c = c + 0x80
	else
		c = c + 0xc0
	endif
	lcd_cmd(c)
endf
// back light connected to RB15
function lcd_bl(oon)
    if oon = 1 then
        poke(LATB+SET,BL)
    else
        poke(LATB+CLR,BL)
    endif
endf

function main()
dim f, f1, f$[15]
	lcd_init()
	// lcd back light
	poke(TRISB+CLR,BL)
	poke(ANSELB+CLR,BL)
	fm_init()
	lcd_bl(1)
	lcd_puts("Frequency")
	//while comkey?(2) = 0
	while 1 = 1
		f = fm_get()
		if f <> f1 then
			f1 = f
			lcd_rc(2,0)
			lcd_puts("               ")
			if f < 0 then
			   f$ = "no input"
			else
			   f$ = f
			   f$ = f$ + " Hz"
			endif
			lcd_rc(2,0)
			lcd_puts(f$)
			wait(250)
		endif
	wend
endf

This evolved over a day and is still experimental. It has been tested from 10Hz to 31KHz which is the only frequency source I have at the moment. It may be useful to explain the functions one by one:

fm_init()

There is not much to initialise as this is carried out each time a reading is taken. @PR2 is set to maximum so that the timer will be able to count the full range. Even though the PPS is sets RB2 to be an input for the IC (Input Capture) it still needs setting to be an input using TRIS and switching off the analogue input.

fm_cap(pre, valid, timer)

This sets up the timer, IC1 and returns when 4 readings have been taken. There are three possible outcomes from this function:

  • Four events may not happen, this could be due to the input frequency being none existent or too slow.
  • Timer 2 may overflow and so the 4 events will not be in incrementing order
  • Four events captured and no overflow of timer 2

The first two will return a value in valid as 0 and the last will return the difference between the count stored in buffer 2 and buffer 1 with valid set to 1. In other words the time taken by the event.

The function has two inputs and one output, pre and timer and inputs and valid is an output. If valid is 1 then the return value can be used.

Pre is set to 5, 4, or 3 which corresponds exactly to the lower bits of the IC1CON register and will set the capture prescaler to either 16, 4 and 0 respectively. This number simply needs adding on to the fixed values and is done by “@IC1CON = 0x80C0+pre” The fixed values “0x80C0″ configure IC1 to be turned on and to cause an interrupt on the 4th event. Higher in the code it is cleared which has the effect of turning off IC1, clearing the prescaler and the buffers. The interrupt for IC1 is also cleared in the code.

Timer is a value from 0 to 7 and needs some adjustment as it is bits 6:4 in the timer control register, that is what the first line of code does. Again, like the IC1 this is cleared and not started until IC1 starts.

The while loop is waiting for an interrupt from IC1 which will happen on the 4th event, however if it does not happen within a certain time then a time out will occur that will set valid to 0, this happens at the very end of the code after else. If all goes well, all 4 values are read from the buffer but just the second and third ones are used, the first one is not used in case there is a problem with the delay between starting the timer and IC1. By the second and third it should have settled down.

fm_freq(pre, timer, value)

This looks like it could do with improving as all it does is to return an actual frequency in Hz given the prescaler values of IC1 and the timer and the value returned. They both have an effect on the value and so both need to be accounted for. On the pre = 5, the dividend (one on the top) is close to the maximum value for a 32bit integer and so the need for dividing the divisor by 100 before doing the calculation.

fm_auto()

Quite a bit of this was done by trial and error but it goes though all of the possible combinations of pre and timer until a valid result is obtained, as soon as a result is obtained it return with it. It was found that starting with the largest prescale on the timer and smallest prescale on the IC1 prescaler worked best.

fm_best()

This is a general purpose statistical mode calculator, the mode is the value that most often occurs and looking practically at the results from the output put this is likely to give the most stable display. It does use, be necessity a couple of global variables.

fm_get()

This is the function that will return the frequency in Hz and will return the frequency or -1.