Xilinx Picoblaze 使用介绍

Picoblaze设计指南

Picoblaze是Xilinx的8位微处理器,其占用资源非常少,可以在CPLD、FPGA里面,实现一个或多个这样的处理单元。本文以Vivado软件来介绍Picoblaze,如果你选择的器件是Spartan-6或更早器件,那请采用ISE软件。

进入Xilinx官网,在搜索框输入picoblaze,搜索后找到Picoblaze lounge进入。或者直接点PicoBlaze 8-bit Microcontroller

Xilinx Picoblaze 使用介绍_第1张图片

在Design Files里面,我们根据自己的器件选择相应的下载,本文采用的是ARTY S7板子,所以下载的是PicoBlaze for UltraScale, 7-series, 6-series FPGAs >> KCPSM6 Rev 9 – September 30, 2014 (ZIP)(注意:下载需要登录Xilinx,如果没有帐号,请注册一个)。

Xilinx Picoblaze 使用介绍_第2张图片

里面2个重要的文档,一个是设计流程介绍(step by step),一个是用户指南,包括完整指令集介绍。本文以SPI Flash编程为例介绍Picoblaze的设计流程。

Vivado里,新建工程,选择xc7s50csga324-1il器件。根据自己爱好语言,添加相应源文件,这里选择verilog语言,添加verilog目录下的kcpsm6.v,添加Uart_and_PicoTerm目录下uart_rx6.vuart_tx6.v两个文件,记得勾选Copy sources into project,如下图:

Xilinx Picoblaze 使用介绍_第3张图片

建立自己的顶层文件(不需要加spiclk,后面会解释),如下图:

Xilinx Picoblaze 使用介绍_第4张图片

打开verilog目录下的kcpsm6_design_template.v文件,复制信号定义跟picoblaze处理器元件安装(把模板里面下面代码复制到pico_uart_spi.v)

Xilinx Picoblaze 使用介绍_第5张图片

将模板里面程序rom元件安装复制到pico_uart_spi.v里面。根据自己喜好修改元件名跟安装名,并修改rom相关参数,vivado里面不支持6系列,只能选择7SUS。默认JTAG LOADER开关是打开的,这个如果你调试完软件代码后不在需要,可以关闭,不过这个功能占用逻辑资源非常少,建议保留。程序空间我们选择默认2K。其实UARTSPI需要的代码非常少。

Xilinx Picoblaze 使用介绍_第6张图片

模板里面复位信号是有cpu_resetjtag loader模块传递上来的rdl信号取或来复位处理器的,我们沿用,这样每次通过jtag loader代码后自动从复位开始运行。模块里面声明kcpsm6_reset(高复位)reg信号,我们设计里面是wire,请在代码里面修改成wire  kcpsm6_reset; 我们将cpu_reset直接连接到外部rst管脚。

Xilinx Picoblaze 使用介绍_第7张图片

打开UART_and_Picoterm\ALTYS_design目录下uart6_altys.v,复制里面的串口收发模块的元件安装代码,如果不需要串口,请直接跳到spi元件实现。

Xilinx Picoblaze 使用介绍_第8张图片Xilinx Picoblaze 使用介绍_第9张图片

继续复制uart6_altys.vuart的信号实现,uart需要一个波特率16倍的参考钟,我们用计数器来实现,外部时钟100MHz, 100,000,000 /(115200*16) 54。复制下面这段代码到pico_uart_spi.v,注意在tx,rx元件前面定义reg en_16_x_baud;

 reg [5:0]  baud_count;

  always @ (posedge clk )

  begin

    if (baud_count == 6'b110101) begin      // counts 54 states including zero

      baud_count <= 6'b000000;

      en_16_x_baud <= 1'b1;                 // single cycle enable pulse

    end

    else begin

      baud_count <= baud_count + 6'b000001;

      en_16_x_baud <= 1'b0;

    end

  end

实现picoblaze读串口状态信号与读串口接收数据,这段代码也可以从上面的模板里面继续拷贝,但信号的定义需要自己加上,我们为了利用其汇编代码,端口地址基本不做修改。读0地址取串口状态,写0地址设置串口复位信号(参考设计里面用的地址1),1地址接收数据,写1地址发送数据。

  always @ (posedge clk)

  begin

    case (port_id[1:0])

        // Read UART status at port address 00 hex

        2'b00 : in_port <= { 2'b00,

                            uart_rx_full,

                            uart_rx_half_full,

                            uart_rx_data_present,

                            uart_tx_full,

                            uart_tx_half_full,

                            uart_tx_data_present };

        // Read UART_RX6 data at port address 01 hex

        // (see 'buffer_read' pulse generation below)

        2'b01 : in_port <= uart_rx_data_out;

        // Read 8 general purpose switches at port address 02 he

        2'b10 : in_port <= switch; //will modify to spi miso input logic;

        // Don't Care for unused case(s) ensures minimum logic implementation  

        default : in_port <= 8'bXXXXXXXX ; 

    endcase;

    // Generate 'buffer_read' pulse following read from port address 01

    if ((read_strobe == 1'b1) && (port_id[1:0] == 2'b01)) begin

        read_from_uart_rx <= 1'b1;

      end

      else begin

        read_from_uart_rx <= 1'b0;

      end

      end

