IcingTomato's Archive A Very Simple Knowledge Archive

What is LVM?

深入浅出讲述 Linux 上的 LVM (Logical Volume Manager 逻辑卷管理) 想象一个情况,我在网上买了个新电脑,自带 Windows 10 操作系统。512GiB 的硬盘我不想分区直接用,过了三个月: 它快满了,我就添置一块硬盘当仓库盘。但是有些文件太大我还要花时间移动到新的仓库盘上…… 虽然现在传输速度很快,但是还要花时间查找能移动和不能移动的文件确实麻烦。所以现在隆重介绍 LVM 逻辑卷管理。 逻辑卷管理的重点在于 可以弹性调整 FlieSystem 的容量 而不是 注重文件传输效率和数据安全 上面。追求读写效率或者数据保护的可以使用 RAID 。 LVM 可以整合成多个物理硬盘在一起,让这些分区看起来像在一块大硬盘上。而且在将来还可以新增或者移除物理硬盘到 LVM 中。 什么是 LVM : PV, PE, VG, LV 的含义 LVM 全名 Logical Volume Manager ,大陆中文译作 逻辑卷管理。台湾中文翻译成 邏輯捲軸管理員。在这里我觉得用『捲軸』来解释更方便,引用一下鸟哥的解释: 之所以稱為『捲軸』可能是因為可以將 filesystem 像捲軸一樣伸長或縮短之故吧! LVM 的做法是将几个实体的 Partitions分区 (或者 Disks硬盘) 通过软件转换成 LVM 最底层的 “块” (PV) ,然后将这些 “块” 组合成一块庞大的 “硬盘” (VG),接着将这块巨大的 “硬盘” 分割成一个个可以格式化的 “小硬盘” (LV),最终就能挂载使用了。但是为什么这样可以对 FileSystem 进行扩容和缩小呢?其实和一个叫 PE 的东西有关。 Physical Volume, PV, 物理卷 物理卷的理解其实很简单,可以近似看作是我们买来的实体的硬盘。但实际上物理卷(PV) 需要调整 硬盘(Disks)/分区(Partitions) 的 系统识别码(systemID) 为 8e( LVM 的识别码),然后再经过 pvcreate 的指令将它转换成 LVM 最底层的 物理卷(PV) ,之后才能将这些 PV 加以使用。调整 systemID 的方法有三种:gdisk, fdisk 和 parted。 注:gdisk是仅为GPT分区使用,fdisk是仅为MBR分区使用,如果用错的的话主引导记录会被清空,切记切记 Volume Group, VG, 卷组 顾名思义 卷组 就是很多个物理卷的凑成的一个组。就好比小学的时候会分小组,小组里面的每个同学就是物理卷,几个同学组成的一个组就是卷组。同理 VG 就是 LVM 将许多个 PV 整合成的东西。那么 VG 最大可以达到多少呢?这个和 PE物理块 和 LVM 的版本有关。在以前,32位的 Linux 操作系统上的 LVM(lvm1) 一个 LV 最大只能支持65534个 PE,假设使用 Linux 的默认设置(一个 PE 大小为4MiB),那么一个 LV 的最大容量也就只有 4M*65534/(1024M/G)=256GiB。不过在64位的操作系统上 LV 几乎不存在大小限制。(主要还是和寻址有关系) Physical Extent, PE, 物理块 LVM 预设的 PE 大小是 4MiB。它是整个 LVM 的最小存储区块,换句话说就是我们写入的每个文件都是往 PE 里面填充的。简单来说 PE 很像在机械硬盘上划分的磁道。所以调整 PE 大小会影响到 LVM 的最大容量的。但是在 CentOS/Red Hat 6 之后的操作系统普遍采用 lvm2 技术,以及64位 CPU 的出现,因此这个限制不复存在。 Logical Volume, LV, 逻辑卷 最终 一大块 VG 会像切蛋糕一样分成一个个 LV ,这些被切出来的 LV 就是能吃的格式化使用的东西了。 那么问题来了:LV 可以随意划分大小吗?答案是不可以。因为 PE 是 LVM 中最小的存储单位,所以 LV 的大小和 PV 的块数相关。在 Linux 系统中,为了方便我们使用 LVM 管理磁盘,LV 通常被命名为 /dev/vg_name/lv_name 的样子。 此外,前文中提及到 LVM 可以弹性变更 FileSystem 的容量,实际上是通过 “交换PE” 来进行扩容和缩小的操作,将原本逻辑卷LV中的物理块PE移出以缩小容量,将空闲的物理块PE移入现有逻辑卷LV以扩容。如下图: VG 内的 PE 会分给虚线部分的 LV ,如果未来这个 VG 要扩充的话,加上其他的 PV 即可。最重要的是如果 LV 要扩充的话,也可以通过加入 VG 内没有使用到的 PE 来扩充的。 Logical Extent, LE, 逻辑块 逻辑卷LV中可以分配的最小存储单元,在同一卷组VG中LE的大小和PE是相同的,并且一一相对。 LVM 工具 fdisk, gdisk 和 parted fdisk [student@node2 ~]## fdisk /dev/sda 欢迎使用 fdisk (util-linux 2.23.2)。 更改将停留在内存中,直到您决定将更改写入磁盘。 使用写入命令前请三思。 命令(输入 m 获取帮助): <==这里可以输入指令,可以按 m 来查看所有指令 命令(输入 m 获取帮助):m 命令操作 a toggle a bootable flag b edit bsd disklabel c toggle the dos compatibility flag d delete a partition #删除一个分区 g create a new empty GPT partition table G create an IRIX (SGI) partition table l list known partition types m print this menu n add a new partition #增加一个新分区 o create a new empty DOS partition table p print the partition table #打印分区表 q quit without saving changes #不储存直接离开 s create a new empty Sun disklabel t change a partition's system id u change display/entry units v verify the partition table w write table to disk and exit #写入分区表并离开 x extra functionality (experts only) 命令(输入 m 获取帮助): gdisk [student@node2 ~]# gdisk /dev/vda <==仔細看,不要加上數字喔! GPT fdisk (gdisk) version 0.8.6 Partition table scan: MBR: protective BSD: not present APM: not present GPT: present Found valid GPT with protective MBR; using GPT. <==找到了 GPT 的分割表! Command (? for help): <==这里可以输入指令,可以按 ? 来查看所有指令 Command (? for help): ? b back up GPT data to a file c change a partition's name d delete a partition #删除一个分区 i show detailed information on a partition l list known partition types n add a new partition #增加一个新分区 o create a new empty GUID partition table (GPT) p print the partition table #打印分区表 q quit without saving changes #不储存直接离开 r recovery and transformation options (experts only) s sort partitions t change a partition's type code v verify disk w write table to disk and exit #写入分区表并离开 x extra functionality (experts only) ? print this menu Command (? for help): parted [student@node2 ~]# parted /dev/sda GNU Parted 3.1 使用 /dev/sda Welcome to GNU Parted! Type 'help' to view a list of commands. (parted) help align-check TYPE N check partition N for TYPE(min|opt) alignment help [COMMAND] print general help, or help on COMMAND mklabel,mktable LABEL-TYPE create a new disklabel (partition table) mkpart PART-TYPE [FS-TYPE] START END make a partition name NUMBER NAME name partition NUMBER as NAME print [devices|free|list,all|NUMBER] display the partition table, available devices, free space, all found partitions, or a particular partition quit exit program rescue START END rescue a lost partition near START and END resizepart NUMBER END resize partition NUMBER rm NUMBER delete partition NUMBER select DEVICE choose the device to edit disk_set FLAG STATE change the FLAG on selected device disk_toggle [FLAG] toggle the state of FLAG on selected device set NUMBER FLAG STATE change the FLAG on partition NUMBER toggle [NUMBER [FLAG]] toggle the state of FLAG on partition NUMBER unit UNIT set the default unit to UNIT version display the version number and copyright information of GNU Parted (parted) PV 阶段 pvcreate :将实体partition 建立成为 PV ; pvscan :搜寻目前系统里面任何具有 PV 的磁盘; pvdisplay :显示出目前系统上面的 PV 状态; pvremove :将 PV 属性移除,让该partition 不具有PV 属性。 VG 阶段 vgcreate :就是主要建立 VG 的指令; vgscan :搜寻系统上面是否有 VG 存在; vgdisplay :显示目前系统上面的 VG 状态; vgextend :在 VG 内增加额外的 PV ; vgreduce :在 VG 内移除 PV ; vgchange :设定 VG 是否启动(active); vgremove :删除一个 VG 。 LV 阶段 lvcreate :建立 LV ; lvscan :查询系统上面的 LV ; lvdisplay :显示系统上面的 LV 状态; lvextend :在 LV 里面增加容量; lvreduce :在 LV 里面减少容量; lvremove :删除一个 LV ; lvresize :对 LV 进行容量大小的调整。 LVM 实操流程 在这里我们就用 RHel 8 RH134 的题目来演练一下如何调整逻辑卷大小以及创建逻辑卷。 十六、调整逻辑卷大小 1)预先创建 2GiB 的分区/dev/vdb1,并用于创建卷组 testvg 2)创建大小为 200MiB 的逻辑卷/dev/testvg/vo,格式化为 xfs 文件系统,并挂载在/mnt/vo 上 3)将逻辑卷/dev/testvg/vo 及其文件系统大小调整到 300MiB,确保文件系统内容保持不变。 [student@node2 ~]# fdisk /dev/vdb Welcome to fdisk (util-linux 2.32.1). Changes will remain in memory only, until you decide to write them. Be careful before using the write command. Device does not contain a recognized partition table. Created a new DOS disklabel with disk identifier 0xae75bf0a. Command (m for help): n Partition type p primary (0 primary, 0 extended, 4 free) e extended (container for logical partitions) Select (default p): Using default response p. Partition number (1-4, default 1): First sector (2048-10485759, default 2048): Last sector, +sectors or +size{K,M,G,T,P} (2048-10485759,default 10485759): +2G Created a new partition 1 of type 'Linux' and of size 2 GiB. Command (m for help): w The partition table has been altered. Calling ioctl() to re-read partition table. Syncing disks. [student@node2 ~]# pvcreate /dev/vdb1 [student@node2 ~]# vgcreate testvg /dev/vdb1 [student@node2 ~]# lvcreate -L 200M -n vo testvg [student@node2 ~]# mkfs.xfs /dev/testvg/vo [student@node2 ~]# mkdir /mnt/vo [student@node2 ~]# vim /etc/fstab /dev/testvg/vo /mnt/vo xfs defaults 0 0 [student@node2 ~]# mount -a [student@node2 ~]# df -hT /dev/testvg/vo # 查看文件系统的类型 和大小 Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/testvg-vo xfs 195M 12M 183M 6% /mnt/vo [student@node2 ~]# lvextend -L 300M /dev/testvg/vo [student@node2 ~]# lvs LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert vo testvg -wi-ao---- 300.00m # 扩展文件系统,ext 类型的文件系统用 resize2fs /dev/testvg/vo,后面接的是逻辑卷的路径。 [student@node2 ~]# xfs_growfs /mnt/vo # 后面接的是挂载点的路径 [student@node2 ~]# df -hT /dev/testvg/vo Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/testvg-vo xfs 295M 13M 283M 5% /mnt/vo 十八、创建逻辑卷 根据以下要求,创建新的逻辑卷: 1)逻辑卷的名字为 mylv,属于 myvg 卷组,大小为 50 个 pe 2)卷组 myvg 中的逻辑卷的 pe 大小应当为 16MiB 3)使用 vfat 文件系统将逻辑卷 mylv 格式化 4)此逻辑卷应当在系统启动时自动挂载到/mnt/mydata 目录下 [student@node2 ~]# fdisk /dev/vdb Welcome to fdisk (util-linux 2.32.1). Changes will remain in memory only, until you decide to write them. Be careful before using the write command. Command (m for help): n Partition type p primary (2 primary, 0 extended, 2 free) e extended (container for logical partitions) Select (default p): Using default response p. Partition number (3,4, default 3): First sector (5244928-10485759, default 5244928): Last sector, +sectors or +size{K,M,G,T,P} (5244928-10485759,default 10485759): +1G Created a new partition 3 of type 'Linux' and of size 1 GiB. Command (m for help): w The partition table has been altered. Syncing disks. [student@node2 ~]# pvcreate /dev/vdb3 [student@node2 ~]# vgcreate -s 16M myvg /dev/vdb3 [student@node2 ~]# lvcreate -l 50 -n mylv myvg [student@node2 ~]# mkfs.vfat /dev/myvg/mylv [student@node2 ~]# mkdir /mnt/mydata [student@node2 ~]# vim /etc/fstab /dev/myvg/mylv /mnt/mydata vfat defaults 0 0 [student@node2 ~]# mount -a [student@node2 ~]# df -h /mnt/mydata/ Filesystem Size Used Avail Use% Mounted on /dev/myvg/mylv 799M 4.0K 799M 1% /mnt/mydata

