1 个不稳定版本

0.1.0 2022 年 4 月 8 日

#295模拟


用于 2 crates

MIT/Apache

12KB
121

Pcb-rs

一个易于编写软件模拟硬件的库


此库提供了两个宏 Chippcb,可以用于编写软件模拟的硬件组件。 Chip 是一个 derive 宏,可以用于结构体,自动实现结构体作为硬件芯片所需的必要接口,您只需实现每个时钟周期将被调用的 tick 函数来运行芯片的逻辑。 pcb 宏用于定义一个 PCB,您可以在其中连接多个芯片,并且它将管理连接芯片的引脚、验证连接并将数据传递到连接的芯片。

此库的一个目标是实现模块化和可重用性,因此创建的 PCB 可以进一步用作其他 PCB 中的芯片,依此类推。

在创建芯片时有一些需要注意的细节,并在解释后 列出

有关使用这些内容实现各种芯片的示例,请参阅 https://github.com/YJDoc2/pcb-rs-examples .

动机

这背后的主要动机是帮助那些希望轻松探索硬件和底层系统的人。由于几个原因,学习硬件设计可能很困难:它不易获得,通常需要面包板、芯片和其他东西来实现基本电路,而且您想要实现的电路越复杂,就越复杂。在软件中做这件事,VHDL 是一个非常强大的替代方案:您可以用它来描述硬件,它可以在您的硬件上合成和运行测试。它甚至可以将它转换为可以直接与 FPGA 一起使用的格式,以实现您的电路为实际硬件。但是它很复杂,有点啰嗦,并且(个人而言)有点令人畏惧。

此库旨在提供一个更简单的方法进入硬件领域,允许您在 Rust 的舒适环境中编写代码,同时也让您了解设计硬件时必须考虑的因素。这牺牲了 VHDL 提供的很多功能,但这是一种权衡,而且由于这个库的目标不是“取代”甚至像 VHDL 之类的替代品,所以这是可以接受的。

芯片宏

这是一个派生宏,它将为用作硬件芯片的结构实现必要的特性和特质。在这里,硬件芯片指的是它可以作为由 pcb-macro 生成的 pcb 中的组件芯片使用。您需要注释要公开为引脚的结构成员,然后实现 HardwareChip 接口,它包含所需的 tick 函数。此函数是芯片处理逻辑所在之处。如果在 pcb-macro 生成的 pcb 中使用,此函数将在每个模拟时钟滴答时被调用。

示例用法

这是一个示例,展示了如何使用此宏将结构体转换为芯片。

use pcb_rs::*;

// Add the derive(Chip), so it will implement the necessary interfaces for your struct
#[derive(Chip)]
struct MyChip{

    // This indicates that this chip has a pin named 'pin1' of type 'u8'
    // and which is an input pin
    #[pin(input)]
    pub pin1 :u8,

    // This indicates that this chip has a pin named 'pin2' of type 'bool'
    // and which is an output pin
    #[pin(output)]
    pub pin2 :bool,

    // This indicates that this chip has a pin named 'pin3' of type 'String'
    // and which is an io pin, and its status is indicated by the
    // 'io_latch' member of this struct
    // Why this needs to be an option is explained after this codeblock
    #[pin(io,io_latch)]
    pub pin3 : Option<String>,

    // Members not marked by #[pin(...)] are not exposed as pins
    // Thus the following are essentially an internal state for this chip

    io_latch:bool,
    some_data1:u8,
    some_data2:String
}

impl Chip for MyChip{
    fn tick(&mut self){
        // here you can implement the logic of the chip
        // this takes &mut self, so you can read values set for input pins
        // and set values for the output pins, so they can be sent to
        // other connected chips in a pcb
    }
}

这里有几个需要注意的地方

引脚数据类型

引脚数据类型可以是任何类型。这样做是为了让人们可以选择自己的难度级别。

  • 您想要8个 bool 引脚,当然。
  • 想要一个 u8 吗?很简单。
  • 想要以字符串格式发送操作码吗?嗯,好吧...

请勿误用此功能!尽量保持引脚数据类型简单,如 u8 等内置数据类型或最坏的情况是 String 或类似拥有数据类型的类型。如果您 必须 使引脚数据类型为结构体或确保重新思考,然后在该结构体上实现 Clone。枚举也可以接受,只要它们的组件遵守上述规则。确保同样实现 clone

