Moja skúsenosť s portovaním knižnice na Arduino

Portovať nízkoúrovňovú knižnicu na novú platformu nie je jednoduché. Poďme sa pozrieť, ako som postupoval ja.

2 years ago   •   22 min read

By Vladimír Záhradník
Zdroj: andreaurpi8, Pixabay

Portovanie softvéru na nové architektúry alebo platformy ma vždy lákalo. Predstavte si, že softvér, ktorý nebol pre vaše zariadenie napísaný, na ňom zrazu beží. Vždy som obdivoval ľudí, ktorí vyvíjajú emulátory pre staršie (ale aj nové) hracie konzoly, robia reverzné inžiniérstvo, prípadne napíšu softvér na konzolu bez prístupu k devkitu.

Špecifiká Arduino platformy

Moja práca bola o poznanie jednoduchšia, ale napriek tomu som sa veľa naučil. Mal som za úlohu rozbehať C-čkovskú knižnicu, ktorá bola písaná pre platformy Windows, Linux a macOS, na platforme Arduino. Autori knižnice spomínali, že portovanie na nové platformy, vrátane mikrokontrolérov, by nemalo byť zložité, avšak Arduino je dosť špecifické. Predovšetkým ide o to, že Arduino nepredstavuje len jednu čipovú architektúru. Je to framework, s ktorým sú kompatibilné viaceré vývojové dosky postavené na odlišných architektúrach — MIPS, ARM, RISC. Zároveň autori definovali v tomto frameworku základné funkcie, ktoré musíte vo vašich programoch použiť. Inak sa vám nepodarí ani vypísať do konzoly „Ahoj, svet.“ V neposlednom rade, tieto čipy sú relatívne málo výkonné, majú sotva 2 kB pamäte, ale zaujímavé sú pre nás preto, že umožňujú programovať aplikácie bežiace v reálnom čase.

Mikrokontrolér nám vie garantovať, že sa kritická úloha bude vykonávať pravidelne, napríklad cez prerušenia. Raspberry Pi môže byť oveľa výkonnejšie, ale aplikácie vyžadujúce beh v reálnom čase dobre nepodporuje. O tom som sa presvedčil už keď sme na ňom robili stopky pre hasičské súťaže a ukázalo sa, že RPi nie je schopné prijímať stabilný tok impulzov. Niektoré skrátka nezachytilo, pretože pod Linuxom práve bežala na zlomok sekundy iná úloha, a prepočet z impulzov na otáčky motora nám kolísal rádovo o plus/mínus 100 otáčok. Mikrokontrolér nemá nad sebou žiadny operačný systém, váš kód beží priamo na ňom. Preto máte oveľa väčšiu kontrolu, ako zabezpečiť beh kódu kritického na čas.

Real-time operačné systémy

Samozrejme, existujú aj real-time operačné systémy. Tie sú upravené na beh úloh kritických na čas, pričom sa pre nich programuje jednoduchšie. Máte viac pamäte, dospelejší systém, výkonnejší procesor a plnohodnotnú implementáciu jazyka C/C++, prípadne aj iných ako Rust či Python/MicroPython. Pre Raspberry Pi som však nič aktuálne nenašiel. Vraj by na ňom mal bežať FreeRTOS od Amazonu, ale informácie sú ešte pre RPi2. Na najnovšom RPi4 teda nemám žiadnu záruku. Potom je tu projekt, ktorý ma osobne zaujíma už dlhšie, Zephyr Project. Ten však Raspberry Pi nemá v zozname podporovaných platforiem. Takže to zhrniem, programovanie aplikácií pre real-time operačné systémy má zmysel preskúmať hlbšie a rozhodne sa na to pozriem.

SimpleMotionV2

Potrebujem rozbehať komunikáciu s driverom, ktorý ovláda servomotor. Je dôležité, aby komunikácia prebiehala veľmi rýchlo, niekoľko sto krát za sekundu. Potrebujem čítať reálne údaje z motora a korigovať pohyb motora, ak vyhodnotím, že pohyb sa vychyľuje z normy. V našom projekte používame driver IONI od firmy Granite Devices. Komunikácia prebieha po zbernici RS485, pričom firma vyvinula vlastný komunikačný protokol, SimpleMotion. RS485 totiž definuje len fyzické parametre prenosovej linky a je na vás, ako budete informácie prenášať.

Firma dáva k produktu IONI aj nástroj Granity, pomocou ktorého viete nastaviť parametre driveru. Avšak ten beží len na niektorých platformách, a hlavne v našej aplikácii, kde potrebujeme čítať dáta v reálnom čase veľmi často, je nepoužiteľný. Šiel som teda viac do hĺbky. Knižnica pre protokol SimpleMotion je k dispozícii ako open-source na ich GitHube.

Skôr než začneme

Kým začneme portovať knižnicu na novú platformu, potrebujeme dať dokopy čo najviac informácií. V mojom prípade som si naštudoval, ako funguje RS485 zbernica. Ďalej som si pozrel všetko, čo som našiel, k driveru IONI a preštudoval som si celú wiki k protokolu SimpleMotion. Nebolo toho však veľa. Informácie sú strohé a neúplné. Dozviete sa, že do driveru môžete zapisovať príkazy dávkovo do buffera a že si ich bude spúšťať sám, čím sa vyhnete potrebe realtime hardvéru, a druhou možnosťou je práve tá, že príkazy zapisujeme priebežne a to veľmi rýchlo. Podľa wiki je horný limit až nejakých 20 000 príkazov za sekundu.

