/* * LM - June 2022 - Ultra QRP SWR bridge interface [Compile for Nano] * - Hilltopper transceiver interface * Sub-version 0.1 - 16 x 2 LCD textual display * Sub-version 0.2 - 2-inch LCD graphical display * Sub-version 0.3 - Redesign - Continuous meter-bar updating * Sub-version 0.4 - Replace solid bars with vertical marker * Sub-version 0.5 - Optionally detect ultra QRP * Sub-version 0.6 - A/D damping (average consecutive analog reads) * Sub-version 0.7 - Alternative regression equation for QRPp */ #define SUBVERSION 0.7 #define LCD_GR // 2-inch color LCD - SPI [Comment-out the unused display type] //#define LCD_CH // 16x2 LCD - i2c //#define FIXED_REV_TEST // For testing with pickup noise on A0 (breadboard version) const boolean CALIBRATION_MODE = false; // Set = true to display raw A/D values only const boolean QRPp = false; // Set = true to use low-power regression equation // Assumes bridge and A-ref are appropriately configured #ifdef LCD_GR #include "LCD_Driver.h" #include "GUI_Paint.h" #include /* * LCD Nano * ------------ * Vcc 5 VDC * Gnd Gnd * DIN D11 * CLK D13 * CSS D10 * DC D7 * RST D8 * BL D9 */ #endif #define QSK 2 // Hilltopper pins relating to Tx-Rx transition #define TXEN 4 // Txen (mixed case) in Hilltopper sketch #define MUTE 6 // Mute (ditto) #define FWD A0 // SWR bridge forward and reverse power to #define REV A1 // MPU analog inputs #ifdef LCD_CH #include // Two-line LCD (16 x 2) const int ROWS = 2; // If 20x4 change ROWS to 4 const int COLS = 16; // If 20x4 change COLS to 20 LiquidCrystal_I2C lcd(0x27, COLS, ROWS); // Instantiate #endif #ifdef LCD_GR #define BL 9 // Backlight DIO pin# const UWORD BACKGROUND_COLOR = BLACK; // Meter bars const unsigned int SCREEN_WIDTH = 320; // Convenience alias const unsigned int SCREEN_HEIGHT = 240; const unsigned int METER_BAR_WIDTH = 200; const unsigned int METER_BAR_HEIGHT = 20; const unsigned int METER_BAR_FILL_HEIGHT = METER_BAR_HEIGHT-6; const unsigned int METER_BAR_X = (SCREEN_WIDTH-METER_BAR_WIDTH)/2; const unsigned int METER_BAR_FILL_X = METER_BAR_X+3; const DOT_PIXEL METER_BAR_LINE_WIDTH = DOT_PIXEL_2X2; const UWORD METER_BAR_MINMAX_COLOR = WHITE; #define TICK "'" // Comment-out to omit meter bar tick marks #define TICK_FONT &Font16 const unsigned int METER_BAR_WIDTH_PER_TICK = METER_BAR_WIDTH / 5; const int TICK_Y_OFFSET = -14; const UWORD TICK_COLOR = WHITE; const UWORD AXIS_TITLE_COLOR = YELLOW; // V2 // Text const unsigned int PWR_X = 20; const unsigned int PWR_LEFT_X = PWR_X; const unsigned int PWR_MIDDLE_X = 185; const unsigned int PWR_RIGHT_X = 228; const unsigned int METER_BAR_MINMAX_LEFT_X = METER_BAR_X - 20; const unsigned int METER_BAR_MINMAX_RIGHT_X = METER_BAR_X + METER_BAR_WIDTH + 10; const unsigned int PWR_AXIS_TITLE_X = 98; const unsigned int PWR_AXIS_TITLE_Y = 20; const int METER_BAR_MINMAX_DELTA_Y = -24; const UWORD PWR_BG_COLOR = BACKGROUND_COLOR; #define PWR_FONT &Font16 const unsigned int FWD_PWR_Y = 65; const UWORD FWD_PWR_FG_COLOR = YELLOW; const unsigned int REV_PWR_Y = 125; const unsigned int REV_AXIS_TITLE_X = 80; const unsigned int REV_AXIS_TITLE_Y = PWR_AXIS_TITLE_Y + 60; const UWORD REV_PWR_FG_COLOR = YELLOW; const unsigned int SWR_X = 100; const unsigned int SWR_LEFT_X = 100; const unsigned int SWR_RIGHT_X = 154; const unsigned int SWR_Y = 187; const unsigned int SWR_AXIS_TITLE_X = 146; const unsigned int SWR_AXIS_TITLE_Y = PWR_AXIS_TITLE_Y + 122; const UWORD SWR_FG_COLOR = YELLOW; const UWORD SWR_BG_COLOR = BACKGROUND_COLOR; #define SWR_FONT &Font16 // Meter fill vertical positions const unsigned int FWD_PWR_FILL_Y = FWD_PWR_Y - 25; const unsigned int REV_PWR_FILL_Y = REV_PWR_Y - 25; const unsigned int SWR_FILL_Y = SWR_Y - 25; const float FWD_PWR_MIN = 0.; // Watts const float FWD_PWR_MAX = 10.; const float FWD_PWR_RANGE = FWD_PWR_MAX - FWD_PWR_MIN; // Equates to full bar const float REV_PWR_MIN = 0.; const float REV_PWR_MAX = 10.; const float REV_PWR_RANGE = REV_PWR_MAX - REV_PWR_MIN; const float SWR_MIN = 1.; // 1:1 const float SWR_MAX = 10.; // If SWR > SWR_MAX, display full bar const float SWR_RANGE = SWR_MAX - SWR_MIN; const char cFWD_PWR_MIN[] = "0"; const char cFWD_PWR_MAX[] = "10"; const char cREV_PWR_MIN[] = "0"; const char cREV_PWR_MAX[] = "10"; const char cSWR_MIN[] = ""; const char cSWR_MAX[] = "10"; int lastForwardPowerPx = METER_BAR_FILL_X; String lastForwardPowerStr = "*"; int lastReversePowerPx = METER_BAR_FILL_X; String lastReversePowerStr = "*"; int lastSwrPx = METER_BAR_FILL_X; String lastSwrStr = ""; String lastMsgStr = ""; String sW; boolean fwdPwrTxSwitch = true; boolean fwdPwrGrSwitch = true; boolean revPwrTxSwitch = true; boolean revPwrGrSwitch = true; boolean swrTxSwitch = true; boolean swrGrSwitch = true; boolean powerChanged = false; // True when either forward or reverse power changed #define MSG_FONT &Font16 #define DATA_LABEL_FONT &Font16 const unsigned int DATA_LABEL_X_OFFSET = 2; const unsigned int DATA_LABEL_Y_OFFSET = 10; #endif const unsigned long JIFFY = 100; // Convenient time constants const unsigned long QTRSEC = 250; const unsigned long HALFSEC = 500; const unsigned long ONESEC = 1000; const unsigned long TWOSEC = 2000; const unsigned long FIVESEC = 5000; const unsigned long ONEDAY = 86400000; // Indefinite pause for testing const unsigned long SWR_MSG_TIMEOUT = TWOSEC; unsigned long lastSwrUpdate = millis(); // Timeout SWR textual display during receive const unsigned long DAMPING_DELAY = 1; const int DAMPING = 0; // Sub-version 0.6 const int MINFWD = 20; // Minimum raw forward A/D for SWR boolean ultraQRP = false; // Reserved - To do: Mode switch int ultraQRPminAD = 40; // Experimental boolean key_down = false; const unsigned long LCD_TR_DELAY = HALFSEC; unsigned long last_key_down = 0; // Reserved float SLOPE; // Pseudo constants (See setup) float INTERCEPT; void setup() { pinMode(A0, INPUT); pinMode(A1, INPUT); analogReference(EXTERNAL); // Linear regression - RMS power as function or raw A/D // Slope and intercept are determined from testing with calibrated SWR bridge SLOPE = 0.01219; // Data from 3.3 volt A-ref measurements INTERCEPT = -2.9257; if (QRPp) { // Sub-version 0.7 SLOPE = 0.00599; // Data from 1.65 volt A-ref INTERCEPT = -2.91011; } #ifdef LCD_CH lcd.init(); lcd.backlight(); displayLCD("Power/SWR Meter"," wa4efs"); // Substitute personalized text delay(TWOSEC); // Extend duration of splash screen myClear(); lcd.noBacklight(); #endif #ifdef LCD_GR // Sketch SWR_meter-2, sub-version 0.3+ initLCD(); splashGR(); delay(FIVESEC); LCD_Clear(BACKGROUND_COLOR); initMeterBarsV2(); delay(ONESEC); #endif key_down = true; // Reserved for transceiver interface } void loop() { #ifdef LCD_GR // 2-inch color graphic LCD if (CALIBRATION_MODE) displayRawAD(); else fastUpdateV2(); #endif #ifdef LCD_CH // 16x2 text-only LCD if (isKeyDown()) { if (CALIBRATION_MODE) { darkenLCD(JIFFY); // Force flash in case values do not change displayRawAD(); } else { lcd.backlight(); // [Next] If false, calibrated power is watts; if true, milliwatts lcdDisplayPwrSWR(false); } delay(LCD_TR_DELAY); } else { lcd.noBacklight(); // Proxy for normal transceiver display (frequency, step, etc.) } #endif } int readForward() { if (DAMPING > 1) { int sumAD = 0; for (int i=0; i 1) { int sumAD = 0; for (int i=0; i 0.) return y; return 0.; } String pwrToStr(float pwr, boolean ultraQRP) { // 16x2 display version String s; if (ultraQRP) return String(pwr * 1000., 0); // Range milliwatts else { if (pwr >= 1) s = String(pwr, 1); // Range watts else s = String(pwr, 2); } if (s.charAt(0) == '0') s = s.substring(1); return s; } String pwrToStr(float pwr) { // Graphic LCD revision if (pwr >= 1.) return String(pwr, 1) + "w"; else return String(pwr*1000., 0) + "mw"; } float vswr(float fwd, float rev) { // Test only - Not needed when forward and reverse measures are voltages if (fwd > 0) { float gamma = sqrt(rev/fwd); return (1+gamma) / (1-gamma); } else return 1.; } void displayRawAD() { #ifdef LCD_CH displayLCD("Fwd " + String(readForward()) + " ","Rev " + String(readReverse()) + " "); #endif #ifdef LCD_GR eraseMsg(lastMsgStr); lastMsgStr = "Fwd " + String(readForward()) + " " + "Rev " + String(readReverse()); displayMsg(lastMsgStr); #endif } #ifdef LCD_CH void lcdDisplayPwrSWR(boolean ultraQRP) { int rawFwd, rawRev; String sFwd, sRev, swr; rawFwd = readForward(); rawRev = readReverse(); float fFwd = fPower(rawFwd); float fRev = fPower(rawRev); sFwd = pwrToStr(fFwd, ultraQRP); sRev = pwrToStr(fRev, ultraQRP); String s1 = "Fwd ", s2 = ""; s1.concat(sFwd); s1.concat(", Rev "); s1.concat(sRev); if ((rawFwd > MINFWD) && (rawFwd > rawRev)) { s2 = "SWR "; s2.concat(String(vswr(fFwd, fRev), 2)); s2.concat(" : 1"); } displayLCD(s1, s2); } void myClear() { lcd.clear(); lcd.setCursor(0,0); } void displayLCD(String line1, String line2) { int row = ROWS/2 - 1; // Same as if..else myClear(); lcd.setCursor(0, row); lcd.print(line1); lcd.setCursor(0, row+1); lcd.print(line2); lcd.backlight(); } void darkenLCD(unsigned long ms) { lcd.noBacklight(); delay(ms); lcd.backlight(); } #endif #ifdef LCD_GR void drawEmptyMeterBar(unsigned int yPos, UWORD outline_color) { Paint_DrawRectangle(METER_BAR_X, yPos, METER_BAR_X + METER_BAR_WIDTH, METER_BAR_HEIGHT + yPos, outline_color, METER_BAR_LINE_WIDTH, DRAW_FILL_EMPTY); } void fillMeterBar(unsigned int yPos, unsigned int toX, UWORD fill_color) { Paint_DrawRectangle(METER_BAR_FILL_X, yPos + 1, toX, yPos + METER_BAR_FILL_HEIGHT, fill_color, METER_BAR_LINE_WIDTH, DRAW_FILL_FULL); } void fillMeterBar(unsigned int yPos, unsigned int fromX, unsigned int toX, UWORD fill_color) { Paint_DrawRectangle(fromX, yPos + 1, toX, yPos + METER_BAR_FILL_HEIGHT, fill_color, METER_BAR_LINE_WIDTH, DRAW_FILL_FULL); } void displayTickMarks(unsigned int yPos, unsigned int fromX, unsigned int toX, unsigned int deltaPx) { for (int posX=fromX; posXnewPx; ndx--) fillMeterBar(FWD_PWR_FILL_Y, ndx, ndx, BACKGROUND_COLOR); } else if (newPx > lastForwardPowerPx) { for(ndx=lastForwardPowerPx; ndxnewPx; ndx--) fillMeterBar(REV_PWR_FILL_Y, ndx, ndx, BACKGROUND_COLOR); } else if (newPx > lastReversePowerPx) { for(ndx=lastReversePowerPx; ndxnewPx; ndx--) fillMeterBar(SWR_FILL_Y+2, ndx, ndx, BACKGROUND_COLOR); } else if (newPx > lastSwrPx) { for(ndx=lastSwrPx; ndx fwdPwr) // Disallow impossible value due to noise or bridge calibration error revPwr = fwdPwr; if (fwdPwrTxSwitch) updateForwardPowerText(fwdPwr); if (fwdPwrGrSwitch) updateForwardPowerMeter(fwdPwrToPixels(fwdPwr)); if (revPwrTxSwitch) updateReversePowerText(revPwr); if (revPwrGrSwitch) updateReversePowerMeter(revPwrToPixels(revPwr)); float swr = vswr(fwdPwr, revPwr); if (swrTxSwitch) updateSwrText(swr); if (swrGrSwitch) { unsigned int swrPx = swrToPixels(swr); if (swrPx > (METER_BAR_X + METER_BAR_WIDTH - 4)) swrPx = METER_BAR_X + METER_BAR_WIDTH - 4; updateSwrMeter(swrPx); } return; } #define SPLASH_FONT &Font16 #define SPLASH_COLOR_1 YELLOW #define SPLASH_COLOR_2 GREEN #define SPLASH_INDENT_1 (SCREEN_WIDTH-130)/2 #define SPLASH_INDENT_2 (SCREEN_WIDTH-240)/2 void splashGR() { Paint_DrawString_EN(SPLASH_INDENT_1, SCREEN_HEIGHT/2-10, "SWR Meter V2", SPLASH_FONT, BACKGROUND_COLOR, SPLASH_COLOR_1); Paint_DrawString_EN(SPLASH_INDENT_2, SCREEN_HEIGHT/2+10, "https://www.lloydm.net", SPLASH_FONT, BACKGROUND_COLOR, SPLASH_COLOR_2); } void displayDataLabels(unsigned int yPos, unsigned int fromX, unsigned int toX, unsigned int deltaPx) { int label = 0; for (int posX=fromX; posX fwdPwr) // Disallow impossible value due to noise or bridge calibration error revPwr = fwdPwr; minAdjustForwardPowerMeter(fwdPwrToPixels(fwdPwr)); minAdjustReversePowerMeter(revPwrToPixels(revPwr)); float swr = vswr(fwdPwr, revPwr); if (powerChanged) { eraseMsg(lastMsgStr); lastMsgStr = pwrToStr(fwdPwr) + " SWR: " + String(swr, 2); displayMsg(lastMsgStr); lastSwrUpdate = millis(); } else if (ultraQRP && (fwdAD > ultraQRPminAD)) { eraseMsg(lastMsgStr); lastMsgStr = "Ultra QRP - raw A/D: " + String(fwdAD); displayMsg(lastMsgStr, BLUE); lastSwrUpdate = millis(); } else if (millis() > (lastSwrUpdate + SWR_MSG_TIMEOUT)) eraseMsg(lastMsgStr); unsigned int swrPx = swrToPixels(swr); if (swrPx > (METER_BAR_X + METER_BAR_WIDTH - 4)) swrPx = METER_BAR_X + METER_BAR_WIDTH - 4; minAdjustSwrMeter(swrPx); powerChanged = false; // Reset forced meter updates return; } #endif