Jednym z popularnych formatów przechowywania kolorowych obrazów jest GIF, którego podstawy obsługi na mikrokontrolerach STM32 przedstawiamy w artykule. Przykład bazuje na modułach z oferty KAMAMI.pl, ale można go zaimplementować na dowolną platformę bazującą na STM32.
W projekcie przedstawiamy sposób wyświetlenia kolorowych obrazów zapisanych w plikach na karcie SD, w popularnym formacie GIF. W przykładzie wykorzystano moduł z wyświetlaczem LCD (KAmodTFT2), moduł do odczytu z kart SD (KAmodMMC), z którą komunikacja odbywa się za pomocą protokołu SPI oraz komputerek zintegrowany z programatorem JTAG – ZL31ARM z wbudowanym mikrokontrolerem STM32F103RB.
Schemat elektryczny niezbędnych połączeń linii we/wy mikrokontrolera STM32 z zestawu ZL31ARM z modułem KAmodMMC i KAmodTFT2 przedstawiono na poniższych rysunkach.
Rys. 1. Schemat podłączenia modułu wyświetlacza
Rys. 2. Schemat podłączenia przycisków
Rys. 3. Schemat podłączenia modułu obsługi karty SD
Konfiguracja projektu
Programy opisane w artykule powstały na bazie bibliotek opisanych w książce „Mikrokontrolery STM32 w praktyce”, dlatego przed rozpoczęciem ich testowania należy pobrać pliki źródłowe przykładów z tej książki (dostępne m.in. na stronie Wydawnictwa BTC). W pliku archiwum znajduje się katalog Libraries, który należy skopiować do katalogu z zainstalowanym środowiskiem µVision (np. C:\Keil\ARM\). Z biblioteki należy wybrać potrzebne pliki i dodać je do własnego projektu: przykładowo plik stm32f10x_conf.h można skopiować np. z katalogu \Project\R9_sd_fatfs tejże biblioteki i nie wymaga on żadnej modyfikacji. Następnie klikamy na Options for Target… i w zakładce C/C++ w polu Define dodajemy następujące parametry: STM32F10X_MD, USE_STDPERIPH_DRIVER. Oprócz tego należy dodać ścieżki dostępu do plików źródłowych w Include Paths (rysunek 4). W zakładce Debug wybieramy Use ST-Link (Deprecated Version) i to samo robimy w zakładce Utilities, gdzie występuje pole Use Target Driver for Flash Programming.
Rys. 4. Dodane ścieżki dostępu w Include Paths
Kolejną rzeczą jest pobranie załącznika „Przykładowy program dla STM32” dla produktu KAmodTFT2, przekopiowanie następujących plików do katalogu projektu i dodanie ich w programie µVision do grupy User: board.h, lcdlib.c, lcdlib.h, SystemInit.c. Następnie zmieniamy wiersz na początku powyższych plików z #include “stm32f10x_lib.h” na #include “stm32f10x.h”. Dodatkowo w celu wyświetlenia symboli tekstowych należy pobrać załącznik do artykułu na stronie http://www.mikrokontroler.pl/node/248 oraz skopiować plik fonts.c do katalogu projektu, a z pliku main.c skopiować fragment dotyczący funkcji LCDPutChar() i zapisać do lcdlib.c.
Dalej w projekcie należy dodać nową grupę FatFS i dołączyć do niej pliki do obsługi systemu plików FAT. Można to zrobić kopiując z katalogu przykładowych programów .\Project\R9_sd_fatfs katalog fatfs do katalogu projektu. Podczas kompilacji może wystąpić błąd odnalezienia pliku podłączanego w SD_stm32.c wtedy należy wiersz #include “stm32f10x_lib.h” zamienić na #include “stm32f10x.h”. Pliki main.c, imageGIF.c oraz imageGIF.h zostaną utworzone oddzielnie. Na rysunku 5 przedstawiono strukturę całego projektu.
Rys. 5. Drzewo projektu
Aby mieć możliwość sterowania programem za pomocą joysticku oraz obsługi modułu KAmodMMC należy odpowiednio zmodyfikować plik board.h którego struktura końcowa powinna wyglądać następująco:
// LCD connection #define PORT_ctrl GPIOC #define RCC_APB2Periph_ctrl RCC_APB2Periph_GPIOC #define DATA GPIO_Pin_4 #define CLK GPIO_Pin_5 #define CS GPIO_Pin_6 #define RES GPIO_Pin_7 // Joystick #define JOY_PORT_OK GPIOB #define JOY_OK GPIO_Pin_5 #define RCC_APB2Periph_JOYOK RCC_APB2Periph_GPIOB #define JOY_PORT GPIOC #define JOY_UP GPIO_Pin_0 #define JOY_DOWN GPIO_Pin_1 #define JOY_LEFT GPIO_Pin_2 #define JOY_RIGHT GPIO_Pin_3 #define RCC_APB2Periph_JOY RCC_APB2Periph_GPIOC // KAmodMMC #define SD_PORT GPIOA #define SD_CS GPIO_Pin_4 #define SD_CLK GPIO_Pin_5 #define SD_MISO GPIO_Pin_6 #define SD_MOSI GPIO_Pin_7
Konfiguracja portów wykorzystywanych przez joystick oraz wyświetlacz w pliku SystemInit.c przebiega następująco:
void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; // LCD lines configuration RCC_APB2PeriphClockCmd(RCC_APB2Periph_ctrl, ENABLE); GPIO_InitStructure.GPIO_Pin = DATA | CLK | CS | RES ; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(PORT_ctrl, &GPIO_InitStructure); // Joystick lines configuration RCC_APB2PeriphClockCmd(RCC_APB2Periph_JOY, ENABLE); GPIO_InitStructure.GPIO_Pin = JOY_UP | JOY_DOWN | JOY_LEFT | JOY_RIGHT; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(JOY_PORT, &GPIO_InitStructure); // Joystick OK line configuration RCC_APB2PeriphClockCmd(RCC_APB2Periph_JOYOK, ENABLE); GPIO_InitStructure.GPIO_Pin = JOY_OK; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(JOY_PORT_OK, &GPIO_InitStructure); // Configuration of LED pins RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); }
Konfiguracja sygnałów taktujących:
//Konfigurowanie sygnalow taktujacych void RCC_Configuration(void){ ErrorStatus HSEStartUpStatus; //zmienna opisujaca rezultat uruchomienia HSE RCC_DeInit(); //Reset ustawien RCC RCC_HSEConfig(RCC_HSE_ON); //Wlaczenie HSE HSEStartUpStatus = RCC_WaitForHSEStartUp(); //Odczekaj az HSE bedzie gotowy if(HSEStartUpStatus == SUCCESS) { FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);// FLASH_SetLatency(FLASH_Latency_2); //ustaw zwloke dla pamieci Flash; zaleznie od //taktowania rdzenia //0:<24MHz; 1:24~48MHz; 2:>48MHz RCC_HCLKConfig(RCC_SYSCLK_Div1); //ustaw HCLK=SYSCLK RCC_PCLK2Config(RCC_HCLK_Div1); //ustaw PCLK2=HCLK RCC_PCLK1Config(RCC_HCLK_Div2); //ustaw PCLK1=HCLK/2 //ustaw PLLCLK = HSE*9 czyli 8MHz * 9 = 72 MHz RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); //wlacz PLL //odczekaj na poprawne uruchomienie PLL while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); //ustaw PLL jako zrodlo sygnalu zegarowego RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); //odczekaj az PLL bedzie sygnalem zegarowym systemu while(RCC_GetSYSCLKSource() != 0x08); } }
Natomiast linie wykorzystywane przez moduł KAmodMMC oraz FatFS są konfigurowane w pliku sd_stm32.c (należy również podłączyć w nim plik board.h) w funkcji power_on():
static void power_on (void) { //... // Konfiguracja wyprowadzen i kontrolera SPI: // Wlaczenie sygnalow zegarowych dla peryferiow RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_SPI1 | RCC_APB2Periph_AFIO, ENABLE); // PA4 jako CS GPIO_InitStructure.GPIO_Pin = SD_CS; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(SD_PORT, &GPIO_InitStructure); //SCK, MISO and MOSI GPIO_InitStructure.GPIO_Pin = SD_CLK | SD_MISO | SD_MOSI; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(SD_PORT, &GPIO_InitStructure); // Konfiguracja SPI SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); // Wlacz SPI SPI_Cmd(SPI1, ENABLE); //... }
Struktura pliku formatu GIF
GIF (Graphics Interchange Format) – jest formatem pliku graficznego stworzonym w 1987 roku przez firmę CompuServe, w którym dane o obrazie są zazwyczaj skompresowane metodą bezstratną. Pliki tego typu są powszechnie używane m.in. na stronach WWW i posiadają kilka ciekawych właściwości, stąd też warto przybliżyć ten temat w zastosowaniu praktycznym.
Problem wyświetlania obrazów pobranych z plików .gif można podzielić na dwa etapy: etap odczytu ogólnych informacji o obrazach wraz z wykonaniem wstępnych czynności oraz etap ich dekompresji i wyświetlenia. Pierwsza część zostanie z grubsza omówiona poniżej, a więcej szczegółów należy szukać w dokumentacji formatu GIF [1]. Natomiast problem dekompresji zostanie opisany w kolejnej sekcji wraz z przedstawieniem prostego przykładu działania.
Pliki formatu GIF mają strukturę blokową (rysunek 6), a same bloki można podzielić na dwie grupy: podstawowe i rozszerzone. Do podstawowych należą bloki: nagłówek/sygnatura, deskryptor ekranu logicznego, globalna paleta kolorów, deskryptor obrazu, lokalna paleta kolorów, blok danych skompresowanych lub nieskompresowanych oraz terminator. Do rozszerzonej grupy można zaliczyć pozostałe typy bloków, które są pewnym rozwinięciem możliwości formatu GIF, jak np. tekst. W danym projekcie uwagę skupiono na realizacji prostych funkcji operujących na podstawowych blokach, które w razie potrzeby mogą być łatwo rozszerzone i uzupełnione w nowe, obsługujące dodatkowe/rozszerzone bloki.
Rys. 6. Ogólna struktura formatu GIF87a
Pierwszym blokiem w pliku GIF jest sygnatura o długości 6 bajtów, z których pierwsze 3 to symbole znakowe GIF, a pozostałe określają wersję formatu (87a lub 89a). Służy ona wyłącznie w celu identyfikacji. Fragment kodu realizującego operację identyfikacji:
f_read(g_fileObject, &BUFFER, 6, &g_odczytanych_bajtow); //Pobierz sygnature pliku GIF if(checkSignature((char*)BUFFER) != 1) return -1; //Sprawdz sygnature
oraz funkcja checkSignature() przyjmująca wskaźnik do ciągu znaków:
//************************************************************ //Funkcja sprawdzajaca sygnature pliku GIF (GIF87a lub GIF89a) //Zwraca: 0 - sygnatura nie jest zgodna // 1 - sygnatura jest zgodna //************************************************************ int checkSignature(char * IN){ char signature[6]; int i; for(i = 0; i < 6; i++) signature[i] = IN[i]; if( (strcmp(signature,"GIF87a") == 0) || (strcmp(signature,"GIF89a") == 0) ) return 1; //Jeżeli sygnatura jest zgodna else return 0; //Jeżeli sygnatura nie jest zgodna }
Kolejnym blokiem znajdującym się zaraz za sygnaturą jest deskryptor ekranu logicznego o długości 7 bajtów, w którym znajdują się takie parametry jak szerokość i wysokość ekranu logicznego w pikselach (w nim „znajdować się” będą obrazy), informacja dotycząca globalnej palety kolorów, liczba bitów na piksel, kolor tła i postać pikseli. Te informacje po odczytaniu z pliku będą zawarte w strukturze:
//Struktura do przechowywania deskryptora ekranu logicznego typedef struct{ unsigned short SCREEN_WIDTH; unsigned short SCREEN_HEIGHT; unsigned char BYTE_5; unsigned char BACKGROUND; unsigned char ASPECT_RATIO; } SCREEN_DESC;
Sam odczyt wygląda następująco:
f_read(g_fileObject, &BUFFER, 7, &g_odczytanych_bajtow); //Pobierz kolejne 7 bajtow //Pobierz LOGICAL SCREEN DESCRIPTOR (*gifImage).screenDesc.SCREEN_WIDTH = (BUFFER[1] << 8) + BUFFER[0]; (*gifImage).screenDesc.SCREEN_HEIGHT = (BUFFER[3] << 8) + BUFFER[2]; (*gifImage).screenDesc.BYTE_5 = BUFFER[4]; (*gifImage).screenDesc.BACKGROUND = BUFFER[5]; (*gifImage).screenDesc.ASPECT_RATIO = BUFFER[6]; //Zazwyczaj wynosi 0
Najważniejsze parametry wykorzystywane w programie z tego bloku będą pobrane z piątego bajtu, którego struktura jest następująca:
- Byte5<7> - flaga obecności globalnej palety kolorów,
- Byte5<6:4> - wartość głębi kolorów,
- Byte5<3> - flaga uporządkowania palety kolorów,
- Byte5<0:2> - rozmiar globalnej palety kolorów.
Kod pobierający potrzebne dane:
byte5 = (*gifImage).screenDesc.BYTE_5; //Dane ogolne o obrazach w pliku GIF bpp = (byte5 & 0x07); //Wartosc bits/pixel - 1 GCT_size = 2 << bpp; //Okreslenie rozmiaru globalnej palety //size(GCT) = 2^(bpp+1)
Globalna paleta kolorów (Global Color Table) jest kolejnym blokiem i zawiera elementy zwane triadami charakteryzujące indeksowane kolory i może być ich maksymalnie 256. Triady składają się z 3 bajtów opisujących kolejno intensywność koloru czerwonego R, zielonego G i niebieskiego B (rysunek 7). Blok GCT jest blokiem opcjonalnym i może być tylko jeden w całym pliku GIF. Jeżeli blok ten występuje to znajduje się on zaraz za deskryptorem ekranu logicznego i jest wykorzystywany przez obrazy nie posiadające lokalnej palety kolorów. W przypadku gdy dany blok danych posiada (i wykorzystuje) lokalną paletę to globalna nie jest w nim używana. W danym projekcie pominięto realizację obsługi plików z lokalną paletą ze względu na ograniczenia w pamięci.
Rys. 7. Struktura bloku GCT
O obecności bloku GCT informuje ustawiony 7 bit (Global Color Table Flag) w piątym bajcie (Packed Fields) deskryptora ekranu logicznego, a 3 najmłodsze bity tego bajtu (liczba n) określają liczbę triad w bloku, która jest zawsze równa liczbie 2 podniesionej do potęgi. Rozmiar rzeczywisty bloku GCT w bajtach jest równy:
size(GCT) = 3 × 2n+1 [Bajt]
Wykorzystując informacje pobrane w poprzednim kroku zostanie sprawdzona obecność globalnej palety i ewentualnie odtworzona. Jeżeli brak takowej to odczyt pliku zostanie przerwany, jednak zawsze istnieje możliwość obejścia tego problemu w postaci stworzenia systemowej palety.
//Sprawdz obecnosc globalnej palety kolorow (GLOBAL COLOR TABLE) if(byte5 & 0x80){ //Utworz tablice kolorow for(i = 0; i < GCT_size; i++){ fresult = f_read(g_fileObject, &BUFFER, 3, &odczytanych_bajtow); g_colorTable[i].R = BUFFER[0]; g_colorTable[i].G = BUFFER[1]; g_colorTable[i].B = BUFFER[2]; } } else return -1;
Za blokiem z globalną paletą znajduje się blok deskryptora obrazu jednak czasami między nimi mogą wystąpić inne bloki rozszerzone, dlatego koniecznym jest znalezienie początku sekcji (bajt o wartości 0x2C) i dopiero za nim można czytać właściwe dane:
//Czytaj kolejne bajty dopoki nie wystapi kod poczatku sekcji (0x2C) do f_read(g_fileObject, &BUFFER, 1, &g_odczytanych_bajtow); while(BUFFER[0] != 0x2C);
Deskryptor obrazu, również nazywany lokalnym, występuje zaraz za bajtem 0x2C w każdej sekcji dotyczącej danych konkretnego obrazu i może graniczyć z blokiem lokalnej palety kolorów lub też z blokiem danych obrazu. Struktura przechowująca ten deskryptor jest następująca:
//Struktura do przechowywania deskryptora obrazu typedef struct{ unsigned short LEFT; unsigned short UP; unsigned short IMAGE_WIDTH; unsigned short IMAGE_HEIGHT; unsigned char BYTE_9; } IMAGE_DESC;
Jest on pobierany w następujący sposób:
f_read(g_fileObject, &BUFFER, 11, &g_odczytanych_bajtow); //Pobierz kolejne 11 bajtow //Pobierz IMAGE DESCRIPTOR (*gifImage).imageDesc.LEFT = (BUFFER[1] << 8) + BUFFER[0]; (*gifImage).imageDesc.UP = (BUFFER[3] << 8) + BUFFER[2]; (*gifImage).imageDesc.IMAGE_WIDTH = (BUFFER[5] << 8) + BUFFER[4]; (*gifImage).imageDesc.IMAGE_HEIGHT = (BUFFER[7] << 8) + BUFFER[6]; (*gifImage).imageDesc.BYTE_9 = BUFFER[8]; imgWidth = (*gifImage).imageDesc.IMAGE_WIDTH; //Szerokosc obrazu [pixels] imgHeight = (*gifImage).imageDesc.IMAGE_HEIGHT; //Wysokosc obrazu [pixels] byte9 = (*gifImage).imageDesc.BYTE_9; // minLZWCodeLength = BUFFER[9] + 1; //Minimalna dlugosc kodu LZW w bitach dataSectionLength = BUFFER[10]; //Dlugosc sekcji z danymi
Pierwsze dwa parametry określają przesunięcie obrazu na ekranie logicznym i zazwyczaj są one równe 0x00. Kolejne parametry to szerokość i wysokość obrazu oraz 9 bajt zawierający informacje o lokalnej palecie kolorów (flaga występowania i ewentualny rozmiar) i czy dane należy odczytywać metodą przeplataną (interlace flag). Pozostałe 2 bajty odnoszą się już do algorytmu dekompresji obrazu i zostaną omówione w kolejnej sekcji.
Odczyt przykładowego pliku GIF
Realizację odczytu pliku GIF i dekompresji danych obrazu najlepiej jest przedstawić na prostym przykładzie obrazka o wymiarach 5x5 pikseli (rysunek 8).
Rys. 8. Przykładowy obrazek
Struktura bajtowa takiego pliku jest przedstawiona poniżej (można zobaczyć w dowolnym edytorze HEX):
Pierwsze dane z pliku (sygnatura i wersja, deskryptor ekranu logicznego):
[47 49 46 38 37 61] – nagłówek „GIF87a” w kodzie ASCII,
[05 00 05 00] – rozmiar ekranu logicznego (5x5 pikseli) w formacie little endian,
[A1] = 10100001b
- (<7> = 1) – występuje globalna paleta kolorów,
- (<6:4> = 010) – głębia kolorów wynosi 2^(2+1) = 8 bitów,
- (<3> = 0) – kolory nie są uporządkowane (od częstszych do rzadkich),
- (<2:0> = 001) – rozmiar globalnej palety wynosi 3*2^(1+1) = 12.
[03] – indeks koloru tła – w danym przypadku zielony,
[00] – stosunek stron obrazu – w większości przypadków wynosi 0, tzn. 1:1
Dalej znajduje się globalna paleta kolorów zawierająca 4 triady (zgodnie z powyższym):
[ R G B]
[0A 0A FF] – prawie niebieski
[FF 0A 0A] – prawie czerwony
[0A FF 0A] – prawie zielony
[FF FF FF] – w danym przypadku nie wykorzystywany
[2C] – oznacza początek sekcji
Kolejno idzie deskryptor obrazu:
[00 00 00 00] – współrzędne lewego górnego rogu obrazu na ekranie logicznym – brak przesunięcia,
[05 00 05 00] – wymiary samego obrazu (5x5 pikseli),
[00] = 00000000b
- (<7> = 0) – brak lokalnej palety kolorów,
- (<6> = 0) – metoda przeplatana (interlace) nie występuje,
- (<5> = 0) – lokalna paleta nie jest uporządkowana (jeżeli występuje),
- (<4:3> = 00) – zarezerwowane,
- (<2:0> = 000) – rozmiar lokalnej palety.
[02] – minimalna liczba bitów (+1) w kodzie LZW – w danym przypadku wynosi 3 bity,
[08] – długość sekcji z danymi to 8 bajtów; stąd wynika, że maksymalna długość może wynosić 255 bajtów, jednak nie ma w tym nic dziwnego, gdyż takich sekcji może być więcej niż jedna.
Etap najważniejszy, czyli dekompresja danych zakodowanych algorytmem LZW (8 bajtów zgodnie z powyższym):
[44 14 A6 97 BC 86 40 01]
Po rozpisaniu na liczby binarne:
44: 0100 0100
14: 0001 0100
A6: 1010 0110
97: 1001 0111
BC: 1011 1100
86: 1000 0100
40: 0100 0000
01: 0000 0001
należy pobierać kolejne kody (od prawej do lewej, od góry do dołu), rozszerzać słownik oraz zapamiętywać ciąg indeksów kolorów (lub je rysować). Ogólny algorytm dekompresji w formie opisu słownego wygląda następująco [3]:
- Wypełnij słownik alfabetem źródła informacji.
- pk := pierwszy kod skompresowanych danych
- Wypisz na wyjście ciąg związany z kodem pk, tj. słownik[pk]
- Dopóki są jeszcze jakieś słowa kodu:
- Wczytaj kod k
- pc := słownik[pk] – ciąg skojarzony z poprzednim kodem
- Jeśli słowo k jest w słowniku, dodaj do słownika ciąg (pc + pierwszy symbol ciągu słownik[k]), a na wyjście wypisz cały ciąg słownik[k].
- W przeciwnym razie dodaj do słownika ciąg (pc + pierwszy symbol pc) i tenże ciąg wypisz na wyjście.
- pk := k
Na początku słownik zawiera 6 elementów, indeksy 4 kolorów oraz 2 kody sterujące:
0: {0} – niebieski,
1: {1} – czerwony,
2: {2} – zielony,
3: {3} – nieużywany,
4: {clear} – kod inicjujący,
5: {end} – kod informujący o zakończeniu skompresowanych danych.
Dalej jest pobierany pierwszy kod (3-bitowy), którego wartość wynosi 4, czyli kod inicjujący, który jest potrzebny do ustawienia pewnych wartości początkowych. Następnym kodem jest wartość 0 co oznacza, że pierwszy piksel ma kolor z indeksem 0 (niebieski). Kolejno idzie kod o wartości 1 (czerwony) i do słownika jest dodawany 6 element:
(1) 6: {0,1}
Później występuje kod 2 i dodawany jest 7 element:
(2) 7: {1,2}
Teraz słownik osiągnął maksymalny rozmiar, więc długość kodu LZW jest zwiększana o 1 bit co oznacza, że kolejne pobierane kody będą 4-bitowe (maksymalnie mogą być 12-bitowe). Stąd otrzymujemy:
(1) 8: {2,1}
(6) 9: {1,0}
(10) 10: {0,1,0}
(7) 11: {0,1,0,1}
(9) 12: {1,2,1}
(12) 13: {1,0,1}
(11) 14: {1,2,1,0}
(6) 15: {0,1,0,1,0}
Ponownie osiągnięto maksymalny rozmiar słownika, więc długość kodu jest zwiększana o 1 bit (w wyniku otrzymuje się 5 bitów):
(8) 16: {0,1,2}
(0) 17: {2,1,0}
(5) KONIEC DANYCH
W taki sposób po podstawieniu za numery od 6 w górę odpowiednich ciągów elementarnych (zgodnie ze słownikiem) na wyjściu otrzymuje się pełny ciąg, który odpowiada indeksom w globalnej palecie kolorów i który zostanie narysowany:
0,1,2,1,0,
1,0,1,0,1,
2,1,0,1,2,
1,0,1,0,1,
0,1,2,1,0
Dalej znajduje się już tylko długość kolejnej sekcji [00] oraz terminator [0x3B] (oznaczający koniec pliku formatu GIF).
Listingi fragmentów programu
Wykorzystywane w programie struktury (w pliku imageGIF.h):
//Struktura do przechowywania deskryptora ekranu logicznego typedef struct{ unsigned short SCREEN_WIDTH; unsigned short SCREEN_HEIGHT; unsigned char BYTE_5; unsigned char BACKGROUND; unsigned char ASPECT_RATIO; } SCREEN_DESC; //Struktura do przechowywania deskryptora obrazu typedef struct{ unsigned short LEFT; unsigned short UP; unsigned short IMAGE_WIDTH; unsigned short IMAGE_HEIGHT; unsigned char BYTE_9; } IMAGE_DESC; //Struktura triady (R,G,B) typedef struct{ unsigned char R; unsigned char G; unsigned char B; } COLOR; //Struktura pliku GIF - deskryptory typedef struct{ SCREEN_DESC screenDesc; IMAGE_DESC imageDesc; } GIF_IMAGE; //Struktura elementu slownika typedef struct{ unsigned short code; unsigned short prevCode; unsigned short nextCode; } DICTIONARY_ENTRY;
Na początku należy ustawić wartości początkowe i wyzerować słownik:
//Ustaw poczatkowa dlugosc slownika currentDictSize = PrimaryDictSize; //Wyzeruj licznik bajtow sekcji counter = 0; //Wyzerowanie slownika for(i = 0; i < MAX_DICT_SIZE; i++) Dictionary[i].prevCode = Dictionary[i].nextCode = 0; //Ustawienie wartosci poczatkowych punktu w lewym gornym rogu (1,131) x_pos = 1; y_pos = 130; //Wyzerowanie liczby dostepnych bitow bitsRemaining = 0;
Fragment kodu realizującego dekompresje danych obrazu:
//Wykonuj dopoki pobrany kod jest rozny od kodu END while( (code = getNextCode()) != code_END){ //Jezeli pobrany kod to CLEAR if(code == code_CLEAR){ currentCodeSize = PrimaryCodeSize; //Ustaw poczatkowa dlugosc kodu (w bitach) currentDictSize = PrimaryDictSize; //Ustaw poczatkowa dlugosc slownika oldcode = getNextCode(); //Pobierz kolejny kod //Jezeli pobrany kod nie zawiera sie w slowniku if(oldcode > currentDictSize){ return -3; //Plik jest uszkodzony } GIFDrawPixel(oldcode); //Rysuj pierwszy piksel continue; //Kolejny krok petli } //Jezeli pobrany kod zawiera sie w slowniku if(code < currentDictSize){ code1 = code; code2 = 0; //Jezeli pobrany kod jest kodem pochodnym while(code1 >= PrimaryDictSize){ Dictionary[code1 - PrimaryDictSize].nextCode = code2; code2 = code1; code1 = Dictionary[code1 - PrimaryDictSize].prevCode; if(code1 >= code2) return -3; } GIFDrawPixel(code1); //Rysuj piksel o podanym kodzie podstawowym while(code2!=0){ GIFDrawPixel(Dictionary[code2 - PrimaryDictSize].code); code2 = Dictionary[code2 - PrimaryDictSize].nextCode; } Dictionary[currentDictSize - PrimaryDictSize].code = code1; Dictionary[currentDictSize - PrimaryDictSize].prevCode = oldcode; ++currentDictSize; //Zwieksz dlugosc slownika o 1 //Jezeli maksymalna dlugosc slownika zostala osiagnieta if(currentDictSize == MAX_DICT_SIZE) return -2; //to przerwij dzialanie funkcji //Jezeli maksymalna dlugosc slowa w slowniku osiagnieta if((currentDictSize) == (0x0001<12) currentCodeSize = 12; oldcode = code; //Zapisz ostatni kod } //W przeciwnym wypadku kod nie wystepuje jeszcze w slowniku else{ code1 = oldcode; code2 = 0; //Jezeli pobrany kod jest kodem pochodnym while(code1 >= PrimaryDictSize){ Dictionary[code1 - PrimaryDictSize].nextCode = code2; code2 = code1; code1 = Dictionary[code1 - PrimaryDictSize].prevCode; if(code1 >= code2) return -3; } GIFDrawPixel(code1); //Rysuj piksel o podanym kodzie podstawowym while(code2!=0){ GIFDrawPixel(Dictionary[code2 - PrimaryDictSize].code); code2 = Dictionary[code2 - PrimaryDictSize].nextCode; } GIFDrawPixel(code1); //Rysuj piksel o podanym kodzie podstawowym Dictionary[currentDictSize - PrimaryDictSize].code = code1; Dictionary[currentDictSize - PrimaryDictSize].prevCode = oldcode; ++currentDictSize; //Zwieksz dlugosc slownika o 1 if(currentDictSize == MAX_DICT_SIZE) return -2; //Jezeli maksymalna dlugosc slowa w slowniku osiagnieta if((currentDictSize) == (0x0001< 12) currentCodeSize = 12; oldcode = code; } }
Procedura pobierająca kolejny kod skompresowanych danych:
//************************************************************ //Funkcja zwracajaca kolejny kod pobrany z bloku danych obrazu //************************************************************ unsigned short getNextCode(void){ unsigned int retval=0, temp; if(bitsRemaining >= currentCodeSize){ retval = (read_code & ((0x01 << currentCodeSize) - 1)); read_code >>= currentCodeSize; bitsRemaining -= currentCodeSize; } else{ retval = (read_code & ((0x01 << bitsRemaining) - 1)); f_read(g_fileObject, &read_code, 1, &g_odczytanych_bajtow); ++counter; if(counter == currentDataSectionLength){ counter = 0; f_read(g_fileObject, ¤tDataSectionLength, 1, &g_odczytanych_bajtow); } if((currentCodeSize - bitsRemaining) <= 8){ temp = (read_code & ((0x01 << (currentCodeSize - bitsRemaining)) - 1)); retval += (temp << bitsRemaining); read_code >>= (currentCodeSize - bitsRemaining); bitsRemaining = 8 - (currentCodeSize - bitsRemaining); } else{ retval += (read_code << bitsRemaining); f_read(g_fileObject, &read_code, 1, &g_odczytanych_bajtow); ++counter; if(counter == currentDataSectionLength){ counter = 0; f_read(g_fileObject, ¤tDataSectionLength, 1, &g_odczytanych_bajtow); } retval += ((read_code & ((0x01 << (currentCodeSize - bitsRemaining - 8)) - 1)) << (bitsRemaining + 8)); read_code >>= (currentCodeSize - bitsRemaining - 8); bitsRemaining = 8 - (currentCodeSize - bitsRemaining - 8); } } return retval; }
Funkcja rysująca zadany kod:
//************************************************************ //Funkcja rysujaca kolejny piksel obrazu na podstawie podanego //kodu odpowiadajacego elementowi globalnej palety kolorow //************************************************************ #define przesuniecieH ((unsigned char) imgWidth/2) #define przesuniecieV ((unsigned char) imgHeight/2) void GIFDrawPixel(unsigned char code){ COLOR rgbColor; unsigned short pixelColor; //Jezeli caly obraz zostal narysowany if(abs(y_pos - 130) == imgHeight) return; //Nic nie rob //Jezeli wiersz zostal narysowany else if((x_pos-1) == imgWidth){ x_pos = 1; //Wybierz pierwsza kolumne --y_pos; //Wybierz kolejny wiersz } rgbColor = g_GlobalColorTable[code]; //Pobierz triade z globalnej palety //Konwertuj do jednolitej postaci: BRG (Blue Red Green) pixelColor = ((rgbColor.B)/16 << 8) + ((rgbColor.R)/16 << 4) + ((rgbColor.G)/16); LCDSetPixel(x_pos-przesuniecieH+66, y_pos+przesuniecieV-66, pixelColor);//Rysuj piksel ++x_pos; //Wybierz kolejna kolumne }
Program główny
W funkcji main() po wywołaniu funkcji konfiguracji portów we/wy, sygnałów zegarowych oraz wyświetlacza jest rejestrowany zewnętrzny nośnik danych (karta SD) na którym w katalogu głównym powinny znajdować się przykładowe pliki graficzne typu GIF. Jeżeli karta SD została poprawnie zmontowana w programie to można wykonać operacje pobrania listy plików z katalogu głównego. Występują tutaj dwa ograniczenia: nazwy powinny posiadać format 8.3, natomiast liczba elementów listy plików jest ograniczona do 16. Tworzenie listy plików wygląda następująco:
while(1){ fresult = f_readdir(&dir, &plikInfo); //Pobierz kolejny element katalogu //Przerwij petle w razie zakonczenia plikow w katalogu if ( (fresult != FR_OK) || (!plikInfo.fname[0]) ) break; strcpy(fileName[index++],(plikInfo.fname)); //Zapamietaj kolejna nazwe pliku } i_max = index; //Zapamietaj liczbe plikow i = 0;
Dalej otwierany jest pierwszy plik z listy, wyświetlana jest jego nazwa i zostaje wywołana funkcja drawGIFImage() z przekazaniem obiektu typu FIL oraz struktury GIF_IMAGE, która wykona wszystkie niezbędne operacje (odczyt, odtworzenie struktury, dekompresja danych) i w efekcie zostanie narysowany obrazek. W przypadku jakichkolwiek błędów jak np. „otwarty plik nie posiada formatu GIF” funkcja ta zwróci wartość (-1) na co program główny zareaguje wybraniem kolejnego pliku z listy. Równie ważnym jest fakt niewystarczającej ilości pamięci RAM w mikrokontrolerze STM32F103RB w zastosowaniu do dekompresji, co wpływa na ograniczony słownik (maksymalnie 2800 wpisów spośród wymaganych 4096) dlatego funkcja drawGIFImage() w przypadku większych obrazków wyświetli je częściowo do momentu pełnego wypełnienia dostępnego słownika i zwróci wartość (-2) po czym zostanie wyświetlony komunikat o nie wystarczającej ilości pamięci RAM.
Interfejs programu pozwala użytkownikowi ustawiać kontrast (GÓRA/DÓŁ), wybierać poprzedni/kolejny plik z listy (LEWO/PRAWO) oraz kończyć działanie programu (OK). Pozostała część funkcji main() jest prezentowana poniżej:
while(FLAG != 0){ fresult = f_open(&g_sFileObject, fileName[index], FA_READ);//Otworz plik z podana nazwa printFileName(fileName[index]); //Pobierz informacje z pliku GIF i narysuj obraz lub zwroc wartosc bledu STATUS = drawGIFImage(&g_sFileObject, &gifImage); fresult = f_close(&g_sFileObject); //Zamknij plik switch(STATUS){ //Jezeli nie wykryto formatu GIF otworz kolejny plik case -1:{ ++index; if(index >= i_max) index = 0; continue; } //Jezeli zasoby pamieci RAM sa ograniczone w stosunku do potrzeb case -2:{ printMemMsg(0xFFF); //Wyswietl komunikat braku pamieci RAM break; } default:{ //operacje... break; } } //Wyswietl rozmiary obrazu w pliku GIF printImageSize(&gifImage, 0xFFF); //Ustawienie kontrastu - przyciski UP i DOWN //Wyswietlenie kolejnego pliku GIF - przyciski LEFT i RIGHT //Wyjscie z programu - przycisku OK while(1){ if(GPIO_ReadInputDataBit(JOY_PORT, JOY_UP) == 0){ Delay(0xAFFFF); SetContrast(contrast == 63 ? contrast : ++contrast); } else if(GPIO_ReadInputDataBit(JOY_PORT, JOY_DOWN) == 0){ Delay(0xAFFFF); SetContrast(contrast == -64 ? contrast : --contrast); } //Plik poprzedni if(GPIO_ReadInputDataBit(JOY_PORT, JOY_RIGHT) == 0){ ++index; if(index >= i_max) index = 0; break; } //Plik nastepny else if(GPIO_ReadInputDataBit(JOY_PORT, JOY_LEFT) == 0){ --index; if(index < 0) index = i_max-1; break; } //Wyjscie z programu else if(GPIO_ReadInputDataBit(JOY_PORT_OK, JOY_OK) == 0){ FLAG = 0; break; } } #define przesuniecieH ((unsigned char) (gifImage).imageDesc.IMAGE_WIDTH/2) #define przesuniecieV ((unsigned char) (gifImage).imageDesc.IMAGE_HEIGHT/2) printClrMsg(0xFFF); //Usun poprzednio narysowany obrazek for(j = 0; j < (gifImage).imageDesc.IMAGE_HEIGHT; j++) for(i = 0; i < ((gifImage).imageDesc.IMAGE_WIDTH); i++) LCDSetPixel((i+1)-przesuniecieH+66, (130-j)+przesuniecieV-66, 0x0); printClrMsg(0x000); printImageSize(&gifImage, 0x000); } } fresult = f_mount(0, NULL); LCDClearScreen(0x000); }
Fot. 9. Widoki wyświetlacza z przykładowymi obrazkami
Widok na przykładowo wyświetlone obrazki o wymiarach 64x64 pikseli jest przedstawiony na fotografii 9. Obrazek po lewej został poprawnie w całości odtworzony, natomiast po prawej nie do końca co można zauważyć w postaci brakujących pikseli. Trudno jest określić czy dany obrazek zostanie w całości narysowany, gdyż to zależy nie tylko od jego wymiarów, ale również od stopnia skomplikowania kolejno następujących po sobie pikseli. Można ten problem w pewnym stopniu rozwiązać, ograniczając dodawanie kolejnych wpisów do słownika i przerywając funkcję drawGIFImage() w przypadku odwołania się do niedostępnych elementów jednak nie zawsze to działa.
Jan Szemiet
Bibliografia
[1] Strona internetowa, na której można znaleźć obszerny opis formatu GIF http://www.w3.org/Graphics/GIF/spec-gif89a.txt
[2] Strona internetowa, http://www.fileformat.info/format/gif/egff.htm
[3] Strona internetowa, z opisem algorytmu LZW http://pl.wikipedia.org/wiki/LZW