Wiki sa odkazuje na zopár príkladov, ako takúto komunikáciu rozbehať. No musím hneď dodať, že už päť rokov ich nikto neaktualizoval a u väčšiny existujúcich príkladov je komentár, že nič nerobia.

Nefungujúce príklady

Najlepší zdroj informácií mi paradoxne poskytol zdrojový kód knižnice, a to nie je bohvie čo. V jej repozitári som našiel podpriečinok doc, kde bol uvedený popis protokolu. Zjednodušene, potom čo otvoríte RS485 linku, komunikujete so zariadeniami asymetricky podľa modelu master/slave. Váš počítač, respektíve mikrokontrolér, je master. Komunikuje s jedným, alebo viacerými slave zariadeniami. Všetky zariadenia sú pripojené na tú istú zbernicu a každé zariadenie má priradenú jedinečnú adresu uzla. Vy, ako master, viete parametre čítať alebo zapisovať. V oboch prípadoch potrebujete vedieť názov parametra a adresu uzla.

Všetky parametre sú zadefinované na wiki stránke protokolu a takisto v hlavičkovom súbore knižnice.

Súbor Readme nám dáva užitočné informácie k portovaniu. Uvádza zoznam povinných súborov, ktoré musia byť skompilované, a tiež voliteľné súbory, ktoré pridávajú nejakú funkčnosť, ale nie sú kritické. Takisto v časti „Porting to new platform“ nachádzame základné inštrukcie, čo treba spraviť.

Pre účely tohto článku sem tieto poznatky uvediem. Neskôr sa na ne budem odvolávať.

Nutné súbory

  • simplemotion.c/.h - Jadro celej knižnice. Definuje API, ktoré volajú používatelia z ich kódu, napríklad funkciu smOpenBus( ... ), ktorá inicializuje komunikáciu, alebo funkciu smRead1Parameter( ... ), ktorá prečíta zo slave zariadenia nejakú hodnotu
  • sm485.h - Definície príkazov pre komunikáciu po RS485
  • sm_consts.c - Predpočítané CRC tabuľky
  • simplemotion_defs.h - Parametre, ktoré môžeme poslať slave zariadeniam
  • simplemotion_private.h - Definícia interných štruktúr a parametrov, ktoré sa používajú len vnútri knižnice a používateľ o nich ani nemá vedieť
  • busdevice.c/.h - Zabezpečuje komunikáciu po RS485 zbernici

Nepovinné súbory

  • bufferedmotion.c/.h - Kód, ktorý pridáva možnosť posielať príkazy do driveru v dávkach. Driver si ich uloží do zásobníka a postupne ich vykonáva
  • devicedeployment.c/.h - Kód, ktorý umožňuje do driveru nahrať firmvér, prípadne poslať doňho nastavenia
  • súbory v priečinku drivers/ - Ovládače pre základné platformy, ktoré implementujú funkčnosť vyžadovanú kódom v busdevice.h a zjednodušujú zostavenie RS485 komunikácie

Ešte dodám, že celá knižnica je napísaná v jazyku C. Neskôr sa to ukáže ako dosť dôležitý fakt.

Začíname portovať

Či už portujete knižnicu, ako je tá moja, alebo nejaký zložitý herný engine, základný postup sa až tak nelíši. V prvom kroku potrebujete dať dokopy čo najviac informácií o zdrojovej platforme, cieľovej platforme a ideálne aj ohľadom architektúry a štruktúry kódu projektu, ktorý sa snažíte portovať. Všetko potrebné som zhrnul v bode vyššie.

V ďalšom kroku som si repozitár so zdrojovými kódmi sklonoval do svojho počítača:

git clone git@github.com:GraniteDevices/SimpleMotionV2.git
Cloning into 'SimpleMotionV2'...
remote: Enumerating objects: 162, done.
remote: Counting objects: 100% (162/162), done.
remote: Compressing objects: 100% (113/113), done.
remote: Total 1316 (delta 95), reused 103 (delta 49), pack-reused 1154
Receiving objects: 100% (1316/1316), 2.03 MiB | 3.55 MiB/s, done.
Resolving deltas: 100% (807/807), done.

Keďže môj počítač je medzi podporovanými platformami, pokúsil som sa knižnicu skompilovať. Chcel som vidieť, aký bude výstup, prípadne aké chyby vyhodí kompilátor. Autori majú rozbehanú automatizáciu cez Travis CI. Stačilo sa mi pozrieť na súbor .travis.yml a našiel som, čo som potreboval. Knižnica má pripravený Makefile a dokonca aj testy. Zostavíme ju jednoducho:

make

A testy (aj keď ich nie je veľa) spustíme takto:

make test

Na zdrojovej platforme sa knižnica úspešne skompilovala. Hoci kompilátor vyhodil nejaké chyby, podľa komentárov v kóde usudzujem, že sú očakávané.

...
drivers/tcpip/tcpclient.c:255:16: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
  255 |     int sockfd=(int)busdevicePointer;//compiler warning expected here on 64 bit compilation but it's ok
      |

Zostavenie knižnice na cieľovej platforme

