6 ms·
Author 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
by lynaghk 6y ago
Author 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
- accountofme 6y agoAs someone who has gone from python -> c++ -> rust I'll agree with you here. Rust feels a lot more natural than c++ to get your ideas into code.
- foldr 6y ago>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. I second this. The idea of checking that a pin is "only used in one place" doesn't really jive with how I think about microcontroller programming. It's very common for one pin to be used for multiple distinct purposes at different times. There's also a lot of different ways of conceptually slicing pin states. For example, if you are charlieplexing LEDs than you'll switch pins between 'input' (high impedance) and 'output' modes, but at a higher level the pin is serving a single function.
- varajelle 6y ago> The idea of checking that a pin is "only used in one place" doesn't really jive with how I think about microcontroller programming. The borrow checker is not checking that the pin is used in "only one place", it is checking that you don't use the same pin for two different purposes at the same time. It make sure that you configure your pin as output pin before using it as an output pin, and that you reconfigure it to input pin when using it as such. (And there are some escape hatch to use when the type system is not sufficient to express that different code paths are disjoint, like RefCell, with runtime check, or unsafe)
- deleted 6y ago[deleted]
- foldr 6y agoAh, I was going on what the OP said ("ensuring that a given pin cannot be used in multiple places"). That seems sensible, but also not particularly valuable. A lot of the time it makes sense both to 'read' and 'write' from a pin (e.g. if it's open-drain with a pullup).
- lulf 6y agoThis was an inaccuracy on my part, sorry for that. It should probably have been "... used in multiple places _at the same time_".
- elcritch 6y agoInteresting write-up! I've barely used Rust but had/have a similar feeling. It's really more akin to C++ and really powerful but also pretty complex. For smaller MCU projects it just feels like overkill. > 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. The whole idea of utilizing TLA+ for a system level check really does seem like something that would be awesome, even if it's unclear how much effort it'd require to instrument an entire project with TLA+. > 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`). Rust just brings a lot of incidental complexity along and still makes some things really difficult. Perhaps it's better in the long run but it's just harder to work with. Similarly, I wanted a simpler language than Rust and started using Nim last summer for embedded projects. Primarily since it compiles to C which let's me target ESP32's and existing RTOS'es without rewriting everything or trying to get LLVM based tools to work with GCC ones. However, it also embraces `lazy` compilation approach to code and it's standard library. I wanted to try your example in Nim. Here's roughly how your example would look in Nim (it appears to duck type fine as well): var # normally just import these from the C headers as-is # but this lets us run it p0* : RefPort0 = addr port0 p1* : RefPort1 = addr port1 var rows = ( ( port: p1, pin: 0 ), ( port: p1, pin: 1 ), ( port: p1, pin: 2 ), ( port: p1, pin: 4 ), ) var cols = ( (port: p0, pin: 13 ), (port: p1, pin: 15 ), ... (port: p0, pin: 2 ) ) proc initKeyboardGPIO() = rows[0].port.pin[rows[0].pin].dir = output for item in rows.fields: item.port.pin[item.pin].dir = output Full example: https://gist.github.com/elcritch/1c8279418fc62f5e941b41a5df41ab79 https://gist.github.com/elcritch/1c8279418fc62f5e941b41a5df4... I've toyed with the thought of adding TLA+ hooks into Nim similar to Dr Nim (https://nim-lang.github.io/Nim/drnim.html https://nim-lang.github.io/Nim/drnim.html) using the effect system. Not sure if Zig has an effect system for a similar setup.
- 1MachineElf 6y agoI really like your Atreus. Are my eyes correct that it's using choc switches? Can you share where you got that one?