1 个不稳定版本
0.1.0 | 2022年4月8日 |
---|
#7 in #electronics
在 pcb-rs 中使用
59KB
1K SLoC
Pcb-rs
一个易于编写软件模拟硬件的库
此库提供两个宏 Chip
和 pcb
,可用于编写软件模拟硬件组件。 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
IO 锁存器
每个IO类型的引脚都必须有一个与之关联的io锁存器,以指示引脚处于输入状态还是输出状态。这必须是一个芯片结构成员,类型为bool。这是由pcb宏生成的pcb在运行时确保io引脚处于正确状态所使用的。有关更多详细信息,请参阅pcb宏部分。
- 如果为真,则io引脚处于输入模式,因此连接的引脚可以处于输出模式,在这种情况下,其值将传递给此引脚
- 如果为假,则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、输入或输出引脚。输入到输入和输出到输出的连接是无效的。
- 暴露的引脚设置正确,再次请参阅暴露的引脚
第一个芯片部分是必需的,其中pin-connection或暴露的引脚部分之一是必要的。有关更多详细信息,请参阅pcb语法。
生成的PCB结构将自身实现Chip
特质,因此可以作为某些其他pcb中的芯片使用。
关于引脚值传输的说明
连接引脚传输引脚值的基本方法是,在pcb的tick函数中迭代添加的芯片,并调用它们的tick函数。然后,它将连接的引脚的值传递给连接的引脚。请注意,芯片的顺序是不可确定的,不应依赖于它。
此tick实现提供的唯一保证是,在调用芯片的tick之后,连接引脚的值将在下一次调用tick之前传递给相应的连接引脚。没有关于何时或按何种顺序设置值的精确保证。
需要注意的是,从一块芯片到另一块芯片传递值时,从芯片的角度看,将会有一个时钟周期的延迟。因此,在时钟周期 ti 设置到输出引脚的值将在时钟周期 ti+1 被连接的芯片看到。如果您期望从另一块芯片获取数据,例如 CPU 向 RAM 发送地址并从其中获取数据,那么获取数据线上的数据将必然需要 2 个时钟周期,即在 ti 拍钟时 CPU 将地址设置在地址引脚上,RAM 将在 ti+1 拍钟时看到它,并在该拍钟时将数据放置在其数据引脚上,这将由 CPU 在下一个拍钟(即 ti+2)的引脚上看到。
如前所述,芯片本身不应直接依赖于调用 tick 函数的非确定顺序,如果您特别想引入竞争条件,更好的选择是创建一个模拟这种非确定行为的芯片,并将需要非确定行为的芯片封装在这个芯片内。当您需要运行在较慢时钟速度下的芯片时,也应采用类似的方法。
库公开的特性和 PCB 接口
此库主要公开以下特性,这些特性通常由宏实现,但在需要的情况下也可以手动实现。
ChipInterface
此特性标记一个结构体为 Chip,它可以在 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,因此该接口的函数也是可用的。请参阅 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),或者使用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
必须与对应引脚的结构体成员名称相同。 - 最后是暴露的引脚格式
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 (LICENSE-APACHE 或 http://www.apache.org/licenses/LICENSE-2.0)
- MIT许可证 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
任选其一。
贡献
除非你明确表示,否则,根据Apache-2.0许可证的定义,你有意提交给作品的所有贡献,都将按照上述方式双许可,而不附加任何额外的条款或条件。
依赖项
~1.5MB
~35K SLoC