Programmeren leer je door het veel te doen of door een cursus te volgen. En op internet staan ook veel voorbeelden waar je het één en ander uit kunt halen.

Als ik je een tip mag geven, laat die registers lekker zitten. Tenzij je specifieke behoeftes hebt, zou ik gewoon lekker gaan voor een library. Zoals eerder gemeld, je kunt beter een Arduino pakken. Daar zijn al talloze voorbeelden en library's voor te vinden. Als je echt praktisch iets wilt gaan maken dan is dat veel handiger.

Daarnaast wat is makkelijker te lezen:


REGA = 0x12;
REGB = ...

...

of


float myVoltage = ReadADC(PIN_1);

Je andere vraag is waar ik de focus op zou leggen. "Eindeloze if/else, nests, delays" Dit is inherent aan het goed ontwerpen van je software. Eerst nadenken dan pas schrijven. Bijna altijd is software goed op te delen in meerdere kleinere functies die een specifieke taak hebben.

https://www.youtube.com/watch?v=CFRhGnuXG-4

Op 10 oktober 2023 13:08:20 schreef BDH:
Hallo Allen,

Daarnaast heb ik zoals je mag verwachten van een afgestuurde HBO'er ook nog wat verstand van hardware. Echter vind ik dat mijn manier van programmeren nog teveel beginnerstijl is, met veel IF's en delay's.

Wat ik vooral wil leren:
Het programmeren met timers, interrupts en het direct schrijven naar registers en vooral die eindeloze 'if/else' nests en delays achter mij te laten. Ik heb het idee dat als ik dat onder de knie heb, er echt een wereld voor mij open gaat.

Chris

Het leuke is dat als je in de code die later geproduceerd wordt door de compiler (assembly dus) je die if's enzo zo vereenvoudigd en wel terug vind in de assembly lijst. Een compiler bestaat vaak uit een aantal kleinere programma delen. Dat waren vroeger letterlijk een aantal kleine losse programma's die je los na elkaar moest aanroepen. Dat was bijvoorbeeld eerst de syntax van je creatie controleren, daarna compileren, assembleren en tenslotte alles linken. Later kwam daar nog een stukje optimaliseren bij. Bij dat optimaliseren werden er bijvoorbeeld dubbele stukken code uitgehaald en vervangen door een subroutine en een jump ofzo, maar ook veelvuldig in kaar geneste lussen werden daar uitgeplozen. Onze menselijke gedachten zijn vaak zo complex dat een ingewikkelde if, then else structuur zo gemaakt is terwijl dat lang niet altijd hoeft.

Tegenwoordig gaat dat compileren nog steeds zo maar ziet men dat niet meer omdat alles met behulp van een make file min of meer geautomatiseerd is. In het geval arduino is de arduino software eigenlijk alleen maar een heel handige user interface. De eigenlijke compiler is een al heel oud programmaatje met de naam AVRDUDE dat wordt aangeroepen door de arduino user interface. Dat programmaatje is ook gewoon los te gebruiken hoor. In linux staat het gewoon in de repository's en kun je het gewoon downloaden.

Om zeer complexe if's enzo te vermijden gebruik ik vaak het idee van een stappen teller. Vroeger zouden ze dat een state machine noemen. Elke stap geeft dan een toestand van de machine aan. Pas als de de toestand van de machine verandert wordt de stap verhoogt met 1. Dus stap vier is beker vullen en stap vijf is beker vol. Het omschakelen van stap vier naar vijf kan dan gebeuren door de beker vol detectie schakelaar. Ergens anders in het programma zit dan een regel dat alleen in stap vier de beker vullen klep geopend kan zijn.
Je hebt dan maar 1 if nodig. N.l.: if stap is 4 dan vul beker is aan. De andere if is dan if beker is vol dan stap wordt 5

Op deze manier kun je groot case statement bouwen of een reeks van enkele niet geneste if's.

Het is wel zaak om delay's niet te gebruiken. Die zijn te hardware afhankelijk. Ga je naar een ander hardware platform dan klopt er vaak niets meer van. Bij de oude arcade spellen was dat vaak een probleem. Met een nieuwe computer ging je spel opeens een stuk sneller. De oplossing was toen om met de timertick die elke seconde een interrupt genereerde de delay's te kalibreren. Ook vertragen ze de afwerking van het hele programma. je kunt dit oplossen met interrupts maar als het er te veel worden lopen die weer uit de stack en geeft de prioriteit weer problemen. Beter is het dan om rondlopende tellers te gebruiken die met een timer interrupt steeds verhoogd worden. Gewoon de momentele waarde optellen bij de waarde die bereikt moet worden na de versterken tijd. En dat weer vergelijken met de de nieuwe waarde van de teller. Dan is tijd voor de cpu om de bewuste actie te gaan doen. In de wachtijd kan de cpu iets anders belangrijks doen. Beter dan in een wachtlusje te lopen rondklooien.

Goede vraag.

Mijn ervaring is dat je via werk in de elektronicabranche het beste kunt groeien daar waar je interesses liggen.
Door over de schouder van een collega mee te kijken in de pauze, samen met een collega een project te doen of zelf voor een uitdaging te staan.

Je kunt niet altijd van baan veranderen, maar je kan wel kijken of het bij je zou kunnen passen.

Durf je (nog) niet te solliciteren als programmeur, zoek dan een baan waarbij je functie breed is en waar je op een afdeling werkt waar collega's niet allemaal hetzelfde doen als jij; liefst allemaal anders. Dan is het makkelijker om van elkaar te leren, met elkaar mee te denken en binnen je functie te groeien naar bijvoorbeeld programmeur. Zelf heb ik zeer goede ervaringen met werken in het onderwijs: Daar is 'iets willen leren' veel natuurlijker dan binnen een bedrijf waar alles alléén maar om geld draait.

In plaats van 'if/else' is een 'select case' vaak duidelijker...

Wat ik zelf voornamelijk belangrijk vind, is dat de code makkelijk leesbaar moet zijn. Als je een functie ziet moet je in een oogopslag kunnen zien wat die functie doet. Er zijn tal van stukken geschreven hoe en waarom.

Een quote: (Gerald Weinberg)
"Code is written and maintained by people. Therefore, make it as readable as possible."

Op 10 oktober 2023 14:48:46 schreef Ex-fietser:
De eigenlijke compiler is een al heel oud programmaatje met de naam AVRDUDE dat wordt aangeroepen door de arduino user interface.

Eeh, bijna.

De eigenlijke compiler (voor de meeste, zoniet alle devices waar arduino mee werkt) is gcc. En gcc is een enorme mammoet van een compiler, die ongeveer alles voor iedere processor aankan, en nog steeds regelmatig bijgewerkt wordt.

avrdude is een klein programmatje om binarys (gecompileerde en gelinkte programmas, die door de CPU uitgevoerd kunnen worden) op een AVR device te programmmeren. avrdude staat volledig los van gcc.

Op 10 oktober 2023 15:19:00 schreef Arco:
In plaats van 'if/else' is een 'select case' vaak duidelijker...

Los van dat zo'n ding in C switch heet, een switch is veel beperkter dan een if/elseif/else constructie. En als je het netjes formatteert, is een switch ook niet perse duidelijker.

Zelf heb ik in mijn mainloop vaak verschillende (mogelijke) taken, die met verschillende intervallen gedaan moeten worden (bijv: Display update elke 10ms, temperatuur regeling elke seconde, keypad check elke 50ms, logfile versturen elke 5 minuten etc).

Dat kan niet netjes met een switch, wel met een if/elseif boom.

Als je daarintegen 20 verschillende commando's moet decoderen kan een switch geschikter zijn.

Als er te veel gebeurt in switches vind ik ze ook niet meer duidelijk. Een switch is wel sneller dan if / elseif statements. Tenminste, wanneer je meerdere cases hebt. Al is snelheid iets waar je tegenwoordig niet meer zo tegenaan loopt.

Voor de OOP mannen onder ons: https://refactoring.guru/design-patterns/state

Voor TS, ik denk wat je vraagt ook gewoon een kwestie van ervaring is. Ik programmeer nu zoon 6 jaar voor een baas. Daarvoor deed ik veel voor de hobby. Het ontwerpen van code is iets wat komt met de jaren. Waar ik zelf veel van leer is sparren met collega's. Het samen ontwerpen van de code voordat je gaat programmeren kan erg leerzaam zijn.

Neem de aanwijzingen van @blurp ter harte, zijn oefeningen sta ik vierkant achter, daar ga je veel van leren. Lees de (relevante delen van de) datasheet van je microcontroller. Stel je vragen hier op CO. :)

