Why I Rewrote Keyboard Firmware from Rust to Zig: Coherence, Craftsmanship, and Fun

For the last year I have been collecting various keyboards , which includes writing firmware for various control schemes.





Initially, I wrote them in Rust, but despite years of development experience with it, I had to fight. Over time, I got my keyboards working, but it took an obscene amount of time and did not give me pleasure.





After repeated suggestions from my more Rust-and-compute-savvy friend Jamie Brandon , I rewrote the firmware to Zig and it worked out very well.





I found this astonishing, given that I had never seen Zig before, and this is a language, not even version 1.0 yet, created by a hipster at the University of Portland , and is described, in fact, with just one page of documentation .





The experience went so well that I now understand Zig (which has used a dozen hours) as well as Rust (which I have used at least a thousand hours).





This, of course, reflects not only me and my interests, but also applies to each of these languages. Therefore, I will have to explain what I want from a systems programming language in the first place.





Also, to explain why I was struggling with Rust, I have to show a lot of complex code that I definitely don't like. My goal here is not to reproach Rust, but to show my (insufficient) reputation: this is so that you can judge for yourself whether I am using Rust in a rational way, or if I completely lost my way.





, , " X , Y", , , Rust Zig, , "Zig's !". ( , , Zig, " , , Rust, , ?").





, . PostScript Ruby (, ), JavaScript, . Clojure ( ClojureScript ), .





2017 . - , , , , , -, . , , :





  1. , ; , , .





  2. , , web assembly, ( ) , ..





( ) , ( , , ..).





.





, Rust 1.18.





Rust, , , : WASM , (Rust Electron), Rust- stm32g4, - ( ; !).





, Rust. - , , Rust , , /. : , .





, , Rust .





, , , . .





, Rust, , , 4- dev-kit' / Atreus'a:





" ". ( , , , 10-100 ). Rust "features", Cargo.toml



:





[dependencies]
cortex-m = "0.6"
nrf52840-hal = { version = "0.11", optional = true, default-features = false }
nrf52833-hal = { version = "0.11", optional = true, default-features = false }
arraydeque = { version = "0.4", default-features = false }
heapless = "0.5"

[features]
keytron = ["nrf52833"]
keytron-dk = ["nrf52833"]
splitapple = ["nrf52840"]
splitapple-left = ["splitapple"]
splitapple-right = ["splitapple"]

# specify a default here so that rust-analyzer can build the project; when building use --no-default-features to turn this off
default = ["keytron"]

nrf52840 = ["nrf52840-hal"]
nrf52833 = ["nrf52833-hal"]
      
      



, keytron



. nrf52833



( ), nrf52833-hal



( , , Rust). Rust . , , :





#[cfg(feature = "nrf52833")]
pub use nrf52833_hal::pac as hw;

#[cfg(feature = "nrf52840")]
pub use nrf52840_hal::pac as hw;
      
      



:





fn read_keys() -> Packet {
    let device = unsafe { hw::Peripherals::steal() };

    #[cfg(any(feature = "keytron", feature = "keytron-dk"))]
    let u = {
        let p0 = device.P0.in_.read().bits();
        let p1 = device.P1.in_.read().bits();

        //invert because keys are active low
        gpio::P0::pack(!p0) | gpio::P1::pack(!p1)
    };

    #[cfg(feature = "splitapple")]
    let u = gpio::splitapple::read_keys();

    Packet(u)
}
      
      



, :





  • - (any



    #[cfg(any(feature = "keytron", feature = "keytron-dk"))]



    ).





  • optional = true



    , Cargo.toml



    ( !).





  • (cargo build --release --no-default-features --features "keytron"



    )





!





- , , - "" :





fn read_keys(port: #[cfg(feature = "splitapple")]
                   nrf52840_hal::pac::P1
                   #[cfg(feature = "keytron")]
                   nrf52833_hal::pac::P0) -> Packet {}
      
      



, RTIC, app



, , , :





