Reading a UART driver again and the same pattern shows up. status is a volatile uint32_t * aimed at the status register, the loop spins while !(*status & RX_READY), then it reads the data register. That part is fine. volatile stops the compiler from caching the read or deleting it.
Now the things it does not do. It does not order ordinary loads and stores around it, so a DMA buffer filled just before the volatile write that starts the transfer can still be written after that write. It is not atomic. *ctrl |= TX_EN is a read and then a write, and an interrupt handler that changes the same register in between gets its change overwritten. It tells the CPU nothing about write buffers or caches. And C11 6.7.3 leaves what counts as an access to a volatile object implementation-defined, so the compiler manual is part of the spec for this code.
The ordering the device needs comes from the architecture's barrier instructions or the vendor header that wraps them. C11 fences were written for threads, and whether they cover device memory is a platform question.
volatile is where a driver starts being correct. It is nowhere near where it finishes.