5 ms·
Having distinct types for P0 and P1 is deliberate and is what is called "type state programming" in the embedded rust book [0]. The advantage is that you can pr
by lulf 6y ago
Having distinct types for P0 and P1 is deliberate and is what is called "type state programming" in the embedded rust book [0]. The advantage is that you can prevent misconfiguration at compile time (ensuring that a given pin cannot be used in multiple places). In the Zig example, it seems to me (and I have zero knowledge of Zig, so sorry if this is inaccurate) that you can potentially introduce bugs where the same pin is used twice.
For a generic led driver, it should not use these types, but instead the trait types from the embedded_hal crate, such as "OutputPin" that is implemented by the different chip-specific HALs. There is an example of a generic led driver that uses these traits at [1].
In general I recommend everyone who wants to try out Rust on embedded to read the embedded rust book, because it clarifies a lot of the reasons and advantages of its approach.
[0] https://docs.rust-embedded.org/book/static-guarantees/typestate-programming.html https://docs.rust-embedded.org/book/static-guarantees/typest...
[1] https://github.com/drogue-iot/drogue-device/blob/main/rt/src/driver/led/matrix.rs#L12 https://github.com/drogue-iot/drogue-device/blob/main/rt/src...
- lynaghk 6y agoAuthor here. I agree that the Rust embedded books are a nice read, and the idea of type state programming --- taking advantage of Rust's ownership and generics system to enforce at compile time transitions between logical states via "zero-sized types" --- is interesting and could be useful in some contexts. However, that is not what is happening here. P0 and P1 are distinct types because they are distinct hardware registers. I think it's great that they're modeled as distinct types; the problem is simply that Rust makes it difficult to conceptually iterate over distinct types (regardless if such iteration occurs at runtime via a loop or at compile-time via an unrolled loop, as per Zig's `inline for`). An aside about "type state programming": Microcontrollers have a lot of functionality packed into the pins (see the STM32 "Alternate Function" datasheet tables). Trying to model all of that using ownership of zero-sized generic types would strike me as a "when all you have is a hammer"-type situation. If a single pin switches between, for example, high-impedance, gpio low, and PWM output depending on what mode your firmware is in, I suspect it'd be a nightmare to pass owned types around Rust functions --- one would have a much easier time (and more likely to be correct) if they checked their design using something like TLA+ / Alloy or implemented the firmware using an explicit statecharts runtime like Quantum Leap's QP framework https://www.state-machine.com/ https://www.state-machine.com/.
- varajelle 6y agoI agree that iterating over types of a tuple is indeed not easy, but in that case, it should be trivial to iterate over an array of `&dyn OutputPin`. Why is that not working in this case?
- jbandela1 6y agoI think you can do something like this with your Rust code using macros. struct P0{} impl P0{ fn write(self:&Self,pin:usize){ std::println!("Writing port P0 on pin {}",pin); } } struct P1{} impl P1{ fn write(self:&Self,pin:usize){ std::println!("Writing port P1 on pin {}",pin); } } macro_rules! for_each_port_pin{ ($port:ident,$pin:ident,$b:block, $(($e1:expr,$e2:expr)),*) =>{ $( let $port = $e1; let $pin = $e2; $b );* } } fn main(){ let p0 = P0{}; let p1 = P1{}; for_each_port_pin!(port,pin, {port.write(pin);}, (&p0,10usize),(&p1,7usize) ); } Rust playground link: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e1d2ad80349ff5c3f49ab8330e7db918 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- afranchuk 6y agoOr a trait...
- alfiedotwtf 6y agoYeah, I was thinking trait while reading that bit... maybe I missed something?
- jokethrowaway 6y agoEven if you didn't have Output Pin, couldn't you just declare a sum type? enum MyPin { P0, P1 } Edit: feel free to ignore, read your answer somewhere else about this You would then have to pattern match when you read the value but I don't see a reason to reach for macros or anything more complicated. That said, really enjoyed the read (and I'll definitely try zig at some point, if only for the speed / compile experience), even if my experience with Rust didn't match yours; my background is a bit different though, I worked with C++ and Haskell in the past, which definitely made rust feel almost natural. Overall I'd say that the compiler helps me not to keep a lot of rust syntax in my mind and just try things until it works
- pron 6y agoWhat is checked at compile-time in Zig is up to the Zig code. It's a little hard to explain because this doesn't work like Lisp (or Rust) macros, but, since Zig is so easy to learn -- despite this revolutionary design -- should mean it's not a problem. As a first approximation (somewhat inaccurate), you could think of Zig as an optionally typed dynamic language that can run introspect (and create) types freely, perform elaborate checks on them etc. (e.g. examine fields and their types, and compare them to other types' fields), and then the programmer gets to say: run these checks at compile-time and make errors compilation errors.
- The_rationalist 6y agoNote that c++ and rust have const fn. But yeah the dinamicity and introspectabibility you describe reminds me of typescript.
- pron 6y agoIt's not about what Zig has but what it doesn't have. Because low-level programming is already complex, language simplicity is a super-important feature that few low-level languages have, and I would say none that are expressive and emphasise safety -- except Zig. You could do those things in C++ with template games and in Rust with macros. But Zig lets you have immense expressivity with a simple, small and easy-to-learn language.
- craftinator 6y ago> It's not about what Zig has but what it doesn't have. Because low-level programming is already complex, language simplicity is a super-important feature This is what made me love Lua for embedded programming. The more inherent complexity (or "exposed complexity" might be a better phrase) in the system, the less inherent complexity you want in the language.
- leshow 6y ago> You could do those things in C++ with template games and in Rust with macros. But Zig lets you have immense expressivity with a simple, small and easy-to-learn language. const fn is (or seems to me to be) exactly what comptime is though. The difference is that rust's const syntax is still slowly allowing more things to be executed at compile time. Like for now, it still can't do any heap allocation.
- kristoff_it 6y ago> In the Zig example, it seems to me (and I have zero knowledge of Zig, so sorry if this is inaccurate) that you can potentially introduce bugs where the same pin is used twice. Given the code in the blog post, yes. Here's a possible solution: pub fn initKeyboardGPIO() void { comptime checkPinsAreUnique(10, rows); comptime checkPinsAreUnique(100, cols); ... } fn checkPinsAreUnique(max_pin: usize, elems: anytype) void { var seen = [1]bool{false} ** (max_pin + 1); inline for (elems) |x| { if (x.pin > max_pin) { @compileError("Found pin value higher than max pin"); } if (seen[x.pin]) { @compileError("Found duplicate pin!"); } seen[x.pin] = true; } } If pins happen to be very sparse, one could switch from a basic array to a comptime hashmap: https://github.com/ziglang/zig/blob/master/lib/std/comptime_string_map.zig https://github.com/ziglang/zig/blob/master/lib/std/comptime_... There's also other ways of approaching the implementation depending on the required level of dynamicism, I just hacked together the quickest solution I could think of.
- sitkack 6y agoWould it be correct to describe this as using comptime to enforce system level constraints? To my naive understanding it looks like comptime combined with type state programming gives one user definable type systems.
- audunw 6y agoDoesn't sound like a problem that's worth trying to resolve at compile time through the type system to me. You complicate the common case for a relatively minor benefit. In Zig I think you can get 99% of the benefit by setting up a framework where you allocate a resource (pin, ppi channel, etc) through a function call. We use this for a testing framework which gives you run-time errors. But with Zig you could probably write this in a way that gives you compile-time errors for statically allocated resources. That should give you a system that works in both compile and run time. Yeah, you can't totally guarantee that a pin isn't allocated, since a programmer can use the pin without calling the resource allocation function. But I feel like that takes you from 99.99% safe to 99.9999% .. worth in in a few obscure applications, but not in most. It's not like I've ever seen any issues from allocating a pin twice in embedded programming. On nRF I guess PPI channels is a more relevant use-case. But then you could very quickly find that you need a more dynamic system that can only detect errors at run-time anyway.
- the__alchemist 6y agoThere's a tradeoff between catching errors at compile time as you describe, and code flexibility. For example, here's a line from a current project using one of the HALs: static SENSOR: Mutex< RefCell< Option< Mlx9061x< I2c< I2C1, ( PB6<Alternate<AF4, Output<OpenDrain>>>, PB7<Alternate<AF4, Output<OpenDrain>>>, ), >, ic::Mlx90614, >, >, >, > = Mutex::new(RefCell::new(None)); The pin types here are due to this type of programming. They aren't used by the I2c peripheral; they're just for the check. If you only use a peripheral struct (eg i2c here) in the main function, the type state system makes sense. If you pass it across function boundaries, or use statically like, this, it may not be. The rust HALs and tutorials that use this pattern tend to leave function boundaries etc (where you need to explicitly declare types) out of examples.