Sine wave generator
Mad A
Posts: 17
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: 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) You don't need all the indirect addressing with movs, you can just read the phase with rdbyte sin, address, and then increment address: 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: 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.
(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:
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!
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.
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
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:
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...
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.