Strona główna Podstawy teoretyczne Poradniki Przykłady

AVR-GCC - dane w pamięci FLASH

Wstęp Podstawy teoretyczne Adresowanie pamięci FLASH Adres słowa Adres bajtu Adresowanie rozszerzone Troszkę podstaw z procesu budowania programu Obsługa danych w pamięci FLASH Dane w pierwszym segmencie 64KiB Pojedyncza wartość liczbowa Tablica wartości stałych Ciągi znaków (string) Tablica struktur Dane w pierwszym segmencie 64KiB – podsumowanie Dane w segmentach powyżej 64KiB Umieszczanie danych w pamięci FLASH Definicja przy pomocy atrybutu PROGMEM Definicja przy użyciu kwalifikatorów __flash i __memx Użycie kwalifikatorów __flashN Dane w zdefiniowanej sekcji Funkcje uniwersalne z wykorzystaniem kwalifikatora __memx

 

Wstęp

Język C w zasadzie został stworzony dla architektury Von Neumann, w której dane i kod wykonywalny współdzielą tę samą przestrzeń adresową. W związku z tym standard języka C nie przewiduje mechanizmów czy też słów kluczowych, które wspierałyby obsługę oddzielnych przestrzeni adresowych. Kompilatory przeznaczone dla architektury Harwardzkiej muszą stosować różnego rodzaju sztuczki, aby się z tą obsługą uporać.

W AVR-GCC początkowo zostało to rozwiązane za pomocą nadawania atrybutu PROGMEM danym przeznaczonym do zapisu w pamięci programu (FLASH). Metoda ta wymaga jednak zastosowania kłopotliwych w użyciu makr. Kłopotliwych nie tylko ze względu na pogorszenie czytelności kodu, ale także ze względu na utratę przez kompilator kontroli nad typami danych odczytywanymi w ten sposób.

Później, od wersji 4.7, zostało dodane (oprócz PROGMEM) inne rozwiązanie. Polega ono na skierowaniu danych do umieszczenia we FLASH poprzez nadanie stałej w momencie definicji kwalifikatora __flash lub __memx. Kwalifikator taki zostaje przypisany do konkretnej stałej, dzięki czemu składnia odczytu danych z pamięci FLASH praktycznie jest taka sama, jak przy odczycie zmiennych z pamięci RAM (kompilator wie, skąd odczytywać dane na podstawie kwalifikatora) i dodatkowo zostaje zachowana w pełni kontrola typów.

Obecnie mamy dwie opcje obsługi danych w pamięci FLASH. Pierwsza z nich jest nieco kłopotliwa w użyciu ze względu na konieczność stosowania makr zmniejszających czytelność kodu i utratę kontroli nad typami. Druga metoda nadal znajduje się w fazie testów, więc można mieć obawy, że nie wszystko działa zawsze zgodnie z założeniem. Osobiście korzystam z tej metody od dłuższego czasu i nie spotkałem się z sytuacją, żeby coś działało nieprawidłowo. Moim skromnym zdaniem warto znać tę drugą, nowszą metodę ze względu na lepszą czytelność kodu i zachowanie kontroli typów.

Postaram się tutaj opisać i porównać obydwie metody oraz wskazać ewentualne problemy, jakie można napotkać przy ich stosowaniu.

Podstawy teoretyczne

Na początek chciałbym opisać kilka istotnych dla przedmiotu sprawy zagadnień. Ich znajomość pozwoli lepiej zrozumieć, na czym polega problematyka odczytu danych z pamięci programu.

• Adresowanie pamięci FLASH

Pojemność pamięci FLASH w ośmiobitowych mikrokontrolerach AVR podawana jest wprawdzie w bajtach, jednak instrukcje mikrokontrolera mają rozmiar dwóch lub czterech bajtów. W związku z tym pamięć programu zorganizowana jest jako słowa 16-bitowe. Wynika z tego oczywiście, że w mikrokontrolerze o pojemności N[KiB] można umieścić maksymalnie N/2 instrukcji.

Pomimo takiej organizacji pamięci FLASH, możliwe jest także odczytanie pojedynczego bajtu.

◦ Adres słowa

Wszystkie instrukcje sterujące wykonywaniem programu, takie jak instrukcje skoku warunkowego i bezwarunkowego (względnego i bezwzględnego) czy też wywołania podprocedury, używają adresu słowa.

Rejestr PC (Program Counter) również zawiera adres słowa – adres aktualnie wykonywanej instrukcji. Jego szerokość (w bitach) jest ściśle związana z pojemnością pamięci FLASH w słowach.

Za pomocą 16-bitowego wskaźnika czy też rejestru PC można zaadresować maksymalnie 128KiB (64Ki słów) pamięci FLASH.

◦ Adres bajtu

Instrukcja odczytu danych z pamięci FLASH (LPMLoad Program Memory) korzysta z adresu bajtu. Adres ten musi zostać umieszczony w rejestrze wskaźnikowym Z (R31:R30). Adres taki jest tworzony poprzez pomnożenie adresu słowa przez 2 (lub przesunięcie bitowe o jeden bit w lewo). Wartość najmniej znaczącego bitu decyduje o tym, który bajt słowa instrukcja ma odczytać:

Za pomocą 16-bitowego wskaźnika Z można zaadresować maksymalnie 64KiB pamięci FLASH.

◦ Adresowanie rozszerzone

Generalnie rejestry wskaźnikowe w ośmiobitowych mikrokontrolerach AVR mają szerokość 16 bitów. Pozwala to na zaadresowanie maksymalnie 64KiB danych w pamięci. W zupełności wystarcza to do obsługi pamięci RAM, jednak istnieją mikrokontrolery, w których pojemność pamięci FLASH kilkakrotnie przekracza tę wartość.

W jaki sposób, w takim przypadku, zaadresować dane spoza limitu 64KiB?

Otóż mikrokontrolery z tak dużymi pojemnościami FLASH, oprócz rejestru PC o dopowiedniej dla danej pojemności ilości bitów, mają dodatkowe rejestry rozszerzające rejestr Z o ilość bitów potrzebną do zaadresowania całego obszaru pamięci programu. Tymi rejestrami są:

• Troszkę podstaw z procesu budowania programu

Przedstawię tutaj tylko wybrane wiadomości w dużym uproszczeniu. Chodzi tylko o ogólne zrozumienie zasad rozmieszczania danych i kodu w pamięci programu przez linker.

Pisany przez nas program musi zawierać co najmniej jeden plik z kodem źródłowym (z rozszerzeniem *.c). Może też być ich więcej. Pliki te nie są bezpośrednio przetwarzane do pliku wykonywalnego. Każdy plik z kodem źródłowym jest najpierw poddany działaniu preprocesora, a następnie przetworzony przez kompilator do pliku obiektowego (plik z rozszerzeniem *.o). Następnie wszystkie pliki obiektowe powstałe w wyniku kompilacji naszych plików źródłowych (nawet, jeśli jest tylko jeden taki plik) zostają skierowane do linkera, który je łączy w jeden plik wynikowy (plik z rozszerzeniem *.elf). Pliki z rozszerzeniami *.hex oraz *.eep, którymi programujemy mikrokontroler powstają w wyniku wydobycia poprzez program objcopy (w naszym przypadku będzie to avr-objcopy) odpowiednich danych właśnie z pliku *.elf.

Nasz plik źródłowy zawiera jednak różne dane: definicje zmiennych w RAM, dane do umieszczenia w pamięci FLASH lub EEPROM oraz kod funkcji. Kompilator musi więc te dane odpowiednio oznaczyć, aby linker mógł je prawidłowo zidentyfikować i rozmieścić. I tak na przykład zmienne w RAM są oznaczane przez kompilator w pliku obiektowym jako sekcja ”.data”, dane w pamięci programu – jako sekcja ”.progmem.data”, dane przeznaczone dla EEPROM – jako sekcja ”.eeprom” a kod funkcji – jako sekcja ”.text” (która docelowo też jest przeznaczona do zapisu w pamięci FLASH).

Pliki obiektowe z kompilatora są plikami wejściowymi dla linkera. Plik wynikowy jest plikiem wyjściowym i również posiada różne sekcje. Nie jest jednak tak, że sekcje z plików obiektowych zawsze pokrywają się z sekcjami w pliku wynikowym. O tym, jakie będą relacje między sekcjami wejściowymi i sekcjami wyjściowymi (czyli w których sekcjach wyjściowych zostaną umieszczone poszczególne sekcje wejściowe i w jakiej kolejności) decydują opcje linkera w linii poleceń oraz specjalny plik konfiguracyjny, tak zwany skrypt linkera (zawierający kod w języku linker command language).

Przykładowo w domyślnych skryptach linkera dla AVR zarówno sekcja wejściowa ”.text” jak i (między innymi) sekcje wejściowe ”.vectors” oraz ”.progmem.data” są przeznaczone do tej samej sekcji wyjściowej ”.text”. Skrypt linkera decyduje również o kolejności umieszczenia poszczególnych sekcji wejściowych w sekcji wyjściowej. Przykładowo standardowa kolejność w domyślnych skryptach linkera dla AVR to:

Oczywiście to tak w uproszczeniu. Pominąłem tutaj kilka (zapewne nie mniej istotnych) sekcji, aby zbytnio nie komplikować tematu.

Opcje i skrypty linkera to temat na osobny, całkiem obszerny artykuł, więc nie będę tutaj opisywał tego szczegółowo. Chodzi tylko o zrozumienie pewnych ogólnych zasad.

Obsługa danych w pamięci FLASH

Postaram się teraz opisać zasady obsługi danych w pamięci programu w różnych sytuacjach przy zastosowaniu obydwu z metod oraz z możliwie dokładnym opisem. Przedstawione przykłady kodu mają za zadanie tylko pokazać zasady zapisu i odczytu danych i nie należy dopatrywać się w nich głębszego sensu, choć starałem się, aby były kompletne i działały prawidłowo w symulatorze Atmel Studio 7 (standard języka -std=gnu99). Niektóre proste przykłady do prawidłowego działania mogą wymagać wyłączenia optymalizacji, gdyż kompilator, zamiast zapisywać dane we FLASH, będzie je traktował jak makra zdefiniowane za pomocą dyrektywy #define.

• Dane w pierwszym segmencie 64KiB

W rzeczywistości należy pamiętać o tym, że w większości przypadków w pierwszej kolejności w pamięci FLASH (począwszy od adresu 0) muszą być zapisane wektory przerwań (chyba że zmieniliśmy to stosując odpowiednie ustawienia fusbitów i rejestrów). W związku z tym limit 64KiB będzie nieco mniejszy, gdyż musimy od niego odjąć rozmiar wektorów przerwań (zależny od typu mikrokontrolera). Oczywistym jest też fakt, że ograniczenie to dotyczy tylko mikrokontrolerów o pojemności większej niż 64KiB.

Jeśli więc piszemy program na mikrokontroler o pojemności mniejszej lub równej 64KiB lub nasze dane do umieszczenia w pamięci FLASH nie przekraczają łącznie 64KiB (pomniejszonych ewentualnie o rozmiar wektorów przerwań), wystarczy nam znajomość atrybutu PROGMEM (oraz makr z grupy pgm_read_xxx(); ) lub kwalifikatora __flash.

Właściwie w obydwu przypadkach definicji, tak za pomocą atrybutu PROGMEM jak i przy użyciu kwalifikatora __flash, kompilator umieści dane w sekcji ”.progmem.data”. Różnica polega tylko na sposobie dostępu do tych danych w kodzie źródłowym.

◦ Pojedyncza wartość liczbowa

Wprawdzie w języku C zwykle pojedyncze stałe definiujemy raczej przy użyciu dyrektywy preprocesora #define, jednak mimo wszystko postanowiłem pokazać, jak zapisać je w pamięci FLASH, a następnie odczytać, bo być może to pozwoli lepiej zrozumieć następne przykłady.

Definicja przy pomocy atrybutu PROGMEM

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3. #include <util/atomic.h>
  4.  
  5. // definicja pojedynczej wartości całkowitej
  6. const uint16_t pulse_width PROGMEM = 5689;
  7. // zmienna w RAM
  8. uint16_t pulse_width_ram;
  9. // definicja funkcji
  10. void set_pwm_P(const uint16_t pw)
  11. {
  12. ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
  13. {
  14. OCR1A = pw;
  15. }
  16. }
  17.  
  18. int main(void)
  19. {
  20. // bezpośredni odczyt wartości całkowitej (FLASH->RAM)
  21. pulse_width_ram = pgm_read_word(&pulse_width);
  22. // przekazanie wartości do funkcji
  23. set_pwm_P( pgm_read_word(&pulse_width) );
  24. // definicja zmiennej w RAM
  25. // zawierającej wskaźnik do wartości we FLASH
  26. const uint16_t * pulse_width_ptr = &pulse_width;
  27. // odczyt za pomocą wskaźnika
  28. pulse_width_ram = pgm_read_word(pulse_width_ptr);
  29. ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
  30. {
  31. OCR1A = pulse_width_ram;
  32. // lub
  33. // OCR1A = pgm_read_word(&pulse_width);
  34. // lub
  35. // OCR1A = pgm_read_word(pulse_width_ptr);
  36. }
  37. while (1)
  38. {
  39. }
  40. }

