12

有点令人困惑的问题。但我真的想学习一些低级编程。事情是,像 Arduino/Etc 这样的开发板。真的隐藏了很多正在发生的事情。

我花了一些时间学习计算机体系结构、逻辑/门/顺序逻辑等。(我什至学习了与这一切相关的半导体和电子学物理,只是为了知道到底发生了什么,因为以及如何使用 CMOS 晶体管等制造门)。

但这就是它结束的地方......而且我希望能够理解指令(如十六进制/或汇编/等代码)是如何通过尽可能简单的计算机移动的(我用过的很多书都去了直接从像盖茨到计算机......没有真正的介于两者之间)。甚至一些简单的事情,比如......将值存储在寄存器或内存位置(可能打印到像素?或其他东西)。

我认为有趣的事情甚至可能是最终编写一个模拟器。我有高级语言的经验,但我听说像 6502 这样的东西可能是一个好的开始,因为你使用了很多汇编,而且指令集不是太大。

有谁知道任何可能有帮助的资源/想法/书籍?我已经阅读了“计算系统要素”,虽然......这是一本好书,但我并不觉得它经历了真​​正发生的事情并看到它发生。这可能更像是一个 Electronics.stackexchange 问题,如果是这样,我深表歉意。

4

6 回答 6

6

概述

你真的有很多选择。我将总结我对如何翻译指令的看法,但我也会提供一些我刚开始时的选择。

我的看法

首先,从二进制输入的角度来思考是最容易的。假设您有一个 16 位微处理器。(也就是说,指令以 16 位二进制编码。)考虑一个将数字放入寄存器的汇编操作 SET。例如:

SET(R1, 12) // Stores 12 into register 1

让我们任意选择(因为即使在任何标准架构中,选择也确实是任意的)选择将 SET 指令翻译成以下 16 位二进制值 I:

0001 0001 0000 1100

基本上,我只是制定了一个约定。但这里是我如何分解它。我选择让位 I[15:12](以大端符号表示)表示特定指令。我选择让整数 1 对应指令 SET。现在我已经决定了那个约定,我可以说如果我有一个 SET 指令,让位 I[11:8] 对应于寄存器。(显然这意味着我将只有 16 个寄存器:4^2=16)。最后,我让位 I[7:0] 对应于我要存储在给定寄存器中的数据。让我们再看一下二进制的 SET(R1, 12) (为了清楚起见,我用换行符分隔每组四个):

if I =  0001 0001 0000 1100
I[15:12] = 0001 (binary) = 1 (decimal) = SET instruction
I[11:8] = 0001 (binary) = 1 (decimal) = R1 since I[15:12] correspond to SET.
I[7:0] = 0000 1100 (8-bit binary) = 12 (decimal) = value to store in R1.

如您所见,微处理器中的其他一切都变得非常简单。假设您在 RAM 中存储 4 行指令。你有一个附在时钟上的计数器。计数器通过 RAM 中的行计数。当时钟“滴答”时,一条新指令从内存中出来。(即下一条指令从 RAM 中出来——尽管在插入 JUMP 语句后这可能有点随意。) RAM 的输出通过多个位选择器。您选择位 I[15:12] 并将它们发送到控制单元 (CLU),它会告诉您要传达的指令即 SET、JUMP 等。然后根据找到的指令,您可以决定允许写入寄存器或添加寄存器,或者您选择在架构中包含的任何其他内容。

现在幸运的是,已经为您选择了机器指令二进制值的任意约定(如果您想遵循它们)。这正是指令集架构 (ISA) 所定义的。例如MIPSHERA等。为了清楚起见,您在设计电路时创建的实际实现被称为微架构

学习资源

文本

哈里斯和哈里斯的书是本科计算机体系结构课程最著名的教材之一。这是一个非常简单和有用的文本。整个事情都可以在这里免费获得一些随机学校的PDF格式。(快速下载!)我发现它非常有帮助。它涵盖了基本电路、离散数学主题,当你读到第 7 章时,构建微处理器已经是小菜一碟了。读完那本书后,我花了大约 3 天时间完成了一个 16 位微处理器。(当然,我有离散数学的背景,但这并不是很重要。)

另一本超级有用且非常标准的书是Hennessy 和 Patterson的书,它可以从一些随机学校以 PDF 格式提供。(快速下载!)哈里斯和哈里斯的书是基于这本书的简化。这本书讲得更详细。

开源微处理器

那里有大量的开源微处理器。当我构建我的第一个微处理器时,能够参考它们对我非常有帮助。带有 Logisim 文件的文件特别好用,因为您可以以图形方式查看它们,然后像​​这样单击并弄乱它们。以下是一些我最喜欢的网站和特定的国会议员:

4位:

16 位:

Open Cores - 我真的不明白这个网站。我申请了一个帐户,但他们还没有真正回来......不是一个大粉丝,但我想如果你有一个帐户,那一定很棒。

工具

逻辑西姆

如前所述,Logisim是一个很好的资源。布局完全是图形化的,您可以通过选择一条线很容易地在任何时间点按位查看正在发生的事情。它是用 Java 编写的,所以我很确定它可以在您想要的任何机器上运行。这也是图形计算机编程语言的一个有趣的历史观点。

模拟

在 Logisim 中,您可以模拟正在运行的实际软件。如果您有一个编译器可以将二进制文件编译到您的目标 ISA,那么您可以简单地将二进制文件或十六进制文件加载到 Logisim RAM 中并运行程序。(如果你没有编译器,编写一个四行汇编程序并自己手动翻译它仍然是可能的,也是一个很好的练习。)模拟是整个过程中最酷和最令人满意的部分!:D Logisim 还提供了一个CLI来以编程方式执行此操作。

高密度脂蛋白

生成/设计微架构的更现代形式是通过使用硬件描述语言 (HDL)。最著名的例子包括 Verilog 和 VHDL。这些通常(令人困惑!)以 Ada 和 C/C++ 等顺序语言为模型。然而,这是迄今为止首选的设计方法,因为模型/设计的验证得到了更好的定义。在我看来,推理文本表示比用图形检查要容易得多。正如程序员不能很好地组织代码一样,硬件开发人员也不能很好地组织微架构图形设计的图形布局。(尽管这个论点当然可以应用于 HDL。)以文本方式记录仍然比以图形方式记录更容易,并且通常使用 HDL 进行更模块化的设计。

如果您有兴趣学习这一点,这里有大量的本科硬件课程,包括开放课程和实验室工作,讨论和学习使用 HDL 描述电路和微架构。你可以通过谷歌搜索找到这些。或者您也可以尝试通过下一步学习 HDL - 将 C/C++ 代码转换为 HDL 的工具。如果您有兴趣,那么Icarus Verilog是一个很好的 Verilog 开源编译器和模拟器。

模拟

使用 Icarus Verilog 之类的工具,您还可以轻松地模拟从二进制文件运行的真实程序。您只需将微处理器包装在另一个 Verilog 脚本中,该脚本通过某些总线将文件或字符串加载到 RAM 中。小菜一碟!:D

HLS

近年来,高水平综合(HLS)也在市场上获得了重要的立足点。这是将 C/C++ 代码转换为实际电路的过程。这是非常不可思议的,因为现有的 C/C++ 可以(但不总是)转换为硬件。

(我说并非总是如此,因为并非所有 C/C++ 代码都是可合成的。在电路中,比特流同时存在于任何地方。在软件中,我们认为代码是顺序的。如果你正在尝试,这是一种糟糕的思考模式设计硬件!!)

但正如您可能猜到的那样,这种能力对于优化硬件上代码的某些方面(例如矩阵运算或一般数学)来说是不可思议的。但是,这与您相关,因为您可以使用 HLS 工具查看如何将点积的 C 实现(例如)转换为 HDL。我个人觉得这是一个很好的学习方式。

模拟

HLS 仿真与仿真 HDL 一样简单,因为高级代码只是简单地转换HDL。然后,您可以完全按照我上面解释的方式对其进行模拟和运行测试。

于 2014-06-25T14:35:54.970 回答
5

尝试构建自己的简单 CPU。它并不像看起来那么难: LOGISIM

于 2013-04-18T12:24:38.060 回答
5

这是一个非常简单的 6502 指令集模拟器的示例框架(因为您不止一次提到 6502)。它从一个简单的 6502 程序开始,这些是我将要演示的唯一说明,但即使使用这样的程序,您也可以理解并立即获得一些工作的满足感。

   and #$00
   ora #$01
 top:
   rol
   bcc top
   and #$00

是的,我很清楚我没有正确启动模拟器。我假设这个二进制文件基于地址零,使用 xa65 汇编程序(apt-get install xa65)。组装后的二进制文件是:

hexdump -C a.o65 
00000000  29 00 09 01 2a 90 fd 29  00                       |)...*..).|
00000009

这是简单的简化指令模拟器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

FILE *fp;

#define MEMMASK 0xFFFF
unsigned char mem[MEMMASK+1];

unsigned short pc;
unsigned short dest;
unsigned char a;
unsigned char x;
unsigned char y;
unsigned char sr;
unsigned char sp;
unsigned char opcode;
unsigned char operand;

unsigned char temp;

int main ( void )
{
    memset(mem,0xFF,sizeof(mem)); //if we execute a 0xFF just exit

    fp=fopen("a.o65","rb");
    if(fp==NULL) return(1);
    fread(mem,1,sizeof(mem),fp);
    fclose(fp);

    //reset the cpu
    pc=0; //I know this is not right!
    a=0;
    x=0;
    y=0;
    sr=0;
    sp=0;
    //go
    while(1)
    {
        opcode=mem[pc];
        printf("\n0x%04X: 0x%02X\n",pc,opcode);
        pc++;
        if(opcode==0x29) //and
        {
            operand=mem[pc];
            printf("0x%04X: 0x%02X\n",pc,operand);
            pc++;
            printf("and #$%02X\n",operand);
            a&=operand;
            if(a==0) sr|=2; else sr&=(~2);
            sr&=0x7F; sr|=a&0x80;
            printf("a = $%02X sr = $%02X\n",a,sr);
            continue;
        }
        if(opcode==0x09) //ora
        {
            operand=mem[pc];
            printf("0x%04X: 0x%02X\n",pc,operand);
            pc++;
            printf("ora #$%02X\n",operand);
            a|=operand;
            if(a==0) sr|=2; else sr&=(~2);
            sr&=0x7F; sr|=a&0x80;
            printf("a = $%02X sr = $%02X\n",a,sr);
            continue;
        }
        if(opcode==0x2A) //rol
        {
            printf("rol\n");
            temp=a;
            a<<=1;
            a|=sr&0x01;
            sr&=(~0x01); if(temp&0x80) sr|=0x01;
            if(a==0) sr|=2; else sr&=(~2);
            sr&=0x7F; sr|=a&0x80;
            printf("a = $%02X sr = $%02X\n",a,sr);
            continue;
        }
        if(opcode==0x90) //bcc
        {
            operand=mem[pc];
            printf("0x%04X: 0x%02X\n",pc,operand);
            pc++;
            dest=operand;
            if(dest&0x80) dest|=0xFF00;
            dest+=pc;
            printf("bcc #$%04X\n",dest);
            if(sr&1)
            {
            }
            else
            {
                pc=dest;
            }
            continue;
        }
        printf("UNKNOWN OPCODE\n");
        break;
    }
    return(0);
}

以及该简单程序的模拟器输出。

0x0000: 0x29
0x0001: 0x00
and #$00
a = $00 sr = $02

