#include #include #include #include #include "pico/stdlib.h" #include "pico/binary_info.h" #include "hardware/spi.h" #include "hardware/clocks.h" #include "hardware/dma.h" #include "blink.pio.h" #include "DAC.pio.h" #include "hardware/gpio.h" // Required for manually toggling GPIO pins (clock) ////////////////////////////////////// // Define GPIO connections for Pico... ////////////////////////////////////// // Note: The SPI Port only works through specific pins, so these connections are defined first. // SPI Port connections... #define SPI_PORT spi0 // Using SPI port 0 // ┌──────────┬───────────────┬─────────────┐────────────────┐ // │ PGA2040 │ Connection │ MCP41010 │ Display module │ // ├──────────┼───────────────┼─────────────┤────────────────┤ #define PIN_RX 16 // │ GPIO 16 │ SPI0 RX │ (unused) │ (unused) │ #define Display_CS 17 // │ GPIO 17 │ Chip select │ (unused) │ SS1 (white) │ #define PIN_CLK 18 // │ GPIO 18 │ SPI0 Clock │ SCK (Pin 2) │ SCK (blue) │ #define PIN_TX 19 // │ GPIO 19 │ SPI0 TX │ SI (Pin 3) │ SDI (green) │ #define Level_CS 20 // │ GPIO 20 │ Chip select │ CS (Pin 1) │ (unused) │ // └──────────┴───────────────┴─────────────┘────────────────┘ #define _DAC_A 0 // DAC channel alias #define _DAC_B 1 // DAC channel alias #define _Up 1 #define _Down -1 #define _Sine_ 0 // Permited values for variable WaveForm_Type #define _Square_ 1 #define _Triangle_ 2 #define _Time_ 3 #define _Funct_ 4 #define _Phase_ 5 #define _Freq_ 6 #define _Level_ 7 #define _Duty_ 8 #define _Range_ 9 #define _Harmonic_ 10 #define eof 255 // EOF in stdio.h -is -1, but getchar returns int 255 to avoid blocking #define CR 13 #define BitMapSize 256 // Match X to Y resolution #define MWidth 12 // Width of terminal command margin (in columns) //#define SysClock 125 // System clock for 0.488 MHz DAC output (Pico default) //#define SysClock 250 // System clock x 2 for 0.977 MHz DAC output #define SysClock 280 // Overclock for 1.000 MHz DAC output // Data for clock face generated by Excel spreadsheet... uint8_t FaceX[] = {235,239,243,247,251,255,255,254,254,254,254,254,254,254,253,253,253,252,252,251,251,250,250,249,248,248,247,246,245,244,244,243,242,241,240,239,221,224,227,231,234,238,236,235,234,233,232,230,229,228,226,225,224,222,221,219,218,216,214,213,211,209,208,206,204,203,201,199,197,195,193,182,184,186,188,190,191,190,188,186,184,182,180,178,176,174,172,170,167,165,163,161,159,157,155,153,150,148,146,144,142,139,137,135,133,131,128,128,128,128,128,128,126,124,122,120,117,115,113,111,109,107,104,102,100,98,96,94,92,90,87,85,83,81,79,77,75,73,71,69,67,75,73,71,69,67,65,64,62,60,58,56,54,53,51,49,47,46,44,43,41,39,38,36,35,33,32,31,29,28,27,25,24,23,22,20,36,33,30,26,23,19,18,17,16,15,14,13,12,12,11,10,9,9,8,7,7,6,6,5,5,4,4,4,3,3,3,3,3,3,2,22,18,14,10,6,2,2,3,3,3,3,3,3,4,4,4,5,5,6,6,7,7,8,9,9,10,11,12,12,13,14,15,16,17,18,36,33,30,26,23,19,20,22,23,24,25,27,28,29,31,32,33,35,36,38,39,41,43,44,46,47,49,51,53,54,56,58,60,62,64,75,73,71,69,67,65,67,69,71,73,75,77,79,81,83,85,87,90,92,94,96,98,100,102, 104,107,109,111,113,115,117,120,122,124,126,128,128,128,128,128,128,131,133,135,137,139,142,144,146,148,150,153,155,157,159,161,163,165,167,170,172,174,176,178,180,182,184,186,188,190,182,184,186,188,190,191,193,195,197,199,201,203,204,206,208,209,211,213,214,216,218,219,221,222,224,225,226,228,229,230,232,233,234,235,236,221,224,227,231,234,238,239,240,241,242,243,244,244,245,246,247,248,248,249,250,250,251,251,252,252,253,253,253,254,254,254,254,254,254,254} ; uint8_t FaceY[] = {128,128,128,128,128,128,128,126,124,122,120,117,115,113,111,109,107,104,102,100,98,96,94,92,90,87,85,83,81,79,77,75,73,71,69,67,75,73,71,69,67,65,64,62,60,58,56,54,53,51,49,47,46,44,43,41,39,38,36,35,33,32,31,29,28,27,25,24,23,22,20,36,33,30,26,23,19,18,17,16,15,14,13,12,12,11,10,9,9,8,7,7,6,6,5,5,4,4,4,3,3,3,3,3,3,2,22,18,14,10,6,2,2,3,3,3,3,3,3,4,4,4,5,5,6,6,7,7,8,9,9,10,11,12,12,13,14,15,16,17,18,36,33,30,26,23,19,20,22,23,24,25,27,28,29,31,32,33,35,36,38,39,41,43,44,46,47,49,51,53,54,56,58,60,62,64,75,73,71,69,67,65,67,69,71,73,75,77,79,81,83,85,87,90,92,94,96,98,100,102,104,107,109,111,113,115,117,120,122,124,126,128,128,128,128,128,128,131,133,135,137,139,142,144,146,148,150,153,155,157,159,161,163,165,167,170,172,174,176,178,180,182,184,186,188,190,182,184,186,188,190,191,193,195,197,199,201,203,204,206,208,209,211,213,214,216,218,219,221,222,224,225,226,228,229,230,232,233,234,235,236,221,224,227,231,234,238,239,240,241,242,243,244,244,245,246,247,248,248,249,250,250,251,251,252, 252,253,253,253,254,254,254,254,254,254,254,235,239,243,247,251,255,254,254,254,254,254,254,254,253,253,253,252,252,251,251,250,250,249,248,248,247,246,245,244,244,243,242,241,240,239,221,224,227,231,234,238,236,235,234,233,232,230,229,228,226,225,224,222,221,219,218,216,214,213,211,209,208,206,204,203,201,199,197,195,193,182,184,186,188,190,191,190,188,186,184,182,180,178,176,174,172,170,167,165,163,161,159,157,155,153,150,148,146,144,142,139,137,135,133,131} ; // (Number of pixels: 421) // Store clock hands co-ordinates... uint8_t HandsX[192] = {} ; // Each hand requires 64 bytes - 3x64=192 uint8_t HandsY[192] = {} ; int Hours=0, Mins=0, Secs=0, Angle, StartX, StartY, Radius ; float Radians ; int tmp ; char MarginFW[MWidth+1], MarginVW[MWidth+1] ; // Fixed Width & Variable Width strings to create a fixed margin unsigned short DAC_channel_mask = 0 ; // Binary mask to simultaneously start all DMA channels const uint32_t transfer_count = BitMapSize ; // Number of DMA transfers per event const float _2Pi = 6.283; // 2*Pi int ParmCnt = 0, Parm[4], WaveForm_Type ; // Storage for 4 command line parameters int SelectedChan, c, i = 0, dirn = 1, result ; int MarginCount = 0 ; // Manual count of characters written to terminal - required to maintain margins float MaxDACfreq ; char inStr[30], outStr[2500], ResultStr[3000], LastCmd[30] ; // outStr large enough to contain the HelpText string static void MCP41020_Write (uint8_t _ctrl, uint8_t _data) ; // Forward definitions static void SPI_Display_Write(int _data) ; class DAC { public: PIO pio; // Class wide var to share value with setter function unsigned short DAC_data[BitMapSize] __attribute__ ((aligned(2048))) ; // Align DAC data (2048d = 0800h) int Funct, Range, PIOnum ; int Level, Freq, Phase, DutyC, Harm, RiseT ; uint StateMachine, ctrl_chan, data_chan, GPIO, SM_WrapBot, SM_WrapTop ; // Variabes used by the getter function... char name ; // Name of this instance float DAC_div ; void StatusString () { // Report the status line for the current DAC object, aligned to current margin settings. char Str1[4], Str2[200], Margin[40] ; // ! Max line length = 100 chars ! if (Range == 1) strcpy(Str1," Hz") ; // Asign multiplier suffix if (Range == 1000) strcpy(Str1,"KHz") ; if (Range == 1000000) strcpy(Str1,"MHz") ; tmp = strlen(inStr) ; // Get number of command line characters // Handle the instances where the length of the command line exceeds the Margin width... if (MarginCount >= MWidth) { printf("\n") ; // Start a newline strcpy(inStr,"") ; // Clear the string MarginCount = 0 ; // Update the length variable } MarginVW[MWidth - MarginCount] = '\0' ; // Calculate padding required for command characters and cursor if (MarginCount == 0) { strcpy(Margin,MarginFW) ; } // Fixed Width margin if no command characters else { strcpy(Margin,MarginVW) ; } // Varable Width margin if command characters are present switch ( Funct ) { // Calculate status sting... case _Sine_: sprintf(Str2,"%sChannel %c: Freq:%3d%s Phase:%03d Level:%03d Wave:Sine Harmonic:%d\n", Margin, name, Freq, Str1, Phase, Level, Harm) ; break; case _Triangle_: if ((RiseT == 0) || (RiseT == 100)) { sprintf(Str2,"%sChannel %c: Freq:%3d%s Phase:%03d Level:%03d Wave:Sawtooth\n", Margin, name, Freq, Str1, Phase, Level) ; } else { sprintf(Str2,"%sChannel %c: Freq:%3d%s Phase:%03d Level:%03d Wave:Triangle Rise time:%d%%\n", Margin, name, Freq, Str1, Phase, Level, RiseT) ; } break; case _Square_: sprintf(Str2,"%sChannel %c: Freq:%3d%s Phase:%03d Level:%03d Wave:Square Duty cycle:%d%%\n", Margin, name, Freq, Str1, Phase, Level, DutyC) ; break ; case _Time_: sprintf(Str2,"%sChannel %c: Freq:%3d%s Phase:%03d Level:%03d Time\n", Margin, name, Freq, Str1, Phase, Level) ; } strcat(ResultStr,Str2) ; inStr[0] = '\0' ; // Reset input string MarginCount = 0 ; } // Setter functions... void ReInit () { // Re-initialises DMA channels to their initial state. // Note: 1) DMA channels are not restarted, allowing for atomic (simultaneous) restart of both DAC channels later. // 2) Cannot use dma_hw->abort on chained DMA channels, so using disable and re-enable instead. // 3) This needs to be performed across both DAC channels to ensure phase sync is maintained. // Disable both DMA channels associated with this DAC... hw_clear_bits(&dma_hw->ch[data_chan].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); hw_clear_bits(&dma_hw->ch[ctrl_chan].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); // Reset the data transfer DMA's to the start of the data Bitmap... dma_channel_set_read_addr(data_chan, &DAC_data[0], false); // Re-enable both DMA channels associated with this DAC... hw_set_bits(&dma_hw->ch[data_chan].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); hw_set_bits(&dma_hw->ch[ctrl_chan].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); } int Set(int _type, int _val) { switch (_type) { case _Freq_: Freq = _val ; // Frequency (numeric) ReInit() ; // Stop and reset the DAC channel (no restart) DACspeed(Freq * Range) ; // Update State machine run speed break ; case _Phase_: Phase = _val ; // Phase shift (0->355 degrees) ReInit() ; // Stop and reset the DAC channel (no restart) DataCalc() ; // Recalc Bitmap and apply new phase value break ; case _Level_: if (_val > 100) _val = 100 ; // Limit max val to 100% Level = _val ; MCP41020_Write(SelectedChan, Level) ; // Control byte for the MCP42010 just happens to be the same value as the SelectedChan variable StatusString () ; // Update the terminal session break ; case _Sine_: Funct = _Sine_ ; Harm = _val ; // Optional command line parameter (default to zero if not provided) DataCalc() ; break ; case _Square_: Funct = _Square_ ; DutyC = _val ; // Optional command line parameter (default to 50% if not provided) DataCalc() ; break ; case _Triangle_: Funct = _Triangle_ ; RiseT = _val ; // Optional command line parameter (default to 50% if not provided) DataCalc() ; break ; case _Time_: Funct = _Time_ ; DataCalc() ; } return (_val) ; } int Bump(int _type, int _dirn) { // _type = Frequency / Phase / Level, Duty, _dirn = Up / Down (_Up = 1, _Down = -1) int val = 0 ; if (_type == _Freq_) { if ((Freq*Range==0) && (_dirn==_Down)) { // Attempt to bump below lower limit MarginVW[MWidth - MarginCount] = '\0' ; // Calculate padding required for command characters and cursor strcpy(ResultStr,MarginVW) ; strcat(ResultStr,"Error - Minimum Frequency\n") ; } // TBD - remove hardcoded Max frequency else if ((Freq*Range==1000000) && (_dirn==_Up)) { // Attempt to bump above upper limit // else if ((Freq*Range>=MaxDACfreq) && (_dirn==_Up)) { // Attempt to bump above upper limit MarginVW[MWidth - MarginCount] = '\0' ; // Calculate padding required for command characters and cursor strcpy(ResultStr,MarginVW) ; strcat(ResultStr,"Error - Maximum Frequency\n") ; } else { // Not at max or min value... Freq += _dirn ; // ... bump if ((Freq == 1000) && (_dirn == _Up)) { // Range transition point Freq = 1 ; // Reset count if (Range == 1) Range = 1000 ; // either Hz=>KHz else if (Range == 1000) Range = 1000000 ; // or KHz=>MHz } if ((Freq==0) && (Range!=1) && (_dirn==_Down)) { // Range transition point Freq = 999 ; // Reset count if (Range == 1000) Range = 1 ; // either KHz=>Hz else if (Range == 1000000) Range = 1000 ; // or MHz=>KHz } val = Freq ; DACspeed(Freq * Range) ; } } if (_type == _Phase_) { Phase += _dirn ; if (Phase == 360) Phase = 0 ; // Top Endwrap if (Phase < 0 ) Phase = 359 ; // Bottom Endwrap val = Phase ; DataCalc(); } // Update Bitmap data to include new DAC phase if (_type == _Level_) { Level += _dirn ; if (Level > 100) { Level = 0 ; } // Top endwrap if (Level < 0 ) { Level = 100 ; } // Bottom endwrap val = Level ; MCP41020_Write(SelectedChan, Level) ; // Control byte for the MCP42010 just happens to be the same value as the SelectedChan variable StatusString () ; } // Update the terminal session if (_type == _Square_) { DutyC += _dirn ; if (DutyC > 100) { DutyC = 0 ; } // Top endwrap if (DutyC < 0 ) { DutyC = 100 ; } // Bottom endwrap val = DutyC ; DataCalc(); } // Update Bitmap with new Duty Cycle value if (_type == _Triangle_) { RiseT += _dirn ; if (RiseT > 100) { RiseT = 0 ; } // Top endwrap if (RiseT < 0 ) { RiseT = 100 ; } // Bottom endwrap val = RiseT ; DataCalc(); } // Update Bitmap with new Duty Cycle value if (_type == _Sine_) { Harm += _dirn ; if (Harm > 10) { Harm = 0 ; } // Top endwrap if (Harm < 0 ) { Harm = 9 ; } // Bottom endwrap val = Harm ; DataCalc(); } // Update Bitmap with new Sine harmonic value return (val) ; } void DACspeed (int _frequency) { // If DAC_div exceeds 2^16 (65,536), the registers wrap around, and the State Machine clock will be incorrect. // A slow version of the DAC State Machine is used for frequencies below 17Hz, allowing the value of DAC_div to // be kept within range. float DAC_freq = _frequency * BitMapSize; // Target frequency... DAC_div = 2 * (float)clock_get_hz(clk_sys) / DAC_freq; // ...calculate the PIO clock divider required for the given Target frequency float Fout = 2 * (float)clock_get_hz(clk_sys) / (BitMapSize * DAC_div); // Actual output frequency if (_frequency >= 34) { // Fast DAC ( Frequency range from 34Hz to 999Khz ) SM_WrapTop = SM_WrapBot ; // SM program memory = 1 op-code pio_sm_set_wrap (pio, StateMachine, SM_WrapBot, SM_WrapTop) ; // Fast loop (1 clock cycle) // If the previous frequency was < 33Hz, we will have just shrunk the assembler from 4 op-codes down to 1. // This leaves the State Machine program counter pointing outside of the new WRAP statement, which crashes the SM. // To avoid this, we need to also reset the State Machine program counter... pio->sm[StateMachine].instr = SM_WrapBot ; // Reset State Machine PC to start of code pio_sm_set_clkdiv(pio, StateMachine, DAC_div); // Set the State Machine clock } else { // Slow DAC ( 1Hz=>33Hz ) DAC_div = DAC_div / 64; // Adjust DAC_div to keep within useable range DAC_freq = DAC_freq * 64; SM_WrapTop = SM_WrapBot + 3 ; // SM program memory = 4 op-codes pio_sm_set_wrap (pio, StateMachine, SM_WrapBot, SM_WrapTop) ; // slow loop (64 clock cycles) // If the previous frequency was >= 34Hz, we will have just expanded the assembler code from 1 op-code up to 4. // The State Machine program counter will still be pointing to an op-code within the new WRAP statement, so will not crash. pio_sm_set_clkdiv(pio, StateMachine, DAC_div); // Set the State Machine clock speed } StatusString () ; // Update the terminal session } void DataCalc () { int i, j, v_offset = BitMapSize/2 - 1; // Shift sine waves up above X axis int _phase; float a,b,x1,x2,g1,g2; // Scale the phase shift to match data size... _phase = Phase * BitMapSize / 360 ; // Input range: 0 -> 360 (degrees) // Output range: 0 -> 255 (bytes) switch (Funct) { case _Sine_: Harm = Harm % 10; // Sine harmonics cycles after 7 for (i=0; i= 1) { a += v_offset/3 * sin((float)_2Pi*3*i / (float)BitMapSize); } // Add 3rd harmonic if (Harm >= 2) { a += v_offset/5 * sin((float)_2Pi*5*i / (float)BitMapSize); } // Add 5th harmonic if (Harm >= 3) { a += v_offset/7 * sin((float)_2Pi*7*i / (float)BitMapSize); } // Add 7th harmonic if (Harm >= 4) { a += v_offset/9 * sin((float)_2Pi*9*i / (float)BitMapSize); } // Add 9th harmonic if (Harm >= 5) { a += v_offset/11 * sin((float)_2Pi*11*i / (float)BitMapSize); } // Add 11th harmonic if (Harm >= 6) { a += v_offset/13 * sin((float)_2Pi*13*i / (float)BitMapSize); } // Add 13th harmonic if (Harm >= 7) { a += v_offset/15 * sin((float)_2Pi*15*i / (float)BitMapSize); } // Add 15th harmonic if (Harm >= 8) { a += v_offset/17 * sin((float)_2Pi*17*i / (float)BitMapSize); } // Add 17th harmonic if (Harm >= 9) { a += v_offset/19 * sin((float)_2Pi*19*i / (float)BitMapSize); } // Add 19th harmonic DAC_data[j] = (int)(a)+v_offset; // Sum all harmonics and add vertical offset } break; case _Square_: b = DutyC * BitMapSize / 100; // Convert % to value for (i=0; i x1) { DAC_data[j] = (BitMapSize - 1) - ((i - x1) * g2); } // Falling section of waveform } break ; } StatusString () ; // Update the terminal session } public: // Each DAC channel consists of... // BitMap data => DMA => FIFO => State Machine => GPIO pins => R-2-R module // Note: The PIO clock dividers are 16-bit integer, 8-bit fractional, with first-order delta-sigma for the fractional divider. // This means the clock divisor can vary between 1 and 65536, in increments of 1/256. // If DAC_div exceeds 2^16 (65,536), the registers will wrap around, and the State Machine clock will be incorrect. // For frequencies below 34Hz, an additional 63 op-code delay is inserted into the State Machine assembler code. This slows // down the State Machine operation by a factor of 64, keeping the value of DAC_div within range. // Parameters... // _name = Name of this DAC channel instance // _pio = Required PIO channel // _GPIO = Port connecting to the MSB of the R-2-R resistor network. // Constructor int DAC_chan(char _name, PIO _pio, uint _GPIO) { pio = _pio, GPIO = _GPIO, name = _name ; // Copy parameters to class vars PIOnum = pio_get_index(pio) ; // Print friendly value Funct = _Sine_, Freq = 100, Level = 50 ; // Start-up default values... Range = 1, Harm = 0, DutyC = 50, RiseT = 50 ; name == 'A' ? Phase=0 : Phase=180 ; // Set Phase difference between channels int _offset; StateMachine = pio_claim_unused_sm(_pio, true); // Find a free state machine on the specified PIO - error if there are none. ctrl_chan = dma_claim_unused_channel(true); // Find 2 x free DMA channels for the DAC (12 available) data_chan = dma_claim_unused_channel(true); // Configure the state machine to run the DAC program... _offset = pio_add_program(_pio, &pio_DAC_program); // Use helper function included in the .pio file. SM_WrapBot = _offset; pio_DAC_program_init(_pio, StateMachine, _offset, _GPIO); // Setup the DAC control channel... // The control channel transfers two words into the data channel's control registers, then halts. The write address wraps on a two-word // (eight-byte) boundary, so that the control channel writes the same two registers when it is next triggered. dma_channel_config fc = dma_channel_get_default_config(ctrl_chan); // default configs channel_config_set_transfer_data_size(&fc, DMA_SIZE_32); // 32-bit txfers channel_config_set_read_increment(&fc, false); // no read incrementing channel_config_set_write_increment(&fc, false); // no write incrementing dma_channel_configure( ctrl_chan, &fc, &dma_hw->ch[data_chan].al1_transfer_count_trig, // txfer to transfer count trigger &transfer_count, 1, false ); // Setup the DAC data channel... // 32 bit transfers. Read address increments after each transfer. fc = dma_channel_get_default_config(data_chan); channel_config_set_transfer_data_size(&fc, DMA_SIZE_32); // 32-bit txfers channel_config_set_read_increment(&fc, true); // increment the read adddress channel_config_set_write_increment(&fc, false); // don't increment write address channel_config_set_dreq(&fc, pio_get_dreq(_pio, StateMachine, true)); // Transfer when PIO SM TX FIFO has space channel_config_set_chain_to(&fc, ctrl_chan); // chain to the controller DMA channel channel_config_set_ring(&fc, false, 9); // 8 bit DAC 1<<9 byte boundary on read ptr. This is why we needed alignment! dma_channel_configure( data_chan, // Channel to be configured &fc, // The configuration we just created &_pio->txf[StateMachine], // Write to FIFO DAC_data, // The initial read address (AT NATURAL ALIGNMENT POINT) BitMapSize, // Number of transfers; in this case each is 2 byte. false // Don't start immediately. All 4 control channels need to start simultaneously // to ensure the correct phase shift is applied. ); DAC_channel_mask += (1u << ctrl_chan) ; // Save details of DMA control channel to global variable. This facilitates // atomic restarts of both channels, and ensures phase lock between channels. DataCalc() ; // Populate bitmap data. DACspeed(Freq * Range) ; // Initialise State MAchine clock speed. return(StateMachine); } }; class blink_forever { // Class to initialise a state machine to blink a GPIO pin PIO pio ; // Class wide variables to share value with setter function public: uint pioNum, StateMachine, Freq, _offset ; blink_forever(PIO _pio) { pio = _pio; // transfer parameter to class wide var pioNum = pio_get_index(_pio); StateMachine = pio_claim_unused_sm(_pio, true); // Find a free state machine on the specified PIO - error if there are none. _offset = pio_add_program(_pio, &pio_blink_program); blink_program_init(_pio, StateMachine, _offset, PICO_DEFAULT_LED_PIN ); pio_sm_set_enabled(_pio, StateMachine, true); } // Setter function... void Set_Frequency(int _frequency){ Freq = _frequency; // Copy parm to class var // Frequency scaled by 2000 as blink.pio requires this number of cycles to complete... float DAC_div = (float)clock_get_hz(clk_sys) /((float)_frequency*2000); pio_sm_set_clkdiv(pio, StateMachine, DAC_div); // Set the State Machine clock speed } }; bool Repeating_Timer_Callback(struct repeating_timer *t) { int i, steps=64, MidX=128, MidY=128 ; // Bump the time... if ((++Secs)>59) Secs=0 ; // Always bump seconds if (Secs==0) { if ((++Mins)>59 ) Mins=0 ; } // Bump minutes when seconds = 0 if ((Mins==0) && (Secs==0)) { if ((++Hours)>24) Hours=0 ; } // Bump hours when minutes and seconds = 0 // Calculate seconds hand... i=0, Radius=127 ; // Radius=Length of seconds hand Angle=270-(Secs*6) ; // Angle in degrees, shifted 90 degree anti-clockwise Radians=Angle*3.14159/180 ; // Angle in radians StartX=Radius*cos(Radians)+MidX ; StartY=Radius*sin(Radians)+MidY ; while(i12 Radians=Angle*3.14159/180 ; // Angle in radians StartX=Radius*cos(Radians)+MidX ; StartY=Radius*sin(Radians)+MidY ; while(isi - Sine wave (default = no harmonics)\n" "%ssin - Sine wave +nth harmonic ( 0->9 )\n" "%ssi+ - Sine wave harmonic + 1\n" "%ssi- - Sine wave harmonic - 1\n" "%ssq - Square wave (default = 50%%%% duty cycle)\n" "%ssqnnn - Square wave with nnn%%%% duty cycle\n" "%ssq+ - Square wave duty cycle + 1%%%%\n" "%ssq- - Square wave duty cycle - 1%%%%\n" "%str - Triangle wave\n" "%strnnn - Triangle wave with nnn%%%% rise time\n" "%str+ - Triangle wave rise time + 1%%%%\n" "%str- - Triangle wave rise time - 1%%%%\n" "%ssw - Sweep frequency (Low, High, Speed, Pause)\n" "%sfrnnn - Frequency = nnn ( 0->999 )\n" "%sfr+ - Frequency + 1\n" "%sfr- - Frequency - 1\n" "%sphnnn - Phase = nnn ( 0->359 degrees )\n" "%sph+ - Phase + 1\n" "%sph- - Phase - 1\n" "%slennn - Level = nnn ( 0->100%%%% )\n" "%sle+ - Level + 1\n" "%sle- - Level - 1\n" "%swhere...\n" "%s = DAC channel A,B or Both\n" "%snnn = Three digit numeric value\n", MarginVW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW, MarginFW ) ; } void SysInfo (DAC DACobj[], blink_forever LED_blinky) { // Print System Info and resource allocation detils, aligned to current margin settings... // Note: 1) The following string requires '%%%%' to print '%' because... // a) ResultStr is copied to outStr using sprintf - this reduces '%%%%' to '%%' // b) outStr is sent to terminal using printf - this reduces '%%' to '%' // 2) There seems to be some upper limit to sprintf, so the string is split into two smaller parts. tmp = strlen(inStr) ; // Get number of command line characters if (tmp != 0) tmp ++ ; // If there are characters, Bump to also allow for cursor MarginVW[MWidth - tmp] = '\0' ; // Calculate padding required for command characters and cursor // First part of string... sprintf(ResultStr,"%s|----------------------------------------------------------|\n" "%s| System Info... |\n" "%s|----------------------------------------------------------|\n" "%s| RP2040 clock frequency: %7.3fMHz |\n" "%s| Max DAC frequency: %7.3fMHz |\n" "%s|----------------------------|-----------------------------|\n" "%s| LED blinker | |\n" "%s|----------------------------| |\n" "%s| PIO: %2d | |\n" "%s| State machine: %2d | |\n" "%s| GPIO: %2d | |\n" "%s| Frequency: %2dHz | |\n" "%s|----------------------------|-----------------------------|\n" "%s| DAC Channel A | DAC Channel B |\n" "%s|----------------------------|-----------------------------|\n" "%s| Level: %3d%%%% | Level: %3d%%%% |\n" "%s| Frequency: %3d | Frequency: %3d |\n" "%s| Multiplier: %7d | Multiplier: %7d |\n" "%s| Phase: %3d | Phase: %3d |\n" "%s| Duty cycle: %3d%%%% | Duty cycle: %3d%%%% |\n" "%s| Sine harmonic: %1d | Sine harmonic: %1d |\n" "%s| Triangle Rise: %3d%%%% | Triangle Rise: %3d%%%% |\n", MarginVW, MarginFW, MarginFW, MarginFW, (float)clock_get_hz(clk_sys)/1000000, MarginFW, MaxDACfreq/1000000, MarginFW, MarginFW, MarginFW, MarginFW, LED_blinky.pioNum, MarginFW, LED_blinky.StateMachine, MarginFW, PICO_DEFAULT_LED_PIN, MarginFW, LED_blinky.Freq, MarginFW, MarginFW, MarginFW, MarginFW, DACobj[_DAC_A].Level, DACobj[_DAC_B].Level, MarginFW, DACobj[_DAC_A].Freq, DACobj[_DAC_B].Freq, MarginFW, DACobj[_DAC_A].Range, DACobj[_DAC_B].Range, MarginFW, DACobj[_DAC_A].Phase, DACobj[_DAC_B].Phase, MarginFW, DACobj[_DAC_A].DutyC, DACobj[_DAC_B].DutyC, MarginFW, DACobj[_DAC_A].Harm, DACobj[_DAC_B].Harm, MarginFW, DACobj[_DAC_A].RiseT, DACobj[_DAC_B].RiseT ) ; printf(ResultStr) ; // Print first part of string // Second part of string... sprintf(ResultStr,"%s|----------------------------|-----------------------------|\n" "%s| Divider: %10.3f | Divider: %10.3f |\n" "%s|----------------------------|-----------------------------|\n" "%s| PIO: %d | PIO: %d |\n" "%s| State machine: %d | State machine: %d |\n" "%s| GPIO: %d->%d | GPIO: %d->%d |\n" "%s| *BM size: %8d | *BM size: %8d |\n" "%s| *BM start: %x | *BM start: %x |\n" "%s| Wrap Bottom: %2x | Wrap Bottom: %2x |\n" "%s| Wrap Top: %2x | Wrap Top: %2x |\n" "%s| DMA ctrl: %2d | DMA ctrl: %2d |\n" "%s| DMA data: %2d | DMA data: %2d |\n" "%s|----------------------------|-----------------------------|\n" "%s *BM = Bit map\n", MarginFW, MarginFW, DACobj[_DAC_A].DAC_div, DACobj[_DAC_B].DAC_div, MarginFW, MarginFW, DACobj[_DAC_A].PIOnum, DACobj[_DAC_B].PIOnum, MarginFW, DACobj[_DAC_A].StateMachine, DACobj[_DAC_B].StateMachine, MarginFW, DACobj[_DAC_A].GPIO, DACobj[_DAC_A].GPIO+7, DACobj[_DAC_B].GPIO, DACobj[_DAC_B].GPIO+7, MarginFW, BitMapSize, BitMapSize, MarginFW, (int)&DACobj[_DAC_A].DAC_data[0], (int)&DACobj[_DAC_B].DAC_data[0], MarginFW, DACobj[_DAC_A].SM_WrapBot, DACobj[_DAC_B].SM_WrapBot, MarginFW, DACobj[_DAC_A].SM_WrapTop, DACobj[_DAC_B].SM_WrapTop, MarginFW, DACobj[_DAC_A].ctrl_chan, DACobj[_DAC_B].ctrl_chan, MarginFW, DACobj[_DAC_A].data_chan, DACobj[_DAC_B].data_chan, MarginFW, MarginFW ) ; // Printing the final part of the string is handled by the calling routine. // This prevents the 'unknown command' mechanism from triggering. } static inline void cs_select(int _gpio) { asm volatile("nop \n nop \n nop"); gpio_put(_gpio, 0); // Active low asm volatile("nop \n nop \n nop"); } static inline void cs_deselect(int _gpio) { asm volatile("nop \n nop \n nop"); gpio_put(_gpio, 1); asm volatile("nop \n nop \n nop"); } static void SPI_Display_Write(int _data) { uint8_t buff[2]; buff[0] = _data / 256; // MSB data buff[1] = _data % 256; // LSB data cs_select(Display_CS); spi_write_blocking(SPI_PORT, buff, 2); cs_deselect(Display_CS); } static void MCP41020_Write (uint8_t _ctrl, uint8_t _data) { // Add a control bit to select a 'Digi-Pot Write' command. // Scale the data byte to be in the range 0->255. // Transmit data over the SPI bus to the Digi-Pot. uint8_t buff[2]; // Depending on wiring, the MCP41020 Digi-Pot may have the channel selection bits reversed. If so, we will need to... // if (_ctrl == 0x01) _ctrl = 0x02 ; // Swap channel A/B bits // else if (_ctrl == 0x02) _ctrl = 0x01 ; // Note: Do not change if both channels are selected (code = 0x03) buff[0] = _ctrl | 0x10 ; // Set command bit to Write data buff[1] = _data * 2.55 ; // Scale data byte (100%->255) cs_select(Level_CS) ; // Transmit data to Digi-Pot spi_write_blocking(SPI_PORT, buff, 2) ; cs_deselect(Level_CS) ; } static void getLine() { char *pPos = (char *)inStr ; // Pointer to start of Global input string int count = 0 ; while(1) { c = getchar(); if (c == eof || c == '\n' || c == '\r') break ; // Non blocking exit putchar(c); // FullDuplex echo *pPos++ = c ; // Bump pointer, store character count ++ ; } *pPos = '\0' ; MarginCount += count ; // Track number of characters on current line return ; } int SetVal(DAC DACobj[], int _Parm) { // Common code for setting frequency, duty cycle, phase, waaveform and level. // Handles options to set a specific value or bump up/down... if (inStr[3] == '+') { // Bump up and grab result for SPI display... if (SelectedChan & 0b01) result = DACobj[_DAC_A].Bump(_Parm,_Up); if (SelectedChan & 0b10) result = DACobj[_DAC_B].Bump(_Parm,_Up) ; } else if (inStr[3] == '-') { // Bump down and grab result for SPI display... if (SelectedChan & 0b01) result = DACobj[_DAC_A].Bump(_Parm,_Down) ; if (SelectedChan & 0b10) result = DACobj[_DAC_B].Bump(_Parm,_Down) ; } else { // Not a bump, so set the absolute value from Parm[0]... if (SelectedChan & 0b01) result = DACobj[_DAC_A].Set(_Parm,Parm[0]) ; if (SelectedChan & 0b10) result = DACobj[_DAC_B].Set(_Parm,Parm[0]) ; dma_start_channel_mask(DAC_channel_mask); // Atomic restart both DAC channels } return result ; } int main() { bool InvX=false, InvY=false ; // Clock mode flags to allow inverted output set_sys_clock_khz(SysClock*1000, true) ; // Set Pico clock speed MaxDACfreq = clock_get_hz(clk_sys) / BitMapSize ; // Calculate Maximum DAC output frequency for given CPU clock speed stdio_init_all() ; spi_init(SPI_PORT, 500000); // Set SPI0 at 0.5MHz. gpio_set_function(PIN_CLK, GPIO_FUNC_SPI); gpio_set_function(PIN_TX, GPIO_FUNC_SPI); // Chip select is active-low, so initialise to a driven-high state... gpio_init(Display_CS); gpio_set_dir(Display_CS, GPIO_OUT); gpio_put(Display_CS, 1); gpio_init(Level_CS); gpio_set_dir(Level_CS, GPIO_OUT); gpio_put(Level_CS, 1); // Setting Max slew rate and gpio drive strength keeps output linear at high frequencies... for (int i=0; i<16; i++) { gpio_set_slew_rate(i, GPIO_SLEW_RATE_FAST); gpio_set_drive_strength(i, GPIO_DRIVE_STRENGTH_12MA); } // Initialise remaining SPI connections... gpio_set_dir(PIN_CLK, GPIO_OUT); gpio_set_dir(PIN_TX, GPIO_OUT); struct repeating_timer timer; add_repeating_timer_ms(-1000, Repeating_Timer_Callback, NULL, &timer); // 7ms - Short enough to prevent Nixie tube flicker memset(MarginFW,' ',MWidth) ; // Initialise Fixed Width margin... MarginFW[MWidth] = '\0' ; // ... and terminate memset(MarginVW,' ',MWidth) ; // Initialise Variable Width margin... MarginVW[MWidth] = '\0' ; // ... and terminate ResultStr[0] = '\0' ; // Reset string // Instantiate objects to control the various State Machines... // Note: Both DAC channels need to be on the same PIO to achieve // Atomic restarts for accurate phase sync. DAC DACobj[2]; // Array to hold the two DAC channel objects DACobj[_DAC_A].DAC_chan('A',pio1,0); // First DAC channel object in array - resistor network connected to GPIO0->8 DACobj[_DAC_B].DAC_chan('B',pio1,8); // Second DAC channel object in array - resistor network connected to GPIO8->16 blink_forever LED_blinky(pio0); // Onboard LED blinky object strcpy(LastCmd,"?") ; // Hitting return will give 'Help' SPI_Display_Write(SysClock) ; // Pico system clock speed (in MHz) MCP41020_Write(0x3, 50) ; // Both channels -> 50% output level LED_blinky.Set_Frequency(1); // Flash LED at 1Hz- waiting for USB connection while (!stdio_usb_connected()) { sleep_ms(100); } // Wait for USB connection... LED_blinky.Set_Frequency(10); // Flash LED at 10Hz - USB connected. SPI_Display_Write(DACobj[_DAC_A].Freq) ; // Frequency => SPI display // Send (optional) start-up messages to terminal... VerText() ; // Version text printf(ResultStr) ; // Update terminal // Atomic Restart - starting all 4 DMA channels simultaneously ensures phase sync between both DAC channels dma_start_channel_mask(DAC_channel_mask); while(1) { ParmCnt=0, Parm[0]=0, Parm[1]=0, Parm[2]=0, Parm[3]=0 ; // Reset all command line parameters memset(MarginVW,' ',MWidth) ; // Re-initialise Variable Width margin... MarginVW[MWidth] = '\0' ; // ... and terminate ResultStr[0] = '\0' ; // Reset string printf(">") ; // Command prompt MarginCount = 1 ; // Reset count and bump for command prompt getLine() ; // Fetch command line // Zero length string = 'CR' pressed... if (strlen(inStr) == 0) { strcpy(inStr,LastCmd) ; // Repeat last command printf("%s", inStr) ; } // One character commands... if (strlen(inStr) == 1) { if (inStr[0] == '?') HlpText() ; // Help text if (inStr[0] == 'V') VerText() ; // Version text if (inStr[0] == 'S') { DACobj[_DAC_A].StatusString() ; DACobj[_DAC_B].StatusString() ; } if (inStr[0] == 'I') SysInfo(DACobj, LED_blinky); // TBD - inconsitant - make these global ?? } // For all remaining commands, the first character selects DAC channel A or B... if (inStr[0] == 'A') { SelectedChan = 0b0001; } // Channel A only if (inStr[0] == 'B') { SelectedChan = 0b0010; } // Channel B only if (inStr[0] == 'C') { SelectedChan = 0b0011; } // Channel A & B // ...and if we aren't bumping a value, there will be one or more numeric parameters... if ((strlen(inStr) != 0) && (inStr[2] != '+') && (inStr[2] != '-')) { i = 2 ; // Skip chars 0, 1 and 2 while (i++ < strlen(inStr) ) { // Starts at char 3 if ( inStr[i] == 'H' ) { // Hz suffix if (SelectedChan & 0b01) DACobj[_DAC_A].Range = 1 ; if (SelectedChan & 0b10) DACobj[_DAC_B].Range = 1 ; } else if ( inStr[i] == 'K' ) { // KHz suffix if (SelectedChan & 0b01) DACobj[_DAC_A].Range = 1000 ; if (SelectedChan & 0b10) DACobj[_DAC_B].Range = 1000 ; // If the command suffix has been entered as 'KHz', then the next loop itteration will detect the 'H' and // overwrite the 'K'. Writing a space to the next char prevents, this from happening. if (inStr[i+1] == 'H') inStr[i+1] = ' ' ; // Overwrite next char with space } else if ( inStr[i] == 'M' ) { // MHz suffix if (SelectedChan & 0b01) DACobj[_DAC_A].Range = 1000000 ; if (SelectedChan & 0b10) DACobj[_DAC_B].Range = 1000000 ; // If the command suffix has been entered as 'MHz', then the next loop itteration will detect the 'H' and // overwrite the 'M'. Writing a space to the next char prevents, this from happening. if (inStr[i+1] == 'H') inStr[i+1] = ' ' ; // Overwrite next char with space } else if ( inStr[i] == ',' ) { ParmCnt++ ; } // Next parameter else if (isdigit(inStr[i])) { Parm[ParmCnt] *= 10; // Next digit. Bump the existing decimal digits Parm[ParmCnt] += inStr[i] - '0'; } // Convert character to integer and add } } // Next two chars select the command... if ((inStr[1]=='p')&(inStr[2]=='h')) SetVal(DACobj,_Phase_) ; // Phase if ((inStr[1]=='l')&(inStr[2]=='e')) SetVal(DACobj,_Level_) ; // Level if ((inStr[1]=='s')&(inStr[2]=='i')) SetVal(DACobj,_Sine_) ; // Sine wave (optional harmonic parameter) if ((inStr[1]=='f')&(inStr[2]=='r')) { // Frequency SetVal(DACobj,_Freq_) ; // Set value dma_start_channel_mask(DAC_channel_mask); // Atomic restart both DAC channels } // The next two commands need different default values... if (strlen(inStr)==3) Parm[0] = 50 ; // If no value provided, set default to 50 if ((inStr[1]=='s')&(inStr[2]=='q')) SetVal(DACobj,_Square_) ; // Set Square wave (optional duty cycle parameter) if ((inStr[1]=='t')&(inStr[2]=='r')) SetVal(DACobj,_Triangle_) ; // Set Triangle wave (optional duty cycle parameter) if ((inStr[1]=='t')&(inStr[2]=='i')) { // Time display... DACobj[_DAC_A].Set(_Phase_, 0) ; // Phase lock DACobj[_DAC_B].Set(_Phase_, 0) ; for (int gpio = 0; gpio < 16; gpio++) { // Grabs the GPIO back from the State machines gpio_init(gpio); gpio_set_dir(gpio, GPIO_OUT); } // Disable both DMA channels associated with this DAC... hw_clear_bits(&dma_hw->ch[DACobj[_DAC_A].data_chan].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); hw_clear_bits(&dma_hw->ch[DACobj[_DAC_B].data_chan].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); // Reset the data transfer DMA's to the start of the data Bitmap... dma_channel_set_read_addr(DACobj[_DAC_A].data_chan, &DACobj[_DAC_A].DAC_data[0], false); dma_channel_set_read_addr(DACobj[_DAC_B].data_chan, &DACobj[_DAC_B].DAC_data[0], false); pio_sm_set_enabled(pio1,0,false) ; // disable State machine 0 !! HARD CODED !! pio_sm_set_enabled(pio1,1,false) ; // disable State machine 1 gpio_clr_mask(0xff) ; // clear first 16 GPIO outputs ResultStr[0] = '\0' ; // String also used as a flag, so needs to be cleared while (ResultStr[0] == '\0') { // exit on keypress float Radians ; // Draw the clock face... for (int i=0; i",MarginVW) ; // Print current status else printf("%sX axis not inverted.\n>",MarginVW) ; } else if ((c=='y') or (c=='Y')) { InvY = !InvY ; if (InvY) printf("%sY axis inverted.\n>",MarginVW) ; // Print current status else printf("%sY axis not inverted.\n>",MarginVW) ; } if ((c=='S') or (c=='s')) { // Set time printf("%sSet time (format HH:MM:SS)\n%s",MarginVW, MarginFW ) ; getLine() ; // Get the console input Parm[0]=0, Parm[1]=0, Parm[2]=0, Parm[3]=0 ; // Reset all command line parameters i=0, ParmCnt=0 ; // Reset all command line counters while (i",MarginFW,Hours,Mins,Secs) ; } else if ((c=='q') or (c=='Q')) { strcpy(ResultStr," Quit clock mode\n") ; // Prevents error message // Re-enable both DMA channels associated with this DAC... hw_set_bits(&dma_hw->ch[DACobj[_DAC_A].data_chan].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); hw_set_bits(&dma_hw->ch[DACobj[_DAC_B].data_chan].al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); pio_sm_set_enabled(pio1,0,true) ; // Re-enable State machine 0 !! HARD CODED !! pio_sm_set_enabled(pio1,1,true) ; // Re-enable State machine 1 // Re-init the state machines ???? } } } } // The final command is a continual loop creating the sweep function... if ((inStr[1] == 's') & (inStr[2] == 'w')) { // Sweep // Parm[0]=Low frequency, Parm[1]=High frequency, Parm[2]=Scan speed, Parm[3]=Low/High pause i = Parm[0]; for (;;) { if (SelectedChan & 0b01) result = DACobj[_DAC_A].Set(_Freq_,i) ; // Set frequency, display status if (SelectedChan & 0b10) result = DACobj[_DAC_B].Set(_Freq_,i) ; // Set frequency, display status dma_start_channel_mask(DAC_channel_mask); // Atomic restart all 4 DMA channels... printf(ResultStr) ; // Update terminal ResultStr[0] = '\0' ; // Reset the string variable SPI_Display_Write(i); // Update SPI display if (i==Parm[0]) { dirn = 1; sleep_ms(Parm[3]); } if (i>=Parm[1]) { dirn =-1; sleep_ms(Parm[3]); } dma_start_channel_mask(DAC_channel_mask); // Atomic restart both DAC channels i = i + dirn; c = getchar_timeout_us (0); // Non-blocking char input if ((c>=32) & (c<=126)) { strcpy(ResultStr," Exit sweep mode\n") ; // Prevents error message break; } // exit on keypress sleep_ms(Parm[2]); // Speed of scan } } if (strlen(ResultStr) == 0) { // No result can only mean unrecognised command strcpy(MarginVW,MarginFW) ; // Reset Variable Width margin tmp = strlen(inStr) ; if (tmp != 0) tmp ++ ; // Bump to allow for cursor character MarginVW[MWidth - tmp] = '\0' ; // Calculate padding for input and cursor sprintf(outStr,"%sUnknown command!\n", MarginVW) ; // Empty response buffer indicates command has not been recognised } else strcpy(outStr,ResultStr) ; printf(outStr) ; // Update terminal outStr[0] = '\0' ; // Clear (reset) the string variable SPI_Display_Write(result) ; // Update SPI display strcpy(LastCmd, inStr) ; // Preserve last command } return 0; }