Структура программы на языке Си
Программа на Си имеет определенную структуру:
1) заголовок;
2) включение необходимых внешних файлов;
3) ваши определения для удобства работы;
4) объявление глобальных переменных (глобальные переменные объявляются вне какой-либо функции, т.е. не после фигурной скобки {, доступны в любом месте программы, значит можно читать их значения и присваивать им значения там, где требуется);
5) описание функций-обработчиков прерываний;
6) описание других функций, используемых в программе;
7) функция main (это единственный обязательный пункт).
Функция имеет { "тело" } в фигурных скобках. Тело – это код на Си, определяющий то, что делает функция. Знак ; после функции не ставится.
Программа на Си начинает работу с функции main(), по необходимости из main() вызываются другие функции программы, по завершении работы функции программа возвращается в main() в то место, откуда функция была вызвана.
main(){
... какой то код программы ...
вызов функции_1; //программа перейдет в функцию_1
строка программы; // будет выполняться после возврата
... какой то код программы ...
}
Функции могут вызываться не только из main(), но и из других функций. Кроме того, описанный выше ход программы может нарушаться прерываниями.
Приведем пример программы на Си с описанной выше структурой (текст в рамке). По мере надобности программа будет разрываться обычным текстом, а затем продолжаться.
/* Пункт 1. Заголовок программы Он оформляется как комментарий, и обычно содержит информацию: - о названии, назначении, версии и авторе программы; - краткое описание алгоритма программы; - пояснения о назначении выводов МК; - другие сведения, которые вы считает полезным указать. */ // комментарий после двух косых черт пишут в одну строку! // Пункт 2. Включение внешних файлов #include <mega16.h> /* перед компиляцией, препроцессор компилятора вставит вместо этой строчки содержимое (текст) заголовочного файла mega16.h - этот файл содержит перечень регистров, имеющихся в МК ATmega16, и соответствие их названий их физическим адресам в МК. Посмотрите его содержание, вызвав CVAVR\inc\mega16.h */ //delay functions #include <delay.h> /* перед компиляцией, препроцессор компилятора вставит вместо этой строчки текст "хидера" delay.h - этот файл содержит функции для создания пауз в программе. Теперь чтобы сделать паузу вам нужно лишь написать: delay_us(N); // сделать паузу N (число) мкс delay_ms(x); // сделать паузу x мс x - может быть переменная или число от 0 до 65535 (тип unsigned int), например, delay_ms(peremennaya)*/ // Пункт 3. Определения пользователя // AD7896 control signals PORTB bit allocation #define ADC_BUSY PINB.0 #define NCONVST PORTB.1 /* после этих двух строк, перед компиляцией, препроцессор компилятора заменит в тексте программы ADC_BUSY на PINB.0 и NCONVST на PORTB.1. Таким образом, вместо того, чтобы помнить, что вывод занятости AD7896 подключен к ножке PB0, вы можете проверять значение осмысленного понятия ADC_BUSY - "АЦП занят", а вместо управления абстрактной ножкой PB1 (через PORTB.1) можете управлять "НьюКонвекшнСтат" - NCONVST - "стартовать новое АЦ преобразование" #define – Это удобно, но вовсе не обязательно. */ | ||
Пункт 4. Объявление переменных
Перед использованием переменной в программе на Си её необходимо объявить, т.е. указать компилятору, какой тип данных она может хранить и как она называется.
Формат объявления переменной таков:
[<storage modifier>] <type definition> <identifier>;
[<storage modifier>] – необязательный элемент,
он нужен только в некоторых случаях и может быть:
extern– если переменная объявляется во внешнем файле, например, в хидере delay.h, приведенном выше;
volatile – ставьте, если нужно предотвратить возможность повреждения содержимого переменной в прерывании, и не позволить компилятору попытаться выкинуть её при оптимизации кода.
Пример:
volatile unsigned char x;
static– если переменная локальная, т.е. объявлена в какой либо функции и должна сохранять свое значение до следующего вызова этой функции.
eeprom– разместить переменную в EEPROM. Значение таких переменных сохраняется при выключении питания и при перезагрузке МК.
Пример:
eeprom unsigned int x;
Если это первая переменная в EEPROM, то её младший байт будет помещен в ячейку 1 EEPROM, а старший в ячейку 2. Необходимо помнить, что запись в EEPROM длительный процесс - 8500 тактов процессора.
Глобальные переменные объявляются до появления в тексте программы какой либо функции. Глобальные переменные доступны в любой функции программы.
Локальные переменные объявляются в самом начале функций, т.е. сразу после фигурной скобки { . Локальные переменные доступны только в той функции, где они объявлены!
<type definition> - тип данных, которые может хранить переменная.
Наиболе часто используемые типы данных:
unsigned char- хранит числа от 0 до 255 (байт);
unsigned int - хранит числа от 0 до 65535 (слово == 2 байта);
unsigned long int - хранит от 0 до 4294967295
(двойное слово == 4 байта).
Вместо unsigned char можно писать просто char, так как компилятор по умолчанию считает char беззнаковым байтом. А если вам нужен знаковый байт, то объявляйте его так:
signed char imya_peremennoi;
<identifier> – имя переменной - некоторый набор символов по вашему желанию, но не образующий зарезервированные слова языка Си. Выше был уже пример идентификатора – имени переменной: imya_peremennoi.
Желательно давать осмысленные имена переменным
и функциям, напоминающие вам об их назначении. Принято использовать маленькие буквы, а для отличия имен переменных от названия функций имена переменных можно, например, начинать с буквы, а названия функций (кроме main конечно) с двух символов подчеркивания.
Например, так: moya_peremennaya , __vasha_funkziya.
Глобальные переменные, а также локальные с модификатором static - при старте и рестарте программы равны 0, если вы не присвоили им (например, оператором =) иное значение при их объявлении или по ходу программы.
Вот несколько примеров объявления переменных:
unsigned char my_peremen = 34;
unsigned int big_peremen = 34034;
Пример массива,содержащего три числа или элемента массива.
char mas[3]={11,22,33};
Нумерация элементов начинается с 0, т.е. элементы данного массива называются mas[0], mas[1], mas[2] и в них хранятся деся-тичные числа 11, 22 и 33.
Где-то в программе вы можете написать: mas[1] = 120;
Теперь в mas[1] будет храниться число 120. Можно не присваивать значений элементам массива при объявлении, но только при объявлении вы можете присвоить значения всем элементам массива сразу. Потом это можно будет сделать только индивидуально для каждого элемента.
Строковая переменная или массив, содержащий строку символов.
char stroka[6]="Hello"; /* Символов (букв) между кавычками 5 , но указан размер строки 6. Дело в том, что строки символов должны заканчиваться десятичным числом 0. Не путайте его с символом '0', которому соответствует десятичное число 48 по таблице ASCII, которая устанавливает каждому числу определенный символ */
Например:
Элемент строки stroka[1] содержит число 101, которому по таблице ASCII соответствует символ 'e'.
Элемент stroka[4] содержит число 111, которому соответствует символ 'o'.
Элемент stroka[5] содержит число 0, которому соответствует символ 'NUL', его еще обозначают вот так '\0'.
Строковая переменная может быть "распечатана" или выведена в USART MK вот так: printf("%s\n", stroka);
flash и const ставятся перед объявлением констант, неизменяемых данных, хранящихся во flash-памяти программ. Они позволяют использовать не занятую программой память МК. Обычно для хранения строковых данных – различных информационных сообщений, либо чисел и массивов чисел.
Примеры:
flash int integer_constant=1234+5;
flash char char_constant=’a’;
flash long long_int_constant1=99L;
flash long long_int_constant2=0x10000000;
flash int integer_array1[ ]={1,2,3};
flash int integer_array2[10]={1,2};
flash char string_constant1[ ]=”This is a string constant”;
const char string_constant2[ ]=”This is also a string constant”.
// Пункт 5. Описание функций-обработчиков прерываний | ||
/* мы будем использовать в этой программе только одно прерывание и значит одну функцию-обработчик прерывания. Программа будет переходить на неё при возникновении прерывания: ADC_INT - по событию "окончание АЦ преобразования" */ interrupt [ADC_INT] void adc_isr(void) { PORTB=(unsigned char) ~(ADCW>>2); /* отобразить горящими светодиодами, подключенными от + питания МК через резисторы 560 Ом к ножкам порта B, старшие 8 бит результата аналого-цифрового преобразования. Сделаем паузу 127 мс, чтобы в реальном устройстве можно было увидеть переключение светодиодов */ delay_ms(127); /* В реальных программах старайтесь не делать пауз в прерываниях! Обработчик прерывания должен быть как можно короче и быстрее */ // начать новое АЦ преобразование ADCSRA|=0x40; } // закрывающая скобка обработчика прерывания | ||
Функция обработчик прерывания может быть названа вами произвольно, как и любая функция, кроме main. Здесь она названа adc_isr. При каком прерывании ее вызывать компилятор узнает из строчки interrupt[ADC_INT]. По первому зарезервированному слову - interrupt - он узнаёт, что речь идет об обработчике прерывания, а номер вектора прерывания (адрес, куда физически, внутри МК, перескочит программа при возникновении прерывания) будет подставлен вместо ADC_INT препроцессором компилятора перед компиляцией - этот номер указан в подключенном нами ранее заголовочном файле ("хидере") описания "железа" МК - mega16.h - это число, сопоставленное слову ADC_INT.
Очень информативна следующая строка программы:
PORTB = (unsigned char) ~(ADCW>>2);
Нужно присвоить значение выражения справа от оператора присваивания той переменной, что указана слева от него. Значит, нужно вычислить выражение справа и поместить его в переменную PORTB. ADCW – это двухбайтовая величина (так она объявлена в файле mega16.h, в котором CodeVisionAVR сохраняет 10-битный результат АЦП, а именно в битах 9_0 (биты с 9-го по 0-й), т.е. результат выровнен обычно – вправо.
VMLAB имеет только 8 светодиодов – значит нужно отобразить 8 старших бит результата - т.е. биты 9_2. Для этого мы сдвигаем все биты слова ADCW вправо на 2 позиции:
ADCW >> 2. Теперь старшие 8 бит результата АЦП переместились в биты 7_0 младшего байта (LowByte - LB) слова ADCW.
>> n означает сдвинуть все биты числа вправо на n позиций.
Это равносильно делению на 2 в степени n.
<< n означает сдвинуть все биты числа влево на n позиций.
Это равносильно умножению на 2 в степени n.
Светодиоды загораются (показывая "1") при "0" на соответствующем выводе МК – значит, нам нужно выводить в PORTB число, в котором "1" заменены "0" и наоборот. Это делает операция побитного инвертирования. Результатом выражения ~(ADCW>>2) будут инвертированные 8 старших бит результата АЦП, находящиеся в младшем байте двухбайтового слова ADCW. В Си в переменную можно помещать только тот тип данных, который она может хранить. Так как PORTB– это байт, а ADCW – это два байта, то прежде чем выполнить оператор присваивания (это знак = ) нужно преобразовать слово (слово - word - значит два байта) ADCW в беззнаковый байт.
Пишем ...(unsigned char) ~(ADCW>>2). Результат этой строки – один байт и мы можем поместить его в PORTB.Если в регистре DDRB все биты равны "1" – т.е. все ножки порта_B выходы, мы безусловно увидим старшие 8 бит результата АЦП горящими светодиодами.
Разберем еще одну строчку:
ADCSRA|=0x40; /* результат поразрядного ИЛИ с маской 01000000 поместить обратно в регистр АDCSRA, т.е. установить бит 6. Обратите внимание на необходимость ставить в конце
выражений точку с запятой – не забывайте! */
// Пункт 6. Функции, используемые в программе /* их может быть столько, сколько вам нужно. У нас будет одна, кроме main и обработчика прерывания. Это будет функция, в которой описано начальное конфигурирование МК в соответствии с поставленной задачей. Удобно над функцией сделать заголовок, подробно поясняющий назначение функции!*/ (void)__init_mk(void) {/* В начале любой функции объявляются локальные переменные – если, конечно, они вам нужны */ /* void - означает пусто. Перед названием функции - void – означает, что функция не возвращает никакого значения. А в скобках после названия означает, что при вызове в функцию не передаются никакие значения. */ // инициализация Port_B DDRB=0xFF; // все ножки сделать выходами PORTB=0xFF; // вывести на все ножки "1" /* настройка АЦП производится записью определенного числа в регистр ADCSRA. Нам нужно: - включить модуль АЦП; - установить допустимую частоту тактирования АЦП при частоте кварца 3.69 МГц. Мы выберем коэффициент деления 64 - это даст частоту такта для процессов в АЦП 57.656 кГц; - включить прерывание по завершению АЦ преобразования. По ДШ для этого нужно записать в регистр ADCSRA число 1000 1110 или 0х8E */ // ADC initialization w Oscillator=3.69MHz // ADC Clock frequency: 57.656 kHz // ADC Interrupts: On ADCSRA=0x8E; /* Теперь выбираем вход АЦП ADC0 (ножка PA0) и внешнее опорное напряжение (это напряжение, код АЦП которого будет 1023) с ножки AREF. Смотрим, что нужно записать для этого в регистр мультиплексора (выбора входа) АЦП ADMUX */ // Нужно записать 0 (он там по умолчанию) ADMUX=0; /* Разрешаем глобально все прерывания, разрешенные индивидуально. Вы наверно поняли, что индивидуально мы разрешили лишь прерывание по завершении АЦП - вот оно то и сможет возникать у нас. */ #asm("sei") } // скобка закрывающая для функции __init_mk() | ||
Так делаются вставки ассемблерных инструкций:
#asm("инструкция на ассемблере"). Обратите внимание – точки с запятой нет. На Си можно управлять всеми программно изменяемыми битами в регистрах МК, но часто используются такие строки:
#asm("sei") // Разрешить ГЛОБАЛЬНО все прерывания
#asm("cli") // Запретить ГЛОБАЛЬНО все прерывания
#asm("nop") // Пауза в 1 такт процессора
#asm("wdr") // Сбросить сторожевой таймер
/* Пункт 7. Главная функция main() - обязательная! Главная функция –программа начинает выполняться с нее */ void main(void){/* В начале любой функции объявляются (если нужны) ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ */ __init_mk();/*Вызываем функцию инициализации, настройки аппаратуры МК. Выполнив ее, программа вернется сюда и будет выполнять следующую строку */ // запускаем первое АЦ преобразование ADCSRA|=0x40; // бесконечный цикл в ожидании прерываний while(1);}/* Программа будет крутиться на этой строчке, постоянно проверяя, истинно ли условие в скобках после while, а так как там константа 1 - то условие будет истинно всегда!*/ // функция main закончена |
Теперь программа будет работать так. По завершении цикла АЦП будет возникать прерывание и программа будет перескакивать в функцию обработчик прерывания adc_isr().
При этом будут автоматически запрещены все прерывания. В конце adc_isr() запускается новое АЦ преобразование и при выходе из обработчика прерывания снова разрешаются глобально прерывания, а программа возвращается опять в бесконечный цикл while(1). Светодиоды будут высвечивать 8-ми битный код АЦ преобразования напряжения на ножке PA0.
Дата добавления: 2016-01-09; просмотров: 1616;