piątek, 28 października 2022

[51] STM32F4 - UART IAP

 W tym poście chciałbym opisać sposób przygotowania IAP dla STM32F4.

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

Projekt dla STM32F4 został zmodyfikowany z projektu udostępnionego przez firmę ST dla mikrokontrolerów STM32F1 oraz STM32Lx, który można pobrać pod tym linkiem:


Opis projektu można pobrać z noty producenta pod tym linkiem

Mapa pamięci flash:


STM32F407 został wyposażony w pamięć flash o pojemności 1024kB. 

Poniżej struktura pamięci układu:


Obszar System memory:


Rozmiar tej pamięci wynosi 30Kbytes. W tej części pamięci zlokalizowany jest bootloader. Dostęp do tej części możliwy jest przez poprawne ustawienie konfiguracji BOOT, lub poprzez konfigurację w pamięci flash. 


Pin BOOT0 jest osobnym pinem na mikrokontrolerze, natomiast pin BOOT1 jest dzielony wraz z pinem BOOT1. 

Gdy chcemy wymusić ładowanie programu zawsze z Main Flash Memmory należy podłączyć pin BOOT0 do GND. 

Obszar OTP (One time Programmable):


Jest to obszar pozwalający na przechowywanie danych zapisanych na stałe, np. dane konfiguracyjne.

Bajty zostały podzielona na 16 bloków danych. Każdy z bloków zawiera po 32 bajty. Dane można zapisywać w pamięci od adresu 0x1FFF7800 do 0x1FFF7A00. Obszar pamięci od adresu 0x1FFF7A01 do 0x1FFF7A0F stanowią lock bity. Poniżej mapa pamięci części OTP pobranej z dokumentacji:


Dane można wprowadzać do bloku OTP do czasu zablokowania go przez wprowadzenie wartości 0x00 do odpowiadającemu mu wartości LOCK. W tym bloku mogą powinny pojawić się tylko dwie rodzaje wartości czyli 0x00 lub 0xFF. 

Poniżej funkcje odpowiedzialne za obsługę bajtów OTP:

  1. OTP_Operation_Status_t OTP_WriteByte(uint8_t block, uint8_t byte, uint8_t data);
  2. uint8_t OTP_ReadData(uint8_t block, uint8_t byte);
  3. OTP_Operation_Status_t OTP_LockBlock(uint8_t block);

Option bytes:

Organizacja pamięci wygląda następująco:


Ta części zawiera bajty odpowiedzialne za ustawienie poziomu ochrony przed odczytem/zapisem do/z pamięci. Ustawienia użytkownika (watchdog, bor itp). Dokładniejsze informacje można znaleźć w dokumentacji producenta (Reference manual str. 88).

Przygotowanie programu:


Większą część projektu można bezpośrednio skopiować z przykładu dla STM32F1. Jedynie na co trzeba zwrócić uwagę jest obsługa pamięci Flash.

Konfigurację projektu zostawię bardzo podstawową. Uruchamiam jedynie UART0 do obsługi danych z podprogramu IAP, przycisk do uruchomienia podprogramu podłączony do PA0 oraz diody podłączone do pinów PD12 do PD15.

Ogólnie wygląda to tak, że w pamięci urządzenia zostają umieszczone dwa programy. Program IAP zostaje wywołany warunkowo i jego zadaniem jest wykonanie specjalnych działań jak, programowanie urządzenia, odczyt programu itp. W związku z tym że oba programy zostały umieszczone w różnych częściach pamięci flash, pozwala im to na osobne działanie.

Na samym początku w obu projektach należy zmodyfikować funkcję SystemInit(). Powinna ona wyglądać w następujący sposób:

  1. void SystemInit(void)
  2. {
  3.   /* FPU settings ------------------------------------------------------------*/
  4.   #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
  5.     SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));  /* set CP10 and CP11 Full Access */
  6.   #endif
  7.  
  8.   //Dodana sekcja.
  9.   /* Reset the RCC clock configuration to the default reset state ------------*/
  10.   /* Set HSION bit */
  11.   RCC->CR |= (uint32_t)0x00000001;
  12.   /* Reset CFGR register */
  13.   RCC->CFGR = 0x00000000;
  14.   /* Reset HSEON, CSSON and PLLON bits */
  15.   RCC->CR &= (uint32_t)0xFEF6FFFF;
  16.   /* Reset PLLCFGR register */
  17.   RCC->PLLCFGR = 0x24003010;
  18.   /* Reset HSEBYP bit */
  19.   RCC->CR &= (uint32_t)0xFFFBFFFF;
  20.   /* Disable all interrupts */
  21.   RCC->CIR = 0x00000000;
  22.   //Koniec dodanej części
  23.  
  24. #if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM)
  25.   SystemInit_ExtMemCtl();
  26. #endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
  27.  
  28.   /* Configure the Vector Table location -------------------------------------*/
  29. #if defined(USER_VECT_TAB_ADDRESS)
  30.   SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
  31. #endif /* USER_VECT_TAB_ADDRESS */
  32. }

Nowsze wersje CubeMx pomijają te deklaracje (znajdujące się w sekcji //Dodana sekcja). Co powoduje błędne działanie całego projektu. 

Na samym początku należy wprowadzić definicję sektorów pamięci flash do projektu, zgodnie z tabelą przedstawioną w poprzednim punkcie:

  1. #define ADDR_FLASH_SECTOR_0     ((uint32_t)0x08000000) /* Sector 0, 16 Kbytes */
  2. #define ADDR_FLASH_SECTOR_1     ((uint32_t)0x08004000) /* Sector 1, 16 Kbytes */
  3. #define ADDR_FLASH_SECTOR_2     ((uint32_t)0x08008000) /* Sector 2, 16 Kbytes */
  4. #define ADDR_FLASH_SECTOR_3     ((uint32_t)0x0800C000) /* Sector 3, 16 Kbytes */
  5. #define ADDR_FLASH_SECTOR_4     ((uint32_t)0x08010000) /* Sector 4, 64 Kbytes */
  6. #define ADDR_FLASH_SECTOR_5     ((uint32_t)0x08020000) /* Sector 5, 128 Kbytes */
  7. #define ADDR_FLASH_SECTOR_6     ((uint32_t)0x08040000) /* Sector 6, 128 Kbytes */
  8. #define ADDR_FLASH_SECTOR_7     ((uint32_t)0x08060000) /* Sector 7, 128 Kbytes */
  9. #define ADDR_FLASH_SECTOR_8     ((uint32_t)0x08080000) /* Sector 8, 128 Kbytes */
  10. #define ADDR_FLASH_SECTOR_9     ((uint32_t)0x080A0000) /* Sector 9, 128 Kbytes */
  11. #define ADDR_FLASH_SECTOR_10    ((uint32_t)0x080C0000) /* Sector 10, 128 Kbytes */
  12. #define ADDR_FLASH_SECTOR_11    ((uint32_t)0x080E0000) /* Sector 11, 128 Kbytes */

Dla STM32 pamięć flash rozpoczyna się od adresu 0x08000000.

Następnie należy zmodyfikować pozostałe definicję z:

  1. /* Define the address from where user application will be loaded.
  2.    Note: this area is reserved for the IAP code */
  3. #define FLASH_PAGE_STEP FLASH_PAGE_SIZE           /* Size of page : 2 Kbytes */
  4. #define APPLICATION_ADDRESS (uint32_t)0x08004000      /* Start user code address: ADDR_FLASH_PAGE_8 */
  5.  
  6. /* Notable Flash addresses */
  7. #define USER_FLASH_END_ADDRESS  0x08040000
  8.  
  9. /* Define the user application size */
  10. #define USER_FLASH_SIZE ((uint32_t)0x00003000) /* Small default template application */
  11.  
  12. /* Define bitmap representing user flash area that could be write protected (check restricted to pages 8-39). */
  13. #define FLASH_PAGE_TO_BE_PROTECTED (OB_WRP_PAGES8TO9 | OB_WRP_PAGES10TO11 | OB_WRP_PAGES12TO13 | OB_WRP_PAGES14TO15 | \
  14. OB_WRP_PAGES16TO17 | OB_WRP_PAGES18TO19 | OB_WRP_PAGES20TO21 | OB_WRP_PAGES22TO23 | \
  15. OB_WRP_PAGES24TO25 | OB_WRP_PAGES26TO27 | OB_WRP_PAGES28TO29 | OB_WRP_PAGES30TO31 | \
  16. OB_WRP_PAGES32TO33 | OB_WRP_PAGES34TO35 | OB_WRP_PAGES36TO37 | OB_WRP_PAGES38TO39  )  

Na:

  1. #define USER_START_ADDRESS ADDR_FLASH_SECTOR_2
  2. #define USER_END_ADDRESS ADDR_FLASH_SECTOR_6
  3. #define USER_FLASH_SIZE ((uint32_t )(USER_END_ADDRESS - USER_START_ADDRESS))
  4. #define USER_WRP_SECTORS ( OB_WRP_SECTOR_2  |  OB_WRP_SECTOR_3  |  OB_WRP_SECTOR_4  |  OB_WRP_SECTOR_5 )
  5. #define APLICATION_ADDRESS USER_START_ADDRESS

Program udostępniony przez producenta wykorzystuje układ STM32F1. Oznacza to konieczność modyfikacji funkcji odpowiedzialnych za zapis oraz odczyt danych z pamięci Flash. 

Na samym początku należy zmodyfikować funkcje odpowiedzialne za odblokowanie funkcji Flash.

Funkcja oryginalna:

  1. void FLASH_If_Init(void)
  2. {
  3.   HAL_FLASH_Unlock();
  4.   __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);
  5.   HAL_FLASH_Lock();
  6. }

Funkcja po modyfikacji:

  1. static void Flash_UnlockClearFlags(void) {
  2.     HAL_FLASH_Unlock();
  3.     __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR |
  4.                            FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR);
  5. }
  6.  
  7. void FLASH_If_Init(void) {
  8.     Flash_UnlockClearFlags();
  9.     HAL_FLASH_Lock();
  10. }

Powyższa funkcja czyści flagi flash w celu odblokowania dostępu do zapisu danych.

Funkcja odpowiadająca za czyszczenie pamięci. Jest to modyfikacja funkcji zapisanej w przykładzie:

  1. uint32_t FLASH_If_Erase(uint32_t start)
  2. {
  3.     uint32_t FirstSector = 0;
  4.     uint32_t SectorError = 0;
  5.     FLASH_EraseInitTypeDef pEraseInit;
  6.     HAL_StatusTypeDef status = HAL_OK;
  7.  
  8.     Flash_UnlockClearFlags();
  9.  
  10.     FirstSector = GetSectorBaseOnAddr(USER_START_ADDRESS);
  11.  
  12.     pEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
  13.     pEraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3;
  14.     pEraseInit.Sector = FirstSector;
  15.     pEraseInit.NbSectors = GetSectorBaseOnAddr(USER_END_ADDRESS) - FirstSector;
  16.     status = HAL_FLASHEx_Erase(&pEraseInit, &SectorError);
  17.  
  18.     HAL_FLASH_Lock();
  19.  
  20.     if (status != HAL_OK) { return FLASHIF_ERASEKO; }
  21.  
  22.     return FLASHIF_OK;
  23. }

Czyszczenie pamięci wykonywane jest w zakresie zdefiniowanym przez użytkownika. W tym przypadku pomiędzy sektorem 2 a sektorem 6.

Funkcja wykonująca zapis danych:

  1. uint32_t FLASH_If_Write(uint32_t destination, uint32_t *p_source, uint32_t length)
  2. {
  3.     uint32_t i = 0;
  4.  
  5.     Flash_UnlockClearFlags();
  6.  
  7.     for (i = 0; (i < length) && (destination <= (USER_END_ADDRESS-4)); i++)
  8.     {
  9.         if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, destination, *(uint32_t*)(p_source+i)) == HAL_OK)
  10.         {
  11.             if (*(uint32_t*)destination != *(uint32_t*)(p_source+i))
  12.             {
  13.                 HAL_FLASH_Lock();
  14.                 return (FLASHIF_WRITINGCTRL_ERROR);
  15.             }
  16.             destination += 4;
  17.         }
  18.         else
  19.         {
  20.             HAL_FLASH_Lock();
  21.             return (FLASHIF_WRITING_ERROR);
  22.         }
  23.     }
  24.  
  25.     HAL_FLASH_Lock();
  26.     return (FLASHIF_OK);
  27. }

Po wpisaniu wartości do pamięci należy sprawdzić czy poprawnie został wykonany zapis danych. Jeśli jest błąd to należy zablokować pamięć flash i zwrócić informacje o przekłamaniu danych. Gdy wartości są poprawne to zwiększamy adres o 4. W przypadku błędu programu również blokujemy pamięć i zwracamy błąd. Blokada pamięci flash (blokada oraz wyłączenie rejestrów kontrolnych) ma za zadanie uchronienie jej przed niepożądanymi operacjami.