Toto by mal byť váš prvý cieľ. Očakáva sa, že knižnicu na novej platforme neskompilujete. Knižnica, ktorá by sa skompilovala bez akýchkoľvek úprav, je zrejme dosť jednoduchá. Arduino IDE, vývojové prostredie pre Arduino, nepoužíva žiadne Makefile súbory. Ak vytvoríte v Arduino IDE sketch (projektový súbor s príponou .ino), tento súbor prejde niekoľkými fázami, kým sa zostaví. Najprv sa predspracuje a transformuje sa na plnohodnotný C++ kód. Arduino preprocesor doňho doplní chýbajúce #include riadky a zahrnie doňho obsah hlavičkových súborov, predovšetkým Arduino.h. Takisto automaticky vytvorí deklarácie funkcií, ktoré máte definované vo vašom projekte. Následne sa snaží dohľadať potrebné závislosti (knižnice) pre váš projekt, vybrať cieľovú architektúru podľa typu vývojovej dosky a v poslednom kroku všetky tieto informácie predá kompilátoru. Ten sa pokúsi kód zostaviť a ak uspeje, firmvér sa nahrá do mikrokontroléra.

Z toho, ako funguje zostavovanie kódu na Arduino, vyplýva, že knižnicu nedokážeme zostaviť priamo. Potrebujeme vytvoriť nejaký jednoduchý sketch, ktorý bude volať funkciu z tejto knižnice. Môže to byť hocičo. Arduino IDE nájde v projekte závislosť na vašu knižnicu a pokúsi sa ju zostaviť. Pravdepodobne v tomto kroku zlyhá, tak ako u mňa.

Knižnicu samozrejme ešte nemáte v registri Arduino knižníc. Arduino vám umožňuje nainštalovať cez IDE .zip archív s kódom knižnice, alebo si viete knižnicu nakopírovať sami. Arduino IDE hľadá knižnice v konkrétnych priečinkoch, v mojom prípade:

[vzahradnik@EliteBook ~]$ ls -la Arduino/libraries/
total 4
drwxr-xr-x 1 vzahradnik vzahradnik  86 jul 25 23:37 .
drwxr-xr-x 1 vzahradnik vzahradnik  58 jul 24 18:38 ..
drwxr-xr-x 1 vzahradnik vzahradnik 234 jul 24 16:06 CONTROLLINO
-rw-r--r-- 1 vzahradnik vzahradnik  86 jul 24 16:05 readme.txt
drwxr-xr-x 1 vzahradnik vzahradnik 146 jul 26 11:16 SimpleMotionV2-Arduino
[vzahradnik@EliteBook ~]$

Je úplne jedno, ako pomenujete priečinok s knižnicou. Arduino IDE hľadá hlavičkové súbory vo všetkých týchto priečinkoch.

Môj úplne prvý Arduino sketch vyzeral nejako takto:

Prvé zostavenie knižnice na Arduino IDE


Prekvapivo, tých chýb bolo oveľa menej ako som čakal. Chybu s includom som vyriešil jednoducho:

diff --git a/devicedeployment.c b/devicedeployment.c
index 0eb954f..2995a41 100755
--- a/devicedeployment.c
+++ b/devicedeployment.c
@@ -1,6 +1,6 @@
 #include "devicedeployment.h"
 #include "user_options.h"
-#include "utils/crc.h"
+#include "crc.h"^M
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>

Zablokovanie funkčnosti, ktorú nie je možné portovať

Prekvapivo po tejto úprave sa knižnica zostavila. Rozhodne to však neznamená, že je aj funkčná. Arduino IDE pravdepodobne kompiluje len to, čo musí. V sketchi includujem súbor simplemotion.h, niekde v ňom sa includuje devicedeployment.h a ten includuje crc.h. Includy teda zbehnú pre všetky súbory naviazané na ten, ktorý zahrňujete v projekte. Avšak skompiluje sa len tá funkčnosť, ktorú používate. Ja som používal jedinú funkciu, smGetVersion(), ktorá na žiadnej inej nezávisela, a preto kód zbehol. Napriek nedokonalostiam je to dobrá začiatočná pozícia. Súbor simplemotion.h totiž includuje aj hlavičkové súbory ako tieto:

...
#include <stdio.h>
#include <stdint.h>
...

Prekvapilo ma, že tieto súbory pre moju architektúru našiel. Arduino framework tieto súbory neimplementuje, takže pravdepodobne budú súčasťou kompilátora GCC pre architektúru AVR, ktorú používam. Neskôr som túto domnienku aj potvrdil. Paralelne s Arduino IDE mám rozbehané aj PlatformIO, ktoré si sťahuje závislosti, ako napríklad tento kompilátor, do priečinka .platformio/packages. Priamo v ňom som našiel priečinok toolchain-atmelavr, obsahujúci hlavičkové súbory poskytované kompilátorom, a tiež priečinok framework-arduino-avr, poskytujúci hlavičkové súbory Arduino frameworku. Možnosť pozrieť si tieto súbory a hľadať v nich definície funkcií či C++ tried sa ukázala ako veľmi výhodná.

Môj kompilátor implementuje väčšinu štandardnej knižnice jazyka C, preto kompilácia prebehla. No v knižnici SimpleMotion sú aj volania, ktoré vyžadujú smerník na súbor a podobne. Prirodzene, na mikrokontroléri so súbormi pracovať nemôžeme. Kód sa síce zostaví, ale to neznamená, že aj funguje. V ďalšom kroku som sa rozhodol zmapovať, aké funkcie zo štandardnej knižnice C SimpleMotion vlastne volá. Vo všetkých súboroch som teda zakomentoval include riadky a pozoroval, na čom padne kompilácia. Kompilácia mi vygenerovala asi cez 2000 riadkov chýb, ale väčšina sa opakovala. Prehľadne som si zmapoval, aké funkcie knižnica používa a určil som tie, ktoré potrebujem nahradiť za nejaký funkčný Arduino variant.

