Tap Tempo LFO with PIC 16F684

Started by ElectricDruid, May 13, 2009, 05:22:13 AM

Previous topic - Next topic

orangeshasta

Hi all,

Sorry for reviving an old thread, but I've got a question.  I've ported a basic version of this assembly code over to C (using the CCS compiler, it's what I know) for the purposes of making a tremolo pedal.  I'm still using the 16F684; and the only potentiometers that I have implemented are frequency and duty cycle (although I do have different waveforms hardcoded for now).  My pins are different, but that shouldn't be an issue.

Anyway, things are working pretty well all-in-all, with one exception: the duty cycle control has an effect on the frequency of the LFO.  I've tried all sorts of things to figure out how to fix this, but haven't come up with anything.  Anyone have any ideas?  I'll put a copy of my code below:


#include <16f684.h>
#device adc=8

#FUSES NOWDT                    //No Watch Dog Timer
#FUSES HS                       //High speed Osc (> 4mhz for PCM/PCH) (>10mhz for PCD)
#FUSES NOBROWNOUT               //No brownout reset
#FUSES PUT                      //Power Up Timer
#FUSES NOCPD                    //No EE protection
#FUSES IESO                     //Internal External Switch Over mode enabled
#FUSES MCLR                     //Master Clear pin enabled
#FUSES NOPROTECT                //Code not protected from reading

#use delay(clock=20M,xtal=20M)

/*********************************************
/  Declare pin defines
/*********************************************/
#define PIN_SPEED_CTRL PIN_C0
#define PIN_DUTY_CTRL PIN_C1
#define PIN_MULT_CTRL PIN_C2
#define PIN_DEPTH_CTRL PIN_C3
#define PIN_SHAPE_CTRL PIN_C4 //Damn, need to change to an analog pin
#define PIN_TEMPO PIN_A2

/*********************************************
/  Declare global variables
/*********************************************/

int1 calc_next_pwm=0, time_to_read_adcs=0;

//Half sine wave lookup table
//Actually a cosine table because it starts
//with max value.
static const unsigned int8 halfsine_table[256] = {
255, 255, 255, 255, 255, 255, 255, 255,
254, 254, 254, 254, 254, 253, 253, 253,
253, 252, 252, 252, 251, 251, 250, 250,
249, 249, 249, 248, 248, 247, 246, 246,
245, 245, 244, 243, 243, 242, 241, 241,
240, 239, 238, 238, 237, 236, 235, 234,
233, 233, 232, 231, 230, 229, 228, 227,
226, 225, 224, 223, 222, 221, 220, 219,
218, 216, 215, 214, 213, 212, 211, 209,
208, 207, 206, 205, 203, 202, 201, 199,
198, 197, 195, 194, 193, 191, 190, 189,
187, 186, 185, 183, 182, 180, 179, 177,
176, 175, 173, 172, 170, 169, 167, 166,
164, 163, 161, 160, 158, 157, 155, 154,
152, 150, 149, 147, 146, 144, 143, 141,
140, 138, 137, 135, 133, 132, 130, 129,
127, 126, 124, 122, 121, 119, 118, 116,
115, 113, 111, 110, 108, 107, 105, 104,
102, 101, 99, 98, 96, 95, 93, 92,
90, 89, 87, 86, 84, 83, 81, 80,
78, 77, 75, 74, 73, 71, 70, 68,
67, 66, 64, 63, 62, 60, 59, 58,
56, 55, 54, 52, 51, 50, 49, 47,
46, 45, 44, 43, 41, 40, 39, 38,
37, 36, 35, 34, 33, 32, 31, 30,
29, 28, 27, 26, 25, 24, 23, 22,
21, 20, 19, 19, 18, 17, 16, 15,
15, 14, 13, 13, 12, 11, 11, 10,
9, 9, 8, 8, 7, 7, 6, 6,
5, 5, 4, 4, 4, 3, 3, 3,
2, 2, 2, 2, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0
};

//This is to convert from an 8-bit A/D value into a 16-bit freq_inc value
//Provides a logarithmic response to the FREQ control using a linear pot
static const unsigned int16 freq_table[256] = {
0x0016, 0x0016, 0x0016, 0x0017, 0x0017, 0x0018, 0x0018, 0x0019,
0x001A, 0x001A, 0x001B, 0x001B, 0x001C, 0x001D, 0x001D, 0x001E,
0x001E, 0x001F, 0x0020, 0x0020, 0x0021, 0x0022, 0x0023, 0x0023,
0x0024, 0x0025, 0x0026, 0x0027, 0x0027, 0x0028, 0x0029, 0x002A,
0x002B, 0x002C, 0x002D, 0x002E, 0x002F, 0x0030, 0x0031, 0x0032,
0x0033, 0x0034, 0x0035, 0x0037, 0x0038, 0x0039, 0x003A, 0x003C,
0x003D, 0x003E, 0x0040, 0x0041, 0x0042, 0x0044, 0x0045, 0x0047,
0x0048, 0x004A, 0x004C, 0x004D, 0x004F, 0x0051, 0x0052, 0x0054,
0x0056, 0x0058, 0x005A, 0x005C, 0x005E, 0x0060, 0x0062, 0x0064,
0x0066, 0x0069, 0x006B, 0x006D, 0x0070, 0x0072, 0x0075, 0x0077,
0x007A, 0x007C, 0x007F, 0x0082, 0x0085, 0x0088, 0x008B, 0x008E,
0x0091, 0x0094, 0x0097, 0x009A, 0x009E, 0x00A1, 0x00A5, 0x00A8,
0x00AC, 0x00B0, 0x00B4, 0x00B8, 0x00BC, 0x00C0, 0x00C4, 0x00C8,
0x00CD, 0x00D1, 0x00D6, 0x00DA, 0x00DF, 0x00E4, 0x00E9, 0x00EE,
0x00F3, 0x00F9, 0x00FE, 0x0104, 0x0109, 0x010F, 0x0115, 0x011B,
0x0121, 0x0128, 0x012E, 0x0135, 0x013C, 0x0142, 0x014A, 0x0151,
0x0158, 0x0160, 0x0167, 0x016F, 0x0177, 0x0180, 0x0188, 0x0190,
0x0199, 0x01A2, 0x01AB, 0x01B5, 0x01BE, 0x01C8, 0x01D2, 0x01DC,
0x01E7, 0x01F1, 0x01FC, 0x0207, 0x0213, 0x021E, 0x022A, 0x0236,
0x0243, 0x024F, 0x025C, 0x026A, 0x0277, 0x0285, 0x0293, 0x02A2,
0x02B0, 0x02BF, 0x02CF, 0x02DF, 0x02EF, 0x02FF, 0x0310, 0x0321,
0x0333, 0x0344, 0x0357, 0x0369, 0x037D, 0x0390, 0x03A4, 0x03B9,
0x03CD, 0x03E3, 0x03F8, 0x040F, 0x0425, 0x043D, 0x0454, 0x046D,
0x0486, 0x049F, 0x04B9, 0x04D3, 0x04EE, 0x050A, 0x0526, 0x0543,
0x0561, 0x057F, 0x059E, 0x05BD, 0x05DD, 0x05FE, 0x0620, 0x0642,
0x0665, 0x0689, 0x06AE, 0x06D3, 0x06F9, 0x0720, 0x0748, 0x0771,
0x079B, 0x07C5, 0x07F1, 0x081E, 0x084B, 0x0879, 0x08A9, 0x08DA,
0x090B, 0x093E, 0x0972, 0x09A7, 0x09DD, 0x0A14, 0x0A4C, 0x0A86,
0x0AC0, 0x0AFD, 0x0B3B, 0x0B7A, 0x0BBA, 0x0BFC, 0x0C3F, 0x0C84,
0x0CCA, 0x0D12, 0x0D5B, 0x0DA6, 0x0DF2, 0x0E41, 0x0E91, 0x0EE2,
0x0F36, 0x0F8B, 0x0FE2, 0x103B, 0x1096, 0x10F3, 0x1152, 0x11B3,
0x1216, 0x127C, 0x12E3, 0x134D, 0x13B9, 0x1428, 0x1499, 0x150C
};

#int_TIMER1
void  TIMER1_ISR(void)
{
clear_interrupt(INT_TIMER1);
time_to_read_adcs = 1;
}

// PWM timer interrupt
// Do calculations in the main loop to keep interrupt short
#int_TIMER2
void  TIMER2_isr(void)
{
clear_interrupt(INT_TIMER2);
calc_next_pwm = 1;
}

#int_EXT
void  EXT_isr(void)
{

}

void main()
{
int8 eightbit, lookup;
int8 wavetype=4;
int32 phase=0;
int32 freq_32,phase_inc_a,phase_inc_b;
int16 pwm_duty=0;

int8 freq_cv=1, dist_cv=128; // Placeholder for frequency and distortion potentiometers
// Keep dist between 4 and 252 for best results.  128 is zero dist.
int8 freq_mul=8;
int16 freq_16;

int32 num_samples, num_a, num_b;

setup_adc_ports(sAN4|sAN5|sAN6|sAN7|VSS_VDD);
setup_adc(ADC_CLOCK_INTERNAL);
setup_wdt(WDT_OFF);
setup_timer_1(T1_INTERNAL|T1_DIV_BY_4);
//setup_timer_2(T2_DIV_BY_4,127,1); // PWM Freq. of 19.5 kHz //Prescaler of 127 limits pwm output to 8 bit
setup_timer_2(T2_DIV_BY_1,255,1); // PWM Freq. of 19.5 kHz
setup_ccp1(CCP_PWM);
set_pwm1_duty(0);

enable_interrupts(INT_TIMER1);
enable_interrupts(INT_TIMER2);
//enable_interrupts(INT_EXT);
enable_interrupts(GLOBAL);
//set_pwm1_duty(duty);
   while(TRUE)
   {
if(calc_next_pwm)
{
if(bit_test(phase,23)) // 24-bit phase accumulator - check to see which half of waveform we're on
phase = (phase + phase_inc_b) % 16777216; // Limit phase to 24-bit
else
phase = (phase + phase_inc_a) % 16777216; // Limit phase to 24-bit

switch(wavetype)
{
case 0: // Sine
eightbit = make8((phase >> 15),0); // MSB indicates which half of the wave, only take bits 16-23
// Eightbit is used to index into LUT
lookup = halfsine_table[eightbit];
if(bit_test(phase,23)) // If we're on second half of wave
lookup ^= 255; // Invert the lookup value using XOR
pwm_duty = (int16) lookup * 4; // Level control, set at max for now
break;
case 1: // Ramp up
eightbit = make8((phase >> 16),0);
pwm_duty = (int16) eightbit * 4; // Level control, set at max for now
break;
case 2: // Ramp down
eightbit = make8((phase >> 16),0);
pwm_duty = (int16) (255 - eightbit) * 4; // Level control, set at max for now
break;
case 3: // Square
if(bit_test(phase,23)) // If we're on second half of wave
pwm_duty = 0;
else
pwm_duty = (int16) 255 * 4; // Level control, set at max for now
break;
case 4: // Triangle
eightbit = make8((phase >> 15),0); // MSB indicates which half of the wave, only take bits 16-23
if(!bit_test(phase,23)) // If we're on second half of wave
pwm_duty = (int16) eightbit * 4; // Level control, set at max for now
else
pwm_duty = (int16) (255 - eightbit) * 4; // Level control, set at max for now
break;
}

set_pwm1_duty(pwm_duty); // This doesn't take effect until PWM timer overflow
calc_next_pwm = 0;
}
else if(time_to_read_adcs)
{
set_adc_channel(4);
freq_cv = read_adc();
set_adc_channel(5);
dist_cv = read_adc(); // Duty
freq_16 = freq_table[freq_cv] * 2; // Gets screwy at high values of freq_cv (>~210)
//freq_16 *= freq_mul; // Fixed by converting to int32 at mul. step
//freq_32 = (int32) freq_16 << 7;
freq_32 = (int32) freq_16 * freq_mul;
num_samples = 16777216 / freq_32;
freq_32 = freq_32 << 7; // Multiply by 128

if(dist_cv == 0)
phase_inc_a = freq_32; // Prevent divide-by-zero
else
phase_inc_a = freq_32 / dist_cv; // First half of wave, divide by dist value

if(dist_cv == 255)
phase_inc_b = freq_32; // Prevent divide-by-zero
else
phase_inc_b = freq_32 / (255 - dist_cv); // Second half of wave, divide by inverse

time_to_read_adcs = 0;
}
   }
}


Thanks in advance!

-Bob

potul

I guess that when you talk about duty cycle you refer to the phase distortion, right?  If done correctly, the phase distortion should not affect your overall frequency.
I've quickly reviewed your code and I can't find anything obviously wrong. The only thing I would maybe do differently is  instead of:

phase_inc_b = freq_32 / (255 - dist_cv);   // Second half of wave, divide by inverse

I would:

phase_inc_b = freq_32 / (256 - dist_cv);   // Second half of wave, divide by inverse

To ensure that with dist_cv=128 you get an undistorted wave.

(I think this can have as well some impact to the total freq)

orangeshasta

Hi, thanks for the response!

Yes, when I say "duty cycle" it's the phase distortion I'm referring to.

As for this:
phase_inc_b = freq_32 / (255 - dist_cv);   // Second half of wave, divide by inverse

My reasoning for doing it like that was to make sure that it compiled to an 8-bit subtraction rather than a 16-bit.  Just trying to save a couple cycles where I can; although to be honest, I've never checked the assembler output to see if it makes much difference.

Anyway I just tried using 256 there instead of 255, and there's no appreciable change to the fact that adjusting the phase distortion/duty cycle changes my frequency.  Any other ideas, anyone?

Thanks again!

potul

I don't see anything else that could be wrong.... Does it happen with all wave shapes?

I would try to debug in mplab sim and verify why the frequency changes.... A change in frequency when applying phase distortion seems to indicate there is an issue with the calculation of phase_inc_a and phase_inc_b. If they are correctly calculated, the total cycle should not change.

swinginguitar

I posted some questions like this over in the stompbox forum concerning the PIC tap LFO, but thought i'd move it over here to get a better shot at some feedback.

1) when using this code/PIC for tap tremolo, how do I select an LED and LDR for the output? Does the color/value matter?

