Vinkkejä sulautettuun C-ohjelmointiin

Sulautettua ohjelmaa kirjoittaessa toimitaan tavallisesti hyvin lähellä rautatasoa ja usein myös ohjelma- sekä käyttömuisti ovat hyvin rajallisia. Toimintaympäristö on siis hyvin erilainen kuin esimerkiksi tavallisessa pc-ympäristössä. Tässä artikkelissa käsitelläänkin joitain sulautetun ohjelmoinnin erityispiirteitä sekä hyviä käytäntöjä, C-kielen näkökulmasta. Artikkelissa ei käydä läpi C-kielen perusteita, eli ainakin perussyntaksin olisi hyvä olla jo tuttua.

Artikkelista tuli melko pitkä, mutta se on jaettu itsenäisiin osiin, jotka voi lukea (tai jättää lukematta :wink: ) haluamassaan järjestyksessä.

Moduulijako

Ensimmäiseksi käsitellään aihetta, joka on hyvin tärkeä kaikessa ohjelmoinissa, nimittäin koodin jäsentäminen selkeästi. Ohjelma kannattaakin jakaa selkeisiin osiin (moduuleihin), joista kukin toteuttaa jonkin selkeän kokonaisuuden. Tällöin ohjelmakoodin lukeminen helpottuu ja jo kertaalleen toteutettuja ja testattuja moduuleita voi käyttää uudelleen. C-kielessä moduulijako tapahtuu tiedostojen avulla. Moduulin rajapinta sijoitetaan otsikkotiedostoon (.h päätteinen) ja varsinainen toteutus lähdekooditiedostoon (.c päätteinen). Moduulin voidaan ajatella olevan musta laatikko, joka tarjoaa rajapinnan kuvaamat toiminnot. Käyttäjän ei siis tarvitse välttämättä tietää/muistaa miten toiminnot on toteutettu. Seuraavana esimerkki moduulista

motor.h

// Tämä on esiprosessorikäsky, joka estää saman tiedoston
// useampikertaisen includoinnin
#ifndef MOTOR
#define MOTOR

// Funktio käynnistää moottorin funktion "motor_set_speed" asettamaan
// nopeuteen. Jos nopeutta ei ole asetettu, on se oletuksena 100%.
void motor_start();

// Funktio sammuttaa moottorin
void motor_stop();

// Funktio asettaa moottorinohjauksen PWM-työjakson 0-100%.
// Parametrin arvot, jotka ovat yli 100, tulkitaan sadaksi prosentiksi.
void motor_set_speed(unsigned char speed);

#endif

motor.c

#include "motor.h"

void motor_start()
{
	// Toteutus...
}

void motor_stop()
{
	// Toteutus...
}

void motor_set_speed(unsigned char speed)
{
	// Toteutus...
}

Moduulia k√§ytett√§isiin siten, ett√§ esimerkiksi p√§√§ohjelmaan (main.c) lis√§t√§√§n alkuun #include ‚Äúmotor.h‚ÄĚ, jonka j√§lkeen rajapinnassa esiteltyj√§ funktioita voi k√§ytt√§√§ normaalisti. Rajapinnassa voi esitell√§ my√∂s vakioita, makroja, ym. Huom. Jos motor.c tiedostoon m√§√§ritell√§√§n funktioita, vakioita, tms., joita ei ole rajapinnassa, niin niit√§ ei voi k√§ytt√§√§ motor.c:n ulkopuolelta. N√§it√§ kutsutaan moduulin yksityiseksi rajapinnaksi.

Perustietotyyppien koot

