środa, 5 kwietnia 2023

STM32H7 - HAL USART DMA TX RX

W tym poście chciałbym opisać sposób obsługi interfejsu USART z wykorzystaniem DMA oraz bibliotek HAL. Całość testowałem na płycie z układem STM32H725.


Cube Mx:


Inicjalizacja wygląda następująco:



HAL TX:


W tym przypadku należy przygotować bufor do wysyłania danych. Całość realizujemy za pomocą jednej funkcji. Tworzymy globalny bufor przechowujący zmienne do wysłania:

  1. uint8_t data[] = "ztest_spr\r\n";
  2. ...
  3. HAL_UART_Transmit_DMA(&huart1, &data[0], sizeof(data));
  4. osDelay(200);

Przy takiej implementacji pojawiają się dwa problemy. Po pierwsze nie możemy zmienić danych w buforze. Taka operacja nie będzie miała żadnego efektu. Dalej będziemy przesyłać dane w niezmienionej formie.

  1. data[6]='m';
  2. HAL_UART_Transmit_DMA(&huart1, &data[0], sizeof(data));
  3. osDelay(200);

W celu rozwiązania powyższego problemu można wyłączyć uruchamianie pamięci D cache:

  1.   /* Enable D-Cache---------------------------------------------------------*/
  2.   //SCB_EnableDCache();

Drugie rozwiązanie. Jakie udało mi się znaleźć (link) dotyczy przygotowania osobnych buforów, które pomijają pamięć cache. Przy ich deklaracji należy pamiętać aby ich rozmiar został zdefiniowany jako potęga dwójki.

  1. struct dmaStruct{
  2.   uint8_t dma_rx[128];
  3.   uint8_t dma_tx[128];
  4. };
  5.  
  6. struct dmaStruct nocache __attribute__ ((aligned(256)));

Teraz do inicjalizacji regionu MPU należy dodać następujące dane:

  1. MPU->RBAR = ((uint32_t)&nocache) | MPU_RBAR_VALID_Msk; // using region slot 0
  2. MPU->RASR =
  3.     MPU_RASR_XN_Msk            |
  4.     (3u << MPU_RASR_AP_Pos)    |
  5.     (4u << MPU_RASR_SIZE_Pos)  |
  6.     (3u << MPU_RASR_SIZE_Pos)  |
  7. (2u << MPU_RASR_SIZE_Pos)  |
  8.     MPU_RASR_ENABLE_Msk        |
  9.     0;

Rozmiar w pamięci musi być wyrównany do długości bufora, co najlepiej aby było wielkością ramki danych. Dzięki temu nie będzie problemów z przesyłaniem danych przez interfejs. 

Struktura rejestru RASR:

Rozmiar bufora definiujemy na pozycjach [5 - 1] (5 bitów) jako potęgi 2 (wzór 2^(SIZE + 1)). 
Czyli aby uzyskać 32 bajty należy w rejestrze w pozycji 4 wprowadzić 1. W moim przypadku, potrzebuję 256 bajtów po 128 na TX i RX. Czyli potrzebuję w miejscu size wprowadzić wartość 7

Opis rejestrów MPU można znaleźć w tym dokumencie.

Po takich operacjach można z bufora korzystać bez problemów z włączonym DCache. 

  1. msgSize = sprintf(nocache.dma_tx, "    ztest_spr\r\n");
  2.  
  3. //..
  4. //..
  5.  
  6. nocache.dma_tx[8] = 'x';
  7. HAL_UART_Transmit_DMA(&huart1, &nocache.dma_tx, msgSize);
  8. osDelay(200);

W przypadku korzystania z buforów lokalnych można spodziewać się pewnych problemów jak np. brak wysyłania danych, dane wysyłane ale w innej formie i z inną prędkością. 

Kolejnym ważnym elementem jest oczekiwanie na przesłanie danych. Jeśli za szybko będziemy chcieli wysyłać dane to zostaną one wysłane w niepełnej formie np. 

  1. msgSize = sprintf(nocache.dma_tx, "    ztest_spr weweweew testuje czy to dziala dobrze czy nie nie wiadomo\r\n");
  2. HAL_UART_Transmit_DMA(&huart1, &nocache.dma_tx, msgSize);
  3. msgSize = sprintf(nocache.dma_tx, "    ztest_spr asdasdasdas testuje czy to dziala dobrze czy nie nie wiadomo\r\n");
  4. HAL_UART_Transmit_DMA(&huart1, &nocache.dma_tx, msgSize);