记得将wire uart_rx_full,uart_rx_half_full,uart_rx_data_present;   reg read_from_uart_rx; wire [7:0] uart_rx_data_out;

wire uart_tx_full,uart_tx_half_full,uart_tx_data_present;四行信号定义添加到串口RX,TX模块前面。

      实现串口TX的数据跟写容许信号,将下面代码复制到串口RX,TX模块前面,这里加入了k_write_strobe信号是为了简化串口发送字符串的汇编代码(outputk指令会产生此写信号,用来直接输出常量到端口),另外需将模板里port_id修改成port_id[1:0]==2’b01,因为后续的SPI接口用了地址2’b11,只用最低位无法区分端口操作。

  wire [7:0] uart_tx_data_in = out_port;

  wire write_to_uart_tx = (write_strobe | k_write_strobe) & (port_id[1:0]==2’b01);

           实现串口复位信号,记得将reg uart_tx_reset, uart_rx_reset; 2个信号定义放到串口RX,TX模块前面,注意由于我们修改了复位端口地址为0,所以要把把模板里面的修改成port_id[0] == 1'b0

  always @ (posedge clk)

  if (k_write_strobe == 1'b1)

      if (port_id[0] == 1'b0) begin

          uart_tx_reset <= out_port[0];

          uart_rx_reset <= out_port[1];

      end

      连接串口端口信号到内部逻辑:  assign uart_rx = rxd;  assign txd = uart_tx;

下面我们开始实现SPI接口,我们这里以最简单的单线SPI为例,如果要实现X2,X4数据位宽的,可以在理解的基础上自行实现。SPI的读写端口我们都设置为3。下面是写SPI的输出信号:

reg spi_clk,spi_cs_b,spi_mosi;

  always @ (posedge clk)

  if (k_write_strobe | write_strobe) begin

      if (port_id[1:0] == 2'b11) begin

          spi_clk <= out_port[0];

          spi_cs_b <= out_port[1];

spi_mosi <= out_port[7];

      end

  end

           SPI输入逻辑则需要添加到上面串口读地址译码逻辑那里,将 2'b10 : in_port <= switch; //will modify to spi miso input logic;   修改为如下代码(注意需要把wire spi_miso;信号定义放在前面):

2'b11 : in_port <= {spi_miso ,7’b0};

连接顶层SPI信号:

  assign spiss = spi_cs_b;

  assign spido = spi_mosi;

  assign spi_miso = spidi;

由于SPI CLK是需要共享FPGA的专用配置管脚CCLK,我们需要调用STARTUPE2原语来实现SPI时钟,所以在顶层端口里面不需要声明。打开语言模板,菜单Tools/Language Templates或图标里面的小灯泡:

Xilinx Picoblaze 使用介绍_第10张图片   Xilinx Picoblaze 使用介绍_第11张图片

如上图,将spi_clk信号接到USRCCLK0上,输出信号用模板里面的,输入信号,除与DONE相关的信号置为1,其他置为0

为了避免每次串口发送数据时候检测发送FIFO状态,我们将picoblazekcpsm6_sleep接到串口发送的半满信号上.加入下面语句assign kcpsm6_sleep= uart_tx_half_full;

保存后,Sources里面,还有ROM是问号,这个不需要我们实现,是汇编器汇编我们的代码的时候生成。但我们需要做简单处理,只需要生成一个空的模块名即可,不需要添加任何端口、信号与逻辑实现:

Xilinx Picoblaze 使用介绍_第12张图片Xilinx Picoblaze 使用介绍_第13张图片

逻辑设计工作基本完成(管脚时钟约束,见后面),但现在还是实现不了,因为rom文件还没生成。

复制Reference_Designs\SPI目录下四个汇编文件到vivado工程目录下面picoblaze_spi.srcs\sources_1\new的目录里:

Xilinx Picoblaze 使用介绍_第14张图片

继续复制verilog目录下ROM_form_JTAGLoader_Vivado_2June14.v到上述名录,改名为ROM_form.v

继续复制kcpsm6.exe到上述目录,复制完如下图:

Xilinx Picoblaze 使用介绍_第15张图片

在上述目录下新建立一个uart_spi_rom.psm空文件,并编辑,我们会把其他参考设计PSM文件包含进来,以充分利用现成的一些函数(参考设计的PSM文件会include其他几个psm文件):

Xilinx Picoblaze 使用介绍_第16张图片

修改PicoTerm_routines.psm里面CONSTANT reset_UART_port, 01CONSTANT reset_UART_port, 00(因为我们逻辑里面做了修改。简单检查一下逻辑实现跟汇编代码是否一致。

Xilinx Picoblaze 使用介绍_第17张图片

进入windows命令行终端或powershell,编译uart_spi_rom.psm,如果没有任何错误的话,会生成四个文件uart_spi_rom.log uart_spi_rom.hex uart_spi_rom.fmt uart_spi_rom.v;当然其他psm文件也会生成相应的fmt文件(fmt只是对psm文件做一个格式化整理)。(注意:Vivado工程里面的rom文件已更新,包含有软件代码的.v文件)

Xilinx Picoblaze 使用介绍_第18张图片

                      现在逻辑可以综合实现整个工程了,如果没有错误,Open Implemented Design,约束管脚位置,并保存约束文件。

Xilinx Picoblaze 使用介绍_第19张图片

约束时钟,并保存(DRCKJTAG的门控时钟,UPDATEBST的并行数据锁存时钟,频率非常低)。

Xilinx Picoblaze 使用介绍_第20张图片

SPI接口也是同步电路,但由于CCLK是专用管脚,在自动分析出来的时钟里面是见不着的,但是也是需要约束的

约束方法见Xilinx Customer Community

直接点Generate Bitstream,软件会重新综合实现并生成bit文件。打开串口调试软件,下载Bit文件。满屏的Welcome! 怎么回事,没换行….

Xilinx Picoblaze 使用介绍_第21张图片

修改代码,往串口输出0x0d换行符号。

Xilinx Picoblaze 使用介绍_第22张图片

JTAGLOADER动态加载软件

通过windows10开始菜单,进入ISE Design Suite 64 Bit Command Prompt(如果我就只是Vivado用户,没有ISE,怎么办?哈哈,那你就得每次修改汇编代码后,编译软件、重新综合逻辑、实现再生成BIT并下载了。当然也不是没有办法,毕竟Jtag loader的源码在我们工程里面有呀,Vivado也开放了Jtag的用户接口给我们,见”Picoblaze Jtag Loader in Vivado”这篇文章,本文不讨论)。将PicoblazeJtag_Loader目录下的相关文件拷贝到我们汇编代码所在目录,WIN10系统请拷贝WIN7的执行文件跟动态库。Linux用户也提供了相应的loader文件。

Xilinx Picoblaze 使用介绍_第23张图片

拷贝完成,切换到ISE命令行,编译并下载软件代码:

Xilinx Picoblaze 使用介绍_第24张图片

Xilinx Picoblaze 使用介绍_第25张图片

OK,串口已经能正常工作了。

硬件已经搭建好了,现在SPI接口就只剩余软件的事情了。编辑N25Q128_SPI_routines.psm,我们代码里面把spi输入输出端口都设置成3了,所以这里要把SPI_output_port修改成如下,如果你的逻辑对数据位置做了调整,那请自行修改这里相应位置:

Xilinx Picoblaze 使用介绍_第26张图片

N25Q128_SPI_routines.psm里面提供了FLASH常用操作,比如读设备ID,读字节,写字节,擦除扇区等。这些汇编代码都很简单,请自行阅读。看一下读ID代码,不用传递参数进去,读ID命令是9F,返回三个值在S9,S8,S7寄存器里面。

Xilinx Picoblaze 使用介绍_第27张图片

ID后,继续读256字节数据,并通过串口每行16个字节显示。完成后代码如下:

Xilinx Picoblaze 使用介绍_第28张图片

思考:为什么每次只读一个字节呢?因为我们的寄存器数目有限,2个寄存器BANK32个。但是picoblaze可以设置Scratch Pad Memory,最大可以到256字节。在我们例化kcpsm6逻辑工程的时候可以通过参数传递进去。Hwbuild参数是在软件执行hwbuild指令时候的返回值,可以当作是硬件版本号使用。访问Pad Memory使用store指令存进去,使用fetch指令提取出来。

  kcpsm6 #(

           .interrupt_vector     (12'h3FF),

           .scratch_pad_memory_size(256),

           .hwbuild                  (8'h00))

ARTY S7板上是S25FL128S,最高可以支持64KB写。修改write_spi_byte,可以看到这个函数调用的02 PP写,我们只需要传输一个字节那个位置传一页数据就可以完成页写。

           Xilinx Picoblaze 使用介绍_第29张图片

修改后,以前的函数尽量保留,无法保留的这里就改名,我们把一页数据保存在Pad Memory传递给页写函数。

Xilinx Picoblaze 使用介绍_第30张图片

完成后的主程序如下:

Xilinx Picoblaze 使用介绍_第31张图片

运行程序,可以在串口调试界面看到输出跟预期望一样,但注意,这里有个小问题,就是我们在循环页写页读,其实我们只成功写一次,为什么(因为我们写以前没有擦除页,以前这个高地址页是空白的,所以不用擦除,但写过后再次写就需要先擦除,你们自行修改即可,擦除函数有现成的,要注意这些命令是否跟你的器件一致。)

Xilinx Picoblaze 使用介绍_第32张图片

至于Picoblaze怎么跟主逻辑交互,我相信这个对大家来说都是很简单的事情。要升级配置SPI FLASH的内容可以通过BRAM,小FIFO传递给Picoblaze

资源占用情况,在vivado里,Open Implemented Design,在tcl console里输入report_utilization -hierarchical  -hierarchical_depth  1 。可以看到如下报告,256字节Pad Memory情况:

Xilinx Picoblaze 使用介绍_第33张图片

64字节Pad Memory情况:

Xilinx Picoblaze 使用介绍_第34张图片

关闭Jtag loader,rom模块几乎不会消耗lut

uart_spi_rom #(

           .C_FAMILY               ("7S"),             //Family 'S6' or 'V6'

           .C_RAM_SIZE_KWORDS         (2),           //Program size '1', '2' or '4'

           .C_JTAG_LOADER_ENABLE   (0))           //Include JTAG Loader when set to 1'b1

Xilinx Picoblaze 使用介绍_第35张图片

附录1:完整参考代码,加上注释,不到200行代码,完成了串口跟SPI通信。

`timescale 1ns / 1ps

//

// Company:  Avnet Ltd

// Engineer: Eric

//

// Create Date: 2019/11/08 23:33:34

// Design Name:  Picoblaze Guide Demo1

// Module Name: pico_uart_spi

// Project Name: pico_uart_spi

// Target Devices: xc7s50-1csga324l

// Tool Versions:  vivado2019.2

// Description:    

//

// Dependencies:

//

// Revision: 1.0

// Revision 0.01 - File Created

// Technical Contact:[email protected]

//

//

module pico_uart_spi(

    input clk,

    input rst,

    input rxd,

    output txd,

    output spiss,

    output spido,

    input spidi

    );

   

wire    [11:0]  address;

wire    [17:0]  instruction;

wire                         bram_enable;

wire    [7:0]               port_id;

wire    [7:0]               out_port;

reg     [7:0]               in_port;

wire                         write_strobe;

wire                         k_write_strobe;

wire                         read_strobe;

reg                          interrupt;            //See note above

wire                         interrupt_ack;

wire                         kcpsm6_sleep;      //See note above

wire                     kcpsm6_reset;      //See note above

wire                         cpu_reset;

wire                         rdl;

wire                         int_request;

  kcpsm6 #(

           .interrupt_vector     (12'h3FF),

           .scratch_pad_memory_size(64),

           .hwbuild                  (8'haa))

  processor (

           .address                (address),

           .instruction (instruction),

           .bram_enable        (bram_enable),

           .port_id                  (port_id),

           .write_strobe         (write_strobe),

           .k_write_strobe     (k_write_strobe),

           .out_port                (out_port),

           .read_strobe          (read_strobe),

           .in_port                  (in_port),

           .interrupt                (interrupt),

           .interrupt_ack        (interrupt_ack),

           .reset                     (kcpsm6_reset),

           .sleep                       (kcpsm6_sleep),

           .clk                         (clk));

          

 uart_spi_rom #(

           .C_FAMILY               ("7S"),             //Family 'S6' or 'V6'

           .C_RAM_SIZE_KWORDS         (2),           //Program size '1', '2' or '4'

           .C_JTAG_LOADER_ENABLE   (1))           //Include JTAG Loader when set to 1'b1

  rom (                               //Name to match your PSM file

          .rdl                          (rdl),

           .enable                   (bram_enable),

           .address                (address),

           .instruction            (instruction),

           .clk                         (clk));

          

  assign kcpsm6_reset = cpu_reset | rdl;     

  assign cpu_reset = rst;

 

  reg en_16_x_baud;

  reg uart_tx_reset, uart_rx_reset;

  wire uart_rx_full,uart_rx_half_full,uart_rx_data_present;

  wire uart_tx_full,uart_tx_half_full,uart_tx_data_present;

  reg  read_from_uart_rx;

  wire [7:0] uart_rx_data_out;

  wire [7:0] uart_tx_data_in = out_port;

  wire write_to_uart_tx = (write_strobe | k_write_strobe) & (port_id[1:0]==2'b01);

  assign uart_rx = rxd;

  assign txd = uart_tx;

  wire spi_miso;

  reg [5:0]  baud_count;

  always @ (posedge clk )

  begin

    if (baud_count == 6'b110101) begin      // counts 54 states including zero

      baud_count <= 6'b000000;

      en_16_x_baud <= 1'b1;                 // single cycle enable pulse

    end

    else begin

      baud_count <= baud_count + 6'b000001;

      en_16_x_baud <= 1'b0;

    end

  end

  assign kcpsm6_sleep= uart_tx_half_full;

  uart_tx6 tx(

      .data_in(uart_tx_data_in),

      .en_16_x_baud(en_16_x_baud),

      .serial_out(uart_tx),

      .buffer_write(write_to_uart_tx),

      .buffer_data_present(uart_tx_data_present),

      .buffer_half_full(uart_tx_half_full ),

      .buffer_full(uart_tx_full),

      .buffer_reset(uart_tx_reset),             

      .clk(clk));

  uart_rx6 rx(

      .serial_in(uart_rx),

      .en_16_x_baud(en_16_x_baud ),

      .data_out(uart_rx_data_out ),

      .buffer_read(read_from_uart_rx ),

      .buffer_data_present(uart_rx_data_present ),

      .buffer_half_full(uart_rx_half_full ),

      .buffer_full(uart_rx_full ),

      .buffer_reset(uart_rx_reset ),             

      .clk(clk ));

  always @ (posedge clk)

  begin

    case (port_id[1:0])

        // Read UART status at port address 00 hex

        2'b00 : in_port <= { 2'b00,

                            uart_rx_full,

                            uart_rx_half_full,

                            uart_rx_data_present,

                            uart_tx_full,

                            uart_tx_half_full,

                            uart_tx_data_present };

        // Read UART_RX6 data at port address 01 hex

        // (see 'buffer_read' pulse generation below)

        2'b01 : in_port <= uart_rx_data_out;

        // Read 8 general purpose switches at port address 02 he

        2'b11 : in_port <= {spi_miso, 7'b0}; //will modify to spi miso input logic;

        // Don't Care for unused case(s) ensures minimum logic implementation 

        default : in_port <= 8'bXXXXXXXX ; 

    endcase;

    // Generate 'buffer_read' pulse following read from port address 01

    if ((read_strobe == 1'b1) && (port_id[1:0] == 2'b01)) begin

        read_from_uart_rx <= 1'b1;

      end

      else begin

        read_from_uart_rx <= 1'b0;

      end

  end  

 

  always @ (posedge clk)

  if (k_write_strobe == 1'b1)

      if (port_id[0] == 1'b0) begin

          uart_tx_reset <= out_port[0];

          uart_rx_reset <= out_port[1];

      end

  reg spi_clk,spi_cs_b,spi_mosi;

  always @ (posedge clk)

  if (k_write_strobe | write_strobe) begin

      if (port_id[1:0] == 2'b11) begin

          spi_clk <= out_port[0];

          spi_cs_b <= out_port[1];

          spi_mosi <= out_port[7];

      end

  end

  assign spiss = spi_cs_b;

  assign spido = spi_mosi;

  assign spi_miso = spidi;

  STARTUPE2 #(

      .PROG_USR("FALSE"),  // Activate program event security feature. Requires encrypted bitstreams.

      .SIM_CCLK_FREQ(10.0)  // Set the Configuration Clock Frequency(ns) for simulation.

   )

   STARTUPE2_inst (

      .CFGCLK(CFGCLK),       // 1-bit output: Configuration main clock output

      .CFGMCLK(CFGMCLK),     // 1-bit output: Configuration internal oscillator clock output

      .EOS(EOS),             // 1-bit output: Active high output signal indicating the End Of Startup.

      .PREQ(PREQ),           // 1-bit output: PROGRAM request to fabric output

      .CLK(1'b0),             // 1-bit input: User start-up clock input

      .GSR(1'b0),             // 1-bit input: Global Set/Reset input (GSR cannot be used for the port name)

      .GTS(1'b0),             // 1-bit input: Global 3-state input (GTS cannot be used for the port name)

      .KEYCLEARB(1'b0), // 1-bit input: Clear AES Decrypter Key input from Battery-Backed RAM (BBRAM)

      .PACK(1'b0),           // 1-bit input: PROGRAM acknowledge input

      .USRCCLKO(spi_clk),   // 1-bit input: User CCLK input

                             // For Zynq-7000 devices, this input must be tied to GND

      .USRCCLKTS(1'b0), // 1-bit input: User CCLK 3-state enable input

                             // For Zynq-7000 devices, this input must be tied to VCC

      .USRDONEO(1'b1),   // 1-bit input: User DONE pin output control

      .USRDONETS(1'b1)  // 1-bit input: User DONE 3-state enable output

   );

endmodule

附录2:仿真设计

目的是为了看一些关键信号,比如读写信号。这个测试向量,我们只需要产生时钟,复位激励信号即可。

module sim_pico_spi_uart(    );

    reg     clk,rst;

    reg     rxd;

    wire    txd;

    reg     spidi;

    wire    spiss,spido;

   

    pico_uart_spi uut(

    .clk    (clk),

    .rst    (rst),

    .rxd    (rxd),

    .txd    (txd),

    .spiss  (spiss),

    .spido  (spido),

    .spidi  (spidi)

    );

   

    initial begin

        rst=1'b1;

        #2000

        rst=1'b0;

     end

    

     initial begin

        clk=0;

      end

      always  #5 clk = ~clk;

     

      initial begin

        spidi=0;

        rxd=1;

       end

   

endmodule

运行行为仿真,添加地址,指令,opcode,status等信号,restart仿真,将opcode,status信号设置成ascii

Xilinx Picoblaze 使用介绍_第36张图片

Picoblaze所有指令都是双时钟周期,送地址、enable信号,下一时钟沿取出指令,然后译码执行。

参考设计的代码里面,写端口没有锁存,有没有风险?

Xilinx Picoblaze 使用介绍_第37张图片

KCPSM6 user guide里面建议如下实现方式,但在我们的设计不需要,这个锁存动作会在uart tx模块发生。

Xilinx Picoblaze 使用介绍_第38张图片

读操作见下面图:

Xilinx Picoblaze 使用介绍_第39张图片

User guide上建议的做法如下:可以看出,对于读操作,外部逻辑需要提前把数据送出来,外部逻辑采集到read_strobe信号有效后应该把下一个数据放在总线上,否则无法正确读取到数据。

Xilinx Picoblaze 使用介绍_第40张图片

可以对着uart_spi_rom.log文件,检查代码运行是否一致。

Xilinx Picoblaze 使用介绍_第41张图片

你可能感兴趣的:(fpga开发)