AVR123:Глава 5

Материал из roboforum.ru Wiki

Перейти к: навигация, поиск

Вернуться на главную

Содержание

Язык Си для AVR.

Как раз то, что необходимо и достаточно для микроконтроллеров.

По умолчанию компилятор CVAVR

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

Минимальная программа на Си может быть такой:

main(){}

Эта программа не делает ни чего полезного — но это уже программа и она показывает что в программе на языке Си — должна быть главная функция main — обязательно !

Скачайте и Распечатайте.Памятка Си для МК на ОДНОЙ странице.

Рассказывая про МК я говорил вам, что: Задача программы МК:

  • читать числа из регистров и памяти МК,
  • делать что-то с числами, данными и
  • записывать числа в регистры и память.

Только так программа может общаться с МК.

Как это делать на языке Си

Регистры МК
( регистры - это ячейки-байты в памяти МК AVR ) в программе на Си имеют названия как и в ДШ и так как числа в большинстве из них можно менять - для программы регистры являются по сути переменными.
Переменная
- это набор ячеек в памяти в которых можно хранить число или числа и менять их. Переменная имеет адрес и имя.
Константа
- это как переменная но менять содержимое нельзя.

Подробней о переменных и константах написано ниже.

1) Чтобы поместить число в переменную (в регистр) в языке Си есть оператор присваивания

это знак "=" ( называемый в математике "равно" ) "=" в Си означает вычислить результат того что справа от оператора присваивания и поместить этот результат в переменную находящуюся левее оператора присваивания.

Информация
Запомните!

В Си знак "=" НЕ означает равенство!



PORTB = PINB + 34;/* Эта строчка на Си означает взять (прочитать, считать)
значение переменной (регистра) PINB, затем прибавить
к нему число 34 и поместить результат в переменную PORTB */
 
ПЕРЕМЕННАЯ = PINC; /* Эта строчка на Си означает взять (прочитать, считать)
значение переменной (регистра) PINC и поместить результат
в переменную с именем ПЕРЕМЕННАЯ */

Чтобы в Си взять (прочитать) число из регистра или значение переменной нужно написать его название НЕ непосредственно с лева от оператора присваивания !

Примеры :
a) Строка где переменная стоит слева от = но через знак &
PORTB & = 0x23;

на Си означает - прочитать содержимое переменной PORTB, затем выполнить "поразрядное (побитное) логическое И" между прочитанным значением и числом 0x23 и поместить (записать, присвоить) результат в переменную PORTB

b) Строка где переменная стоит непосредственно слева от =
PORTB = 0x23;

на Си означает - не читая содержимое переменной PORTB присвоить ей значение 0x23 уничтожив то что было там раньше.

:Вместо & "И" (AND - только 1 и 1 дают 1) могут быть и другие побитные логические операции:
• | "ИЛИ" (OR только 0 и 0 дают 0)
• ^ "Исключающее ИЛИ" (XOR изменить бит напротив "1")
• ~ "инвертирование битов" (INV изменить биты регистра)
• арифметические операции: + - * / %

С оператором присваивания используются вот такие сокращения:

ДЛИННАЯ ЗАПИСЬ СМЫСЛ СОКРАЩАЕТСЯ ДО
x = x + 1; добавить 1 x++; или ++x;
x = x - 1; вычесть 1 x--; или --x;
x = x + y; прибавить y x += y;
x = x - y; вычесть y x -= y;
x = x * y; умножить на y x *= y;
x = x / y; поделить на y x /= y;
x = x % y; остаток от деления x %= y;
x--; вычесть 1 x -= 1;
x++; добавить 1 x += 1;
Примеры :
00010010 | 01001111 // "ИЛИ" - только 0 и 0 дают 0
// англ. название OR
 
01011111 // это результат
 
// только биты_5 в обоих числах были нули
 
00010010 & 01001111 // "И" - только 1 и 1 дают 1
// англ. название AND
 
00000010 // это результат
 
// только биты_2 в обоих числах были единицы
 
00010010 ^ 01001111/* "исключающее ИЛИ" - результат любое из пары чисел в котором инвертированы (изменены) биты напротив битов
равных "1" в другом числе.
англ. название XOR */
01011101 // это результат
/* изменились биты во втором числе напротив
установленных битов 4 и 1 первого числа. */
 
~ 01001111 /* инвертировать биты
те что были "1" станут "0" и наоборот */
10110000 // это результат
Запомните !
Результатом поразрядных (побитных)

логических операций : & | ^ ~ является число ! Которое может быть интерпретировано компилятором как "истина" если оно не ноль и "ложно" если число ноль.

Числа

В компиляторе можно записывать в виде указанном в его Help ! Раздел - константы - Constants.

например - Целые числа могут быть записаны :

- в десятичной форме - 1234
 
- в двоичной форме с префиксом 0b так: 0b101001
 
- в шестнадцатеричной форме с префиксом 0x так: 0x5А
 
- в восьмеричной форме с префиксом 0 так: 0775

Числа с плавающей точкой имеют в записи эту точку и какое либо число после этой точки, так:

61.234
или так: 73.0
и так: .786
 
и могут иметь в конце F вот так: 61.234F

ТОЧКА ОБЯЗАТЕЛЬНА !

Различные представления числа D3h равно 0xD3 равно 0b1101 0011 равно 211
шестнадцатеричное число 0xD3
0 x D 3
двоичное представление - число 0b1101 0011
0 b 1 1 0 1 0 0 1 1
номера бита 7 6 5 4 3 2 1 0
два в степени равной номеру бита
128 64 32 16 8 4 2 1
число 211 в десятичном виде это сумма степеней двойки где биты равны "1"
Сложите +128 +64 +16 +2 +1

Четыре бита это 1 "нибл" (полубайт) или 1 символ в 16-ричной системе или десятичное число от 0 до 15.

