官方参考例程AN4657。
https://www.st.com/en/embedded-software/x-cube-iap-usart.html
只有STM32F10C这个例程。
我们需要的STM32F407的例程,在cubemx中找到接近的。
STM32Cube_FW_F4_V1.26.2\Projects\STM324xG_EVAL\Applications\IAP\IAP_Main
++++++++++++++++++++++++++++++++++++++++++
在main.h中,声明了两个外部定义的全局变量。
/* Exported variables --------------------------------------------------------*/
extern UART_HandleTypeDef UartHandle;
在其他C文件中,如果包含了main.h,那么就可以使用外部全局变量了。
首先来看main.c文件。
UART_HandleTypeDef UartHandle;
这里可以看出,我们需要在cubemx中,开启一个USART。
/* Private variables ---------------------------------------------------------*/
extern pFunction JumpToApplication;
extern uint32_t JumpAddress;
声明了外部定义的函数指针,以及一个外部定义的全局变量。
/* Private function prototypes -----------------------------------------------*/
static void IAP_Init(void);
void SystemClock_Config(void);
这里声明了IAP_Init静态函数,这个函数中,主要是初始化了USART和CRC模块。这几个函数,cubemx会自动生成代码。
这里声明了systemclock_config函数,这个函数中,主要是初始化RCC,这个函数,cubemx会自动生成代码。
来看看main的函数体。
/* Test if Key push-button on STM32L476G-EVAL Board is pressed */
if (BSP_PB_GetState(BUTTON_TAMPER) == GPIO_PIN_SET)
{
/* Initialise Flash */
FLASH_If_Init();
/* Execute the IAP driver in order to reprogram the Flash */
IAP_Init();
/* Display main menu */
Main_Menu ();
}
/* Keep the user application running */
else
{
/* Test if user code is programmed starting from address "APPLICATION_ADDRESS" */
if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
{
/* 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();
}
}
while (1)
{}
首先检测拨码开关的状态,如果是拨码开关置位,那么就执行IAP功能函数。
否则就执行hand-off。
在hand-off里面,最关键的宏定义,就是APPLICATION_ADDRESS。位于flash_if.h中。
这是我们需要移植的地方。
/* End of the Flash address */
#define USER_FLASH_END_ADDRESS 0x080FFFFF
/* Define the user application size */
#define USER_FLASH_SIZE (USER_FLASH_END_ADDRESS - APPLICATION_ADDRESS + 1)
/* Define the address from where user application will be loaded.
Note: the 1st sector 0x08000000-0x08003FFF is reserved for the IAP code */
#define APPLICATION_ADDRESS (uint32_t)0x08004000
我们重点看看IAP功能函数。
首先是flash_if_init函数。
void FLASH_If_Init(void)
{
/* Unlock the Program memory */
HAL_FLASH_Unlock();
/* Clear all FLASH flags */
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGSERR | FLASH_FLAG_WRPERR | FLASH_FLAG_OPTVERR);
/* Unlock the Program memory */
HAL_FLASH_Lock();
}
我们不需要修改这个函数。
之后是IAP_init函数。
之后是main_menu函数。
+++++++++++++++++++++++++++++++++++++++++
来看看main_menu函数。位于menu.c中。
这是我们需要移植的。
void Main_Menu(void)
{
uint8_t key = 0;
Serial_PutString((uint8_t *)"\r\n= By MCD Application Team =");
/* Test if any sector of Flash memory where user application will be loaded is write protected */
FlashProtection = FLASH_If_GetWriteProtectionStatus();
while (1)
{
Serial_PutString((uint8_t *)"\r\n=================== Main Menu ============================\r\n\n");
Serial_PutString((uint8_t *)" Download image to the internal Flash ----------------- 1\r\n\n");
Serial_PutString((uint8_t *)" Upload image from the internal Flash ----------------- 2\r\n\n");
Serial_PutString((uint8_t *)" Execute the loaded application ----------------------- 3\r\n\n");
if(FlashProtection != FLASHIF_PROTECTION_NONE)
{
Serial_PutString((uint8_t *)" Disable the write protection ------------------------- 4\r\n\n");
if((FlashProtection & (FLASHIF_PROTECTION_PCROPENABLED | FLASHIF_PROTECTION_RDPENABLED)) != 0x0)
{
Serial_PutString((uint8_t *)" The write protection disable will erase all the flash\r\n");
while(1);
}
}
else
{
Serial_PutString((uint8_t *)" Enable the write protection -------------------------- 4\r\n\n");
}
/* Clean the input path */
__HAL_UART_FLUSH_DRREGISTER(&UartHandle);
/* Receive key */
HAL_UART_Receive(&UartHandle, &key, 1, RX_TIMEOUT);
switch (key)
{
case '1' :
/* Download user application in the Flash */
SerialDownload();
break;
...
default:
Serial_PutString((uint8_t *)"Invalid Number ! ==> The number should be either 1, 2, 3 or 4\r");
break;
}
}
}
在函数中,首先是打印一系列显示信息,然后用UART接收一个输入ASCII值,并存放在buffer中,这里,这个buffer是临时变量key,然后,根据Key的ASCII值,执行对应的函数。
这里,如果key是’1’,则调用serialdownload函数。
++++++++++++++++++++++++++++++++++++++++
来看看serialdownload函数。位于menu.c函数。
void SerialDownload(void)
{
uint8_t number[11] = {0};
uint32_t size = 0;
COM_StatusTypeDef result;
result = Ymodem_Receive( &size );
if (result == COM_OK)
{
Serial_PutString(aFileName);
}
else if (result == COM_LIMIT)
{
Serial_PutString((uint8_t *)"\n\n\rThe image size is higher than the allowed space memory!\n\r");
}
}
这里调用了Ymodem_Receive函数。
++++++++++++++++++++++++++++++++++++++++++++++
来看看Ymodem_Receive函数。位于ymodem.c文件中。
COM_StatusTypeDef Ymodem_Receive ( uint32_t *p_size )
{
/* Initialize flashdestination variable */
flashdestination = APPLICATION_ADDRESS;
while ((session_done == 0) && (result == COM_OK))
{
{
switch (ReceivePacket(aPacketData, &packet_length, DOWNLOAD_TIMEOUT))
{
case HAL_OK:
switch (packet_length)
{
case 2:
Serial_PutByte(ACK);
break;
default:
if (*p_size > (USER_FLASH_SIZE + 1))
{
HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
}
Serial_PutByte(ACK);
}
}
else /* Data packet */
{
if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
{
Serial_PutByte(ACK);
}
}
}
break;
}
break;
}
}
}
return result;
}
这里面使用的主要的函数是,ReceivePacket,Serial_PutByte,HAL_UART_Transmit。
+++++++++++++++++++++++++++++++++++++++++++++++++
来看看Serial_PutByte函数。位于common.c文件中。
HAL_StatusTypeDef Serial_PutByte( uint8_t param )
{
/* May be timeouted... */
if ( UartHandle.State == HAL_UART_STATE_TIMEOUT )
{
UartHandle.State = HAL_UART_STATE_READY;
}
return HAL_UART_Transmit(&UartHandle, ¶m, 1, TX_TIMEOUT);
}
void Serial_PutString(uint8_t *p_string)
{
uint16_t length = 0;
while (p_string[length] != '\0')
{
length++;
}
HAL_UART_Transmit(&UartHandle, p_string, length, TX_TIMEOUT);
}
内部调用的函数是HAL_UART_Transmit。
+++++++++++++++++++++++++++++++++++++++++++++++++
来看看ReceivePacket函数。位于ymodem.c文件中。
static HAL_StatusTypeDef ReceivePacket(uint8_t *p_data, uint32_t *p_length, uint32_t timeout)
{
status = HAL_UART_Receive(&UartHandle, &char1, 1, timeout);
if (status == HAL_OK)
{
switch (char1)
{
case CA:
if ((HAL_UART_Receive(&UartHandle, &char1, 1, timeout) == HAL_OK) && (char1 == CA))
{
packet_size = 2;
}
break;
}
if (packet_size >= PACKET_SIZE )
{
status = HAL_UART_Receive(&UartHandle, &p_data[PACKET_NUMBER_INDEX], packet_size + PACKET_OVERHEAD_SIZE, timeout);
}
}
return status;
}
主要是调用了HAL_UART_Receive函数。
++++++++++++++++++++++++++++++++++++++++++++++
这里,我们可以看出来,所有的官方代码中,使用的句柄,都是UartHandle。
但是cubemx生成的代码中,不是使用这个句柄。所以,我们在移植时,需要将这几个句柄关联起来。
+++++++++++++++++++++++++++++++++++++++++++++
再来看看各个H文件。
main.h文件。
/* Exported variables --------------------------------------------------------*/
extern UART_HandleTypeDef UartHandle;
声明了外部全局变量。
flash_if.h文件。
/* Define the address from where user application will be loaded.
Note: this area is reserved for the IAP code */
#define APPLICATION_ADDRESS (uint32_t)0x08004000 /* Start user code address: ADDR_FLASH_PAGE_8 */
/* End of the Flash address */
#define USER_FLASH_END_ADDRESS 0x080FFFFF
/* Define the user application size */
#define USER_FLASH_SIZE (USER_FLASH_END_ADDRESS - APPLICATION_ADDRESS + 1)
定义了FLASH的地址相关的各个宏。
#include "main.h"
删除之前的包含的头文件,替换为main.h。
common.h文件。
无需移植。但是要删除之前的包含的头文件,替换为main.h。
ymodem.h文件。
无需移植。
+++++++++++++++++++++++++++++++++++++++++++++++++
实战。
首先在cubemx中,生成一个IAP_USART工程。
配置好SWD,
配置好RCC,
配置好USART1,
timebase使用systick。
这样,最基本的硬件环境就具备了。
然后,在cubemx生成的工程代码中,添加我们自己需要移植的代码。
新建一个文件夹。IAP。从官方代码中,复制我们需要的几个文件。
cubemx的安装目录下面,找到STM324xG_EVAL的例程。
D:\Program Files\STMicroelectronics\STM32Cube\Repository\STM32Cube_FW_F4_V1.26.2\Projects\STM324xG_EVAL\Applications\IAP\IAP_Main
拷贝出如下文件。
common.c,common.h,
flash_if.c,flash_if.h,
menu.c,menu.h,
ymodem.c,ymodem.h。
修改各个H文件,
删除之前的包含头文件,添加main.h。
然后,修改main.c。
添加IAP相关代码。
我们不使用button,所以,只能使用USART来完成通信。
我们需要借助HAL_Delay函数和USART的input来完成业务代码。
首先,设置一个key_val,用来获取用户输入。
/* USER CODE BEGIN PV */
uint8_t key_val;
/* USER CODE END PV */
使用HAL_UART_Receive来接收一个输入值。
/* Receive key */
HAL_UART_Receive(&UartHandle, &key_val, 1, 3000);
HAL_UART_Receive函数传入一个参数,timeout,单位是systick,也就是1ms。
如果超时,仍然没有收到数据,则函数返回值是HAL_TIMEOUT。(HAL_StatusTypeDef类型)
如果没有超时,并成功接收完成,则函数返回值是HAL_OK。(HAL_StatusTypeDef类型)
/* USER CODE BEGIN 2 */
Serial_PutString((uint8_t *)"\r\n===press u to update app=========");
key_val = 0;
HAL_UART_Receive(&huart1, &key_val, 1, 3000);
if (key_val == 'u')
{
/* Initialise Flash */
FLASH_If_Init();
/* Display main menu */
Main_Menu ();
}
/* Keep the user application running */
else
{
/* Test if user code is programmed starting from address "APPLICATION_ADDRESS" */
if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
{
/* 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 */
上电后,在3000ms内,轮询接收字符,要么接收到字符返回,要么超时返回,
如果接收到的字符是u,那么进入IAP功能代码。否则执行hand-off功能。
在PV中声明外部全局变量。
/* USER CODE BEGIN PV */
uint8_t key_val;
extern pFunction JumpToApplication;
extern uint32_t JumpAddress;
/* USER CODE END PV */
在main.h文件中,添加
#define UartHandle huart1
extern UART_HandleTypeDef UartHandle;
将huart1 赋予一个别名 UartHandle 。
至此,工程可以编译成功,但是体积较大。
为工程瘦身,修改menu.c文件。
删除不需要的Serial_PutString。
为了能够是IAP适用于各种不同的APP的RCC设置,还要修改systemclock_config。
对于使用HSE作为外部时钟源,并启动PLL的RCC配置,一般在cubemx中生成的代码如下所示:
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
可以看出,它分为两部分,
首先是配置PLL,即OSC,
然后是配置CLK,即SYSCLK,HCLK,PCLK1,PCLK2。
对于STM32,如果配置了PLL,那么再次配置PLL,是不可修改的。
如果想再次修改PLL,需要先让出PLL,然后配置PLL,最后再用PLL作为SYSCLK的时钟源。
也就是说,最关键的操作,就是让出PLL,不使用HSE,而是使用HSI。
我们需要在IAP中添加功能函数,完成这个工作,在退出IAP,执行hand-off的时候,让出PLL。
在common.c中,添加如下功能函数。
void SystemClock_Config_Use_HSI(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
{
Error_Handler();
}
}
然后,在main中,修改hand-off。
在main_menu中,修改hand-off。
添加SystemClock_Config_Use_HSI函数的调用。
++++++++++++++++++++++++++++++++++++++++++++++++
再修改APP工程。
首先用cubemx生成一个工程,按照常规的步骤编写代码,编译成功。
然后,修改工程的相关设置选项。
1)将常规模式下编译成功的APP工程,复制一份,另取一个名字,建议添加一个后缀,标明offset地址。
例如,将led修改为led_offset16K。
2)打开keil工程,修改工程选项,option->target->IROM1,
start修改为0x08004000,size修改为0x000FC000,
3)添加after build run,option->user->after build->run#1,
D:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o .\led.bin .\led\led.axf
4)修改main函数,修改VTOR。
在main函数的首句,添加
/* USER CODE BEGIN 1 */
...
SCB->VTOR = FLASH_BASE | 0x4000;
/* USER CODE END 1 */
5)安装secureCRT,在安装文件夹下建一个download文件夹,用来存放准备发送的bin文件。
将之前生成的led.bin文件,复制到download中。
6)打开secureCRT,连接串口,
注意,secureCRT连接串口时,会控制RTS,DTR等信号,探索者板子上,RTS和DTR会分别控制RESET和BOOT0,所以,如果使用secureCRT,不能用这个串口usart1,而应使用usart2或者usart3。
如果需要使用usart1,解决办法,去掉R73,使一键下载电路不起作用。