Sulautettujen j√§rjestelmien ohjelmoinnissa on usein tarvetta lukea tai kirjoittaa tietynlevyinen rekisteri. Rekisterin koko selvi√§√§ kontrollerin/prosessorin datalehdest√§, mutta mink√§tyyppiseen muuttujaan arvo tulisi tallentaa? ANSI C standardi m√§√§rittelee esimerkiksi int-tyyppien koot hyvin v√§lj√§sti: ‚Äúshort int <= int <= long int.‚ÄĚ Kannattaa siis pit√§√§ mieless√§, ett√§ tietotyyppien koossa voi olla k√§√§nt√§j√§- ja alustakohtaisia eroja. Ainakaan ei kannata tehd√§ perusteettomia oletuksia, vaan on parempi tarkistaa asia k√§√§nt√§j√§n dokumenteista. Monille k√§√§nt√§jille on my√∂s tehty valmiit typedef m√§√§rittelyt, joilla muuttujien koot on helppo saada oikeiksi.

// Monille kääntäjille löytyy valmiit typedef-määrittelyt perustietotyypeille
// stdint.h tiedostosta
#include <stdint.h>

// typedef:n avulla voidaan antaa tietotyypille uusi nimi:
// huom. Nimen perään lisätään usein _t, joka kertoo, että kyseessä
// on typedef eikä natiivi tyyppi.
typedef unsigned char my_uint8_t;

int main()
{
   // Kuten nimestä selviää, kyseessä 8-bittinen etumerkitön kokonaisluku.
	uint8_t x = 0;

	// sama kuin: unsigned char y = 1;
	my_uint8_t y = 1;
}

Vakiot ja makrot

Vakioiden sekä makrojen avulla voidaan parantaa ohjelmakoodin luettavuutta sekä helpottaa muutosten tekemistä huomattavasti. Makrojen avulla voidaan myös helpottaa koodin siirtämistä toiselle kontrollerille, kun kontrollerispesifiset asiat, kuten käytetyt pinnit, määritellään makrojen avulla. Katsotaan ensin, miten vakioita voidaan määritellä.

// Vakio varatun sanan const avulla
const unsigned char maksiminopeus = 100;

// Vakio makron avulla
#define MINIMINOPEUS 10

Näiden kahden tavan erona on se, että ensimmäisessä tapauksessa luodaan muuttuja, jonka arvoa ei vain voi muuttaa. Tällöin vakio käyttää yleensä datamuistia, joka on usein hyvin rajallinen. Definellä määritelty vakio sen sijaan toimii siten, että esiprosessori korvaa kaikki MINIMINOPEUS merkkijonot luvulla 10, ennen koodin käännöstä. Lopullisessa ohjelmassa ei siis varata muuttujaa, vaan arvo 10 on ohjelmakoodissa literaalina. Definellä määriteltynä vakio siis monistuu koodiin ja kasvattaa ohjelman kokoa. Kolmas mahdollisuus on käyttää const-vakioita ja käskeä kääntäjää sijoittamaan muuttuja nimenomaan ohjelmamuistiin.

Vakion määrittely ohjelmamuistiin AVR:llä.

#include <avr/pgmspace.h>

const char kehote[] PROGMEM = "Syötä komento: ";

Vakion määrittely ohjelmamuistiin PIC:llä.

rom const char kehote[] = "Syötä komento: ";

Seuraavaksi katsotaan, mitä muuta definellä voisi tehdä.

// Ledin käyttämä portti PIC-kontrollerilla
#define LED_SUUNTA TRISGbits.TRISG0
#define LED_POWER PORTGbits.RG0

// Käyttäjäkomennot
#define KOMENTO_RUN   'r'
#define KOMENTO_STOP 's'

Nyt led voidaan sytytt√§√§ koodissa komennolla ‚ÄúLED_POWER = 1;‚ÄĚ, joka on itsess√§√§n hyvin kuvaava eik√§ vaadi juuri kommentointia. Lis√§ksi jos ledi halutaan vaihtaa toiseen pinniin, niin ainoastaan define√§ tarvitsee muuttaa. Makrojen avulla voi tehd√§ my√∂s yksinkertaista funktiota muistuttavia toimintoja, mutta ne on j√§tetty t√§st√§ pois, sill√§ niill√§ on turhan helppo ampua itse√§√§n jalkaan. :slight_smile:

Katsotaan kuitenkin miten makrot voisivat toimia testauksen apuna.

// Kommentoi/poista tämä rivi, kun testitulosteita ei haluta
#define TESTAUS

// koodia...

// Testituloste, joka sisällytetään ohjelmaan, vain silloin, kun
// TESTAUS makro on määritelty 
#ifdef TESTAUS
printf("Tämä on testituloste\n\r");
#endif

Muistin käyttö

Kuten aikaisemmin jo todettiin, niin sulautetuissa järjestelmissä käyttömuistia on usein hyvin rajallinen määrä (erityisesti pienissä mikrokontrollereissa), joten sen käyttöön kannattaa kiinnittää erityistä huomiota. Mikrokontrollereita ohjelmoitaessa kannattaakin siis välttää dynaamista muistinvarausta, syviä aliohjelmakutsupuita sekä rekursioita (funktio joka kutsuu itseään).

Dynaamisella muistinvarauksella tarkoitetaan C-kielen new-operaattorilla ajonaikana varattavaa muistia. Kaikista pienimmissä kontrollereissa dynaaminen muistin varaaminen ei välttämättä ole edes mahdollista. Aliohjelmakutsupuulla tarkoitetaan rakennetta, joka kuvaa eri funktioiden välisen kutsuhierarkian. Eli mitä useampi funktio kutsuu aina uutta funktiota, sitä syvempi puu muodostuu. Tästä voi seurata ongelmia, koska jokaisella funktiokutsulla pitää varata muistista tilaa paluuosoitteelle, parametreille sekä paikallisille muuttujille. Tällöin on riskinä, että muisti loppuu ja juuri tästä syystä myös rekursiota tulisi välttää. Näiden ongelmien ymmärtämiseksi tarkastellaan seuraavaksi pinoa.

Pino (stack) on LIFO (last in first out) muistirakenne, jota käytetään funktiokutsun parametrien, paikallisten muuttujien sekä paluuosoitteen talletukseen. LIFO-rakenne tarkoittaa sitä, että viimeiseksi lisätty alkio saadaan ensimmäisenä ulos. Seuraava kuva selventää pinon toimintaa funktiokutsussa.

Periaatekuva pinon toiminnasta (pinon rakenne vaihtelee eri prosessoreilla).
stack.png

Huomataan, että kun funktiokutsun sisällä on toinen funktiokutsu, niin pinon koko kasvaa nopeasti ja muisti vapautuu vasta, kun ohjelma palaa funktiosta. Huomataan myös, että parametrien määrä ja koko vaikuttavat pinon koon kasvuun. Tästä syystä funktiolle ei kannata koskaan välittää suuria parametrejä, vaan kannattaa käyttää osoittimia. Pinon kokoon vaikuttavat myös paikalliset muuttujat, joten sulautetuissa järjestelmissä on täysin perusteltua käyttää globaaleja muuttujia.

C-kääntäjä huolehtii pino-operaatioista automaattisesti, mutta siitä huolimatta on hyvä ymmärtää ainakin periaattellisella tasolla, miten pino toimii ja mitkä asiat vaikuttavat sen kokoon.

Kääntäjän kirjastot

Viimeisenä aiheena käsitellään lyhyesti kääntäjän kirjastoja. Tässä kappaleessa ei varsinaisesti käydä läpi minkään tietyn mikrokontrollerin kirjastoja, vaan lähinnä muistutetaan, että kaikkea ei aina tarvitse/kannata tehdä itse. Toki opiskelun kannalta on hyvä tehdä asiat alusta asti itse, mutta muuten voi olla hyödyllistä käyttää valmista koodia. Monille mikrokontrollereille löytyy valmista koodia esimerkiksi sarjaportin, i2c-väylän, ajastimien, viiveiden, ym. käyttämiseen. Seuraavana lyhyt esimerkki kuinka kääntäjän kirjastot voisivat helpottaa elämää sekä linkit AVR:n sekä PIC:n kirjastoihin, joista asiaa voi lähteä tutkimaan eteenpäin.

// Tarvittavat kirjastot
#include <usart.h>
#include <stdio.h>

// koodia...

// Sarjaportin alustus ja tulostus
OpenUSART1( USART_TX_INT_OFF &
USART_RX_INT_OFF &
USART_ASYNCH_MODE &
USART_EIGHT_BIT &
USART_CONT_RX &
USART_BRGH_HIGH,
25 );

printf("Tulostus sarjaporttiin\n\r");

PIC C18 kääntäjän kirjastot:
http://ww1.microchip.com/downloads/en/devicedoc/MPLAB_C18_Libraries_51297f.pdf

AVR-LIB kirjastot:
http://www.nongnu.org/avr-libc/user-manual/modules.html

Mielestäni näiden kääntäjien mukana tulevien kirjastojen hyöty on hieman kyseenalainen. Ennen kontrollerin oheislaitteen (ajastin, uart, adc, yms.) käyttöönottoa joutuu joka tapauksessa tutustumaan sen toimintaan ja rakenteeseen datalehden avulla. Samassa paikassa on yleensä dokumentoitu myös oheislaitteen säätö- ja tilarekisterit. Nämä kirjastothan sisältävät kasan wrapperifunktioita, jotka vain välittävät parametreja oikeisiin rekistereihin. Jos kirjastoja haluaa käyttää, joutuu lisäksi tutustumaan funktioiden syntaksiin.

Yleisimpien oheislaitteiden käyttäminen rekistereiden kautta ei pitäisi tuottaa ongelmia. Sen sijaan olen huomannut, että joskus oikean dokumentaation löytäminen on turhan hankala. Joidenkin piirien kohdalla kaikki piirin oheislaitteet on dokumentoitu kokonaisuudessaan piirin omassa datalehdessä. Näin esim. 8-bittisten PIC ja AVR kontrollereiden kanssa. Sen sijaan 16- ja 32-bittisten PIC piirien datalehdissä oheislaitteet on esitetty vain suppeasti ja niiden tarkempi kuvaus löytyy piiriperheiden datalehdistä (Family Reference Manual / Family User’s Guide / yms.). Näin myös TI:n MSP430 kontrollereiden kanssa. Jostain syystä oikean PDF:n löytäminen TI:n sivulta on lähes mahdotonta ilman googlea.

UART debuggausta kannattaa yleensä välttää, mutta joskus sitä joutuu kuitenkin käyttämään. Omissa projekteissa olen käyttänyt seuraavaa funktiota debuggaukseen:

debug.h

#ifndef DEBUG_H
#define DEBUG_H
#include <stdarg.h>

#define MAIN    1
#define LCD     2
#define SENSOR  4
#define INPUT   8
#define MOTOR   16
#define ADC     32
#define TEMP    64

extern volatile unsigned char debug_level;

//#define NDEBUG

#if defined(NDEBUG) && defined(__GNUC__)
/* gcc's cpp has extensions; it allows for macros with a variable number of
   arguments. We use this extension here to preprocess debug away. */
#define debug(level, format, args...) ((void)0)
#else
void debug(unsigned char level, char *format, ...);
/* print a message, if it is considered significant enough.
   Adapted from [K&R2], p. 174 
   http://oopweb.com/CPP/Documents/DebugCPP/Volume/techniques.html	  */
#endif

#endif /* DEBUG_H */

debug.c

#include "debug.h"
#include <stdio.h>

volatile unsigned char debug_level = 0
                                    + MAIN 
                                    + LCD
                                    + SENSOR
                                    //+ INPUT 
                                    //+ MOTOR
                                    + ADC
                                    //+TEMP
                                    ;

