|
|
|
|
185576 119 0 0 |
|
Опции темы | Поиск в этой теме |
19.10.2010, 15:59 | 1 |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
Основы программирования контроллеров AVR на Си
По просьбам посетителей форума пишу мини-учебник по началам программирования.
Предполагается, что на компьютере уже установлен компилятор Си для AVR (avr-gcc, он же в Windows называется WinAVR) с прилагаемой к нему стандартной библиотекой avr-libc. Также предполагается, что читатель умеет запускать его и делать из файла *.c файл *.hex, а потом прошивать его в микроконтроллер. Все это здесь объясняться не будет, а рассмотрим мы только написание собственно программы, то есть файла на Си. 1. Основы синтаксиса Си. Простейшие программы. Начнем с того, что напишем на Си программу, которая ничего не делает. Вот она: Код:
int main(void) { /* не делаем ничего */ return 0; } основное тело программы в Си пишется в функции main Знаками /* ... */ в Си обозначаются комментарии. Теперь следовало бы написать программу "Hello World". Но у нас нет экрана, куда можно было бы вывести такое сообщение. Поэтому мы просто включим светодиод. Подключим светодиод к выводу PORTB0 нашего микроконтроллера через резистор так, чтобы он горел при появлении единицы на этом выводе. Программа, включающая его, будет выглядеть так: Код:
#include <avr/io.h> int main() { DDRB = 0x01; PORTB = 0x01; return 0; } 2. Работа с портами ввода-вывода AVR. Для каждого порта микроконтроллера в библиотеке <avr/io.h> определены три специальных имени, через которые с ним можно работать из программы. Это PORTx, PINx и DDRx, где x - какая-то буква. Например, DDRB и PORTB. Каждый из них представляет собой 1 байт (число из 8 битов, то есть от 0 до 255), каждый бит соответствует одному из проводов порта (нумерация битов справа налево от 0 до 7). Регистр DDR отвечает за направление порта: 0 - вход, 1 - выход. Регистр PORT в режиме выхода просто выдает 0 или 1 на соответствующий вывод. Регистр PORT в режиме входа включает (1) или выключает (0) резистор подтяжки вывода на плюс. Регистр PIN во всех режимах отражает реальное состояние нулей и единиц на ножках контроллера. Он только для чтения, пытаться изменять его бесполезно. У порта на выходе его содержимое просто повторяет содержимое регистра PORT (если только на выводах контроллера не КЗ), а у порта на входе это и есть регистр, через который читают входы. Обращаются к регистрам из Си просто - подставляют их в арифметические выражения. Чтобы зажечь светодиод на PORTB0, нам надо сделать две вещи - переключить PORTB0 в режим выхода (поставить DDRB в двоичное 00000001, то есть B7-B1 входы, а B0 выход), а потом в PORTB поднять 0-1 бит (поставить тоже в 00000001, то есть на B7-B1 выключить резисторы подтяжки [они входы], а B0 сделать единицей). Оба числа в десятичной системе - просто 1. Можно было бы написать: Код:
DDRB = 1; PORTB = 1; Код:
DDRB = 0x01; PORTB = 0x01; Если мы хотим зажечь два светодиода сразу, на PORTB0 и на PORTB7, то мы напишем это так: Код:
int main() { DDRB = 0x81; PORTB = 0x81; return 0; } Упражнение. Что надо написать, чтобы зажечь PORTB4, PORTB0 и PORTB7 одновременно? |
19.10.2010, 17:42 | 2 |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
3. Условия и циклы.
До сих пор мы работали только с выходами контроллера и выполняли только одну операцию. В реальности контроллер обычно следит за внешними событиями и реагирует на них. Научимся заставлять контроллер повторять одни и те же действия бесконечно. Пусть на PORTB подключены 8 светодиодов, а на PORTA - 8 кнопок на землю (при отпущенной кнопке на выводе 1 за счет резистора подтяжки, при нажатой - 0). Светодиоды подключены с выводов на плюс питания через резисторы - при 0 горят, при 1 гаснут. Задача: сделать, чтобы при нажатии кнопки соответствующий светодиод загорался, а при отпускании - гас. Совершенно очевидно, что основная часть кода программы будет выглядеть так: Код:
PORTB = PINA; Код:
DDRB = 0xFF; DDRA = 0xFF; PORTA = 0xFF; Код:
#include <avr/io.h> int main() { DDRB = 0xFF; DDRA = 0xFF; PORTA = 0xFF; while (1) { PORTB = PINA; } return 0; } Следующая задача. В той же схеме переделаем прошивку так, чтобы при нажатии любой кнопки все светодиоды загорались, а при отпускании - гасли. Зажечь все - это PORTB = 0xFF, а погасить - это PORTB = 0x00. Если нажата хотя бы одна кнопка, то PINA будет не равен 0xFF. В Си условия "больше", "меньше" и т.п. пишутся так: < меньше > больше >= больше или равно <= меньше или равно == равно != не равно Обратите внимание, "равно" пишется двумя знаками равенства. Один знак равенства - это присваивание, а не сравнение! Это частая ошибка. Если надо составить сложное условие из простых, то используются операторы: && и || или ! не а также простые круглые скобки, как в арифметике, для указания порядка действий. Например: ! (x > 3) || (y < 4) означает "x не больше 3 или y меньше 4". И еще одна особенность: если в качестве условия подставить просто число, без всяких сравнений, это будет то же самое, что "число не равно 0". Именно этим мы пользовались, когда мы писали while (1) - это было то же самое, что написать while (1 != 0). Осталось понять, как пишется "если". Очень просто - if. Программа выглядит теперь так: Код:
#include <avr/io.h> int main() { DDRB = 0xFF; DDRA = 0xFF; PORTA = 0xFF; while (1) { if (PINA != 0xFF) { PORTB = 0x00; } else { PORTB = 0xFF; } } return 0; } |
19.10.2010, 18:30 | 3 |
Завсегдатай Фонарёвки
|
Получилось немного в кучу В идеале, "синтаксис Си" и "вещи, специфичные для МК", надо написать отдельными пособиями. И то и другое интересно, но кто уже имеет представление о самом Си, тому будет немного скучно читать про циклы и условия
|
19.10.2010, 18:36 | 4 | |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
Цитата:
#include <avr/io.h> Регистры периферии отбражаются на volatile uint8_t-переменные, имена которых совпадают с именами регистров в даташите микроконтроллера. avr-gcc -mmcu=процессор -Os -Wall -Wextra --pedantic -o file.bin file.c avr-objcopy -O ihex file.bin file.hex Ну и про прерывания отдельно объяснить. Я решил специально написать одновременно про AVR и про Си, потому что многим требуется "по-быстрому" написать прошивку, не вникая в тонкости языка. Я хочу дать рецепт, как делать не содержащие сложных алгоритмов прошивки, способные мигать светодиодами, делать ШИМ, крутить шаговые двигатели и т.п. Для более сложных задач вроде взаимодействия с компьютером знания одного лишь Си все равно недостаточно, там уже алгоритмы всерьез задействуются. |
|
07.11.2010, 18:35 | 5 |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
4. Переменные.
Почти всегда в программах требуется сохранять промежуточные результаты вычислений, вести подсчет и так далее. Для этого используются переменные - именованные области для хранения данных. Обычно считается, что переменной соответствует некоторая ячейка оперативной памяти, но современные компиляторы умеют использовать под переменные и регистры. В языке Си переменная обязана иметь тип. Тип переменной указывает, какого рода хранится информация в ней. Переменные бывают беззнаковые и знаковые; беззнаковые хранят число в виде просто скольки-то битов, а знаковые используют дополнительный код. Основные типы переменных: Код:
char - целое число для хранения кода символа, обычно 1 байт (8 бит) short - маленькое целое число, обычно 2 байта (16 бит) int - обычное целое число, обычно 2-4 байта (16-32 бита) long - большое целое число, обычно 32 бита long long - очень длинное число (64 бита), поддерживается не всеми системами Простой пример работы с переменными: Код:
int a; int b; int c; a = 3; b = 4; c = a + b; Нельзя забывать, что AVR - 8-битная система, и при работе с переменными больше 8 бит может генерироваться довольно длинный код. Нетрудно заметить, что стандарт Си не гарантирует точных размеров переменных. Это может быть неудобно, поэтому в стандартной библиотеке существуют предопределенные типы фиксированных размеров. Для их использования надо включить stdint.h. Код:
#include <stdint.h> int8_t - знаковый 8 бит uint8_t - беззнаковый 8 бит ("просто байт") int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t - числа от 16 до 64 бит Код:
#include <stddef.h> size_t - беззнаковое число достаточного размера, чтобы в него влез размер любого блока памяти ssize_t - то же, знаковое |
07.11.2010, 18:47 | 6 |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
5. Оператор asm.
Попробуем помигать светодиодом. Код:
#include <avr/io.h> int main() { DDRB = 0x01; while (1) { PORTB = 0x01; PORTB = 0x00; } return 0; /* программа никогда не попадет сюда */ } Мы пока не хотим учиться работать с аппаратными таймерами AVR, предназначенными для этой задержки, поэтому просто заставим процессор ничего не делать (точнее, делать нечто бесполезное). Например, так: Код:
#include <avr/io.h> int main() { unsigned int c; DDRB = 0x01; while (1) { PORTB = 0x01; c = 0; while (c < 50000) { c = c + 1; } PORTB = 0x00; c = 0; while (c < 50000) { c = c + 1; } } return 0; /* программа никогда не попадет сюда */ } Код:
#include <avr/io.h> int main() { unsigned int c; DDRB = 0x01; while (1) { PORTB = 0x01; c = 0; while (c < 50000) { asm volatile ("nop"); c = c + 1; } PORTB = 0x00; c = 0; while (c < 50000) { asm volatile ("nop"); c = c + 1; } } return 0; /* программа никогда не попадет сюда */ } Оператор asm позволяет вставить в программу кусочек, написанный на ассемблере. Слово "volatile" здесь означает "не оптимизировать ассемблерную вставку". В данном случае мы вставили просто nop (команду ничегонеделания), но можно вставить и довольно длинный код. Ассемблерные вставки в Си достаточно хитрые - в них можно доверить компилятору самому выбрать свободные регистры и даже небольшую оптимизацию. На практике же asm используется только для вызова таких вещей, как nop, sleep, cli и sei, недоступных из Си напрямую. |
07.11.2010, 18:54 | 7 |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
6. Цикл for.
В нашей программе дважды встретилась конструкция: Код:
x = 0; while (x < 50000) { ... x = x + 1; } Код:
x = x + 1; Код:
++x; Код:
x = 0; while (x < 50000) { ... ++x; } for (с чего начать ; до каких пор продолжать ; что делать после каждого прохода) С его использованием код будет выглядеть следующим образом: Код:
for (x = 0; x < 50000; ++x) { ... } |
07.11.2010, 19:01 | 8 |
Завсегдатай Фонарёвки
|
Расскажи, как Си хранит переменные. Обычно я видел листинги, где на вызове каждой функции, он вытаскивает переменные из озу списком (иногда длинным) команд LDS, а в конце пихает обратно списком команд STS.
Слышал (но не видел ), что он может и в регистрах хранить. Можно обьявить какую-то переменную так, чтобы она всегда была в регистре? (например, если требуется быстрый доступ к ней). |
07.11.2010, 19:26 | 9 |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
Если в компиляторе включена оптимизация (флаг -O2 или -Os в командной строке gcc), то компилятор автоматически оптимизирует размещение переменных. Обычно это означает, что "нужные" переменные оказываются в регистрах, а "ненужные" не создаются вообще. ("Ненужной" становится переменная, которая используется исключительно как промежуточный результат вычислений, и от которой компилятор в состоянии избавиться - например, заменив на константу). Ячейки оперативной памяти и LDS/STS используются тогда, когда либо не хватает регистров, либо переменная по логике программы обязана находиться именно в памяти (например, если к ней применяют взятие адреса и обращение через указатели, если переменная объявлена со словом volatile, является большим массивом или структурой и т.п.).
Если в компиляторе полностью выключена оптимизация (опция -O0 ) или компилятор просто "дебильный" (gcc таковым не является), тогда действительно все переменные попадут в память. В старых компиляторах применялось слово-подсказка register для размещения переменных в регистрах. Современные компиляторы это слово обычно игнорируют, использовать его не рекомендуется. Я тестировал возможности gcc/g++ по размещению переменных в регистрах. Даже подобная конструкция: Код:
char c[128]; char p; c[32] = 12; p = c[32]; Еще очень важно при работе с переменными правильно указывать типы. А именно, использовать каждый тип строго по назначению. Неоптимальности легко возникают на конструкциях вида Код:
int x; unsigned char y; uint8_t z = x + y; |
07.11.2010, 19:35 | 10 |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
Реальный пример того, как работает оптимизация переменных. Исходный текст:
Код:
#include <avr/io.h> int main() { unsigned c[10000]; unsigned y; c[15] = 0xFF; y = c[15]; DDRA = y; PORTA = y; while (1) { } return 0; } Код:
avr-gcc -mmcu=atmega16 -Os -S avr.c Код:
.file "avr.c" __SREG__ = 0x3f __SP_H__ = 0x3e __SP_L__ = 0x3d __CCP__ = 0x34 __tmp_reg__ = 0 __zero_reg__ = 1 .text .global main .type main, @function main: /* prologue: function */ /* frame size = 0 */ ldi r24,lo8(-1) out 58-32,r24 out 59-32,r24 .L2: rjmp .L2 .size main, .-main |
07.11.2010, 19:43 | 11 | |||
Завсегдатай Фонарёвки
|
Цитата:
Цитата:
Цитата:
|
|||
08.11.2010, 21:42 | 12 | |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
Цитата:
Моя ошибка - не тот код запостил. От замены unsigned на int, char и даже на long ассемблер не меняется. |
|
08.11.2010, 22:08 | 13 |
Завсегдатай Фонарёвки
|
Напиши какой-нидь пример с прерыванием
Если надо сгенерировать сигнал определённой частоты (или какое-то событие со строгим интервалом обрабатывать) - без прерываний по таймеру ну никак не обойтись. |
08.11.2010, 23:51 | 14 |
Ветеран Фонарёвки
Регистрация: 15.02.2010 Последняя активность: 24.08.2019 11:36
Сообщений: 1342
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
+1. Я отказался от многих прерываний в индикаторе, но от компаратора таймера отказаться нельзя. Нужно в определённые моменты без тормозов тушить светодиоды, а в Си я пока не представляю как работать с прерываниями...
|
10.11.2010, 19:34 | 15 |
Завсегдатай Фонарёвки
|
Я так понимаю, в зависимости от компилятора, есть аж 3 варианта, для каждого надо писать по-своему
Для IAR Код:
#pragma vector=name_vect __interrupt void interruptName(void) { } Код:
interrupt(VECTOR_NUMBER) interruptName(void) { } Код:
#include <avr/io.h> #include <avr/interrupt.h> //обработчик прерывания таймера Т0 ISR(TIMER0_COMP_vect) { } |
10.11.2010, 22:01 | 16 |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
Мы рассматриваем gcc (WinAVR) по очень простой причине - на других компиляторах не получается такая лихая оптимизация сишной простыни в три команды ассемблера. Вторая причина - все-таки сильно нарушать стандарты ANSI не стоит, а gcc им полностью соответствует.
Полная документация на работу с прерываниями: http://avr-libc.nongnu.org/... Вкратце: Код:
#include <avr/interrupt.h> ISR(TIMER0_COMP_vect) { } Код:
while(1) { PORTA = 0xFF; asm volatile ("sleep"); PORTA = 0x00; asm volatile ("sleep"); } Вообще же для повышения надежности и контролируемости программы не рекомендуется делать в прерываниях что-либо кроме установки флажков. Напоминаю, что переменные, к которым обращаются из прерываний, обязаны быть объявлены с модификатором volatile во избежание переоптимизации в неработоспособный код. Код:
volatile uint8_t flag; ISR(TIMER0_COMP_vect) { flag = 1; } |
10.11.2010, 22:25 | 17 | ||
Завсегдатай Фонарёвки
|
Цитата:
А обычно он паралельно что-то ещё должен делать, а не только спать. Цитата:
|
||
10.11.2010, 23:58 | 18 | |
Увлеченный
Регистрация: 21.06.2010 Последняя активность: 01.08.2015 23:26
Сообщений: 180
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
Цитата:
У микроконтроллера нет такого объема памяти, чтобы он мог что-то обрабатывать долгое время, не общаясь с внешним миром. А внешний мир - это прерывания, ввод-вывод и sleep. Пример надуманный, на практике так не бывает. |
|
11.11.2010, 00:27 | 19 | ||
Завсегдатай Фонарёвки
|
Цитата:
А если там будет общение с внешними девайсами? Оно тем более может затянуться. Цитата:
Хорошо, отойдём от абстракции. Написал я недавно WAV-плеер на тиньке, прекрасно играющий с microsd карточки. На ассемблере Теперь представь: на мк с тактовой частотой 8мгц, надо играть звук 48000гц. При этом, без джиттеров (т.е. периоды между семплами должны быть равны). Свободное процессорное время отдано на обработку других устройств ввода\вывода. По расчётам, выходит, что у мк есть ~166 тактов на семпл. За это время надо прочитать 2-4 байта с карты памяти, при этом ещё и обработать протокол самой карты, а оставшееся время отдать на обработку устройств ввода. И всё это без джиттеров должно быть! Самое логичное - код, проигрывающий семпл, надо писать в прерывание, т.к. он должен выполняться немедленно. Код, который должен выполняться в фоновом режиме, писать в основном цикле. Он будет выполняться между прерываниями. Мне такой вариант кажется самым производительным. А как бы сделал ты? Только не говори, что взял бы для этой задачи другой мк или другую тактовую частоту |
||
11.11.2010, 03:51 | 20 |
Ветеран Фонарёвки
Регистрация: 15.02.2010 Последняя активность: 24.08.2019 11:36
Сообщений: 1342
Сказал(а) спасибо: 0
Поблагодарили: 0 раз(а) в 0 сообщениях
|
Цитата:
Тут то ладно, временные интервалы между прерываниями одинаковые. Разбросать "фоновый" код между ними не проблема. А что если прерывания могут возникать непоймикогда? Например программный ШИМ. И на которые тоже нужно реагировать незамедлительно, бросая все свои текущие дела. По-моему тут гемора с флагами гораздо больше, чем с отдельным обработчиком прерывания. Да и ты не уточнил, что WAV плеер тебе нужен не всегда. Тут возможно прерывание проще отключить, чем разбираться с ненужными слипами, и тем что за ними. Да и как быть с пробуждением от любого внешнего, или левого внутреннего прерывания? Нужно ещё и смотреть что именно тебя разбудило... Я уже попробовал променять прерывания на Sleep. Во многих местах это действительно оправдано, но не во всех. Не могу я представить как на "слипах" построить серьёзную программу, нормально реагирующую на все события, в любых их комбинациях. Прерывания с этим сами разберутся, а там думать и огородить приходится. В простых же программах, практически однозадачных (индикатор, восстановитель фьюзов и т.п.), получилось проще заюзать слип... |