Interpreter Vs Compiler : Differences Between Interpreter and Compiler

In this article, you will learn the differences between interpreters and compilers. We generally write a computer program using a high-level language. A high-level language is one that is understandable by us, humans. This is called source code. However, a computer does not understand high-level language. It only understands the program written in 0’s and 1’s in binary, called the machine code. To convert source code into machine code, we use either a compiler or an interpreter. Both compilers and interpreters are used to convert a program written in a high-level language into machine code understood by computers. However, there are differences between how an interpreter and a compiler works. Interpreter Vs Compiler Interpreter Compiler Translates program one statement at a time. Scans the entire program and translates it as a whole into machine code. Interpreters usually take less amount of time to analyze the source code. However, the overall execution time is comparatively slower than compilers. Compilers usually take a large amount of time to analyze the source code. However, the overall execution time is comparatively faster than interpreters. No Object Code is generated, hence are memory efficient. Generates Object Code which further requires linking, hence requires more memory. Programming languages like JavaScript, Python, Ruby use interpreters. Programming languages like C, C++, Java use compilers. Working of Compiler and Interpreter

DNS Principle and Resolution Process

网络通讯大部分是基于TCP/IP的,而TCP/IP是基于IP地址的,所以计算机在网络上进行通讯时只能识别如“202.96.134.133”之类的IP地址,而不能认识域名。我们无法记住10个以上IP地址的网站,所以我们访问网站时,更多的是在浏览器地址栏中输入域名,就能看到所需要的页面,这是因为有一个叫“DNS服务器”的计算机自动把我们的域名“翻译”成了相应的IP地址,然后调出IP地址所对应的网页。 什么是DNS? DNS( Domain Name System)是“域名系统”的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,它用于TCP/IP网络,它所提供的服务是用来将主机名和域名转换为IP地址的工作。DNS就是这样的一位“翻译官”,它的基本工作原理可用下图来表示。 DNS域名称 域名系统作为一个层次结构和分布式数据库,包含各种类型的数据,包括主机名和域名。DNS数据库中的名称形成一个分层树状结构称为域命名空间。域名包含单个标签分隔点,例如:im.qq.com。 完全限定的域名 (FQDN) 唯一地标识在 DNS 分层树中的主机的位置,通过指定的路径中点分隔从根引用的主机的名称列表。 下图显示与主机称为 im 内 qq.com DNS 树的示例。 主机的 FQDN 是 im.qq.com。 DNS 域的名称层次结构 DNS域名称空间的组织方式 按其功能命名空间中用来描述 DNS 域名称的五个类别的介绍详见下表中,以及与每个名称类型的示例。 DNS 和 Internet 域 互联网域名系统由名称注册机构负责维护分配由组织和国家/地区的顶级域在 Internet 上进行管理。 这些域名按照国际标准 3166。 一些很多现有缩写,保留以供组织中,以及两个字母和三个字母的国家/地区使用的缩写使用下表所示。一些常见的DNS域名称如下图: 资源记录 DNS 数据库中包含的资源记录 (RR)。 每个 RR 标识数据库中的特定资源。我们在建立DNS服务器时,经常会用到SOA,NS,A之类的记录,在维护DNS服务器时,会用到MX,CNAME记录。 常见的RR见下图: DNS服务的工作过程 当 DNS 客户机需要查询程序中使用的名称时,它会查询本地DNS 服务器来解析该名称。客户机发送的每条查询消息都包括3条信息,以指定服务器应回答的问题。 指定的 DNS 域名,表示为完全合格的域名 (FQDN) 。 指定的查询类型,它可根据类型指定资源记录,或作为查询操作的专门类型。 DNS域名的指定类别。 对于DNS 服务器,它始终应指定为 Internet 类别。例如,指定的名称可以是计算机的完全合格的域名,如im.qq.com,并且指定的查询类型用于通过该名称搜索地址资源记录。 DNS 查询以各种不同的方式进行解析。客户机有时也可通过使用从以前查询获得的缓存信息就地应答查询。DNS 服务器可使用其自身的资源记录信息缓存来应答查询,也可代表请求客户机来查询或联系其他 DNS 服务器,以完全解析该名称,并随后将应答返回至客户机。这个过程称为递归。 另外,客户机自己也可尝试联系其他的 DNS 服务器来解析名称。如果客户机这么做,它会使用基于服务器应答的独立和附加的查询,该过程称作迭代,即DNS服务器之间的交互查询就是迭代查询。 DNS 查询的过程如下图所示。 在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。 如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/ip参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。 如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。 如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至13台根DNS,根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找qq.com域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机。 如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。 从客户端到本地DNS服务器是属于递归查询,而DNS服务器之间就是的交互查询就是迭代查询。 附录 本地DNS配置转发与未配置转发数据包分析 新建一DNS,具体怎么建我这里就不再描述了,见《在Win2003中安装bind【部署智能DNS】》 DNS服务器不设转发: 在192.168.145.228服务器上安装上wireshark软件,并打开它,设置数据包为UDP过滤,在192.168.145.12客户机上用nslookup命令查询一下www.sohu.com,马上可以看到本地DNS服务器直接查全球13台根域中的某几台,然后一步步解析,通过递代的方式,直到找到www.sohu.com对应的IP为220.181.118.87。 本地DNS服务器得到www.sohu.com的IP后,它把这个IP返回给192.168.145.12客户机,完成解析。 DNS服务器设置转发 因www.sohu.com域名在第一步的验证中使用过,有缓存,为了不受上步实验干扰,我们在客户机上192.168.145.12上nslookup www.baidu.com。从图上看,本地DNS把请求转发至192.168.133.10服务器,133.10服务器把得到的IP返回给本地DNS,然后本地DNS再把IP告诉DNS客户机,完成解析。

Dialect in Kansai, Japan

近畿方言(きんきほうげん)は、主に近畿地方(大阪府、京都府、兵庫県、和歌山県、奈良県、滋賀県、三重県)で用いられる日本語の方言の総称である。西日本方言に属する。上代から近世中期までの中央語である畿内語・近世上方語の系統を汲む方言で、現在も東京方言や首都圏方言に次ぐ認知度と影響力を持つ(後述)。 大阪弁変換 【関西弁】大阪・京都などでよく使う関西独特の言葉・例文18選 日常会話で使う関西弁 【あかん】(akan) 標準語の「ダメ」「よくない」などの意味で使われます。 使用例 「そんなことしたらあかんやん」(そんなことをしてはいけないよ)、「なんであかんの?」(どうしてダメなの?)、「この店あかんわ」(この店はよくないね) 【ええ】(ee) 標準語の「いい」「良い」などの意味で使われます。 使用例 「ええよ」(OKの返事) 使用例 「その服ええやん」(それはいい服だね。その服は良く似合うよ)、「この店ええやん」(ここはいい店だね) 【おもろい】(omoroi) 標準語では、「おもしろい」の意味です。 使用例 「この漫画おもろいなぁ」(この漫画はおもしろいね)、「おもろい人やなぁ」(ユニークな人だね) 【ほんま】(honma) 標準語の「本当」や強調するときの「本当に」「とても」の意味で使われます。 使用例 「ほんまほんま」(相槌や肯定) 使用例 「ほんまにおいしいわ」(本当においしいね。とてもおいしいね) 【ちゃう】(chau) 標準語では「違う」の意味で、会話では2回繰り返して使うことが多いです。 会話例 「これ?」(これのこと?)「それちゃう」(それじゃないよ) 会話例 「これでええ?」(これでいいの?)「ちゃうちゃう」(全く違うよ) 【めっちゃ】(meccya) 標準語の「とても」や「すごく」など、程度や大きさを表す意味で使われます。 使用例 「めっちゃおいしい」(とてもおいしいね)、「めっちゃ似合う」(とても似合うよ)、「めっちゃ大きい」(すごく大きいね) 【せや】(seya) 標準語の「そうだ」の意味で使われます。語尾に「~ねん」を付けて使うことが多いです。 会話例 「明日仕事?」(明日は仕事なの?)「せやねん」(そうなんだ) 【はよ】(hayo) 標準語の「早く」の意味で使われます。 使用例 「はよして」(早くしてほしいな)、「はよ行こ」(早く行こうよ)、「はよ帰ろ」(早く帰ろう) 【なんでやねん】(nandeyanen) 漫才のツッコミでよく使われる言葉として知られています。標準語では「どうしてなんだよ」「何を言ってるの」「そんなわけがない」というような意味ですが、もう少しやわらかいニュアンスで使われることが多いです。冗談を言われたときの返し言葉として使ってみてください。 会話例 「うちの犬、英語話すねん」(うちの犬は英語を話すのよ)「なんでやねん」(そんなわけないでしょ) 【かまへん】(kamahen) 標準語の「かまわない」が、かまわへん→かまへんと変化。「構わない」や「問題ない」のほか、OKの意味でも使われます。 会話例 「行けなくてごめん」「かまへん、かまへん」(そんなの気にしないで) 会話例 「遅れても大丈夫?」「かまへんでー」(大丈夫だよ-) お店で使う関西弁 【まいど】(maido) 「毎度お世話になります」や「毎度ありがとうございます」を省略して、「まいど」だけで「こんにちは」に代えて挨拶として使ったり、居酒屋などのお店では店員が「いらっしゃいませ」の意味でも使います。 【おおきに】(ookini) 標準語の「ありがとう」の意味です。若い人はあまり使わなくなってきましたが、京都の花街ではよく耳にします。「おおきに」のほか、「いつもありがとうございます」の意味で「まいどおおきに」が、居酒屋などのお店で今もよく使われます。 【なんぼ】(nanbo) 標準語の「いくら」(値段・量)の意味で使われます。 使用例 「これなんぼ?」(この値段はいくらですか?)、「なんぼですか?」(この量はどのくらいですか?) 【似おてる】(nioteru) 標準語では、「似合ってる」の意味です。 使用例 「よう似おてますね」(よく似合っていますね)「よう似おてはりますよ」(よくお似合いですよ) 【おばんざい】(obanzai) 一般家庭で作るお惣菜を意味します。気軽に食べられる家庭料理のイメージで、店名やメニュー名の一部に使われることもあります。 【炊いたん】(taitan) 「炊いたもの」ですが、関東の「煮物」を意味します。関西では、「煮る」を「炊く」と表現することもあり、お惣菜を指して京都をはじめとする関西地方で使われます。「大根の炊いたん」(大根の煮物)、「小芋の炊いたん」(小芋の煮物)など、居酒屋でメニュー名になっている場合もあります。 【酒のアテ】(sakenoate) 関東では、「酒のつまみ」や「酒の肴」と言います。酒を飲むときに添える一品のことで、それぞれの酒に合うちょっとした料理を指します。 【突き出し】(tsukidashi) 関東では、「お通し」と言います。居酒屋などで、注文にかかわらず最初に出てくる料理のことです。 そのほか、標準語と異なる単語や表現が多い関西弁ですが、ほかにも語尾の変化が大きな特徴といえます。 例 そうやねん(そうだよね)、あかんねん(ダメなんだ)、ちゃうねん(違うんだよ)・・・など、語尾の「ねん」「やねん」は、「~だよ」「~なんだよ」を意味します。 例 そうやん(そうでしょう)、あかんやん(ダメだよね)、ちゃうやん(違うよね)・・・など、語尾の「やん」は、「~でしょう」「~だよね」を意味します。 語尾に付く「~はる(しはる)」は敬語の表現で、大阪と京都で少し違います。大阪では「行きはる」(行かれる)「書きはる」(お書きになる)、京都では「行かはる」(行かれる)「書かはる」(お書きになる)になります。 また、「飴ちゃん」や「おかいさん(お粥さん)」など、食べ物などに「ちゃん」や「さん」を付けることも、関西のおもしろい特徴として知られています。

HTTP Status Codes

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含HTTP状态码的信息头(server header)用以响应浏览器的请求。 HTTP状态码的英文为HTTP Status Code。 下面是常见的HTTP状态码: 200 - 请求成功 301 - 资源(网页等)被永久转移到其它URL 404 - 请求的资源(网页等)不存在 500 - 内部服务器错误 HTTP状态码分类 HTTP状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型: HTTP状态码分类 分类 分类描述 1** 信息,服务器收到请求,需要请求者继续执行操作 2** 成功,操作被成功接收并处理 3** 重定向,需要进一步的操作以完成请求 4** 客户端错误,请求包含语法错误或无法完成请求 5** 服务器错误,服务器在处理请求的过程中发生了错误 HTTP状态码列表: HTTP状态码列表 状态码 状态码英文名称 中文描述 100 Continue 继续。客户端应继续其请求 101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议 200 OK 请求成功。一般用于GET与POST请求 201 Created 已创建。成功请求并创建了新的资源 202 Accepted 已接受。已经接受请求,但未处理完成 203 Non-Authoritative Information 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本 204 No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 205 Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 206 Partial Content 部分内容。服务器成功处理了部分GET请求 300 Multiple Choices 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择 301 Moved Permanently 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替 302 Found 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI 303 See Other 查看其它地址。与301类似。使用GET和POST请求查看 304 Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 305 Use Proxy 使用代理。所请求的资源必须通过代理访问 306 Unused 已经被废弃的HTTP状态码 307 Temporary Redirect 临时重定向。与302类似。使用GET请求重定向 400 Bad Request 客户端请求的语法错误,服务器无法理解 401 Unauthorized 请求要求用户的身份认证 402 Payment Required 保留,将来使用 403 Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求 404 Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面 405 Method Not Allowed 客户端请求中的方法被禁止 406 Not Acceptable 服务器无法根据客户端请求的内容特性完成请求 407 Proxy Authentication Required 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权 408 Request Time-out 服务器等待客户端发送的请求时间过长,超时 409 Conflict 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突 410 Gone 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置 411 Length Required 服务器无法处理客户端发送的不带Content-Length的请求信息 412 Precondition Failed 客户端请求信息的先决条件错误 413 Request Entity Too Large 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息 414 Request-URI Too Large 请求的URI过长(URI通常为网址),服务器无法处理 415 Unsupported Media Type 服务器无法处理请求附带的媒体格式 416 Requested range not satisfiable 客户端请求的范围无效 417 Expectation Failed 服务器无法满足Expect的请求头信息 500 Internal Server Error 服务器内部错误,无法完成请求 501 Not Implemented 服务器不支持请求的功能,无法完成请求 502 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应 503 Service Unavailable 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中 504 Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求 505 HTTP Version not supported 服务器不支持请求的HTTP协议的版本,无法完成处理

The Tutorial for FPGA Player - Episode 5: Verilog - Part Two

本期,我们将继续讲解之前没有讲完的Verilog代码。上一期的教程已经介绍了Verilog最核心的一些操作,本期则将介绍一些有用的其它操作,他们最终也可以用核心的操作代码表示出来,但是通常而言编写起来更为简便。本期同样会介绍仿真工具的使用,在开发过程中非常有用。不过在开始新的内容之前,先来讲讲上一次留的作业。 上期练习 上期留了一个作业,就是实现第五期里面讲过的状态机。这里首先复述一下第五期的状态机:假设一个贩卖机,只卖矿泉水,价格定为2元,只接受1元硬币或者5角硬币,多不找零,设计一个状态机来描述它的行为。这个机器的话,它有两个输入,投入5角或者投入1元;以及一个输出,是否已经付了足够多的钱。 如同之前一样,假设表示投入5角硬币的信号叫a,表示投入1元硬币的信号叫b,输出是否已经付够钱的信号叫c。同时定义这个系统有S0-S4一共5个状态,分别表示当时已经投入了0、0.5、1、1.5和2元。 要用Verilog来实现这个状态,第一步肯定是先写一个整体的模块框架,再往里面加入东西。于是参考上期的声明module的方法,先写下如下的代码: module vending( input clk, input rst, input a, input b, output c); endmodule 上面的代码定义了一个叫vending的模块,有四个输入,clk、rst、a和b,一个输出c,主体没有内容。clk提供时钟,rst提供复位。首先来考虑输出吧。状态机的输出是由当前状态决定的,所以需要有一个变量(触发器)来保存当前的状态,比如叫做state: reg [2:0] state; 有了state之后就可以描述输出的逻辑了。一种方法是直接用第五期的逻辑表达式: assign c = state[2] && !state[1] && !state[0]; 另外一种方法则是使用always语句块(如上期所说,如果需要在always语句块中赋值,则被赋值的信号需要声明为reg类型,如这里需要把output c修改成output reg c) always @(*) begin if (state == 3’d4) c = 1‘b1; // 只有在S4输出1 else c = 1’b0; end 两者虽然写法不同,但是最终产生的电路是等效的,而且很有可能是相同的。接下来要处理的就是如何根据输入转换状态了。通常的做法是申明另外一个变量,用来保存即将进入的状态,随后设计两个always语句块,一个负责产生下一个状态,另外一个负责让状态机进入下一个状态。其中产生状态的语句块,应该是异步的,也就是用组合逻辑实现,这样等时钟到来的时候,下一个状态的值就已经是和输入对应的了;而进入下一个状态的语句块则应该是和时钟同步的,使用时序逻辑来实现。 reg [2:0] next_state; always @(*) begin next_state = 3’d0; if (state == 3’d0) begin if ((a == 0)&&(b == 0)) next_state = 3’d0; else if ((a == 0)&&(b == 1)) next_state = 3’d2; else if ((a == 1)&&(b == 0)) next_state = 3’d1; end else if (state == 3’d1) begin if ((a == 0)&&(b == 0)) next_state = 3’d1; else if ((a == 0)&&(b == 1)) next_state = 3’d3; else if ((a == 1)&&(b == 0)) next_state = 3’d2; end else if (state == 3’d2) begin if ((a == 0)&&(b == 0)) next_state = 3’d2; else if ((a == 0)&&(b == 1)) next_state = 3’d4; else if ((a == 1)&&(b == 0)) next_state = 3’d3; end else if (state == 3’d3) begin if ((a == 0)&&(b == 0)) next_state = 3’d3; else if ((a == 0)&&(b == 1)) next_state = 3’d4; else if ((a == 1)&&(b == 0)) next_state = 3’d4; end else if (state == 3’d4) begin next_state = 3’d4; end end always @(posedge clk, negedge rst) begin if (!rst) state <= 3'd0; else state <= next_state; end 注意到上面的代码中,产生下一状态的部分,其实就是对第五期的状态表的直接描述,而没有经过任何的化简(第五期内我们首先化简了逻辑)。这也就是用always语句块描述组合逻辑的一个优点,编写的代码可以更接近于要实现的功能一些,而不必须要是具体的门电路逻辑。那么至此,这个作业就写完了。需要指出的是,这只是一种可能的实现,不同的人写状态机有不同的风格,关于不同风格的写法和优劣各位可以自行搜索资料学习。 Verilog中的其它语句 如之前所说,上期介绍了Verilog中的核心语句,不过Verilog还有一些其它的语句可以方便开发。比如前面的状态机中的if else语句,其实可以用case语句来代替: case (state) 3'd0: if xxx yyy 3'd1: if xxx yyy endcase 概念上和C语言的switch语句类似,语法上也比较接近,不过不需要break,同一个条件下多条语句需要用begin end。整体格式如下: case (表达式) 表达式: 语句 表达式, 表达式: 语句 表达式: begin 语句 语句 end default: 语句 endcase 从上面的格式可以看到,同一个分支可以匹配多个条件,也可以有默认情况。使用case语句不见得会让代码更短,但是使用得当的话可以提高代码的可读性。 那么相比传统的编程语言,还有什么语句缺席了呢?循环语句。仔细考虑一下,Verilog真的需要循环语句吗?硬件中的循环是怎么实现的?循环需要如同之前状态机的结构,一个时钟信号输入,让状态触发器的数值变化,这样来实现类似循环的结构。听起来不应该是用一条语句来实现的东西吧?所以Verilog中就不应该需要循环语句了吧? 不过Verilog还是提供了循环语句。只是这种循环的功能很有限,如同之前的case语句一样,只是一种用于提高代码可读性的做法。Verilog中的循环也只是一种语义上的循环,并非真实的硬件循环。举一个例子,你有4对32位整数,希望把它们加起来: reg [31:0] i1a, i2a, i3a, i4a; reg [31:0] i1b, i2b, i3b, i4b; wire [31:0] i1c, i2c, i3c, i4c; assign i1c = i1a + i1b; assign i2c = i2a + i2b; assign i3c = i3a + i3b; assign i4c = i4a + i4b; 这样就能产生四个加法器,同时计算四组加法。但是这样,要更多组加法就要写更多行代码,显然循环会是一种比较好的简便写法。 reg [31:0] ia[0:3]; reg [31:0] ib[0:3]; reg [31:0] ic[0:3]; always @(*) begin integer i; for (i = 0; i < 4; i = i + 1) begin ic[i] = ia[i] + ib[i]; end end 虽然在这个例子中实际的行数相比直接写更多,但是某些情况下(如要操作的数更多,单个操作更为复杂等等),可能是有助于提高可读性的。如同之前所说,这个并不会真正产生循环的硬件,只是语义上的循环。最终产生的硬件是和上面分开写的写法等效且可能是相同的,四个独立的加法器共同工作,而非一个加法器循环处理四组数字。 最后一个不算是语句,只是一个运算符,就是三目判断运算符? :。使用方法和C语言中一致,用在赋值当中,例如如下的用法 assign a = b ? c : d; 等同于 always@(*) begin if (b) a = c; else a = d; end 同样,也是一种很好用的简便写法。 Verilog中的双向信号 双向信号也是代码中需要使用的一个特性。通常来说,不建议在Verilog模块内部使用双向信号,模块间互联应该尽可能使用独立的输入和输出信号。但是,在和外界沟通的时候,如同数据总线一类的信号必须要是双向的。好在,Verilog支持了双向信号。 Verilog中,输入信号的关键词是input,输出信号的关键词是output,双向信号的关键词就很简单的是inout。通常处理双向信号的方法如下: inout signal; // 双向信号 wire direction; // 信号方向 wire signal_input; // 双向信号的输入 wire signal_output; // 双向信号的输出 assign signal = direction ? signal_output : z; // 当方向为输出时输出信号,否则设置为z assign signal_input = direction ? x : signal; // 当方向为输入时输入信号,否则设置为x 需要注意的是,为了控制双向信号,需要一个额外的信号,用来指明信号的方向,这里的情况是定义为了 1 为输出,0为输入,当然这个可以自己修改。另外里面出现了两个以前没有出现过的信号状态,一个是z,一个是x。z表示高阻,或者也可以理解为不输出;x表示无效。当输入输出有多位的时候也应该使用多位的x和z,比如8‘bz表示8位高阻。需要读写的时候,所有的读取从signal_input读取,所有的写入写入进signal_output,随后设置direction为需要的方向。 仿真 很多玩单片机的朋友可能不喜欢仿真,觉得程序就是要烧写进板子运行才好玩。也确实是这样,但是就我个人的经验而言,Verilog中仿真重要的多。一来是初学者写代码不熟练时容易出现错误,而这类Bug可能并不容易发现;二来是Verilog的程序综合和实现(类似于软件中的编译吧)速度相比于软件而言慢得多,即使是很简单的代码通常也需要好几分钟才能完成,复杂的则需要数十分钟甚至数小时。一般以前调试小软件中那种修改一下编译测试一下是否修复的方法在这就并不适用了。在综合前最好先确认代码是能用的,而确认的方法就是通过仿真。本期就以上面的状态机为例,演示一下如何进行仿真。因为这个和所使用的FPGA有关,这里依然是分成Intel和Xilinx两部分演示。 Intel 大体的流程和之前第三期时的流程是一致的,因为最终目的也都是把代码综合、实现之后下载到开发板当中运行。创建工程的步骤这里就不一一赘述,具体的可以参考第三期中的过程,这里只是讲几个重点。 首先在选择设备的界面,如果最终需要把设计烧录进FPGA测试,则必须要选择对应的型号,否则可以随意选择。DE10-Lite开发板上的器件型号为10M50DAF484C7G。 在新的工程中建立两个文件,一个是状态机的源代码,另外一个用于仿真的测试代码(test bench)。首先来建立状态机的源代码。直接按照上面的步骤输入代码,但是需要注意的是模块名需要和文件名一致。我这里文件名保存为lesson_7.v,模块名称也就需要相对应的修改成lesson_7。代码应该如下: 随后是仿真文件,或者叫testbench。Testbench也是一段Verilog代码,不过并不会被综合成硬件,只会在仿真器中运行,作用是测试需要真实下载到板子里的程序。本次要用的Testbench如下: module testbench(); reg clk, rst, a, b; wire c; wire [2:0] state; lesson_7 DUT(clk, rst, a, b, state, C); initial begin // 复位 a = 0; b = 0; rst = 0; #(5) clk = 1; #(5) clk = 0; // 什么都不做 a = 0; b = 0; rst = 1; #(5) clk = 1; #(5) clk = 0; // 投入5角 a = 1; b = 0; rst = 1; #(5) clk = 1; #(5) clk = 0; // 投入5角 a = 1; b = 0; rst = 1; #(5) clk = 1; #(5) clk = 0; // 投入5角 a = 1; b = 0; rst = 1; #(5) clk = 1; #(5) clk = 0; // 投入5角 a = 1; b = 0; rst = 1; #(5) clk = 1; #(5) clk = 0; // 检查是否已经被解锁 if (c != 1) begin $display("测试1失败 Test 1 Failed"); $finish; end // 复位 a = 0; b = 0; rst = 0; #(5) clk = 1; #(5) clk = 0; // 检查是否已经被复位 if (c != 0) begin $display("测试2失败 Test 2 Failed"); $finish; end // 投入1元 a = 0; b = 1; rst = 1; #(5) clk = 1; #(5) clk = 0; // 投入1元 a = 0; b = 1; rst = 1; #(5) clk = 1; #(5) clk = 0; // 检查是否已经被解锁 if (c != 1) begin $display("测试3失败 Test 3 Failed"); $finish; end $display("测试成功 Test Success"); $finish; end endmodule 大体来说就是,先实例化要测试的模块。随后依次提供输入。提供输入后再使用if语句来测试输出是否符合预期,如果不符则提示失败并结束仿真。里面的#(5)表示延迟5ns。每条提示中都加入了中英文两种提示,原因是Intel这边用的ModelSim仿真工具并不能显示中文,只能加上英文输出;而Xilinx这边用的ISim没有这个问题。 点击菜单中的Tools-Run Simulation Tool-RTL Simulation运行仿真: 如果你遇到了没有指定仿真工具的错误,则需要进入设置(Tools-Options)里面手动指定,如下图: 仿真语言选择Verilog HDL。ModelSim启动后,选择菜单中的Compile – Compile… 选择需要仿真的文件,这里就是lesson_7.v和testbench.v两个文件: 编译完成后应该可以在work里看见这两个模块: 要开始仿真,在testbench上右键,选择Simulate。进入仿真界面后,可以选择需要的信号观察波形输出,比如这里选择所有的信号: 最后点击上方的Run开始仿真。因为目前的仿真还很简单,很快就会完成,会出现是否结束的提示。如果需要观察波形则点否,如果点是将会直接关闭仿真软件。从下面的输出可以看到测试成功完成了,状态机表现和预期一致。 那么至此我们已经知道这个状态机是可以用的了,可以烧写进板子测试了。不过别忘了,在烧写进板子之前,需要分配引脚定义。这里比如让LEDR0为c输出,SW1和SW0分别为a和b输入,而KEY0作为rst,KEY1作为clk,最后同时在LED3-LED1上输出当前的状态。 根据原理图,不难找到这些IO对应的引脚: SW0 – C10 SW1 – C11 LEDR0 – A8 LEDR1 – A9 LEDR2 – A10 LEDR3 – B10 KEY0 – B8 KEY1 – A7 这些硬件的电压都为3.3V(原理图里指出)。 将对应的引脚信息输入进Pin Planner,把电压都设定到3.3V,就完成了引脚分配。 完成后重新生成编程文件(Generate programming files),运行烧写工具(Tools-Programmer)烧写进板子即可。 Xilinx Xilinx这边的过程也是类似的,首先是建立工程,选择目标设备,随后建立文件编写代码。值得注意的是,Xilinx这边在建立仿真文件的时候需要选择Verilog Test Fixure: 随后便会提示这个testbench对应的是哪个模块,这里唯一的模块就是lesson_7,直接继续。可以注意到ISE自动生成了testbench的框架,我们只需要编写initial begin end内的测试输入输出即可,十分方便。在补充完成测试主体后应该如图: 在左边的任务窗格选择Simulation,选中Testbench,在下方的ISim Simulator中选择Simulate Behavioral Model(仿真行为模型) 如果没有出现错误,Isim会自动打开并且运行测试,应该可以直接在下方看到测试成功的提示,同时在上方也可以直接看到所有的测试波形。 现在我们已经简单测试了这个状态机是可以工作的,剩下的步骤也就是编辑UCF文件生成编程文件下载了。UCF文件同样是按照实际硬件的定义来: NET a LOC="AJ6"; NET a IOSTANDARD="LVCMOS33"; NET b LOC="AK7"; NET b IOSTANDARD="LVCMOS33"; NET clk LOC="U8"; NET clk IOSTANDARD="LVCMOS33"; NET rst LOC="V8"; NET rst IOSTANDARD="LVCMOS33"; NET c LOC="H18"; NET c IOSTANDARD="LVCMOS25"; NET state<0> LOC="L18"; NET state<0> IOSTANDARD="LVCMOS25"; NET state<1> LOC="G15"; NET state<1> IOSTANDARD="LVCMOS25"; NET state<2> LOC="AD26"; NET state<2> IOSTANDARD="LVCMOS25"; 保存后生成编程文件,使用Tools-iMPACT下载至板子即可。 总结 本次的教程到这里也就差不多告一段落了。本期我们继续了上期没有完成的Verilog教学,讲解了一个简单的作业,并且在Intel和Xilinx两家的环境里对程序进行了仿真,这些将会是之后经常需要使用的功能。这里也给大家留个作业:想要把这个状态机的代码修改下,让它只需要投入1.5元就可以购买,需要怎么做呢?提示:根据你的实现,可能最少只需要修改1行代码即可。而下期开始我们将正式开始谈一谈CPU这个概念,也就是整个游戏机的核心。我们下次见。

