Napisano dnia 2.09.2019 r. o godzinie 20:30
Autor: Piotr Sperka
Ostatnimi czasy pracowałem nad bootloaderami dla mikrokontrolerów opartych o architekturę ARM. W wolnym czasie zająłem się czymś w sumie podobnym, jednak z goła innym – postanowiłem napisać bootloader dla ATTiny13, czyli jednego z najmniejszych układów z rodziny AVR. W przeciwieństwie do swojego większego rodzeństwa, ATTiny13 nie posiada sprzętowego wsparcia dla uruchamiania z sektora bootloadera, nie posiada możliwości realokacji tablicy przerwań. Układ nie posiada nawet sprzętowego UARTu, który postanowiłem wykorzystać jako interfejs komunikacyjny. μBoot-AvrTiny (jak nazwałem efekt swojej pracy) w swej najprostszej postaci zajmuje jedynie 80 słów (160 bajtów) i umożliwia wykorzystanie funkcji UART w programie użytkownika.
Do komunikacji z komputerem postanowiłem wykorzystać UART. Przemawia za tym głównie ogromna popularność interfejsu i łatwość jego obsługi praktycznie z każdego komputera, tabletu czy smartfona z przejściówką USB za kilka złotych. Jak już wspomniałem, ATTiny13 nie jest wyposażony sprzętowy UART. Jednak nic straconego – już wiele lat temu Atmel wypuścił notę AVR305, w której opisana została optymalna implementacja programowego UARTu. Wykorzystałem więc gotowy kod ze wspomnianej noty z niewielką modyfikacją pozwalającą wykorzystywać jedynie jedno wyprowadzenie do transmisji w obie strony (half-duplex). Takie połączenie zapewnia dodatkowo „sprzętowe” echo (wszystko co wysyłamy linią Tx, automatycznie wraca do nas linią Rx). Przy zegarze 1,2MHz bez problemu osiągalna jest przepływność 9600 baud/s w standardowej konfiguracji 8N1. Przepływność można regulować przy pomocy stałej baud umieszczonej na początku kodu. Wzór do jej obliczenia znajduje się we wspomnianej wyżej nocie AVR305.
Mikrokontrolery AVR umożliwiają programowanie samego siebie (ang. self-programming), dzięki czemu w ogóle możliwe jest działanie bootloadera. Należy jednak pamiętać, że funkcję tę trzeba aktywować we fuse-bitach. Programowanie pamięci FLASH musi odbywać się stronami, dodatkowo przed zapisaniem danych na stronie trzeba ją wyczyścić – wynika to ze sposobu działania pamięci FLASH. W przypadku omawianego mikrokontrolera, strona ma 32 bajty. EEPROM nie ma już takich obostrzeń i programowanie może odbywać się zarówno stronami (w tym wypadku 4 bajty), jak i bajt po bajcie.
Programowanie każdej strony pamięci FLASH przebiega następująco:
Odpowiednio skomentowany fragment kodu odpowiedzialny za zapis strony pamięci wygląda następująco:
_PROGRAM_PAGE: ldi R25, 0x10 ; 16 words to write into flash ldi R24, 0x01 ; New value for SPMCSR register rcall getchar ; Get more significant part of Z register mov R31, Rxbyte rcall getchar ; Get less significant part of Z register mov R30, Rxbyte _PP_GDL: rcall getchar ; Get Data Loop, get more significant byte of word mov R0, Rxbyte rcall getchar ; Get less significant byte of word mov R1, Rxbyte out SPMCSR, R24 spm ; Write word to temp buffer dec R25 breq _PP_ERASE inc R30 ; Increment PCWORD by 2 (Z reg) inc R30 rjmp _PP_GDL ; Erase page _PP_ERASE: ldi R24, 0x03 ; New value for SPMCSR out SPMCSR, R24 spm ; Write flash ldi R24, 0x05 ; New value for SPMCSR andi R30, 0xE0 ; Zero PCWORD in Z out SPMCSR, R24 spm ; Send response ldi Txbyte, 'Y' rcall putchar
Odczyt danych z pamięci FLASH jest już znacznie prostszy. Poniższy kod powinien tłumaczyć się sam:
_READ_FLASH: ldi R24, LOW(2*(FLASHEND + 1)) ldi R25, HIGH(2*(FLASHEND + 1)) clr R30 clr R31 _RFL: lpm Txbyte, Z+ rcall putchar sbiw R24, 1 brne _RFL
W bardziej rozbudowanej wersji bootloadera możliwe jest również odczytywanie i programowanie pamięci EEPROM. Zarówno odczyt, jak i zapis, możliwy jest bajt po bajcie, w związku z czym kod jest znacznie prostszy niż w przypadku pamięci programu:
_READ_EEPROM: clr R25 _REL: sbic EECR, EEPE ; Wait for completion of previous write rjmp _READ_EEPROM out EEARL, R25 ; Set up address (r17) in address register sbi EECR,EERE ; Start eeprom read by writing EERE in Txbyte, EEDR ; Read data from data register rcall putchar inc R25 cpi R25, (EEPROMEND + 1) ; EEPROM is 64 bytes, so we need only one register to count brne _REL rjmp _READ_LOOP _WRITE_EEPROM: sbic EECR, EEPE ; Wait for completion of previous write rjmp _WRITE_EEPROM ldi R16, (0<<EEPM1)|(0<<EEPM0) ; Set Programming mode out EECR, R16 rcall getchar ; Set up address (r17) in address register out EEARL, Rxbyte rcall getchar ; Write data (r16) to data register out EEDR, Rxbyte sbi EECR, EEMPE ; Write logical one to EEMPE sbi EECR, EEPE ; Start eeprom write by setting EEPE ; Send response ldi Txbyte, 'Y' rcall putchar rjmp _READ_LOOP
Nasz mikrokontroler wyposażony jest w jedynie 1kB pamięci programu, stąd położyłem duży nacisk na jak najmiejszy rozmiar bootloadera. To z kolei prowadzi to wniosku, że protokół komunikacyjny musi być naprawdę prosty. Aby odczytać całą pamięć FLASH, należy wysłać znak „R”. W odpowiedzi otrzymamy 1024 bajty odpowiadające zawartości pamięci. Zapis jest odrobinę bardziej złożony. Na początek należy wysłać znak „P”, a następnie ramkę:
Z = (NUMER_STRONY_FLASH << 5) & 0xFFFF
Zaprogramowanie strony jest potwierdzane wysłaniem znaku „Y”. Jeśli odpowiedzi nie ma, znaczy że coś poszło nie tak (np. wysłaliśmy za mało danych).
Gdy wkompilowana jest obsługa pamięci EEPROM, odczyt działa podobnie jak w przypadku pamięci programu. Po odebraniu znaku „E”, urządzenie odeśle 64 bajty z zawartością pamięci. Programowanie polega na wysłaniu znaku „F”, a następnie wysłaniu dwóch znaków: adresu (0-63) i danych (0-255). Sukces zostanie potwierdzony odesłanym znakiem „Y”.
Ponieważ wykorzystujemy programowy UART, układ nie jest w stanie odbierać nowych bitów podczas przetwarzania wcześniejszych danych. Powoduje to, że wysyłanie z komputera danych w postaci tablicy bajtów (czyli ciągiem, bez przerw pomiędzy bajtami) najczęściej kończy się błędami transmisji. Remedium na ten problem jest wysyłanie danych bajt po bajcie, za każdym razem wywołując funkcję flush.
Powyżej zamieściłem przykładowy schemat układu w którym może działać bootloader. Dioda podłączona do PB3 służy tylko i wyłącznie do testów, natomiast C1 i R2 można teoretycznie wyeliminować. Aby przetestować działanie bootloadera, a także ułatwić korzystanie z niego, napisałem bardzo prosty program w Pythonie. Program ten umożliwia odczytanie zawartości obu pamięci do pliku binarnego, a także zapis do pamięci z plików binarnych. Pliki hex nie są obecnie wspierane, jednak po pierwsze są dostępne programy konwertujące hex do bin, a po drugie nic nie stoi na przeszkodzie dopisania takiej funkcji w razie potrzeby.
Program dba, aby nie nadpisać bootloadera wgrywanym programem, dba również o podział pliku binarnego na odpowiednie strony i wysyłanie danych do układu. Zapewnia również dokonanie jedynej niezbędnej zmiany w pliku binarnym – modyfikuje instrukcję rjmp pod adresem 0x00 (adres od którego startuje mikrokontroler po resecie) tak, by układ skoczył do bootloadera. Normalnie w tym miejscu umieszczamy instrukcję skoku do początku programu.
Aby uruchomić bootloader, należy zewrzeć linię danych do masy (przycisk Boot), zresetować mikrokontroler (przycisk Reset), puścić przecisk Reset, a na koniec puścić przycisk Boot. Uruchomienie w normlanym trybie odbywa się poprzez zresetowanie mikrokontrolera z linią danych w stanie wysokim.
Prezentowany bootloader jest bardzo prosty, i choć spełnia swoją funkcję, trzeba być świadomym pewnych ograniczeń. Po pierwsze program musi rozpoczynać się pod adresem 0x0A (bezpośrednio po tablicy przerwań), gdyż do tego miejsca skacze bootloader, aby uruchomić aplikację użytkownika. Można to oczywiście zmienić kompilując swoją wersję bootloadera. Po drugie, możliwe jest nadpisanie (uszkodzenie) kodu bootloadera, poprzez próbę zapisu stron, na których się znajduje. To po stronie komputera (np. wspomnianego pythonowego programu) jest zadbanie, aby nadpisywać tylko te strony, które są do tego przeznaczone. Po stronie komputera jest również takie zmodyfikowanie programu, aby ten po resecie skakał do bootloadera.
Podsumowując, udało mi się napisać prosty, lecz w pełni funkcjonalny bootloader dla mikrokontrolera ATTiny13, mieszczący się w jedynie 80 słowach kodu. Dodatkowo część bootloadera (funkcje wysyłające i odbierające dane poprzez UART) można wykorzystać w aplikacji użytkownika. Jeżeli temat Cię zainteresował i chcesz go wypróbować, zapraszam Cię na mojego GitHuba. Znajdziesz tam źródła i skompilowane wersje bootloadera wraz z testowymi aplikacjami (miganie LEDem oraz wykorzystanie funkcji UART z bootloadera) i skryptem Pythonowym: https://github.com/PiotrSperka/avrTinyBootloader.
Na koniec standardowo – jeżeli masz jakiekolwiek uwagi, pytania lub propozycje, zapraszam do kontaktu. Do zobaczenia w następnym artykule!