środa, 13 grudnia 2017

[33.2] STM32F4 - ENC28J60 - Przysyłanie danych z serwera UDP do klienta

W tym poście chciałbym przedstawić projekt układu wysyłającego dane z serwera UDP do klienta. Przesyłane będą informacje o z dwóch czujników DS18B20, dwóch przetworników analogowo cyfrowych.
[Źródło: http://www.st.com/en/evaluation-tools/stm32f4discovery.html]

Ten projekt jest kontynuacją wcześniejszego posta odnośnie serwera UDP. Dane od układu do klienta będą wysyłane po odebraniu zapytania od aplikacji. Będzie ona cyklicznie przesyłała zapytania do serwera. Po wysłaniu odbierze ona potrzebne dane które przedstawi na wykresie oraz w tabeli. Dane przyporządkowane będą do czasu systemowego komputera.

Cube Mx:


Na samym początku dorzucę kilka rzeczy do STM'a w programie CubeMx.

Pierwszą rzeczą jest przetwornik ADC. Wybrałem dwa wolne czyli ADC1 IN1 podłączone do PA1 oraz ADC2 IN0 pod pinem PA0.

Następnym elementem są czujniki temperatury. Tutaj wystarczy uruchomić jeden pin jako wyjście.

Program STM:


Wykorzystam program przedstawiony we wcześniejszym poście. Do niego zostanie dodana obsługa termometrów oraz kanałów ADC. Po otrzymaniu odpowiedniej komendy od klienta dane zostaną przesłane w następującej formie:

ST:T1:<temperatura>;T2:<temperatura>;ADC1:<wartość z ADC>;ADC2:<wartość z ADC>;<Obliczone CRC>;

Obliczanie CRC odbywa się z całej ramki danych. Jest ona przekazywana do funkcji, która zwraca obliczoną wartość na jednym bajcie:

  1. static uint8_t crcCalculate(uint8_t const message[], int nBytes)
  2. {
  3.     uint8_t i = 0;
  4.     uint8_t j = 0;
  5.     uint8_t crc = 0;
  6.       for (= 0; i < nBytes; i++)
  7.       {
  8.         crc ^= message[i];
  9.         for (= 0; j < 8; j++)
  10.         {
  11.           if ((crc & 1)>0)
  12.           {
  13.             crc ^= 0xA5;
  14.           }
  15.           crc >>= 1;
  16.         }
  17.       }
  18.       return crc;
  19. }

W moim przypadku przesyłane będą jedynie dane z czujników, dlatego nie będę kodował ramki danych. Jeśli chciało by się zaimplementować np. sterowanie przekaźnikami czy zmianę parametrów urządzenia bądź przesyłane dane nie mogły by być odczytywane przez osoby postronne, to należy ramkę danych zakodować. Z prostszych metod mogło by to być np. XOR z odpowiednim kluczem, czy Szyfr Cezara.

Przygotowanie ramki danych:

  1. static void udpReplayCheckFrame(enc28j60_frame_t *frame, uint16_t length, char *arrayReceive)
  2. {
  3.     extern uint16_t ADC_Val[2];
  4.     extern float glob_tempDataDs[2];
  5.     extern char glob_tempSign[2];
  6.     uint8_t calculateCrc = 0;
  7.     uint16_t port=0;
  8.     char buffer[60];
  9.     char bufferToSend[70];
  10.     ip_frame_t *ip_frame = (void*)(frame->dataBuffer);
  11.     udp_frame_t *udp_frame = (void*)(ip_frame->dataBuffer);
  12.     /* check receive frame */
  13.     if(strcmp(arrayReceive, "SendData\r\n") != 0)
  14.     {
  15.         /* Error different strings */
  16.         free(ip_frame);
  17.         free(udp_frame);
  18.         return;
  19.     }
  20.     /* If frame ok then prepare frame to send */
  21.     port = udp_frame->destiPort;
  22.     udp_frame->destiPort = udp_frame->sourcePort;
  23.     udp_frame->sourcePort = port;
  24.     udp_frame->packLength = 0;
  25.     /* prepase buffer */
  26.     sprintf(buffer,"ST:T1:%c%.2f;T2:%c%.2f;ADC1:%u;ADC2:%u",        glob_tempSign[0],
  27.                                                                     glob_tempDataDs[0],
  28.                                                                     glob_tempSign[1],
  29.                                                                     glob_tempDataDs[1],
  30.                                                                     ADC_Val[0],
  31.                                                                     ADC_Val[1]);
  32.     /* calculate CRC */
  33.     calculateCrc = crcCalculate((uint8_t*)buffer, strlen(buffer));
  34.     sprintf(bufferToSend,"%s;%u;\r\n", buffer, calculateCrc);
  35.     strcpy((char*)udp_frame->dataBuffer,bufferToSend);
  36.     length = strlen((char*)udp_frame->dataBuffer) + sizeof(udp_frame_t);
  37. #ifdef DEBUG_UDP_FIL
  38.     sprintf(buffer,"Calculate CRC: %u\r\n", calculateCrc);
  39.     HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen(buffer),0x1000);
  40. #endif
  41.     udp_frame->packLength = CONVERT_16BIT(length);
  42.     udp_frame->contSum=0;
  43.     udp_frame->contSum=checksum((uint8_t*)udp_frame-8, length+8, Prot_UPD);
  44.     ipSendData(frame, length+sizeof(ip_frame_t));
  45.     free(ip_frame);
  46.     free(udp_frame);
  47. #ifdef DEBUG_UDP_FIL
  48.     HAL_UART_Transmit(&huart2,(uint8_t*)"\r\nDATASEND\r\n",12,0x1000);
  49. #endif
  50. }

Ramka danych przygotowywana jest w funkcji odbierającej dane. Tam następuje podjęcie decyzji o przesyłaniu danych. Przygotowanie ramki sprawdzenie odpowiedzi itp. wykonywane jest w jednej funkcji. Program odpowiada na przesłanie komendy SendData\r\n. Inne przesłane komendy będą ignorowane.

Obliczenia dla DS18B20 jak i dla ADC wykonywane są ciągiem w pętli głównej. Gdzie przekazywane są do zmiennych globalnych a z nich podawane do funkcji przesyłających dane.

Konwersja dla dwóch kanałów ADC:

  1. uint16_t ADC_Val[2] = { 0 };
  2. void adcConversionADC1()
  3. {
  4.     float vsense;
  5.     char buffer[50];
  6.     const float V25 = 0.76;
  7.     const float SupplyVoltage = 3.0;
  8.     const float ADCResolution = 4095.0;
  9.     HAL_ADC_Start(&hadc1);
  10.     if (HAL_ADC_PollForConversion(&hadc1, 20) == HAL_OK)
  11.     {
  12.         ADC_Val[0] = HAL_ADC_GetValue(&hadc1);
  13.         vsense = (SupplyVoltage*ADC_Val[0])/ADCResolution;// Przeliczenie wartosci zmierzonej na napiecie
  14.         sprintf((char*)buffer,"ADC Value0: %u; Vsense: %f\r\n", ADC_Val[0], vsense);
  15.         HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen((char*)buffer),0x1000);
  16.     }
  17. }
  18. void adcConversionADC2()
  19. {
  20.     float temp;
  21.     float vsense;
  22.     char buffer[50];
  23.     const float V25 = 0.76;
  24.     const float SupplyVoltage = 3.0;
  25.     const float ADCResolution = 4095.0;
  26.     HAL_ADC_Start(&hadc2);
  27.     if (HAL_ADC_PollForConversion(&hadc2, 20) == HAL_OK)
  28.     {
  29.         ADC_Val[1] = HAL_ADC_GetValue(&hadc2);
  30.         vsense = (SupplyVoltage*ADC_Val[1])/ADCResolution;// Przeliczenie wartosci zmierzonej na napiecie
  31.         temp = ((vsense-V25)/SupplyVoltage)+25;// Obliczenie temperatury
  32.         sprintf((char*)buffer,"ADC Value2: %u; Vsense: %f\r\n", ADC_Val[1], vsense);
  33.         HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen((char*)buffer),0x1000);
  34.     }
  35. }

Pobranie temperatury z dwóch kanałów:

  1. float glob_tempDataDs[2] = {0.0};
  2. char glob_tempSign[2];
  3. void ds18b20ReadTempFromDev(void)
  4. {
  5.     uint8_t buffer[60];
  6.     uint8_t i;
  7.     uint8_t device[DS18B20_CONNECTED_DEV];
  8.     uint16_t readDataRaw;
  9.     float tempValue;
  10.     char sign;
  11.     for(i=1;i<=ds18b20_data.Dev_Cnt;i++)
  12.     {
  13.         ds18b20DevConTemp(NO_SKIP_ROM, i);
  14.     }
  15.     for(i=1;i<=ds18b20_data.Dev_Cnt;i++)
  16.     {
  17.         ds18b20ReadScratchpad(NO_SKIP_ROM, device, i);
  18.         sprintf((char*)buffer,"READ SCTARCHPAD %d: %02X %02X %02X %02X %02X %02X %02X %02X; ",
  19.                 i, device[0], device[1], device[2], device[3], device[4], device[5], device[6], device[7]);
  20.         HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen((char*)buffer),0x1000);
  21.         readDataRaw = ((uint16_t)device[1]<<8)|device[0];
  22.         if(ds18b20ReadSign(readDataRaw))    {   sign='-';   }
  23.         else                                {   sign='+';   }
  24.         tempValue = ds18b20GetTemp(readDataRaw);
  25.         /* Write temperature value into global value, to get it quick from it */
  26.         if(== 1)
  27.         {
  28.             glob_tempDataDs[0] = tempValue;
  29.             glob_tempSign[0] = sign;
  30.         }
  31.         else if(== 2)
  32.         {
  33.             glob_tempDataDs[1] = tempValue;
  34.             glob_tempSign[1] = sign;
  35.         }
  36.         sprintf((char*)buffer,"RegVal: 0x%04X - %c%.2f\r\n", readDataRaw, sign, tempValue);
  37.         HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen((char*)buffer),0x1000);
  38.     }
  39. }

Główna funkcja programu wygląda następująco:

  1. int main(void)
  2. {
  3.   /* USER CODE BEGIN 1 */
  4.   /* USER CODE END 1 */
  5.   /* MCU Configuration----------------------------------------------------------*/
  6.   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  7.   HAL_Init();
  8.   /* USER CODE BEGIN Init */
  9.   /* USER CODE END Init */
  10.   /* Configure the system clock */
  11.   SystemClock_Config();
  12.   /* USER CODE BEGIN SysInit */
  13.   /* USER CODE END SysInit */
  14.   /* Initialize all configured peripherals */
  15.   MX_GPIO_Init();
  16.   MX_SPI1_Init();
  17.   MX_USART2_UART_Init();
  18.   MX_TIM2_Init();
  19.   MX_ADC1_Init();
  20.   MX_ADC2_Init();
  21.   /* USER CODE BEGIN 2 */
  22.   netInitialize();
  23.   ds18b20InitReadData();
  24.   HAL_TIM_Base_Start_IT(&htim2);
  25.   HAL_ADC_MspInit(&hadc1);
  26.   HAL_ADC_MspInit(&hadc2);
  27.   /* USER CODE END 2 */
  28.   /* Infinite loop */
  29.   /* USER CODE BEGIN WHILE */
  30.   while (1)
  31.   {
  32.   /* USER CODE END WHILE */
  33.   /* USER CODE BEGIN 3 */
  34.       ds18b20ReadTempFromDev();
  35.       HAL_Delay(500);
  36.       ds18b20AlarmSearch();
  37.       HAL_Delay(250);
  38.       adcConversionADC1();
  39.       HAL_Delay(10);
  40.       adcConversionADC2();
  41.   }
  42.   /* USER CODE END 3 */
  43. }

Program w C#:


Poniżej przedstawię szybki program w C#, którego zadaniem będzie działanie jako klient UDP i przesyłanie zapytań o dane do serwera. Po przesłaniu zapytania dane zostaną odebrane i zapisane w pliku tekstowym, wyświetlone w tabeli oraz przedstawione na wykresie.

Okno główne aplikacji prezentuje się następująco:


Tutaj wysyłanie jak i odbieranie danych odbywa się w tle. Dane co 1 sekundę są przesyłane po ich wysłaniu następuje oczekiwanie na otrzymanie odpowiedzi.

W oknie programu znajdują się dwa pola tekstowe do wprowadzania danych odnośnie IP oraz portu. Przycisk Get Data przesyła dane do STM'a po czym czeka na odbiór. Przycisk Stop przerywa wysyłanie żądania do płytki. W polu listBox wyświetlane są odebrane dane. Poniżej znajdują się dwa wykresy na górnym przedstawiona jest temperatura a na dolnym wartości z ADC.

Odebrana ramka danych przed wyświetleniem sprawdza CRC, jeśli jest ono błędne to ramka zostaje odrzucona.

Cały projekt z programem dla STM'a jak i dla C# można pobrać z dysku google pod tym linkiem.