如果您打算让他人使用芯片,请选择一个数据类型名称表示法,并坚持使用它。参见 引脚类型说明 了解原因。我推荐的方法是使用 rust std 类型的完全限定路径,并为也由您的库公开的自定义类型使用类型名称。虽然这不是硬性要求,但只要使用芯片的每个人都同意如何限定类型名称,就可以。虽然您的芯片可能与可能引起问题的其他芯片一起使用,而且我真的很不想做xkcd-标准的事情

IO 引脚
数据类型

当您需要一个 IO 类型的引脚时,例如 RAM 的数据总线,它必须设置为三态。这解释得详细一点在 说明部分,但简而言之,如果您想将多个输出引脚连接到同一个输入引脚,它们必须是三态的。三态引脚由最外层包装类型为 Option 来表示,并且它有点像颜色标记了引脚,它只能连接到其他三态引脚,而不能连接到非三态引脚。理论上有一个情况,IO 引脚可以连接到多个引脚,而无需设置为三态,即所有其他引脚都是输入类型。这种情况可以简化为一个更简单的情况——将 IO 引脚简单地作为输出引脚,因为当 IO 引脚也处于输入状态时,什么也不会发生,因为没有输入引脚在组中。因此,在这种情况下,应该将 IO 更改为仅输出引脚,因此此库假定所有 IO 引脚都会连接到多个输入和输出引脚,因此必须设置为三态。

IO 引脚实际发送/接收的数据将是 Option 中包含的类型,即 Option 中的 T。因此,如果您想要数据类型为 Option 本身,请再次阅读引脚数据类型部分,并且如果您仍然确信,请将其包装在 Option 中,因此数据引脚的最终类型将是 Option>。

IO 锁存器

每个IO类型的引脚都必须有一个关联的io锁存器来指示该引脚处于输入状态还是输出状态。这必须是一个芯片结构体成员,类型为bool。这被pcb宏生成的pcb用于确保在运行时IO引脚处于正确的状态。有关更多信息,请参阅pcb宏部分。

  • 如果为true,则IO引脚处于输入模式,因此连接的引脚可以处于输出模式,在这种情况下,其值将传递给此引脚。
  • 如果为false,则IO引脚处于输出模式,因此IO引脚的值将传递给处于输入状态的连接IO引脚。

pcb在运行时检查最多一个引脚处于输出模式。

PCB宏

这是一个功能宏,可以用来指定和获取多个芯片连接的实现。它基本上接受关于在PCB中哪些芯片、它们如何连接以及哪些引脚暴露在PCB外的简单文本信息,并创建一个构建器逻辑来验证给定的芯片和一个实现所需特质的PCB结构体。

芯片在运行时由构建器给出并验证以生成结构体。

use pcb_rs::*;

// This will create two structs : 'MyPCBBuilder' and 'MyPCB'
// both will be public ( with 'pub' access specifier).
pcb!(MyPCB{
    // These comments are allowed
    /* and these */
    // but not doc comments

    // fist list all the chips that will be used in this pcb
    // the names are not required to be same as the actual chip struct
    chip c1;
    chip c2;
    chip c3;

    // now declare the connections
    // pin names MUST be same as the chip struct member names
    c1::pin1 - c2::pin2;
    c2::pin3 - c3::p1;
    c3::p2 - c1::pin2;

    // Now list the pins which should be exposed by the pcb
    expose c1::pin3 as pin1;
    expose c2::pin1,c3::p4 as p5;

});

fn main(){
    // these var names can be anything, I prefer to keep them same
    // as the chip names in the above declaration for simplicity
    let c1 = Box::new(MyChip1::default());
    let c2 = Box::new(MyChip2::default());
    let c3 = Box::new(MyChip3::default());

    let temp = MyPCBBuilder::new();
    // first param to add_chip is the chip name,
    // as declared in the pcb!(..)
    let pcb = temp.add_chip("c1",c1)
                .add_chip("c2",c2)
                .add_chip("c3",c3)
                .build().unwrap();
    // do stuff with the pcb, generally put in a loop and call tick()
}

构建器结构体提供了add_chip(name_str,boxed_chip)函数来将芯片添加到PCB。在build()调用中,它验证添加的芯片,并验证

  • 所有列出的芯片都已添加
  • 芯片具有所需的引脚,如连接和暴露的引脚
  • 连接的引脚具有正确的数据类型,并且是兼容的类型。请参阅引脚类型暴露的引脚。这里的兼容类型意味着输入引脚可以连接到输出或IO引脚,输出引脚可以连接到输入或IO引脚,而IO引脚可以连接到IO、输入或输出引脚。输入到输入和输出到输出的连接是无效的。
  • 暴露的引脚设置正确,请再次参阅暴露的引脚

