cembeddedstm32bit-fieldsstm32cubeide

STM32 - Is reading/writing singular bits inside registers with these custom structs ok?


Reading and writing from registers in STM32 seems to be super clunky and unintuitive to read, eg:

BRvalue = (SPI_CR1_BR & SPI1->CR1) >> SPI_CR1_BR_Pos; // Read current BR value
SPI1->CR1 &= ~SPI_CR1_BR; // Clear bits in BR position.
SPI1->CR1 |= 0b010 << SPI_CR1_BR_Pos; // Write new bits.

Coming from programming PIC's with XC8, they have these nice structs set up (inside a union because sometimes they have multiple structs where they define bit fields as singular bits too, or use different words to name bits etc.) that allow reading and writing of singular bits inside registers. I liked the balance of simplicity and readability, so wrote my own for this register for example:

typedef union {
    struct {
        volatile unsigned CPHA :1;
        volatile unsigned CPOL :1;
        volatile unsigned MSTR :1;
        volatile unsigned BR :3;
        volatile unsigned SPE :1;
        volatile unsigned LSBFIRST :1;
        volatile unsigned SSI :1;
        volatile unsigned SSM :1;
        volatile unsigned RXONLY :1;
        volatile unsigned CRCL :1;
        volatile unsigned CRCNEXT :1;
        volatile unsigned CRCEN :1;
        volatile unsigned BIDIOE :1;
        volatile unsigned BIDIMODE :1;
    };
} SPI1CR1bits_t;
SPI1CR1bits_t *SPI1CR1bits = (SPI1CR1bits_t*) &(SPI1->CR1);

Which allows the read / write operation to be done in a MUCH more elegant way:

BRvalue = SPI1CR1bits->BR; // Read value
SPI1CR1bits->BR = 0b010; // Write value

I've tested it and it seems to be working great. It also works for reading status bits that HW changes.

I haven't found this common practice though with STM32. Is there a reason why? What would be some problems I would encounter reading / writing with this method? Are there things I should be cautious of?


Solution

  • Using C bit fields will only partially work.

    The main problem is that compilers implement bit fields assuming that the underlying integer variables behave like regular memory that can be read and written. So they implement write operations by first reading the underlying integer variable, modifying the relevant bits and finally writing the underlying integer variable. This assumption does not hold for all STM32 registers.

    It works if all the bits in the register are of type rw, r or are unused. But it does not work it a register contains bits with a different behavior such as rc_w0, rc_w1, rc_r, t etc. In these cases, writing to the bit fields will change unintended bits in most cases.

    The STM32 reference manuals document the behavior of the register bits in details and there is an explanation of the different bit behavior in the first chapter.

    enter image description here

    So the use of bit fields must be restricted to registers containing only rw, r and unused bits.

    A less severe issue is that the C standard does not specify how the bit field bits are mapped to the bits of the underlying integer variable. Each compiler can implement it differently. In practice, they only differ if a struct uses more bits than fit into a single integer variable or if the bit fields in a struct use different integer data types.

    This shouldn't affect STM32 registers however as they are all 32-bit integer. So just consistently use uint32_t as the integer data type and make a separate struct for each register, and you should be safe.