"В уме" удобно оперировать ниблами:двоичный десятичный 16-ричный

двоичный десятичный 16-ричный
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
1010 10 A
1011 11 B
1100 12 C
1101 13 D
1110 14 E
1111 15 F

Для перевода чисел из одного вида в другой можно использовать калькулятор Windows в инженерном виде.

Есть в Си операции которые изменяют значение переменной и без оператора присваивания :

PORTA++; /* Эта строчка на Си означает взять значение переменной PORTA
добавить к ней 1 и записать результат обратно в PORTA
говорят: Инкрементировать регистр PORTA */
 
PORTC--; /* Эта строчка на Си означает
обратное действие!
Декрементировать - вычесть 1 из значения регистра PORTC */

Инкремент и декремент удобно использовать для изменения значения различных переменных счетчиков.

Важно помнить что они имеют очень низкий приоритет - поэтому чтобы быть уверенными в порядке выполнения желательно писать их отдельной строчкой программы !

Информация
Обратите внимание !

В конце выражения или конструкции в программе на Си ставят точку с запятой.



Длинные выражения можно писать в несколько строк.

/* ЗЕЛЕНЫМ я пишу комментарий к программе
в Си он может быть написан в несколько
строк
 
и пустых строк тоже */
 
// или в одну после двух черточек

Компилятор игнорирует все что написано в комментариях !

Вы не компилятор !

Не игнорируйте, пишите комментарии и читайте !

Когда инкремент или декремент используется в выражении то важно где стоят два знака + или - перед переменной или после переменной :

a=4;
b=7;
 
a = b++; /* Эта строчка на Си означает
Взять значение переменной b присвоить его переменной a затем добавить 1 к переменной b
и сохранить результат в b
 
Теперь a будет содержать число 7
b будет содержать число 8 */
 
a=4;
b=7;
 
a = ++b; /* Эта строчка на Си означает
Взять значение переменной b затем добавить к нему 1 и сохранить результат в b и этот же результат присвоить переменной a
 
Теперь a будет содержать число 8
и b будет содержать число 8 */

2) Арифметические операции в Си

x + y // сложение
x - y // вычитание
x * y // умножение
 
x / y /* деление.
 
Если числа целые результат - целое число с отброшенной дробной частью - не округленное !
 
т.е. если в результате деления на калькуляторе получается 6.23411 или 6.94 то результат будет просто целое число 6 - запомните !
 
Если числа с плавающей точкой, то есть float или double и записываются с точкой и числом после точки, то и результат будет число с плавающей точкой без отбрасывания дробной части
131.9739 / 6.18 даст 21.355 */
 
x % y // вычислить остаток от деления нацело
 
// примеры:
 
5 / 2 // даст 2
 
5 % 2 // даст 1
 
75 / 29 // даст 2
 
75 % 29 // даст 17

3) Операторы сравнения (или отношения):

используются для сравнения переменных, чисел (констант) и выражений.
x < y // X меньше Y
x > y // больше
x <= y // меньше или равно
x >= y // больше или равно
x == y // равно
x != y /* не равно
 
Результат выполнения этих операторов:
 
"истина" это "1" (точнее "не ноль")
 
"ложно" это "0"
 
Значения хранимые в переменных (в регистрах)
х и у НЕ изменяются!
 
Берутся (считываются) значения хранящиеся (или содержащиеся) в переменных и сравниваются */
 
! /* "НЕ" - логическое отрицание */

4) Логические операции :

| | // "ИЛИ" - только "ложь" и "ложь"
// дают "ложь"
 
&& // "И" - только "истина" и "истина"
// дают "истина"
 
! // "НЕ" - логическое отрицание
 
/* Правило - в Си считается:
 
"Ложь" (False) только ноль.
 
"Истина"(True)- не ноль. или так: (!0)
 
* /
 
!(истина) // дает "ложь"
 
!(ложь) // дает "истина"

В результате логической операции вы получаете НЕ ЧИСЛО, а логическое значение "истина" или "ложь"

Для логических операций && и || берутся результаты выражений слева и справа от знака операции преобразованные в "истину" или "ложь" и определяется логический результат операции.

Компилятор, для определенности наверно, результат "истина" превращает в 1 а не в любое отличное от 0 число.

Логические операции могут объединять несколько проверяемых условий.

Пример:
if((выражение1)&&((выражение2) || (выражение3)))
{/*
Код программы здесь будет выполняться если:
Выражение1 "Истина" (значит не ноль) и хотя бы одно из выражений 2 и 3 тоже "Истина" (значит не ноль).
 
* /};

Подробнее о логических операциях обязательно прочитайте по в низу этой страницы !

Приоритет операций в языке Си перечислены в порядке убывания приоритета. Операции, приведённые на одной строчке, имеют одинаковый приоритет. Операции, помеченные как R->L, исполняются справа налево.

  1. () [] -> .
  2. Унарные (R->L): ! ~ - * & sizeof (type) ++ --
  3. Бинарные арифметические: * / %
  4. Бинарные арифметические + -
  5. Сдвиг: << >>
  6. Сравнение: < <= > >=
  7. Сравнение: == !=
  8. Битовая: &
  9. Битовая: ^
  10. Битовая: |
  11. Логическая: &&
  12. Логическая: ||
  1. Тернарная (R->L): ?:
  2. Операции с присваиванием (R->L):
= += -= = /= &= |= ^= <<= >>=
{

{InfoBlock|Чтобы точно знать порядок выполнения операций программой используйте скобки ( )

( () + ( () * () ) ) Ведь скобки ( ) имеют наивысший приоритет.|Совет:}}

Самое интересное !

Ходовые конструкции на Си В компиляторе [CVAVR заготовки этих конструкций находятся под ярлыком "Code Templates" слева вверху. Вы можете выбирать нужные заготовки и вставлять их в свою программу.

Image:05 temp.png

5) if(){ }else{ }; идеальная конструкция если вам нужно выполнить какую то часть программы при наличии каких либо

условий или при их отсутствии :

if (выражение) { код на Си /* делать этот код если выражение "истина" - т.е. результат его вычисления
не ноль */
}
else { код на Си /* делать этот код если выражение "ложь" - т.е. результат его вычисления равен нулю */
};
 
