czwartek, 27 lipca 2023

STM32H7 - Biblioteka CException

W tym poście chciałbym opisać zastosowanie biblioteki CException w układzie STM32H7. 

Biblioteka CException pozwala na wprowadzenie obsługi wyjątków w języku C. Można ją pobrać pod tym linkiem. Wykorzystuje ona funkcje setjmp oraz longjmp

Opis, Przykładowe użycie:


Do działa potrzebujemy dwóch plików, które dołączamy do projektu:

  • CException.c
  • CException.h

Pliki zostają po prostu skopiowane do projektu, który został wygenerowany przez program CubeMx. Natomiast biblioteka jest łatwa w implementacji i nie będzie żadnego problemu z jej wdrożeniem na innych układach. Warunkiem jest jedynie zaimplementowana obsługa funkcji setjmp oraz longjmp. 

Biblioteka wprowadza dodatkowe bloki Try{ } Catch { }, które pozawalają na obsługę wyjątków.

Bloki Try oraz Catch są zdefiniowane jako define. Zdefiniowano je w pliku .h:

  1. //Try (see C file for explanation)
  2. #define Try                                                         \
  3.     {                                                               \
  4.         jmp_buf *PrevFrame, NewFrame;                               \
  5.         unsigned int MY_ID = CEXCEPTION_GET_ID;                     \
  6.         PrevFrame = CExceptionFrames[MY_ID].pFrame;                 \
  7.         CExceptionFrames[MY_ID].pFrame = (jmp_buf*)(&NewFrame);     \
  8.         CExceptionFrames[MY_ID].Exception = CEXCEPTION_NONE;        \
  9.         CEXCEPTION_HOOK_START_TRY;                                  \
  10.         if (setjmp(NewFrame) == 0) {                                \
  11.             if (1)
  12.  
  13. //Catch (see C file for explanation)
  14. #define Catch(e)                                                    \
  15.             else { }                                                \
  16.             CExceptionFrames[MY_ID].Exception = CEXCEPTION_NONE;    \
  17.             CEXCEPTION_HOOK_HAPPY_TRY;                              \
  18.         }                                                           \
  19.         else                                                        \
  20.         {                                                           \
  21.             e = CExceptionFrames[MY_ID].Exception;                  \
  22.             (void)e;                                                \
  23.             CEXCEPTION_HOOK_START_CATCH;                            \
  24.         }                                                           \
  25.         CExceptionFrames[MY_ID].pFrame = PrevFrame;                 \
  26.         CEXCEPTION_HOOK_AFTER_TRY;                                  \
  27.     }                                                               \
  28.     if (CExceptionFrames[CEXCEPTION_GET_ID].Exception != CEXCEPTION_NONE)
  29.  

Ich dokładne działanie zostało opisane w pliku CException.c w komentarzu.

Poniżej jakaś prosta funkcja prezentująca zastosowanie biblioteki:

  1. void CheckThrow(uint8_t a, uint8_t b) {
  2.     uint8_t sum = a + b;
  3.  
  4.     if(sum > 150 && sum < 200)
  5.     {
  6.        printf("Throw 0x54\r\n");
  7.        fflush(stdout);
  8.         Throw(0x54);
  9.     }
  10.     else if(sum > 200)
  11.     {
  12.       printf("Throw 0x88\r\n");
  13.       fflush(stdout);
  14.         Throw(0x88);
  15.     }
  16.     else
  17.     {
  18.         //do some stuff
  19.     }
  20. }
  21.  
  22. void Worker_Test(void)
  23. {
  24.     volatile CEXCEPTION_T e;
  25.  
  26.     Try {
  27.         CheckThrow(100, 46);
  28.         CheckThrow(200, 1);
  29.         CheckThrow(100, 59);
  30.     }
  31.     Catch(e) {
  32.       if(e == 0x54)
  33.       {
  34.           printf("catch code 0x54\r\n");
  35.           fflush(stdout);
  36.       }
  37.       else if(e == 0x88)
  38.       {
  39.           printf("catch code 0x88\r\n");
  40.           fflush(stdout);
  41.       }
  42.       else{
  43.           printf("catch diff code");
  44.       }
  45.     }
  46. }

