Last active
April 6, 2026 14:48
-
-
Save Mictronics/313ba73e41d6dc869957c00a9313306c to your computer and use it in GitHub Desktop.
Active Antenna Tuner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * SCPI commands send using Linux netcat on command line | |
| * echo "SOUR:FREQ:CW 123457000" | nc -q 1 192.168.178.232 5025 | |
| * echo "SOUR:FREQ:CW 31.458 MHz" | nc -q 1 192.168.178.232 5025 | |
| * echo "SOUR:FREQ:FIX 31.458 MHz" | nc -q 1 192.168.178.232 5025 | |
| * echo "*RST" | nc -q 1 192.168.178.232 5025 | |
| * echo "SYST:ERR?" | nc -q 1 192.168.178.232 5025 | |
| * echo "*IDN?" | nc -q 1 192.168.178.232 5025 | |
| * echo "*OPC?" | nc -q 1 192.168.178.232 5025 | |
| * SYSTem:LAN:CONFig <Static IP>, <Static Gateway>, <Static Subnet>, <Port> | |
| */ | |
| #include <Arduino.h> | |
| #include <SPI.h> | |
| #include <EEPROM.h> | |
| #include <SoftSPIB.h> | |
| #include <Ethernet.h> | |
| #include <LedControl.h> | |
| #include <AsyncTimer.h> | |
| #include <Vrekrer_scpi_parser.h> | |
| #define UNUSED(x) (void)(x) | |
| // Line ending | |
| #define CR 13 | |
| #define LF 10 | |
| /* | |
| * Ethernet configuration | |
| * | |
| */ | |
| #define EE_LAN_CONFIG 0 | |
| struct { | |
| uint16_t valid = 0; | |
| IPAddress ip = { 192, 168, 1, 248 }; | |
| IPAddress gateway = { 192, 168, 1, 1 }; | |
| IPAddress subnet = { 255, 255, 255, 0 }; | |
| uint16_t port = 5025; | |
| } lanConfig; | |
| byte mac[] = { 0xA8, 0x61, 0x0A, 0xAE, 0xA8, 0x06 }; | |
| EthernetServer server = EthernetServer(lanConfig.port); | |
| /* Create a new SPI port with: | |
| * Pin 7 = MOSI, | |
| * Pin 8 = MISO, | |
| * Pin 9 = SCK | |
| * Pin 6 = GATE | |
| */ | |
| SoftSPIB rs485SPI(7, 8, 9); | |
| #define RS485_GATE 6 | |
| // RS485 data buffer | |
| uint8_t rs485Data[5] = { 0, 0, 0, 0, 0 }; | |
| // Client command via ethernet | |
| String clientCmd; | |
| /* Create a new LedControl variable. | |
| * We use pins 12,11 and 10 on the Arduino for the SPI interface | |
| * Pin 14 is connected to the DATA IN-pin of the first MAX7221 | |
| * Pin 15 is connected to the CLK-pin of the first MAX7221 | |
| * Pin 16 is connected to the LOAD(/CS)-pin of the first MAX7221 | |
| * There will only be a single MAX7221 attached to the arduino | |
| */ | |
| LedControl led = LedControl(14, 15, 16, 1); | |
| // Create new timer | |
| AsyncTimer ledTimer; | |
| uint16_t ledTimerId = 0; | |
| // Create simulated instrument | |
| SCPI_Parser instrument; | |
| bool opc = true; | |
| // Declare reset function @ address 0 | |
| void (*resetFunc)(void) = 0; | |
| void setup() { | |
| instrument.RegisterCommand(F("*OPC?"), &OperationComplete); | |
| instrument.RegisterCommand(F("*IDN?"), &Ident); | |
| instrument.RegisterCommand(F("*RST"), &Reset); | |
| instrument.RegisterCommand(F("SOURce:FREQuency:CW"), &Frequency); | |
| instrument.RegisterCommand(F("SOURce:FREQuency:FIX"), &Frequency); | |
| instrument.RegisterCommand(F("SOURce:FREQuency:FIXed"), &Frequency); | |
| instrument.RegisterCommand(F("SYSTem:ERRor?"), &GetLastError); | |
| instrument.RegisterCommand(F("SYSTem:LAN:CONFig?"), &GetLanConfig); | |
| instrument.RegisterCommand(F("SYSTem:LAN:CONFig"), &SetLanConfig); | |
| Serial.begin(9600); | |
| // Initialize LED display | |
| led.shutdown(0, false); | |
| led.setIntensity(0, 3); | |
| led.clearDisplay(0); | |
| // Initialize RS485 driver | |
| rs485SPI.begin(); | |
| rs485SPI.setBitOrder(MSBFIRST); | |
| rs485SPI.setDataMode(SPI_MODE3); | |
| rs485SPI.setClockDivider(SPI_CLOCK_DIV128); | |
| pinMode(RS485_GATE, OUTPUT); | |
| digitalWrite(RS485_GATE, LOW); | |
| // Read LAN config from EEPROM | |
| EEPROM.get(EE_LAN_CONFIG, lanConfig); | |
| if (lanConfig.valid != 0xDEAD) { | |
| SetDefaultLanConfig(); | |
| } | |
| // Initialize the ethernet device | |
| server = EthernetServer(lanConfig.port); | |
| Ethernet.begin(mac, lanConfig.ip, lanConfig.gateway, lanConfig.subnet); | |
| // Start listening for clients | |
| server.begin(); | |
| Serial.print("Server @ "); | |
| Serial.println(Ethernet.localIP()); | |
| // Display power saving timeout | |
| ledTimerId = ledTimer.setInterval([]() { | |
| led.shutdown(0, true); | |
| }, | |
| 5000); | |
| } | |
| void SetDefaultLanConfig() { | |
| lanConfig.ip[0] = 192; | |
| lanConfig.ip[1] = 168; | |
| lanConfig.ip[2] = 1; | |
| lanConfig.ip[3] = 248; | |
| lanConfig.gateway[0] = 192; | |
| lanConfig.gateway[1] = 168; | |
| lanConfig.gateway[2] = 1; | |
| lanConfig.gateway[3] = 1; | |
| lanConfig.subnet[0] = 255; | |
| lanConfig.subnet[1] = 255; | |
| lanConfig.subnet[2] = 255; | |
| lanConfig.subnet[3] = 0; | |
| lanConfig.port = 5025; | |
| lanConfig.valid = 0xDEAD; | |
| EEPROM.put(EE_LAN_CONFIG, lanConfig); | |
| resetFunc(); //call reset | |
| } | |
| void loop() { | |
| char* p; | |
| // Process timer handler | |
| ledTimer.handle(); | |
| // Indicate ethernet hardware error | |
| if (Ethernet.hardwareStatus() == EthernetNoHardware) { | |
| led.setChar(0, 7, 'E', false); | |
| } | |
| // Indicate link status | |
| if (Ethernet.linkStatus() == Unknown || Ethernet.linkStatus() == LinkOFF) { | |
| led.setChar(0, 7, ' ', false); | |
| } else if (Ethernet.linkStatus() == LinkON) { | |
| led.setChar(0, 7, 'L', false); | |
| } | |
| EthernetClient client = server.available(); | |
| if (client && client.connected()) { | |
| instrument.ProcessInput(client, "\n"); | |
| } | |
| } | |
| void Ident(SCPI_C cmd, SCPI_Parameters para, Stream& interface) { | |
| // valid IDN | |
| interface.println("Mictronics,Elektra Antenna Tuner, 2026"); | |
| } | |
| void Reset(SCPI_C cmd, SCPI_Parameters para, Stream& interface) { | |
| resetFunc(); //call reset | |
| } | |
| void OperationComplete(SCPI_C cmd, SCPI_Parameters para, Stream& interface) { | |
| interface.println(opc); | |
| } | |
| void GetLanConfig(SCPI_C commands, SCPI_P parameters, Stream& interface) { | |
| UNUSED(parameters); | |
| UNUSED(commands); | |
| char s[60]; | |
| sprintf(s, "%u.%u.%u.%u,%u.%u.%u.%u,%u.%u.%u.%u,%u\n", | |
| lanConfig.ip[0], lanConfig.ip[1], lanConfig.ip[2], lanConfig.ip[3], | |
| lanConfig.gateway[0], lanConfig.gateway[1], lanConfig.gateway[2], lanConfig.gateway[3], | |
| lanConfig.subnet[0], lanConfig.subnet[1], lanConfig.subnet[2], lanConfig.subnet[3], | |
| lanConfig.port); | |
| interface.print(s); | |
| } | |
| void SetLanConfig(SCPI_C commands, SCPI_P parameters, Stream& interface) { | |
| UNUSED(commands); | |
| uint8_t len = parameters.Size(); | |
| if (len != 4) { | |
| instrument.last_error = SCPI_Parser::ErrorCode::InvalidParameter; | |
| return; | |
| } | |
| // Do not change order! LIFO reading. | |
| String str_port = String(parameters.Pop()); | |
| String str_subnet = String(parameters.Pop()); | |
| String str_gateway = String(parameters.Pop()); | |
| String str_ip = String(parameters.Pop()); | |
| if (!lanConfig.subnet.fromString(str_subnet)) { | |
| instrument.last_error = SCPI_Parser::ErrorCode::InvalidParameter; | |
| return; | |
| } | |
| if (!lanConfig.gateway.fromString(str_gateway)) { | |
| instrument.last_error = SCPI_Parser::ErrorCode::InvalidParameter; | |
| return; | |
| } | |
| if (!lanConfig.ip.fromString(str_ip)) { | |
| instrument.last_error = SCPI_Parser::ErrorCode::InvalidParameter; | |
| return; | |
| } | |
| lanConfig.port = str_port.toInt(); | |
| if (lanConfig.port == 0) { | |
| instrument.last_error = SCPI_Parser::ErrorCode::InvalidParameter; | |
| return; | |
| } | |
| lanConfig.valid = 0xDEAD; | |
| EEPROM.put(EE_LAN_CONFIG, lanConfig); | |
| instrument.last_error = SCPI_Parser::ErrorCode::NoError; | |
| } | |
| uint32_t parseFrequency(String input) { | |
| input.trim(); | |
| input.toUpperCase(); | |
| input.replace(" ", ""); | |
| double value = input.toDouble(); // Extracts numeric part | |
| if (input.endsWith("GHZ")) { | |
| return (uint32_t)(value * 1e9); | |
| } else if (input.endsWith("MHZ")) { | |
| return (uint32_t)(value * 1e6); | |
| } else if (input.endsWith("KHZ")) { | |
| return (uint32_t)(value * 1e3); | |
| } else if (input.endsWith("HZ")) { | |
| return (uint32_t)(value); | |
| } | |
| // No unit → SCPI default = Hz | |
| return (uint32_t)(value); | |
| } | |
| uint32_t freq = 0; | |
| void Frequency(SCPI_C cmd, SCPI_Parameters para, Stream& interface) { | |
| opc = false; | |
| led.shutdown(0, false); | |
| ledTimer.reset(ledTimerId); | |
| // Measurement frequency | |
| if (para.Size() > 0) { | |
| freq = constrain(parseFrequency(String(para[0])), 30000000, 512000000); | |
| } | |
| // Extract digits | |
| int digits[6]; | |
| digits[0] = (freq / 1000) % 10; // 1 kHz | |
| digits[1] = (freq / 10000) % 10; // 10 kHz | |
| digits[2] = (freq / 100000) % 10; // 100 kHz | |
| digits[3] = (freq / 1000000) % 10; // 1 MHz | |
| digits[4] = (freq / 10000000) % 10; // 10 MHz | |
| digits[5] = (freq / 100000000) % 10; // 100 MHz | |
| // Leading zero suppression | |
| bool leading = true; | |
| for (int i = 5; i >= 3; i--) { // only MHz part | |
| if (digits[i] == 0 && leading && i != 3) { | |
| led.setChar(0, i, ' ', false); // blank | |
| } else { | |
| leading = false; | |
| led.setDigit(0, i, digits[i], (i == 3)); // decimal point at MHz | |
| } | |
| } | |
| // Show kHz digits | |
| led.setDigit(0, 2, digits[2], false); | |
| led.setDigit(0, 1, digits[1], false); | |
| led.setDigit(0, 0, digits[0], false); | |
| // RS485 Output | |
| memset(rs485Data, 0, sizeof(rs485Data)); | |
| EncodeFrequency(freq, rs485Data); | |
| rs485SendData(rs485Data); | |
| opc = true; | |
| } | |
| void GetLastError(SCPI_C commands, SCPI_P parameters, Stream& interface) { | |
| switch (instrument.last_error) { | |
| case instrument.ErrorCode::BufferOverflow: | |
| interface.println(F("Buffer overflow error")); | |
| break; | |
| case instrument.ErrorCode::Timeout: | |
| interface.println(F("Communication timeout error")); | |
| break; | |
| case instrument.ErrorCode::UnknownCommand: | |
| interface.println(F("Unknown command received")); | |
| break; | |
| case instrument.ErrorCode::NoError: | |
| interface.println(F("No Error")); | |
| break; | |
| } | |
| instrument.last_error = instrument.ErrorCode::NoError; | |
| } | |
| void ErrorHandler(SCPI_C commands, SCPI_P parameters, Stream& interface) { | |
| //This function is called every time an error occurs | |
| /* The error type is stored in my_instrument.last_error | |
| Possible errors are: | |
| SCPI_Parser::ErrorCode::NoError | |
| SCPI_Parser::ErrorCode::UnknownCommand | |
| SCPI_Parser::ErrorCode::Timeout | |
| SCPI_Parser::ErrorCode::BufferOverflow | |
| */ | |
| /* | |
| For BufferOverflow errors, the rest of the message, still in the interface | |
| buffer or not yet received, will be processed later and probably | |
| trigger another kind of error. | |
| Here we flush the incomming message | |
| */ | |
| if (instrument.last_error == SCPI_Parser::ErrorCode::BufferOverflow) { | |
| delay(2); | |
| while (interface.available()) { | |
| delay(2); | |
| interface.read(); | |
| } | |
| } | |
| /* | |
| For UnknownCommand errors, you can get the received unknown command and | |
| parameters from the commands and parameters variables. | |
| */ | |
| } | |
| /* | |
| * Send manchester word via RS485 interface. | |
| * @param data Pointer to manchester word array. | |
| * | |
| */ | |
| void rs485SendData(uint8_t* data) { | |
| digitalWrite(RS485_GATE, HIGH); | |
| for (int8_t i = 0; i < 5; i++) { | |
| rs485SPI.transfer(data[i]); | |
| } | |
| delayMicroseconds(100); | |
| digitalWrite(RS485_GATE, LOW); | |
| } | |
| /* | |
| * Round to 10 kHz precision. | |
| * @param freq_kHz Input frequency in kHz. | |
| * @return Output frequency rounded to 10 kHz. | |
| * | |
| */ | |
| uint32_t roundTo10kHz(uint32_t freq_kHz) { | |
| return (uint32_t)(((freq_kHz + 5) / 10) * 10); | |
| } | |
| /* | |
| * Encode frequency into 20 bit manchester word. | |
| * @param frequency given in Hz. | |
| * @param result Pointer to manchester word array. | |
| * | |
| * Bit | |
| * 0 Sync | |
| * 1 Sync | |
| * 2 Sync | |
| * Bit weighting in MHz | |
| * 3 327.680 (MSB) | |
| * 4 163.840 | |
| * 5 81.920 | |
| * 6 40.960 | |
| * 7 20.480 | |
| * 8 10.240 | |
| * 9 5.120 | |
| * 10 2.560 | |
| * 11 1.280 | |
| * 12 0.640 | |
| * 13 0.320 | |
| * 14 0.160 | |
| * 15 0.080 | |
| * 16 0.040 | |
| * 17 0.020 | |
| * 18 0.010 (LSB) | |
| * 19 Parity | |
| */ | |
| void EncodeFrequency(uint32_t frequency, uint8_t* result) { | |
| uint8_t parity = 0; | |
| uint8_t bitPos = 0; | |
| uint32_t f = roundTo10kHz((uint32_t)(frequency / 1000)); // Round to 10 kHz precision | |
| // sync | |
| // 11 | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| // 00 | |
| bitPos++; | |
| bitPos++; | |
| if (f >= 327680) { | |
| f -= 327680; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 163840) { | |
| f -= 163840; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 81920) { | |
| f -= 81920; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 40960) { | |
| f -= 40960; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 20480) { | |
| f -= 20480; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 10240) { | |
| f -= 10240; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 5120) { | |
| f -= 5120; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 2560) { | |
| f -= 2560; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 1280) { | |
| f -= 1280; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 640) { | |
| f -= 640; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 320) { | |
| f -= 320; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 160) { | |
| f -= 160; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 80) { | |
| f -= 80; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 40) { | |
| f -= 40; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 20) { | |
| f -= 20; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (f >= 10) { | |
| f -= 10; | |
| bitPos++; | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| parity++; | |
| } else { | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| bitPos++; | |
| } | |
| bitPos++; | |
| if (parity % 2 == 0) { | |
| // Even parity set bit to 1 | |
| result[bitPos / 8] |= (1 << (7 - (bitPos % 8))); | |
| } | |
| // else leave 0 for odd parity | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment