基于GD32H7的SD卡Bootloader代码,以及BUG解决

思路

目的是实现GD32H759系统板的SD卡bootloader,进行SD卡升级,思路可以参考硬汉哥的教学BSP视频教程第18期:基于NAND,eMMC,SD卡和U盘的BootLoader实战,带CRC完整性校验(2022-06-16) - STM32H7 - 硬汉嵌入式论坛 - Powered by Discuz!

流程就是通过外部设备获取代码的二进制可执行文件,也就是bin文件,可以直接写入到内部Flash中执行.

为此为了减少上位机的依赖,我选择使用SD卡来存储bin文件,读取后写入Flash中,通过板载的按键来切换升级代码或者直接启动APP.

外设配置

Flash

通过查看系统结构图,GD32H7的Flash挂载在FMC总线上,通过FMC总线访问,由于GD已经编写了对应FMC的代码,可以直接通过地址访问,这里使用惠勤智远的示例代码,直接读写内部Flash.

void fmc_write_32bit_data(uint32_t address, uint32_t *data_32, uint32_t length)
{
    fmc_state_enum fmc_state = FMC_READY;
    uint32_t addrx = 0;
    uint32_t endaddr = 0;
    uint32_t i;
    
    if (address < FMC_START_ADDRESS || address % 4 ||       /* 写入地址小于 FMC_START_ADDRESS, 或不是4的整数倍, 非法. */
        address > (FMC_END_ADDRESS))                        /* 写入地址大于 FMC_END_ADDRESS, 非法. */
    {
        return;
    }
    
    fmc_unlock();                     /* 解锁FMC_CTL寄存器 */
    
    fmc_all_flags_clear();            /* 清除FMC标志位  */
  
    addrx = address;                  /* 写入的起始地址 */
    endaddr = address + length * 4;   /* 写入的结束地址 */

    while (addrx < endaddr)           /* 扫清一切障碍.(对非FFFFFFFF的地方,先擦除) */
    {
        if (*(volatile uint32_t *)addrx != 0XFFFFFFFF)     /* 有非0XFFFFFFFF的地方,要擦除这个扇区 */
        {          
            fmc_state = fmc_sector_erase(addrx);           /* 擦除扇区 */
          
            if (FMC_READY != fmc_state)             
            {
                break;                                     /* 发生错误 */
            }
            
            fmc_all_flags_clear();                         /* 清除FMC标志位  */
            SCB_CleanInvalidateDCache();                   /* 清除无效的D-Cache */
        }
        else
        {
            addrx += 4;
        }
    }
    
    if (FMC_READY == fmc_state)
    {
        for (i = 0; i < length; i++)
        {
            if (FMC_READY != fmc_word_program(address, data_32[i])) /* 写入数据 */
            {
                 break;             /* 写入异常 */  
            }
            
            address += 4;
            fmc_all_flags_clear();  /* 清除FMC标志位 */ 
        }
    }

    SCB_CleanInvalidateDCache();    /* 清除无效的D-Cache */
    
    fmc_lock();                     /* 锁定FMC_CTL寄存器 */
}

/**
 * @brief       从指定地址开始,读出指定长度的数据
 * @param       address    : 起始地址
 * @param       data_32    : 数据指针
 * @param       length     : 要读取的字(32位)数
 * @retval      无
 */
void fmc_read_32bit_data(uint32_t address, uint32_t *data_32, uint32_t length)
{ 
    uint32_t i;
  
    for (i = 0; i < length; i++)
    {
        data_32[i] = *(volatile uint32_t *)address;     /* 读取4个字节(1个字) */
        address += 4;                                   /* 偏移4个字节 */
    }
}

SD卡

由于需要使用SD卡存储bin文件,需要配置SDIO接口,初始化SD卡,随后挂载Fatfs文件,由于此部分参考较多,可以自行参考,挂载成功以后可以通过该函数验证是否成功,查看是否有bin文件.

void ScanSDCardFile(const char *path) {
    

    res = f_opendir(&dir, path);
    if (res != FR_OK) {
        printf("Error opening directory %s! Error code: %d\n", path, res);
        return;
    }
    while (1) {
        res = f_readdir(&dir, &fno);
        if (res != FR_OK || fno.fname[0] == 0) {
            break;
        }
        if (fno.fattrib & AM_DIR){
            printf("DIR:  %s\r\n", fno.fname);
        } else {
            printf("FILE: %s\r\n", fno.fname);
        }
    }
    f_closedir(&dir);
}

其他

为了方便调试,可以使能USART等等,可以参考上一篇USART IDLE+DMA配置.GD32H759 USART 使能IDLE+DMA接受 以及DMA发送_gd32 链表保存usart1-CSDN博客

代码编写

flash内容结构为

地址0x8000000-0x8010000 为bootloader程序区域,大小64kb 0x10000

地址0x8010000-最后 为app程序区域

bootloader大小可以自行设置,不要小于bootloader程序大小就好

读取BIN文件

首先第一步我们需要确认BIN文件是否为可行文件,通过查看startup文件,其中有几个保留位,我们将其设置为版本号等等,读取该保留位判断是否为所需保留位.

第一步,使用fatfs打开bin文件,由于fil文件对象所需内存较大,将其设置为全局变量,置于较大的堆中.

FATFS fs;        // 文件系统对象
FIL fil;          // 文件对象
DIR dir;  
FRESULT res;      // 操作返回值
FILINFO fno;       // 文件信息对象
void NewAPPInit()
{
   
    int res_sd=f_mount(&fs,"0:",0);
    char data[10];
    int num;
    if (res_sd != FR_OK){
        printf("mount fail , error code:%d ",res_sd);
        return ;
    }
    LOAD("文件系统挂载完成\r\n");
    
}

在挂载成功以后检查是否有bin文件,有则打开读取版本号.

void APPDownload(void)
{
    uint32_t data[1024]; 
    UINT bytesRead;
    uint32_t totalBytesRead = 0;
    res = f_open(&fil, "/GD32.bin", FA_READ);
    if (res != FR_OK){
        ERROR("打开BIN文件错误\r\n");
        return; 
    }
    else{  
    INFO("已找到BIN文件,开始读取\r\n");
    res = f_stat("/GD32.bin", &fno);
    if (res != FR_OK){
        ERROR("获取文件信息错误\r\n");
        f_close(&fil);
        return;
    }
    INFO("文件:%lu 字节\r\n", fno.fsize);  // fno.fsize 存储的是文件的大小
    ReadSDAppVersion();
    f_lseek(&fil, 0);
    while (1){
        res = f_read(&fil, data, sizeof(data), &bytesRead);
        if (res != FR_OK){
            ERROR("读取文件错误\r\n");
            break;
        }
        Delay(5);
        fmc_write_32bit_data(APP_FLASH_ADDR+totalBytesRead,data,bytesRead/4);
        totalBytesRead+= bytesRead;
        INFO("下载进度->%d \%\r\n",totalBytesRead*100/fno.fsize);
        if (bytesRead < sizeof(data)){
            INFO("文件读取完成,共%d\r\n", totalBytesRead);  // 提示文件读取完成
            break;
        }
    }
    f_close(&fil);
    }
}

读取版本号,由于GD32是32位,所以startup文件中断向量表一个代表uint32_t的大小,也就是4字节,我们需要读取的是第八位开始,所以前面有7*4字节的偏移也就是28,同理,最后一位需要52的偏移.

获取到版本号.

void ReadSDAppVersion(void)
{   
    uint32_t appVersion[5];
    UINT bytesRead;
    res = f_lseek(&fil, 28);
    res = f_read(&fil, appVersion,16, NULL);
    res = f_lseek(&fil, 52);
    res = f_read(&fil, &appVersion[4], 4, NULL);
    INFO("SD APP版本 %d.%d.%d.%d.%d\r\n",appVersion[0],appVersion[1],appVersion[2],appVersion[3],appVersion[4]);
}

执行效果,由于测试版本没有设置版本号所以是默认的0000

同理,编写Flash读取版本号代码

void ReadVersion(uint32_t addr)
{   
    uint32_t data[5];
    uint32_t temp;
    fmc_read_32bit_data(addr+sizeof(uint32_t)*7,data,4);
    fmc_read_32bit_data(addr+sizeof(uint32_t)*13,&data[4],1);
    if(data[1]==-1&data[2]==-1&data[3]==-1&data[4]==-1){
        ERROR("未找到程序\r\n");
    }
    else{
    if(data[0]==1)
        INFO("FLASH Bootloader版本%d.%d.%d.%d\r\n",data[1],data[2],data[3],data[4]);
    else
        
        INFO("FLASH APP 版本%d.%d.%d.%d\r\n",data[1],data[2],data[3],data[4]);
    }
}

为了增强复用性,我设置第一位为区分bootloader和app的标志位,在bootloader启动时也可以读取boot版本.

由于在flash中,如果bin文件烧录地址错误也会导致版本号读取错误,通常情况下有两种

1bin文件未烧录,flash全为0xff,读取值为-1

2bin文件地址错误,则读取值不为0

读取完成以后开始烧录代码,使用data[1024]作为缓冲区,读取完成以后延时一会写入flash,也可以在此处加入回读确保写入正确.

 while (1){
        res = f_read(&fil, data, sizeof(data), &bytesRead);
        if (res != FR_OK){
            ERROR("读取文件错误\r\n");
            break;
        }
        Delay(5);
        fmc_write_32bit_data(APP_FLASH_ADDR+totalBytesRead,data,bytesRead/4);
        totalBytesRead+= bytesRead;
        INFO("下载进度->%d \%\r\n",totalBytesRead*100/fno.fsize);
        if (bytesRead < sizeof(data)){
            INFO("文件读取完成,共%d\r\n", totalBytesRead);  // 提示文件读取完成
            break;
        }
    }

当读取完成以后调用ReadAppVerison查看更新的APP版本号,可以看到APP代码执行成功

APP跳转

APP跳转这里最主要就是三部分

void JumpToApp(void)
{
    if (((*(volatile  uint32_t *)APP_FLASH_ADDR) & 0x2FF00000) == 0x24000000)     /* 检查栈顶地址是否合法 */
    {
        INFO("开始跳转APP\r\n");
        APP jumpapp;
        /*
        sys_intx_disable();
        for (int i = 0; i < 8; i++)
        {
            NVIC->ICER[i]=0xFFFFFFFF;
            NVIC->ICPR[i]=0xFFFFFFFF;
        }
        SysTick->CTRL   =   0;
        SysTick->LOAD   =   0;
        SysTick->CTRL   =   0; //开启SYSTICK
        sys_intx_enable();
        
        */
        //__set_PSP(*(__IO uint32_t *)APP_FLASH_ADDR); 
        
        dma_deinit(DMA0, DMA_CH0);
        dma_deinit(DMA0, DMA_CH1);
        usart_deinit(USART1);
        sdio_deinit(SDIO0);
        SysTick->CTRL = 0;
        SysTick->LOAD = 0;
        SysTick->VAL = 0;
        NVIC_DisableIRQ(USART1_IRQn);
        
        sys_intx_disable();
        
        sys_msr_msp(*(volatile uint32_t *)APP_FLASH_ADDR);
        jumpapp = (APP) * (volatile uint32_t *)(APP_FLASH_ADDR + 4);
        
        /* 跳转到APP */
        jumpapp();
    }
}

重点

在APP的起始程序需要加上这一句

sys_intx_enable();
SCB->VTOR = FLASH_BASE|0x10000 ;

在bootloader跳转app中,我们手动关闭了中断,所以需要先打开中断,随后需要设置中断向量表,其中0x10000是bootloader的大小,由于我们APP的地址为0x8010000,起始就是中断向量表,VTOR寄存器存储中断向量表基址,手动将VTOR从bootloader的中断向量表设置为APP的.

1设置MSP主栈指针到APP复位程序地址,也就是 DCD     Reset_Handler                     ; Reset Handler,这里是属于APP的代码,通过函数指针在bootloader中执行的第一条APP代码,程序跳转带APP

2为APP运行提供干净的环境,由于在bootloader中我们使用了一部分外设,如果我们没有将其复位,可以在后面APP初始化或者运行时发生冲突,所以需要复位.同理中断的存在有也可能在跳转过程中打断跳转过程,所以需要调用sys_intx_disable();关闭中断,这里是GD对于cmsis库的封装,调用了__disabel_irq();函数,关闭了除NMI以外的所以中断

3设置中断向量表,由于中断程序的调用是发生异常以后,服务程序通过中断向量表查找对应的中断程序地址,例如通过查看boot代码的map文件,USART1_IRQ中断服务程序的地址是0X80013B8,发生串口异常以后就会跳转到该地址执行串口中断程序.这里后面也会提到.

由于我们使用bootloader跳转到app,所以有两个中断向量表的存在,一个指向bootloader中的中断,另一个是app的中断,二者不能互相调用.必须设置好

编写中的BUG

FATFS文件读写

这里的话经常会发生莫名其妙的问题,大部分都和f_open以后没有关闭有关,可以在每次烧录完代码以后断电试一试,保持上电状态下,文件的打开状态可能不能变,导致找不到文件或者无法打开

跳转以后的中断无法执行

这里就是中断向量表没有设置的问题

详细的BUG情况,使用串口APP程序的情况下复现,其中bootloader也使用串口,查看其MAP文件,其中串口的中断程序位于0x80013b8 大小0xcc

    [Anonymous Symbol]                       0x080013b8   Section        0  lto-llvm-c8e1a5.o(.text.USART1_IRQHandler)
    0x080013b8   0x080013b8   0x000000cc   Code   RO          366    .text.USART1_IRQHandler  lto-llvm-c8e1a5.o

app的串口中断代码,可以见得两个并不在同一个区域.

    [Anonymous Symbol]                       0x08010838   Section        0  usart.o(.text.USART1_IRQHandler)
0x08010838   0x08010838   0x00000070   Code   RO          103    .text.USART1_IRQHandler  usart.o

开始运行,正常效果是串口打印信息,同时输入内容以后将内容打印出来

但是执行app跳转以后,串口接收时会卡死系统,如果关闭中断,或者关闭串口接收中断,系统正常运行

debug情况,可以看到系统运行到了0x80013b8区域,很明显这个是bootloader的串口中断程序,同时我们使能了串口接收中断,若关闭该中断,则正常运行

关闭中断,系统正常运行在delay延时中,没有执行bootloader的代码,可以由此确定问题出在了访问到了错误的中断程序.

系统执行中断需要查找中断向量表,访问对应flash地址执行代码,所以访问到错误的地址我们可以确认是中断向量表存在问题,结合bootloader的中断向量表情况,确认是在app代码段中,还是bootloader的中断向量表,没有加载app的中断向量表,所以我们手动调用SCB_VTOR寄存器,加载APP的中断向量表

修改以后下载APP,可以看到正确进入中断程序.

其他

keil生成的hex文件不能直接使用,这个是16进制的代码文件,下载的是二进制的代码bin文件,需要使用到hex2bin软件生成对应bin文件.

代码工程打包

链接:https://pan.baidu.com/s/1nvMF6huKfJs5rV021JZnrQ 
提取码:7lek

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值