News:

SMF for DIYStompboxes.com!

Main Menu

AVR-powered wavetable LFO

Started by idlefaction, March 19, 2006, 06:36:30 AM

Previous topic - Next topic

idlefaction

I have an LFO implemented on AVR, it's a DDS type LFO, which uses lookup tables, so it's a 'wavetable' type LFO.  You can put in hyper-sine, triangle, square, smooth out some white noise, whatever.  The extra neat thing is, the AtTiny15L I used requires NO ADDITIONAL COMPONENTS!  Clock and reset circuitry is all internal.

The only things you need are the AVR chip and a power supply bypass cap.  It has two CV inputs, one for Speed and the other for Wave which selects wavetables.  If you used an AtTiny25, which has more flash memory, you could add more wavetables.  The chip itself still has unused I/O pins and ADC converters so there is room to add UARTcontrol, footswitch for tap tempo, more knobs, whatever else...


; Voltage Controlled Low Frequency Oscillator
; Should use an AtTiny15L

; The PWM sine wave output is on pin 6 aka PB1.
; Input voltage for SPEED is on ADC1/PB2/pin 7. Valid voltages are 0-5V.
; Input voltage for WAVE is on ADC2/PB3/pin 3. Valid voltages are 0-5V.

; Timer0 Overflow interrupt starts a new ADC conversion.

; ADC Complete interrupt cycles through ADC channels 1&2.
; Channel 1 loads the ADC value into the adder register, controlling the
; output frequency.
; Channel 2 uses the 2 MSB of the ADC value to set the base address of
; the lookup table.

; Timer1 runs the dds. Timer1 runs all the time in
; PWM mode, stepping through values from the lookup table.

; The Watchdog Timer is in System Reset mode.

#include <avr/io.h>

; minimum speed - 16 bit number
#define fMINlo 0x02E
#define fMINhi 0x00

#define sreg_save r1
#define adc_channel r21

; 24-bit accumulator
#define acchi r30
#define accmd r29
#define acclo r28

; 24-bit adder register
#define addhi r20
#define addmd r19
#define addlo r18

; Used in initialisation & Timer0 overflow interrupt
#define temp r17
#define temp2 r16

.global main
main:
; set up the Watchdog Timer
; Timer set to 16K-cycles at 1MHz = 16 ms
in temp, _SFR_IO_ADDR(WDTCR)
sbr temp, (1<<WDE)
out _SFR_IO_ADDR(WDTCR), temp

; Turn off internal pullup resistor on PB2
in temp, _SFR_IO_ADDR(PORTB)
cbr temp, (1<<PB2)
out _SFR_IO_ADDR(PORTB), temp

        ; Set PB1 (OC1A) pin as output
; Set PB2 pin as input
; All other pins are inputs by default.
in temp, _SFR_IO_ADDR(DDRB)
sbr temp, (1<<DDB1)
cbr temp, (1<<DDB2)
out _SFR_IO_ADDR(DDRB), temp

; Set system clock / 256 as Timer0 source; 24Hz ADC sample rate
in temp, _SFR_IO_ADDR(TCCR0)
sbr temp, (1<<CS01) | (1<<CS00)
out _SFR_IO_ADDR(TCCR0), temp

        ; Set system clock * 16 as Timer1 source; 25.6MHz or a PWM
; frequency of 100kHz
        ; and set Timer1 to Non-inverted PWM mode
in temp, _SFR_IO_ADDR(TCCR1)
sbr temp, (1<<PWM1) | (1<<CS10) | (1<<COM1A1)
out _SFR_IO_ADDR(TCCR1), temp

        ; enable timer0 overflow interrupt
in temp, _SFR_IO_ADDR(TIMSK)
sbr temp, (1<<TOIE0)
out _SFR_IO_ADDR(TIMSK), temp

; ADC1 initially selected as input (PB2/pin 7)
in temp, _SFR_IO_ADDR(ADMUX)
sbr temp, (1<<MUX0)
out _SFR_IO_ADDR(ADMUX), temp