第一个芯片部分是必需的,而引脚连接或暴露的引脚部分是必要的。有关更多信息,请参阅PCB语法

生成的PCB结构体会自己实现Chip特质,因此可以用作某些其他PCB中的Chip。

关于引脚值传递的说明

连接引脚的基本值传递方式是,在PCB的tick函数中,它遍历添加的芯片,并调用它们的tick函数。然后它获取连接的引脚的值,并将它们传递给连接的引脚。请注意,芯片的顺序是不确定的,不应依赖。

此tick实现唯一保证的是,在调用芯片的tick之后,连接引脚的值将在下一次调用tick之前传递给相应的连接引脚。没有关于何时或以何种顺序设置值的精确保证。

需要注意的另一件事是,从芯片的角度来看,从一个芯片到另一个芯片传递值将会有一个精确的时钟周期延迟。因此,在时钟周期 ti 设置到输出引脚的值将在时钟周期 ti+1 被连接的芯片看到。如果您期望从另一个芯片接收数据,例如CPU向RAM发送地址并从它那里获取数据,则必然需要2个时钟周期来在RAM的数据引脚上获取数据,即在第 ti 次滴答时,CPU将地址设置在地址引脚上,RAM将在 ti+1 次滴答时看到它,并在此滴答时将数据放置在其数据引脚上,CPU将在下一个滴答(即 ti+2)时在其数据引脚上看到这些数据。

如前所述,芯片本身不应直接依赖于调用 tick 函数的非确定性顺序,如果您特别希望有竞态条件,更好的选择是创建一个模拟这种非确定性行为的芯片,并将需要非确定性行为的芯片包装在这个芯片中。当您需要以较慢的时钟速度运行的芯片时,应使用类似的方法。

库公开的特性和PCB接口

此库主要公开以下特性,通常由宏实现,但在需要的情况下也可以手动实现。

ChipInterface

此特性将一个结构体标记为芯片,该芯片可以用在PCB上。这通常通过 Chip 宏派生。

pub trait ChipInterface {
    /// gives a mapping from pin name to pin metadata
    fn get_pin_list(&self) -> HashMap<&'static str, PinMetadata>;

    /// returns value of a specific pin, typecasted to Any
    fn get_pin_value(&self, name: &str) -> Option<Box<dyn Any>>;

    /// sets value of a specific pin, from the given reference
    fn set_pin_value(&mut self, name: &str, val: &dyn Any);

    // The reason to include it in Chip interface, rather than anywhere else,
    // is that I couldn't find a more elegant solution that can either directly
    // implement on pin values which are typecasted to dyn Any. Thus the only way
    // that we can absolutely make sure if a pin is tristated or not is in the
    // Chip-level rather than the pin level. One major issue is that the data of
    // which type the pin is is only available in the Chip derive macro, and cannot be
    // used by the encompassing module in a way that will allow its usage in user programs
    // which does not depend on syn/quote libs.
    /// This is used to check if a tristatable pin is tristated or not
    fn is_pin_tristated(&self, name: &str) -> bool;

    /// This returns if the io pin is in input mode or not, and false for other pins
    fn in_input_mode(&self, name: &str) -> bool;
}

Chip

这包含芯片的实际逻辑,并且通常在芯片的情况下需要手动实现。对于PCB,pcb! 宏为芯片实现了这一点。

pub trait Chip {
    /// this will be called on each clock tick by encompassing module (usually derived by pcb! macro)
    /// and should contain the logic which is to be "implemented" by the chip.
    ///
    /// Before calling this function the values of input pins wil be updated according to
    /// which other pins are connected to those, but does not guarantee
    /// what value will be set in case if multiple output pins are connected to a single input pin.
    ///
    /// After calling this function, and before the next call of this function, the values of
    /// output pins will be gathered by the encompassing module, to be given to the input pins before
    /// next call of this.
    ///
    /// Thus ideally this function should check values of its input pins, take according actions and
    /// set values of output pins. Although in case the chip itself needs to do something else, (eg logging etc)
    /// it can simply do that and not set any pin to output in its struct declaration.
    fn tick(&mut self) -> ();
}

