Mikrokontrolery STM32 z wbudowanym interfejsem USB device doskonale nadają się do implementacji w nich konwerterów USB/RS232 (zarówno dla Windows jak i Linuksa) – przykład takiego rozwiązania przedstawiamy w artykule.
Uwaga! Projekty przygotowano w wersji dla Linuksa oraz Windows. Wersje źródłowe są dostępne na końcu artykułu.
Prezentowane rozwiązanie udostępnia 2 wirtualne porty szeregowe, które mogą być używane niezależnie w tym samym czasie. W projekcie nie są obsługiwane linie RTS, DTR itp., nie mniej jednak można je łatwo dodać (poza linią CTS, której nie obsługuje klasa USB CDC). Schemat elektryczny proponowanego rozwiązania pokazano na rys. 1, a schemat aplikacji blokowy na rys. 2.
Rys. 1. Schemat elektryczny konwertera
Rys. 2. Budowa aplikacji
W projekcie można używać jednego lub 2 wirtualnych portów szeregowych, w zależności od definicji zawartych w nagłówku converter.h. Najważniejszy element tego pliku, służący do konfiguracji, pokazano na list. 1.
List. 1. Parametry konfigurujące projekt
1 2 3 4 5 6 7 8 9 |
#define USE_VCOM1 1 #define USE_VCOM2 1 #if(USE_VCOM1) #define VCOM1_USART_NUM 1 #endif #if(USE_VCOM2) #define VCOM2_USART_NUM 2 #endif |
Jeżeli definicja USE_VCOMx jest ustawiona na 1, wówczas wybrany wirtualny COM będzie widoczny w komputerze. Definicja VCOMx_USART_NUM mówi zaś, z którym fizycznym portem szeregowym procesora dany wirtualny port ma się komunikować. Aby można było jednocześnie transmitować dane do dwóch USART-ów sprzężono je z USB za pomocą buforów kołowych a cała transmisja odbywa się na przerwaniach, co pokazano na rys. 3.
Rys. 3. Model transmisji danych przez konwerter
Od strony USB i biblioteki vCOM są widoczne dwa wirtualne urządzenia vcom1 i vcom2. Gdy odbieramy dane przez wybrany vcom z komputera, wówczas przepisujemy je do nadawczego bufora kołowego (przypisanego po danego portu USART) i włączamy przerwanie od nadawania danych przez USART. W przerwaniu tym sprawdzamy, czy w buforze nadawczym są dane – jeśli tak, wówczas je odczytujemy i wysyłamy – jeśli nie, wówczas blokujemy przerwanie.
Podobnie jest w przypadku odbioru danych przez USART – jeśli zostanie wygenerowane przerwanie odbiorcze, wówczas wpisujemy odebrane dane do bufora odbiorczego. W momencie gdy przez dany vcom można przesłać kolejne dane do hosta jest wywoływana odpowiednia funkcja podpięta do danego obiektu vcom. Następuje w niej sprawdzenie, czy mamy dane w buforze odbiorczym portu USART, z którym dany vcom współpracuje. Jeśli są jakieś dane, wówczas odczytujemy tyle, ile da się przesłać w jednej paczce do hosta i wysyłamy je.
Główny element konwertera to plik converter.c. W jego nagłówku znajdują się wszystkie potrzebne ustawienia – mapowanie elementów vcom na poszczególne porty USART, numery portów i pinów interfejsów USART itp.
Zainicjowanie biblioteki USB jest wykonywane przy starcie systemu. Nie zagłębiając się w nią zbyt mocno powiem, że używamy tu gotowego automatu do tworzenia konfiguracji dzięki czemu stworzenie deskryptora konfiguracji i potrzebnych struktur używanych przez bibliotekę USB jest bardzo proste. Zainicjowanie USB odbywa się za pomocą funkcji pokazanej na list. 2.
List. 2. Funkcja inicjująca interfejs USB w mikrokontrolerach STM32
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
void usbd_init(void) { USBD_device_descriptor *usbd_dev_desc; CCM_session_params ccm_session; // ----------------------------- // Inicjalizacja konfiguracji // ----------------------------- // otwarcie konfiguracji do automatycznego tworzenia // za pomocą biblioteki CCM CCM_open_config(USBD_STM32, &usbdc, &ccm_session); // dodanie wirtualnego portu szeregowego (vCOM) // (lub 2 vCOM-ów) do tworzonej przez nas konfiguracji #if(USE_VCOM1) CDC_VCOM_add_item(&(vcom1_params.vcom), &ccm_session); #endif #if(USE_VCOM2) CDC_VCOM_add_item(&(vcom2_params.vcom), &ccm_session); #endif // zamkniecie sesji CCM i zainstalowanie konfiguracji // - po wyjsciu z tej funkcji mamy gotowy deskryptor konfiguracji // w tablicy usbd_conv_descriptor oraz wypelniona i zainstalowana // strukture usbdc. CCM_close_and_install_config(usbd_conv_descriptor, &ccm_session); //---------------------------------------------------- //Zmiana pol deskryptora urzadzenia (Devce Descriptor) //---------------------------------------------------- usbd_dev_desc = USBD_get_dev_descriptor_pointer( & usbdc); // tu wypelniamy sobie parametry vendorID i productID // w tym projekcie ustawione przykladowe wartości tak jak w demo ST usbd_dev_desc->idVendor = 0x0483; usbd_dev_desc->idProduct = 0x5740; // jakiej wersji USB uzywamy usbd_dev_desc->bcdUSB = 0x0110; // klasa urzadzenia od razu informuje, // ze jest to urzadzenie komunikacyjne usbd_dev_desc->bDeviceClass = 0x02; // ------------------------------ // Elementy opcjonalne - stringi // ------------------------------ // zainstalowanie stringow USBDS_install(( uint8_t*) Manufacturer_String_Descriptor, &( usbd_dev_desc-> iManufacturer), & usbdc); USBDS_install(( uint8_t*) Product_String_Descriptor, &( usbd_dev_desc-> iProduct), &usbdc); // przygotowujemy SerialNumber_String_Descriptor SerialNumber_fill(); USBDS_install(( uint8_t*) SerialNumber_String_Descriptor, &( usbd_dev_desc-> iSerialNumber), &usbdc); // na koniec – wlaczenie peryferium USB USBD_activate(&usbdc); } |
Następuje w niej utworzenie i zainstalowanie konfiguracji (wywołanie pierwszych 4 funkcji) a następnie dodatkowo zmiana niektórych pól deskryptora urządzenia i zainstalowanie stringów – ciągów znakowych unicode opisujących urządzenie.
Tak stworzona konfiguracja dla dwóch obiektów vcom ma 4 interfejsy: 2 interfejsy CCI (Communication Class Interface) pełniące funkcję notyfikacyjną i 2 interfejsy CDI (Communication Data Interface) służące do transmisji danych. Każdy interfejs typu CCI posiada jeden punkt końcowy (endpoint) typu interrupt (przerwaniowy) natomiast interfejs CDI posiada 2 punkty końcowe typu bulk (masowe) – jeden typu IN i jeden OUT służące do przesyłu danych Tx i Rx danego obiektu vcom. Z tego wynika, że na jeden wirtualny port szeregowy przypadają 3 punkty końcowe. Mikrokontrolery STM32 z wbudowanymi interfejsami USB posiadają ich 8, więc łatwo obliczyć, że można na tym procesorze zaimplementować max. 2 wirtualne COM-y. Zajmą one 7 punktów końcowych – 3 na 2 porty + zerowy punkt zawsze używany do transmisji kontrolnej.
Inicjalizacja portu USART następuje zaś – zgodnie z ideą klasy CDC – dopiero w czasie działania konwertera, gdy nadejdzie od hosta żądanie otwarcia portu. W tym momencie wywoływana jest przez bibliotekę CDC odpowiednia funkcja podpięta do obiektu vcom. Następuje w niej sparsowanie przesłanych przez hosta parametrów pracy portu USART. W tej aplikacji zaimplementowałem ustawianie takich parametrów jak: prędkość transmisji, liczba bitów stopu i typ parzystości. Następnie wywoływana jest napisana do tego celu prosta funkcja inicjująca port USART, przedstawiona na list. 3.
List. 3. Funkcja inicjująca port USART
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
static void configure_usart( uint8_t usart_num, uint32_t baudrate, uint16_t stop_bits, uint16_t patity, uint8_t enable_irq) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; USART_TypeDef* USARTx; GPIO_TypeDef* GPIO_Rx_Port; GPIO_TypeDef* GPIO_Tx_Port; uint16_t GPIO_Rx_Pin; uint16_t GPIO_Tx_Pin; uint8_t NVIC_IRQChannel; // identify USART number, reset the peripheral and enable its clock { if(usart_num == 1) { USARTx = USART1; GPIO_Rx_Port = USART1_RX_PORT; GPIO_Tx_Port = USART1_TX_PORT; GPIO_Rx_Pin = USART1_RX_PIN; GPIO_Tx_Pin = USART1_TX_PIN; NVIC_IRQChannel = USART1_IRQn; // reset peripheral and enable its clock RCC_APB2PeriphResetCmd( RCC_APB2Periph_USART1, ENABLE); RCC_APB2PeriphResetCmd( RCC_APB2Periph_USART1, DISABLE); RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1, ENABLE); } else if(usart_num == 2) { USARTx = USART2; GPIO_Rx_Port = USART2_RX_PORT; GPIO_Tx_Port = USART2_TX_PORT; GPIO_Rx_Pin = USART2_RX_PIN; GPIO_Tx_Pin = USART2_TX_PIN; NVIC_IRQChannel = USART2_IRQn; // reset peripheral and enable its clock RCC_APB1PeriphResetCmd( RCC_APB1Periph_USART2, ENABLE); RCC_APB1PeriphResetCmd( RCC_APB1Periph_USART2, DISABLE); RCC_APB1PeriphClockCmd( RCC_APB1Periph_USART2, ENABLE); } else if(usart_num == 3) { USARTx = USART3; GPIO_Rx_Port = USART3_RX_PORT; GPIO_Tx_Port = USART3_TX_PORT; GPIO_Rx_Pin = USART3_RX_PIN; GPIO_Tx_Pin = USART3_TX_PIN; NVIC_IRQChannel = USART3_IRQn; // reset peripheral and enable its clock RCC_APB1PeriphResetCmd( RCC_APB1Periph_USART3, ENABLE); RCC_APB1PeriphResetCmd( RCC_APB1Periph_USART3, DISABLE); RCC_APB1PeriphClockCmd( RCC_APB1Periph_USART3, ENABLE); } else { return; } } // configure GPIO { GPIO_InitStructure.GPIO_Pin = GPIO_Tx_Pin; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init( GPIO_Tx_Port, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Rx_Pin; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init( GPIO_Rx_Port, &GPIO_InitStructure); } // configure USART peripheral // and enable it { USART_InitStructure.USART_BaudRate = baudrate; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = stop_bits; USART_InitStructure.USART_Parity = patity ; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // Initialize registers USART_Init(USARTx, &USART_InitStructure); // Enable the USART USART_Cmd(USARTx, ENABLE); } // Enable the USART Interrupt if(enable_irq) { NVIC_InitStructure.NVIC_IRQChannel = NVIC_IRQChannel; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // Configure the transmit and receive ISR USART_ITConfig(USARTx, USART_IT_TXE, ENABLE); USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE); } } |
Funkcję tę podzielono na sekcje ilustrujące co należy zrobić, aby uruchomić port USART w procesorze STM32.
Została ona stworzona, gdyż mimo że standardowe biblioteki producenta napisane są w sposób bardzo przemyślany, to skonfigurowanie USART-a zajmuje kilka linii i wymaga dokonania ustawień w kontrolerach różnych peryferiów.
W pierwszej sekcji następuje zidentyfikowanie numeru portu, który inicjujemy – 1, 2 lub 3. W tym samym miejscu uzupełniamy zmienne funkcji oraz – co ważniejsze – następuje tu zerowanie USART-a i zainicjowanie taktującego go zegara w kontrolerze RCC. Jest to bardzo ważna czynność, gdyż w procesorze STM32 każde peryferiom ma wyłączany zegar i po włączeniu procesora wszystkie interfejsy są wyłączone.
W drugiej sekcji następuje skonfigurowanie linii GPIO. Aby USART mógł komunikować się ze światem zewnętrznym należy skonfigurować odpowiednio linie GPIO, gdyż domyślnie po zerowaniu procesora wszystkie są skonfiurowane jako uniwersalne linie wejściowe. Należy je ustawić w tryb: GPIO_Mode_AF_PP dla linii Tx i GPIO_Mode_IN_FLOATING dla Rx.
W trzeciej sekcji następuje skonfigurowanie portu USART i włączenie go w jego własnym kontrolerze. Dokonuje się tego przy pomocy funkcji bibliotecznych. Wypełniamy odpowiednią strukturę z parametrami pracy interfejsu i wywołujemy funkcję inicjującą. Następnie wydajemy polecenie włączenia portu.
W ostatniej sekcji – jeśli nam jest to potrzebne (a w tej aplikacji jest potrzebne) – następuje skonfigurowanie i włączenie przerwania portu USART w kontrolerze NVIC a następnie włączenie przerwania w jego własnym kontrolerze. Konfigurujemy priorytety danego kanału po czym włączamy generowanie danego przerwania przy nadawaniu i odbieraniu.
Sterowniki
Obecnie konwerter działa z systemem operacyjnym Linux. Jako sterownik używany jest moduł CDC_ACM. Bardzo dobrze przemyślana idea budowy sterowników pod tym systemem operacyjnym sprawia, że nie ważne, czy urządzenie udostępnia jeden wirtualny port szeregowy czy większą ich liczbę, pamięć masową, urządzenie interfejsu HID itp. O ile liczba punktów końcowych na to pozwala sterowniki zawsze będą je właściwie obsługiwać.
Po podłączeniu prezentowanego konwertera do komputera wykrywane są 2 urządzenia: /dev/ttyACM0 i /dev/ttyACM1, co można sprawdzić w katalogu /dev/ lub w terminalu komendą dmesg wyświetlającą log systemowy – pod jego koniec powinno widoczne być coś podobnego do:
1 2 3 4 5 6 |
[ 5564.891091] usb 2-3.3: new full speed USB device using ehci_hcd and address 5 [ 5565.004053] usb 2-3.3: configuration #1 chosen from 1 choice [ 5565.021979] cdc_acm 2-3.3:1.0: This device cannot do calls on its own. It is not a modem. [ 5565.022006] cdc_acm 2-3.3:1.0: ttyACM0: USB ACM device [ 5565.022856] cdc_acm 2-3.3:1.2: This device cannot do calls on its own. It is not a modem. [ 5565.022880] cdc_acm 2-3.3:1.2: ttyACM1: USB ACM device |
Plików tych można używać jak normalnych portów szeregowych na Linuksie. Jeżeli pliki urządzeń nie są tworzone, wówczas albo nie mamy załadowanego modułu CDC_ACM albo moduł nie rozpoznaje numerów VendorID i ProductID naszego urządzenia – można wtedy ręcznie podać te numery sterownikowi. Takich urządzeń można oczywiście podpiąć do komputera więcej – będziemy wtedy mieli więcej urządzeń ttyACMx.
Ze względu na specyficzną ideę budowania sterowników w Windows sprawa jest bardziej skomplikowana – sterownik, który jest używany przy firmowym ST demo USB działa tu (po ustawieniu odpowiednich VendorID i ProductID) tylko z pojedynczym wirtualnym portem szeregowym używając systemowego usbser.sys – przy dwóch trzeba niestety zmodyfikować sterownik.
Piotr Wojtowicz
piotreklc60@gmail.com