; enable ADC (ADEN=1)
; single conversion mode (ADFR=0)
; enable ADC complete interrupt (ADIE=1)
; ADC clock set to 100kHz - 1.6MHz / 16
in temp, _SFR_IO_ADDR(ADCSR)
sbr temp, (1<<ADEN) | (1<<ADPS2) | (1<<ADIE)
out _SFR_IO_ADDR(ADCSR), temp

        ; clear accumulator
        ldi     acchi,0x00
        ldi     accmd,0x00
        ldi     acclo,0x00

        ; clear adder registers
        ldi     addlo,0x01
        ldi     addmd,0x00
        ldi     addhi,0x00

; start with sine wave
ldi ZH, 0x01

; ADC channel starts by reading
; the SPEED control.
ldi adc_channel, 0x01

sei

        rjmp    DDS

; starts a new ADC conversion
; ADC_vect interrupt updates dds after the ADC finishes up.;
; 7 CLOCK CYCLES (+ 4 for the interrupt) @ 1.6MHz = 4.375 uS
; triggers every 40960 uS (0.04 sec, 24Hz)

.global SIG_OVERFLOW0
SIG_OVERFLOW0:
in sreg_save, _SFR_IO_ADDR(SREG)

; start a new conversion
        in      temp, _SFR_IO_ADDR(ADCSR)
        sbr     temp, (1<<ADSC)
        out     _SFR_IO_ADDR(ADCSR), temp

out _SFR_IO_ADDR(SREG), sreg_save
reti

; ADC has finished! Update dds with new ADC value
;
; 9 CLOCK CYCLES + 4 for interrupt @ 1.6MHz = 9.4 uS
; triggers every 40960 uS (0.04 sec, or 24Hz)
; triggers around 13 ADC clocks = 130 uS after each Timer0 overflow.

.global SIG_ADC
SIG_ADC:
in sreg_save, _SFR_IO_ADDR(SREG)

cpi adc_channel, 0x01
breq adc_channel_1
cpi adc_channel, 0x02
breq adc_channel_2
reti

; SPEED - ADC is ten bits so effectively 1024 possible values
adc_channel_1:
in addlo, _SFR_IO_ADDR(ADCL)
in addmd, _SFR_IO_ADDR(ADCH)

; multiply by 2 - this gets a useable range, I got this by trial and error.
lsl addlo
rol addmd

; add minimum value. if you make it 0x00, you can stop the
; oscillator.
ldi temp, fMINlo
add addlo, temp
ldi temp, fMINhi
adc addmd, temp

; set channel to 2
ldi adc_channel, 0x02
in temp, _SFR_IO_ADDR(ADMUX)
cbr temp, (1<<MUX0)
sbr temp, (1<<MUX1)
out _SFR_IO_ADDR(ADMUX), temp

out _SFR_IO_ADDR(SREG), sreg_save
reti

; WAVE - sets ZH to one of 0x01 (sine), 0x02 (saw)
adc_channel_2:
in ZH, _SFR_IO_ADDR(ADCH)
lsr ZH
inc ZH

; set channel to 1
ldi adc_channel, 0x01
in temp, _SFR_IO_ADDR(ADMUX)
sbr temp, (1<<MUX0)
cbr temp, (1<<MUX1)
out _SFR_IO_ADDR(ADMUX), temp

out _SFR_IO_ADDR(SREG), sreg_save
reti

; add value to accumulator
; load byte from current table in ROM
; output byte to port
; repeat
;
; 10 CLOCK CYCLES @ 1.6MHz = 6.25 uS

DDS:
; add the adder register to the accumulator
; acchi is actually ZL.
; ZH is set in the initialisation section.
        add acclo, addlo
        adc     accmd, addmd
        adc     acchi, addhi
; do the table lookup, result in r0
        lpm
; set the new PWM value
out _SFR_IO_ADDR(OCR1A), r0

wdr
rjmp DDS

; ugly kludge - determined by trial and error - ends up with sine at 0x0100
; this is because the dynamic linker determines the final addresses, not the .org directive.
.org 0x00EA
sine:

        .byte   0x80,0x83,0x86,0x89,0x8c,0x8f,0x92,0x95,0x98,0x9c,0x9f,0xa2,0xa5,0xa8,0xab,0xae
        .byte   0xb0,0xb3,0xb6,0xb9,0xbc,0xbf,0xc1,0xc4,0xc7,0xc9,0xcc,0xce,0xd1,0xd3,0xd5,0xd8
        .byte   0xda,0xdc,0xde,0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xed,0xef,0xf0,0xf2,0xf3,0xf5
        .byte   0xf6,0xf7,0xf8,0xf9,0xfa,0xfb,0xfc,0xfc,0xfd,0xfe,0xfe,0xff,0xff,0xff,0xff,0xff
        .byte   0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfd,0xfc,0xfc,0xfb,0xfa,0xf9,0xf8,0xf7
        .byte   0xf6,0xf5,0xf3,0xf2,0xf0,0xef,0xed,0xec,0xea,0xe8,0xe6,0xe4,0xe2,0xe0,0xde,0xdc
        .byte   0xda,0xd8,0xd5,0xd3,0xd1,0xce,0xcc,0xc9,0xc7,0xc4,0xc1,0xbf,0xbc,0xb9,0xb6,0xb3
        .byte   0xb0,0xae,0xab,0xa8,0xa5,0xa2,0x9f,0x9c,0x98,0x95,0x92,0x8f,0x8c,0x89,0x86,0x83
        .byte  0x80,0x7c,0x79,0x76,0x73,0x70,0x6d,0x6a,0x67,0x63,0x60,0x5d,0x5a,0x57,0x54,0x51
        .byte   0x4f,0x4c,0x49,0x46,0x43,0x40,0x3e,0x3b,0x38,0x36,0x33,0x31,0x2e,0x2c,0x2a,0x27
        .byte   0x25,0x23,0x21,0x1f,0x1d,0x1b,0x19,0x17,0x15,0x13,0x12,0x10,0x0f,0x0d,0x0c,0x0a
        .byte  0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x03,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00
        .byte  0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x02,0x03,0x03,0x04,0x05,0x06,0x07,0x08
        .byte  0x09,0x0a,0x0c,0x0d,0x0f,0x10,0x12,0x13,0x15,0x17,0x19,0x1b,0x1d,0x1f,0x21,0x23
        .byte  0x25,0x27,0x2a,0x2c,0x2e,0x31,0x33,0x36,0x38,0x3b,0x3e,0x40,0x43,0x46,0x49,0x4c
        .byte  0x4f,0x51,0x54,0x57,0x5a,0x5d,0x60,0x63,0x67,0x6a,0x6d,0x70,0x73,0x76,0x79,0x7c
saw:
.byte 0xff,0xfe,0xfd,0xfc,0xfb,0xfa,0xf9,0xf8,0xf7,0xf6,0xf5,0xf4,0xf3,0xf2,0xf1,0xf0
.byte 0xef,0xee,0xed,0xec,0xeb,0xea,0xe9,0xe8,0xe7,0xe6,0xe5,0xe4,0xe3,0xe2,0xe1,0xe0
.byte 0xdf,0xde,0xdd,0xdc,0xdb,0xda,0xd9,0xd8,0xd7,0xd6,0xd5,0xd4,0xd3,0xd2,0xd1,0xd0
.byte 0xcf,0xce,0xcd,0xcc,0xcb,0xca,0xc9,0xc8,0xc7,0xc6,0xc5,0xc4,0xc3,0xc2,0xc1,0xc0
.byte 0xbf,0xbe,0xbd,0xbc,0xbb,0xba,0xb9,0xb8,0xb7,0xb6,0xb5,0xb4,0xb3,0xb2,0xb1,0xb0
.byte 0xaf,0xae,0xad,0xac,0xab,0xaa,0xa9,0xa8,0xa7,0xa6,0xa5,0xa4,0xa3,0xa2,0xa1,0xa0
.byte 0x9f,0x9e,0x9d,0x9c,0x9b,0x9a,0x99,0x98,0x97,0x96,0x95,0x94,0x93,0x92,0x91,0x90
.byte 0x8f,0x8e,0x8d,0x8c,0x8b,0x8a,0x89,0x88,0x87,0x86,0x85,0x84,0x83,0x82,0x81,0x80
.byte 0x7f,0x7e,0x7d,0x7c,0x7b,0x7a,0x79,0x78,0x77,0x76,0x75,0x74,0x73,0x72,0x71,0x70
.byte 0x6f,0x6e,0x6d,0x6c,0x6b,0x6a,0x69,0x68,0x67,0x66,0x65,0x64,0x63,0x62,0x61,0x60
.byte 0x5f,0x5e,0x5d,0x5c,0x5b,0x5a,0x59,0x58,0x57,0x56,0x55,0x54,0x53,0x52,0x51,0x50
.byte 0x4f,0x4e,0x4d,0x4c,0x4b,0x4a,0x49,0x48,0x47,0x46,0x45,0x44,0x43,0x42,0x41,0x40
.byte 0x3f,0x3e,0x3d,0x3c,0x3b,0x3a,0x39,0x38,0x37,0x36,0x35,0x34,0x33,0x32,0x31,0x30
.byte 0x2f,0x2e,0x2d,0x2c,0x2b,0x2a,0x29,0x28,0x27,0x26,0x25,0x24,0x23,0x22,0x21,0x20
.byte 0x1f,0x1e,0x1d,0x1c,0x1b,0x1a,0x19,0x18,0x17,0x16,0x15,0x14,0x13,0x12,0x11,0x10
.byte 0x0f,0x0e,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00