2) what board should i get for flashing the PIC? Is there just a simple (read: cheap) USB interface out there that is recommended?

3) In general terms,  how are the various waveforms generated by the PIC? How does the waveform selector vary the waveform?

4) would it be more/less advantageous to hook this up to a digital pot rather than the LDR for modulating volume?

5) slightly off topic, but could a microcontroller be employed for channel switching (either in an amp/preamp or "multichannel pedal")? What other components would be needed?


orangeshasta

Hi Potul,

Yes, it happens with all wave shapes, so at least it's consistent.  That's a good idea using MPLAB sim; I'm used to using a debugger, but the 16F684 doesn't support one.  That sounds like a good alternative.  I'll play around with that this weekend.


Hi Swinginguitar,

1) I'm using a Silonex NSL-32 (http://www.silonex.com/audiohm/index.html), available from either Allied or Newark.  It's nice because the LED and LDR are packaged into one sealed package.

2) At home, I use a PicKit 2 for programming.  It's made by Microchip, is relatively inexpensive, and supports most of the PIC line.

3) In general terms, the PIC varies the duty cycle of its PWM output proportionally with the shape of the desired waveform.  To get the waveform from that signal, all you have to do is feed the PWM through a low-pass filter.  This is basically the same way that class-D amplifiers work.  Wikipedia has a great article on pulse-width modulation that explains the principle pretty well; also check out their article on Direct Digital Synthesis.

