Od dłuższego czasu napisałem sobie specjalne makra pod AVR definiujące wszystkie porty, aby np. zamiast pisać:
móc napisać:
Makra te definiują w stałej np. A0 wszystkie informacje o tym porcie, tj:
- adres rejestru DDR dla A0 (DDR),
- adres rejestru PORT dla A0 (PORTA),
- adres rejestru PIN dla A0 (PINA),
- numer portu dla A0 (0).
Każda z tych zmiennych jest 8 bitowa, potrzebujemy więc 32 bity:
i teraz jeśli chcemy się dowiedzieć np. jaki jest rejestr DDR dla końcówki A0, po prostu wyłuskujemy to, np. makrem:
zatem makro as_output wygląda tak:
Ponadto sprytny kompilator C widząc, że wszystko dzieje się na stałych zoptymalizuje kod tak, że wywołanie tych funkcji uprości się do dwóch rozkazów assemblera, np:
czyli w zasadzie wyjdzie tak samo, jakbyśmy napisali.
Dużo czytelniej, a tak samo szybko. Często jednak kusi nas do tego stopnia, że zamiast przekazywać takie końcówki wprost do funkcji, chcielibyśmy stworzyć ich tablicę, np:
Słowem klucz jest tutaj const – jeśli dodamy je przed deklaracja tablicy, kompilator uzna, że wartości tablicy nie będą się zmieniać, więc będzie mógł od razu stwierdzić, że powyższe wywołanie będzie równoznaczne temu z przykładu wyżej. Często jednak chcemu np. iterować po takiej tablicy lub wręcz zmieniać jej wartość w trakcie programu – wtedy nie możemy dać const. I tutaj pojawia się mój kłopot – kompilator wtedy nie może rozwinąć takiego makra do jednej instrukcji assemblera, lecz musi te wszystkie przesunięcia bitowe zamienić na rozkazy procesora. I tu pojawia się problem – kompilator zamienia to w taki sposób, że już instrukcja ustawienia bitu nie jest tożsama z instrukcją sbi/cbi z assemblera. Kompilator najpierw odczytuje do rejestru wartość np. zmiennej DDRC, portem ustawia w niej odpowiedni bit, a potem tak zmodyfikowaną wartość spowrotem zapisuje do rejestru DDR. Problem jest jednak taki, że gdy działamy na przerwaniach, to takie ciągi instrukcji mogą być przerwane przerwaniami i w rzeczywistości dostajemy niezłą sieczkę na portach – sam tego doświadczyłem, bo gdy program testował joystick, to wartość na wyświetlaczu siedmiosegmentowym, sterowanym w przerwaniu zmieniała się jak szalona. W obecnej chwili rozwiązałem to tak, że przed każda z instrukcji np. as_input(...), as_output(...), out(.., 0), out(..., 1), in(...) wyłączam przerwania, a po – włączam. Jest to jednak nieekonomiczne, macie jakiś pomysł jak zmusić kompilator aby kompilował te instrukcje z wykorzystaniem rozkazów sbi/cbi ?
W obecnej chwili – np. gdy przed deklaracją tablicy wręcz dodam „volatile”, kompilator kompiluje mi to do:
Code:
#define BTN1_DDR DDRC
#define BTN1_PORT PORTC
#define BTN1_PIN PINC
#define BTN1_P PC0
#define BTN2_DDR DDRB
#define BTN2_PORT PORTB
#define BTN2_PIN PINB
#define BTN2_P PB1
#define BTN3_DDR DDRD
#define BTN3_PORT PORTD
#define BTN3_PIN PIND
#define BTN3_P PD7
...
cbi(BTN1_DDR, BTN1_P) //przycisk BTN1 jako wejście
sbi(BTN1_PORT, BTN1_P); //włącz rezystor obciągający na BTN1
cbi(BTN2_DDR, BTN2_P) //przycisk BTN2 jako wejście
sbi(BTN2_PORT, BTN2_P); //włącz rezystor obciągający na BTN2
cbi(BTN3_DDR, BTN3_P) //przycisk BTN3 jako wejście
sbi(BTN3_PORT, BTN3_P); //włącz rezystor obciągający na BTN3
while (get_bit(BTN1_PIN, BTN1_P)); //czekaj aż przycisk BTN1 nie zostanie wciśnięty
móc napisać:
Code:
#define BTN1 C0
#define BTN2 B1
#define BTN3 D7
as_input(BTN1, PULLUP_ON);
as_input(BTN2, PULLUP_ON);
as_input(BTN3, PULLUP_ON);
while (!in(BTN1));
Makra te definiują w stałej np. A0 wszystkie informacje o tym porcie, tj:
- adres rejestru DDR dla A0 (DDR),
- adres rejestru PORT dla A0 (PORTA),
- adres rejestru PIN dla A0 (PINA),
- numer portu dla A0 (0).
Każda z tych zmiennych jest 8 bitowa, potrzebujemy więc 32 bity:
Code:
#define A0 (PA0 | CAST1(&PORTA) << 8 | CAST1(&PINA) << 16 | CAST1(&DDRA) << 24)
i teraz jeśli chcemy się dowiedzieć np. jaki jest rejestr DDR dla końcówki A0, po prostu wyłuskujemy to, np. makrem:
Code:
#define DDR(name) \
(*(volatile uint8_t*)(int)(((name) >> 24) & 0xFF))
zatem makro as_output wygląda tak:
Code:
#define as_output(pinname) \
sbi(DDR(pinname), P(pinname))
Ponadto sprytny kompilator C widząc, że wszystko dzieje się na stałych zoptymalizuje kod tak, że wywołanie tych funkcji uprości się do dwóch rozkazów assemblera, np:
Code:
00000024: 98A0 CBI 0x14,0 Clear bit in I/O register
+00000025: 9AA8 SBI 0x15,0 Set bit in I/O register
18: as_input(BTN2, PULLUP_ON);
czyli w zasadzie wyjdzie tak samo, jakbyśmy napisali.
Code:
cbi ddrc, 0
sbi portc, 0
Dużo czytelniej, a tak samo szybko. Często jednak kusi nas do tego stopnia, że zamiast przekazywać takie końcówki wprost do funkcji, chcielibyśmy stworzyć ich tablicę, np:
Code:
.
const uint32_t btns[] = {BTN1, BTN2, BTN3};
as_input(btns[0], PULLUP_ON);
as_input(btns[1], PULLUP_ON);
as_input(btns[2], PULLUP_ON);
Słowem klucz jest tutaj const – jeśli dodamy je przed deklaracja tablicy, kompilator uzna, że wartości tablicy nie będą się zmieniać, więc będzie mógł od razu stwierdzić, że powyższe wywołanie będzie równoznaczne temu z przykładu wyżej. Często jednak chcemu np. iterować po takiej tablicy lub wręcz zmieniać jej wartość w trakcie programu – wtedy nie możemy dać const. I tutaj pojawia się mój kłopot – kompilator wtedy nie może rozwinąć takiego makra do jednej instrukcji assemblera, lecz musi te wszystkie przesunięcia bitowe zamienić na rozkazy procesora. I tu pojawia się problem – kompilator zamienia to w taki sposób, że już instrukcja ustawienia bitu nie jest tożsama z instrukcją sbi/cbi z assemblera. Kompilator najpierw odczytuje do rejestru wartość np. zmiennej DDRC, portem ustawia w niej odpowiedni bit, a potem tak zmodyfikowaną wartość spowrotem zapisuje do rejestru DDR. Problem jest jednak taki, że gdy działamy na przerwaniach, to takie ciągi instrukcji mogą być przerwane przerwaniami i w rzeczywistości dostajemy niezłą sieczkę na portach – sam tego doświadczyłem, bo gdy program testował joystick, to wartość na wyświetlaczu siedmiosegmentowym, sterowanym w przerwaniu zmieniała się jak szalona. W obecnej chwili rozwiązałem to tak, że przed każda z instrukcji np. as_input(...), as_output(...), out(.., 0), out(..., 1), in(...) wyłączam przerwania, a po – włączam. Jest to jednak nieekonomiczne, macie jakiś pomysł jak zmusić kompilator aby kompilował te instrukcje z wykorzystaniem rozkazów sbi/cbi ?
W obecnej chwili – np. gdy przed deklaracją tablicy wręcz dodam „volatile”, kompilator kompiluje mi to do:
Code:
0000002F: 93DF PUSH R29 Push register on stack
+00000030: 93CF PUSH R28 Push register on stack
+00000031: B7CD IN R28,0x3D In from I/O location
+00000032: B7DE IN R29,0x3E In from I/O location
+00000033: 972C SBIW R28,0x0C Subtract immediate from word
+00000034: B60F IN R0,0x3F In from I/O location
+00000035: 94F8 CLI Global Interrupt Disable
+00000036: BFDE OUT 0x3E,R29 Out to I/O location
+00000037: BE0F OUT 0x3F,R0 Out to I/O location
+00000038: BFCD OUT 0x3D,R28 Out to I/O location
17: volatile uint32_t btns[] = {BTN1, BTN2, BTN3};
+00000039: 01DE MOVW R26,R28 Copy register pair
+0000003A: 9611 ADIW R26,0x01 Add immediate to word
+0000003B: E6E0 LDI R30,0x60 Load immediate
+0000003C: E0F0 LDI R31,0x00 Load immediate
+0000003D: E08C LDI R24,0x0C Load immediate
+0000003E: 9001 LD R0,Z+ Load indirect and postincrement
+0000003F: 920D ST X+,R0 Store indirect and postincrement
+00000040: 5081 SUBI R24,0x01 Subtract immediate
+00000041: F7E1 BRNE PC-0x03 Branch if not equal
19: as_input(btns[0], PULLUP_ON);