PIC18F45K80系列MCU固件升级方案

最近一段时间在做一个通过RS485对PIC18F45K80系列单片机进行固件升级的项目,现将一些过程分享给大家,同时也希望大家提出一些建议,以使程序更加稳定和可靠。

Microchip官方提供了一个8位单片机的升级方案,链接如下:

8-bit Bootloader | Microchip Technology

还有其他网友提供的升级过程,下面这个链接的方案说得非常好,值得阅读一下:

给PIC16F18446 curiosity nano板做个bootloader - Microchip论坛 - PIC单片机论坛 - Microchip(微芯科技)MCU官方技术支持论坛 - 21ic电子技术开发论坛

我也是参考了上面的方案后做的升级程序。Microchip提供的方案是同步升级,也就是一边发送升级文件一边写入到单片机的ROM,使用了Microchip提供的上位机软件UnifiedHostApplication。下载地址如下:

https://www.microchip.com/en-us/tools-resources/develop/libraries/microchip-bootloaders/8-bit

升级部分的程序我把它称之为Bootloader,应用程序部分我称之为End Application,PIC18F45K80系列的ROM大小为0x8000字节,Bootloader部分我使用的ROM地址范围是:0-0x1800。

由于项目要求在固件升级过程中单片机连接的输出信号必须要保持,End Application使用了锁存器,这样单片机在复位更新固件时主要外围电路还可以正常工作。另外项目还要求如果固件升级失败可以恢复到旧的版本,由于单片机本身ROM空间不够,所以增加了外部Flash存储器MX25L1606EM2I-12G,升级程序的总体流程大致如下:

1、将版本信息写入到升级固件中(解析后的Hex的文件),我是将固件文件的第一个字节作为新的版本号,例如:0x11表示版本V1.1,0x12表示版本V1.2等等;

2、单片机正常运行时,根据通信协议,上位机软件首先获取正在运行的固件版本信息,如果版本相同则不用升级,如果不同则首先发送版本号、固件的数据长度以及固件的校验和,这些信息会保存在外部存储器MX25L1606EM2I-12G的指定位置(FLASH_Block4,起始地址为0x00030000的大小为64KB的块),这样做的目的是当单片机重启升级时会检测接收到的固件是否完整;

3、开始发送升级文件(确保升级文件是可用并严格测试过的),每次发送64字节,单片机每收到一包数据都会保存在MX25L1606EM2I-12G的指定位置(FLASH_Block3,起始地址为0x00020000的大小为64KB的块)并发送回应数据,直到接收完成;

4、上位机发送完升级数据后,改写ROM的最末尾地址0x7FFF的内容为一个不是0x55的值(确保ROM的最后一个块没有写入程序),锁存输出状态,然后发送重启单片机的指令;

5、单片机重启进入Bootloader,根据ROM的最末尾地址0x7FFF的值来决定是否升级,如果不是0x55则执行Update_Firmware这个函数;

6、Update_Firmware首先判断FLASH_Block3中的数据是否完整和有效,计算FLASH_Block3中的校验和并和FLASH_Block4预先保存的校验和进行比较,如果一样那证明数据包是正确的,如果不同则证明数据包不完整,擦除FLASH_Block3和FLASH_Block4并改写ROM的最末尾地址0x7FFF的值为0x55,直接运行ROM中的旧程序;

7、如果FLASH_Block3中新程序校验和正确,则首先将ROM中的旧程序保存在指定位置(FLASH_Block2,起始地址为0x00010000的大小为64KB的块),保存完毕后分别计算ROM中程序的校验和及FLASH_Block2中数据和校验和,如果二者的值相同,则进入下一步,不同则回到第5步开始;

8、擦除ROM中的数据并将FLASH_Block3中的数据写入到ROM,计算校验和,如果二者的值相同则改写ROM的最末尾地址0x7FFF的值为0x55并重启运行新程序,如果校验和不同则又回到步骤5,这里主要没有做重试次数判断,如果更新指定次数后仍然失败,则运行旧程序;

在执行Update_Firmware函数时每个重要步骤都会通过RS485发送反馈结果给上位机程序。

Microchip官方的Bootloader程序可以在我上面提到的链接中下载或者按照文档指导直接用MCC生成,比较简单,我这里只是将原来的Run_Bootloader ()函数注释掉,换成了Update_Firmware。

Update_Firmware函数代码如下:

/* @function name: Update_Firmware
 * @description: the main process to update the firmware
 * ===========================================================================================================
 *      At first we try to read several bytes from the external flash where the new firm resides and check whether they are valid
        if not, we then need to read backup of the previous application. 
        If both the new application and the old one are invalid, which, in most cases, is impossible,we don't need to erase the ROM and execute other instructions.
        Instead we just modify the last byte of the ROM which should have a valid end application(actual it is the running application).
 * ===========================================================================================================
 * @params: none
 * @return: none
 */
