TAQOZ Reloaded v2.8 - SI5351 dual RF signal source control
This code controls the frequency of the two independent channels of an SI5351 chip over the range 8kHz to 120MHz.
N.B. Two constants may need altering:-
fref needs changing to the clock frequency driving your SI5351
vfoadr may need changing to the address of your SI5351, as reported by the Taqoz startup message
The commands are as follows:-
RFinit ( -- ) Initialise the SI5351 ready for use. Turns output pins CLK0 and CLK1 off ( no output pulses )
CLK0 ( -- ) Select the output from pin CLK0 as the currently selected channel - all commands will go to that channel until the other channel is selected
CLK1 ( -- ) Select the output from pin CLK1 as the currently selected channel - all commands will go to that channel until the other channel is selected
RFtune ( frequency -- ) Set the currently selected channel to 'frequency' Hz. This has been tested over the frequency range 8000 to 120000000 Hz
RFon ( -- ) Turn the currently selected channel on, approx 3.3V high pulses will appear at the output
RFoff ( -- ) Turn the currently selected channel off
There are some test words at the bottom to check the data being created and read the SI5351 settings. During development, the application note AN619 from SI was useful, as was this website. Referring to the SI5351 diagram on the latter website, my code holds three of the frequency setting parameters constant - e=0, f=1 and c = 1048575, to simplify the maths. The value of c is the largest permitted and this allows b to vary over the widest range and thus give the smallest tuning resolution. The OMD division ratio is maintained as an even integer which is said to minimise clock jitter. All these simplifications are to improve tuning speed, when controlled by a rotary encoder, so as to sound as smooth as possible - radio enthusiasts spend a lot of time hunting for weak signals, so any notchy tuning artifact would be unwelcome. I've swept the output in frequency and listened to it with a receiver and it sounds like an analogue oscillator, which is great. Measurement of the SWEEP word showed that RFtune took an average of 245uS @ 200MHz P2 clock. That equates to being able to update frequency ~4000 times/s! The precision of the maths means the actual frequency is within about 2Hz of the demanded frequency, so be aware of that.
Only if used in a radio application, for best purity of the signal generated, I've read it's best to use just one channel per SI5351 as the cross talk between channels increases clock jitter slightly. For other applications, it's usually not an issue.
Here's the code:-
TAQOZ --- SI5351 DRIVER version 1 for TAQOZ Reloaded v2.8 by Bob Edwards G4BBY May 2021 --- Optimised for radio tuning by minimising the number of bytes sent to the SI5351 --- when a tuning step is demanded --- N.B. In the CONSTANTS section, you need to check vfoadr matches your si5351 i2c address --- and fref is the frequency clocking your si5351 - mine was quite a way out from the nominal 25MHz IFDEF *SI5351* FORGET *SI5351* } pub *SI5351* ." SI5351 dual channel RF oscillator driver ver 1" ; ALIAS I2C.START <I2C --- Matches the stop alias, I2C> pri @. @ . ; --- Used quite a lot for debug display --- CREATE and DOES> so we can define a new RECORD type pub CREATE ( -- ) [C] GRAB [C] CREATE: --- Using the next word in the input stream as the name, create a VARIABLE type dictionary entry [C] GRAB --- make sure CREATE: has run before anything more HERE 2- 0 REG W! --- save the address of the code after DOES> in the REG scratchpad area ; pub DOES> ( -- ) R> --- the first word location in the new word being defined 0 REG W@ --- retrieve the address stored on scratchpad W! --- set the first word to execute as the address of the code after DOES> ; --- Record types --- start a record definition, leave zero offset into the record tos 0 := [FIELDS ( 0 -- ) --- define a new field within a record pre FIELD CREATE OVER [C] || + ( offset1 datasize -- offset2 ) DOES> R> W@ + ( recaddr -- recaddr+offset ) ; --- finalise a record definition4 FIELD BAB ALIAS := FIELDS] --- define a record array type pre RECORDS ( fielddef recordnumber <arrayname> -- ; arrayindex -- adr ) GRAB CREATE --- in the dictionary entry... org@ [C] || --- save start address of data OVER [C] || --- and save the record size in bytes * [C] res --- then in the dataspace allot reqd no. of bytes DOES> R> DUP >R W@ --- read the start address of the data R> 2+ W@ --- read the record size in bytes ROT * + --- and compute the start address of the reqd record ; private --- constants $C0 := vfoadr --- SI5351 i2c address 25008325 := fref --- nominal crystal frequency driving the si5351 750000000 := pllmid --- PLL frequency midrange 600000000 := pllmin --- PLL minimum permitted frequency 900000000 := pllmax --- PLL maximum permitted frequency 1048575 := C --- FMD parameter C is maintained as a constant --- variables long frequency --- required o/p frequency for the active channel long Rfreq --- reqd OMD o/p frequency, before R divider long pllfreq --- reqd pll frequency long A --- A parameter of the FMD divider long B --- B parameter of the FMD divider byte R --- The R divider parameter byte LASTREG --- address of SI5351 register last written to in autoincrement mode byte pllreset --- pllreset set 1 if pll reset needed byte RFactive --- set to 0 or 1 to select CLK0 or CLK1 as active channel for cmds long @1 --- scratch register 1 long @2 --- scratch register 2 public --- scratch register access pri 1@ @1 @ ; pri 2@ @2 @ ; pri 1! @1 ! ; pri 2! @2 ! ; --- Storage definition for one RF channel - remember word and long boundaries --- the size of the record has to be maintained a multiple of 4, so all records will be aligned --- This can be achieved by an extra dummy entry if need be --- NB keep .msx_p1, .msna_p2 and .msna_p1 in this order and together --- This ordering is relied on to speed up register transmission to the SI5351 [FIELDS 4 FIELD .frequency --- output frequency 4 FIELD .OMD --- OMD 4 FIELD .msx_p1 --- OMD intermediate value + rx_div + msx_divby4 4 FIELD .msna_p2 --- FMD intermediate value 4 FIELD .msna_p1 --- FMD intermediate value FIELDS] RFparam --- Create a 3 record array for the two independent RF sources + another record as scratchpad org@ 4 ALIGN org --- Ensure array starts on a long boundary RFparam 3 RECORDS RFparams --- record 0 = CLK0 params, record 1 = CLK1 params --- record 3 = copy of last params of active channel --- to allow SI5351 register changes to be detected --- CLK0, CLK1 are used to select the active channel to be worked on, this state stored in variable RFactive pri RFparams@ RFactive C@ RFparams ; ( -- adr ) --- start adr of active channel params pri RFoldparams@ 2 RFparams ; ( -- adr ) --- start adr of params from last cycle pub CLK0 0 RFactive C! ; ( -- ) --- select CLK0 as the active channel pub CLK1 1 RFactive C! ; ( -- ) --- select CLK1 as the active channel --- Calculation of SI5351 register values --- from the new output frequency and old OMD setting - the latter is allowed to be any number on start up --- calculate the new OMD as d + e=0 / f=1, which is just d, and new pll frequency required --- calculate R divider and equivalent o/p frequency pri RCALC ( freq1 -- R freq2 ) DUP 500000 < IF 2 --- R = 2 seed value 7 0 DO 2DUP * 500000 > --- check if freq1 * R > 500kHz IF LEAVE --- yes, f now > 500kHz,so leave loop ELSE 2* --- no, then set R = R * 2 THEN LOOP --- and try again SWAP 2DUP * SWAP DROP ELSE 1 SWAP --- R is 1 for frequencies over 500000, freq2 is no change THEN ; --- calculate the new Output Multisynth Divider, flg=1 if pll reset reqd. pri OMD ( freq oldOMD -- newOMD pllfreq flg ) 2DUP * DUP pllmin pllmax WITHIN IF ( freq oldOMD pllfreq ) ROT DROP 0 ELSE ( freq oldOMD pllfreq ) DROP DROP DUP ( freq freq ) pllmid SWAP / ( freq newOMD ) DUP 1 AND IF 1+ THEN ( freq newOMD' ) SWAP OVER * 1 ( newOMD pllfreq ) THEN ; --- calculate a,b for parameters a + b / c for the Feedback Multisynth Divider from the pll frequency --- C is a constant, $FFFFF, the largest value permitted, so that widest b range gives best frequency precision pri FMD ( pllfreq -- a b ) DUP fref / SWAP ( -- a pllfreq ) C fref */ ( -- a c*pllfreq/Fref ) OVER C * - ( -- a b ) ; ( -- a b ) --- The SI5351 requires the above parameters to be packed in the following way... --- calculate msna_p1 pri msna_p1! A @ 7 << --- A*128 B @ 7 << C / --- 128*B/C + 512 - RFparams@ .msna_p1 ! ; --- calculate msna_p2 pri msna_p2! B @ 7 << DUP --- 128*B C / --- (128*B)/C C * --- C*((128*B)/C) - $F00000 + --- Set top four bits of msna_p3, which is a constant $FFFFF RFparams@ .msna_p2 ! --- 128*B-C*((128*B)/C) ; --- calculate msx_p1, complete with msx_divby4 and rx_div bit fields pri msx_p1! ( -- ) RFparams@ >R R> DUP >R .OMD @ 128 * 512 - Rfreq @ 150000000 > IF $0C0000 + --- MS0_DIVBY4 THEN R C@ DUP 1 = IF DROP 0 ELSE | 20 << --- R0_DIV THEN + R> .msx_p1 ! ; --- Copy the active channel params record to the scratchpad record pri OLDPARAM! ( -- ) RFparams@ RFoldparams@ RFparam CMOVE ; --- calculate and store all parameters in the currently selected channel record pri PARAM! ( frequency -- ) RFparams@ >R DUP frequency ! --- o/p frequency in active channel record RCALC SWAP R C! --- save the R divider parameter DUP Rfreq ! --- save the OMD divider o/p frequency R> DUP >R .OMD @ OMD pllreset C! --- save pllreset - 1 = reset needed DUP pllfreq ! --- save pllfreq FMD B ! --- save A A ! --- save B R> .OMD ! --- save OMD msna_p1! --- calculate msna_p1 msna_p2! --- calculate msna_p2 msx_p1! --- calculate msx_p1 ; --- SI5351 I2C words --- Read byte at register adr in si5351 pri VFOC@ ( adr -- byte ) <I2C vfoadr I2C! I2C! --- select the register at 'addr' <I2C vfoadr 1+ I2C! nakI2C@ I2C> --- read the contents ; --- Write byte to register adr in si5351 pri VFOC! ( byte adr -- ) <I2C vfoadr I2C! I2C! I2C! I2C> ; --- Write a byte to SI5351 register using autoincrement address where possible pri INCVFOC! ( byte adr -- ) DUP LASTREG C@ 1+ = --- is this register = last register +1? IF DROP I2C! --- yes, just send the register value ELSE <I2C vfoadr I2C! I2C! I2C! --- no, set up the register address and then send value THEN ; --- part of sendFMD and sendOMD pri sendbytes ( ptr_to_data -- ) 3 FOR --- Do for all 3 bytes, decrementing new byte pointer DUP I - C@ 1@ C@ <> --- Are the new and old byte values different? IF DUP I - C@ 2@ INCVFOC! --- Yes, so send the new byte to the SI5351 register 2@ LASTREG C! --- Save reg address to check for autoincrement THEN @1 -- --- Point to next lowest old byte @2 ++ --- Increment register address NEXT DROP ; --- Send FMD to the SI5351 - all bytes if 1st time, else only changed bytes pri sendFMD ( -- ) RFactive C@ IF 36 ELSE 28 THEN 2! --- register start address, depends on active channel selected, store in memory 2 RFoldparams@ .msna_p1 2 + 1! --- memory 1 = pointer to MS byte of old .msna_p1 RFparams@ .msna_p1 2 + --- pointer to MS byte of .msna_p1 sendbytes RFoldparams@ .msna_p2 2 + 1! --- memory 1 = pointer to MS byte of old .msna_p1 RFparams@ .msna_p2 2 + --- pointer to MS byte of .msna_p2 sendbytes ; --- Send OMD to the SI5351 - all bytes if 1st time, else only changed bytes pri sendOMD ( -- ) RFactive C@ IF 52 ELSE 44 THEN 2! --- register start address, depends on active channel selected, store in memory 2 RFoldparams@ .msx_p1 2 + 1! --- memory 1 = pointer to bottom of previous params, MS byte of old .msx_p1 RFparams@ .msx_p1 2 + --- point to MS byte of new .msx_p1 sendbytes ; --- Wait until si5351 initialisation complete pri VFOINIT? ( -- ) BEGIN 0 VFOC@ $80 AND 0= UNTIL ; --- Disable all clock outputs pri VFO_OPS_DIS ( -- ) $FC 3 VFOC! ; --- Power down all clocks using register 16-23 pri VFOCLKSOFF ( -- ) 8 FOR $80 I 16 + VFOC! NEXT ; --- Set crystal as both PLL source pri VFOXTAL ( -- ) 0 15 VFOC! ; --- Set all disabled outputs low pri VFOOUTLOW ( -- ) 0 24 VFOC! ; --- Init Multisynth constants pri VFOSYNTHINIT ( -- ) $FF 26 VFOC! $FF 27 VFOC! $FF 34 VFOC! $FF 35 VFOC! 0 42 VFOC! 1 43 VFOC! 0 47 VFOC! 0 48 VFOC! 0 49 VFOC! 0 50 VFOC! 1 51 VFOC! 0 55 VFOC! 0 56 VFOC! 0 57 VFOC! ; --- Power up CLK, PLL, MS for currently selected channel pri VFOPLLON ( -- ) RFactive C@ IF $4F 16 ELSE $6F 17 THEN VFOC! ; --- Reference load setup pri VFOREFSET ( -- ) $12 183 VFOC! ; --- CLK output enable for currently selected channel pub RFon ( -- ) 3 VFOC@ RFactive C@ IF $FD ELSE $FE THEN AND 3 VFOC! ; pub RFoff ( -- ) 3 VFOC@ RFactive C@ IF $02 ELSE $01 THEN OR 3 VFOC! ; --- reset the PLL on the currently active channel pri RESETPLL ( -- ) RFactive C@ IF $80 ELSE $20 THEN 177 VFOC! ; --- Initialise the SI5351 ready for frequency setting pub RFinit ( -- ) VFOINIT? --- wait until si5351 has initialised VFO_OPS_DIS --- disable both outputs VFOCLKSOFF --- power down all clocks VFOXTAL --- set crystal as PLL source VFOOUTLOW --- set all disabled O/Ps low VFOSYNTHINIT --- initialise multisynth constants VFOREFSET --- Reference load start up RFactive C@ 2 0 DO I RFactive C! VFOPLLON --- power up CLK, PLL, MS0 RFoff --- but set both o/ps off for now LOOP RFactive C! ; --- set the si5351 current channel to frequency pub RFtune ( frequency -- ) 0 LASTREG C! --- Ensure we get a proper I2C address setting start OLDPARAM! PARAM! --- calculate register values sendOMD --- send the OMD registers to the SI5351 sendFMD --- send the FMD registers to the SI5351 I2C> --- end the i2C transfer pllreset C@ IF RESETPLL THEN --- reset the pll ; --- Test Words --- display all params for the selected channel pub RFparam. ( -- ) CRLF CRLF ." RFparams settings..." RFparams@ CRLF ." frequency = " frequency @. CRLF ." R = " R C@ . CRLF ." Rfreq = " Rfreq @. CRLF ." OMD = " DUP .OMD @. CRLF ." pllfreq = " pllfreq @. CRLF ." A = " A @. CRLF ." B = " B @. CRLF ." C = " C . CRLF ." msna_p1 = " DUP .msna_p1 @. CRLF ." msna_p2 = " DUP .msna_p2 @. CRLF ." msna_p3 = " $FFFFF . CRLF ." msx_p1 = " DUP .msx_p1 @. CRLF ." pllreset = " pllreset C@ . CRLF ; --- Display ONE si5351 register pub VFOREG. ( adr -- ) TAB ." Register " DUP DEC . TAB ." : " --- display register address DUP VFOC@ --- read the register DUP . ." decimal " TAB --- and display it in decimal .BYTE ." hex " TAB --- also in hex DROP CRLF --- Taqoz needs CRLF rather than the CR in Tachyon DEC ; pri .RES TAB ." Reserved" CRLF ; --- Display all si5351 registers pub VFOREGS. ( -- ) CRLF CRLF TAB ." si5351 REGISTER READOUT" CRLF CR 4 FOR I VFOREG. NEXT .RES 9 VFOREG. .RES 13 FOR I 15 + VFOREG. NEXT .RES 7 FOR I 29 + VFOREG. NEXT .RES 134 FOR I 37 + VFOREG. NEXT .RES 177 VFOREG. .RES 183 VFOREG. .RES 187 VFOREG. ; --- This checks that the params were calculated correctly by back-calculating the output frequency pub FREQCHECK. ( -- ) RFparams@ >R fref A @ * --- fref * A fref B @ C */ --- fref * B / C + R C@ / R> .OMD @ / CRLF CRLF ." The si5351 params will give an output of " . ." Hz" CRLF ; --- Display the PLL lock status - PLL locked if healthy pub LOCK. 0 VFOC@ CRLF ." Channel A pll " DUP $20 AND IF ." unlocked" ELSE ." locked" THEN CRLF ." Channel B pll " $40 AND IF ." unlocked" ELSE ." locked" THEN CRLF ; --- Tune from start to stop frequency, stepping 10Hz as fast as possible, stop if key pressed pub SWEEP ( startfreq stopfreq -- ) RFinit >R RFon BEGIN --- save the stop frequency on the R stack DUP RFtune 10 + DUP R> DUP >R => KEY 0<> OR UNTIL DROP R> DROP --- clear the R and data stacks ; pub SWEEPtest 3600000 3800000 SWEEP ; END
Here's my lash up of an adapter to connect the SI5351 breakout board to the P2 on the default I2C pins.