A Simple Signal Generator

pwm_signal2

A square wave generator can be handy for simulating sensors and providing an input to a micro-controller.  This signal generator can be adjusted from 10 Hz - 10 kHz, with an adjustable duty cycle from 1 to 99 percent.

 

Theory of Operation

There are a variety of methods that can be used to generate a string of pulses.  Among the easiest to understand is based on delay statements.  Turn an output on for a period of time, and then turn it off for a period of time.  If these two periods are equal, the result will be a square wave with a 50% duty cycle.  The down side of this method is that the controller is always occupied waiting to change state, and can't do anything else while an output is being generated.

This is the method that has been used here.  The main program loop turns a port pin on, and at a prescribed time later, turns it off in an endless loop.  To this simple loop, 2 "if-then" statements have been added to check if a switch is pressed, in which case, a subroutine is called to set frequency and duty cycle parameters.  If you examine the code, you'll notice one of these if-then statements occurs in the "on" part of the loop, and one occurs in the "off" part of the loop.  This has been done to keep the "delay" caused by checking the port pin symmetric in both parts of the loop.  These if-then statements don't require much time to execute.  Allowing 2 µsec for each of these subroutines provides fairly accurate frequency results.

The period of a waveform or the time for one complete cycle, is equal to 1/frequency of the signal.  For a 10 kHz signal, the period is 100 µsec.  For a 50 Hz signal, it's 20,00 µsec.

The duty cycle is the percentage of time a signal is high during each cycle.  A duty cycle of 0 means it's a 0 volt DC signal.  50% is a symmetric square wave.  a duty cycle of 99% means the signal is at its positive potential 99% of the time, and 0 for 1% of the time.

Multiplying the duty cycle x the period yields the time for the output to be on.  The remainder of the period the output is off.

When a push button is pressed, the subroutine to set the frequency and duty cycle is called.  The subroutine sends a message out via the UART, asking first for frequency followed by duty cycle.  Error-checking is done to see that the input value is a valid integer number in the operating range - 10 Hz - 10 kHz for frequency and 1 - 99 for duty cycle.  Additionally, just pressing [Enter] keeps the previous value.  Note that this subroutine preempts generating the signal.

 

The Hardware

The hardware for this signal generator is non-critical.  I built the signal generator around a TAP-28 board with an 18F242 controller and a 20 MHz crystal.  The hardware is pretty simple and any 18F-series part should work fine.

PortC.2 is used for the output.  S2 on PortB.4 is used to exit the main program loop for setting the parameters.  The PIC's hardware USART is used to communicate via the Tx and Rx pins to the PC using the PICkit 2 UART tool.  The TAP-28 is powered by the PICkit 2.

 

The Results

This signal generator is a handy tool of reasonable accuracy.  It's low cost and if a USB-UART cable was used to replace the PICkit 2 interface, it would be a nice low-cost addition to the electronics bench.  On the other hand, the software will run on most PIC hardware with only a couple minor changes, so it could be loaded onto a handy dev board when it's needed.

The picture below is the interface shown on the PICkit 2 UART tool screen.  I've input a frequency of 1000 Hz.  My trusty Fluke 45 benchtop meter reads 1001.2 Hz.  Close enough for me!

sig_gen_UART_output

 

The traces below were made with the PICKit 2 logic analyzer.

sig_gen_output

I believe they're pretty self-explanatory. 

 

The Code

Finally, the revised, working code.

Signal Generator
{
*****************************************************************************
* Name : Sig Gen.bas *
* Author : Jon Chandler *
* Notice : Copyright (c) 2010 Jon Chandler *
* : All Rights Reserved *
* Date : 6/6/2010 *
* Version : 1.0 *
* Notes : Defaults to 5 KHz, 50% duty cycle. Output is PortC.2. *
* : Low on PORTB.4 allows entering new values via UART. 9600/N/1 *
* : *
* :Designed for the TAP-28 and worth every cent you paid for it *
*****************************************************************************
}
 
Device = 18f242
Clock = 20
 
Include "USART.bas"
Include "Convert.bas"
Include "utils.bas"
 
Dim DelayTotal As LongWord
Dim DelayOn As LongWord
Dim DelayOff As LongWord
Dim Duty As Word
Dim Freq As String
Dim DutyCycle As String
Dim FreqOld As String
Dim DutyCycleOld As String
 
Sub ClearUSART()
 Dim tmpByte As Byte 
 While USART.DataAvailable = True
 tmpByte = RCRegister
 Wend 
 USART.ClearOverrun
End Sub
 