#if defined(NDEBUG) && defined(__GNUC__)
/* Nothing. debug has been "defined away" in debug.h already. */
#else
void debug(unsigned char level, char* format, ...) {
#ifdef NDEBUG
	/* Empty body, so a good compiler will optimise calls
	   to debug away */
#else
        va_list args;

        if(!(debug_level & level))
			return;

        va_start(args, format);
        vprintf(format, args);
        va_end(args);
#endif /* NDEBUG */
}
#endif /* NDEBUG && __GNUC__ */
#include "debug.h"
...
int test = 123;
debug(MAIN, "test: %04X\n", test);

debug() funktio ottaa parametreina debuggaustason ja tulostettavan merkkijonon. Merkkijono tulostetaan vain jos kyseinen taso on sallittu debug_level muuttujassa. Määrittelemällä debug.h tiedostossa NDEBUG, kääntäjä poistaa koko debug() funktion ja sen kutsut. Funktion käyttö on kätevää, koska ei tarvitse erikseen kirjoittaa esikääntäjän ehtolauseita jokaisen funktion kutsun yhteydessä.

Viiveistä sen verran, että esim. tekemissäni sovelluksissa asioiden ajastaminen on toteutettu melkein aina hieman eri tavalla, riippuen sovelluksen rakenteesta. Mitään viivefunktioita ei tietenkään kannata käyttää, ellei ole käytössä käyttöjärjestelmää.

Kiitos Andreille hyvästä ja asiallisesta kommentista!

Kääntäjän kirjastojen kanssa olen jokseenkin samaa mieltä kanssasi, sillä ne eivät todellakaan ole oikotie onneen, vaan käytetyn kontrollerin oheislaitteiden toiminta sekä rakenne tulee tuntea, vaikka valmiita kirjastoja käyttäisikin. Myös valmiiden funktioiden syntaksiin, toimintaan ja mahdollisiin esivaatimuksiin/oletuksiin joutuu tutustumaan, kuten jo mainitsitkin. Siitä huolimatta olen sitä mieltä, että valmiiden kirjastojen tarjonta kannattaa ainakin silmäillä läpi. Jos käyttää usein samaa kontrolleria/kontrolleriperhettä, niin saattaa sieltä kirjastosta löytyä jotain hyödyllistäkin. :slight_smile:

Esimerkiksi printf on mielestäni melko kätevä, jos tarvitsee nopeasti tulostaa sarjaportiin jotain, koska silloin ei tarvitse alkaa käsin muuntamaan esimerkiksi kokonaislukuja merkkijonoksi. Toinen kätevä funktio on sprintf, jota voi käyttää syötteen muotoiluun.

#include <stdio.h>

int main()
{
    char buffer[15];
    int i = 100;

    // Muunnetaan i:n arvo heksadesimaaliksi ja tallennetaan bufferiin
    sprintf(buffer, "Hex: %#06x", i);

    // Nyt buffer sisältää merkkijonon: "Hex: 0x0064"
}

Debuggaus on aiheena sen verran laaja, että siitä voisi varmasti kirjoittaa useammankin artikkelin, joten tämän artikkelin aiheen ja kohderyhmän huomioonottaen ajattelin, että testituloste sarjaporttiin yhdistettynä yksinkertaiseen esiprosessorin #if rakenteeseen olisi sopivan yksinkertainen esimerkki. Se ei varmastikkaan ole ainoa eikä paras tapa, mutta uskoisin, että aloittelevankin C-ohjelmoijan olisi helppo käyttää sitä omissa projekteissaan.

Koko ajatus debuggauksesta on pöljä. Jos homma menee debuggaukseksi, on peli jo useinmiten menetetty. debugattu koodi kun tahtoo tuoda aina kummallisia uusia bugeja, jotka tulevat vasta jossain oudossa tilanteessa esiin. Lisäksi päätön debuggaus vain johtaa koodin kasvamiseen melko hallitsemattomasti.

