Osciloscop, USBTMC și Apple Mac

Am avut o perioadă plină la serviciu și lipsă de timp liber pentru orice proiect personal. Așa se întâmplă mereu după perioada de vară. Totuși am reușit să fac ceva, destul de măreț :D: am finalizat rutina de comunicare USBTMC dintre Mac și osciloscopul Agilent. Pentru cei care nu știu, USBTMC (USB Test & Measurement Class) sau USB488, este un subset al standardului VISA (Virtual Instrument Software Architecture), un API I/O pentru comunicarea dintre computere și instrumentele de test și control. Având un nivel de abstractizare destul de scăzut, comunicarea prin USB488 trebuie implementată la nivel de driver.

În urmă cu aproape un an începeam un proiect ambițios la care am vrut de mult ori să renunț, dar îmi pare bine acum că nu l-am abandonat: să scriu un driver pentru Mac, pentru osciloscopul meu Agilent. Când am achiziționat aparatul nu știam că Agilent nu suportă platforma OS X (sistemul de operare de pe Mac-uri). Chestia asta m-a enervat destul de tare fiindcă aproape toată „infrastructura” mea informatică se bazează pe Apple. Cei de la Agilent (mai nou se numesc Keysight… 😀 ) oferă un suport excepțional pentru Windows și există chiar și un driver pentru Linux, sub licență GNU GPL scris prin 2007 de un neamț, Stefan Kopp. Dar nimic pentru Mac. Nici măcar cei de la National Instruments nu au un driver dedicat. Desigur, există LabView pentru Mac, care instalează și drivere, dar LabView e o platformă uriașă, cu cost prohibitiv. În plus, are o sumedenie de lucruri care nu-mi trebuie — nu am nevoie de tot porcul pentru un singur cârnat.

Dar a scrie un driver fără a avea documentație e ca și cum un orb ar merge pe funie. Pe OS X, driverele funcționează ca extensii ale kernel-ului (nucleul sistemului de operare), orice problemă acolo provocând ceea ce se numește kernel panic. Mai clar, sistemul crapă și trebuie repornit. De pildă celebrele windows blue screen of death erau, de fapt, probleme provocate de drivere. Mai mult, structura kernelului de Mac nu este la fel de simplă ca a celui de Linux, existând ceva provocări de arhitectură pe un microkernel care numai BSD nu este (deși se laudă că e FreeBSD—based). Arhitectura driverelor unui sistem Apple OS X diferă destul de mult de cea a sistemelor Linux și, mai nou, pentru a complica și mai mult lucrurile, Apple solicită semnarea digitală a driverelor pentru a putea fi distribuite oficial (nu că m-ar interesa; doar așa, ca fapt divers).

În al doilea rând, când programezi drivere pentru Mac, nu poți face debugging eficient cu un singur computer. E nevoie de un computer pe care scrii codul propriu-zis și o a doua mașină pe care instalezi driverul; pe cea din urmă pui sistemul într-un mod special de debugging pentru kernel și îi citești mesajele pe mașina de coding, printr-o legătură TCP/IP. Belea. Motivul e de la sine înțeles: când sistemul de debugging semnalizează un registru corupt și intră în bucla de halt, nu mai poți face nimic pe mașina aceea, ai nevoie de încă una pe care să analizezi coada de mesaje.

Pe moment, am creat un driver USB serial care agață osciloscopul, rezervă interfața USB pe o clasă creată de mine, interfață la care mă conectez din aplicație (nu din driver) cu o rutină care împachetează comenzile SCPI într-un wrapper usbtmc de tip REQUEST_DEV_DEP_MSG_OUT și le transmite aparatului care răspunde cu un mesaj DEV_DEP_MSG_IN. Interfața citește apoi mesajul și-l afișează în consolă. Iată cum arată registrul de comunicare și log-ul aplicației, pentru o comandă :POD1:DISPlay 1 (care pornește subsistemul de analiză port paralel):