...
# stdio.h
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.h:106:52: error: unknown type name 'FILE'
  106 | LIB void smSetDebugOutput( smVerbosityLevel level, FILE *stream );
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.c:1131:18: warning: incompatible implicit declaration of built-in function 'snprintf'

/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.c:111:17: warning: implicit declaration of function 'fprintf' [-Wimplicit-function-declaration]
  111 |                 fprintf(smDebugOut,"%s: %s",smBus[handle].busDeviceName, buffer);
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:858:7: warning: implicit declaration of function 'fopen' [-Wimplicit-function-declaration]
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:865:5: warning: implicit declaration of function 'fseek' [-Wimplicit-function-declaration]
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:866:16: warning: implicit declaration of function 'ftell' [-Wimplicit-function-declaration]
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:877:9: warning: implicit declaration of function 'fclose' [-Wimplicit-function-declaration]
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:885:15: warning: implicit declaration of function 'fread' [-Wimplicit-function-declaration]

# stddef.h
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.h:199:39: error: unknown type name 'size_t'
  199 | LIB int smDescribeSmStatus(char* str, size_t size, SM_STATUS status);
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.c:21:18: error: 'NULL' undeclared here (not in a function)
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:840:13: warning: implicit declaration of function 'strcpy' [-Wimplicit-function-declaration]

# stdarg.h
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.c: In function 'smDebug':
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.c:95:5: error: unknown type name 'va_list'

# string.h
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.c: In function 'smOpenBus':
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.c:248:5: warning: implicit declaration of function 'strncpy' [-Wimplicit-function-declaration]
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/simplemotion.c:967:2: warning: implicit declaration of function 'memcpy' [-Wimplicit-function-declaration]

/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:97:13: warning: incompatible implicit declaration of built-in function 'strlen'
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:101:6: warning: implicit declaration of function 'strncmp'
/home/vzahradnik/Arduino/libraries/SimpleMotionV2/devicedeployment.c:288:5: warning: incompatible implicit declaration of built-in function 'sprintf'
...

Už na prvý pohľad vidím, že so súbormi pracuje len kód na nahrávanie firmvéru do driverov, devicedeployment.c. Je to logické, odniekiaľ tie dáta potrebuje čítať. Táto funkčnosť však nie je kritická, je uvedená len ako voliteľná, a preto som ju zablokoval. Využil som na to direktívy preprocesora. Keď kompilujete knižnicu pre Arduino, niekde sa zadefinuje flag ARDUINO. Následne si môžem kontrolovať, či beží kód v tomto prostredí. Robí sa to takto:

#ifdef ARDUINO
// Veci špecifické pre Arduino
#endif

Rozhodol som sa pridať túto kontrolu hneď na začiatok hlavičky. Takto zabezpečím, že na Arduine sa vytvorí „prázdny“ objektový súbor. Tým, že sa kód z hlavičky vyhodí, nebude jeho API dostupné z knižnice. Podobne musíme pridať klauzulu aj do C-čkovského súboru, ktorý sa snaží implementovať funkcie vyhodené z kódu.

#ifndef SMDEPLOYMENTTOOL_H
#define SMDEPLOYMENTTOOL_H

// Zakáž kompiláciu na Arduino doskách
#ifndef ARDUINO

...

#endif // ARDUINO
#endif // SMDEPLOYMENTTOOL_H

Čo je a čo nie je možné portovať?

Kód som analyzoval a priebežne upravoval pre Arduino. S výnimkou kódu, ktorý chcel príkazy čítať zo súboru, sa zvyšok dal nejako upraviť. Najdôležitejšie body sú tieto:

  • Funkčnosť pre komunikáciu po zbernici definovaná v súbore busdevice.h
  • Funkcia void smDebug( ... ) zapisuje logy do súboru. Ja som ju prerobil, aby na Arduine zapisovala do konzoly. Áno, je to pomalé, ale je to len na ladenie.

Na oba prípady sa teraz podrobne pozrieme.

Komunikácia so zbernicou RS485 (busdevice.h)

Tento modul je základom komunikácie s ostatnými zariadeniami protokolom SimpleMotion. Definuje štyri callback funkcie a ak ich implementujete pre vaše zariadenie, mala by komunikácia začať fungovať. Zvyšok kódu v busdevice.h/.c je už platformovo nezávislý. Tieto štyri funkcie zabezpečujú otvorenie a zatvorenie komunikačného kanála, čítanie dát zo zbernice a posielanie dát po zbernici. Je tam ešte jedna funkcia, ktorá slúži napríklad na to, že sa flushne RX/TX buffer.

Autori tejto knižnice zároveň pripravili ovládače pre najbežnejšie platformy. Tie zabezpečia komunikáciu po RS485 cez sériovú linku, prípadne cez USB prevodník s čipom FTDI. Sú platformovo špecifické, fungujú pod Windowsom, Linuxom, aj macOS. Štandardne sú tieto ovládače zakázané a používateľ ich musí povoliť pri kompilácii flagom ENABLE_BUILT_IN_DRIVERS. V takom prípade si používateľ zavolá funkciu smOpenBus( const char * devicename ), kde jej zadá ako parameter názov portu (povedzme /dev/ttyS0). Kód sa postupne snaží volať jednotlivé ovládače a pokiaľ niektorý z nich nevráti pri inicializácii chybu, komunikácia sa považuje za úspešnú. Ovládače priamo v sebe implementujú tieto štyri základné callback funkcie.

