środa, 25 listopada 2020

[44] STM32F4 - Funkcja printf na konsole, logowanie danych

W tym poście chciałbym opisać w jaki sposób przenieść funkcję printf. Tak aby uzyskać wyświetlenie danych na konsoli. Do testów wykorzystałem płytkę STM32F7 Discovery oraz STM32F407VG Discovery.

[Źródło: http://www.st.com/en/evaluation-tools/stm32f4discovery.html]

Przesyłanie danych UART:


Przesyłanie danych przez UART w celu debugowania aplikacji lub jej sprawdzania może wpłynąć na samo działanie programu. Zwłaszcza gdy jest wykorzystany tryb blokujący, co przy większej ilości znaków może znacząco spowolnić działanie całej aplikacji a nawet spowodować poprawne lub błędne działanie niektórych modułów. W tym przypadku lepiej zostać przy przesłaniu jak najkrótszych wiadomości w miejscach gdzie jest to konieczne.

Z tego powodu dobrym pomysłem jest użycie DMA do przesyłania informacji. Ponieważ transmisja nie będzie wpływała na działanie pozostałych elementów.

Przesłanie danych z użyciem trybu blokującego:

  1. static void MX_USART1_UART_Init(void)
  2. {
  3.   /* USER CODE BEGIN USART1_Init 0 */
  4.   /* USER CODE END USART1_Init 0 */
  5.   /* USER CODE BEGIN USART1_Init 1 */
  6.   /* USER CODE END USART1_Init 1 */
  7.   huart1.Instance = USART1;
  8.   huart1.Init.BaudRate = 115200;
  9.   huart1.Init.WordLength = UART_WORDLENGTH_8B;
  10.   huart1.Init.StopBits = UART_STOPBITS_1;
  11.   huart1.Init.Parity = UART_PARITY_NONE;
  12.   huart1.Init.Mode = UART_MODE_TX_RX;
  13.   huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  14.   huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  15.   if (HAL_UART_Init(&huart1) != HAL_OK)
  16.   {
  17.     Error_Handler();
  18.   }
  19.   /* USER CODE BEGIN USART1_Init 2 */
  20.  
  21.   /* USER CODE END USART1_Init 2 */
  22. }

  1. void log_SendData(UART_HandleTypeDef *huart, uint8_t *arrayToSend, uint8_t arraySize)
  2. {
  3.       HAL_UART_Transmit(huart, arrayToSend, arraySize, 1000);
  4. }
  5.  
  6. log_SendData(&huart1, (uint8_t*)"Dziala\r\n", 8);

Wysyłanie z użyciem rejestrów:

  1. void logger_SendData(uint8_t *arrayToSend, uint8_t arraySize)
  2. {
  3.   uint16_t count = 0;
  4.   while (count<arraySize)
  5.   {
  6.     while (!(((USART1->SR) & (USART_SR_TXE)) == (USART_SR_TXE))) {}
  7.     USART1->DR = *(arrayToSend+count);
  8.     count++;
  9.   }
  10. }

Wysyłanie z użyciem bibliotek LL:

  1. void logger_SendData(uint8_t *arrayToSend, uint8_t arraySize)
  2. {
  3.   uint16_t count = 0;
  4.   while (count<arraySize)
  5.   {
  6.     while (!LL_USART_IsActiveFlag_TXE(USART1)) {}
  7.     LL_USART_TransmitData8(USART1,*(uint8_t*)(arrayToSend+count));
  8.     count++;
  9.   }
  10. }

Funkcje można zmodyfikować, np. przez kończenie wysłanej wiadomości znakiem \0 (NUL), gdy napotkamy ten znak to przerywamy wysyłanie. Problem pojawia się gdy chcemy przesłać tablicę w której zero może wystąpić wcześniej i transmisja zostanie przerwana. Można to rozwiązać przez zastosowanie dwóch funkcji przesyłających. Jedna do przesłania standardowych stringów, druga natomiast wysyła dane o podanej wielkości.

Powyżej podstawowa funkcja przesyłająca dane w trybie blokującym. Wykorzystywana funkcja HAL_UART_Transmit()

Kolejnym rozwiązaniem jest wysyłanie danych z użyciem DMA.

Tutaj wystarczy zmienić instrukcję przesyłającą dane. Gdy korzystamy z bibliotek HAL'a oraz mieć uruchomione przerwania od USART.

  1. void log_SendData(UART_HandleTypeDef *huart, uint8_t *arrayToSend, uint8_t arraySize)
  2. {
  3.       HAL_UART_Transmit_DMA(huart, arrayToSend, arraySize);
  4. }

Tutaj warto dołożyć pętle while sprawdzającą czy można wysłać dane po DMA. 

  1. void log_SendData(UART_HandleTypeDef *huart, uint8_t *arrayToSend, uint8_t arraySize)
  2. {
  3.       while(!(huart->gState == HAL_UART_STATE_READY)) { }
  4.       HAL_UART_Transmit_DMA(huart, arrayToSend, arraySize);
  5. }

W przypadku braku sprawdzenia stanu mogą nastąpić problemy z przesyłaniem danych ponieważ transmisja nie zostanie wykonana do czasu aż DMA nie zakończy poprzedniej operacji.

Korzyści z wykorzystania DMA występują przy wysyłaniu całej paczki danych. W przypadku gdy dane byłyby wysyłane pojedynczo to po każdym przesłaniu danych należałoby czekać aż poprzednia wartość zostanie wysłana.

Printf:


W celu przekierowania funkcji printf na uart należy zmienić definicję funkcji _write, która została zdefiniowana w pliku syscalls.c:

  1. __attribute__((weak)) int _write(int file, char *ptr, int len)
  2. {
  3.     int DataIdx;
  4.  
  5.     for (DataIdx = 0; DataIdx < len; DataIdx++)
  6.     {
  7.         __io_putchar(*ptr++);
  8.     }
  9.     return len;
  10. }

Więc w celu przekierowania danych z funkcji na konsolę wystarczy dodać przesłanie danych funkcją Hal_Transmit. 

  1. int _write(int file, char *ptr, int len)
  2. {
  3.   int DataIdx;
  4.  
  5.   for (DataIdx = 0; DataIdx < len; DataIdx++)
  6.   {
  7.     HAL_UART_Transmit(&huart1, (uint8_t *)ptr++, 1, HAL_MAX_DELAY);
  8.   }
  9.   return len;
  10. }

lub za pomocą rejestrów:

  1. int _write(int file, char *ptr, int len)
  2. {
  3.     int DataIdx;
  4.  
  5.     for (DataIdx = 0; DataIdx < len; DataIdx++)
  6.     {
  7.         while (!(((USART1->SR) & (USART_SR_TXE)) == (USART_SR_TXE))) {}
  8.         USART1->DR = *(ptr++);
  9.     }
  10.     return len;
  11. }

lub za pomocą DMA:

  1. int _write(int file, char *ptr, int len)
  2. {
  3.     while(!(huart1.gState == HAL_UART_STATE_READY)) { }
  4.     HAL_UART_Transmit_DMA(&huart1, (uint8_t *)ptr, len);
  5.     return len;
  6. }

Ogólnie funkcja _write musi zawierać procedurę przesłania znaku przez UART.

Dodatkowo należy pamiętać, że po takiej implementacji każdy przesyłany string z funkcji printf musi zawierać znak przejścia do nowej linii ('\n') w innym przypadku nie nastąpi buforowanie danych ani wejście do funkcji _write. Co oznacza, że przesyłane dane muszą wyglądać np. tak:

  1. printf("1234\n");
  2. printf("test\n");

Poniższa instrukcja spowoduje wypisanie wszystkich wprowadzonych znaków ale dopiero po funkcji zawierającej \n:

  1. printf("1234");
  2. printf("dzia");
  3. printf("test\n");

Aby pominąć konieczność dodania \n należy ustawić sposób buforowania danych na NULL:

  1. setvbuf(stdout, NULL, _IONBF, 0);

Można ją wywołać np w funkcji main zanim nastąpi przesyłanie danych funkcją printf().

Kolejną możliwością jest wywołanie funkcji fflush() po funkcji printf():

  1. printf("1234");
  2. fflush(stdout);

Kolejnym elementem jest przesłanie numeru w formacie float. Tutaj należy uruchomić flagę obsługujący ten format dla funkcji printf(). Najnowsza wersja STM32CubeIde informuje użytkownika o braku wsparcia dla tego rozwiązania, oraz wyświetla flagę jaką należy uruchomić:


Custom print:


Funkcja printf() zużywa dosyć sporo zasobów pamięci, co może być istotne gdy chcemy wykorzystać tą funkcję na małym mikrokontrolerze. W takim przypadku dobrze jest przygotować sobie okrojoną wersję np. przesyłającą dane w formacie HEX.

  1. uint8_t printArray[100];
  2.  
  3. static uint8_t convert(uint8_t *arrayPointer, uint8_t positionInBuffer, uint32_t wartosc, uint8_t dlugosc)
  4. {
  5.     uint8_t i = 0u;
  6.     uint8_t temp = 0u;
  7.     uint8_t x = 0u;
  8.     uint32_t temp_wartosc;
  9.  
  10.     *(arrayPointer + positionInBuffer++) = 0x30;
  11.     *(arrayPointer + positionInBuffer++) = 0x78;
  12.  
  13.     for(i = 0; i < dlugosc;i++)
  14.     {
  15.         temp_wartosc = wartosc >> (28-(i*4));
  16.  
  17.         temp = temp_wartosc & 0xF;
  18.         if(temp < 10 )
  19.         {
  20.             x = temp + 0x30;
  21.         }
  22.         else
  23.         {
  24.             x = temp + 0x37;
  25.         }
  26.  
  27.         *(arrayPointer + positionInBuffer++) = x;
  28.     }
  29.  
  30.     return positionInBuffer;
  31. }
  32.  
  33. void print(UART_HandleTypeDef *huart, uint8_t *msg_string, uint32_t wartosc)
  34. {
  35.     uint8_t i = 0u;
  36.  
  37.     memset(printArray, 0x00, 100);
  38.  
  39.     for (i = 0u; msg_string[i]; i++)
  40.     {
  41.         if(msg_string[i] == 0x25)
  42.         {
  43.             if(msg_string[i+1] == 0x78)
  44.             {
  45.                 i = convert(&printArray[0], i, wartosc, 8);
  46.             }
  47.          }
  48.          else{
  49.              printArray[i] = msg_string[i];
  50.          }
  51.     }

  52.   while(!(huart1.gState == HAL_UART_STATE_READY)) { }
  53.     HAL_UART_Transmit_DMA(huart, printArray, (i-1));
  54. }

Zapis danych musi się wykonywać do tablicy globalnej ponieważ gdy wysyłanie jest na końcu to DMA nie zdąży wysłać danych przed wyjściem z funkcji. Co spowoduje utratę części przesyłanej wiadomości.

Innym prostszym sposobem jest przygotowanie ramki z wykorzystaniem funkcji sprinf. 

  1. uint8_t array[100] = {0x00};
  2. uint8_t bufferSize = sprintf((char *)array,"test array %d\r\n", 0x33);
  3. log_SendData(&huart1, array, bufferSize);