naivi esimerkki:

void print_second_char(char * str){
  print(str[1]);
}

Tuo koodihan heitt√§√§ voltin jos str == NULL tai str osoittaisi merkkijonoon, joka olisi vain ‚Äú‚ÄĚ. Perinteisess√§ debuggauksessa etsitt√§isiin paikka josta se l√§htee lapasesta, kirjoitettaisiin pari if-lausetta eri paikkoihin ja katsottaisiin, ett√§ se nyt toimii ja jostain virheenk√§sittelyst√§ sitten kipattaisiin, kun tapahtuu sekuntilaskurin ylivuoto tai jotain muuta odoteltavaa.

Jos on oikeasti mietitty ja suunniteltu mitä tehdään ja laitettu se yksi if-lause oikeaan paikkaan niin tuo funktio olisi jotain tämän kaltaista:

void print_second_char(char * str){
  if(str != NULL && strlen(str) > 1){
    print(str[1]);
  }
}

ja se toimisi aina oikeilla merkkijonoilla. Pöljyyshän iskee, kun sitä käytettäisiin vaikka näin:

char foo = 'F';
print_second_char(&foo);

str != NULL ja strlen menee kunnes tulee ‚Äė\0‚Äô vastaan. Antaa sielt√§ sitten jonkin pituuden. Tuo tulostaa sitten tuon foon vierest√§ seuraavan arvon. K√§√§nt√§ess√§ ei mit√§√§n ongelmaa, mutta jostain tullut pieni lipsahdus korvain v√§liss√§ voi moisen tehd√§.

Ja tämän kaltaiset asiat ovat ihan oikeasta elämästä. Usein vain se tulee tuohon tilaan kun kaikki höttö suodatetaan ympäriltä pois. Siksi kannattaa miettiä aina ihan oikeasti mitä tapahtuu. Miltä joku structi näyttää muistissa, miten osoittimet seikkailevat ja koodin läpi lukeminen on paras tapa napata suunnittelumokat.

Eli itse käytän seuraavaa periaatteita. void funktioita ei ole ja jos funktiolle syötetään mitä tahansa, se selviytyy siitä ja virheistä annetaan paluuarvo. En myöskään castaa ja paikallisia muuttujia ei syötetä koskaan funktiolle. Lisäksi paluu arvoja nuuskitaan. Helppo pyöräyttää debuggerista läpi ja katsoa mistä tultiiin takaisin. Silloin siellä on mokatattu.

Itse käsitän debuggauksen terminä siten, että siihen kuuluu kaikki ohjelman virheiden tai virheellisen toiminnallisuuden etsimiseen käytetyt keinot, joista testitulosteet ovat vain yksi esimerkki. Muita tapoja ovat esimerkiksi ohjelman ajaminen käsky kerrallaan, breakpointit, jne. Ei debuggaus tietenkään auta suunnitteluvirheisiin, huonoihin ohjelmointikäytäntöihin tai vääriin oletuksiin, mutta vaikka edellä mainitut olisivatkin kunnossa, niin kyllä niitä virheitä väistämättä tulee.

Klassinen virhe, joka menee käännöksestä läpi, mutta jossa ei ole loogisesti mitään järkeä.
(hyvä kääntäjä osaa antaa varoituksen)

// Tarkoitus vertailla onko a nolla
if(a = 0)
{
    // If:n sisään ei tulla koskaan a:n arvosta riippumatta.
}

Kuvasit juuri testauksen. Testaushan sitten osoittaa vain että ohjelma ei toimi väärin testisyötteellä. Debuggaus on siis sitä, kun raivokkaasti hakataan näppistä ja huudetaan, että toimi peijooni ja törkitään miljoonaan kohtaan virityksiä, jotka toimivat ehkä jossain vaiheesa.