Alternatívou je implementovať tieto callback funkcie priamo vo vašom kóde. V takom prípade zavoláte vo vašom kóde funkciu smOpenBusWithCallbacks( ... ), ktorej definujete pomocou parametrov referencie na callback funkcie. Aj keď je tento spôsob funkčný a úspešne som ho otestoval s mojím Controllinom, keďže robíme Arduino knižnicu, môžeme si dovoliť začleniť kód na inicializáciu zbernice priamo do knižnice v podobe nového ovládača. Zatiaľ som implementoval jednoduchý ovládač pre priemyslové Controllino Maxi/Mega, ktoré práve používam a tiež pre ESP32 Grey od M5Stack.

Ak sa nám doteraz knižnicu darilo úspešne skompilovať, po povolení flagu ENABLE_BUILT_IN_DRIVERS sa to zmení. Zrazu sa začnú kompilovať všetky ovládače z priečinka drivers a kým kompilátor poskytoval niektoré základné hlavičkové súbory štandardného C-čka, u ovládačov už narazíte. Pracujú napríklad so socketmi, majú volania na pthread, a tak ďalej. Najprv som skúšal upraviť kód pomocou #ifdef ARDUINO #endif blokov, len aby kód skompiloval. Ukázalo sa, že kód je už taký špecifický, že to nemá cenu. Nakoniec som spravil niečo podobné ako pri súbore devicedeployment.h vyššie a zablokoval som všetok existujúci kód pre každý ovládač.

#ifndef SM_D2XX_H
#define SM_D2XX_H

#ifndef ARDUINO
// Pôvodný kód
#endif // ARDUINO
#endif

Po tejto úprave už kompilácia opäť bežala. Mohol som sa pustiť do vytvorenia ovládača pre RS485 komunikáciu pre Controllino a ESP32 Grey. Ako som už napísal, základom je implementovať zopár funkcií, ktoré si knižnica zavolá. Obe dosky majú ukážkové príklady pre RS485 komunikáciu. Rozbehať zbernicu sa dá už pár riadkami kódu. Oba ovládače sú si veľmi podobné, fungovanie si vysvetlíme na ovládači pre Controllino. Pre predstavu, takto jednoducho zabezpečíme čítanie dát:

smint32 controllinoRs485PortRead(smBusdevicePointer busdevicePointer, smuint8 *buf, smint32 size)
{
    smint32 n = 0;

    // Čítaj iba keď sú dostupné dáta
    if (Serial3.available() > 0) {
          n = Serial3.readBytes(buf, size);

    }

    return n;
}

Oproti ostatným ovládačom je tento kód minimalistický. Všetko za nás rieši Arduino framework. Podobne som implementoval aj ostatné funkcie. Spomeniem však jednu vec, ktorá sa ukázala ako problémová. Arduino mnohé veci implementuje ako C++ objekty (všimnite is objekt Serial3). Keďže je knižnica písaná v C-čku, narazil som na problémy s interoperabilitou medzi C a C++. Tieto problémy preberieme v samostatnej časti.

Logovanie

Logovanie je implementované pomocou internej funkcie void smDebug( ... ). Tá zapisuje logy do súboru, čo v prípade Arduina nie je možné. Všetky volania funkcie printf som nahradil priamym zápisom na konzolu cez objekt Serial. Ten je dostupný v celom Arduino projekte, stačí zahrnúť knižnicu Arduino.h. Úprava vyzerala nejako takto:

...
if(smIsHandleOpen(handle)==smtrue)
{
    #ifdef ARDUINO
    Serial.print(smBus[handle].busDeviceName);
    Serial.print(": ");
    Serial.print(buffer);
    #else
    fprintf(smDebugOut,"%s: %s",smBus[handle].busDeviceName, buffer);
    #endif
...
}

Všimnite si, že jedno volanie fprintf(...) som nahradil niekoľkými volaniami funkcie print(...). Tá neumožňuje špecifikovať formát a spojiť viacero premenných do jedného reťazca. Rozmýšľal som, že by som jej alokoval zásobník, do ktorého by som zapisoval hotové reťazce pomocou funkcie snprintf(...), nakoniec som sa však rozhodol šetriť pamäť. Nie je jej až tak veľa a rýchlosti by to tiež veľmi nepomohlo. Výpis na konzolu je veľmi pomalý a je skutočne vhodný len počas ladenia programu. V produkčnom programe kompiláciu výpisov zakážete vymazaním flagu ENABLE_DEBUG_PRINTS. To môžete spraviť aj priamo vo vašom Arduino sketchi, a to takto:

#undef ENABLE_DEBUG_PRINTS

Poďme sa teraz pozrieť na viaceré problémy, ktoré som riešil dlhé hodiny. Poukazujú na to, aký je občas vývoj zradný.

Zostavovacie prostredie Arduino IDE

Ako som už spomenul, Arduino IDE robí mnoho vecí automaticky. Váš kód upraví, zistí si závislosti a zostaví binárny firmvér, ktorý nahrá na vývojovú dosku. U jednoduchých projektov to funguje celkom dobre, ale ak použijete knižnicu, ako je tá moja, narazíte na viaceré problémy.

Ten najčastejší bol problém linkera.

Knižnica sa nezostaví

