ATtiny2313A ASM do C: PORTD 0xAA i 0x55, opóźnienie 64 s

Pytanie

Jak wyglądał by ten kod asm w j.C
.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
nop
nop
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

Odpowiedź Sztucznej Inteligencji


Bezpośrednia odpowiedź na pytanie

Tak, ten kod AVR ASM można przepisać na C. Najprostszy i funkcjonalnie równoważny zapis wygląda tak:

#define F_CPU 8000000UL
#include <avr/io.h>
#include <stdint.h>
#include <avr/cpufunc.h>
static void del_64s(void)
{
    volatile uint16_t coarse = 0x07A1;  // odpowiednik R21:R20
    volatile uint16_t inner;
    volatile uint16_t fine  = 0x1FFD;   // odpowiednik R29:R28
    do {
        inner = 0xFFFF;                  // odpowiednik R19:R18
        do {
            inner--;
        } while (inner != 0);
        coarse--;
    } while (coarse != 0);
    do {
        fine--;
    } while (fine != 0);
}
int main(void)
{
    /*
     * W typowym AVR-GCC stos jest inicjalizowany automatycznie
     * przed wejściem do main(), więc ręczne ustawianie SPL
     * zwykle nie jest potrzebne.
     */
    DDRD  = 0xFF;
    PORTD = 0xFF;
    while (1) {
        PORTD = 0xAA;   // 0b10101010
        _NOP();
        _NOP();
        del_64s();
        PORTD = 0x55;   // 0b01010101
        _NOP();
        _NOP();
        del_64s();
    }
}

Kluczowe punkty

  • To jest tłumaczenie funkcjonalne, a nie idealnie cykl-w-cykl.
  • Wartości 16-bitowe są tu:
    • R21:R20 = 0x07A1
    • R19:R18 = 0xFFFF
    • R29:R28 = 0x1FFD
  • Jeśli zależy Ci na identycznym czasie wykonania jak w ASM, najlepiej zostawić procedurę opóźnienia w asemblerze albo użyć timera sprzętowego.

Bezpośrednia odpowiedź na pytanie Szczegółowa analiza problemu

Ten program jest napisany dla AVR, najpewniej ATtiny2313A. Robi trzy rzeczy:

  1. Inicjalizuje stos.

  2. Ustawia cały PORTD jako wyjście.

  3. W pętli nieskończonej wystawia na PORTD naprzemiennie:

    • 0xAA = 10101010
    • 0x55 = 01010101

    z bardzo długim opóźnieniem między zmianami.

Co dokładnie robi oryginalny ASM

Inicjalizacja stosu

ldi r16, low (RAMEND)
out SPL, r16

Dla ATtiny2313A to jest poprawne, bo wystarcza ustawienie SPL.
W C, przy standardowym starcie z AVR-GCC, robi to kod startowy przed main().

Konfiguracja portu

ldi r16, 0xff
out ddrd, r16
out portd, r16

Odpowiednik w C:

DDRD = 0xFF;
PORTD = 0xFF;

Ustawienie liczników opóźnienia

ldi r18, 0xff
ldi r19, 0xff
ldi r20, 0xa1
ldi r21, 0x07

To daje:

  • R19:R18 = 0xFFFF
  • R21:R20 = 0x07A1

Uwaga: kolejność bajtów ma znaczenie.
W AVR para rejestrów R21:R20 oznacza słowo 16-bitowe:

\[
0x07A1
\]

a nie 0xA107.

To był błąd w jednej z przykładowych odpowiedzi.

Główna pętla

ldi r16, 0b1010_1010
out portd, r16
nop
nop
rcall del_64s
ldi r16, 0b0101_0101
out portd, r16
nop
nop
rcall del_64s
rjmp main

W C to naturalnie zapisuje się jako:

while (1) {
    PORTD = 0xAA;
    _NOP();
    _NOP();
    del_64s();
    PORTD = 0x55;
    _NOP();
    _NOP();
    del_64s();
}

Jak działa del_64s

Procedura:

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

Logika jest taka:

  1. Skopiuj licznik z R21:R20 do R25:R24.
  2. Dla każdej iteracji zewnętrznej:
    • załaduj 0xFFFF do R31:R30
    • zliczaj w dół do zera
  3. Po zakończeniu pętli zewnętrznej wykonaj jeszcze krótką pętlę korekcyjną 0x1FFD.

