1 个不稳定版本
0.1.0 | 2022年4月8日 |
---|
#131 in 仿真
19KB
210 代码行
Pcb-rs
一个易于编写软件仿真硬件的库
此库提供了两个宏 Chip
和 pcb
,可用于编写软件仿真硬件组件。 Chip
是一个派生宏,可用于在结构体上自动实现作为硬件芯片对待的结构体所需接口,您只需实现一个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类型的引脚时,例如RAM的数据总线,它必须设置为三态。这解释得更详细,在注释部分中,但简而言之,如果您想要将多个输出引脚连接到同一个输入引脚,它们必须是三态的。三态引脚由最外层的Option类型指示,并且它以一种色彩标记引脚,表示它只能连接到其他三态引脚,不能连接到非三态引脚。理论上有一种情况,一个IO引脚可以连接到多个引脚,而不需要是三态的,即当所有其他引脚都是输入类型时。这种情况可以简化为一个更简单的情况——将IO引脚改为输出引脚,因为当IO引脚也是输入状态时,什么也不会发生,因为没有输入引脚在组中。因此,在这种情况下应该将IO改为仅输出引脚,因此这个库假定所有IO引脚都会连接到多个输入和输出引脚,因此必须要是三态的。
通过IO引脚发送/接收的实际数据将是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中的芯片使用。
关于引脚值传输的说明
连接引脚值传输的基本方式是,在pcb的tick函数中,它遍历添加的芯片,并调用它们的tick函数。然后它将连接引脚的值传递给连接的引脚。请注意,芯片的顺序是不确定的,不应依赖于它。
此tick实现提供的唯一保证是,在调用芯片的tick之后,连接引脚的值将在下一次调用tick之前传递给相应的连接引脚。没有关于何时以及以何种顺序设置值的精确保证。
需要注意的另一件事是,从一块芯片到另一块芯片传递值,从芯片的角度来看,会有恰好一个时钟周期的延迟。因此,在时钟周期 ti 设置到输出引脚的值,将在时钟周期 ti+1 被连接的芯片看到。如果你期望从另一块芯片(如CPU给RAM发送地址并从它那里获取数据)的数据,那么在数据线上获取数据必然需要2个时钟周期,即在第 ti 次滴答时,CPU将在地址引脚上设置地址,它将在 ti+1 次滴答时被RAM看到,并在此滴答中将数据放置在其数据引脚上,这将由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
这是一个仅用于标记结构体可以作为芯片使用的特性。对于实现 ChipInterface
、Chip
和 Downcast
的任何类型,都会自动实现,因此不需要手动实现。
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,因此该函数也是可用的。有关使用 get_value 和 set_value 方法(可能经常使用)的示例,请参阅 https://github.com/YJDoc2/pcb-rs-examples。
注意事项
遗憾的是,这只是一个硬件模拟库,因此在某些方面无法精确模拟现实世界的硬件。以下注释显示了该库的一些特性。
可三态引脚
在实际的PCB中,各个引脚只能传输电压(因此是比特),并且它们相互连接。在这里,我们允许引脚具有更复杂的数据类型,但这可能会在运行时导致恐慌。(从技术上讲,类型在文件级别解析,两个文件可以有两个相同名称的不同类型,并且在生成宏时,类型没有解析,所以在运行时如果没有足够的信息,我们无法确定这两个类型是否相同。)
如果我们只允许每个引脚只有一个连接,那么实现连接多个设备的芯片可能会变得复杂,甚至可能根本无法建立共享连接。例如:在某个特定系统中,RAM必须连接到CPU和DMA模块。现在如果我们不允许多个连接,我们就不能将RAM的数据引脚连接到CPU和DMA。这意味着只能有一个设备可以访问RAM,或者我们必须在RAM和其他组件之间添加一个间接层,以便CPU和DMA可以请求这个组件,而这个组件的引脚将连接到RAM。即便如此,在这个组件中,由于相同的问题,我们也不能将间接芯片的数据引脚和允许引脚都连接到CPU和DMA。这意味着我们需要为每个连接的组件提供一个引脚,并且需要一种基于优先级的机制来处理多个组件请求访问数据引脚的情况。当需要连接的组件数量增加时,这可能会变得非常低效。
在现实世界中,这个问题通常通过两种方法来解决:请参阅这里以获得良好的解释。在这个库中,我们使用三态逻辑。这样,理想情况下,只有连接的芯片之一会有有效的输出(高或低),其他芯片将处于高阻态,在这种状态下,该引脚实际上就像没有连接一样。尽管在多个芯片连接到同一引脚并在同一时间进入非高阻态的情况下,可能会引起问题,甚至可能损坏实际芯片。也请参阅这里。
在这个库中,我们使用rust std::option::Option来表示一个引脚是三态的,并且只有当所有引脚都是三态时,才允许多个引脚连接到同一引脚。当多个三态引脚同时具有Some(_)时,这相当于多个引脚同时处于高/低状态,因此代码将在运行时恐慌,相当于芯片烧毁。
三态引脚必须用std::option::Option类型包装,并且可以使用完全限定的路径(std::option::Option / ::std::option::Option)或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>
这里至少需要一个<chip-name>::<pin-name>
在expose
之后,可以在这里以逗号分隔的形式指定多个引脚。在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 (LICENSE-APACHE 或 http://apache.ac.cn/licenses/LICENSE-2.0)
- MIT许可证 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
任选。
贡献
除非您明确声明,否则您有意提交以供包含在您的工作中的任何贡献,根据Apache-2.0许可证定义,应按上述方式双重许可,而不附加任何额外条款或条件。
依赖关系
~1.5MB
~35K SLoC