Arduino, GPIB, SCPI, ChatGPT, Keithley 2000 a AutoREAD
Zajímavá směsice z titulku vznikla postupnou potřebou realizovat automatický odměr napětí z mutimetru Keithley 2000. A o tom, jak se (ne)vedlo, co se podařilo a co se podařilo obejít bude dnešní článek.
Pro přesný zdroj napětí s DAC AD5791 (časem ho taky popíšu, zatím jsem dost napjatej, jak to celé dopadne) jsem potřeboval sledovat jeho teplotní závislost. A protože se jedná (teoreticky) o hrozně hnusně nechutně přesný zdroj napětí, potřeboval jsem taky (prakticky) hrozně hnusně nechutně přesně měřit :)
Nejdřív jsem k tomu měl připojený multimetr HP34401A. Ale... nějak jsem zjistil, že tahle mašinka opravdu měří na svých 6.5 míst a zbylá čísla buď odhaduje, dopočítává, nebo matematicky doluje ze šumu, jednak jsem chtěl mít křížovou kontrolu. A protože se mi nedávno podařilo na eBay za dost slušné peníze vydražit multimetr Keithley 2000, který je sice taky "oficiálně" jen 6.5 místný, ale přes GPIB nebo RS232 umí poslat ještě o dvě číslice navíc, které jsou prý o něco důvěryhodnější, než u toho HP, tak jsem ho chtěl také zařadit do měřicí sestavy.
Udělal jsem tedy další GPIB kabel (dle AR488, tedy Arduino Nano, přidrátované napřímo ke GPIB/IEE488 konektoru), ale za nic na světě se mi nedařilo přes tuhle kombinaci spustit AutoREAD, tedy donutit multimetr, aby mi každých X sekund poslal naměřená data přes nějaký port do PC (v PC si to zobrazuju přes SerialPlot, anžto nevládnu lepšími nástroji).
Ani konzultace s autorem AR488 nevedla k ničemu rozumnému, kromě zjištění, že mi to nechodí, ač by asi mělo. Různými pokusy jsem strávil celý víkend, a v zoufalství jsem se obrátil i na ChatGPT. Ten mi potvrdil, že by to fakt chodit mělo, ale taky mi navrhnul, jestli se na to nechci vykašlat a jen udělat "udělátko" na RS232 kabel, které bude měřáku v pravidelných intervalech posílat příkaz READ?.
To se mi vůbec nelíbilo, ale když jsem ani třetí den neslavil úspěch s AR488, podvolil jsem se a zadal ChatGPT pár promptů. A on mi poradil vzít Arduino, k němu přidrátovat převodník TTL/RS232 a přes SoftwareSerial přeposílat data mezi USB portem z PC a RS232 na multimetru s tím, že do toho každých X vteřin navíc ještě odešle příkaz READ?.
Vzali jsme to od nejjednodušší verze přes pár rozšíření - a ve výsledku je kód, který přenáší data oběma směry, na převodníku se dá nastavit pár drobností (zakončení řádku, nastavení četnosti AutoREAD) a v kódu (ano, přímo v kódu, já už to nechtěl dál komplikovat) nastavených pár maker, která umí na požádání odeslat do multimetru, včetně jednoho pro init měření.
Nakonec z něj vypadl docela uvěřitelný kód, já našel potřebný HW, nahrál ho do něj - a světe div se, nechodilo to! Tak jsem si zanadával na všechny ty roboty, počítače, internety a vůbec celej svět, a šel jsem hledat chybu.
A ukázalo se, že ChatGPT věří informacím, které dostává, A že jedna z nich je, že SoftwareSerial na Arduinu Nano je použitelný.
Nevěřte tomu. Není.
Kdo to někdy zkusil, zjistil že teoreticky by všechno fungovat mělo, jenom je to děsně nespolehlivé, neumí to FullDuplex, vypadávají tomu data a celé je to spíš něco mezi pokusem o vtip a o komplikovanou pomstu celému světu.
Ale taky že stačí použít nějakou chytřejší verzi SoftSerialu, a zlepší se to.
Takže jsem použil NeoSWSerial.h, změnil jedinou vstupní deklaraci, a od té doby to jede a měří a funguje a tak dál...
První měření naznačovalo, že menší vlastní šum má ta potvora HP34401A, což mne trochu zamrzelo. Ale pak se ukázalo, že jsem jenom blbej a stačilo "pozapínat matematiku" - plné rozlišení, pomalé měření, průběžný průměr (viz INIT makro níže), a už data vypadají o hodně veseleji. Dost se mi ulevilo.
Ono když si člověk vypne displej, aby se mu zbytečně "neošoupával", tak se dá celkem snadno přehlédnout, že je multimetr v jiném nastavení, než by měl být...
Kdybyste někdo měl cukání a něco podobného potřeboval, tak se to celé skládá z jednoho Arduino Nano V3, k tomu jeden RS232 převodník a čtyři dráty, propojující RX převodníku na D2, TX na D3, zem a +5V na Arduinu (a u mne tedy ještě jeden gender changer, protože mi neseděla pohlaví na RS232 konektorech, ale to je u sériáku takový běžný folklór).
A k tomu už jen kousek kódu, který do toho Arduina nahrajete.
Jednoduché, funkční, rozšiřitelné a pro mne plně dostačující. ChatGPT mi nabízel ještě další rozvoj - editovatelná makra, uložitelná do EEPROM, všechny parametry do EEPROM, logování na připojenou SD kartu atd., ale už jsem to nechtěl komplikovat, je to v podstatě jednoúčelové zařízení a času není nazbyt. (V kódu je správně definované jen makro INIT, dalěí se budou dopisovat až podle potřeby, kterou zatím nemám.)
Takže tady je ten kód, nakládejte s ním dle libosti:
/*
Kod generovan ChatGPT
Upraven nasazenim NeoSWSerial, se standardnim SoftSerial to nejelo.
*/
#include <NeoSWSerial.h>
NeoSWSerial gpibSerial(2, 3); // RX, TX k multimetru
unsigned long lastQueryTime = 0;
unsigned long interval = 1000;
bool autoReadEnabled = true;
String terminator = "\r\n";
String pcCommand = "";
// === DEFINICE MAKER ===
const char* macro_INIT[] = {
"*RST",
"DISP:ENAB 0",
"TERM CRLF",
"*IDN?",
"FUNC \"VOLT:DC\"",
"VOLT:DC:DIGITS 7",
"VOLT:DC:NPLC 10",
"VOLT:DC:AVER:TCON MOV",
"VOLT:DC:AVER:COUN 10",
"VOLT:DC:AVER:STAT ON",
"AUTO ON 4000",
NULL
};
const char* macro_LOGGING[] = {
"*RST",
"DISP:ENAB 0",
"TERM CRLF",
"*IDN?",
"AUTO ON 2000",
NULL
};
const char* macro_MEAS[] = {
"*RST",
"DISP:ENAB 1",
"TERM CRLF",
"*IDN?",
"AUTO ON 2000",
NULL
};
const char* macro_SCAN[] = {
"*RST",
"DISP:ENAB 1",
"TERM CRLF",
"*IDN?",
"AUTO ON 2000",
NULL
};
struct Macro {
const char* name;
const char* const* commands;
};
Macro macros[] = {
{"INIT", macro_INIT},
{"LOGGING", macro_LOGGING},
{"MEAS", macro_MEAS},
{"SCAN", macro_SCAN},
{NULL, NULL}
};
// === SETUP ===
void setup()
{
delay(1000);
Serial.begin(115200);
gpibSerial.begin(19200);
Serial.println("\r\nSCPI Bridge ready. Set multimeter to 19200 Bd.");
Serial.println("Use: AUTO ON [ms], AUTO OFF, TERM CR|LF|CRLF, MACRO <name>, MACROS\r\n");
delay(1000);
runMacro("INIT"); // automaticky po startu
Serial.println();
}
void loop()
{
unsigned long now = millis();
if (autoReadEnabled && (now - lastQueryTime >= interval))
{
sendToMeter("READ?");
lastQueryTime = now;
}
while (Serial.available())
{
char c = Serial.read();
if (c == '\n' || c == '\r')
{
pcCommand.trim();
if (pcCommand.length() > 0)
{ handleCommand(pcCommand); }
pcCommand = "";
}
else
{ pcCommand += c; }
}
while (gpibSerial.available())
{
char c = gpibSerial.read();
Serial.write(c);
}
}
// === ODESLÁNÍ DO MULTIMETRU ===
void sendToMeter(String cmd)
{
gpibSerial.print(cmd);
gpibSerial.print(terminator);
}
// === SPUŠTĚNÍ MAKRA ===
void runMacro(const char* macroName)
{
for (int i = 0; macros[i].name != NULL; i++)
{
if (strcasecmp(macroName, macros[i].name) == 0)
{
Serial.print("[Running macro ");
Serial.print(macros[i].name);
Serial.println("]");
const char* const* cmds = macros[i].commands;
for (int j = 0; cmds[j] != NULL; j++)
{
String line = cmds[j];
handleCommand(line); // důležité! -> rozpozná i AUTO ON atd.
delay(100);
}
Serial.println("[Macro complete]");
return;
}
}
Serial.print("[Macro not found: ");
Serial.print(macroName);
Serial.println("]");
}
void listMacros()
{
Serial.println("[Available macros:]");
for (int i = 0; macros[i].name != NULL; i++)
{
Serial.print(" - ");
Serial.println(macros[i].name);
}
}
// === ZPRACOVÁNÍ PŘÍKAZU Z PC ===
void handleCommand(String cmd)
{
cmd.trim();
if (cmd.equalsIgnoreCase("AUTO OFF"))
{
autoReadEnabled = false;
Serial.println("[AUTO READ disabled]");
}
else if (cmd.startsWith("AUTO ON"))
{
autoReadEnabled = true;
lastQueryTime = millis();
String val = cmd.substring(8);
val.trim();
long newInterval = val.toInt();
if (newInterval >= 100)
{
interval = newInterval;
Serial.print("[AUTO READ enabled, interval ");
Serial.print(interval);
Serial.println(" ms]");
}
else
{ Serial.println("[AUTO READ enabled]"); }
}
else if (cmd.startsWith("TERM "))
{
String mode = cmd.substring(5);
mode.trim();
mode.toUpperCase();
if (mode == "CR")
{
terminator = "\r";
Serial.println("[Terminator set to CR]");
}
else if (mode == "LF")
{
terminator = "\n";
Serial.println("[Terminator set to LF]");
}
else if (mode == "CRLF")
{
terminator = "\r\n";
Serial.println("[Terminator set to CRLF]");
}
else
{ Serial.println("[Unknown terminator. Use TERM CR | LF | CRLF]"); }
}
else if (cmd.startsWith("MACRO "))
{
String macroName = cmd.substring(6);
macroName.trim();
runMacro(macroName.c_str());
}
else if (cmd.equalsIgnoreCase("MACROS"))
{ listMacros(); }
else
{ sendToMeter(cmd); } // defaultně SCPI příkaz
}