Komunikacja pomiędzy PC, a płytką DISCOVERY oparta została na interfejsie UART. W celu podłączenia urządzeń wymagana jest przejściówka USB-UART, najlepiej, wyposażona w układ FT232. Konfiguracja mikrokontrolera pod obsługę UART przedstawiona została w części 1 omawianej biblioteki.
Projekt do pobrania z: https://github.com/PiotrPoterala/stm32f429i-disco_stepper_motor
oraz biblioteki: https://github.com/PiotrPoterala/PP_Library
Struktura klas
Mechanizm odbioru/wysyłania danych oparty został na wzorcu projektowym Dekorator. Pozwala on na:
- dynamiczne dodawanie nowych funkcji do istniejących klas w trakcie działania programu,
- „opakowanie” oryginalnej klasy w klasę „dekorującą”.
W przypadku omawianego projektu:
- Component to PIOdevice – wirtualna klasa bazowa, dostarczająca interfejs do realizacji wysyłania/odbierania danych po UART,
- ConcreteComponent to PSerialPortRTX5 – dziedziczy po PIOdevic; klasa implementująca metody zdefiniowane w klasie bazowej z wykorzystaniem funkcji specyficznych dla zastosowanego systemu czasu rzeczywistego. W przypadku korzystania z FreeRTOS’a jako ConcreteComponent mogłaby funkcjonować np. klasa PSerialPortFreeRTOS.
- Decorator to PIOdeviceDecorator – dziedziczy po PIOdevice i agreguje referencję do obiektu klasy PIOdevice. Zawierając ten sam zestaw metod jak obiekt docelowy (Component) i delegując mu wszystkie żądania stanowi interfejs dla „nakładek”.
- ConcreteDecorator to defORTX5atCommandInterpreter – „nakładka”; dziedziczy po PIOdeviceDecorator; implementuje tylko te metody wirtualne, których ostateczny wynik działania ma być zmieniony, dodając pewne operacje, przed lub po przekazaniu żądania do obiektu docelowego (Component). W przypadku omawianego projektu, do metody readLine() dodane zostały operacje:
- interpretacji danych odebranych z portu UART,
- przesłania obrobionych danych (poprzez kolejkę) do wątku realizującego podstawowe zadania projektu (a więc obrót wirnika silnika o określony kąt, z określoną trajektorią prędkości obrotowej).
UART – wysyłanie danych w projekcie opartym na RTX5
Bezpośrednie wysłanie danych na port UART
PIOdevice*serialPort= new PSerialPortRTX5(USART1);
serialPort->open(PIOdevice::ReadWrite);
serialPort->write("test");
serialPort->close();
Wysłanie danych za pośrednictwem PTextStream:
PIOdevice* serialPort = new PSerialPortRTX5(USART1);
PTextStream out(serialPort);
serialPort->open(PIOdevice::ReadWrite);
out<<”test”;
serialPort->close();
Metoda write() przepisuje ciąg znaków do kolejki nadawczej sendQueue, (i ponieważ UART nie jest wstanie jednocześnie nadawać i odbierać danych) wyłącza odbieranie danych przez port UART (CR1_RE) i włącza przerwania od pustego rejestru transmisji danych (CR1_TXEIE=1).
pp_rtx5_serial_port.cpp
bool PSerialPortRTX5::write(string &data){
bool answer=true;
if(openMode==WriteOnly || openMode==ReadWrite){
for(auto it: data){
if(osMessageQueuePut(sendQueue, &it, 0, osWaitForever)==osOK){
name->CR1&=~USART_CR1_RE;
name->CR1|=USART_CR1_TXEIE;
}else{
answer=false;
break;
}
}
}else{
answer=false;
}
return answer;
}
W przypadku, gdy SR_TXE=1 (bit ustawiany sprzętowo) wywoływany jest podprogram przerwania, w którym poprzez metodę portListen(), pobierany jest znak z kolejki nadawczej sendQueue i wstawiany do rejestru DR (pełniącego funkcję bufora nadawczego).
pp_rtx5_serial_port.cpp
void PSerialPortRTX5::portListen(){
if(port->SR & USART_SR_RXNE){
receiveSignAndWriteToReceiveQueue();
}else if(port->SR & USART_SR_TXE){
sendSignFromSendQueue();
}
}
...
int PSerialPortRTX5::sendSignFromSendQueue(){
char sign;
osStatus_t status;
status=osMessageQueueGet(sendQueue, &sign, NULL, 0);
if(status==osOK){
port->DR=sign;
}else{
port->CR1&=~USART_CR1_TXEIE;
port->CR1|=USART_CR1_RE;
}
return status;
}
Po wysłaniu wszystkich znaków z kolejki nadawczej, następuje wyłączenie przerwania od pustego rejestru CR1_TXEIE i ponowne włączenie odbieranie danych przez port UART.
UART – odbieranie danych w projekcie opartym na RTX5
W omawianym projekcie, odbieranie danych jest domyślnym trybem pracy portu UART, a więc domyślnie ustawiony jest bit RE w rejestrze CR1 oraz włączone jest przerwanie od pełnego rejestru odbioru danych (CR1_ RXNEIE=1).
W przypadku, gdy SR_RXNE=1 (bit ustawiany sprzętowo) wywoływany jest podprogram przerwania, w którym poprzez metodę portListen(), pobierany jest znak z rejestru DR i wstawiany do kolejki odbiorczej receiveQueue.
pp_rtx5_serial_port.cpp
void PSerialPortRTX5::receiveSignAndWriteToReceiveQueue(){
char receiveChar;
receiveChar=(uint8_t)(port->DR);
osMessageQueuePut (receiveQueue, &receiveChar, 0, 0);
}
Pojawienie się danych w kolejce receiveQueue odblokowuje wątek vReceiveAndInterpretDataFromComUartThread w którym pobierane są dostępne dane z receiveQueue i zapisywane w zmiennej receiveString, aż do napotkania znaku nowej linii (pojawienie się znaku nowej linii powoduje ustawienie flagi getStringFlag na wartość true). Powyższe operacje zaimplementowane zostały w metodzie waitForReadyRead () wywoływanej w ciele pętli wątku vReceiveAndInterpretDataFromComUartThread. Do momentu wywołania readLine() nie przyjmowane są dane z kolejki receiveQueue. Jeżeli metoda readLine() zwraca pusty ciąg znaków (readLine().empty()==true) oznacza to, że dane do odczytu nie są gotowe.
pp_rtx5_serial_port.cpp
bool PSerialPortRTX5::waitForReadyRead(int usec){
char receiveChar;
int tick;
if(usec==osWaitForever){
tick=osWaitForever;
}else{
tick=(OS_TICK_FREQ/1000000)*usec;
}
if(getStringFlag==false){
if(osMessageQueueGet(receiveQueue, &receiveChar, NULL, tick) == osOK){
receiveString+=receiveChar;
if(receiveChar=='\n'){
getStringFlag=true;
}else if(receiveString.size()>64){
receiveString.clear();
}
return true;
}
}
return false;
}
Interpretacja odebranych danych
Interpretacja odebranych danych, w postaci AT komend, następuje w „dekoracji” metody readLine().
W przypadku podesłania AT+TRVV (przejazd o wartość) lub AT+TRVCO (przejazd na współrzędne) parametry danej operacji wysyłane są z wykorzystaniem kolejki taskCommunicationQueues do wątku vRealizationFunctionThread odpowiadającego za realizację podstawowych zadań projektu, a więc za kontrolę obrotów silnika krokowego. Ramka danych „ładowana” do kolejki zawiera informacje o:
- Źródle wywołania danej operacji(w tym przypadku jest to AT komenda),
- Typie operacji do realizacji,
- Parametrach operacji.
pp_rtx5_at_commands_interpreter.cpp
string defORTX5atCommandInterpreter::readLine(){
string receiveString=PIOdeviceDecorator::readLine();
if(receiveString.empty()==false){
PString data(receiveString);
string answer="FAIL\r\n";
int index=0;
if(data.find("AT+TRVV")!=string::npos){
map<char, double> values;
vector<int> valuesToSend;
index=data.find("AT+TRVV");
data.erase(0, index+7);
values=data.findValuesAfterAcronims();
if(!values.empty()){
valuesToSend.push_back(qMARK_ATC);
valuesToSend.push_back(AT_TAG_TRVV);
valuesToSend.push_back(values.size());
for(auto it=values.begin(); it!=values.end(); ++it){
valuesToSend.push_back((*it).first);
valuesToSend.push_back((*it).second*pow(10.0, phyCoord->getParamUnit((*it).first)));
}
taskCommunicationQueues->xQueueSendConteinerToBackWithSemaphore(valuesToSend);
answer="OK\r\n";
};
}else if(data.find("AT+TRVCO")!=string::npos){
map<char, double> values;
vector<int> valuesToSend;
index=data.find("AT+TRVCO");
data.erase(0, index+8);
values=data.findValuesAfterAcronims();
if(!values.empty()){
valuesToSend.push_back(qMARK_ATC);
valuesToSend.push_back(AT_TAG_TRVCO);
valuesToSend.push_back(values.size());
for(auto it=values.begin(); it!=values.end(); ++it){
valuesToSend.push_back((*it).first);
valuesToSend.push_back((*it).second*pow(10.0, phyCoord->getParamUnit((*it).first)));
}
taskCommunicationQueues->xQueueSendConteinerToBackWithSemaphore(valuesToSend);
answer="OK\r\n";
};
}else if(data.find("AT+BASEC")!=string::npos){
index=data.find("AT+BASEC");
if(data.at(index+8)=='?'){
answer="BASEC ";
answer+=baseCoord->getStringWithParams();
answer+="\r\n";
}else{
data.erase(0, index+8);
baseCoord->setParamsBasedString(&data);
answer="OK\r\n";
}
}else if(data.find("AT+PHYC")!=string::npos){
index=data.find("AT+PHYC");
if(data.at(index+7)=='?'){
answer="PHYC ";
answer+=phyCoord->getStringWithParams();
answer+="\r\n";
}else{
data.erase(0, index+8);
phyCoord->setParamsBasedString(&data);
answer="OK\r\n";
}
}
if(response)PIOdeviceDecorator::write(answer);
}
return receiveString;
}
Lista interpretowanych AT komend
AT+BASEC X<x>\r\n | Ustawienie współrzędnych operatora Odpowiedź: OK: zatwierdzenie odbioru danych, FAIL: brak parametrów, Parametry: <x>: nowa wartość współrzędnej X [mm], |
AT+BASEC? \r\n | Pobranie współrzędnych operatora. Odpowiedź: BASEC X<x>\r\n Parametry: <x>: wartość współrzędnej X [mm] |
AT+PHYC X<x> \r\n | Ustawienie współrzędnych fizycznych. Odpowiedź: OK: zatwierdzenie odbioru danych, FAIL: brak parametrów, Parametry: <x>: nowa wartość współrzędnej X [mm], |
AT+PHYC? \r\n | Pobranie współrzędnych fizycznych. Odpowiedź: PHYC X<x>\r\n Parametry: <x>: wartość współrzędnej X [mm], |
AT+TRVCO X<x>\r\n | Przejazd na współrzędne. Odpowiedź: OK: zatwierdzenie odbioru danych, FAIL: brak parametrów, Parametry: <x>: wsp. operatora X na jaką ma przejechać maszyna [mm], |
AT+TRVV X<x> r\n | Przejazd o wartość. Odpowiedź: OK: zatwierdzenie odbioru danych, FAIL: brak parametrów, Parametry: <x>: wartość przejazdu po osi X [mm], |