Flash空间规划
本文使用的单片机为笙科的A9129F6,Flash大小为64KB,其空间规划如下。
起始地址 | 结束地址 | 用途 |
---|---|---|
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 RAM空间范围为0x0001-0xffff
设置app程序使用的Flash空间范围为0x4000-0xefff,XDATA RAM空间范围为0x0001-0xffff
第二步:在app程序的启动文件中设置Reset Vector和Startup段的地址
第三步:设置编译bootloader程序时不自动产生中断向量表,设置编译app程序时产生的中断向量表的保存位置为0x4000
第四步:设置烧写bootloader程序和app程序时只擦除需要的扇区,而不是全片擦除(但是这样设置后烧完app程序,0号扇区仍然会被覆盖)
实现从Bootloader程序跳转到主程序
#include <A9129F6.h>
#include <LIB_Uti.h>
#include <stdio.h>
#include "macros.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;
uart_init();
printf("Bootloader\n");
while (1)
{
if (uart_data)
{
printf("Interrupt occurred\n");
if (uart_data == '\r')
{
jump_to_application();
}
uart_data = 0;
}
printf("i=%d\n", i);
i++;
Delay10ms(50);
}
}
汇编实现Bootloader中断向量表
中断向量表必须用汇编语言实现,不能用C语言实现。因为这涉及到保护现场和恢复现场。
假设bootloader和app程序里面都用到了UART中断,而其他中断(如TIMER0、RFINT和KEYINT)只有app程序在用。
新建一个名叫interrupt.a51的汇编文件,添加到工程中。内容如下:
CSEG AT 0x0003
LJMP 0x4003 ; INT0_ISR (重定向到APP程序相应的中断向量表上,下同)
CSEG AT 0x000b
LJMP 0x400b ; 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 ; 自定义代码块
UART_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,UART_ISR_APP ; 如果A的值不等于0,则跳转到UART_ISR_APP标签上;否则不跳转,继续往下执行
; 恢复现场
POP PSW ; 恢复PSW寄存器的原有内容
POP DPL ; 恢复DPTR寄存器的原有内容
POP DPH
POP ACC ; 恢复A寄存器的原有内容
EXTRN CODE(BOOTLOADER_UART_ISR) ; 引用bootloader程序中的C语言函数
LJMP BOOTLOADER_UART_ISR ; 执行bootloader程序中的C语言函数,然后不返回了
UART_ISR_APP:
POP PSW
POP DPL
POP DPH
POP ACC
LJMP 0x4023 ; 执行APP程序中的C语言函数,然后不返回了
END
Bootloader中没有用到的中断,直接用CSEG AT和LJMP语句重定向到APP程序。无论APP程序用没用到,最好都写上。
Bootloader中用到了的中断,那就需要判断一下xdata 0x0000处的标志变量,再决定是执行bootloader的ISR,还是app的ISR。
如果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关键字):
/* 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关键字):
/* UART interrupt handler */
void UART_ISR(void) interrupt 4
{
char c;
if (RI)
{
c = SBUF;
RI = 0;
console_receive(c);
}
}
/* 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();
}
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函数。
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++;
}
}
}