Sine wave generator
Hi everyone, I am very new to the propeller, assembly, and this forum. I am trying to build a function generator and so I am trying to output a sin wave on 8 output pins to go to a D/A converter. I have successfully output a ramp, so I know my LSB and MSB are in the right place and for that function the output pins were performing as expected. I am trying to use the sine table with no success. I was hoping someone could point me in a better direction. With some embarrassment I give you what I have so far:
CON
_clkmode = xtal1 + pll16x
_xinfreq = 5_000_000
VAR
byte index
byte angles[360]
'address:=@angles[0]
PUB main
repeat 360
angles[index] := index
index+=1
dira[0..7]~~
cognew(@begin,1)
address:=@angles[0]
DAT
begin mov dira,#$FF
movs ptr,address 'mov ptr,angles[0] 'Point to first array element.
:loop movs :add,ptr 'Put pointer value into add instruction's source field
add ptr,#1 'Increment the pointer. (Due to pipelining, there has to be at least one instruction here.)
rdlong sin,address 'move value stored at address into sin
:add add address,0-0 'add ptr to address
call #getsin 'call get sin
'wrlong sin,sin 'write value of sin to sin
mov outa,sin 'write sin to output pins
djnz ctr,#:loop 'decrement counter and restart loop
movs ctr,#360 'Initialize counter.
movs ptr,address
jmp #:loop
getsin test sin,sin_90 wc 'get quadrant 2|4 into c
test sin,sin_180 wz 'get quadrant 3|4 into nz
negc sin,sin 'if quadrant 2|4, negate offset
or sin,sin_table 'or in sin table address >> 1
shl sin,#1 'shift left to get final word address
rdlong sin,sin 'read word sample from $E000 to $F000
negnz sin,sin 'if quadrant 3|4, negate sample
getsin_ret ret ' (this subroutine adapted from Prop manual)
sin_90 long $0800
sin_180 long $1000
sin_table long $E000 >>1
sin long 0
ptr long 0
ctr long 360
address long @address
I am uncertain I am accessing the array correctly, or even declaring it correctly. On an oscilloscope I get a periodic but non-coherent output from the DAC. I am also fairly sure I am not calling get_sin correctly as when I comment the entire routine out, my output is the same. Any help would be appreciated. Also, does anyone know more about programming the propeller in C? That would be easier for me for sure. Also any tips for debugging propeller asm?
CON
_clkmode = xtal1 + pll16x
_xinfreq = 5_000_000
VAR
byte index
byte angles[360]
'address:=@angles[0]
PUB main
repeat 360
angles[index] := index
index+=1
dira[0..7]~~
cognew(@begin,1)
address:=@angles[0]
DAT
begin mov dira,#$FF
movs ptr,address 'mov ptr,angles[0] 'Point to first array element.
:loop movs :add,ptr 'Put pointer value into add instruction's source field
add ptr,#1 'Increment the pointer. (Due to pipelining, there has to be at least one instruction here.)
rdlong sin,address 'move value stored at address into sin
:add add address,0-0 'add ptr to address
call #getsin 'call get sin
'wrlong sin,sin 'write value of sin to sin
mov outa,sin 'write sin to output pins
djnz ctr,#:loop 'decrement counter and restart loop
movs ctr,#360 'Initialize counter.
movs ptr,address
jmp #:loop
getsin test sin,sin_90 wc 'get quadrant 2|4 into c
test sin,sin_180 wz 'get quadrant 3|4 into nz
negc sin,sin 'if quadrant 2|4, negate offset
or sin,sin_table 'or in sin table address >> 1
shl sin,#1 'shift left to get final word address
rdlong sin,sin 'read word sample from $E000 to $F000
negnz sin,sin 'if quadrant 3|4, negate sample
getsin_ret ret ' (this subroutine adapted from Prop manual)
sin_90 long $0800
sin_180 long $1000
sin_table long $E000 >>1
sin long 0
ptr long 0
ctr long 360
address long @address
I am uncertain I am accessing the array correctly, or even declaring it correctly. On an oscilloscope I get a periodic but non-coherent output from the DAC. I am also fairly sure I am not calling get_sin correctly as when I comment the entire routine out, my output is the same. Any help would be appreciated. Also, does anyone know more about programming the propeller in C? That would be easier for me for sure. Also any tips for debugging propeller asm?
Comments
If I understand your code right you want to read the angle array in the PASM cog and write the resulting sine value to P0..P7.
For that you need to pass the address of angle[0] to the PASM cog in the PAR register:
cognew(@begin, @angle[0])
remove all the lines with 'address' in the Spin code.In the PASM cog you need to move the pointer in par to the variable address, then you can increment this later (PAR is not changeable)
begin mov address, par mov dira, #$FF
You don't need all the indirect addressing with movs, you can just read the phase with rdbyte sin, address, and then increment address:rdbyte sin, address add address, #1 call #getsin
Now you get the sin value in the range -$FFFF...+$FFFF from the sine table in ROM. to fit this to 8 bits you can shift it by 17:sar sin, #17 mov outa, sin
do that in a loop 360 times and with the ctr variable as you have done already and then move address back to the first array element: address=par.Andy
Edit: I just forget that the sin routine needs the angle not from 0 to 359, but from 0 to 8191 for a full circle, so your Spin code must declare a word array for angle and fill it with
angle[index] := index *8192 / 360
Then in the PASM code you read the array with rdword instead od rdbyte and increment the address by 2.
CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 VAR byte index word angles[360] PUB main repeat 360 angles[index] := index*8192/360 index+=1 dira[0..7]~~ cognew(@begin,1) DAT begin mov address, par mov dira,#$FF :loop rdword sin, address add address, #2 call #getsin 'call get sin shr sin, #17 mov time,cnt add time,delay waitcnt time,delay mov outa, sin 'write sin to output pins djnz ctr,#:loop 'decrement counter and restart loop mov ctr,360 'Initialize counter. mov address,par jmp #:loop getsin test sin,sin_90 wc 'get quadrant 2|4 into c test sin,sin_180 wz 'get quadrant 3|4 into nz negc sin,sin 'if quadrant 2|4, negate offset or sin,sin_table 'or in sin table address >> 1 shl sin,#1 'shift left to get final word address rdlong sin,sin 'read word sample from $E000 to $F000 negnz sin,sin 'if quadrant 3|4, negate sample getsin_ret ret ' (this subroutine adapted from Prop manual) sin_90 long $0800 sin_180 long $1000 sin_table long $E000 >>1 sin long 0 ptr long 0 ctr long 360 address long @address delay long 10 time res 1
(I hope the code appears in a box this time.) Thanks again, Arna
it is kind of funny how often you see your mistakes just seconds after clicking Post Quick Reply.
so its cognew(@begin, @angle[0]) ...
Enjoy!
Mike
But you are close !
yes, the cognew misses still the array address as second parameter. This second parameter value lands in the PAR register, now it will be 1 and your rdword's access the hubram from that address.
Then in the Spin code, make index a long, a byte will not go to 360.
You don't need the dira[0..7]~~, you set the dira in the cog anyway, and the Spin cog gets terminatedat the end of the main methode
In the PASM code:
- I think the rdlong in getsin must be a rdword, the sin table is stored in words.
- I guess you have connected a DAC at P7..P0, then you should add an offset to the sin value, so that zero is in the middle of the DAC range ($80). The sin value from the table is a signed 16bit value, so you should shift right with SAR and not SHR:
sar sin, #17 add sin, #$80 mov outa, sin
and the delay may be a bit short, I dont remember what is the minimal value so that waitcnt not hangs, but 20 should be safe.
Andy
Ah, and mov ctr, # 360 after the loop
When you have this delay-per-sample working correctly, which will have zero jitter but some granularity, another variant approach I've thought of (on paper) for Sine generation, is to config a COG Counter in NCO mode, and run a fast-polling loop on the upper 12 bits of PHSx value, and apply those bits as the Sine-table index (with the same quadrant shuffle you do now)
These bits will follow a sawtooth, the same as your present scan counter does.
Your sine out will have NCO frequency precision, and will scale naturally over frequency.
At very low NCO output rates, you will read the same PHSx value multiple times, as polling is faster than INC, and at higher Fout, your polling will skip some values, but still give a valid sine.
I think the cross-over point is ~9765.625Hz, and your frequency granularity is 18.6 milli hertz.
Above this, the scan loop and sawtooth may 'beat', but that will change the sample 'dots' along the sine wave, and any LPF will smooth that.
The next step would be to add a Clocked video i2s out, and use a stereo DAC chip..
There is room in a COG to support both Gen modes.
The hard-step size may have benefits where super low jitter matters, and the NCO tracker would be easy to sweep.
Pulling the getsin in-line once you have it working, will shave a couple of cycles off the loop.
Whoa! That was way back in the 2006 thread on "Spin code examples for the beginner". The version of that program that runs 8 sine wave generators to the leds on the demo board or the quick start is still mesmerizing.
{ Just need it some orders of magnitude faster to feed an i2s DAC ... }
What kind of speed are you looking for?
There is an old object I have in my personal library dated back in 2006 I think that Chip wrote originally that uses PWM and updates the duty with the correct phase position of the desired sinewave. The original file had an update rate of about 250kHz.
I modified and cleaned that file up so that the update rate to the PWM duty cycle is now over 500kHz.
Note: The 3dB roll off with the selected components is at about a 90kHz sinewave
Feel free to use the attached files however you wish. I think there are enough notes within the DEMO file and the driver file to get you started. If not let me know and I will try to help.
EDIT: I do like Tracy Allen's 'sinewave3.spin'... it seems to be smoother at higher frequencies than what I just posted. I might see If I can get his to work using only one I/O pin. Attached is a second version implementing Tracy Allen's sinewave using only one pin. See the 'cat' variable at the bottom of the Sinewave_v2.spin file. The sample rate was improved from 524kHz to 1.25MHz from the previous version 1 to the new version 2.
Oops, ahem, I'm sure I wrote i2s DAC.... (now corrected)
There, the logical speed is 'whatever the chip can do' which seems to be 192KHz, so the scan and send loop needs to be above this. (paced with waitvid to match 192KHz ?)
(ie output two 16/24/32 bit serial samples at this rate : i2s has CLK, Data and a R.L frame toggle)
Some users might want Sine and Cos, some might want two independent frequencies.
This one http://www.akm.com/datasheets/ak4388a_f02e.pdf is 86c/100+, and Nuvoton have a nice looking
NAU8402, that has a -ve charge pump and 2V RMS drive... Shows 82c at Arrow, but no stocks.
A PWM DAC on one COG, and an i2s DAC on another would be a nice example...
Here is the code with the latest corrections. I get some output at P0..P7 but have no DAC and no Scope connected.
There was a bug with the shift of the sine output, we need to shift it by 9 and not 17, that's my fault, sorry!
CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 VAR long index word angles[360] PUB main repeat index from 0 to 359 angles[index] := index*8192/360 cognew(@begin,@angles[0]) DAT begin mov address, par mov dira,#$FF :loop rdword sin, address add address, #2 call #getsin mov time,cnt add time,delay waitcnt time,delay sar sin, #9 add sin, #$80 mov outa, sin 'write sin to output pins djnz ctr,#:loop 'decrement counter and restart loop mov ctr,#360 'Initialize counter. mov address,par jmp #:loop getsin test sin,sin_90 wc 'get quadrant 2|4 into c test sin,sin_180 wz 'get quadrant 3|4 into nz negc sin,sin 'if quadrant 2|4, negate offset or sin,sin_table 'or in sin table address >> 1 shl sin,#1 'shift left to get final word address rdword sin,sin 'read word sample from $E000 to $F000 negnz sin,sin 'if quadrant 3|4, negate sample getsin_ret ret ' (this subroutine adapted from Prop manual) sin_90 long $0800 sin_180 long $1000 sin_table long $E000 >>1 sin long 0 ptr long 0 ctr long 360 address long 0 delay long 10 time res 1
Andy
The evaluation board from digikey is: EVAL-AD9833SDZ-ND A bit pricy, (~$70) but the chips are in the $10 range. You might want to see if it fits your application like it did mine.
KB
I think a Prop can do 18.6 milli hertz, and drive a 16b value into a cheap i2s DAC - and do that using 1/8 of the chip.
- but no, it is not a 10 minute task.
CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 VAR long index word angles[360] PUB Get_samples repeat index from 0 to 360 angles[index] := index*8192/360 cognew(@begin,@angles[0]) DAT begin mov address, par mov dira,#$FF :loop rdword sin, address add address, #2 call #getsin mov time,cnt add time,delay waitcnt time,delay sar sin, #9 add sin, #$80 mov cogmem, sin 'write sin cog memory rather than output pins 'mov outa,cogmem add cogmem, #2 'increment memory location djnz ctr,#:loop 'mov ctr,cntstart 'Initialize counter. 'mov address,par 'jmp #:loop mov ctr,#360 mov cogmem,start :rdmem mov outa,cogmem add cogmem, #2 djnz ctr,#:rdmem mov cogmem, start mov ctr,#360 jmp #:rdmem getsin test sin,sin_90 wc 'get quadrant 2|4 into c test sin,sin_180 wz 'get quadrant 3|4 into nz negc sin,sin 'if quadrant 2|4, negate offset or sin,sin_table 'or in sin table address >> 1 shl sin,#1 'shift left to get final word address rdword sin,sin 'read word sample from $E000 to $F000 negnz sin,sin 'if quadrant 3|4, negate sample getsin_ret ret ' (this subroutine adapted from Prop manual) sin_90 long $0800 sin_180 long $1000 sin_table long $E000 >>1 sin long 0 ptr long 0 ctr long 360 cntstart long 360 address long 0 cogmem long $03A0 start long $03A0 delay long 9 time res 1
Above some ceiling, you are going to need to reduce the number of 'dots' in your Sine, and once you have an adder-version working, you can use a Counter in NCO mode (which is just an adder ) and use the upper bits as your quadrant index. Top 2 bits would be Quadrant choice, and next 12 bits are the phase inside that quadrant.
The Prop manual says this is legal for reading the adder
mov Result, phsa 'Get current phase value
Edit: I see in the post above
http://forums.parallax.com/showthread.php?140784-Sine-wave-generator&p=1105768&viewfull=1#post1105768
Beau gives Sinewave_v2.spin, which is a PASM version that does exactly this.
Did you try that ?
If you want smooth/clickless updates of new Freq values, calculated elsewhere, you would need to move the
rdlong frqa, pointer
inside the loop, which will slow it down slightly.
You may want to run both Software choices, as a skipping adder will always deliver points on a sine wave, but not always the same points every cycle, and if you no not smooth with a LPF that slight difference might be noticed.
http://forums.parallax.com/showthread.php?112389
CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 OUT_PIN = 15 FREQ = 100_000 PUB start | i repeat i from $01 to $ff sine[i] := (sin(i << 5) << 15) + $8000_0000 frqa0 := frqval(FREQ) cognew(@sine, 0) PUB sin(x) : value | t '' Sine of the angle x: 0 to 360 degrees == $0000 to $2000, if (x & $fff == $800) value := $ffff else if (x & $800) t := -x & $7ff else t := x & $7ff value := word[$e000][t] if (x & $1000) value := -value PRI frqval(a) : f 'Thanks, Tracy! repeat 32 a <<= 1 f <<= 1 if a => clkfreq a -= clkfreq f++ DAT sine jmp #start_sine sine_table long 0-0[255] start_sine mov sine,_0x8000_0000 mov frqa,frqa0 mov ctra,ctra0 mov ctrb,ctrb0 mov dira,dira0 :loop mov frqb,0 mov acc,phsa shr acc,#24 movs :loop,acc jmp #:loop _0x8000_0000 long $8000_0000 ctra0 long %00001 << 26 frqa0 long 0-0 ctrb0 long %00110 << 26 | OUT_PIN dira0 long 1 << OUT_PIN acc res 0
It uses DUTY mode output to produce a pretty good sine wave up to about 500 kHz. Beyond that, things start to fall apart. Here's a plot of the waveform filtered by a simple RC filter (R = 2.2K, C = 220 pF):
Here's the spectrum in the near vicinity of the fundamental, with no apparent spurious sidebands due to phase jitter:
Although it would be possible to shorten the loop a little, it would come at the expense of frequency resolution.
-Phil
Gives more X-Dots at high frequencies, at the cost of less X-dots at low frequencies, but the Y dots can be higher precision, is a DAC is available to use it. They are stored as 32 bit values.
Not too surprising, at 400KHz you have 10 samples in the X axis, (so are taking stretched strides thru that 256 full sine table), but the DAC only has 20 clocks to build a Y axis value, before a new one arrives.
If this is insufficient for anyone, and they have deep pockets, this news is topical :
http://www.eetimes.com/electronics-products/rf-microwave-products/4390550/Analog-Devices-introduces-superfast-direct-digital-synthesizers
This has a 3.5GHz (!) NCO rate and a 12bit DAC, and claims 271 pico Hz resolution. Yours from just $119 ea/100
At 100KHz, you are very close to /800 (adder will be 99999.997764Hz) - a better test might be one that more evenly runs /800 /801, so try an adder value of 5372066.
This will output an average of 100062.526762, but achieve it by alternating 100125.15644, & 100000.0 Hz
Your spectrum then should show two equal peaks 125.156Hz appart
It is already only 5 lines long - not much fat there !
I think Frequency resolution is determined more by the Adder, the loop affects the 'Dots on the Graph', not so much the underlying frequency (or frequencies, as I prefer to think of the NCO output )
I could see that making the Table 512 x 16, rather than 256 x 32 could give a useful gain of more X axis dots, for little real Y axis cost (not in a Prop DAC anyway)
A direct table allows any waveform to be synth'd, but as most change slowly, one way to get more resolution, would be to store a signed delta in the table.
Edit : oops, just dawned on me that delta storage is only possible in a no-skips/no repeats readout, so it would be of no use here, as an adder index design has both skips and repeats, depending on frequency.
The good thing about a table, is you can take as long as you like creating it, and read-back is very fast.
It is as though a voice came from Heaven with the answer.
I've encountered a mystery that has me totally flummoxed.
Phil's lovely code, two posts up, works great.
I've added a tiny bit here:
CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 OUT_PIN = 15 FREQ = 100_000 PUB start | i repeat i from $01 to $ff sine[i] := (sin(i << 5) << 15) + $8000_0000 frqa0 := frqval(FREQ) cognew(@sine, 0) PUB sin(x) : value | t '' Sine of the angle x: 0 to 360 degrees == $0000 to $2000, if (x & $fff == $800) value := $ffff else if (x & $800) t := -x & $7ff else t := x & $7ff value := word[$e000][t] if (x & $1000) value := -value PRI frqval(a) : f 'Thanks, Tracy! repeat 32 a <<= 1 f <<= 1 if a => clkfreq a -= clkfreq f++ DAT sine jmp #start_sine sine_table long 0-0[255] start_sine mov sine,_0x8000_0000 mov frqa,frqa0 mov ctra,ctra0 mov ctrb,ctrb0 mov dira,dira0 :loop mov frqb,0 mov fum, foo mov temp, phsa 'add temp, fum ' <= this fails add temp, foo ' <= this works mov phsa, temp mov acc,phsa shr acc,#24 movs :loop,acc jmp #:loop _0x8000_0000 long $8000_0000 ctra0 long %00001 << 26 frqa0 long 0-0 ctrb0 long %00110 << 26 | OUT_PIN dira0 long 1 << OUT_PIN foo long 10 acc res 0 fum res 0 temp res 0
As you can see, inside the endless loop I've incorporated a few lines of code to modify phsa.
As it sits, it runs perfectly fine.
But if I comment out add temp, foo
and uncomment add temp, fum
it breaks.
What in the world am I doing wrong?
FRQB from the 256 entry table. All looks reasonable, and fum should always be identical to foo.
This isn't some kind of name clash for 'fum' on some other part of the code?
Looks like foo and fum are not the same - one is a constant, and the other a register, & you need to tell the assembler that with # or no #
You can make it work like this...
acc res 1 . ' <<need to res 1, not zero! fum res 1 temp res 1
Keep in mind that at 100kHz, phsa advances by 5_368_709 (frqa0) at each clock tick, once every 12.5 nanoseconds. That makes it 800 steps to traverse the phase accumulator.