Agilent Technologies
Pipe ref 1: Bulk OUT
Pipe ref 2: Bulk IN
Pipe ref 3: Interrupt IN
Enter a SCPI command...
USBTMC: usbtmc_write called
USBTMC: Can send remaining bytes in a single transaction...
USBTMC: setup I/O buffer for DEV_DEP_MSG_OUT message...
USBTMC: Instrument command: :POD1:DISPlay 1
USBTMC: Append write buffer (instrument command) to USBTMC message...
USBTMC: Check if this is the last transfer...
USBTMC: n_bytes: 29
USBTMC: this_part: 17
USBTMC: Add zero bytes to achieve 4-byte alignment...
n_bytes: 32
Added 0x00 for:
usbtmc_buffer[29]
Added 0x00 for:
usbtmc_buffer[30]
Added 0x00 for:
usbtmc_buffer[31]
USBTMC: Buffer content is:
usbtmc_buffer[0] = 01
usbtmc_buffer[1] = 01
usbtmc_buffer[2] = FE
usbtmc_buffer[3] = 00
usbtmc_buffer[4] = 11
usbtmc_buffer[5] = 00
usbtmc_buffer[6] = 00
usbtmc_buffer[7] = 00
usbtmc_buffer[8] = 01
usbtmc_buffer[9] = 00
usbtmc_buffer[10] = 00
usbtmc_buffer[11] = 00
usbtmc_buffer[12] = 3A
usbtmc_buffer[13] = 50
usbtmc_buffer[14] = 4F
usbtmc_buffer[15] = 44
usbtmc_buffer[16] = 31
usbtmc_buffer[17] = 3A
usbtmc_buffer[18] = 44
usbtmc_buffer[19] = 49
usbtmc_buffer[20] = 53
usbtmc_buffer[21] = 50
usbtmc_buffer[22] = 6C
usbtmc_buffer[23] = 61
usbtmc_buffer[24] = 79
usbtmc_buffer[25] = 20
usbtmc_buffer[26] = 31
usbtmc_buffer[27] = 0A
usbtmc_buffer[28] = 00
usbtmc_buffer[29] = 00
usbtmc_buffer[30] = 00
usbtmc_buffer[31] = 00
USBTMC: End buffer content.
USBTMC: store bTag (in case we need to abort)...
USBTMC: increment bTag -- and increment again if zero...
USBTMC: Incremented bTag = 2
Program ended with exit code: 0

Mesajul de control începe de la byte-ul 13 (usbtmc_buffer[12]) și se termină cu un carriage return (0x0A — \n) la byte-ul 28 (primul byte se găsește la poziția 0 a buffer-ului — usbtmc_buffer[0] —, de accea numerotarea este decalată cu 1):

...
usbtmc_buffer[12] = 3A
usbtmc_buffer[13] = 50
usbtmc_buffer[14] = 4F
usbtmc_buffer[15] = 44
usbtmc_buffer[16] = 31
usbtmc_buffer[17] = 3A
usbtmc_buffer[18] = 44
usbtmc_buffer[19] = 49
usbtmc_buffer[20] = 53
usbtmc_buffer[21] = 50
usbtmc_buffer[22] = 6C
usbtmc_buffer[23] = 61
usbtmc_buffer[24] = 79
usbtmc_buffer[25] = 20
usbtmc_buffer[26] = 31
usbtmc_buffer[27] = 0A
...

Secvența de control începe întotdeauna de la byte-ul 13. Până la această poziție, primii 12 byte conțin header-ul mesajului REQUEST_DEV_DEP_MSG_OUT. Numărul total de byte trebuie să fie divizibil cu 4. Pentru alinierea la limita de 4 (boundary alignment, boundary padding), ultimii byte se anulează (indiferent de numărul lor; notă: poziția 28 a buffer-ului este byte-ul 29 — vezi mai sus).

...
usbtmc_buffer[27] = 0A --> Carriage return = \n
usbtmc_buffer[28] = 00 --> byte nul
usbtmc_buffer[29] = 00 --> byte nul
usbtmc_buffer[30] = 00 --> byte nul
usbtmc_buffer[31] = 00 --> byte nul; total: 32 bytes -> divizibil cu 4

Pe moment, comunicarea din aplicația–client se face extrem de rudimentar, prin transmiterea unei variabile de tip char* care conține secvența de comandă:

char text[] = ":POD1:DISPlay 1\n";
//char text[] = ":SAVE:IMAGe:FORMat PNG\n";
//char text[] = ":SAVE:IMAGe:STARt somefile.png\n";
//char text[] = ":DISPlay:ANNotation:BACKground OPAQue\n";
//char text[] = ":DISPlay:ANNotation:TEXT 'This is an Agilent DSOX2002A... and I have managed to control it from my Mac... with a custom-developed USBTMC driver...'\n";
//char text[] = "DISPlay:ANNotation:COLor RED\n";
//char text[] = "DISPlay:VECTors 0\n";

uWrite(text, sizeof(text));

Iată, de pildă, rezultatul unei succesiuni de comenzi:

Rezultatul unei secvențe de comenzi SCPI transmise unui osciloscop Agilent DSOX 2002A, prin driverul USBTMC și aplicația client: :POD1:DISPlay 1, :DISPlay:ANNotation:BACKground OPAQue, :DISPlay:ANNotation:TEXT, DISPlay:ANNotation:COLor RED.

Rezultatul unei secvențe de comenzi SCPI transmise unui osciloscop Agilent DSOX 2002A, prin driverul USBTMC și aplicația client: :POD1:DISPlay 1, :DISPlay:ANNotation:BACKground OPAQue, :DISPlay:ANNotation:TEXT, DISPlay:ANNotation:COLor RED.” width=”630″ height=”378″ class=”size-medium wp-image-490

