该系列中的先前文章
也许,今天我什至会打破传统,并不会在Redd复杂系统上而是在常规布局上调试项目。首先,我知道绝大多数读者都无法使用这种复杂的软件,但是他们确实可以使用Ali Express。嗯,其次,我懒得用双USB设备和主机连接起来围起来一个花园,而且还无法应付新出现的干扰。
早在2017年,我就在网络上寻找现成的解决方案,发现了一件如此美妙的事情,或者更确切地说是它的祖先。现在他们将所有东西都放在一个专门的板上,但是到处都有Xilinx的简单面包板的照片,WaveShare的板与之相连(您可以在此处了解有关内容)。让我们看一下该板的照片。
它一次具有两个USB连接器。此外,该图显示它们已并行化。您可以将USB设备插入A型插座,也可以将电缆连接到微型USB连接器,我们将其插入主机。 OpenVizsla项目的说明说这种方式有效。唯一可惜的是,该项目本身很难阅读。您可以在github上使用它,但我不会提供指向页面上指示的帐户的链接,无论如何大家都会找到它,但是它已被MiGen重做,而是我在2017年找到的版本:http:// github。 com /超嵌入式/核心,它在干净的Verilog上,并且有usb_sniffer分支。在那里,所有事情都不直接通过ULPI进行,而是通过ULPI到UTMI转换器进行(这两个不雅词都是与高速USB 2.0通道以及处理器和FPGA可以理解的总线相匹配的物理级微电路),然后才可以与此UTMI一起使用。那里的一切运作方式,我还没有弄清楚。因此,我宁愿从头开始开发,因为我们很快就会看到那里的一切令人恐惧而不是困难。
您可以使用什么硬件
标题中问题的答案很简单:在具有FPGA和外部存储器的任何人上。当然,在本系列文章中,我们将仅考虑Altera FPGA(英特尔)。但是,请记住,来自ULPI微电路(位于该手帕上)的数据以60 MHz运行。此处不接受长电线。将CLK线连接到GCK组的FPGA输入上也很重要,否则一切都会起作用,然后失败。最好不要冒险。我不建议您以编程方式转发它。我试过了。所有这些都以GCK组的一条腿的金属丝结束。
对于今天的实验,应我的要求,一个朋友给我焊接了这样的系统:
带FPGA和SDRAM的微模块(在ALI上查找,用短语FPGA AC608表示))和WaveShare的同一ULPI板。这就是模块在其中一位卖家的照片中显示的方式。我太懒了,无法从机箱上拧开它:
顺便说一句,就像我的机箱照片一样,通风孔非常有趣。在模型上,绘制一个实体层,然后在切片器中设置40%的填充,并说您需要从底部到顶部制作零个实体层。结果,3D打印机会自己绘制通风孔。非常舒适。
通常,查找硬件的方法很明确。现在我们开始设计分析仪。相反,在前两篇文章中我们已经完成了分析仪的工作(在这里我们使用硬件,在这里-可以使用它),现在我们将简单地设计一个面向问题的探头,以捕获来自ULPI微电路的数据。
头部应该能做什么
就逻辑分析仪而言,一切都很简单。有数据。我们连接到它们并开始打包,然后将它们发送到AVALON_ST总线。这里的一切都更加复杂。 ULPI规范可在此处找到。九十三张无聊的文字。就个人而言,这使我感到沮丧。 WaveShare板上安装的USB3300芯片的说明看起来更加简单。你可以在这里得到。尽管自2017年12月以来我仍然积蓄了勇气,但有时我会感到沮丧,因此有时会阅读并立即关闭文档。
从描述中可以明显看出,ULPI具有一组必须在开始工作之前填写的寄存器。这主要是由于上拉电阻和端接电阻。这是一张图片来说明这一点:
根据角色(主机或设备)以及所选的速度,必须包括不同的电阻器。但是我们既不是主机,也不是设备!我们必须断开所有电阻,以免干扰总线上的主要设备!这是通过写入寄存器来完成的。
好,速度快。必须选择工作速度。为此,您还需要写入寄存器。
配置完所有内容后,您就可以开始获取数据了。但是以ULPI的名义,字母“ LP”表示“低引脚数”。腿数的这种非常减少导致了如此激烈的协议,只是坚持下去!让我们仔细看看该协议。
ULPI协议
对于普通人来说,ULPI协议有点不寻常。但是,如果您坐在文档旁打坐,那么就会出现一些或多或少可以理解的功能。显然,开发人员已尽一切努力来真正减少使用的联系人数量。
我不会在此处重新键入完整的文档。让我们将自己限制在最重要的事情上。其中最重要的是信号的方向。记住它是不可能的,最好每次查看图片:
ULPI LINK是我们的FPGA。
数据接收时序图
静止时,我们必须向数据总线发出一个常量0x00,它对应于IDLE命令。如果数据来自USB总线,则交换协议将如下所示:
循环将以DIR信号最多飞到一个的事实开始。首先,它将有一个时钟周期,以便系统有时间切换数据总线的方向。进一步-经济奇迹开始了。看到NXT信号的名称了吗?从我们这里传送时意味着NEXT。这是一个完全不同的信号。当DIR为1时,我将称为NXT C / D。低级别-我们有一个团队。高数据。
也就是说,我们必须始终在高DIR上固定9位(DATA总线和NXT信号)(然后通过软件对第一个时钟进行滤波),或者从DIR起飞后的第二个时钟开始固定。如果DIR线降为零,我们将数据总线切换为写入状态,然后再次开始广播IDLE命令。
通过数据接收-很明显。现在,让我们分析寄存器的工作。
写入ULPI寄存器的时序图
要写入寄存器,请使用以下临时房屋(我故意切换到行话,因为我觉得我倾向于GOST 2.105,这很无聊,所以我
将其移开):首先,我们必须等待状态DIR = 0。在时钟T0,我们必须在数据总线上将TXD CMD设置为常数。这是什么意思?您无法马上弄清楚,但是如果您仔细阅读文档,结果可以在此处找到所需的值:
也就是说,高数据位应设置为值“ 10”(对于整个字节,掩码为0x80),而低位应设置为寄存器号。
接下来,您应该等待NXT信号发出。有了这个信号,微电路确认它听到了我们的声音。在上图中,我们在时钟T2等待它,并在下一个时钟(T3)上设置数据。在时钟T4上,ULPI将接收数据并删除NXT。我们将在STP中标记单位交换周期的结束。同样在T5上,数据将被锁存到内部寄存器中。该过程已结束。这是为数不多的结论的回报。但是我们只需要在启动时就写入数据,因此,当然,我们将不得不遭受开发的困扰,但是这不会特别影响工作。
从ULPI寄存器读取的时序图
老实说,对于实际任务,读取寄存器不是那么重要,但让我们也来看看。阅读至少对确保我们正确实施记录有用。
我们看到在我们面前是前两个临时房屋的爆炸性混合物。我们按照写入寄存器的方式设置地址,然后根据读取数据的规则获取数据。
好?让我们开始设计一个自动机,它将为我们塑造这一切吗?
头部结构图
从上面的描述中可以看到,磁头必须一次连接到两条总线:AVALON_MM用于访问寄存器,而AVALON_ST用于发布要存储在RAM中的数据。头部的主要是大脑。因此,它应该是一个状态机,它将生成我们之前考虑的时间图。
让我们从接收数据的功能开始其开发。这里应该记住,我们不能以任何方式影响来自ULPI总线的流量。从那里开始的数据,如果它开始运行,它将继续运行。他们不在乎AVALON_ST总线是否准备就绪。因此,我们将简单地忽略总线的不可用性。在真实的分析仪中,有可能在数据输出没有准备就绪的情况下添加警报指示。在本文的框架内,所有内容都应该很简单,因此让我们在将来记住这一点。为了确保总线的可用性,就像在逻辑分析仪中一样,我们将有一个外部FIFO模块。总的来说,用于接收数据流的自动机的过渡图如下:
DIR起飞-开始接收。我们在wait1中挂了一个时钟,然后在DIR等于1时接受它。降为零-经过一个时钟(尽管不是必需的事实,但现在我们将状态wait2设置为空闲)返回空闲状态。
到目前为止,一切都很简单。不要忘记,不仅D0_D7线,而且NXT线都应连接到AVALON_ST总线,因为它确定了现在正在传输的内容:命令或数据。
寄存器写周期可能具有不可预测的执行时间。从AVALON_MM总线的角度来看,这不是很好。因此,我们将使其更加棘手。让我们创建一个缓冲寄存器。数据将进入其中,之后将立即释放AVALON_MM总线。从正在开发的自动机的角度来看,出现have_reg输入信号(已接收到寄存器中的数据,应发送该数据)和reg_served输出信号(这意味着寄存器发布过程已完成)。将写入逻辑添加到自动机的过渡图上的寄存器。
我用红色突出显示了DIR = 1条件,以表明它具有最高优先级。这样就可以排除机器新分支中DIR信号为零的期望。根本无法登录到具有不同值的分支。 SET_CMDw状态为蓝色,因为它最有可能是纯虚拟的。这些只是要执行的动作!没有人会费心在数据总线上以及在过渡期间设置相应的常数!除其他外,在STPw状态下,还可将reg_served信号锁定一个时钟周期,以清除AVALON_MM总线的BSY信号,从而允许一个新的写周期。
好了,仍然需要添加一个分支来读取ULPI寄存器。在这里,情况恰恰相反。公交服务机向我们发送请求,并等待我们的回复。接收到数据后,他就可以对其进行处理。它可以与总线暂停或轮询一起使用,这些已经是该机器的问题。今天,我决定进行一项调查。正在请求数据-出现了BSY。BSY如何消失-您可以接收读取的数据。总体而言,该图采用以下形式:
也许在开发过程中会进行一些调整,但是现在,我们将坚持使用此图。毕竟,这不是报告,而是有关开发方法的说明。而且这种技术使得您首先需要绘制一个过渡图,然后-根据此图进行逻辑调整,以针对弹出的细节进行调整。
从AVALON_MM端实现自动机的功能
使用AVALON_MM总线时,可以采用两种方式。首先是创建总线访问延迟。我们在上一篇文章中探讨了这种机制,我警告说它充满了问题。第二种方式是经典。输入状态寄存器。在事务开始时,设置BSY信号,完成时将其复位。并将所有事情分配给总线主控逻辑(Nios II处理器或JTAG桥接器)。每个选项都有其自身的优点和缺点。由于我们已经完成了带有总线延迟的变体,所以让我们今天通过状态寄存器进行所有更改。
我们设计主机
我想引起您注意的第一件事是我最喜欢的RS触发器。我们有两台机器。第一个服务于AVALON_MM总线,第二个服务于ULPI接口。我们发现它们之间的连接要经过几个标志。每个标志只能写入一个进程。每个自动机由其自己的过程实现。怎样成为?一段时间以来,我刚刚开始添加RS触发器。我们有两个位,因此它们必须由两个RS触发器生成。他们来了:
//
always_ff @(posedge ulpi_clk)
begin
//
if (reg_served)
write_busy <= 0;
else if (have_reg)
write_busy <= 1;
//
if (read_finished)
read_busy <= 0;
else if (reg_request)
read_busy <= 1;
end
一只进程的公鸡已送达,第二只公鸡的have_reg。RS触发器在其自身的过程中根据其自身生成write_busy信号。同样,read_busy由read_finished和reg_request组成。您可以采取不同的方法,但是在创作路径的这个阶段,我喜欢这种方法。
这就是设置BSY标志的方式。黄色代表写作过程,蓝色代表阅读过程。Verilogov过程具有一个非常有趣的功能。在其中,您可以分配值不是一次,而是几次。因此,如果我希望信号在一个时钟周期内起飞,则在过程开始时将其无效(我们看到两个信号都在那里无效),并根据在一个时钟周期内执行的条件将其设置为1。输入条件将覆盖默认值。在所有其他情况下,它将起作用。因此,向数据端口写入会在一个时钟周期内启动have_reg信号的输出,而向控制端口写入位0将向reg_request信号的输出启动。
相同的文字。
// AVALON_MM
always_ff @(posedge ulpi_clk)
begin
// ,
//
have_reg <= 0;
reg_request <= 0;
if (write == 1)
begin
case (address)
0 : addr_to_ulpi <= writedata [5:0];
//
1 : begin
data_to_ulpi <= writedata [7:0];
have_reg <= 1;
end
2 : begin
//
reg_request <= writedata[0];
force_reset = writedata [31];
end
3: begin end
endcase
end
end
如上所述,一个时钟周期足以将相应的RS触发器设置为1。从这一刻起,已设置的BSY信号开始从状态寄存器中读取:
相同的文字。
// AVALON_MM
always_comb
begin
case (address)
// ( )
0 : readdata <= {26'b0, addr_to_ulpi};
//
1 : readdata <= {23'b0, data_from_ulpi};
// 2 - , -
//
3 : readdata <= {30'b0, (reg_request | read_busy), (have_reg | write_busy)};
default: readdata <= 0;
endcase
end
实际上,自然而然地,我们熟悉了AVALON_MM总线上服务的进程。
让我也提醒您有关使用ulpi_data总线的原则。该总线是双向的。因此,您应该使用一种标准的技术来处理它。这是声明相应端口的方式:
inout [7:0] ulpi_data,
我们可以从该总线读取,但不能直接写入。相反,我们为记录创建一个副本。
logic [7:0] ulpi_d = 0;
然后,通过以下多路复用器将此副本连接到主总线:
// inout-
assign ulpi_data = (ulpi_dir == 0) ? ulpi_d : 8'hzz;
我试图在Verilog代码中尽可能多地评论主机的逻辑。正如我在过渡图的开发过程中所期望的那样,在实际实现中,逻辑有所改变。一些州被排除在外。不过,通过比较图形和源文本,我希望您了解在那里所做的一切。因此,我不会谈论这台机器。最好根据实际实验的结果提供与修改前相关的模块全文作为参考。
模块的全文。
module ULPIhead
(
input reset,
output clk66,
// AVALON_MM
input [1:0] address,
input write,
input [31:0] writedata,
input read,
output logic [31:0] readdata = 0,
// AVALON_ST
input logic source_ready,
output logic source_valid = 0,
output logic [15:0] source_data = 0,
// ULPI
inout [7:0] ulpi_data,
output logic ulpi_stp = 0,
input ulpi_nxt,
input ulpi_dir,
input ulpi_clk,
output ulpi_rst
);
logic have_reg = 0;
logic reg_served = 0;
logic reg_request = 0;
logic read_finished = 0;
logic [5:0] addr_to_ulpi;
logic [7:0] data_to_ulpi;
logic [7:0] data_from_ulpi;
logic write_busy = 0;
logic read_busy = 0;
logic [7:0] ulpi_d = 0;
logic force_reset = 0;
//
always_ff @(posedge ulpi_clk)
begin
//
if (reg_served)
write_busy <= 0;
else if (have_reg)
write_busy <= 1;
//
if (read_finished)
read_busy <= 0;
else if (reg_request)
read_busy <= 1;
end
// AVALON_MM
always_comb
begin
case (address)
// ( )
0 : readdata <= {26'b0, addr_to_ulpi};
//
1 : readdata <= {23'b0, data_from_ulpi};
// 2 - , -
//
3 : readdata <= {30'b0, (reg_request | read_busy), (have_reg | write_busy)};
default: readdata <= 0;
endcase
end
// AVALON_MM
always_ff @(posedge ulpi_clk)
begin
// ,
//
have_reg <= 0;
reg_request <= 0;
if (write == 1)
begin
case (address)
0 : addr_to_ulpi <= writedata [5:0];
//
1 : begin
data_to_ulpi <= writedata [7:0];
have_reg <= 1;
end
2 : begin
//
reg_request <= writedata[0];
force_reset = writedata [31];
end
3: begin end
endcase
end
end
//
enum {idle,
wait1,wr_st,
wait_nxt_w,hold_w,
wait_nxt_r,wait_dir1,latch,wait_dir0
} state = idle;
always_ff @ (posedge ulpi_clk)
begin
if (reset)
begin
state <= idle;
end else
begin
//
source_valid <= 0;
reg_served <= 0;
ulpi_stp <= 0;
read_finished <= 0;
case (state)
idle: begin
if (ulpi_dir)
state <= wait1;
else if (have_reg)
begin
// ,
// ,
//
ulpi_d [7:6] <= 2'b10;
ulpi_d [5:0] <= addr_to_ulpi;
state <= wait_nxt_w;
end
else if (reg_request)
begin
// -
ulpi_d [7:6] <= 2'b11;
ulpi_d [5:0] <= addr_to_ulpi;
state <= wait_nxt_r;
end
end
// TURN_AROUND
wait1 : begin
state <= wr_st;
// ,
source_valid <= 1;
source_data <= {7'h0,!ulpi_nxt,ulpi_data};
end
// DIR - AVALON_ST
wr_st : begin
if (ulpi_dir)
begin
// ,
source_valid <= 1;
source_data <= {7'h0,!ulpi_nxt,ulpi_data};
end else
// wait2,
// , - .
state <= idle;
end
wait_nxt_w : begin
if (ulpi_nxt)
begin
ulpi_d <= data_to_ulpi;
state <= hold_w;
end
end
hold_w: begin
// , ULPI
// . NXT
// ...
if (ulpi_nxt) begin
// , AVALON_MM
reg_served <= 1;
ulpi_d <= 0; // idle
ulpi_stp <= 1; // STP
state <= idle; // - idle
end
end
// STPw ...
// ...
// . , NXT
// ,
wait_nxt_r : begin
if (ulpi_nxt)
begin
ulpi_d <= 0; //
state <= wait_dir1;
end
end
// ,
wait_dir1: begin
if (ulpi_dir)
state <= latch;
end
//
// -
latch: begin
data_from_ulpi <= ulpi_data;
state <= wait_dir0;
end
// ,
wait_dir0: begin
if (!ulpi_dir)
begin
state <= idle;
read_finished <= 1;
end
end
default: begin
state <= idle;
end
endcase
end
end
// inout-
assign ulpi_data = (ulpi_dir == 0) ? ulpi_d : 8'hzz;
// reset ,
assign ulpi_rst = reset | force_reset;
assign clk66 = ulpi_clk;
endmodule
程序员指南
ULPI寄存器地址端口(+0)
ULPI总线寄存器的地址应放在偏移量为+0的端口中,该地址将用于工作
ULPI寄存器数据端口(+4)
写入该端口时:将自动开始写入ULPI寄存器的过程,该过程的地址已在寄存器地址的端口中设置。在之前的写操作完成之前,禁止对该端口进行写操作。
读取时:此端口将返回从ULPI寄存器的最后一次读取中获得的值。
ULPI控制端口(+8)
读数始终为零。写入的位分配如下:
位0-当写入单个值时,启动读取ULPI寄存器的过程,该寄存器的地址在ULPI寄存器的地址端口中设置。
位31-写入1时,将复位信号发送到ULPI芯片。
其余位保留。
状态端口(+ 0x0C)
只读。
位0-WRITE_BUSY。如果等于1,则正在写入ULPI寄存器。
位1-READ_BUSY。如果等于1,则正在进行ULPI寄存器的读取过程。
其余位保留。
结论
我们熟悉了USB分析仪头的物理组织方法,设计了用于ULPI微电路的基本自动机,并为该头实现了SystemVerilog模块草案。在下面的文章中,我们将研究建模过程,模拟此模块,然后对其进行实际实验,根据其结果,我们将最终确定代码。也就是说,到最后,我们至少还有四篇文章。