4) Personal preference, I guess - though I think you'd get a smoother output from the LDR.

5) Not sure exactly what you mean, but you could either use a microcontroller with some relays (higher fidelity, lower durability), or use an analog multiplexer IC like the 4051: http://search.digikey.com/scripts/DkSearch/dksus.dll?Detail&name=296-2057-5-ND

Hides-His-Eyes

The filter required to get a 'smooth' output using PWM without an opto is large. Could it be simplified with an inductor?

potul

Quote from: orangeshasta on May 06, 2011, 11:07:01 AM

3) In general terms, the PIC varies the duty cycle of its PWM output proportionally with the shape of the desired waveform.  To get the waveform from that signal, all you have to do is feed the PWM through a low-pass filter.  This is basically the same way that class-D amplifiers work.  Wikipedia has a great article on pulse-width modulation that explains the principle pretty well; also check out their article on Direct Digital Synthesis.


The DDS article in Electricdruid's web site is a great article to understand how to generate different waveforms using lookup tables. Algo the phase distortion explantion is a good read.

JKowalski

#128
Quote from: Hides-His-Eyes on May 06, 2011, 03:21:39 PM
The filter required to get a 'smooth' output using PWM without an opto is large. Could it be simplified with an inductor?

You can simplify it, but inductors are not needed.

