Язык C для микроконтроллеров
Чем программирование микроконтроллера на C отличается от программирования для компьютера: почему int на AVR 16-битный, а char на ARM беззнаковый, зачем нужен volatile и чего он не делает, как устроен доступ к регистрам в avr-libc и CMSIS, как держать константы во Flash и где прячется неопределённое поведение. Со ссылками на стандарт C11 и ABI платформ.
Обновлено Редакция GAW
Программа для микроконтроллера пишется на том же C, что и программа для компьютера, но условия другие: 8- или 32-битное ядро без операционной системы, регистры периферии по фиксированным адресам, прерывания, которые меняют данные в любой момент, и несколько килобайт ОЗУ. Ниже — то, что в этих условиях нужно знать о языке, со ссылками на стандарт C11 (черновик N1570) и на ABI avr-gcc и ARM.
Коротко
- Размеры типов зависят от платформы: у avr-gcc
int— 16 бит иcharзнаковый, у ARM (AAPCS)int— 32 бита иcharбеззнаковый. Пишитеuint8_t,int16_t,uint32_tиз<stdint.h>. - volatile обязателен для регистров и для переменных, общих с прерываниями; атомарности он не даёт.
- Регистры — это обращения через указатель на
volatileпо фиксированному адресу; так устроены заголовки avr-libc и CMSIS. - Константы на AVR по умолчанию попадают в ОЗУ — для Flash нужен PROGMEM; у Cortex-M
constостаётся во Flash. - Переполнение знакового типа — неопределённое поведение, беззнакового — счёт по модулю 2^N.
Типы и их размеры
| Тип | avr-gcc | GCC для ARM (AAPCS32) |
|---|---|---|
char | 1 байт, знаковый | 1 байт, беззнаковый |
short | 2 байта | 2 байта |
int | 2 байта | 4 байта |
long | 4 байта | 4 байта |
long long | 8 байтов | 8 байтов |
| указатель | 2 байта | 4 байта |
double | 4 или 8 байтов — зависит от настройки компилятора | 8 байтов |
Данные avr-gcc — из раздела ABI вики GCC, ARM — из таблицы соответствия типов стандарта AAPCS32. Отсюда два правила. Первое: используйте типы точной ширины uint8_t, int16_t, uint32_t из <stdint.h> — стандарт требует определить их на любой платформе, где есть целые такой ширины в дополнительном коде (C11, 7.20.1.1), а размер int на разных МК разный. Второе: не используйте «просто char» для чисел — на AVR и ARM он ведёт себя по-разному при значениях больше 127.
С 16-битным int на AVR связана частая ошибка: выражение uint16_t a = 300; uint32_t x = a * 1000; вычисляется в 16-битном int и переполняется до присваивания. Нужно расширить один из операндов: (uint32_t)a * 1000. Та же формула на ARM работает, потому что int там 32-битный, — и код, перенесённый с ARM на AVR, ломается молча.
Целочисленная арифметика
Перед вычислениями типы уже int продвигаются до int (C11, 6.3.1.1), так что uint8_t + uint8_t считается в int, и результат 200 + 100 = 300 не теряется, пока его не присвоили байту. Беззнаковые типы никогда не переполняются в смысле стандарта — результат берётся по модулю 2^N (6.2.5p9), поэтому счётчики, таймстемпы и сравнения вроде (uint32_t)(now - start) >= timeout надёжно работают и при переходе через ноль. Переполнение знакового типа — неопределённое поведение (6.5p5): компилятор может, например, считать, что i + 1 > i всегда истинно. Для счётчиков и всего, что может перейти через максимум, берите беззнаковые типы.
Тонкости сдвигов и инверсии маленьких типов разобраны в статье о битовых операциях.
volatile
Стандарт говорит, что объект с квалификатором volatile может меняться способами, неизвестными компилятору, поэтому каждое обращение к нему выполняется строго по правилам абстрактной машины (C11, 6.7.3p7). Компилятор не имеет права выбросить чтение «лишнего» флага, объединить две записи в одну или вынести чтение из цикла. Без volatile такой код может зависнуть:
volatile uint8_t data_ready; /* set in an interrupt handler */
ISR(USART_RX_vect)
{
data_ready = 1;
}
void wait_for_data(void)
{
while (!data_ready) { /* without volatile the compiler may read it once */
}
data_ready = 0;
}
volatile нужен в трёх случаях: регистрам периферии, переменным, которые меняет обработчик прерывания и читает основная программа (или наоборот), и переменным, которые меняет другое ядро или DMA. Ставить volatile везде «на всякий случай» не стоит — он запрещает оптимизации и раздувает код.
Атомарность
volatile не делает обращение неделимым. 8-битный AVR читает uint16_t или uint32_t за несколько команд, и если между ними сработает прерывание и изменит переменную, основная программа получит половину старого и половину нового значения. На 32-битном Cortex-M чтение выровненного uint32_t — одна команда, но операция counter++ и там состоит из чтения, сложения и записи.
В avr-libc для таких мест есть макрос ATOMIC_BLOCK из <util/atomic.h>: он запрещает прерывания на время блока и восстанавливает прежнее состояние флага I при выходе:
#include <util/atomic.h>
volatile uint32_t ticks; /* incremented in a timer interrupt */
uint32_t get_ticks(void)
{
uint32_t copy;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
copy = ticks;
}
return copy;
}
На Cortex-M то же делают функциями CMSIS __disable_irq() и __enable_irq() или сохранением и восстановлением регистра PRIMASK.
Доступ к регистрам
Регистр периферии — это ячейка по фиксированному адресу, поэтому доступ к нему — разыменование указателя на volatile нужного размера. В avr-libc макрос _SFR_IO8(адрес) разворачивается в _MMIO_BYTE() — *(volatile uint8_t *)(адрес) с поправкой на смещение регистров ввода-вывода, — и PORTB = 0x20; превращается в запись по адресу 0x25 у ATmega328P. В CMSIS для ARM регистры модуля описаны структурой, поля которой помечены квалификаторами __I, __O и __IO — это всё тот же volatile, — а имя модуля — указатель на структуру по базовому адресу: GPIOA->ODR.
/* the same idea without vendor headers */
#define PORTB_REG (*(volatile uint8_t *)0x25) /* ATmega328P: PORTB data address */
typedef struct {
volatile uint32_t CRL, CRH, IDR, ODR, BSRR, BRR, LCKR;
} gpio_regs; /* STM32F1 GPIO layout */
#define GPIOA_REGS ((gpio_regs *)0x40010800UL)
Своими определениями пользуются редко — заголовки производителя уже содержат все регистры и биты, — но понимать, что за GPIOA->ODR стоит обычный указатель, полезно при отладке и при чтении карты памяти. Сравнение HAL, LL и прямой работы с регистрами — в статье о HAL, LL и CMSIS.
Константы и Flash
У AVR память программ и память данных — разные адресные пространства, и обычный указатель указывает в ОЗУ. Поэтому все инициализированные данные, в том числе const, avr-gcc при старте копирует из Flash в ОЗУ. Таблица на килобайт съедает половину ОЗУ ATmega328P. Чтобы данные остались во Flash, их объявляют с атрибутом PROGMEM и читают специальными функциями (руководство avr-libc, «Data in Program Space»; там же описан второй способ — именованное адресное пространство __flash):
#include <avr/pgmspace.h>
static const uint8_t sine[256] PROGMEM = { 128, 131, 134 /* ... */ };
uint8_t sine_at(uint8_t i)
{
return pgm_read_byte(&sine[i]); /* LPM instruction, not a RAM load */
}
У ARM Cortex-M Flash, ОЗУ и периферия находятся в одном 32-битном адресном пространстве, и const-данные компоновщик оставляет во Flash — их читают обычным указателем, без специальных функций.
Плавающая точка
У AVR нет блока плавающей точки, и каждая операция с float выполняется программно — вызовом библиотечной подпрограммы вместо одной команды. У Cortex-M4F и M7 есть аппаратный FPU, но у M4F он одинарной точности: операции с float выполняются аппаратно, а с double — программно. Поэтому константы в формулах пишут с суффиксом f — 0.5f, а не 0.5, иначе выражение молча пересчитается в double. Там, где FPU нет, дробные числа часто удобнее хранить с фиксированной точкой.
Частые вопросы
Зачем нужен volatile в программе для микроконтроллера?
Он сообщает компилятору, что значение может измениться без его ведома — аппаратурой или обработчиком прерывания, — поэтому каждое обращение к переменной в программе должно превратиться в настоящее чтение или запись памяти (C11, 6.7.3). Без volatile компилятор вправе прочитать флаг один раз и крутить цикл while (!flag) бесконечно. Volatile нужен регистрам периферии и переменным, общим для основной программы и прерываний.
Делает ли volatile доступ атомарным?
Нет. Volatile только запрещает компилятору выкидывать и переупорядочивать обращения. 16-битная переменная на 8-битном AVR читается двумя командами, и прерывание между ними может изменить её наполовину. Такие обращения выполняют при запрещённых прерываниях — в avr-libc для этого есть ATOMIC_BLOCK(ATOMIC_RESTORESTATE) из <util/atomic.h>.
Какой размер у int на микроконтроллере?
Зависит от компилятора и платформы: у avr-gcc int 16-битный, у компиляторов для ARM по стандарту вызовов AAPCS — 32-битный. Поэтому в прошивках используют типы точной ширины из <stdint.h> — uint8_t, int16_t, uint32_t: их размер одинаков везде, где они определены.
Почему константный массив занимает ОЗУ на AVR?
У AVR раздельные адресные пространства памяти программ и данных, и обычный указатель на const указывает в ОЗУ. Поэтому avr-gcc копирует константы при старте в ОЗУ. Чтобы оставить таблицу во Flash, её объявляют с атрибутом PROGMEM и читают функциями pgm_read_byte() и подобными из <avr/pgmspace.h>. У ARM Cortex-M Flash и ОЗУ лежат в одном адресном пространстве, и const-данные читаются прямо из Flash обычным указателем.
Источники
- ISO/IEC 9899:201x (C11), Committee Draft N1570, April 2011 — 6.2.5p9 — беззнаковая арифметика по модулю 2^N; 6.3.1.1 — целочисленное продвижение; 6.5p5 — переполнение знакового типа — неопределённое поведение; 6.7.3p7 — volatile; 7.20.1.1 — типы точной ширины intN_t и uintN_t
- GCC Wiki: avr-gcc — ABI and implementation defined behaviour — размеры типов avr-gcc: char знаковый 1 байт, int 2, long 4, long long 8, double и long double — 4 или 8 байтов в зависимости от настройки
- Arm. Procedure Call Standard for the Arm Architecture (AAPCS32) — таблица соответствия типов C: char — беззнаковый байт, int и long — 32-битное слово, long long — двойное слово
- AVR-LibC: <avr/sfr_defs.h>, <util/atomic.h>, <avr/pgmspace.h> (github.com/avrdudes/avr-libc) — _SFR_IO8 и _MMIO_BYTE — доступ к регистрам через указатель на volatile; ATOMIC_BLOCK(ATOMIC_RESTORESTATE); PROGMEM и pgm_read_byte
- AVR-LibC User Manual: Data in Program Space — почему GCC кладёт const-данные в ОЗУ у гарвардского AVR; PROGMEM с pgm_read_*() и именованное адресное пространство __flash
- Arm. CMSIS 5: core_cm3.h и cmsis_gcc.h (github.com/ARM-software/CMSIS_5) — квалификаторы __I, __O, __IO — это volatile для полей структур регистров; __disable_irq, __enable_irq, __get_PRIMASK, __set_PRIMASK
Нашли ошибку?
Смотрите также
- Программирование МКC для МК, битовые операции, фиксированная точка, регистры
- Битовые операции в CМаски, сдвиги, установка и проверка битов, ловушки стандарта C
- Фиксированная точкаФормат Q, умножение со сдвигом, округление, насыщение, q15 и _Fract
- AVRATmega, ATtiny, AVR Dx/Ex: семейства, прошивка, инструменты
- HAL, LL или регистрыУровни STM32Cube, что делает HAL, правила совмещения, один код тремя способами