The Tutorial for FPGA Player - Episode 5: Verilog - Part One

首先,恭喜大家,一直跟到了这个教程的第五期。如之前所说,从本期开始,我们将正式开始使用Verilog给FPGA写代码。而要写的东西,这是第2-4期里面介绍过的组合逻辑,时序逻辑和状态机。而Verilog也将会是在这之后用于描述各种硬件的语言。 其实读Verilog代码本身并不复杂,有软件编程经验的人就不难理解代码的含义,毕竟其语法和C语言很接近;但是写Verilog代码,就是另外一回事了。Verilog里面有很多和C接近的概念和语句,比如赋值,比如if-else,比如for循环,等等。但是Verilog的目标结果,逻辑门,却又和C的目标结果,程序,太不一样了。他们确实某种程度上是接近的语言,但是语言里有接近的东西并不表示他们就能实现接近的功能。或者反过来,想要实现同样的功能,未必会使用接近的方法。所以说,学习Verilog最好的捷径就是不要走捷径,从数字电路开始学习,而并非一开始就学习代码,以至于被代码所迷惑。 Verilog程序模块 Verilog程序如同其它的编程语言程序一样,有它特定的源文件格式。Verilog的源代码后缀名为.v,每一个Verilog文件都是一个Verilog模块,各位可以类比为编程语言中的函数。基本格式如下: module 模块名(模块输入输出信号); 模块内容 endmodule 其中这个模块名通常需要和对应的文件名相同,同一个文件只定义一个模块,比如adder.v里就应该只定义一个叫adder的模块。这个要求和Java对类的要求很相似。 输入输出信号则是接近一个函数的返回值和参数,只不过在Verilog里并不把参数和返回值放到不同的地方定义,而是都写在一起。所有的参数或者返回值,最终都只是导线而已。而导线根据驱动信号的方向,可以有输入和输出区别。至于需要多少个输入多少个输出,那就取决于具体的程序了。 模块内容则是模块内部的逻辑,也许有代码块(always),也许只是一些简单的接线(assign)。不过别忘了,一切最后都会回归到硬件。 最后说说模块的实例化,或者说调用。如前面所说,模块类似于软件编程语言里面的函数,它也确实有对应的函数名,参数,返回值等等类似的概念。那么要使用这个“函数”,自然也就需要一种调用的方法。只不过,Verilog里的调用,并不是像编程语言一样在特定位置执行特定代码(毕竟本身就没有“执行代码”这种操作),而是新复制一份这个模块所表示的硬件,然后连接对应的导线。这一点在参数的定义时其实也就有所体现,定义的输入输出并非是变量,而是导线, 也就是说,传递的内容并不是数值,而是连接。一旦连接被确定,传输就是时时进行的(因为线被连接上了)。这个和编程语言里的调用非常不一样,所以务必进行区分。具体的例子我们之后就会提到。 Verilog模块参数 模块参数的格式其实很固定,就是 方向 类型 宽度 名称。方向可以是输入(input)或者输出(output),注意方向是对于模块而言的,从外界进入到模块是input,而从模块输出到外部则是output。类型的话在Verilog里基础类型只有两种,一种是wire一种是reg。需要说明的是,不要望文生义,Verilog里的wire是指导线没错,但是reg并不说明这是一个寄存器。具体的区别会在后面单独解释。如果没有标明类型,通常会默认为wire,但是这个默认值是可以修改的。宽度表示这个信号的位宽。在其它的各种编程语言中,数据类型通常都会指定这个变量的位宽,比如说C语言中char就是8位,int通常是32位,double通常是64位,等等。而Verilog里类型并不能顺便指定这个变量的位宽,位宽需要单独指定。如果没有指定,通常而言就默认为1位。这其实是个Verilog里非常常见的错误,多位的信号忘了指定宽度,默认成了1位。名称也就是名称,和其它语言中的变量名称是一样的概念。接下来举几个例子。 input wire a, // 定义一个1位的wire型输入信号,名为a output wire b, // 定义一个1位的wire型输出信号,名为b output reg c, // 定义一个1位的reg型输出信号,名为c input wire [3:0] d, // 定义一个4位(3至0)的wire型输入信号,名为d output wire [9:0] e, // 定义一个10位(9至0)的wire型输出信号,名为e output reg [0:8] f, // 定义一个9位(0至8)的reg型输出信号,名为f 以上就是一些例子,具体的用途当然还是需要结合整个模块来理解。 Verilog内部信号定义 内部信号定义就类似于软件编程语言中的变量。只不过这里的变量,不一定具有存储数值的能力,有可能只是作为导线起到接线的作用。内部信号定义的语法格式几乎是和模块参数的是一样的,只是去掉了 方向 定义。毕竟不需要和外界通信,自然也就没有输入输出的说法。以下是几个例子。 wire a; // 定义一个1位的wire型信号,名为a reg [31:0] b; // 定义一个32位的reg型信号,名为b Verilog的表达式和运算符 Verilog作为一个类C语言,其表达式、运算符和C语言都非常接近。但是需要指出的是,在Verilog里进行数字运算的时候,综合器会根据操作产生需要的运算器(加法器、乘法器等等)有些时候默认的行为可能并不合适,可能需要特别注意。以下是几个常见的运算符,完整的这里就不列出了。 运算符 运算 --------------------------- + 加法 - 2's compliment 减法 << 左移 ~ 按位取反 & 按位与 ^ 按位异或 ? : 三目选择 Verilog数值表示 Verilog的数值表示相比其它的编程语言要复杂一些。原因很简单,前面在信号定义里提到了,一个信号的宽度可以是任意数值,而不是根据类型固定的几个数值。为此,数值的表示方式中也得能够体现这个特点。Verilog中数值的格式为:宽度 ‘ 进制 数字。宽度就是位宽,一个数字,和之前信号定义里的位宽对应。进制可以为二进制(b)十进制(d)和十六进制(h),最后的数字就是要表示的数字。比如说要表示一个8位的十进制数255,就写作 8’d255 。同样的数字用二进制表示为 8’b11111111,十六进制表示为 8’hFF。这三种写法是完全等效的,只是看具体应用时哪种方便了。当然,宽度需要大于足够表示这个数字的最低宽度,比如还是255,255最少需要8位来表示,所以并不能写作4’d255,但是可以写20’d255,因为20位足够大了。以下是一些例子。 5'd16 // 5位十进制数16 7'h23 // 7位十六进制数0x23 2'b10 // 2位二进制数0b10 Verilog程序语句 - assign Verilog的程序语句基本可以分为两大类,一类是固定赋值语句(assign),另外一类是代码块(always)中使用的语句。之前说的if-else,for一类的语句都是配合语句块使用的。 先来讲assign。assign的作用很简单,就是接线。比如说有两条线a和b,要把他们连接起来,通常来说语句如下: assign a = b; 或者 assign b = a; 这两种写法有什么区别吗?如果只是接线,那么接上就是接上了,a和b接起来与b和a接起来真的有区别吗?答案是,有。虽然只是导线,但是导线最终还是会连接到输入输出端口或者逻辑门上,这样导线也就有了驱动方和被驱动方的区别。比如a如果连接到了一个输入接口上(从外部进入到FPGA),而b连接到了一个输出接口上(从FPGA输出到外部),这样合理的写法就是b=a,信号会从外部进入FPGA连接到a上,随后从a连接到b,再输出到FPGA外部。当编写代码的时候,应该清楚,虽然这是连线,但是右边永远应该是驱动方,左边应该是被驱动方,就像编程语言里面数据从右向左传输一样。 值得注意的是,assign能够做的不单单只是简单的连线。assign已经足以实现很多组合逻辑了。 举个例子,我们用之前在第三期里面见过的原理图和代码。来自第三期的一个半加器的原理图: 提示一下,这个电路的作用是,输入两个1位的二进制数,计算它们的和,输出2位的结果。我们通过观察输入数字和输出数字的关系的方法,发现这个电路其实只需要两个逻辑门就可以实现需要的功能,于是也就画出了这样的电路图。其中U1和U3就是两个逻辑门元件。同时我们也讲了这个电路可以很简单的用FPGA来实现:把两个按键的输入和两个LED的输出都连接上FPGA(通常开发板上都有),然后在FPGA内完成所需的逻辑。我们现在重新再来看一次这个代码。 module lesson3( input wire a, input wire b, output wire c, output wire d); assign c = a & b; assign d = a ^ b; endmodule 以上就是当时给出的代码。现在已经可以看出来了,lesson3是模块名,括号里的定义都是对于输入输出信号的定义,而模块主体是两句assign语句。 首先先来看参数部分。参数部分定义了四个信号,分别叫a b c d,两个输入两个输出,都没有定义宽度,所以默认为1位,类型为wire。这个代码就是实现上面的那个电路,而电路中有两个按键,是从外部输入进模块的,所以定义了两个输入信号。同理为了输出两个LED需要的信号,定义了两个输出信号。 再来看模块主体。模块主体只有两条assign,内容也非常简明,一个是把a和b做与运算后连接上c,另外一个是把a和b做异或运算后连接上d。这也就是对之前电路图的表示了。 注意到这里出现了一个等号,是不是说明这个操作就类似于软件编程语言中的赋值呢?并不是。赋值所表示的是,计算出右边的结果,复制保存到左边的变量当中。而Verilog的assign,如之前所说,只是连接的作用。比如assign c = a & b;就是表示,产生一个能计算a & b的电路(一个与门),并把结果和c连接起来。从效果来说的话,和赋值的区别就是,赋值是一瞬间发生的事情,获取a和b的值,计算a&b,存入c。赋值完成后,c和a、b就没有别的关系了,即使a和b的值在后面发生了变化,c也会保留先前的值。而assign这里,c只是一条导线,c没有记忆,c并不能保留a & b的值。如果a、b发生变化,c也会随即发生变化。 为此,作为总结的话,assign语句可以用于描述组合逻辑,assign的左值就是组合逻辑的输出,而右侧的表达式则是组合逻辑的逻辑表达式。 Verilog程序语句 - always 讲完了上面的assign,我们已经可以用Verilog来描述组合逻辑了。但是还不能描述时序逻辑。时序逻辑需要使用一种称为always语句块的东西。这里选取第4期的第一个例子,按下按键让灯改变状态的例子: 当时也给出了对应的Verilog代码: module light( input key, output reg light); always@(posedge key) begin light <= ~light; end endmodule 还是一样,一个模块,模块名称为light,有两个信号,一个是输入,叫做key,没有指定类型(默认为wire);另外一个是输出,叫做light,类型为reg。显然key就是按键输入,而light就是灯的输出。注意到这里light被定义成了reg类型,这里确实表示灯应该连接到一个寄存器(触发器)上。 模块主体中只有一个always语句块,always语句块的格式如下: always@(触发条件) begin 语句1 语句2 … end always语句块的含义就是,当触发条件满足的时候,执行语句。begin和end的作用就类似于C语言中的{ },只是把多个语句并在一起而已。 这个例子中的触发条件只有一个,就是posedge key,表示key输入信号的上升沿(posedge)触发。如果需要下降沿则是negedge。而语句只有一条,light <= ~light。这个确实是一个赋值语句,表示计算右边的值,存入左边的寄存器。当然实际上发生的事情就是,产生一个能够计算右边结果的电路(非门),接入保存左边信号(reg light)的触发器的输入端,并且把always的触发条件接入触发器的时钟输入。这样,当触发条件满足的时候,其实也就是给触发器产生了时钟,触发器的输入端,也就是赋值语句右边的结果,会被存入寄存器。刚刚的解释请对照着原理图再理解一次。以上就是“触发条件满足 执行语句”这个听起来很“软件”的操作的硬件实现。 然而上面的这个解释,其实就是暗示了一个限制:因为时钟永远是几乎同时到达各个触发器,所以各个赋值只能是同时发生的。如果你写以下的代码: a <= b; c <= a; 会发生什么呢?如果是软件编程语言中的赋值,不难看出b的值会被存入c。而这里,因为赋值是同时发生的,所以a原先的值会被存入c,而b的值会被存入a。所以这个赋值操作(<=)的名字也就是非阻塞赋值,表示上一条赋值语句的执行并不会阻塞下一条赋值语句的执行,也就是说所有赋值同时发生。这也就是符合硬件实际情况的设计。 以上可以看出,always语句块可以用来实现时序逻辑的触发器部分。不过,always语句块除了可以实现时序逻辑,还能实现组合逻辑。组合逻辑不需要等待任何时钟就会发生,或者说一旦输入变化,输出就会发生变化。为此,Verilog中,用always语句块来实现组合逻辑的写法就是 always @(*) begin 语句1 语句2 … end 星号(*)就表示了任何输入发生变化都会触发,也就是像组合逻辑的表现一样。如果用always来重写前面的那个组合逻辑的例子,就会是这样: always @(*) begin c = a & b; d = a ^ b; end 注意到这里的赋值用的不再是之前的 <= 了,而是变成了 = 。这个赋值在Verilog里称为阻塞式赋值。也就是和传统编程语言一样的赋值方式,前面的先执行,后面的晚执行。所以你可以写出类似这样的代码: always @(*) begin c = 1'b0; if ((a == 1'b1) || (b == 1'b0)) c = 1'b1; end 这段代码完全可以按照传统编程语言的思路来理解,首先将c赋值为0,随后,如果a为1,或者b为0,那么就将c赋值为1,否则就保持不变。但是事情真的是这样的吗?并不是。不如来考虑下,如果代码真的是这么执行的,会发生什么事情。当模块中有任何信号发生变化的时候,c都会先赋值为0,随后按照ab的输入判断是不是要赋值为1。这就是这个代码所表示的意思。那么,假设现在a为1,b为0,c应该结果是1吧?如果b从0变成了1,会发生什么呢?c还是会先变成0,然后发现条件成立,再变成1。 对吗?并不对。c会保持为1,条件仍然成立,c会一直保持为1。阻塞式赋值只是Verilog中的一个语法糖。真实的硬件并不能够实现赋值的先后顺序,设计一个阻塞式赋值的语法只是为了方便描述逻辑。如刚刚那个always块,其实就等效于如下的assign语句: assign c = a | ~b; 对应的硬件无非就是一个非门加上一个或门而已。并没有什么特殊的“赋值电路”,这个电路就可以实现,在任何输入发生变化的时候产生相应的输出。而至于Verilog中关于“阻塞式赋值”的设定,只是为了方便描述逻辑而存在的。有了阻塞式赋值的语法,我们在写代码的时候就可以在一个always语句块多次对同一个变量赋值,只有最后一次结果才会被保留。通常的做法就是先给信号设定一个初始值,随后在使用不同的语句按照情况赋新的值。 做一个简单的总结的话,always语句有两种写法。第一种是用于描述时序逻辑的always语句,形式为always@(posedge 时钟信号),代码块中只使用非阻塞式赋值(<=)。而第二种,则是用于描述组合逻辑的always语句,形式为always@(*),代码块中只使用阻塞式赋值(=)。前面讲了那么多原理上的东西,是为了让大家明白Verilog中代码和实际硬件的对应关系,这样也就能帮助大家理解为什么在Verilog中不能像C一样写代码。 练习 前面的这些就已经介绍完了Verilog中最重要的一些部分。不难发现,Verilog缺少一种真正的能让代码按顺序执行的方式(时序逻辑中所有代码同时执行;组合逻辑中所有代码可以认为是时刻在执行)。这种功能并不是Verilog故意缺失,而是因为这种代码并没有真实的硬件对应。但是这种功能经常又是必要的,怎么办呢?解决方法就是使用我们在第五期里面讲过的状态机。 这里呢,就给大家留一个作业,参考今天给出的两个时序逻辑和组合逻辑的例子,把他们结合起来,实现在第五期里面讲过的状态机。下一期将会公布答案,并且复习其在FPGA上的实现流程。下期同样也会介绍仿真软件的使用。那么我们下次见。

