51内核单片机实现Bootloader跳转到用户程序,要求两个程序都要支持中断

Flash空间规划

本文使用的单片机为笙科的A9129F6,Flash大小为64KB,SRAM大小为8KB。
Flash空间规划如下。

起始地址结束地址用途
0x00000x3fff

Bootloader程序

0x40000xefff

用户程序(APP程序)

0xf000

0xffff

存放设备配置信息

程序间跳转实现起来很简单,只需要使用函数指针就行了。
但是难点在于51
单片机的中断向量表不支持重定向,中断发生时只能固定从(0x0003+8n)处开始执行。
bootloaderapp都有自己的中断向量表,而中断发生时进入的始终是bootloader的向量表。
程序需要有一个标志变量(定义到xdata0地址处),用于判断当前执行的是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函数。

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恢复”的过程,并不是先保护两次再恢复两次,所以结论是汇编里面的保护现场并不多余。

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
你好!对于你的问题,可能有几个原因导致HAL_Delay函数在跳转到APP程序后无法工作。以下是一些常见的可能原因和解决方法: 1. 时钟配置:确保在跳转到APP程序后,时钟配置与bootloader中的配置相匹配。如果时钟配置不正确,可能会导致HAL_Delay函数无法正常工作。可以使用调试器来检查时钟配置是否正确。 2. 中断配置:在跳转到APP程序后,确保中断配置与bootloader中的配置相同。如果中断配置不正确,可能会干扰HAL_Delay函数的正常运行。可以使用调试器来检查中断配置是否正确。 3. 调用位置:确定你在APP程序中正确地调用了HAL_Delay函数。确保在需要延时的地方正确地调用了该函数,并且没有其他代码干扰了延时的执行。 4. 代码重定位:如果你的APP程序使用了代码重定位(例如使用链接脚本),可能需要适当地配置重定位地址。确保重定位地址与bootloader中的配置相匹配,以确保HAL_Delay函数可以正确地执行。 5. 系统时钟频率:检查系统时钟频率是否正确配置,并且与HAL库中的设置相匹配。如果系统时钟频率不正确,可能会导致HAL_Delay函数无法正常工作。 如果以上方法都没有解决问题,那么可能需要更详细地检查你的APP程序代码,以确定是否有其他因素导致HAL_Delay函数无法正常工作。希望这些信息对你有所帮助!如果你有任何其他问题,请随时提问。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巨大八爪鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值