打印
[活动专区]

【开源活动】-基于国民N32G45x的SD卡IAP升级开发

[复制链接]
606|8
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
uant|  楼主 | 2023-3-23 09:00 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 uant 于 2023-3-23 09:20 编辑

#申请原创#    @安小芯 本次项目完成走了不少坑,也更理解了代码实现的原理,整个过程算是一波三折,不过还是顺利的完成了整个功能的测试,至于遇到的坑在最后经验总结中聊聊。    哪么开始之前,我就先把自己理解的原理讲解下:
    要想实现固件的自升级,就是把新的固件按一个固定的模式写入到Flash中,程序运行会有一个入口,这就是入口地址,而ARM的好像都是从0x8000000这个地址开始,至少我目前用到的都是这样,当我们使用某种方式(编程器或串口烧录)成功把固件从这个起始地址写入到Flash中,哪么就正常来说就可以正常运行。但想要程序运行时把新固件从这个地址会写入到Flash会遇到一个问题,因程序本身也是从0x8000000这个地址运行,运行时又要读取Flash中数据,而向这个地址写入数据的话,就会把本身的运行环境破坏,也就是运行时读取到的数据被改变了,导致程序出错卡死。所以如果是自身更新自身的话,肯定是有问题的,最好的办法就是另一个程序来更新,由更新程序来向另一个地址来写入,这样本身数据不会被破坏,也就能正常更新了。
    通过以上的分析,我们就可以做这样的事情,让一个程序先运行,这个程序呢不干别的事情,仅做更新工作,另一个程序专门负责业务逻辑,当需要更新时,我们就可以跳到专门负责更新的程序中,更新完成后就自动跳到业务逻辑程序中。
    为了方便解说,我们就把负责更新的程序叫做Bootloader,负责业务逻辑的程序叫做APP(这里引用了国民技术官方示例的说法,进行统一方便大家在看官方示例时容易理解)。而我们想要实现SD卡升级功能,就需驱动SD卡,而文件存储在SD卡中都是按二进制方式存储的,想要读取文件升级就需要一个文件系统,也就是文件系统移植,所以本文将从以下几点开始讲解:
1、基于SPI的SD卡驱动的移植;
2、FATFS文件系统的移植;
3、IAP升级的实现
4、使用微信小程序实现IAP升级

然后就是开发环境和硬件了:
# 开发环境
系统: windows 11
开发软件: Keil 5.36
DAP-Link: 板载 NS-Link

# 开发板
N32G45XVL-STB v1.1

一、基于SPI的SD卡驱动的移植

1、基础知识及硬件连接
   在讲SD卡移植之前,我们稍稍提下SD卡方面的内容,我们先来了解下 SD卡驱动的两种方式:  SDIO、SPI,以下是他们的异同点:
【SDIO模式】
    CLK:时钟,通信过程需要的东西,没这个东西,数据会不稳定
    CMD:命令,可下达命令,例如读取SD卡的信息,或是写入数据等等
    DAT0、1、2、3:四条数据线
    VCC、VSS:电源和地

【SPI模式】
    CLK:时钟,理由同SDIO里面的CLK
    MOSI:命令或数据输出到SD卡
    MISO:SD卡传输数据到主机,数据线
    CS:片选,选择是否要操作当前的SD卡    VCC、VSS:电源和地

从上面我们可以看到 SDIO模式有四条数据线,而SPI模式下只有一条数据线 MISO,所以说SDIO速度会优于SPI模式。但是SDIO需要硬件支持,而SPI却更灵活,就算没有硬件SPI,也可以使用多余的IO进行模拟,通用性更好。考虑到后期方便移植,所以本次还是以SPI模式方式来移植,因为仅需要改动比较少的代码就可以在不同芯片上运行起来,更方便和快捷。  
关于这方面的知道,我给出了几个参考的文章的地址:
SD卡移植及时序讲解: https://blog.csdn.net/xiaolong1126626497/article/details/127985503

我使用的是SD卡扩展卡,样式如下:

   这个上面已经标识清楚了SD卡的引脚信息,比较方便连接,如果是SD卡槽或TF卡槽也不要紧,下面也给出连接方法:
   SD卡和卡座



TF卡和卡座


以上图片来源于:https://blog.csdn.net/jiangfutao/article/details/124466153

接下来就是连接扩展板和开发板了,接线方式如下:
开发板        扩展板
PB12    ----  CS
PB13    ----  SCK
PB14    ----  MISO
PB15    ----  MOSI
5V       ----  VCC
GND    ----  GND  
请注意:这里扩展板的VCC一定要连开发板5V,因为扩展板上用的是AMS1117,这个LDO压降比较大,如果使用3.3V供电的话,输出可能降到2.2V,这样可能导致TF卡无法正常工作。

接下来看下连接完成的样子:

好了,开发板连接完成了,下面就可以上电写代码部分了。


2、软件移植
    SD卡驱动编写
#ifndef SD_DRIVER_H_
#define SD_DRIVER_H_

#include "main.h"

// SD SPI初始化定义
#define SD_SPIx                                           SPI2
#define SD_SPI_PERIPH                                RCC_APB1_PERIPH_SPI2
#define SD_SPI_PIN_PORT                        GPIOB
#define SD_SPI_PIN_PERIPH                RCC_APB2_PERIPH_GPIOB
#define SD_SPI_PIN_MISO                        GPIO_PIN_14
#define SD_SPI_PIN_MOSI                        GPIO_PIN_15
#define SD_SPI_PIN_SCK                        GPIO_PIN_13
#define SD_SPI_PIN_CS                                GPIO_PIN_12

//SD卡类型
#define ERR             0x00
#define MMC                                0x01
#define V1                                0x02
#define V2                                0x04
#define V2HC                        0x06

#define DUMMY_BYTE                                 0xFF
#define MSD_BLOCKSIZE                         512

//CMD定义
#define CMD0    0       //卡复位
#define CMD1    1
#define CMD8    8       //命令8 ,SEND_IF_COND
#define CMD9    9       //命令9 ,读CSD数据
#define CMD10   10      //命令10,读CID数据
#define CMD12   12      //命令12,停止数据传输
#define CMD16   16      //命令16,设置扇区大小(SectorSize) 应返回0x00
#define CMD17   17      //命令17,读扇区(sector)
#define CMD18   18      //命令18,读多个扇区(Multi sector)
#define CMD23   23      //命令23,设置多扇区(sector)写入前预先擦除N个block
#define CMD24   24      //命令24,写扇区(sector)
#define CMD25   25      //命令25,写多个扇区(Multi sector)
#define CMD41   41      //命令41,应返回0x00
#define CMD55   55      //命令55,应返回0x01
#define CMD58   58      //命令58,读OCR信息
#define CMD59   59      //命令59,使能/禁止CRC,应返回0x00

//数据写入回应字意义
#define MSD_DATA_OK                0x05
#define MSD_DATA_CRC_ERROR         0x0B
#define MSD_DATA_WRITE_ERROR       0x0D
#define MSD_DATA_OTHER_ERROR       0xFF
//SD卡回应标记字
#define MSD_RESPONSE_NO_ERROR      0x00
#define MSD_IN_IDLE_STATE          0x01
#define MSD_ERASE_RESET            0x02
#define MSD_ILLEGAL_COMMAND        0x04
#define MSD_COM_CRC_ERROR          0x08
#define MSD_ERASE_SEQUENCE_ERROR   0x10
#define MSD_ADDRESS_ERROR          0x20
#define MSD_PARAMETER_ERROR        0x40
#define MSD_RESPONSE_FAILURE       0xFF


#define SD_CS_LOW                        GPIO_ResetBits(SD_SPI_PIN_PORT,SD_SPI_PIN_CS);
#define SD_CS_HIGH                GPIO_SetBits(SD_SPI_PIN_PORT,SD_SPI_PIN_CS);

// SD卡SPI初始化
void SD_SPI_Init(void);
// SD初始化
DSTATUS SD_Init(void);
// 读盘
DRESULT SD_ReadDisk(uint8_t*buf,uint32_t sector,uint8_t cnt);
// 写盘
DRESULT SD_WriteDisk(uint8_t* buf,uint32_t sector,uint8_t cnt);

#endif



代码编写:
// SD_Driver.c
#include "SD_Driver.h"
// SD卡类型
uint8_t SD_TYPE=0x00;
// SPI初始化状态,防止反复进行初始化
uint8_t SD_SPI_Init_Status = 0;
SPI_InitType SD_SPI_Struct;
// SD卡SPI初始化
void SD_SPI_Init(void)
{
        if(SD_SPI_Init_Status)return ;
      
        SD_SPI_Init_Status = 1;
      
        // 开启GPIO和复用时钟
        RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_AFIO | SD_SPI_PIN_PERIPH, ENABLE);
        // 根据配置的SPI对象开启SPI时钟
        if(SD_SPI_PERIPH == RCC_APB2_PERIPH_SPI1){
                RCC_EnableAPB2PeriphClk(SD_SPI_PERIPH, ENABLE);
        }else{
                RCC_EnableAPB1PeriphClk(SD_SPI_PERIPH, ENABLE);
        }
      
        // GPIO初始化
        GPIO_InitType SD_GPIO_Struct;
        GPIO_InitStruct(&SD_GPIO_Struct);
      
        SD_GPIO_Struct.GPIO_Mode                 = GPIO_Mode_AF_PP;
        SD_GPIO_Struct.GPIO_Speed                = GPIO_Speed_50MHz;
        SD_GPIO_Struct.Pin                                = SD_SPI_PIN_MOSI | SD_SPI_PIN_SCK;                              
        GPIO_InitPeripheral(SD_SPI_PIN_PORT,&SD_GPIO_Struct);
      
        SD_GPIO_Struct.GPIO_Mode                = GPIO_Mode_IN_FLOATING;
        SD_GPIO_Struct.Pin                                = SD_SPI_PIN_MISO ;
        GPIO_InitPeripheral(SD_SPI_PIN_PORT,&SD_GPIO_Struct);
      
        SD_GPIO_Struct.GPIO_Mode                = GPIO_Mode_Out_PP;
        SD_GPIO_Struct.Pin                                = SD_SPI_PIN_CS ;
        GPIO_InitPeripheral(SD_SPI_PIN_PORT,&SD_GPIO_Struct);
        SD_CS_HIGH;
      
        // 初始化SPI1
        SPI_InitStruct(&SD_SPI_Struct);
      
        SD_SPI_Struct.DataDirection = SPI_DIR_DOUBLELINE_FULLDUPLEX;                // 工作模式:全双工
        SD_SPI_Struct.SpiMode       = SPI_MODE_MASTER;                                        // SPI主从配置:主机模式
        SD_SPI_Struct.DataLen       = SPI_DATA_SIZE_8BITS;                                        // 数据长度:8bit
        SD_SPI_Struct.CLKPOL        = SPI_CLKPOL_HIGH;                                        // 时钟极性:高
        SD_SPI_Struct.CLKPHA        = SPI_CLKPHA_SECOND_EDGE;                                // 触发沿:第二个边沿触发
        SD_SPI_Struct.NSS           = SPI_NSS_SOFT;                                                // NSS控制:软件控制
        SD_SPI_Struct.BaudRatePres  = SPI_BR_PRESCALER_256;                                // 分频系数
        SD_SPI_Struct.FirstBit      = SPI_FB_MSB;                                                        // 数据传输高位优先
        SD_SPI_Struct.CRCPoly       = 7;                                                                        // CRC检验
        SPI_Init(SD_SPIx, &SD_SPI_Struct);
        // 开启SPI
        SPI_Enable(SD_SPIx,ENABLE);
      
}
// SPI数据读写
uint8_t SPIx_ReadWriteByte(uint8_t byte)
{
        /* 等待数据发送寄存器清空 */
        while (SPI_I2S_GetStatus(SD_SPIx, SPI_I2S_TE_FLAG) == RESET);
         
        /* 通过SPI发送出去一个字节数据 */
        SPI_I2S_TransmitData(SD_SPIx, byte);
         
        /* 等待接收到一个数据(接收到一个数据就相当于发送一个数据完毕) */
        while (SPI_I2S_GetStatus(SD_SPIx, SPI_I2S_RNE_FLAG) == RESET);
         
        /* 返回接收到的数据 */
        return SPI_I2S_ReceiveData(SD_SPIx);
}
// SPI速度设置
void SPIx_SetSpeed(u8 SpeedSet)
{
        SD_SPI_Struct.BaudRatePres = SpeedSet ;
        SPI_Init(SD_SPIx, &SD_SPI_Struct);
        SPI_Enable(SD_SPIx,ENABLE);
}
SD 卡初始化


///////////////////////////////////////////////////////////////
//发送命令,发完释放
//////////////////////////////////////////////////////////////
int SD_sendcmd(uint8_t cmd,uint32_t arg,uint8_t crc)
{
        DRESULT r1;
  uint8_t retry;

  SD_CS_LOW;
        systick_delay_us(20);
  //SD_CS_ON;
        do{
                retry=SPIx_ReadWriteByte(0xFF);
        }while(retry!=0xFF);

  SPIx_ReadWriteByte(cmd | 0x40);
  SPIx_ReadWriteByte(arg >> 24);
  SPIx_ReadWriteByte(arg >> 16);
  SPIx_ReadWriteByte(arg >> 8);
  SPIx_ReadWriteByte(arg);
  SPIx_ReadWriteByte(crc);
  if(cmd==CMD12)SPIx_ReadWriteByte(0xFF);
  do
        {
                r1=SPIx_ReadWriteByte(0xFF);
        }while(r1&0X80);
        
        return r1;
}


//读取指定长度数据
DRESULT SD_ReceiveData(uint8_t *data, uint16_t len)
{

        uint8_t r1;
        SD_CS_LOW;
        
        do
        {
                r1 = SPIx_ReadWriteByte(0xFF);        
                systick_delay_us(100);
        }while(r1 != 0xFE);        
        
        while(len--)
        {
                *data = SPIx_ReadWriteByte(0xFF);
                data++;
        }
        
        SPIx_ReadWriteByte(0xFF);
        SPIx_ReadWriteByte(0xFF);                                                                                                   
        return RES_OK;
}
//向sd卡写入一个数据包的内容 512字节
DRESULT SD_SendBlock(uint8_t* buf,uint8_t cmd)
{        
        uint16_t t;        
        uint8_t r1;        
        do{
                r1=SPIx_ReadWriteByte(0xFF);
        }while(r1!=0xFF);
        
        SPIx_ReadWriteByte(cmd);
        if(cmd!=0XFD)//不是结束指令
        {
                for(t=0;t<512;t++)SPIx_ReadWriteByte(buf[t]);//提高速度,减少函数传参时间
            SPIx_ReadWriteByte(0xFF);//忽略crc
            SPIx_ReadWriteByte(0xFF);
                t=SPIx_ReadWriteByte(0xFF);//接收响应
                if((t&0x1F)!=0x05)return RES_WRPRT;//响应错误                                                                                                                     
        }                                                                                                                                                                       
  return RES_OK;//写入成功
}


//获取CID信息
uint8_t SD_GETCID (uint8_t *cid_data)
{
                uint8_t r1;
          r1=SD_sendcmd(CMD10,0,0x01); //读取CID寄存器
                if(r1==0x00){
                        r1=SD_ReceiveData(cid_data,16);
                }
                SD_CS_HIGH;
                if(r1)return 1;
                else return 0;
}
//获取CSD信息
uint8_t SD_GETCSD(uint8_t *csd_data){
                uint8_t r1;         
    r1=SD_sendcmd(CMD9,0,0x01);//发CMD9命令,读CSD寄存器
    if(r1==0)
        {
            r1=SD_ReceiveData(csd_data, 16);//接收16个字节的数据
    }
        SD_CS_HIGH;//取消片选
        if(r1)return 1;
        else return 0;
}
//获取SD卡的总扇区数
uint32_t SD_GetSectorCount(void)
{
    uint8_t csd[16];
    uint32_t Capacity;  
    uint8_t n;
                uint16_t csize;                                             
        //取CSD信息,如果期间出错,返回0
    if(SD_GETCSD(csd)!=0) return 0;            
    //如果为SDHC卡,按照下面方式计算
    if((csd[0]&0xC0)==0x40)         //V2.00的卡
    {        
                        csize = csd[9] + ((uint16_t)csd[8] << 8) + 1;
                        Capacity = (uint32_t)csize << 10;//得到扇区数                           
    }
                else//V1.XX的卡
    {        
                        n = (csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2;
                        csize = (csd[8] >> 6) + ((uint16_t)csd[7] << 2) + ((uint16_t)(csd[6] & 3) << 10) + 1;
                        Capacity= (uint32_t)csize << (n - 9);//得到扇区数   
    }
    return Capacity;
}
//
u32 GetSDCardSectorCount(void)
{
    u8 csd[16];
    u32 Capacity=0;  
          u16 csize;
                //获取SD卡的CSD信息,包括容量和速度信息,存放CID的内存,至少16Byte
                SD_sendcmd(CMD9,0,0x01);//发SDCard_CMD9命令,读CSD
          SD_ReceiveData(csd,16);//接收16个字节的数据
                SD_CS_HIGH;//取消片选
                SPIx_ReadWriteByte(0xff);//提供额外的8个时钟
    if((csd[0]&0xC0)==0x40)  //SDHC卡,按照下面方式计算
    {        
                        csize=csd[9]+(csd[8]<<8)+1;
                        Capacity=csize<<10;//得到扇区数                           
    }
    return Capacity;
}

// SD卡初始化
DSTATUS SD_Init(void)
{
        /*1. 初始化底层IO口*/
        SD_SPI_Init();
        
        // 设置SPI速度,初始化时速度尽量低,防止初始化失败,初始化完成后尽量提高速度
        SPIx_SetSpeed(SPI_BR_PRESCALER_256);

        SD_CS_LOW;
        /*2. 发送最少74个脉冲*/
         for(uint8_t i=0;i<10;i++)SPIx_ReadWriteByte(0xFF);
        
        
        /*3. 进入闲置状态*/
        uint8_t r1;
        do{
                r1 = SD_sendcmd(CMD0 ,0, 0x95);        
        }while(r1!=0x01);
        
        
        uint8_t buff[6] = {0};
        uint16_t retry;
        
        /*4. 鉴别SD卡类型*/
        SD_TYPE = 0;
        if(SD_sendcmd(CMD8,0x1AA,0x87)==0x01)
        {
                for(uint8_t i=0;i<4;i++)buff[i]=SPIx_ReadWriteByte(0xFF);        //Get trailing return value of R7 resp
                //printf("R7 Resp:%02X %02X %02X %02X\n",buff[0],buff[1],buff[2],buff[3]);
                if(buff[2] == 0x01 && buff[3] == 0xAA)
                {
                        do
                        {
                                SD_sendcmd(CMD55,0,0x01);                    //发送SDCard_CMD55
                                r1=SD_sendcmd(CMD41,0x40000000,0x01);//发送SDCard_CMD41
                        }while(r1);

                        if(SD_sendcmd(CMD58,0,0x01)==0)                //鉴别SD2.0卡版本开始
                        {
                                for(uint8_t i=0;i<4;i++)buff[i]=SPIx_ReadWriteByte(0XFF);//得到OCR值
                                
                                if(buff[0]&0x40)
                                {
                                        SD_TYPE=V2HC;
                                }
                                else
                                {
                                        SD_TYPE=V2;
                                }
                        }
                }
                else
                {
                        SD_sendcmd(CMD55,0,0X01);                        //发送CMD55
                        r1=SD_sendcmd(CMD41,0,0X01);        //发送CMD41
                        if(r1<=1)
                        {               
                                SD_TYPE=V1;
                                retry=0XFFFE;
                                do //等待退出IDLE模式
                                {
                                        SD_sendcmd(CMD55,0,0X01);        //发送CMD55
                                        r1=SD_sendcmd(CMD41,0,0X01);//发送CMD41
                                }while(r1&&retry--);
                        }else//MMC卡不支持CMD55+CMD41识别
                        {
                                SD_TYPE=MMC;//MMC V3
                                retry=0XFFFE;
                                do //等待退出IDLE模式
                                {                                                                                            
                                        r1=SD_sendcmd(CMD1,0,0X01);//发送CMD1
                                }while(r1&&retry--);  
                        }
                        if(retry==0||SD_sendcmd(CMD16,512,0X01)!=0)SD_TYPE=ERR;//错误的卡
                }
        }

        char cardType[10]={0};
        switch(SD_TYPE)
        {
                case MMC:{ strcpy(cardType,"MMC");break;}
                case V1:{ strcpy(cardType,"SDV1");break;}
                case V2:{ strcpy(cardType,"SDV2");break;}
                case V2HC:{ strcpy(cardType,"SDHC");break;}
                default:{ strcpy(cardType,"ERROR");break;}
        }
        printf("CartType=%s\n",cardType);
        
        SD_CS_HIGH;                //取消片选
         SPIx_ReadWriteByte(0xff);//提供额外的8个时钟
        SPIx_SetSpeed(SPI_BR_PRESCALER_8);

        if(SD_TYPE)return FR_OK;
        else return FR_INT_ERR;
}
至此,主要的SPI工作就完成了,我们回到 main.h中来测试下能不能成功驱动起来
// main.h
#include "main.h"
#include "log.h"
#include "SD_Driver.h"

int main(void)
{
        log_init();
  SD_Init();

  // 获取扇区数和大小
  uint32_t Sector = SD_GetSectorCount();
  printf("SectorCount=%d Size=%.2fMB\n",Sector,(Sector/(1024*1024.0f))*MSD_BLOCKSIZE);

  while(1)
  {

  }

}

我的是32G的TF卡,实际打印消息如下:


如果出现这样的信息,就说明我们SD卡已经初始化成功了,接下来就可以愉快的进行文件系统的移植了!

2、FatFS文件系统的移植
1)配置说明
    因为最终我们是希望实现在SD卡中拷入固件文件方式进行升级,而我们之前配置的并不能获取到文件系统的信息,就是无法直接通过文件名来访问到文件,所以接下来我们就要移植下文件系统,这里使用的是目前嵌入式系统广泛使用的FatFS文件系统。
    开始移植前可能是一头雾水,不知道从哪开始,要做些什么!其实,如果能明白FatFS移植要处理些什么,哪移植起来就非常的简单,如果不明白,哪就是非常的艰难,我因为没有找到一篇非常清楚讲解移植要做些什么,所以花了差不多三天时间才算是搞定,因为从各种各样的资料中获取到自己所需的信息,花费了太多查找资料的时间。
    请看下图:
   
    从上图中,我们可以看到,移植就是需要处理状态获取、进**初始化、然后实现读与写,移植过程其实就是需要实现图上的几个接口的功能就可以了,剩下的工作就交给FatFS。所以说会者不难,难者不会,就是这个道理。
  2)开始配置
    现在我们就开始移植吧,首先我们访问 fatfs 的官方网站: http://elm-chan.org/fsw/ff/00index_e.html,将页面滚动到最后,找到箭头所指的位置,点击后可以跳到所有发布的下载地址页面:

我这里显示的最新版本是 FatFs R0.15 (跳到这个页面的原理就是怕之后版本会有变化,而这里会有所有版本,以免版本不一致导致报错等情况的出现)

   
下载后解压,可以看到 documents(文档目录)和source(源码目录),而source下就是我们需要的文件内容



在我们工程目录下新建一个文件夹 FATFS,将 source 目录下所有的.h .c文件复制过去,然后在Keil5中将新建一个组 FATFS,将所有的 .c 加进去,并且在配置里将头文件路径中将FATFS加入,以下是配置细节:







最后点 Ok,软件部分就配置完了。

  3)移植及配置
// ffconf.h 文件
// 文件系统配置

// 是否启用只读  0不启用
#define FF_FS_READONLY        0

// 是否启用 格式化 改为1 启用
#define FF_USE_MKFS                1

// 是否使用启用 seek 改为1 启用
#define FF_USE_FASTSEEK        1

// 字符编码 包含中文所以使用936
#define FF_CODE_PAGE        936

// 是否开始长文件名 ,根据自己需要,启用后固件会成倍增加
#define FF_USE_LFN                0

// 驱动器号 我们这里使用SD卡,所以改为2
#define FF_VOLUMES                2

// 无RTC,本次没有配置RTC,所以改为1
#define FF_FS_NORTC                1

// 是否启用 exFat 支持,启用后必须开启 FF_USE_LFN
#define FF_FS_EXFAT                0


根据前面思维导图中说明,我们需要实现一些初始化函数,而有些我们已经处理了,接着就要驱动文件中添加以下方法及实现了:
// SD_Driver.h
// SD_Driver.h 添加以下声明


// FatFs文件系统初始化
DSTATUS SD_disk_initialize(BYTE pdrv);
// FatFs文件系统状态
DSTATUS SD_disk_status(BYTE pdrv);
// FatFs文件系统ioctl
DRESULT SD_disk_ioctl(BYTE pdrv,BYTE cmd,void *buff);


// SD_Driver.c

// SD_Driver.c 添加以下内容


DSTATUS SD_disk_status(BYTE pdrv)
{
DSTATUS stat;

uint8_t ciddata[16]= {0};
if(SD_GETCID(ciddata)) //调用SD卡的获取设备状态函数接口
stat = RES_ERROR;
else
stat = RES_OK;

return stat;
//return STA_NOINIT;/* Drive not initialized */
}


/*@pdrv Physical drive nmuber to identify the drive */
DSTATUS SD_disk_initialize (BYTE pdrv)
{
DSTATUS stat;
uint8_t ciddata[16]= {0};
SD_Init();//初始化SD卡
if(SD_GETCID(ciddata)) //重新获取一下SD卡的ID,看是否初始化成功
stat = RES_ERROR;
else
stat = RES_OK;

return stat;

}


DRESULT SD_disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT res;
// if(pdrv == DEV_SD)
// {
switch (cmd)
{
case CTRL_SYNC:
res = RES_OK;
break;
case GET_SECTOR_SIZE:
*(DWORD*)buff = 512; //每个扇区的大小为512字节
res = RES_OK;
break;
case GET_BLOCK_SIZE:
*(WORD*)buff = 8;
res = RES_OK;
break;
case GET_SECTOR_COUNT:
*(DWORD*)buff = SD_GetSectorCount();//调用获取SD卡扇区个数的函数接口
res = RES_OK;
break;
default:
res = RES_PARERR;
break;
}
//}
if(res)
return RES_PARERR;
return res;
}


// 配置 diskio.c

// 配置 diskio.c


#include "ff.h"                        /* Obtains integer types */
#include "diskio.h"                /* Declarations of disk functions */
#include "SD_Driver.h"


/* Definitions of physical drive number for each drive */
#define DEV_RAM                0        /* Example: Map Ramdisk to physical drive 0 */
#define DEV_MMC                1        /* Example: Map MMC/SD card to physical drive 1 */
#define DEV_USB                2        /* Example: Map USB MSD to physical drive 2 */


/*-----------------------------------------------------------------------*/
/* Get Drive Status                                                      */
/*-----------------------------------------------------------------------*/

DSTATUS disk_status (
        BYTE pdrv                /* Physical drive nmuber to identify the drive */
)
{
        DSTATUS stat;
        int result;

        switch (pdrv) {
        case DEV_RAM :
                //result = RAM_disk_status();

                // translate the reslut code here

                return stat;

        case DEV_MMC :
                result = SD_disk_status(pdrv);

                // translate the reslut code here

                return result;

        case DEV_USB :
                //result = USB_disk_status();

                // translate the reslut code here

                return stat;
        }
        return STA_NOINIT;
}



/*-----------------------------------------------------------------------*/
/* Inidialize a Drive                                                    */
/*-----------------------------------------------------------------------*/

DSTATUS disk_initialize (
        BYTE pdrv                                /* Physical drive nmuber to identify the drive */
)
{
        DSTATUS stat;
        int result;

        switch (pdrv) {
        case DEV_RAM :
                //result = RAM_disk_initialize();

                // translate the reslut code here

                return stat;

        case DEV_MMC :
                result = SD_disk_initialize(pdrv);

                // translate the reslut code here

                return result;

        case DEV_USB :
                //result = USB_disk_initialize();

                // translate the reslut code here

                return stat;
        }
        return STA_NOINIT;
}



/*-----------------------------------------------------------------------*/
/* Read Sector(s)                                                        */
/*-----------------------------------------------------------------------*/

DRESULT disk_read (
        BYTE pdrv,                /* Physical drive nmuber to identify the drive */
        BYTE *buff,                /* Data buffer to store read data */
        LBA_t sector,        /* Start sector in LBA */
        UINT count                /* Number of sectors to read */
)
{
        DRESULT res;
        int result;

        switch (pdrv) {
        case DEV_RAM :
                // translate the arguments here

                //result = RAM_disk_read(buff, sector, count);

                // translate the reslut code here

                return res;

        case DEV_MMC :
                // translate the arguments here

                result = SD_ReadDisk(buff, sector, count);

                // translate the reslut code here

                return result;

        case DEV_USB :
                // translate the arguments here

                //result = USB_disk_read(buff, sector, count);

                // translate the reslut code here

                return res;
        }

        return RES_PARERR;
}



/*-----------------------------------------------------------------------*/
/* Write Sector(s)                                                       */
/*-----------------------------------------------------------------------*/

#if FF_FS_READONLY == 0

DRESULT disk_write (
        BYTE pdrv,                        /* Physical drive nmuber to identify the drive */
        const BYTE *buff,        /* Data to be written */
        LBA_t sector,                /* Start sector in LBA */
        UINT count                        /* Number of sectors to write */
)
{
        DRESULT res;
        int result;

        switch (pdrv) {
        case DEV_RAM :
                // translate the arguments here

                //result = RAM_disk_write(buff, sector, count);

                // translate the reslut code here

                return res;

        case DEV_MMC :
                // translate the arguments here

                result = SD_WriteDisk((uint8_t *)buff, sector, count);

                // translate the reslut code here

                return result;

        case DEV_USB :
                // translate the arguments here

                //result = USB_disk_write(buff, sector, count);

                // translate the reslut code here

                return res;
        }

        return RES_PARERR;
}

#endif


/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions                                               */
/*-----------------------------------------------------------------------*/

DRESULT disk_ioctl (
        BYTE pdrv,                /* Physical drive nmuber (0..) */
        BYTE cmd,                /* Control code */
        void *buff                /* Buffer to send/receive control data */
)
{
        DRESULT res;
        int result;

        switch (pdrv) {
        case DEV_RAM :

                // Process of the command for the RAM drive

                return res;

        case DEV_MMC :

                // Process of the command for the MMC/SD card

                return res;

        case DEV_USB :

                // Process of the command the USB drive

                return res;
        }

        return RES_PARERR;
}

我们来测试下,回到 main.h
#include "main.h"
#include "log.h"
#include "SD_Driver.h"

int main(void)
{
        log_init();
  
        FATFS *fs;
        fs = malloc(sizeof(FATFS));
        FRESULT fr = f_mount(
                                                fs,                        // FATFS对象
                                                "1:",                // 挂载的盘符 相当于win系统下的 C:
                                                1                                // 挂载选项 0 延迟挂载 1立即挂载
                                        );        
        if(fr == FR_OK)
        {
                printf("挂载SD成功.\n");
        }else if(fr == FR_NO_FILESYSTEM)
        {
                // 没有文件系统
                printf("没有文件系统,开始格式化 ...\n");
               
                BYTE work[FF_MAX_SS]; /* Work area (larger is better for processing time) */
                fr = f_mkfs("1:", 0, work, sizeof work);
                printf("格式化结果: %s %d\r\n", fr==FR_OK?"成功":"失败",fr);        
        }
        
        // 开始读写文件测试
        char path[] = "1:test.txt";                        // 同样1:是盘符
        char data[] = "Write Data Content.";
        
        // 写入文件测试
        FIL fil;
        fr = f_open(&fil,path,FA_CREATE_ALWAYS | FA_WRITE);
        if(fr == FR_OK)
        {
                UINT br = 0;
                f_write(&fil,data,sizeof(data),&br);
                f_close(&fil);
               
                printf("创建文件成功,写入字节数: %d\n",br);
        }else{
                printf("创建文件失败\n");
        }
        
        // 读取文件测试,读取刚写入的文件内容
        fr = f_open(&fil,path,FA_READ);
        if(fr == FR_OK)
        {
                UINT br = 0;
                char tmp[100]={0};
               
                f_read(&fil,tmp,100,&br);
                f_close(&fil);
               
                printf("读取文件成功,文件内容: %s\n",tmp);
        }else{
                printf("打开文件失败\n");
        }
        
        // 删除文件
        fr = f_unlink(path);
        printf("删除文件结果: %s\n",fr == FR_OK?"成功":"失败");
        
  while(1)
  {

  }

}



实际执行结果:



三、IAP升级实现
    之前我们也分析了下,想要实现IAP,就需要两套程序,一套专门负责升级的叫Bootloader,一套负责业务逻辑的叫APP。而即然是两套程序,都写入到MCU的Flash中,地址肯定要不一样,不然不就后面一个把前面的内容给覆盖了,你说是不是。所以这里就需要对空间进行分配,并且可以方便的进行程序间隔离,相互不影响。
    这里用到的开发板是 N32G45XVL-STB v1.1,芯片型号是 N32G457VEL7,Flash 512K (16进制表示 0x80000 ),内存144K (16进制表示 0x24000 )。给Bootloader 划分 16K的存储空间和16K的运行内存,剩余的空间和内存分配给APP。因分配给Bootloader空间有限,所以就没有办法实现非常复杂的功能,这里编写时需要注意。
   


    下面就要编写相应的程序了,为了便于观察,我们使用了板载灯和串口打印信息做为提示,当板载PB5亮起时,表示正在升级程序,当板载PA8闪烁时,表示成功升级并跳到了APP中。


1)Bootloader编写
    IROM1(Flash 存储空间) 起始地址是 0x8000000,Bootloader是需要先启动,所以他的起始地址就是 0x8000000 , 16K 就是 0x4000,Size 填入 0x4000。RAM(内存) 起始地址是 0x20000000,16K 是 0x4000,Size 填入 0x4000,Bootloader在Keil5中配置如下:

Bootloader keil5配置


    我们先来编写IAP程序,处理Flash写入、跳转等工作:
iap.h
#ifndef IAP_H_
#define IAP_H_

#include "main.h"

#define FLASH_APP_BASE_ADDR                 0x08004000                                // Bootloader 预留16K空间,APP程序从0x08004000开始                                                   
#define FLASH_START_ADDR        FLASH_APP_BASE_ADDR
#define app_update_flag_addr    0x08040000-4              // APP更新标志,存在BOOT,注意不能覆盖BOOT代码

typedef  void (*iapfun)(void);                                                                        // 定义一个函数类型的参数.           
void iap_load_app(u32 appxaddr);                                                                // 跳转到APP程序执行
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 applen);                // 在指定地址开始,写入bin
void IAP_UPDATE_APP(void);
int32_t app_flag_write(uint32_t data ,uint32_t start_add);


#endif


iap.c

#include "iap.h"

#include "string.h"

iapfun jump2app;  
uint8_t uart_receiveBIN_ok;

uint8_t pages_number = 0;
uint32_t ready_write_addr = 0;

uint8_t flash_buf[];
uint8_t receive_app_done;

__asm void MSR_MSP(u32 addr)
{
    MSR MSP, r0                         //set Main Stack value
    BX r14
}

/**================================================================
                APP 跳转
                appxaddr:用户代码起始地址.
================================================================*/
void iap_load_app(u32 appxaddr)
{
        if(((*(vu32*)appxaddr)&0x0FFFFFFF) < 1024*512)                // 检查栈顶地址是否合法.
        {
                jump2app = (iapfun)*(vu32*)(appxaddr+4);                                
                MSR_MSP(*(vu32*)appxaddr);                                                // 初始化堆栈指针
                jump2app();                                                                                // 跳转到APP.
        }
}               
/**================================================================
================================================================*/
int32_t app_flag_write(uint32_t data ,uint32_t start_add)
{
        FLASH_Unlock();
        //
        FLASH_EraseOnePage(start_add);                        //写之前先擦一遍,每次擦2K
        if (FLASH_COMPL != FLASH_ProgramWord(start_add, data))                //写
        {
                FLASH_Lock();
                //printf("flash write fail! \r\n");
                return 1;
        }
        FLASH_Lock();
    return 0;
}
/**================================================================
================================================================*/
#define FLASH_PAGE_SIZE                 2048                                 

/**
* [url=home.php?mod=space&uid=247401]@brief[/url]
* @param void
* @return
* - `SUCCESS: 表示操作成功
* - 其它值表示出错
*/
int32_t app_flash_write(uint32_t *data ,uint32_t Flash_address)
{
    uint32_t i;
        uint32_t start_add;
        start_add = Flash_address;
               
        FLASH_Unlock();
        //
        for(i = 0;i<FLASH_PAGE_SIZE/FLASH_PAGE_SIZE;i++)
        {
                FLASH_EraseOnePage(start_add+i*FLASH_PAGE_SIZE);                        //写之前先擦一遍,每次擦2K
        }
        //
        for(i=0;i<FLASH_PAGE_SIZE/4 ;i++)
        {
                if (FLASH_COMPL != FLASH_ProgramWord(start_add+i*4, data[i]))                //写
                {
                        FLASH_Lock();
                        //printf("flash write fail! \r\n");
                        receive_app_done = 0;
                        return 1;
                }
        }
        FLASH_Lock();
    return 0;
}
/**================================================================
                //升级APP
================================================================*/
void IAP_UPDATE_APP(void)
{
        ready_write_addr = FLASH_APP_BASE_ADDR + pages_number*2048;
        //
        while(app_flash_write((uint32_t *)flash_buf , ready_write_addr));                //IAP每次升级2K
        //
        memset(flash_buf,0x00,2048);
        pages_number++;

}


    编写完成后,我们回到 main.c 中完成逻辑部分:
main.c
#include "main.h"
#include "SD_Driver.h"
#include "iap.h"


// 文件盘符
#define DRIVE_LETTER        "1:"
#define FLASH_PAGE_SIZE                2048

__IO uint32_t count_time = 0;

void IAP_Upgrade(char *path);
extern int32_t app_flash_write(uint32_t *data ,uint32_t Flash_address);

/**================================================================
                读取Flash
================================================================*/
uint32_t FLASH_ReadWord(uint32_t address)
{
    return *(__IO uint32_t*)address;
}

// 初始化PB5做为升级指示
void LED_Init(void);
#define LED_ON        GPIO_SetBits(GPIOB,GPIO_PIN_5)
#define LED_OFF        GPIO_ResetBits(GPIOB,GPIO_PIN_5)

int main(void)
{
  // 检查标志位,判断有没有app,如果有,直接跳转到app中
  if(FLASH_ReadWord(app_update_flag_addr) == 0x12345678)
  {
    // 跳转前设置中断向量表
    SCB->VTOR = FLASH_START_ADDR;
    // 跳转到APP起始地址,期间不能被其他中断打断,否则会跳转失败
    iap_load_app(FLASH_START_ADDR);        
               
    return 0;
  }
        
  // 没有app 则进行升级,升级请将固件拷到SD卡中,插入到卡槽中
        
  log_init();
  LED_Init();
  printf("\n");
  printf("Start running Bootloader.\n");
               
        
  // 传入要升级的文件名称
  IAP_Upgrade("project.bin");

        
        
  while(1)
  {
    // 升级失败,PB5快闪
    systick_delay_ms(100);
    systick_delay_ms(100);
    if(GPIO_ReadOutputDataBit(GPIOB,GPIO_PIN_5) == RESET)
    {
      LED_ON;
    }else{
      LED_OFF;
    }
  }
}

void LED_Init(void)
{
  RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB,ENABLE);
        
  GPIO_InitType GPIO_InitStrucetre;
  GPIO_InitStruct(&GPIO_InitStrucetre);
        
  GPIO_InitStrucetre.GPIO_Mode                = GPIO_Mode_Out_PP;
  GPIO_InitStrucetre.Pin                                        = GPIO_PIN_5;
  GPIO_InitPeripheral(GPIOB,&GPIO_InitStrucetre);

}

void IAP_Upgrade(char *path)
{
  char _path[50]={0};
  // 判断是否有盘符,没有则加上盘符
  if(strncmp(path,DRIVE_LETTER,strlen(DRIVE_LETTER)) != 0)
  {
    sprintf(_path,"%s%s",DRIVE_LETTER,path);
  }else{
    strcpy(_path,path);
  }
        
        
  FATFS *fs;
  fs = malloc(sizeof (FATFS));           /* Get work area for the volume */
  FRESULT res=f_mount(fs,DRIVE_LETTER,1);                //挂载
        
  if(res == FR_OK)
  {
    printf("SD Mount Ok.\n");
               
    // 开始检查是否存在指定的文件
    FILINFO fno;
    FRESULT fr = f_stat(_path,&fno);
    if(fr != FR_OK)
    {
      printf("Upgrade file not found\n");
      return;
    }
        
  }else{
    printf("SD Not Mount.\n");
    return;
  }
        
  printf("Upgrade Start ... \n");
        
  LED_ON;
        
  // 写入Flash页码
  uint16_t pages_number = 0;
  // 读写Flash地址
  uint32_t ready_write_addr = 0;
  char buff[FLASH_PAGE_SIZE] ;
  UINT br = 0;
        
  FIL fil;  
  FRESULT fr = f_open(&fil, _path, FA_READ);
  if(fr == FR_OK)
  {
    do{
      memset(buff,0,FLASH_PAGE_SIZE);
      f_read(&fil,buff,FLASH_PAGE_SIZE,&br);
      if(br > 0)
      {
        ready_write_addr = FLASH_APP_BASE_ADDR + pages_number*FLASH_PAGE_SIZE;
        while(app_flash_write((uint32_t *)buff , ready_write_addr));                //IAP每次升级2K
        pages_number++;
      }
    } while(br);
               
    f_close(&fil);
               
    if(pages_number > 0)
    {
      printf("Upgrade completed, start jumping to app.\n\n\n");
                        
      // 升级完成操作 释放GPIOB
      GPIO_DeInit(GPIOB);
     // 写IAP升级标志
     app_flag_write(0x12345678 ,app_update_flag_addr);
                        
     //NVIC_SystemReset(); // 复位
    // 跳转前设置中断向量表
    SCB->VTOR = FLASH_START_ADDR;        
    // 跳转到APP起始地址,期间不能被其他中断打断,否则会跳转失败                        
    iap_load_app(FLASH_START_ADDR);                                
   }
  }else{
    printf("File opening failed\n");
  }
}



   
    以上程序编写完成后可以直接烧入开发板中,因为此时没有拷入升级文件,所以PB5灯应该会快闪才对,因为没升级文件无法升级。


2)APP(业务逻辑)程序编写

    Flash总大小 0x80000 被Bootloader占用了16K 就剩下了 0x7C000,内存总大小 0x24000,剩余就是0x20000,并且之前空间已经被占用,所以新的超始地址就需要向后偏移相应的大小,以下就是App的配置:

app keil5配置

    我们可以看到IROM1的起始地址变成了 0x8004000,就是0x8000000+0x4000(Bootloader的大小),大小要减去16K变成了 0x7C000,就是0x80000-0x4000,IRAM1也是一样的道理。

    写app程序之前,我们要先了解一个知识点:中断向量表。

    什么是中断向量表?
      中断向量表就是中断向量的列表。
      中断向量表在内存中保存,其中存放着中断源(中断向量号或者中断类型号)所对应的中断处理程序的入口地址。
      一个中断源对应一个中断处理程序,这种关系索引表,就是中断向量表。


    提到这个的原因就是因为我们app开始时的中断向量表地址是  0x08000000,而我们app程序整体向后移了16K,如果我们app程序中有用到中断功能,哪么如果还是指向之前的中断向量表的地址,就会无法执行或直接卡死,因为这个地址已经变了,但我们没有去更新。在写Bootloader程序时,大家应该也注意到了跳转app前设置中断向量表,虽然这里设置了,但只能影响跳转时的,在进入APP又会变成无效了,这是为什么呢?
    因为啊,在进入程序前,会有个 SystemInit() 初始化系统时钟,而这里又设置了中断向量表,导致我们跳转前设置的又变成了无效。不信的话,大家可以编写一个简单的定时器程序,做一个更新中断,在初始化定时前和之后都打印一条消息,我们会发现,一定初始化定时器程序后,立马就卡死了,就是因为此地址不对。
    初始化时钟的程序在 system_n32g45x.c 文件中,在此文件里找到 void SystemInit(void) 函数,在函数末尾我们可以看到这样一个配置(红框中):

    可以看到这里又重置了中断向量表的地址,FLASH_BASE 是Flash的起始地址,也就是 0x08000000 再与上了一个偏移地址 VECT_TAB_OFFSET ,我们点右键跳转到他的定义,就会发现 VECT_TAB_OFFSET 配置的地址是 0x00,所以才会出现这样的原因,此时我只需要把 VECT_TAB_OFFSET 的值改为 0x4000,就可以了,就像这样(注意红框中值变化):


    需要注意的地方已经讲完了,剩余的就是正常编写程序就可以了,我这里为了方便演示,我就做了一个点灯和定时器,正常运行APP后,我们点亮板载的PA8,并闪烁,初始化CountTime 1ms更新一次的定时器,并且每隔500ms打印一条消息。APP部分也是基于之前的SD卡程序演示文件的读写并打印消息,方便观察运行情况。为了在进入app后也能升级,我们添加了一个按键,使用的就是 开发板上的WAKEUP按键,当我们将有固件的TF卡插入后,我们点击这个按键就会开始升级。
    注意:这里为了简单点,把固件名称固定为 “ Project.bin ”,Bootloader就是读取这个文件就可以升级。升级现象就是:当按下WAKEUP按键后,PB5灯先亮起,开始升级,升级完成后会熄灭,之后跳转到APP中,PA8 亮起并闪烁,整个升级过程完成。
    好了,打开main.c 进行开始编写代码:

main.c
#include "main.h"
#include "SD_Driver.h"
#include "iap.h"

extern int32_t app_flash_write(uint32_t *data ,uint32_t Flash_address);

void SD_Test(void);

// 按键软件防抖配置
#define KEY_LOCK_TIME                50
uint32_t KeyPressTime = 0;
// 按键初始化
void Key_Init(void);

// LED初始化
void LED_Init(void);
#define LED_ON        GPIO_SetBits(GPIOA,GPIO_PIN_8)
#define LED_OFF        GPIO_ResetBits(GPIOA,GPIO_PIN_8)

int main(void)
{
  log_init();
  LED_Init();
  CountDown_Init();
  Key_Init();
        
  printf("App Init Ok.\n");
        
  LED_ON;
  
  // SD卡测试
  SD_Test();
        
  uint32_t t = count_time;
  while(1)
  {
    if(count_time - t>= 500)
    {
      t = count_time;
                        
      printf("Test Msg, CountTime=%d\n",count_time);
                        
      if(GPIO_ReadOutputDataBit(GPIOA,GPIO_PIN_8) == RESET)
      {
        LED_ON;
      }else{
        LED_OFF;
      }
    }
               
    // USART接收数据打印
    if(USART_Recv_Flag)
    {
      printf("Recv: %s\n",USART_RecvBuff);
                        
      USART_Recv_Flag = 0;
      USART_Recv_Size = 0;
      memset(USART_RecvBuff,0,USART_MAX_SIZE);
    }
  }

}

void SD_Test(void)
{
        
    FATFS *fs;
    fs = malloc(sizeof(FATFS));
    FRESULT fr = f_mount(
            fs,                        // FATFS对象
            "1:",                // 挂载的盘符 相当于win系统下的 C:
            1                                // 挂载选项 0 延迟挂载 1立即挂载
        );        
    if(fr == FR_OK)
    {
      printf("挂载SD成功.\n");
    }else if(fr == FR_NO_FILESYSTEM)
    {
      // 没有文件系统
      printf("没有文件系统,开始格式化 ...\n");
               
      BYTE work[FF_MAX_SS]; /* Work area (larger is better for processing time) */
      fr = f_mkfs("1:", 0, work, sizeof work);
      printf("格式化结果: %s %d\r\n", fr==FR_OK?"成功":"失败",fr);        
    }else{
       printf("SD卡挂载失败.\n");
       return;
    }
        
    // 开始读写文件测试
    char path[] = "1:test.txt";                        // 同样1:是盘符
    char data[] = "Write Data Content.";
        
    // 写入文件测试
    FIL fil;
    fr = f_open(&fil,path,FA_CREATE_ALWAYS | FA_WRITE);
    if(fr == FR_OK)
    {
      UINT br = 0;
      f_write(&fil,data,sizeof(data),&br);
      f_close(&fil);
               
      printf("创建文件成功,写入字节数: %d\n",br);
    }else{
      printf("创建文件失败\n");
    }
        
    // 读取文件测试,读取刚写入的文件内容
    fr = f_open(&fil,path,FA_READ);
    if(fr == FR_OK)
    {
      UINT br = 0;
      char tmp[100]={0};
               
      f_read(&fil,tmp,100,&br);
      f_close(&fil);
               
      printf("读取文件成功,文件内容: %s\n",tmp);
    }else{
      printf("打开文件失败\n");
    }
        
    // 删除文件
    fr = f_unlink(path);
    printf("删除文件结果: %s\n",fr == FR_OK?"成功":"失败");
        
}


// 按键初始化
void Key_Init(void)
{
        RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA,ENABLE);
        
        GPIO_InitType  KeyGpio_InitStruct;
        GPIO_InitStruct(&KeyGpio_InitStruct);
        
        KeyGpio_InitStruct.GPIO_Mode        = GPIO_Mode_IN_FLOATING;
        KeyGpio_InitStruct.Pin                                = GPIO_PIN_0;
        GPIO_InitPeripheral(GPIOA,&KeyGpio_InitStruct);
        
        // 中断线配置
        GPIO_ConfigEXTILine(GPIOA_PORT_SOURCE,GPIO_PIN_SOURCE0);
        
        // 按键中断触发方式
        EXTI_InitType   Exti_InitStruct;
        Exti_InitStruct.EXTI_Line                        = EXTI_LINE0;
        Exti_InitStruct.EXTI_LineCmd        = ENABLE;
        Exti_InitStruct.EXTI_Mode                        = EXTI_Mode_Interrupt;
        Exti_InitStruct.EXTI_Trigger        = EXTI_Trigger_Falling;
        EXTI_InitPeripheral(&Exti_InitStruct);
        
        // 配置中断
        NVIC_InitType NVIC_InitStructure;

        NVIC_InitStructure.NVIC_IRQChannel                   = EXTI0_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x05;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0x0F;
        NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
        NVIC_Init(&NVIC_InitStructure);
        
}


void EXTI0_IRQHandler(void)
{
  if(EXTI_GetITStatus(EXTI_LINE0) != RESET)
  {
    EXTI_ClrITPendBit(EXTI_LINE0);
               
    // 软件防抖
    if( GPIO_ReadInputDataBit(GPIOA,GPIO_PIN_0) != RESET || count_time - KeyPressTime <= KEY_LOCK_TIME)return;
    KeyPressTime = count_time;
               
    printf("Key Press\n");
               
    // 跳转前将中断向量表改回去
    SCB->VTOR = FLASH_BASE;
    // 清除升级标志
    app_flash_write(0x00,app_update_flag_addr);
    // 复位,开始升级
    NVIC_SystemReset();
  }
}


void LED_Init(void)
{
    RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA,ENABLE);
        
    GPIO_InitType GPIO_InitStrucetre;
    GPIO_InitStruct(&GPIO_InitStrucetre);
        
    GPIO_InitStrucetre.GPIO_Mode                = GPIO_Mode_Out_PP;
    GPIO_InitStrucetre.Pin                                        = GPIO_PIN_8;
    GPIO_InitPeripheral(GPIOA,&GPIO_InitStrucetre);
        
}


    至此,本次项目已经全部完成了,并且附上完整实现代码,希望能给大家带来帮助。
    最后一部分我也准备了真实应用实例,通过微信小程序来实现升级,有兴趣可以继续阅读下一部分内容。

四、使用微信小程序实现IAP升级

    前言:微信是目前使用最广泛的通讯方式了,其推出的小程序也简单易用,如果升级能结合微信小程序来实现,则能极快的减少升级成本,仅需要把固件通过微信下发,客户就可以直接使用手机进行升级,这将是一个非常快捷且节约成本的升级方案了,再也不用千里迢迢跑到客户哪边去升级了。
    所以,接下来做的就是实现微信小程序的升级功能,本次实例的方案使用的是 AT固件的蓝牙模块,这样移植起来非常简便,不过要求支持透传功能,下面介绍下使用到的蓝牙模块:
蓝牙模块:集芯微 G75-C2G4A12S3a
类型:AT固件,支持透传
价格:不到4元
优势:价格便宜,使用简单,容易购买到
缺点:AT固件只能发送字符型,无法发送二进制数据,因为遇到\0就会截断,该模块单包数据有限制,实测 96字节可以发,128字节模块直接死掉,应该是溢出了

说了模块,就是连接方式了:
模块           开发板
VDD   ----    3V3
GND   ----    GND
RXD   ----    PB10
TXD   ----    PB11

模块用到了几个AT命令
    1、修改蓝牙名称:
       为了便于标识,我们将蓝牙名称修改为 " IAP_N32G457 ",命令描述如下:


    2、数据透传开启命令:
        这里数据主要是乃至数据透传,这样可以方便进行数据的交互。开启透传后,连接上蓝牙的设备发送过来的数据会直接通过串口发给MCU,同样MCU通过串口发给蓝牙模块的数据会直接发送到连接上来的设备端。命令描述如下:

  
    3、配置保存命令,用于将设置保存下来:


    以上直接截图原文档中内容,文末我也会将此文档上传上来。

    说完了模块的几条命令,下面就来配置一下与模块通讯的串口了,我们新建一个ble.h ble.c 来处理蓝牙相关的内容。

// ble.h
#ifndef BLE_H_
#define BLE_H_

#include "main.h"

// AT固件蓝牙串口配置
#define BLE_USART_RCC                                RCC_APB1_PERIPH_USART3
#define BLE_USARTx                                        USART3

#define BLE_GPIO_RCC                                RCC_APB2_PERIPH_GPIOB
#define BLE_GPIO_PORT                                GPIOB
#define BLE_PIN_TX                                        GPIO_PIN_10
#define BLE_PIN_RX                                        GPIO_PIN_11

#define BLE_IRQn                                                USART3_IRQn
#define BLE_IRQHandle                                USART3_IRQHandler


// 定义蓝牙收到消息状态
typedef struct {
        char cmd[18];
        char content[100];
        char status[10];
} BleResultStatus;


// 蓝牙名称
#define BLE_NAME                "IAP_N32G457"

// 蓝牙每次最大接收数
#define BLE_MAX_RECV_SIZE                1024
// 蓝牙接收缓存区
extern char BLE_RecvBuff[BLE_MAX_RECV_SIZE];
// 蓝牙接收数据大小
extern uint16_t BLE_Recv_Size;
// 蓝牙接收完成标志
extern uint8_t BLE_Recv_Flag;
// 连接状态
extern uint8_t BLE_Connected ;

void BLE_Init(void);
// 发送数据
void BLE_SendData(const char *data,uint16_t size);
uint8_t BleSendCommand(char *cmd, BleResultStatus *result);
uint8_t BleRestoreStatus(char *data, BleResultStatus *result);
void BLE_Clear_Flag(void);

#endif


// ble.c
#include "ble.h"

// 蓝牙接收缓存区
char BLE_RecvBuff[BLE_MAX_RECV_SIZE] = {0};
// 蓝牙接收数据大小
uint16_t BLE_Recv_Size = 0;
// 蓝牙接收完成标志
uint8_t BLE_Recv_Flag = 0;
// 连接状态
uint8_t BLE_Connected = 0;


void BLE_Init(void)
{
        
        // 配置中断
        NVIC_InitType NVIC_InitStructure;

        /* Enable the USARTz Interrupt */
        NVIC_InitStructure.NVIC_IRQChannel                   = BLE_IRQn;
        //NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 1;
        NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
        NVIC_Init(&NVIC_InitStructure);
        
        
        if(BLE_USART_RCC != RCC_APB2_PERIPH_USART1 && BLE_USART_RCC != RCC_APB2_PERIPH_UART6 && BLE_USART_RCC != RCC_APB2_PERIPH_UART6 )
        {
                RCC_EnableAPB1PeriphClk(BLE_USART_RCC,ENABLE);
        }else{
                RCC_EnableAPB2PeriphClk(BLE_USART_RCC | RCC_APB2_PERIPH_AFIO,ENABLE);
        }
        
        RCC_EnableAPB2PeriphClk(BLE_GPIO_RCC,ENABLE);
        
        
        GPIO_InitType GPIO_InitStructure;
  USART_InitType USART_InitStructure;
        GPIO_InitStructure.Pin        = BLE_PIN_TX;
        GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_InitPeripheral(BLE_GPIO_PORT, &GPIO_InitStructure);

        GPIO_InitStructure.Pin       = BLE_PIN_RX;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
        GPIO_InitPeripheral(BLE_GPIO_PORT, &GPIO_InitStructure);

        USART_InitStructure.BaudRate            = 115200;
        USART_InitStructure.WordLength          = USART_WL_8B;
        USART_InitStructure.StopBits            = USART_STPB_1;
        USART_InitStructure.Parity              = USART_PE_NO;
        USART_InitStructure.HardwareFlowControl = USART_HFCTRL_NONE;
        USART_InitStructure.Mode                = USART_MODE_TX | USART_MODE_RX;

        // init uart
        USART_Init(BLE_USARTx, &USART_InitStructure);
        
        // 配置接收中断
        USART_ConfigInt(BLE_USARTx,USART_INT_RXDNE,ENABLE);
        // 配置空闲中断
        USART_ConfigInt(BLE_USARTx,USART_INT_IDLEF,ENABLE);

        // enable uart
        USART_Enable(BLE_USARTx, ENABLE);

        
        BleResultStatus result;
        // 取消透传
        BleSendCommand("AT+BT_TRANS=0\n\r",&result);
        systick_delay_ms(10);
        // 获取蓝牙名称
        BleSendCommand("AT+BT_NAME?\n\r",&result);
        if(strcmp(result.content,BLE_NAME)==0)
        {
                printf("蓝牙名称正确无需修改\n");
        }else{
                printf("蓝牙名称不正确,当前蓝牙名称: %s\n",result.content);
                // 开始修改蓝牙名称
                char buff[32]={0};
                sprintf(buff,"AT+BT_NAME=%s\n\r",BLE_NAME);
                BleSendCommand(buff,&result);
                printf("蓝牙名称修改结果: %s\n",result.status);
                if(strcmp(toupper(result.status),"OK") == 0)
                {
                        // 保存修改结果
                        BleSendCommand("AT+UT_CFGSV\r\n",&result);
                        printf("蓝牙保存结果: %s\n",result.status);
                }
               
        }
        systick_delay_ms(10);
        // 开启透传
        BleSendCommand("AT+BT_TRANS=1\n\r",&result);
}



void BLE_IRQHandle(void)
{
        if (USART_GetIntStatus(BLE_USARTx, USART_INT_RXDNE) != RESET)
        {
                BLE_RecvBuff[BLE_Recv_Size++] = USART_ReceiveData(BLE_USARTx);
        }
        
        if (USART_GetIntStatus(BLE_USARTx, USART_INT_IDLEF) != RESET)
        {
                USART_ReceiveData(BLE_USARTx);
                BLE_RecvBuff[BLE_Recv_Size++] = '\0';
               
                BLE_Recv_Flag = 1;
               
                // 检查是否连接消息,如果是则开启透传模式
                if(strstr(BLE_RecvBuff,"+BT_CONN"))
                {
                        BLE_Clear_Flag();
                        BleSendCommand("AT+BT_TRANS=1\n\r",NULL);
                        BLE_Connected = 1;
                }else if(strstr(BLE_RecvBuff,"+DISCONN")){
                        BLE_Connected = 0;
                }
        }
}


// 发送数据
void BLE_SendData(const char *data,uint16_t size)
{
        for(int i=0;i<size;i++)
        {
                USART_SendData(BLE_USARTx, (uint8_t)data[i]);
    while(USART_GetFlagStatus(BLE_USARTx, USART_FLAG_TXDE) == RESET);
        }
}

void BLE_Clear_Flag(void)
{
        BLE_Recv_Flag = 0;
        BLE_Recv_Size = 0;
        memset(BLE_RecvBuff,0,BLE_MAX_RECV_SIZE);
}

// 发送配置命令
uint8_t BleSendCommand(char *cmd, BleResultStatus *result)
{
        if(result != NULL)memset(result,0,sizeof(BleResultStatus));
        
        char _command[30]={0};
        if(strstr(cmd,"\r\n")){
                strcpy(_command,cmd);
        }else{
                strcpy(_command,cmd);
                strcat(_command,"\r\n");
        }
        
        // 发送命令
        BLE_SendData(_command,strlen(_command));
        if(result == NULL)return 1;
        
        // 等待获取返回信息
        uint32_t consume_time = count_time;
        while(!BLE_Recv_Flag && count_time-consume_time<500);
        if(!BLE_Recv_Flag)return 0;
        
        
        // 解析返回值信息
        uint8_t status=BleRestoreStatus(BLE_RecvBuff,result);
        // 清除接收状态
        BLE_Clear_Flag();
        return status;
}


// 解析命令状态
uint8_t BleRestoreStatus(char *data, BleResultStatus *result)
{
        if(data[0] == 0x0a){
                //printf("有换行符\n");
                strcpy(data ,data+1);
        }
        
        
        char seq[3] = "\r\n";
        char *token = strtok(data+1,seq);
        if(token == NULL)return 0;
        
        char tmp[60]={0};
        while(token != NULL)
        {
                if(strstr(token,"+")){
                        strcpy(tmp,token);
                }else if(strlen(token)>1){
                        strcpy(result->status,token);
                }
                token = strtok(NULL, seq);
        }
        
        memset(seq,0,3);
        strcpy(seq,":");
        
        token = strtok(tmp,seq);
        while(token != NULL){
                if(strstr(token,"+")){
                        strcpy(result->cmd,token);
                }else if(strlen(token)>0){
                        strcpy(result->content,token);
                }
                token = strtok(NULL, seq);
        }
        
        return 1;
}


    接下来就是处理蓝牙消息了

#define PACK_SUB_SIZE 512
// 包临时数据缓存区
char Pack_Buff[1024];
// 固件分包块数量
uint16_t Bin_Sector = 0;
// 分包接收到的固件大小
uint16_t Bin_CurrSize = 0;
// 固件大小
uint16_t Bin_Size = 0;
// 包大小
uint16_t Pack_Size = 0;
uint16_t Pack_Recv_Size = 0;
// 分包接收状态 0 等待 1 正在接收
uint8_t Pack_Recv_Flag = 0;
// 固件名称
#define BIN_NAME        "1:Project.bin"
// 固件缓存名称
#define BIN_CACHE_NAME        "1:tmp.bin"
FIL bin_fil;

// 蓝牙数据处理
void BLE_DataDisponse(void)
{
        if(!BLE_Recv_Flag)return;
        
        char buff[30]={0};
        
        if(strstr(BLE_RecvBuff,"UPGRADE")){
                // 升级请求
                if(mountSD() != FR_OK)
                {
                        printf("挂载SD失败\n");
                        strcpy(buff,"UPGRADE_ERROR");
                        BLE_SendData(buff,strlen(buff));
                        goto end_label;
                }
               
                //
                f_unlink(BIN_CACHE_NAME);
                // 以新建方式打开文件
                FRESULT fr = f_open(&bin_fil,BIN_CACHE_NAME,FA_CREATE_ALWAYS | FA_WRITE);
                if(fr != FR_OK)
                {
                        printf("创建文件失败\n");
                        strcpy(buff,"UPGRADE_ERROR");
                        BLE_SendData(buff,strlen(buff));
                        
                        
                        goto end_label;
                }
               
                strcpy(buff,"READY");
                BLE_SendData(buff,strlen(buff));
                printf("UPGRADE READY\n");
               
        }else if(strstr(BLE_RecvBuff,"LENTEST:")){
               
                // 确定每包大小
                sprintf(buff,"LEN:%d",strlen(BLE_RecvBuff));
                BLE_SendData(buff,strlen(buff));
               
        }else if(strstr(BLE_RecvBuff,"BIN:")){
               
                // 获取bin文件大小
                Bin_Size = atoi((strchr(BLE_RecvBuff,':')+1));
                printf("Bin Size=%d\n",Bin_Size);
               
        }else if(strstr(BLE_RecvBuff,"PACK:")){
               
                // 获取包大小
                Pack_Size = atoi((strchr(BLE_RecvBuff,':')+1));
                memset(Pack_Buff,0,sizeof(Pack_Buff));
                Pack_Recv_Size = 0;
                Pack_Recv_Flag =1;
                printf("Pack Size=%d\n",Pack_Size);
               
        }else if(strstr(BLE_RecvBuff,"BIN_END:")){
                f_close(&bin_fil);
                strcpy(buff,"START_UPGRADE");
                BLE_SendData(buff,strlen(buff));
               
                // 等待100ms后开始升级
                systick_delay_ms(100);
                printf("固件接收完成,开始升级\n");
               
                // 删除原固件文件
                f_unlink(BIN_NAME);
                // 将缓存重命名
                f_rename(BIN_CACHE_NAME,BIN_NAME);
               
                systick_delay_ms(100);
               
                USART_DeInit(BLE_USARTx);
                USART_ClrIntPendingBit(BLE_USARTx,USART_INT_RXDNE);
                USART_ClrIntPendingBit(BLE_USARTx,USART_INT_IDLEF);
               
        // 跳转前将中断向量表改回去
                SCB->VTOR = FLASH_BASE;
                // 清除升级标志
                app_flash_write(0x00,app_update_flag_addr);
               
                 //关闭所有中断,防止重启事件被打断
                __set_FAULTMASK(1);
                // 复位,开始升级
                NVIC_SystemReset();
               
        }else if(Pack_Recv_Flag){
                // 开始接收数据
                strcat(Pack_Buff,BLE_RecvBuff);
                if(strlen(Pack_Buff) >= Pack_Size)
                {
                        // 本组包接收完成,开始解包
                        char pack[1024] = {0};
                        uint16_t len = base64_decode(Pack_Buff,strlen(Pack_Buff),pack);
                        printf("Recv Packlen=%d BinLen:%d\n",strlen(Pack_Buff),len);
                        // 打印接收解码后的数据
//                        printf("Pack: \n");
//                        for(uint16_t i=0;i<512;i++)
//                        {
//                                printf("%02X ",pack[i]);
//                                if((i+1)%32 == 0)printf("\n");
//                        }
//                        printf("\n");
                        
                        if(len>512)len=512;
                        // 写入到SD卡虽
                        UINT br=0;
                        f_write(&bin_fil,pack,len,&br);
                        Bin_Sector++;
                        Pack_Recv_Size+=len;
                        
                        // 一组分包处理完成,开始重置状态
                        Pack_Recv_Flag = 0;
                        Pack_Size = 0;
                        memset(Pack_Buff,0,sizeof(Pack_Buff));
                        //printf("分包接收完成\n");
                }
                //
                BLE_Clear_Flag();
                // 回复本次接收的长度
                sprintf(buff,"RECV:%d",strlen(BLE_RecvBuff));
                BLE_SendData(buff,strlen(buff));
                return;
               
        }
        
end_label:
        printf("BLE Recv: %s\n",BLE_RecvBuff);
        BLE_Clear_Flag();
}


微信小程序蓝牙升级流程:
1、微信小程序连上设备后,发送 UPGRADE 命令,请求升级
2、设备端收到 UPGRADE 命令开始挂载 SD 卡,并做升级前检查,准备就绪后回复命令 READY ,表示准备就绪
3、小程序收到 READY 命令后,开始构建一个长数据包(通常为128位),并以 LENTEST: 开头,测试单包最大支持的发送长度
4、设备端收到 LENTEST: 命令后,回复收到长度,格式 LEN:x,x为实际收到的长度
5、小程序端收到 LEN:x 后确认单包最大可以发送的数据包大小,开始读取固件并按512字节每块进行读取,数据使用base64编码成文本格式数据,
先发送bin文件总大小和每块数据发送前都会发送组包文件大小,格式为:
   1)发送bin文件大小格式为 BIN:x,x为bin大小
   2)每块数据编码完成后发送分包大小命令 PACK:x, x为本次分包编码后文件大小
6、设备端收到 BIN:x 后开始清空缓存区,准备接收数据
7、小程序开始发送数据,设备端每次接收完每块拆分数据后会回复本次分包大小,格式为 RECV:x ,x为接收到的长度,并将收到的数据存到缓存中
8、设备端每完整接收完一块编码数据(即每块 512字节编码后的文本内容)后,会解码成二进制数据存入到SD卡中,直到接收完成为止
9、小程序端在固件全部发送完成后,会发送命令 BIN_END:CRC32 命令 (CRC32内容目前为空),表示本次发送结束。设备端接收到完成命令后,检验完成后开始设备升级。

    最后附上小程序码:


    小程序使用说明:因为每个蓝牙的服务ID都是不一样的,而使用到的模块是能用的模块,不可能仅我使用,所我的这个小程序是通过蓝牙的服务ID和蓝牙名称双重编定确认为可管理的设备的是,蓝牙的服务ID为 6E400001-B5A3-F393-E0A9-E50E24DCCA9E ,蓝牙名称需要以 IAP_ 开始,如果大家能修改自己的服务ID和名称可以尝试下。

    整个升级流程完成,此部分内容截图不太好展示,所以后面将以视频方式呈现。

五、总结分享:
    终于完成了整个项目,从开始到完成还是花了大力气的,中间也遇到了许多困难,也得到了一些热心群友的帮助,在此非常感谢。    说下遇到的问题:
     1、最开始遇到了就是SD卡驱动和FATFS文件系统方面,因为完全不知道从哪开始,不知道要做些什么,还有就是开始选型,SD卡驱动到底是用SPI模式还是SDIO,毕竟N32G457是支持SDIO的,但最终选择SPI,主要是考虑到后面我在用其它款MCU时不用再费劲了,只要改下SPI驱动部分就可以很愉快的移植了,速度虽然会慢一些,但暂时应该不会做视频存储方面,应该能达到使用需求了,其实说到底还是想偷下懒了
    2、就是中断向量表的问题,就是跳到APP中时已经设置了,但进入程序后一旦开启中断,就会卡死的问题,因为升级部分也是参考官方的示例,连官方示例都没有修改APP中中断向量值,所以就一直以为跳转时设置了就可以了,没想到在进入APP后,在 SystemInit() 函数中又把中断向量表改回去了,这个后面得到群友的提醒才明白,原来不是我设置的不对,而是又被改了。不过在官方示例中是有说到,但在实例中Bootloader中有做,但APP中去没有处理,走了不少弯路。





  




IAP_SD卡升级.zip (3.61 MB)


微信小程序实现IAP升级.zip (3.61 MB)


G75蓝牙模块-AT指令集-v1.0.0.pdf (423.09 KB)



  

使用特权

评论回复
沙发
jobszheng| | 2023-3-23 10:07 | 只看该作者
楼主 厉害了

您这帖子是不是预定了一等奖了。
我只有争取第二名的机会了 555..

使用特权

评论回复
板凳
uant|  楼主 | 2023-3-23 12:51 | 只看该作者
jobszheng 发表于 2023-3-23 10:07
楼主 厉害了

您这帖子是不是预定了一等奖了。

过奖了,不过这个好像没有设置奖项,都一样

使用特权

评论回复
地板
gyh974| | 2023-3-23 13:54 | 只看该作者
小程序是怎么做的?

使用特权

评论回复
5
sy12138| | 2023-3-23 15:37 | 只看该作者
太强了

使用特权

评论回复
6
uant|  楼主 | 2023-3-24 13:29 | 只看该作者
gyh974 发表于 2023-3-23 13:54
小程序是怎么做的?

不难的,可以找些资料学习下

使用特权

评论回复
7
安小芯| | 2023-3-24 15:11 | 只看该作者
jobszheng 发表于 2023-3-23 10:07
楼主 厉害了

您这帖子是不是预定了一等奖了。

郑工,你要加油,完成后发布出来。

使用特权

评论回复
8
uant|  楼主 | 2023-4-6 09:38 | 只看该作者

使用特权

评论回复
9
fcgao| | 2023-6-29 16:10 | 只看该作者
这个fatfs移植后  每次 f_write只能512字节, 如果超过会报错,错误码为9(FR_INVALID_OBJECT),楼主有碰到过吗?

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

4

主题

19

帖子

1

粉丝