Ohjelmaa tosiaan kannattaa suorittaa debuggerissa ja katsoa miten se toimii. Vielä parempi jos on oikein paperille kirjoittanut syötteen ja toivotun lopputuloksen. Silloin ei tule lipsuttua suunnitelmasta niin helposti.

Itsellä menee helposti tuntikin kymmenen minutin hommaan kun katselee ja miettii, että mistä kohtaa epätoivottu käytös on paras tappaa. Helpoin paikka se ei ole useinkaan.

Periaatteessa järkevillä testitulosteilla voi selvittää paikan mihin nakataan debuggerin brakepointit. Tässä myös kannattaa ottaa huomioon se että tulostuksen teko hidastaa ohjelman toimintaa tai sarjaportti tukkeutuu eli lähetetään liikaa mitä se pystyis lähettään.

Mutta kun yleensä sulautettuja tehdään, niin skooppi / logiikka-analysaattori on joissain tapauksissa ehoton apu ongelman ratkaisemiseksi. Esim. miksi jokuväylä ei toimi oikein. Samalla tällä näkee, että toimiiko rauta ympärillä oikein.

Debuggerilla tosiaan tarkoitan rautatason debuggausta JTAG tai vastaavalla liitännällä. Esim Atmellilla pikkupiireissä dW ja Cortex M3 sarjan prossilla SWD.

Ihan hyvää perustietoa, ei ehkä ihan aloittelijoiden matskua, mutta noheva kaveri hyötyy tästäkin :slight_smile:

Debuggauksesta sanoisin sen verran, että JTAG yms debug-viritykset ovat usein etenkin vanhempien henkilöiden mielestä turhia ja yliarvostettuja :smiley: Voisin kuitenkin väittää tähän vastaan, koska debuggerilla nähdään flashin ja rammin sisältö ja se auttaa joskus todella paljon esim. jos pointtereiden kanssa on ongelmia tms joita ei ilman lisäkikkailuja saa selville. Lisäksi taulukoiden läpi käymistä voidaan suorittaa rivi-riviltä tai solu-solulta (miten sen nyt haluaa sanoa) yms hyödyllistä. Vieläpä: debuggerilla voidaan testata peripheraaleja (ADC, UART/USART jne) suoraan kirjoittamalla arvoja rekistereihin jolloin myös ympärillä olevaa rautaa voidaan testata step-by-step menetelmällä.

Jos se nyt jollekin jäi epäselväksi, niin sanotaan kans että tarkoituksena ei ole vanhempia suunnittelijoita mollata. Kunnioitan heidän kokemusta asiassa, mutta ei saa olla liian ennakkoluuloinen asioiden suhteen :slight_smile:

Ns. printf-debuggauksessa on yksi hyvä puoli mitä ei JTAG laitteista löydy, nimittäin ohjelman kulun seuraaminen eli ns. trace ominaisuus. Voit katsella printf tulosteista että miten ja mistä päädyttiin virhetilanteeseen - mitä tapahtui juuri ennen virhettä :slight_smile:

Eli eivät nämä menetelmät ole toisiaan poissulkevia vaan täydentävät toisiaan.

Toihan toimii varmasti PC ympäristössä hyvin, kun esim kovolle ehtii tallentaa / näyttää tarpeeksi nopsaa. Sulautetuissa tulee ongelmaksi tuon kuluttama aika, mikä voi estää vikaa esiintymästä tai jumittaa koko laitteen, jos on tungettu liikaa debuggia.

Esim harvoin tapahtuviin virheisiin tuo on kyllä erittäin hyvä.

Tietenkään tulosteita ei pidä edes yrittää tehdä muutaman sadan mikrosekunnin välein. Eli ihan joka ohjelmaloopilla sitä ei kannata edes yrittää. Myös voi laittaa ohjelmaan haaran, jolla vikatilanne havaitaan ja tässä haarassa tulostellaan kontrollerin rekistereitä ja/tai muuttujia.

Ihan kätevä lisä jtagille.