Ik programmeer al van jongs af aan, zo'n 40 jaar, nooit zwaar professioneel (wel scripts,php,bash etc) verder allerhande talen en platformen.

Wat ik in sommige gevallen wel elegant vind als alternatief voor ifs of switches zijn arrays:


// duisternissensor, bewegingssensor, kind_in_bedsensor = lamp_sterkte
a[ 0 ][ 0 ][ 0 ] = 0   ;  
a[ 0 ][ 0 ][ 1 ] = 0   ; //kind ligt misschien ziek op bed
a[ 0 ][ 1 ][ 0 ] = 20  ; //klein beetje licht overdag bij beweging, als test.
a[ 0 ][ 1 ][ 1 ] = 40    ; //kind springt op bed, lichtje iets feller
a[ 1 ][ 0 ][ 0 ] = 40   ; // hou boeven buiten lampje  
a[ 1 ][ 0 ][ 1 ] = 10   ; // zwak lampje als kind slaapt
a[ 1 ][ 1 ][ 0 ] = 100  ; // iemand in de kamer = vol licht.
a[ 1 ][ 1 ][ 1 ] = 10   ; // zwak lampje als kind slaapt

while (true) { 

 setLampBrightness( a[readDarkness()][readMovement()][readBed()] )

} 

Ik heb de logica niet helemaal uitgeplozen en de formatering kan netter, maar feitelijk maak je zo een 'state table' zonder enkele if/else/or/and achtige constructie.

@TS, als aanvulling op blurp zijn oefeningen:

Zonder interrupts, enkel vanuit de main:
- Maak 5 LEDS die allemaal op een andere snelheid knipperen.
- LED 1 = 1 Hz, LED 2 = 2 Hz, enz.
- Extra punten: Als een ledje 10x aan en uit is geweest mag hij de 5 seconden constant aan en daarna weer terug naar knipperen.

@K7Jz, Dat is best een interessante opzet. Als je het wat uitbreid kan je het volgende maken. Dat geeft de mogelijkheid om complexere zaken af te handelen. Nu wordt de logica echt gescheiden van de state bepaling. Je zou dat nog makkelijk kunnen aanpassen natuurlijk.

(Kladblok code, geen idee of het compileerd.)



typedef void (*StateFunction)(int stateChanged);

void emptyRoom(int stateChanged) {
    if (stateChanged) {
        printf("Setting lamp brightness to 0.\n");
    }
}

void someoneInRoom(int stateChanged) {
    if (stateChanged) {
        printf("Setting lamp brightness to 100.\n");
    }
}


int main()
{
    a[ 0 ][ 0 ][ 0 ] = emptyRoom;
    a[ 0 ][ 0 ][ 1 ] = someoneInRoom;
    
    StateFunction currentFunctionPointer = NULL;
    
    while (1) {
        int darkness = readDarkness();
        int movement = readMovement();
        int bed = readBed();
        
        StateFunction nextFunctionPointer = a[darkness][movement][bed];
        int stateChanged = (currentFunctionPointer != nextFunctionPointer);
        if (stateChanged) {
            currentFunctionPointer = nextFunctionPointer;
        }
        currentFunctionPointer(stateChanged);
    }
    return 0;
}

@hardbass, @K7Jz:

Waar jullie langzaam heen bewegen is de formele definitie van een state-machine:

New_state = transition_matrix * (old_state + inputs)
output = output_matrix * state

Voor een embedded systeem kan dit handig zijn: Als je 200 mogelijke toestanden hebt met 16 inputs en 8 outputs ben je met 48 byte aan matrix-code, en een paar regels met 8-bit AND/OR logica in een loopje klaar.

Een switch met 200 cases, en if-elsifs voor alle 16 inputs in elke 200 cases is veel ingewikkelder.

Documenteren van deze enorme state-machine, en alle overgangen is echter een heel ander verhaal.

Voor TS is dit natuurlijk embedded programmeren 2.0, de vervolgcursus.

Klopt. Je zou ook nog zo iets kunnen doen:

EDIT: hmm, ik laat het nog even staan want ik heb het al gemaakt. Maar misschien is het beter om TS niet te veel te overdonderen met allerlei ingewikkelde code.

@TS, laat je niet afschrikken. Code leren doe je niet in een weekje.