void Update_Firmware(void)
{
    uint32_t chk_sum=0;
    uint8_t buffer[68]={0};
    uint8_t rom_buf[64]={0};//used to save the data retrieved from the ROM
    uint32_t flashAddr=FLASH_Sector3;
    uint8_t n=0;
    uint32_t   address=NEW_RESET_VECTOR;//the default address is 0x1800,that is the ROM address where the end application resides
    uint16_t numOfBlocks = AVAIL_BLOCKS;//0x1A0=416, total number of blocks of the ROM(each block has 64 bytes), and the whole size of the ROM is 0x8000=0x6800+0x1800,available bytes:416*64=0x6800,the Bootloader occupies 0x1800
    
    RS485_H();       
    __delay_ms(100); 
    Flash_BufferRead(buffer, flashAddr, 4);
    if(buffer[0]==0xFF && buffer[1]==0xFF && buffer[2]==0xFF && buffer[3]==0xFF)
    {
        while (TXSTA2bits.TRMT == 0);
        EUSART2_Write(FW_INVALID);
        __delay_ms(100);         
        flashAddr=FLASH_Sector2;
        Flash_BufferRead(buffer, flashAddr, 4);
        if(buffer[0]==0xFF && buffer[1]==0xFF && buffer[2]==0xFF && buffer[3]==0xFF)//the backup firmware is invalid
        {
            //there is no valid firmware, so we just execute the current application
            //try to set the last byte of the ROM to 0x55 and reset
            //before writing, we need to erase the last block of the ROM
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(BK_FW_INVALID);
            __delay_ms(100); 
            TBLPTR =address+(AVAIL_BLOCKS-1)*ERASE_FLASH_BLOCKSIZE;
            EECON1 = 0x94;       // Setup writes, 1001 0100 bit4: flash erase enable bit
            Write_Cycle();
            TBLPTR  = END_FLASH - 1;
            EECON1 = 0x84;
            TABLAT = APPLICATION_VALID;
            asm("TBLWT *");
            EECON2 = 0x55;
            EECON2 = 0xAA;
            EECON1bits.WR = 1;
            NOP();
            NOP(); 
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(LOAD_OLD_APP_RUNNING); 
            __delay_ms(100); 
            return;
            //BOOTLOADER_INDICATOR = BL_INDICATOR_OFF;
            //RESET();
        }
        //the backup firmware is valid
        TBLPTRL = address&0xFF;
        TBLPTRH = (uint8_t)(address>>8);
        TBLPTRU = (uint8_t)(address>>16);
        if (((int)TBLPTR & (~LAST_WORD_MASK)) <NEW_RESET_VECTOR)//In most cases, this won't happen
        {            
            return;
        }
        for (uint16_t i=0; i < numOfBlocks; i++)//erase the whole ROM
        {
            if (TBLPTR >= END_FLASH)
            {                
                return;
            }
            EECON1 = 0x94;       // Setup writes
            Write_Cycle();
            TBLPTR += ERASE_FLASH_BLOCKSIZE;
        }
        //copy data from the external flash and then write it to the ROM, and each time we retrieve 64 bytes        
        Flash_BufferRead(buffer, flashAddr, WRITE_FLASH_BLOCKSIZE);
        while(!(buffer[0]==0xFF && buffer[1]==0xFF && buffer[2]==0xFF && buffer[3]==0xFF))
        {
            TBLPTRU = (uint8_t)((address & 0x00FF0000) >> 16);
            TBLPTRH = (uint8_t)((address & 0x0000FF00)>> 8);
            TBLPTRL = (uint8_t)(address & 0x000000FF); 
            EECON1 = 0xA4;       // Setup writes
            if (TBLPTR < NEW_RESET_VECTOR || TBLPTR >= END_FLASH)//in most cases, this won't happen, for the old firmware is copied from the ROM
            {                
                return;
            }
            for (uint16_t  i = 0; i < WRITE_FLASH_BLOCKSIZE; i ++)
            {
                /*The Table Latch (TABLAT) is an eight-bit register mapped into the SFR space. The Table Latch register
                is used to hold 8-bit data during data transfers between program memory and data RAM.*/
                TABLAT = buffer[i];
                chk_sum+=buffer[i];                
                asm("TBLWT *+");//Table write
                if (((TBLPTRL & LAST_WORD_MASK) == 0x00)//the last byte
                  || (i == WRITE_FLASH_BLOCKSIZE - 1))
                {
                    asm("TBLRD *-");
                    Write_Cycle();
                    asm("TBLRD *+");//TBLPTR is incremented after the read/write
                }            
            }
            address+=WRITE_FLASH_BLOCKSIZE;
            flashAddr+=WRITE_FLASH_BLOCKSIZE;
            Flash_BufferRead(buffer, flashAddr, WRITE_FLASH_BLOCKSIZE);
        }//end while
        if((flashAddr>FLASH_Sector2)&&(System_Checksum((uint16_t)(flashAddr-FLASH_Sector2))==chk_sum))//check sum calculating
        {
            TBLPTR  = END_FLASH - 1;
            EECON1 = 0x84;
            TABLAT = APPLICATION_VALID;
            asm("TBLWT *");
            EECON2 = 0x55;
            EECON2 = 0xAA;
            EECON1bits.WR = 1;
            NOP();
            NOP();
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(LOAD_OLD_APP_FLASH);//update the firmware by using the old version from flash successfully
            __delay_ms(100); 
            return;
            //BOOTLOADER_INDICATOR = BL_INDICATOR_OFF;
            //RESET();
        }
        else //TODO
        {
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(ROM_CHK_SUM_ERROR);
           __delay_ms(100); 
            address=NEW_RESET_VECTOR;
            Validate_App(address, 0);//invalidate the application flag
        }
    }
    else    //the new firmware has data, note: before erasing the ROM, we need to copy the old firmware in it to the external flash, 64 bytes each time
    {        
        uint8_t version[8]={0x00};//get the version and check sum information
        uint16_t page_data_num=0;//the actual bytes except ROM address
        uint16_t single_data=0;//the single bytes that are less than one page except ROM address
        uint16_t data_len=0;//total bytes including ROM address
        chk_sum=0;
        while (TXSTA2bits.TRMT == 0);
        EUSART2_Write(LOAD_NEW_APP); 
        __delay_ms(100); 
        Flash_BufferRead(version, FLASH_Sector4, 8);        
        if(version[0]==0xFF && version[1]==0xFF)//invalid version information, just run the old application
        {
            //try to set the last byte of the ROM to 0x55 and reset
            //before erasing, we need to erase the last block of the ROM
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(NEW_APP_NOT_INTEGRAL); //the received new firmware is not integral
            __delay_ms(100);
            address=NEW_RESET_VECTOR;
            Validate_App(address, 1);
        }        
        else//validate the check sum as well as the integrity of the new firmware
        {
            data_len=(uint16_t)((version[2]<<8)+version[3]);
            uint16_t page_num=data_len/(WRITE_FLASH_BLOCKSIZE+4);
            uint16_t single=data_len%(WRITE_FLASH_BLOCKSIZE+4);
            page_data_num=data_len/(WRITE_FLASH_BLOCKSIZE+4);
            single_data=data_len%(WRITE_FLASH_BLOCKSIZE+4);
            //ulong _temp=((0x000000FF&version[4])<<24) | ((0x000000FF&version[5])<<16) | ((0x000000FF&version[6])<<8) | version[7];//0x00248FBE
            flashAddr=FLASH_Sector3;
            while(page_num--)
            {                
                Flash_BufferRead(buffer, flashAddr, WRITE_FLASH_BLOCKSIZE+4);
                for (uint16_t i=0; i < WRITE_FLASH_BLOCKSIZE+4; i++)
                {
                    chk_sum+=buffer[i];
                }
                flashAddr+=WRITE_FLASH_BLOCKSIZE+4;                
            }
            if(single>0)
            {
                Flash_BufferRead(buffer, flashAddr, single);
                for (uint16_t i=0; i < single; i++)
                {
                    chk_sum+=buffer[i];
                }
            }
            if(!(((chk_sum&0xFF000000)>>24==version[4])&&
                 ((chk_sum&0x00FF0000)>>16==version[5])&&
                 ((chk_sum&0x0000FF00)>>8==version[6])&&
                 ((chk_sum&0xFF)==version[7])))//if the check sum is invalid then exit
            {
                Flash_BlockErase(FLASH_Sector3);//since the new firmware is invalid and not integrity, we discard it
                Flash_BlockErase(FLASH_Sector4);
                while (TXSTA2bits.TRMT == 0);
                EUSART2_Write(NEW_FW_CHK_SUM_ERROR);//the check sum of the new firmware is error
                __delay_ms(100); 
                address=NEW_RESET_VECTOR;
                Validate_App(address, 1); //directly execute the end application that is running
            }
            else
            {
                //Flash_BlockErase(FLASH_Sector2);//prepare to the save the old firmware
                while (TXSTA2bits.TRMT == 0); // wait for last byte to shift out before
                EUSART2_Write(NEW_FW_CHK_SUM_OK);
                __delay_ms(100); 
            }
        }
        Flash_BlockErase(FLASH_Sector2);//prepare to the save the old firmware
        address=NEW_RESET_VECTOR;//0x001800;
        TBLPTR = address;//load the address, In addition, TBLPTR can be modified automatically for the next table read operation.
        flashAddr=FLASH_Sector2;        
        EECON1 = 0xC0;
        while(!(rom_buf[0]==0xFF && rom_buf[1]==0xFF && rom_buf[2]==0xFF && rom_buf[3]==0xFF))
        {
            for(n=0;n<ERASE_FLASH_BLOCKSIZE;n++)
            {
                asm("TBLRD *+");
                rom_buf[n]=TABLAT;//read byte from ROM
            }
            if(n>=ERASE_FLASH_BLOCKSIZE)//each time we write 64 bytes to the external flash
            {                
                Flash_BufferWrite(rom_buf,flashAddr,ERASE_FLASH_BLOCKSIZE);
                flashAddr+=ERASE_FLASH_BLOCKSIZE;//write the next 64 bytes to the flash            
            }
        }
        //here we need to check whether the old firmware has been saved correctly
        flashAddr=FLASH_Sector2;
        chk_sum=0;
        Flash_BufferRead(buffer, flashAddr, WRITE_FLASH_BLOCKSIZE);
        while(!(buffer[0]==0xFF && buffer[1]==0xFF && buffer[2]==0xFF && buffer[3]==0xFF))
        {                       
            for (uint16_t  i = 0; i < WRITE_FLASH_BLOCKSIZE; i ++)
            {                
                chk_sum+=buffer[i];
            }
            flashAddr+=WRITE_FLASH_BLOCKSIZE;
            Flash_BufferRead(buffer, flashAddr, WRITE_FLASH_BLOCKSIZE);
        }//end while
        if(System_Checksum(flashAddr-FLASH_Sector2)==chk_sum)//check sum calculating
        {
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(SAVE_OLD_APP_OK);//save the old version to the external flash
            __delay_ms(100);
        }
        else
        {
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(SAVE_OLD_APP_FAILED);
            __delay_ms(100);
            address=NEW_RESET_VECTOR;
            Validate_App(address, 0);//invalidate the last byte of the ROM, we try again to save the old firmware
        }
        
        address=NEW_RESET_VECTOR;//0x001800;
        TBLPTRL = address&0xFF;
        TBLPTRH = (uint8_t)(address>>8);
        TBLPTRU = (uint8_t)(address>>16);
        if (((int)TBLPTR & (~LAST_WORD_MASK)) <NEW_RESET_VECTOR)
        {            
            return;
        }
        for (uint16_t i=0; i < numOfBlocks; i++) //erase the whole ROM
        {
            if (TBLPTR >= END_FLASH)
            {                
                return;
            }            
            EECON1 = 0x94;       // Setup writes, 1001 0100 bit4: flash erase enable bit
            Write_Cycle();
            TBLPTR += ERASE_FLASH_BLOCKSIZE;
        }
        chk_sum=0;
        flashAddr=FLASH_Sector3;//new firmware block
        Flash_BufferRead(buffer, flashAddr, WRITE_FLASH_BLOCKSIZE+4);
        while(!(buffer[0]==0xFF && buffer[1]==0xFF && buffer[2]==0xFF && buffer[3]==0xFF))
        {
            TBLPTRL = buffer[0];
            TBLPTRH = buffer[1];
            TBLPTRU = buffer[2];
            //EXTENDED=buffer[3];
            EECON1 = 0xA4;       // Setup writes
            if (TBLPTR < NEW_RESET_VECTOR || TBLPTR>=END_FLASH)//invalid ROM address
            {
                Flash_BlockErase(FLASH_Sector3);
                Flash_BlockErase(FLASH_Sector4);
                while (TXSTA2bits.TRMT == 0);
                EUSART2_Write(ROM_ADDR_ERROR); 
                __delay_ms(100); 
                address=NEW_RESET_VECTOR;
                Validate_App(address, 0);//invalidate the last byte of the ROM
                //return ;
            }           
            for (uint16_t  i = 0; i < WRITE_FLASH_BLOCKSIZE; i ++)
            {
                /*The Table Latch (TABLAT) is an eight-bit register mapped into the SFR space. The Table Latch register
                is used to hold 8-bit data during data transfers between program memory and data RAM.*/
                TABLAT = buffer[i+4];
                chk_sum+=buffer[i+4];
                /*
                if (TBLPTR >= END_FLASH)
                {
                    return ;
                }
                */
                asm("TBLWT *+");//Table write
                if (((TBLPTRL & LAST_WORD_MASK) == 0x00)//the last byte
                  || (i == WRITE_FLASH_BLOCKSIZE - 1))
                {
                    asm("TBLRD *-");//Table read
                    Write_Cycle();
                    asm("TBLRD *+");//TBLPTR is incremented after the read/write
                }            
            }
            flashAddr+=WRITE_FLASH_BLOCKSIZE+4;
            Flash_BufferRead(buffer, flashAddr, WRITE_FLASH_BLOCKSIZE+4);
        }//end while
        uint16_t sum=0;
        if(single_data>0)
        {
            sum=data_len-(page_data_num+1)*4;
        }
        else
        {
            sum=data_len-(page_data_num*4);
        }
        if(System_Checksum(sum)==chk_sum)//check sum calculating
        { 
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(UPDATE_FW_OK);//update the new firmware successfully
            __delay_ms(100); 
            address=NEW_RESET_VECTOR;
            Validate_App(address, 1);
        }
        else            //TODO
        { 
            while (TXSTA2bits.TRMT == 0);
            EUSART2_Write(ROM_CHK_SUM_ERROR);           
            __delay_ms(100); 
            address=NEW_RESET_VECTOR;
            Validate_App(address, 0);
        }
    }
}

