본문 바로가기
임베디드 개발/STM32 (ARM Cortex-M)

STM32 ] 커스텀 부트로더 (IAP)

by eteo 2023. 2. 15.

 

부트로더 Code Placement

 

이전 글에서 언급했지만 ST사에서 만든 내장 부트로더는 System memory에 있고 그걸 지울 수 는 없다.

 

이글에서는 예제에서 제공하는 커스텀 부트로더(IAP)를 사용해볼건데 IAP는 Sector 0 ~ Sector 1을 사용하고, Sector 2 ~ Sector 11 에는 User Application을 위치시킬 것이다.

즉, 커스텀 부트로더의 Base Address는 0x0800 0000 이 되고, User Application의 base address 는 0x0800 8000이 된다.

 

 

 

 

 

 

보통 부트로더에서 지원하는 커맨드들

 

부트로더에 구현할 것들은 예를들면 다음과 같을 수 있다.

 

  • 부트로더에서 사용가능한 커맨드 확인
  • 부트로더의 버전 확인
  • MCU 칩의 ID 확인
  • 메모리 덤프
  • 플래시의 Read/Write protection status확인/disable/enable
  • 플래시 ERASE
  • 플래시 WRITE
  • 특정 주소로 점프

 

 

 

 

 

부트로더→펌웨어 실행 flow

 

flow chart

 

 

MCU가 리셋되면 처음엔 부트로더가 실행되고 특정 조건이 만족되지 않으면 바로 펌웨어로 점프하고 특정조건이 만족되면, 호스트(PC)와 커맨드, 응답을 주고받기 위해 필요한 UART 세팅 등을 한 후에 커맨드를 대기한다.

 

여기서 특정조건이란,

 

1. 특정 버튼을 누른 상태로 리셋을 하는 경우

2. 리셋으로 지워지지 않는 메모리에 플래그를 두고, 해당 플래그가 켜진 상태로 리셋된 경우 (역으로 펌웨어에서 Soft Reset을 통해 부트로더를 실행할 수 있다.)

 

등을 들 수 있다.

 

이 아래 예제에서도 리셋시 유저버튼이 눌린 경우에만 부트로더로 작동할 것이다.

 

또한 부트로더에서 펌웨어의 시작 주소로 점프하기 전에 펌웨어가 정상인지 CRC 체크를 하고, 부트로더에서 사용한 장치를 disable 해줄 필요가 있다. CRC는 보통 PC프로그램에서 바이너리 파일을 보낼 때 계산해서 특정공간에 write한다.

 

 

출처 : 유튜버 Baram, https://www.youtube.com/watch?v=kdiXoxmikzY

 

 

 

 

 

부트로더 - 호스트(PC)간 Communication

 

먼저 부트로더를 통해 바이너리 파일을 플래시에 다운로드 하기 위해선 부트로더와 PC간 커맨드와 응답을 주고받아야 한다. 인터페이스는 Virtual COM port 와 연결된 USART3를 통해 할 것이고, 주고받는 프로토콜은 자유롭게 정할 수 있다.

 

 

예를 들면 다음과 같다.

시퀀스 다이어그램 예시

 

 

ST사의 IAP 예제에서는 단순히 메뉴의 번호를 터미널에 입력하면 원하는 커맨드를 실행할 수있고, 바이너리 데이터 전송은 YMODEM 프로토콜을 사용한다.

 

 

 

 

 

 

User Application

 

 

 

아래 경로에 IAP 예제가 있다.

C:\Users\user_name\STM32Cube\Repository\STM32Cube_FW_F4_V1.27.0\Projects\STM324x9I_EVAL\Applications\IAP

 

IAP_binary_template도 제공이 되는데 굳이 쓸필요 없고 직접 만든다.

 

 

유저버튼을 클릭하면 LED가 토글되고 1초간격으로 터미널에 User Application이 실행중임을 출력한다.

 

  /* USER CODE BEGIN 2 */
  uint32_t before_tick;
  char somedata[] = "User Application is now running\r\n";
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  if(HAL_GetTick()-before_tick >= 1000)
	  {
		  before_tick = HAL_GetTick();
		  HAL_UART_Transmit(&huart3, (uint8_t*)somedata, sizeof(somedata), HAL_MAX_DELAY);
	  }
    /* USER CODE END WHILE */
    
// ...

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	static uint32_t before_tick = 0;

	if(GPIO_Pin == GPIO_PIN_13)
	{
		if(HAL_GetTick() - before_tick >= 200)
		{
			before_tick = HAL_GetTick();
			HAL_GPIO_TogglePin(LD1_GPIO_Port, LD1_Pin);
			HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
			HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
		}
	}
}
/* USER CODE END 4 */

 

 

 

 

User Flash Base Address 수정하기

 

링커스크립트 파일(STM32F429ZITX_FLASH.ld)에 가서 플래시의 시작 주소 0x8000000이 아니라 0x8008000으로 수정한다. 앞의 32K는 부트로더 영역으로 썼으니 LENGTH도 2016K로 수정한다.

 

 

 

 

 

 

 

Vector Table Offset 수정하기

 

커스텀 부트로더가 올라간 경우 아래 그림과 같이 벡터 테이블이 2개가 된다. MCU 리셋시 먼저 부트로더가 실행되고 유저버튼이 눌린 상태라면 부트로더가 동작할 것이고 유저버튼이 눌리지 않은 상태라면 바로 User Application의 Reset Handler를 호출하여 컨트롤을 넘길 것이다.

 

