前言
(1)如果有嵌入式企业需要招聘湖南区域日常实习生,任何区域的暑假Linux驱动/单片机/RTOS的实习岗位,可C站直接私聊,或者邮件:[email protected],此消息至2025年1月1日前均有效
(2)在上一章节中,我们详细介绍了如何让Bootloader引导进入APP程序。但是上一章节的工程是无法使用中断和RTOS的,为什么呢?本文将会详细介绍。
(3)上一章节博客地址:Bootloader/IAP零基础入门(1.0) —— 设计一个Bootloader引导进入APP的程序,不含中断向量偏移
(4)系列教程仓库链接:Github仓库
(5)注意:本章节只是做一个Bootloader引导进入APP的程序教程。虽然可以实现引导程序,但是并没有实现Flash擦除功能,因此并不能算真正的Bootloader。
前期准备
APP程序修改
(1)这里延续使用使用上一章节的工程。只不过在
APP
程序中,加入一个串口回传机制,需要使用到串口中断。
(2)首先初始化串口,波特率115200
,使能串口中断。
(3)进入
usart.c
文件,按Ctrl+F
搜索USER CODE BEGIN 0
,补充如下代码。
/* USER CODE BEGIN 0 */
uint16_t g_usart_rx_sta = 0;
uint8_t g_rx_buffer[1]; /* HAL库使用的串口接收缓冲 */
/* 接收缓冲, 最大USART_REC_LEN个字节. */
uint8_t g_usart_rx_buf[USART_REC_LEN];
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
/* USER CODE END 0 */
(4)补充串口接受回调函数,在
usart.c
中按Ctrl+F
搜索USER CODE BEGIN 1
,补充如下代码。
/* USER CODE BEGIN 1 */
/**
* @brief 串口数据接收回调函数
数据处理在这里进行
* @param huart:串口句柄
* @retval 无
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) /* 如果是串口1 */
{
if ((g_usart_rx_sta & 0x8000) == 0) /* 接收未完成 */
{
if (g_usart_rx_sta & 0x4000) /* 接收到了0x0d(即回车键) */
{
if (g_rx_buffer[0] != 0x0a) /* 接收到的不是0x0a(即不是换行键) */
{
g_usart_rx_sta = 0; /* 接收错误,重新开始 */
}
else /* 接收到的是0x0a(即换行键) */
{
g_usart_rx_sta |= 0x8000; /* 接收完成了 */
}
}
else /* 还没收到0X0d(即回车键) */
{
if (g_rx_buffer[0] == 0x0d)
g_usart_rx_sta |= 0x4000;
else
{
g_usart_rx_buf[g_usart_rx_sta & 0X3FFF] = g_rx_buffer[0];
g_usart_rx_sta++;
if (g_usart_rx_sta > (USART_REC_LEN - 1))
{
g_usart_rx_sta = 0; /* 接收数据错误,重新开始接收 */
}
}
}
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)g_rx_buffer, 1);
}
}
/* USER CODE END 1 */
(5)在
usart.h
文件中补充如下代码。
/* USER CODE BEGIN Private defines */
#define USART_REC_LEN 200 /* 定义最大接收字节数 200 */
/* USER CODE BEGIN Prototypes */
extern uint16_t g_usart_rx_sta;
extern uint8_t g_rx_buffer[1]; /* HAL库使用的串口接收缓冲 */
/* 接收缓冲, 最大USART_REC_LEN个字节. */
extern uint8_t g_usart_rx_buf[USART_REC_LEN];
/* USER CODE END Prototypes */
(5)在
main.c
文件按Ctrl+F
搜索USER CODE BEGIN 1
,补充如下代码。
/* USER CODE BEGIN 1 */
uint8_t len = 0;
uint16_t times = 0;
/* USER CODE END 1 */
(6)在
main.c
文件按Ctrl+F
搜索USER CODE BEGIN 3
,补充如下代码。
/* USER CODE BEGIN 3 */
if (g_usart_rx_sta & 0x8000) /* 接收到了数据? */
{
len = g_usart_rx_sta & 0x3fff; /* 得到此次接收到的数据长度 */
printf("\r\n您发送的消息为:\r\n");
HAL_UART_Transmit(&huart1,(uint8_t*)g_usart_rx_buf, len, 1000); /* 发送接收到的数据 */
while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) != SET); /* 等待发送结束 */
printf("\r\n\r\n"); /* 插入换行 */
g_usart_rx_sta = 0;
}
else
{
times++;
if (times % 500 == 0)
{
printf("\r\n正点原子 STM32开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n\r\n");
}
if (times % 500 == 0) printf("请输入数据,以回车键结束\r\n");
if (times % 100 == 0) /* 闪烁LED,提示系统正在运行. */
{
HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
}
HAL_Delay(10);
}
}
/* USER CODE END 3 */
Bootloader修改
(1)本章节的
Bootloader
和上一章节相比,只是多了下面这一行代码。
SCB->VTOR = AppStartFlash; //APP的中断向量表是从0x8002000起始
工程整合
(1)工程整合与上一章节一样。因为每次配置
STM32CubeMX
,Keil MDK
的相关设置都会被初始化默认状态。因此,这里我们需要重新配置一下APP
的Keil MDK
配置。具体的配置原因不做赘述,上一章节已作详细说明。
Bootloader代码讲解
VTOR寄存器与中断的关系
(1)我们发现,这一章节的
Bootloader
只是对VTOR
寄存器进行了一次重新赋值。为什么不直接放在上一章节讲解?明明就一句代码的事情。这肯定是因为大有讲究。
(2)对于M3/M4
内核的芯片,我们首先需要明白一点。CPU
是怎么知道哪里是中断函数,地址对应的函数到底是串口中断还是外部中断。
(3)这个时候我们就需要看看M3权威指南的7.3 向量表章节了。
(4)从下面我们可以获取到以下信息:
- 当发生异常时候,M3/M4内核芯片需要在中断向量表中找到异常函数地址。
- 中断向量表默认在0地址处。
- 0xE000_ED08地址处的VTOR寄存器可以指示中断向量表的起始地址。
- VTOR寄存器的低7位必须都为0。
- VTOR寄存器上电复位的bit7-bit28全为0。结合低7位必须为0,可以得出第二天结论,即M3/M4内核芯片中断向量表上电复位时候,默认为0地址处。(注意,因为STM32F103存在地址映射功能,因此0地址与0x0800 0000对应)
M3/M4中断是流程
(1)上面我们知道了
VTOR
寄存器可以指示中断向量表的起始地址。而这个寄存器复位指示的向量表起始地址为0
地址(STM32
芯片中,0
地址与0x0800 0000
对应)
(2)现在我们以复位中断为例,假设我们按下复位键,此时会触发复位中断。
<1>CPU
首先会根据VTOR
寄存器找到向量表的起始地址0x08000000
<2>然后CPU
自己记得,复位中断是在向量表的起始地址偏移4字节位置,也就是0x08000004
。
<3>此时CPU
读取0x08000004
地址处的数据,并且跳转到这个地址。例如这里0x08000004
地址处存放08000145
,因为Thumb
指令集的原因,bit0
必须为1
,因此实际上应该跳转的是08000144
地址处。
<4>我们看08000144
地址处,就能够发现这里是复位中断。其他中断执行流程与这个同理。都是CPU
先去向量表起始地址,再根据芯片固化的中断偏移地址+向量表起始地址,找到存储中断函数地址处,然后根据这个存储的数据跳转到该地址执行任务。
为什么APP程序需要进行中断向量偏移
(1)有了上面的基础,回答这个问题其实显而易见了。首先,我们知道,
APP
的程序起始地址不会是在0x0800 0000
,而是在我们设置的0x800 2000
处。因此,APP
的中断向量表并不是在0x0800 0000
,而是在0x800 2000
。于是,我们的Bootloader
执行完程序之后,跳转到APP
程序之前,需要设置VTOR
寄存器进行中断向量偏移。
拓展思考
不支持中断向量偏移的芯片怎么办?
(1)学习了上面的教程之后,我们对
Bootloader
引导进入APP
程序又有了进一步的了解。这个时候善于思考的同学肯定要说了,我这里讲的程序是只针对M3/M4
内核的芯片吗?
(2)因为我对不同的内核了解也有限,目前只对M3/M4
内核略懂,因此我也不敢保证所有内核芯片都是这样的。于是我们就要考虑一个问题,如果有些内核的芯片,并不支持中断向量表进行偏移,应该怎么办?
<1>方法很简单,我直接跳转到APP Flash
的起始地址,进行偏移即可。原理与上一章节所说的,Bootloader
跳转到APP
程序相同,不做赘述。
#define AppStartFlash 0x8002000UL
typedef void (*HandlerFunction)(void); //定义一个函数指针类型
void Reset_Handler(void)
{
HandlerFunction APP_Handler = (HandlerFunction)*(volatile uint32_t *)(AppStartFlash + (4 * 1));
APP_Handler();
}
void NMI_Handler(void)
{
HandlerFunction APP_Handler = (HandlerFunction)*(volatile uint32_t *)(AppStartFlash + (4 * 2));
APP_Handler();
}
void HardFault_Handler(void)
{
HandlerFunction APP_Handler = (HandlerFunction)*(volatile uint32_t *)(AppStartFlash + (4 * 3));
APP_Handler();
}
//...
void PendSV_Handler(void)
{
HandlerFunction APP_Handler = (HandlerFunction)*(volatile uint32_t *)(AppStartFlash + (4 * 14));
APP_Handler();
}
//...
Bootloader的中断向量有存在的必要吗?
(1)现在我们对中断流程,以及中断向量表已经拥有了一定的认识。那么,我们是否能够思考一个问题。中断向量表对于
Bootloader
真的一定有存在的价值吗?
<1>这个时候我们又需要回到中断向量表的意义了。我们知道,中断向量表是存储在Flash
的VTOR
指示的地址处,当发生中断时候,CPU
会找到VTOR
指示的地址,再根据是什么类型的中断进行地址偏移,最终跳转到中断函数里面。
<2>但是,假如我们的Bootloader
并不会产生任何的中断,那有存在的意义吗?如果你想要尽可能的压缩空间,并且真的不会使用到中断,那么Bootloader
是可以不要中断向量表的。
(2)这个时候我们再思考一下,假如我们只用到了串口1中断函数,并且想要尽可能的压缩Bootloader
程序空间,中断向量表又应该如何处理呢?
<1>因为我们知道,CPU
是根据VTOR
寄存器指示的中断向量表起始地址进行偏移,而我们这里又只需要用到串口1的中断函数,CPU
的这个中断偏移用户是无法修改的。因此,我们可以得出结论,如果Bootloader
只需要使用串口1的中断,那么串口1中断之前的中断向量表都是需要进行保留的。串口1中断后面的中断向量表,可以进行删除。
(3)对于不支持中断向量表偏移的芯片,Bootloader
能够删除中断向量表吗?
<1>这个显然是不可以的,之前Bootloader
删除中断向量表,是因为Bootloader
不需要使用中断,并且进入APP程序之前就能够让中断向量表进行偏移。因此并不会影响到APP
程序的中断。
<2>但是如果是不支持中断向量表偏移的芯片,将Bootloader
的中断向量表删除了,之后进入APP
程序之后,发生中断将会产生未知的错误。
参考
标签:1.1,中断向量,usart,APP,rx,地址,Bootloader From: https://blog.csdn.net/qq_63922192/article/details/137123772(1)中科院:嵌入式与微机原理总复习
(2)CM3权威指南
(3)百问网:29_异常向量表基地址无法修改时怎么办