Встраиваемые системы
C для embedded
Марсоход Curiosity работает на bare-metal C на процессоре RAD750 (мощность 5 Вт). Каждый регистр датчиков, каждый мотор, каждый бит телеметрии управляется через memory-mapped I/O. Ошибка в volatile или битовой маске - и $2.5 млрд застряли на Марсе.
- **STM32 HAL Library:** весь HAL написан на том же паттерне - volatile структуры на фиксированных адресах, CMSIS типы, BSRR для атомарного GPIO
- **FreeRTOS portmacro.h:** critical sections через __disable_irq/__enable_irq или BASEPRI регистр - inline asm для Cortex-M
- **Arduino:** analogRead(), digitalWrite() - обёртки над memory-mapped регистрами ATmega/SAMD. Прямой доступ к регистрам в 10-50 раз быстрее функций HAL
Исторический контекст
В 1978 году Деннис Ричи опубликовал «The C Programming Language» (K&R), в котором volatile ещё не существовало. Ключевое слово volatile добавлено в стандарт ANSI C89/C90 именно для embedded-программирования: процессорам нужен способ сказать компилятору «не оптимизируй этот доступ». До volatile разработчики использовали хаки: глобальные переменные, asm-вставки или флаги компилятора отключения оптимизации. MISRA C (1998, Motor Industry Software Reliability Association) кодифицировал обязательное использование volatile для аппаратных регистров в safety-critical системах.
volatile: запрет оптимизации компилятора
**Toyota Prius, 2010. NHTSA расследует внезапные ускорения. Один из выводов: переменные состояния педали газа не были помечены volatile - компилятор GCC оптимизировал чтение из памяти, кешируя значение в регистре.** В embedded C без volatile компилятор вправе считать что переменная может измениться только кодом программы. Периферийные регистры меняются аппаратурой - компилятор об этом не знает.
**volatile vs const:** комбинация `volatile const uint32_t *REG` - регистр только для чтения (hardware не должен его изменять через наш код), но значение может меняться аппаратурой. Типично для status регистров. **volatile не гарантирует атомарность** - для 64-битных переменных на 32-битных MCU нужны дополнительные меры.
Переменная `bool flag = false` используется в ISR (пишет `flag = true`) и в main loop (читает). Без volatile что может пойти не так?
Bitfields и битовые операции: управление регистрами
Периферийные регистры упаковывают несколько полей в одно 32-битное слово: бит 0 - enable, биты 1-3 - режим, биты 4-7 - прерывания. Bitfields C позволяют работать с ними как со структурой, компилятор генерирует нужные AND/OR/shift операции.
**Проблема битовых полей:** стандарт C не гарантирует порядок полей в памяти - разные компиляторы могут размещать биты по-разному. Для аппаратных регистров надёжнее использовать ручные bit mask операции с явными смещениями, чем bitfield структуры. CMSIS (Arm Cortex) использует макросы для этого.
Нужно установить биты 4-7 регистра в значение 0b1010, не затронув остальные биты. Какая операция правильная?
Inline Assembly: когда C недостаточно
**Arm Cortex-M: инструкция WFI (Wait For Interrupt) останавливает ядро до следующего прерывания, снижая потребление с 50 мА до 0.5 мА.** В C нет аналога - нужен inline assembly. Аналогично для атомарных операций, барьеров памяти, чтения специальных регистров процессора.
**CMSIS intrinsics:** для Cortex-M проще использовать __WFI(), __DSB(), __ISB() из cmsis_compiler.h - это переносимые обёртки над inline asm, работающие с GCC, IAR и Keil. Прямой inline asm нужен только для нестандартных инструкций или тонкой оптимизации.
В inline assembly `"memory"` в списке clobbers означает что?
Memory-Mapped I/O: периферия как память
**Arm Cortex-M: вся периферия (GPIO, UART, SPI, таймеры, ADC) доступна через адресное пространство процессора.** Вместо отдельных IN/OUT инструкций (как в x86) - обычные операции load/store на специфические адреса. STM32F4: GPIOA регистры начинаются с 0x40020000. Запись в `*(uint32_t*)0x40020018 = 0x01` устанавливает пин PA0.
**BSRR (Bit Set/Reset Register):** атомарное управление GPIO без read-modify-write. Запись в BSRR аппаратно атомарна - нет window где прерывание может повредить состояние. Это критично в многозадачном RTOS-окружении. Запись в ODR требует атомарной read-modify-write или использования критической секции.
volatile достаточно для безопасного доступа к переменным из ISR и main кода
volatile предотвращает оптимизацию компилятора, но не обеспечивает атомарность. Для многобайтных переменных (int64 на 32-bit MCU) или операций read-modify-write нужны атомарные инструкции или отключение прерываний.
На Cortex-M4 запись uint32_t одна инструкция STR - атомарна. Но uint64_t требует двух инструкций STRD или STR+STR - прерывание между ними видит промежуточное состояние. Для таких случаев используют __disable_irq()/__enable_irq() или LDREX/STREX инструкции.
Почему структуры для memory-mapped регистров объявляются с `volatile` полями, а не через обычные указатели?
C для embedded: главное
- **volatile:** запрет оптимизации - каждое чтение/запись идёт в реальную память. Обязательно для аппаратных регистров и переменных, изменяемых ISR
- **Bitfields:** удобная запись для работы с регистрами, но порядок бит не гарантирован стандартом. В критичном коде - явные маски и сдвиги
- **Inline assembly:** для WFI, барьеров памяти, атомарных инструкций LDREX/STREX; в Cortex-M часто заменяется CMSIS intrinsics
- **Memory-Mapped I/O:** вся периферия Cortex-M - структуры по фиксированным адресам. BSRR для атомарного GPIO без read-modify-write
- **volatile не гарантирует атомарность:** для многобайтных переменных и RMW операций нужны отключение прерываний или атомарные инструкции
Вопросы для размышления
- В FreeRTOS задача A и задача B (разные приоритеты) обращаются к одной volatile uint32_t переменной. Задача A читает, задача B пишет. Достаточно ли volatile для корректной работы - или нужно что-то ещё?