Hallo,

Een atmega328 heeft 6 pinnen met PWM op basis van timer0, 1 en 2. Nu wil ik 8 leds kunnen faden en heb ik enkele timers nodige voor andere zaken, dus moet ik overschakelen naar software. Om het wat efficiënt te houden maak ik gebruik van "Binary Code Modulation" in plaats van PWM. De interrupt routine heb ik geschreven in assembly en neemt slecht 50 clock cycles in beslag. De code ga ik niet posten omdat deze in principe werkt en om het overzicht wat te behouden. In plaats daarvan een illustratief voorbeeld die mijn vraag moet verduidelijken: stel een timer die bij elke interrupt een variable met één vermeerderd.

File inc_isr.S: de interrupt service routine die variable "_var" met 1 vermeerderd


#include <avr/io.h>

.extern _var

.global TIMER1_COMPA_vect
TIMER1_COMPA_vect:

    in      r1, _SFR_IO_ADDR(SREG)
    push    r2

    lds     r2, _var
    inc     r2
    sts     _var, r2

    out     _SFR_IO_ADDR(SREG), r1
    pop     r2
    clr     r1
    reti

File foo.c: initialisatie timer1 en opvragen waarde "_var"


static volatile uint8_t _var;

void init() {
    _var = 0;
    // initialisatie timer1
}

uint8_t getVar() {
    uint8_t csreg = SREG;
    cli();

    uint8_t var = _var;

    SREG = csreg;

    return var;
}

Dit compileert en werkt perfect, maar nu wil ik hetzelfde bereiken in C++.

File: foo.h: prototype van een klasse "Foo"


class Foo {

public:
    Foo();
    uint8_t getVar();

private:
    uint8_t _var;
};

Deze code compileert maar heeft een fout bij het linken, namelijk: "undefined reference to _var". Heeft iemand enig idee hoe ik dit werkend kan krijgen ?
Om terug te keren naar mijn effectieve toepassing, in de ISR voor het genereren van het BCM signaal heb ik ook een aantal "external defined variables" die private gedeclareerd zijn in een klasse, wat dus resulteert in dezelfde fout bij het linken.

Steve

Dat gaat niet werken omdat _var onderdeel is van het class object. (Zelfs niet als je die public maakt)
Je moet dan ergens je pointer naar het betreffende object bewaren (en die in de ISR accessen) anders krijg je het niet voor elkaar.

Dan kun je via wat knutselwerk de _var in die class instance bereiken.

Zoiets effe in pseudocode:
object = new Foo()
global_ptr = &object._var

Die global_ptr moet weer ergens in de ISR gebruikt worden.

Maar waarom wil je dit persé in een class doen als je maar een instance fatsoenlijk kunt gebruiken? (althans waar de _var opgehoogd moet worden).

Oh ja: Nog even over dat stukje assembly: Is r1 altijd '0' gedefinieerd door het compiler framework? Waarschijnlijk wel (anders werkt het niet goed), ik heb dat soort constructies ook in de MIPS cpu's gezien.

De code die ik gepost heb is louter ter verduidelijking. Inderdaad de compiler verondersteld dat r1 altijd 0 is.

Voor mijn toepassing heb ik een klasse die er ongeveer zo uit ziet:


class BinaryCodeModulation {

public:
    BinaryCodeModulation();

    begin(uint8_t pins, uint16_t speed);

    void update();
    uint8_t getStatus();
    void setPin(uint8_t value, uint8_t pin);

    void irq_handler();   // zie edit

private:

    volatile uint8_t _bcmStatus;
    uint8_t _bcmBuffer[BCM_NBITS];
};

extern BinaryCodeModulation bcm;

De variabelen "_bcmStatus" en "_bcmBuffer" worden in de assembly interrupt routine gebruikt.



.extern _bcmStatus
.extern _bcmBuffer

.global TIMER_COMPA_vect
TIMER_COMPA_vect
    
    ; code
    reti

In C heb je dan:


static volatile uint8_t _bcmStatus;
static uint8_t _bcmBuffer[BCM_NBITS];

void begin(uint8_t pins, uint16_t speed) {

}

void update() {

}

uint8_t getStatus() {

}

void setPin(uint8_t value, uint8_t pin) {

}

Nu wou ik hetzelfde eens proberen met een klasse in C++ maar daar krijg ik dus tweemaal een fout bij het linken: undefined reference to ... . Ik voel wel ergens aan dat wat ik wil doen niet zo gebruikelijk is en wellicht beter bij de C-versie blijf, maar toch.

[Bericht gewijzigd door steve04 op (19%)]

Ten eerste zijn die variablen Private, dus kun je er niet bij.

Ten tweede moet je om erbij te kunnen de class instance zelf hebben (pointer dus).
Van daar uit kun je dan een eventuele public variable bij.
Maar daarmee gooi je eigenlijk ook het idee van encapsulation wat je met C++ wilt bereiken weg.
Daarvoor zou je dan een "setter" of in jou geval een "incrementer" method moeten maken. Dan kun je die variable toch weer private houden.

De performance wordt er niet echt beter op want je moet via een paar pointer operaties weer vanuit assembly de class method aanroepen die de interne variable update.
Kan wel maar ik weet niet exact hoe je dat vanuit assembly weer aan elkaar gelinked krijgt. De compiler maakt daar van die rare namen van onder water (symbol mangling genoemd).

[Bericht gewijzigd door henri62 op (34%)]

Een mogelijke oplossing zou zijn om te werken met inline assembly in een public function bv. "irq_handler()" die toegevoegd wordt aan de klasse.


void irq_handler() {
   asm volatile(...);
}

Dan kan je via de globale instantie die functie aanroepen in de ISR


ISR(TIMER1_COMPA_vect) {
   bcm.irq_handler();
}

Dit is hoe Arduino het doet met de HardwareSerial alleen wordt er geen inline assembly gebruikt. Ik denk dit eens te gaan proberen want aan de ISR kan je ook de attribute ISR_NAKED meegeven, hierdoor gaat de compiler geen context saving toepassen.

Een tweede mogelijkheid is het voorbeeld volgen van hoe Arduino het doet met de Wire bibliotheek. De basis wordt uitgewerkt in C (zie twi.h en twi.c), waarvan Wire gebruik maakt.

[Bericht gewijzigd door steve04 op (11%)]

Wat jij doet is een class instance maken voor 8 leds tegelijk. De private variable uint8_t _bcmBuffer[BCM_NBITS]; duid hierop. Dat is nu net niet de bedoeling van object oriented iets in elkaar zetten.
Wat je werkelijk zou moeten doen is EEN object maken per LED die je wilt PWM-men. Het probleem is nu dat je dan 8 uint8_t bytes allemaal "los" in iedere class hebt zitten waardoor het nog lastiger wordt.

Als oplossing zou je een class kunnen maken met een static array van 8 bytes (die de PWM waarden voorstellen) een teller die start op 0.
Bij elke nieuw instance hoog je de teller aan het eind op. Elke instance gebruikt nu het volgende byte tot er maximaal 8 instances zijn. Een variant op het zgn singleton design pattern (nu niet een maar 8).

Die array kun je dan eenmalig via een pointer in assembly gebruiken (via een index van 0-7 natuurlijk).

Door de bank genomen wordt het er niet echt overzichtelijker op. Dat is ook precies de reden waarom in een redelijke hekel heb aan C++ op een klein embedded systeem. Wordt als snel een zooitje als je iets fatsoenlijk wilt optimaliseren.

Ik gebruik geen PWM maar Binary Code Modulation. Een voorbeeld, stel de intensiteit voor een led is 10011001 en de laagste on tijd is 1us. Voor bit0 zal de output gedurende 1us hoog zijn, voor bit1 2us laag, voor bit2 4us laag, voor bit3 8us hoog, voor bit4 16us hoog, voor bit5 32us laag, voor bit6 64us laag en tenslotte voor bit7 128us hoog. Je update de output dus 8x per cyclus en geen 256x zoals bij PWM. De array _bcmBuffer bevat voor elke bit de juiste output code. Inderdaad er kunnen max. 8 leds aangestuurd worden want er zijn ook 8 pinnen per poort.

De inhoud van _bcmBuffer zou in dit geval het volgende zijn


00000001 bit0
00000000 bit1
00000000 bit2
00000001 bit3
00000001 bit4
00000000 bit5
00000000 bit6
00000001 bit7