// } else { это не обязательный элемент конструкции, без него так :
 
if (выражение) { код на Си /* делать этот код если выражение "истина" - т.е. результат его вычисления не ноль */
};

6)while(){ }; условный цикл ( цикл с условием ) - используйте если вам

нужно выполнять какой то код программы пока выполняется (существует,

справедливо, "истино" - значит "не ноль") некоторое условие, результат вычисления выражения :

while (выражение) { код на Си /* делать этот код если выражение "истина" - т.е. результат его вычисления не ноль.
Пока выполняется этот код выражение не проверяется на истинность !
После выполнения кода происходит переход к строке while снова проверять истинность выражения */
};

Цикл while имеет вариант

do - while при котором код в { } выполняется по меньшей мере один раз не зависимо от истинности условия в скобках :

do { код на Си /* сделать этот код один раз
 
затем, если выражение есть "истина" - т.е. результат его вычисления не ноль - опять делать код с начала, и так до тех пор пока выражение истина */ }
while (выражение);

7) for(;;){ }; - этот цикл позволяет выполнить часть программы нужное число раз:

char i; /* объявление переменной для for это обычная переменная Си и
значит может иметь любое допустимое имя по вашему желанию и тип */
for (i=5; i
''i = 5'' это начальное выражение, то что в начале будет в переменной i
 
Число 5 просто для примера, может быть таким, как позволяет объявление типа переменной i, в нашем случае это char в большинстве компиляторов по-умолчанию это без знаковый символьный тип - он может хранить числа от 0 до 255
''i < 20'' - контрольное выражение
Может быть с разными операторами отношения, важно лишь чтобы по ходу цикла оно становилось когда-то "ложью" - иначе цикл "зациклится" т.е. ни когда не кончится.
 
''i += 4'' - это счетчик или изменение переменной цикла.
Обычно это ''i++'' т.е.к переменной добавляется 1 каждый "прогон" цикла. Но опять же может быть таким какое вам требуется.
 
; Начальным условием: - может быть любое допустимое в Си выражение результатом которого является целое число.
; Контрольное выражение: - определяет до каких пор будет выполнятся цикл.
; Счетчик : - показывает как изменяется начальное выражение перед каждом новом выполнении цикла .
; Выражение : - значит это может быть не просто переменная, а что-то посложней, например:
 
''i =(7 + i*4)'' или ''i ='' (функция других переменных)
 
<source lang="c">
Циклы ''for(;;)'' и ''while()''
 
часто используют вот так:
 
while(1);
 
for (;;);
 
/* Так написанные эти циклы означают :
 
МК выполнять эту строчку пока есть питание, нет сброса и нет прерывания.
Когда возникает прерывание, программа переходит на обработчик прерывания и (если в обработчике нет перехода в другое место программы) по завершении кода обработчика опять возвращается в такой цикл. */
 
while(1){
код программы
};
 
for (;;){
код программы
};
 
/* Так написанные эти циклы означают :
 
МК выполнять код программы пока есть питание, нет сброса и нет прерывания.
Когда возникает прерывание, программа переходит на обработчик прерывания и (если в обработчике нет перехода в другое место программы) по завершении кода обработчика опять возвращается сюда и продолжает выполнять код программы */

8)switch(){ }; - оператор множественного выбора,

позволяет вам сделать выбор из нескольких вариантов.

switch (выражение) {
case 5: код на Си
/* этот код будет выполняться если результат вычисления выражения равен числу 5
 
на этом работа оператора switch закончится */
break;
 
case -32: код на Си
/* этот код будет выполняться если результат вычисления выражения равен отрицательномц числу -32
 
на этом работа оператора switch закончится */
break;
 
case 'G': код на Си
/* этот код будет выполняться если результат вычисления выражения равен числу соответствующему символу G в таблице ASCII
 
на этом работа оператора switch закончится */
break;
 
default: код на Си
/* этот код будет выполняться если результат вычисления выражения не равен ни 5 ни -32 ни 'G'
 
А так же после выполнения кода не
имеющего в конце break;
 
на этом работа оператора switch закончится */
};
 
/* switch закончен - выполняется дальнейший код программы */
case 
- может быть столько сколько вам нужно, чтобы программа работала быстрее старайтесь наиболее вероятные варианты располагать выше!
default 
- не обязателен. Его можно расположить и не в конце.
break; 
- если его не использовать то найдя нужный вариант программа будет выполнять и следующие ниже условия case