În concluzie, toată această muncă a fost un mic coșmar. Dar am avut prezența de spirit să nu insist prea mult atunci când rezultatele se lăsau așteptate. În ciuda funcționalităților limitate ale acestui program, simplul fapt că am reușit să implementez un protocol de comunicare pe o platformă pentru care nu există nici un proiect similar (și nici documentație) este o realizare. Satisfacția e mare. Deschide posibilități nelimitate pentru portarea funcționalităților SCPI pe o platformă cu totul nouă și proiecte open–source pentru întreaga comunitate a celor interesați. Urmează să rafinez aplicația client și să implementez și rutina de citire. Apoi trec la nivelul următor — portarea pe un driver pur serial, care să implementeze în kernel punctele de intrate în /dev, tty.* respectiv cu.* Mai am de analizat posibilitatea de a prelucra pachetele de date prin USB lucru care, dacă ar fi posibil, ar permite realizarea unei aplicații care să afișeze în timp real ecranul osciloscopului. Mai mult, fiind un driver care implementează protocolul usb488, pot controla de pe Mac orice aparat care implementează protocolul de comunicare, deci inclusiv analizorul de spectru DSA 815TG. În plus, cunoștiințele acumulate despre USB și protocoalele de comunicare și aspectele specifice sistemului de operare de la Apple, mă tentează să încerc portarea pe Mac a aplicației de control a stației mele Yaesu FT-60D. Când am cumpărat-o la Friedrichshafen, am luat și cablul de control care venea la pachet cu un CD cu driverele și aplicația de control pentru Windows.

ADMS-J1 Kit programare Yaesu FT-60. Cablul este roșu și sexy. :D Dar este o interfață USB standard.

ADMS-J1 Kit programare Yaesu FT-60. Cablul CT57A este roșu și sexy. Dar este o interfață USB standard. Nimic special. Specială este doar mufa jack de 3.5mm (are patru secțiuni de contact).

Prețul mi s-a părut porcesc, precum și faptul că aplicația are licență, nu poate fi instalată gratuit. Dar cablul e roșu și sexy și mi-a plăcut. În sine e un device USB standard care este montat de sistem ca unitate standard EHCI (Extensible Host Controller Interface). De aici până la crearea unui driver care să sprijine comunicarea serială (prin /dev) sau USB prin pipe bulk, nu mai este decât un pas:

Yaesu ADMS-J1 — IORegistryExplorer, o aplicație pentru Mac cu care se pot analiza toate obiectele I/O provenite din instanțierea diverselor subclase I/O Kit: cablul este montat ca unitate USB EHCI (Extensible Host Controller Interface).

Yaesu ADMS-J1 — IORegistryExplorer, o aplicație pentru Mac cu care se pot analiza toate obiectele I/O, instanțe I/O Kit: cablul CT57A este văzut de sistem ca unitate USB EHCI (Extensible Host Controller Interface).

Yaesu ADMS-J1 — IORegistryExplorer -> descriptorii interfeței cablului de programare CT57A. Se observă perechea de descriptori 0x2100/0x9e51 (idVendor / idProduct) utilizatp în cod pentru identificarea unică a dispozitivului (împreună cu bcdDevice).

Yaesu ADMS-J1 — IORegistryExplorer -> descriptorii interfeței cablului de programare CT57A. Se observă perechea de descriptori 0x2100/0x9e51 (idVendor / idProduct) utilizatp în cod pentru identificarea unică a dispozitivului (împreună cu bcdDevice).

Yaesu ADMS-J1 — IOUSBInterfaceUserClientV3, clasa driver care asigură accesul la dispozitiv de la nivelul aplicației client, așa cum este afișată în IORegistryExplorer. Dacă este expusă ca punct de intrare în /dev, va utiliza parametrul locationID pentru a da o denumire POSIX-compatibilă port-ului serial (de pildă /dev/tty.fd120000).

Yaesu ADMS-J1 — IOUSBInterfaceUserClientV3, clasa driver care asigură accesul la dispozitiv de la nivelul aplicației client, așa cum este afișată în IORegistryExplorer. Dacă este expusă ca punct de intrare în /dev, va utiliza parametrul locationID pentru a da o denumire POSIX-compatibilă port-ului serial (de pildă /dev/tty.fd120000).

Pentru mine a fost un proiect fascinant. Un proiect software presărat cu foarte multe eșecuri, hacking, reverse engineering prin USB, citit o tonă de documentație (doar documentația USB are cam o mie de pagini), mii de încercări (am ajuns la build 2.783) și mii de crash-uri. Ajunsesem să cred că voi fi nevoit să-mi cumpăr un Mac nou 😀 . Dar am învățat o sumedenie de lucruri. Majoritatea vor face subiectul unor articole pe care le voi scrie pe alauda.ro, blog-ul de aici nefiind, totuși, orientat pe programare. Dar mi s-a părut utilă adăugara acestui articol pe yo3iti.ro dată fiind importanța subiectului în electronică.

Despre YO3ITI

Iubitor de pisici, muzică, foto. Măritat.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *