이글의 전부 또는 일부, 사진, 소스프로그램 등은 저작자의 동의 없이는 상업적인 사용을 금지합니다. 또한, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.



인터넷 여러 곳을 찾아 보았지만 STM32F103 DFU Bootloader 만들기에 관해 일목 요연하게 정리된 것을 찾기가 어려웠습니다. 이리저리 해보다가 완벽하게 동작하는 STM32F103용 DFU Bootloader를 만들게 되었습니다.


본 글에서는 DFU Bootloader를 만들어 보고, 다음 글에서는 지난 번에 만들었던 RA8835 GLCD 제어하기(제6편)에서 만들었던 프로젝트를 부트로더를 이용해서 STM32F103에 기록해 보도록 하겠습니다.




1. DFU용 Bootloader 프로젝트 만들기



MCU로 STM32F103CBT6을 선택하여 프로젝트를 만듭니다. 필자가 사용하고 있는 Blue Pill의 MCU는 STM32F103C8T6이지만, 이 MCU는 실제로는 128KB의 플래시메모리를 가지고 있습니다. STM32F103CBT6와 STM32FC8T6은 플래시메모리 용량외에는 모두 동일하므로, 필자는 Blue Pill의 MCU가 STM32F103CBT6인 것처럼 사용하겠습니다.

 

STM32F103시리즈에도 처음부터 부트로더가 내장되어 있습니다. BOOT1 핀을 low(0)로 하고 BOOT0 핀을 high(1)로 하면 내장된 부트로더가 작동합니다. 다만, STM32F103시리즈에 내장된 부트로더는 USART만 지원하여, USB로는 펌웨어 업데이트 등을 할 수가 없습니다. 내장된 부트로더로 펌웨어 업데이트를 하려면 ST-LINK/V2나 USB-TTL을 같이 가지고 다녀야 합니다. USB로 펌웨어 업데이트가 가능하다면 USB 포트가 있는 Blue Pill 같은 경우 USB 케이블만 있으면 됩니다.

 

Blue Pill의 USB 포트로 펌웨어 업데이트가 가능하도록 부트로더를 만들어 Blue Pill의 STM32F103C8T6에 기록해 보겠습니다.

 

DFU 부트로더 동작의 전체적인 흐름은 다음과 같습니다.

1) 부트로더를 플래시메모리 시작 지점(0x0800 0000)에 올려 놓는다.

2) 부트로더 이후의 플래시메모리에 펌웨어 프로그램을 올려 놓는다.

    (ex: 0x0800 3000, 본 글에서는 0x0801 0000)

3) 부트시 펌웨어 업데이트 조건을 충족하는지 판단한다.

   (ex: 펌웨어 업데이트 핀의 조건, 정상적인 펌웨어가 있는지 등)

4) 3)에 따라 펌웨어 업데이트를 하든지, 펌웨어 프로그램을 실행하든지 한다.

 

 

 

먼저 부트로더 프로그램을 작성하겠습니다.

 

① STM32CubeMx에서 MCU를 STM32F103CBT6로 선택하고 다음과 같이 pinout을 설정합니다.


작업한 내용은 다음과 같습니다.

1) [SYS] 항목에서 [Debug]를 [Serial Wire]로 선택

2) [RCC] 항목에서 [HSE]를 [Crystal/Ceramic Resonator]로 지정

3) [USB] 항목에서 Device(FS) 앞에 체크

4) 위에 있는 [USB_DEVICE] 항목에서 [Class For FS IP]를 [Download Firmware Update]로 선택

5) GPIO PC14 핀을 GPIO_Input으로 지정하고 라벨을 Firmup_Pin으로 붙임.

6) [Clock Configuration] 탭을 클릭



② ①의 6) 단계를 실행하면 다음과 같은 팝업 창이 나옵니다. USB를 FS(Full Speed)로 사용하려면 USB Clock이 48MHz이어야 하는데 현재 그렇게 설정되지 않아서 나오는 메시지입니다. [Yes] 버튼을 누르면 STM32CubeMx가 클럭을 설정해 줍니다.




③ 다음 그림과 같이 USB clock이 48MHz로 설정된 것을 확인하고, Configuration 탭을 누릅니다.




④ [Middlewares]에 있는 [USB Device] 버튼을 누릅니다.




