μBoot-AvrTiny – bootloader dla ATTiny13

Napisano dnia 2.09.2019 r. o godzinie 20:30
Autor: Piotr Sperka

Wstęp

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.

Komunikacja z komputerem

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.

Zapis i odczyt pamięci

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 FLASH

Programowanie każdej strony pamięci FLASH przebiega następująco:

  • Załadowanie tymczasowego bufora 32. bajtami danych.
  • Wyczyszczenie strony.
  • Zapis danych z bufora do strony.

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

Programowanie EEPROM

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

Protokół komunikacyjny

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.

Obsługa bootloadera

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.

Ograniczenia

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.

Podsumowanie

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!