PIClist RUS
микроконтроллеры PIC и интерфейсы
техническая документация
статьи и разработки на русском языке

Уроки PIC24 - Глава 3. Объявление переменных и другие циклы

« назад на главную страницу

Оригинал: "Programming 16-Bit PIC Microcontrollers in C. Learning to Fly the PIC24", Lucio Di Jasio, 2007

Перевод с английского © piclist.ru


План урока

В предыдущем уроке мы узнали, что в основе программы встраиваемого приложения лежит цикл. В этом уроке мы продолжим исследование различных способов, предлагаемых языком Си для выполнения циклов. Заодно мы рассмотрим объявление целочисленных переменных, а также операторов инкремента и декремента, вкратце коснёмся объявления массивов и их использования. Как и в любом хорошем уроке, за теорией следует практика, и мы заключим урок, надеюсь, занимательным упражнением, в котором применим все понятия и инструменты, рассмотренные в предыдущих уроках.

Перечень необходимого

В этом уроке мы продолжим использовать программный симулятор MPLAB SIM, также нам понадобится демонстрационная плата Explorer16. Создайте новый проект с названием "More Loops" и новый исходный файл "More.c".

Урок

В цикле while содержимое фигурных скобок выполняется, если и до тех пор, пока логическое выражение возвращает булево значение "истина" (т.е. не ноль). Логическое выражение вычисляется до выполнения цикла, а это означает, что если оно вернёт значение "ложь" в самом начале, тело цикла никогда не выполнится.

Цикл do

Если вам нужен такой цикл, который выполняется хотя бы один раз, и только последующие повторения зависят от логического выражения, то позвольте представить вам синтаксис цикла do:

do {
  // здесь располагается ваш код...
 
} while ( x);

Пусть вас не смущает то, что в синтаксисе цикла do используется ключевое слово while, - поведение этих двух циклов сильно различается.

В цикле do код между фигурными скобками всегда исполняется первым, и только затем оценивается логическое выражение. Конечно, если нам нужен только бесконечный цикл для функции main(), то в этом случае нет никакой разницы, использовать do или while:

main()
{
 // код инициализации // главный цикл приложения
 do {
  … 
  } while (1)
} // main

Если вы посмотрите на забавный пример ниже, он поможет вам проанализировать поведение цикла:

do{
  // здесь располагается ваш код...
} while (0);

Код внутри этого цикла будет выполнен лишь один единственный раз (другими словами, организация такого цикла бессмысленна).

Теперь давайте посмотрим на более полезный пример, в котором мы используем цикл while для выполнения кода заданное количество раз.

Прежде всего, нам понадобятся переменные для счёта. То есть нам потребуется выделить одну или более ячеек памяти ОЗУ для хранения значения счётчика.

Примечание: в предыдущих двух уроках мы смогли опустить тему переменных, благодаря тому, что положились исключительно на предопределённые переменные: SFR-регистры PIC24.

Объявление переменных

С помощью следующего синтаксиса мы можем объявить целочисленную переменную:

int c;

Как только мы использовали ключевое слово int для объявления переменной c как 16-битного знакового целого, компилятор C30 создаст размещение для двух байтов памяти. Позже компоновщик определит, где эти два байта будут размещаться в физической памяти ОЗУ выбранной модели PIC24.

Переменная c, будучи 16-разрядной, позволит нам считать от -32 768 до 32 767. Если нам нужен более широкий числовой диапазон, мы можем выбрать тип long (знаковый):

long c;

Для этой переменной компилятор MPLAB C30 использует 32 бита (4 байта).

Если же вам нужен счётчик меньшего размера, и вам подойдёт диапазон от -128 до +127, вы можете воспользоваться типом char:

char c;

В этом случае компилятор будет использовать 8 битов (1 байт).

Все три типа можно дополнительно модифицировать атрибутом unsigned:

unsigned char c;   // диапазон значений 0..255
unsigned int  i;   // диапазон значений 0..65,535
unsigned long l;   // диапазон значений 0..4,294,967,295

А это типы переменных с плавающей точкой:

float f;      	// объявление переменной с плавающей точкой с 32-битной точностью
long double d; // объявление переменной с плавающей точкой с 64-битной точностью

Цикл for

Теперь вернёмся к нашему примеру со счётчиком. Нам нужна простая целочисленная переменная, которую мы будем использовать как индекс-счётчик, и которая сможет меняться в диапазоне от 0 до 5. Для этого выбираем в качестве типа переменной тип char:

char i;  // объявляем i как 8-битное знаковое целое
i = 0;   // инициализируем индекс-счётчик
while (i<5)
{
 // здесь располагается ваш код... 
 // он будет выполнен для i = 0, 1, 2, 3, 4
  
 i = i + 1;    // инкремент
} 

В Си имеется третий тип цикла, который был разработан специально для упрощения записи такого общего случая: цикл for. Вот как его можно использовать для этого примера:

for (i=0; i<5; i=i+1)
{
 // здесь располагается ваш код... 
 // он будет выполнен для i = 0, 1, 2, 3, 4
}

Согласитесь, что синтаксис цикла for весьма компактен и более лёгок в написании. А также он удобнее в чтении и отладке. Три выражения, разделённые точкой с запятой, вписанные в круглые скобки сразу после ключевого слова for, - это те самые выражения, которые мы использовали в предыдущем примере с while:

- инициализация индекса;

- проверка на завершение цикла с помощью логического выражения;

- продвижение индекса-счётчика ещё на один шаг (в нашем случае увеличиваем его на единицу).

Можно даже подумать, что цикл for - это сокращённый синтаксис цикла while. Действительно, логическое выражение проверяется до выполнения цикла, и, если в самом начале будет получено значение "ложь", тело цикла никогда не выполнится.

Думаю, сейчас самое время взглянуть на ещё одно удобное сокращение, принятое в Си, - специальные операторы, зарезервированные для операций инкремента и декремента:

++ - инкремент, как, например, в i++; равноценно i = i + 1;
-- - декремент, как, например, в i--; равноценно i = i - 1;

В последующих главах будет рассказано гораздо больше, но на данный момент - это всё.

Больше примеров с циклами

Давайте рассмотрим несколько примеров по использованию цикла for и операторов инкремента/декремента.

Для начала посчитаем от 0 до 4:

for (i=0; i<5; i++)
{
 // здесь располагается ваш код...
 // он будет выполнен для i = 0, 1, 2, 3, 4
}

А теперь от 4 до 0:

for ( i=4; i>=0; i--)  // обратите внимание, что в таком цикле переменная i должна быть знакового типа
{
 // здесь располагается ваш код...
 // он будет выполнен для i = 4, 3, 2, 1, 0
}

А можем ли мы применить цикл for в качестве бесконечного главного цикла программы? Почему бы и нет:

main()
{
 // 0. Код инициализации
 ...
 
 // 1. Главный цикл приложения
 for ( ; 1; )
 {
  ... 
 }
} // main

Если вам нравится, вы можете использовать этот вариант. Большинство же программистов (и мы в том числе) по установившемуся обычаю используют для этого оператор while.

Массивы

Перед написанием очередного примера мы познакомимся с ещё одной возможностью языка Си: массивами. Массив - это просто непрерывный блок памяти, содержащий заданное число идентичных элементов одинакового типа. Объявив массив один раз, вы сможете получать доступ к каждому элементу с помощью имени массива и индекса. Объявить массив также просто, как и одинарную переменную - просто добавьте после имени переменной квадратные скобки с указанием в них нужного числа элементов:

char c[10]; // объявляет c как массив из 10 8-битных целых
int  i[10]; // объявляет i как массив из 10 16-битных целых
long l[10]; // объявляет l как массив из 10 32-битных целых

Те же квадратные скобки используются и для доступа к значению каждого элемента массива:

a = c[0];     		// скопировать значение первого элемента массива c в переменную a
c[1] = 123;   		// присвоить значение 123 второму элементу массива c
i[2] = 12345;		// присвоить значение 12345 третьему элементу массива i
l[3] = 123* i[4];	// в четвёртый элемент массива l занести значение, равное 123 умножить на значение пятого элемента массива i

