使用 Linux 进行 FPGA 编程
自由软件许可和像 GNU/Linux 这样的操作系统使得学习编程和以无数种方式定制最先进的软件成为可能。然而,破解软件已不再是最后的边界。如果您可以直接破解集成电路——也就是说,告诉芯片连接其内部晶体管以创建您想要的精确的、实时的数字硬件,会怎么样?这正是您可以使用现场可编程门阵列 (FPGA) 做的事情。在本文中,我将解释如何仅使用您的 Linux 计算机和廉价的开发板来完成这项工作。
这不是 Linux Journal 第一次报道 FPGA(请参阅“资源”),但自那些文章撰写以来,这些设备取得了巨大的进步。此外,功能强大的开发板的价格也大幅下降。如今,您可以使用 200-300 美元的基于 FPGA 的开发板、一台典型的个人计算机和一平方英尺的桌面空间来完成非常酷的事情。最后,FPGA 社区现在足够大且足够稳定,可以使初学者的生活更加轻松。主要的 FPGA 制造商提供文本或视频教程以及论坛,即使是新手也可以在其中找到支持。最重要的是,像 Opencores.org 这样的网站以与自由软件相同的精神(和许可证)共同开发和发布 Linux 兼容的 FPGA 和开发板,例如 EUS 100LX。总而言之,准入门槛现在比几年前低得多,这使得 FPGA 设计既成为一种很酷的爱好,也成为即使是高中也能负担得起且有趣的课程补充。
数字集成电路 (IC) 是一种仅处理二进制数字的芯片——这意味着信号只能假定两种状态之一:0 或 1、高电压或低电压等等。FPGA 是一种 IC,由数字逻辑门阵列组成。这些基本电路,每个电路由几个晶体管组成,实例化一个触发器或查找表,能够实现最多四个二进制信号的任何布尔函数。FPGA 的魔力在于逻辑门之间的连接(您需要的实际电路)在通电时通过读取写入位文件的配置指令来完成。更改文件会更改 FPGA 的功能。
尽管微处理器非常灵活,但它们始终并且只是微处理器:单用途硬件,只能(相对缓慢地)执行一种机器语言的指令。相反,FPGA 会变成您需要的任何硬件。它可以变成微处理器、游戏机、实时 IP 交换机或加密设备、防盗服务器或您可以想象的任何其他东西。唯一的限制是您的电路不能需要比芯片上物理存在的晶体管或外部引脚更多,并且它的速度不能超过从门到门的固有传播延迟。
话虽如此,现代 FPGA 功能足够强大,可以让您在其中挤入多个 Linux 兼容的微处理器,例如 Nios、PowerPC 或 Microblaze,并且仍然有足够的空间用于您自己的定制电路。在许多情况下,您可以从库中加载经过认证的 CPU 设计,并通过简单的命令将它们放置在硅片上,从而在单个芯片内创建非常灵活、完整的系统。
主要的 FPGA 和其他可编程 IC 制造商是 Altera 和 Xilinx,其次是 Lattice 和 Atmel。尽管本文中的示例使用了 Xilinx 产品,但所有供应商的一般程序都是相同的,并且它们都具有类似的开发板。在所有情况下,设计软件都是闭源的且通常很昂贵,但可以下载有效期为一两个月的免费试用版,或功能减少但免费升级且没有到期日期的免费版本。
当前设计 FPGA 的方法是用硬件描述语言 (HDL)(如 Verilog 或 VHDL)编写行为模型,该语言支持并发和同步电路。并发允许您创建完全并行、独立的进程,每个进程都描述如何连续更新某些变量。相反,同步电路是由触发器组成的电路,这些触发器仅在某些时钟信号的边沿改变其状态。
在设计完成并通过 HDL 模拟器验证后,编译器会创建一个逻辑门列表和必须连接它们的导线(网络)列表,以重现 HDL 模型的功能。在逻辑综合之后,布局程序读取网络表和几个约束文件,以找出必须使用 FPGA 内部的哪些逻辑门,以及哪些物理内部导线必须将它们相互连接。最终结果是 FPGA 在通电时读取的位文件。
官方的 Xilinx 设计套件称为 ISE Foundation (www.xilinx.com/ise),功能减少的版本称为 Webpack。这两个程序都在 Windows、Red Hat Enterprise 和 SUSE Linux Enterprise(32 位或 64 位)上运行。其他 Linux 发行版也可能有效,但不能保证。
ISE 有一个图形安装程序,您必须接受软件许可并在 Xilinx 网站上免费注册后输入您获得的密钥。完成后,您将在安装目录中找到一个名为 settings32.sh 或 settings64.sh 的脚本——您必须运行该脚本才能将 Xilinx 软件添加到您的路径中。之后,输入ise在提示符下启动 Project Navigator(图 1)。这是许多专门程序的前端,每个程序对应一个设计阶段。您也可以从命令行运行大多数这些后端实用程序。Navigator 包括一个 Tcl 提示符,如果您选择“Project→Generate Tcl Script”,它将保存您通过 GUI 输入的所有命令作为 Tcl 脚本。
其他 ISE 组件,如模拟器、FPGA 编辑器和 ChipScope,都具有图形界面。当软件无法按照您的规范执行时,您可以使用 FPGA 编辑器手动放置和连接单个门。ChipScope 就像一个带有 USB 探头的软件示波器。在综合期间,您可以向您的设计添加特殊电路,这些电路将缓冲您想要看到的内部信号,并通过 USB 电缆将它们发送到 ChipScope 软件进行显示。我们稍后将看到 ISE HDL 模拟器的工作原理。
我从 Xilinx 为本文获得的开发板是 Spartan-3AN Starter Kit(图 2),它基于 Spartan XC3S700AN FPGA(图 3),其中包含约 70 万个系统门。它周围有几个存储芯片、一个 50MHz 板载时钟、一个用于外部时钟的连接器以及几个额外的组件,从 D/A 和 A/D 转换器到通用 I/O 引脚、各种 LED、滑块和按钮,最后是一个两行 LCD 显示屏。端口(图 4)足以用这个开发板制作一台完全定制的 Linux PC:10/100 以太网/PHY、USB、键盘、VGA、串行和用于 PWM 音频的立体声迷你插孔。随附通用电源适配器和 USB 电缆,以及四个不同的位文件,演示了 FPGA 的功能。相应的设计文件可以从 Xilinx 网站免费下载。
为了向您展示设计定制数字硬件是什么样的,以及 FPGA 开发软件是如何工作的,我修改了加载到 Starter Kit 中的演示电路之一,即 Xilinx 高级工程师 Ken Chapman 的 DNA 读取器。
Spartan FPGA 有一个唯一的 ID 号码,称为 DNA。DNA 读取器显示介绍字符串“DNA Reader by Ken Chapman”,然后此号码显示在 LCD 屏幕上(图 5),工作原理如图 6 所示。Xilinx 硬件宏称为 dna_port 从硅芯片读取 DNA ID。PicoBlaze 处理器首先显示介绍字符串,然后从 dna_port 获取 DNA ID,最后通过 lcd_d 数据总线一次一个字符地将其发送到 LCD 接口。PicoBlaze 代码存储在 dna_ctrl ROM 中。
我的修改包括一个小型的额外电路,该电路动态地将默认的介绍字符串覆盖为“M Fioretti Linux Journal”。请注意,这只是为了演示目的而进行的 hack。在现实世界中,如果您真的需要更改该字符串,那么重写 PicoBlaze 汇编代码会更有意义。然而,因为这是一篇关于 FPGA 中 HDL 设计的文章,所以我选择了一个基于易于阅读的 HDL 代码的解决方案,其效果在一张图片中很容易看到。
我的额外电路在图 6 中以红色显示:一个计数器和解码器,用于检测 PicoBlaze 何时驱动 LCD 数据端口 (lcd_d) 并向其发送不同的字符。与此额外硬件对应的 VHDL 源代码如清单 1 所示,这不是我使用的完整、可工作的 VHDL 文件,而只是一段摘录,旨在让您了解 HDL 编码的工作原理。
第 1-12 行定义了顶层电路 reading_dna 模块的输入和输出端口。您必须在使用之前声明所有内部寄存器和导线(第 17-23 行)。HDL 支持层次结构;您可以通过声明其他模块并将它们的所有端口连接到正确的信号来实例化它们(第 26-49 行)。第 53 行显示了同步过程的第一个示例。根据 cnt_ops 计数器的值,只要时钟有正边沿(第 56 行)并且处理器将信号 write_strobe 和 port_id(6) 设置为高电平,lcd_output_data 寄存器就会从处理器或我的额外逻辑加载字符(第 62-68 行)。从第 80 行开始的 cnt_and_new_chars 过程完成实际工作。首先,它采样 LCD 使能信号以计数(第 91 行)对 LCD 的写入访问。在写入发生一个周期后,使用新的计数器值(第 99 行),该过程计算应显示的下一个 current_character。如果您查看第 101-125 行,您会看到显示器应显示 ASCII 字符串“M Fioretti Linux Journal”,而不是 DNA 号码。快速模拟(图 7)证明新过程在正确的时间将这些字符发送到显示器——也就是说,当 lcd_rs 信号为高电平时(低电平将指示 LCD 配置命令)。
清单 1. VHDL 源代码
1 entity reading_dna is 2 Port ( led : out std_logic_vector(7 downto 0); 3 lcd_d : inout std_logic_vector(7 downto 0); 4 lcd_rs : out std_logic; 5 lcd_rw : out std_logic; 6 lcd_e : out std_logic; 7 j2_30 : out std_logic; 8 j2_26 : out std_logic; 9 j2_22 : out std_logic; 10 j2_14 : out std_logic; 11 clk : in std_logic); 12 end reading_dna; 13 -- 14 architecture Behavioral of reading_dna is 15 -- 16 17 -- start extra signals for LJ demo 18 signal lcd_e_copy : std_logic; 19 signal lcd_e_del_1 : std_logic; 20 signal lcd_e_del_2 : std_logic; 21 signal current_character : std_logic_vector(7 downto 0); 22 signal cnt_ops : integer range 0 to 49999999 := 0; 23 -- end extra signals for LJ demo 24 begin 25 26 device_dna: dna_port 27 port map( din => dna_din, 28 read => dna_read, 29 shift => dna_shift, 30 dout => dna_dout, 31 clk => dna_clk); 32 33 processor: kcpsm3 34 port map( address => address, 35 instruction => instruction, 36 port_id => port_id, 37 write_strobe => write_strobe, 38 out_port => out_port, 39 read_strobe => read_strobe, 40 in_port => in_port, 41 interrupt => interrupt, 42 interrupt_ack => interrupt_ack, 43 reset => kcpsm3_reset, 44 clk => clk); 45 46 program_rom: dna_ctrl 47 port map( address => address, 48 instruction => instruction, 49 clk => clk); 50 51 kcpsm3_reset <= '0'; 52 53 output_ports: process(clk) 54 begin 55 56 if clk'event and clk='1' then 57 if write_strobe='1' then 58 59 -- 8-bit LCD data output address 40 hex. 60 61 if port_id(6)='1' then 62 -- lcd_output_data <= out_port; 63 --extra code for LJ demo 64 if ((cnt_ops >= 8 and cnt_ops <= 17) or 65 (cnt_ops >= 19 and cnt_ops <= 32)) then 66 lcd_output_data <= current_character; 67 else 68 lcd_output_data <= out_port; 69 end if; --end extra code for LJ demo 70 end if; 71 72 end if; 73 74 end if; 75 76 end process output_ports; 77 78 -- LCD interface 79 80 cnt_and_new_chars: process(clk) 81 begin 82 if clk'event and clk='1' then 83 84 if port_id(5)='1' and write_strobe='1' then 85 lcd_e_copy <= out_port(0); 86 end if; 87 88 lcd_e_del_1 <= lcd_e_copy; 89 lcd_e_del_2 <= lcd_e_del_1; 90 91 if (lcd_e_copy ='1' and lcd_e_del_1='0') then -- posedge 92 if cnt_ops=49999999 then -- inc counter 93 cnt_ops <= 0; 94 else 95 cnt_ops <= cnt_ops + 1; 96 end if; -- if cnt_ops=49999999 97 end if; -- end (lcd_e_copy ='1' and lcd_e_del_1='0') 98 99 if (lcd_e_del_1 ='1' and lcd_e_del_2='0') then -- posedge 100 case cnt_ops is -- character generator 101 when 8 => current_character <= "01001101"; -- M 102 when 9 => current_character <= "00100000"; -- space 103 when 10 => current_character <= "01000110"; -- F 104 when 11 => current_character <= "01101001"; -- i 105 when 12 => current_character <= "01101111"; -- o 106 when 13 => current_character <= "01110010"; -- r 107 when 14 => current_character <= "01100101"; -- e 108 when 15 => current_character <= "01110100"; -- t 109 when 16 => current_character <= "01110100"; -- t 110 when 17 => current_character <= "01101001"; -- i 111 112 when 19 => current_character <= "01001100"; -- L 113 when 20 => current_character <= "01101001"; -- i 114 when 21 => current_character <= "01101110"; -- n 115 when 22 => current_character <= "01110101"; -- u 116 when 23 => current_character <= "01111000"; -- x 117 when 24 => current_character <= "00100000"; -- space 118 when 25 => current_character <= "01001010"; -- J 119 when 26 => current_character <= "01101111"; -- o 120 when 27 => current_character <= "01110101"; -- u 121 when 28 => current_character <= "01110010"; -- r 122 when 29 => current_character <= "01101110"; -- n 123 when 30 => current_character <= "01100001"; -- a 124 when 31 => current_character <= "01101100"; -- l 125 when 32 => current_character <= "00100000"; -- space 126 127 when others => current_character <= "00100000"; -- space 128 129 end case; 130 end if; -- end (lcd_e_del_1 ='1' and lcd_e_del_2='0') 131 132 133 end if; -- clk'event and clk='1' 134 end process cnt_and_new_chars;
将这个非常简单的 HDL 模型转换为硅片上正确连接的门的过程同样简单。一次双击图 1 中 Project Navigator 左中面板中的图标:synthesize(综合)、Implement Design(实现设计)、Generate Programming File(生成编程文件)和 Configure Target Device(配置目标设备)。如果您直接单击最后一个,ISE 无论如何都会按正确的顺序执行所有之前的步骤,但按步骤执行是更好的学习方式。最终,您将获得位文件和最终报告,如图 8 所示,显示使用了多少硅片。请记住,我们刚刚做的是实际的硬件——也就是说,晶体管直接连接以实时执行我们命令它们执行的操作。使其真正发生的所有剩余工作是将位文件加载到 FPGA 中。图 9 显示了结果。
由于篇幅限制,我只对 FPGA 设计流程给出了非常有限的视角。将 FPGA 推向极限需要大量的技能和经验。我没有谈到布局规划、优化或模拟策略,也没有深入探讨如何在 FPGA 内部运行 Linux。所有这些都是未来文章的优秀主题。
我写这篇文章的目标只是为了表明,开始学习这些技能非常容易,并且已经有一个强大的社区来帮助您。例如,学生们可以考虑 FPGA 是否是他们成为下一个 Linus 或 Steve Jobs 所需的东西。在我看来,任何已经在教授编程的高中都应该在其课程中添加 FPGA。如果您的学校已经在这样做,请告诉我。
我要感谢 Xilinx 的 K. Chapman 和 F. Porpora,以及 DekItalia.com 的 FPGA 大师,他们在准备这篇文章时给了我很大的帮助。
资源
Michael Baxter 撰写的适用于 Linux 的 Xilinx FPGA 设计工具:www.linuxjournal.com/article/6857
Michael Baxter 撰写的点菜式嵌入式系统:www.linuxjournal.com/article/6073
Spartan Starter Kit 数据表:www.xilinx.com/products/devkits/HW-SPAR3AN-SK-UNI-G.htm
OpenCore 的 Linux 和 Xilinx FPGA 开发板:www.opencores.org/?do=project&who=eus100lx
Xilinx Linux 论坛:forums.xilinx.com/xlnx/board?board.id=ELINUX
Xilinx 文档:www.xilinx.com/design
Marco (mfioretti.com) 是一位自由作家、活动家和教师,专注于开放数字标准和技术及其与公民权利和教育的关系和影响。他也是《数字自由家庭指南》(digifreedom.net) 的作者。