It's written to compile using gcc under linux.

Adding Tap Tempo is on my list, I want to make it MILAN compatible so I'm not entirely sure how to go about it just yet.  Perhaps just a UART input that accepts 'wavetable' and 'speed' values, and the tap tempo can be implemented in a seperate box - I like being able to space my stomp switches out a bit!

Enjoy, comments welcome, this is my first assembler program  :-)
Darren
NZ

idlefaction

Oh yeah, the other neato thing about the AtTiny15L is that it's an 8-pin micro, available in 8-pin DIP package, so really small and easy to use in homebrew projects.  =)

And they cost less than a dollar.
Darren
NZ

Peter Snowberg

VERY cool Darren! 8) 8) 8)

Unfortunately the Tiny15 can't speak MILAN because it lacks the USART but I keep thinking of a low speed sub-set version that would allow for simpler control interfaces. Maybe a simple 19200bps link? I dunno.

One of the great things about AVRs is that the code can just be ported to a chip that does have the serial peripherials.

Great job!
Eschew paradigm obfuscation

R.G.

Good work. This is a great AVR version of the 8 pin PIC LFO I've mentioned in the fora before.

QuoteThe only things you need are the AVR chip and a power supply bypass cap.
Yep. That plus an LED/resistor pair and some means of programming is all you need to develop it too.

Again, good work.

Now get that tap tempo in there. I've already posted the pseudocode on the effects forum before.
R.G.

In response to the questions in the forum - PCB Layout for Musical Effects is available from The Book Patch. Search "PCB Layout" and it ought to appear.

Dave_B

Help build our Wiki!

moosapotamus

moosapotamus.net
"I tend to like anything that I think sounds good."

bioroids

Hi

Thanks for posting this!

I have a question anyway: why do you need to use 24 bit accumulator/adder if the wavetables are only 256 bytes long ? ??? Is this to allow lower frequency oscillations (i.e. with more than 256 steps/cycle) without using longer tables?

Luck!

Miguel
Eramos tan pobres!

idlefaction

Quote from: bioroids on March 26, 2006, 09:23:58 PM
I have a question anyway: why do you need to use 24 bit accumulator/adder if the wavetables are only 256 bytes long ? ??? Is this to allow lower frequency oscillations (i.e. with more than 256 steps/cycle) without using longer tables?

There are ways you could optimise this code but for general consumption I figured I'd make it as simple to understand what I was doing as possible.  The reason I used 24 bits was to do with the range and minimum frequencies you could possibly get out of this code if you wanted to.  I think it's worth stepping thru it so here goes!

The number of bits in the accumulator determines the resolution of the frequency:

Resolution (0.00953Hz) = frequency that the DDS loop runs at (1/6.25 uS = 160000Hz) / 2^number of bits in accumulator
Output Frequency = Value of adder * Resolution

The resolution with a 16 bit accumulator is 2.44Hz, not fine enough for a phaser or tremolo.  The 24 bit accumulator gave a resolution of 0.00953Hz which is great.