Примечание: в языке Си индексация элементов массива начинается с 0, то есть для массива с размером N элементы будут иметь индексы 0, 1, 2, ... (N-1).

Именно в работе с массивами цикл for показывает все свои преимущества. Взгляните на этот пример: здесь мы объявляем массив из 10 целых чисел и инициализируем каждый элемент константным значением 1:

int a[10]; 	// объявляем массив на 10 значений типа int: a[0], a[1], a[2]...a[9]
int i;  	// индекс цикла
for (i=0; i<10; i++)
{
 a[i] = 1;
}

Новый пример

Лучший способ завершить наш урок - собрать в одном примере все элементы Си, изученные нами на данный момент. В нашем новом проекте мы с помощью светодиодов, подключенных к PORTA на демонстрационной плате Explorer16, попробуем изобразить "бегущую строку". Как насчёт строки "Hello World?", или более скромной "HELLO"?

А вот и код:

#include 

// 1. Задаём временные константы
#defi ne SHORT_DELAY 100
#defi ne LONG_DELAY 800

// 2. Объявляем и инициализируем массив с растром сообщения
char bitmap[30] = { 
 0b11111111, // H
 0b00001000,
 0b00001000,
 0b11111111,
 0b00000000,
 0b00000000,
 0b11111111, // E
 0b10001001,
 0b10001001,
 0b10000001,
 0b00000000,
 0b00000000,
 0b11111111, // L
 0b10000000,
 0b10000000,
 0b10000000,
 0b00000000,
 0b00000000,
 0b11111111, // L
 0b10000000,
 0b10000000,
 0b10000000,
 0b00000000,
 0b00000000,
 0b01111110, // O
 0b10000001,
 0b10000001,
 0b01111110,
 0b00000000,
 0b00000000
 };
 
// 3. Основная программа
main()
{
 // 3.1 Объявление переменных
 int i;     // i будет работать как индекс 
 
 // 3.2 Инициализация
 TRISA =   0xff00; // выводы PORTA, подключенные к светодиодам, настраиваем как выходы
 T1CON =   0x8030; // TMR1 включен, предделитель 1:256 Tclk/2
 
 // 3.3 Главный цикл   
 while( 1)
 {
    // 3.3.1 Цикл вывода изображения
    for( i=0; i<30; i++)
    {
     // обновить светодиоды
     PORTA = bitmap[i];    
     // короткая пауза
     TMR1 = 0;
     while (TMR1 < SHORT_DELAY)
     {
     }
    } // for i
    
    // 3.3.2 длинная пауза
    PORTA = 0;    // выключить светодиоды
    // длинная пауза
    TMR1 =  0;
    while (TMR1 < LONG_DELAY)
    {
    }
  } // главный цикл
} // main

В разделе 1 мы объявили пару временных констант для управления скоростью мигания для выполнения и отладки.

В разделе 2 мы объявили и инициализировали массив 8-битных целых чисел из 30 элементов, в каждом из которых содержится конфигурация светодиодов для нашей "бегущей строки". Подсказка: если выделить маркером единички, вы увидите наше сообщение.

Раздел 3 содержит основную часть программы, состоящую из объявления переменных (3.1), затем из инициализации микроконтроллера (3.2), и, собственно, главного цикла (3.3).

Главный цикл (while) в свою очередь делится на две части:

3.3.1 - Содержит последовательность мигания светодиодов, все 30 шагов, которая будет проиграна на плате слева направо. Цикл for используется для доступа к каждому элементу массива по порядку. Цикл while используется для создания нужной задержки мигания с помощью таймера Timer1.

3.3.2 - Содержит паузу перед повторным проигрыванием, реализованную на цикле while с более длинной задержкой на таймере Timer1.

Тестирование с помощью логического анализатора

Для тестирования программы сначала воспользуемся программным симулятором MPLAB SIM и окном логического анализатора.