校验和函数及Validate_App函数和其他主要用到的函数:

/* @function name: Validate_App
 * @description: write the last byte of the ROM to the application valid or invalid value, that is, valid: 0x55 invalid: any other value except 0x55
 * @params: addr: the block to be erased, for we need to erase the last block and then write the value
 *                  the 0x1A0 means the total number of blocks available for the end application.
 *                  valid, 1: write 0x55, 0: any other value except 0x55
 * @return: none
 */
void Validate_App(ulong addr, uint8_t valid)
{
    TBLPTR =addr+(AVAIL_BLOCKS-1)*ERASE_FLASH_BLOCKSIZE;
    EECON1 = 0x94;       // Setup writes, 1001 0100 bit4: flash erase enable bit
    Write_Cycle();
    NOP();
    NOP();
    TBLPTR  = END_FLASH - 1;
    EECON1 = 0x84;
    if(valid)
    {
        TABLAT = APPLICATION_VALID;
    }
    else
    {
        TABLAT = APPLICATION_INVALID;
    }
    asm("TBLWT *");
    EECON2 = 0x55;
    EECON2 = 0xAA;
    EECON1bits.WR = 1;
    NOP();
    NOP();
    BOOTLOADER_INDICATOR = BL_INDICATOR_OFF;
    RESET();
}

// *****************************************************************************************
// Calculate Checksum
// In:	[<0x08><DataLengthL><DataLengthH> <unused><unused> <ADDRL><ADDRH><ADDRU><unused>...]
// OUT:	[9 byte header + ChecksumL + ChecksumH]
//        Cmd     Length-----              Address---------------  Data ---------
// In:   [<0x08> <0x00><0x00><0x00><0x00> <0x00><0x00><0x00><0x00>]
// OUT:  [<0x08> <0x00><0x00><0x00><0x00> <0x00><0x00><0x00><0x00> <CheckSumL><CheckSumH>]
// *****************************************************************************
uint32_t System_Checksum(uint16_t length)
{
#if END_FLASH > 0x10000
    uint32_t  i;
    uint32_t  length;
#else    
    uint32_t check_sum2 = 0; 
#endif 

    TBLPTRL = 0x00;//address where the check sum calculating starts
    TBLPTRH = 0x18;
    TBLPTRU = 0x00;//this is no use to the PIC18F45K80 series MCU
    EECON1 = 0xC0;//0x80
    
    //length = 0x6800;//Bootloader occupies 0x1800,the end application 0x8000-0x1800=0x6800
#if END_FLASH > 0x10000
    length += ((uint32_t) frame.EE_key_1) << 16;
#endif 
    //pages=length/64;
    //single=length%64;
    
    for (uint16_t i = 0; i < length; i++)
    {
        asm("TBLRD *+");
        check_sum2 += (uint16_t)TABLAT;  
     }
    
    /*
    if(single>0)
    {
         for (uint16_t i = 0; i < single; i++)
        {
            asm("TBLRD *+");
            check_sum2 += (uint16_t)TABLAT;  
         }
    }*/
     return (check_sum2);
    
    /*
    for (i = 0; i < length; i += 2)
    {
        asm("TBLRD *+");
        check_sum += (uint16_t)TABLAT;
        asm("TBLRD *+");
        check_sum += (uint16_t)TABLAT << 8;
     }
     frame.data[0] = (uint8_t) (check_sum & 0x00FF);
     frame.data[1] = (uint8_t)((check_sum & 0xFF00) >> 8);
     return (11);
     */
}

/*
 * function name: Write_Cycle
 * description: execute a write cycle
 * parameters: none                      
 * return: none
 */
void Write_Cycle(void)
{
    EECON1bits.EEPGD = 1;	//EEPGD:闪存程序存储器或数据EEPROM 选择位
							//1 = 访问闪速程序存储器
							//0 = 访问数据EEPROM
    EECON1bits.CFGS = 0;	//CFGS:闪存程序存储器/ 数据EEPROM 或配置寄存器选择位
							//1 = 访问配置寄存器
							//0 = 访问闪存程序存储器或数据EEPROM
    EECON1bits.WREN = 1;	//WREN:闪存程序/ 数据EEPROM 写使能位
							//1 = 允许闪存程序/ 数据EEPROM 的写周期
							//0 = 禁止闪存程序/ 数据EEPROM 的写周期
    STATUSbits.C = 0;
    if(INTCONbits.GIE) 
        STATUSbits.C = 1;
    INTCONbits.GIE = 0; 	//关全局中断

    EECON2 = 0X55;
    EECON2 = 0XAA;

    EECON1bits.WR = 1; 	//1 启动读/写周期;0 写周期完成
    Nop();
    Nop();
    while(EECON1bits.WR);	//等待写周期完成
    EECON1bits.WREN = 0;	//0 禁止向程序存储器或EEPROM写操作

    if(STATUSbits.C) 
    INTCONbits.GIE = 1;	//IPEN=1,开中断
}

// *****************************************************************************
// The bootloader code does not use any interrupts.
// However, the application code may use interrupts.
// The interrupt vector on a PIC18F is located at
// address 0x0008 (High) and 0x0018 (Low). 
// The following function will be located
// at the interrupt vector and will contain a jump to
// the new application interrupt vectors
// *****************************************************************************

 asm ("psect  intcode,global,reloc=2,class=CODE,delta=1");
 asm ("GOTO " str(NEW_INTERRUPT_VECTOR_HIGH));

 asm ("psect  intcodelo,global,reloc=2,class=CODE,delta=1");
 asm ("GOTO " str(NEW_INTERRUPT_VECTOR_LOW));
// *****************************************************************************
void BOOTLOADER_Initialize ()
{
    //EE_Key_1 = 0;
    //EE_Key_2 = 0;   
    BOOTLOADER_INDICATOR = BL_INDICATOR_ON;
    if (Bootload_Required () == true)
    {
        //Run_Bootloader ();     // generic comms layer
        Update_Firmware();
    }    
    STKPTR = 0x00;
    BSR = 0x00;
    BOOTLOADER_INDICATOR = BL_INDICATOR_OFF;
    asm ("goto  "  str(NEW_RESET_VECTOR));
}

相关的宏定义:

#define     UPDATE_FW_OK                13  //the firmware has been updated successfully
#define     LOAD_OLD_APP_RUNNING        1 //use the old version of the end application from the running application
#define     LOAD_OLD_APP_FLASH          2 //use the old version of the end application from backup
#define     FW_INVALID                  3 //new firmware is invalid or perhaps has not been downloaded yet
#define     BK_FW_INVALID               4 //the backup firmware is invalid
#define     ROM_ADDR_ERROR              5 //ROM range is error
#define     ROM_CHK_SUM_ERROR           6 //the check sum of the bytes written to the ROM is not equal to that of the bytes stored in the external flash   
#define     LOAD_NEW_APP                7 //using new firmware
#define     NEW_APP_NOT_INTEGRAL        8 //the new firmware is not integral
#define     NEW_FW_CHK_SUM_ERROR        9//the new firmware has error
#define     NEW_FW_CHK_SUM_OK           10//the check sum of the new firmware is integral
#define     SAVE_OLD_APP_OK             11//save the old version of the app to the external flash
#define     SAVE_OLD_APP_FAILED         12//save the old version of the app failed

// *****************************************************************************
#define	MINOR_VERSION	0x01       // Version
#define	MAJOR_VERSION	0x01
#define APPLICATION_VALID  0x55
#define APPLICATION_INVALID  0x58
#define ERROR_ADDRESS_OUT_OF_RANGE   0xFE
#define ERROR_INVALID_COMMAND           0xFF
#define COMMAND_SUCCESS                      0x01

// To be device independent, these are set by mcc in memory.h
#define  LAST_WORD_MASK                         (WRITE_FLASH_BLOCKSIZE - 1)
#define  NEW_RESET_VECTOR                       0x1800//0x800
#define  NEW_INTERRUPT_VECTOR_HIGH      0x1808//0x808
#define  NEW_INTERRUPT_VECTOR_LOW       0x1818//0x818

#define AVAIL_BLOCKS     0x1A0   //available blocks of the ROM for the end application

上位机程序如下,由于上位机程序运行在Ubuntu系统中,我在Visual Studio2022中下载安装了VS Linux Debugger并使用GDB的方式和Ubuntu客户机连接,不过我目前只是安装了虚拟机,但是程序是可以运行的。在VMware Workstation Pro中设置好串口后就可以在虚拟机中使用Windows系统下的串口资源了。

上位机的主要工作如下:

1、获取目前正在运行的单片机程序的版本信息,如果新固件与原来的版本不一样,则进入下一步,否则程序退出;

2、读取hex格式的升级文件,获取文件大小及计算好校验和发送给单片机,同时读取升级文件的内容并保存在一个数组中(由于文件约为20K多一些,就没有使用读取一部分发送一部分的方式,数组也是根据文件大小来动态分配的内存);

3、发送升级数据,每次64字节,直到发送完成;

4、发送重启单片机指令让其开始更新固件,同时监测单片机更新固件过程中发送的反馈数据;

5、程序执行完成。

#include <cstdio>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
//#include <fcntl2.h>
#include <stdlib.h>
#include <error.h>
#include <malloc.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/times.h>
#include <unistd.h>
#include <sys/time.h>

#define		BUFFER_SIZE				64		//每次最大发送数据大小
#define		COMM_BUFFER_SIZE		150		//实际发送数据缓冲大小
#define		BAUD_RATE				9600	//串口波特率
#define		NUM_RETRY				3		//串口发送重试次数

#define		SOI						0x7E	//数据包起始标志
#define		EOI						0x0D	//数据包结束标志
#define		CID1					0x42	//固定指令
#define		CMD_FIRMWARE_DATA		0xFE	//升级包指令
#define		CMD_ERASE				0xFD	//擦除固件信息指令
#define		CMD_RESET				0xFC	//MCU重启指令
#define		CMD_READ_SW_INFO		0xA1	//读取软硬件版本信息

#define		NORMAL					0		//正常
#define		VER_ERR					-1		//VER版本错误
#define		CHK_ERR					-2		//CHKSUM错误
#define		LCHK_ERR				-3		//LCHKSUM错误
#define		CID2_INV				-4		//无效命令
#define		CMD_ERR					-5		//命令格式错误
#define		DATA_INV				-6		//无效数据
#define		EXE_FAULT				-7		//命令执行失败
#define		NO_BASE_ID				-8		//错误码标志,当操作的从机与返回数据的从机地址不一致时就会返回该错误标志
#define		ERROR_RETRY_LIMIT		-9		//重试次数已到错误


typedef struct termios termios_t;//串口类型结构体

typedef struct serial_data {
	char databuf[100];//发送/接受数据
	int serfd;//串口文件描述符
}ser_Data;

void* sersend(void* arg);
void* serrecv(void* arg);

unsigned char flash_write_buffer[COMM_BUFFER_SIZE] = { 0x00 };//每次要发送的更新程序缓冲
unsigned char parsed_rcv_buff[BUFFER_SIZE] = { 0x00 };//经过解析后的接收数据缓冲

/* @function name: sth_value
 * @description: 把字符转成16进制的本身的值,例如 '1'->0x1 '9B'-> 0x9B
 * @params: c: 要转换的字符
 * @return: 对应的16进制格式
 */
int sth_value(char c)
{
	int value;
	if ((c >= '0') && (c <= '9'))
		value = 48;
	else if ((c >= 'a') && (c <= 'f'))
		value = 87;
	else if ((c >= 'A') && (c <= 'F'))
		value = 55;
	else
	{
		printf("invalid data %c", c);
		return -1;
	}
	return value;
}


/* @function name: str_to_hex
 * @description: 这个函数会把str的内容按字面每两个组合成一个16进制的数
 * @params: str: 要转换的字符串
 * 			data: 转换后的16进制数数组
 * 			len: 要转换的数据长度
 * @return: 总共转换的数据长度
 */
int str_to_hex(char* str, unsigned char* data, int len)
{
	int sum = 0;
	int high = 0;
	int low = 0;
	int value = 0;
	int j = 0;

	//char data[256] = {0};
	//printf("%d\n", len);
	for (int i = 0; i < len; i++)
	{
		//printf("high-n:0x%02x\n", str[i]);
		value = sth_value(str[i]);
		high = (((str[i] - value) & 0xF) << 4);//获取数据,成为高4位
		//printf("high:0x%02x\n", high);
		//printf("low-n:0x%02x\n", str[i+1]);
		value = sth_value(str[i + 1]);
		low = ((str[i + 1] - value) & 0xF);//获取数据,成为低4位
		//printf("low:0x%02x\n", low);
		sum = high | low; //组合高低四位数,成为一byte数据
		//printf("sum:0x%02x\n", sum);
		j = i / 2; //由于两个字符组成一byte数,这里的j值要注意
		data[j] = sum;//把这byte数据放到数组中
		i = i + 1; //每次循环两个数据,i的值要再+1
	}
	return len;
}

/* @function name: extra_sub_str
 * @description: 提取子串,从src中提取自startIndex开始的连续len个字符,构成子串
 * @params: str: 要提取的字符串
 * 			data: 起始索引
 * 			len: 要提取的长度
 * @return: 提取到的子字符串
 */
char* extra_sub_str(const char* src, int startIndex, int len) {
	char* substr = (char *)malloc(sizeof(char) * (len + 1));
	int i = 0;
	if (substr == NULL) {
		printf("Apply for memory failed.\n");
		return NULL;
	}
	if (strlen(src) < startIndex + len) {
		printf("Index exceeds bounds.\n");
		free(substr);
		return NULL;
	}
	for (i = 0; i < len; i++) {//提取子串		
		substr[i] = src[startIndex + i];
	}
	substr[len] = '\0';//添加字符串末尾标志
	return substr;
}

/* @function name: change_send_data
 * @description: 将要发送的数据拆分成串口协议支持的格式,例如:要发送0xAF,则会拆分成0x3A和0x3F两个字节
 *				 因此最终发送的有效数据长度是原来的2倍
 * @params: data: 要发送的数据缓冲
 * 			len: 要拆分的数据长度
 * @return: 拆分处理后总共发送的数据长度
 */
int change_send_data(unsigned char* data, int len)
{
	int i; unsigned char chk_sum;
	int ptr = 0;

	if (len == 0) return 0;

	flash_write_buffer[ptr++] = SOI;  //起始位

	chk_sum = 0;
	for (i = 0; i < len; i++)
	{		
		flash_write_buffer[ptr++] = (unsigned char)((data[i] >> 4) + 0x30);//0x3A=0xA7>>4=0x0A,0x0A+0x30=0x3A
		flash_write_buffer[ptr++] = (unsigned char)((data[i] & 0x0F) + 0x30);//0x37=0x07&0x0F=0x07,0x07+0x30=0x37
		chk_sum += (unsigned char)((data[i] >> 4) + 0x30);//check sum
		chk_sum += (unsigned char)((data[i] & 0x0F) + 0x30);
	}

	flash_write_buffer[ptr++] = (unsigned char)((chk_sum >> 4) + 0x30);
	flash_write_buffer[ptr++] = (unsigned char)((chk_sum & 0x0F) + 0x30);
	flash_write_buffer[ptr++] = EOI;//结束位

	return ptr;
}

/*
 * *****************************RS485通信数据帧的格式****************************
 *
 * -----------------------------------------------------------------------------------------------------
 * 序号         1           2           3           4           5           6           7           8
 * -----------------------------------------------------------------------------------------------------
 * 字节数       1           1           1           1           1           n           1           1
 * -----------------------------------------------------------------------------------------------------
 * 格式         SOI         ADR         CID1        CID2        LENGTH      INFO        CHKSUM      EOI
 * -----------------------------------------------------------------------------------------------------
 * 说明         帧头        地址        命令1       命令2       数据长度    数据        校验        帧尾
 * -----------------------------------------------------------------------------------------------------
 *
 *
 * 基本格式说明:
 *
 * 1、SOI和EOI是固定的起始和结束标志位,分别是0x7E和0x0D
 * 2、ADR:485通信的从机地址(1-254)
 * 3、CID1:控制标识码(直流配电0x42),这里是固定值0x42
 * 4、CID2:命令信息,这是具体的动作类型
 * 5、LENGTH:INFO中的数据长度
 * 6、CHKSUM:校验和码
 *
 * 数据格式:
 *
 * 除了起始和结束位是16进制的实际值传输外,其他各项都是以16进制拆成两字节外加0x30传输
 * 例如:当CID1=0x42,实际传输的是两个字节0x34和0x32,传输0xFD时,实际传输的是0x3F和0x3D
 *
 */
 /* @function name: parse_received_data
  * @description: 用于记录关键步骤的执行结果或者是错误码
  * @params: buf, 收到的数据缓冲
  * 		 len, 收到的数据长度
  * 		 id, RS485从机地址
  * @return: none
  */
unsigned char parse_received_data(unsigned char* buf, int len, unsigned char id)
{
	unsigned char i, j, chk_sum;

	j = 0;
	chk_sum = 0;
	for (i = 1; i < len - 1;)
	{
		//比如收到的数据是0x3A和0x37,则实际要合并为0xA7
		//0x3A-0x30=0x0A,0x0A<<4=0xA0
		//0x37-0x30=0x07
		//0xA0+0x07=0xA7
		//buf中保存的数据是从第二个数据开始的(起始位的下一个)
		parsed_rcv_buff[j++] = (((buf[i] - 0x30) << 4) + (buf[i + 1] - 0x30));
		i += 2;
	}

	chk_sum = 0;
	for (i = 1; i < len - 3; i++)
	{
		chk_sum += buf[i];
	}

	//判断接收到的数据中的485从机地址是否和要操作的从机地址匹配
	if (!((parsed_rcv_buff[0] == id) || (parsed_rcv_buff[0] == 0x00)))
		return(NO_BASE_ID);

	//校验和错误
	if (chk_sum != parsed_rcv_buff[j - 1])
		return(CHK_ERR);

	//错误的命令,数据帧错误
	if ((buf[0] != SOI) || (buf[len - 1] != EOI) || (parsed_rcv_buff[1] != CID1))
		return(CMD_ERR);


	//因为每个字节都拆成了0x30+(0-F)之间的值,所以不能大于0x3F
	for (i = 1; i < len - 1; i++)
	{
		if ((buf[i] > 0x3F) || (buf[i] < 0x30))
		{
			return (DATA_INV);
		}
	}

	return (NORMAL);
}

/* @function name: send_package
 * @description: 发送数据包
 * @params: serport1fd: 串口句柄
 * 			buf: 要发送的数据缓冲
 * 			len: 要发送的数据长度
 * 			num_retry: 发送重试次数
 * @return: 错误代码,如果发送成功返回实际发送的字节数
 */
int send_package(int serport1fd, unsigned char* buf, int len, int num_retry)
{
	int retry_count = num_retry;
	int return_status;
	while (1)
	{
		if (!retry_count) return ERROR_RETRY_LIMIT;
		retry_count--;
		return_status = write(serport1fd, buf, len);

		switch (return_status)
		{
		case 0:
			continue;
		default:
			return return_status;
		}

		break;
	}
	return return_status;
}

/*
* function name: main
* descrption: 主函数
* params: argc, 参数长度
*		  argv, 参数列表
*		  程序共有3个参数,分别说明如下:
*		  argv[1]: 升级文件名,hex后缀
*		  argv[2]: RS485从机地址
*		  argv[3]: 串口设备资源
* return: none
*/
int main(int argc, char* argv[])
{
	//printf("%s Welcome you back!\n", "Download Slave");
	//return 0;
	//pthread_t pid1, pid2;
	//pthread_attr_t *pthread_arr1, *pthread_arr2;
	//pthread_arr1 = NULL;
	//pthread_arr2 = NULL;
	int serport1fd;
	int i = 0;
	int size = 0;
	int ret = 0;
	int data_ptr = 0;
	FILE* fp;
	const char* pFile;
	unsigned char rcv_buff[BUFFER_SIZE] = { 0 };//receive buffer
	int rcv_len = 0;//receive data length
	long chk_sum = 0;
	unsigned char hex[2] = { 0x00 };
	unsigned char out_buffer[BUFFER_SIZE + 4] = { 0x00 };//64 bytes data+4 bytes address
	unsigned char version_old = 0x00;//software version read from the MCU
	unsigned char version_new = 0x00;//the new software version
	unsigned char rs485_id = 0x00;//RS485 slave address

	printf("DownloadSlave is loaded!\n"); /* prints !!!DownloadSlave!!! */
	if (strlen(argv[1]) < 5)
	{
		printf("Invalid firmware file!\n");
		exit(0);
	}
	pFile = strrchr(argv[1], '.'); //判断输入的更新固件文件名最后输出.的位置
	if (pFile != NULL) {
		if (strcmp(pFile, ".hex") != 0)
		{
			printf("Invalid firmware file!\n");
			exit(0);
		}
	}
	else
	{
		printf("Please enter a valid hex file name!\n");
		exit(0);
	}
	if (strlen(argv[2]) != 2)
	{
		printf("RS485 client ID is invalid!\n");
		exit(0);
	}

	printf("Prepare to open serial port!\n");
	termios_t* ter_s = (termios_t*)malloc(sizeof(*ter_s));//进行串口参数设置
	//argv[3]表示串口资源,一般ttyS0对应COM1,ttyS1对应COM2
	//目前虚拟机上对应的串口资源是ttyS1
	serport1fd = open(argv[3], O_RDWR | O_NOCTTY | O_NDELAY);//不成为控制终端程序,不受其他程序输出输出影响
	if (serport1fd < 0) {
		printf("%s Open faild\r\n", argv[3]);
		exit(0);
	}
	bzero(ter_s, sizeof(*ter_s));
	ter_s->c_cflag |= CLOCAL | CREAD; //激活本地连接与接收使能
	ter_s->c_cflag &= ~CSIZE;//失能数据位屏蔽
	ter_s->c_cflag |= CS8;//8位数据位
	ter_s->c_cflag &= ~CSTOPB;//1位停止位
	ter_s->c_cflag &= ~PARENB;//无校验位
	ter_s->c_cc[VTIME] = 0;
	ter_s->c_cc[VMIN] = 0;

	/*1 VMIN> 0 && VTIME> 0
		VMIN为最少读取的字符数,当读取到一个字符后,会启动一个定时器,在定时器超时事前,如果已经读取到了VMIN个字符,则read返回VMIN个字符。如果在接收到VMIN个字符之前,定时器已经超时,则read返回已读取到的字符,注意这个定时器会在每次读取到一个字符后重新启用,即重新开始计时,而且是读取到第一个字节后才启用,也就是说超时的情况下,至少读取到一个字节数据。
		2 VMIN > 0 && VTIME== 0
		在只有读取到VMIN个字符时,read才返回,可能造成read被永久阻塞。
		3 VMIN == 0 && VTIME> 0
		和第一种情况稍有不同,在接收到一个字节时或者定时器超时时,read返回。如果是超时这种情况,read返回值是0。
		4 VMIN == 0 && VTIME== 0
		这种情况下read总是立即就返回,即不会被阻塞。
	*/
	cfsetispeed(ter_s, B9600);//设置输入波特率
	cfsetospeed(ter_s, B9600);//设置输出波特率
	tcflush(serport1fd, TCIFLUSH);//刷清未处理的输入和/或输出

	if (tcsetattr(serport1fd, TCSANOW, ter_s) != 0) {
		printf("Com set error!\r\n");
		free(ter_s);
		close(serport1fd);
		exit(0);
	}
	//read the software version from MCU
	rs485_id = str_to_hex(extra_sub_str(argv[2], 0, 2), hex, 2);
	unsigned char ver_info[] = {
		rs485_id,
		CID1,
		CMD_READ_SW_INFO,//command
		0x00//data length
	};
	int len = change_send_data(ver_info, sizeof(ver_info));
	/*
	*	--->7E 30 32 34 32 3A 31 30 30 39 33 0D //send buffer formatted	
		<---7E 30 32 34 32 30 30 30 33 30 32 31 33 31 34 3B 36 0D //received buffer unparsed
	*/
	ret = send_package(serport1fd, flash_write_buffer, len, NUM_RETRY);
	for (i = 0; i < len; i++)
	{
		printf("Data info: %X\r\n", flash_write_buffer[i]);//hex format
	}
	if (ret > 0) {
		printf("Send out QUERY command successfully!\r\n");
	}
	else {
		printf("Send data error!\r\nError code: %d\n", ret);		
		free(ter_s);
		close(serport1fd);
		exit(0);
	}
	usleep(100000);
	rcv_len = read(serport1fd, rcv_buff, BUFFER_SIZE);
	if (rcv_len > 0)
	{
		if (parse_received_data(rcv_buff, rcv_len, rs485_id) == NORMAL)
		{
			version_old = parsed_rcv_buff[5];//please refer to the protocol,parsed_rcv_buff[6]=hardware version
			printf("Valid response from the client. Data len=%d\n", rcv_len);
		}
		else
		{
			printf("Client response has error!\n");
			free(ter_s);
			close(serport1fd);
			exit(0);
		}
	}	
	else
	{
		printf("Client has no response!\n");
		free(ter_s);
		close(serport1fd);
		exit(0);
	}	

	char* buffer = (char*)malloc(10 * sizeof(char));//one single line of the hex file
	if (buffer == NULL) {
		//memory allocating failed
		printf("Apply for memory failed.!\n");
		free(ter_s);
		close(serport1fd);
		exit(0);
	}

	fp = fopen(argv[1], "r");//open the hex file
	if (fp == NULL)
	{
		printf("Can not load file %s!\n", argv[1]);
		free(ter_s);
		close(serport1fd);
		exit(0);
	}
	//get the file size of the hex file
	fseek(fp, 0, SEEK_END);//locate the end of the file
	size = ftell(fp); //get the offset and the size of the file
	//printf("file size------>:%d\n", size);
	//fseek(fp, 0, SEEK_SET);
	rewind(fp);//relocate the start of the file
	int page_nums = ((size - 6) + 2) / 6 / BUFFER_SIZE;//total pages to be sent, the first byte is version
	int single = (((size - 6) + 2) / 6) % BUFFER_SIZE;//the total bytes that are less than a page
	printf("file size: ------>:%d\n", size);
	printf("number of pages: ------>:%d\n", page_nums);
	printf("single bytes: ------>:%d\n", single);
	unsigned char* data_buffer = (unsigned char*)malloc((size - 6 + 2) / 6 * sizeof(unsigned char));//total bytes of the hex file
	if (data_buffer == NULL)
	{
		printf("Apply for memory failed!\n");
		free(buffer);
		fclose(fp);
		free(fp);
		free(ter_s);
		close(serport1fd);
		exit(0);
	}
	i = 0;
	while (!feof(fp))
	{
		if (i > 0)
		{
			fgets(buffer, 10, fp);//read one line of the txt file		
			//printf("%s\n",extra_sub_str(buffer,2,2));
			char* temp = extra_sub_str(buffer, 2, 2);
			str_to_hex(temp, hex, 2);
			data_buffer[i-1] = hex[0];
			chk_sum += hex[0];
			//printf("%s", buffer);
			free(temp);
		}
		else//the first line is the software version
		{
			fgets(buffer, 10, fp);
			char* temp = extra_sub_str(buffer, 2, 2);
			str_to_hex(temp, hex, 2);
			version_new = hex[0];
			free(temp);
		}
		i++;
	}
	fclose(fp);
	if (version_new == version_old)//the same version
	{
		printf("Please make sure the new firmware %X is the latest version!\n", version_new);
		free(ter_s);
		close(serport1fd);
		free(buffer);
		free(data_buffer);
		data_buffer = NULL;
		buffer = NULL;
		exit(0);
	}	
	printf("Check sum of all bytes: ------>:%d\n", (int)chk_sum);
	//str_to_hex(extra_sub_str(argv[2], 0, 2), hex, 2);
	unsigned char erase[] = {
		rs485_id,
		CID1,
		CMD_ERASE,
		0x08,//data length
		0x56,//letter 'V'
		version_new,//version
		((size - 6 + 2) / 6) >> 8,//data length, two bytes
		((size - 6 + 2) / 6) & 0xFF,
		(chk_sum & 0xFF000000) >> 24,//check sum info, four bytes
		(chk_sum & 0x00FF0000) >> 16,
		(chk_sum & 0x0000FF00) >> 8,
		chk_sum & 0x000000FF
	};
	len = change_send_data(erase, sizeof(erase));
	//ret = write(serport1fd, flash_write_buffer, len);
	ret = send_package(serport1fd, flash_write_buffer, len, NUM_RETRY);
	for (i = 0; i < len; i++)
	{
		printf("Data info: %X\r\n", flash_write_buffer[i]);//hex format
	}
	if (ret > 0) {
		printf("Send out CHECK SUM command successfully!\r\n");
	}
	else {
		printf("Send data error!\r\nError code: %d\n", ret);
		//goto error_proc;
		free(data_buffer);
		free(buffer);
		data_buffer = NULL;
		buffer = NULL;
		free(ter_s);
		close(serport1fd);
		exit(0);
	}
	usleep(800000);//wait for the MCU to erase the flash
	rcv_len = read(serport1fd, rcv_buff, BUFFER_SIZE);
	if (rcv_len > 0)
	{
		//unsigned error_code=parse_received_data(rcv_buff, rcv_len, 0x02);
		if (parse_received_data(rcv_buff, rcv_len, rs485_id) == NORMAL)
		{
			printf("Valid response from the client. Data len=%d\n", rcv_len);
		}
		else
		{
			printf("Client response has error!\n");
			//goto error_proc;
			free(data_buffer);
			free(buffer);
			data_buffer = NULL;
			buffer = NULL;
			free(ter_s);
			close(serport1fd);
			exit(0);
		}
	}
	else
	{
		printf("Client has no response!\n");
		//goto error_proc;
		free(data_buffer);
		free(buffer);
		data_buffer = NULL;
		buffer = NULL;
		free(ter_s);
		close(serport1fd);
		exit(0);
	}

	while (page_nums--)
	{
		out_buffer[0] = rs485_id;
		//out_buffer[0] = (unsigned char)argv[2];
		out_buffer[1] = CID1;
		out_buffer[2] = CMD_FIRMWARE_DATA;//command
		out_buffer[3] = BUFFER_SIZE;//data length
		for (i = 0; i < BUFFER_SIZE; i++)
		{
			out_buffer[i + 4] = data_buffer[data_ptr + i];
		}

		int ptr = change_send_data(out_buffer, BUFFER_SIZE + 4);
		//ret = write(serport1fd, flash_write_buffer, ptr);
		ret = send_package(serport1fd, flash_write_buffer, ptr, NUM_RETRY);
		if (ret > 0) {
			//printf("Send out package successfully!\r\n");
		}
		else {
			printf("Send out package error!\r\nError code: %d\n", ret);
			//goto error_proc;
			free(data_buffer);
			free(buffer);
			data_buffer = NULL;
			buffer = NULL;
			free(ter_s);
			close(serport1fd);
			exit(0);
		}
		usleep(200000);
		rcv_len = read(serport1fd, rcv_buff, BUFFER_SIZE);
		if (rcv_len > 0)
		{
			//unsigned error_code=parse_received_data(rcv_buff, rcv_len, 0x02);
			ret = parse_received_data(rcv_buff, rcv_len, rs485_id);
			if (ret == NORMAL)
			{
				//printf("Valid response from the client. Data len=%d\n", rcv_len);
				//
				//for (i = 0; i < rcv_len; i++)
				//{
					//printf("Received data: %x\n", rcv_buff[i]);
				//}
			}
			else
			{
				printf("Client response has error!\r\nError code: %d\n", ret);
				//
				//for (i = 0; i < rcv_len; i++)
				//{
					//printf("Received data: %x\n", rcv_buff[i]);
				//}
				//goto error_proc;
				free(data_buffer);
				free(buffer);
				data_buffer = NULL;
				buffer = NULL;
				free(ter_s);
				close(serport1fd);
				exit(0);
			}
		}
		else
		{
			printf("Client has no response!\n");
			//goto error_proc;
			free(data_buffer);
			free(buffer);
			data_buffer = NULL;
			buffer = NULL;
			free(ter_s);
			close(serport1fd);
			exit(0);
		}
		//tcflush(serport1fd, TCIFLUSH);
		//parse the received buffer according to the protocol used in EPA390
		data_ptr += BUFFER_SIZE;
		if (data_ptr >= (size - 6 + 2) / 6)//no single bytes
		{
			printf("All packages have been sent out with %d pages!\n", page_nums);
		}
		printf("Sending packages: %d%s completed...\r\n",(data_ptr * 100) / ((size - 6 + 2) / 6), "%");
		printf("\033[A");
	}//end while
	if (single > 0)
	{
		out_buffer[0] = rs485_id;
		out_buffer[1] = CID1;
		out_buffer[2] = CMD_FIRMWARE_DATA;
		out_buffer[3] = single;
		for (i = 0; i < single; i++)
		{
			out_buffer[i + 4] = data_buffer[data_ptr + i];
		}
		int ptr = change_send_data(out_buffer, single + 4);
		//ret = write(serport1fd, flash_write_buffer, ptr);
		ret = send_package(serport1fd, flash_write_buffer, ptr, NUM_RETRY);
		if (ret > 0) {
			printf("Send out the last package successfully!\r\n");
		}
		else {
			printf("Send out package error!\r\nError code: %d\n", ret);
			//goto error_proc;
			free(data_buffer);
			free(buffer);
			data_buffer = NULL;
			buffer = NULL;
			free(ter_s);
			close(serport1fd);
			exit(0);
		}
		usleep(200000);
		rcv_len = read(serport1fd, rcv_buff, BUFFER_SIZE);
		if (rcv_len > 0)
		{
			//unsigned error_code=parse_received_data(rcv_buff, rcv_len, 0x02);
			if (parse_received_data(rcv_buff, rcv_len, rs485_id) == NORMAL)
			{
				printf("Valid response from the client. Data len=%d\n", rcv_len);
				//
				//for (i = 0; i < rcv_len; i++)
				//{
					//printf("Received data: %x\n", rcv_buff[i]);
				//}
			}
			else
			{
				printf("Client response has error!\n");				
				//for (i = 0; i < rcv_len; i++)
				//{
					//printf("Received data: %x\n", rcv_buff[i]);
				//}
				//goto error_proc;
				free(data_buffer);
				free(buffer);
				data_buffer = NULL;
				buffer = NULL;
				free(ter_s);
				close(serport1fd);
				exit(0);
			}
		}
		else
		{
			printf("Client has no response!\n");
			//goto error_proc;
			free(data_buffer);
			free(buffer);
			data_buffer = NULL;
			buffer = NULL;
			free(ter_s);
			close(serport1fd);
			exit(0);
		}
		data_ptr += single;
		if (data_ptr >= (size - 6 + 2) / 6)
		{
			printf("All data has been sent out with %d single bytes!\n", single);
		}
	}
	printf("Prepare to reset the MCU and update the firmware...\n");
	sleep(3);	
	//sent the reset command to update the firmware
	unsigned char reset_mcu[] = {
		rs485_id,
		CID1,
		CMD_RESET,
		0x00
	};
	len = change_send_data(reset_mcu, sizeof(reset_mcu));
	//ret = write(serport1fd, flash_write_buffer, len);
	ret = send_package(serport1fd, flash_write_buffer, len, NUM_RETRY);
	for (i = 0; i < len; i++)
	{
		printf("Data info: %X\r\n", flash_write_buffer[i]);//hex format
	}
	if (ret > 0) {
		printf("Send out RESET command successfully!\r\n");
	}
	else {
		printf("Send data error!\r\nError code: %d\n", ret);
		free(data_buffer);
		free(buffer);
		data_buffer = NULL;
		buffer = NULL;
		free(ter_s);
		close(serport1fd);
		exit(0);
		//goto error_proc;
	}
	//sleep(1);
	printf("Ready to run the bootloader...\r\n");
	//struct timespec start;
	struct timeval start;
	__TIME_T_TYPE now = 0;
	__TIME_T_TYPE old = 0;
	gettimeofday(&start, NULL);
	old = start.tv_sec;
	now = start.tv_sec + 2;
	while (now - old <= 5)//check the state of the firmware update process
	{
		usleep(10);
		rcv_len = read(serport1fd, rcv_buff, BUFFER_SIZE);
		/*
		if (rcv_len > 0)
		{
			printf("The received data is: %d", rcv_buff[0]);
		}
		*/
		gettimeofday(&start, NULL);
		now = start.tv_sec;
		switch (rcv_buff[0])
		{
			case 13:
				rcv_buff[0] = 0xFF;
				printf("***** Updating the new firmware succeeded! *****\r\n");
				break;
			case 1:
				rcv_buff[0] = 0xFF;
				printf("The bootloader is now using the old version of the end application from the running application.\r\n");
				break;
			case 2:
				rcv_buff[0] = 0xFF;
				printf("The bootloader is trying to use the old firmware from backup.\r\n");
				break;
			case 3:
				rcv_buff[0] = 0xFF;
				printf("The new firmware is invalid or missing!\r\n");
				break;
			case 4:
				rcv_buff[0] = 0xFF;
				printf("The backup firmware, that is, the old application, is invalid or missing!\r\n");
				break;
			case 5:
				rcv_buff[0] = 0xFF;
				printf("ROM address is out of range!\r\n");
				break;
			case 6:
				rcv_buff[0] = 0xFF;
				printf("The check sum of the bytes written to the ROM is not equal to that of the bytes stored in the external flash.\r\n");
				break;
			case 7:
				rcv_buff[0] = 0xFF;
				printf("The bootloader is trying to use the new downloaded firmware!\r\n");
				break;
			case 8:
				rcv_buff[0] = 0xFF;
				printf("The new firmware is not integral.\r\nMaybe some bytes of it are missing.");
				break;
			case 9:
				rcv_buff[0] = 0xFF;
				printf("The check sum of the new firmware has error!\r\n");
				break;
			case 10:
				rcv_buff[0] = 0xFF;
				printf("The check sum of the new firmware is integral and correct.\r\n");
				break;
			case 11:
				rcv_buff[0] = 0xFF;
				printf("The bootloader is now saving the old firmware to the external flash.\r\n");
				break;
			case 12:
				rcv_buff[0] = 0xFF;
				printf("Saving the old firmware to the external flash failed!\r\n");
				break;
			default:
				rcv_buff[0] = 0xFF;
				break;
		}		
	}
	free(data_buffer);
	free(buffer);
	data_buffer = NULL;
	buffer = NULL;
	free(ter_s);
	close(serport1fd);
	return EXIT_SUCCESS;
/*
error_proc:	
	free(data_buffer);
	free(buffer);
	data_buffer = NULL;
	buffer = NULL;
	free(ter_s);
	close(serport1fd);
	exit(0);
*/
}

MX25L1606EM2I-12G Flash的驱动如下(我是使用IO口模拟SPI通信),这个驱动似乎和W25Q16JVUXIQ系列可以通用:

flash.c

#include "flash.h"
#include "mcc_generated_files/mcc.h"


#define 	PORTBIT(ADR,BIT_LOC) ((unsigned)(&ADR)*8+(BIT_LOC))

static  bit SON	@PORTBIT(PORTB,5);//MISO

typedef enum {RESET = 0, SET = !RESET} FlagStatus;


/*
 * 函数名:Flash_ReadWriteByte
 * 描述  :模拟SPI通信的写字节并返回数据
 * 输入  :dat:要发送的一字节数据
 * 输出  : 无
 * 调用  :外部调用
 */
uchar Flash_ReadWriteByte(uchar dt) 
{
    uchar i=0;
    uchar temp = 0;

    for (i = 8; i > 0; i--) 
    {
        if (dt & 0x80) 
        {
            SIN(1);
        } 
        else 
        {
            SIN(0);
        }

        dt <<= 1;
        SCKN(1);
        temp <<= 1;
        if (SON) //接收数据
        {
            temp++;
        }
        SCKN(0);
    }

    return temp;
}


/*
 * 函数名:Write_One_Byte
 * 描述  :模拟SPI通信的写字节
 * 输入  :dat:要发送的一字节数据
 * 输出  : 无
 * 调用  :外部调用
 */
void Write_OneByte(uchar dat)
{
	uchar i;	
	
	for(i=0;i<8;i++)//写入一字节数据
	{
		SCKN(0);//SCK处于空闲状态
		//Delay_us(10);
		if (dat & 0x80)//如果当前数据传输的比特位是1,则从机SI写入1,否则为写入0
		{
			SIN(1);
		}
		else
		{
			SIN(0);
		}
		SCKN(1);//SCK开始采样
		//Delay_us(10);//稳定数据
		dat <<=1;//数据一位一位地发送到从机的SI
	}
	SCKN(0);//SCK处于空闲状态
}

/*
 * 函数名:Read_One_Byte
 * 描述  :模拟SPI通信的读一字节程序
 * 输入  :无
 * 输出  : 无
 * 调用  :外部调用
 */
uchar Read_OneByte(void)
{
	uchar i, temp;
	SCKN(0);//为0时设备SCK处于空闲状态
	temp=0;	
	for(i=0;i<8;i++)
	{		
		SCKN(1);//SCK开始采样
		//Delay_us(10);//稳定数据
		temp <<=1;//一位一位移入数据
		//temp |= SON;//从从机的SO输出口得到数据
        temp |= (PORTD &(1<<2));//从从机的SO输出口得到数据
		SCKN(0);//为0SCK处于空闲状态	
		//Delay_us(10);//稳定数据
	}
	return temp;
}

/*******************************************************************************
* Function Name  : Flash_WriteEnable
* Description    : Enables the write access to the FLASH.
* Input          : None
* Output         : None
* Return         : None
*******************************************************************************/
void Flash_WriteEnable(void)
{
  /* Select the FLASH: Chip Select low */
  CS(0);

  /* Send "Write Enable" instruction */
  //Write_OneByte(MX25L1605D_WriteEnable);
  Flash_ReadWriteByte(MX25L1605D_WriteEnable);
  /* Deselect the FLASH: Chip Select high */
  CS(1);
}

/*******************************************************************************
* Function Name  : Flash_WaitForWriteEnd
* Description    : Polls the status of the Write In Progress (WIP) flag in the
*                  FLASH's status  register  and  loop  until write  opertaion
*                  has completed.
* Input          : None
* Output         : None
* Return         : None
*******************************************************************************/
void Flash_WaitForWriteEnd(void)
{
  uchar FLASH_Status = 0;

  /* Select the FLASH: Chip Select low */
  CS(0);

  /* Send "Read Status Register" instruction */
  //Write_OneByte(MX25L1605D_ReadStatusReg);
  Flash_ReadWriteByte(MX25L1605D_ReadStatusReg);

  /* Loop as long as the memory is busy with a write cycle */
  do
  {
    /* Send a dummy byte to generate the clock needed by the FLASH
    and put the value of the status register in FLASH_Status variable */
    //FLASH_Status = Write_OneByte(Dummy_Byte);
    FLASH_Status = Flash_ReadWriteByte(Dummy_Byte);
  }
  while ((FLASH_Status & WIP_Flag) == SET); /* Write in progress */

  /* Deselect the FLASH: Chip Select high */
  CS(1);
}

/*******************************************************************************
* Function Name  : Flash_BlockErase
* Description    : Erases the specified FLASH Block.
*                  查看手册块擦除时间大约为400ms
* Input          : BlockAddr: address of the block to erase.
* Output         : None
* Return         : None
*******************************************************************************/
void Flash_BlockErase(ulong blockAddr)
{
  /* Send write enable instruction */
  Flash_WriteEnable();
  Flash_WaitForWriteEnd();
  
  /* Select the FLASH: Chip Select low */
  CS(0);
  
  /* Send Sector Erase instruction */
  //Write_OneByte(MX25L1605D_BlockErase);
  Flash_ReadWriteByte(MX25L1605D_BlockErase);
  
  /* Send SectorAddr high nibble address byte */
  //Write_OneByte((blockAddr & 0xFF0000) >> 16);
  Flash_ReadWriteByte((blockAddr & 0xFF0000) >> 16);
  
  /* Send SectorAddr medium nibble address byte */
  //Write_OneByte((blockAddr & 0xFF00) >> 8);
  Flash_ReadWriteByte((blockAddr & 0xFF00) >> 8);
  
  /* Send SectorAddr low nibble address byte */
  //Write_OneByte(blockAddr & 0xFF);
  Flash_ReadWriteByte(blockAddr & 0xFF);
  
  /* Deselect the FLASH: Chip Select high */
  CS(1);
  
  /* Wait the end of Flash writing */
  Flash_WaitForWriteEnd();
}