0x0002: 0x09
0x0003: 0x01
ora #$01
a = $01 sr = $00

0x0004: 0x2A
rol
a = $02 sr = $00

0x0005: 0x90
0x0006: 0xFD
bcc #$0004

0x0004: 0x2A
rol
a = $04 sr = $00

0x0005: 0x90
0x0006: 0xFD
bcc #$0004

0x0004: 0x2A
rol
a = $08 sr = $00

0x0005: 0x90
0x0006: 0xFD
bcc #$0004

0x0004: 0x2A
rol
a = $10 sr = $00

0x0005: 0x90
0x0006: 0xFD
bcc #$0004

0x0004: 0x2A
rol
a = $20 sr = $00

0x0005: 0x90
0x0006: 0xFD
bcc #$0004

0x0004: 0x2A
rol
a = $40 sr = $00

0x0005: 0x90
0x0006: 0xFD
bcc #$0004

0x0004: 0x2A
rol
a = $80 sr = $80

0x0005: 0x90
0x0006: 0xFD
bcc #$0004

0x0004: 0x2A
rol
a = $00 sr = $03

0x0005: 0x90
0x0006: 0xFD
bcc #$0004

0x0007: 0x29
0x0008: 0x00
and #$00
a = $00 sr = $03

0x0009: 0xFF
UNKNOWN OPCODE

如果您从头开始编写完整的 6502 指令集,那么它是一个长周末的最低工作量,当您发现一些东西时,您可能最终会重新启动项目几次,非常自然。处理器的硬件(通常,不一定是 6502)在概念上与模拟器中发生的情况并没有太大的不同。您必须获取指令、解码指令、获取操作数、执行并保存结果。就像在软件中执行此操作一样,在硬件中,您可以创建有趣的方法来使其更快或更小,或者无论您的目标是什么。

如果你实现整个东西,6502仍然是一个大项目,没有z80那么大,但是像risc16这样的东西可能需要半个小时来理解和编写整个模拟器(然后再花半个小时来制作一个汇编器)。pic12、14 或 16 比 risc16 工作量更大,但也不会太多,可以快速完成它,并且是一种设计简单的教育体验。pdp11 和 msp430 毫无疑问在某种程度上是相关的,两者都有很好的记录(我提到的所有这些都有很好的记录)并且很好/大部分是正交的,并且类似 RISC 的解码与像 6502/z80/x86 这样的 cisc 是不同的体验。(gcc/gnu 工具原生支持 pdp11)。如果您可以围绕分支延迟槽运行算法,则 Mips 非常简单。

祝你好运,玩得开心……

于 2013-04-19T00:57:57.387 回答
4

麻省理工学院将其计算结构课程 6.004 的所有资源放到网上。它本质上是一个计算机体系结构简介课程,非常注重基于项目的学习。

OpenCourseWare 版本在这里。我将特别关注实验室,特别是 2、3、5 和 6,它们将带您从构建加法器到 ALU 一直到非常简单的处理器(Beta)。

最新课程资料的链接在这里

我记得当我上这门课时,第一次在理解简单的逻辑门和电子元件如何转化为我用来模拟它的复杂机器方面,一切都“点击”了。强烈推荐的资源。

于 2014-06-22T01:28:11.750 回答
3

Noam Nisan 和 Shimon Schocken 2005 ISBN 9780262640688 所著的The Elements of Computing Systems是一本可以让您逐步从大门到功能齐全的计算机甚至更远的文本。他们的网站位于www.nand2tetris.org

于 2013-04-18T17:29:48.507 回答
0

《汇编语言的艺术》的前几章讨论了如何使用锁存器物理存储位,并介绍了如何使用锁存器来组装寄存器以保存数据字节,以及如何在硬件中实现诸如左/右移位之类的指令。可能没有你想要的那么深入,但是当我读到这些东西时,它真的让我大开眼界。

于 2013-05-14T22:12:42.680 回答