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.
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:
- static void MX_USART1_UART_Init(void)
- {
- /* USER CODE BEGIN USART1_Init 0 */
- /* USER CODE END USART1_Init 0 */
- /* USER CODE BEGIN USART1_Init 1 */
- /* USER CODE END USART1_Init 1 */
- huart1.Instance = USART1;
- huart1.Init.BaudRate = 115200;
- huart1.Init.WordLength = UART_WORDLENGTH_8B;
- huart1.Init.StopBits = UART_STOPBITS_1;
- huart1.Init.Parity = UART_PARITY_NONE;
- huart1.Init.Mode = UART_MODE_TX_RX;
- huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
- huart1.Init.OverSampling = UART_OVERSAMPLING_16;
- if (HAL_UART_Init(&huart1) != HAL_OK)
- {
- Error_Handler();
- }
- /* USER CODE BEGIN USART1_Init 2 */
- /* USER CODE END USART1_Init 2 */
- }
- void log_SendData(UART_HandleTypeDef *huart, uint8_t *arrayToSend, uint8_t arraySize)
- {
- HAL_UART_Transmit(huart, arrayToSend, arraySize, 1000);
- }
- log_SendData(&huart1, (uint8_t*)"Dziala\r\n", 8);
Wysyłanie z użyciem rejestrów:
- void logger_SendData(uint8_t *arrayToSend, uint8_t arraySize)
- {
- uint16_t count = 0;
- while (count<arraySize)
- {
- while (!(((USART1->SR) & (USART_SR_TXE)) == (USART_SR_TXE))) {}
- USART1->DR = *(arrayToSend+count);
- count++;
- }
- }
Wysyłanie z użyciem bibliotek LL:
- void logger_SendData(uint8_t *arrayToSend, uint8_t arraySize)
- {
- uint16_t count = 0;
- while (count<arraySize)
- {
- while (!LL_USART_IsActiveFlag_TXE(USART1)) {}
- LL_USART_TransmitData8(USART1,*(uint8_t*)(arrayToSend+count));
- count++;
- }
- }
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.
- void log_SendData(UART_HandleTypeDef *huart, uint8_t *arrayToSend, uint8_t arraySize)
- {
- HAL_UART_Transmit_DMA(huart, arrayToSend, arraySize);
- }
Tutaj warto dołożyć pętle while sprawdzającą czy można wysłać dane po DMA.
- void log_SendData(UART_HandleTypeDef *huart, uint8_t *arrayToSend, uint8_t arraySize)
- {
- while(!(huart->gState == HAL_UART_STATE_READY)) { }
- HAL_UART_Transmit_DMA(huart, arrayToSend, arraySize);
- }
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:
- __attribute__((weak)) int _write(int file, char *ptr, int len)
- {
- int DataIdx;
- for (DataIdx = 0; DataIdx < len; DataIdx++)
- {
- __io_putchar(*ptr++);
- }
- return len;
- }
Więc w celu przekierowania danych z funkcji na konsolę wystarczy dodać przesłanie danych funkcją Hal_Transmit.
- int _write(int file, char *ptr, int len)
- {
- int DataIdx;
- for (DataIdx = 0; DataIdx < len; DataIdx++)
- {
- HAL_UART_Transmit(&huart1, (uint8_t *)ptr++, 1, HAL_MAX_DELAY);
- }
- return len;
- }
lub za pomocą rejestrów:
- int _write(int file, char *ptr, int len)
- {
- int DataIdx;
- for (DataIdx = 0; DataIdx < len; DataIdx++)
- {
- while (!(((USART1->SR) & (USART_SR_TXE)) == (USART_SR_TXE))) {}
- USART1->DR = *(ptr++);
- }
- return len;
- }
lub za pomocą DMA:
- int _write(int file, char *ptr, int len)
- {
- while(!(huart1.gState == HAL_UART_STATE_READY)) { }
- HAL_UART_Transmit_DMA(&huart1, (uint8_t *)ptr, len);
- return len;
- }
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:
- printf("1234\n");
- printf("test\n");
Poniższa instrukcja spowoduje wypisanie wszystkich wprowadzonych znaków ale dopiero po funkcji zawierającej \n:
- printf("1234");
- printf("dzia");
- printf("test\n");
Aby pominąć konieczność dodania \n należy ustawić sposób buforowania danych na NULL:
- 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():
- printf("1234");
- 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.
- uint8_t printArray[100];
- static uint8_t convert(uint8_t *arrayPointer, uint8_t positionInBuffer, uint32_t wartosc, uint8_t dlugosc)
- {
- uint8_t i = 0u;
- uint8_t temp = 0u;
- uint8_t x = 0u;
- uint32_t temp_wartosc;
- *(arrayPointer + positionInBuffer++) = 0x30;
- *(arrayPointer + positionInBuffer++) = 0x78;
- for(i = 0; i < dlugosc;i++)
- {
- temp_wartosc = wartosc >> (28-(i*4));
- temp = temp_wartosc & 0xF;
- if(temp < 10 )
- {
- x = temp + 0x30;
- }
- else
- {
- x = temp + 0x37;
- }
- *(arrayPointer + positionInBuffer++) = x;
- }
- return positionInBuffer;
- }
- void print(UART_HandleTypeDef *huart, uint8_t *msg_string, uint32_t wartosc)
- {
- uint8_t i = 0u;
- memset(printArray, 0x00, 100);
- for (i = 0u; msg_string[i]; i++)
- {
- if(msg_string[i] == 0x25)
- {
- if(msg_string[i+1] == 0x78)
- {
- i = convert(&printArray[0], i, wartosc, 8);
- }
- }
- else{
- printArray[i] = msg_string[i];
- }
- }
- while(!(huart1.gState == HAL_UART_STATE_READY)) { }
- HAL_UART_Transmit_DMA(huart, printArray, (i-1));
- }
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.
- uint8_t array[100] = {0x00};
- uint8_t bufferSize = sprintf((char *)array,"test array %d\r\n", 0x33);
- log_SendData(&huart1, array, bufferSize);