В языке существует ряд модификаторов, которые позволяют компилятору распорядиться функциями, переменными и всем таким более мудро и рационально.
Модификатор static. Он имеет два смысла. Первый, когда он используется по отношению к функциям и глобальным переменным, звучит примерно так: объекты, помеченные им, видны только в пределах этого модуля и нигде больше. То есть в модуле dma.c может быть переменная buffer с каким-то буфером и в модуле usart.c может быть переменная с таким же названием, и при этом они не будут конфликтовать. Если модификатор не использовать, то линкёр обязательно ругнётся, что есть два объекта взаимоисключающих. Точно так же ведут себя и функции.
Потому все локальные вспомогательные функции лучше помечать этим модфикатором.
Второй смыс: все переменные, помеченные им внутри функции, сохраняют своё значение между вызовами функции а так же под них выделяется область памяти (в то время как обычные переменные внутри функций обычно создаются на стеке или помещаются в регистры).
Потому все локальные вспомогательные функции лучше помечать этим модфикатором.
Второй смыс: все переменные, помеченные им внутри функции, сохраняют своё значение между вызовами функции а так же под них выделяется область памяти (в то время как обычные переменные внутри функций обычно создаются на стеке или помещаются в регистры).
Модификатор volatile. Он указывает компилятору, что значение переменной может измениться в любой момент (в другом потоке или прерывании) и потому оптимизировать использование этой переменной не стоит.
Бывают иногда циклы по ожиданию какого-либо события. Например, пока не закончится отправка данных.
while(!complete) {}
Бывают иногда циклы по ожиданию какого-либо события. Например, пока не закончится отправка данных.
while(!complete) {}
Если переменная по недосмотру не объявлена volatile, то компилятор может эту конструкцию развернуть следующим образом:
; while(!complete) {}
LDR R1, &complete ; Прочитать в регистр R1 адрес переменной complete
LDR R0, R1 ; Прочитать в регистр R0 значение переменной по адресу R1
CMP R0, 0 ; Сравнить регистр R0 с нулём
Метка_цикла:
JZ Метка_цикла ; Если равно нулю, то идём на Метка_цикла.
... ; Тут какой-то дальнейший код
; while(!complete) {}
LDR R1, &complete ; Прочитать в регистр R1 адрес переменной complete
LDR R0, R1 ; Прочитать в регистр R0 значение переменной по адресу R1
CMP R0, 0 ; Сравнить регистр R0 с нулём
Метка_цикла:
JZ Метка_цикла ; Если равно нулю, то идём на Метка_цикла.
... ; Тут какой-то дальнейший код
Но проблема в том, что если и изменится содержимое переменной, код об этом уже никогда не узнает, потому что компилятор оптимизировал этот код, исключив повторное чтение её значения! В то время как корректный код (с volatile) скомпилируется в такую конструкцию:
; while(!complete) {}
LDR R1, &complete ; Прочитать в регистр R1 адрес переменной complete
Метка_цикла:
LDR R0, R1 ; Прочитать в регистр R0 значение переменной по адресу R1
CMP R0, 0 ; Сравнить регистр R0 с нулём
JZ Метка_цикла ; Если равно нулю, то идём на Метка_цикла.
... ; Тут какой-то дальнейший код
; while(!complete) {}
LDR R1, &complete ; Прочитать в регистр R1 адрес переменной complete
Метка_цикла:
LDR R0, R1 ; Прочитать в регистр R0 значение переменной по адресу R1
CMP R0, 0 ; Сравнить регистр R0 с нулём
JZ Метка_цикла ; Если равно нулю, то идём на Метка_цикла.
... ; Тут какой-то дальнейший код
Как видно, он в каждую итерацию цикла проверяет значение переменной и покинет его, когда нужно.
Модификатор const. Он показывает, что значение переменной нельзя менять (не может измениться). Это, например, может подсказать компилятору разместить значение переменной или массива не в оперативной памяти, а в неизменяемой памяти (в микроконтроллере это может быть критично ввиду недостатка этой самой оперативной памяти). Или же позволить компилятору оптимизировать вызовы или код функций с параметрами, помеченными в функции как неизменяемые:
void lcd_textout(const char * text);
void lcd_textout(const char * text);
Глобальные переменные с этим модификатором обязательно должны быть инициализированы (это логично, иначе накой нам они нужны? =)).
Модификатор extern. Впрочем, сложно назвать это слово модификатором. Поскольку оно всего лишь говорит о том, что переменная или функция объявлена (и, соответственно, память под неё уже выделена где-то, или скомпилированный код где-то есть) в другом модуле. Причём модуль может быть представлен как исходным кодом, так и библиотекой. Если есть желание сделать глобальную переменную доступной из других модулей (это обычно оправдано лишь в маленьких проектах), то объявление со словом extern должно присутствовать в прилагающемся заголовочном файле. Вариант, когда подобные объявления присутствуют в ещё десятке исходных файлов, нежелателен: а вдруг захочется поменять тип переменной? И искать потом, где надо ещё его поменять, чтобы всё скомпилировалось? Не здорово.
Есть ещё модификаторы типов, определяющие наличие знака: signed и unsigned, но мы же хорошие люди и используем типы, где всё это уже железно определено: uint8_t, int32_t и т.д. Потому ими пользоваться не будем.