More than a year ago I've faced with Rust and become a fan of that language & ecosystem. And I belive - the potantial of Rust is much higher than just use it as desktop compiler, thats why it is coming to browsers (wasm), to microcontrollers, and even to GPUs. But what if we can go futher and try to compile Rust code (modified of course) to FPGA? Sounds promising, we already saw such approach in scala (Chisel), python (myhdl) and others, even rust-inspired (hoodlum).
But we need that it will be a valid Rust code!
Here is how simple combinatorial full adder may looks:
///
/// function without async - is a combinatorial functions they always will be inlined
/// it is a sort of `module` in verilog, but without state
///
fn sum(a: i1, b: i1, c: i1) -> (i1, i1) {
let a_xor_b = a ^ b;
let sum = a_xor_b ^ c;
let carry = (a & b) | (a_xor_b & c);
(carry, sum)
}
///
/// we can leverage the power of rust's trait system
///
fn adder<T: Radix + Default>(a: T, b: T) -> (i1, T) {
let res = T::default();
let c = false;
for i in 0 ..= T::Length { // it will be unrolled.
let (c, r) = sum(a.bit(i), b.bit(i), c);
res.assign(i, r);
}
(c, res)
}
Lets imagine how the simple Counter may looks like:
///
/// struct is a entity with state
///
pub struct Counter {
val: Reg<u32>
}
impl Counter {
pub fn new() -> Self {
Counter {
val: Reg::default()
}
}
///
/// `inc` is a signal
/// it has no input data and no output data,
/// just a pin where we can send a pulse and Counter will count up
///
pub async fn inc(&self) {
await self.val.set(self.val.get() + 1);
}
pub fn read(&self) -> u32 {
self.val.get()
}
pub async fn reset(&self) {
await self.val.set(0);
}
}
Hmm, looks interesting, but why do we need async
?
Verilog has an interesting operator - <=
.
I thing, basicaly, it does very similar what rust compiler do with async
functions - generating the state machine.
Now let imagine how can we use the Counter:
pub struct Display<U: Serial> {
serial: U
}
impl<U: Serial> Display<U> {
pub fn new(serial: U) -> Self {
Self { serial }
}
pub async fn print<R: Radix>(&self, val: R) {
await self.serial.send(val.byte(0));
await self.serial.send(val.byte(1));
await self.serial.send(val.byte(2));
await self.serial.send(val.byte(3));
}
}
pub struct Main<U: Serial> {
cnt: Counter,
disp: Display<U>,
}
impl<U: Serial> Main<U> {
pub fn new(serial: U) -> Self {
Main {
cnt: Counter::new(),
disp: Display::new(serial),
}
}
pub fn operation(&self, a: u32, b: u32) -> u32 {
let c = a * b;
let (_, res) = adder(self.cnt.val.get(), c);
res
}
///
/// here the `test` basicaly is a state machine
///
pub async fn test(&self, clk: Wire<u1>, rst: Wire<u1>) {
loop {
await select! {
rst_sig @ rst.event() => if rst_sig.is_posedge() {
await self.cnt.reset();
},
clk_sig @ clk.event() => if clk_sig.is_posedge() {
if self.cnt.read() == 12 {
await self.disp.print(self.operation(self.cnt.read(), 12));
await self.cnt.reset();
}
await self.cnt.inc();
await self.disp.print(self.cnt.read());
}
};
}
}
}
It looks pretty much to regular Rust's code, and it will be possible to compile such code to varity of targets, on x86 it will be a simulator, but for fpga it will be either Verilog code or sort of netlist (as a LLVM backend that can generate Verilog from subset of IR).
Pros:
- utilize Rust ecosystem (cargo, RLS, etc.)
- it will be a yet another target for rust (not another project), and it will be supported by community as long as community support Rust language itself
- most of Rust's features will be in use (i.e. trait system)
- shared libraries
Cons:
- some of features will be not possible to use (such as references and borrowings)
- developers is still has to understand RTL basics to code
What are your opinion, guys?