Język C dla mikrokontrolerów 8051. Formatowanie za pomocą <stdio.h>.

Pisząc program w języku C czasami warto zadać sobie pytanie: czy naprawdę muszę tworzyć funkcję dokonującą konwersji wartości dziesiętnej na szesnastkową? Przecież chociażby biblioteka o nazwie STDIO zawiera w sobie możliwość formatowania zarówno danych wejściowych, jak i wyjściowych. Czy nie ma możliwości skorzystania z czyjejś pracy i zaoszczędzenia własnego czasu?

Biblioteka o nazwie STDIO.H (standard input – output) zawiera szereg funkcji umożliwiających odczyt i wyprowadzanie znaków do (z) standardowego urządzenia wejścia – wyjścia. W „dużym” komputerze role tych urządzeń spełniają klawiatura i monitor. W przypadku mikrokontrolera przyjęto, że funkcje STDIO wykorzystują interfejs szeregowy UART (po konwersji poziomów napięć wyjściowych – RS232) traktując go jako standardowe urządzenie do komunikacji z użytkownikiem.
W przypadku kompilatora RC-51 nastawy UART dokonywane są tuż po uruchomieniu napisanej dla mikrokontrolera aplikacji. Zajmuje się tym funkcja _C_INIT_IO. Ustawia ona TIMER 1 jako generator sterujący transmisją, nadając jego rejestrowi TH1 predefiniowaną wartość początkową. Domyślnie jest to 0xE8, co odpowiada prędkości transmisji 1200 bps przy częstotliwości zegara 11,0592 MHz. Wartość tę można zmienić używając polecenia #pragma (na przykład polecenie #pragma DEFJ(TIM1_INIT=0xFD) przy tej samej częstotliwości generatora zegarowego, ustawi prędkość transmisji na wartość 19200 bps). Ale jeśli byłyby to tylko i wyłącznie instrukcje wysyłania oraz odbioru znaków, nie warto by było poruszać tego tematu. Istnieje bowiem mnóstwo dobrych opracowań na temat bibliotek wykorzystywanych przy programowaniu w języku C. Na listingu 1 zamieszczono nagłówki funkcji predefiniowane przez firmę Raisonance.

printf() = formatowane wyjście

Każdy, kto kiedykolwiek wykorzystywał funkcje predefiniowane w STDIO.H wie, że umożliwiają one formatowanie danych. Zgodnie ze specyfikacją standardu ANSI, szereg z nich dokonuje przekształceń wewnętrznych wartości na znaki lub odwrotnie. W tym odcinku kursu szczególną uwagę poświęcimy funkcji printf() dającej programiście nie tylko szereg możliwości wykorzystania, lecz również pozwalającej na redukcję czasu koniecznego do stworzenia aplikacji.
Wyjściowa funkcja printf() tłumaczy wewnętrzne wartości na znaki. Jednym słowem bajty danych zamieniane są na postać zrozumiałą przez człowieka.

int printf(char *wzorzec, argument_1, argument_2 ... )

Przekształcenie odbywa się według i pod nadzorem wzorca zapisanego we „wzorzec”. Funkcja przekształca, formatuje i wypisuje swoje argumenty do standardowego wyjścia.


List. 1. Funkcje STDIO.H predefiniowane przez RAISONANCE, producenta pakietu RC-51

extern int_getkey(void);
extern intgetchar (void);
extern charungetchar (char c) reentrant;
extern char*gets (char *s) reentrant;
extern intputchar (const int c );
extern int puts (const char *s ) reentrant;
extern int printf(const char *format, ...) reentrant;
extern int sprintf(char *buffer, const char *format, ...) reentrant;
extern int scanf(const char *format, ...) reentrant;
extern int sscanf(const char *buffer, const char *format, ...) reentrant;