The Tutorial for FPGA Player - Episode 4: State Machine

大家好,欢迎回到FPGA教程的第四期。说是FPGA教程,结果一直在和大家讲数字电路的事情,但是毕竟数字电路是一切的基础,如果不理解数字电路,也许一开始玩FPGA不会有太大问题,但是越到后面就会觉得越玄。所以拥有良好的基础还是相当必要的。不过好消息是,本期就是数字电路基础的最后一期了。下一期开始就可以开始写Verilog玩FPGA了。 什么是状态机 其实各位可能之前已经听说过了状态机这个名词。状态机是个软硬件中都非常常用的设计。其实更像是一种设计模板,把需要实现的功能,划分成状态,然后套进状态机的模板中,最后实现这个状态机。那为什么要这么麻烦套成状态机再实现状态机,而不是直接实现这个电路呢?原因其实也很简单,一旦逻辑复杂到一定程度,“直接实现”会变得很难管理,而最后为了提高可维护性通常还是得引入状态机的设计。这样还不如一开始就做状态机呢。 状态机,顾名思义,必然是和 状态 相关的。状态机的主要思想就是,把一个机器的运行情况人为划分成很多不同的状态,而机器的输出,根据当前所处的状态来决定,而机器的输入则会影响到机器之后的状态。进一步到具体机器的设计,其实无非也就是要回答两个问题: 在什么状态下要做什么事情 怎么样会导致状态发生改变 比如最简单的例子,一盏灯,按一下会亮,再按一下会灭。这个机器就可以定义为有两个状态,一个是灯亮,一个是灯灭。回答第一个问题,什么状态下要做什么事情:灯亮的状态下,输出亮;灯灭的状态下,输出灭。回答第二个问题,怎么样会导致状态发生改变:灯亮的状态下,如果按键按下,就进入灯灭的状态;灯灭的状态下,如果按键按下,就进入灯亮的状态。无论何种状态下,如果没有按键按下,则停留在当前的状态。如果画成状态图的话,大致如下: 从这个图里也能清楚的看到之前提的两个问题。圆圈就是表示状态,这里只有两个状态,每个状态有一个自己的编号(0和1)以及状态对应的输出(灭和亮),回答了第一个问题;箭头则是表示状态的转换,不同的输入会导致什么样的结果,回答了第二个问题。 再看一个稍微复杂些,但是也很常见的例子,自动贩卖机。假设一个贩卖机,只卖矿泉水,价格定为2元,只接受1元硬币或者5角硬币,多不找零,设计一个状态机来描述它的行为。这个机器的话,它有两个输入,投入5角或者投入1元;以及一个输出,是否已经付了足够多的钱。 一个简单的想法,仍然按照之前的做法,设计两个状态,一个是付够钱了,另外一个是还没付够钱。那输出也就是直接和状态对应的。然后,考虑输入,就出问题了。如果当前没付够钱,怎么样知道投入5角或者1元之后就付够了呢?加个变量来记录?如果考虑到“当前状态”这个说法本身就是一个变量的话,完全可以把“付了多少钱”这一信息也编码到状态当中。把状态分为,没有付钱,付了0.5元,付了1元,付了1.5元和付了2元这五种状态。这样状态转换也就容易设计的,比如从0.5元这个状态,如果付0.5元就到1元的状态,如果付1元就到1.5元的状态。状态图示如下: 注意代表每个状态的圆圈中都有两行,第一行写的是状态的名称,通常为了之后实现(程序或者电路)方便,会标注一个从0开始的数字,而第二行则是这个状态对应的输出,如这里只能是 是 或者 否,而前4个状态都是否,只有投够钱,到达最后一个状态才会变成 是。其它的箭头则是表示如何进入下一个状态。 以上的两个例子听起来很简单,但是这并不表示状态机只能用来实现这类很简单的东西。在以后的教程中就会看见,整个GameBoy的设计当中,许多关键的地方都需要用状态机来实现。所以,理解状态机的概念和实现还是很有必要的。当然,也如前面所说,状态机并不一定需要要用74或者FPGA来实现,软件设计中也经常会使用到状态机的思想,这是一个通用的框架。 用逻辑电路实现状态机 不过当然,这个教程讲的是逻辑电路和FPGA,所以自然还是讲讲如何用逻辑电路来实现状态机。还记得之前说过的组合逻辑和时序逻辑吧?状态机就是一个同时需要用到两部分的电路。具体而言,进入下个状态由时序逻辑电路控制,而状态控制的输出则是由组合逻辑电路控制。 之前的第二个例子当中提到了,“状态”这个东西本身就是变量,为此,在设计状态机的时候可以不引入别的变量。变量,自然需要有可以存储数据的元件来存储,而在逻辑电路中,就是上一节中讲的触发器。一个触发器可以存储1个bit的数据,比如要实现上面那个售货机的电路,因为可以有5个不同的状态,这也就至少需要3个bit来存储(1个bit显然只能是两个状态,0或者1,而2个bit则可以有4个状态,从00到11,3个就可以有8个状态了,虽然这里只需要5个),那么这个状态机的中心就是三个触发器。而这三个触发器,也就是这个状态机的时序逻辑电路部分了。 那么组合逻辑电路部分呢?还记得之前说的设计状态机的两个基本问题吗?用逻辑电路实现状态机无非也就是回答这两个问题。 首先第一个问题,在什么状态下要做什么事情。可以根据上面的信息列一个表,这个表描述的是当前状态和输出信号的关系: 其中Q2 Q1和Q0其实也就是3个触发器所存储的值,分别可以是0或者1。在这个简单的例子当中,不难看出输出值就等于Q2的值,或者说如果希望让1只对应S4(1 0 0)的话,可以写成 C = Q2 && !Q1 && !Q0 回答第二个问题,怎么样会导致状态发生改变。定义两个输入,一个a表示投入5角,一个b表示投入1元,列一个当前状态,输入信号,和下一个状态的关系表。 这个表,如之前所说,回答的是第二个问题,不同状态如何变化。注意到这里出现了一些无效状态,这里就不多加考虑他们的处理,但是如果是做正式的产品,这些状态自然也需要纳入考虑,提高产品的稳定性。 但是这个表怎么用呢?从设计电路的角度来说,电路上关心的是,如何设计一个电路,给定当前状态和输入信息,产生下一个状态的编码。比如考虑其中的D2信号,也就是下一个状态编码的其中一位,在S0、S1的时候保持为0,在S2且输入为01时为1否则为0,在S3且输入为01和10是为1否则为0,而在S4是则始终为1。各位可能已经听出来了,这就一个输入为5bit(3位状态+2位外部输入)输出为1bit的组合逻辑电路。而把这些关系输入到软件当中(当然也可以手动化简),这样就能得到如下的逻辑表达式,表示了输入和输出的关系: **D2 = (Q1 && b)   (Q1 && Q0 && a)   Q2** **D1 = (!Q2 && !Q1 && b)   (!Q1 && Q0 && a)   (Q1 && !Q0 && !b)   (Q1 && !a && !b)** **D0 = (!Q1 && Q0 && !a)   (!Q2 && !Q0 && a)   (Q0 && !a && !b)** 如图是D2这一位的设置,A B C D E分别是Q2 Q1 Q0(状态)a b(输入)。X表示这个结果无所谓,如无效状态和未使用的状态。 到这里为止,就得到了所有需要的表达式,一个输出表达式,三个下一状态的表达式。 把这些电路全部画出来,并且连接起来的话,这个状态机也就完成了。 回过来再考虑一下整个状态机的架构的话,也就是分为三个部分,最左边的是用来产生下一状态的三个组合逻辑,中间的是保存当前状态的触发器,而右边的则是输出逻辑。这个套路可以用于各类的状态机的设计。 其实,从下一节开始,我们就不会再这样画表、具体考虑各种逻辑了,而是用Verilog直接写代码来描述需要的行为,而不是需要的逻辑。但是这一节仍然介绍这些内容,是希望大家能明白,即使以后不用画这些表设计这些电路了,Verilog到最后产生的仍然是这些东西。如果写Verilog却不知道实际对应的电路,就很容易写出不能用实际硬件实现的代码,或者说对应到软件里,也就是无法编译的代码了。

