A few years ago, just after rustc got AVR support, I decided it'd be fun to port a (very) simplistic roguelike, writing everything from the HAL up. What I ended up doing was representing the MMIO registers and their bits using unit structs, and then defining and implementing some traits to tie it all together. I ended up with usage code looking like this (configuring the i2c bus):
// Set the SDA and SCL pins to input, and enable the internal pullups.
DDRC::clear_bits(DDRC::DDRC4 | DDRC::DDRC5);
PORTC::set_bits(PORTC::PORTC4 | PORTC::PORTC5);
// Initiliazing the prescaler to 1, and bit rate for a 400KHz transmission rate.
TWSR::clear_bits(TWSR::TWPS0 | TWSR::TWPS1);
TWBR::set_raw_value(TWI_BIT_RATE);
// Enable the TWI module, and the ACK.
TWCR::set_value(TWCR::TWEN | TWCR::TWEA);
While my representation doesn't protect against races or aliased access, I did manage to get it to protect against reading write-only bits, writing read-only bits, and using bits with the wrong register, all at compile time. The error messages were a bit unpleasant, but I was quite happy with how it turned out.
I could have implemented the *Assign operators for the register structs, but I'm not a huge fan of those. I feel it's a bit too opaque and error-prone, and preferred the clear function names.
One thing that complicates matters is that you need to do volatile read and writes, not just standard pointer access. So using raw pointers, while possible, actually becomes kinda verbose.
I could have implemented the *Assign operators for the register structs, but I'm not a huge fan of those. I feel it's a bit too opaque and error-prone, and preferred the clear function names.
One thing that complicates matters is that you need to do volatile read and writes, not just standard pointer access. So using raw pointers, while possible, actually becomes kinda verbose.