本帖最后由 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)
|
|