The Tutorial for FPGA Player - Episode 3: Sequential Logic

上期教程中介绍了组合逻辑的使用,而本期教程则要来讲讲时序逻辑。 那么当然,第一个要回答的问题就是,组合逻辑电路和时序逻辑电路有什么区别。如果重新考虑之前做过的电路,不难发现一个特点,这些电路都是给定输入,得到输出。只要输入是一定的,输出也就是一定的。所有影响输出的因素只是输入而已。听起来很自然,没有什么问题对吧?甚至可能还觉得奇怪,如果我给定了输出,但是却不能确定输出,那不是乱套了吗?考虑这么一个需求。还是有一盏灯,有一个按钮,现在需要,按下按键,灯点亮,然后一直保持点亮,再按下按键,灯关闭,并保持关闭。不难看出,单独给定输入状态(按键按下或者没有按下)根本没法知道灯是开着还是关着,也就是输出不单单取决于当前输入,还取决于当前的状态。换句话说,电路有了自己的记忆。而在分析时,除了要考虑当前发生的事情,还需要考虑之前发生过的事情, 多了时间维度。这种电路被称为时序逻辑电路。 锁存器与触发器 记忆这事在单片机或者任何软件编程环境里都好说啊,无非是一个变量的事情。但是如果说回到电路角度,应该怎么解决这个问题呢?引入新的逻辑门,比如说 存储门什么的东西?其实完全不用,只要用之前在组合逻辑时用过的那些东西就可以做出能够存储状态的电路哦。原理图如下 看着有些奇怪是不是,这种电路应该怎么分析呢?因为一眼看过去似乎找不到什么输入,只有一个输出Q。不妨考虑下其中任意一条线为高或低,随后计算出来其它所有线的电平,发现没有出现逻辑上的冲突,也就是只要保持通电,这些电平就会一直保持这个状态。但是如果这时,由于外部的信号,改变了其中任意一条线的电平,所有的电平都会变化,并一直保持新的状态。所以呢,这也就是能够存储1个bit的电路了。 不过刚刚其实没有说清楚,什么叫由于外部信号改变了电平啊?怎么改变啊?是的,得有一种可靠的方式来修改状态。而这种方式不是唯一的,一种简单的方法如下: 图中有两条信号线,分别是S和R,表示置位(Set)和复位(Reset),分别可以让这个电路保存的值设置为1或者0。比如可以考虑,当前电路里面保存的数值是0,如果S线为1,就会使对应的NOR门状态发生变化 ,进而使得整个电路保存的数值变为1。当然如果本来就是1,那就什么都不会发生。 不过感觉不太对啊,根据以前使用单片机相关电路的经验,一般都是来一条数据线直接传输数据(高或者低)而不是分成S和R分别传输高或者低。毕竟S的时候不能R,R的时候不能S,弄两条线看起来就有些浪费。要实现直接输入数据而不是R、S,也很简单 现在,如果输入为高,那么S为高,R为低,输出就会被设置成高;如果输入为低,那么S为低,R为高,输出就会被设置为低。听起来很棒是不是?完全不是。输出只会复制输入的情况,结果就是变成了一条存在延迟的导线而已。如果输入“消失”,输出也会随之消失,也就没有什么“记忆”可言了。所以还需要有一条线,用来指示当前的输入是否有效,是否需要存储当前的输入。电路修改成如下: 带有写入使能的记忆电路 现在好了,有了一条使能(Enable)线,用来标记输入是否有效。当Enable为高的时候外部的数据可以进入这个记忆电路;而Enable为低的时候,无论输入信号怎么变,输出信号都不会发生变化, 就像是被锁住了一样,于是这种电路的名字,就叫做锁存器。而这种输入数据的就被称为D锁存器,前面那种输入S和R信号的则被称为SR锁存器。 确实,锁存器听起来像是非常可靠的存储设备,也确实在过去被大量使用,但是现在的数字电路设计中大多已经转向使用触发器来代替锁存器。所以这里我们也就来介绍一下触发器。 首先,锁存器存在一个“缺陷”。Enable为低的时候保存信号电平没有问题,然而Enable为高的时候,就有点意思了。Enable为高的时候,其实也就基本是图?中描述的导线的情况,输出就是对输入的简单复制。不如说,这时整个锁存器是“透明”的,输入信号可以直接穿过锁存器达到输出。而触发器,相比于锁存器,就并不是透明的,因为触发器并不依赖于电平来指示是否要写入,而是依赖于时钟的变化。触发器会在时钟从低到高变化(或者从高到低变化,取决于具体使用的触发器)的瞬间,储存输入的数据,并且在其余时间保持这个数值。这里就简单放一下D触发器的原理图,使用和之前同样的分析方法就可以理解其工作原理。如果不能理解,也没有关系吧,会用就行了。 当然,平时用的时候如果都画全这么一个原理图,一来太累,二来也不清晰,所以锁存器和触发器都可以画成单独的符号,需要的时候使用这个符号即可: 总结一下,这里介绍了两种存储元件,一种叫锁存器,一种叫触发器。对于锁存器来说,重要的是电平状态,高电平存储,低电平保持。而对于触发器来说,重要的是时钟边沿,时钟从低到高的瞬间存储,其它情况保持。注:当然也存在极性相反的原件,如低电平存储高电平保持的锁存器和始终从高到低瞬间存储的触发器。另外“触发器”这一名词曾经可以用来指代各种存储元件,包括现在讨论的锁存器也曾经是触发器的一种。而现在,通常而言触发器特指边沿触发的触发器,电平触发的被称为锁存器。其实锁存器和触发器的类型并不止今天介绍的这几种,但是为了保持简洁,这里只讲和我们的目标——FPGA GameBoy最相关的,其它的就不做介绍了。 实例1 灯 其实到这里位置,时序电路需要的新的组件已经介绍完了。确实,所有涉及的只不过是锁存器和触发器而已,而这两者本身也无非只是之前的基础逻辑门的组合罢了。然而,虽然说起来只不过是一些逻辑门的组合罢了,有了这两种元件之后,逻辑电路所能实现的功能瞬间强大了很多,电路的设计和分析难度也随之上升了一个台阶。前面的那个实例,还好说,只涉及一个输入和一个输出,本身比较简单,简单凑一凑设计就出来了。而如果要设计更为复杂的东西,还是束手无策。而我这里能做的,就是提供更多例子,帮助大家理解时序逻辑电路常见的“套路”。而这个实例,也就是开始的,按一下开,再按一下关闭的电灯。 首先,这个电路有一个输出,一个输入,分别是电灯的输出和按键的输入。简单考虑下不难注意到,要求里面提到了保持电灯的状态,那也就是需要一个触发器,用来记忆并保持电灯的状态,而电灯的输出直接接入到触发器的输出上。而每当按键按下的时候,电灯的状态可以发生变化,也就是触发器需要在按键按下的时候存储新的状态,那么按键也就是触发器的时钟信号了。新的状态,按照要求就是一个和当前的状态不同的状态:如果现在开着就关掉,如果现在关着就打开。于是新的状态生成也很简单了,当前状态加一个非门。 另外可以考虑下如果这里直接把触发器换成锁存器会发生什么事情。由于锁存器是透明的,在按下按键的时候,锁存器会不断试图存入输入的值,而不难发现输入的值和当前的输出相关,而透明状态下输出又会因为输入而发生变化……结果就是发生了循环,当前状态会不断在打开和关闭(1和0)之前切换,而电灯则是与之对饮进入高速闪烁状态,而最终的状态,则是放开按键时所处的状态,基本而言就是开和关之间随机一种。可以说整个是一团糟了,没有办法使用。透明带来的问题也就是在使用锁存器进行设计时必须考虑的。我们这里之后的设计,都将全部使用触发器完成,不使用锁存器。 这里顺便附上这个电路的Verilog实现,各位有兴趣的话可以在自己的实验平台上做这个实验。但是,由于按键抖动的原因(各位如果玩过单片机、Arduino一类的,应该已经很明白按键抖动是什么了吧?),可能仍然会导致按键按下状态变化多次。还是再说明一次,这里的Verilog代码仅仅只是用来实验用的,看不懂代码含义也没有关系,可以自己试着摸索摸索,不然的话之后也会有具体的介绍。 以上电路对应的Verilog代码 module light( input key, output reg light); always@(posedge key) begin light <= ~light; end endmodule 实例2 计数器 计数器,自然也有多种实现方法,不同的实现方法实现出来的计数器也有不同的特性。比如这里,希望能够实现一个不断自增的计数器,也就是数值不断+1。不知道大家还记不记得上一期出现过的加法器呢?它可以实现二进制数的相加。如果我们修改这个加法器,让它永远执行+1的操作,并且把结果喂给加法器的输入,即可实现自增的功能。不过,直接连接肯定是不行的,输出的同时输入也在发生变化,也就是像之前说的那样出现了循环,最后做出的电路也就没有什么太大的意义。需要做的,也就是用触发器把加法器的输入和输出隔离开来,确保不会立即进入输入。而触发器的时钟信号,也就是让触发器保存当前输入的信号,在这里也就真的成了控制计数器速度的时钟。比如说时钟信号是1Hz,也就是每秒会有1个电平从低到高的变化(上升沿),那么触发器就会更新一次,也就是整个计数器按照1Hz的速度向上计数的效果了。 注意到这里的加法器十分简单,简单到只剩下了两个门。上一期为了实现1位数的加法也永乐两个门把?现在只用了两个门却实现了2位数的加法,怎么做到的呢?原因也很简单,因为这里不需要考虑进位,而且也是固定的+1操作,其实整个电路就可以不用从加法器的角度去考虑,而是直接考虑这是一个输入当前计数值,输出下一个计数值的组合电路,其真值表如下: 按照要求,输出永远是输入+1,而实现这个组合逻辑电路,也就只需要两个门就足够了。按照之前的思路,把组合逻辑部分的输入和输出用触发器隔开,也就实现了需要的效果。 这里同样附上Verilog代码 module counter( input clk, output reg [2:0] count); always@(posedge clk) begin count <= count + 1; end endmodule 结语 本期简单向大家介绍了时序电路的概念,介绍了时序电路的基本组成部分:触发器和寄存器,并展示了时序电路一个基本的应用:计数器。这期的内容可能也是和以前一样,并不那么容易理解。一下子没理解没关系,再读一遍。一旦明白了,一切就很简单了。而下一期我们则将要介绍数字电路基础中非常重要的一个部分:状态机。状态机是时序电路设计中非常常用的一个“套路”,提供了一种系统的设计时序逻辑电路的方法,而不是只是像现在一样想办法凑。同时它也是本系列教程中,数字电路部分的最后一个主题。在那之后,就可以开始FPGA相关的内容啦。 也许有观众依然不理解,最终要用的就是FPGA,为什么还要费那么大力气来介绍数字电路呢?毕竟FPGA已经把很多东西都抽象了, 开发时使用的也是编程语言,而不是画逻辑门。确实没错,开发FPGA不需要任何我们现在在画的逻辑门。然而,如果没有现在的这些铺垫,就很难理解FPGA最终编程时究竟是在做什么。如果具象的东西都没理解,势必会给抽象表达的理解带来更大的困难。不过为了避免大家学的东西太多头晕,这里我只介绍了和今后FPGA最直接相关的概念,一些不太那么相关的也就没有介绍。各位如果有兴趣自然可以找更多书来看。但是反过来说,这里所有介绍的,都是重中之重,也请各位希望继续跟着玩的,务必理解这些内容。