이후 User Application이 실행되고 인터럽트가 트리거되면 어떻게 될까? ARM Cortex 프로세서의 default Vector Table 위치는 0x0000 0000이고 ST사에서 0x0800 0000 alias 해두었는데 거기는 부트로더의 벡터 테이블이 있다. 그러니 User Application의 벡터 테이블 위치가 디폴트 위치가 아니고 다른 위치로 바뀌었다는 것을 프로세서에게 알려주어야한다. 이는 VTOR(Vector Table Relocation Register)를 통해 설정할 수 있다.

 

 

 

 

 

 

 

스타트업파일(startup_stm32f429zitx.s)로 가면 시스템에 전원이 인가되었을 때 처음 실행되는 리셋 핸들러의 동작이 나와있는데 그 중 main함수를 호출하기 전에 SystemInit 함수로 분기하는 문장이 있다.

 

 

 

 

 

 

그 SystemInit 함수를 따라가보면 Vector Table location을 변경시 설정해주어야하는 부분이 있는데 평소에는 주석처리되어있다.

 

 

 

 

 

 

주석을 해제하고 Vector Table Offset를 0x00008000U로 수정해준다.

 

 

수정 후 빌드하면 .map 파일에도 반영 된다.

 

 

 

 

.bin 파일 만들고 flash 0x0800 8000에 다운로드 하기

 

 

프로젝트 우클릭 - Properties - C/C++ Build - Setting - Tool Settings -MCU Post build outputs

 

Convert to binary file 체크하고 빌드한다.

 

프로젝트 폴더에 찾아가면 .bin 파일이 생성되었다.

 

 

 

 

 

STM32CubeProgrammer 실행

 

Connect 후 Full chip erase

 

 

 

 

0x08008000 위치에 다운로드하기

 

 

Disconnect

 

 

 

 

 

 

 

부트로더

 

 

예제에서 common.h, flash_if.h, ymodem.h, menu.h, common.c, flash_if.c, ymodem.c, menu.c 를 가져오고 아래와 같이 작성한다.

 

/* USER CODE BEGIN Includes */
#include "menu.h"
/* USER CODE END Includes */

// ...

/* USER CODE BEGIN PV */
extern pFunction JumpToApplication;
extern uint32_t JumpAddress;
/* USER CODE END PV */

int main(void) {
	HAL_Init();
	SystemClock_Config();

	MX_GPIO_Init();
	MX_USART3_UART_Init();
	MX_RTC_Init();
    
	/* USER CODE BEGIN 2 */
	/* Test if Key push-button is pressed */
	if (HAL_GPIO_ReadPin(USER_Btn_GPIO_Port, USER_Btn_Pin) == GPIO_PIN_SET) {
		/* Display main menu */
		Main_Menu();
	}
	/* Keep the user application running */
	else {
		volatile uint32_t msp = (*(__IO uint32_t*)APPLICATION_ADDRESS);
		/* Test if user code is programmed starting from address "APPLICATION_ADDRESS" */
		if (msp >= 0x20000000 && msp <= 0x20030000) {
			/* Jump to user application */
			JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
			JumpToApplication = (pFunction) JumpAddress;
			/* Initialize user application's Stack Pointer */
			__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
			JumpToApplication();
		}
	}
	/* USER CODE END 2 */

	while (1) 
    { }

}

 

 

 

User Application으로 Jump

 

User App으로 점프하는 부분을 살펴보자

 

/* Test if user code is programmed starting from address "APPLICATION_ADDRESS" */
if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
{
	// ...
}

 

위 내용은 APPLICATION_ADDRESS 즉, 0x08000000의 첫 4byte에 저장된 Main Stack Pointer 값을 읽고 유효한 범위에 있는지 확인한다.

 

근데 링커스크립트 파일을 보면 알겠지만, STM32F429ZI 보드의 RAM(SRAM1+SRAM2+SRAM3)은 CCM(Core Coupled Memory)를 제외하고 192Kbyte이고 MSP는 램의 끝인 0x20030000 이다.

 

위 코드는 MSP가 SRAM1 영역(112Kbyte) 0x20000000~0x2001BFFF에 있다고 가정하는 코드라서 맞지 않다. 따라서 & 연산을 0x2FFC0000 랑 하든지 범위를 확인하는 코드로 바꾸어야 한다.

 

/* Highest address of the user mode stack */
_estack = 0x20030000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;;      /* required amount of heap  */
_Min_Stack_Size = 0x400;; /* required amount of stack */

/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 2048K
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 192K
CCMRAM (rw)      : ORIGIN = 0x10000000, LENGTH = 64K
}

 

 

 

그 다음 4byte에 저장된 리셋핸들러의 주소를 펑션포인터에 대입하고 메인 스택포인터를 0x08008000 주소로 설정한 뒤 User Application의 리셋핸들러를 호출하여 실행한다.

 

JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
JumpToApplication = (pFunction) JumpAddress;
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
JumpToApplication();

 

 

 

 

 

 

부트로더 메뉴

 

 

 

리셋시 유저버튼을 누르고 있으면 진입하는 부트로더의 메뉴는 다음과 같다.

 

 

 

 

 

 

 

 

 

 

 

 

테라텀에서 YMODEM을 통해 .bin 파일을 전송하여 프로그램 할 수 있고, 파일을 수신할 수도 있다. 파일을 수신하려면 먼저 File - Change directory로 경로설정을 해주어야 한다.