Flash空间规划
本文使用的单片机为笙科的A9129F6,Flash大小为64KB,SRAM大小为8KB。
Flash空间规划如下。
起始地址 | 结束地址 | 用途 |
---|---|---|
0x0000 | 0x3fff | Bootloader程序 |
0x4000 | 0xefff | 用户程序(APP程序) |
0xf000 |
0xffff |
存放设备配置信息 |
程序间跳转实现起来很简单,只需要使用函数指针就行了。
但是难点在于51单片机的中断向量表不支持重定向,中断发生时只能固定从(0x0003+8n)处开始执行。
bootloader和app都有自己的中断向量表,而中断发生时进入的始终是bootloader的向量表。
程序需要有一个标志变量(定义到xdata的0地址处),用于判断当前执行的是bootloader还是APP程序。如果当前是在执行APP程序,那么中断发生后先是运行bootloader程序的向量表,判断这个标志变量后,再主动跳转到APP程序的中断向量表中执行。
Keil工程配置
第一步:
设置bootloader程序使用的Flash空间范围为0x0000-0x3fff,XDATA(也就是SRAM)空间范围为0x0001-0x1fff
设置app程序使用的Flash空间范围为0x4000-0xefff,XDATA SRAM空间范围为0x0001-0x1fff
第二步:在app程序的启动文件中设置Reset Vector和Startup段的地址
第三步:设置编译bootloader程序时不自动产生中断向量表,设置编译app程序时产生的中断向量表的保存位置为0x4000
第四步:设置烧写bootloader程序和app程序时只擦除需要的扇区,而不是全片擦除(但是这样设置后烧完app程序,0号扇区仍然会被覆盖)
实现从Bootloader程序跳转到主程序
#include <A9129F6.h>
#include <stdio.h>
#include "macros.h"
#include "systick.h"
#include "uart.h"
char uart_data;
#define APP_FLASH_ADDR 0x4000
#define VECTOR_TABLE (*(uint8_t xdata *)0x0000)
typedef void (code *Runnable)(void);
static void jump_to_application(void)
{
Runnable run = (Runnable)APP_FLASH_ADDR;
printf("Jump to application...\n");
EA = 0;
ES = 0;
VECTOR_TABLE = 1;
run();
}
int main(void)
{
int i;
VECTOR_TABLE = 0;
EA = 1;
systick_init();
uart_init();
printf("Meross Bootloader\n");
while (1)
{
if (uart_data)
{
printf("Interrupt occurred\n");
if (uart_data == '\r')
{
jump_to_application();
}
uart_data = 0;
}
printf("i=%d, time=%lu\n", i, sys_now());
i++;
delay_ms(500);
}
}
汇编实现Bootloader中断向量表
中断向量表必须用汇编语言实现,不能用C语言实现。因为这涉及到保护现场和恢复现场,不允许带interrupt关键字的C函数去调用另一个带interrupt关键字的C函数。
假设bootloader和app程序里面都用到了UART和TIMER0中断,而其他中断(如RFINT和KEYINT)只有app程序在用。
新建一个名叫interrupts.a51的汇编文件,添加到工程中。内容如下:
CSEG AT 0x0003
LJMP 0x4003 ; INT0_ISR (重定向到APP程序相应的中断向量表上, 下同)
CSEG AT 0x000b
LJMP TIMER0_ISR
CSEG AT 0x0013
LJMP 0x4013 ; INT1_ISR
CSEG AT 0x001b
LJMP 0x401b ; TIMER1_ISR
CSEG AT 0x0023
LJMP UART_ISR
CSEG AT 0x002b
LJMP 0x402b ; TIMER2_ISR
CSEG AT 0x003b
LJMP 0x403b ; INT2_ISR
CSEG AT 0x0043
LJMP 0x4043 ; USBINT_ISR
CSEG AT 0x004b
LJMP 0x404b ; I2SINT_ISR
CSEG AT 0x0053
LJMP 0x4053 ; RFINT_ISR
CSEG AT 0x005b
LJMP 0x405b ; KEYINT_ISR
CSEG AT 0x0063
LJMP 0x4063 ; WATCHDOG_ISR
CSEG AT 0x006b
LJMP 0x406b ; I2C_ISR
CSEG AT 0x0073
LJMP 0x4073 ; SPI_ISR
CSEG AT 0x0100 ; 自定义代码块
TIMER0_ISR:
; 保护现场
PUSH ACC ; 保存A寄存器的原有内容
PUSH DPH ; 保存DPTR寄存器(高字节)的原有内容
PUSH DPL ; 保存DPTR寄存器(低字节)的原有内容
PUSH PSW ; 保存PSW(程序状态)寄存器的原有内容
MOV PSW,#0x00 ; 清除PSW程序状态值
MOV DPTR,#0x0000 ; DPTR寄存器赋值为0
MOVX A,@DPTR ; 从XDATA 0x0000地址处读取一个字节,存到A寄存器中
CJNE A,#0x00,APP_TIMER0_ISR ; 如果A的值不等于0,则跳转到APP_TIMER0_ISR标签上;否则不跳转,继续往下执行
; 恢复现场
POP PSW ; 恢复PSW寄存器的原有内容
POP DPL ; 恢复DPTR寄存器的原有内容
POP DPH
POP ACC ; 恢复A寄存器的原有内容
EXTRN CODE(BOOTLOADER_TIMER0_ISR) ; 引用bootloader程序中的C语言函数
LJMP BOOTLOADER_TIMER0_ISR ; 执行bootloader程序中的C语言函数,然后不返回了
APP_TIMER0_ISR:
POP PSW
POP DPL
POP DPH
POP ACC
LJMP 0x400b ; 执行APP程序中的C语言函数,然后不返回了
UART_ISR:
PUSH ACC
PUSH DPH
PUSH DPL
PUSH PSW
MOV PSW,#0x00
MOV DPTR,#0x0000
MOVX A,@DPTR
CJNE A,#0x00,APP_UART_ISR
POP PSW
POP DPL
POP DPH
POP ACC
EXTRN CODE(BOOTLOADER_UART_ISR)
LJMP BOOTLOADER_UART_ISR
APP_UART_ISR:
POP PSW
POP DPL
POP DPH
POP ACC
LJMP 0x4023
END
Bootloader中没有用到的中断,直接用CSEG AT和LJMP语句重定向到APP程序,不用管APP程序用没用到。
Bootloader中用到了的中断,那就需要判断一下xdata 0x0000处的标志变量,再决定是执行bootloader的ISR,还是app的ISR。同样也不用管APP程序用没用到。
如果xdata 0x0000=0,就执行bootloader的ISR,如果xdata 0x0000!=0,就执行APP的ISR。
根据A9129F6的芯片手册,0x0003是INT0中断的向量地址,0x000b是TIMER0中断的向量地址,……,0x0073是SPI中断的向量地址。
这些都属于bootloader的中断向量表空间,代码空间有限,应该只写一条LJMP跳转指令。
其他复杂的代码块要放在一个专门的区域内,在本文中是CSEG AT 0x0100。这个地址可以随意指定,在这个区域下可以放置多种中断的程序,要新添加其他中断的话不用再自己新建CSEG数据段了,直接复制UART_ISR:到LJMP 0x4023这段代码,然后再作相应修改,放到END语句前就行。
Bootloader程序中的中断服务函数(必须都要加上interrupt关键字):
/* Timer 0 interrupt handler */
void BOOTLOADER_TIMER0_ISR(void) interrupt 1
{
TL0 = 0xd5; // reload timer 0
TH0 = 0xfb;
TF0 = 0; // clear timer 0 overflow flag
systick_counter++; // increment microsecond counter
}
/* UART interrupt handler */
void BOOTLOADER_UART_ISR(void) interrupt 4
{
char c;
extern char uart_data;
if (RI)
{
c = SBUF;
RI = 0;
uart_data = c;
}
}
APP程序中的中断服务函数(必须都要加上interrupt关键字):
/* Timer 0 interrupt handler */
void TIMER0_ISR(void) interrupt 1
{
TL0 = 0xd5; // reload timer 0
TH0 = 0xfb;
TF0 = 0; // clear timer 0 overflow flag
systick_counter++; // increment microsecond counter
led_process();
}
/* UART interrupt handler */
void UART_ISR(void) interrupt 4
{
char c;
if (RI)
{
c = SBUF;
RI = 0;
console_receive(c);
}
}
void RF_ISR(void) interrupt 10
{
EIF = EIF_RFINTF; // RFINTF->0
rf_flag = 1;
}
void KEYINT_ISR(void) interrupt 11
{
EIF = EIF_KEYINTF; // clear interrupt flag
}
加上interrupt关键字的目的是为了保证函数代码以RETI汇编指令结尾。
特别注意:以后如果修改了APP程序的代码并重新编译,必须在烧写完APP程序后,再烧写一下bootloader程序,才能保证APP程序正常运行。也就是说改一次代码要烧写两次。
APP程序里面虽然也有自己的中断向量表,但是在中断发生时是先进入bootloader程序的中断向量表,然后再跳转到APP程序的中断向量表。
如果只烧写了APP程序,没有烧写bootloader程序,那么APP里面所有的中断服务函数都无法执行!
所以APP程序要判断一下bootloader程序到底有没有烧写,如果没有烧写,应该在串口中给出错误提示,然后停止执行程序。
判断的方法是看APP的中断服务函数到底能不能得到执行。见下面的systick_test函数。文章来源:https://www.toymoban.com/news/detail-604837.html
static uint32_t systick_counter;
/* Get the microsecond counter */
uint32_t sys_now(void)
{
return systick_counter;
}
/* Verify if this APP was started from the bootloader */
void systick_test(void) large
{
int i = 0;
uint32_t start;
start = sys_now();
while (sys_now() == start)
{
if (i == 30000)
{
printf("Please download the bootloader program before running this APP\n");
i = -1;
}
else if (i >= 0)
{
i++;
}
}
}
有的人会有这样的疑问:C语言函数加了interrupt关键字之后会自动生成保护现场的代码,那汇编里面保护现场不就多余了吗?
其实并不是。仔细看汇编代码,中断发生时先进入的是汇编函数,在汇编函数里面先保护现场,再进行寄存器操作和条件判断,然后恢复现场后再跳转到C语言的带interrupt关键字的函数,然后C语言函数里面再保护现场和恢复现场。现场的确是保护和恢复了两次,但是这两次并不是重叠的,而是分开的。具体来说就是“汇编保护、汇编操作、汇编恢复、C保护、C操作、C恢复”的过程,并不是先保护两次再恢复两次,所以结论是汇编里面的保护现场并不多余。文章来源地址https://www.toymoban.com/news/detail-604837.html
到了这里,关于51内核单片机实现Bootloader跳转到用户程序,要求两个程序都要支持中断的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!