Someone earlier PM'd me about doing some things with a TAPLFO chip, so I drew this up (i mentioned the inverter trick earlier in the topic)

This method gets around the difficulty in using non-rail-to-rail op amps to play with 0-5 volt signals... The circuit in ED's datasheet is made for synth things where bipolar supplies are common and the output signal whould be +/- 5v. The one below is made for single supply power and to give you a LFO around 4.5V.



Basically, the 0-5 volt input to the inverters will trigger a 0-9V output. If you divide this output down to the 4.5V Vref, you can get any amplitude PWM signal based around 4.5V. Sticking a filter onto this is easy.This will give you a 2-7V LFO waveform (2.5V peak centered around 4.5V) which was application specific for him. To change the max P-P voltage simply adjust the voltage divider (27k/30k) ratio.

If you have a spare op-amp in the circuit this is very easy to implement. And inverters are always useful!

The roll-off is pretty sharp. He tried it and said it works well. The only problem he had with it was ticking on sharp transistion waveforms (square, sawtooth). This is typical for those waveforms in any scenario and to fix that you could bypass the power rails better, lower the frequency cutoff a bit to smooth the sharp transistions (which also improves HF attenuation), etc.

Hides-His-Eyes


orangeshasta

Hi all again,

I finally got a chance to figure out MPLAB sim, so that I could check that all my math was executing properly.  As far as I can tell, none of my math operations are doing anything unexpected.  Anyone have any other ideas?  To anyone who's used ElectricDruid or JKowalski's code, is the frequency even supposed to stay constant when changing phase distortion?

Any and all ideas would be appreciated, I'm starting to get frustrated!  ???

potul

Have you checked the values of inc_a and inc_b in MPLAB? You should verify they stay correct when changing phase.

orangeshasta

Yup, they stay correct...maybe there's something wrong with my overall method?  I thought I copied the method from ElectricDruid's assembly code (and excellent website) almost exactly, but maybe I'm wrong.  One difference I know of is that I don't increment the phase and set the PWM at the interrupt level, to keep my interrupts short.  I wouldn't think that would make much difference though...

orangeshasta

Hi all one more time,

Not sure if anyone is curious; but I figured out my problem.  Turns out all my calculations WERE correct - it was simply an ADC problem.  I neglected to add a delay after changing ADC channels, so the reading of the freq_cv value was actually being affected by the reading of the previous dist_cv value.  Once I added a delay of about 25uS (just like ElectricDruid does in his assembly code), it works like a charm.

Thanks to potul for the help - if you hadn't reminded me about the MPLAB SIM, I never would have figured that out!

potul

I'm happy to see you solved the issue.... those ADC problems are usually hard to troubleshoot.

swinginguitar

#135
Reviving this thread to ask a question after looking at the code a little more:

what does 9177280/mSecs represent in this line?:

; Do a calculation of 9177280/mSecs to find required RAW_INC
call   TempoCalculation