Tento problém som riešil aspoň hodinu. Súbory som v priečinku mal a kód ich volal správne, tak prečo ich Arduino linker nevie nájsť? Asi to súvisí s ich špecifikáciou knižníc. Kedysi sa kód Arduino knižníc ukladal priamo do koreňa repozitára. Keď nájde Arduino takto organizovaný kód, automaticky predpokladá, že všetky C-čkovské súbory sú v tej istej hierarchii, kód v podpriečinkoch neskompiluje vôbec. Riešením bolo preorganizovať kód tak, aby zodpovedal novšej špecifikácii. Tá očakáva kód knižnice v priečinku src a tiež definovaný manifest s popisom knižnice v koreni repozitára. Po týchto jednoduchých úpravách už bol kód skompilovaný.

Výpis na sériovú linku nefunguje

Na tento problém som opäť narazil pri kompilácii projektu cez Arduino IDE. Keď skompilujem sketch a zahrniem doňho moju knižnicu, nefunguje výpis na sériovú linku. Dlho som skúmal, či nemám v kóde niečo, čo by narušilo základnú funkčnosť výpisu Arduino frameworku, ale nič som nenašiel. Predpokladám, že opäť len Arduino IDE niečo do kompilácie nezahrnulo. Rovnaký projekt som vyskúšal aj v prostredí PlatformIO, kde výpis na sériovú linku funguje bezchybne. Osobne si myslím, že pri portovaní je dobré sa vyhnúť čo najviac problémom a preto odporúčam stabilné prostredie, kde máte zostavovanie viac pod kontrolou. Knižnicu by ste mali odladiť pre Arduino IDE až úplne na záver, keď už viete, že kód funguje.

Interoperabilita medzi C a C++ kódom

Knižnica SimpleMotion bola napísaná v čistom C-čku. Neobsahuje žiadne veci z C++. Ja som sa snažil do knižnice zasahovať čo najmenej, aby sa moje zmeny dali jednoducho upraviť pri začleňovaní nového kódu z upstreamu. Chcel som zachovať možnosť knižnicu zostaviť na existujúcich platformách, akurát s tým, že medzi podporované platformy pribudne aj Arduino.

Lekcia, ktorú som sa naučil veľmi tvrdo, znie:

Ak zdrojový súbor má príponu .c, kompiluje sa v jazyku C a ak má príponu .cpp, kompiluje sa v jazyku C++.

Myslel som si, že prípona súboru je nepodstatná a že Arduino automaticky používa kompilátor pripravený zostaviť C++ kód. Avšak mýlil som sa. Aj tento kompilátor totiž rozlišuje, čo kompiluje. Keďže môj kód sa kompiloval ako C-čkovský, dostával som zvláštne chyby, ako napríklad neznáme kľúčové slovo class. Všetky C++ objekty, ktoré Arduino používa, tak nemôžem v režime jazyka C vôbec použiť. Riešením je premenovať koncovku súboru z .c na .cpp, ale tým sa ukázali ďalšie problémy. Keď sa súbor kompiluje ako C++, veci, ktoré sú v C-čku ešte povolené, zrazu v C++ vyhodia v lepšom prípade upozornenie, v horšom rovno chybu.

.pio/libdeps/uno/SimpleMotionV2-Arduino/src/simplemotion.cpp:1107:258: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
         smDebug(handle,SMDebugLow,"Previous SM call failed and changed the SM_STATUS value obtainable with getCumulativeStatus(). Status before failure was %d, and new error flag valued %d has been now set.\n",(int)smBus[handle].cumulativeSmStatus,(int)stat);

Konkrétne túto chybu som vyriešil tak, že som upravil deklaráciu funkcie, aby očakávala ako parameter reťazcovú konštantu char const *. Avšak povedal som si, že najlepšie bude, ak všetok Arduino C++ kód oddelím do samostatného modulu, ktorého API sa dá volať aj z C-čkovského kódu. Vytvoril som modul arduino_helper.h/.cpp a umiestnil som ho priamo k ovládačom pre Controllino a pre M5stack. Celkovo sa moje zmeny v knižnici dosť sprehľadnili. Pôvodný súbor simplemotion.c má minimum zmien a všetko dôležité sa deje v tomto module.

Problémy s linkerom a kľúčové slovo extern

Myslel som si, že viem, čo kľúčové slovo extern robí. Po hodinách ladenia som si však uvedomil, že moje vedomosti ani zďaleka nestačili. Teraz mu už však rozumiem dobre.

Toto kľúčové slovo má tri významy: Prvý význam: Ak ho uvediete u premennej, sprístupníte ju aj ostatným modulom. Zvyčajne takéto premenné umiestníte do hlavičkového súboru, aby ich mali ostatné moduly jednoducho dostupné.

Príklad:

/* Hlavička main.h */
extern int variable;

/* Hlavný modul */
#include <stdio.h>
#include "main.h"

int main() {
    variable = 0;
}

/* Ďalší modul */
#include "main.h"

void someFunction() {
    variable = 1;
}


Štandardne sú premenné viditeľné len v tom module, kde ich zadefinujete. Týmto výrazom to zmeníte. V hlavnom C-čkovskom súbore už túto premennú priamo používame, a rovnako tak aj v iných moduloch, ktoré si tento hlavičkový súbor načítajú. Názov premennej musí byť v celej knižnici jedinečný, inak sa bude linker sťažovať.

Druhý význam: Ak extern uvediete pri deklarácii funkcie, sprístupníte ju aj ostatným modulom. U funkcií platí, že extern je uvedený implicitne a nemusíte ho uvádzať. Aj tu však platí, že názov funkcie musí byť v celej knižnici jedinečný. C-čko nepozná menné priestory.

Príklad:

/* Hlavička main.h */
extern int doSomeStuff();

