Hi Omar, great write-up! Just a hint for your enum to integer value conversion you can do this with simply type-casting using the repr attribute. This would shorten your code where you pass an enum variant to the MAX7219, because there is no need to match against all the enum variants. Illustrative example: // C style represenation of enum in memory, here u8 #[repr(u8)] enum DigitRowAddress { Digit0 = 0x01 , Digit1 = 0x02 , Digit2 = 0x03 , Digit3 = 0x04 , Digit4 = 0x05 , Digit5 = 0x06 , Digit6 = 0x07 , Digit7 = 0x08 , } fn draw_row_or_digit ( per: MaxSpi, cs: & mut Pin< 'A' , 6_u8 , Output>, digit_addr: DigitRowAddress, led_data: u8 , ) -> Result <(), AddressError> { let send_array: [ u8 ; 2 ] = [digit_addr as u8 , led_data]; transmit_raw_data(&send_array, per, cs).unwrap(); Ok (()) } Another thing I'd improve, is in your matches against enumerations that are exhaustive, e.g. DigitRowAddress, most of the time it's better to not match against _ for any variant, as first of all it cannot happen because you covered all cases already (and the chip wont add new pins during its lifetime) - and second: if somebody added Digit8 to the DigitRowAddress enum, your code keeps happily compiling even though your match block is now broken. At runtime it will error out, but it could already error at compile time which is preferred, especially on embedded systems without visual feedback. The type system of Rust allows many of those compile time sanity verifications and makes embedded programming a joy. Aiming to only allow valid states at compile time basically made me stop using debuggers :)