[Прочитайтеподробней о switch с примерами.

[Скачайте и распечатайте таблицу символов ASCII на ОДНОЙ странице!

9) goto - оператор безусловного (немедленного) перехода.

... какой-то код нашей программы на Си ...
 
mesto_5: /* сюда мы попадем после выполнения строки программы goto mesto_5   */ 
    код будет выполнятся после goto mesto_5;
 
... какой-то код нашей программы на Си ...
 
mesto_1: /* сюда мы попадем после выполнения строки программы goto mesto_1   */
    код будет выполнятся после goto mesto_1;
 
... какой-то код нашей программы на Си ...
 
goto mesto_1; /* перейти в то место программы где в начале строки написано mesto_1:     */
 
... какой-то код нашей программы на Си ...
 
goto mesto_5; /* перейти в то место программы где в начале строки написано mesto_5:     */
 
... какой-то код нашей программы на Си ...



Используйте goto с осторожностью! Думайте к чему может привести выполнение функций или конструкций вашей программы не до конца.

CVAVR помогает вам в контроле не позволяя где угодно втыкать goto


	Оператор Си   ?   работает почти как  if - вот так: 

Пример функция в которую передается значение переменной val_d

Вызов функции и передач в нее числа хранящегося в переменной с именем peremennaya

resultat = funkziya(peremennaya);


В функции число из peremennaya будет помещено в val_d и обработано.

int funkziya(int val_d) {

 return ((val_d>511)?(-1024+val_d):(val_d));

}

( (выражение) ? ( если выражение истина ) : ( если выражение ложно ) )

если val_d>511 то функция возвратит val_d уменьшенное на 1024

если val_d=<511 то функция возвратит val_d не меняя его.

Возвращаемое функцией значение val_d будет помещено в переменную в строке вызова функции - resultat

Подробнее о функциях будет написано ниже.


Ну вот - ПОЧТИ всё что нужно нам из Си !


Как использовать описанное выше вы можете посмотреть в примерах к компилятору !

Примеры в папке :

C:\CVAVR\EXAMPLES

Открывайте файлы исходников .с и разбирайте текст программ - что делает каждая строчка кода !

Это великолепный способ само-обучения программированию !

Новичку понадобятся для понимания программ написанных профи :

Указатели, Структуры и Союзы.

Прочитайте о них в он-лайн книгах по Си которые расположены ниже на страничке. Примеры применения указателей, структур и союзов в разных компиляторах вы найдете в FAQ к курсу по AVR - скачайте и читайте.

Структура программы на языке Си.

Программа на языке Си это текстовый файл, обычно с расширением .c

Текст программы называют исходным или "исходником" или "сурцом" от анг. source code - это вам ключевые слова для поиска

Весь исполняемый код программы на Си находится в функциях - т.е. в фигурных скобках { исполняемый код программы }


Программа на Си имеет определенную структуру :

1) заголовок

2) включение необходимых внешних файлов - #include

3) ваши определения для удобства работы - #define

4) объявление глобальных переменных и констант

   Глобальные переменные и константы   

- объявляются вне какой либо функции.

   т.е. не после фигурной скобки   {

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

5) описание функций - обработчиков прерываний

6) описание других функций используемых в программе

7) функция main <- это единственный обязательный пункт !


Это не жесткий порядок а ориентировочный !

Иногда п.6 это прототипы функций, а сами функции описываются полностью после п.7

Прототип функции - показывает образец того как применять функцию в программе, какие значения в нее передаются и если она возвращает какое-то значение то прототип указывает тип возвращаемых данных. Прототип не имеет скобок { } а после скобок ( ) ставится  ;

Функция - имеет { "тело" } в фигурных скобках. Тело это код на Си определяющий то что делает функция.

после вызова функции не ставится !

Программа на Си начинает работу с функции main()

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

main(){
 
... какой то код программы ...
 
вызов функции_1; //программа перейдет в функцию_1
 
строка программы; // будет выполнятся после 
                  // возврата из функции_1 
 
... какой то код программы ...
 
}

На странице 3. "Прерывания в AVR" - вы уже читали, что описанный выше ход программы может нарушаться прерываниями.




Пример программы на Си

с описанной выше структурой я буду писать на голубом фоне.

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

Ниже будет написан пример ОДНОЙ программы на Си.

/* пункт 1   заголовок программы 
 
Он оформляется как комментарий, и обычно содержит информацию 
 
- о названии, назначении, версии и авторе программы 
- краткое описание алгоритма программы 
- пояснения о назначении выводов МК и режиме его работы, фьюзы
- компилятор, инструменты и их версии 
- другие сведения которые вы считает полезным указать 
 
*/
 
// комментарий после двух косых черт пишут в одну строку!
 
 
 
// пункт 2   включение внешних файлов 
 
#include <mega16.h> /* перед компиляцией, препроцессор компилятора вставит вместо этой строчки содержимое (текст) заголовочного файла "хидера" mega16.h - этот файл содержит перечень регистров имеющихся в МК ATmega16 и соответствие их названий их физическим адресам в МК.
 
Посмотрите его содержание !!! 
 
CVAVR\inc\mega16.h           
 
Не забывайте указать какой МК вы используете в свойствах проекта в компиляторе  */
 
 
//delay functions
#include <delay.h> 
/* перед компиляцией, препроцессор компилятора вставит вместо этой строчки текст "хидера" delay.h - этот файл содержит функции для создания пауз в программе.
 
Теперь чтобы сделать паузу вам нужно лишь написать :   */
 
delay_us(N); /* сделать паузу N (число) микроСек 
это должна быть константа - НЕ переменная !!!
например delay_us(12 + 7*3); 
например delay_us(117);           */
 
delay_ms(x); /* сделать паузу x милиСек
x - может быть переменная, выражение или число 
от 0 до 65535 (тип unsigned int)   
например delay_ms(3280);
например delay_ms(переменная);  
например delay_ms(переменная*4 + 760);   */
 
 
// пункт 3    определения пользователя
 
// AD7896 control signals PORTB bit allocation
#define ADC_BUSY PINB.0
#define NCONVST PORTB.1
/* после этих двух строк, перед компиляцией, препроцессор компилятора заменит в тексте программы ADC_BUSY на PINB.0 и NCONVST на PORTB.1 
<source lang="C">
Таким образом вместо того что бы помнить что вывод занятости AD7896 подключен у вас к ножке PB0 вы можете проверять значение осмысленного понятия ADC_BUSY - "АЦП занят" 
 
а вместо управления абстрактной ножкой PB1 (через PORTB.1) вы можете управлять "НьюКонвешнСтат" - NCONVST - "стартовать новое АЦ преобразование"
<source lang="C">
#define   -   Это   удобно !   Но ВОВСЕ не обязательно.    
*/
#define INIT_TIMER0 TCNT0=0x100L-F_XTAL/64L/500L 
/* этот пример показывает что определения могут быть и сложней !  */
  1. define - может содержать и некоторые переменные, вместо которых в тексте программы могут быть подставлены и числа и слова. Может определять даже сложные, полноценные функции.

Например:

  1. define invbit(p,n) (p=p^bit(n))

Здесь переменные величины это 'p' и 'n'. Теперь для инвертирования бита 5 в регистре PORTB вам достаточно написать в программе:

invbit(PORTB,5);

Кроме того в самой правой части эти переменные величины могут быть связаны и арифметическими операциями и таких переменных может быть много.

Примеры #define есть в FAQ к курсу.

Определения БИТ-ов AVR (соответствие номера бита в регистре его названию по ДШ) есть в "хидерах" .h в компиляторах ICC, IAR, WinAVR и других компиляторах,

Но их нет в хидерах CodeVisionAVR - это не позволяет напрямую вставлять в текст программы примеры кода из даташит МК

Поэтому я сделал для вас файл - заголовок m8_128.h содержащий определения битов некоторых AVR.

Скачайте его и добавьте в программу вот так:

(Если вы читаете курс с начала и делаете то, что предлагается то этот файл у вас уже есть).

#include <mega16.h> 
/* сперва обычный "хидер"-заголовок для МК
   используемого в вашей программе */
 
#include <m8_128.h> 
/* затем мой "хидер"-заголовок с определениями 
   названий и номеров битов для используемого МК */

Теперь вы можете использовать примеры на Си из ДШ на ваш МК !

Мой файл m8 128.h содержит определения битов для микроконтроллеров ATmega8 ATmega16 ATmega32 ATmega64 ATmega128


Для всех AVR определения битов есть в .h заголовках WinAVR вот архив (127 Кб) скачайте и используйте если понадобится.


Мастер начального кода программы в компиляторе ICC умеет по вашему желанию автоматически делать

  1. define для ножек для ножек МК !

Подробней про это и с картинкой смотри в соответствующей задаче курса.

Пункт 4.

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

Перед использованием переменной в программе на Си её необходимо объявить - т.е. указать компилятору какой тип данных она может хранить и как она называется.

Наиболее подробно об этом по ссылке:

1.2. ТИПЫ ДАННЫХ И ИХ ОБ ЯВЛЕНИЕ

Ниже сжато - самое главное:

Формат объявления переменной таков:

[<storage modifier>] <type definition> <identifier>;


[<storage modifier>]
- необязательный элемент,

он нужен толон нужен только в некоторых случаях и может быть:

extern 
- если переменная может использоваться в других файлах исходного кода программы, например объявляется во внешнем файле - хидере delay.h приведенном выше, а используется в основном файле программы.
volatile 
- ставьте если нужно предотвратить возможность повреждения содержимого переменной в прерывании, и не позволить компилятору попытаться выкинуть её при оптимизации кода.

Ставьте всегда если не знаете точно - нужно или нет !

пример:

volatile unsigned char x;