#[app(device = nrf52833)]
const APP: () = {
  //your code here...
};
      
      



? .





Rust .





: , ( ) :





, . , 1.10 (col0) 0.13 ( 1) , , K8 . , Rust :





  1. .





  2. Rust.





, .





, , P0's pin 10, :





P0.pin_cnf[10].write(|w| {
    w.input().disconnect();
    w.dir().output();
    w
});
      
      



, :





for (port, pin) in &[(P0, 10), (P1, 7), ...] {
    port.pin_cnf[pin].write(|w| {
        w.input().disconnect();
        w.dir().output();
        w
    });
}
      
      



, - (P0, usize) (P1, usize) - .





, :





type PinIdx = u8;
type Port = u8;

const COL_PINS: [(Port, PinIdx); 7] =
    [(1, 10), (1, 13), (1, 15), (0, 2), (0, 29), (1, 0), (0, 17)];

pub fn init_gpio() {
    for (port, pin_idx) in &COL_PINS {
        match port {
            0 => {
                device.P0.pin_cnf[*pin_idx as usize].write(|w| {
                    w.input().disconnect();
                    w.dir().output();
                    w
                });
            }
            1 => {
                device.P1.pin_cnf[*pin_idx as usize].write(|w| {
                    w.input().disconnect();
                    w.dir().output();
                    w
                });
            }
            _ => {}
        }
    }
}
      
      



, .





, , , ? , , :





pub fn read_keys() -> u64 {
    let device = unsafe { crate::hw::Peripherals::steal() };

    let mut keys: u64 = 0;

    macro_rules! scan_col {
        ($col_idx: tt; $($row_idx: tt => $key:tt, )* ) => {
            let (port, pin_idx) = COL_PINS[$col_idx];

            ////////////////
            //set col high
            unsafe {
                match port {
                    0 => {
                        device.P0.outset.write(|w| w.bits(1 << pin_idx));
                    }
                    1 => {
                        device.P1.outset.write(|w| w.bits(1 << pin_idx));
                    }
                    _ => {}
                }
            }

            cortex_m::asm::delay(1000);

            //read rows and move into packed keys u64.
            //keys are 1-indexed.
            let val = device.P0.in_.read().bits();
            $(keys |= ((((val >> ROW_PINS[$row_idx]) & 1) as u64) << ($key - 1));)*

            ////////////////
            //set col low
            unsafe {
                match port {
                    0 => {
                        device.P0.outclr.write(|w| w.bits(1 << pin_idx));
                    }
                    1 => {
                        device.P1.outclr.write(|w| w.bits(1 << pin_idx));
                    }
                    _ => {}
                }
            }

        };
    };

    //col_idx; row_idx => key ID
    #[cfg(feature = "splitapple-left")]
    {
        scan_col!(0; 0 => 1 , 1 => 8  , 2 => 15 , 3 => 21 , 4 => 27 , 5 => 33 ,);
        scan_col!(1; 0 => 2 , 1 => 9  , 2 => 16 , 3 => 22 , 4 => 28 , 5 => 34 ,);
        scan_col!(2; 0 => 3 , 1 => 10 , 2 => 17 , 3 => 23 , 4 => 29 , 5 => 35 ,);
        scan_col!(3; 0 => 4 , 1 => 11 , 2 => 18 , 3 => 24 , 4 => 30 , 5 => 36 ,);
        scan_col!(4; 0 => 5 , 1 => 12 , 2 => 19 , 3 => 25 , 4 => 31 , 5 => 37 ,);
        scan_col!(5; 0 => 6 , 1 => 13 , 2 => 20 , 3 => 26 , 4 => 32 , 5 => 38 ,);
        scan_col!(6; 0 => 7 , 1 => 14 ,);
    }

    #[cfg(feature = "splitapple-right")]
    {
        scan_col!(0; 0 => 1 , 1 => 8  , 2 => 15 , 3 => 23 , 4 => 30 , 5 => 37 ,);
        scan_col!(1; 0 => 2 , 1 => 9  , 2 => 16 , 3 => 24 , 4 => 31 , 5 => 38 ,);
        scan_col!(2; 0 => 3 , 1 => 10 , 2 => 17 , 3 => 25 , 4 => 32 , 5 => 39 ,);
        scan_col!(3; 0 => 4 , 1 => 11 , 2 => 18 , 3 => 26 , 4 => 33 , 5 => 40 ,);
        scan_col!(4; 0 => 5 , 1 => 12 , 2 => 19 , 3 => 27 , 4 => 34 , 5 => 41 ,);
        scan_col!(5; 0 => 6 , 1 => 13 , 2 => 20 , 3 => 28 , 4 => 35 , 5 => 42 ,);
        scan_col!(6; 0 => 7 , 1 => 14 , 2 => 21 , 3 => 29 , 4 => 36 , 5 => 22 ,);
    }

    keys
}
      
      