/*******************************************************************************
* Function Name  : SPI_FLASH_PageWrite
* Description    : Writes more than one byte to the FLASH with a single WRITE
*                  cycle(Page WRITE sequence). The number of byte can't exceed
*                  the FLASH page size.
* Input          : - pBuffer : pointer to the buffer  containing the data to be
*                    written to the FLASH.
*                  - WriteAddr : FLASH's internal address to write to.
*                  - NumByteToWrite : number of bytes to write to the FLASH,
*                    must be equal or less than "SPI_FLASH_PageSize" value.
* Output         : None
* Return         : None
*******************************************************************************/
void Flash_PageWrite(uchar* pBuffer, ulong WriteAddr, uint NumByteToWrite)
{
  /* Enable the write access to the FLASH */
  Flash_WriteEnable();

  /* Select the FLASH: Chip Select low */
  CS(0);
  
  /* Send "Write to Memory " instruction */
  Flash_ReadWriteByte(MX25L1605D_PageProgram);
  
  /* Send WriteAddr high nibble address byte to write to */
  Flash_ReadWriteByte((WriteAddr & 0xFF0000) >> 16);
  
  /* Send WriteAddr medium nibble address byte to write to */
  Flash_ReadWriteByte((WriteAddr & 0xFF00) >> 8);
  
  /* Send WriteAddr low nibble address byte to write to */
  Flash_ReadWriteByte(WriteAddr & 0xFF);

  if(NumByteToWrite > SPI_FLASH_PerWritePageSize)//最大256字节写入一次
  {
     NumByteToWrite = SPI_FLASH_PerWritePageSize;
     //printf("\n\r Err: SPI_FLASH_PageWrite too large!");
  }

  /* while there is data to be written on the FLASH */
  while (NumByteToWrite--)
  {
    /* Send the current byte */
    Flash_ReadWriteByte(*pBuffer);
    /* Point on the next byte to be written */
    pBuffer++;
  }

  /* Deselect the FLASH: Chip Select high */
  CS(1);

  /* Wait the end of Flash writing */
  Flash_WaitForWriteEnd();
}

/*******************************************************************************
* Function Name  : SPI_FLASH_BufferWrite
* Description    : Writes block of data to the FLASH. In this function, the
*                  number of WRITE cycles are reduced, using Page WRITE sequence.
* Input          : - pBuffer : pointer to the buffer containing the data to be
*                    written to the FLASH.
*                  - WriteAddr : FLASH's internal address to write to.
*                  - NumByteToWrite : number of bytes to write to the FLASH.
* Output         : None
* Return         : None
*******************************************************************************/
void Flash_BufferWrite(uchar* pBuffer, ulong WriteAddr, uint NumByteToWrite)
{
  uchar NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;

  Addr = WriteAddr % SPI_FLASH_PageSize;
  count = SPI_FLASH_PageSize - Addr;
  NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
  NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

  if (Addr == 0) /* WriteAddr is SPI_FLASH_PageSize aligned  */
  {
    if (NumOfPage == 0) /* NumByteToWrite < SPI_FLASH_PageSize */
    {
      Flash_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
    }
    else /* NumByteToWrite > SPI_FLASH_PageSize */
    {
      while (NumOfPage--)
      {
        Flash_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;
        pBuffer += SPI_FLASH_PageSize;
      }

      Flash_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
  }
  else /* WriteAddr is not SPI_FLASH_PageSize aligned  */
  {
    if (NumOfPage == 0) /* NumByteToWrite < SPI_FLASH_PageSize */
    {
      if (NumOfSingle > count) /* (NumByteToWrite + WriteAddr) > SPI_FLASH_PageSize */
      {
        temp = NumOfSingle - count;

        Flash_PageWrite(pBuffer, WriteAddr, count);
        WriteAddr +=  count;
        pBuffer += count;

        Flash_PageWrite(pBuffer, WriteAddr, temp);
      }
      else
      {
        Flash_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
      }
    }
    else /* NumByteToWrite > SPI_FLASH_PageSize */
    {
      NumByteToWrite -= count;
      NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
      NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

      Flash_PageWrite(pBuffer, WriteAddr, count);
      WriteAddr +=  count;
      pBuffer += count;

      while (NumOfPage--)
      {
        Flash_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;
        pBuffer += SPI_FLASH_PageSize;
      }

      if (NumOfSingle != 0)
      {
        Flash_PageWrite(pBuffer, WriteAddr, NumOfSingle);
      }
    }
  }
}

/*******************************************************************************
* Function Name  : Flash_BufferRead
* Description    : Reads a block of data from the FLASH.
* Input          : - pBuffer : pointer to the buffer that receives the data read
*                    from the FLASH.
*                  - ReadAddr : FLASH's internal address to read from.
*                  - NumByteToRead : number of bytes to read from the FLASH.
* Output         : None
* Return         : None
*******************************************************************************/
void Flash_BufferRead(uchar* pBuffer, ulong ReadAddr, uint NumByteToRead)
{
  /* Select the FLASH: Chip Select low */
  CS(0);

  /* Send "Read from Memory " instruction */
  Flash_ReadWriteByte(MX25L1605D_ReadData);

  /* Send ReadAddr high nibble address byte to read from */
  Flash_ReadWriteByte((ReadAddr & 0xFF0000) >> 16);
  /* Send ReadAddr medium nibble address byte to read from */
  Flash_ReadWriteByte((ReadAddr& 0xFF00) >> 8);
  /* Send ReadAddr low nibble address byte to read from */
  Flash_ReadWriteByte(ReadAddr & 0xFF);

  while (NumByteToRead--) /* while there is data to be read */
  {
    /* Read a byte from the FLASH */
    *pBuffer = Flash_ReadWriteByte(Dummy_Byte);
    /* Point to the next location where the byte read will be saved */
    pBuffer++;
  }

  /* Deselect the FLASH: Chip Select high */
  CS(1);
}

flash.h
/* 
 * File:   flash.h
 * Author: PD821
 *
 * Created on March 11, 2023, 3:12 PM
 */
//#include "GLOABL.h"
#define		uchar		unsigned	char
#define		uint		unsigned	int
#define		ulong		unsigned	long

#ifndef FLASH_H
#define	FLASH_H

#ifdef	__cplusplus
extern "C" {
#endif
    
/* 模拟SPI通信的相关引脚的定义
 * 
 *      SCKN: 时钟, RC0, Pin32
 *      SIN: 从机输入, RE0, Pin25
 *      SON: 从机输出, RB5, Pin15
 *      CS: 片选信号, RB0, Pin8
 */ 
#define SCKN(a)	if (a)	\
					SetBit(LATC,0);\
					else		\
					ClearBit(LATC,0)

#define SIN(a)	if (a)	\
					SetBit(LATE,0);\
					else		\
					ClearBit(LATE,0)

#define CS(a)	if (a)	\
					SetBit(LATB,0);\
					else		\
					ClearBit(LATB,0)


/* Private typedef -----------------------------------------------------------*/
//#define SPI_FLASH_PageSize      4096
#define SPI_FLASH_PageSize      		256
#define SPI_FLASH_PerWritePageSize      256

/* 用于执行对flash进行操作的各类命令------------------------------------*/
#define MX25L1605D_WriteEnable		    0x06 
#define MX25L1605D_WriteDisable		    0x04 
#define MX25L1605D_ReadStatusReg		0x05 
#define MX25L1605D_WriteStatusReg		0x01 
#define MX25L1605D_ReadData			    0x03 
#define MX25L1605D_FastReadData		    0x0B 
#define MX25L1605D_FastReadDual		    0x3B 
#define MX25L1605D_PageProgram		    0x02 
#define MX25L1605D_BlockErase			0xD8 
#define MX25L1605D_SectorErase		    0x20 
#define MX25L1605D_ChipErase			0xC7 
#define MX25L1605D_PowerDown			0xB9 
#define MX25L1605D_ReleasePowerDown	    0xAB 
#define MX25L1605D_DeviceID			    0xAB 
#define MX25L1605D_ManufactDeviceID   	0x90 
#define MX25L1605D_JedecDeviceID		0x9F 

#define WIP_Flag                  		0x01  //Write In Progress (WIP) flag
#define Dummy_Byte                		0x00  //发送任意数据
 
//Flash基地址及要使用的区域定义,一个块占用64K字节
#define	FLASH_WriteAddress              0x00000000
#define FLASH_Sector1                   FLASH_WriteAddress
#define FLASH_Sector2                   (FLASH_WriteAddress+0x00010000)//old firmware
#define FLASH_Sector3                   (FLASH_WriteAddress+0x00020000)//new firmware
#define FLASH_Sector4                   (FLASH_WriteAddress+0x00030000)//version, data length and check sum
#define FLASH_Sector5                   (FLASH_WriteAddress+0x00040000)//application valid flag

void Flash_SectorErase(ulong SectorAddr);
void Flash_BlockErase(ulong blockAddr);
void Flash_BufferWrite(uchar* pBuffer, ulong WriteAddr, uint NumByteToWrite);
void Flash_PageWrite(uchar* pBuffer, ulong WriteAddr, uint NumByteToWrite);
void Flash_BufferRead(uchar* pBuffer, ulong ReadAddr, uint NumByteToRead);
void Write_Cycle(void);

#ifdef	__cplusplus
}
#endif

#endif	/* FLASH_H */

希望大家可以提出一些意见和建议,和大家共同学习。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值