Jak wspomniałem wcześniej, w przypadku mikrokontrolera 8051, jest to interfejs UART. Wzorzec zawiera obiekty dwojakiego rodzaju: zwykłe znaki, które są przesyłane do wyjścia oraz specyfikacje przekształceń. Każda z nich wskazuje na sposób, w jaki zostanie przekształcony i wypisany dany argument. Specyfikację przekształcenia rozpoczyna znak % a kończy znak dla niego charakterystyczny. Między znakiem % i znakiem przekształcenia mogą – według następującej kolejności - wystąpić:
- znak „–” (minus) polecający dosunięcie przekształconego argumentu do lewego krańca jego pola,
- liczba określająca rozmiar pola (argument zostanie wypisany w postaci o rozmiarze co najmniej pola, a jeśli będzie taka potrzeba, zostanie uzupełniony znakami odstępu z prawej lub lewej strony w zależności od żądania dosunięcia znaków w lewo),
- znak „.” (kropka) oddzielający rozmiar pola argumentu od jego precyzji,
- liczba określająca precyzję, to jest maksymalną liczbę znaków dla tekstu, liczbę cyfr po kropce dziesiętnej dla liczb zmienno-pozycyjnych, minimalną liczbę cyfr dla wartości całkowitych,
- litera „h”, jeśli argument całkowity należy wyprowadzić w postaci short, lub „l” („el”) jeśli argument należy wyprowadzić jako long.
W tabeli 1 zestawiono podstawowe znaki przekształcenia dla funkcji printf(). Szerokość pola lub precyzję można w specyfikacji zastąpić znakiem „*” (gwiazdki), co oznacza, że żądany argument należy wyprowadzić i przekształcić korzystając z kolejnego argumentu funkcji. Uwaga: musi on być typu int! Na przykład wypisanie co najwyżej max znaków z S wygląda następując: printf(„%.*s”, max, S);

Tab. 1. Podstawowe przekształcenia funkcji printf().

Znak formatujący

Typ przekształcanego argumentu

Opis przekształcenia. Przekształcenie do postaci:

 d lub i
 o
 x lub X
 u
 c
 s
 f
 e lub E


 g lub G

 p
 n

 %

int
int
int
int
int
char*
double
double

double

void*
int*


 

 liczba dziesiętna ze znakiem
 liczba ósemkowa bez znaku i bez wiodącego zera
 liczba szesnastkowa bez znaku i bez wiodącego zera z użyciem małych liter dla wzorca 0x i dużych dla 0X
 liczba dziesiętna bez znaku
 pojedynczy znak po przekształceniu do typu unsigned char
 tekst wypisywany do napotkania znaku końca łańcucha /0 lub osiągnięcia zadanej precyzji
 liczba dziesiętna ze znakiem w postaci [-]xxx.yyy, gdzie liczba cyfr po kropce (yyy) określona jest przez precyzję
 liczba dziesiętna ze znakiem w tzw. notacji inżynierskiej (na przykład 3.45234e-10); podobnie jak wyżej, liczba cyfr po kropce
 określana jest przez precyzję
 jeśli wykładnik potęgi jest mniejszy od –4 lub >= precyzji, to przyjmuje się specyfikację identyczną z wzorcem e (E); inaczej jest stosowana specyfikacja f
wskaźnik – reprezentacja zależy od konkretnej implementacji
 liczbę znaków wypisanych w TYM wywołaniu printf zapisuje się do odpowiedniego argumentu; nie są wykonywane żadne przekształcenia
 nie ma przekształcenia (%%); zostanie wypisany znak %

 

Stosując funkcję printf() należy pamiętać, że wykorzystuje ona swój pierwszy argument do określenia typu, rozmiarów i liczby pozostałych argumentów. Jeśli programista poda zły wzorzec przekształceń, to mimo opisywanej wcześniej filozofii języka C (zaufaj programiście, on wie co robi), funkcja będzie „zdezorientowana” i na wyjściu wyprowadzone zostaną błędne rezultaty jej pracy. Programista powinien mieć świadomość, że efekt wywołań funkcji printf() w postaci printf(s) oraz printf(„%s”,s) może być zupełnie odmienny, aczkolwiek kompilator języka C dopuszcza stosowanie jednej i drugiej postaci. Jeśli jednak nie podamy wzorca wyprowadzanego łańcucha, to może się okazać, że gdy w zmiennej s wystąpią znaki specjalne (% *), łańcuch, który zamierzamy wyprowadzić, zostanie potraktowany jako wzorzec. Na koniec tej krótkiej prezentacji, warto również wspomnieć o funkcji sprintf(), będącej odmianą printf() lecz z tą różnicą, że nie wyprowadza ona danych, tylko zapisuje je w pamięci.

 

List. 2. Przykłady użycia funkcji printf().

const char* TEKST = "Tekst przykładowy";
printf(„:%s:”,TEKST);  →      :Tekst przykładowy:
printf(„:%10s:”,TEKST); 
    :Tekst przykładowy:
printf(„:%.10s:”,TEKST); 
   :Tekst przy:
printf(„:%25.s:”,TEKST); 
   :        Tekst przykładowy:
printf(„:%-25.s:”,TEKST); 
  :Tekst przykładowy        :
printf(„:%025.10s:”,TEKST);
:Tekst przy               :

int X = 123;

printf(„%s %04X %s”, „123 Dec. =”, X, „Hex”); 123 Dec. = 007B Hex
printf(„%s %o %s”, „123 Dec. =”, X, „Oct”);
123 Dec. = 173 Oct

 

Dla praktyków – obsługa wyświetlacza LCD z wykorzystaniem funkcji printf()

Teraz dotarliśmy wreszcie do meritum tego artykułu. Oczywiście – chciałem w krótki sposób zaprezentować funkcje STDIO.H, jednak celem tego artykułu jest nie tyle ich prezentacja ile wytłumaczenie metody, dzięki której można zaprząc je do pracy. Z doświadczenia wiem, że 80% tworzonych przeze mnie aplikacji nie korzysta z interfejsu UART i nic nie stoi na przeszkodzie w wykorzystaniu STDIO.H dla innych potrzeb.
Funkcja printf() jest zaimplementowana od szczegółu do ogółu. Co to oznacza? U podstaw jej działania leży funkcja putchar() wysyłająca pojedynczy znak przez UART. Funkcja printf() nie wie, gdzie i z pomocą jakiego interfejsu wyprowadzane są dane. Zajmuje się tym putchar() i to ją właśnie należy zmienić, aby znaki wysyłane były nie przez UART, ale na przykład na wyświetlacz LCD. Oczywiście, o ile UART i jego obsługa są pewnym standardem w obrębie rodziny mikrokontrolerów 8051, o tyle implementacja obsługi wyświetlacza zależy od konkretnego środowiska, w którym pracuje mikrokontroler.
W przykładzie programu z listingu 2 dokonałem zmiany definicji putchar() w taki sposób, że znaki wysyłane są na wyświetlacz LCD a nie przez UART. Wykorzystałem tu bibliotekę funkcji obsługi LCD z jednego z poprzednich odcinków kursu. Odpowiednie pliki można znaleźć w materiałach dołączanych dodatkowo do artykułu.

 

List. 2. Przykład programu zmieniającego definicję funkcji putchar()

//podmiana funkcji putchar()
//oryginalnie funkcja PUTCHAR wykorzystuje tylko rejestr R7 i akumulator
//jeśli poniższa używa czegoś więcej - może nie funkcjonować
//należy uważnie przyglądać się rejestrom

#include <reg51.h>
#include <stdio.h>
#include <lcd4b.h>

//zmiana definicji putchar(), metoda 1, mniej bezpieczna
intputchar (const int c)
{
    LcdWrite(c);
    return (0);
}

void main(void)
{
    int x = 241;
//inicjalizacja LCD w trybie 4 bity
    LcdInitialize();
    LcdClrScr();
//zamiana liczby x na wartość szesnastkową
    printf("%d %s %02x %s", x, "dec =", x, "hex");
//koniec programu
    while (1);
}

Jak widać na podstawie przykładu programu, redefinicja putchar() nie jest zbyt trudna do wykonania. Nagłówek funkcji musi być zgodny ze zdefiniowanym wcześniej przez producenta pakietu. Można zobaczyć jego pożądany wygląd otwierając właściwy zbiór nagłówkowy o rozszerzeniu „H” (np. STDIO.H). Ciało może być zestawem dowolnych instrukcji.
Tworząc redefinicje, należy zwrócić szczególną uwagę na to, jakie rejestry będą modyfikowane przez nową funkcję. Zgodnie z dokumentacją producenta (a do niej należy każdorazowo odwoływać się tworząc redefinicję) funkcja printf() spodziewa się, że putchar() modyfikuje wyłącznie zawartość rejestrów UART, R7 i ACC mikrokontrolera oraz przydzielonego na zmienne obszaru pamięci. Jeśli nowo napisana funkcja zmienia zawartość również innych rejestrów, musi być zastosowana inna metoda redefinicji, zaprezentowana w przykładzie z listingu 3. Przed użyciem putchar_c() wewnątrz putchar(), wszystkie żywotne rejestry mikrokontrolera są zapamiętywane na stosie i odtwarzane po powrocie z wywołania funkcji. Listing prezentuje również fragment kodu w języku asembler 8051 wykonywany podczas wywołania putchar(). Słowo kluczowe reentrant języka RC-51 informuje kompilator o tym, że funkcja może być wywoływana przez wiele procesów jednocześnie.

 

List. 3. Bezpieczna redefinicja putchar() oraz odpowiadający jej listing programu po kompilacji. Można zauważyć, że wszystkie ważne rejestry zapamiętywane są na stosie przed wywołaniem putchar (PUSH) i odtwarzane po powrocie (POP).

#include <reg51.h>
#include <stdio.h>
#include <lcd4b.h>

// zamiana funkcji putchar(), metoda 2, bezpieczna
voidputchar_c (const int c) reentrant
{
    LcdWrite(c);
}

intputchar (const int c)
{
    putchar_c(c);
    return(0);
}

voidmain (void)
{
    int x = 134;
//inicjalizacja LCD w trybie 4 bity
    LcdInitialize();
    LcdClrScr();
//zamiana liczby x na wartość szesnastkową
    printf("%d %s %02x %s",x,"dec.to",x,"hex");
//koniec programu
    while (1);
}

            ; FUNCTION _putchar (BEGIN)
                                           ; SOURCE LINE # 15
0000 C0F0           PUSH   B
0002 C083           PUSH   DPH
0004 C082           PUSH   DPL
0006 C0D0           PUSH   PSW
0008 C000           PUSH   AR0
000A C001           PUSH   AR1
000C C002           PUSH   AR2
000E C003           PUSH   AR3
0010 C004           PUSH   AR4
0012 C005           PUSH   AR5
0014 C006           PUSH   AR6
              ; Register R4R5 is assigned to parameter c
0016 120000  R      LCALL  ?putchar_c
0019 D006           POP    AR6
001B D005           POP    AR5
001D D004           POP    AR4
001F D003           POP    AR3
0021 D002           POP    AR2
0023 D001           POP    AR1
0025 D000           POP    AR0
0027 D0D0           POP    PSW
0029 D082           POP    DPL
002B D083           POP    DPH
002D D0F0           POP    B
002F 22             RET   

Inny przykład redefinicji putchar (tu wykorzystano również metodę mniej bezpieczną) pokazano na listingu 4. Oryginalnie (i zgodnie ze specyfikacją standardu ANSI) putchar wysyła po każdym argumencie o wartości 0x0A znak o kodzie 0x0D. Tworzą one w sumie sekwencję składającą się na znak nowej linii (powrót karetki – CR=0x0D oraz znak nowej linii – LF=0x0A). W niektórych aplikacjach jest to jednak cecha niepożądana a wręcz przeszkadzająca. Nowa definicja funkcji putchar nie posiada już tej właściwości.

 

List. 4. Przykład własnej definicji putchar()

//nowa definicja funkcji putchar wysyłająca dane przez UART
int putchar (const int c)
{
    SBUF = c;

    TI = 0;
    while (!TI);
}

Przedstawione tu przykłady tworzenia własnych funkcji zamieniających oryginalne definicje, to wierzchołek góry. Istnieje bowiem cały szereg różnych możliwości – począwszy od bibliotek obsługi standardowego wejścia – wyjścia aż po bibliotekę MATH (operacje matematyczne na liczbach zmienno-pozycyjnych). Wszystko zależy od inwencji programisty i od faktycznych potrzeb aplikacji. Wykorzystując biblioteki należy jednak pamiętać o tym, że oferują one szereg różnych możliwości kosztem zajętej pamięci programu mikrokontrolera.

 

Jacek Bogusz
j.bogusz@easy-soft.net.pl

http://www.tomaszbogusz.blox.pl/

ZałącznikWielkość
Przykład programu z artykułu (stdio-lcd.zip)15.99 KB

Dodaj nowy komentarz

Zawartość pola nie będzie udostępniana publicznie.