Also, what is this?:

; Set up limits for the Tempo CV
   ; Set Upper limit
   ; hi = tempo cv + 4
   ; addlw 255-Hi
   movlw   D'4'
   addwf   TEMPO_CV, w
   movwf   TEMPO_UPPER
   comf   TEMPO_UPPER, f   ; Turn Hi into 255-Hi

   ; Set lower limit
   ; lo = tempo_cv -4
   ; addlw   (Hi-lo)+1
   ; Note that this always gives me 9, since hi-lo cancels
   ; the tempo cv.
   movlw   D'9'
   movwf   TEMPO_LOWER

ElectricDruid

Quote from: swinginguitar on June 29, 2011, 02:50:42 PM
Reviving this thread to ask a question after looking at the code a little more:

what does 9177280/mSecs represent in this line?:

; Do a calculation of 9177280/mSecs to find required RAW_INC
call   TempoCalculation

Aha! Now this I *do* know!

The tempo is calculated by using a timer to measure the time between two taps. This time (in milliseconds) is then used to calculate the increment that the LFO would have to use to generate the same frequency. Since the phase accumulator is 24-bit (e.g. 16777215 max value) and the output sample rate is 19.5KHz, and the RAW_INC base rate is actually 1/2 the real tempo (to avoid fractions in the multipliers), the magic number needed comes out at 9177280.


Quote from: swinginguitar on June 29, 2011, 02:50:42 PM
Also, what is this?:

; Set up limits for the Tempo CV
   ; Set Upper limit
   ; hi = tempo cv + 4
   ; addlw 255-Hi
   movlw   D'4'
   addwf   TEMPO_CV, w
   movwf   TEMPO_UPPER
   comf   TEMPO_UPPER, f   ; Turn Hi into 255-Hi

   ; Set lower limit
   ; lo = tempo_cv -4
   ; addlw   (Hi-lo)+1
   ; Note that this always gives me 9, since hi-lo cancels
   ; the tempo cv.
   movlw   D'9'
   movwf   TEMPO_LOWER

This is because the tempo can be set by either taps on the tap tempo button or by twisting the tempo knob. In the case that the tempo has just been set by the tap tempo button, we don't want the next reading from the ADC for the tempo knob to change the setting, *unless the knob has moved*. So we set up a couple of upper and lower limits, and if the ADC reading goes outside of those, then we reckon the knob has been moved, and the tempo knob position sets the tempo. If it wasn't done like this, a little tiny bit of noise in the ADC results (which there always is) would reset the tapped tempo to whatever the current knob position is.

Hope this helps - if you've got more questions about the magic number, PM me and I'll dig out the notes I made about it.
Tom

frequencycentral

Woot! Just got my first TAPLFO2 going. This will become part of my modular synth. Now I'm working  on a second version with CV inputs and attenuators for Tempo, Waveform, Multiplier and Wave Distortion.

Thanks Tom for a really great project!



http://www.frequencycentral.co.uk/

Questo è il fiore del partigiano morto per la libertà!

Bunyaman

Hey men! Does anyone have a PCB of this LFO without tremolo module (Sprint layout or other soft, or just a picture). I want to use it in other effects(delays, tromolos,phasers,  etc). I will be grateful!
Just a week ago, thought about TAP TEMPO, when suddenly I found this super project. Author, RESCECT!

p.s. Sorry for my english, if it not quite correct, I`m from Ukraine :)

.Mike

I think my PIC Tremolo that uses Tom's VCLFO is the closest you are going to get. You can easily remove the tremolo components.

The VCLFO came before the TAPLFO. They are very similar.

You will only have to do minimal modifications to the schematic, since the datasheet TAPLFO tremolo circuit is based on my drawing.

http://www.diystompboxes.com/smfforum/index.php?topic=77471.0

Good luck! :)

Mike
If you're not doing it for yourself, it's not DIY. ;)

My effects site: Just one more build... | My website: America's Debate.