Ik heb de ISR geschreven in assembly omwille van de efficiëntie, slecht 50 clocks (of 3.1us @16MHz).

Inderdaad ik werk ook liever in C maar tegenwoordig zijn bijna alle bibliotheken die je op het net kan vinden voor Arduino in C++ of een mix van C en C++. Dus wou ik ook maar eens proberen.

Komt een beetje op hetzelfde neer alleen zitten de bitjes geroteerd in de "matrix" van 8x8 bits.
Ik zou gewoon voor de leds een globaal array maken en niet moeilijk doen. De rest kan eventueel wel nog in C++.

P.S. Ik heb eens een RC servo sturing met 8 uitgangen gemaakt die met twee timers een resolutie haalt van 250nS. Allemaal onafhankelijk van elkaar. Werkt ook met een stuk geoptimaliseerde assembly.
De rest daar is allemaal C-code.

Het probleem hier is dat elk Foo object zijn eigen _var variable heeft. Dus dan moet je in de interrupt aangeven welke daarvan gebruikt moet worden.

Maar misschien heb je maar een enkel foo object. Dan kan je de _var declareren als static member en dan wordt die wel toegankelijk via een interrupt routine.

Maar de juiste syntax is dan wel even puzzelen.

En dan nog is het veel gemakkelijker om _var als globale variabele te declareren en buiten het object te houden. Dat kan ook in C++.

Op 1 september 2017 17:29:15 schreef steve04:
Ik heb de ISR geschreven in assembly omwille van de efficiëntie, slecht 50 clocks (of 3.1us @16MHz).

Premature optimization is the root of all evil.

Als jij 1 microseconde als "tijdstap" hebt, hoe regel je dat met 3 microseconde aan interrupt tijd?

Ik prefereer om goed over algorithmes na te denken en dan gewoon in C (of C++) de boel te schrijven ipv lastige assembly te gaan schrijven.

Klopt, 1us "tijdstap" is niet mogelijk maar het was ook enkel bedoeld als voorbeeld. Zolang het niet te ingewikkeld wordt vind ik assembly best wel te doen. Hieronder de code:


#include "bcm.h"

;; declare external defined variables
.extern _bcmTmrTopB0
.extern _bcmPinMask
.extern _bcmStatus
.extern _bcmBuffer

;; update output macro
.macro  udo
        lds   wr0, _bcmPinMask              ; wr0 = _bcmPinMask           (2)
        in    wr1, _SFR_IO_ADDR(BCM_PORT)   ; wr1 = BCM_PORT              (1)
        and   wr1, wr0                      ; wr1 &= wr0                  (1)
        ld    wr0, Z                        ; wr0 = *Z                    (2)
        or    wr1, wr0                      ; wr1 |= wr0                  (1)
        out   _SFR_IO_ADDR(BCM_PORT), wr1   ; BCM_PORT = wr1              (1)
.endm

;; delay with a 4-cycle resolution
;; @0 = counter, and @1 = delay N times
;; N = 1 (4), N = 2 (8),...,N = 255 (1020), N = 0 (1024)
.macro  d4c delay
        ldi   wr1, \delay                    ; wr1 = dela                  (1)
1:      dec   wr1                            ; delay--                     (1)
        nop                                  ; no operation                (1)
        brne  1b                             ; branch if not zero          (1/2)
.endm

;; Timer1 Output Compare A Match Interrupt Service Routine
.global TIMER1_COMPA_vect
TIMER1_COMPA_vect:

        ;; context saving
        in    csreg, _SFR_IO_ADDR(SREG)     ; save SREG                   (1)
        push  r30                           ; save ZH:ZL                  (2)
        push  r31                           ;                             (2)

        ;;
        ldi   R30, lo8(_bcmBuffer)          ; ZH:ZL = &(_bcmBuffer)       (1)
        ldi   R31, hi8(_bcmBuffer)          ;                             (1)
        lds   wr0, _bcmStatus               ;                             (2)
        tst   wr0                           ; wr0 &= wr0                  (1)
        breq  fC                            ; branch if zero              (1/2)
        cpi   wr0, BCM_NBITS                ; wr0 - BCM_NBITS             (1)
        breq  nC                            ; branch i zero               (1/2)

        ;; next bit
nB:     clr   wr1                           ; wr1 = 0                     (1)
        add   R30, wr0                      ; R31:R30 += wr0              (1)
        adc   R31, wr1                      ;                             (1)
        inc   wr0                           ; wr0++                       (1)
        sts   _bcmStatus, wr0               ; _bcmStatus = wr0            (2)
        udo                                 ; update output               (8)
        lds   wr0, OCR1AL                   ; wr1:wr0 = OCR1A             (2)
        lds   wr1, OCR1AH                   ;                             (2)
        lsl   wr0                           ; C <- wr0 <- 0               (1)
        rol   wr1                           ; C <- wr1 <- C               (1)
        rjmp  end                           ; jump to end                 (2)

        ;; first cycle (_bcmStatus == 0)
fC:     d4c   1                             ; sync. delay                 (4)
        ;; next cycle  (_bcmStatus == 1..BCM_NBITS)
nC:     d4c   1                             ; sync. delay                 (4)
        ldi   wr0, 1                        ; wr0 = 1                     (1)
        sts   _bcmStatus, wr0               ; _bcmStatus = wr0            (2)
        udo                                 ; update output               (8)
        lds   wr0, _bcmTmrTopB0             ; wr1:wr0 = _bcmTmrTopB0      (2)
        lds   wr1, _bcmTmrTopB0+1           ;                             (2)

        ;; update Timer1 TOP value
end:    sts   OCR1AH, wr1                   ; OCR1A = wr1:wr0             (2)
        sts   OCR1AL, wr0                   ;                             (2)

        ;; restore context
        out   _SFR_IO_ADDR(SREG), csreg     ; restore SREG                (1)
        pop   r31                           ; restore ZH:ZL               (2)
        pop   r30                           ;                             (2)
        reti                                ; return from interrupt       (4)

Enkele toelichtingen: het bestand moet een .S extensie (geen .s) hebben anders wordt de C preprocessor niet uitgevoerd. wr0, wr1 en csreg zijn symbolische namen voor registers r2, r16 en r17. In C kan je ook een variable permanent binden aan een register, de compiler zal dan deze registers voor niks anders gebruiken (dit staat in bcm.h).


register uint8_t wr0 asm("r16");

Op die manier kan de context saving/restore een stuk korter en dat is wel interessant want een push en pop instructie nemen ieder afzonderlijk 2 clocks in beslag. Natuurlijk mag je hierin niet overdrijven, zodat de compiler niet "vastloopt" omdat er te weinig registers overblijven. Wanneer je cijfers gebruikt als labels kan je werken met de toevoeging b (back) of f (forward). Bv. "rjmp 1b" betekend: jump naar het eerste label "1" voor de instructie. "lo8" en "hi8" geven respectievelijk de low en high byte van een 16 bit integer terug. ZH:ZL of r31:r30 kunnen gebruikt worden voor indirect adresseren. Een macro moet eindigen op ".endm" en niet op ".endmacro". Dit komt omdat avr-libc hoofdzakelijk bedoeld is om AVR microcontrollers te kunnen programmeren in C en C++, daarnaast is er ook een gelimiteerde ondersteuning voor assembly. Om dezelfde reden mag je ook niet werken met "@..." als argument in een macro maar moet je gebruik maken van een "\". Tot slot de cijfers tussen de haakjes duiden het aantal clocks aan. De ISR neemt 50 clocks in beslag (rjmp naar de ISR en reti inbegrepen). De output wordt steeds op de 27'ste clock geüpdatet.

De variable _bcmStatus geeft aan welke code op de output staat(0= disabled, 1=bit0,...8=bit7). Hierdoor kan ik in mijn main() werken via "time slots" bv.


void main() {
   
   uint8_t status = bcmGetStatus();

   // 1 stap = 8us, elke taak wordt uitgevoerd aan 490Hz 

   if(status == 4) {
      // taak1, actief voor 8 stappen  (64us)
   }else if(status == 5) {
      // taak2, actief voor 16 stappen (128us)
   }else if(status == 6) {
      // taak3, actief voor 32 stappen (256us)
   }else if(status == 7) {
      // taak4, actief voor 64 stappen (512us)
   }else if(status == 8) {
      // taak5 actief voor 128 stappen (1024us)
   }
}