Sub InputParam()
 
 USART.ClearOverrun
 
 freqError:
 USART.Write("Desired Frequency? (Hz)", 13, 10) 
 ClearUSART
 USART.Read(Freq)
 
 If Freq = "" Then
 Freq = FreqOld
 End If
 
 If IsDecValid(Freq) = false Then
 USART.Write("Frequency must be an integer value between 10 - 10000", 13, 10) 
 GoTo freqerror
 EndIf
 If StrToDec(Freq) < 10 Or StrToDec(Freq) > 10000 Then
 USART.Write("Frequency must be an integer value between 10 - 10000", 13, 10) 
 GoTo FreqError
 EndIf
 
 FreqOld = Freq
 
 DutyError:
 USART.Write("Desired Duty Cycle? (%)", 13, 10) 
 ClearUSART
 USART.Read(DutyCycle)
 
 If DutyCycle = "" Then
 DutyCycle = DutyCycleOld
 End If
 
 If IsDecValid(DutyCycle) = false Then
 USART.Write("Duty Cycle must be an integer value between 1 and 99", 13, 10) 
 GoTo dutyerror
 EndIf
 If StrToDec(DutyCycle) < 1 Or StrToDec(DutyCycle) > 99 Then
 USART.Write("Duty Cycle must be an integer value between 1 and 99", 13, 10) 
 GoTo dutyerror
 EndIf
 
 DutyCycleOld = DutyCycle
 
 Duty = StrToDec(DutyCycle)
 
 USART.Write("Frequency = ", Freq, 13, 10, "Duty Cycle = ", DecToStr(Duty), 13, 10)
 
 DelayTotal = 1000000/StrToDec(Freq)
 
 DelayOn = DelayTotal * Duty / 100
 DelayOff = DelayTotal - DelayOn
 
 DelayOn = DelayOn - 2 'adjust timing to compensate for instruction cycles
  DelayOff = DelayOff - 2 'adjust timing to compensate for instruction cycles
  
 ' usart.write ("delay-on= ",dectostr(delayon), " delay-off = ", dectostr(delayoff), 13,10)
  
End Sub
 
 
// main program start
SetAllDigital
 
SetBaudrate(br9600)
USART.ReadTerminator = 13
 
'startup default
  Freq = DecToStr(5000)
FreqOld=Freq
DutyCycle = DecToStr(50)
DutyCycleOld=DutyCycle
 
DelayOn = 98
DelayOff= 98
 
Output(PORTC.2)
 
USART.Write("Signal Generator - 10 Hz - 10 KHz at 1% - 99% duty cycle", 13, 10) 
USART.Write("Press S2 to enter new value", 13, 10, 13, 10) 
 
While 1 = 1
 
High(PORTC.2)
 If PORTB.4 = 0 Then '2 if statements to keep delays constant in on and off sections
  InputParam
 EndIf
DelayUS(DelayOn)
 
Low(PORTC.2)
 If PORTB.4 = 0 Then
 InputParam
 EndIf
DelayUS(DelayOff)
 
Wend

 


Posted: 8 years 6 months ago by be80be #5523
be80be's Avatar
This is a good ideal I have added a pot to it to set the variables. Can't get a full
100Khz 78khz as high as my 18f1320 will go with out a crystal.

Thank for all the info still the best picmicro site for all us basic users
Posted: 8 years 6 months ago by Jon Chandler #5524
Jon Chandler's Avatar
I played a little with using a ten-turn pot to set the frequency but I did something a little different.

First, I used some math to create a log response, so at low frequencies, you get small changes, and at higher frequency, larger changes....so maybe the amount of rotation to changes from 100 Hz - 200 HZ is the same amount of rotation to change from 1kHz to 2 KHz. This process works when the moment of the pot is large/fast.

For setting an exact frequency if the shaft it turned slowly, the frequency is changed by 1 Hz / ADC count. This makes it possible to dial in an exact number.

I need to fine-tune the method some more but it seems workable. The only issue is if the shaft is turned a long way slowly, you may run out of travel before you get the frequency you want. It takes a little practice.
Posted: 8 years 6 months ago by Graham Mitchell #5525
Graham Mitchell's Avatar
A thought comes to mind with your approach Jon - what about using a dedicated interrupt which continuously monitors the ADC?

If it were set to operate at ~5Hz, then it would appear responsive and provide enough time between samples to find the rate-of-change. A quick overview of the interrupt:

  • Sample ADC
  • Compare with last reading to calculate the rate-of-change
  • Set a variable which contains the increment (or decrement) number
  • Set a flag which indicates new information
  • Rinse and repeat
In the interrupt, it'd be best to check the "new info" flag before altering the Increment variable (could lead to data corruption).

Perhaps another mode could be used to counter the POTs turn limitation. If the rate-of-change is fast enough, then do nothing.

It's just a thought, some tinkering would have to be done (perhaps bump the samples up to 100Hz, and average every X samples to limit rate-of-change miss-reads?)
Posted: 8 years 6 months ago by Jon Chandler #5526
Jon Chandler's Avatar
One problem with this simple approach using delay statements is that anything else in the program loop impacts the timing. This is why a button press is used to go to a separate subroutine to adjust frequency. Ans also why the output stops when that subroutine is executing.

This limitation makes the pot-adjustment method impossible. The output can't be monitored and adjusted at the same time. This isn't a huge problem when using the PC to type in the desired frequency, but really limits any possibility of dynamic adjustment.

A more advanced way to solve the problem would be to use a high frequency interrupt to generate events at perhaps 1 micro-second and a subroutine to toggle the output state after the appropriate number of counts have occurred. A pot-reading routine could be incorporated to make adjustments on-the-fly.

This is a case of "good enough for what I need." A lot of features could be added to make a full-featured signal generator, but this is more than enough for simulating a sensor with a pulse stream output.

Forum Activity

  • No posts to display.

Member Access