!





, scan_col!



, , keys



: u64 .





, Rust.





, , , , . Google " Rust" , :





  • usize ; ( ), / , / , .





  • ( ) ; , .





  • ; , .





, , , , Rust , , . C (#define, #ifdef



..), , Rust . ( !). Rust - Rust Analyzer , , , C.





, Rust - , RFC - , , , .





, , ?





, Zig , - , -- - .





Zig,

Zig. (. Rust Zig).





: , - Zig, .





.





, dk.zig







usingnamespace @import("register-generation/target/nrf52833.zig");
usingnamespace @import("ztron.zig");

pub const led = .{ .port = p0, .pin = 13 };
      
      



atreus.zig







usingnamespace @import("register-generation/target/nrf52840.zig");
usingnamespace @import("ztron.zig");

pub const led = .{ .port = p0, .pin = 11 };
      
      



.





ztron.zig



@import("root")



("root" - , ; !) :





usingnamespace @import("root");

export fn setup() void {

    led.port.pin_cnf[led.pin].modify(.{
        .dir = .output,
        .input = .disconnect,
    });

}
      
      



"feature" , Cargo.toml



, . Cargo.toml !





, , : devkit, zig build-obj dk.zig



; Atreus - zig build-obj atreus.zig



.





, Zig , . ( - , , ).





- ? , , ... :





const rows = .{
    .{ .port = p1, .pin = 0 },
    .{ .port = p1, .pin = 1 },
    .{ .port = p1, .pin = 2 },
    .{ .port = p1, .pin = 4 },
};

const cols = .{
    .{ .port = p0, .pin = 13 },
    .{ .port = p1, .pin = 15 },
    .{ .port = p0, .pin = 17 },
    .{ .port = p0, .pin = 20 },
    .{ .port = p0, .pin = 22 },
    .{ .port = p0, .pin = 24 },
    .{ .port = p0, .pin = 9 },
    .{ .port = p0, .pin = 10 },
    .{ .port = p0, .pin = 4 },
    .{ .port = p0, .pin = 26 },
    .{ .port = p0, .pin = 2 },
};

pub fn initKeyboardGPIO() void {
    inline for (rows) |x| {
        x.port.pin_cnf[x.pin].modify(.{
            .dir = .input,
            .input = .connect,
            .pull = .pulldown,
        });
    }

    inline for (cols) |x| {
        x.port.pin_cnf[x.pin].modify(.{
            .dir = .output,
            .input = .disconnect,
        });
    }
}
      
      



inline for .





, - , , "" - .





:





const col2row2key = .{
    .{ .{ 0,  1 }, .{ 1, 11 }, .{ 2, 21 }, .{ 3, 32 } },
    .{ .{ 0,  2 }, .{ 1, 12 }, .{ 2, 22 }, .{ 3, 33 } },
    .{ .{ 0,  3 }, .{ 1, 13 }, .{ 2, 23 }, .{ 3, 34 } },
    .{ .{ 0,  4 }, .{ 1, 14 }, .{ 2, 24 }, .{ 3, 35 } },
    .{ .{ 0,  5 }, .{ 1, 15 }, .{ 2, 25 }, .{ 3, 36 } },
    .{                         .{ 2, 26 }, .{ 3, 37 } },
    .{ .{ 0,  6 }, .{ 1, 16 }, .{ 2, 27 }, .{ 3, 38 } },
    .{ .{ 0,  7 }, .{ 1, 17 }, .{ 2, 28 }, .{ 3, 39 } },
    .{ .{ 0,  8 }, .{ 1, 18 }, .{ 2, 29 }, .{ 3, 40 } },
    .{ .{ 0,  9 }, .{ 1, 19 }, .{ 2, 30 }, .{ 3, 41 } },
    .{ .{ 0, 10 }, .{ 1, 20 }, .{ 2, 31 }, .{ 3, 42 } },
};

pub fn readKeys() PackedKeys {
    var pk = PackedKeys.new();

    inline for (col2row2key) |row2key, col| {
        // set col high
        cols[col].port.outset.write_raw(1 << cols[col].pin);

        delay(1000);

        const val = rows[0].port.in.read_raw();
        inline for (row2key) |row_idx_and_key| {
            const row_pin = rows[row_idx_and_key[0]].pin;
            pk.keys[(row_idx_and_key[1] - 1)] = (1 == ((val >> row_pin) & 1));
        }

        // set col low
        cols[col].port.outclr.write_raw(1 << cols[col].pin);
    }

    return pk;
}
      
      



, Ziginline for



, Rust ( , , ), / .





, // const-, . , ( ) :





pub const switch_count = comptime {
    var n = 0;
    for (col2row2key) |x| n += x.len;
    return n;
};
      
      



, Rust:





scan_col!(0; 0 => 1 , 1 => 8  , 2 => 15 , 3 => 21 , 4 => 27 , 5 => 33 ,);
      
      



( , - , Rust 500 , , ).





Rust'?

Zig Rust, . , , - " " - Rust.





, , Rust - , . , Rust , , , , , .





, , :





fn main() {
    let message = "hello world"; // a regular immutable variable definition
}

let message = "hello world"; // doesn't work at toplevel

const message: &str = "hello world"; // you have to write `const` and declare the type yourself.
      
      



, , . , :





  • , , , .





  • , , " " , .





  • , const



    , let



    , , let



    , consts data- .





  • - 100 , , , , ..





, Rust - --, . (. " " ).





: , , , , . ( , : , , , , , , , , ..).





, , Rust?





"" :





: , ?





Rust , , .





, if



/, , ( ). , , .





, "" , Zig - . , , : comptime



inline for



, , , , , , , - Zig!





Zig?

- , , , , - . , ; =D





, Zig .





- , , : , .





"" : Rust, Emacs , MacBook Air 2013 :





Rust 1.50 70 (, 90 ), target/



450 .





Zig 0.7.1, , 5 , zig-cache/



1.4. !





"" - ; , , . Zig:





, .





Zig, , , . . . .





, , , - - "".





: ", , while



", .





, , Zig, . //.





Zig, Zig.





, , .





, !





-Zig- WASM, ! (zig build-lib -target wasm32-freestanding -O ReleaseSmall foo.zig



foo.wasm



, !).





, , , Zig . , Zig - , ; , , . .





, . , , Rust, . ; XML Zig- ( continue comptime).





, Zig ; , , , , . . , .





, , , Zig: , .





I either successfully use Zig for my embedded hobby projects, one-time WASM helpers and the necessary bindings to the C API, or, in the struggle to complete these tasks, I finally begin to understand and appreciate more what problems Rust protects me from.





Anyway, I'm very happy!





Acknowledgments

Thanks to Julia Evans , Pierre Yves Bacc, Laura Lindsey , Jamie Brandon, and Boats for their thoughtful discussion of Rust / Zig and constructive feedback on this article!








All Articles