Definicja przy pomocy kwalifikatora __flash

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3. #include <util/atomic.h>
  4.  
  5. // definicja pojedynczej wartości całkowitej
  6. const __flash uint16_t pulse_width = 5689;
  7. // zmienna w RAM
  8. uint16_t pulse_width_ram;
  9. // definicja funkcji
  10. void set_pwm_P(const uint16_t pw)
  11. {
  12. ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
  13. {
  14. OCR1A = pw;
  15. }
  16. }
  17.  
  18. int main(void)
  19. {
  20. // bezpośredni odczyt wartości całkowitej (FLASH->RAM)
  21. pulse_width_ram = pulse_width;
  22. // przekazanie wartości do funkcji
  23. set_pwm_P( pulse_width );
  24. // definicja zmiennej w RAM
  25. // zawierającej wskaźnik do wartości we FLASH
  26. const __flash uint16_t * pulse_width_ptr = &pulse_width;
  27. // odczyt za pomocą wskaźnika
  28. pulse_width_ram = *pulse_width_ptr;
  29.  
  30. ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
  31. {
  32. OCR1A = pulse_width_ram;
  33. // lub
  34. // OCR1A = pulse_width;
  35. // lub
  36. // OCR1A = *pulse_width_ptr;
  37. }
  38. while (1)
  39. {
  40. }

Jeżeli teraz porównamy kilka linijek kodu, to nietrudno zauważyć różnice w czytelności:

Język: C Zwiń
  1. // bezpośredni odczyt wartości całkowitej (FLASH->RAM)
  2. pulse_width_ram = pgm_read_word(&pulse_width); /* PROGMEM */
  3. pulse_width_ram = pulse_width; /* __flash */
  4. // przekazanie wartości do funkcji
  5. set_pwm_P( pgm_read_word(&pulse_width) ); /* PROGMEM */
  6. set_pwm_P( pulse_width ); /* __flash */
  7. // odczyt poprzez wskaźnik
  8. OCR1A = pgm_read_word(pulse_width_ptr); /* PROGMEM */
  9. OCR1A = *pulse_width_ptr; /* __flash */

Dodatkowym problemem w przypadku użycia PROGMEM jest nie tylko konieczność użycia makr w stylu pgm_read_xxx(), lecz również to, że muszą one być adekwatne do odczytywanego typu stałej zapisanej w pamięci FLASH. Jeżeli w powyższym przykładzie zamiast użyć makra pgm_read_word() napiszemy pgm_read_byte(), nie otrzymamy od kompilatora żadnego ostrzeżenia, a odczyt będzie nieprawidłowy (odczytany zostanie tylko jeden bajt).

Odczyt danych zadeklarowanych do zapisu w pamięci programu przy użyciu kwalifikatora __flash jest pozbawiony tej wady. Kompilator zna typ odczytywanych danych i zawsze automatycznie odczyta prawidłową ilość bajtów.

◦ Tablica wartości stałych

Definicja przy pomocy atrybutu PROGMEM

Język: C Zwiń
  1. /*
  2.  * test_flash.c
  3.  */
  4.  
  5. #include <avr/io.h>
  6. #include <avr/pgmspace.h>
  7. #include <util/delay.h>
  8.  
  9.  
  10. // tablica zawierająca 4 elementy 8-bajtowe
  11. // czyli 64-bitowe (uint64_t)
  12. // w razie potrzeby można oczywiście użyć dowolnego
  13. // typu i dowolnej ilości (odpowiednich dla tego typu)
  14. // wartości
  15. const uint64_t sn_array[] PROGMEM = {
  16. 0x5200248A78AC4598,
  17. 0x670125AD2F54AA2D,
  18. 0x6600055AC52B6879,
  19. 0xC000A2A5FD510230
  20. };
  21.  
  22. // ”proteza” funkcji odczytującej "serial number"
  23. // z pastylki iButton (np. DS1990R)
  24. uint64_t iButton_read_sn(void)
  25. {
  26. return 0xC000A2A5FD510230;
  27. }
  28. // prosta funkcja sprawdzająca uprawnienia odczytanego
  29. // numeru seryjnego do otwarcia drzwi
  30. // parametry:
  31. // sn_read - odczytany numer seryjny
  32. // allowed_sns - adres tablicy w pamięci programu
  33. // z uprawnionymi numerami seryjnymi
  34. // cnt - ilość elementów tablicy
  35. void access_control( uint64_t sn_read,
  36. const uint64_t allowed_sns[],
  37. uint8_t cnt )
  38. {
  39. uint64_t temp;
  40. for ( uint8_t i = 0; i<cnt; i++ )
  41. {
  42. // przy tak dużym rozmiarze elementu tablicy
  43. // konieczne jest użycie funkcji memcpy_P()
  44. // do jego odczytania; elementy o mniejszym
  45. // rozmiarze należy odczytywać za pomocą
  46. // makr pgm_read_xxx() zależnie od typu
  47. memcpy_P( &temp, &allowed_sns[i], sizeof(temp) );
  48. if ( sn_read == temp )
  49. {
  50. PORTA |= 0x01;
  51. //_delay_ms(2000);
  52. PORTA &= 0xFE;
  53. //_delay_ms(1000);
  54. break;
  55. } // if ( sn_read == temp )
  56. } // for ( uint8_t i = 0; i<cnt; i++ )
  57. } // void access_control ()
  58. int main(void)
  59. {
  60. // pin 0 portu A jako wyjście sterujące dostępem
  61. DDRA = 0x01;
  62. // włączenie rezystorów pull-up na pozostałych
  63. // pinach
  64. PORTA = 0xFE;
  65. // odczyt wartości pojedynczego elementu tablicy
  66. // w pamięci FLASH (dla elementów o mniejszym rozmiarze
  67. // należy użyć makr pgm_read_xxx() w zależności od typu)
  68. uint64_t test;
  69. memcpy_P( &test, &sn_array[2], sizeof(test) );
  70. // zmienna w RAM będąca wskaźnikiem do tablicy
  71. // w pamięci FLASH
  72. const uint64_t * test_ptr = sn_array;
  73. // odczyt sn_array[1] poprzez wskaźnik
  74. memcpy_P( &test, test_ptr+1, sizeof(test) );
  75. while (1)
  76. {
  77. access_control( iButton_read_sn(),
  78. sn_array,
  79. sizeof(sn_array)/
  80. sizeof(sn_array[0]) );
  81. } // while (1)
  82. } // main ()

Definicja przy pomocy kwalifikatora __flash

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3. #include <util/delay.h>
  4.  
  5. // tablica zawierająca 4 elementy 8-bajtowe
  6. // czyli 64-bitowe (uint64_t)
  7. // w razie potrzeby można oczywiście użyć dowolnego
  8. // typu i dowolnej ilości (odpowiednich dla tego typu)
  9. // wartości
  10. const __flash uint64_t sn_array[] = {
  11. 0x5200248A78AC4598,
  12. 0x670125AD2F54AA2D,
  13. 0x6600055AC52B6879,
  14. 0xC000A2A5FD510230
  15. };
  16.  
  17. // ”proteza” funkcji odczytującej "serial number"
  18. // z pastylki iButton (np. DS1990R)
  19. uint64_t iButton_read_sn(void)
  20. {
  21. return 0xC000A2A5FD510230;
  22. }
  23. // prosta funkcja sprawdzająca uprawnienia odczytanego
  24. // numeru seryjnego do otwarcia drzwi
  25. // parametry:
  26. // sn_read - odczytany numer seryjny
  27. // allowed_sns - adres tablicy w pamięci programu
  28. // z uprawnionymi numerami seryjnymi
  29. // cnt - ilość elementów tablicy
  30. void access_control( uint64_t sn_read,
  31. const __flash uint64_t allowed_sns[],
  32. uint8_t cnt )
  33. {
  34. // nie ma konieczności deklarowania zmiennej pomocniczej
  35. // uint64_t temp;
  36. for ( uint8_t i = 0; i<cnt; i++ )
  37. {
  38. // bezpośrednie porównanie odczytanego numeru
  39. // seryjnego z zapisanym w pamięci FLASH,
  40. // tak samo jak w przypadku zmiennej
  41. // umieszczonej w pamięci RAM
  42. if ( sn_read == allowed_sns[i] )
  43. {
  44. PORTA |= 0x01;
  45. //_delay_ms(2000);
  46. PORTA &= 0xFE;
  47. //_delay_ms(1000);
  48. break;
  49. } // if ( sn_read == allowed_sns[i] )
  50. } // for ( uint8_t i = 0; i<cnt; i++ )
  51. } // void access_control()
  52. // ------------------- funkcja main ---------------------------------
  53. int main(void)
  54. {
  55. // pin 0 portu A jako wyjście sterujące dostępem
  56. DDRA = 0x01;
  57. // włączenie rezystorów pull-up na pozostałych
  58. // pinach
  59. PORTA = 0xFE;
  60. // odczyt wartości pojedynczego elementu tablicy
  61. // w pamięci FLASH
  62. uint64_t test = sn_array[2];
  63. // zmienna w RAM będąca wskaźnikiem do tablicy
  64. // w pamięci FLASH
  65. const __flash uint64_t * test_ptr = sn_array;
  66. // odczyt elementu tablicy sn_array[1] poprzez wskaźnik
  67. test = *(test_ptr+1);
  68. while (1)
  69. {
  70. access_control( iButton_read_sn(),
  71. sn_array,
  72. sizeof(sn_array)/
  73. sizeof(sn_array[0]) );
  74. } // while (1)
  75. } // main ()

◦ Ciągi znaków (string)

Jest to jeden z typów danych najczęściej zapisywanych w pamięci FLASH, dlatego przedstawię nieco bardziej rozbudowany przykład.

Definicja przy pomocy atrybutu PROGMEM

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3. #include <string.h>
  4.  
  5. // przykładowy ciąg znaków we FLASH
  6. const char pgm_string[] PROGMEM = "Test string";
  7. // wskaźnik w pamięci RAM do ciągu znaków we FLASH
  8. const char * pgm_string_ram_ptr = pgm_string;
  9. // wskaźnik w pamięci FLASH do ciągu znaków we FLASH
  10. const char * const pgm_string_pgm_ptr PROGMEM = pgm_string;
  11.  
  12. // typ wyliczeniowy dla komend
  13. typedef enum {
  14. PLAY,
  15. STOP,
  16. PAUSE,
  17. REVERSE,
  18. REWIND,
  19. FAST_FORWARD,
  20. PREVIOUS,
  21. NEXT,
  22. RECORD,
  23. EJECT,
  24. CMD_CNT /* wartość oznaczająca ilość komend */
  25. } CMD;
  26.  
  27. // ciągi znaków we FLASH
  28. const char _play[] PROGMEM = "PLAY";
  29. const char _stop[] PROGMEM = "STOP";
  30. const char _pause[] PROGMEM = "PAUSE";
  31. const char _reverse[] PROGMEM = "REVERSE";
  32. const char _rewind[] PROGMEM = "REWIND";
  33. const char _fast_forward[] PROGMEM = "FAST_FORWARD";
  34. const char _previous[] PROGMEM = "PREVIOUS";
  35. const char _next[] PROGMEM = "NEXT";
  36. const char _record[] PROGMEM = "RECORD";
  37. const char _eject[] PROGMEM = "EJECT";
  38. // tablica wskaźników w pamięci FLASH
  39. // do ciągów znaków we FLASH
  40. // można tak:
  41. // const char * commands[] PROGMEM = {
  42. // cmd0, cmd1, cmd2, cmd3, cmd4,
  43. // cmd5, cmd6, cmd7, cmd8, cmd9 };
  44. // lecz dla pewności przyporządkowania indeksów
  45. // poszczególnym komendom lepiej moim zdaniem zrobić tak:
  46. const char * const commands[] PROGMEM = {
  47. [PLAY] = _play,
  48. [STOP] = _stop,
  49. [PAUSE] = _pause,
  50. [REVERSE] = _reverse,
  51. [REWIND] = _rewind,
  52. [FAST_FORWARD] = _fast_forward,
  53. [PREVIOUS] = _previous,
  54. [NEXT] = _next,
  55. [RECORD] = _record,
  56. [EJECT] = _eject
  57. };
  58.  
  59. // "proteza" funkcji do odbioru komend poprzez uart
  60. // tutaj tylko wczytuje kolejne komendy z tablicy
  61. // 'commands[]' we FLASH do zmiennej 'recv_cmd' w pętli
  62. // głównej programu
  63. void uart_receive_cmd(char * dest)
  64. {
  65. static uint8_t i = 0;
  66. strcpy_P(dest, pgm_read_ptr(&commands[i]));
  67. if (++i >= CMD_CNT) i = 0;
  68. }
  69.  
  70. // "proteza" funkcji do wyświetlania na lcd ciągu
  71. // znaków zapisanych we FLASH,
  72. // tutaj wysyła na PORTA ciąg znaków z tablicy
  73. // we FLASH wskazywanej przez argument funkcji
  74. void lcd_str_P(const char * str)
  75. {
  76. char c;
  77. while ( (c=pgm_read_byte(str++)) ) PORTA = c;
  78. }
  79. // ------------------- funkcja main ---------------------------------
  80. int main(void)
  81. {
  82. CMD id; // iterator pętli interpretującej odebraną
  83. // komendę
  84. char recv_cmd[14]; // tablica pomocnicza dla odbieranych
  85. // przez uart komend
  86. // odczyt pojedynczego znaku z ciągu znaków
  87. char c = pgm_read_byte(&pgm_string[2]);
  88. // odczyt pojedynczego znaku za pomocą wskaźnika w RAM
  89. c = pgm_read_byte( pgm_string_ram_ptr+3 );
  90. // odczyt pojedynczego znaku za pomocą wskaźnika we FLASH
  91. c = pgm_read_byte( pgm_read_ptr(&pgm_string_pgm_ptr)+8 );
  92. // kopiowanie całego ciągu znaków FLASH->RAM
  93. // 'pgm_string' -> recv_cmd
  94. strcpy_P(recv_cmd, pgm_string);
  95. // wyświetlenie ciągu znaków
  96. lcd_str_P(pgm_read_ptr(&commands[REVERSE]));
  97. while (1)
  98. {
  99. // wczytanie komendy do zmiennej 'recv_cmd'
  100. uart_receive_cmd(recv_cmd);
  101. // porównanie komendy ze zmiennej 'recv_cmd'
  102. // z kolejnymi komendami w tablicy 'commands[]'
  103. for (id = 0; id < CMD_CNT; id++)
  104. {
  105. if ( strcmp_P( recv_cmd,
  106. pgm_read_ptr(&commands[id])) == 0 )
  107. {
  108. switch (id)
  109. {
  110. case PLAY:
  111. PORTA = 0x01;
  112. break;
  113. case STOP:
  114. PORTA = 0x02;
  115. break;
  116. case PAUSE:
  117. PORTA = 0x04;
  118. break;
  119. case REVERSE:
  120. PORTA = 0x08;
  121. break;
  122. case REWIND:
  123. PORTA = 0x10;
  124. break;
  125. case FAST_FORWARD:
  126. PORTA = 0x20;
  127. break;
  128. case PREVIOUS:
  129. PORTA = 0x21;
  130. break;
  131. case NEXT:
  132. PORTA = 0x22;
  133. break;
  134. case RECORD:
  135. PORTA = 0x24;
  136. break;
  137. case EJECT:
  138. PORTA = 0x28;
  139. break;
  140. default:
  141. break;
  142. } // switch (id)
  143. break;
  144. } // if ( strcmp_P() == 0 )
  145. } // for (id = 0; id < CMD_CNT; id++)
  146. } // while (1)
  147. } // main()

Definicja przy pomocy kwalifikatora __flash

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3. #include <string.h>
  4.  
  5. // makro umieszczające ciąg ‘s’ w pamięci programu
  6. // i zwracające wskaźnik do tego ciągu
  7. #define PGMSTR(s) ((const __flash char[]) { s })
  8.  
  9. // przykładowy ciąg znaków we FLASH
  10. const __flash char pgm_string[] = "Test string";
  11. // wskaźnik w pamięci RAM do ciągu znaków we FLASH
  12. const __flash char * pgm_string_ram_ptr = pgm_string;
  13. // wskaźnik w pamięci FLASH do ciągu znaków we FLASH
  14. const __flash char * const __flash pgm_string_pgm_ptr = pgm_string;
  15.  
  16. // typ wyliczeniowy dla komend
  17. typedef enum {
  18. PLAY,
  19. STOP,
  20. PAUSE,
  21. REVERSE,
  22. REWIND,
  23. FAST_FORWARD,
  24. PREVIOUS,
  25. NEXT,
  26. RECORD,
  27. EJECT,
  28. CMD_CNT /* wartość oznaczająca ilość komend */
  29. } CMD;
  30.  
  31. // ciągi znaków we flash; można zrobić tak jak w przypadku
  32. // użycia PROGMEM: najpierw ciągi znaków we FLASH
  33. // const __flash char _play[] = "PLAY";
  34. // const __flash char _stop[] = "STOP";
  35. // const __flash char _pause[] = "PAUSE";
  36. // const __flash char _reverse[] = "REVERSE";
  37. // const __flash char _rewind[] = "REWIND";
  38. // const __flash char _fast_forward[] = "FAST_FORWARD";
  39. // const __flash char _previous[] = "PREVIOUS";
  40. // const __flash char _next[] = "NEXT";
  41. // const __flash char _record[] = "RECORD";
  42. // const __flash char _eject[] = "EJECT";
  43.  
  44. // a następnie wskaźniki we FLASH do tablic we FLASH
  45. // const __flash char * const __flash commands[] = {
  46. // [PLAY] = _play,
  47. // [STOP] = _stop,
  48. // [PAUSE] = _pause,
  49. // [REVERSE] = _reverse,
  50. // [REWIND] = _rewind,
  51. // [FAST_FORWARD] = _fast_forward,
  52. // [PREVIOUS] = _previous,
  53. // [NEXT] = _next,
  54. // [RECORD] = _record,
  55. // [EJECT] = _eject
  56. // };
  57.  
  58. // można też prościej metodą, która nie wymaga deklarowania
  59. // tablic '_play[]', '_stop[]', '_pause[]' itd.
  60. // nie mamy wtedy wprawdzie bezpośredniego dostępu
  61. // do tych ciągów, tylko poprzez tablicę wskaźników,
  62. // jednak zwykle jest tak (jak w tym przypadku),
  63. // że bezpośredni dostęp nie jest nam potrzebny
  64. const __flash char * const __flash commands[] = {
  65. [PLAY] = PGMSTR("PLAY"),
  66. [STOP] = PGMSTR("STOP"),
  67. [PAUSE] = PGMSTR("PAUSE"),
  68. [REVERSE] = PGMSTR("REVERSE"),
  69. [REWIND] = PGMSTR("REWIND"),
  70. [FAST_FORWARD] = PGMSTR("FAST_FORWARD"),
  71. [PREVIOUS] = PGMSTR("PREVIOUS"),
  72. [NEXT] = PGMSTR("NEXT"),
  73. [RECORD] = PGMSTR("RECORD"),
  74. [EJECT] = PGMSTR("EJECT")
  75. };
  76. // "proteza" funkcji do odbioru komend poprzez uart
  77. // tutaj tylko wczytuje kolejne komendy z tablicy
  78. // 'commands[]' we FLASH do zmiennej 'recv_cmd' w pętli
  79. // głównej programu
  80. void uart_receive_cmd(char * dest)
  81. {
  82. static uint8_t i = 0;
  83. strcpy_P( dest, commands[i] );
  84. if (++i >= CMD_CNT) i = 0;
  85. }
  86.  
  87. // "proteza" funkcji do wyświetlania na lcd ciągu
  88. // znaków zapisanych we FLASH,
  89. // tutaj wysyła na PORTA ciąg znaków z tablicy
  90. // we FLASH wskazywanej przez argument funkcji
  91. void lcd_str_P(const __flash char * str)
  92. {
  93. char c;
  94. while ( (c=*str++) ) PORTA = c;
  95. }
  96.  
  97.  
  98. // ------------------- funkcja main ---------------------------------
  99. int main(void)
  100. {
  101. CMD id; // iterator pętli interpretującej odebraną
  102. // komendę
  103. char recv_cmd[14]; // tablica pomocnicza dla odbieranych
  104. // przez uart komend
  105. // odczyt pojedynczego znaku z ciągu znaków
  106. char c = pgm_string[2];
  107. // odczyt pojedynczego znaku za pomocą wskaźnika w RAM
  108. c = *(pgm_string_ram_ptr+3);
  109. // odczyt pojedynczego znaku za pomocą wskaźnika we FLASH
  110. c = *(pgm_string_pgm_ptr+8);
  111. // kopiowanie całego ciągu znaków FLASH->RAM
  112. // 'pgm_string' -> 'recv_cmd'
  113. strcpy_P(recv_cmd, pgm_string);
  114. // wyświetlenie ciągu znaków
  115. lcd_str_P(commands[REVERSE]);
  116. while (1)
  117. {
  118. // wczytanie komendy do zmiennej 'recv_cmd'
  119. uart_receive_cmd(recv_cmd);
  120. // porównanie komendy ze zmiennej 'recv_cmd'
  121. // z kolejnymi komendami w tablicy 'commands[]'
  122. for (id = 0; id < CMD_CNT; id++)
  123. {
  124. if ( strcmp_P( recv_cmd, commands[id] ) == 0 )
  125. {
  126. switch (id)
  127. {
  128. case PLAY:
  129. PORTA = 0x01;
  130. break;
  131. case STOP:
  132. PORTA = 0x02;
  133. break;
  134. case PAUSE:
  135. PORTA = 0x04;
  136. break;
  137. case REVERSE:
  138. PORTA = 0x08;
  139. break;
  140. case REWIND:
  141. PORTA = 0x10;
  142. break;
  143. case FAST_FORWARD:
  144. PORTA = 0x20;
  145. break;
  146. case PREVIOUS:
  147. PORTA = 0x21;
  148. break;
  149. case NEXT:
  150. PORTA = 0x22;
  151. break;
  152. case RECORD:
  153. PORTA = 0x24;
  154. break;
  155. case EJECT:
  156. PORTA = 0x28;
  157. break;
  158. default:
  159. break;
  160. } // switch (id)
  161. break;
  162. } // if ( strcmp_P() == 0 )
  163. } // for (id = 0; id < CMD_CNT; id++)
  164. } // while (1)
  165. } // main()

Myślę, że kolejne porównanie nie wymaga komentarza:

Język: C Zwiń
  1. // odczyt pojedynczego znaku z ciągu znaków
  2. char c = pgm_read_byte(&pgm_string[2]); /* PROGMEM */
  3. char c = pgm_string[2]; /* __flash */
  4. // odczyt pojedynczego znaku za pomocą wskaźnika w RAM
  5. c = pgm_read_byte( pgm_string_ram_ptr+3 ); /* PROGMEM */
  6. c = *(pgm_string_ram_ptr+3); /* __flash */
  7. // odczyt pojedynczego znaku za pomocą wskaźnika we FLASH
  8. c = pgm_read_byte( pgm_read_ptr(&pgm_string_pgm_ptr)+8 ); /* PROGMEM */
  9. c = *(pgm_string_pgm_ptr+8); /* __flash */
  10. // wyświetlenie ciągu znaków
  11. lcd_str_P(pgm_read_ptr(&commands[REVERSE])); /* PROGMEM */
  12. lcd_str_P(commands[REVERSE]); /* __flash */

◦ Tablica struktur

Struktury to również typ danych dosyć często zapisywany w pamięci FLASH (może być używany przykładowo do tworzenia elementów menu). Oczywiście poniższe przykłady to tylko pokazanie sposobu definiowania struktur w pamięci programu oraz ich późniejszego odczytu, a nie przykłady budowania i obsługi menu. Przedstawione tam funkcje nie robią niestety nic pożytecznego, za to mają pokazać, w jaki sposób przekazać dane ze struktury do funkcji oraz jak ich wewnątrz funkcji użyć.

Definicja przy pomocy atrybutu PROGMEM

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3.  
  4. #define LABELS_COUNT sizeof(labels)/sizeof(labels[0])
  5. // struktura opisująca kolor
  6. typedef struct {
  7. uint8_t r;
  8. uint8_t g;
  9. uint8_t b;
  10. } Color;
  11. // struktura opisująca etykietę
  12. typedef struct {
  13. uint16_t pos_x;
  14. uint16_t pos_y;
  15. uint8_t width;
  16. uint8_t height;
  17. const char * text_ptr;
  18. Color bg_color;
  19. Color fg_color;
  20. } Label;
  21. // w przypadku PROGMEM należy najpierw zapisać ciągi znaków
  22. // w pamięci FLASH, aby podczas definiowania struktury można było
  23. // umieścić wskaźniki do nich
  24. const char label_0[] PROGMEM = "Red";
  25. const char label_1[] PROGMEM = "Green";
  26. const char label_2[] PROGMEM = "Blue";
  27.  
  28. // tablica struktur (etykiet) w pamięci FLASH
  29. const Label labels[] PROGMEM = {
  30. {
  31. 10,20,50,30,label_0,
  32. { 150,0,0 },
  33. { 255,255,255 }
  34. },
  35. {
  36. 10,60,50,30,label_1,
  37. { 0,150,0 },
  38. { 255,255,255 }
  39. },
  40. {
  41. 10,100,50,30,label_2,
  42. { 0,0,150 },
  43. { 255,255,255 }
  44. }
  45. };
  46.  
  47. // funkcje obsługujące struktury we FLASH
  48.  
  49. void draw_rectangle_P(uint16_t x, uint16_t y,
  50. uint8_t w, uint8_t h,
  51. Color c) {
  52. // to tylko przykładowe operacje mające na celu np.
  53. // łatwiejsze zaobserwowanie wartości podczas
  54. // debugowania/symulacji
  55. TCNT1 = x;
  56. TCNT1 = y;
  57. PORTA = w;
  58. PORTA = h;
  59. PORTA = c.r;
  60. PORTA = c.g;
  61. PORTA = c.b;
  62. }
  63.  
  64. // funkcja (niby)wyświetlająca tekst
  65. // w rzeczywistości musiałaby również zawierać koordynaty,
  66. // które tutaj pominąłem dla uproszczenia -
  67. // - przekazanie koordynatów było pokazane we funkcji
  68. // draw_rectangle()
  69. void draw_text_P(const char * t, Color c) {
  70. char tmp;
  71. // odczyt do końca ciągu znaków (czyli do znaku ‘\0’)
  72. while ( (tmp=pgm_read_byte(t++)) ) PORTB = tmp;
  73. PORTB = c.r;
  74. PORTB = c.g;
  75. PORTB = c.b;
  76. }
  77. void draw_label_P(const Label * l_ptr) {
  78. // Color to struktura składająca się z 3 bajtów, więc nie da się
  79. // odczytać jej żadnym z makr z grupy pgm_read_xxx(), należy użyć
  80. // zmiennej pomocniczej oraz funkcji memcpy_P()
  81. Color tmp_color;
  82. memcpy_P(&tmp_color, &l_ptr->bg_color, sizeof(tmp_color));
  83. // pozostałe elementy struktury możemy odczytać za pomocą makr
  84. // pgm_read_xxx() przekazując bezpośrednio do funkcji
  85. // draw_rectangle()
  86. draw_rectangle_P(pgm_read_word(&l_ptr->pos_x),
  87. pgm_read_word(&l_ptr->pos_y),
  88. pgm_read_byte(&l_ptr->width),
  89. pgm_read_byte(&l_ptr->height),
  90. tmp_color);
  91. // ponownie odczyt struktury Color wymaga użycia funkcji mamcpy_P()
  92. memcpy_P(&tmp_color, &l_ptr->fg_color, sizeof(tmp_color));
  93. // do funkcji draw_text_P() musimy przekazać wskaźnik do ciągu znaków
  94. // nie możemy tego zrobić bezpośrednio za pomocą operatora &
  95. // ponieważ jest on zapisany wewnątrz struktury Label w pamięci FLASH
  96. // należy więc użyć makra pgm_read_ptr()
  97. draw_text_P(pgm_read_ptr(&l_ptr->text_ptr), tmp_color);
  98. }
  99.  
  100. // funkcja (niby:) wyświetlająca menu
  101. void draw_menu_P() {
  102. uint8_t i;
  103. for (i=0; i<LABELS_COUNT; i++)
  104. {
  105. draw_label_P(&labels[i]);
  106. }
  107. }
  108.  
  109.  
  110. // ------------------- funkcja main ---------------------------------
  111. int main(void)
  112. {
  113. uint8_t ui8;
  114. uint16_t ui16;
  115. const Label *l_ptr = &labels[2];
  116. // odczyt wartości 16-bitowej
  117. ui16 = pgm_read_word(&labels[1].pos_x);
  118.  
  119. // odczyt wartości 8-bitowej
  120. ui8 = pgm_read_byte(&labels[1].bg_color.g);
  121.  
  122. // odczyt elementu struktury poprzez wskaźnik
  123. // do struktury (będącej elementem tablicy)
  124. ui16 = pgm_read_word(&l_ptr->pos_y);
  125.  
  126. while (1)
  127. {
  128. draw_menu_P();
  129. } // while(1)
  130. } // main()

Definicja przy pomocy kwalifikatora __flash

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3.  
  4. #define PGMSTR(s) ((const __flash char[]) { s })
  5. #define LABELS_COUNT sizeof(labels)/sizeof(labels[0])
  6.  
  7. // struktura opisująca kolor
  8. typedef struct {
  9. uint8_t r;
  10. uint8_t g;
  11. uint8_t b;
  12. } Color;
  13. // struktura opisująca etykietę
  14. typedef struct {
  15. uint16_t pos_x;
  16. uint16_t pos_y;
  17. uint8_t width;
  18. uint8_t height;
  19. // tutaj istotne jest przekazanie kompilatorowi
  20. // informacji, że poniższy wskaźnik będzie
  21. // odnosił się do pamięci FLASH
  22. const __flash char * text_ptr;
  23. Color bg_color;
  24. Color fg_color;
  25. } Label;
  26.  
  27. // w przypadku __flash ciągi znaków możemy definiować
  28. // bezpośrednio wewnątrz struktury za pomocą makra PGMSTR()
  29. // zostaną utworzone anonimowe tablice znaków w pamięci
  30. // FLASH, a wskaźniki do nich zostaną umieszczone w strukturach,
  31. // czyli również w pamięci FLASH
  32. // tablica struktur (etykiet) w pamięci FLASH
  33. const __flash Label labels[] = {
  34. {
  35. 10,20,50,30,
  36. PGMSTR("Red"),
  37. { 150,0,0 },
  38. { 255,255,255 }
  39. },
  40. {
  41. 11,61,51,31,
  42. PGMSTR("Green"),
  43. { 0,150,0 },
  44. { 255,255,255 }
  45. },
  46. {
  47. 12,102,52,32,
  48. PGMSTR("Blue"),
  49. { 0,0,150 },
  50. { 255,255,255 }
  51. }
  52. };
  53.  
  54. // funkcje obsługujące struktury we FLASH
  55. void draw_rectangle_P(uint16_t x, uint16_t y,
  56. uint8_t w, uint8_t h,
  57. Color c) {
  58. // to tylko przykładowe operacje mające na celu np.
  59. // łatwiejsze zaobserwowanie wartości podczas
  60. // debugowania/symulacji
  61. TCNT1 = x;
  62. TCNT1 = y;
  63. PORTA = w;
  64. PORTA = h;
  65. PORTA = c.r;
  66. PORTA = c.g;
  67. PORTA = c.b;
  68. }
  69.  
  70. // funkcja (niby:) wyświetlająca tekst
  71. // w rzeczywistości musiałaby również zawierać koordynaty,
  72. // które tutaj pominąłem dla uproszczenia -
  73. // - przekazanie koordynatów było pokazane we funkcji
  74. // draw_rectangle()
  75. void draw_text_P(const __flash char * t, Color c) {
  76. char tmp;
  77. while ( (tmp = *t++) ) PORTB = tmp;
  78. PORTB = c.r;
  79. PORTB = c.g;
  80. PORTB = c.b;
  81. }
  82.  
  83. void draw_label_P(const __flash Label * l_ptr) {
  84. // w przypadku użycia __flash możemy elementy struktury
  85. // przekazać bezpośrednio do funkcji, bez konieczności
  86. // użycia funkcji memcpy_P() czy też makr pgm_read_xxx()
  87. draw_rectangle_P(l_ptr->pos_x, l_ptr->pos_y,
  88. l_ptr->width, l_ptr->height,
  89. l_ptr->bg_color);
  90. // odczyt wskaźnika zapisanego w strukturze w pamięci FLASH
  91. // możemy wykonać tak, jakby był w pamięci RAM przekazując go
  92. // bezpośrednio do funkcji
  93. draw_text_P(l_ptr->text_ptr, l_ptr->fg_color);
  94. }
  95. // (niby)funkcja wyświetlająca menu
  96. void draw_menu_P() {
  97. uint8_t i;
  98. for (i=0; i<LABELS_COUNT; i++)
  99. {
  100. draw_label_P(&labels[i]);
  101. }
  102. }
  103.  
  104. // ------------------- funkcja main ---------------------------------
  105. int main(void)
  106. {
  107. uint8_t ui8;
  108. uint16_t ui16;
  109. const __flash Label *l_ptr = &labels[2];
  110.  
  111. // odczyt wartości 16-bitowej
  112. ui16 = labels[1].pos_x;
  113.  
  114. // odczyt wartości 8-bitowej
  115. ui8 = labels[1].bg_color.g;
  116.  
  117. // odczyt elementu struktury poprzez wskaźnik
  118. // do struktury (będącej elementem tablicy)
  119. ui16 = l_ptr->pos_y;
  120.  
  121.  
  122. while (1)
  123. {
  124. draw_menu_P();
  125. } // while(1)
  126. } // main()

Jeszcze jedno porównanie:

Język: C Zwiń
  1. // –-- funkcja draw_label() w przypadku użycia PROGMEM ---------------------
  2. void draw_label_P(const Label * l_ptr) { /* PROGMEM */
  3. Color tmp_color; /* PROGMEM */
  4. memcpy_P(&tmp_color, &l_ptr->bg_color, sizeof(tmp_color)); /* PROGMEM */
  5. draw_rectangle_P(pgm_read_word(&l_ptr->pos_x), /* PROGMEM */
  6. pgm_read_word(&l_ptr->pos_y), /* PROGMEM */
  7. pgm_read_byte(&l_ptr->width), /* PROGMEM */
  8. pgm_read_byte(&l_ptr->height), /* PROGMEM */
  9. tmp_color); /* PROGMEM */
  10. memcpy_P(&tmp_color, &l_ptr->fg_color, sizeof(tmp_color)); /* PROGMEM */
  11. draw_text_P(pgm_read_ptr(&l_ptr->text_ptr), tmp_color); /* PROGMEM */
  12. }
  13.  
  14. // –-- ta sama funkcja w przypadku użycia __flash --------------------------
  15. void draw_label_P(const __flash Label * label_ptr) { /* __flash */
  16. draw_rectangle_P(l_ptr->pos_x, l_ptr->pos_y, /* __flash */
  17. l_ptr->width, l_ptr->height, /* __flash */
  18. l_ptr->bg_color); /* __flash */
  19. draw_text_P(l_ptr->text_ptr, l_ptr->fg_color); /* __flash */
  20. }

◦ Dane w pierwszym segmencie 64KiB – podsumowanie

Ze względów oczywistych niemożliwe jest pokazanie przykładów wszystkich możliwych kombinacji. Omówienie różnych typów danych oraz sposobów ich użycia wykracza poza ramy tego artykułu. Moim celem było jedynie pokazanie specyfiki umieszczania tych danych w pamięci programu przy użyciu dwóch możliwych metod i pokazanie różnic między tymi metodami. Myślę, że każdy kto potrafi już operować danymi w pamięci RAM (tzn. definiować, uzyskiwać do nich dostęp bezpośredni lub poprzez wskaźniki, przekazywać jako argumenty do funkcji) w łatwy sposób, na podstawie powyższych przykładów, powinien stworzyć własny kod spełniający jego oczekiwania.

• Dane w segmentach powyżej 64KiB

Tym razem nie będę pokazywał aż tylu przykładów z różnymi typami danych. Skoncentruję się raczej na omówieniu różnic pomiędzy obsługą danych poniżej i powyżej granicy 64KiB. Różnice te są niezależne od typu danych, więc nadal można korzystać z przykładów z poprzedniego rozdziału, zmieniając tylko to, co konieczne.

◦ Umieszczanie danych w pamięci FLASH

Wiemy już, że zarówno makro PROGMEM, jak i kwalifikator __flash oznaczają dane do zapisu w sekcji .progmem.data, którą później linker lokuje na początku pamięci FLASH. Nie każdy chyba jednak wie, że istnieją również kwalifikatory __flash1 __flash2 __flash3 __flash4 oraz __flash5 , które oznaczają dane do zapisu w sekcjach (odpowiednio) .progmem1.data .progmem2.data .progmem3.data .progmem4.data oraz .progmem5.data. Przeznaczeniem poszczególnych sekcji powinny być kolejne 64KiB segmenty pamięci FLASH, czyli tak jak sekcja .progmem.data (__flash) jest zapisywana w pierwszym segmencie 64KiB (czyli o indeksie 0), tak sekcja .progmem1.data (__flash1) powinna być zapisana w drugim segmencie 64KiB (czyli o indeksie 1), sekcja .progmem2.data (__flash2) powinna być zapisana w trzecim segmencie 64KiB (czyli o indeksie 2) itd.

Użyłem sformułowania 'powinny być', dlatego że tak naprawdę domyślne skrypty linkera (te dołączone do toolchain’u) obecnie nie wspierają tej techniki. Nie wiem tak do końca jaka jest tego przyczyna, prawdopodobnie nowe skrypty są nadal w fazie testów przed dopuszczeniem do powszechnego użycia. Dokładniejsze omówienie tematu skryptów linkera wykracza jednak poza zakres tego artykułu, więc nie będę się tutaj rozpisywał (choć oczywiście można o tym poczytać gdzie indziej i poeksperymetować). Póki co powinniśmy przyjąć, że niezależnie od numeru sekcji czy też kwalifikatora, linker i tak umieszcza wszystkie dane w sekcji .progmem.data. Nie należy więc używać tych numerowanych kwalifikatorów (__flashN) do deklarowania danych we FLASH, dlatego że odczyt danych może być nieprawidłowy – dane mogą zostać zapisane w innym segmencie, niż kompilator będzie się ich spodziewał. Właściwie wspomniałem o nich, ponieważ mogą się one przydać w sytuacjach, kiedy dokładnie wiemy, gdzie dane zostaną umieszczone, ale o tym później.

Kiedy deklarujemy dużo danych do zapisu we FLASH, prędzej czy później może dojść do sytuacji, że ich łączny rozmiar plus to, co linker umieszcza przed tymi danymi (czyli np. wektory przerwań, ale czasami też inne sekcje) przekroczy limit 64KiB. Ważne jest, żebyśmy o tym wiedzieli. O ile możliwe jest (choć może być kłopotliwe) policzenie, ile zajmą nasze dane, o tyle trudno będzie przewidzieć rozmiar sekcji umieszczonych przed nimi.

Jak się dowiedzieć, czy przekroczyliśmy już limit 64KiB i którego bloku danych to ewentualnie dotyczy?

Możemy oczywiście przeanalizować plik *.map wygenerowany podczas budowania programu, jednak jest to mało wygodne i dość czasochłonne. Spróbujmy więc wydobyć te informacje bezpośrednio z pliku *.elf w inny sposób, używając do tego celu aplikacji avr-objdump.exe

Najpierw utworzymy plik o nazwie np. memobjects.bat zawierający tekst:

Język: Winbatch Zwiń
  1. @echo off
  2. SET COMMON_TEXT=O .
  3. SET TAB=
  4. echo List of memory sections objects
  5. echo.
  6. FOR %%i IN (%2 %3 %4 %5 %6 %7 %8 %9) DO (
  7. IF NOT "%%i"=="" (
  8. echo .%%i section
  9. avr-objdump %1 -t | find "%COMMON_TEXT%%%i%TAB%" | sort
  10. echo.
  11. )
  12. )

i zapisujemy w dogodnym dla siebie miejscu. Najlepiej skopiować powyższy tekst, jednak gdyby ktoś chciał koniecznie przepisywać ręcznie, należy zwrócić uwagę, że w linii 3. po tekście SET TAB= zaraz za znakiem równości powinien zostać wpisany znak tabulatora (który nie jest widoczny na listingu). W innym przypadku skrypt może nie działać prawidłowo.

Atmel Studio 7

Wybieramy w menu:

Tools→External tools…

Pojawi się okno do konfiguracji narzędzi zewnętrznych. Jeśli wcześniej mieliśmy utworzone już jakieś narzędzia klikamy najpierw na przycisk Add. Jeśli jest to pierwsze tworzone przez nas narzędzie od razu możemy wprowadzać dane:

Zatwierdzamy wszystko klikając na przycisk Apply, a później na OK. W menu Tools powinna się teraz pojawić nowa pozycja o nazwie, jaką wprowadziliśmy w polu Title.

Eclipse MARS 2

Wybieramy w menu:

Run→External Tools…→External Tools Configurations…

Pojawi się okno do konfiguracji narzędzi zewnętrznych. Po lewej stronie znajduje się pole z listą. Zwykle bezpośrednio po instalacji lista zawiera jeden wpis – Program. Zaznaczamy ten wpis. Nad listą powinien znajdować się wiersz pięciu ikonek. Klikamy na pierwszą od lewej (New launch configuration), po czym wpisujemy:

Zatwierdzamy klikając kolejno przyciski Apply i Close. Teraz gdy wybierzemy menu Run→External tools... powinniśmy zauważyć nową pozycję o nazwie wprowadzonej w polu Name podczas konfiguracji.

Dzięki zastosowaniu zmiennych środowiskowych nasze narzędzie będzie uniwersalne – nie trzeba będzie go konfigurować inaczej przy każdym kolejnym projekcie. Z drugiej jednak strony należy pamiętać (dotyczy Eclipse), że w momencie uruchomienia narzędzia musimy mieć wybrany właściwy projekt (i konfigurację Debug/Release). Jeśli spróbujemy uruchomić nasze narzędzie, kiedy np. będzie akurat aktywne okno konsoli, operacja zakończy się błędem.

Efekt działania naszego narzędzia będzie widoczny w Atmel Studio 7 w oknie Output, natomiast w Eclispe w oknie Console i będzie wyglądał mniej więcej tak (oczywiście rekordów będzie zapewne więcej):

    List of memory sections objects

.text section
000000e4 g     O .text	00000014 pgm_string
000000f8 g     O .text	00000007 pgm_array

Znaczenie poszczególnych kolumn jest następujące:

  1. adres w pamięci FLASH w postaci liczby szesnastkowej,
  2. literka g oznacza symbol globalny,
  3. literka O oznacza, że jest to obiekt (a nie na przykład jedna z funkcji, które też przecież są umieszczane w pamięci FLASH),
  4.  .text oznacza sekcję w której znajduje się obiekt,
  5. liczba w postaci szesnastkowej oznaczająca rozmiar,
  6. nazwa symbolu, czyli nazwa stałej zadeklarowana przez nas w kodzie.

WAŻNE:
Na podstawie uzyskanych w ten sposób informacji możemy odpowiednio zmieniać definicje i sposób odczytu naszych danych. Niestety istnieje tutaj pewna niedogodność niezależna od użytej metody obsługi naszych danych (PROGMEM czy __flash). W miarę rozbudowy naszego programu, kiedy będziemy dodawali nowe bloki danych do pamięci FLASH, może zmieniać się kolejność ich rozmieszczenia, przez co może być konieczne ponowne modyfikowanie programu adekwatnie do nowych adresów początkowych i końcowych. Zwykle dane są umieszczane we FLASH w odwrotnej kolejności, niż są zadeklarowane, więc dodawanie nowych danych zawsze przed wcześniej zdefiniowanymi powinno nas uchronić od problemów, ponieważ nowe dane zostaną dołączone na końcu i adresy poprzednich się nie zmienią. Nadal jednak pozostaje problem, gdy dane będziemy deklarować w innych plikach *.c – decyduje wtedy kolejność kompilacji, której nie mamy pod kontrolą, przynajmniej jeśli korzystamy z automatycznie generowanego pliku Makefile.

Na dodatek trzeba też uważać na opcje kompilatora. Przykładowo wyłączenie optymalizacji (czyli ustawienie poziomu optymalizacji na -O0 np. na potrzeby debugowania) może spowodować zmianę kolejności rozmieszczenia danych we FLASH i co za tym idzie nieprawidłowy ich odczyt.

Zapewne skrypt linkera, który umieszczałby dane w odpowiednich sekcjach __flashN w dużej mierze wyeliminowałby powyższe problemy. Póki co trzeba sobie jednak radzić w inny sposób. Dobrym sposobem na uzyskanie kontroli nad rozmieszczeniem danych może być tworzenie własnych sekcji w obszarze pamięci FLASH, ale o tym później. Najpierw spróbuję pokazać, jak to zrobić przy zastosowaniu standardowych, lepiej wszystkim znanych metod.

◦ Definicja przy pomocy atrybutu PROGMEM

Stosując PROGMEM, dane definiujemy tak samo, niezależnie od tego w jakim segmencie 64KiB się znajdą. Tutaj różnica będzie polegać tylko na sposobie odczytu tych danych. Jeśli choćby jeden bajt zdefiniowanego przez nas obszaru danych we FLASH znajdzie się poza pierwszym segmentem 64KiB, należy przyjąć, że obszar taki będzie wymagał specjalnego traktowania, czyli odczytu poprzez tzw. „dalekie wskaźniki” (ang. „far pointers”). „Dalekie”, ponieważ ich rozmiar jest 32‑bitowy, więc mogą wskazywać na adresy położone „daleko”, czyli poza pierwszym segmentem 64KiB.

Stwórzmy wstępnie taki oto przykładowy kod, nie uwzględniający jeszcze rozmieszczenia danych we FLASH:

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3.  
  4. #define MAX_SIZE PTRDIFF_MAX
  5.  
  6. // definiujemy dwie duże tablice danych
  7. const uint16_t pgm_array1[MAX_SIZE/sizeof(uint16_t)] PROGMEM = {0};
  8. const uint8_t pgm_array2[MAX_SIZE] PROGMEM = {0};
  9.  
  10. int main(void)
  11. {
  12. uint16_t i;
  13.  
  14. while (1)
  15. {
  16. for (i=0; i<MAX_SIZE/sizeof( pgm_array1[0] ); i++)
  17. {
  18. PORTB = pgm_read_word(&pgm_array1[i]);
  19. }
  20. for (i=0; i<MAX_SIZE; i++)
  21. {
  22. PORTC = pgm_read_byte(&pgm_array2[i]);
  23. }
  24. }
  25. }

W celu sprawdzenia rozmieszczenia danych w naszym programie, najpierw wydajemy polecenie budowania (Build→Build Solution [F7] w Atmel Studio, lub Project→Build project w Eclipse), a następnie uruchamiamy nasze narzędzie (znajdujące się w menu Tools w Atmel Studio lub w menu Run→External tools w Eclipse). W wyniku tej operacji w oknie Output w Atmel Studio lub w oknie Console w Eclipse powinniśmy otrzymać następujący tekst:

000000cc g     O .text	00007fff pgm_array2
000080cb g     O .text	00007ffe pgm_array1

Oznacza to, że nasza tablica pgm_array2 została umieszczona pod adresem 0xCC i ma rozmiar 0x7FFF, natomiast tablica pgm_array1 została umieszczona pod adresem 0x80CB i ma rozmiar 0x7FFE. Wszystkie wartości podane są w systemie szesnastkowym. Można oczywiście przekształcić je na system dziesiętny, jednak moim zdaniem lepiej dokonać obliczeń w systemie szesnastkowym, gdyż łatwiej wtedy ocenić przekroczenie limitu. Obecnie chyba każdy kalkulator posiada opcję obliczeń w systemie szesnastkowym, więc nikomu nie powinno to sprawić większego problemu.

„Far pointer” jest wymagany zawsze wtedy, kiedy adres końcowy zdefiniowanego przez nas bloku danych jest większy od maksymalnej wartości, jaką może przyjąć liczba całkowita szesnastobitowa bez znaku, czyli od 0xFFFF (65535). W systemie szesnastkowym jest to największa liczba, jaką można zapisać za pomocą czterech cyfr, więc każdy adres składający się z większej ilości cyfr (nie licząc poprzedzających ją zer nieznaczących) oznacza przekroczenie limitu (w systemie dziesiętnym ta granica nie jest tak oczywista).

Ze względu na to, że nasze narzędzie nie podaje adresu końcowego musimy go obliczyć. Robimy to za pomocą wzoru:

adres_końcowy = adres_początkowy + rozmiar – 1

więc dla pgm_array2 adres końcowy będzie równy (obliczenia oczywiście w systemie szesnastkowym):

CC + 7FFF - 1 = 80CA

Widzimy, że adres ten jest czterocyfrowy, co oznacza, iż mieści się w zakresie liczby szesnastobitowej, możemy w takim przypadku do odczytu takiego bloku danych używać makr z grupy pgm_read_xxx(), które jako argument przyjmują adres szesnastobitowy.

Liczymy teraz adres końcowy dla tablicy pgm_array1:

80CB + 7FFE - 1 = 1 00C8

Jak widać adres (w systemie szesnastkowym) ostatniego elementu tablicy jest pięciocyfrowy. Oznacza to, że nie da się go zaadresować za pomocą 16‑bitowego wskaźnika. W związku z tym do odczytu elementów tej tablicy nie można użyć makra pgm_read_word(), ponieważ obsługuje ono tylko adres szesnastobitowy. Zamiast tego należy zastosować makro pgm_read_word_far(). Makro to jednak wymaga wskaźnika 32‑bitowego, więc nie można użyć zwyczajnie operatora & do pobrania adresu odczytywanych danych, ponieważ wynikiem takiej operacji będzie wskaźnik 16‑bitowy. Do pobrania adresu 32‑bitowego trzeba użyć makra pgm_get_far_address().

Biorąc pod uwagę powyższe wymagania okazuje się, że napisany przez nas wcześniej kod odczytujący elementy tablicy pgm_array1 nie będzie działał prawidłowo. Powinien on wyglądać następująco:

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3.  
  4. #define MAX_SIZE PTRDIFF_MAX
  5.  
  6. // definiujemy dwie duże tablice danych
  7. const uint16_t pgm_array1[MAX_SIZE/sizeof(uint16_t)] PROGMEM = {0};
  8. const uint8_t pgm_array2[MAX_SIZE] PROGMEM = {0};
  9.  
  10. int main(void)
  11. {
  12. uint16_t i;
  13. // definiujemy "far pointer" i przypisujemy mu adres pierwszego elementu
  14. uint_farptr_t array1_ptr = pgm_get_far_address(pgm_array1[0]);
  15.  
  16. while (1)
  17. {
  18. for (i=0; i<MAX_SIZE/sizeof( pgm_array1[0] ); i++)
  19. {
  20. PORTB = pgm_read_word_far( array1_ptr );
  21. // do ‘wskaźnika’ dodajemy rozmiar elementu (wytłumaczenie poniżej)
  22. array1_ptr += sizeof( pgm_array1[0] );
  23. }
  24. for (i=0; i<MAX_SIZE; i++)
  25. {
  26. PORTC = pgm_read_byte(&pgm_array2[i]);
  27. }
  28. }
  29. }

Dopiero teraz tablica pgm_array1 będzie odczytywana prawidłowo.

Należy tutaj zwrócić tutaj uwagę na pewien istotny fakt. Wskaźniki w avr-gcc mają rozmiar 16 bitów, co nie jest wystarczające do zaadresowania danych znajdujących się poza granicą 64KiB. Dlatego też należałoby użyć typu o większym zasięgu, czyli np. wskaźników 32‑bitowych. Jednakże uint_farptr_t (zwracany przez makro pgm_get_far_address()) tak naprawdę nie jest wskaźnikem tylko typem całkowitym 32‑bitowym. Jest to istotne ze względu na arytmetykę wskaźników. W przypadku prawdziwego wskaźnika, operacja array2_ptr++ spowodowałaby zwiększenie wartości wskaźnika o 2, ponieważ elementy tablicy są dwubajtowe, więc adres kolejnego elementu jest zawsze większy o 2 od adresu poprzedniego. Jednak ze względu na to, że array2_ptr jest typu uint_farptr_t, operacja taka nie zwróci prawidłowego rezultatu – po prostu wartość zmiennej zostanie powiększona o 1, niezależnie od tego, jaki jest rozmiar elementów tablicy. Programista musi więc osobiście zadbać o prawidłowe obliczanie adresu, dlatego właśnie należy zastosować inny sposób obliczenia adresu (podczas inkrementacji wskaźnika), na przykład:

Język: C Zwiń
  1. array2_ptr += sizeof( pgm_array2[0] );

Znając powyższe zasady można sobie poradzić z zapisem i odczytem danych za pomocą makr z pliku nagłówkowego pgmspace.h, choć każdy chyba przyzna, że jest to dość kłopotliwe.

◦ Definicja przy użyciu kwalifikatorów __flash i __memx

Podobnie jak poprzednio stwórzmy wstępnie kod, nie biorący pod uwagę granicy 64KiB:

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3.  
  4. #define MAX_SIZE PTRDIFF_MAX
  5.  
  6. // definiujemy dwie duże tablice danych (oczywiście w rzeczywistości
  7. // powinny one zawierać dane - tutaj dla testu inicjujemy zerami)
  8. const __flash uint16_t pgm_array1[MAX_SIZE/sizeof(uint16_t)] = {0};
  9. const __flash uint8_t pgm_array2[MAX_SIZE] = {0};
  10.  
  11. int main(void)
  12. {
  13. uint16_t i;
  14.  
  15. while (1)
  16. {
  17. for (i=0; i<MAX_SIZE/sizeof(pgm_array1[0]); i++)
  18. {
  19. PORTC = pgm_array1[i];
  20. }
  21. for (i=0; i<MAX_SIZE; i++)
  22. {
  23. PORTB = pgm_array2[i];
  24. }
  25. }
  26. }

W celu sprawdzenia rozmieszczenia danych uruchamiamy nasze narzędzie (tak jak poprzednio – patrz wyżej), w wyniku czego w oknie Output w Atmel Studio lub w oknie Console w Eclipse powinno pojawić co następuje:

000000cc g     O .text	00007fff pgm_array2
000080cb g     O .text	00007ffe pgm_array1

W tym przypadku to tablica pgm_array1 wykracza poza limit 64KiB, więc trzeba zmienić kod tak, by odczyt danych był prawidłowy. Tym razem jednak będzie to o wiele prostsze. Wystarczy bowiem w deklaracji tablicy pgm_array1 zamienić kwalifikator __flash na kwalifikator __memx:

Język: C Zwiń
  1. const __memx uint16_t pgm_array1[MAX_SIZE/sizeof(uint16_t)] = {0};

i to wszystko, co musimy zrobić. Reszta kodu może pozostać bez zmian. Zastosowanie kwalifikatora __memx zamiast __flash spowoduje następujące zmiany w kodzie wynikowym kompilatora:

Można byłoby w tej chwili zadać pytanie: Dlaczego w ogóle stosować kwalifikator __flash, a nie tylko __memx, skoro ten drugi jest wygodniejszy w użyciu? Można przecież za jego pomocą odczytać dowolną komórkę pamięci FLASH.

Odpowiedź brzmi: Ze względu na wydajność kodu (szybkość działania).

Należy pamiętać, że czas wykonania operacji arytmetycznych i logicznych szczególnie w mikrokontrolerach 8‑bitowych jest zależny od rozmiaru operandów (w bajtach). Operacje odczytu i zapisu wskaźnika 24‑bitowego w pamięci RAM również zajmują więcej taktów. Poza tym użycie __memx wymaga aktualizowania na bieżąco rejestru RAMPZ, co również wymaga dodatkowych taktów. Dlatego odczyt danych za pomocą wskaźników 16‑bitowych jest szybszy. Oczywiście nikt nikomu nie zabroni stosowania __memx do odczytu danych w dowolnym miejscu wedle uznania, np. we fragmentach kodu, które nie są krytyczne czasowo.

◦ Użycie kwalifikatorów __flashN

Wspomniałem wcześniej o tym, że standardowe skrypty linkera w toolchain’ie nie wspierają przestrzeni adresowych __flashN. Pomimo tego ich obsługa jest zaimplementowana w kompilatorze. Oznacza to, że po znalezieniu danych oznaczonych kwalifikatorem __flashN, będzie on generował kod odczytujący te dane z N‑tego segmentu 64KiB (ustawiając odpowiednio rejestr RAMPZ i używając instrukcji ELPM). Piszę o tym, ponieważ mogą zdarzyć się sytuacje, w których warto z tych kwalifikatorów skorzystać.

Przypuśćmy, że do poprzedniego przykładowego kodu dopisaliśmy jeszcze jedną dużą tablicę i tablica pgm_array1 trafiła (w całości) do drugiego segmentu 64KiB (czyli o indeksie 1). Zależy nam jednak na maksymalnie wydajnym odczycie, więc użycie __memx nie będzie optymalne, ze względów, o których pisałem nieco powyżej.

Język: C Zwiń
  1. // definiujemy trzy duże tablice danych (oczywiście w rzeczywistości
  2. // powinny one zawierać dane - tutaj dla testu inicjujemy zerami)
  3. const __flash uint16_t pgm_array1[MAX_SIZE/sizeof(uint16_t)] = {0};
  4. const __flash uint16_t pgm_array2[MAX_SIZE/sizeof(uint16_t)] = {0};
  5. const __flash uint8_t pgm_array3[MAX_SIZE] = {0};

Sprawdzamy rozmieszczenie danych:

000000cc g     O .text	00007fff pgm_array3
000080cb g     O .text	00007ffe pgm_array2
000100c9 g     O .text	00007ffe pgm_array1

Po obliczeniu adresu końcowego pgm_array1:

1 00C9 + 7FFE = 1 80C7

okazuje się, że interesująca nas tablica znalazła się w całości w drugim segmencie 64KiB (adres początkowy i końcowy należą do tego samego segmentu 64KiB), czyli tam, gdzie teoretycznie powinna się znaleźć po zadeklarowaniu z użyciem kwalifikatora __flash1. Jest to istotne, bo tylko w takiej sytuacji możemy użyć kwalifikatora __flash1. Gdyby adresy początkowy i końcowy należały do różnych segmentów, konieczne byłoby użycie kwalifikatora __memx.

W tym konkretnym przypadku można bez obaw użyć kwalifikatora __flash1 do zadeklarowania tablicy pgm_array1. W efekcie można napisać taki przykładowy kod (tym razem dla odmiany przy użyciu wskaźników):

Język: C Zwiń
  1. #include <avr/io.h>
  2.  
  3. #define MAX_SIZE PTRDIFF_MAX
  4.  
  5. // definiujemy trzy duże tablice danych (oczywiście w rzeczywistości
  6. // powinny one zawierać dane - tutaj dla testu inicjujemy zerami)
  7.  
  8. // tutaj można użyć __flash1, bo tablica znajduje się w całości
  9. // w segmencie 64KiB o indeksie 1
  10. // można też użyć __memx, ale będzie nieco wolniej :)
  11. const __flash1 uint16_t pgm_array1[MAX_SIZE/sizeof(uint16_t)] = {0};
  12.  
  13. // tutaj trzeba użyć __memx, ponieważ adres końcowy jest
  14. // w innym segmencie 64KiB niż początkowy
  15. const __memx uint16_t pgm_array2[MAX_SIZE/sizeof(uint16_t)] = {0};
  16. // można użyć __flash, bo tablica znajduje się w całości
  17. // w segmencie 64KiB o indeksie 0
  18. // lub wolniejszego, ale wygodniejszego __memx
  19. const __flash uint8_t pgm_array3[MAX_SIZE] = {0};
  20.  
  21. int main(void)
  22. {
  23. uint16_t i;
  24.  
  25. // definiujemy wskaźniki do tablic
  26. const __flash1 uint16_t *array1_ptr = &pgm_array1[0];
  27. const __memx uint16_t *array2_ptr = &pgm_array2[0];
  28. const __flash uint8_t *array3_ptr = &pgm_array3[0];
  29.  
  30. while (1)
  31. {
  32. for (i=0; i<MAX_SIZE/sizeof(pgm_array1[0]); i++)
  33. {
  34. PORTC = *array1_ptr++;
  35. // można także: PORTC = pgm_array1[i];
  36. }
  37. for (i=0; i<MAX_SIZE/sizeof(pgm_array2[0]); i++)
  38. {
  39. PORTC = *array2_ptr++;
  40. // można także: PORTC = pgm_array2[i];
  41. }
  42. for (i=0; i<MAX_SIZE; i++)
  43. {
  44. PORTB = *array3_ptr++;
  45. // można także: PORTC = pgm_array3[i];
  46. }
  47. }
  48. }

Podsumowując, taki sposób definiowania danych we FLASH może być nieco kłopotliwy, szczególnie w projektach, w których definiujemy ich dużo, bo po każdym dodaniu danych wskazane jest sprawdzenie rozmieszczenia danych i ewentualnie wykonanie korekt w kodzie ze względu na możliwość zmiany adresów. Niemniej pokonanie tych niedogodności jest niezbędne w przypadku korzystania ze standardowych skryptów linkera. Myślę jednak, że kiedy zna się z grubsza zasady, którymi kieruje się kompilator i linker, łatwo wypracować sobie skuteczne metody dzięki którym zastosowanie tej metody nie jest jakoś szczególnie uciążliwe.

◦ Dane w zdefiniowanej sekcji

Zdarzają się sytuacje, kiedy z różnych powodów chcielibyśmy umieścić nasze dane w pamięci FLASH pod konkretnym adresem (na przykład po to, by mieć możliwość maksymalnego zoptymalizowania pętli odczytu tych danych). GCC oferuje do osiągnięcia tego celu atrybut 'section' dodawany przy definiowaniu zmiennej, dzięki któremu można umieścić dane w zdefiniowanej wcześniej przez siebie sekcji pod wskazanym przez nas adresem.

Najpierw musimy więc zdefiniować sekcję, w której później będziemy umieszczać nasze dane.

Atmel Studio 7

Możliwość zdefiniowania własnej sekcji znaleźć można w menu:

Project&→Properties→Toolchain→AVR/GNU Linker→Memory Settings

W linii opisanej jako „FLASH segment” należy kliknąć ikonkę „Add Item” i w okienku, które się pojawi wpisać oczekiwane parametry sekcji w formacie:

.nazwa=adres_hex

gdzie:

Przykładowo, jeśli chcemy utworzyć sekcję o nazwie .waveforms, która zaczyna się od adresu bajtu 0x10100 (czyli 256 bajtów za początkiem drugiego segmentu 64KiB), dzielimy adres bajtu przez 2, po czym wynik wpisujemy w pole tekstowe w następujący sposób:

.waveforms=0x8080

Eclipse MARS 2

Tego środowiska rzadko używam do programowania AVR, więc nie znam zbyt dobrze pluginu. Niestety nie udało mi się znaleźć tutaj wygodniejszego sposobu zdefiniowania sekcji (np. w opcjach projektu), więc uznałem, że trzeba to zrobić poprzez dodanie opcji linkera.

Należy uruchomić z menu:

Project→Properties→C/C++ Build→Settings→ (zakładka Tool Settings) AVR C Linker→General

i w polu tekstowym Other Arguments wpisać odpowiednie dane w formacie:

-Wl,-section-start=.nazwa=adres_hex

gdzie:

Przykładowo dla sekcji o nazwie .waveforms rozpoczynającej się od adresu bajtu 0x10100 należy wpisać:

-Wl,-section-start=.waveforms=0x10100

Jeśli teraz będziemy chcieli, aby nasze wcześniej utworzone narzędzie wyświetliło nam informacje o rozmieszczeniu danych w naszej sekcji, powinniśmy w opcjach narzędzia (Memory objects) w polu Arguments (na końcu) dopisać jej nazwę.

W celu umieszczenia danych w nowo utworzonej sekcji musimy w momencie definiowania zmiennej nadać jej odpowiedni atrybut, przykładowo:

Język: C Zwiń
  1. const uint8_t sine_wave[] __attribute__ ((section (".waveforms"))) =
  2. { 0 /* tutaj dane naszej tablicy z wartościami próbek */};

Ze względu na to, że taki zapis jest dość długi i niewygodny w użyciu, warto stworzyć makro, które uprości nam życie:

Język: C Zwiń
  1. // na początku pliku *.c lub w którymś z dołączanych plików nagłówkowych
  2. // definiujemy makro
  3. #define WAVEFORMS __attribute__ ((section (".waveforms")))
  4.  
  5. // i później deklaracja w kodzie - umieszczamy dane w naszej sekcji
  6. const uint8_t sine_wave[] WAVEFORMS =
  7. { 0 /* tutaj dane naszej tablicy z wartościami próbek */};

Nadany przez nas atrybut to informacja dla linkera, gdzie ma umieścić dane. Kompilator natomiast nie wie, gdzie znajduje się sekcja (nawet, że znajduje się w pamięci read‑only), więc istotne jest, aby zmienną poprzedzić kwalifikatorem const, ponieważ pozwoli mu to np. wygenerować błąd w przypadku omyłkowej próby modyfikacji takiej zmiennej.

Skoro już zapisaliśmy jakieś dane w naszej sekcji, na pewno będziemy chcieli je odczytywać w trakcie działania programu. Dane jednak są umieszczone w pamięci FLASH, więc bezpośredni odczyt w stylu:

Język: C Zwiń
  1. uint8_t sample = sinewave[i];

nie zadziała. Mamy tutaj dwie możliwości: użyć makr z pliku pgmspace.h lub zrealizować odczyt przy pomocy wskaźników z kwalifikatorami __flash, __flashN lub __memx.

Odczyt danych z wykorzystaniem pgmspace.h z naszej przykładowej sekcji, która znajduje się w drugim segmencie 64KiB, musimy zrealizować go przy pomocy makr pgm_read_xxx_far(), adresy pobierając za pomocą makra pgm_get_far_address(). Dodatkowo należy pamiętać o tym, że adres zwracany przez makro nie jest typowym wskaźnikiem, tylko liczbą 32‑bitową, więc programista musi zadbać osobiście np. o prawidłowe obliczanie adresu kolejnych elementów tablicy w zależności od typu tych elementów, o czym pisałem już wcześniej, lub pobierać adres każdej zmiennej wewnątrz struktury osobno (nie da się za pomocą tego wskaźnika odwołać do elementu struktury za pomocą operatora ‑>).

Wybierając drugą opcję, właściwie wystarczy utworzyć wskaźnik z kwalifikatorem __flash1 i za jego pomocą odczytywać dane. Oczywiście dotyczy to sytuacji (takiej, jak w podanym powyżej przykładzie), kiedy blok odczytywanych danych znajduje się w całości w drugim segmencie 64KiB. Gdyby zaistniała sytuacja, w której początek i koniec danych znajdowały się w różnych sekcjach, musimy użyć kwalifikatora __memx, który operuje wskaźnikami 24‑bitowymi.

Dla porównania podam może dwa możliwie proste przykłady:

użycie makr pgmspace.h

Język: C Zwiń
  1. #include <avr/io.h>
  2. #include <avr/pgmspace.h>
  3.  
  4. #define WAVEFORMS __attribute__ ((section(".mysection")))
  5.  
  6. struct color {
  7. uint8_t red;
  8. uint8_t green;
  9. uint8_t blue;
  10. };
  11.  
  12. // definiujemy tablicę w utworzonej sekcji
  13. const uint16_t sine_wave[1024] WAVEFORMS =
  14. {0 /* tutaj wartości próbek */};
  15. // definiujemy strukturę
  16. const struct color my_color WAVEFORMS = {
  17. 156, 16, 220
  18. };
  19.  
  20. int main(void)
  21. {
  22. uint16_t i;
  23. // definiujemy 'wskaźnik' do początku tablicy
  24. uint_farptr_t sine_wave_ptr = pgm_get_far_address(sine_wave);
  25. uint16_t tmp_sample;
  26. while (1)
  27. {
  28. // odczyt tablicy
  29. for (i=0; i<sizeof(sine_wave)/sizeof(sine_wave[0]); i++)
  30. {
  31. tmp_sample = pgm_read_word_far(sine_wave_ptr);
  32. PORTD = (uint8_t)tmp_sample;
  33. PORTE = (uint8_t)(tmp_sample>>8);
  34. sine_wave_ptr += sizeof(sine_wave[0]);
  35. }
  36.  
  37. //odczyt struktury
  38. PORTA = pgm_read_byte_far(pgm_get_far_address(my_color.red));
  39. PORTB = pgm_read_byte_far(pgm_get_far_address(my_color.green));
  40. PORTC = pgm_read_byte_far(pgm_get_far_address(my_color.blue));
  41. }
  42. }

użycie kwalifikatora __flash1

Język: C Zwiń
  1. #include <avr/io.h>
  2.  
  3. #define WAVEFORMS __attribute__ ((section(".mysection")))
  4.  
  5. struct color {
  6. uint8_t red;
  7. uint8_t green;
  8. uint8_t blue;
  9. };
  10.  
  11. // definiujemy tablicę w utworzonej sekcji
  12. const uint16_t sine_wave[1024] WAVEFORMS =
  13. {0 /* tutaj wartości próbek */};
  14.  
  15. // definiujemy strukturę
  16. const struct color my_color WAVEFORMS = {
  17. 156, 16, 220
  18. };
  19. int main(void)
  20. {
  21. uint16_t i;
  22.  
  23. // definiujemy wskaźnik do początku tablicy
  24. const __flash1 uint16_t * sine_wave_ptr = sine_wave;
  25.  
  26. // definiujemy wskaźnik do struktury
  27. const __flash1 struct color * my_color_ptr = &my_color;
  28.  
  29. uint16_t tmp_sample;
  30.  
  31. while (1)
  32. {
  33.  
  34. // odczyt tablicy
  35. for (i=0; i<sizeof(sine_wave)/sizeof(sine_wave[0]); i++)
  36. {
  37. tmp_sample = *sine_wave_ptr++;
  38. PORTD = (uint8_t)tmp_sample;
  39. PORTE = (uint8_t)(tmp_sample>>8);
  40. }
  41.  
  42. //odczyt struktury
  43. PORTA = my_color_ptr->red;
  44. PORTB = my_color_ptr->green;
  45. PORTC = my_color_ptr->blue;
  46. }
  47. }

Obydwa kody realizują to samo zadanie. Ocenę czytelności kodu oraz wygody użycia obydwu metod pozostawiam czytelnikowi.

Niestety istnieją również pewne niedogodności związane z definiowaniem danych we własnej sekcji.

Jedną z nich jest pobieranie 24‑bitowego wskaźnika do danych w naszej sekcji. Gdyby było konieczne zastąpienie wskaźnika z kwalifikatorem __flash1 na taki z kwalifikatorem __memx (np. ze względu na położenie tablicy po obu stronach granicy segmentów 64KiB, zwykła zamiana kwalifikatorów nie wystarczy:

Język: C Zwiń
  1. // const __flash1 uint16_t * sine_wave_ptr = sine_wave;
  2. const __memx uint16_t * sine_wave_ptr = sine_wave;

Po wprowadzeniu takiej modyfikacji program wprawdzie się skompiluje, jednak nie będzie prawidłowo odczytywał danych. Dlaczego? Kompilator po prostu zmienne zdefiniowane z atrybutem section traktuje jako ulokowane w pamięci RAM i stamtąd będzie próbował odczytywać dane (nie dotyczy to atrybutu PROGMEM – pobranie wskaźnika na dane oznaczone tym atrybutem będzie prawidłowe).

Trzeba więc użyć sposobu, dzięki któremu możliwe będzie pobranie prawidłowego („pełnego”) adresu danych. Osobiście rozwiązałem ten problem za pomocą następującego makra:

Język: C Zwiń
  1. // makro zwracające 24-bitowy wskaźnik do zmiennej ‘var’
  2. #define get_memx_address(var) \
  3. ({ \
  4.   __int24 tmp; \
  5.   \
  6.   __asm__ __volatile__( \
  7.   \
  8.   "ldi %A0, lo8(%1)" "\n\t" \
  9.   "ldi %B0, hi8(%1)" "\n\t" \
  10.   "ldi %C0, hh8(%1)" "\n\t" \
  11.   : \
  12.   "=d" (tmp) \
  13.   : \
  14.   "p" (&(var)) \
  15.   ); \
  16.   (__memx typeof(var)*)tmp; \
  17. })

Aby nie psuć sobie czytelności kodu wstawkami asemblerowymi i/lub udostępnić makro dla innych modułów programu, można makro umieścić w osobnym pliku nagłówkowym, który później można dołączyć dyrektywą #include.

W celu zmiany typu wskaźników w poprzednim przykładzie kodu powinno teraz wyglądać w ten sposób:

Język: C Zwiń
  1. // definiujemy wskaźnik do początku tablicy
  2. // zamiast: const __flash1 uint16_t * sine_wave_ptr = sine_wave;
  3. // nie: const __memx uint16_t * sine_wave_ptr = sine_wave;
  4. // tylko:
  5. const __memx uint16_t * sine_wave_ptr =
  6. get_memx_address(sine_wave[0]);
  7. // istotne jest to, że nie używamy operatora &, za to (w przypadku tablicy)
  8. // musimy podać indeks elementu (musi to być wartość stała, czyli nie możemy podać
  9. // w nawiasie kwadratowym nazwy zmiennej np. sine_wave[i]), którego adres chcemy pobrać;
  10. // 0 oczywiście oznacza początek tablicy, ale możemy też pobrać adres dowolnego elementu
  11. // definiujemy wskaźnik do struktury
  12. // zamiast: const __flash1 uint16_t * my_color_ptr = &my_color;
  13. // nie: const __memx uint16_t * my_color_ptr = &my_color;
  14. // tylko:
  15. const __memx struct color * my_color_ptr =
  16. get_memx_address(my_color);
  17. // w przypadku struktury lub innych zmiennych (nie tablicowych) podajemy
  18. // tylko nazwę zmiennej (bez operatora &)

Innym problemem, aczkolwiek nieco podobnej natury, jest definiowanie wskaźników do naszych danych jako zmiennych globalnych. Chodzi mi tutaj o sytuację podobną do tej z przykładu pokazującego umieszczanie ciągów znaków we FLASH, coś w stylu (skrótowo):

Język: C Zwiń
  1. // globalnie, czyli przed funkcjami
  2. // ciągi znaków we FLASH
  3. const char _play[] PROGMEM = "PLAY";
  4. const char _stop[] PROGMEM = "STOP";
  5.  
  6. // globalnie, czyli przed funkcjami
  7. // wskaźniki (zapisane we FLASH) do ciągów znaków we FLASH
  8. const char * const commands[] PROGMEM = {
  9. _play, _stop
  10. };

W tym przykładzie zarówno ciągi znaków, jak i tablica zawierająca wskaźniki do tych ciągów, zostaną zapisane w pamięci FLASH.

Bazując na tym przykładzie można by się spodziewać, że pisząc analogiczny kod, ale już z użyciem własnej sekcji, otrzymamy taki sam efekt:

Język: C Zwiń
  1. // globalnie, czyli przed funkcjami
  2. // tablice liczb 16-bitowych bez znaku w sekcji .waveforms (we FLASH)
  3. const uint16_t sine_wave[1024] WAVEFORMS = {0};
  4. const uint16_t sawtooth_wave[1024] WAVEFORMS = {0};
  5.  
  6. // globalnie, czyli przed funkcjami
  7. // wskaźniki (zapisane we FLASH)
  8. // do tablic liczb 16-bitowych bez znaku we FLASH
  9. const uint16_t * const wave_ptrs[] WAVEFORMS = {
  10. sine_wave, sawtooth_wave
  11. };

jednak byłoby tak tylko wtedy, gdybyśmy naszą sekcję .waveforms zdefiniowali w pierwszym segmencie 64KiB (ta sama zasada zresztą dotyczy również PROGMEM – jeżeli umieścimy dużo danych w pamięci programu i nasze ciągi znaków wylądują w drugiej sekcji, powyższy kod nie będzie działał prawidłowo). W związku z tym, że sekcję umieściliśmy w drugim segmencie, powyższa metoda nie zadziała.

Myślę, że podstawowa przyczyna takiego stanu rzeczy jest dość oczywista – uzyskane w ten sposób wskaźniki mają rozmiar 16‑bitowy, czyli zbyt mały, by zaadresować dane powyżej 64KiB. Ktoś mógłby powiedzieć: „No ale przecież mamy do dyspozycji ‘wskaźniki’ 32‑bitowe (far pointers).” Owszem, jednak ich uzyskanie wymaga użycia makra pgm_get_far_address(), którego można użyć tylko wewnątrz funkcji, czyli nie da się z jego pomocą zdefiniować zmiennej globalnej. Z kolei wewnątrz funkcji nie da się zdefiniować danych w sekcji .waveforms (w ogóle nie można używać atrybutu section do definiowania zmiennych lokalnych, czyli wewnątrz funkcji). Użycie kwalifikatora __flash (a tym bardziej __memx) do zdefiniowania wskaźników na nasze dane poza funkcją też nie zda egzaminu. Próba zdefiniowania tablicy wskaźników do tablic i zapisanie jej w naszej sekcji .waveforms w ten sposób:

Język: C Zwiń
  1. // globalnie, czyli przed funkcjami
  2. // tablice liczb 16-bitowych bez znaku w sekcji .waveforms (we FLASH)
  3. const uint16_t sine_wave[1024] WAVEFORMS = {0};
  4. const uint16_t sawtooth_wave[1024] WAVEFORMS = {0};
  5.  
  6. // globalnie, czyli przed funkcjami
  7. // próba zapisu tablicy wskaźników w sekcji .waveforms (we FLASH)
  8. // do tablic liczb 16-bitowych bez znaku w sekcji .waveforms (we FLASH)
  9. // nie zadziała
  10. const __flash1 uint16_t * const wave_ptrs[] WAVEFORMS = {
  11. sine_wave, sawtooth_wave
  12. };

zakończy się błędem: „initializer element is not computable at load time”. Wytłumaczenie przyczyny jest dosyć zawiłe, więc nie będę tutaj opisywał tego szczegółowo, ale generalnie chodzi o to (co wynika ze standardu C), że nie można dokonać konwersji pomiędzy wskaźnikami do różnych przestrzeni adresowych w trakcie ładowania programu (czyli przed jego wejściem do funkcji main() ).

Może się wydawać, że to nie jest poważny problem. Można przecież zdefiniować wskaźniki w pamięci RAM wewnątrz funkcji main() i później przekazywać je do różnych funkcji, ale nie zawsze jest to tak wygodne rozwiązanie, jak globalna tablica wskaźników dostępna w każdej funkcji w obrębie danego pliku. Można oczywiście zadeklarować tablicę wskaźników w RAM jako zmienną globalną, a później zainicjować ją wewnątrz funkcji main(), ale wtedy nie można zadeklarować wskaźników jako const, przez co mogą być podatne na niezamierzone modyfikacje.

Język: C Zwiń
  1. // globalnie, czyli przed funkcjami
  2. // tablice liczb 16-bitowych bez znaku w sekcji .waveforms (we FLASH)
  3. const uint16_t sine_wave[1024] WAVEFORMS = {0};
  4. const uint16_t sawtooth_wave[1024] WAVEFORMS = {0};
  5.  
  6. // dostępna globalnie tablica wskaźników w RAM
  7. // inicjowana we funkcji main()
  8. // zadziała, jednak wskaźniki nie mogą być oznaczone jako ‘const’
  9. // dodatkowa niedogodność to konieczność podania rozmiaru tablicy
  10. const __flash1 uint16_t * /*->const<-*/ wave_global_ptrs[2];
  11.  
  12. int main(void)
  13. {
  14. // zainicjowanie elementów tablicy dostępnej GLOBALNIE
  15. wave_global_ptrs[0] = (const __flash1 uint16_t *)sine_wave;
  16. wave_global_ptrs[1] = (const __flash1 uint16_t *)sawtooth_wave;
  17.  
  18. // definicja tablicy dostępnej tylko LOKALNIE wewnątrz funkcji main()
  19. // elementy tablicy (wskaźniki mogą być oznaczone jako ‘const’
  20. // przez co ich przypadkowa modyfikacja jest mało prawdopodobna
  21. const __flash1 uint16_t * const wave_local_ptrs[] = {
  22. (const __flash1 uint16_t *)sine_wave,
  23. (const __flash1 uint16_t *)sawtooth_wave
  24. };
  25. // --- reszta kodu ---
  26. }

Należy też wziąć pod uwagę, że możemy potrzebować tablicy zawierającej dużo więcej wskaźników (niż w przedstawionym powyżej przykładzie), przynajmniej po 2 bajty każdy (3 w przypadku __memx, lub nawet 4 przy wykorzystaniu uint_farptr_t) i wtedy umieszczenie ich w pamięci RAM może okazać się problemem, ze względu na jej ograniczone zasoby.

Rozwiązaniem tego problemu może być zdefiniowanie wskaźników za pomocą wartości liczbowych równych adresom danych. Oczywiście jest ono nieco kłopotliwe, pracochłonne i mało eleganckie, ale ma też duże zalety. Otrzymujemy bowiem tablicę wskaźników, która nie zajmuje pamięci RAM, jest dostępna globalnie i jest przeznaczona tylko do odczytu, dzięki czemu nie ma ryzyka nieintencjonalnej zmiany ich wartości.

W celu utworzenia takiej tablicy wskaźników, definiujemy najpierw swoje zmienne:

Język: C Zwiń
  1. // globalnie, czyli przed funkcjami
  2. // tablice liczb 16-bitowych bez znaku w pamięci FLASH
  3. const uint16_t sine_wave[1024] WAVEFORMS = {0};
  4. const uint16_t sawtooth_wave[1024] WAVEFORMS = {0};

Następnie należy dowiedzieć się, pod jakimi adresami zastały one umieszczone. Niestety zanim zmienne nie będą w jakiś sposób użyte w kodzie, linker ich nie dołączy do pliku wynikowego. Można oczywiście napisać jakiś fragment (później do niczego niepotrzebny), w którym te dane będą użyte, aby wymusić dołączenie ich przez linker do pliku wynikowego *.hex, ale wygodniejszym i bardziej eleganckim rozwiązaniem będzie dodanie opcji -u symbol do linii poleceń linkera (opcja ta to skrót od opcji --undefined=symbol).

W Atmel Studio 7 służy do tego celu:

menu Project→Properties…→Toolchain→AVR/GNU Linker→Miscellaneous→pole tekstowe Other Linker Flags

lub w Eclipse MARS 2 to samo miejsce, w którym wcześniej definiowaliśmy naszą sekcję, czyli:

menu Project→Properties→C/C++ Build→Settings→(zakładka Tool Settings) AVR C Linker→General→pole tekstowe Other Arguments

W zasadzie wpisanie jednej zmiennej znajdującej się w danej sekcji powinno spowodować, że linker dołączy całą sekcję, czyli wszystkie zmienne, które w tej sekcji zostały zdefiniowane. W naszym przypadku oznacza to, że dodanie linkerowi opcji:

-u sine_wave

powinno spowodować dołączenie przez linker nie tylko tablicy sine_wave, ale także tablicy sawtooth_wave. W niektórych przypadkach (np. kiedy mamy nasze tablice umieszczone w innych sekcjach) może być konieczne dodanie pozostałych zmiennych, czyli w naszym przypadku:

-u sine_wave -u sawtooth_wave

Po tym zabiegu i skompilowaniu projektu, zmienne powinny już być dołączone do pliku wynikowego, nawet jeśli nie są nigdzie używane w kodzie. Uruchomienie teraz naszego narzędzia wydobywającego informacje o rozmieszczeniu danych w pamięci FLASH (pod warunkiem dodania w opcjach narzędzia do pola Arguments nazwy naszej sekcji .waveforms, jak to opisałem wcześniej) powinno wygenerować co następuje:

00010100 g     O .waveforms	00000800 sawtooth_wave
00010900 g     O .waveforms	00000800 sine_wave

Należy tu zwrócić uwagę na to, że kolejność umieszczenia danych w pamięci FLASH jest odwrotna do kolejności definicji w kodzie. Oczywiście możemy sobie przyjąć dowolną kolejność naszych wskaźników w tablicy, chodzi tylko o to, by mieć świadomość, jakie wartości adresów są przyporządkowane poszczególnym nazwom zmiennych. Gdybyśmy chcieli zachować taką samą kolejność wskaźników jak definicji, deklaracja naszej tablicy powinna wyglądać następująco:

Język: C Zwiń
  1. // globalnie, czyli przed funkcjami
  2. // tablica wskaźników do tablic liczb 16-bitowych bez znaku (w sekcji .waveforms)
  3. // tablica wskaźników nie zostanie zapisana w sekcji .waveforms,
  4. // tylko w pierwszym segmencie 64KiB
  5. const __memx uint16_t * const __flash wave_ptrs[2] = {
  6. (const __memx uint16_t *)0x10900, /* sine_wave */
  7. (const __memx uint16_t *)0x10100, /* sawtooth_wave */
  8. };

Wprawdzie tablica wskaźników nie zostanie zapisana w naszej sekcji .waveforms, tylko (standardowo) w pierwszym segmencie 64KiB, jednak nie powinno to stanowić problemu, ponieważ zwykle tablice wskaźników nie mają jakiegoś znaczącego rozmiaru, za to obsługa zapisanych w ten sposób wskaźników będzie wygodniejsza.

Przedstawię teraz kod demonstrujący opisany powyżej sposób. Nie jest to wprawdzie jakiś użyteczny projekt, choć powinien działać prawidłowo na mikrokontrolerze ATmega2560. Kod generuje za pomocą sygnału PWM na pinie OC1A cztery przebiegi (rozdzielczość 10-bitowa) – sinus, piła, trójkąt i "użytkownika" – każdy o częstotliwości około 15Hz oraz o czasie trwania około 1 sekundy z przerwami trwającymi około 0,5 sekundy. Jeśli ktoś dysponuje mikrokontrolerem ATmega2560 i chciałby poeksperymentować, to oprócz przedstawionych poniżej plików main.c oraz wavedata.h należy do projektu dołączyć plik wavedata.c (do ściągnięcia w postaci spakowanej ZIP), zawierającego tablice z próbkami przebiegów, którego nie zaprezentowałem tutaj, ze względu na jego obszerność.

Plik main.c:

Język: C Zwiń
  1. // main.c
  2.  
  3. #include <avr/io.h>
  4. #include <stddef.h>
  5. #include <util/delay.h>
  6. #include <avr/interrupt.h>
  7. #include <stdbool.h>
  8. #include "wavedata.h"
  9.  
  10. // definiujemy tablicę wskaźników do tablic
  11. const __memx uint16_t * const __flash wave_ptrs[] = {
  12. SINE_PTR, /* sine_wave */
  13. SAWTOOTH_PTR, /* sawtooth_wave */
  14. TRIANGLE_PTR, /* triangle_wave */
  15. CUSTOM_PTR /* custom_wave */
  16. };
  17.  
  18. // zmienna przechowująca wskaźnik aktualnie generowanego przebiegu
  19. volatile const __memx uint16_t * current_wave_ptr;
  20.  
  21. // flaga stop służąca do zatrzymania generatora przebiegu
  22. // po zakończeniu pełnego okresu
  23. volatile bool stop_flag = false;
  24.  
  25. // funkcja startująca generowanie przebiegu
  26. void start_generator(const __memx uint16_t * wave_ptr);
  27.  
  28. // funkcja zatrzymująca generowanie przebiegu
  29. inline void stop_generator(void) { stop_flag = true;}
  30.  
  31. // ------------------------------------------------------------------
  32. int main(void) {
  33.  
  34. uint8_t i = 0;
  35.  
  36. // konfiguracja timera
  37. // tryb 7 - fast PWM 10 bit, wyjście OC1A nieodwracające
  38. TCCR1A = (1<<COM1A1) | (1<<WGM11) | (1<<WGM10);
  39. TCCR1B = (1<<WGM12);
  40.  
  41. // wartość OCR1A równa połowie rozdzielczości
  42. // co odpowiada wirtualnemu poziomowi 0V generowanego
  43. // przebiegu
  44. OCR1A = VIRTUAL_ZERO;
  45.  
  46. // start generowania PWM
  47. TCCR1B |= (1<<CS10);
  48.  
  49. // pin OC1A (pin 5 na porcie B) jako wyjście
  50. DDRB |= (1<<DDB5);
  51.  
  52. // włączenie przerwań
  53. sei();
  54.  
  55. while (1) {
  56.  
  57. // uruchomienie generowania sygnału
  58. start_generator(wave_ptrs[i++]);
  59. // tutaj przekazujemy wskaźniki z tablicy na podstawie indeksu
  60. // inkrementowanego w pętli, ale można również wywołać funkcję
  61. // przekazując bezpośredni wskaźnik do któregoś z przebiegów np.:
  62. // start_generator(SINE_PTR);
  63.  
  64. // ograniczenie wartości 'i' do ilości wskaźników w tablicy
  65. if (i == sizeof(wave_ptrs)/sizeof(wave_ptrs[0])) i = 0;
  66. // włączenie opóźnienia około 1 sekundy;
  67. // ze względu na włączoną obsługę przerwań funkcja
  68. // _delay_ms() nie odmierzy czasu zbyt dokładnie
  69. // ale nie to jest tutaj celem
  70. // w trakcie opóźnienia przebieg jest generowany
  71. // poprzez procedurę obsługi przerwania
  72. _delay_ms(1000);
  73.  
  74. // zatrzymanie generowania przebiegu
  75. // zatrzymanie nastąpi dopiero po zakończeniu generowania
  76. // pełnego okresu
  77. stop_generator();
  78.  
  79. // przerwa między generowanymi przebiegami
  80. _delay_ms(500);
  81. }
  82. }
  83. // ------------------------------------------------------------------
  84.  
  85. // procedura obsługi przerwania od przepełnienia timera 1
  86. ISR(TIMER1_OVF_vect) {
  87. // zmienna zawierająca indeks tablicy dla aktualnej próbki
  88. static uint16_t j = 0;
  89.  
  90. // wpisanie próbki o indeksie 'j' do rejestru OCR1A
  91. // dzięki użyciu kwalifikatora __flash i zapisaniu
  92. // tablicy wskaźników w pierwszym segmencie 64KiB
  93. // w celu wprowadzenia wartości aktualnej próbki
  94. // do rejestru OCR1A wystarczy poniższa linijka
  95. OCR1A = *(current_wave_ptr + j++);
  96.  
  97. // po osiągnięciu końca tablicy zaczynamy od początku
  98. if (j == SAMPLES_CNT) j = 0;
  99.  
  100. // zatrzymanie generowania sygnału dopiero po dokończeniu
  101. // generowania pełnego okresu
  102. if ( (j == 1 ) && stop_flag ) {
  103. j = 0;
  104. TIMSK1 &= ~(1<<TOIE1);
  105. }
  106. }
  107. // funkcja startująca generowanie przebiegu
  108. void start_generator(const __memx uint16_t * wave_ptr) {
  109. // ustawienie aktualnego wskaźnika na tablicę
  110. // z żądanym przebiegiem
  111. current_wave_ptr = wave_ptr;
  112.  
  113. // wyzerowanie flagi 'stop'
  114. stop_flag = false;
  115.  
  116. // wyzerowanie flagi przerwania od przepełnienia
  117. TIFR1 = (1<<TOV1);
  118.  
  119. // wyzerowanie licznika
  120. TCNT1 = 0;
  121.  
  122. // zezwolenie na obsługę przerwań od przepełnienia
  123. TIMSK1 |= (1<<TOIE1);
  124. }

Plik wavedata.h:

Język: C Zwiń
  1. // wavedata.h
  2. #ifndef WAVEDATA_H_
  3. #define WAVEDATA_H_
  4.  
  5. // definicja rzutowania na wskaźnik __memx do danych typu int16_t
  6. #define MEMX_I16_PTR(p) (const __memx uint16_t *)(p)
  7.  
  8. // definicje adresów do poszczególnych tablic
  9. #define SINE_PTR MEMX_I16_PTR(0x11800)
  10. #define SAWTOOTH_PTR MEMX_I16_PTR(0x11000)
  11. #define TRIANGLE_PTR MEMX_I16_PTR(0x10800)
  12. #define CUSTOM_PTR MEMX_I16_PTR(0x10000)
  13.  
  14. // ustalamy ilość elementów tablic na 1024
  15. // rozmiar (w bajtach) wyniesie:
  16. // 1024 * sizeof(int16_t) = 1024 * 2 = 2048 bajtów
  17. #define SAMPLES_CNT 1024
  18. #define VIRTUAL_ZERO (SAMPLES_CNT/2-1)
  19.  
  20. #endif /* WAVEDATA_H_ */

• Funkcje uniwersalne z wykorzystaniem kwalifikatora __memx

Chyba powszechnie wiadomo, że do obsługi danych w pamięci FLASH, służą inne funkcje bibliotek standardowych, niż do obsługi danych w pamięci RAM (mają one przyrostek „_P” lub „_PF”). Wynika to z faktu, że należy w inny sposób odczytywać takie dane, a sam wskaźnik (standardowy 16‑bitowy) nie informuje o tym, gdzie dane się znajdują. Dlatego szereg funkcji standardowych musi mieć trzy wersje. Jedna z wersji traktuje przekazywany do funkcji wskaźnik jako wskaźnik do RAM, druga (z przyrostkiem „_P”) – jako wskaźnik do FLASH poniżej limitu 64KiB, trzecia (z przyrostkiem „_PF”) – jako wskaźnik do FLASH powyżej 64KiB. Przykładami takich funkcji mogą być:

Jeśli tworzymy własne funkcje, również musimy stosować tę samą zasadę. Każdy chyba przyzna, że – choć relatywnie szybkie, ze względu na 16‑bitowe wskaźniki (oczywiście nie dotyczy to funkcji z przyrostkiem „_PF”) – nie jest to wygodne rozwiązanie.

Problem ten możemy jednak rozwiązać, stosując jako parametry funkcji wskaźniki z kwalifikatorem __memx. Wprawdzie funkcja taka będzie nieco wolniejsza ze względu na to, że takie wskaźniki są 24‑bitowe, ale nie wszystkie funkcje są krytyczne czasowo (np. obsługa menu) i nie zawsze musimy oszczędzać każdy takt zegara, a wygoda używania jednej funkcji do obsługi danych zapisanych zarówno w pamięci FLASH (w dowolnym segmencie 64KiB) jak i RAM jest moim zdaniem nieoceniona.

Jak to się dzieje, że jest to możliwe?

Wskaźnik z kwalifikatorem __memx łączy w sobie wszystkie przestrzenie adresowe mikrokontrolera w ten sposób, że zawiera regiony:

adres startowy przeznaczenie
0x000000 FLASH
0x80nnnn RAM
0x810000 EEPROM
0x820000 FUSE
0x830000 LOCK
0x840000 SIGNATURE
0x850000 USER_SIGNATURE

Adres startowy regionu RAM (opisany jako 0x80nnnn) jest zależny od architektury mikrokontrolera, dla którego jest generowany kod. Może się on wahać od 0x800040 dla architektury avrtiny do 0x800200 dla architektury avr6 (dla architektur avrxmega wynosi nawet 0x802000).

Rozmiary (czyli także adresy końcowe) poszczególnych regionów również zależą od architektury mikrokontrolera.

Nas interesują tylko pierwsze dwa regiony. Jak łatwo zauważyć, podstawową cechą, która odróżnia te regiony jest siódmy bit najstarszego bajtu adresu. Kiedy więc przekazujemy do funkcji wskaźnik z kwalifikatorem __memx (bajt2:bajt1:bajt0), kompilator generuje kod, który na podstawie tego właśnie bitu odróżnia lokalizację danych:

Niestety kompilator nie obsługuje w ten sposób danych zapisanych np. w regionie EEPROM (czy też w innych regionach). Gdybyśmy próbowali przekazać do funkcji wskaźnik do pamięci EEPROM, zostanie on potraktowany jako wskaźnik do pamięci RAM (pomimo tego, że adres jest z zakresu regionu EEPROM). Kompilator sprawdza bowiem tylko siódmy bit najstarszego bajtu, a nie sprawdza bitu 0 najstarszego bajtu, który w przypadku regionu EEPROM jest równy 1 (w odróżnieniu do regionu RAM, gdzie jest zerem), więc nie może odróżnić RAM i EEPROM. Inaczej mówiąc, wszystkie adresy rozpoczynające się od jedynki są traktowane jako wskaźniki do pamięci RAM, więc chcąc operować na danych w EEPROM musimy utworzyć osobną funkcję.

Postaram się przedstawić w poniższym przykładzie kodu, jak taką funkcję stworzyć i jak jej używać. Będzie to kod dla ATmega2560 zawierający uniwersalną funkcję do wysyłania ciągów znaków poprzez UART (start sekcji .strings: adres słowa równy 0x10000 lub adres bajtu równy 0x20000):

Język: C Zwiń
  1. // main.c
  2.  
  3. #include <avr/io.h>
  4. #include <avr/pgmspace.h>
  5. #include <stdlib.h>
  6.  
  7. // makro zwracające 24-bitowy wskaźnik do zmiennej ‘var’
  8. #define get_memx_address(var) \
  9. ({ \
  10.   __int24 tmp; \
  11.   \
  12.   __asm__ __volatile__( \
  13.   \
  14.   "ldi %A0, lo8(%1)" "\n\t" \
  15.   "ldi %B0, hi8(%1)" "\n\t" \
  16.   "ldi %C0, hh8(%1)" "\n\t" \
  17.   : \
  18.   "=d" (tmp) \
  19.   : \
  20.   "p" (&(var)) \
  21.   ); \
  22.   (__memx typeof(var)*)tmp; \
  23. })
  24.  
  25. // początek sekcji np. 0x10000(word) lub 0x20000(byte)
  26. #define STRINGS __attribute__ ((section(".strings")))
  27.  
  28. // zdefiniowanie prędkości transmisji
  29. #define BAUDRATE 9600
  30. // obliczenie wartości rejestru UBRR0
  31. #define UBRR0_VALUE (F_CPU - 8UL*BAUDRATE)/16/BAUDRATE // U2X0 = 0
  32.  
  33. // zdefiniowany literał łańcuchowy
  34. #define DEFINED_STRING_LITERAL "Defined string literal.\n"
  35.  
  36. // ciąg znaków w pamięci RAM; sposób nie zalecany, ponieważ
  37. // ciąg zajmuje zarówno pamięć FLASH jak i RAM
  38. const char ram_string[] = "RAM string.\n";
  39.  
  40. // bufor znakowy np. do odbierania wyników
  41. // działania funkcji konwertujących, takich jak 'itoa()'
  42. char ram_buffer[16];
  43.  
  44. // ciąg znaków w pamięci FLASH umieszczony tam
  45. // przy pomocy makra PROGMEM
  46. const char flash_str_pgm[] PROGMEM = "PROGMEM string.\n";
  47.  
  48. // ciąg znaków w pamięci FLASH umieszczony tam
  49. // przy pomocy kwalifikatora __flash
  50. const __flash char flash_str_flash[] = "__flash string.\n";
  51.  
  52. // ciąg znaków w pamięci FLASH umieszczony tam
  53. // przy pomocy kwalifikatora __memx
  54. const __memx char flash_str_memx[] = "__memx string.\n";
  55.  
  56. // ciąg znaków w pamięci FLASH umieszczony tam
  57. // przy pomocy zdefiniowanej sekcji
  58. const char flash_str_custom[] STRINGS = "FLASH custom section string.\n";
  59.  
  60. // funkcja uniwersalna zdolna do wysłania poprzez UART
  61. // dowolnego z powyższych ciągów znaków
  62. void uart_puts(const __memx char * str);
  63.  
  64. // funkcja wysyłająca pojedynczy znak
  65. void uart_putc(char c);
  66.  
  67. int main(void)
  68. {
  69. uint8_t i = 0;
  70. // konfiguracja USART0
  71. // ustawienie prędkości transmisji na wartość ustawioną
  72. // przez definicję BAUDRATE
  73. UBRR0 = UBRR0_VALUE;
  74. // włączenie nadajnika i odbiornika
  75. UCSR0B = (1<<RXEN0) | (1<<TXEN0);
  76. // ramka 8-bitowa, 1 bit stopu, bez bitu parzystości
  77. UCSR0C = (1<<UCSZ01) | (1<<UCSZ00);
  78.  
  79. while (1)
  80. {
  81. // --- wyświetlenie numeru pętli ---
  82. // anonimowy ciąg znaków umieszczony przez makro PSTR
  83. // w pamięci FLASH
  84. uart_puts( PSTR("Loop number: ") );
  85. // sposób użycia bufora w RAM do wyświetlenia
  86. // wartości zmiennej
  87. uart_puts( utoa(++i, ram_buffer, 10) );
  88. // anonimowy ciąg znaków umieszczony w pamięci RAM;
  89. // nie zalecane, szczególnie dla dłuższych ciągów,
  90. // ponieważ ciąg zajmuje zarówno pamięć FLASH jak i RAM
  91. uart_puts( ".\n" );
  92.  
  93. // zdefiniowany literał znakowy; jak wyżej - makro
  94. // PSTR umieści anonimowy ciąg znaków w pamięci FLASH
  95. // stamtąd będzie odczytany i wysłany przez funkcję
  96. // 'uart_puts()'
  97. uart_puts( PSTR(DEFINED_STRING_LITERAL) );
  98.  
  99. // ciąg znaków w pamięci RAM; sposób nie zalecany, ponieważ
  100. // ciąg zajmuje zarówno pamięć FLASH jak i RAM
  101. uart_puts( ram_string );
  102.  
  103. // ciąg znaków w pamięci FLASH umieszczony tam
  104. // przy pomocy makra PROGMEM
  105. uart_puts( flash_str_pgm );
  106.  
  107. // ciąg znaków w pamięci FLASH umieszczony tam
  108. // przy pomocy kwalifikatora __flash
  109. uart_puts( flash_str_flash );
  110.  
  111. // ciąg znaków w pamięci FLASH umieszczony tam
  112. // przy pomocy kwalifikatora __memx
  113. uart_puts( flash_str_memx );
  114.  
  115. // ciąg znaków w pamięci FLASH umieszczony tam
  116. // przy pomocy zdefiniowanej sekcji
  117. // należy pamiętać, że argumentem makra
  118. // 'get_memx_address' musi być pierwszy element
  119. // ciągu znaków, czyli należy użyć indeksu 0 w nawiasie
  120. // kwadratowym po nazwie zmiennej
  121. uart_puts( get_memx_address( flash_str_custom[0] ) );
  122. } // while (1)
  123. } // main()
  124.  
  125.  
  126. void uart_puts(const __memx char * str) {
  127. while (*str) uart_putc(*str++);
  128. }
  129.  
  130. void uart_putc(char c) {
  131. while ( !(UCSR0A & (1<<UDRE0)) );
  132. UDR0 = c;
  133. }

Podsumowanie

Przedstawiłem tutaj kilka przykładów kodu, jednak nie sposób rozpatrzyć wszystkich możliwych przypadków obsługi danych w pamięci FLASH. W pamięci tej można umieścić praktycznie każdy typ danych jak np. typy całkowite, zmiennoprzecinkowe, wskaźniki, ciągi znaków, struktury, tablice. Odczyt danych poszczególnych typów będzie inny i zależny od implementacji i potrzeb danego projektu. Dlatego starałem się przedstawić tutaj ogólne zasady obsługi, aby każdy mógł samodzielnie znaleźć własny, najbardziej optymalny dla swojego projektu sposób.