/* Hlavný modul */
#include <stdio.h>
#include "main.h"

int main() {
    doSomeStuff();
}

/* Ďalší modul */
#include "main.h"

void someFunction() {
    doSomeStuff();
}


Tretí význam: extern "C" Tento príklad je pri interoperabilite C/C++ najdôležitejší. C++ kompilátor názvy funkcií upravuje. Robí to preto, aby vedel rozlíšiť viacero funkcií, ktoré majú rovnaký názov, ale iné parametre (typické preťažovanie funkcií). Aby však bolo možné volať C++ funkcie z C-čkovského kódu (a opačne), týmto príkazom poviete kompilátoru, aby funkcie nepremenoval. Inými slovami, v knižnici môžete mať C++ kód v jednom module, C kód v iných moduloch, a napriek tomu spolu môžu komunikovať prostredníctvom funcií.

V praxi sa tento blok pridáva cez direktívy preprocesora. Ak si hlavičkový súbor načíta modul napísaný v C++, vie, že funkcie majú mať názvy kompatibilné s C-čkovským kódom. A modul napísaný v C-čku direktívu extern vôbec neuvidí.

#ifndef SIMPLEMOTION_H
#define SIMPLEMOTION_H

...

#ifdef __cplusplus
extern "C"{
#endif

// Funkcie, ktorých názvy budú kompatibilné s tým, čo očakáva C-čkovský kód
LIB int smDescribeStatus(char* str, size_t size, int32_t status);

#ifdef __cplusplus
}
#endif
#endif // SIMPLEMOTION_H
Direktíva __cplusplus je definovaná len vtedy, ak sa súbor, ktorý hlavičku includuje, kompiluje v režime C++. V praxi to znamená, že musí mať koncovku .h alebo .cpp.

Externý kód nekompatibilný s C

Keď som pridával podporu pre ESP32 dosky, na skompilovanie projektu som musel pridať do knižnice viacero hlavičkových súborov. Ukázalo sa, že po pridaní sa začne kompilátor sťažovať na chyby ako napríklad neznámy typ s názvom class. Zjavne kód knižnice pre ESP32 neobsahuje direktívu extern "C" a definuje v sebe triedy. Keď pridáte takéto hlavičkové súbory do knižnice, súbory v režime C sa nezostavia.

/home/vzahradnik/.platformio/packages/framework-arduinoespressif32/cores/esp32/Stream.h:38:13: error: expected '=', ',', ';', 'asm' or '__attribute__' before ':' token
 class Stream: public Print
             ^
In file included from .pio/libdeps/m5stack-grey/SimpleMotionV2-Arduino/src/drivers/arduino/arduino_helper.h:22:0,
                 from .pio/libdeps/m5stack-grey/SimpleMotionV2-Arduino/src/simplemotion.h:24,
                 from .pio/libdeps/m5stack-grey/SimpleMotionV2-Arduino/src/simplemotion_private.h:8,
                 from .pio/libdeps/m5stack-grey/SimpleMotionV2-Arduino/src/sm_consts.c:1:
/home/vzahradnik/.platformio/packages/framework-arduinoespressif32/libraries/Wire/src/Wire.h:38:1: error: unknown type name 'class'
 class TwoWire: public Stream


Riešenie tohto problému sa ukázalo byť jednoduché. Vedel som, že objektový kód sa môže zostaviť len v režime C++ a že chyby vznikajú len pri kompilácii súborov v režime C. Využil som direktívu, ktorá mi overí, v akom režime modul kompilujem. Tentokrát som však nepridal kľúčové slovo extern "C", ale problematické hlavičkové súbory. Zostavovanie už prebehlo bez problémov.

#ifdef __cplusplus
#ifdef ESP32
#include <Wire.h>
#include <FS.h>
#include <SPIFFS.h>
#include <SD.h>
#include <HTTPClient.h>
#endif

