当前位置:   article > 正文

基于flash的FPGA的在线升级_fpga在线升级

fpga在线升级

基于flash的FPGA的在线升级

一、理论

1.1 在线升级概念

在线升级是指通过网络或其他远程方式对软件、固件或系统进行更新和升级的过程。FPGA的在线升级是指在运行时对FPGA芯片中的逻辑配置进行更新或修改,而无需物理更换芯片。一般开发阶段,开发人员常用JTAG对FPGA进行配置,用于工程的功能修改\调试\更新。但当投入为产品时,想要进行FPGA的固件更新,再通过JTAG来配置FPGA显然是比较麻烦的,所以需要在线升级功能。


1.2 FPGA的配置方式

不同型号和系列的FPGA可能会支持不同的配置模式,这里以7系列FPGA为例,其支持以下几种配置方式:
在这里插入图片描述

图1.1 7系列fpga配置方式

对常用的方式作简介:

  • Master SPI 配置模式:通过SPI总线将配置文件从外部Flash中加载到FPGA内部。这种配置模式可以使用SPI Flash作为配置存储器。在这种模式下,FPGA器件将作为SPI总线的主设备,控制SPI Flash的读取和写入操作。本文基于flash的FPGA在线升级在硬件上正是采用这一种配置方式。
  • Slave Serial 配置模式:通常使用另外的微处理器通过一个串行接口,将配置数据从主控芯片发送到 FPGA配置接口,此方式FPGA被动进行配置。
  • SelectMAP 配置模式:是通过JTAG接口将配置文件从外部Flash中加载到FPGA内部。这种配置模式可以使用JTAG接口实现FPGA的配置。
  • JTAG配置模式:JTAG配置默认最高优先级,在M[2:0]的任何选择下,优先JATG进行配置。

1.3 配置文件bit、bin、mcs

  1. Bitstream(bit):Bitstream是一种二进制文件格式,用于存储FPGA的配置信息。它包含了FPGA逻辑元件的连接和功能等详细信息,以及配置所需的时序和逻辑设置。Bitstream文件通常由FPGA开发工具生成,并通过不同的配置方式(如JTAG、SPI等)加载到FPGA中。

  2. Binary(bin):Binary也是一种二进制文件格式,但与Bitstream不同,它通常是指纯粹的二进制数据文件,没有特定的FPGA配置结构。在某些情况下,可以将FPGA的配置数据导出为二进制文件,这样可以方便地进行备份、传输或其他处理。

  3. MCS(Motorola S-record):MCS是Motorola S-record文件的缩写,是一种常见的文本文件格式(ASCII文件),用于存储数据和程序代码。在FPGA领域,MCS文件通常用于存储FPGA的配置数据。MCS文件包含了地址、数据和校验等信息,可以用于直接编程或烧录FPGA。

  • 总结:bit和bin都是二进制文件,但bit是带有头信息的配置文件,bin文件是不带头信息的的配置文件,如图所示,就前面一部分配置信息不一样,其他的都一样。mcs是ASCII文件,其中两个ASCII字符用于表示数据的每个字节HEX文件,mcs文本结构可参考这篇博客(https://blog.csdn.net/hanhailong0525/article/details/122382501)。
    在这里插入图片描述

    图1.2 bit文件和bin文件16进制对比

二、方案设计及实现

2.1 目标

上位机能够将新的配置文件.mcs更新到FPGA的外部存储器件flash中,从而实现FPGA的在线升级。

2.2 实现方案

上位机通过pcie总线(并行)与FPGA相连,FPGA通过spi总线(串行)与flash相连。首先,上位机对.mcs文件进行预处理,把预处理后的文件数据传到FPGA中,然后FPGA将数据以并转串的方式写入flash中,更换flash里旧的配置文件,从而完成在线升级功能。其中spi_B为FPGA自我配置的专用spi引脚;spi_A为FPGA的普通IO口,用于FPGA向flash写.mcs文件数据的spi通道。
在这里插入图片描述

图2.1 在线升级实现方框图

2.3 硬件设计

在这里插入图片描述

图2.2 7系列FPGA spi配置接口
  1. 硬件上FPGA配置方式选择Master SPI 配置模式,即M[2:0]为3’b001。
  2. 由于FPGA上电启动,通过FPGA专用的spi引脚对flash读取,完成自身配置,配置完成之后FPGA专用引脚的CCLK便不能当做普通IO口使用,所以选择多路复用器进行spi通道选择。当需要在线升级功能时,选择spi_A通道,将.mcs文件并转串写入FLASH中;在完成在线升级后,再将多路复用器切换到spi_B通道。多路复用器选型为6通道、1:2、多路复用/复解器的器件TS3A27518ERTWR。
  3. FLASH由于项目要求,选择读写操作类似于M25P16存储器的一个国产芯片GD25B128ESIG,容量为16MB。

2.4 逻辑设计

FPGA的任务是将上位机下传的预处理后的.mcs文件数据进行并转串处理,然后写入到flash中。设计中上位机将配置文件一个Byte一个Byte的下传,FPGA每接收到一个Byte,便将其并转串,通过spi写入到flash中。因此逻辑上需要设计spi逻辑接口模块,由上位机控制。本质上实现上位机对flash进行读写操作,而FPGA只起着数据转发的作用。
spi接口逻辑设计模块如下:

  • spi_interface.v
`timescale 1ns / 1ps
module spi_interface(
  input ADSP_CLK,       //系统时钟
  input clk_1mhz,       //生成spi cclk时钟,
  input wr_start,       //上位机控制spi写
  input rd_start,       //上位机控制spi读 
  input spi_miso,       //spi miso

  input  [7:0] data_to_flash,   //上位机要写入flash的Byte数据 
  output [7:0] data_to_dsp,     //从flash读取要传入上位机的Byte数据

  output spi_mosi,      //spi mosi
  output reg spi_sck    //spi sck
	 );
 

wire spi_miso;
wire spi_mosi;
wire wr_clk;
wire rd_clk;


always @ (posedge ADSP_CLK) begin
    if(wr_start == 1'b1) begin
		  spi_sck <= wr_clk;
		end
    else if(rd_start == 1'b1) begin
		  spi_sck <= rd_clk;
		end
end

//spi写子模块
spi_write_data spi_write_data_inst(
   .adsp_clk 		(ADSP_CLK)		,
	 .ref_freq		(clk_1mhz)		,
	 .wr_start		(wr_start)		,
	 .data_to_flash	        (data_to_flash)	        ,
	 .wr_clk		(wr_clk)		,
	 .spi_mosi		(spi_mosi)
	 );

//spi读子模块
spi_read_data spi_read_data_inst(
   .adsp_clk		(ADSP_CLK)	,
	 .ref_freq		(clk_1mhz)	,
	 .rd_start		(rd_start)	,
	 .spi_miso		(spi_miso)	,
	 .rd_clk		(rd_clk)	,
	 .data_to_dsp	(data_to_dsp)
	 );

endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • spi_read_data.v

`timescale 1ns / 1ps
module spi_read_data(
	 adsp_clk,
	 ref_freq,
	 rd_start,
	 spi_miso,
	 
	 rd_clk,
	 data_to_dsp
	 );

parameter data_width = 8;
parameter cnt_bit = 3;

input adsp_clk;
input	ref_freq;
input	rd_start;
input	spi_miso;
	 
output rd_clk;
output [data_width-1:0] data_to_dsp;

reg ref_freq_reg;
reg [cnt_bit:0] cnt_clk;
reg ref_freq_en;
reg ref_freq_en1;
reg [cnt_bit:0] cnt_rd;
reg [cnt_bit-1:0] data_bitsel;
reg spi_miso_reg;
reg [data_width-1:0] data_to_dsp_reg;


always @ (posedge adsp_clk)
  begin
    ref_freq_reg <= ref_freq;
  end

always @ (posedge adsp_clk)
  begin
    if(rd_start == 1'b1)
	  begin
		  if((ref_freq_reg == 1'b1) && (ref_freq == 1'b0))
		  begin
			  if(cnt_clk < data_width)
				begin
				  ref_freq_en <= 1'b1;   			
					cnt_clk <= cnt_clk + 1'b1;
				end
				else
				begin
				  ref_freq_en <= 1'b0;			
				end
			end
		end
	  else
	  begin
		  ref_freq_en <= 1'b0;
		  cnt_clk <= 4'b0000;
		end
  end

always @ (posedge adsp_clk)
begin
  if((ref_freq_reg == 1'b1) && (ref_freq == 1'b0))
  begin
    ref_freq_en1 <= ref_freq_en;
  end
end

always @ (posedge adsp_clk)
begin
	spi_miso_reg <= spi_miso;
end

always @ (posedge adsp_clk)
  begin
    if(ref_freq_en == 1'b0)
	  begin
		  cnt_rd <= 4'b0000;
		  data_bitsel <= data_width - 1'b1; 
		end
	  else
	  begin
      if((ref_freq_reg == 1'b0) && (ref_freq == 1'b1))    	///上升沿
      begin
        if(cnt_rd <= data_width)   
        begin
          data_to_dsp_reg[data_bitsel] <= spi_miso_reg;   	///相当于串行转8位并行
          data_bitsel <= data_bitsel - 1'b1;
          cnt_rd <= cnt_rd +1'b1;
        end
      end
		end
  end


assign rd_clk = (ref_freq_en & ref_freq);
assign data_to_dsp = data_to_dsp_reg;

endmodule

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • spi_write_data.v
`timescale 1ns / 1ps
module spi_write_data(
	 adsp_clk,
	 ref_freq,
	 wr_start,
	 data_to_flash,
	 
	 wr_clk,
	 spi_mosi
	 );

parameter data_width = 8;
parameter cnt_bit = 3;

input adsp_clk;
input ref_freq;
input wr_start;
input [data_width-1:0] data_to_flash;

output wr_clk;
output spi_mosi;

reg ref_freq_reg;
reg ref_freq_en;
reg ref_freq_en1;
reg [cnt_bit:0] cnt;
reg [cnt_bit-1:0] data_bitsel;
reg [data_width-1:0] data_to_flash_reg;
reg spi_mosi_reg;
reg spi_mosi_reg1;


always @(posedge adsp_clk)
  begin
    ref_freq_reg <= ref_freq;   ///一拍延时为的是构成后期的下降沿
  end

always @(posedge adsp_clk)
  begin
    if(wr_start == 1'b0)   //不能往SPI flash里面写数据
	  begin
		  ref_freq_en <= 1'b0;
		  cnt <= 4'b0000;
		  data_bitsel <= data_width - 1'b1;   
		  data_to_flash_reg <= data_to_flash;
		end
	  else    ///此时开始往flash里面写fpga里面发过来的数据
	  begin
		  if((ref_freq_reg == 1'b1) && (ref_freq == 1'b0))  ///ref_freq_reg、ref_freq=1MHz //此处是FPGA下降沿的常用写法//
		  begin
			  if(cnt < data_width)
				begin
          ref_freq_en <= 1'b1;
          spi_mosi_reg <= data_to_flash_reg[data_bitsel];   ///移位过程。FPGA发给SPI Flash的是字节(8位并行数据),而SPI只能接受串行数据,故每字节都按高位到低位读取
          data_bitsel <= data_bitsel - 1'b1;     ///相当于并转串
          cnt <= cnt + 1'b1;
				end
				else
        begin
          ref_freq_en <= 1'b0;  //ref_freq_en 为控制data_to_flash读写的
        end
			end
		end
  end

always @(posedge adsp_clk)
  begin
    if((ref_freq_reg == 1'b1) && (ref_freq == 1'b0))
	  begin
		  ref_freq_en1 <= ref_freq_en;
		  spi_mosi_reg1 <= spi_mosi_reg; 
		end
  end

assign spi_mosi = spi_mosi_reg1;
assign wr_clk = (ref_freq_en1 & ref_freq);    ///ref_freq_en==1有效时,可对spiflash进行写入;ref_freq是为1MHz的信号。也即当ref_freq_en==1写入有效时,写时钟为1MHz
 
endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • spi接口模块功能使用总结:例化spi_interface.v模块,当进行写操作时,上位机先将一个Byte数据放到data_to_flash[7:0]中,然后使控制信号wr_start由0变1,产生一个上升沿,且持续至少8个spi_clk时钟周期,使Byte数据并转串发出去;当进行读操作时,先使控制信号rd_start由0变1,产生一个上升沿,且持续至少8个spi_clk时钟周期,使要读的Byte数据串转并接收并放在寄存器data_to_dsp[7:0],由上位机读取。该spi接口功能模块全过程由上位机控制。

2.5 软件设计

2.5.1 flash驱动软件设计

所选型的存储芯片型号为GD25B128E,简介如图所示,该芯片读写方式与M25P16相类似,结合FPGA的spi接口模块逻辑设计,笔者给出FLASH的软件驱动代码作为参考,该驱动代码主要控制FPGA的spi_interface接口逻辑模块,间接性地操控FPGA对FLASH的读写操作,本质上是上位机对FLASH的读写操作。

在这里插入图片描述

图2.3 FLASH的数据手册简介截图
  • FLASH驱动代码

  • flash.c

//==============================================================================
//
// Title:		flash.c
// Purpose:		A short description of the implementation.
//
// Created on:	2023/9/13 at 17:52:11 by Windows User.
// Copyright:	P R C. All Rights Reserved.
//
//==============================================================================

//==============================================================================
// Include files

#include <Ivi.h>
#include "flash.h"
#include "stdio.h"

//地址定义
#define regW_spi_wr				0x12C
#define regW_spi_rd				0x12d
#define regW_wrflash_data 		0x12e
#define regW_spi_flash_cs 		0x12f

#define regW_fpga_prog_b_ctrl  	0x130
#define regW_sw_sel 		    0x131
#define regW_sw_en  		    0x132
#define regW_fpga_prog_b_en		0x133

#define regR_rdflash_data 		0x12C

//高低电平定义
#define sHigh 0x01
#define sLow  0x00

//命令定义 转为32位
#define Fcmd_05H 0x05    //Read Status Register-1
#define Fcmd_35H 0x35    //Read Status Register-2
#define Fcmd_15H 0x15    //Read Status Register-3
#define Fcmd_06H 0x06    //Write Enable
#define Fcmd_60H 0x60    //Chip Erase
#define Fcmd_C7H 0xC7    //Chip Erase
#define Fcmd_20H 0x20    //Sector Erase 
#define Fcmd_02H 0x02    //Page Program
#define Fcmd_03H 0x03    //Read Data

extern ViSession vi;  

//定义一个 全局fifo 用来上位机下发数据
char *mcs_data_p = NULL;


void delayunit()
{
	int i,j;
	for(i=0; i<1; i++)
	{
		for(j=0; j<100; j++);
	}

}


void delayus(unsigned int us )
{
	int i;
	for(i=0; i<us; i++)
	{
		delayunit();
	}

}



/******************************************************************
函数名:Spi_sendByte
备注:  上位机通过pcie写入一个字节,然后fpga的spi并转串发送
参数:  @pByte 要写入的字节
备注:  
*****************************************************************/
void Spi_sendByte(unsigned char pByte)
{
	ViSession io = Ivi_IOSession(vi);
    WriteReg(io, regW_wrflash_data, (unsigned int)pByte);	// 上位机下发一个字节	
    delayus(1);												// 延时
    WriteReg(io, regW_spi_wr, sHigh);						// 拉高spi_wr spi指令发送
    delayus(10);											// 延时
    WriteReg(io, regW_spi_wr, sLow);						// 拉低spi_wr
    delayus(1);												// 延时

}


/******************************************************************
函数名:Spi_recvByte
备注:  fpga的spi串转并接收一个字节,上位机通过pcie总线读接收到
参数:  返回一个字节
备注:
*****************************************************************/
unsigned char Spi_recvByte(void)
{
	ViSession io = Ivi_IOSession(vi);
    unsigned int rd_data;
	delayus(1);	
    WriteReg(io, regW_spi_rd, sHigh);			// 拉高spi_rd spi指令发送
    delayus(10);								// 延时
	WriteReg(io, regW_spi_rd, sLow);			// 拉低spi_rd
    rd_data = ReadReg(io, regR_rdflash_data);	//读取数据    
	delayus(1);									// 延时
    return ((unsigned char)rd_data);

}


/******************************************************************
函数名:Flash_wait_ready
功能: 不断访问寄存器1的WIP值,等待flash内部准备完成
参数:
备注:
*****************************************************************/
void Flash_wait_ready(void)
{
	ViSession io = Ivi_IOSession(vi);
    unsigned char  rState;
    unsigned char fBusy = 1;
    while(fBusy)
    {
        WriteReg(io, regW_spi_flash_cs, sLow);  // cs拉低
        delayus(5);
        Spi_sendByte(Fcmd_05H);                     // 发送读取状态寄存器1的命令05H
        rState = Spi_recvByte();                // 接收返回的状态寄存器1值
	    delayus(5);
        WriteReg(io, regW_spi_flash_cs, sHigh);	// cs拉高	  
        if((rState & 0x01) == 0)                // 判断WIP的值,若为高,代表flash繁忙,等待flash准备好;
        {										//若为低,代表flash空闲,可进行下一步操作,跳出循环
            //fBusy = 0;
			break;
        }
        delayus(200); 
    } 

}


/******************************************************************
函数名:Flash_write_command
功能: 仅往flash里写入一个字节的命令
参数: @Fcmd  要写入flash的命令
备注:
*****************************************************************/
void Flash_write_command(unsigned int Fcmd)
{
	ViSession io = Ivi_IOSession(vi);
    WriteReg(io, regW_spi_flash_cs, sLow);	// cs拉低
    delayus(5);
    Spi_sendByte((unsigned char)Fcmd);      // 发送命令
    delayus(5);
    WriteReg(io, regW_spi_flash_cs, sHigh);	// cs拉高

}


/******************************************************************
函数名:Flash_Chip_Erase
功能:  flash 擦除全部,擦除后值都是1    命令 60H或C7H
参数: @Fcmd  要写入flash的命令
备注:
*****************************************************************/
void Flash_Chip_Erase()
{
	ViSession io = Ivi_IOSession(vi);	
    Flash_write_command(Fcmd_06H);      // 让flash处于可写状态
    delayus(100);
    Flash_write_command(Fcmd_C7H);      // 写入擦除命令
    delayus(100);
    Flash_wait_ready();                 // 等待flash内部完成擦除工作
    delayus(100);

}


/******************************************************************
函数名:Flash_Sector_Erase
功能:  flash 扇区擦除 命令20H
参数: @flashAddr  擦除扇区的地址
备注:
*****************************************************************/
void Flash_Sector_Erase(unsigned int flashAddr)
{
	ViSession io = Ivi_IOSession(vi);	
    Flash_write_command(Fcmd_06H);      			// 让flash处于可写状态
    delayus(100);
//    printf("flash Sector Erase ing...\n");
    WriteReg(io, regW_spi_flash_cs, sLow);			// cs拉低
    delayus(5);
    Spi_sendByte(Fcmd_20H);                 			// 上位机下发命令-Sector_Erase-
    Spi_sendByte(((flashAddr & 0x00FF0000)>>16)); 	// 下发起始地址 16bit~24bit
    Spi_sendByte(((flashAddr & 0x0000FF00)>>8)); 	// 下发起始地址 8bit~16bit
    Spi_sendByte(((flashAddr & 0x000000FF)>>0)); 	// 下发起始地址 0bit~8bit
	delayus(5);
	WriteReg(io, regW_spi_flash_cs, sHigh);			// cs拉高
    delayus(100);
    Flash_wait_ready();                    			// 等待flash工作完成扇区擦除
	delayus(100);
//    printf("finsh Sector Erase!\n");
}


/******************************************************************
函数名:Flash_Read_State 
功能: 读取状态寄存器的值,有三个状态寄存器,分别是State1->05H,State2->35H,state3->15H
参数: @Fcmd  要读取的寄存器命令
备注:
*****************************************************************/
unsigned char Flash_Read_State(unsigned char Fcmd)
{
	ViSession io = Ivi_IOSession(vi);
    unsigned char rState;
	WriteReg(io, regW_spi_flash_cs, sLow);  // cs拉低
	delayus(5);
	Spi_sendByte(Fcmd);      // 发送读取状态寄存器1的命令05H
	rState = Spi_recvByte();                // 接收返回的状态寄存器1值
	delayus(5);
	WriteReg(io, regW_spi_flash_cs, sHigh);	// cs拉高
	return rState;			// 返回状态值

}


/******************************************************************
函数名:Flash_Page_Program
功能: flash的页写入操作
参数:  @flashAddr  写入页的起始地址24bit
        @lenth      写入的数据长度
        @dataArry[] 需要写入的数据数组
备注:长度lenth要小于等于256个字节
*****************************************************************/
void Flash_Page_Program(unsigned int flashAddr, unsigned char dataArry[], int lenth)
{
	ViSession io = Ivi_IOSession(vi);
    int num;
    unsigned char wState=0;
    unsigned char ReadDataArry[lenth];
    Flash_write_command(Fcmd_06H);   				// 先让flash处于可写状态
    delayus(50);
    WriteReg(io, regW_spi_flash_cs, sLow);			// cs拉低
    delayus(5);
    Spi_sendByte(Fcmd_02H);                 			// 上位机下发命令-page program-
    Spi_sendByte(((flashAddr & 0x00FF0000)>>16)); 	// 下发起始地址 16bit~24bit
    Spi_sendByte(((flashAddr & 0x0000FF00)>>8)); 	// 下发起始地址 8bit~16bit
    Spi_sendByte(((flashAddr & 0x000000FF)>>0)); 	// 下发起始地址 0bit~8bit
    for(num = 0; num < lenth; num++) 				// 连续写入lenth个数据
    {
        Spi_sendByte(dataArry[num]);
    }
    delayus(5);
    WriteReg(io, regW_spi_flash_cs, sHigh);	    	// cs拉高
	delayus(50);
    Flash_wait_ready();
    	 
}


/******************************************************************
函数名:Flash_Read_Data_Byte
备注: flash进行数据读取
参数:  @flashAddr  要读取的起始地址24bit
        @lenth      要读取的数据长度
        @dataArry[] 读取到数据要存放的数组
备注:lenth长度不限
*****************************************************************/
void Flash_Read_Data_Byte(unsigned int flashAddr, unsigned char dataArry[], int lenth)
{
	ViSession io = Ivi_IOSession(vi);
    int num;
	unsigned char readData;
    WriteReg(io, regW_spi_flash_cs, sLow);			// cs拉低
    delayus(5);
    Spi_sendByte(Fcmd_03H);                 			// 上位机下发命令-Read Data Bytes-
    Spi_sendByte(((flashAddr & 0x00FF0000)>>16)); 	// 下发起始地址 16bit~24bit
    Spi_sendByte(((flashAddr & 0x0000FF00)>>8)); 	// 下发起始地址 8bit~16bit
    Spi_sendByte(((flashAddr & 0x000000FF)>>0)); 	// 下发起始地址 0bit~8bit
    for(int NUM = 0; NUM < lenth; NUM++)	    	// 读取十个数据,并存放到数组flash_arry[]
    {
		readData = Spi_recvByte();					
        dataArry[NUM] = (unsigned char)readData;
    }   
    delayus(5);
    WriteReg(io, regW_spi_flash_cs, sHigh);			// cs拉高		

}


/******************************************************************
函数名:Test_PageWrite()
功能:  往flash里页写入个数,然后在读回来比较,测试
参数:  @PageNum 测试页的个数
备注:  
*****************************************************************/
void Test_PageWrite(long int PageNum)
{
	unsigned char testArry[257] = {0};
	unsigned char flash_arry[257] = {0};
	unsigned int WAddr = 0;
	unsigned int RAddr = 0;
	unsigned int cnt_reg = 0;
	int ff_flag=0;
	//全部擦除
	Flash_Chip_Erase();
	//页写入的值
	for(int i=0; i<256; i++)
	{
		testArry[i]=0xaa;
	}
	printf("page program Start...\n");
	for(int iii=0; iii<PageNum; iii++)
	{
		Flash_Page_Program(WAddr, testArry, 256);
		if(((iii*100)/PageNum)>cnt_reg)
		{
		   cnt_reg = (iii*100)/PageNum;
		   printf("%d%%\n", cnt_reg);
		}
		WAddr += 0x100;
		Delay(0.001);
	}
	printf("100%%\n");
	printf("page program finish!\n");

	cnt_reg = 0;

	printf("check page program...\n");
	for(int ii=0; ii<PageNum; ii++)
	{	
		Flash_Read_Data_Byte(RAddr, flash_arry, 256);
		for(int NUM = 0; NUM < 256; NUM++)	    							
		{
			if(flash_arry[NUM] != 0xff)
			{
				ff_flag++;
			}
		}
		//检查0xff
		if(ff_flag == 0)  // 此时读到的全为0xff,记录地址
		{
			printf("err RAddr:%x\n", RAddr);
		} 
		//打印进度
		if(((ii*100)/PageNum)>cnt_reg)
		{
		   cnt_reg = (ii*100)/PageNum;
		   printf("%d%%\n", cnt_reg);
		}	
		RAddr += 0x100;
		ff_flag = 0;    //复位错误0xff标志位
	} 
	printf("100%%\n");
	printf("Check PP finish!\n");	


}


/******************************************************************
函数名:TestPressure_PageWrite()
功能:  往flash里页写入个数,然后在读回来比较,测试
参数:  @PageNum 测试页的个数
备注:  
*****************************************************************/
void TestPressure_PageWrite(long int PageNum)
{
	unsigned char testArry[257] = {0};
	unsigned char flash_arry[257] = {0};
	unsigned int WAddr = 0;
	unsigned int RAddr = 0;
	unsigned int cnt_reg = 0;
	int ff_flag=0;
	//全部擦除
	Flash_Chip_Erase();
	//页写入的值
	for(int i=0; i<256; i++)
	{
		testArry[i] = i;
	}
	// 写入flash
	printf("page program Start...\n");
	for(int iii=0; iii<PageNum; iii++)
	{
		Flash_Page_Program(WAddr, testArry, 256);
		if(((iii*100)/PageNum)>cnt_reg)
		{
		   cnt_reg = (iii*100)/PageNum;
		   printf("%d%%\n", cnt_reg);
		}
		WAddr += 0x100;
		Delay(0.001);
	}
	printf("100%%\n");
	printf("page program finish!\n");
	// 检查flash的值
	cnt_reg = 0;
	printf("check page program...\n");
	for(int ii=0; ii<PageNum; ii++)
	{	
		Flash_Read_Data_Byte(RAddr, flash_arry, 256);
		for(int NUM = 0; NUM < 256; NUM++)	    							
		{
			if(flash_arry[NUM] != testArry[NUM])
			{
				ff_flag++;
			}
		}
		// 判断并打印出错的页地址
		if(ff_flag != 0)  
		{
			printf("err RAddr:%x\n", RAddr);
		} 
		// 打印进度
		if(((ii*100)/PageNum)>cnt_reg)
		{
		   cnt_reg = (ii*100)/PageNum;
		   printf("%d%%\n", cnt_reg);
		}	
		RAddr += 0x100;
		ff_flag = 0;  
	} 
	printf("100%%\n");
	printf("Check PP finish!\n");	


}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
2.5.2 mcs文件处理软件设计

这软件部分主要对.mcs文件进行预处理,将mcs文件转换成 在flash里存储FPGA的配置文件,即.bin文件。如上1.3部分所述mcs、bit和bin三者之间的关系,因为mcs文件包含了地址、数据和校验等信息,且是ASCII形式的文件,预处理操作即是.mcs文件将不必要的配置信息(地址、数据和校验等)去掉,且将ASCII格式转换成16进制格式文件的操作。实际上也可以直接将.bin文件直接写入到flash中。这部分是师兄写的,就不附上全码了,下面附上部分参考代码,仅供参考代码思路,结合.mcs文件结构来看比较容易懂。最重要的部分,我们实际只用把类型为type 00的数据段读取出来即可,再次推荐这篇博客(https://blog.csdn.net/hanhailong0525/article/details/122382501):

  • 软件大致的流程框图如下
    在这里插入图片描述
图2.4 在线升级软件流程图
  • 代码如下:
/******************************************************************************
 * @fn			static uint8_t xilUpdate_TransASCII2HEX(_In_ char ascii)
 * @brief		将ascii转为Hex
 * @param[in]	ascii				: 字符码
 * @param[out]	无
 * @return		Hex					: 转换后的hex
 * @note		
 ******************************************************************************/
static uint8_t xilUpdate_TransASCII2HEX(_In_ char ascii)
{
	if ((ascii >= 'A') && (ascii <= 'F'))
		return ascii - 'A' + 10;
	else if ((ascii >= 'a') && (ascii <= 'z'))
		return ascii - 'a' + 10;
	else
		return ascii - '0';
}
/******************************************************************************
 * @fn			uint32_t xilUpdate_FileTotalSize(_In_ const char * path)
 * @brief		获取文件大小 (以字节为单位)
 * @param[in]	path				: 文件路径
 * @param[out]	无
 * @return		size				: 文件大小
 * @note		
 ******************************************************************************/
uint32_t xilUpdate_FileTotalSize(_In_ const char * path)
{
	uint32_t	size = 0;
	FILE *		fp = NULL;
	
	if ((fp = fopen(path, "r")) == 0) 
		return 0;
	fseek(fp, 0, SEEK_END);
	size = ftell(fp);
	fclose(fp);

	return size;
}
/******************************************************************************
 * @fn			sint32_t xilUpdate_TranslateMCS2BIN(_In_ const char * mcsPath, 
 *													_In_ const char * binPath)
 * @brief		将MCS文件转换为BIN文件
 * @param[in]	mcsPath				: MCS文件路径
 * @param[in]	binPath				: BIN文件路径
 * @param[out]	无
 * @return		retVal < 0			: 错误码
 *				retVal > 0			: BIN文件大小
 * @note		
 ******************************************************************************/
sint32_t xilUpdate_TranslateMCS2BIN(_In_ const char * mcsPath, _In_ const char * binPath)
{
	uint8_t		checkSum = 0;
	sint32_t	MCSEof = FALSE;
	sint32_t	error = UPDATE_ERROR_NONE;
	char *		mcsDat = NULL;
	FILE *		mcs_fp = NULL;
	FILE *		bin_fp = NULL;
	M2B_Data	M2Bdat = { 0 };
	MCS_DataFormat * datFmt = NULL;

	mcs_fp = fopen(mcsPath, "r");
	bin_fp = fopen(binPath, "wb");
	if ((mcs_fp == NULL) || (bin_fp == NULL))
		return 0;
	mcsDat = (char *)malloc(sizeof(char) * 64);
	memset(mcsDat, 0, 64);
	datFmt = (MCS_DataFormat *)mcsDat;
	while (MCSEof == FALSE)
	{
		fgets(mcsDat, 64, mcs_fp);											///< 按行读取
		if (datFmt->header != ':')
		{
			error = UPDATE_ERROR_FORMAT_ERR;
			goto Exit;
		}
		M2Bdat.byteCnt  = (uint8_t)((ASCII2HEX(datFmt->byteCnt[0]) << 4) + ASCII2HEX(datFmt->byteCnt[1]));///< 数据长度
		M2Bdat.recType  = (uint8_t)((ASCII2HEX(datFmt->recType[0]) << 4) + ASCII2HEX(datFmt->recType[1]));///< 数据类型
		M2Bdat.hexAddr  = (uint16_t)((ASCII2HEX(datFmt->hexAddr[0]) << 12) + (ASCII2HEX(datFmt->hexAddr[1]) << 8)
					    + (ASCII2HEX(datFmt->hexAddr[2]) << 4) + (ASCII2HEX(datFmt->hexAddr[3])));///< 地址
		checkSum = M2Bdat.byteCnt + M2Bdat.recType + ((M2Bdat.hexAddr >> 8) & 0xff) + (M2Bdat.hexAddr & 0xff);
		switch (M2Bdat.recType)
		{
			case MCS_TYPE_DATA_RECORD : {									///< Type = 0: Data Record(数据记录)
				for (int cnt = 0; cnt < (M2Bdat.byteCnt << 1); cnt += 2)	///< 读取数据
					M2Bdat.dataRecord[cnt >> 1] = (uint8_t)((ASCII2HEX(datFmt->data.dataRecord[cnt]) << 4) + ASCII2HEX(datFmt->data.dataRecord[cnt + 1]));
				for (int cnt = 0; cnt < M2Bdat.byteCnt; cnt ++)				///< 计算校验和
					checkSum += M2Bdat.dataRecord[cnt];
				M2Bdat.checkSum = (uint8_t)(((ASCII2HEX(*(mcsDat + 9 + (M2Bdat.byteCnt << 1)))) << 4) + ASCII2HEX(*(mcsDat + 9 + (M2Bdat.byteCnt << 1) + 1)));///< 校验和
				M2Bdat.dataCounter += M2Bdat.byteCnt;
				fwrite(&M2Bdat.dataRecord[0], 1, M2Bdat.byteCnt, bin_fp);	///< 写入数据
			}break;
			case MCS_TYPE_END_OF_FILE : {									///< Type = 1: End of File Record(文件结尾记录)
				MCSEof = TRUE;
			}break;
			case MCS_TYPE_EXT_SEG_ADDR : {									///< Type = 2: Extended Segment Address Record(段地址记录)
				M2Bdat.segAddress = (ASCII2HEX(datFmt->data.segAddress[0]) << 12)///< 读取段地址
								  + (ASCII2HEX(datFmt->data.segAddress[1]) << 8)
								  + (ASCII2HEX(datFmt->data.segAddress[2]) << 4)
								  + (ASCII2HEX(datFmt->data.segAddress[3]));
				M2Bdat.checkSum = (uint8_t)((ASCII2HEX(*(mcsDat + 13)) << 4) + ASCII2HEX(*(mcsDat + 14)));///< 读取校验和
				checkSum += (M2Bdat.segAddress & 0xff) + ((M2Bdat.segAddress >> 8) & 0xff);///< 计算校验和
				fseek(bin_fp, M2Bdat.segAddress << 16, SEEK_SET);			///< 设定写入位置
			}break;
			case MCS_TYPE_EXT_LINEAR_ADDR : {								///< Type = 4: Extended Linear Address Record(线性地址记录)		
				M2Bdat.offsetAddr = (ASCII2HEX(datFmt->data.offsetAddr[0]) << 12)///< 读取线性地址
								  + (ASCII2HEX(datFmt->data.offsetAddr[1]) << 8)
								  + (ASCII2HEX(datFmt->data.offsetAddr[2]) << 4)
								  + (ASCII2HEX(datFmt->data.offsetAddr[3]));
				M2Bdat.checkSum = (uint8_t)((ASCII2HEX(*(mcsDat + 13)) << 4) + ASCII2HEX(*(mcsDat + 14)));///< 读取校验和
				checkSum += (M2Bdat.offsetAddr & 0xff) + ((M2Bdat.offsetAddr >> 8) & 0xff);///< 计算校验和
				fseek(bin_fp, M2Bdat.offsetAddr << 16, SEEK_SET);			///< 设定写入位置
			}break;
			default : break;
		}
		checkSum = (uint8_t)(0x100 - checkSum);
		if ((MCSEof == FALSE) && (checkSum != M2Bdat.checkSum)) 			///< 检测校验和
		{
			error = UPDATE_ERROR_CHECK_FAIL;
			goto Exit;
		}
	}
	error = (sint32_t)M2Bdat.dataCounter;
Exit:
	free(mcsDat);
	fclose(mcs_fp);
	fclose(bin_fp);
	return error;
}


/******************************************************************************
 * @fn			sint32_t xilUpdate_ProgramBIN2FLASH(_In_ const char * binPath)
 * @brief		在线升级,往FLASH写入bin文件
 * @param[in]	binPath				: bin文件路径
 * @param[out]	无
 * @return		retVal < 0			: 错误码
 *				retVal > 0			: BIN文件大小
 * @note		
 ******************************************************************************/
sint32_t xilUpdate_ProgramBIN2FLASH(_In_ const char * binPath)
{
	uint32_t	fsize;
	uint32_t	wrNum = XILUPDATE_SET_FLASH_OPS_SIZE; // 页的大小,256个字节
	uint32_t	blknum;
	uint32_t	counter = 0;
	sint32_t	error = UPDATE_ERROR_NONE;
	uint32_t	cnt_reg = 0;
	unsigned char flash_arry[260];
	uint8_t *	datBuf = NULL;
	FILE *		fp = NULL;
	printf("Update Online Begin:\n");	
	fsize = xilUpdate_FileTotalSize(binPath);								///< 获取文件大小
	if ((fp = fopen(binPath, "rb")) == NULL)
		return 0;
	datBuf = (uint8_t *)malloc(sizeof(uint8_t) * XILUPDATE_SET_FLASH_OPS_SIZE);
	blknum = fsize / XILUPDATE_SET_FLASH_OPS_SIZE;							///<blknum表示页的字节大小,为256个字节
	blknum += (fsize % XILUPDATE_SET_FLASH_OPS_SIZE > 0) ? 1 : 0;			///< 计算写入的数据块数量,即要写入flash中要有多少页
	/* 擦除flash */
	if ( flash_ops.flash_EraseChip != NULL)
	printf("Flash Chip Erase ing...\n");
	flash_ops.flash_EraseChip();
	printf("Chip Erase Done!\n");
	/* 开始往flash写入数据 */
	printf("Program Bin To Flash ing...\n");
	printf("0%%");
	for (uint32_t cnt = 0; cnt < blknum; cnt ++)
	{
		fread(datBuf, 1, wrNum, fp);	///< 读取BIN文件
		if (flash_ops.flash_WriteBuffer != NULL)	///< 每次按页的大小写入flash
			flash_ops.flash_WriteBuffer(XILUPDATE_SET_FLASH_PRAMADDR + cnt * XILUPDATE_SET_FLASH_OPS_SIZE, datBuf, wrNum);//该函数第一个参数为flash地址;第二个参数为要写入flash的数据缓存;第三个参数为要写入flash的数据量
		counter += wrNum;
		wrNum = ((fsize - counter) > XILUPDATE_SET_FLASH_OPS_SIZE) ? XILUPDATE_SET_FLASH_OPS_SIZE : fsize - counter;
		if(((cnt*100)/blknum)>cnt_reg)
		{
		   cnt_reg = (cnt*100)/blknum;
		   printf("\r%d%%", cnt_reg);
		
		}

	}
	printf("\r100%%\n");
	printf("Program Bin To Flash Done!\n");
	error = (sint32_t)counter;
Exit:
	free(datBuf);
	fclose(fp);
	return error;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189

三、实验结果及总结

实验结果:基于flash的在线升级,flash的读写时钟频率为1MHz,升级的.mcs文件大小为30MB,实际写入flash的文件.bin大小为11MB,整个升级过程耗时约5min,升级完成后掉电重启,fpga能够自配置,并能够实现所升级的功能,表明升级成功。
总结:要理解FPGA的升级就是FPGA配置文件的更换,基于flash的FPGA升级就是要把新的配置文件替代掉原来存放flash的旧配置文件,使得每次FPGA上电重启都是更具新的配置文件进行配置。本次实验关键的地方:

  1. 理解.mcs和.bin文件之间的关系及区别
  2. 理解flash的读写操作及时序
  3. 保证上位机下传的字节数据以并转串的形式能够正确无误写入flash中
  4. 注意上位机到FPGA数据传输会产生的跨时钟域问题(这里笔者被搞了一周的时间,超级难过,最后问师兄,师兄说信号打两拍就解决了…)

文章写的有点草率,若能够帮到路过的你,是我的荣幸。你们的点赞,是我写文章的动力(比心),欢迎大家学习交流~

最后建议希望对大家有用:做技术自己单干对自己提升肯定很大,但一定不要闷头自己死磕,遇到瓶颈一定要多问,千万别憋大招,技术有交流才能更快的成长,不是每个坑都踩一遍才算成长,能知道坑在哪里不去踩也算成长!

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号