Снова читаю драйвер UART и снова вижу тот же шаблон. status объявлен как volatile uint32_t * и указывает на регистр состояния, цикл крутится, пока !(*status & RX_READY), потом читается регистр данных. Это нормально. volatile не даёт компилятору закешировать чтение или выбросить его.
Теперь о том, чего volatile не делает. Он не упорядочивает обычные загрузки и сохранения вокруг себя, поэтому буфер для DMA, заполненный прямо перед volatile-записью, которая запускает передачу, может оказаться записан уже после неё. Он не атомарен. *ctrl |= TX_EN это чтение и затем запись, и если обработчик прерывания изменит тот же регистр между ними, его изменение пропадёт. Процессору он ничего не сообщает о буферах записи и кешах. А C11 в 6.7.3 оставляет определение доступа к volatile-объекту на усмотрение реализации, так что руководство компилятора здесь тоже часть спецификации.
Порядок, который нужен устройству, дают барьерные инструкции архитектуры или заголовок производителя, который их оборачивает. Барьеры C11 писались для потоков, а покрывают ли они память устройств, решает платформа.
С volatile корректность драйвера только начинается. До конца ещё далеко.