typedef struct _args {
    void (*nextState)(_args*);
    bool onEntry;
} Args;

void FirstState(Args *args);
void SecondState(Args *args);

void FirstState(Args *args) {
    printf("FirstState\n");
    args->nextState = SecondState;
}

void SecondState(Args *args) {
    printf("SecondState\n");
    args->nextState = FirstState;
}

int main() {
    Args args = { .nextState = NULL, .onEntry = true };
    void (*currentState)(Args*)  = FirstState;
    
    for(int i=0; i<10; i++)
    {
        currentState(&args);
        
        bool stateChanged = args.nextState != NULL && currentState != args.nextState;
        if(stateChanged)
        {
            currentState = args.nextState;
        }
        args.onEntry = stateChanged;
    }

    return 0;
}

@hieronder, ha betrapt :+
Dit is ook wel een beetje een eigenschap van C. Wanneer je met pointers gaat werken, zeker met functie pointers wordt het al snel een brij van haakjes. Een typedef voor de functiepointer zou al wat kunnen helpen.

Ook nog een leuke trouwens:
https://gist.github.com/FedericoPonzi/f026ff5c210e738ee03d271c8fc0f29b

Op 11 oktober 2023 09:52:16 schreef hardbass:
Klopt. Je zou ook nog zo iets kunnen doen:


ONLEESBARE BRIJ

Ik las ergens dat iemand zei dat je human readable code zou moeten schrijven...

Dit valt daar in ieder geval niet onder...

Leesbare (geen WORN :) ) code is inderdaad belangrijk, zeker voor toekomstig onderhoud ervan.

Bij een vorig bedrijf deed ik firmware ontwikkeling samen met een collega.
Een andere groep programmeurs (o.a. de baas) maakte de PC software ervoor.

Op een gegeven moment zei de baas bij een project "ik ga eens zelf de firmware maken hiervoor, lijkt me leuk als afwisseling"
Hij heeft die firmware (50+ A4'tjes) toen in 2 weken in elkaar gedraaid, en het deed bij benadering wat het zou moeten doen.

Er bleven echter heel veel vreemde dingen gebeuren. Na een weekje foutzoeken gaf 'ie het op en gaf de boel aan ons. ("kunnen jullie eens kijken wat er mis is?")

We hebben toen 2 dagen in die code gekeken, en daarna wisten we nog steeds niet wat er waar gebeurde in die code en waarom dat (wel of niet) gebeurde...
(Was een onleesbare brei van code, de baas kon zelf ook niet meer goed omschrijven hoe het eigenlijk werkte)

We hebben toe de hele boel weggekiept en zijn opnieuw begonnen... :+
(en we hebben de baas gevraagd om dat nooit meer te doen en zich tot 'gewone' software te beperken, daar was 'ie uitstekend in)

Waar staat WORN voor? Als ik daarop zoek krijg ik hele andere dingen.

WORN = "Write Once Read Never" ;)

Ah zo, ja genoeg grapjes op het internet.

Op 11 oktober 2023 10:16:09 schreef Roches:
Ik las ergens dat iemand zei dat je human readable code zou moeten schrijven...

Dit valt daar in ieder geval niet onder...

Er is wel wat op aan te merken, maar hardbass zijn code was niet onleesbaar.

Natuurlijk is het onzinnig om een state machine met twee states op die manier te implementeren, maar het was als voorbeeld bedoelt.

Je kunt ook discussieren over nut&noodzaak van functies voor state-overgangen, maar daar is in het algemeen wel wat voor te zeggen (Embedded systemen moeten vaak "dingen" doen bij een state overgang. Dus heb je bij die state-overgangen sowieso een functie nodig. Dus is het simpeler altijd een functie te doen)

Functie pointers in C zijn krachtig, maar ook moeilijke gereedschap. Niet voor de beginner, maar zeker ook niet uitsluiten!

Op 11 oktober 2023 11:54:12 schreef blurp:
[...]

Er is wel wat op aan te merken, maar hardbass zijn code was niet onleesbaar.
...

Misschien voor een ervaren programmeur niet onleesbaar maar voor velen is het dat wel (behalve de plaatjes >:) ).

De doelstellingen van de TS zijn ook nogal vaag omschreven en omdat hij (voorlopig) niet meer reageert is het ook moeilijk om dit topic een andere richting uit te sturen.