static 
- если переменная локальная т.е. объявлена в какой либо функции после скобки { и должна сохранять свое значение до следующего вызова этой функции.
register 
- разместить переменную в регистрах AVR - это может ускорить доступ к ней. CVAVR по-умолчанию размещает переменные в регистрах до их заполнения. Но размещение переменных в регистрах делает их не видимыми при отладке в PROTEUS.
eeprom 
- разместить переменную в EEPROM. Это энергонезависимая память - значение таких переменных сохраняется при выключении питания и при перезагрузке МК.

пример: eeprom unsigned int x;

Если это первая переменная в EEPROM то её младший байт будет помещен в ячейку 1 EEPROM а старший в ячейку 2. Ячейка 0 не используется так как рекомендует производитель. CVAVR похоже не использует и 0 и 1 ячейки EEPROM. Необходимо помнить что запись в EEPROM длительный процесс - по таблице "Table 1. EEPROM Programming Time" это 8500 тактов процессора.

Количество записей в ячейки EEPROM ограничено !

Подробней в "Accessing the AVR internal EEPROM".

Книги и учебники по радиоэлектронике и микроконтроллерам там

Скачать весь курс по AVR одним архивом


Глобальные переменные объявляются до появления их в тексте какой либо функции. После объявления, глобальные переменные доступны в любой функции программы.

Локальные переменные объявляются в самом начале функций - т.е. сразу после фигурной скобки {

Локальные переменные доступны только в той функции где они объявлены! В разных функциях могут быть объявлены локальные переменные с одинаковыми именами - я не советую вам так делать.

Советую не использовать ЛОКАЛЬНЫЕ переменные в главной функции main.


<type definition> - тип данных которые может хранить переменная.
 
наиболе часто используемые типы данных :
 
unsigned char - хранит числа от 0 до 255 (байт)
 
unsigned int - хранит числа от 0 до 65535 (слово == 2 байта)
 
unsigned long int - хранит от 0 до 4294967295  (двойное слово == 4 байта)

Подробнее все типы данных посмотрите в Help

CVAVR\bin\CVAVR.HLP

Раздел "Data Types"

Уточняйте ТИПЫ данных в руководстве к вашему компилятору !

Вместо unsigned char - можно писать писать просто char, так как компилятор по-умолчанию считает char без знаковым байтом.

А если вам нужен знаковый байт то объявляйте его так :

signed char imya_peremennoi;

<identifier> - имя переменной - некоторый набор символов по вашему желанию, но не образующий зарезервированные слова языка Си.

Выше был уже пример идентификатора - имени переменной: imya_peremennoi


Давайте осмысленные имена переменным и функциям - напоминающие, "говорящие" вам об их назначении.

Принято использовать маленькие буквы, а для отличия имен переменных от названия функций - имена переменных можно например начинать с буквы, а названия функций (кроме main конечно) с символа подчеркивания.

Например так :

 moya_peremennaya        _vasha_funkziya 	 

Внимание!

Глобальные переменные, а также локальные с модификатором static - при старте и рестарте программы равны 0 если вы не присвоили им (например оператором =) иное значение при их объявлении или по ходу программы.


Подробные примеры объявления переменных посмотрите пожалуйста в разделе Variables в "Хелп" компилятора CVAVR.


Вот несколько примеров объявления переменных :

unsigned char my_peremen = 34; unsigned int big_peremen = 34034;

Это объявлены две переменные и им присвоены значения.

Первая my_peremen - символьного типа - это 1 байт, она может хранить число от 0 до 255. В данном случае в ней хранится число 34.

Вторая big_peremen - целого типа, два байта, в ней может хранится число от 0 до 65535 , а в примере в неё поместили десятичное число 34034.


Пример массива содержащего 3 числа или элемента массива.

char mas[3]={11,22,33};

Нумерация элементов начинается с 0. Т.е. элементы данного массива называются

mas[0], mas[1], mas[2]

и в них хранятся десятичные числа 11, 22 и 33.

Где то в программе вы можете написать:

mas[1] = 210;

Теперь в mas[1] будет хранится число 210

- массивы могут быть многомерными, - можно не присваивать значений элементам

  массива при объявлении.

НО только при объявлении вы можете присвоить значения всем элементам массива сразу ! Потом это можно будет сделать только индивидуально для каждого элемента.


Строковая переменная или массив содержащий строку символов.

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);

Вы можете преобразовать строковую переменную в число! Если исходная строка символов такая  :

char stroka[]="3654694"; то вот так:

perem_1 = atoi(stroka);

мы поместим в переменную perem_1 (которую должны были ранее в программе объявить как "беззнаковую целую") число 36546. Это число влезет в переменную perem_1 которая может хранить числа от 0 до 65535. А вот 9 и 4 уже не поместятся. Для бОльших чисел есть функция - atol() Чтобы использовать эти функции необходимо включить в начале программы заголовочный файл :

  1. include <stdlib.h>


Для преобразования числа в строку

есть itoa() и ltoa()

и аналогичные функции для чисел с плавающей точкой.

Подробнее об этих и других полезных функциях смотрите раздел "Standard Library Functions" справки компилятора CodeVisionAVR.

Советую вам скачать заголовочный файл

m8 128.h

Он содержит названия битов МК ATmega8 -16 -32 -64 -128 и сокращенные названия типов данных как в компиляторе IAR.

вот отрывок из него:

#define u8 unsigned char // 0 to 255
#define s8 signed char // -128 to 127
 
#define u16 unsigned int // 0 to 65535
#define s16 signed int // -32768 to 32767
 
#define u32 unsigned long int 
// 0 to 4294967295
 
#define s32 signed long int 
// -2147483648 to 2147483647
 
#define f32 float // ±1.175e-38 to ±3.402e38
#define d32 double // ±1.175e-38 to ±3.402e38

После включения моего "хидера" в текст вашей программы вы сможете писать вместо длинного

unsigned long int <имя 32 битной переменной>

коротко :

u32 <имя 32 битной переменной>

u - без знаковая - значит не отрицательная
s - значит переменная со знаком 

32 - количество бит в переменной


КОНСТАНТЫ.

flash и const ставятся перед объявлением констант - неизменяемых данных хранящихся во флэш памяти программ. Они позволяют вам использовать не занятую программой память МК. Обычно для хранения строковых данных - различные информационные сообщения, либо чисел и массивов чисел.

КОНСТАНТЫ ПРИМЕРЫ из CVAVR help

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 int multidim_array[2][3]={{1,2,3},{4,5,6}};
flash char string_constant1[ ]=”This is a string constant”;
 
const char string_constant2[ ]=”This is also a string constant”;

В других компиляторах могут быть отличия !





Следующий пункт в структуре программы на Си для МК

пункт  5  
 
Описание функций-обработчиков прерываний. 
 
 
Механизм прерываний подробно описан 
на странице 3  -  Прерывания в AVR 
            
     /* 
 
Конкретно в  ЭТОЙ программе - есть только одно прерывание 
и значит одна функция обработчик прерывания.
 
Программа будет переходить на неё при возникновении прерывания : 
 
ADC_INT - по событию "окончание АЦ преобразования"  
 
*/ 
 
interrupt [ADC_INT] void adc_isr(void)
{ 
PORTB=(unsigned char) (~(ADCW>>2));
/* отобразить горящими светодиодами подключенными 
от + питания МК через резисторы 560 Ом к ножкам порта_B  старшие 8 бит результата аналого-цифрового преобразования  
 
Сделаем паузу 127 мСек - просто как пример пауз */ 
delay_ms(127); 
 
 
/* 
 
В реальных программах старайтесь 
не делать пауз в прерываниях ! 
 
Обработчик прерывания должен быть 
как можно короче и быстрее.  
 
Например - в обработчике прерывания вы только устанавливаете флаги (биты или переменная) означающие состояние кнопок, значения переменных или регистров, а обрабатываете  это уже в основном цикле программы, через конструкции  if - else  или  switch   (описаны выше!) 
 
*/
 
// начать новое АЦПреобразование
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 /* биты 1 и 0 вылетают вправо из числа в небытие, бит_9 перемещается в позицию бит_7, бит_8 в позицию бит_6 и так далее до бит_2 становится бит_0 */

Теперь старшие 8 бит результата АЦП встали в биты7_0 младшего байта (LowByte - LB) слова ADCW

	>> n 	означает сдвинуть все биты числа вправо на n  позиций 

это равносильно делению на 2 в сепени n

	<< n 	

означает сдвинуть все биты числа влево на n позиций это равносильно умножению на 2 в сепени n


Сдвиг используется очень часто !


Светодиоды подключены так как написано выше - т.е. подключены правильно !

Загораются (показывая "1") при "0" на соответствующем выводе МК - значит нам нужно выводить в PORTB число в котором "1" заменены "0" и наоборот - это делает как я рассказал выше:

~ операция побитного инвертирования - меняет значения битов.

Значит результатом этого выражения

~(ADCW >> 2)

будут инвертированные 8 старших бит результата АЦП находящиеся в младшем (правом - LB) байте двух байтового слова ADCW


Выше я уже говорил что :

в Си в переменную можно помещать только тот тип данных который она может хранить !


Так как PORTB это байт, а ADCW - это два байта, то прежде чем выполнить оператор присваивания (это знак = ) нужно преобразовать слово (слово - word - значит два байта) ADCW в без знаковый байт.

	Преобразование типов данных - делают так : 

перед тем что надо преобразовать записывают в скобках ( ) тип данных к которому нужно преобразовать.

Пишем ...

(unsigned char) (~(ADCW>>2))

Результат этой строки - один байт и мы можем поместить его в PORTB


Если в регистре DDRB все биты равны "1" - т.е. все ножки порта_B выходы, мы безусловно увидим старшие 8 бит результата АЦП горящими светодиодами.


Вам должна быть абсолютно понятна разобранная строка:

PORTB = (unsigned char) (~(ADCW>>2));


Если это не так то

повторите разбор, и перечитайте материал по Си по использованным операторам Си.


Разберем еще одну строчку

ADCSRA|=0x40;

Обратите внимание на необходимость ставить в конце выражений точку с запятой - не забывайте !

Эта строка означает следующее:

Двигаемся слева на право :

- берем значение переменной ADCSRA (это регистр МК - значит программа прочитает его, возьмет число из него)

- выполняем с этим числом операцию обозначаемую вертикальной черточкой | ( это поразрядная операция ИЛИ - только "0" и "0" дают "0" ) с числом 0x40

- присвоим или поместим результат поразрядного ИЛИ обратно в переменную ADCSRA - т.е. запишем результат в регистр ADCSRA

0x40 это в двоичном виде: 0100 0000

так как в результате поразрядного ИЛИ только два "0" дают "0" биты в ADCSRA напротив нулей не изменятся, а вот бит_6 в ADCSRA оказывается напротив "1" и теперь он станет "1" не зависимо от того каким он был до этого !

т.е. смысл рассматриваемой строки программы

ADCSRA|=0x40;

"установить" (т.е. сделать "1") бит_6 в регистре ADCSRA


Число справа от составных операторов |= &= ^= обычно называют маской,

и говорят "наложить маску" - так как в результате меняются лишь те биты которые нужно изменить.

Управление отдельными битами в переменной или регистре.

Как изменить только некоторые биты не изменяя остальные.

Для обнуления нужных бит используют обозначаемое знаком & поразрядное логическое И - только "1" и "1" дает "1"

PEREM &=(~0x04); // обнулить бит_2 в переменной PEREM Скобки здесь я добавил для улучшения читаемости кода.


Самоконтроль - важно:

а) обязательно разберитесь почему обнуляется бит_2

б) Как в двоичном виде выглядит результат (~0x04)