⑤ [USBD_DFU_APP_DEFAULT_ADD (Base Address 0x)] 항목에 나중에 로드될 실행프로그램이 위치할 메모리상의 주소를 지정합니다. 실제로 DFU Bootloader가 12KB가 조금 안되기 때문에 이 주소를 0x08003000 번지로 지정하면 됩니다. 다만 필자는 STM32F103C8T6의 플래시메모리 64KB 이후의 영역에서 동작하는 것을 확인하기 위해서 주소를 0x08010000으로 지정했습니다. 이 값은 프로젝트를 생성하고 난 다음, 헤더파일 usbd_conf.h 안에 매크로 USBD_DFU_APP_DEFAULT_ADD로 정의되어 있습니다.



USBD_DFU_MEDIA Interface 항목에는 위의 그림에서와 같이 @Internal Flash   /0x08000000/12*001Ka,52*001Kg,64*001Kg를 입력합니다. 필자도 아래에 영문으로 설명된 내용을 보고 파악했습니다. 필자가 파악한 내용은 다음과 같습니다.

@표시 뒤에 메모리 설명자를 입력(위의 경우 Internal Flash)

영역을 구분하기 위해 / 입력

0x로 시작하는 최대 여덟 자리까지의 메모리 시작 주소를 입력

영역을 구분하기 위해 / 입력

두 자리 이내로 섹터 수를 기록(위의 경우 12)

섹터 수와 섹터 크기를 구분하는 구분자로 *를 입력

최대 3자리로 섹터 크기를 입력(위의 경우 001)

크기의 단위를 바이트면 B, 킬로면 K, 메가면 M 입력(위의 경우 K)

섹터 속성을 1자로 입력.(읽기 가능 a, 삭제 가능 b, 읽기 및 삭제 가능 c, 쓰기 가능 d, 읽기 및 쓰기 가능 e, 삭제 및 기록 가능 f, 읽기 및 삭제 기록 가능 g)(위의 경우 a)


STM32F130CB의 플래시메모리는 1KB 크기의 128개 섹터로 구성되어 있습니다. 이중에서 앞의 12KB는 부트로더가 사용하므로 읽기만 가능하도록 지장하고(12*001Ka), 나머지 영역은 읽기, 삭제, 쓰기 모두 가능하다고 입력했습니다(52*001Kg,64*001Kg). 섹터 수는 두 자리 이내이어야 하므로 116*001Kg로 하지 않고 52*001Kg와 64*001Kg로 분리하였습니다.   



⑦ [Device Descriptor] 탭을 클릭하여 VID와 PID를 확인합니다. VID는 1155(0x483), PID는 57105(0xDF11)임을 확인하고 [OK] 버튼을 누릅니다. 이 두 숫자는 나중에 사용합니다.




⑧ [Project] 메뉴에서 [Settings]를 선택하여 프로젝트명을 정하고 [True Studio]로 지정합니다. 이후에 [Generate Code]와 [Generate Report]를 실행합니다.





2. Bootloader 코딩하기



1) main() 함수 코딩하기


main() 함수에 다음과 같이 두 곳에 코드를 입력합니다.


  /* USER CODE BEGIN 1 */
  typedef  void (*pFunction)(void);
  uint32_t JumpAddress;
  void (*Jump_To_Application)();
  /* USER CODE END 1 */


/* USER CODE BEGIN 2 */
  if (HAL_GPIO_ReadPin(Firmup_Pin_GPIO_Port, Firmup_Pin_Pin) != GPIO_PIN_RESET)
  {
    if (((*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD - 1) & 0x2FFFB000 ) == 0x20000000)
    {
      JumpAddress = *(__IO uint32_t*) (USBD_DFU_APP_DEFAULT_ADD + 4);
      Jump_To_Application = (pFunction) JumpAddress;
      __set_MSP(*(__IO uint32_t*) USBD_DFU_APP_DEFAULT_ADD);
      Jump_To_Application();
    }
  }
  MX_USB_DEVICE_Init();
  /* USER CODE END 2 */


특히 위 코드 중에서
MX_USB_DEVICE_Init();
은 원래 STM32FCubeMx가 만든 코드로서
/* USER CODE BEGIN 2 */
바로 위에 있던 것을 이 위치로 옮겨 놓았습니다.


이 코드를 원래의 위치에 그대로 두면 나중에 부트로더가 올려 놓은 펌웨어 프로그램이 제대로 실행되지 않을 수 있습니다. STM32CubeMx에서 [Generate Code]를 할 때마다 이 코드가 /* USER CODE BEGIN 2 */ 위에 다시 생성됩니다. 따라서 [Generate Code] 작업을 한 후에는 잊지말고 이 코드를 옮겨 놓아야 합니다.

<2021.01.10.> 아래 댓글의 @manhee님께서 알려주신 바에 따라 내용을 추가합니다.

STM32CubeMx의 [Project Manager]에서 [Advanced Settings]를 선택한 후에 [Function Name]의 MX_USB_Device_init의 [Do not Generate Function Call]에 체크를 하면 STM32CubeMx가 코드를 생성할 때에 MX_USB_DEVICE_Init() 함수를 호출하는 부분을 만들지 않습니다.




이렇게 하면 바로 위에서 언급한 MX_USB_DEVICE_Init() 함수 옮기기는 하지 않아도 됩니다. 다만, [Generate Code]를 할때에 main.c 프로그램에 #include "usb_device.h"를 넣어 주지않기 때문에 /*USER Code Includes*/ 아래에  #include "usb_device.h"를 넣어 주어야 합니다.


또한 STM32CubeMx의 버전도 바뀌었고, 사용하는 라이브러리들도 버전이 바뀌면서 컴파일된 코드의 크기도 늘어났습니다. 새로이 [Generate Code]를 하고 나서 부트로더를 컴파일하면 12KB가 넘습니다. 이에 따라 위에서 다룬 몇 항목의 값을 바꾸어 주어야 합니다.

USBD_DFU_APP_DEFAULT_ADD의 값으로 지정했던 0x08003000의 값을 적절한 새로운 값으로 바꾸어야 하고, USBD_DFU_MEDIA Interface 항목에 입력했던 값 @Internal Flash   /0x08000000/12*001Ka,52*001Kg,64*001Kg의 내용도 바꾸어 주어야할 것 같습니다.





Bootloader 코드에 의하면 펌웨어 업데이트 모드로 들어가지 않고 기존의 코드를 실행하려면 다음의 두 조건을 충족시켜야 합니다.


첫째는 프로젝트 설정할 때에 정했던 Firmup_Pin(PC14)의 상태가 low(0)가 아니어야 합니다.
if (HAL_GPIO_ReadPin(Firmup_Pin_GPIO_Port, Firmup_Pin_Pin) != GPIO_PIN_RESET)


둘째는 플래시메모리 영역을 살펴보고, 정상적으로 프로그램이 기록되어 있지 않으면 펌웨어 업데이트 모드로 들어갑니다.
if (((*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD - 1) & 0x2FFFB000 ) == 0x20000000)

USBD_DFU_APP_DEFAULT_ADD는 위의 ⑤단계에서 펌웨어 프로그램이 실행되는 주소값을 갖도록 지정한 매크로입니다. 실행프로그램이 로드되는 첫 4바이트는 Stack Pointer가 있습니다. STM32F103Cx시리즈는 RAM 크기가 20KB이기 때문에 RAM의 주소는 0x2000000부터 0x20004FFF입니다. 정상적이라면 stack pointer는 이 사이의 값을 가져야 하며, 프로그램이 실행되기 전의 스택포인터는 RAM의 마지막 주소 + 1의 값인 0x20005000의 값을 갖습니다. 즉 정상적인 펌웨어가 기록되어 있다면 (*USBD_DFU_APP_DEFAULT_ADD - 1)의 값은 0x20004FFF입니다. 이값을 0x2FFFB000과 AND 연산을 하면 0x20000000의 값을 가지게 됩니다. 최초의 stack pointer 값을 이용하여 실행프로그램이 펌웨어에 있는지 여부를 판단한 후에, 정상적인 펌웨어가 있으면 펌웨어 프로그램을 실행하고 그렇지 않으면 펌웨어 업데이트 모드로 진입하도록 합니다.


위의 두 조건을 모두 충족시키면 Jump_To_Application(); 명령에 의해 이미 기록되어 있는 펌웨어 프로그램이 실행됩니다. 두 조건 중 하나라도 충족시키지 못하면 MX_USB_DEVICE_Init(); 명령을 실행한 후에 펌웨어 업데이트 모드로 진입합니다.





2. udbd_dfu_it.c에 있는 함수 코딩하기



다음과 같이 usbd_dfu_it.c에 있는 함수 다섯 개를 수정합니다.


1) MEM_If_Init_FS() 함수

uint16_t MEM_If_Init_FS(void)
{
  /* USER CODE BEGIN 0 */
  HAL_FLASH_Unlock();
  __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR);
  return (USBD_OK);
  /* USER CODE END 0 */
}

Bootloader 프로그램이 플래시메모리에 기록하기 위해서 플래시메모리 lock을 해제합니다.



2) MEM_If_DeInit_FS() 함수

uint16_t MEM_If_DeInit_FS(void)
{
  /* USER CODE BEGIN 1 */
  HAL_FLASH_Lock();
  return (USBD_OK);
  /* USER CODE END 1 */
}