Działa to tak dlatego, że bufor jest dosyć duży i nie ma możliwości przesłania całej wiadomości tak szybko jak nastąpi kolejne zapytanie. W funkcji HAL_UART_Transmit_DMA jest na samym początku instrukcja warunkowa:

  1. HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
  2. {
  3.   /* Check that a Tx process is not already ongoing */
  4.   if (huart->gState == HAL_UART_STATE_READY)
  5.   {

Jeśli stan nie będzie ustawiony na READY to nastąpi wyjście z funkcji ze statusem HAL_BUSY. I wysyłanie danych przepadnie. 

Rozwiązaniem takiego problemu jest albo oczekiwanie na wysłanie danych przez wykonanie blokady za pomocą pętli while():

  1. msgSize = sprintf(nocache.dma_tx, "    ztest_spr weweweew testuje czy to dziala dobrze czy nie nie wiadomo\r\n");
  2. while(huart1.gState != HAL_UART_STATE_READY) { }
  3. HAL_UART_Transmit_DMA(&huart1, &nocache.dma_tx, msgSize);
  4. while(huart1.gState != HAL_UART_STATE_READY) { }
  5. msgSize = sprintf(nocache.dma_tx, "    ztest_spr asdasdasdas testuje czy to dziala dobrze czy nie nie wiadomo\r\n");
  6. HAL_UART_Transmit_DMA(&huart1, &nocache.dma_tx, msgSize);

Będzie to działało poprawnie, natomiast blokada urządzenia nie jest najlepszym pomysłem. Należy oczywiście pamiętać o tym, żeby modyfikować bufor dma_tx dopiero po przesłaniu danych. W innym przypadku, gdy bufor będzie modyfikowany przed zakończeniem wysyłania, ramka danych będzie zawierała zmodyfikowane fragmenty. Dużo lepszym rozwiązaniem (wymagającym też nieco więcej pracy) jest przygotowanie bufora kołowego. Przygotowaną ramkę dodajemy do bufora i co określoną ilość czasu sprawdzamy czy są jakieś dane przewidziane do wysłania na urządzenie, oraz czy DMA skończyło wysyłać wcześniejszą ramkę danych.

HAL RX:


W tej części opiszę odbieranie danych. Dane będą zapisywane do wcześniej zdeklarowanego bufora w sposób opisany powyżej. 

Zakładam tutaj, że chcemy odebrać dowolną ilość znaków. I nie wiemy w jakiej ilości pakiety będą przesyłane. Czyli mogą być wysłane krótkie wiadomości lub długie. 

Przy odbieraniu danych można zastosować standardowy bufora danych i odbieranie danych pojedynczo. W takim przypadku bufor dla RX może być ustawiony na mniejszą ilość znaków. 

Po standardowej inicjalizacji DMA w funkcji main włączamy nasłuchiwanie. 

  1. HAL_UART_Receive_DMA(&huart1, nocache.dma_rx, 1);

Teraz odebranie bajtu danych spowoduje wygenerowanie przerwania. Nasłuchiwanie będzie uruchamiane każdorazowo, już w obsłudze przerwania po odebraniu danych.

  1. volatile uint8_t buffor_rx_test[128] = {0x00};
  2. volatile uint8_t i = 0;
  3. void DMA1_Stream1_IRQHandler(void)
  4. {
  5.   /* USER CODE BEGIN DMA1_Stream1_IRQn 0 */
  6.  
  7.   /* USER CODE END DMA1_Stream1_IRQn 0 */
  8.   HAL_DMA_IRQHandler(&hdma_usart1_rx);
  9.   /* USER CODE BEGIN DMA1_Stream1_IRQn 1 */
  10.   buffor_2[i++] = nocache.dma_rx[0];
  11.   HAL_UART_Receive_DMA(&huart1, nocache.dma_rx, 1);
  12.   if(i == 128){ i = 0; }
  13.   /* USER CODE END DMA1_Stream1_IRQn 1 */
  14. }

Tak odebrane dane możemy obsługiwać poprzez oczekiwanie na konkretny znak np \0 (jeśli odbieramy ciągi znaków lub wiemy kiedy zakończy się transmisja), lub timeout, który będzie resetowany po odebraniu kolejnego bajta danych. Po jego przekroczeniu przechodzimy do sprawdzania poprawności odebranej ramki danych.

Drugi sposób dotyczy zastosowania przerwania HAL_UARTEx_ReceiveToIdle_DMA. Jest on o tyle ciekawy, że pozwoli na odbiór całej ramki danych za jednym razem.

Inicjalizacja DMA identyczna jak wcześniej. Tym razem jednak potrzebujemy większego bufora. Tak aby odbierał całą ramkę za jednym razem. 

  1. HAL_UARTEx_ReceiveToIdle_DMA(&huart1, nocache.dma_rx, 128);

Zakończenie odbierania danych zostanie zasygnalizowane wyzwoleniem przerwania:

  1. void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
  2. {
  3.     if (huart->Instance == USART1)
  4.     {
  5.         HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t *) nocache.dma_rx, 128);
  6.     }
  7. }