А вот так более понятно:

 PEREM &=(~(1 << 2)); // обнулить бит_2 в переменной PEREM


Обнулить биты 5, 3 и 0 в переменной PEREM

 PEREM &=(~ (  (1 << 5)|(1 << 3)|(1 << 0)  ) );

конечно здесь вместо (1 << 0) можно написать просто (1)


"Установить" - сделать "1" - биты 7, 5 и 3 в переменной PEREM

 PEREM |=(1 << 7)|(1 << 5)|(1 << 3);


Обязательно разберитесь как это работает - вы должны это понимать и ГЛАВНОЕ использовать в своих программах.

Например (1 << 4) означает: взять число 1 и все его биты сдвинуть в лево на 4 позиции - в итоге мы получим двоичное 10000. Эти вычисления компилятор сделает сам и в программе заменит всё, что правее = на число-результат.

Вместо номеров битов вы можете использовать их названия из даташит. Но в CVAVR для этого надо включить мой заголовок о котором написано выше или самому сделать определения битов из архива выложеного выше. А в IAR надо отметить галочку "Элау бит дефинишнс" в свойствах проекта.

 WDTCR |= (1 << WDTOE) | (1 << WDE);

эта строка программы "Установит" - сделает "1" биты WDTOE и WDE в регистре WDTCR


/* Пункт 6      Функции используемые в программе   */
 
// их может быть столько сколько вам нужно.
 
// у нас будет одна, кроме main и 
// обработчика прерывания.
 
/*  =================================
 
Это будет функция в которой описано начальное 
конфигурирование МК в соответствии с 
поставленной задачей 
 
Удобно над функцией сделать заголовок
подробно поясняющий назначение функции !
 
=====================================  */
 