HardwareModule

这是一个标记特性,用于指示一个结构体可以用作芯片。对于实现了 ChipInterfaceChipDowncast 的任何类型,它会自动实现,因此不需要手动实现。

pub trait HardwareModule: ChipInterface + Chip + Downcast {}

PCB构建器接口

由 pcb! 宏生成的构建器结构体具有以下公共函数

new() -> Builder

创建一个新的构建器

add_chip(chip_name,boxed_hardware_module) -> ()

这将在PCB中添加一个芯片。名称必须与在 pcb!(...) 中定义的芯片列表中的名称相同,boxed_hardware_module 是实际芯片,它实现了 HardwareModule 特性,并包装在 Box 中。

build(mut self)->std::result::Result<pcb, error>

这验证了添加的芯片,如果正确,则返回包含芯片和功能逻辑的 pcb 结构体。

PCB接口

由 pcb! 宏生成的 pcb 结构体具有以下公共函数

get_chip(&self,chip_name)->Option<&T>

如果有的话,返回具有给定名称的组件芯片的不可变引用。

get_chip_mut(&mut self,chip_name)->Option<&mut T>

如果有的话,返回具有给定名称的组件芯片的可变引用。

注意:对于上述两种情况,由于PCB无法知道芯片的类型,必须在存储返回芯片的变量上手动添加类型注解,即

let t :&MyChip1 = pcb.get_chip("chip1").unwrap();
let t :&mut MyChip2 = pcb.get_chip_mut("chip2").unwrap();

除此之外,PCB还实现了 ChipInterface,因此该函数也是可用的。请参阅 https://github.com/YJDoc2/pcb-rs-examples 中的示例,了解如何使用 get_value 和 set_value 方法,这些方法可能经常使用。

注意

遗憾的是,这只是一个硬件模拟库,因此有一些边缘情况无法精确模拟现实世界的硬件。以下注释显示了该库的怪癖。

三态引脚

在现实PCB中,单个引脚只能传输电压(从而是位),并相互连接。在这里,我们允许引脚具有更复杂的数据类型,但代价是可能发生运行时恐慌。(因为技术上类型是在文件级别解析的,两个文件可以有两个具有相同名称的不同类型,在生成宏时,类型没有解析,所以我们没有足够的信息知道这两种类型是否相同,直到运行时。)

如果我们只允许每个引脚一个连接,那么连接多个设备的芯片实现将变得复杂,甚至可能根本无法建立共享连接。例如:在某个系统中,RAM必须连接到CPU和DMA模块。现在如果我们不允许多个连接,我们就无法将RAM的数据引脚连接到CPU和DMA。这意味着只能有一个设备可以访问RAM,或者我们必须在RAM和其他组件之间添加一层间接层,使得CPU和DMA请求这个组件,而这个组件的引脚将连接到RAM。即使这样,在这个组件中,我们也无法将那个间接芯片的数据引脚允许引脚同时连接到两个设备,因为存在同样的问题。这意味着我们需要为每个连接的组件分配一个引脚,并且需要一些基于优先级的机制来处理多个组件请求访问数据引脚的情况。当需要连接的组件数量增加时,这会变得相当低效。

在现实世界中,这类问题通常通过两种方法解决:请参阅这里获取详细的解释。在这个库中,我们使用三态逻辑。这样,理想情况下,连接的芯片中只有一个是有效的输出(高电平或低电平),而其他芯片将处于高阻态,此时该引脚实际上就像没有连接一样。尽管在多个芯片连接到同一引脚并在同一时间进入非高阻态时,可能会引发问题,甚至可能烧毁实际的芯片。也请参阅这里

在这个库中,我们使用rust std::option::Option来表示一个引脚是三态的,并且只有当所有引脚都是三态时,才允许多个引脚连接到同一个引脚。当多个三态引脚同时存在Some(_)时,这相当于多个引脚同时处于高电平/低电平状态,因此代码在运行时会崩溃,相当于芯片被烧毁。

三态引脚必须被std::option::Option类型包装,std::option::Option可以使用完全限定路径(std::option::Option / ::std::option::Option),或者在使用use std::option之前)或直接使用Option。任何其他使用方式都不会被视为三态引脚。

这种实现方式不是最佳或最优雅的方式,但这是我唯一能找到的可行方式。

关于引脚三态的说明

输入引脚绝不能处于三态(设置为None),除非您希望忽略输入。将输入类型设置为三态引脚将导致PCB不发送连接的输出引脚值,因为在现实生活中,三态就像将该引脚从板上移除一样,跳过这种行为的目的与此类似。

PCB的语法!

请注意,引脚名称不能是Rust关键字。pcb!宏有三个部分,必须按特定顺序列出。分号是重要的且必需的。在宏中可以有//注释和/**/注释,但不能有///注释(文档注释)。

  • 首先列出芯片声明的第一个列表,格式为chip <chip-name>;。这是一个必要部分,因为没有芯片的PCB是没有意义的。
  • 然后是引脚连接的列表,格式如下:<chip-name>::<pin-name> - <chip-name>::<pin-name>;这里的chip-name对应于第一部分中声明的芯片名称。而pin-name必须与对应引脚的struct成员名称相同。
  • 最后是暴露的引脚,格式如下:expose <chip-name>::<pin-name>(,<chip-name>::<pin-name>)* as <pin-name>。在这里,至少需要在expose之后提供一个<chip-name>::<pin-name>,并且可以在这里指定多个引脚,以逗号分隔。在as之后使用的<pin-name>将作为pcb暴露的引脚名称,并且当此pcb作为其他pcb中的芯片使用时应该使用。

除了这些,必须指定连接列表或暴露的引脚之一,或者两者都可以指定。

请参阅暴露的引脚部分的注释,以了解指定多个引脚作为单个引脚的精确语义。

关于引脚类型

不幸的是,因为类型信息在宏展开时没有解析(并且还没有表示类型的类型),我们使用类型字符串来表示PinMetadata中的类型。不幸的是,这意味着对于芯片的连接引脚,类型在作为字符串处理时必须完全相同。

  • u8是有效的,但是

  • std::option::Option和Option不会被当作相同的类型处理

因为它们的字符串表示不同。请注意,这可能导致运行时错误(即在构建pcb之后),因为Option可以表示两个不同文件中的两种不同类型。我不知道如何解决这个问题,因此最好除了原始数据类型之外使用完全显式的类型。

关于io引脚的使用

当涉及引脚的io类型时,这个库有一些偏见。仅在引脚将被用于输入和输出时才应声明引脚IO类型。不要为每个引脚使用io,并且每个io引脚的锁存变量应作为结构体的非引脚成员保留,并且不应暴露。它应该是bool类型。

关于暴露引脚短接

pcb! 允许在硬件中模拟短路引脚的同时,将多个引脚暴露为一个引脚。这在某些情况下很有用,例如,当某个门的输入被暴露,并且相同的值连接到一个非门,然后将非门的输出提供给另一个门(例如D触发器)时;或者你可能希望将相同的输入提供给多个芯片。

尽管如此,当将多个芯片暴露为单个芯片时,必须遵循一些规则,如果不遵循这些规则,则行为是未定义的。

  • 在暴露时,只能短路输入类型引脚。输出类型引脚的短路没有意义,因为两个输出可能不同,并且没有确定的方法来确定哪个值应作为短路单个暴露引脚的值,除非有一些任意规则或限制。因此,只能短路和暴露输入类型引脚。

  • 如果短路并暴露了多个引脚,则这些引脚不应连接到PCB上的任何其他引脚。这是通过以下方式实现的:

    • 如果与任何短路引脚连接的内部引脚是输入类型且未连接到任何其他引脚,则它也应该以相同的方式暴露。
    • 否则,该引脚是输出类型或IO类型。连接到输出类型是不正确的,因为:
      • 对于输出引脚,这意味着应将输出引脚的值提供给输入引脚,但我们暴露输入引脚并短路它们,以便可以从外部设置值,从而类似于第1点的打结问题。
      • 对于IO引脚,暴露的引脚也必须是三态的,并且当IO引脚处于输出状态时,将出现与上述相同的问题。
  • 如果外部芯片中的三态引脚不在三态状态(Some(...)),则将设置其值;否则,当设置从外部接收的值时,将忽略这些值。

  • 尽可能不要短路三态引脚,因为在不正常情况下它们可能会产生未定义的行为。


许可证

根据您的选择,许可如下:

贡献

除非您明确声明,否则您提交的任何有意包含在作品中的贡献(根据Apache-2.0许可证定义),均应按上述方式双重许可,而不附加任何额外条款或条件。

依赖项

~33KB