Skip to content

Instantly share code, notes, and snippets.

@Mictronics
Last active April 6, 2026 14:48
Show Gist options
  • Select an option

  • Save Mictronics/313ba73e41d6dc869957c00a9313306c to your computer and use it in GitHub Desktop.

Select an option

Save Mictronics/313ba73e41d6dc869957c00a9313306c to your computer and use it in GitHub Desktop.
Active Antenna Tuner
/*
* 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