Picoblaze设计指南
Picoblaze是Xilinx的8位微处理器,其占用资源非常少,可以在CPLD、FPGA里面,实现一个或多个这样的处理单元。本文以Vivado软件来介绍Picoblaze,如果你选择的器件是Spartan-6或更早器件,那请采用ISE软件。
进入Xilinx官网,在搜索框输入picoblaze,搜索后找到Picoblaze lounge进入。或者直接点PicoBlaze 8-bit Microcontroller
在Design Files里面,我们根据自己的器件选择相应的下载,本文采用的是ARTY S7板子,所以下载的是PicoBlaze for UltraScale, 7-series, 6-series FPGAs >> KCPSM6 Rev 9 – September 30, 2014 (ZIP)(注意:下载需要登录Xilinx,如果没有帐号,请注册一个)。
里面2个重要的文档,一个是设计流程介绍(step by step),一个是用户指南,包括完整指令集介绍。本文以SPI Flash编程为例介绍Picoblaze的设计流程。
在Vivado里,新建工程,选择xc7s50csga324-1il器件。根据自己爱好语言,添加相应源文件,这里选择verilog语言,添加verilog目录下的kcpsm6.v,添加Uart_and_PicoTerm目录下uart_rx6.v、uart_tx6.v两个文件,记得勾选Copy sources into project,如下图:
建立自己的顶层文件(不需要加spiclk,后面会解释),如下图:
打开verilog目录下的kcpsm6_design_template.v文件,复制信号定义跟picoblaze处理器元件安装(把模板里面下面代码复制到pico_uart_spi.v)。
将模板里面程序rom元件安装复制到pico_uart_spi.v里面。根据自己喜好修改元件名跟安装名,并修改rom相关参数,vivado里面不支持6系列,只能选择7S跟US。默认JTAG LOADER开关是打开的,这个如果你调试完软件代码后不在需要,可以关闭,不过这个功能占用逻辑资源非常少,建议保留。程序空间我们选择默认2K。其实UART跟SPI需要的代码非常少。
模板里面复位信号是有cpu_reset跟jtag loader模块传递上来的rdl信号取或来复位处理器的,我们沿用,这样每次通过jtag loader代码后自动从复位开始运行。模块里面声明kcpsm6_reset(高复位)是reg信号,我们设计里面是wire,请在代码里面修改成wire kcpsm6_reset; 我们将cpu_reset直接连接到外部rst管脚。
打开UART_and_Picoterm\ALTYS_design目录下uart6_altys.v,复制里面的串口收发模块的元件安装代码,如果不需要串口,请直接跳到spi元件实现。
继续复制uart6_altys.v的uart的信号实现,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或图标里面的小灯泡:
如上图,将spi_clk信号接到USRCCLK0上,输出信号用模板里面的,输入信号,除与DONE相关的信号置为1,其他置为0。
为了避免每次串口发送数据时候检测发送FIFO状态,我们将picoblaze的kcpsm6_sleep接到串口发送的半满信号上.加入下面语句assign kcpsm6_sleep= uart_tx_half_full;
保存后,在Sources里面,还有ROM是问号,这个不需要我们实现,是汇编器汇编我们的代码的时候生成。但我们需要做简单处理,只需要生成一个空的模块名即可,不需要添加任何端口、信号与逻辑实现:
逻辑设计工作基本完成(管脚时钟约束,见后面),但现在还是实现不了,因为rom文件还没生成。
复制Reference_Designs\SPI目录下四个汇编文件到vivado工程目录下面picoblaze_spi.srcs\sources_1\new的目录里:
继续复制verilog目录下ROM_form_JTAGLoader_Vivado_2June14.v到上述名录,改名为ROM_form.v
继续复制kcpsm6.exe到上述目录,复制完如下图:
在上述目录下新建立一个uart_spi_rom.psm空文件,并编辑,我们会把其他参考设计PSM文件包含进来,以充分利用现成的一些函数(参考设计的PSM文件会include其他几个psm文件):
修改PicoTerm_routines.psm里面CONSTANT reset_UART_port, 01为CONSTANT reset_UART_port, 00(因为我们逻辑里面做了修改。简单检查一下逻辑实现跟汇编代码是否一致。
进入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文件)
现在逻辑可以综合实现整个工程了,如果没有错误,Open Implemented Design,约束管脚位置,并保存约束文件。
约束时钟,并保存(DRCK为JTAG的门控时钟,UPDATE是BST的并行数据锁存时钟,频率非常低)。
SPI接口也是同步电路,但由于CCLK是专用管脚,在自动分析出来的时钟里面是见不着的,但是也是需要约束的
约束方法见Xilinx Customer Community
直接点Generate Bitstream,软件会重新综合实现并生成bit文件。打开串口调试软件,下载Bit文件。满屏的Welcome! 怎么回事,没换行….
修改代码,往串口输出0x0d换行符号。
JTAGLOADER动态加载软件
通过windows10开始菜单,进入ISE Design Suite 64 Bit Command Prompt(如果我就只是Vivado用户,没有ISE,怎么办?哈哈,那你就得每次修改汇编代码后,编译软件、重新综合逻辑、实现再生成BIT并下载了。当然也不是没有办法,毕竟Jtag loader的源码在我们工程里面有呀,Vivado也开放了Jtag的用户接口给我们,见”Picoblaze Jtag Loader in Vivado”这篇文章,本文不讨论)。将Picoblaze里Jtag_Loader目录下的相关文件拷贝到我们汇编代码所在目录,WIN10系统请拷贝WIN7的执行文件跟动态库。Linux用户也提供了相应的loader文件。
拷贝完成,切换到ISE命令行,编译并下载软件代码:
OK,串口已经能正常工作了。
硬件已经搭建好了,现在SPI接口就只剩余软件的事情了。编辑N25Q128_SPI_routines.psm,我们代码里面把spi输入输出端口都设置成3了,所以这里要把SPI_output_port修改成如下,如果你的逻辑对数据位置做了调整,那请自行修改这里相应位置:
N25Q128_SPI_routines.psm里面提供了FLASH常用操作,比如读设备ID,读字节,写字节,擦除扇区等。这些汇编代码都很简单,请自行阅读。看一下读ID代码,不用传递参数进去,读ID命令是9F,返回三个值在S9,S8,S7寄存器里面。
读ID后,继续读256字节数据,并通过串口每行16个字节显示。完成后代码如下:
思考:为什么每次只读一个字节呢?因为我们的寄存器数目有限,2个寄存器BANK供32个。但是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写,我们只需要传输一个字节那个位置传一页数据就可以完成页写。
修改后,以前的函数尽量保留,无法保留的这里就改名,我们把一页数据保存在Pad Memory传递给页写函数。
完成后的主程序如下:
运行程序,可以在串口调试界面看到输出跟预期望一样,但注意,这里有个小问题,就是我们在循环页写页读,其实我们只成功写一次,为什么(因为我们写以前没有擦除页,以前这个高地址页是空白的,所以不用擦除,但写过后再次写就需要先擦除,你们自行修改即可,擦除函数有现成的,要注意这些命令是否跟你的器件一致。)
至于Picoblaze怎么跟主逻辑交互,我相信这个对大家来说都是很简单的事情。要升级配置SPI FLASH的内容可以通过BRAM,小FIFO传递给Picoblaze。
资源占用情况,在vivado里,Open Implemented Design,在tcl console里输入report_utilization -hierarchical -hierarchical_depth 1 。可以看到如下报告,256字节Pad Memory情况:
64字节Pad Memory情况:
关闭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
附录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。
Picoblaze所有指令都是双时钟周期,送地址、enable信号,下一时钟沿取出指令,然后译码执行。
参考设计的代码里面,写端口没有锁存,有没有风险?
KCPSM6 user guide里面建议如下实现方式,但在我们的设计不需要,这个锁存动作会在uart tx模块发生。
读操作见下面图:
User guide上建议的做法如下:可以看出,对于读操作,外部逻辑需要提前把数据送出来,外部逻辑采集到read_strobe信号有效后应该把下一个数据放在总线上,否则无法正确读取到数据。
可以对着uart_spi_rom.log文件,检查代码运行是否一致。