Bootloader 프로그램이 플래시 메모리에 다 기록한 후에 플래시메모리를 lock합니다.



3) MEM_If_Erase_FS() 함수

uint16_t MEM_If_Erase_FS(uint32_t Add)
{
  /* USER CODE BEGIN 2 */
  FLASH_EraseInitTypeDef pEraseInit;
  uint32_t SectorError;
  pEraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
  pEraseInit.NbPages = 1;
  pEraseInit.PageAddress = Add;
  pEraseInit.Banks = FLASH_BANK_1;
  if(HAL_FLASHEx_Erase(&pEraseInit,&SectorError)!=HAL_OK)
  {
    return (USBD_FAIL);
  }
  return (USBD_OK);
  /* USER CODE END 2 */
}

Bootloader 프로그램이 플래시메모리를 지웁니다. STM32F103의 경우 한 번 호출에 1KB씩 지웁니다.



4) MEM_If_Write_FS() 함수

uint16_t MEM_If_Write_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* USER CODE BEGIN 3 */
  uint32_t i = 0;
  for(i = 0;i < Len;i += 4)
  {
    if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
       (uint32_t)(dest + i),*(uint32_t *)(src + i)) == HAL_OK)
    {
      if(*(uint32_t *)(src + i) != *(uint32_t *)(dest + i))
    	  return 2;
    }
    else
      return 1;
  }
  return (USBD_OK);
  /* USER CODE END 3 */
}

Bootloader 프로그램이 메모리의 src 번지 내용을 플래시메모리의 dest 번지에 Len 바이트만큼 기록합니다.



5) MEM_If_Read_FS() 함수

uint8_t *MEM_If_Read_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* Return a valid address to avoid HardFault */
  /* USER CODE BEGIN 4 */
  uint32_t i = 0;
  uint8_t *psrc = src;
  for(i = 0;i < Len;i++) dest[i] = *psrc++;
  return (uint8_t*)(USBD_OK);
  /* USER CODE END 4 */
}

Bootloader 프로그램이 메모리 src 번지로부터 dest 번지로 Len 바이트만큼 복사합니다.


<2021.01.10.> 아래 댓글의 @manhee님께서 지적하신 내용에 따라 수정합니다.

MEM_If_read_FS() 함수에 cubeMX가 주석문으로 유효한 주소를 리턴하라고 지적하고 있습니다. 이 주석문을 제대로 보지 않고 cubeMX가 기본적으로 제시한 USBD_OK 값을 포인터로 형변환해서 리턴하는 오류를 범했습니다. 위 함수는 dest를 리턴하도록 아래와 같이 수정해야 합니다.

uint8_t *MEM_If_Read_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* Return a valid address to avoid HardFault */
  /* USER CODE BEGIN 4 */
  uint32_t i = 0;
  uint8_t *psrc = src;
  for(i = 0;i < Len;i++) dest[i] = *psrc++;
  return dest;
  /* USER CODE END 4 */
}




이 프로젝트를 컴파일하여 STM32F103C8T6에 기록하고 PC와 USB 포트로 연결하면 장치 관리자에 다음과 같이 STM32 DownLoad Firmware Update로 나타납니다.



 

아직 드라이버가 설치되지 않아 위와 같이 나타납니다. 드라이버는 다음 편 글에서 프로그램을 설치하면 같이 설치됩니다.

 

드라이버가 정상적으로 설치되면 다음과 같이 STM Device in DFU Mode로 나타납니다.

 


소스 프로그램을 압축하여 첨부합니다.

STM32F103DFUBootloader.zip

<2021.01.10. 위 소스프로그램은 cubeIDE에서 수정하여 컴파일했습니다.>

stm32CubeMx가 버전업되면서 실행파일의 크기가 커졌습니다. 새로운 버전의 stm32CubeMx로 [GenerateCode]를 하면 실행 파일의 크기가 커지므로 위에서 언급했던 몇 항목의 내용을 수정해야 합니다. [USBD_DFU_APP_DEFAULT_ADD (Base Address 0x)]에 입력한 주소값 0x08003000, USBD_DFU_MEDIA Interface 값으로 입력한 12*001Ka,52*001Kg 등의 값을 새로 정해야 합니다. 현재 올린 압축 파일에서는 [GenerateCode]를 실행하지 않고 컴파일했습니다.





다음 글에서는 RA8835 GLCD 제어하기(제6편)에서 만들었던 프로젝트를 부트로더를 이용해서 STM32F103에 기록해 보도록 하겠습니다.




블로그 이미지

엠쿠스

Microprocessor(STM32, AVR)로 무엇인가를 만들어 보고자 학습 중입니다.

,