/*
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" ? // #includeEOF ( gpsdo_pic_main.c ) */// 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__ ? /*