Blok Catch zostanie wywołany po pierwszej instukcji Throw, co oznacza, że ostatnia funkcja CheckThrow nie zostanie wykonana. Dodatkowo wszystkie zadania w funkcji, które są umieszczone za blokiem Throw() nie zostaną wykonane. Wynika to z faktu, że w bloku Throw() znajduje się wykonanie skoku (longjmp) do wcześniej ustawionego skoku przez blok Try().

W pliku CException.h znajdują się cztery wartości na jakie warto zwrócić uwagę: 

  1. //This is the value to assign when there isn't an exception
  2. #ifndef CEXCEPTION_NONE
  3. #define CEXCEPTION_NONE      (0x5A5A5A5A)
  4. #endif
  5.  
  6. //This is number of exception stacks to keep track of (one per task)
  7. #ifndef CEXCEPTION_NUM_ID
  8. #define CEXCEPTION_NUM_ID    (1) //there is only the one stack by default
  9. #endif
  10.  
  11. //This is the method of getting the current exception stack index (0 if only one stack)
  12. #ifndef CEXCEPTION_GET_ID
  13. #define CEXCEPTION_GET_ID    (0) //use the first index always because there is only one anyway
  14. #endif
  15.  
  16. //The type to use to store the exception values.
  17. #ifndef CEXCEPTION_T
  18. #define CEXCEPTION_T         unsigned int
  19. #endif

CEXCEPTION_NONE jest wartością jaka jest domyślnie ustawiana dla wyjątku, gdy nie został wywołany żaden wyjątek. Jest to wartości czterobajtowa, ponieważ zmienna CEXCEPTION_T została ustawiona jako unsigned int. Zmienna w formacie unsigned int jest w większości przypadków 4 bajtowa (dla STM też jest to wartość 4 bajtowa), natomiast nie musi tak być. Dlatego warto zweryfikować jakie wartości możemy wprowadzić do zmiennej zanim zaczniemy pracę z biblioteką.

CEXCEPTION_NUM_ID jest to ilość wyjątków jakie należy sprawdzać. Według autora należy zastosować jeden numer dla jednego procesu.

CEXCEPTION_GET_ID definiuje z ilu indeksów tablicy, ze zdefiniowanymi wyjątkami możemy pobierać dane. 
 

Ważne: 


Na co należy zwrócić uwagę podczas stosowania tej biblioteki. 

W przypadku biblioteki CException należy pamiętać aby nie używać instrukcji return w bloku Try. Dotyczy to także instrukcji goto. Wynika to z tego, że bloki te alokują pamięć, która zostaje wyczyszczona i zwolniona dopiero po zakończeniu bloku Catch. Wspomniane instrukcje doprowadzą do wycieku pamięci w układzie bądź do innego nie przewidywalnego zachowania. 

Dodatkowo w przypadku gdy chcemy aktualizować dane w bloku Try{ } oraz uzyskać do nich dostęp w po Throw(), należy użyć modyfikatora volatile do zmiennej. Pozwoli to na poprawne ustawienie danych w zmiennej. np. funkcja:

  1. #define ERROR_THROW_CODE 0x34
  2.  
  3. void UpdateTest(uint8_t a)
  4. {
  5.   volatile CEXCEPTION_T e;
  6.   uint8_t b = 0;
  7.  
  8.   Try {
  9.     a = 7 + 5;
  10.     b = a;
  11.     Throw(ERROR_THROW_CODE);
  12.   }
  13.   Catch(e) {
  14.       if(e == ERROR_THROW_CODE)
  15.       {
  16.         printf("ERROR_THROW_CODE Catch\r\n");
  17.       }
  18.       else
  19.       {
  20.         printf("Diff Code Catch\r\n");
  21.       }
  22.   }
  23.   printf("a = %d, b = %d", a, b);
  24.   fflush(stdout);
  25. }

Z włączoną optymalizacją -O3 zwróci następujące informacje:

  1. ERROR_THROW_CODE Catch
  2. a = 12, b = 0

Teraz jeśli zostawimy -O3 oraz dołożymy modyfikator volatile to otrzymamy:

  1. ERROR_THROW_CODE Catch
  2. a = 12, b = 12

W obu przypadkach gdy zamienimy optymalizację na -O0 to wszystko będzie działało poprawnie. Wynika z tego, że zawsze w takich przypadkach należy umieszczać modyfikator volatile, aby uniknąć późniejszych błędów.