ISIX-RTOS – przykład 4 – wątkowa obsługa portu szeregowego RS232
Opis systemu ISIX-RTOS i jego funkcji opublikowaliśmy w artykule „Mini system operacyjny dla STM32 – wprowadzenie”, który można przeczytać tu. |
W przykładzie czwartym pokażemy, w jaki sposób napisać uniwersalną klasę/sterownik obsługi portu szeregowego. Obsługa portu szeregowego przyda się z pewnością w większości aplikacji, chociażby do tworzenia komunikatów diagnostycznych na etapie uruchamiania projektu. Sterownik dla portu szeregowego napiszemy z wykorzystaniem systemu przerwań. W przypadku urządzeń znakowych najbardziej odpowiednim będzie użycie kolejek FIFO, jednej nadawczej oraz drugiej odbiorczej. Ponieważ w kontekście przerwań nie możemy wywoływać funkcji blokujących, należy użyć specjalnych metod z przyrostkiem _isr (push_isr(), pop_isr() klasy fifo.
Aby pokazać możliwości pracy wielowątkowej w przykładzie stworzymy dwa wątki:
- odbiorczy, służący do odbioru danych z portu szeregowego, który w zależności od odebranego znaku będzie sterował pracą diod LED:D1 i D2 zamontowanych na płytce STM32Butterfly,
- nadawczy, którego działanie sprowadzać się będzie do odczytu stanu joysticka oraz transmisję poprze UART informacji tekstowej o jego aktualnej pozycji.
Rys. 1. Schemat elektryczny konwertera napięciowego RS232 i sposób jego dołączenia do płytki STM32Butterfly
Po podłączeniu zestawu STM32Butterfly do interfejsu RS232 komputera w celu przetestowania działania aplikacji należy uruchomić ulubiony program terminalowy (np. Minicom, Hyperterminal itp.) oraz skonfigurować wybrany port szeregowy z następującymi parametrami transmisji: prędkość – 115200 b/s, liczba bitów danych: 8, 1 bit stopu, brak kontroli parzystości i kontroli przepływu. Po zaprogramowaniu mikrokontrolera w oknie terminala powinien pojawić się komunikat informujący o uruchomieniu programu. Po wciśnięciu na klawiaturze PC klawisza A mamy możliwość włączenia diody LED D1 i jej wyłączenia za pomocą klawisza B. W podobny sposób możemy sterować pracą diody LED D2 – służą do tego celu klawisze C i D. Przechylenie osi joysticka powoduje wyświetlona informacji tekstowej o jej położeniu. Sposób działania aplikacji z podziałem na wątki przedstawiono na rys. 2.
Rys. 2. Sposób działania przykładowej aplikacji z podziałem na wątki
Aplikacja składa się z dwóch wątków, które używają jednego portu szeregowego. Jeden wątek jest odpowiedzialny za odczyt danych z portu szeregowego oraz włączanie i wyłączanie diod LED D1 i D2. Drugi wątek jest odpowiedzialny za cykliczny odczyt stanu joysticka oraz – w przypadku wykrycia odchylenia od położenia standardowego – wysłania informacji o kierunku wychylenia jego osi. Hierarchię klas aplikacji przedstawiono na rys. 3.
Rys. 3. Hierarchia klas projektu
Podobnie jak we wszystkich prezentowanych przykładach klasa the_serialapp jest klasą aplikacji przechowującą wszystkie obiekty. Statyczny obiekt tej klasy jest tworzony w funkcji głównej main() (list. 1).
List. 1. Funkcja główna main
//App main entry point int main() { //The application object static app::the_serialapp app; //Start the isix scheduler isix::isix_start_scheduler(); }
Deklaracje klasy obiektu aplikacji przedstawiono na list. 2.
List. 2. Deklaracja klasy serialapp
//The application class class the_serialapp { public: //App Constructor the_serialapp(): usart(USART2),ledrcv(usart),keytran(usart) {} private: //Serial device dev::usart_buffered usart; //The blinker class led_receiver ledrcv; //The key transmitter class key_transmitter keytran; };
Klasa the_serialapp zawiera obiekt usart klasy led_receiver, która stanowi obiekt portu szeregowego RS232. Obiekt ledrcv klasy led_receiver odpowiedzialny jest za odbiór znaków z portu szeregowego oraz sterowanie pracą LED w zależności od odebranego znaku. Obiekt keytran klasy key_transmitter odpowiedzialny jest za odczyt stanu styków joysticka oraz wysyłanie informacji do portu. Oba obiekty przyjmują referencję do wspólnego obiektu klasy usart_buffered oraz dziedziczą z klasy isix::task_base, więc stanowią odrębne wątki. Transmisja z wykorzystaniem portu szeregowego RS232 jest dupleksowa. Ponieważ jeden wątek tylko odczytuje dane z portu, natomiast drugi tylko zapisuje dane do tego portu, pracują one zupełnie niezależnie i nie wymagają wzajemnej synchronizacji za pomocą semafora, jak miało to miejsce w przypadku obsługi magistrali I2C, która jest simpleksowa. Klasa usart_buffered jest uniwersalną klasą sterownika portu szeregowego RS232 wykorzystującą sprzętowy port USART mikrokontrolera rodziny STM32. Klasa została napisana w taki sposób, aby była możliwość użycia dowolnego portu szeregowego dostępnego w mikrokontrolerze. Deklaracja klasy znajduje się w pliku i2c_host.cpp (list. 3).
List. 3. Deklaracja klasy sterownika portu szeregowego
class usart_buffered { friend void usart1_isr_vector(void); friend void usart2_isr_vector(void); public: enum parity //Baud enumeration { parity_none, parity_odd, parity_even }; //Constructor explicit usart_buffered( USART_TypeDef *_usart, unsigned cbaudrate = 115200, std::size_t queue_size=192, parity cpar=parity_none ); //Set baudrate void set_baudrate(unsigned new_baudrate); //Set parity void set_parity(parity new_parity); //Putchar int putchar(unsigned char c, int timeout=isix::ISIX_TIME_INFINITE) { int result = tx_queue.push( c, timeout ); start_tx(); return result; } //Getchar int getchar(unsigned char &c, int timeout=isix::ISIX_TIME_INFINITE) { return rx_queue.pop(c, timeout ); } //Put string into the uart int puts(const char *str); //Get string into the uart int gets(char *str, std::size_t max_len, int timeout=isix::ISIX_TIME_INFINITE); private: static const unsigned IRQ_PRIO = 1; static const unsigned IRQ_SUB = 7; private: void start_tx(); void isr(); void irq_mask() { ::irq_mask(IRQ_PRIO, IRQ_SUB); } void irq_umask() { ::irq_umask(); } void periphcfg_usart1(bool is_alternate); void periphcfg_usart2(bool is_alternate); private: USART_TypeDef *usart; isix::fifo tx_queue; isix::fifo rx_queue; volatile bool tx_en; private: //Noncopyable usart_buffered(usart_buffered &); usart_buffered& operator=(const usart_buffered&); };
Klasa została zaprzyjaźniona z handlerami przerwań portów szeregowych, które zostały wcześniej zadeklarowane z linkowaniem typu C, co powoduje wyłączenie manglowania nazw. Funkcje obsługi przerwań są wywoływane przez kontroler sprzętowy w momencie wystąpienia przerwania bez dodatkowych parametrów, co wymusza istnienie dostępu do instancji klasy obsługującej port szeregowy, poprzez wskaźnik lub referencję globalną. Wskaźniki dostępu do poszczególnych instancji klas przypisanych do portów szeregowych zostały umieszczone w nienazwanej przestrzeni nazw w pliku implementacji (usart_buffered.cpp), przez co dostęp do wskaźników jest możliwy tylko w obrębie danego modułu (list. 4).
List. 4. Fragment implementacji klasy portu szeregowego
namespace //Object pointers for interrupt { usart_buffered *usart1_obj; usart_buffered *usart2_obj; }
Zadeklarowanie przyjaźni funkcji z klasą umożliwia wywołanie dowolnych metod z funkcji zaprzyjaźnionej, co zostało wykorzystane do wywołania metody isr() stanowiącej wektor obsługi przerwania. Klasa obsługi portu szeregowego zawiera dwa obiekty tx_queue, rx_queue (list. 5) klasy isix::fifo, które są wykorzystywane jako bufor nadajnika oraz bufor odbiornika. Konstruktor klasy przyjmuje cztery parametry, adres wybranego kontrolera portu szeregowego (np. USART1, USART2), prędkość transmisji z ustawionym argumentem domyślnym na 115200, wielkość kolejek FIFO ustawionych domyślnie na 192 bajty oraz tryb kontroli parzystości z domyślnym argumentem ustawionym na parity_none.
List. 5. Implementacja konstruktora klasy portu szeregowego
/*----------------------------------------------------------*/ //! Constructor called for usart buffered usart_buffered::usart_buffered(USART_TypeDef *_usart, unsigned cbaudrate, std::size_t queue_size ,parity cpar ) : usart(_usart), tx_queue(queue_size), rx_queue(queue_size) , tx_en( false ) { if(_usart == USART1) { periphcfg_usart1(false); } else if(_usart == USART2) { periphcfg_usart2(true); } //Enable UART usart->CR1 = CR1_UE_SET; //Setup default baudrate set_baudrate( cbaudrate ); set_parity( cpar ); //One stop bit usart->CR2 = USART_StopBits_1; //Enable receiver and transmitter and anable related interrupts usart->CR1 |= USART_Mode_Rx |USART_RXNEIE | USART_Mode_Tx ; if( _usart == USART1 ) { usart1_obj = this; //Enable usart IRQ with lower priority nvic_set_priority( USART1_IRQn,IRQ_PRIO, IRQ_SUB ); nvic_irq_enable( USART1_IRQn, true ); } else if( _usart == USART2 ) { usart2_obj = this; //Enable usart IRQ with lower priority nvic_set_priority( USART2_IRQn,IRQ_PRIO, IRQ_SUB ); nvic_irq_enable( USART2_IRQn, true ); } }
Konstruktor odpowiedzialny jest za inicjalizację wybranego układu USART zgodnie z zadanymi parametrami. Na liście inicjalizacyjnej konstruktora są tworzone obiekty kolejek FIFO o zadanej wielkości. Następnie w zależności od numeru portu szeregowego inicjalizowane są linie GPIO tak, aby pełniły one funkcję obsługi układu peryferyjnego, oraz włączany jest wybrany USART, co realizowane jest przez metody periphcfg_usart1(), oraz periphcfg_usart2(). Następnie włączany jest port szeregowy oraz jest konfigurowany podzielnik układu tak, aby pracował z zadaną prędkością poprzez wywołanie metodyset_baudrate(). W następnej kolejności wywoływana jest metoda set_parity(), której zadaniem jest odpowiednie skonfigurowanie bitu parzystości. W zależności od wykorzystanego układu USART, do wskaźników obiektów przypisanych do przerwania przypisywane są adresy obiektu, oraz w kontrolerze NVIC włączane są przerwania. Sterownik posiada dwie podstawowe metody interfejsu umożliwiające wysłanie oraz odbieranie znaków, które mogą być wykorzystane przez inne klasy.
List. 6. Definicja metody wysłania znaku do portu szeregowego
int usart_buffered::putchar(unsigned char c, int timeout=isix::ISIX_TIME_INFINITE) { int result = tx_queue.push( c, timeout ); start_tx(); return result; }
Metoda putchar()(list. 6) jest odpowiedzialna za wysyłanie znaku do portu szeregowego, przyjmuje ona dwa parametry znak do wysłania, oraz maksymalny dopuszczalny czas oczekiwania, na miejsce w kolejce FIFO. Działanie tej metody jest bardzo proste i sprowadza się do próby zapisania danych do kolejki, a następnie wywołanie metody start_tx(), które zadaniem jest rozpoczęcie nadawania znaków, pozostała część jest realizowana przez procedurę obsługi przerwania.
List. 7. Definicja metody odebrania znaku z portu szeregowego
//Getchar int getchar(unsigned char &c, int timeout=isix::ISIX_TIME_INFINITE) { return rx_queue.pop(c, timeout ); }
Metoda getchar()(list. 7) umożliwia odbieranie znaków z portu szeregowego, przyjmuje ona dwa argumenty: referencję do znaku oraz maksymalny czas oczekiwania na ten znak. Działanie tej metody sprowadza się jedynie do wywołania metodypop() kolejki odbiorczej. Jeżeli w buforze jest jakiś znak umieszczony przez procedurę obsługi przerwania wówczas następuje jego odczytanie. Jeżeli w buforze nie ma ani jednego znaku następuje zablokowanie aktualnego wątku do momentu odebrania znaku.
Cała praca realizowana jest głównie przez procedury obsługi przerwania, które są wywoływane w momencie, gdy na wybranym porcie szeregowym jest miejsce w buforze nadawczym, lub został odebrany jakiś znak. Zgłoszenie przerwania od danego portu szeregowego powoduje rozpoczęcie wykonania funkcji usart1_isr_vector() lub usart2_isr_vector() – list. 8.
List. 8. Implementacja funkcji obsługi przerwań
//Usart 1 void usart1_isr_vector(void) __attribute__ ((interrupt)); void usart1_isr_vector(void) { if(usart1_obj) usart1_obj->isr(); } //Usart 2 void usart2_isr_vector(void) __attribute__ ((interrupt)); void usart2_isr_vector(void) { if(usart2_obj) usart2_obj->isr(); }
W przypadku, gdy do wskaźnika przypisanego do danego portu szeregowego został przypisany jakiś obiekt, wówczas wywoływana jest metoda isr() – list. 9, odpowiedzialna za realizację procedury obsługi przerwania.
List. 9. Implementacja metody obsługi przerwań klasy portu szeregowego
void usart_buffered::isr() { uint16_t usart_sr = usart->SR; if( usart_sr & USART_RXNE ) { //Received data interrupt unsigned char ch = usart->DR; //fifo_put(&hwnd->rx_fifo,ch); rx_queue.push_isr(ch); } if(tx_en && (usart_sr&USART_TXE) ) { unsigned char ch; if( tx_queue.pop_isr(ch) == isix::ISIX_EOK ) { usart->DR = ch; } else { usart->CR1 &= ~USART_TXEIE; tx_en = false; } } }
Działanie procedury obsługi przerwania jest bardzo proste: sprowadza się do odczytania statusu, kontrolera USART oraz podjęciu odpowiedniej akcji. W przypadku, gdy przerwanie zostało wygenerowane w wyniku odebrania znaku, wówczas jest on odczytywany z rejestru danych, a następnie przekazywany do kolejki. Do wysłania znaku do kolejki FIFO używana jest nieblokująca metoda push_isr(), dedykowana procedurom obsługi przerwań. W przypadku, gdy zostało wygenerowane przerwanie, w wyniku braku danych w buforze nadawczym, wówczas znak odczytywany jest z kolejki nadawczej za pomocą nieblokującej metody pop_isr(), a następnie odczytany znak zapisywany jest do rejestru danych układu USART. W przypadku, gdy nie ma danych w kolejce, zerowana jest flaga zgłoszenia przerwania
Klasaled_receiver (list. 10) odpowiedzialna jest za odbieranie danych z portu szeregowego oraz sterowanie diodami LED w zależności od kodu odebranego znaku. Klasa dziedziczy z klasy bazowej isix::task_base.
List. 10. Deklaracja klasy led_receiver
//Serial receiver task class class led_receiver: public isix::task_base { public: //Constructor led_receiver(dev::usart_buffered &_serial); protected: //Main thread method virtual void main(); private: //Stack configuration static const unsigned STACK_SIZE = 256; static const unsigned TASK_PRIO = 3; //The usart obj ref dev::usart_buffered &serial; };
Klasa zawiera referencję do obiektu sterownika portu szeregowego usart_buffered. Realizacja zadania odbywa się w metodzie wirtualnej main(), stanowiącą odrębny wątek systemowy – list. 11.
List. 11. Implementacja metody main klasy led_receiver
//Main task/thread function void led_receiver::main() { while(true) { unsigned char c; //Receive data from serial if(serial.getchar(c)==isix::ISIX_EOK) { //Check for received char switch(c) { //On led 1 case 'a': case 'A': io_clr( LED_PORT, LED1_PIN ); break; //Off led 1 case 'b': case 'B': io_set( LED_PORT, LED1_PIN ); break; //On led 2 case 'c': case 'C': io_clr( LED_PORT, LED2_PIN ); break; //Off led 2 case 'd': case 'D': io_set( LED_PORT, LED2_PIN ); break; } } } }
Działanie metody jest proste, sprowadza się do wywołania metody getchar() obiektu sterownika portu szeregowego, która blokuje się do momentu odebrania znaku. W przypadku odebrania prawidłowego kodu znaku, odpowiednie diody LED są włączane lub wyłączane.
Klasa key_transmiter(list. 12) odpowiedzialna jest za odczyt stanu joysticka oraz – w przypadku wykrycia zwarcia jego styków – wysłaniu informacji do portu szeregowego.
List. 12. Deklaracja klasy led_transmitter
class key_transmitter: public isix::task_base { public: //Constructor key_transmitter(dev::usart_buffered &_serial); protected: //Main thread method virtual void main(); private: //Stack and prio cfgs static const unsigned STACK_SIZE = 256; static const unsigned TASK_PRIO = 3; //The usart obj ref dev::usart_buffered &serial; };
Podobnie jak poprzednio klasa przechowuje referencje do obiektu portu szeregowego, a realizacja wątku dokonywana jest przez metodę wirtualną main() – list. 13.
List. 13. Implementacja metody main klasy key_transmitter
//Main task - read joy key and sent it to RS232 void key_transmitter::main() { //Previous key variable static short p_key = -1; serial.puts("Key A - enable LED1\r\n"); serial.puts("Key B - disable LED1\r\n"); serial.puts("Key C - enable LED2\r\n"); serial.puts("Key D - disable LED2\r\n"); serial.puts("Press joy on the stm32 butterfly\r\n"); for(;;) { //Get key short key = get_key(); //Check if any key is pressed if(key!=0 && p_key==0) { switch(key) { case KEY_OK: serial.puts("Key OK pressed\r\n"); break; case KEY_LEFT: serial.puts("Key LEFT pressed\r\n"); break; case KEY_RIGHT: serial.puts("Key RIGHT pressed\r\n"); break; case KEY_UP: serial.puts("Key UP pressed\r\n"); break; case KEY_DOWN: serial.puts("Key DOWN pressed\r\n"); break; } } //Previous key assignement p_key = key; //Wait short time isix::isix_wait( isix::isix_ms2tick(DELAY_TIME) ); } }
Na początku za pomocą metody puts() sterownika portu szeregowego są wysyłane teksty powitalne do portu szeregowego, a następnie program wchodzi do pętli głównej. Pętla główna wykonywana jest cyklicznie z czasem DELAY_TIME (25 ms), co umożliwia sprawdzenie stany joysticka eliminując drgania zestyków. W przypadku wykrycia zmiany stanu portów sprawdzany jest numer klawisza, a następnie za pomocą metody puts sterownika portu szeregowego, wypisywane są komunikaty, informujące o pozycji joysticka.