(void)_init_mk(void) {
/* Вначале любой функции объявляются 
ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ - если конечно они вам нужны */
 
/* void - означает пусто. 
 
Перед названием функции - void - означает что функция не возвращает ни какого значения. А в скобках после названия означает что при вызове в функцию не передаются ни какие значения.  */
 
// инициализация Port_B 
DDRB=0xFF;  // все ножки сделать выходами
PORTB=0xFF; // вывести на все ножки "1"
 
 
 
/* настройка АЦП - производится записью определенного числа в регистр "ADC Control and Status Register A" – ADCSRA
 
посмотрите его описание в ДШ МК мега16.
 
Нам нужно: 
 
- Включить модуль АЦП
 
- Установить допустимую частоту тактирования АЦП при частоте кварца 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")

Внимание !

Так делаются Ассемблерные вставки в CVAVR :

  1. asm ("инструкция на ассемблере")

Обратите внимание - точки с запятой в конце НЕТ ! Такие вставки принято иногда делать. НО они не являются необходимыми.

На языке Си можно управлять ВСЕМИ программно изменяемыми битами в регистрах МК !


	Напоминаю ...    

Все регистры МК перечислены в таблице в конце ДШ с указанием номеров страниц с подробным описанием регистра и его битов.

Часто используются такие ассемблерные вставки :

#asm("sei") // Разрешить ГЛОБАЛЬНО все прерывания
 
#asm("cli") // Запретить ГЛОБАЛЬНО все прерывания
 
#asm("nop") // Пауза в 1 такт процессора 
 
#asm("wdr") // Сбросить сторожевой таймер
 
            
     // все функция закончена
} // скобка закрывающая для функции _init_mk()      
           
Далее...           
     /* 
   Пункт 7       Главная функция  main()  -  обязательная ! 
*/
 
/*  =================================
Главная функция - 
 
Си программа начинает выполнятся с нее!
 
=====================================  */
 
void main(void){
/* Вначале любой функции объявляются 
   (если нужны)  ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ  */
 
_init_mk();/*Вызываем функцию инициализации, настроийки аппаратуры МК. Выполнив ее программа вернется сюда и будет выполнять следующую строку  */
 
// запускаем первое АЦП 
ADCSRA|=0x40;
 
// бесконечный цикл в ожидании прерываний 
while(1);
/* Программа будет крутится на этой строчке постоянно проверяя истинно ли условие в 
скобках после while а так как там константа 
1 - то условие будет истинно всегда!
 
  // функция main закончена
} // скобка закрывающая для функции main()



Эта программа на Си будет работать так :

По завершении АЦП будет возникать прерывание и программа будет перескакивать в функцию обработчик прерывания adc_isr()

При этом будут автоматически запрещены все прерывания ГЛОБАЛЬНО !

В конце adc_isr() запускается новое АЦ преобразование и при выходе из обработчика прерывания снова разрешаются глобально прерывания, и программа возвращается опять в бесконечный цикл while(1)

Такая чехарда будет продолжаться пока есть питание МК и не будет сброса.

Светодиоды будут высвечивать 8-ми битный код АЦП напряжения на ножке PA0

Всё - программа на Си написана и разобрана.

Вам должно быть все ясно и абсолютно понятно!

Если это не так то перечитайте, подумайте, повторите разбор, почитайте рекомендованное ниже по Си.

Еще щепотка Си :

Пример:

делать что-то пока на ножке PBn есть "1"

Как AVR переводит напряжения на выводах МК в логические уровни - рассказано с картинками, графиками на 2странице 2 курса - Устройство микроконтроллера AVR.

while(PINB & (1 << n)){ // для любого компилятора
             Какой-то код программы    
/* Какой-то код будет выполнятся снова и снова, пока проверка условия в скобках после while будет давать "истину" - значит пока на ножке PBn есть логическая единица  
 
Проверка условия выполняется в начале и затем каждый раз по завершении выполнения какого-то кода 
 
Пока выполняется какой-то код проверка того что на ножке PBn не производится. */
                                              };
 
примечание  -  в CVAVR можно написать проще   
while(PINB.n){   // но для регистров с адресом до 31 
 
 
 
Пример:  выполнить что-то если  на ножке PCn есть "0" 
 
if((~PINC)&(1 << n)){ // для любого компилятора
                     что-то
 /* что-то начнет выполняться если на ножке PCn "0" */
                    };
 
примечание  -  в CVAVR можно написать проще   
if(!(PINC.n)){
 
 
Помните !      Выполнение  чего-то  может быть прервано прерыванием. 
После завершения обработки прерывания выполнение чего-то продолжится. 
 
Примечание  -  Условие :  
 
if((~PINC)&(1 << n)) {
 
можно записать и вот так : 
 
if(!(PINC & (1 << n))) {
 
 
 
Пример:  выполнить что-то если  на ножке PBn есть "1" 
 
if((PINB)&(1 << n)){ // для любого компилятора
                     что-то
 /* что-то начнет выполняться если на ножке PBn "1" */
                    };
 
примечание  -  в CVAVR можно написать проще   
if(PINB.n){

К битам регистров с адресами от 0 до 31 в компиляторе CodeVisionAVR можно обратится (и читать и записывать) проще.

Вот так: REGISTR.BIT

Пример:

PINB.2   или  PORTA.5    или   DDRC.7

Пример:

if(!PINB.2){
                этот код /* Если на ножке PB2 низкий 
логический уровень - то выполнить этот код */
           };   
 
Пример:
PORTA.3 = 1; /* Сделать бит 3 в регистре 
PORTA единицей - говорят: "установить бит" 
по англ. "set bit" */
 
Пример:
PORTB.6 = 0; /*  Сделать бит 6 в регистре 
PORTB нулем - говорят: "очистить, сбросить бит" 
по англ. "clear bit"   */

Битовые операции подробно описаны в задаче 1

и конечно в справке - help - компиляторов !

Теперь вы должны знать

- как записать число в регистр, в переменную

- как изменить бит в регистре

- как взять число из регистра

- как выполнить что-то в зависимости от

 значения бита в регистре или в переменной

- записывайте возникающие вопросы !

   и лучше на бумагу - моторная память !

- найдите в DataSheet (ДШ) регистры и устройства МК использованные в задаче, прочитайте о них подробней.

- если вопросы остались перечитайте задачу снова !

- если вопросы не разрешены, ищите ответ:

1) в help и документации компилятора, симулятора, других используемых программ!

2) поиском Windows в папках и help компилятора и симулятора.

3) поиском Windows в папке где сохранен у вас курс.

4) в моем не структурированном мини-AVRFAQ - это сборник ответов на часто задаваемые мне по курсу вопросы и советы по применению МК от знающих людей.


Дальше - страница 6

Задачи - Упражнения Курса Практическая работа с Компилятором с Симулятором с МК и внешними устройствами


--Nekolex 20:47, 27 сентября 2009 (UTC)