Funkcja pobierająca status blokady zapisy pamięci flash:

  1. uint32_t FLASH_If_GetWriteProtectionStatus(void)
  2. {
  3.     uint32_t ProtectedSector = FLASHIF_PROTECTION_NONE;
  4.     FLASH_OBProgramInitTypeDef OptionsBytesStruct;
  5.  
  6.     Flash_UnlockClearFlags();
  7.     HAL_FLASHEx_OBGetConfig(&OptionsBytesStruct);
  8.     HAL_FLASH_Lock();
  9.  
  10.     ProtectedSector = ~(OptionsBytesStruct.WRPSector) & USER_WRP_SECTORS;
  11.  
  12.     if(ProtectedSector != 0) { return FLASHIF_PROTECTION_WRPENABLED; }
  13.  
  14.     return FLASHIF_PROTECTION_NONE;
  15. }

Funkcja ustawiająca blokadę odczytu pamięci flash:

  1. uint32_t FLASH_If_WriteProtectionConfig(uint32_t protectionstate)
  2. {
  3.     FLASH_OBProgramInitTypeDef OBInit;
  4.     HAL_StatusTypeDef status = HAL_OK;
  5.  
  6.     HAL_FLASH_OB_Unlock();
  7.    
  8.     OBInit.WRPState = (protectionstate == FLASHIF_WRP_ENABLE ? OB_WRPSTATE_ENABLE : OB_WRPSTATE_DISABLE);
  9.    
  10.     /* Configure sector write protection */
  11.     OBInit.OptionType = OPTIONBYTE_WRP;
  12.     OBInit.Banks = FLASH_BANK_1;
  13.     OBInit.WRPSector = USER_WRP_SECTORS;
  14.  
  15.     Flash_UnlockClearFlags();
  16.     HAL_FLASHEx_OBProgram(&OBInit);
  17.     status = HAL_FLASH_OB_Launch();
  18.  
  19.     HAL_FLASH_OB_Lock();
  20.     HAL_FLASH_Lock();
  21.  
  22.     if(status != HAL_OK) { return FLASHIF_PROTECTION_ERRROR; }
  23.     else { return FLASHIF_OK; }
  24. }

Przesyłanie i odbiór danych:


Dane do układu przesyłane są za pomocą protokołu YMODEM. Każdy blok danych zawiera 1024 bajty, na sumę kontrolną składają się 4 bajty.

Poniżej przedstawię jedynie krótki opis funkcji udostępnionych przez ST, ponieważ tutaj nie trzeba było wykonywać dużej ilości zmian.

Do testów wykorzystywałem program TeraTerm. Wymagało to wprowadzenia zmiany w funkcji ReceivePacket. Tak aby z ramki usunąć dodatkowy bajt SOH. Jest to jedyna zmiana jaka musiała zostać wprowadzona do części przesyłania nowego programu.

Wysłanie pierwszej części danych przechodzi poprawnie:


Problemy zaczynają się po otrzymaniu kolejnego pakietu:


Błąd ten jest specyficzny dla programu TeraTerm. Najprościej się go pozbyć poprzez wykrycie dodatkowego bajtu i jego usunięcie z ramki. 

  1. static uint8_t CheckIfExtraSOH(HAL_StatusTypeDef opStatus, uint8_t recVal, uint8_t bufData)
  2. {
  3.     if((opStatus == HAL_OK) &&
  4.        (recVal == SOH) &&
  5.        (bufData == SOH))
  6.     {
  7.         return 1;
  8.     }
  9.     return 0;
  10. }