extern "C" {
#endif
...

Funkcie, ktoré majú ako parametre C++ objekty

Ak máte niekde v kóde funkcie, ktoré očakávajú ako parameter smerník na objekt, takýto kód sa neskompiluje. Ako som už ukázal, pre C-čkovské moduly je pojem trieda neznámy a kompilátor vyhlási pri tomto type chybu. Momentálne som problém obišiel tak, že som zadefinoval funkciu, ktorá očakáva smerník na blok void a neskôr v C++ kóde si tento smerník pretypujem na C++ objekt. Viem, že to nie je dokonalé, ale funguje to.

Najprv som si zadefinoval cez typedef nový typ:

typedef void* ArduinoSerial;

Následne už som mohol tento typ použiť ako parameter funkcie:

LIB void smSetDebugOutput( smVerbosityLevel level, ArduinoSerial serial );

V Arduino sketchi normálne pošlem odkaz na objekt Serial:

smSetDebugOutput(SMDebugLow, &Serial);

Ten sa pretypuje na ArduinoSerial, prelezie cez C-čkovský kód až do arduino_helper modulu, kde si ho pretypujem na smerník na objekt Print. Následne už môžem zapisovať na sériovú linku tak, ako som zvyknutý.

/*
 * arduino_helper.cpp
 *
 */
Print* consoleOut = NULL;

#ifdef ENABLE_DEBUG_PRINTS
void arduinoPrintMessage(const char* message) {
    if (consoleOut != NULL) {
        consoleOut->print(message);
    }
}
#endif // DEBUG PRINTS

Bitové operácie, makrá a upozornenia

Aj keď mi kompilácia zbehne, všimol som si množstvo chýb v makrách. Analýzou som zistil, že makrá sa používajú len v troch funkciách určených prevažne na debugovanie. Odnastavením flagu ENABLE_DEBUG_PRINTS sa väčšiny upozornení zbavím. Toto však nemôže byť trvalé riešenie.

...
pio/libdeps/uno/SimpleMotionV2-Arduino/src/simplemotion.cpp: In function 'int smDescribeFault(char*, size_t, int32_t)':
.pio/libdeps/uno/SimpleMotionV2-Arduino/src/simplemotion_defs.h:84:25: warning: left shift count >= width of type [-Wshift-count-overflow]
 #define BV(bit) (1<<(bit))
                         ^
.pio/libdeps/uno/SimpleMotionV2-Arduino/src/simplemotion.cpp:1189:20: note: in definition of macro 'APPEND_IF'
   if (((source) & (name)) != 0) { \
...


Po hlbšej analýze som našiel problém. Arduino dosky postavené na platforme AVR definujú veľkosť typu int 16 bitov. To som potvrdil pre istotu tak, že som skompiloval jednoduchý sketch, kde bolo volanie sizeof(int). Dostal som hodnotu 2 bajty, čiže 16 bitov. Knižnica SimpleMotion definuje bitové masky a u niektorých hodnôt prekračujeme rozsah 16 bitov. Problém sa nachádza na dvoch miestach — v súbore definícii simplemotion_defs.h a tiež v súbore, kde sa nachádza kód na výpočet CRC, crc.c.

Za väčšinu upozornení bol zodpovedný jeden riadok:

#define BV(bit) (1<<(bit))

Chybu som upravil tak, že som namiesto typu int použil long, ktorý má na platforme AVR 4 bajty.

#ifdef __AVR__
/* Int on Arduino AVR boards is only 2 bytes, which is not enough
 * for all values defined here. We'll use long instead.
 */
#define BV(bit) (1L<<(bit))
#else
#define BV(bit) (1<<(bit))
#endif

Prečo som nepoužil #ifdef ARDUINO? Nuž, Arduino framework beží na rôznych platformách. Ukázalo sa, že napríklad na platforme ESP32 upozornenia nedostávam, pretože typ int má dostatok bajtov. Preto je lepšie upraviť definíciu makra len pre tie platformy, kde je to nutné. Podobné úpravy som spravil aj v kóde na výpočet CRC. Knižnica sa už zostavuje bez chýb.

Rozdiely v kompilácii medzi PlatformIO a Arduino IDE

Už som spomenul viacero problémov s kompiláciou pod Arduino IDE. Ak však siahnete na alternatívy, u nich môžete naraziť na iné problémy. Vo všeobecnosti však ladím knižnicu tak, aby bežala pod PlatformIO IDE. Proces zostavovania je oveľa jasnejší a mám nad ním väčšiu kontrolu. Zároveň sa kompiluje všetok kód knižnice, nie len vybrané časti podľa toho, ako sa Arduino IDE rozhodne. Keď kompilujem pod PlatformIO, odpadá mi viacero problémov. Chybové výpisy sú oveľa prehľadnejšie. A hlavne, v takto skompilovanom projekte správne funguje aj výpis na sériovú linku. Niečo, čo zatiaľ v Arduino IDE poriešené nemám.

Projekt v PlatformIO IDE

Hard fork

Už keď som začal na porte knižnice pre Arduino robiť, bolo mi jasné, že zmeny nebude možné do upstreamu začleniť. Pridať riadky špecifické pre Arduino nestačí. Aby bola knižnica kompatibilná, je potrebné preusporiadať kód.

  • Všetok zdrojový kód musí ísť do podpriečinka src
  • Je potrebné napísať príklady použitia do priečinka examples
  • Je potrebné zadefinovať manifest knižnice s aktuálnym popisom
  • A je potrebné dať žiadosť o pridanie knižnice do oficiálnych repozitárov pre Arduino IDE a PlatformIO
  • Bolo by tiež vhodné nastaviť automatizované spúšťanie zostavy pre Arduino dosky cez Travis CI s využitím testovacieho frameworku Arduino CI

Fork som vytvoril pod organizáciou, s ktorou spolupracujem. Nájdete ho tu. V tejto chvíli ešte neviem, do akej miery ho budem udržiavať. Rád by som však knižnicu odovzdal komunite dostatočne funkčnú, aby fungovala pod Arduino IDE aj PlatformIO. Keďže zatiaľ mám podporu hlavne pre Controllino a jednu dosku s čipom ESP32, rozhodne je možné pridať ovládače aj pre ďalšie dosky alebo rozširujúce moduly.

Záver

Tento projekt ma naučil jedno: každý port je špecifický a vždy musíte vedieť, čo robíte. Som si istý, že niektoré problémy, na ktoré som narazil, by iných neprekvapili. Ja ako vývojár, ktorý programuje vo viacerých jazykoch, som rád, že som sa dokázal relatívne rýchlo adaptovať a úspešne projekt dokončiť. Moje znalosti sú opäť o niečo väčšie a verím, že sú prenositeľné aj na iné projekty, na ktorých budem v budúcnosti robiť.

Portovanie kódu bol pre mňa zaujímavý rébus. Úloha, ktorá nebola jednoduchá a zároveň som sa nemal ani veľmi koho pýtať. Hlavne pre krátkosť času. O to viac som na výsledok hrdý.

Máte skúsenosti s portovaním kódu? Pochváľte sa v komentári nižšie.

Spread the word

Keep reading