The max frequency with an 8 bit adder is 0.00953*(2^8)=2.43Hz.  I wanted to be able to go faster than this, so I gave it a 16-bit adder next, allowing the top speed to be 0.00953*(2^16)=624Hz.  But a 24 bit adder, which is something you might want to have, would add another line to the DDS loop which changes the maths below; in order to simplify it for myself, I just made the DDS loop use the 24 bit adder so I could plug and play values into the relevant formula below, to re-use this code a bit easier.

If you want a specific frequency range for your particular instance of this LFO, you could get the fMIN and the amount to scale the ADC this way:

0.00953Hz * ( 2 ^ n ) = frequency range

Plug values in for n to find the number of bits you need to cover your intended freqeuncy range.  In my case I wanted about a 20Hz range so 0.00953 * (2^11) = 19.5Hz was perfect.  I had a 10 bit ADC value, which I needed to convert to 11 bits, so I left shifted it once:

; multiply by 2 - this gets a useable range, I got this by trial and error.
lsl addlo
rol addmd

If you decided to make it work over a 20Hz-20kHz range, as a bench function generator perhaps, which is 0.00953Hz * (2^21) = 19.99kHz, you'd need a 21 bit range in the adder. So left shift the 10-bit ADC value eleven times.  You could left shift three times and read the ADC values directly into addmd and addhi instead.  The eight bits in addlo would provide the other eight bits of left-shifting.

You'll notice that left shifting changes the resolution of the frequency output also!  Since we still only have a 10-bit ADC, the output resolution will be:

Resolution of output frequency = Frequency range / 2^10
(or, substituting in the formula for frequency range above & simplifying -)
Resolution of output frequency = 0.00953Hz * (2 ^ number of bits left shifted)

So with the 20Hz/11-bit range, the resolution is 0.019Hz, and for the 20Hz-20kHz function generator, the resolution is 19.5Hz.

To find the value of fMIN to use in the code:

fMIN = minimum freqeuncy / 0.00953Hz

So in my case I wanted a minimum freqeuncy of 0.44Hz which came out at 0x2E.  If you wanted 0.5Hz you would get 0x34.  For our 20Hz-20kHz frequency generator, you would use 0x0832.

You can see that a 16 bit adder is enough for a phaser or tremolo pedal.  But if you did want to use it at audio frequencies, you need a 24 bit adder.  So I made the adder a 24 bit number to accomodate that without needing to change the DDS loop which changes the 0.0953Hz value permeating the above maths!

Whew!  I'm glad there's a record of that on the internet now cos I think that would be pretty helpful for someone wanting to use this code for something.  If anyone wants to port it to compile in AVR Studio, feel free. 

=D
Darren
NZ

idlefaction

Quote from: bioroids on March 26, 2006, 09:23:58 PM
I have a question anyway: why do you need to use 24 bit accumulator/adder if the wavetables are only 256 bytes long ? ??? Is this to allow lower frequency oscillations (i.e. with more than 256 steps/cycle) without using longer tables?

Oh, and in actual answer to your question  ;) , if you think of what's happening in the accumulator, it can be considered as three 8 bit numbers.  When the first one overflows, the carry goes into the next one up.  So you've hit the nail on the head, this allows you to have slower oscillations and make a really accurate sine wave at lower frequencies even though the wavetable's only 8 bits long.

=D
Darren
NZ

bioroids

Thanks for the explanation!

Dees this method provides a good resolution (that may be the wrong term), I mean, at the lower frequencies you are gonna stay at the same "sample" on the table for several cycles, if I understand it right.

Luck

Miguel
Eramos tan pobres!

Peter Snowberg

When you start indexing through a wave table at a slow enough rate to start repeating data and you have more output bits available than your wave table is wide, you are best off with using interpolation to find a value in-between the values of your table. When the table and your D/A have the same resolution, interpolation is not necessary.
Eschew paradigm obfuscation

Dave_B

Is there a best way to get interpolated values?  My thinking:
======================
Example:
Current LFO Value = 15,000
Next LFO Value = 16,000
To double the amount of data, you'd:
    1) Subtract the "next" value from the "current" value.  Giving you 1,000 in this case.
    2) Divide it by two.  Leaving you with 500.
    3) Add that number to the "current" value.  15,500.
======================
Of course you'd have to subtract when the waveform is heading downward. 
Is that right?  We're not talking exponential, are we?
For those who've never worked with these critters, it's real easy to divide and multiply by two, four, eight, etc.
Help build our Wiki!

