Co można polepszyć w tym kodzie , jak by wyglądał w języku C ,.nolist
.include "tn2313adef.inc"
;.include "m32def.inc"
.list
.cseg
.equ fosc = 8 ;czestotliwosc kwarcu w MHz
.org 0x0000
rjmp ResetProcessor
ResetProcessor:
; ldi r16, high(RAMEND) ; init stack pointer
; out SPH, r16
ldi r16, low (RAMEND) ; init stack pointer
out SPL, r16
ldi r16, 0xff
out ddrd, r16
out portd, r16
ldi r18, 0xff
ldi r19, 0xff
ldi r20, 0xa1
ldi r21, 0x07
main:
ldi r16, 0b1010_1010
out portd, r16
nop
nop
rcall del_64s
ldi r16, 0b0101_0101
out portd, r16
rcall del_64s
rjmp main
del_64s:
MOVW R24, R20
Coarse:
MOVW R30, R18 ;
Loop:
SBIW R30, 0x01 ;
BRNE Loop ;
SBIW R24, 0x01 ;
BRNE Coarse ;
ldi r28, 0xfd
ldi r29, 0x1f
Fine:
SBIW R28, 0x01 ;
BRNE Fine
ret
.exit
Tak — ten kod można wyraźnie polepszyć, głównie pod kątem:
Najważniejsze wnioski:
0xAA i 0x55.del_64s jest ręcznie skalibrowanym opóźnieniem programowym._delay_ms() w pętli,Program dla AVR:
10101010 (0xAA),01010101 (0x55),Czyli funkcjonalnie jest to klasyczne „miganie”, ale nie jedną diodą, tylko całym portem we wzorcu naprzemiennym.
Kod nie jest zły. Ma kilka mocnych stron:
SPL jest poprawna.To ostatnie jest istotne: w ATtiny2313A nie trzeba ustawiać SPH, bo RAM jest mały i stos mieści się w zakresie młodszego bajtu.
Masz w kodzie wartości:
0xff0xa10x070xfd0x1fOne mają sens, ale bez komentarza trudno zrozumieć, dlaczego akurat takie.
Lepiej zapisać je jako stałe opisowe, np.:
.equ PATTERN_A = 0xAA
.equ PATTERN_B = 0x55
.equ DELAY_INNER = 0xFFFF
.equ DELAY_OUTER = 0x07A1
.equ DELAY_FINE = 0x1FFD
To od razu mówi, do czego służą.
To jest najważniejsza praktyczna uwaga.
Masz:
.equ fosc = 8
ale to niczego nie ustawia sprzętowo. To tylko symbol asemblera.
Jeżeli mikrokontroler faktycznie pracuje z innym zegarem niż 8 MHz, to cały czas opóźnienia będzie inny. Dla AVR to bardzo częsty problem, bo fabrycznie często aktywny jest CKDIV8, więc zamiast 8 MHz procesor działa z 1 MHz.
Wtedy Twoje „64 s” stanie się około:
\[
64 \cdot 8 = 512\ \text{s}
\]
czyli ponad 8 minut.
To trzeba zawsze sprawdzać względem:
nop nop niekoniecznie są zbędneNa pierwszy rzut oka można powiedzieć: „usuń nop, są niepotrzebne”.
Ale tutaj trzeba uważać.
Twój kod wygląda na ręcznie dostrojony cyklowo. Te dwa nop prawdopodobnie nie są przypadkowe. Bardzo możliwe, że zostały dodane po to, aby całkowity czas między kolejnymi zapisami do PORTD wynosił dokładnie 64 s przy 8 MHz.
To znaczy:
del_64s dają prawie 64 s,rcall,ret,ldi,out,nop,Zatem tych nop nie usuwałbym bez ponownego przeliczenia cykli.
To ważna korekta: w wielu „ogólnych” odpowiedziach takie instrukcje uznaje się odruchowo za zbędne, ale tutaj najprawdopodobniej pełnią rolę kalibracyjną.
Masz:
ldi r18, 0xff
ldi r19, 0xff
ldi r20, 0xa1
ldi r21, 0x07
a potem w del_64s:
MOVW R24, R20
MOVW R30, R18
To jest poprawne i szybkie, ale oznacza, że:
r18..r21 są de facto „zarezerwowane” dla opóźnienia,del_64s nie jest samowystarczalna,Lepsze są dwa podejścia:
del_64s modyfikuje:
r24:r25r28:r29r30:r31W tym konkretnym programie nie szkodzi, bo po wyjściu i tak nadpisujesz r16, a reszta nie jest dalej używana. Ale jako dobra praktyka:
push/pop),Kod jest „na sztywno” przywiązany do:
Jeżeli kiedyś chcesz zmienić:
to trzeba ręcznie grzebać w kilku miejscach. Lepiej zdefiniować to na początku jako stałe.
To jest najważniejsze z punktu widzenia inżynierskiego.
Obecnie procesor:
W praktyce produkcyjnej dla takich zadań zwykle wybiera się:
To daje lepszą powtarzalność i łatwiej rozwijać program.
Tak — wygląda na to, że tak, i to nieprzypadkowo.
Przy założeniu, że CPU pracuje naprawdę z 8 MHz:
nop najpewniej domykają bilans czasowy.Czyli tu ważna korekta względem części przykładowych odpowiedzi:
Poniżej wersja bardziej czytelna, ale zachowująca tę samą ideę.
.nolist
.include "tn2313adef.inc"
.list
.cseg
.equ PATTERN_A = 0xAA
.equ PATTERN_B = 0x55
; Rejestry pomocnicze
.def tmp = r16
.def inL = r18
.def inH = r19
.def outL = r20
.def outH = r21
.org 0x0000
rjmp ResetProcessor
ResetProcessor:
; Inicjalizacja stosu - dla ATtiny2313A wystarczy SPL
ldi tmp, low(RAMEND)
out SPL, tmp
; Konfiguracja PORTD jako wyjście
ldi tmp, 0xFF
out DDRD, tmp
out PORTD, tmp
; Stałe do opóźnienia 64 s przy 8 MHz
; Uwaga: rzeczywisty czas zależy od fuse bitów i zegara CPU
ldi inL, 0xFF
ldi inH, 0xFF
ldi outL, 0xA1
ldi outH, 0x07
main:
ldi tmp, PATTERN_A
out PORTD, tmp
nop
nop
rcall delay_64s
ldi tmp, PATTERN_B
out PORTD, tmp
rcall delay_64s
rjmp main
delay_64s:
; R24:R25 = licznik zewnętrzny
movw r24, r20
coarse:
; R30:R31 = licznik wewnętrzny
movw r30, r18
loop1:
sbiw r30, 1
brne loop1
sbiw r24, 1
brne coarse
; Drobna korekcja czasu
ldi r28, 0xFD
ldi r29, 0x1F
fine:
sbiw r28, 1
brne fine
ret
.exit
Ta wersja nie zmienia idei działania, ale jest:
W praktyce współczesnego programowania AVR najczęściej stosuje się dziś następujące podejście:
Dla prostych eksperymentów edukacyjnych ręczne pętle opóźniające są nadal przydatne, bo:
SBIW, BRNE, RCALL, RET,Natomiast dla projektu, który ma być rozwijany, lepiej przejść na:
Jeżeli chcesz tylko zachować tę samą funkcję logiczną, to C może wyglądać tak:
#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <stdint.h>
#define PATTERN_A 0xAA
#define PATTERN_B 0x55
static void delay_64s(void)
{
for (uint8_t s = 0; s < 64; ++s) {
for (uint8_t i = 0; i < 100; ++i) {
_delay_ms(10);
}
}
}
int main(void)
{
DDRD = 0xFF;
PORTD = 0xFF;
while (1) {
PORTD = PATTERN_A;
delay_64s();
PORTD = PATTERN_B;
delay_64s();
}
}
_delay_ms(64000)?Bo bardzo duże pojedyncze opóźnienia z _delay_ms() nie są dobrym pomysłem. Lepiej dzielić je na mniejsze fragmenty, np. po 10 ms.
To daje:
Jeżeli chcesz zrobić to poprawnie inżyniersko, to lepsza jest wersja z timerem.
Dla ATtiny2313A można użyć Timer1 w trybie CTC i wygenerować dokładnie 1 s przy 8 MHz.
Przy preskalerze 256:
\[
\frac{8\,000\,000}{256} = 31\,250\ \text{Hz}
\]
Czyli dla 1 sekundy:
\[
OCR1A = 31249
\]
Poniżej wariant bez przerwań, z pollingiem flagi:
#define F_CPU 8000000UL
#include <avr/io.h>
#include <stdint.h>
#define PATTERN_A 0xAA
#define PATTERN_B 0x55
static void timer1_init_1s(void)
{
TCCR1A = 0x00;
TCCR1B = (1 << WGM12) | (1 << CS12); // CTC, preskaler 256
OCR1A = 31249; // 1 s przy 8 MHz
}
static void delay_1s(void)
{
TCNT1 = 0;
TIFR = (1 << OCF1A); // skasowanie flagi przez wpisanie 1
while ((TIFR & (1 << OCF1A)) == 0) {
;
}
}
static void delay_64s(void)
{
for (uint8_t i = 0; i < 64; ++i) {
delay_1s();
}
}
int main(void)
{
DDRD = 0xFF;
PORTD = 0xFF;
timer1_init_1s();
while (1) {
PORTD = PATTERN_A;
delay_64s();
PORTD = PATTERN_B;
delay_64s();
}
}
To podejście ma kilka zalet:
Można, ale nie polecam. Trzeba by pisać puste pętle z volatile, np.:
static void delay_like_asm(void)
{
volatile uint16_t coarse;
volatile uint16_t inner;
volatile uint16_t fine;
for (coarse = 0x07A1; coarse != 0; --coarse) {
for (inner = 0xFFFF; inner != 0; --inner) {
;
}
}
for (fine = 0x1FFD; fine != 0; --fine) {
;
}
}
Problem jest taki, że:
Takie C jest tylko „podobne logicznie”, ale nie jest dobrym sposobem na dokładne opóźnienie.
W tym konkretnym przypadku kwestie etyczne i prawne są marginalne, ale technicznie warto pamiętać o dwóch sprawach:
Jeżeli na PORTD sterujesz diodami LED bezpośrednio, zwróć uwagę na:
To jest ważniejsze niż sam kod. Błąd sprzętowy może uszkodzić układ nawet wtedy, gdy program jest poprawny.
Przy ustawianiu fuse bitów i zegara trzeba zawsze sprawdzać dokumentację konkretnego AVR. Sam kod nie wystarczy — konfiguracja układu ma bezpośredni wpływ na czas.
nop są kalibracyjneNajprostsza metoda weryfikacji:
To jest dużo pewniejsze niż „wydaje mi się, że działa za wolno”.
_delay_ms() jest wygodna, ale nadal blokująca.Dodatkowo ważna korekta merytoryczna do przykładowych odpowiedzi:
Jeżeli chcesz iść krok dalej, warto przećwiczyć:
Dobry kierunek rozwoju byłby taki:
_delay_ms(),To daje pełną ścieżkę od nauki do projektu praktycznego.
PORTD = 0xAA/0x55 i delay_64s().Jeżeli chcesz, mogę w następnym kroku przygotować jeszcze: