Is it a good approach to impl access to cpu register fields?

I tried to implement access to system registers of a CPU with the ability to access separate fields. I would love to hear opinions of more experienced developers if my approach is OK or I'm doing all wrong, and why?

RegisterField trait:

use core::ops::{Shl, Sub};

pub trait RegisterField<T>
where T:
    Copy +
    From<u8> +
    Shl<Output = T> +
    Sub<Output = T>
{
    fn attributes(&self) -> (u8, u8); // (field_start, field_size)

    fn mask(&self) -> T {
        let attr = self.attributes();
        ((T::from(1) << T::from(attr.1)) - T::from(1)) << T::from(attr.0)
    }
}

Trait for read-only register types:

use core::ops::{BitAnd, BitOr, Not, Shl, Shr, Sub};

pub trait ReadOnly<T>
where T:
    Copy +
    From<u8> +
    BitAnd<Output = T> +
    BitOr<Output = T> +
    Not<Output = T> +
    Shr<Output = T> +
    Shl<Output = T> +
    Sub<Output = T>
{
    // #[inline]
    fn get(&self) -> T;

    #[inline]
    fn get_field<U: RegisterField<T>>(&self, field: U) -> T {
        (self.get() & field.mask()) >> T::from(field.attributes().0)
    }
}

Then I define each register as a separate submodule and create const to refer to it:

pub const MPIDR_EL1: mpidr_el1::Reg = mpidr_el1::Reg {};

pub mod mpidr_el1 {
    use crate::register::cpu::ReadOnly;
    use crate::register::RegisterField;

    pub enum Field {
        Aff0,
        // Aff1,
        // Aff2,
        // MT,
        // U,
        // Aff3
    }

    impl RegisterField<u64> for Field {
        fn attributes(&self) -> (u8, u8) {
            match self {
                Field::Aff0 => (0, 8), // (field_start, field_size)
                // Field::Aff1 => (8, 8),
                // Field::Aff2 => (16, 8),
                // Field::MT => (24, 1),
                // Field::U => (30, 1),
                // Field::Aff3 => (32, 8)
            }
        }
    }

    pub struct Reg;

    impl ReadOnly<u64> for Reg {
        #[inline]
        fn get(&self) -> u64 {
            let r: u64;
            unsafe {
                asm!("mrs $0, mpidr_el1":"=r"(r):::"volatile")
            }
            r
        }
    }
}

Then I access registers in functions like

pub fn core_number() -> u8 {
    use system_register::mpidr_el1::Field::Aff0;

    (system_register::MPIDR_EL1.get_field(Aff0) & 0b11) as u8
}

it's an OK approach but it seems a bit of duplication to have to specify the whole register path both for the field parameter as for the register

also, this way doesn't allow for reading the register once then accessing multiple fields

but i'm kind of used to the way that svd2rust generates register accesses, which would be like this to read:

// read one field from a register
let val = system_register::MPIDR_EL1.read().aff0();
// read multiple fields from one register
let reg = system_register::MPIDR_EL2.read();
let val1 = reg.aff0();
let val2 = reg.aff1();

or to write

system_register::MPIDR_EL1.write(|w| w.aff0(new_value)
                                      .aff1(...));

or to do a read/modify/write cycle

system_register::MPIDR_EL1.modify(|r,w| w.aff0(new_value));

but ymmv

This is an interesting idea. But how can I achieve this? As I understand, read() method should return Self. But how can I do it for a trait? Or traits are not applicable in this approach?