bioroids

Yes, that would be linear interpolation if I'm right. Basically you're drawing a straight line between the two data points.

I think that's much better than no interpolation at all. Does it make sense to use more advanced methods like polynoms or splines? Assuming the AVR can handle the math...

Luck

Miguel
Eramos tan pobres!

Peter Snowberg

Division and multiplication by powers of 2 are amazingly simple in binary math. The AVRs also have a 2 cycle hardware multiplier.
Eschew paradigm obfuscation

idlefaction

As Peter pointed out, you'd only want to go to the lengths of this sort of thing if you had more bits in your output than your wavetable.  Since the PWM is running off an 8-bit counter in this code, giving it 8 bits of resolution, and the wavetable is also 8 bits, you'd gain very little...

If you think of a 1Hz output frequency, with a 100kHz PWM output, it steps through 256 values once in a second so only actually changes value every 400 or so samples!!!

Adding interpolation would be fun and a good thing to know how to do tho so feel free  =D

I haven't built the hardware to test the additions yet, and suspect I'll have to do a bit of debugging first, but the latest version of this I'm using for a tremolo pedal.  I've added two more channels of ADC - another pot and an LDR.  The pot uses the 2 MSB to choose between four 'modes' (off/subtract/add/off), the LDR is added or subtracted from the speed depending on the setting.

One of the 'off' settings is reserved for later tinkering - I thought I'd have a go at using the LDR voltage as a 'tap tempo' input!  Will let y'all know how I fare.  It seems that I'd want to up the ADC sampling frequency quite a lot if I did this. 

Cheers,
Darren
NZ

Dirk_Hendrik

#15
I was working on the same thing yesterday and made 2 tables. One contained 90 values which together made up a quarter sine wave when graphed. The other contained 22 values which together made up a sine wave when graphed. Naturally the resolution of the 22 value wave was a lot rougher.

When interpolating between these 22 values to get a total of 90 values, the error when comparing them to the values in the first table was never more than 1 bit (at 8 bit resolution) and only happened about 5 times.  However, form a memory perspective it's 4 times smaller than storing 90 values (or even more if you want to store the whole wave cycle)

source used:
http://www.dattalo.com/technical/theory/sinewave.html
More stuff, less fear, less  hassle and less censoring? How 'bout it??. To discuss what YOU want to discuss instead of what others decide for you. It's possible...

But not at diystompboxes.com...... regrettably

idlefaction

Quote from: Dirk_Hendrik on May 03, 2006, 10:05:05 AM
When interpolating between these 22 values to get a total of 90 values, the error when comparing them to the values in the first table was never more than 1 bit (at 8 bit resolution) and only happened about 5 times.  However, form a memory perspective it's 4 times smaller than storing 90 values (or even more if you want to store the whole wave cycle)

That's really cool.  I didn't follow the maths through, but it's pretty amazing that he ended up finding you could do a 1% error sine wave using an 8 value table  :-o
Darren
NZ

Dirk_Hendrik

Not 1%. Only 1 bit in  8 bit quantization. Than means only 1/256*100= 0.39%  :icon_wink:

More stuff, less fear, less  hassle and less censoring? How 'bout it??. To discuss what YOU want to discuss instead of what others decide for you. It's possible...

But not at diystompboxes.com...... regrettably

idlefaction

Quote from: Dirk_Hendrik on May 06, 2006, 03:46:55 AM
Not 1%. Only 1 bit in  8 bit quantization. Than means only 1/256*100= 0.39%  :icon_wink:



My mistake!   :D  :D  :D  :D
Darren
NZ

Ronan

Nearly 9 years later, I'm thinking about a simple digital LFO for a phaser. Is this (above, first post) a good way to go? I don't need anything too fancy. I have ATtiny13, and a programmer, I used to write in assembly but the last time I did it was about 4 or 5 years ago, so I'm a bit rusty on it, but that can be fixed. I guess I'm basically asking, are there better ways to do it now that some time has passed? I would hope to use an 8-pin uC and just a simple sine wave and triangle out (PWM) would be fine. I am thinking of using an opamp after the uC to scale and invert the output as required to interface with LM13700, or use an LED/LDR interface before the opamp. Just wondering what the current concensus is, any comments welcome.