Gezien zijn geschiedenis denk ik dat hij meer hardware georiënteerd wil programmeren, maar daar kan de TS uitsluitsel over geven.

Dat was inderdaad ook het idee. Als je straks 10 state's hebt waar van alles en nog wat in moet gebeuren dan is het vaak leesbaarder dan een hele grote switch case. Het opdelen van de states naar hun eigen functie kan dan een middel zijn. Dat Args ding zou ik dan eerder context noemen, zodat je die kan gebruiken om bepaalde variabelen tussen states te kunnen delen.

Dit is gelijk het probleem met de definitie 'leesbare code'. Dit is nogal afhankelijk van de lezer en persoonlijke voorkeuren.

Wat ik persoonlijk nog mooier vind, is een class te maken per state zoals in mijn eerdere link stond. Maar dan moet je wel een OOP taal gebruiken.

@hardbass:

Ook zonder OOP taal en functiepointers kun je leesbare code schrijven:


enum houdingen {slaap, gaap, eet, douche, loop, zit, lig, dans, spring, speel, zing};
struct toestand {
    houdingen houding;
    // andere toestandsvariabelen
};

while (1) {
    switch (toestand.houding) {
        case slaap:
            behandel_slaap(&toestand);
            break;
        case gaap:
            behandel_gaap(&toestand);
            break;
        //etc
}

Dit geeft exact dezelfde machinecode als het OOP pattern met meerdere classes, maar je moet het wat meer uitspellen.

De switch wordt misschien heel lang, maar heel voorspelbaar en iedere boerel*l snapt het. Saai, maar simpel en prima te debuggen en onderhouden.

Met een tabel met functiepointers per toestand kun je de switch vervangen door een tabel-lookup. Concept komt uit FORTRAN-66, heette daar een "computed GOTO".

Afhankelijk van je source-opbouw kan de compiler dat overigens sowieso doen.
(als de compiler de enum en alle functies en de switch op hetzelfde punt beschikbaar heeft kan ie het patroon herkennen, en het implementeren met een tabel en lookup. Voordeel is dat de compiler niet in de war raakt van de vele haakjes rond een functiepointer)

Absoluut waar, zo is het voor iedereen te lezen. Buitenom dat is het ook voor ervaren programmeurs makkelijker te begrijpen. Dit zou ook mijn voorkeur hebben t.o.v. het geneuzel met pointers. Het enige nadeel wat ik me kan bedenken is dat veel zaken dubbel getypt worden. Dan kunnen er over tijd foutjes in sluipen. Voornamelijk met kopiëren en plakken, of het hernoemen van zaken. Maar dat is denk ik acceptabel over de leesbaarheid van de code.

Ik doelde eigenlijk meer op dat je niet te veel in een switch wil doen. In jouw voorbeeld wordt er per state een functie aangeroepen. Dat is dus zeer makkelijk te volgen. Ook al staan er 1000 states in, is het nog steeds duidelijk. Ik heb een heel mooi voorbeeld uit onze codebase, maar ik denk niet dat het op prijs gesteld wordt als ik dat zo hier neer gooi. Om een idee te geven, deze statemachine bestaat uit +- 400 regels code en heeft 8 states. Dat komt op een gemiddelde van 50 regels per state. Vol met if else's om te bepalen naar welke state er geschakeld moet worden. En dan staat er om deze switch nog een hoop om zaken uit te lezen en te berekenen. Hij staat voor binnenkort op het lijstje om die eens aan te pakken.

@hardbass:

Ik weet precies wat je bedoelt. Dat soort constructies ontstaan vaak in de loop van de tijd. Het begint met een enkele if in een case, die krijgt er een else bij en dan groeit het als onkruid.

Opruimen is vaak een enorm geklooi omdat er gebruik gemaakt wordt van allelei variabelen die alleen in de scope van de switch bestaan.

Jah, en dan niet te vergeten dat er door de tijd heen allerlei voorwaarden ontstaan die misschien wel of niet meer logisch zijn. Als je zoiets opruimt, is er een groot gevaar dat het daarna niet meer helemaal hetzelfde werkt. Ik heb die switch zelf ooit in het leven geroepen. Toen was die nog klein en schattig, maar door de tijd heen is het een akelige puber geworden.

Maar goed, ik heb nu wel meer dan genoeg dit topic gekaapt. O-)