The Tutorial for FPGA Player - Episode 2: Combinatorial Logic

在进行了两期的背景介绍后,本期也是终于是进入正题了。如果你之前有了解过数字电路,大概就听说过数字电路大致可以分为组合逻辑电路和时序逻辑电路,而大部分的电路都是这两者的结合。本期所要介绍的也就是这其中的前者:组合逻辑。顺便,上期之后,大概部分玩家购买的74芯片或者FPGA开发板也已经到货了吧?本期也会附带关于如何用74或者FPGA实现这些实验的说明。 第一个例子还是从第一期举过的例子开始好了。想要设计一个电路,里面有两个开关和一个灯泡,希望实现两个开关任意一个打开的时候灯泡点亮。当然,一个显而易见的解决方案就是把两个开关并联,如下图所示: 但是如果我们用像一般考虑单片机电路一样的思路去考虑,那么就可以把两个开关看作是两个输入口,而灯泡是一个输出口,那么这个电路就会变成这样: 而这个盒子里面所包括的就是需要实现的电路。这个电路可以是一块74,也可以是一块单片机,或者只是像图1一样简单的连接起来而已。这里就来考虑下,如果要用单片机来实现要怎么做吧。很简单,一句if语句的事情,直接翻译要求, **if ((a == 1)   (b == 1)) c = 1; else c = 0;** 可以看见逻辑运算已经出现了,输入1为高或者输入2为高时,输出1为高,否则输出1为低,这样就是这个简单例子的逻辑。另外等于1 可以省略,就变成了 **if (a   b) c = 1; else c = 0;** ,再考虑到因为逻辑运算的结果本身就是1或者0,这里甚至不需要if,只需要 **c = (a   b)** 即可。 只有一个简单的 **“或”运算(   , OR)** 的关系,如果要用图示描述出来的话: 中间这个像箭头一样的东西也就是“或”运算的符号了。除了或之外,当然还有其它的符号,以下是6个最常用的逻辑运算的符号: 现在再来一个例子吧,假设还是两个输入,现在希望有且仅有一个开关打开(也就是需要有一个开关打开,但是不能两个都打开)的时候让灯泡点亮,那要怎么实现呢?一种实现思路是,如果第一个开关打开且第二个开关没有打开,或者第二个开关打开且第一个开关没有打开,这两种情况下让灯泡点亮,否则就熄灭。用C语言的逻辑表达式表述的话大致是 **c = (((a == 1)&&(b == 0))   ((b == 1)&&(a == 0)));** 如果试图用和第一个例子一样的图示来表示逻辑关系的话,就会遇到问题:这里出现了一些新的运算。比如 “与”运算(&&, AND) ,这个在上面的常用表里面有。而 “等于” 运算虽然第一个例子里也出现了,但是被省略掉了。然而这次除了等于1之外还出现了等于0的运算,这就没法省略了。那么有没有等于运算的符号呢?回答是肯定的,然而等于并不是一个基础运算,这里并不使用。一个代替等于0运算的方法就是,先做否运算,再判断等于1,即把 (a == 0) 改写成 (!a == 1) ,这个写法可能有些奇怪,毕竟一般不会这么写,但是确实是可行的。整个逻辑运算也就变成了 **c =(a&&(!b))   (b&&(!a))** ,这样就能把图画出来了: 然而实现这个逻辑的方法并不是唯一的,如果调转一下思路,这个逻辑也可以表达为:如果有两个开关有任意一个打开,且两个开关有任意一个没有打开,那就能说明有且只有一个开关被打开了。表达式为 **c = (a   b)&&((~a)   (~b))** ,示意图如下: 虽然确实换了一种方法,但是似乎使用的逻辑门数量并没有变少啊?确实,但是如果简单看下的话,这个里面的两个非门和一个或门可以合并成一个与非(’NAND’)门,或者,其实整个逻辑可以只用一个门来完成: 有且只有一个输入为高时输出高,这其实就是 异或(XOR)门 的功能了。举上面这个例子是为了说明一个道理:使用逻辑表达式或者原理图示来说明一个逻辑,其实可能并不是最优的方案。同一个逻辑可能存在多种不同的表达方式。为了解决这个问题,可以使用 真值表(Truth table) 来表示逻辑: a(输入) b(输入) c(输出) 0 0 0 0 1 1 1 0 1 1 1 0 对于上面这个逻辑,这个真值表是唯一的。看表的方法也很简单,左边两列a和b在这里是输入,右边一列c在这里是输出,比如要知道a打开(1) b没有打开(0)时c的输出,只要找到 1 0这一行,就能看见c的输出是1。如果两者都为打开(1),那么就是最后一行,c为0。 这里稍微展开一下,没有理解没有关系,不影响玩。其实我上面三种画法,第一种和第二种其实分别对应了两种常见表示方法,一种叫 SOP(Sum of Product,乘积之和) ,另外一种叫 POS(Product of Sum,和之乘积) 。这两种的取名方式是因为,通常的逻辑算式中,与预算并非用&&表示,而是用乘法的点 (·) 而或运算也并非用 **   ** ,而是用加法的加号 (+) ,非运算是在式子上面画横线。于是图4和图5的式子写法分别是: 一个是两个乘法的结果加起来,也就是乘积之和SOP,另外一个是两个加法的结果乘起来,自然就是和之乘积POS了。而且这里其实都是和真值表一一对应的。式1里面的两个乘积项其实就对应了真值表里面输出为1的两行,而式2里面的每个加法项分别也就对应了真值表里面输出为0的两行。想要用逻辑表达任意一个真值表,只需要把每一个输出为1的行用乘法 (与运算,AND,&&) 表示出来,再全部加起来 **(或运算,OR,   )** 即可;或者也可以把输出为0的行用加法加起来,最后全部乘起来。这也分别就是POS和SOP了。当然通常这样表达出来的结果并非最优,要获得优化的结果还需要使用卡诺图等等。另外用乘法和加法来表示 && 和 **   ** 也不仅仅只是写着方便,这两个运算和一般计算数字的加法和乘法存在相似性,部分数字运算规则也可以应用到逻辑算式中,用于化简逻辑代数式,或者进行其它运算等等。由于本教程只是教大家玩FPGA,而不是教大家学离散数学或者逻辑电路,这些内容也就不展开讲了。感兴趣的可以自己去搜索一下。 所以,总结一下上面的内容,对于这样的,输出信号只取决于当前输入信号的(即只需要知道当前的输入信号就可以决定输出信号的)这类电路,就称为组合逻辑数字电路。或者也可以理解为,判断的内容只有输入信号的一条if语句。不知道各位也没有看明白呢?如果是第一次接触可能确实比较费解,有必要的话建议重新再读一遍。如果觉得没问题了,来做个实例吧,也能帮助加深理解。 实例:设计一个电路,可以计算1位二进制数的加法。 这个听起来完全不像之前的例子啊,之前都是开关,灯啊什么的,这里怎么突然就开始做算术了呢?其实并没有什么区别。1位二进制数加法,也就是要把两个1位的二进制数加起来,两个数分别可以是0或者1,换句话说,也就是两个开关。而1位二进制数加法可能有下面四种情况: 0+0=0 0+1=1 1+0=1 1+1=10 如果考虑把结果像之前一样也接上灯泡的话,那就和之前的事情非常接近了,只不过为了表示两位数字的输出现在需要两盏灯。不过,两盏灯又需要怎么设计呢……?首先还是先画真值表吧,假定两个输入分别是a和b,输出则是c和d: a(输入) b(输入) c(输出) d(输出) 0 0 0 0 0 1 0 1 1 0 0 1 1 1 1 0 不难发现这个真值表看起来和上面四种情况的算式很像,只是加上了框框,并且把十位的零都写出来了而已。确实,真值表就是这样画的,列举所有可能的情况,在表里面写上每种情况的输入和输出。 现在剩下的问题就是怎么画成之前的电路图了。其实也不难做,单独考虑每个输出对应的逻辑,最后画在一起即可。比如这里有c和d两个输出,先考虑c的情况。对于c而言,只有当a和b都为1的时候c才是1,或者说只有当两个开关都打开时灯才会点亮,其实也就是c = a&&b这么一个逻辑。而d则是之前第二个例子中相同的逻辑,只需要一个异或就能完成,表达式为d = a^b。 下一步就是具体实现这个电路了。我这里分为三个部分,74芯片,Xilinx FPGA和Intel(Altera) FPGA。各位可以根据自己有的芯片来进行实验。如果没有的话,在以后也会讲如何使用仿真工具进行开发。 74芯片 如前面所说,这个电路需要两个门,一个与门一个异或门,对应的芯片分别为74HC08和74HC86。这两款芯片的内部连接方式可以通过查找对应的数据手册找到: (74HC08) (74HC86) 电路的话也并不复杂,只是把上面的示意图里面的逻辑符号替换成芯片,并增加必要的电阻罢了: 如果没有74HC86,d输出自然也可以选择像图4或者图5那样使用多个独立的门芯片来搭建,比如完全按照图4的原理图来,需要一片74HC04非门,和一片74HC32或门。 (74HC04) (74HC32) 虽然芯片只是加了一片,不过接线变得复杂了不少,这也说明在使用74芯片搭建电路时,优化还是很重要的。 Intel FPGA 接下来讲讲如何在Intel FPGA平台上完成这个实验。就像玩单片机一样,玩FPGA需要在电脑上安装开发环境,在里面编写代码,然后再通过烧录器烧录进FPGA才能完成。通常对于单片机玩家来说,这个开发环境叫做Keil或者IAR。Intel FPGA的开发环境叫做Quartus,而且Intel提供了一个免费版本的Quartus,可以用于小规模设备的开发,通常来说只要使用免费版本的就足够了。具体的软件安装过程我这里不再赘述,如果有疑问可以在网上找到很多的帮助。 打开Quartus应用程序,在欢迎页面选择New Project Wizard或者从菜单选择File - New Project Wizard打开创建新工程的向导 自己选取要保存工程的文件夹,取个名字,Project Type选择Empty Project(空工程), 添加文件的页面直接点击下一步,直到来到这个选择器件的页面: 请按照自己开发板上的芯片型号选择。这个型号印在了FPGA上,通常也可以从开发板的用户手册、原理图钟得到。如我的是MAX10系列的10M50DAF484,就在这里选择这个型号。选择完成后可以直接点Finish完成创建。 创建完工程后还需要创建主程序文件。开发FPGA使用的并不是C语言,C语言毕竟是用于开发软件的语言,开发逻辑电路(硬件)有其专门的语言,比如本教程使用的Verilog语言。不过好在Verilog的语法和C还是较为相似的,熟悉C语言的话学习Verilog语言本身并不困难(但是适应硬件开发的思路转变就不简单了)。本期并不会系统讲解Verilog的使用,只是简单使用体验一下FPGA的使用而已。新建一个文件(不是工程),在弹出的对话框中选择Verilog HDL File。 在新的文件中输入以下代码: module lesson3( input wire a, input wire b, output wire c, output wire d ); assign c = a & b; assign d = a ^ b; endmodule 保存为lesson3.v。注意第一行后的lesson3需要和文件名匹配,如果你保存为了别的文件名,比如adder.v,那么第一行也应该相应的写成module adder才行。如果没有问题的话,保存后可以点击菜单栏的蓝色右箭头尝试进行综合(类似于程序 编译 的过程),应该可以顺利通过。如果没有,请检查刚刚保存的lesson3.v是否被加入了工程并设置为了顶层文件(可以使用左侧的面板查看)。 完成后可以看见左边几个任务都是绿钩子,或者可能是黄感叹号,都表示顺利通过了。在中间的Flow Summary里面可以看到一些报告信息,比如使用的逻辑数量,一共有49760个,这里只用到了3个。 不过这样还没有完成,在上面的程序中只是说了会有a b c d四条线,但是并没有说这四条线会连接到那些引脚上。接下来我们就来进行引脚分配的工作。首先第一步当然是要确定怎么连接。我的开发板(DE0-Lite)上有几个拨动开关,我决定就把a和b连接到拨动开关上,而c和d则是连接到两个LED输出上。通过翻原理图可以得知两个开关分别连接在C10和C11上,而两个LED分别连接在A8和A9上。打开Assignment菜单中的Pin Planner 可以注意到a b c d已经出现在了里面,而且有一些自动定义的位置。按照之前得知的信息在Location一栏填入引脚。Fitter Location可以不用理会,IO Standard这里要按照开关和LED连接的块的电源电压填写,通常也可以在原理图里看到,我这里是3.3V,也就是LVCMOS33。 完成后直接关闭窗口。重新点击蓝色右箭头进行综合。值得一提的是,不少开发板厂家会把板上的引脚配置预先定义好,要使用时只需要导入一个定义文件即可,而不需要每次这样手动查原理图配置。以后也会讲解如何使用那种方式完成,本期这么做一来是为了让大家体验这个过程,二来也是方便使用不同开发板甚至是最小系统板的玩家可以应用到自己的板子上。 现在就可以把程序(位流)烧录进FPGA测试了。使用USB线连接FPGA开发板至电脑,使用Tools菜单中的Programmer打开烧录工具。注意首次使用可能需要为烧录器安装驱动程序。如果你的Programmer窗口中左上方显示No Hardware则需要打开设备管理器为USB Blaster安装驱动。驱动可以在Quartus的安装目录中找到,如C:\intelFPGA_lite\18.0\quartus\drivers\usb-blaster。 确认左上角显示USB Blaster之后就可以点击Start开始烧录了,烧录完成后应该就可以观察到效果了。 Xilinx FPGA 如果你选购的是Xilinx FPGA的开发板,也没关系,整体过程也是大同小异的。需要注意的是,Xilinx曾经使用ISE作为开发环境,现在新的芯片已转为使用Vivado开发。鉴于目前最常用入门的芯片Spartan6仍然需要使用ISE开发,这里也使用ISE进行演示。Xilinx也像Intel一样推出了免费版本的ISE,叫做ISE WebPACK,对于我们来说完全足够了。具体安装方法这里不进行赘述,只讲使用。 在开始菜单中打开Project Navigator(ISE的主程序) 点击左侧的New Project…或者是菜单中的File - New Project打开新建工程窗口。也是一样,输入工程名称,保存位置,顶层文件类型选择HDL,在属性页中选择自己的设备。 其它不用修改,完成创建。在左侧窗格的Hierarchy中右键选择New Source建立新文件 在向导中选择Verilog Module并在右边输入文件名。 这个项目中有4个信号,a b c d,分别为两个输入和两个输出,这里就这么填写。 建立文件后注意到框架已经在了,只需要在中间插入两行逻辑即可,整体程序应该和Intel FPGA中列出的相同。 assign c = a & b; assign d = a ^ b; 当然也和Intel FPGA那边一样,需要定义引脚,不过不是使用图形化的工具,而是直接编辑约束文件。和上面一样打开添加新文件的窗口,在左侧选择Implementation Constraints File,并在右边输入文件名constraints.ucf 根据原理图可知我的开发板LED连接到了H18和L18上,而两个按钮则是在AJ6和AK7上。另外需要知道所连接的块(Bank)的电压,通常也可以在原理图里找到。我的板子上按钮为3.3V而LED为2.5V。其它开发板可能是不同值,通常可能为3.3V。在新的约束文件中,根据上面得到的信息写入以下内容: NET a LOC = AJ6; NET a IOSTANDARD = LVCMOS33; NET b LOC = AK7; NET b IOSTANDARD = LVCMOS33; NET c LOC = L18; NET c IOSTANDARD = LVCMOS25; NET d LOC = H18; NET d IOSTANDARD = LVCMOS25; 其中a b c d就是四个要定义的信号的名字了。如果不确定后面的IOSTANDARD,可以参考开发板提供的参考程序中的ucf文件。编辑完成后,双击左侧任务窗口中的Generate Programming File开始综合并生成编程文件。如果没有问题的话左侧应该是三个绿钩。 在右侧可以看到综合的报告,如逻辑的使用量等等。通过USB连接开发板到电脑,使用Tools - iMPACT打开编程工具。点击左侧的Boundary Scan进入设备检测界面,在右侧右键,点击Initialize Chain检测设备。 双击FPGA设备,在弹出的窗口中选择刚刚生成的文件。注意到下面的bypass已经变成了选择的文件名。选中FPGA,在左侧的任务列表里面双击Program。一段时间后程序就应该被写入了。按下按钮可以注意到LED变化。