fbpx

Nie tak dawno napisałem artykuł o tym, jak można zaimplementować odbiór UART po DMA. Udało mi się z powodzeniem napisać taką “bezobsługową” implementację dla STM32F411. Wielu z moich czytelników korzysta również z tanich płytek z F103 a przeportowanie biblioteki sprawiało ogromne problemy. Dostosowałem mój kod i pokażę Ci, na czym polegały różnice i ewentualne trudności.

Na początku zachęcam do zapoznania się ze wpisem dla STM32F411, w którym szczegółowo opisałem jak udało mi się zrealizować odbiór UART po DMA. W tym wpisie zajmuję się jedynie przeportowaniem tej metody na STM32F103C8 na prośbę czytelników.

Różnice między F4 a F1

Ze znaczących różnic wymieniłbym tylko jedną. Jest nim moduł DMA, który pełni istotną funkcję w odbiorze. W mikrokontrolerach serii STM32F1 DMA jest “biedniejsze”. W każdym znajdującym się module DMA znajduje się tylko kilka kanałów, z którymi możemy pobierać dane. W F103C8 jest łącznie 12 kanałów.

STM32F4 ma już trochę więcej “bajerów” w DMA. Jak już wspomniałem o kanałach, to pewnie czujesz, że tutaj będzie różnica, co nie? Otóż w F4 mamy coś takiego jak Strumienie (Stream). F411RE ma tych strumieni aż 16(po 8 na każdy z dwóch kontrolerów DMA). Na każdy taki strumień składa się do 8 kanałów. To jest jedna z głównych różnic, która może powodować kłopoty przy portowaniu.

Skoro mamy bardziej rozbudowane DMA, to oczywiście będą inne rejestry odpowiedzialne za wszystko. W moim kodzie posługiwałem się częściowo właśnie “gołymi” rejestrami. Dokładne różnice omówię zaraz przy kodzie.

Jest jeszcze jedna istotna “mała” różnica. Otóż pamiętasz tę “sztuczkę” w której zatrzymanie DMA wywoływało z automatu przerwanie od zakończenia transmisji? To jest działanie prawdziwe jedynie dla Streamów, czyli np. mikrokontrolery F4. Dla STM32F103 zatrzymując kanał DMA nie uraczymy niestety takiego przerwania.

STM32CubeIDE – konfiguracja

Zacznijmy od konfiguracji w CubeIDE. Wersja, z której korzystam to v1.1.0 ze zintegrowanym CubeMX v5.4.0 natomiast biblioteki HAL to F1 v1.8.0.

Jako platformę testową użyłem znanego dobrze BluePill’a, którego możesz kupić z pewnego źródła – u mnie 🙂
Konfiguracja jest bardzo podobna do tej z poprzedniego wpisu, który dotyczył rodziny F4. Musisz skonfigurować UART2 (lub inny) na prędkość 115200 Bits/s oraz włączyć kanał DMA dla odbierania na tym UART’cie. Oprócz tego należy włączyć globalne przerwania UART2, aby móc korzystać z przerwania IDLE.

Poza tym potrzebujesz jeszcze kilku rzeczy. Ustaw pin PC13 jako wyjście. Znajduje się na nim wbudowana w płytkę LED’ka, którą będziemy sterować komendami UART.
Na końcu włącz Debugowanie po Serial Wire – bardzo ułatwia pracę 😉

oraz ustaw sobie większy zegar HCLK. Ja dałem 64 MHz.

Kod

Tak skonfigurowany projekt można wygenerować. Zacznę od pliku main.c. Tutaj nic się nie zmienia w stosunku do kodu z F4. Tak samo inicjalizujesz bibliotekę oraz w prosty sposób parsujesz dane przychodzące z UART2.

  /* USER CODE BEGIN 2 */
  UARTDMA_Init(&huartdma, &huart2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  if(UARTDMA_IsDataReady(&huartdma))
	  {
		  UARTDMA_GetLineFromBuffer(&huartdma, ParseBuffer);
		  if(strcmp(ParseBuffer, "ON") == 0)
		  {
			  HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
		  }
		  else if(strcmp(ParseBuffer, "OFF")  == 0)
		  {
			  HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
		  }
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Próba przekopiowania biblioteki przekopiowanej z F4 zakończy się masą błędów. Pamiętaj też o odpowiednim wstawieniu kodów przerwań. błędy wynikają ze wspomnianych wyżej różnic pomiędzy DMA w obydwu mikrokontrolerach. Tak wyglądało drzewo rejestrów dla F411

A tak dla dzisiejszego bohatera – F1

Możesz zauważyć, że różnica jest dosyć spora. Jednak tylko na pierwszy rzut oka wygląda to groźnie. Dużo funkcji w rejestrach pokrywa się ze sobą, lecz jest inaczej nazwana. Możesz zauważyć takie podobieństwa jak DMA_LISR/DMA_HISR oraz DMA_ISR. Podwójny rejestr wynika z większej ilości statusów w F4.
Pousuwajmy błędy zgłaszane przez kompilator!

“Błędne” nazwy rejestrów

Najczęściej występującym błędem będzie nieznana nazwa rejestru lub bitu w nim, z których korzystamy. Pojedźmy od góry pliku. Pierwsze co się zgłasza to

huartdma->huart->hdmarx->Instance->CR &= ~DMA_SxCR_EN;

Kompilator nie zna rejestru CR ani bitu DMA_SxCR_EN. Zerkając w notę F411 możemy zobaczyć, że jest to rejestr konfiguracyjny Streamu, do którego wpisujemy zanegowany bit włączania DMA. Znajdujemy się w funkcji przerwania UART IDLE więc jest to po prostu wyłączenie DMA po odebraniu ciągu znaków.
Co trzeba zrobić? Znaleźć jak się nazywa rejestr i bit odpowiedzialny za to samo w F103! Jak mówiłem, rejestry mają podobne nazwy. Tak naprawdę większość z nich różni się tym, że w F411 nazywają się “Stream X…”, a dla F103 “Channel X…”.  Tak właśnie jest dla rejestru CR, czyli Configuration Register.

Szybkie zerknięcie w Reference Manual i już wiemy, że rejestr ma skrót CCR, a bit włączający DMA to EN. Zgodnie z konwencją HALowską błędna linijka powinna zostać poprawiona na taką

huartdma->huart->hdmarx->Instance->CCR &= ~DMA_CCR_EN;

I kompilator już się od niej odczepił. Dawaj mi tu kolejny problem. Tym razem kompilator czepia się, że nie zna StreamBaseAddress. Jest to element struktury HALowskiej od DMA. Nic dziwnego, że jej nie zna, bo w F1 nie ma Stream’ów. Na logikę biorąc to ta zmienna jest wskaźnikiem na podstawę adresową strumienia DMA. Należy zmienić ją na DmaBaseAddress. Znalazłem to szperając w definicji tej struktury.
Zamień więc

DMA_Base_Registers *DmaRegisters = (DMA_Base_Registers *) huartdma->huart->hdmarx->StreamBaseAddress;

na 

MA_Base_Registers *DmaRegisters = (DMA_Base_Registers *) huartdma->huart->hdmarx->DmaBaseAddress;

Jednak tutaj możesz czegoś nie zauważyć. Chwilę wcześniej została zdefiniowana taka struktura

typedef struct
{
	__IO uint32_t ISR;   // DMA interrupt status register
	__IO uint32_t Reserved0;
	__IO uint32_t IFCR;  // DMA interrupt flag clear register
} DMA_Base_Registers;

To są właśnie te bazowe rejestry, czyli statusowy i czyszczący przerwania. Między nimi jest jedno pole Reserved0. Dlaczego? Zauważ, że w F411 masz po dwa rejestry. Są “górne” i “dolne”. Łącznie po 64 bity na status i czyszczenie. W strukturze zostało to właśnie w taki sposób ujęte, że za 32-bitowym LISR są kolejne 32 bity, które traktować można jako kolejne z łącznie “wirtualnego” 64-bitowego rejestru ISR.
W F103 nie masz “górnych” i “dolnych” rejestrów. Jest tylko jeden ISR i IFCR. Dlatego można tę rezerwację wyrzucić, aby zgadzały się nazwy z tym, na co będą wskazywać.

typedef struct	
{	
	__IO uint32_t ISR;   // DMA interrupt status register	
	__IO uint32_t IFCR;  // DMA interrupt flag clear register	
} DMA_Base_Registers;

Teraz mamy trochę inny problem. Kompilator zgłasza, że w linijce

maRegisters->IFCR = DMA_FLAG_TCIF0_4 << huartdma->huart->hdmarx->StreamIndex;

są dwa błędy. Nieznana flaga oraz przesunięcie. Szybko zerkając w dokumentację możesz zauważyć, że flaga DMA_FLAG_TCIF0_4 to flaga Transfer Complete dla Streamu nr 0 lub 4 w zależności, z którym rejestrem mamy do czynienia w F4. Łatwo można zauważyć, że trzeba ją zamienić na jej odpowiednik z F1, czyli DMA_IFCR_CTCIF1. Zmiennej StreamIndex kompilator nie zna dokładnie z tego samego powodu co trochę wyżej nie znał adresu bazowego rejestrów DMA. Po prostu zamień Stream na Channel otrzymując całość wyglądającą tak

DmaRegisters->IFCR = DMA_IFCR_CTCIF1 << huartdma->huart->hdmarx->ChannelIndex;

Dalej widoczny jest problem z rejestrem, w którym przechowywana jest liczba pozostałych do przetransferowania danych po DMA. Tutaj jest prosto, bo według dokumentacji

Length = DMA_RX_BUFFER_SIZE - huartdma->huart->hdmarx->Instance->NDTR;

wystarczy zamienić na

Length = DMA_RX_BUFFER_SIZE - huartdma->huart->hdmarx->Instance->CNDTR;

Ostatnie różnice w obsłudze przerwania DMA znajdziesz przy jego wyjściu. Są tam linijki dotyczące czyszczenia przerwania DMA, ustawiania adresu początkowego do wpisywania przez DMA, ustawienie ilości danych do transferu DMA oraz samo wystartowanie DMA.
W każdej z nich kompilator zwraca błędy.

DmaRegisters->IFCR = 0x3FU << huartdma->huart->hdmarx->StreamIndex; 		// Clear all interrupts	
huartdma->huart->hdmarx->Instance->M0AR = (uint32_t) huartdma->DMA_RX_Buffer; // Set memory address for DMA again	
huartdma->huart->hdmarx->Instance->NDTR = DMA_RX_BUFFER_SIZE; // Set number of bytes to receive	
huartdma->huart->hdmarx->Instance->CR |= DMA_SxCR_EN;            	// Start DMA transfer

Po wcześniejszych opisach powinieneś sobie już z nimi poradzić. Może z jednym małym wyjątkiem….
Możesz zauważyć tajemniczą wartość 0x3F wpisywaną do rejestru IFCR. W rejestrze tym kasowane są flagi przerwania poprzez wpisanie jedynki na odpowiednie miejsce. Dlaczego więc 0x3F? Bierze się to stąd, że pierwsze 6 flag dotyczy jednego Streamu w F4. Ta wartość jest przesuwana o StreamIndex w lewo, aby wybrać właściwe flagi.

W F1 tych flag jest mniej bo tylko 4, więc będziesz musiał wpisać 0x0F do adekwatnego rejestru.

Przeszukując i dopasowując wszystkie nazwy dla F1 całość wychodzi następująco

DmaRegisters->IFCR = 0x0FU << huartdma->huart->hdmarx->ChannelIndex; 		// Clear all interrupts
huartdma->huart->hdmarx->Instance->CMAR = (uint32_t) huartdma->DMA_RX_Buffer; // Set memory address for DMA again
huartdma->huart->hdmarx->Instance->CNDTR = DMA_RX_BUFFER_SIZE; // Set number of bytes to receive
huartdma->huart->hdmarx->Instance->CCR |= DMA_CCR_EN;            	// Start DMA transfer

W całym kodzie pozostała jeszcze tylko jedna błędna linijka podczas inicjalizacji biblioteki UARTDMA. Dotyczy ona wyłączenia przerwania od połowy transferu. Zmienia się oczywiście nazwa rejestru oraz bitu.
W F4 było tak

huartdma->huart->hdmarx->Instance->CR &= ~DMA_SxCR_HTIE; // Disable DMA Half Complete interrupt

a w F1 jest tak

huartdma->huart->hdmarx->Instance->CCR &= ~DMA_CCR_HTIE; // Disable DMA Half Complete interrupt

Prosta zmiana 🙂

Czy to zadziała?

Jeszcze nie! Pamiętasz jak trochę wyżej napisałem, że dla DMA z kanałami taki jak jest w F103 nie wywoła się przerwanie Transfer Complete po zatrzymaniu DMA? Co zrobić? Ja wymyśliłem takie coś, aby dodać po prostu ręczne wywołanie funkcji obsługi przerwania przed wyjściem z przerwania obsługującego IDLE UART. 
Cała funkcja będzie więc wyglądała tak

void UARTDMA_UartIrqHandler(UARTDMA_HandleTypeDef *huartdma)
{
	if(huartdma->huart->Instance->SR & UART_FLAG_IDLE)       // Check if Idle flag is set
	{
		volatile uint32_t tmp;
		tmp = huartdma->huart->Instance->SR;                      // Read status register
		tmp = huartdma->huart->Instance->DR;                      // Read data register
		huartdma->huart->hdmarx->Instance->CCR &= ~DMA_CCR_EN; 	  // Disable DMA - it will force Transfer Complete interrupt if it's enabled
																  // BUT! It's only for DMA Streams(i.e F4), not for Channels like there in F103!
		tmp = tmp; // For unused warning
		UARTDMA_DmaIrqHandler(huartdma); // Since DMA IRQ won't start independently for Channels, we have to handle it manually
	}
}

Jednak pamiętaj, że funkcja UARTDMA_DmaIrqHandler(huartdma) powinna również znaleźć się w obsłudze przerwania Transfer Complete. Pomimo założenia, że komunikaty powinny być krótsze niż bufor DMA, może się zdarzyć, że jednak przyjdzie coś dłuższego (życie weryfikuje :)). Wtedy istnieje niebezpieczeństwo, że bufor DMA się zapełni, a przerwania IDLE UART jeszcze nie dostaniesz. W tym wypadku czyszczenie bufora DMA wykona się w przerwaniu Transfer Complete.

Podsumowanie

Czy przeportowanie z F411 na F103 było takie trudne? I tak i nie… Łatwa była zmiana samych rejestrów. Gorzej było ze wpadnięciem na pomysł, że przerwanie DMA TC nie wywoła się po “ręcznym” wyłączeniu DMA. Sam początkowo nie wiedziałem, o co chodzi, że przerwanie “nie wstaje” 🙂 Jednak wszystko dobrze się skończyło i otrzymujesz ode mnie gotowy poradnik takiego portowania oraz kod dla BluePill 😉

kurs stm32

Pełny projekt wraz z biblioteką znajdziesz jak zwykle na moim GitHubie: link

Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podyskutować na ten temat, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.

5/5 - (4 votes)

Podobne artykuły

.

14 komentarzy

Grzegorz · 07/06/2020 o 10:23

Czy zamiast DMA_IFCR_CTCIF1 u kolegi w projekcie nie powinno być DMA_IFCR_CTCIF6 – bo 6 kanał DMA?

    Mateusz Salamon · 07/06/2020 o 11:15

    Chodzi o tę linjkę? DmaRegisters->IFCR = DMA_IFCR_CTCIF1 << huartdma->huart->hdmarx->ChannelIndex; // Clear Transfer Complete flag

    Ma być ..F1. Zobacz, że przesuwam ten bit o ilość kanałów. W ten sposób ten kod jest uniwersalny niezależnie na którym kanale/streamie DMA będzie ten UART 🙂

      Grzegorz · 07/06/2020 o 12:18

      A tak – jasne jak słońce. Czas odpocząć…….

Pio · 28/01/2020 o 17:53

Przy szybkim baudrate biblioteka ma problem z ilością znaków zbliżoną do wielkości bufora.
Przerwanie od USART IDLE występuje wcześniej niż DMA kończy kopiowanie wszystkich znaków.
Np bufor 64 bajty – przerwanie IDLE występuje gdy DMA zdążyło skopiować dopiero 51 znaków.
Więc rozwiązanie z wywołaniem przerwania DMA z IDLE jest błędne.

    Pio · 28/01/2020 o 17:54

    Zapomniałem dodać. Baud 526500

    Mateusz Salamon · 28/01/2020 o 17:59

    Wow! Dzięki za ten test! Nie pomyślałem, aby przetestować z tak wysokim baudem. Będę musiał coś wymyślić… Można na szybko dać chamskie i mało eleganckie oczekiwanie na zwolnienie DMA. A tak z ciekawości – do czego używasz tak wysokiego bauda?

    Mateusz Salamon · 28/01/2020 o 18:01

    I czy mógłbyś opisać dokładniej co robisz, że tak wychodzi? Jak i kiedy przychodzą ramki na przykład. Tutaj w komentarzu albo na maila: mateusz@msalamon.pl

      Pio · 28/01/2020 o 18:26

      Robię translację z UART 9-bit na 2 ramki 8-bit i składanie ich z powrotem.
      Ogólnie problem występuje przy jeszcze szybszym baudzie 921600 po stronie 8-bitowej.

      Urządzenie obsługuje jedną opcję na raz (tj albo 8-bit -> 9-bit albo na odwrót).
      Problem mam przy odbiorze 8 bitów więc tak jak w tutku.
      9-bitowej strony jeszcze nie testowałem, ale różni się tylko typem bufora (16-bitowy) i konfiguracją DMA na HalfWord.

      Tutaj trzeba teraz wpaść na jakiś błyskotliwy pomysł. Oczekiwanie w przerwaniu jest niekoszerne 😉 Jakby w przerwaniu IDLE był dostęp do ilośći danych odebranych przez UART można by to wykorzystać, np zapisać w strukturze i w pętli głównej obsłużyć DMA zgodnie z ilością.

      Szubki baud to wymóg urządzenia które owe dane wysyła i nie mam tu pola manewru.

      Pio · 29/01/2020 o 21:20

      Ogólnie z tym DMA i UARTem1 to dziwne rzeczy się dzieją.
      Ogólnie co bym nie zrobił to przy 8bitach dostaję przerwanie IDLE po 51 znakach.
      Oczywiście reszta idzie w śmietnik. Trzeba by sprawdzić czy jakiegoś błędu nie wyłapuje serial.

      Mateusz Salamon · 29/01/2020 o 21:46

      No niezłe masz zadanie. Wiesz o czym pomyślałem jeszcze. Masz dostęp do analizatora logicznego? Zobacz co się dzieje po tym 51-szym znaku. Ten IDLE w MCU nie jest idealny, a w zasadzie transmisje na UART nie są. Przerwanie IDLE wystrzeli zawsze, gdy pojawi się przerwa w transmisji o długości JEDNEGO znaku. Może masz jakieś małe przytkanie na transmisji, stąd wykrywa IDLE. Im szybsza transmisja tym mniejszy czas do decyzji o wystawieniu IDLE.

      Pio · 01/02/2020 o 13:09

      Cóż, po wielu kombinacjach doszedłem do wniosku że trzeba to wszystko maksymalnie uporościć.
      Idealnym rozwiązaniem w moim przypadku jest użycie DMA w trybie CIRCULAR i tylko przerwania IDLE.
      Podczas IDLE zapisujemy jako head pozycję z CNDTR. Tail jak dotychczas zostaje sterowany z odczytu.
      Pozbywamy się przy tym drugiego bufora.
      Można dać odpowiednio duży na DMA i obsługa tego jest znacznie uproszczona.
      Do tego nie przejmujemy się już co w danym momencie robi DMA, a obsługę przepełnienia bufora łatwo zrobić w samym przerwaniu IDLE (sprawdzamy tail’a, bieżącą pozycję i z głowy).

      Jednym słowem prostota wygrała. Zniknęły wszystkie problemy 🙂

      Mateusz Salamon · 01/02/2020 o 13:12

      Super! Pewnie spróbuję kiedyś zrobić tak, jak mówisz.
      Z drugim buforem miałem taki zamysł, aby w nim właśnie trzymać dane do parsowania i faktycznie sprawdza mi się to w moich zastosowaniach.

Marek · 12/12/2019 o 11:38

A tych przerywan nie da się z pod hal włączyć? Bez zabawy z rejstrami?

    Mateusz Salamon · 12/12/2019 o 11:39

    Niestety z tego co widziałem to HAL nie ma obsługi IDLE w swoich handlerach 🙁 nie wiem czemu tego nie zrobili

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *