/*
This file is linked with the documentation ! */
//---------------------------------------------------------------------------
// File: C:\pic\GPSDO\gpsdo_pic_main.c
// Author: Wolfgang Buescher, DL4YHF
// Date: 2016-02-12
// Development System : Microchip MPLAB IDE v8.85,
// XC8 C Compiler ("Free Mode") V1.35,
// later also "MPLAB X" because "MPLAB" debugger is severely bugged.
// To test the algorithms *without* MPLAB,
// Borland C++ or any decent C compiler will do.
//
//
//---------------------------------------------------------------------------
//
//
// Literature: [PIC16F1783 DS] : PIC16F1782/3 datasheet, DS40001579E,
// saved as C:\datasheets\pic\PIC16F1782_3_datasheet.pdf
// REVISIONS: (latest entry first)
// 2016-02-13: Due to the incredibly poor (some say 'deliberately bloated')
// code produced XC8 V1.35 "free", decided to give CC5X (free)
// another chance, and adapted everything to compile with
// CC5X besides XC8 and Borland C++ Builder.
// But CC5X immediately whined about multiple C files in one project :
// > Warning[1] .. : Relocatable ASM and MPLINK is not recommended
// > (see README.TXT)
// and (story told in CC5X's LINKER.TXT, not README.TXT):
// > Currently it is best to use a single C module for several reasons.
// > MPLINK support was mainly offered to enable asm modules to be added.
// > Limitations when using MPLINK:
// > 1. Asm mode debugging only (C source code appear as comments)
// (WB: heavens, no. Not again. We want true SOURCE LEVEL debugging.)
// > 2. Multiple C modules does not allow the static local variable
// > stack to be calculated for the whole program, meaning that much
// > more RAM space will be used for local variables.
// > 3. Call level checking must be done manually
// > 4. Computed goto will be slower because the compiler
// > can not check 256 byte address boundary crossing.
// > 5. Inefficient RAM bank updating, meaning mode code.
// WB: Ok, "message understood". So back to stuffing
// 'all we need' (in the C part) INSIDE A SINGLE MODULE.
// To avoid having to turn the main module (gpsdo_pic_main.c)
// into a bulky monster, the extra C source modules (ADC, UART,..?)
// are now #included as if they were HEADER files.
// 2016-02-14 : gave up, no 16-bit SFR accesses in CC5X,
// smaller but NON-FUNCTIONAL code.
// Thrown out ! Will never try this again. CC5X + MPLABX = waste of time.
//
// 2016-01-27: Removed many notes from the "brainstorming phase"
// from this sourcecode, and dumped them into the
// HTML file.
// 2015-12-21: Started project, brainstorming phase, initial tests
// because looking at the PIC16F1782/3 Errata, this chip
// seems to have a couple of bugs .
//
//*************************************************************************
#include "switches.h" // project specific 'compiler' switches & options
// (it seems impossible to define the include files in MLPAB-X,
// and pass those settings on to the "custom translator" CC5X.
// So, like it or not, everything (*.c, *.h) had to be dumped
// into the stupid MPLAB-X 'project directory'. What a mess. )
// Include an awful lot of compiler-specific junk ...
#ifdef __BORLANDC__ // compiling with Borland C ? Use WB's "PIC emulator" ..
# include "pic_emulator/xc.h"
#elif (defined __XC8) // compiling for PIC, using Microchip's "XC8" compiler ?
# include "xc.h"
// What a mess. "xc.h" includes "htc.h" .
// "htc.h" includes "hc.h" if not already included. Whow.
// "htc.h" also includes "cci.h", whatever that stands for.
// "htc.h" also includes "xc8debug.h", but "xc8debug.h" contains almost nothing.
// "cci.h" also includes "__at.h" . Yet another intuitive name.
// Which of the above obfuscated headers actually includes "pic16lf1783.h" ?
// #include // No-No ! (included from xc.h, whatever 'xc' means)
# include "stdint.h" // WB surrendered, now using 'uint8_t' instead of BYTE, etc
#else // neither Borland, nor XC8; (forget about CC5X ... it's NOT A C COMPILER)
# error Your compiler is not supported here yet. Please add support yourself.
#endif
#include "uart_pic.h" // "UART functions for PIC" by DL4YHF
#include "adc_pic.h" // ADC initialisation, may also be PIC-specific
#include "gpsdo.h" // header for THIS module
//---------------------------------------------------------------------------
// PIC16LF1783 Configuration Bit Settings
//---------------------------------------------------------------------------
// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.
// Support for PIC16(L)F1783 was added in DL4YHF's WinPic,
// so the settings for CONFIG WORD 1 and CONFIG WORD 2,
// which are hopefully transferred into the HEX file by voodo magic
// can be examined in WinPic (via Microchip's *.dev file) .
// CONFIG1 . Details in PIC16(L)F1782/3 DS40001579, page 40 of 434 ...
#pragma config FOSC = ECH // Oscillator Selection (ECH, External Clock, High Power Mode (4-32 MHz, CLKIN)
#pragma config WDTE = OFF // Watchdog Timer Enable ? (don't.. this firmware doesn't feed it)
#pragma config PWRTE = ON // Power-up Timer Enable ? (advisable when there is no external reset circuitry)
#pragma config MCLRE = ON // MCLR Pin Function Select (MCLR/VPP pin function is MCLR)
#pragma config CP = OFF // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config CPD = OFF // Data Memory Code Protection (Data memory code protection is disabled)
#pragma config BOREN = ON // Brown-out Reset Enable (Brown-out Reset enabled)
#pragma config CLKOUTEN = OFF // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF // Internal/External Switchover (Internal/External Switchover mode is disabled)
#pragma config FCMEN = ON // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is enabled)
// CONFIG2 . Details in PIC16(L)F1782/3 DS40001579, page 42 of 434 ...
#pragma config WRT = OFF // Flash Memory Self-Write Protection (Write protection off)
#pragma config PLLEN = ON // PLL Enable (4x PLL enabled; 10 -> 40 MHz slightly violating the spec)
#pragma config STVREN = ON // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will cause a Reset)
#pragma config BORV = LO // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LPBOR = OFF // Low Power Brown-Out Reset Enable Bit (Low power brown-out is disabled)
#pragma config LVP = ON // Low-Voltage Programming Enable (Low-voltage programming enabled)
//;**************************************************************************
//; *
//; Port assignments *
//; *
//;**************************************************************************
//
// Connections / circuit principle with 'typical' voltages (DC, average)
//
// 1.8V 3.6V 6.4V 10V
// +3.6 V __ __ ___ __ __
// O .--|__|--*--|__|--*--|___|---|__|--*--|__|--O + 12 V
// RGB-LED | 610| 1k | 2k2 | /|\5k 4k7 | 4k7 (stable)
// indicator| Hz,|50% __|__ __|__ |(10 ---.
// .----*----. |PWM _____ _____ | turn /|\ 10 V
// B| G| R| |duty 10|uF 4.7|uF | trim- | reference
// --- --- --- |cycle _|_ _|_ | pot) _|_ or Zener
// /|\ /|\ /|\ | ____ | 5V ICSP (Mclr/
// | | | PWM for |VC |<--------- an.out an.in | Vpp)
// .-. .-. .-. V-Ctrl |OCXO| Vctrl ADC 80kHz | (I + Q) .-.
// | | | | | | /|\ |____| sampling/ /|\ | | | |
// |_| |_| |_| | 10MHz| ___ speed test | | | |_|
// |1k |2k |2k | \|/ | /|\ | \|/ \|/ |
// | | | | | | | | | | |
// _ _ _ _ _ _ _ _ _ _ _ _ _ _
// | | | | | | | | | | | | | | | | | | | | | | | | | | | |
// ----------------------------------------------------------------------
// | 14 13 12 11 10 9 8 7 6 5 4 3 2 1 |
// | RC3 RC2 RC1 RC0/ RA6 OSC1 GND RA5 RA4 RA3 RA2 RA1 RA0 MCLR*|
// | PSMC1A [RA7] /DAC /AN1 /AN0 __|
// | OUT - |
// | PIC16(L)F1783, "SPDIP-28" / |
// | \ |
// | ICSPCLK/ -__|
// | PSMC2A/ CCP1/ TXD/ RXD/|
// | RC4 RC5 RC6 RC7 GND +Vcc RB0 RB1 RB2 RB3 RB4 RB5 RB6 RB7 |
// | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// ----------------------------------------------------------------------
// |_| |_| |_| |_| |_| |_| |_| |_| |_| |_| |_| |_| |_| |_|
//
// | | | | | | |
// | | | | /|\ | |
// | | _|_ | | \|/ /|\ .
// \|/ \|/ | GPS | |
// | ? +3.6V Sync UART and
// | low- (1Hz) programming
// 13.33333 MHz noise _ adapter (ICSP)
// for SDR-IQ _| ICSP adapter pins :
// ('programmable 1 = MCLR/Vpp 2 = Vcc
// frequency output', 3 = GND 4 = PGD/RXD
// PFO : 40 MHz / N without FFA, 5 = PGC/TXD 6 = LVP (nc)
// 20 MHz / (N + M/16) with FFA)
//
// (*) MCLR/Vpp has an internal pull-up resistor, so just leave it
// unconnected during normal operation. The ICSP adapter will
// set MCLR/Vpp to +9 V (!) for the PIC16LF1783 to enter programming mode.
//
// An overview of ALL alternate port functions is in the PIC16F1782/3
// datasheet, DS40001579E, "28-PIN ALLOCATION TABLE" on page 5 .
// Some of the alternate pin functions must be assigned via APFCON !
//
#define PORTA_DIRECTIONS 0b00000011 // port A I/O mode (L=out, H=in !)
#define PORTA_INIT_DATA 0b00000000 // port A initial data (shortly after power-on)
// ||||||||_ RA0 / *AN0*
// |||||||__ RA1 / *AN1*
// ||||||___ RA2 / AN2 / VREF- / DACOUT1
// |||||____ RA3 / AN3 / VREF+ / DACVREF+
// ||||_____ *RA4* / T0CKI (here: ADC sampling clock test output)
// |||______ RA5 / AN4
// ||_______ RA6 / OSC2/CLKOUT
// |________ RA7 / PSMC1CLK / PSMC2CLK / OSC1/**CLKIN**
#define IOP_DEBUG_PIN1_LO PORTAbits.RA6=0 // used for debugging (via scope)
#define IOP_DEBUG_PIN1_HI PORTAbits.RA6=1 // used for debugging (via scope)
#define IOP_ADCCLK_PIN_LO PORTAbits.RA4=0 // also used for debugging (via scope)
#define IOP_ADCCLK_PIN_HI PORTAbits.RA4=1 // also used for debugging (via scope)
#define PORTB_DIRECTIONS 0b10000001 // port B I/O mode (L=out, H=in !)
#define PORTB_INIT_DATA 0b00000000 // port B initial data
// ||||||||_ RB0 / AN12 / PSMC1IN / **CCP1** (input for GPS sync pulse)
// |||||||__ RB1 / AN10 / OPA2OUT
// ||||||___ RB2 / AN8 / OPA2IN-
// |||||____ RB3 / AN9 / OPA2IN+ / CCP2
// ||||_____ RB4 / AN11
// |||______ RB5 / AN13 / T1G / SDO
// ||_______ RB6 / / *TXD* (out) / SDI / *ICSPCLK*
// |________ RB7 / DACOUT2 / *RXD* (in) / SCK / *ICSPDAT*
#define IOP_GPS_SYNC_ACTIVE PORTBbits.RB0
#define PORTC_DIRECTIONS 0b00000000 // port C I/O mode (L=out, H=in !)
#define PORTC_INIT_DATA 0b01001111 // port C initial data
// ||||||||_ RC0 / **PSMC1A** : 16-bit PWM for OCVCXO frequency control
// |||||||__ *RC1* / PSMC1B / CCP2 (red LED, high=off)
// ||||||___ *RC2* / PSMC1C / CCP1 (green LED, high=off)
// |||||____ *RC3* /PSMC1D/SCK/SCL (blue LED, high=off)
// ||||_____ RC4 / PSMC1E / SDI / SDA (i2c)
// |||______ RC5 / PSMC1F / SDO (spi)
// ||_______ RC6 / **PSMC2A** / (TXD)
// |________ RC7 / PSMC2B / (RXD)
#define IOP_RED_LED_ON PORTCbits.RC1=0 // RGB-LED (common anode) : 0 = ON
#define IOP_RED_LED_OFF PORTCbits.RC1=1 // RGB-LED (common anode) : 1 = off
#define IOP_GREEN_LED_ON PORTCbits.RC2=0 // RGB-LED (common anode) : 0 = ON
#define IOP_GREEN_LED_OFF PORTCbits.RC2=1 // RGB-LED (common anode) : 1 = off
#define IOP_BLUE_LED_ON PORTCbits.RC3=0 // RGB-LED (common anode) : 0 = ON
#define IOP_BLUE_LED_OFF PORTCbits.RC3=1 // RGB-LED (common anode) : 1 = off
// some bits on PORTA (old project, in PIC16F873A) remain available to communicate serially
// with the PLL synthesizer chip. THREE of those pins are required for the PMB2306:
#define IOP_PLL_EN PORTAbits.RA0 // serial "enable" . Pin 4 ("DA") at the PMB2306T (P-DSO14).
// > Enable line of the serial control with internal pull-up resistor.
// > When EN = H the input signals CLK and DA are disabled
// > internally. When EN = L the serial control is activated.
// > The received data are transferred into the latches
// > with the positive edge of the EN-signal.
#define IOP_PLL_DATA PORTAbits.RA1 // serial data line. Pin 4 ("DA") at the PMB2306T (P-DSO14).
// > Serial data input with internal pull-up resistor.
// > The last two bits before the EN-signal define the destination address.
// > In a byte-oriented data structure the transmitted data have to end
// > with the EN-signal, i.e. bits to be filled in (don?t care)
// > are transmitted first.
#define IOP_PLL_CLK PORTAbits.RA2 // serial clock line. Pin 5 ("CLK") at the PMB2306T (P-DSO14).
// > Clock line with internal pull-up resistor. The serial data are
// > read into the internal shift register with the positive edge
// > (see pulse diagram for serial data control).
// Other I/Os on Port A, not directly related with the PLL chip:
#define IOP_IN_RX_TX PORTAbits.RA3 // RX/TX sensing input (HI=RX, LO=TX)
//*************************************************************************
// *
// Constants and timing parameters *
// *
//*************************************************************************
#define PIC_CLOCK_FREQ_HZ equ .40000000 ; processor clock frequency in Hertz (*not* the instruction cycle!)
#define PLL_REF_FREQ_HZ equ .10000000 ; input REFERENCE frequency for the PLL
#define PLL_MAX_R_COUNTER equ .5000 ; max divisor for the REFERENCE frequency -> min. phase comparator frequency:
// example: PLL_REF_FREQ_HZ / PLL_MAX_R_COUNTER = 50 MHz / 5000 = 10 kHz (min phase comparator frequency, important for the loop filter design)
#define DEFAULT_VFO_FREQ equ .70160000 ; default VFO frequency in Hertz
#define RECEIVE_OFFSET_HZ 3579000 // 3.579 MHz = colour burst crystal frequency
//---------------------------------------------------------------------------
// Initial EEPROM contents ... note the clumsiness of this,
// it's not even possible to embed VARIABLES or STRUCTS in EEPROM
// so how do we know the ADDRESSES of the stuff located by __EEPROM_DATA ?
// As usual, others stumbled across similar problems, and asked:
// > Is there a work-around out there that will allow initialization
// > of a specific EEPROM location, without starting at the beginning ?
// __EEPROM_DATA seems to be a MACRO. It would be interesting to find
// its definition. But that would be asking too much (for MPLAB "IDE").
// Use Total Commander's glorious full-text search instead..
// for 14-bit cores, it's in PIC.H, and the macro is just TERRIBLE.
// It can ONLY accept EXACTLY EIGHT BYTES, which must be NUMERIC,
// no C strings, no structs, no multi-byte integer constants... eeeek !
//---------------------------------------------------------------------------
#if ( EEPROM_SIZE > 0 ) // do we have an EEPROM ?
# define EEP_ADDR_PFO_PERIOD_L 0x00 // 16-bit 'period' (divisor for the programmable frequency output)
# define EEP_ADDR_PFO_PERIOD_H 0x01 // upper 8 bits. F_out = 40 MHz / (PFO_PERIOD+2)
# define EEP_ADDR_PFO_FFA 0x02 // 4-bit 'FFA' (fractional frequency adjust for the PSMC)
// Initial EEPROM contents at offset 0x00 .. 0x07 : Programmable Frequency Output:
__EEPROM_DATA ( 2, 0, 0x00, 0x00, ' ', 'P', 'F', 'O' ); /* 0x00 .. 0x07 */
// |lo____hi| |
// | |
// PFO_PERIOD PFO_FFA
// With FFA=0 : f_PFO = 40 MHz / ( 1 + PERIOD_L + 256 * PERIOD_H )
// Example: f_PFO = 40 MHz / ( 1 + 83 + 256 * 0 ) = approx 476190.4762 Hz
__EEPROM_DATA ( 'd', 'l', '4', 'y', 'h', 'f', '.', 0x00 );
#else
# if( ! SWI_STANDALONE_SIMULATOR ) // only when compiling for a 'real' PIC:
# error "Wot, no EEPROM ?!"
# endif
#endif
//---------------------------------------------------------------------------
// data types : moved to gpsdo.h
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// global variables (true "local" variables don't exist in a PIC16 ! )
//---------------------------------------------------------------------------
uint16_t wWaitCounter; // "uint16_t" = the data type formerly known as "WORD"
uint8_t bTemp; // "uint8_t" = the data type formerly known as "BYTE"
uint8_t bMainLoopCounter;
int8_t i8Temp;
int16_t i16Temp; // temporary 16-bit signed integer value
U_4Byte i32TempX; // general purpose 32-bit value (usually the 'left operand')
U_4Byte i32TempY; // general purpose 32-bit value (usually the 'right operand' and/or result)
int16_t i16LowpassIn; // 16-bit input for the lowpass (running at Fs = 610 Hz)
U_4Byte i32LowpassOut; // lowpass-filtered output (connected to the VCOCXO's "Vctrl"-input)
uint8_t bLowpassSpeed; // 1: tau=107 seconds, 2: 53.7 s, 3: 35.8 s, 4: 26.8 s, ... 8: 13.4 s
uint16_t u16VctrlBias; // Ideally 32767 for a 50 % PWM duty cycle,
// determined during the initial 255-second "coarse" frequency measurement.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// CIC filter variables (decimating filter for the analog input, not GPSDO-related) .
// Based on an AVR(!) code sample by Bruce Land, Cornell University:
// > Filter parmeters: N=4, R=4, M=2, i.e.:
// > -- N = order=4 (4 integrators) [number of integrator and comb stages]
// > -- R = downsample=4 [decimation ratio, called D in Lacoste's article]
// > -- M = delay (after downsample) =2 [differential delay, "usually 1 but sometimes 2")]
// > Bits required is B_out = N*log2(R*M) + B_in
// > or 4*3+16=28, so 32 bit longs should be fine .
// Or, from another article (Mercury Receiver) about CIC filters:
// > For CIC decimators, the gain G at the output of the final comb section is:
// > Gain = (R * M) ^ N
// > Assuming two's complement arithmetic, we can use this result to calculate
// > the number of bits required for the last comb due to bit growth.
// > If B_in is the number of input bits, then the number of ouput bits, B_out, is:
// > B_out = N * log2( R * M ) + B_in
// > It also turns out that B_out bits are needed for EACH (?) integrator and
// > comb stage. The input needs to be sign extended to B_out bits, but LSB's
// > can either be truncated or rounded at later stages.
// >
// For a 12-bit ADC: B_in = 12, N=4, R=4, M=2 .
// B_out = 4 * log2( 4 * 2 ) + 12 = 4 * 3 + 12 = 24
// (which means 24-bit integer arithmetics
// may be sufficient. XC8 nativaly supports that type,
// Microchip call it 'signed short long'. )
//
// PIC-implementation specific details and notes (concerning the CIC filter):
//
// - To avoid wasteful bank-switching in the CIC filter code,
// all filter variables were squeezed inside one RAM bank.
// There may be more elegant solutions than absolute addresses,
// but the inline assembler cannot access struct components..
//
#if SWI_STANDALONE_SIMULATOR
# define __AT_(addr) // don't even think about absolute addresses on a PC
# define cic_t long /* standard C compilers don't support 24 bit so use 32 (at least)*/
# define cic_s 4 // size of each 'cic_t' element in bytes
#else
// Compiling for PIC16F1783 ? Try 24-bit integer,
// and locate the CIC filter variables in ONE BANK !
// "signed short long" is XC8's name for 24-bit :
# if(0)
# define cic_t long
# define cic_s 4 // size of each 'cic_t' element in bytes
# else
# define cic_t signed short long
# define cic_s 3 // size of each 'cic_t' element in bytes
# endif
// sizeof(T_CIC_Filter) is approximately 19 * 4 bytes;
// each of the PIC16F1783's RAM-banks has 80 bytes 'general purpose' registers,
// besides the SFRs and a tiny 'common RAM' area.
// Bank 0 : general purpose registers at 0x020 .. 0x06F;
// Bank 1 : general purpose registers at 0x0A0 .. 0x0EF;
// Bank 2 : general purpose registers at 0x120 .. 0x16F; etc..
# define __AT_(addr) @ addr+0x0A0
#endif
cic_t xx __AT_(0*cic_s); // input to the CIC filter
cic_t yy __AT_(1*cic_s); // output from the CIC filter (brought down to 16 bit again)
cic_t integrator1 __AT_(2*cic_s);
cic_t integrator2 __AT_(3*cic_s);
cic_t integrator3 __AT_(4*cic_s);
cic_t integrator4 __AT_(5*cic_s);
cic_t last_integrator4 __AT_(6*cic_s);
cic_t last2_integrator4 __AT_(7*cic_s);
cic_t comb1 __AT_(8*cic_s);
cic_t comb2 __AT_(9*cic_s);
cic_t comb3 __AT_(10*cic_s);
cic_t comb4 __AT_(11*cic_s);
cic_t last_comb1 __AT_(12*cic_s);
cic_t last_comb2 __AT_(13*cic_s);
cic_t last_comb3 __AT_(14*cic_s);
cic_t last_comb4 __AT_(15*cic_s);
cic_t last2_comb1 __AT_(16*cic_s);
cic_t last2_comb2 __AT_(17*cic_s);
cic_t last2_comb3 __AT_(18*cic_s);
cic_t last2_comb4 __AT_(19*cic_s);
uint8_t downsample_cnt __AT_(20*cic_s); // downsampling counter for the CIC filter
uint8_t fflags __AT_(1+20*cic_s); // flags for the filter output (send where, etc)
// end of the CIC filter variables
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
char sz80Temp[81]; // string buffer for the incredible "high-resolution" plotter
char cDebugMode; // 0 : don't send debug-messages to the serial port (UART),
// 'n' : emit numeric values (for debugging),
// 'p' : plot some of those values (also for debugging),
// ...
// Variables for measurement of the OCVCXO frequency, error integral, etc :
uint8_t bSyncPulseCounter; // number of GPS pulses, 0..255 (stops at 255)
int16_t i16FreqOffset; // measured frequency MINUS 10 MHz, unit: see ProcessCapturedSyncPulse()
uint16_t wPrevCapture; // timer value captured on PREVIOUS sync pulse
int16_t i16ErrorIntegral; // summed-up error value (for the controller's 'I'-part)
U_4Byte i32ErrorIntegralIntegral; // integrated error-integral (to get not only the FREQUENCY-, but also the PHASE-error back to zero)
//---------------------------------------------------------------------------
// Interrupt handler ("there can be only one" .. on a midrange PIC..)
//---------------------------------------------------------------------------
#if SWI_STANDALONE_SIMULATOR
void ADC_Sampling_ISR(void) // for simulation, periodically called from PIC_Emulator()
#else
void __interrupt ADC_Sampling_ISR(void) // Interrupt function, XC8 flavour.
// No need to specify an 'origin' here.
#endif
{
// XC8 automatically generates the required interrupt context saving/restoring code.
//
// Cycle counts below are relative to the entry (breakpoint at ADC_Sampling_ISR),
// where the 'Instruction Cycles' counter in MPLAB's "Stopwatch" was reset .
// HERE (after the ISR's red tape), PIC16F1783, XC8: 5 cycles
//
// Since we only use the interrupt for the ADC, there's no need to check the IRQ source.
// It would be nice to have a HARDWARE-triggered A/D conversion, but that's impossible
// with the remaining resources (PSMC1, PSMC2, CCP1, CCP2 could, but they're all occupied !).
// Having a fast-sampling analog input wasn't the primary goal of the GPSDO,
// and a PIC16F178x would have a tough time (with lots of assembler code)
// to perform reasonable anti-aliasing anyway.
// The A/D converter, and its "sampling trigger" (actually Timer2 in the PIC16)
// is initialized in a separate module by DL4YHF - see ADC_PIC16F1783.C .
// To test if the conversion is sufficiently jitter-free (with a scope),
// show the interrupt handler's activity on one of the GPIO pins:
IOP_ADCCLK_PIN_HI; // -> BSF 0x0C, 0x04 (0x0C = &PORTA)
// Before any conditional code (which would cause phase jitter),
// read the ADC result register (in BANK 1),
// store the result in the filter's input variable (xx, also in BANK 1),
// and start the next conversion :
#if ( SWI_STANDALONE_SIMULATOR ) // no inline assembler for the standalone simulator (running on a PC)...
xx = ADRES; // "A/D Result" .. what a name for a register
// ex (AVR) : xx = ((int)(ADCH)<<4) ; // read a/d converter
// |___ why ?? must be AVR-specific !
//
ADCON0bits.GO = 1; // tell the ADC what to do : "GO" = "start next conversion"
// -> BSF 0x1d, 0x1 ; set the 'GO'-bit in ADCON0
// Perform the missing sign-expansion ? Not neccessary for the simulation,
// but implemented in assembler (further below).
#else // not SWI_STANDALONE_SIMULATOR but compiling for a real PIC uC...
# asm
// Assembler variant of the above "C" code :
BANKSEL(_xx) // select the RAM-bank with the CIC filter variables *AND* 'ADRESL/H'
// -> pseudo instruction, in fact "MOVLB 0x1" which no-one would understand)
#if(1) // (1) : process real data from ADC, (0) : TEST with constant input for MPLAB-SIM
MOVF BANKMASK(ADRESL), W // read ADRESL (A/D conversion result, low byte)
MOVWF BANKMASK(_xx+0) // store in bits 7..0 of variable 'xx' (filter input)
MOVF BANKMASK(ADRESH), W // read ADRESH (A/D conversion result, high byte)
MOVWF BANKMASK(_xx+1) // store in bits 15..8 of variable 'xx' (filter input)
# if(0) // failed attempt to 'synchronize' the ADC clock prescaler by turning the ADC off and on again
// (the ADC clock precaler doesn't seem to be reset by this)
CLRF BANKMASK(ADCON0) // turn the ADC off
BSF BANKMASK(ADCON0), 0 // turn the ADC on again
NOP // > The GO/DONE bit should not be set in the same instruction that turns on the ADC
# endif // turn the ADC off and on again to clear its prescaler ?
BSF BANKMASK(ADCON0), 1 // start next conversion as early as possible (bit 1 = "GO")
// (the current drawn by the sample and hold input caused
// a 400 ns long 'spike' on the active analog input,
// which should appear a constant time after the rising edge
// from 'IOP_ADCCLK_PIN_HI'. But the time was NOT constant :
// __________________________________ _
// Test pin ("ADCCLK") : __| |_..._|
// ____ _______________________________________
// Analog input voltage: |/
// __________ _________________________________
// another time: |/
// ______________________ _____________________
// another time: |/
// | | | | |
// delta t (ns): 0 36(!) 436 2436 ns 12500 ns
//
// -> See 'ADC speed test' in C:\pic\GPSDO\ADC_PIC16F1783.c !
// (A 12-bit A/D conversion seemed to require 17, not 15, "T_AD" cycles)
// 1 / ( 17 * 0.8 us) = max. 73.52941176 kHz.... )
#else // test with constant input; watch the filter output (yy) in MPLAB-SIM !
MOVLW ( -10 & 255) // dummy value replacing ADRESL (ADC result, low byte)
MOVWF BANKMASK(_xx+0) // store in bits 7..0 of variable 'xx' (filter input)
MOVLW ( -10 >> 8) // dummy value replacing ADRESH (ADC result, high byte)
MOVWF BANKMASK(_xx+1) // store in bits 15..8 of variable 'xx' (filter input)
#endif
#if( SWI_ADC_BITS_PER_SAMPLE==10 ) // PIC16F1783: ADC configured for 10 bits/sample ?
// Shift the A/D converter result (already in 'xx') two bits to the left
// to leave the code further below (which expects a "12-bit ADC") unchanged.
// (Running the ADC at 10 instead of 12 bit resolution saves TWO 'T_AD' cycles).
// Using 10 bits only was -more of less- just a test to find out
// if 12 bit really gives 12 bit more dynamic range, or if the
// ADC in the 'slightly overclocked PIC16F1783' is too noisy anyway,
// and if a MUCH FASTER dsPIC with 10 bit ADC would be better than
// a SLOWER ADC (in another dsPIC) with 12 bit resolution. )
// See DS page 143: in "10-bit 2's compliment" mode, result was in ADRES bits 9..0,
// so shift 'xx' two times left to have the msbit in bit 11 again:
LSLF BANKMASK(_xx+0) // bit 7->CARRY, bits 6..0 -> bits 7..1, clear bit 0
RLF BANKMASK(_xx+1) // bits 14..8 -> bits 15..9, CARRY-> bit 8
LSLF BANKMASK(_xx+0) // 2nd 16-bit "left-shift" ..
RLF BANKMASK(_xx+1) // e.g. multiply 16-bit value by four in 4 instruction cycles..
// we can JUST afford doing that HERE, at fs_in :)
#endif // ADC running in 10-bit conversion mode ?
//
// Strange relation between the input voltage (fed into pin 'AN0')
// and the result (in ADRES) :
// Vin | 0.00 V | 1.024 V | 2.048 V (=Vref) | > 2.048 V
// -------+------------+----------------+-----------------+------------
// ADRES | -5..+5 (!) | 2047 +/- noise | 4093 ... 4095 | 4095 (saturated)
// |
// |__ no typo, ADRES was "slightly negative" sometimes !
// (would the '12 bit ADC' provide 13 bits with differential input?)
//
// Vin will be biased with 1.024 V input (half reference voltage
// as selected per 'FVRCON' in ADC_Init() ),
// so the above offset must be subtracted later (to save clock cycles: AFTER decimation).
//
// From the PIC16F1782/3 datasheet, page 143 :
// > Two's complement is right justified with the sign
// > extended into the most significant bits .
// To avoid an overflow in the CIC filter calculation (which only supports 12-bit input),
// avoid negative input from ADRES here:
BTFSS BANKMASK(_xx+1),7 // bit 15 in 'xx' set ? suprise surprise ...
GOTO xx_not_negative
CLRF BANKMASK(_xx+0) // -> clear xx bits 7..0
CLRF BANKMASK(_xx+1) // -> clear xx bits 15..8
xx_not_negative: // should get HERE when the input is properly biased (and not clipping)
CLRF BANKMASK(_xx+2) // clear bits 23..16 of 'xx' (no SIGN EXPANSION required anymore)
# endasm
#endif // SWI_STANDALONE_SIMULATOR ?
//========================================================
// Below: 4th order CIC (N=4, R=4, M=2)
// for low-pass filtered decimation .
//
// took 356 cycles on an AVR(!) when downsampling
// took 172 cycles on an AVR(!) when just integrating
// (#cycles required on a PIC16 shown further below).
//
// See article by Bruce Land, Cornell University, locally saved as
// C:\literatur\DSP_Andere\CIC_Filter_and_DSP_for_GCC__Cornell_2013.htm ,
// and Circuit Cellar, October 2009, page 50 (alias page 52 of the PDF),
// C:\literatur\DSP_Andere\Circuit_Cellar_October_2009.pdf .
//
// _________ ________
// integrator1, 2,3,4 | drop 3 | |Comb |
// xx ---->(+)------------- ... --| of 4 |-----|section,|---> yy
// ('ADRES') | ______ | | samples | |details |
// . | | | | |_________| . |_below__|
// . '---| Z^-1 |--' .
// . |______| .
// fs_in fs_out = fs_in/4
//
// On a PIC16, with F_osc=40 MHz, F_cycle=10 MHz, and F_sample=80 kHz,
// there are only 10/0.08 = 125 instruction cycles between two conversions
// so it will be tough to run the complete filter 'in time' !
// Because the PIC isn't fast enough to calculate all this
// in a SINGLE PERIOD of the *INPUT* sampling clock,
// the following block must be separated into FOUR BLOCKS
// of similar execution time. We wouldn't do that in "C" of course.
// To avoid cluttering the original C code (below),
// the hand-crafted assembler code is shown further below.
//
#if( SWI_STANDALONE_SIMULATOR ) // 2016-02-15 : The code originally generated by XC8
// was MUCH too slow.. this would only be ok for a few kHz sampling !
//
// Calculate the four integrators. This code runs at the ADC sampling rate (fs_in).
integrator1 += xx ;
// EMU_Truncate24Bit( &integrator1 );
//
// Test with xx=1, at THIS point, using 32-bit integers (24 bit example in the PIC assembler version):
// integrator1 = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 100, ...... 1000,
// integrator2 = 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 4950, 499500,
// integrator3 = 0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220, 166650, 166666500,
// integrator4 = 0, 1, 5, 15, 35, 70, 126, 210, 330, 495, 715, 4249575, -1199714710,
// comb1 = 0, 0, 0, 0, 35, 35, 35, 35, 330, 330, 330, 1091706, 1307521506,
// comb2 = 0, 0, 0, 0, 35, 35, 35, 35, 330, 330, 330, 256656, 31331856,
// comb3 = 0, 0, 0, 0, 35, 35, 35, 35, 330, 330, 330, 43776, 504576,
// yy = 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, ... 1, 1(!),
//
if( (integrator1==100) || (integrator1==1000) || (integrator1==10000) )
{ xx=xx; // <<< place for a 'conditional breakpoint'
}
integrator2 += integrator1;
// EMU_Truncate24Bit( &integrator2 );
integrator3 += integrator2;
// EMU_Truncate24Bit( &integrator3 );
integrator4 += integrator3;
// EMU_Truncate24Bit( &integrator4 );
++downsample_cnt;
// Run the 'combs' (Kammfilter) for every 4th input sample.
// This generates the decimated, low-pass filtered output
// (at fs_out = fs_in/4) .
//
// ex: if( (downsample_cnt & 0x03) == 0) // << this would be very slow on a PIC
if( downsample_cnt & 4 ) // -> single-bit-test, faster on a PIC16 (btfss+goto)
{ downsample_cnt = 0;
//
// Here for every FOURTH sample (e.g. at fs_out, not fs_in).
//
//.....................................................................
// Block diagram of the *COMB* section by WB, trying to .
// understand the names and purposes .
// of the variables used in the code fragment further below, .
// looking for a way to eliminate a few of these variables... .
// .
// .
// --o-- = circuit node (with variable-name shown above) .
// | .
// .
// + .
// ->-(+)->- = Subtractor (note the signs of the inputs) .
// -| .
// .
// .-. .
// |D| = Delay stage (delays for a single sample) .
// '-' .
// .
// integrator4 comb1 + comb2 + comb3 + 'yy' .
// ->-o---->---(+)->-o---->---(+)->-o---->---(+)->-o---->---(+)-->-- .
// | -| | -| | -| | -| .
// | .-. .-. | | .-. .-. | | .-. .-. | | .-. .-. | .
// '-|D|-|D|-' '-|D|-|D|-' '-|D|-|D|-' '-|D|-|D|-' .
// '-' '-' '-' '-' '-' '-' '-' '-' .
// .
// Names: ^ ^ ^ ^ ^ ^ ^ ^ .
// /|\ /|\ /|\ /|\ /|\ /|\ /|\ /|\ .
// | | | | | | | | .
// | | | | | | | | .
// last_ last2_ last_ last2_ last_ last2_ last_ last2_ .
// integrator4 comb1 comb2 comb3 .
// .
//.....................................................................
//
// The above COMB SECTION is the price to pay for N=4, R=4, M=2 .
// ( another price to pay is the passband droop,
// but the receiver will compensate it on 'his' side.
// At least the PIC16 cannot run the Chebyshev compensator,
// which would follow right next to 'yy' in the above schematics.
// )
//
// First calculate the four 'comb' outputs (yy is the 4th) :
comb1 = integrator4 - last2_integrator4;
// EMU_Truncate24Bit( &comb1 );
comb2 = comb1 - last2_comb1;
// EMU_Truncate24Bit( &comb2 );
comb3 = comb2 - last2_comb2;
// EMU_Truncate24Bit( &comb3 );
# if(1)
yy = (int16_t)((comb3 - last2_comb3)>>12) ; // original gain scaling was >>12 (divide by 4096) !
// Caution: the behaviour of the bitwise shift right operator may be 'implementation specific'.
// Borland expanded the sign as one would expect, other compilers didn't .
// Test with xx=1, at THIS point, using 32-bit integers (24 bit example in the PIC assembler version):
// call # : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ... 250,
// integrator1 = 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, ... 1000,
// comb1 = 35, 330, 1330, 3547, 7490, 13674, 22610, 34810, 50786, 70050, 96114, 126490, ... 1323377930,
// comb2 = 35, 330, 1295, 3216, 6160, 10128, 15120, 21136, 28176, 36240, 45328, 55440, ... 31585680,
// comb3 = 35, 330, 1260, 2886, 4865, 6912, 8960, 11008, 13056, 15104, 17152, 19200, ... 506624,
// yy = 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, ... 1,
// w. xx=10: yy = 0, 0, 2, 6, 8, 9, 9, 10, 10, 10, ...
# else
yy = (int16_t)((comb3 - last2_comb3) ) ;
# endif
//
// Update state .. these are the EIGHT delay elements in the schematics.
//
//
last2_integrator4 = last_integrator4;
last_integrator4 = integrator4; // <- this cannot be 'postponed' ..
last2_comb1 = last_comb1; // <- but these calculations may ..
last_comb1 = comb1; // .. be postponed until the next
last2_comb2 = last_comb2; // conversion cycle, to spread
last_comb2 = comb2; // the CPU load more equally
last2_comb3 = last_comb3; // between subsequent interrupts.
last_comb3 = comb3; // <- output of the LAST delay line
} // end if < time to run the 'combs' and to update the CIC 'states' ? >
// Cycles required by PIC16F1783, using the above C code compiled with 'XC8 free':
// FORGET IT ! The "free" edition produces severely bloated code,
// it's ok for the initialisation, but not for the filter code.
//
#else // Try to do this in PIC16 inline assembler ?
# asm // Oh well. Using a stupid C compiler (XC8 'free')
// to write most of this stuff in Assembler...
// BANKSEL(_xx) // select the RAM-bank with the CIC filter variables, e.g. 'MOVLB 0x01'.
// (not necessary when already in the right bank - see code above)
// Calculate the four integrators. This code runs at the ADC sampling rate (fs_in, e.g. 80 kHz).
// integrator1 += xx ; // shown in 'disassembly' |--|
MOVF BANKMASK(_xx), W // address : 0xA0 & 0x7F = 0x20 in the MOVF operand !
ADDWF BANKMASK(_integrator1+0), F // address : 0xA4 & 0x7F = 0x24 ...
MOVF BANKMASK(_xx+1), W // address : 0xA1 & 0x7F = 0x20 .. etc, etc
ADDWFC BANKMASK(_integrator1+1), F // address : 0xA5 & 0x7F = 0x25
MOVLW 0 // bits 23..16 : shouldn't this be SIGN EXTENDED (0xFF when negative) ??
ADDWFC BANKMASK(_integrator1+2), F // address : 0xA6 & 0x7F = 0x26 (bits 23..16, incremented on Carry)
# if( sizeof(cic_t) >= 4 ) // optional, for four-byte integer..
ADDWFC BANKMASK(_integrator1+3), F // address : 0xA7 & 0x7F = 0x27 (bits 31..24, optional)
# endif
//
// Test with xx=1, at THIS point, using 24-bit integers (32 bit example in the plain C version further above):
// integrator1 = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 100, ...... 1000,
// integrator2 = 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 4950, 499500,
// integrator3 = 0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220, 166650, 166666500,
// integrator4 = 0, 1, 5, 15, 35, 70, 126, 210, 330, 495, 715, 4249575, -1199714710,
// integrator2 += integrator1;
MOVF BANKMASK(_integrator1+0), W
ADDWF BANKMASK(_integrator2+0), F
MOVF BANKMASK(_integrator1+1), W
ADDWFC BANKMASK(_integrator2+1), F
MOVF BANKMASK(_integrator1+2), W
ADDWFC BANKMASK(_integrator2+2), F
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_integrator1+3), W
ADDWFC BANKMASK(_integrator2+3), F
# endif
// integrator3 += integrator2;
MOVF BANKMASK(_integrator2+0), W
ADDWF BANKMASK(_integrator3+0), F
MOVF BANKMASK(_integrator2+1), W
ADDWFC BANKMASK(_integrator3+1), F
MOVF BANKMASK(_integrator2+2), W
ADDWFC BANKMASK(_integrator3+2), F
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_integrator2+3), W
ADDWFC BANKMASK(_integrator3+3), F
# endif
// integrator4 += integrator3;
MOVF BANKMASK(_integrator3+0), W
ADDWF BANKMASK(_integrator4+0), F
MOVF BANKMASK(_integrator3+1), W
ADDWFC BANKMASK(_integrator4+1), F
MOVF BANKMASK(_integrator3+2), W
ADDWFC BANKMASK(_integrator4+2), F
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_integrator3+3), W
ADDWFC BANKMASK(_integrator4+3), F
# endif
// At THIS point, approximately 40 cycles after entering the ISR ...
// ++downsample_cnt;
INCF BANKMASK(_downsample_cnt), F
// switch( downsample_cnt ) ..
MOVF BANKMASK(_downsample_cnt), W
ADDWF pcl, F // add downsample_cnt to the current program counter, low byte.
// No risk due to 8-bit overflow here because the interrupt service handler
// is located a low address (to make the above computed goto possible).
NOP // downsample_cnt = 0 : never occurs after incrementing downsample_cnt !
GOTO cic_emit_yy_lo // downsample_cnt = 1 : emit old output (yy, LSB) to UART
GOTO cic_calc_combs2 // downsample_cnt = 2 : calculate the combs, 2nd part
GOTO cic_emit_yy_hi // downsample_cnt = 3 : emit old output (yy, MSB) to UART
cic_calc_combs1: // in THIS 'phase' of downsampling, calculate the combs, 1st part
// 4th downsampling phase: calculate combs
CLRF BANKMASK(_downsample_cnt) // back from 4th to 1st downsampling-phase
// First part of the 'combs' calculation (see C source and schematics above).
// At this point, t = 43 instruction cycles after entering the ISR.
//
// comb1 = integrator4 - last2_integrator4; 'zero' the stopwatch in MPLAB here !
MOVF BANKMASK(_last2_integrator4+0),W // LSByte first.. RIGHT OPERAND for the subtraction
SUBWF BANKMASK(_integrator4+0),W // "Subtract W from f" ... here, with the destination in W, not 'f' ("file") !
MOVWF BANKMASK(_comb1+0) // store result bits 7..0
MOVF BANKMASK(_last2_integrator4+1),W // middle byte next..
SUBWFB BANKMASK(_integrator4+1),W
MOVWF BANKMASK(_comb1+1) // store result bits 15..8
MOVF BANKMASK(_last2_integrator4+2),W // higher byte next..
SUBWFB BANKMASK(_integrator4+2),W
MOVWF BANKMASK(_comb1+2) // store result bits 23..16
// -> 24-bit subtraction WITH RESULT IN A THIRD VARIABLE requires 9 cycles.
# if( sizeof(cic_t) >= 4 ) // optional, for four-byte integer..
MOVF BANKMASK(_last2_integrator4+3),W // highest byte last..
SUBWFB BANKMASK(_integrator4+3),W
MOVWF BANKMASK(_comb1+3) // store result bits 31..24
# endif
// comb2 = comb1 - last2_comb1;
// BSF _CARRY_ // Carry ~ "nothing borrowed" (for SUBWF)
MOVF BANKMASK(_last2_comb1+0),W // LSByte first..
SUBWF BANKMASK(_comb1+0),W // note the destination is W, not F here!
MOVWF BANKMASK(_comb2+0) // store result bits 7..0
MOVF BANKMASK(_last2_comb1+1),W // middle byte next..
SUBWFB BANKMASK(_comb1+1),W
MOVWF BANKMASK(_comb2+1) // store result bits 15..8
MOVF BANKMASK(_last2_comb1+2),W // higher byte next..
SUBWFB BANKMASK(_comb1+2),W
MOVWF BANKMASK(_comb2+2) // store result bits 23..16
# if( sizeof(cic_t) >= 4 ) // optional, for four-byte integer..
MOVF BANKMASK(_last2_comb1+3),W // highest byte last..
SUBWFB BANKMASK(_comb1+3),W
MOVWF BANKMASK(_comb2+3) // store result bits 31..24
# endif
// comb3 = comb2 - last2_comb2 :
MOVF BANKMASK(_last2_comb2+0),W // LSByte first..
SUBWF BANKMASK(_comb2+0),W // note the destination is W, not F here!
MOVWF BANKMASK(_comb3+0) // store result bits 7..0
MOVF BANKMASK(_last2_comb2+1),W // middle byte next..
SUBWFB BANKMASK(_comb2+1),W
MOVWF BANKMASK(_comb3+1) // store result bits 15..8
MOVF BANKMASK(_last2_comb2+2),W // higher byte next..
SUBWFB BANKMASK(_comb2+2),W
MOVWF BANKMASK(_comb3+2) // store result bits 23..16
# if( sizeof(cic_t) >= 4 ) // optional, for four-byte integer..
MOVF BANKMASK(_last2_comb2+3),W // highest byte last..
SUBWFB BANKMASK(_comb2+3),W
MOVWF BANKMASK(_comb3+3) // store result bits 31..24
# endif
// yy = (int16_t)((comb3 - last2_comb3)>>12) ; remember the 'CIC gain' !
// 8 of these ^^ 12 shifts are achieved via byte-offset.
// Since the original input (from the ADC) was 12 bits wide,
// and we will send 16-bit samples via the UART,
// don't remove the 'gain' in yy completely.
// This may actually give a few more bits of effecitve resolution.
// Test with xx=1, at THIS point, using 23-bit integers (32 bit example in the "C" variant further above):
// call # : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ... 250,
// integrator1 = 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, ... 1000,
// comb1 = 35, 330, 1330, 3547, 7490, 13674, 22610, 34810, 50786, 70050, 96114, 126490, ... 1323377930,
// comb2 = 35, 330, 1295, 3216, 6160, 10128, 15120, 21136, 28176, 36240, 45328, 55440, ... 31585680,
// comb3 = 35, 330, 1260, 2886, 4865, 6912, 8960, 11008, 13056, 15104, 17152, 19200, ... 506624,
// yy(*16) = 0, 0, 1, 4, 10, 15, 16, 16, 16, 16, 16, 16, ... 1,
// w. xx=10: "" = 0, 1, 12, 48, 100, 141, 158, 160, 160, 160, ...
// yy remained stable even when the integrators or combs rolled over,
// and the signs of the intergrators and combs 'flipped like crazy'.
// After thousands of comb calculations, with xx=2047 (constant): yy = 32752 .
// After thousands of comb calculations, with xx=4095 (constant): yy = 65520 .
// After thousands of comb calculations, with xx=2047 (constant): yy = 32752 .
// After thousands of comb calculations, with xx=2047 (constant): yy = 32752 .
//
MOVF BANKMASK(_last2_comb3+1),W // LSByte first.. (without comb3 bits 7..0)
SUBWF BANKMASK(_comb3+1),W // minus , destination in W
MOVWF BANKMASK(_yy+0) // store result bits 7..0
MOVF BANKMASK(_last2_comb3+2),W // middle byte next..
SUBWFB BANKMASK(_comb3+2),W
MOVWF BANKMASK(_yy+1) // store result bits 15..8
// To let the result (yy) look nice, the sign could be extended into bits 23..16:
// But since we never use those bits (yet?), don't waste time with that.
# if( sizeof(cic_t) >= 4 ) // optional, for four-byte integer.. 24 bit out ?
# endif
// Update 'previous' and 'previous previous' values for the CIC filter's comb section:
// last2_integrator4 = last_integrator4;
MOVF BANKMASK(_last_integrator4+0), W // load LSB..
MOVWF BANKMASK(_last2_integrator4+0) // store MSB, etc..
MOVF BANKMASK(_last_integrator4+1), W
MOVWF BANKMASK(_last2_integrator4+1)
MOVF BANKMASK(_last_integrator4+2), W
MOVWF BANKMASK(_last2_integrator4+2)
# if( sizeof(cic_t) >= 4 ) // optional, for four-byte integer.. 24 bit out ?
MOVF BANKMASK(_last_integrator4+3), W
MOVWF BANKMASK(_last2_integrator4+3)
# endif
// last_integrator4 = integrator4;
MOVF BANKMASK(_integrator4+0), W // load LSB..
MOVWF BANKMASK(_last_integrator4+0) // store MSB, etc..
MOVF BANKMASK(_integrator4+1), W
MOVWF BANKMASK(_last_integrator4+1)
MOVF BANKMASK(_integrator4+2), W
MOVWF BANKMASK(_last_integrator4+2)
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_integrator4+3), W
MOVWF BANKMASK(_last_integrator4+3)
# endif
// At THIS point, the value in 'integrator4' isn't needed anymore,
// thus the calculations below (cic_calc_combs2) can be postponed
// until the next A/D conversion cycle(s) .
// The PIC16 firmware was almost running out of time to spend
// in the interrupt handler (approx 90 cycles after entering the ISR),
// so it's time to return from the interrupt here :
goto cic_exit
// - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// above: first part of the COMB FILTER calculations .
// below: second part of the COMB FILTER calculations,
// performed in the (n+2) nd interrupt .
// - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cic_calc_combs2: // calculate the comb filters, 2nd part
// last2_comb1 = last_comb1;
MOVF BANKMASK(_last_comb1+0), W // load LSB..
MOVWF BANKMASK(_last2_comb1+0) // store MSB, etc..
MOVF BANKMASK(_last_comb1+1), W
MOVWF BANKMASK(_last2_comb1+1)
MOVF BANKMASK(_last_comb1+2), W
MOVWF BANKMASK(_last2_comb1+2)
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_last_comb1+3), W
MOVWF BANKMASK(_last2_comb1+3)
# endif
// last_comb1 = comb1;
MOVF BANKMASK(_comb1+0), W // load LSB..
MOVWF BANKMASK(_last_comb1+0) // store MSB, etc..
MOVF BANKMASK(_comb1+1), W
MOVWF BANKMASK(_last_comb1+1)
MOVF BANKMASK(_comb1+2), W
MOVWF BANKMASK(_last_comb1+2)
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_comb1+3), W
MOVWF BANKMASK(_last_comb1+3)
# endif
// last2_comb2 = last_comb2;
MOVF BANKMASK(_last_comb2+0), W // load LSB..
MOVWF BANKMASK(_last2_comb2+0) // store MSB, etc..
MOVF BANKMASK(_last_comb2+1), W
MOVWF BANKMASK(_last2_comb2+1)
MOVF BANKMASK(_last_comb2+2), W
MOVWF BANKMASK(_last2_comb2+2)
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_last_comb2+3), W
MOVWF BANKMASK(_last2_comb2+3)
# endif
// last_comb2 = comb2;
MOVF BANKMASK(_comb2+0), W // load LSB..
MOVWF BANKMASK(_last_comb2+0) // store MSB, etc..
MOVF BANKMASK(_comb2+1), W
MOVWF BANKMASK(_last_comb2+1)
MOVF BANKMASK(_comb2+2), W
MOVWF BANKMASK(_last_comb2+2)
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_comb2+3), W
MOVWF BANKMASK(_last_comb2+3)
# endif
// At this point (with 24-bit integers), t = 69 instruction cycles...
// last2_comb3 = last_comb3;
MOVF BANKMASK(_last_comb3+0), W // load LSB..
MOVWF BANKMASK(_last2_comb3+0) // store MSB, etc..
MOVF BANKMASK(_last_comb3+1), W
MOVWF BANKMASK(_last2_comb3+1)
MOVF BANKMASK(_last_comb3+2), W
MOVWF BANKMASK(_last2_comb3+2)
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_last_comb3+3), W
MOVWF BANKMASK(_last2_comb3+3)
# endif
// last_comb3 = comb3;
MOVF BANKMASK(_comb3+0), W // load LSB..
MOVWF BANKMASK(_last_comb3+0) // store MSB, etc..
MOVF BANKMASK(_comb3+1), W
MOVWF BANKMASK(_last_comb3+1)
MOVF BANKMASK(_comb3+2), W
MOVWF BANKMASK(_last_comb3+2)
# if( sizeof(cic_t) >= 4 )
MOVF BANKMASK(_comb3+3), W
MOVWF BANKMASK(_last_comb3+3)
# endif
GOTO cic_exit // end if < time to run the 'combs' and to update the CIC 'states' ? >
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// above: second part of the comb calculations (running at fs_out)
// below: no CIC calculations but output via UART.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cic_emit_yy_lo: // in THIS phase, emit low-pass filtered output (yy, LSB) to the UART
// unnecessary: BANKSEL(_yy) // select the RAM-bank with the CIC filter variables
// Added 2016-02-21 : The PIC's analog input will be biased by Vref/2.
// To have a (signed) output of 'zero' despite that (ADRES=2047 +/- noise),
// subtract 32752(!) from 'yy'. The result is stored back in 'yy'.
MOVLW ( 32767 & 255 ) // Bias value (details above), lower byte
SUBWF BANKMASK(_yy+0),F // minus , destination in REGISTER ("file")
MOVLW ( 32767 >> 8 ) // Bias value, upper byte
SUBWFB BANKMASK(_yy+1),F // subtract offset from yy, bits 15..8
// The effect can be checked with Spectrum Lab's input monitor (scope).
// Select 'Coupling: DC' for that purpose.
BTFSC BANKMASK(_fflags),FFLAG_UART_START // start DAC->UART output ? (ONLY AT THE LSBYTE)
BSF BANKMASK(_fflags),FFLAG_UART_ACTIVE // yes -> activate the output NOW
BTFSS BANKMASK(_fflags),FFLAG_UART_ACTIVE // DAC->UART output active ?
GOTO cic_exit // no -> do NOT send 'yy' to the UART
MOVF BANKMASK(_yy+0), W // load lowpass filtered, decimated output (LSB)
BANKSEL (TX1REG) // select RAM- and register bank of the UART TX register
MOVWF BANKMASK(TX1REG) // send LSByte to UART
//
// When tested with the simulator, got here at t = 46 cycles after entering the ISR
//
GOTO cic_exit
cic_emit_yy_hi: // in THIS phase, emit low-pass filtered output (yy, MSB) to the UART
// ex: BANKSEL(_yy) // select the RAM-bank with the CIC filter variables
BTFSS BANKMASK(_fflags),FFLAG_UART_ACTIVE // DAC->UART output active ?
GOTO cic_exit // no -> do NOT send 'yy' to the UART
MOVF BANKMASK(_yy+1), W // load lowpass filtered, decimated output (MSB)
BANKSEL (TX1REG) // select RAM- and register bank of the UART TX register
MOVWF BANKMASK(TX1REG) // send MSByte to UART
//
// When tested with the simulator, also got here at t = 46 cycles after entering the ISR.
// That means, between writing the LSByte (above) and the MSByte (here),
// exactly TWO 80-kHz interrupts have passed, and the UART had
// 2/(80kHz) = 25 us to send the previous byte. At 460800 bits/second,
// and 10 "bits per byte" (including start- and stopbit), the UART requires
// 10/(460.8kHz) = 21.7 us to send a single byte, thus NO NEED TO WAIT here.
// GOTO cic_exit // don't waste TWO precious cycles (for a 'goto') here
cic_exit:
// Timing tests with MPLAB (simulator), after zeroing MLPAB's 'stopwatch'
// with a breakpoint on the entry of ADC_Sampling_ISR(), and another breakpoint
// on the closing curly brace below ("return from interrupt") :
// downsample_cnt 0 -> 1 : spent ~ 53 cycles in the interrupt (integrators and UART output only)
// downsample_cnt 1 -> 2 : spent ~ 86 cycles in the interrupt (comb calculations, 2nd part)
// downsample_cnt 2 -> 3 : spent ~ 51 cycles in the interrupt (integrators and UART output only)
// downsample_cnt 3 -> 0 : spent ~ 94 cycles in the interrupt (comb calculations, 1st part)
// Confirmed with an oscilloscope and real target hardware,
// using the pulse length between IOP_ADCCLK_PIN_HI and IOP_ADCCLK_PIN_LO.
// Measured 8.6 us -> Ok (with 80 kHz interrupt frequency ~ 12.5 us).
// Enough CPU time remaining for the main loop.
// No adverse effect on the GPSDO's phase noise.
// Reasonable image rejection - see spectra in the "html" folder.
# endasm
#endif // < use the original C code, or hand-crafted PIC assembler for the CIC filter ? >
IOP_ADCCLK_PIN_LO; // -> BCF 0x0C, 0x04
PIR1bits.TMR2IF = 0; // clear "Timer2 Interrupt Flag" (don't let the interrupt 'pend')
} // end ADC_Sampling_ISR() [the one-and-only interrupt service routine for midrange PICs]
// ... other subroutines which MAY not work properly when crossing a 256-byte boundary go here ...
//---------------------------------------------------------------------------
void ClearMem( uint8_t *pbDest, uint8_t nBytes) // clears a block of memory
{
while(nBytes--)
{ *pbDest++ = 0x00;
}
} // end ClearMem()
//---------------------------------------------------------------------------
void LimitToPlusMinus32000( int16_t *pi16 )
{
// If the XC8 compiler is smart enough, it uses one of the two
// 'INDF' registers to pass the pointer, and produce only a few lines of assembly.
// But the result (in the *.am listing) looked terrible - don' try to understand it !
if( *pi16 > 32000 )
{ *pi16 = 32000;
}
if( *pi16 < -32000 ) // -> movf, movwf, clrf fsr1h, ... (about 20!! instructions for this line)
{ *pi16 = -32000; // -> movf, movwf, clrf fsr1h, movlw 0, movwi [0]fsr1, movlw 0x83, movwi [1]fsr1, superfluent goto, return .
}
} // end LimitToPlusMinus32000()
//---------------------------------------------------------------------------
void LimitYToPlusMinus32k(void)
{
if( i32TempY.i32 > 32767 )
{ i32TempY.i32 = 32767;
}
else if( i32TempY.i32 < -32767 )
{ i32TempY.i32 = -32767;
}
// This could be easily optimized .. first look at the upper 16 bits only,..
} // end LimitYToPlusMinus32k()
//---------------------------------------------------------------------------
void FillString( char *pszDest, uint8_t pattern, uint8_t len)
{
while(len--) // <- even THIS was too complicated for CC5X (so WB ditched it)
{ *pszDest++ = pattern;
}
*pszDest = '\0';
}
//---------------------------------------------------------------------------
void Plot( char *pszDest, uint8_t pattern, int pos )
// Plots into a space-filled string :
// min center max|
// | | |
// | | |
// 0 1 2 3 4 5 6 7
// 01234567890123456789012345678901234567890123456789012345678901234567890123456789
// <------------------ 39 characters ---->|<------------- 39 characters --------->
// | "0"
{ pos += 39; // character index for the zero indicator (vertical center line)
if( pos < 0 )
{ pos = 0;
}
if( pos > 78 )
{ pos = 78;
}
pszDest[pos] = pattern;
}
//---------------------------------------------------------------------------
void wait_10ms(void)
{
wWaitCounter=15625; // 15625 for Fcycle = 10 MHz, XC8 'free'
while( wWaitCounter-- )
{
}
// when running on the internal oscillator, the above loop
// took 15 seconds instead of 10 ms
}
//---------------------------------------------------------------------------
void wait_1ms(void)
{
wWaitCounter=1562; // 1562 for Fcycle = 10 MHz, XC8 'free'
while( wWaitCounter-- )
{
}
// when running on the internal oscillator, the above loop
// took 1.5 seconds instead of 1 ms
}
//---------------------------------------------------------------------------
void ResetIntegralsAndLowpass(void) // -> ErrorIntegral = LowpassIn = LowpassOut = 0
{ i16LowpassIn = 0;
i16ErrorIntegral = 0;
i32ErrorIntegralIntegral.i32 = 0;
i32LowpassOut.i32 = 0;
} // end ResetIntegralsAndLowpass()
//---------------------------------------------------------------------------
void ProcessCapturedSyncPulse(void)
// [in] CCPR1 : 16-bit hardware timer, captured at the rising edge
// of the GPS sync pulse .
// Counts the number of OCXO clock cycles (10 MHz) .
// [out] i16FreqOffset = measured OXCO frequency minus 10 MHz .
// Ideally zero. Unit is 'OCXO cycles per second'.
// With f_oxco = 10.0x MHz, one step is approximately 100 ns.
// The SUMMED UP i16FreqOffset readings, taken over time,
// but limited to a certain maximum is later used
// to *PHASE-LOCK* the OCVCXO to the GPS reference
// ( -> I controller ) .
// The MOMENTARY i16FreqOffset reading will steer
// the output control voltage, like a *FREQUENCY LOCK* loop
// ( -> P controller ) .
// [out] i16ErrorIntegral = error integral. Details further below.
// i32ErrorIntegralIntegral = integral of i16ErrorIntegral.
// [out] i16LowpassIn = input for the software lowpass, see block diagram.
{
// any non-brain-damaged C compiler allows THIS:
i16FreqOffset = (int16_t)CCPR1 - (int16_t)wPrevCapture;
wPrevCapture = CCPR1; // save 16-bit timer capture register for the next time
// The timer- and capture registers are only 16 bit wide.
// With a 1-second sync interval, and 10 MHz OCXO, the difference
// between two readings should be 10000000 (decimal) = 0x00989680 .
// Only the lower 16 bits are actually read -> ideally 0x9680 .
// After subtracting the above 'ideal' value, the rest should be "almost zero":
i16FreqOffset -= 0x9680; // -> error value, signed, ideally ZERO,
// one step = ca. 100 ns error ( = 1 / 10 MHz capture timer input frequency,
// which IN THIS CASE (PIC16F1783) is , because Ftimer = Fcycle = Fosc/4,
// Fosc=4*Focxo -> capture timer input frequency = 4 * 10 MHz / 4 = 10 MHz.
// A 'better' microcontroller where the capture-timer runs at 40 MHz
// would give 25 ns instead of 100 ns capture resolution.
// But anyway, we can measure the 10 MHz OCXO frequency with 1 Hz resolution
// at 1 second gate time, thus the UNIT of 'i16FreqOffset' is simply HERTZ !
// Verified by measuring 16FreqOffset at various control voltages,
// VCOCXO made by OFC, labelled "OFC MC598X4 - 010W 10.000000 MHz"
// |_ 1Hz ?!
// Vctrl = 0 V -> 10 MHz - 57 Hz -> i16FreqOffset = -57
// Vctrl = 5 V -> 10 MHz + 0 Hz -> i16FreqOffset = 0
// Vctrl = 12 V -> 10 MHz + 80 Hz -> i16FreqOffset = +80 .
// -> "Electrical" frequency tuning range = -57/10e3 = -5.7e-6 = -5.7 ppm (?!)
// to +80/10e3 = 8.0e-6 = +8.0 ppm (!?)
// A quite large tuning range for a "10.000000 MHz"-VCOCXO !
// External circuitry (resistor divider for the Vctrl output)
// now reduces the tuning range to +/- 5 Hz .
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// above: coherent measurement of the VCOCXO frequency error
// below: implementation of the control loop, during and after warm-up
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if( bSyncPulseCounter < 255 )
{ ++bSyncPulseCounter;
if( bSyncPulseCounter > 1 ) // the pulses produces garbage readings !
{
if( (i16FreqOffset < -2/*Hz*/) || (i16FreqOffset > 2/*Hz*/) )
{ bSyncPulseCounter = 1; // frequency still "too far off" -> prolong 'warm-up' phase
}
// During the initial 255 seconds (or more if 'the oven is cold'),
// use a faster control loop to find the best possible Vctrl 'bias' point .
LimitToPlusMinus32000( &i16ErrorIntegral );
// Any non-brain-damaged, real C compiler should have no trouble with this (CC5X didn't understand)
i16ErrorIntegral += i16FreqOffset; // <- won't overflow at +/- 32767 ...
i32TempY.i32 = 64/*Ki*/ * i16ErrorIntegral; // fast-settling integrating controller only
LimitYToPlusMinus32k();
i16LowpassIn = i32TempY.i16.lo;
if( bSyncPulseCounter==255 ) // finished coarse measurement -> switch to normal loop controller (further below)
{ // Use the current 'Vctrl' value as the new 'Vctrl bias' value.
// Under ideal conditions, this will keep the error integral,
// and the error-integral-integral ZERO after switching the loop mode:
# if( C_VCTRL_NEGATIVE_SLOPE )
// LATER: PSMC1DC = (uint16_t)( i32LowpassOut.i16.hi + u16VctrlBias ); // frequency too high -> INCREASE PWM DC
u16VctrlBias += i32LowpassOut.i16.hi;
# else
// LATER: PSMC1DC = (uint16_t)( u16VctrlBias - i32LowpassOut.i16.hi ); // frequency too high -> DECREASE PWM DC
u16VctrlBias -= i32LowpassOut.i16.hi;
# endif
ResetIntegralsAndLowpass(); // -> ErrorIntegral = LowpassIn = LowpassOut = 0
bLowpassSpeed = 1; // switch to the low-speed output lowpass (tau = 107 sec)
# if( SWI_STANDALONE_SIMULATOR )
EMU_fSimulationPaused = TRUE;
# endif // SWI_STANDALONE_SIMULATOR ?
} // end if( bSyncPulseCounter==255 )
} // end if( bSyncPulseCounter > 2 )
} // end if( bSyncPulseCounter < 255 )
else // 'norma' operation -> use slow, low-noise loop controller.
{ // Accumulate the error signal (Regelabweichung), using a numeric integrator:
if( (i16FreqOffset >= -SWI_MAX_FREQ_OFFSET_HZ) && (i16FreqOffset <= SWI_MAX_FREQ_OFFSET_HZ) )
{ // Integrate the error value, limited to 16 bit signed :
LimitToPlusMinus32000( &i16ErrorIntegral );
i16ErrorIntegral += i16FreqOffset; // <- won't overflow at +/- 32767
i32ErrorIntegralIntegral.i32 += i16ErrorIntegral;
// Beware: two integrators in series, in a loop with NEGATIVE feedback
// can easily turn the control loop into an oscillator .
// To prevent oscillation, extra measures are taken further below.
//
// Phase (error) = integral of frequency (error). In this case 'coherent'
// because the capture-feeding timer keeps running continously (it's not
// stopped / started or restarted with each new sync pulse).
// Thus i16ErrorIntegral can be used to steer the VCOCXO (Vctrl)
// like a *PLL* later (phase locked, not just a frequency locked loop).
// If each sync pulse would restart the measurement of the frequency offset,
// there would be a phase slip. This way there is no phase slip,
// and i16ErrorIntegral keeps track all oscillator cycles *since initial lock* .
// More details in gpsdo_pic_notes.htm .
//
// For absolute phase measurements and coherent PSK experiments,
// the VCOCXO phase (measured against the GPS signal) shall always
// 'crawl back' to the original value (of the first lock).
// Integrator #1 would be sufficient to keep the FREQUENCY error zero
// in the long run, but cannot eliminate the PHASE error.
// Integrator #2 was added to bring down i16ErrorIntegral to zero
// (-> removes phase error after sufficiently long time).
//
//
// Block diagram with FREQUENCY- and PHASE- locking loop
// -----------------------------------------------------
// __________
// | |
// _\|Integrator|_ ErrorIntegralIntegral
// | /| #2 | |
// | |__________|(*) Ki2
// ____________ | | ____________
// | | | Ki1 | | Very 'slow'|
// freqOffset--| Integrator |------>----(*)--(+)--| Lowpass |-->--.
// /|\ | #1 | ErrorIntegral | (1st order)| |
// | |____________| |____________| |
// ____|____ ________ _______ _____ |
// | Timer | | | |analog | | | duty cycle |
// | Capture |--<--| VCOCXO |---<---|lowpass|--<--| PWM |-<----(-)----'
// |_________| |________| Vctrl |_______| |_____| |
// /|\ /|\ .
// | sync pulse |
// ( 1 PPS from GPS ) VctrlBias
//
//
//
// Estimation of the control path 'gain' (Regelstrecke):
// Input (PWM for Vctrl): 0 ... 65535 (ideally 32767)
// Output(freqOffset) : -5 Hz .. + 5 Hz (ideally zero)
// G_path = 10 Hz / 65536 = approximately 150 uHz per PWM-step
//
// Test cases :
//
// (2) Single 'I' controller only (integrating),
// can keep the FREQUENCY error zero in the long run
// (because an integrator has infinite gain at DC),
// but doesn't return to original PHASE:
// Ki1 = 256 : loop oscillation period ~ 350 seconds,
// Error peaks [Hz]: -1.127 0.510 -0.231 0.105 -0.048 0.023 -0.011 0.002 .
// Completely stabilized after ~ 1870 seconds .
// Ki1 = 64 : loop oscillation period ~ 754 seconds,
// Error peaks [Hz]: -5.000 1.520 -0.262 0.045 -0.008 .
// Completely stabilized after ~ 1945 seconds .
// Ki1 = 32 : nicely damped loop oscillation period ~ 1270 seconds,
// Error peaks [Hz]: -5.000 1.064 -0.053 0.004 .
// Completely stabilized after ~ 2000 seconds .
// ( close enough to the limit of aperiodic oscillation
// = aperiodischer Grenzfall.. what's that in english ?)
// Ki1 = 16 : no detectable loop oscillation period,
// Error peaks [Hz]: -5.000 0.696 (nothing more, strong damping).
// Completely stabilized again after ~2342 seconds,
// but more remaining phase error when VCOCXO had to be 'pulled far' !
// (no remaining frequency error, but WB also wanted to eliminate
// the PHASE error, which seemed to require an additional 'slow loop')
//
// (3) Double 'I' controller with two integrators as in the block diagram,
// can bring down the 'phase error' to zero (in the long run),
// but needs extra care to avoid oscillation.
// Ki2 must be much less than Ki1 (e.g. Ki1=32, Ki2=0.0625) .
//
# if(0) // single, simple 'I' controller:
i32TempY.i32 = 32/*Ki*/ * i16ErrorIntegral; // integrating controller only
# else // double integrator:
// (error integral plus an 'integral of the error integral')
i32TempY.i32 = 32/*Ki*/ * i16ErrorIntegral // integrated frequency error (-> phase error)...
+ (i32ErrorIntegralIntegral.i32 >> 4); // integrated phase error (with very low 'gain' to avoid oscillation)
// |___ divide by 2^N, e.g.: Ki2 = 1 / (2^4) = 0.0625
# endif // < simple or double 'I' controller > ?
// Tests results with EMU_dblExtraOscErr_Hz = 1 plus 'full swing' step at t_sim = 10 sec,
// time constant of the output lowpass still tau = 65536 / 610.3 Hz = 107 sec.
// Ki1 | Ki2 | Settling time | Peak error (frequency offsets),
// | | ( -> *PAUSED* ) | remarks (overshoot ? etc..)
// -----+---------+-----------------+---------------------------
// 32 | 0.03125 | 8243 s = 2.3hrs | Error peaks [Hz]: -4.000 1.409 -0.301
// 32 | 0.0625 | 3110 s = 52 min | Error peaks [Hz]: -4.000 1.518 -0.551 0.032 ("author's choice")
// 32 | 0.125 | 5416 s = 1.5hrs | Error peaks [Hz]: -4.000 1.726 -1.061 0.434 -0.181 0.075 -0.031 0.013
// 16 | 0.0625 | 9259 s = 2.6hrs | Error peaks [Hz]: -4.000 1.465 -0.944 0.469 -0.233 0.116 -0.058 0.029
// 64 | 0.0625 | 8995 s = 2.5hr | Error peaks [Hz]: -4.000 1.665 -0.468 -0.059 (too much phase noise)
// -> decided to use Ki=32, Ki2=0.0625 (which don't require multiply or divide, only bitwise shifting)
//
LimitYToPlusMinus32k();
i16LowpassIn = i32TempY.i16.lo;
}
else // frequency offset is "off the spec" .. oscillator warm-up ?
{ // wait until the oscillator stops drifting !
}
} // end if < not the FIRST sync pulse >
} // end ProcessCapturedSyncPulse()
//---------------------------------------------------------------------------
void UpdatePFO(void)
// Updates the PFO (programmable frequency output) .
// [in] EEPROM[ EEP_ADDR_PFO_PERIOD_L, _H, _FFA ] : period and 'fractional' part
// [out] PSMC2PR; PSMC2DC = PSMC2PR>>1;
{
// PIC16F1782/3 datasheet, Ch. 24.9, p. 220, "Fracional Frequency Adjust (FFA)":
// ------------------------------------------------------------------------
// > FFA is a method by which PWM resolution can be
// > improved on 50% fixed duty cycle signals. Higher
// > resolution is achieved by altering the PWM period by a
// > single count for calculated intervals. This increased
// > resolution is based upon the PWM frequency
// > averaged over a large number of PWM periods. For
// > example, if the period event time is increased by one
// > psmc_clk period (TPSMC_CLK) every N events, then
// > the effective resolution of the average event period is
// > TPSMC_CLK/N. (...)
// > The FFA function is only available when using one of the two Fixed Duty Cycle
// > modes of operation. In fixed duty cycle operation each PWM period is comprised
// > of two period events. That is why the PWM periods in Table 24-3 example
// > calculations are multiplied by two as opposed to the normal period calculations
// > for normal mode operation.
// >
// W.B.: The GPSDO's FPO primary purpose was to deliver a 13.3333333 MHz signal
// (multiplied to 66.6666666 MHz for an SDR-IQ), which would be impossible
// with the *basic* formula for the '50 % fixed duty cycle' mode of the PSMC:
// f_out = f_psmc / ( 2 * (PSMCxPR + 1) )
// f_out_m0 = 40 MHz / ( 2 * ( 0 + 1) ) = 20 MHz
// f_out_m1 = 40 MHz / ( 2 * ( 1 + 1) ) = 10 MHz
// f_out_m2 = 40 MHz / ( 2 * ( 2 + 1) ) = 6.666 MHz
// If the FFA is *not* needed (-> less phase noise) :
// f_out = f_psmc / ( PSMCxPR + 1 )
// f_out_13 = 40 MHz / ( 2 + 1 ) = 13.3333 MHz ("without" phase noise)
// -> If the FFA isn't necessary, don't use it !
} // end UpdatePFO()
//---------------------------------------------------------------------------
// main function
//---------------------------------------------------------------------------
void main(void)
{
//-------------------------------------------------------------------------
// initialize all I/O ports
#if SWI_USE_BINARY_NUMBERS // unfortunately, some C compilers don't understand these nice binary constants:
ANSELA = 0b00000011; // Configure RA0+RA1 as analog pins, the rest for digial I/O
ANSELB = 0b00000000; // Configure all pins of port B as digital I/O (not analog-in)
APFCON = 0b01000110; // "ALTERNATE PIN FUNCTION CONTROL REGISTER" (page 111) ...
// ||||||||______ b0 : CCP2 Input/Output Pin Selection
// ||||||| 1 = CCP2 is on pin RB3
// ||||||| 0 = CCP2 is on pin RC1
// |||||||_______ b1 : RX Pin Selection
// |||||| 1 = RX is on pin RB7 / ICSPDAT <<<<
// |||||| 0 = RX is on pin RC7
// ||||||________ b2 : TX Pin Selection
// ||||| 1 = TX is on pin RB6 / ICSPCLK <<<<
// ||||| 0 = TX is on pin RC6
// |||||_________ b3 : MSSP Serial Data (SDA/SDI) Output Pin Selection
// |||| 1 = SDA/SDI is on pin RB6
// |||| 0 = SDA/SDI is on pin RC4
// ||||__________ b4 : MSSP Serial Clock (SCL/SCK) Pin Selection
// ||| 1 = SCL/SCK is on pin RB7
// ||| 0 = SCL/SCK is on pin RC3
// |||___________ b5 : MSSP SDO Pin Selection
// || 1 = SDO is on pin RB5
// || 0 = SDO is on pin RC5
// ||____________ b6 : CCP1 Input/Output Pin Selection (here: input for GPS SYNC)
// | 1 = CCP1 is on pin RB0 <<<<
// | 0 = CCP1 is on pin RC2
// |_____________ b7 : C2OUT Pin Selection
// 1 = C2OUT is on pin RA6
// 0 = C2OUT is on pin RA5
// > SLEW RATE CONTROL (PIC16F1782/3 datasheet page 112, applies to other ports, too)
// > The SLRCONA register controls the slew rate option for each port pin.
// > Slew rate control is independently selectable for each port pin.
// > When an SLRCONA bit is set, the corresponding port pin drive is
// > slew rate limited. When an SLRCONA bit is cleared, the corresponding
// > port pin drive slews at the maximum rate possible.
// WB: The RESET state of the SLRCONx bits is H="slow" (limited slew rate),
// which caused a 13.3333 MHz output (from the PSMC) to reach
// ONE VOLT ONLY instead of 3.5 V ! Thus, turn the slew-rate-limiters OFF.
// After that, the rise- and fall times on RC6 / PSMC2A were below 5 ns.
SLRCONA = 0b00000000; // slew rate control for RA7..RA0 : L="fast", H="slow" (!)
SLRCONB = 0b00000000; // slew rate control for RB7..RB0 : L="fast", H="slow" (!)
SLRCONC = 0b00000000; // slew rate control for RC7..RC0 : L="fast", H="slow" (!)
// Port Direction registers. Microchip calls them "tristate" registers:
TRISA = PORTA_DIRECTIONS; // initialise port A directions... beware, TRISA is in another bank .
// The C compiler should have inserted something like "MOVLB 0x1" here, which SWITCHES THE REGISTER BANK in a PIC16F1xxx !
PORTA = PORTA_INIT_DATA ; // initial data for port A
//
TRISB = PORTB_DIRECTIONS; // initialise port B ...
PORTB = PORTB_INIT_DATA ; // initial data for port B
TRISC = PORTC_DIRECTIONS; // initialise port C ...
PORTC = PORTC_INIT_DATA ; // initial data for port C
// XC8 produced a 3-instruction sequence for the above line, to switch the register bank:
// 0x7F6: MOVLW 0x7
// 0x7F7: MOVLB 0x0 // "Move literal to low nibble in BSR", in simple words: "switch the register bank"
// 0x7F8: MOVWF PORTC
// -> bsf IOP_PLL_EN ; idle state for 'ENable' = H = no serial transfer
#endif // ( SWI_USE_BINARY_NUMBERS ) .. only required to compile the 'real' firmware, not supported by Borland C
//-------------------------------------------------------------------------
// Initialize hi-res PWM output (to steer the OCVCXO and produce the SDR clock).
// The PWM output can be 'amplified' by an external CMOS gate if necessary,
// which *must* be fed with a noise-free reference voltage .
// Duty cycle resolution is over 16 bits (due to the PSMC, details below).
// Fortunately the PSMC (in a PIC16F178x) is not limited to Fosc/4 .
// But unfortunately Timer1 and -being connected to the same timer-
// the Capture/Compare units (CCP1+CCP2) still are limited to Fosc/4 .. sigh.
//
// "PSMC" (Programmable Switch Mode Control, PIC16F1783 has TWO of them, x=1 or 2.
// Here: PSMC1 used as simple 16-bit PWM for the OCVCXO control voltage,
// PSMC2 used to generate a clock for the external SDR or "VFO".
//
// "PxCSRC" "PxCPRE"
// bits 1..0 | bits 1..0 |
// \|/ \|/ 16-bit counter
// ___ _____ __________
// PSMCxCLK ->-| | 40MHz ? | | | |
// "64 MHz" ->-|MUX|---->-----| / 1 |-->--| PSMCxTMR |---
// Fosc (*) ->-|___| |_____| |__________| |
// CLR /|\ |
// (*) Fosc, not Fosc/4 ! "period | |
// event" | \|/
// _________ | |
// |PSMCxPR |-->--compare -- |
// |_________| /|\ |
// ("period") |________________|
// |
// _________ _ |
// |PSMCxPH |-->--compare --> _/ out | (-> rising edge of PWM output,
// |_________| /|\ | PIC16F1783 : PSMC1A = RC0,
// ("phase") |________________| pin 11 of 28-pin "SPDIP" )
// _________ _ |
// |PSMCxDC |-->--compare --> \_ out | (-> falling edge of PWM output)
// |_________| /|\ |
// ("duty cycle") |________________|
//
// Details about the follwing register configrations
// in the PIC16F1782/3 datasheet (DS40001579E), page 245,
// "Table 24-5: SUMMARY OF REGISTERS ASSOCIATED WITH PSMC".
// Each register in Table 24-5 is even LINKED to the relevant details (pages).
// Fortunately, the C compiler (XC8) eliminates the daunting task of
// selecting the correct register banks.
// That's the purpose 'MOVLB' (similar as BANKSEL in the old days).
#if SWI_USE_BINARY_NUMBERS
PSMC1CON = 0b00000000; // PSMC1 CONTROL REGISTER ..
PSMC2CON = 0b00000000; // PSMC2 CONTROL REGISTER ..
// | |__|___ b3..0 : 0000 = single PWM waveform generation
// |___________ b7 : L=PSMC module disabled
PSMC1MDL = 0b00000000; // MODULATION CONTROL REGISTER ..
PSMC2MDL = 0b00000000; // .. same for the 2nd PSMC
// | |__|___ b3..0 : 0000 = "Modulation Source is PxMDLBIT"
// |___________ b7 : L = "Periodic Modulation Mode Enabled"
PSMC1SYNC = 0b00000000; // SYNCHRONIZATION CONTROL REGISTER
PSMC2SYNC = 0b00000000; // .. same for the 2nd PSMC
// \|___ b1..0 : 00 = "PSMC is sync'd with PERIOD event"
// 10 = PSMC1 is synchronized with the PSMC2 module
PSMC1CLK = 0b00000000; // PSMC CLOCK CONTROL REGISTER
PSMC2CLK = 0b00000000; // .. same for the 2nd PSMC
// \| \|____ b1..0 : 00 = Fosc system clock (here: 40 MHz, "slightly overclocked")
// | 01 = "64 MHz" clock in from PLL
// |________ b5..3 : 00 = prescaler divide by ONE
PSMC1POL = 0b00000000; // PSMC POLARITY CONTROL REGISTER. 0 = active-high
PSMC2POL = 0b00000000;
PSMC1BLNK = 0b00000000; // PSMC BLANKING CONTROL REGISTER. 0 = no blanking
PSMC2BLNK = 0b00000000;
PSMC1REBS = 0b00000000; // PSMC RISING EDGE BLANKED SOURCE REGISTER. here: no blanking input
PSMC2REBS = 0b00000000;
PSMC1FEBS = 0b00000000; // PSMC FALLING EDGE BLANKED SOURCE REGISTER. here: no blanking....
PSMC2FEBS = 0b00000000;
PSMC1PHS = 0b00000001; // PSMC PHASE SOURCE REGISTER
PSMC2PHS = 0b00000001;
// | ||||_____ b0 : P1PHST: PSMCx Rising Edge Event on Time Base match ?
// . ... H = Rising edge event will occur when PSMCxTMR = PSMCxPH
// L = time base will not cause rising edge event
PSMC1DCS = 0b00000001; // PSMC DUTY CYCLE SOURCE REGISTER
PSMC2DCS = 0b00000001;
// | ... |_____ b0 : H="Falling edge event will occur when PSMCxTMR = PSMCxDC"
// | (guess this is what we need for entirely 'DC'-register controlled PWM-DutyCycle)
// | L= Time base will not cause falling edge event
// |____________ b7 : H="falling edge event will occur when PCMCxIN pin goes true"
// L="PSMCxIN pin will not cause falling edge event"
PSMC1PRS = 0b00000001; // PSMC PERIOD SOURCE REGISTER
PSMC2PRS = 0b00000001;
// | ... |_____ b0 : H="Period event will occur and PSMCxTMR will reset when PSMCxTMR = PSMCxPR"
// | (guess this is what we need for 'PR'-register controlled PWM-frequency)
// | L="Time base will not cause period event"
// |____________ b7 : H="Period event will occur and PSMCxTMR will reset when PSMCxIN pin goes true"
// L="PSMCxIN pin will not cause period event"
#endif // ( SWI_USE_BINARY_NUMBERS )
// PWM period = ( PSMCxPR[15..0] + 1 ) / Fpsmc_clk .
// Examples: (65535+1) / 40 MHz = 1.384 ms = 1 / 610.351564 Hz
// ( 84 +1) / 40 MHz = 2.125 us = 1 / 470.5882353 kHz
// ( 83 +1) / 40 MHz = 2.100 us = 1 / 476.1904762 kHz
// ( 2 +1) / 40 MHz = 75 ns = 1 / 13.33333333 MHz
PSMC1PR = 0xFFFF; // PSMC PERIOD COUNT REGISTER ("C" allows 16 bit access)
#if ( EEPROM_SIZE > 0 )
PSMC2PRL = EEPROM_READ( EEP_ADDR_PFO_PERIOD_L ); // PSMC PERIOD COUNT REGISTER (here: 'programmable frequency output')
PSMC2PRH = EEPROM_READ( EEP_ADDR_PFO_PERIOD_H ); // upper 8 bits of the " " " "
#else
PSMC2PR = 0x00002; // hard-coded output frequency (13.3333 MHz for SDR-IQ)
#endif
// PWM duty cycle (DC) to 50 % initially :
PSMC1DC = 0x7FFF; // PSMC 1 DUTY CYCLE COUNT REGISTER (16 bit access)
PSMC2DC = PSMC2PR>>1; // PSMC 2 DUTY CYCLE COUNT REGISTER (16 bit access)
// Note (about reprogramming the DutyCycle register later):
// > The 16-bit duty cycle value is double-buffered
// > before it is presented to the 16-bit time base for comparison.
// > The buffered registers are updated on the first period
// > event Reset after the PSMCxLD bit of the PSMCxCON
// > register is set.
//
// rising edge of PWM output at the BEGIN of each cycle (0=no phase offset) :
PSMC1PHH = 0x00; // PSMC PHASE COUNT HIGH BYTE REGISTER
PSMC2PHH = 0x00;
PSMC1PHL = 0x00; // PSMC PHASE COUNT LOW BYTE REGISTER
PSMC2PHL = 0x00;
#if SWI_USE_BINARY_NUMBERS
// dead-band, auto-shutdown, etc: not required yet :
PSMC1ASDC = 0b00000000; // PSMC AUTO-SHUTDOWN CONTROL REGISTER (0=no shutdown)
PSMC2ASDC = 0b00000000;
PSMC1ASDL = 0b00000000; // PSMC AUTO-SHUTDOWN OUTPUT LEVEL REGISTER
PSMC2ASDL = 0b00000000;
PSMC1ASDS = 0b00000000; // PSMC AUTO-SHUTDOWN SOURCE REGISTER
PSMC2ASDS = 0b00000000;
#endif // ( SWI_USE_BINARY_NUMBERS )
PSMC1DBR = 0x00; // PSMC RISING EDGE DEAD-BAND TIME REGISTER
PSMC2DBR = 0x00; // (number of clock periods in rising edge "dead band")
PSMC1DBF = 0x00; // PSMC FALLING EDGE DEAD-BAND TIME REGISTER (..)
PSMC2DBF = 0x00; // (unfortunately 8 bits only, otherwise this could be used
// to generate a square-wave multi-phase output...)
#if SWI_USE_BINARY_NUMBERS
PSMC1FFA = 0b00000000; // PSMC FRACTIONAL FREQUENCY ADJUST REGISTER
// |__|__ b3..0 : number of clock periods to add each period event time.
// The fractional time period is 1 / (16*psmc_clk) .
// (Note: this fractional division causes phase noise.
// We don't need if for PSMC1, which drives the Vctrl PWM)
#endif // SWI_USE_BINARY_NUMBERS ?
// The 2nd PSMC is used as the 'programmable frequency output',
// so use the register value from the EEPROM for the 'fractional frequency adjust':
#if ( EEPROM_SIZE > 0 )
bTemp = EEPROM_READ( EEP_ADDR_PFO_FFA ); // bit 5: FFA-'enable', bits 3..0: FFA-'value'
PSMC2FFA = bTemp & 0x0F; // 0..15 "PSMC clocks" (details on the FFA in 'SetPFO()' )
#else
PSMC2FFA = 0x00; // no 'fractional frequency adjust'
#endif
PSMC1BLKR = 0x00; // PSMC RISING EDGE BLANKING TIME REGISTER
PSMC2BLKR = 0x00;
// > BLKR = Rising Edge Blanking Time bits
// = "Unsigned number of PSMCx psmc_clk clock periods in rising edge blanking"
PSMC1BLKF = 0x00; // PSMC FALLING EDGE BLANKING TIME REGISTER
PSMC2BLKF = 0x00;
#if SWI_USE_BINARY_NUMBERS
PSMC1STR0 = 0b00000001; // PSMC STEERING CONTROL REGISTER 0
PSMC2STR0 = 0b00000001;
// ||||||||______ b0 : PWM Steering PSMCxA Output Enable, mode dependent, too complex to be explained here..
// nc|||||_______ b1 : PWM Steering PSMCxB Output Enable bit
// ||||________ b2 : PWM Steering PSMCxC Output Enable bit
// |||_________ b3 : PWM Steering PSMCxD Output Enable bit
// ||__________ b4 : PWM Steering PSMCxE Output Enable bit
// |___________ b5 : PWM Steering PSMCxF Output Enable bit .
// For MOST bits in ..STR0: H = "Single PWM output is active on pin PSMCx"
// See PIC16F178x datasheet, page 245, "Table 24-5: SUMMARY OF REGISTERS ASSOCIATED WITH PSMC".
PSMC1STR1 = 0b00000000; // PSMC STEERING CONTROL REGISTER 1
PSMC2STR1 = 0b00000000;
// | ||______ b0 : 3-Phase Steering High Side Modulation Enable bit
// | |_______ b1 : 3-Phase Steering Low Side Modulation Enable bit
// |_____________ b7 : PWM Steering Synchronization bit. 0 = "immediately"
PSMC1INT = 0b00000000; // PSMC TIME BASE INTERRUPT CONTROL REGISTER ..
PSMC2INT = 0b00000000; // (counter overflow interrupt enable bit, etc etc, 0 = disabled)
PSMC1OEN = 0b00000001; // PSMC OUTPUT ENABLE CONTROL REGISTER ..
PSMC2OEN = 0b00000001; // .. similar for the 2nd PSMC, with output on PSMC2A / RC6
// ||______ b0 : PWM Output Enable for output 'A'
// | H = PWM output is active on PSMCx output y pin
// . L = PWM output is not active, normal port functions in control..
// (bit 0 controls "Output A", bit 1 "B", bit 2 "C", etc etc.
// Note the similar-sounding purpose of the ..STR0 register ! )
PSMC1CON = 0b11000000; // PSMC CONTROL REGISTER ..
PSMC2CON = 0b11000000;
// || |__|___ b3..0 : 0000 = single PWM waveform generation
// ||_________ b6 : PSMCxLD: PSMC Load Buffer Enable bit
// | H= "PSMCx registers are ready to be updated" (here, they ARE ready from the software's point of view)
// | L= "PSMCx buffer update complete" (details below)
// |__________ b7 : H=PSMC module enabled
// From the PIC16F1782/3 datasheet, page 222, 24.10.3 "Module Enabled Updates" :
// > The sequence for loading the buffer registers when the
// > PSMC module is enabled is as follows:
// > 1. Software updates all registers.
// > 2. Software sets the PSMCxLD bit.
// > 3. Hardware updates all buffers on the next period event.
// > 4. Hardware clears PSMCxLD bit.
// WB: At THIS POINT (during init), the PSCMC was *NOT* enabled on entry,
// but it cannot hurt setting the "Load Buffer Enabled" bit despite that:
// Almost done.. configure the PSMC1 & PSMC2 interrupts :
PIE4 &= 0b11001100; // PERIPHERAL INTERRUPT ENABLE REGISTER 4
// || ||_____ b0 : "PSMC1SIE", L=PSMC1 auto-shutdown interrupt disabled
// || |______ b1 : "PSMC2SIE", L=PSMC2 auto-shutdown interrupt disabled
// ||_________ b4 : "PSMC1TIE", L=PSMC1 time base interrupts disabled
// |__________ b5 : "PSMC2TIE", L=PSMC2 time base interrupts disabled
// At this point, both "PSMC"s should produce simple PWM on their 'A' outputs.
#endif // SWI_USE_BINARY_NUMBERS ?
IOP_ADCCLK_PIN_HI; // test (RA3?)
NOP(); // "Read-modify-write sequence on the same PORT may fail" (they won't, but thanks for the warning, CC5X)
IOP_ADCCLK_PIN_LO;
//-------------------------------------------------------------------------
// Initialize the PIC's Capture/Compare unit to 'time' the GPS sync period.
// As it turned out later, the maximum timer input frequency
// for the PIC16F178x's CAPTURE/COMPARE unit is still limited
// to Fosc/4, same as the timers in stonage PICs. Aaargh !
// (WB decided to "slightly overclock" the PIC with the 10 MHZ OCXO input,
// multiplied by four with the internal PLL, to achieve Fosc=40 MHz,
// instead of the "32 MHz maximum clock" for the PIC16(L)F1783 .
// -> capture timer clock frequency = Fosc/4 = 40 MHz / 4
// -> 100 ns TRUE CAPTURE RESOLUTION
// (still not even close to the "12.5 ns"-story told by the datasheet,
// but better than 1 / ( 10 MHz / 4 ) = 400 ns capture resolution,
// which may cause significant phase noise if the control loop
// needs to be faster than expected. )
// From the PIC16F1782/3 datasheet, DS40001579E page 247 :
// > Capture mode makes use of the 16-bit Timer1 resource.
// > When an event occurs on the CCPx pin, the 16-bit CCPRxH:CCPRxL
// > register pair captures and stores the 16-bit value of the TMR1H:TMR1L
// > register pair, respectively. (...)
// > Note: Clocking Timer1 from the system clock (Fosc)
// > should not be used in Capture mode.
// > In order for Capture mode to recognize the trigger event
// > on the CCPx pin, Timer1 must be clocked from the
// > instruction clock (Fosc/4) .
// WB : Aaaargh ! Would LOVE to have Timer1 running at Fosc = 40 MHz,
// but that wouldn't do any good anyway, because (on page 176) :
// > When the Fosc internal clock source is selected, the Timer1 register
// > value will in crement by four counts every instruction clock cycle .
// WB : Aaaaaargh ! Farewell, capture resolution in the sub-100-ns-range !
// Anyway, initialize Timer1 first, as described in DS40001579E page 247 .
// This is trivial because Timer1 doesn't even have a 'reload', aka 'period'-
// register.. it simply increments from 0x0000 to 0xFFFF,
// from where it overflows to 0x0000, thus a 65536 'Fosc/4' cycles:
// 10 MHz / 65536 = 152.5878906 Hz = 1 / 6.5536 milliseconds .
// (may be useful to drive the multiplexed 7-segment display one day..) .
// With the one-pulse-per-second GPS sync signal, Timer1 will overflow
// 152 or 153 times between two sync pulses, but that's no problem...
// see Timer1 interrupt handler .
TMR1 = 0x0000; // Start counting at zero (for what it's worth)
// TMR1 = 16-bit combination of TMR1H + TMR1L .
PIE1bits.TMR1GIE = 0; // disable "Timer1 Gate Interrupt" (b7)
PIE1bits.TMR1IE = 0; // and "Timer1 Overflow Interrupt" (b0)
#if SWI_USE_BINARY_NUMBERS
T1CON = 0b00000001; // TIMER1 CONTROL REGISTER
// ||||||||______ b0 : TMR1ON : 1 = Timer1 enabled
// |||||||_______ b1 : unused
// ||||||________ b2 : T1SYNC : 0 = do not synchronize async clock input
// |||||_________ b3 : T1OSCEN: 0 = dedicated Timer1 oscillator circuit disabled
// ||\|__________ b5..4 : T1CKPS (prescaler) : 00bin = divide by ONE
// \|____________ b7..6 : TMR1CS (clk source): 00bin = Fosc/4 (01=Fosc is USELESS)
T1GCON = 0b00000000; // TIMER1 GATE CONTROL REGISTER
// ||||||\|______ b1..0 : Timer1 Gate Source Select bit (doesn't matter here)
// ||||||________ b2 : Timer1 Gate Current State bit
// |||||_________ b3 : Timer1 Gate Single-Pulse Acquisition Status bit
// ||||__________ b4 : Timer1 Gate Single-Pulse Mode bit
// |||___________ b5 : Timer1 Gate Toggle Mode bit
// ||____________ b6 : Timer1 Gate Polarity bit (doesn't matter here)
// |_____________ b7 : Timer1 Gate Enable bit : 0 = Timer1 counts regardless of gate
#endif // SWI_USE_BINARY_NUMBERS ?
// Above: Preparation of Timer1 for the purpose of INPUT CAPTURE.
// Below: Initialisation of the INPUT CAPTURE itself...
// - *Each* RISING edge of the GPS sync pulse on CCP1/RB0
// shall copy TMR1H:TMR1L (16 bit)
// into the capture register, CCPR1H:CCPR1L .
APFCONbits.CCP1SEL = 1; // CCP1 Input/Output Pin Selection :
// 1 = CCP1 is on pin RB0
// 0 = CCP1 is on pin RC2
PIE1bits.CCP1IE = 0; // disable interrupt from Capture/Compare Module #1
#if SWI_USE_BINARY_NUMBERS
CCP1CON= 0b00000101; // CCP1 Control Register [DS40001579E page 255]
// ||\|\__|______ b3..0 : Capture/Compare 1 Mode Select Bits
// .. | 0101 = Capture mode: every rising edge
// nc |__________ b5..4 : PWM Duty Cycle least significant bits. Igored in Capture mode.
#endif // SWI_USE_BINARY_NUMBERS ?
PIR1bits.CCP1IF = 0; // clear Capture/Compare 1 interrupt flag
// At this point, each rising edge on the GPS sync pulse (one pulse per second)
// should be captured as a 16-bit value in CCPR1H:CCPR1L .
// Whenever new capture was made, the PIC will also set CCP1IF ("interrupt flag").
// But to use as few interrupts as possible (we may need interrupts for the ADC later),
// the CCP1IF-flag does not fire an interrupt. Instead, it's polled in the main loop
// which is no problem because this only happens once per second.
UART_Init(); // initialize the UART (serial port for testing, control, and ADC->PC)
// The serial data line, presented to the PC's RS-232 'RXD',
// should be IDLE now. On a true RS-232, an IDLE line has *negative*(!)
// voltage, which for historic reasons is called 'MARK STATE' (1) !
ADC_Init(); // initialize the A/D converter (but don't start the ADC interrupt yet)
// Details about the ADC in the PIC16F1782/3 datasheet, DS40001579E,
// pages 141.., Ch. 17.1, "ADC Configuration" .
// Note: ADC-associated port pins have already been initialized, e.g. RA0/AN0,
// including the TRIS and ANSEL settings (see gpsdo_pic_main.c) .
#ifndef __BORLANDC__
// To avoid sending garbage with the wrong bitrate,
// check if the PIC runs with the intended clock source. Details further below.
wait_1ms(); // waits 1 ms with the correct clock source, but 1.5 sec
// if the external clock is missing, and the PIC runs in "Oscillator Fail Mode" !
// If all works well, the above 1-ms delay looks like a "long stop bit"
// to the remote receiver.
if( PIR2bits.OSFIF ) // OSFIF: "Oscillator Fail Interrupt Flag" (H when pending)
{ PIR2bits.OSFIF = 0; // clear the "Oscillator Fail Interrupt Flag"
RESET(); // reset the whole device. Maybe it restarts with the EXTERNAL osc...
} // end if < FSCM error trip >
#if(0) // TEST for the serial port's output polarity and timing:
// _ _ _ _ _ _logic "0" !
UART_SendChar(0x55); // __________| |_| |_| |_| |_| |________________________________
// S 0 1 2 3 4 5 6 7 (stop bit = idle = logic '1' !! )
// |<---- 78 us ---->|
#endif // TEST ?
wait_1ms();
#endif // ndef __BORLANDC__ ?
UART_SendString("\r\nDL4YHF GPSDO V1.1\r\n"); // use strings economically .. this is a PIC with very small CODE memory !
IOP_ADCCLK_PIN_HI; // test (RA3?)
NOP(); // "Read-modify-write sequence on the same PORT may fail" (they won't, but..)
IOP_ADCCLK_PIN_LO;
// Init application variables, etc..
ClearMem( (uint8_t*)&xx, 20*cic_s ); // clear everything that belongs to the CIC filter
downsample_cnt = 0; // XC8 does clear global variables automatically (presumably to save ROM)
bSyncPulseCounter = 0; // haven't seen a complete GPS sync cycle yet !
i16FreqOffset = 0; // frequency offset (measured OXCO minus 10 MHz) not measured yet
ResetIntegralsAndLowpass(); // -> ErrorIntegral = LowpassIn = LowpassOut = 0
bLowpassSpeed = 8; // use a 'fast' lowpass before the Vctrl output during initialisation
u16VctrlBias = 32767; // begin with ideal pulse width modulator duty cycle : 50 percent
cDebugMode = 0; // 0 (zero) : do NOT send 'debug messages' via UART (*)
// 'n' : numeric output (once every GPS pulse)
// 'p' : plotter (crude ASCII output, but ok to check the damping)
fflags = (1< 23.2 Timer2 Interrupt
// > Timer2 can also generate an optional device interrupt.
// > The Timer2 output signal (TMR2-to-PR2 match)
// > provides the input for the 4-bit counter/postscaler. This
// > counter generates the TMR2 match interrupt flag which
// > is latched in TMR2IF of the PIR1 register. The interrupt
// > is enabled by setting the TMR2 Match Interrupt Enable
// > bit, TMR2IE, of the PIE1 register
// Note: Like many other 'on chip peripherals' (except the PSMC),
// Timer2 isn't fed with the OSC frequency (here: 40 MHz)
// but with the instruction cycle frequency (here: 10 MHz) !
PR2 = 199; // interrupt frequency := 10 MHz / (PR2+1) .. examples below.
// PR2=199 : fs_in=50000.0 Hz, fs_out=12500.0 Hz, quite a waste of bandwidth but
// with "only" 12500 samples * 2 bytes to send over the UART, and 500 kBit/second,
// there is enough space for another byte (i.e. a 3-byte "sample frame") :
// 12500 Hz * 3 * 12 bit = 450 kBit/second .
// The third byte can be used for frame sync, AND (maybe in future)
// to pass on the NMEA stream from the GPS receiver to the PC .
// PR2=124 : fs=80kHz, 1/ 80 kHz = 12.5 us = 15.6 "T_AD" cycles, but too fast for the ADC with 12 bit/sample.
// Even with only 10 bit / sample, there was still jitter on the S&H input
// due to the non-sychronizeable ADC clock prescaler !
// PR2=135 : fs=73529.41176 Hz -> 17.0 "T_AD" cycles, no jitter, but what an ugly frequency !
// PR2=149 : fs_in=66666.66666 Hz, fs_out = 16.6666 kHz (easy to remember but..)
// BUT: The sampling interval isn't an integer multiple of the ADC clock
// (after the div-by-32 prescaler) -> sampling point observed as the spike
// on the analog input (AN0/RA0) showed a 600 ns jitter again !
// Obviously, setting the "GO"-bit in ADCON0 does not synchronize the
// ADC's internal clock prescaler (which we need for T_AD=0.8us),
// causing jitter in the real sampling time .
// -> Sampling intervals must be integer multiples of T_AD = 0.8 us,
// furthermore T_sample must be at least 17 (not 15!) * T_AD (for 12 bit),
// i.e.: 1 / (17*0.8us) = 73529.41176 Hz, 1 / (18*0.8us) = 69444.4444 Hz,
// 1 / (19*0.8us) = 65789.47368 Hz, 1 / (20*0.8us) = 62500.0000 Hz,
// 1 / (21*0.8us) = 59523.80952 Hz, 1 / (22*0.8us) = 56818.1818 Hz,
// 1 / (23*0.8us) = 54347.82609 Hz, 1 / (24*0.8us) = 52083.3333 Hz,
// 1 / (25*0.8us) = 50000.00000 Hz
// DL4YHF decided to use fs_in = 50 kHz, which is a pity because
// the PIC16F1783 would be fast enough to run the CIC filter at fs_in = 80 kHz.
//
#if SWI_USE_BINARY_NUMBERS
T2CON = 0b00000100;
// ||||||\|_ b1..0 = "T2CKPS" : Timer2 Clock Prescaler, 0=1:1 (don't divide)
// ||||||___ b2 = "TMR2ON" : 1 = timer2 on, 0=timer 2 off
// |\__|____ b6..3 = "T2OUTPS" : Output Postscaler, 0=1:1 (don't divide)
// |________ n.c.
#endif
// Enable Timer2 interrupt (to read and process the analog input)
PIE1bits.TMR2IE = 1; // DS40001579E page 80 : "Timer2 Interrupt Enable"
INTCONbits.PEIE = 1; // DS40001579E page 79 : "Peripheral Interrupt Enable"
INTCONbits.GIE = 1; // DS40001579E page 79 : "Global Interrupt Enable"
while(1) // end less main loop .... only left when oscillator-fault causes a RESET
{
// IOP_RED_LED_ON; // Length of this pulse: 100 ns (with Fcyc = 40 MHz/4 = 10 MHz),
// IOP_RED_LED_OFF; // PIC16F1783 'slightly overclocked' (Fcyc max = 32 MHz / 4 = 8 MHz).
//-------------------------------------------------------------------------
// Minimalistic 'command handler', controlled by single characters
// received from the serial port (the PIC's "EUSART") :
bTemp = UART_ReadChar();
if( bTemp ) // successfully read another character from the RX-FIFO...
{
// Life would be easy if not only the polarity of the TXD-OUTPUT,
// but also the polarity of the PIC'S RXD-INPUT could be inverted.
// Then we could get away without the usual RS-232 level converter,
// like the once-famous MAX232 which *inverts* the signal.
// Crude fix: Fix the garbled received character
// (with an EEEXTRA long startbit, which was in fact the the IDLE line
// state, which has the same voltage like a stopbit).
// Example: Someone has sent 0x55 (upper case letter 'U').
// Waveform seen with an o-scope on the RXD line :
// _ _ _ _ _ _logic "0" !
// __________| |_| |_| |_| |_| |________________________________
// S 0 1 2 3 4 5 6 7 (stop bit = idle = logic '1' !! )
//
// Signal 'seen by the PIC's EUSART' :
// __________ _ _ _ _ ________________________________
// . .|_| |_| |_| |_| |_|
// .......... 0 1 2 3 4 5 6 7 |
// . | | |_ one stopbit, no idle time, but next startbit
// . | |_ first bit with 'variable content'.
// "erroneous |
// data, |__ "looks" like the 1st data bit
// no stopbit" but was in fact the REAL start bit .
# if( RXD_INVERT_POLARITY ) // invert the polarity of received data by software ?
bTemp = UART_InvertRcvdChar( bTemp );
# endif // RXD_INVERT_POLARITY ?
if( bTemp != 'a' )
{ fflags = 0; // stop sending filtered analog input to the UART
}
if( fflags == 0 ) // only when the UART isn't occupied by the filter-output:
{ UART_SendString("rx=");
UART_SendChar( bTemp );
UART_SendChar( '=' );
UART_SendDecimal( bTemp );
UART_SendCrNl();
}
// Above: just a simple "echo test" for the serial port .
// Below: command "interpreter" ...
switch( bTemp )
{ case 'a' : // start analog input
cDebugMode = 0; // .. UART isn't available for debug-output anymore
fflags |= (1< ErrorIntegral = LowpassIn = LowpassOut = 0
break;
case 'z' : // 'zero' Vctrl (jumps to the minimum frequency)
ResetIntegralsAndLowpass();
i32LowpassOut.i16.hi = -32767; // almost 0 % duty cycle
break;
case 'm' : // 'max' Vctrl (jumps to the maximum frequency; same as the "automatic" test initiated by pic_emulator.c)
ResetIntegralsAndLowpass();
i32LowpassOut.i16.hi = 32767; // almost 100 % duty cycle
break;
default : break; // unknown test command
}
} // end if( UART_CheckForRx() )
//-------------------------------------------------------------------------
// If a GPS sync pulse has captured the 16-bit 10 MHz timer value,
// process it HERE (in the main loop, to avoid disturbing the ADC sampling interrupt)
if( PIR1bits.CCP1IF )
{ PIR1bits.CCP1IF = 0; // clear Capture/Compare 1 interrupt flag
ProcessCapturedSyncPulse(); // -> measure number of timer ticks since last GPS sync pulse
switch ( cDebugMode )
{ case 'n' : // numeric display of frequency error, error integral, etc.. in numeric form
if( bSyncPulseCounter < 255 ) // still in the coarse 'init' phase ?
{ UART_SendString( "t=" ); // error signal (Regelabweichung in Hertz)
UART_SendDecimal( bSyncPulseCounter );
UART_SendChar( ' ' );
}
UART_SendString( "df=" ); // error signal (Regelabweichung in Hertz)
UART_SendDecimal( i16FreqOffset );
UART_SendString( " i1=" ); // error integral (Fehlerintegral)
UART_SendDecimal( i16ErrorIntegral );
UART_SendString( " i2=" ); // integral of the error integral
UART_SendDecimal( (i32ErrorIntegralIntegral.i32 >> 4) );
UART_SendString( " lpi=" ); // lowpass input (Stellgröße ohne Offset, ideal 0)
UART_SendDecimal( i16LowpassIn );
UART_SendString( " lpo=" ); // lowpass output (tiefpassgefilterte Stellgröße ohne Offset, ideal 0)
UART_SendDecimal( i32LowpassOut.i16.hi );
UART_SendString( " pwm=" ); // Vctrl output (pulse width modulator register value, including bias)
UART_SendDecimal( (int16_t)PSMC1DC - 32767 );
UART_SendString( " xx=" ); // Vctrl output (pulse width modulator register value, including bias)
UART_SendDecimal( (int16_t)xx );
UART_SendString( " yy=" ); // Vctrl output (pulse width modulator register value, including bias)
UART_SendDecimal( (int16_t)yy );
UART_SendCrNl();
break;
case 'p' : // plot frequency error, error integral, etc.. in a kind-of DIAGRAM
FillString( sz80Temp, ' ', 80 );
sz80Temp[0] = '|'; // marker for the lower endstop
sz80Temp[39] = '|'; // marker for the center ("0")
sz80Temp[78] = '|'; // marker for the upper endstop
Plot( sz80Temp, 'e', i16FreqOffset ); // ideally "all zero"
Plot( sz80Temp, 'i', i16ErrorIntegral % 40 );
Plot( sz80Temp, 'o', i32LowpassOut.i16.hi % 40 );
UART_SendString( sz80Temp );
UART_SendCrNl();
break;
} // end switch ( cDebugMode )
} // end if( PIR1bits.CCP1IF ) [new sync pulse captured, happens once per second]
//-------------------------------------------------------------------------
// Update the low-pass filtered Vctrl output, approx 610.3 times per second,
// to keep the phase noise as low as possible .
// Equivalent analog circuit :
// ____
// i16LowpassIn O-----|____|----*----O i32LowpassOut.i16.hi
// R |
// (100 kOhm) __|__
// _____
// C |
// (1070 uF) _|_
//
// R * C (time constant) = 65536 / f_sample [610.3 Hz]
// = 107 seconds .
// -3 dB corner frequency = 1 / (2*pi*R*C) = 1.5 mHz .
//
//
// To keep the interrupt free for 'other purposes', this happens in the
// main loop, but synchronized by the 610.3 HZ PWM timer (40 MHz / 65536).
// Doing this IN SOFTWARE (instead of using a bulky ANALOG RC lowpass)
// is more flexible, and the filter's time constant (tau ~ RC) can be
// adjusted via software if necessary (even lower phase noise, etc).
// Details about how to poll for the PWM (here: PSMC) 'period event'
// are in the PIC16F1782/3 datasheet (DS40001579E), page 194 :
// > The match (PSMCxTMR = PSMCxPR) will generate a period match interrupt,
// > thereby setting the PxTPRIF bit of the PSMC Time Base Interrupt Control
// > PSMCxINT) register (..)
if( PSMC1INTbits.P1TPRIF ) // "PSMC 1 Time Base Period Interrupt Flag" set ?
{ // here approximately 610 times each second... if the main loop is fast enough
PSMC1INTbits.P1TPRIF = 0; // clear PSMC 1 period interrupt flag
IOP_DEBUG_PIN1_HI; // TEST: Is the digital filter 'fast enough' for f_Sample=610 Hz ?
// Update the first order lowpass filter (with 0 dB DC gain).
// Implementation : y[k] = (1-alpha)*x[k] + (alpha)*y[k-1]
// Simplified filter constant (alpha, close to 1.0) :
// tau_samples = 1 / (1-alpha)
// alpha = 1 - (1/tau_samples)
// tau [seconds] = tau_samples * T_Sample
// Examples: alpha = 0.9 gives a time constant of 10 samples,
// alpha = (1-1/256) = 0.996 for tau = 256 samples,
// alpha = (1-1/65536) = 0.99998474 for tau = 65536 samples .
// To avoid floating point maths:
// Instead of dividing by 65536, use the following 4-byte
// C-union (i32LowpassOut) in memory:
// ______________________
// | | | | |
// | b3 | b2 | b1 | b0 |
// |_____|_____|____|_____|
// . .
// |<---------i32-------->| ('accumulator' for the digital lowpass.
// . . . i16LowpassIn is added to THIS part.)
// . |<-.i16.l->| (fractional part for tau=65536 samples)
// |<--.i16.h->| (delivers the 'accu' DIVIDED by 65535)
//
// To realize variable RC time constants,
// simply run the lowpass-algorithm 1 to 8 times in a loop:
for( bTemp=0; bTemp INCREASE PWM DC
# else
PSMC1DC = (uint16_t)( u16VctrlBias - i32LowpassOut.i16.hi ); // frequency too high -> DECREASE PWM DC
# endif
// -> PSMC 1 DUTY CYCLE COUNT REGISTER (16 bit access)
// From the PIC16F1782/3 datasheet, page 222, 24.10.3 "Module Enabled Updates" :
// > When the PSMC module is enabled (PSMCxEN = 1),
// > the PSMCxLD bit of the PSMC Control (PSMCxCON) register must be used.
// > When the PSMCxLD bit is set, the transfer from the
// > register to the buffer occurs on the next period event. [here: 610 times each second]
// > The PSMCxLD bit is automatically cleared by hardware
// > after the transfer to the buffers is complete.
PSMC1CONbits.PSMC1LD = 1; // see details above ! (transfer from double-buffered PSMC1DC)
} // end if < PSMC 1 period interrupt flag > ? [happens 610 times per second]
//-------------------------------------------------------------------------
// Sync pulse indicator and software PWM for the RGB indicator LED .
// Don't waste precious time in the interrupt for this !
// The following code is also executed 610 times per second,
// synchronized by a hardware timer, but not occupying the interrupt.
if( IOP_GPS_SYNC_ACTIVE ) // GPS sync pulse currently active:
{ // Green flashes along with the sync pulse when "ok",
// Red flashes : momentary frequency is TOO HIGH,
// Blue flashes : momentary frequency is TOO LOW .
if( i16FreqOffset > 2/*Hz*/ )
{ IOP_RED_LED_ON;
IOP_GREEN_LED_OFF;
IOP_BLUE_LED_OFF;
}
else if( i16FreqOffset < -2/*Hz*/ )
{ IOP_RED_LED_OFF;
IOP_GREEN_LED_OFF;
IOP_BLUE_LED_ON;
}
else // momentary frequency error ZERO : green flash
{ // (the error integral may still be large, to return to the original phase)
IOP_RED_LED_OFF;
IOP_GREEN_LED_ON;
IOP_BLUE_LED_OFF;
}
}
else // time between two pulses : software PWM showing error integral (with polarity).
{ IOP_GREEN_LED_OFF;
if( i16ErrorIntegral >= 0 ) // positive error integral : RED
{ IOP_BLUE_LED_OFF;
if( i16ErrorIntegral > 255 )
{ IOP_RED_LED_ON;
}
else // i16ErrorIntegral <= 255
{ if( i16ErrorIntegral > PSMC1TMRH )
{ IOP_RED_LED_ON;
}
else
{ IOP_RED_LED_OFF;
}
}
}
else // NEGATIVE error integral : BLUE (also value-dependent intensity)
{ IOP_RED_LED_OFF;
if( i16ErrorIntegral <= -255 )
{ IOP_BLUE_LED_ON;
}
else // i16ErrorIntegral between -1 and -254
{
if( (-i16ErrorIntegral) > PSMC1TMRH )
{ IOP_BLUE_LED_ON;
}
else
{ IOP_BLUE_LED_OFF;
}
}
}
} // end else < between two GPS sync pulses >
//-------------------------------------------------------------------------
// Check the oscillator source. Sometimes a PIC16F1783 ran much slower
// than expected, despite correct settings in CONFIG1 ("ECH") + CONFIG2 ("4x PLL").
// Reason: The OCXO was a 'slow starter', and when turning on the OCXO
// and the PIC at the same time (same supply voltage), the PIC started running
// with the internal oscillator after the Fail-Safe Clock Monitor (FSCM)
// detected the absence of an external clock after approximately 2 ms !
//
// > When the external clock fails, the FSCM switches the device clock
// > to an internal clock source and sets the bit flag OSFIF of the
// > PIR2 register. Setting this flag will generate an interrupt if the
// > OSFIE bit of the PIE2 register is also set. The device firmware can
// > then take steps to mitigate the problems that may arise from a
// > failed clock. The system clock will continue to be sourced from
// > the internal clock source until the device firmware successfully
// > restarts the external oscillator and switches back to external operation.
// ... this is exactly what happened during 'breadboard testing' .
// To avoid this, check if the CPU runs from the *intended* source:
if( PIR2bits.OSFIF ) // OSFIF: "Oscillator Fail Interrupt Flag" (H when pending)
{ IOP_RED_LED_ON; // There's a problem with the oscillator .. FSCM switched to internal RC osc ?
// > The OSFIF bit should be cleared prior to switching to the
// > external clock source. If the Fail-Safe condition still exists,
// > the OSFIF flag will again become set by hardware.
PIR2bits.OSFIF = 0; // clear the "Oscillator Fail Interrupt Flag"
RESET(); // reset the whole device. Maybe it restarts with the EXTERNAL osc...
} // end if < FSCM error trip > ?
# if( SWI_STANDALONE_SIMULATOR )
if( (!EMU_fKeyboardControlled) && (i16LowpassIn == i32LowpassOut.i16.hi) && ( EMU_i64Tsim_ns > 20000000000) // min. 20 seconds ...
# if (SWI_CONTROLLER_PRINCIPLE==CONTROLLER_PRINCIPLE_II )
&& (i16ErrorIntegral==0)
# endif
)
{ EMU_fSimulationPaused = TRUE; // guess the transient is over, and the loop has completely settled
// (the custom 'emulator environment' will pause the simulation and show the results somewhere)
}
PIC_Emulator();
# endif // SWI_STANDALONE_SIMULATOR ?
} // end of the 'endless' main loop
} // end main()
#ifdef __BORLANDC__
void EMU_StartTest(void) // similar as the manual 'm'-test, but initiated by pic_emulator.c after a few 'simulated' seconds
{
ResetIntegralsAndLowpass();
i32LowpassOut.i16.hi = +32767; // warp almost 100 % duty cycle
// (the standalone simulator, pic_emulator.c, will now analyse the response)
} // end EMU_StartTest()
#endif // def __BORLANDC__ ?
/*
EOF ( gpsdo_pic_main.c )
*/