国产FPGA测评【七】FPGA解析串口数据写入FLASH
[复制链接]
本帖最后由 yyliu 于 2023-2-12 20:17 编辑
声明:1.本帖如有对引用其他网站资源,均附上了网址,针对本帖中可能出现的侵权行为,请及时联系本人修改或删除。
2. 未经本人允许,请勿转载。若本帖存在错误或不足之处,烦请指正,本人会及时修改。
3.本帖代码根据正点原子代码修改,并给出了源码,若对正点原子构成侵权,请及时联系本人删除。
0.说明
本帖根据正点原子的FLASH读写实验和串口模块,融合为一个工程文件,来实现特定业务:FPGA解析串口数据写入FLASH。
0.1 为什么学习FLASH
FLASH按照原理划分包括NOR FLASH和NAND FLASH,一般FLASH的存储容量都比较大,可以存储系统镜像、掉电后仍需保存的重要数据。FLASH的操作接口目前大多是SPI串行接口,以前并口FLASH使用较多,具有多根地址线和数据线,传输速度较快,但是硬件设计和走线相对串行总线更复杂。而对EEPROM来说,其存储容量更小,操作接口一般为I2C接口。
存储器有磁性存储、光学存储和半导体存储器,我们常用的半导体存储器的分类如下:
0.2 为什么学习SPI协议
前面提到操作FLASH的串行接口为SPI,因此学好SPI协议是对FLASH读写的关键。后面将用一小节讲解SPI的操作时序。
0.3 为什么通过串口写数据到FLASH
正点原子提供了串口回环实验源码,即通过上位机发送数据到FPGA,随后立即转发回来。
本帖将实现串口助手给FPGA发数据,FPGA将接收到的命令写入FLASH,将设定好的数据写入FLASH,并从FLASH中读出后发送给串口助手,以加深我们对串口和SPI通信协议的理解,同时锻炼设计工程框架和移植代码的能力。
1.SPI操作时序
本帖的硬件设计比较简单,就是FPGA与EEPROM的I2C电路设计,网上相关内容较多,可自行学习。本帖直接介绍软件的设计。
以华邦的W25Q128FW芯片为例,介绍其读写时序。
上图是FLASH的数据输入和输出管脚对时间的要求,如高电平持续时间、低电平持续时间等,CS与SCLK的时间关系等。
主机对FLASH需要进行读和写操作,主机写入时用的是FLASH的DI信号线,主机读出时使用的是FLASH的DO信号线。
主机向FLASH写指令的时序如上图。CS为高时SCL的空闲态可以为高电平也可以为低电平。假设CLK的空闲态为低电平,在上升沿时采样DI引脚的数据,FLASH在下降沿将数据存入自己的SSPBUF。等待8bit数据传输完成后就完成了一个字节数据的读取。
对FLASH的其他读写操作类似,可以参考W25Q128FW的datasheet。
2.软件总体设计
实验目标:
串口助手发送0xaa数据给FPGA,作为FPGA程序的启动信号。FPGA接受到串口发送过来的启动程序信号0xaa后,将对板载的FLASH芯片先进行全擦除,然后写入数据0-255,再把数据0-255读出,读出数据正确后再把写入数据的扇区进行扇区擦除,最后再读取一遍数据看看数据是否擦除成功。读写正确则点亮led3,读写错误则led3灯闪烁。在把SPI数据读出的同时,将读出的数据通过串口发送给串口助手。
本帖的总体设计框图如下:
锁相环模块(pll_spi):锁相环模块的作用主要是生成100Mhz的时钟给读写模块(flash_rw)和SPI驱动模块(spi_drive)使用。我们的板载FLASH芯片最高可以进行104Mhz的时钟操作,本节实验我们使用锁相环生成了100Mhz时钟再进行二分频产生50Mhz的FLASH工作时钟。之所以这样二分频操作主要是为了方便控制SPI协议的时钟极性。
读写模块(flash_rw):读写模块的作用主要是产生要写入FLASH的数据和给SPI驱动模块(spi_drive)发送操作指令。
LED 灯模块(led_alarm):LED 灯模块就比较简单了,就是检测 FLASH 读写数据是否正确,如果正确那么SPI驱动模块(spi_drive)给出的错误标志信号为0,此时驱动LED3常亮,如果检测到读写错误标志拉高,则驱动 LED3闪烁。
SPI 驱动模块(spi_drive):SPI 驱动模块是本节实验最重要的一个模块。它实现了基于 SPI 协议 FLASH通信时序,完成了对FLASH芯片擦除、写入数据、读取数据以及轮询状态寄存器等操作。并且对读出的数据进行检查给出读写正确与否的标志。
uart_recv和uart_send模块前面的帖子已经介绍过。
uart_spi_loop模块:该模块还接收uart_recv的数据,将数据写入FLASH读出后,并通过uart_send模块发送给串口助手。
3.关键代码分析
本帖需要对正点原子FLASH读写实验的读写模块、SPI 驱动模块、顶层模块进行修改,并将串口发送模块和串口接收模块集成到工程当中。
由于前面几期介绍了串口通信与I2C、双端口RAM进行数据交互的案例,本帖不再给出详细的设计源码,大家可以根据系统总体框图去修改代码。本节将重点介绍下SPI通信源码的关键部分。
1.SPI 驱动模块(spi_drive)关键代码
assign idel_flag=(current_state==IDLE)?1:0;//空闲状态标志
assign idel_flag_r=idel_flag0&&(~idel_flag1);//空闲状态标志的上升沿
always @(posedge clk_100m or negedge sys_rst_n )begin
if(!sys_rst_n)begin
idel_flag0<=1'b1;
idel_flag1<=1'b1;
end
else begin
idel_flag0<=idel_flag;
idel_flag1<=idel_flag0;
end
end
always @(posedge clk_100m or negedge sys_rst_n )begin
if(!sys_rst_n)
w_data_req<=1'b0;
else if((bit_cnt+2)%8==0&&bit_cnt>=30&&clk_cnt==0&¤t_state==WRITE)
w_data_req<=1'b1;
else
w_data_req<=1'b0;
end
w_data_req是写FPGA向FLASH中写入数据的请求信号,最难理解的else if((bit_cnt+2)%8==0&&bit_cnt>=30&&clk_cnt==0&¤t_state==WRITE),这条代码表示当状态机处于写数据状态、bitcnt大于30(因为写数据时需要8bit指令加上24bit数据,所以前32bit都不用提供数据,这里之所以是bit cnt>30而不是bit cnt>32是因为需要提前两个时钟周期发送数据请求,(bit_cnt+2)%8==0(bit_cnt+2也是为了提前两个时钟周期,对8进行取模运算是因为一个字节数据等于8bit因此bit cnt每增加8才向数据读写模块发送一次写数据请求)和clk cnt==0时向数据读写模块发送写数据请求,这样读写数据模块就会给SPI驱动模块发送一字节数据。
always @(posedge clk_100m or negedge sys_rst_n )begin//读出的数据移位寄存
if(!sys_rst_n)
rd_data_buffer<=8'd0;
else if(bit_cnt>=32&&bit_cnt<=2080&&clk_cnt==0&¤t_state==READ)
rd_data_buffer<={rd_data_buffer[6:0],spi_miso};
else
rd_data_buffer<=rd_data_buffer;
end
assign uart_send_flag = (bit_cnt%8==0&&bit_cnt>=40&&clk_cnt==1&¤t_state==READ);
always @(posedge clk_100m or negedge sys_rst_n )begin//检查读出的数据是否正确
if(!sys_rst_n)
data_check<=8'd0;
else if(bit_cnt%8==0&&bit_cnt>=40&&clk_cnt==1&¤t_state==READ)
data_check<=data_check+1'd1;
else
data_check<=data_check;
end
always @(posedge clk_100m or negedge sys_rst_n )begin//读出的数据
if(!sys_rst_n)
begin
r_data<=8'd0;
//uart_send_flag<=0;
end
else if(bit_cnt%8==0&&bit_cnt>38&&clk_cnt==1&¤t_state==READ)
begin
r_data<=rd_data_buffer;
//uart_send_flag <= 1;
end
else
begin
r_data<=r_data;
//uart_send_flag<=0;
end
end
第一个always语句表示:当状态机处于读状态时将读出的数据进行移位寄存,前32bit是指令和地址,从第33bit开始是读出的数据,每次在clk_cnt=0时(SPI时钟处于数据传输状态)移位寄存一次。
第二个always语句表示:将移位寄存的数据取出来传递给rdata,这个rdata就是最终读出的数据。判断条件就是每隔8bit提取一次数据,并且在clk_cnt==1时提取,因为移位寄存是在clkcnt==0时进行的。
第三个always语句表示:定义了一个计数器,r_data每提取一字节数据data_check就加一,data_check是在bit_cnt>46以后才开始加一的,因为r_data第一次提取数据的时候是当bit cnt==40时提取的,而第一个数据刚好就是0,因此在这个节点data check不需要累加,因为data check本身就是0,如果累加就会造成data check与r data刚好错位了。
我们接下来看状态机的读写操作:
READ: begin
stdone<=1'b0;
if(dely_state_cnt==10)
spi_cs<=1'b0;
else if(dely_cnt==1&&bit_cnt<8) begin
spi_clk0<=~spi_clk0;
spi_clk<=spi_clk0;
spi_mosi<=cmd_buffer[7];
end
else if(bit_cnt>=8&&bit_cnt<32&&spi_cs==0)begin
spi_clk0<=~spi_clk0;
spi_clk<=spi_clk0;
spi_mosi<=addr_buffer[23];
end
else if(bit_cnt>=32&&bit_cnt<2080)begin
spi_clk0<=~spi_clk0;
spi_clk<=spi_clk0;
spi_mosi<=1'b0;
end
else if(bit_cnt==2080&&clk_cnt==0) begin
spi_clk<=1'b0;
spi_mosi<=1'b0;
stdone<=1'b1;
end
else if(bit_cnt==2080&&clk_cnt==1) begin
spi_cs<=1'b1;
end
end
WRITE: begin
stdone<=1'b0;
if(dely_state_cnt==10)
spi_cs<=1'b0;
else if(dely_cnt==1&&bit_cnt<8) begin
spi_clk0<=~spi_clk0;
spi_clk<=spi_clk0;
spi_mosi<=cmd_buffer[7];
end
else if(bit_cnt>=8&&bit_cnt<32&&spi_cs==0)begin
spi_clk0<=~spi_clk0;
spi_clk<=spi_clk0;
spi_mosi<=addr_buffer[23];
end
else if(bit_cnt>=32&&bit_cnt<2080)begin
spi_clk0<=~spi_clk0;
spi_clk<=spi_clk0;
spi_mosi<=data_buffer[7];
end
else if(bit_cnt==2080&&clk_cnt==0) begin
spi_clk<=1'b0;
spi_mosi<=1'b0;
stdone<=1'b1;
end
else if(bit_cnt==2080&&clk_cnt==1) begin
spi_cs<=1'b1;
end
end
WRITE表示状态机处于写状态时的操作,在这个阶段,FPGA会将0-255数据写入FLASH中,注意在WRITE状态时一次性将256个数据写完才会跳转到下一个状态。
READ表示状态机处于读状态时的操作,在这个阶段,FPGA会将0-255数据从FLASH中读出,注意在READ状态时一次性将256个数据读完才会跳转到下一个状态,并不是写一个数据到FLASH后读一个数据出来。
2.读写模块(flash_rw)关键代码
always @(posedge sys_clk or negedge sys_rst_n )begin
if(!sys_rst_n)
flash_start<=0;
else if(flash_start<=5 && pc_recv_data==8'haa)
flash_start<=flash_start+1;
else
flash_start<=flash_start;
end
always @(posedge sys_clk or negedge sys_rst_n )begin
if(!sys_rst_n)
cmd_cnt<=0;
else if(flash_start==4 && pc_recv_data==8'haa)
spi_start<=1'b1;
else if(idel_flag_r&&cmd_cnt<10 && pc_recv_data==8'haa)begin
cmd_cnt<=cmd_cnt+1;
spi_start<=1'b1;
end
else begin
cmd_cnt<=cmd_cnt;
spi_start<=1'b0;
end
end
flash_start这个计数器的作用是产生一个spi_start开始信号用来启动SPI驱动模块,当flash_start 等于4时拉高spi_start,只拉高一个时钟周期。
cmd_cnt计数器,每当检测到SPI驱动模块的状态空闲标志位的上升沿时cmd cnt就会加一,根据cmd_cnt的值来发送不同的指令给SPI驱动模块。每当检测到一次写数据请求spi_data就会加一,写数据请求一共会拉高255次,因此写入FLASH的数据就是0~255。
|