1) Выполните сборку проекта (согласно соответствующему списку действий);

2) Откройте окно логического анализатора;

3) Нажмите кнопку "Channels" и добавьте все выводы, подключенные к светодиодам, т.е. RA0..RA7.

Списки шагов "Настройка MPLAB SIM" и "Настройка логического анализатора" помогут вам проверить, что вы ничего не забыли.

Затем предлагаем вам вернуться к окну редактора и установить курсор на первой команде раздела 3.3.2, и выполнить команду "Run to Cursor". Тогда программа выполнит весь раздел вывода сообщения (3.3.1) и остановится перед длинной задержкой. Как только симулятор остановился на строке с курсором, вы можете переключиться в окно логического анализатора и проверить выходную диаграмму. Она должна выглядеть так:

Снимок окна логического анализатора после первой прокрутки строки

Рис. 3-1. Снимок окна логического анализатора после первой прокрутки "строки"

Чтобы помочь вам увидеть, что же получилось, для нескольких первых шагов последовательности я добавил точки, представляющих включенные светодиоды. Если вы попробуете представить светодиоды везде, где соответствующие выводы выдали высокий уровень, вы сможете прочитать наше "сообщение".

Использование демонстрационной платы Explorer16

Если у вас есть плата Explorer16, то веселье можно удвоить. Внутрисхемно запрограммируйте PIC24 при помощи MPLAB ICD2.

Если всё прошло удачно, и свет в комнате не очень яркий, то вы сможете увидеть мигающее сообщение, если "подвигаете" платой. Хотя этот эксперимент далёк от совершенства. При использовании логического анализатора мы можем выбрать, какую часть последовательности визуализировать и с какой точностью, и "зафиксировать" её на экране. Что же касается демонстрационной платы, то синхронизировать её движение с миганием светодиодов достаточно проблематично.

Попробуйте откорректировать временные константы под удобную вам скорость (после нескольких экспериментов удалось подобрать значения 100 и 800 для короткой и длинной задержек соответственно, хотя ваши предпочтения могут отличаться).

Подводим итоги

В этом уроке мы кратко рассмотрели объявление некоторых основных типов переменных, включая целые числа и числа с плавающей точкой различных размеров. Также мы применили объявление и инициализацию массива, чтобы создать нашу светодиодную последовательность, а с помощью цикла for воспроизвели её.

Замечания для экспертов по ассемблеру

Если вы подумаете, что операторы инкремента и декремента будут оттранслированы компилятором в ассемблерные команды inc и dec, вы будете почти правы. "Почти", потому что на самом деле эти операторы гораздо умнее. Если переменная является целым числом, как в примере выше, то это действительно так. Но вот если они применяются к указателю (тип переменной, в которой хранится адрес памяти, а не данные), то они будут менять адрес на необходимое количество байтов, в зависимости от того, какой тип имеет указатель. Например, указатель на 16-битное целое (int) меняет свой адрес на 2, указатель на 32-битное длинное целое (long int) меняет свой адрес на 4, и т.д.. Чтобы удовлетворить своё любопытство, переключитесь на ассемблерный код и посмотрите, как MPLAB C30 выбирает наилучший код, в зависимости от ситуации.

Циклы в Си могут сбивать с толку. Нужно ли проверять условие вначале или в конце? Использовать цикл for или нет? На самом деле в некоторых случаях алгоритм сам продиктует вам, какой тип цикла использовать, но в основном выбор будет за вами. Выбирайте такой цикл, который сделает ваш код более читаемым, либо, если это не имеет значения, пользуйтесь тем, который вам больше нравится и будет удобнее.

Замечания для экспертов по микроконтроллерам PIC

В зависимости от архитектуры целевого микроконтроллера и, в конечном счёте, от арифметико-логического устройства (АЛУ), работа с байтами или словами может сильно отличаться с точки зрения эффективности и компактности кода. Если в PIC16 и PIC18 имеется весьма сильный стимул применять байтовые целые, где это только возможно, то в PIC24 с его 16-разрядной архитектурой можно манипулировать двухбайтовыми целыми с той же эффективностью. Единственным ограничивающим фактором постоянного использования 16-битных целых в компиляторе MPLAB C30 является ограниченность внутренних ресурсов микроконтроллера, а именно в этом случае - памяти ОЗУ.

