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
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();
}
}
R21:R20 = 0x07A1R19:R18 = 0xFFFFR29:R28 = 0x1FFDTen program jest napisany dla AVR, najpewniej ATtiny2313A. Robi trzy rzeczy:
Inicjalizuje stos.
Ustawia cały PORTD jako wyjście.
W pętli nieskończonej wystawia na PORTD naprzemiennie:
0xAA = 101010100x55 = 01010101z bardzo długim opóźnieniem między zmianami.
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().
ldi r16, 0xff
out ddrd, r16
out portd, r16
Odpowiednik w C:
DDRD = 0xFF;
PORTD = 0xFF;
ldi r18, 0xff
ldi r19, 0xff
ldi r20, 0xa1
ldi r21, 0x07
To daje:
R19:R18 = 0xFFFFR21:R20 = 0x07A1Uwaga: 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.
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();
}
del_64sProcedura:
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:
R21:R20 do R25:R24.0xFFFF do R31:R300x1FFD.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);
}
volatile jest ważneBez 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.
Nie. Da zbliżone zachowanie funkcjonalne, ale nie identyczne czasowo.
Powód:
-O0, -Os, -O2Czyli:
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:
NOPRCALLLDIOUTRETTo znaczy, że kod nie jest „przypadkowym” opóźnieniem, tylko ręcznie kalibrowaną zwłoką.
W praktyce współczesnego programowania AVR taki kod pisze się dziś rzadziej jako „gołe” pętle opóźniające. Zamiast tego stosuje się:
_delay_ms() dla prostych testówPowody:
Z punktu widzenia inżynierskiego, jeśli chcesz tylko „miganie co 64 s”, to lepszym rozwiązaniem jest timer niż puste pętle.
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.
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).SPL w main() jest zazwyczaj zbędna.Dyrektywa:
.equ fosc = 8
nie ustawia fizycznie częstotliwości mikrokontrolera. Ona tylko definiuje stałą w asemblerze.
Rzeczywista częstotliwość zależy od:
CKDIV8Jeżeli mikrokontroler realnie nie pracuje z 8 MHz, to opóźnienie nie będzie wynosiło 64 s.
W tym przypadku nie ma szczególnych kwestii etycznych ani prawnych, ale są kwestie bezpieczeństwa technicznego:
Użyj pierwszej wersji z volatile uint16_t.
Najlepsze opcje są trzy:
Dla 64 s sensowniejszy jest timer niż pętla programowa, bo:
F_CPU zgodnie z rzeczywistym zegarem_NOP() albo _delay_us()Warto dalej sprawdzić:
volatileJeżeli chcesz, mogę Ci od razu przygotować także:
volatile uint16_t oraz ustawianie PORTD = 0xAA i PORTD = 0x55.0xA107.Jeśli chcesz, mogę teraz przepisać ten program na „porządne C z Timer1”, bez pętli opóźniających.