W C najbliższy odpowiednik to:

static void del_64s(void)
{
    volatile uint16_t coarse = 0x07A1;
    volatile uint16_t inner;
    volatile uint16_t fine = 0x1FFD;
    do {
        inner = 0xFFFF;
        do {
            inner--;
        } while (inner != 0);
        coarse--;
    } while (coarse != 0);
    do {
        fine--;
    } while (fine != 0);
}

Dlaczego volatile jest ważne

Bez volatile kompilator może uznać, że puste pętle nic nie robią, i je usunąć.
Wtedy opóźnienie zniknie albo będzie zupełnie inne.

Czy ten kod w C da dokładnie 64 s

Nie. Da zbliżone zachowanie funkcjonalne, ale nie identyczne czasowo.

Powód:

  • ASM ma dokładnie znaną liczbę cykli.
  • C jest kompilowany do innego kodu maszynowego.
  • Wynik zależy od:
    • poziomu optymalizacji -O0, -Os, -O2
    • wersji kompilatora
    • użycia rejestrów przez kompilator

Czyli:

  • ASM: kontrola cykl po cyklu
  • C: kontrola logiczna, ale nie cyklowa

Ile naprawdę trwa opóźnienie w ASM

Dla efektywnego zegara 8 MHz ta procedura została dobrana praktycznie idealnie pod 64 s między kolejnymi zmianami stanu PORTD.

Wartości:

\[
R21:R20 = 0x07A1 = 1953
\]

\[
R19:R18 = 0xFFFF = 65535
\]

\[
R29:R28 = 0x1FFD = 8189
\]

W tej konstrukcji uwzględniono nie tylko same pętle, ale też narzut instrukcji takich jak:

  • NOP
  • RCALL
  • LDI
  • OUT
  • RET

To znaczy, że kod nie jest „przypadkowym” opóźnieniem, tylko ręcznie kalibrowaną zwłoką.


Aktualne informacje i trendy

W praktyce współczesnego programowania AVR taki kod pisze się dziś rzadziej jako „gołe” pętle opóźniające. Zamiast tego stosuje się:

  • timer sprzętowy
  • przerwania
  • ewentualnie krótkie _delay_ms() dla prostych testów

Powody:

  • lepsza powtarzalność
  • mniejsze zużycie CPU
  • łatwiejsza konserwacja kodu
  • mniejsza zależność od optymalizacji kompilatora

Z punktu widzenia inżynierskiego, jeśli chcesz tylko „miganie co 64 s”, to lepszym rozwiązaniem jest timer niż puste pętle.


Wspierające wyjaśnienia i detale

Wersja bardziej „idiomatyczna” w C

Jeśli nie zależy Ci na odwzorowaniu pętli assemblerowych 1:1, tylko na tym, żeby program robił to samo użytkowo, można napisać:

#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <avr/cpufunc.h>
static void del_64s(void)
{
    for (uint16_t i = 0; i < 64000; i++) {
        _delay_ms(1);
    }
}
int main(void)
{
    DDRD  = 0xFF;
    PORTD = 0xFF;
    while (1) {
        PORTD = 0xAA;
        _NOP();
        _NOP();
        del_64s();
        PORTD = 0x55;
        _NOP();
        _NOP();
        del_64s();
    }
}

Ta wersja jest prostsza, ale nadal nie daje takiej samej precyzji jak oryginalny ASM.

Najważniejsze korekty względem przykładowych odpowiedzi

W przesłanych odpowiedziach były istotne nieścisłości:

  • R21:R20 to 0x07A1, nie 0xA107.
  • 2 × NOP to nie _delay_us(10) ani nawet niekoniecznie _delay_us(1).
  • Czas opóźnienia był w przykładach liczony błędnie jako ok. 32 s albo ok. 67 s.
  • W standardowym projekcie AVR-GCC ręczna inicjalizacja SPL w main() jest zazwyczaj zbędna.

Uwaga o zegarze

Dyrektywa:

.equ fosc = 8

nie ustawia fizycznie częstotliwości mikrokontrolera. Ona tylko definiuje stałą w asemblerze.
Rzeczywista częstotliwość zależy od:

  • fuse bitów
  • źródła zegara
  • ewentualnego dzielnika CKDIV8

Jeżeli mikrokontroler realnie nie pracuje z 8 MHz, to opóźnienie nie będzie wynosiło 64 s.


Aspekty etyczne i prawne

W tym przypadku nie ma szczególnych kwestii etycznych ani prawnych, ale są kwestie bezpieczeństwa technicznego:

  • jeśli PORTD steruje przekaźnikami, tranzystorami lub obciążeniem mocy, trzeba zadbać o:
    • separację
    • ograniczenie prądu
    • diody zabezpieczające
    • zgodność poziomów napięć
  • przy pracy z urządzeniami zewnętrznymi należy uwzględnić EMC i odporność na zakłócenia

Praktyczne wskazówki

Jeśli chcesz tylko tłumaczenie ASM na C

Użyj pierwszej wersji z volatile uint16_t.

Jeśli chcesz zachować dokładny timing

Najlepsze opcje są trzy:

  • zostawić procedurę opóźnienia w ASM
  • użyć inline asm
  • najlepiej: użyć timera sprzętowego

Jeśli chcesz rozwiązanie poprawne projektowo

Dla 64 s sensowniejszy jest timer niż pętla programowa, bo:

  • procesor nie marnuje całego czasu na zliczanie
  • można wykonywać inne zadania
  • wynik jest stabilniejszy

Najlepsze praktyki

  • zdefiniuj F_CPU zgodnie z rzeczywistym zegarem
  • sprawdź fuse bity
  • nie polegaj na pustych pętlach C do dokładnego odmierzania długich czasów
  • do krótkich opóźnień używaj _NOP() albo _delay_us()
  • do długich czasów używaj timerów

Ewentualne zastrzeżenia lub uwagi dodatkowe

  • Kod w C pokazany wyżej jest równoważny funkcjonalnie, ale nie binarnie równoważny.
  • Kompilator może wygenerować różny kod zależnie od opcji.
  • Jeśli potrzebujesz opóźnienia „dokładnie co do cyklu”, to zwykłe C nie jest właściwym narzędziem.
  • Jeżeli używasz ATtiny2313A z ustawieniami fabrycznymi, rzeczywista częstotliwość może nie być efektywnie 8 MHz bez odpowiednich fuse bitów.

Sugestie dalszych badań

Warto dalej sprawdzić:

  • jak skonfigurować Timer1 do generowania 1 s i zliczać 64 okresy
  • jak działają fuse bity AVR
  • jak kompilator AVR-GCC tłumaczy pętle volatile
  • jak liczyć cykle instrukcji AVR
  • jak używać przerwań do odmierzania czasu zamiast opóźnień blokujących

Jeżeli chcesz, mogę Ci od razu przygotować także:

  1. wersję w C z timerem,
  2. wersję 1:1 z inline asm,
  3. albo wersję dla konkretnego kompilatora AVR-GCC / Microchip Studio.

Krótkie podsumowanie

  • Ten ASM da się przepisać na C dość prosto.
  • Najbliższy odpowiednik to pętle na volatile uint16_t oraz ustawianie PORTD = 0xAA i PORTD = 0x55.
  • Wartość licznika zewnętrznego to 0x07A1, nie 0xA107.
  • Kod w C będzie poprawny funkcjonalnie, ale nie zagwarantuje identycznego czasu jak ASM.
  • Do dokładnych 64 s w praktycznym projekcie lepiej użyć timera sprzętowego.

Jeśli chcesz, mogę teraz przepisać ten program na „porządne C z Timer1”, bez pętli opóźniających.

Oceń odpowiedź:
Zastrzeżenie: Odpowiedzi udzielone przez sztuczną inteligencję (model językowy) mogą być niedokładne i wprowadzające w błąd. Elektroda nie ponosi odpowiedzialności za dokładność, rzetelność ani kompletność prezentowanych informacji. Wszystkie odpowiedzi powinny być zweryfikowane przez użytkownika.

Zadaj dodatkowe pytanie Sztucznej Inteligencji

Czekaj (2min)...