Замечания для экспертов по Си

Несмотря на то, что PIC24 имеет относительно большой объём памяти ОЗУ, в области встраиваемых приложений всегда будет идти борьба за снижение стоимости и размеров. Если вы учились программировать на Си на ПК, вы, скорее всего, никогда не задумывались об использовании в качестве индекса в цикле какого-либо типа данных меньше int. Теперь же самое время подумать об этом. В некоторых случаях, убрав из вашего приложения лишний байт, вы сможете выбрать меньшую модель PIC24, таким образом сэкономив несколько рублей, что в дальнейшем, при производстве тысячных или миллионных партий поможет сэкономить значительные деньги. Другими словами, если вы научитесь задавать вашим переменным минимально необходимый размер, вы станете более лучшим разработчиком встраиваемых систем, ведь, в конце концов, в этом и состоит инженерное искусство.

Советы и трюки

Это уже третий урок, и вы, вероятно, заметили, что в третий раз мы указываем вам начать симуляцию установкой курсора на первой строке кода и выполнить команду "Run To Cursor" (или установить точку останова), вместо простого пошагового прохода через код. Почему бы нам просто не запустить, например, режим анимации сразу после полной сборки проекта?

Всё дело в инициализационном коде C0. Добавим также, что это ещё и из-за навязчивого желания MPLAB'а оградить вас от низкоуровневых деталей. Действительно, MPLAB даже не хочет показывать курсор (большая зелёная стрелка), если вы попытаетесь пройти через этот код в пошаговом режиме - это просто сбивает с толку. Он не позволит вам посмотреть трассировку кода C0 даже в окне "Disassembly Listing".

Но код C0 начинает делать для вас интересные вещи, и вам, наверное, становится любопытно. Например, в нашем последнем упражнении мы объявили массив с именем bitmap[] и проинициализировали его определённой последовательностью значений. Массив, будучи структурой данных, во время выполнения программы размещается в памяти ОЗУ, так что компилятор должен указать коду C0 скопировать содержимое массива из таблицы во Flash-памяти сразу же после запуска программы.

Единственный способ взглянуть на внутреннюю работу кода C0 - это открыть окно памяти программ "Program Memory" ("View->Program Memory"), выбрать режим "Symbolic" (с помощью кнопок вверху окна) и терпеливо исследовать ассемблерный код. Несколько характерных "меток" в этом коде немного помогут вам разобраться.

Первая строка в окне памяти программ будет соответствовать вектору сброса PIC24 и всегда будет содержать переход на правильное начало программы.

0000  goto _reset

Вам понадобиться прокрутить несколько страниц с таблицей векторов прерываний (о которой вы скоро узнаете). В принципе, вам нужно найти метку _reset. Чуть ниже вы распознаете три важных участка кода:

- инициализация указателя стека (w15)

   _reset mov.w #0x81e,w15

- вызов подпрограммы инициализации переменных (ОЗУ)

    rcall _data_init

- вызов функции main()

    call main

- команда программного сброса по завершении программы

    reset

Надеюсь, что на данный момент ваше любопытство удовлетворено. Если в будущем при отладке вы не найдёте курсор, все шансы за то, что он именно здесь. Если вдруг из-за чего-то (сбоя программы или внешнего события) будет происходить сброс микроконтроллера, вам придётся пройти в пошаговом режиме через самое сердце кода C0.

Упражнения

1) Улучшите синхронизацию светодиодов и "движения" платы, добавив ожидание нажатия кнопки.

2) Добавьте переключатель, чтобы задавать инверсное движение и "проигрывать" последовательность зажигания светодиодов в обратном порядке.


© PIClist-RUS (piclist.ru), 2009 г.

PIClist RUS (piclist.ru) © 2009
все права сохранены. перепечатка статей и переводов с данного сайта запрещена.