W głównej funkcji odczytującej odebrane dane wygląda to następująco:

  1. static HAL_StatusTypeDef ReceivePacket(uint8_t *p_data, uint32_t *p_length, uint32_t timeout)
  2. {
  3.     uint32_t crc;
  4.     uint32_t packet_size = 0;
  5.     HAL_StatusTypeDef status;
  6.     uint8_t char1;
  7.  
  8.     *p_length = 0;
  9.     status = HAL_UART_Receive(&huart2, &char1, 1, timeout);
  10.  
  11.     if(CheckIfExtraSOH(status, char1, aPacketData[0]))
  12.     {
  13.         status = HAL_UART_Receive(&huart2, &char1, 1, timeout);
  14.     }
  15.  
  16.     if(status == HAL_TIMEOUT) {
  17.         *p_length = packet_size;
  18.         return status;
  19.     }
  20.  
  21.     if (status == HAL_OK)
  22.     {
  23.         switch (char1)
  24.         {
  25. //...
  26. //...
  27. //...
  28. }

Bez dołożenia takiej funkcji nie było możliwości poprawnego przesłania danych do urządzenia.


Przesyłanie i odbieranie danych obsługiwane jest przez dwie funkcje:

  1. COM_StatusTypeDef Ymodem_Receive(uint32_t *p_size);
  2. COM_StatusTypeDef Ymodem_Transmit(uint8_t *p_buf, const uint8_t *p_file_name, uint32_t file_size);

Do urządzenia przesyłany jest plik binarny wygenerowany podczas kompilacji. 

Opis projektu:


Po uruchomieniu programu następuje sprawdzenie czy przycisk został wciśnięty. Jeśli tak to następuje wykonywanie procedury IAP, przez którą można zaprogramować mikrokontroler, odczytać program, uruchomić program lub ustawić blokadę odczytu.

Przy generowaniu drugiego projektu jaki ma być wykorzystywany do zapisania na urządzeniu z aplikacją IAP należy pamiętać o odpowiednich modyfikacjach lokalizacji pamięci Flash. Tak aby oba programy były wgrywane w osobne miejsca w pamięci. W innym przypadku nastąpi nadpisanie części IAP co uniemożliwi poprawne wgrywanie programu do systemu.

Wywołanie projektu następuje przez wciśnięcie przycisku na płytce discovery, który jest podłączony do linii PA0:

  1. static void JumpToProgram(void)
  2. {
  3.     if (((*(__IO uint32_t*)APLICATION_ADDRESS) & 0x2FFD0000 ) == 0x20000000)
  4.     {
  5.         JumpAddress = *(__IO uint32_t*) (APLICATION_ADDRESS + 4);
  6.         JumpToApplication = (pFunction) JumpAddress;
  7.         __set_MSP(*(__IO uint32_t*) APLICATION_ADDRESS);
  8.         JumpToApplication();
  9.     }
  10. }
  11.  
  12. if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
  13.     FLASH_If_Init();
  14.     Main_Menu();
  15. } else {
  16.     JumpToProgram();
  17. }

Jeśli przycisk wciśnięty to następuje wejście do funkcji Main_Menu(), która odpowiada za możliwości wgrywania nowego programu. W innym przypadku następuje wejście do projektu użytkownika.

Definicja w pliku Flash obszarów pamięci dla projektu zawierającego obsługę IAP jest następująca:

  1. MEMORY
  2. {
  3.     RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
  4.     CCMRAM (rw)      : ORIGIN = 0x10000000, LENGTH = 64K
  5.     FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 1024K
  6. }

Natomiast dla programu wykonywalnego nie obsługującego IAP jest to już:

  1. MEMORY
  2. {
  3.     RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
  4.     CCMRAM (rw)      : ORIGIN = 0x10000000, LENGTH = 64K
  5.     FLASH (rx)      : ORIGIN = 0x8008000, LENGTH = 1024K
  6. }

Jak już wspomniałem wcześniej programy muszą być wgrywane w różne lokalizacje w pamięci tak aby zapewnić poprawne działanie w zależności od wybranego rozwiązania.

Program testowy mruga diodami i przesyła krótką informację po UART przy starcie:

  1.   /* Initialize all configured peripherals */
  2.   MX_GPIO_Init();
  3.   MX_USART2_UART_Init();
  4.   /* USER CODE BEGIN 2 */
  5.   Serial_PutString((uint8_t *)"Prog_spec\n\r");
  6.   /* USER CODE END 2 */
  7.  
  8.   /* Infinite loop */
  9.   /* USER CODE BEGIN WHILE */
  10.   while (1)
  11.   {
  12.     /* USER CODE END WHILE */
  13.  
  14.     /* USER CODE BEGIN 3 */
  15.       HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
  16.       HAL_Delay(100);
  17.       HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
  18.       HAL_Delay(100);
  19.       HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14);
  20.       HAL_Delay(100);
  21.       HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_15);
  22.       HAL_Delay(100);
  23.   }
  24.   /* USER CODE END 3 */
  25. }

Projekt można pobrać z dysku Google pod tym linkiem.

Bibliografia: