» »

Устройство и работа портов ввода-вывода. AVR. Учебный курс. Работа с портами ввода-вывода. Практика Avr программирование работа с портами

15.10.2020

Рассмотрим примеры настройки портов в CodeVision AVR
Например DDRB=0x02; данная запись означает что вторая ножка порта В настроена как выход, но откуда взялось это число????

Для начала переведем данную запись в более понятный нам вид:
приставка 0х означает что число записано в шестнадцатеричной системе исчисления, чтобы легко понять его суть нужно перевести его в двоичную систему. Поможет нам в этом калькулятор Windows из набора стандартных программ, сразу переводим его в режим программист.

Переключаем его в режим шестнадцатеричной системы (галочка HEX), и вводим наше число 0х02 просто как 2.

теперь не нажимаем никаких равно и пр. просто переключаемся в двоичную систему счисления (галочка Bin)

Получили число 10. Что же оно значит? У нашей ATmega8 имеется 8 ножек на порту В (обведены на картинке)

так вот если представить число 10 как 00000010, то это будет означать что только вторая ножка настроена как выход, остальные как вход.

PORTB.7 PORTB.6 PORTB.5 PORTB.4 PORTB.3 PORTB.2 PORTB.1 PORTB.0
0 0 0 0 0 0 1 0

Тут нужно отметить, что если вместо
DDRB=0x02;
запишем
DDRB=0b00000010;
то оно тоже будет работать, т.е. это равнозначные записи одного и того же числа, в разных системах счисления.

С DDRB разобрались 1-выход, 0-вход, а что же означает
PORTB=0x01;
тут принцип тот же самый, но:
если нога сконфигурирована как выход и значение PORTB будет равно единице на этой же ноге, то ножка после прошивки будет включена по умолчанию (т.е. на ней будет напряжение), если 0, то ножка будет выключена. В первом уроке мы могли бы заменить запись PORTB.0=1; записью PORTB=0x01; и получили тот же самый результат.

На самом деле, правильным решением включения/выключения ножки с точки зрения языка Си, являются следующие конструкции:

PORTD |= (1<<3); //включить ножку 3 порта D PORTD |= (1<<2)|(1<<5)|(1<<6); //включить ножки 2, 5, 6 порта D PORTD &= ~(1<<2); //выключить ножку 2 PORTD &= (~((1<<0)|(1<<2)|(1<<4))); //выключить ножки 0, 2, 4

Если нога сконфигурирована как вход и значение PORTB будет равно единице на этой же ноге, то к ножке будет подключен подтягивающий резистор, для устранения помех. Если вам внутренний резистор не нужен то просто установите 0 на этой ножке.

Надеюсь все понятно, если что пишите, задавайте вопросы.

Любое устройство на микроконтроллере AVR использует порты ввода вывода. Для работы с портами у AVR`ок есть три регистра: PORTx, PINx и DDRx, где x - буква порта, например A, B, C и т.д.
Регистр DDRx - определяет направление выводов микроконтроллера, PINx позволяет читать их состояние, "осязать" внешний мир, а PORTx в зависимости от направления вывода или задает его логический уровень, или подключает подтягивающий резистор.
Выводы микроконтроллера в проекте обычно задают с помощью макроопределений - define`ов. Мы получаем некую "отвязку" от железа и в дальнейшем это позволяет нам переназначать выводы на другие порты. Например, это может выглядеть так.


#define BUT_PIN 3
#define BUT_PORTX PORTB
# define BUT_DDRX DDRB
#define BUT_PINX PINB

Неудобство такого подхода состоит в том, что для каждого вывода нужно определять три регистра. Бывает, что два (только PORTx и DDRx), но это тоже неудобно, если выводов много. Существует другой подход, позволяющий сократить число макроопределений. Разберемся в чем он заключается.

В микроконтроллер atmega16 регистр PORTB имеет адрес 0x18, а регистры DDRB и PINB - 0x17 и 0x16 соответственно. Тоже самое и с регистрами остальных портов, они тоже расположены друг за другом. Мы можем определить в проекте только один регистр, а к остальным обращаться вычисляя их адрес. За основу можно взять любой из них, главное ничего не напутать. Лучше всего для этих целей использовать макросы. Если отталкиваться от регистров PORTx, то макросы будут выглядеть так.


//это макросы для доступа к регистрам порта
#define PortReg(port) (*(port))
#define DirReg(port) (*((port) - 1))
#define PinReg(port) (*((port) - 2))

Макросы принимают в качестве параметра адрес регистра PORTx. Для взятия адреса регистра используется оператор &. Посмотрим, как можно использовать эти макросы.


//определили вывод мк
#define BUT_PIN 3
#define BUT_PORT PORTB
...
// конфигурируем вывод как вход
DirReg(&BUT_PORT) &= ~(1<//включаем подтягивающий резистор
PortReg(&BUT_PORT) |= (1<

...
//проверяем нажата ли кнопка
if(! (PinReg(&BUT_PORT)&(1< ...
}

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

Компилятор преобразует эти макросы в очень компактный код. Точно такой же, как если бы мы обращались к регистрам используя их имена. И дело в том, что с точки зрения ассемблера это так и есть. Если вы посмотрите ассемблерный код этих примеров, то увидите, что обращение к регистрам осуществляется методом прямой адресации с помощью команд IN, OUT. Поэтому я и озаглавил этот раздел "ненастоящая работа с портом через указатели". Указатели вроде как используются, но на самом деле нет.
Такой подход можно использовать не со всеми микроконтроллерами AVR, потому что в некоторых моделях регистры порта располагаются не по соседним адресам. Как, например, регистры порта F в микроконтроллере ATmega128.

Настоящая работа с портом через указатель

Иногда приходится прибегать к работе с портом, используя настоящий указатель. Для этого создается переменная указатель, которая инициализируется адресом какого-нибудь регистра порта. Делается это следующим образом.


//объявляем указатель на регистр
//обязательно должно присутствовать volatile
volatile uint8_t *portReg;

//инициализация
//передаем адрес регистра PORTB
portReg = &PORTB;

//вывод в порт через указатель
//перед указателем ставиться оператор *
(*portReg) = 0xff;

Также этот указатель можно передавать в функцию.


void OutPort(volatile uint8_t *pReg, uint8_t data)
{
*pReg = data;
}

...
//записываем в PORTB число 0xff
OutPort(&PORTB, 0xff);

//а здесь уже не нужен оператор &
//так как мы передаем переменную с адресом порта
OutPort(portReg, 0xff);

Работа с портом через указатель открывает большие возможности. Например, мы можем определить структуру, которая будет хранить все настройки пина микроконтроллера и обращаться к выводу, используя эту структуру. Или можем определить структуру виртуального порта содержащую выводы микроконтроллера из разных физических портов.
Все это так, но есть ложка дегтя. Работа с регистрами порта через указатель "тяжеловесна" с точки зрения размера кода и его быстродействия. Чтобы в этом убедиться, достаточно взглянуть на получаемый ассемблерный код. Если эти два фактора не критичны, то такой подход можно использовать, если нет, то придется работать по старинке.
Также п ри работе с портом через указатели, даже операция установки/сброса разряда будет неатомарна. Атомарность операций в этом случае нужно обеспечивать самостоятельно.

Вот небольшой пример, как можно использовать указатели при работе с портом.


//струтура для хранения настроек вывода - номера и порта
typedef struct outputs{
uint8_t pin;
volatile uint8_t *portReg;
}outputs_t;

//функция инициализации
void OUT_Init(outputs_t *out, uint8_t pin, volatile uint8_t *port, uint8_t level)
{
//сохраняем настройки в структуру
out->pin = pin;
out->portReg = port;

//конфигурируем вывод на выход
(*(port-1)) |= (1<

//задаем логический уровень
if (level) {
(*port) |= (1< }
else{
(*port) &= ~(1< }
}

//установить на выходе 1
void OUT_Set(outputs_t *out)
{
(*(out->portReg)) |= (1 << out->pin);
}

//установить на выходе 0
void OUT_Clear(outputs_t *out)
{
(*(out->portReg)) &= ~(1 << out->pin);
}

Пример использования


//определили вывод мк
#define OUT1_PIN 4
#define OUT1_PORT PORTB
...
//объявляем переменную для хранения
//настроек пина
outputs_t out1;

//инициализируем ее
OUT_Init(&out1, OUT1_PIN, OUT1_PORT, 0);

//устанавливаем 1 на выводе OUT1_PIN
OUT_Set(&out1);

Еще один пример работы с портом через указатели есть в коде к статье "

Побитовые операции основаны на логических операциях, которые мы уже рассмотрели ранее. Они играют ключевую роль при программировании микроконтроллеров AVR и других типов. Практически ни одна программа не обходится без применения побитовых операций. До этого мы намеренно избегали их, чтобы облегчить процесс изучения программирования МК.

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

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

Побитовые операции

Чаще всего при программировании микроконтроллеров AVR мы пользовались , поскольку она имеет большую наглядность по сравнению с и хорошо понятна для начинающих программистов МК. Например, нам нужно установить только 3-й бит порта D. Для этого, как мы уже знаем, можно воспользуемся следующим двоичным кодом:

PORTD = 0b00001000;

Однако этой командой мы устанавливаем 3-й разряд в единицу, а все остальные (0, 1, 2, 4, 5, 6 и 7-й) мы сбрасываем в ноль. А теперь давайте представим ситуацию, что 6-й и 7-й разряды задействованы как входы АЦП и в это время на соответствующие выводы МК поступает сигнал от какого-либо устройства, а мы, применяемой выше командой, обнуляем эти сигналы. В результате чего микроконтроллер их не видит и считает, что сигналы не приходили. Поэтому вместо такой команды нам следует применить другую, которая бы установила только 3-й бит в единицу, при этом не влияя на остальные биты. Для это обычно применяется следующая побитовая операция:

PORTD |= (1<<3);

Синтаксис ее мы подробно разберем далее. А сейчас еще один пример. Допустим нам нужно проверить состояние 3-го разряда регистра PIND, тем самым проверяя состояние кнопки. Если данный разряд сброшен в ноль, то мы знаем, что кнопка нажата и далее выполняется код команды, который соответствует состоянию нажатой кнопки. Ранее мы бы воспользовались следующей записью:

if (PIND == 0b00000000)

{ какой-либо код}

Однако с помощью нее мы проверяем не отдельный, – 3-й, а сразу все биты регистра PIND. Поэтому даже если кнопка нажат и нужный разряд сброшен, но в это время на какой-либо другой вывод порта D поступит сигнал, то соответствующий быт установится в единицу, и условие в круглых скобках будет ложным. В результате код, находящийся в фигурных скобках, не будет выполняться даже при нажатой кнопке. Поэтому для проверки состояния отдельного 3-го бита регистра PIND следует применять побитовую операцию:

if (~PIND & (1<<3))

{ какой-либо код}

Для работы с отдельными битами микроконтроллера в арсенале языка программирования C имеются , с помощью которых можно изменять или проверять состояние одного или нескольких отдельных бит сразу.

Установка отдельного бита

Для установки отдельного бита, например порта D, применяется побитовая операция ИЛИ. Именно ее мы применяли в начале статьи.

PORTD = 0b00011100; // начальное значение

PORTD = PORTD | (1<<0); применяем побитовую ИЛИ

PORTD |= (1<<0); // сокращенная форма записи

PORTD == 0b00011101; // результат

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

Для примера установим еще 6-й разряд порта D.

PORTD = 0b00011100; // начальное состояние порта

PORTD |= (1<<6); //

PORTD == 0b01011100; // результат

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

PORTB = 0b00011100; // начальное значение

PORTB |= (1<<0) | (1<<6) | (1<<7); //

PORTB == 0b1011101; // результат

Сброс (обнуление) отдельных битов

Для сброса отдельного бита применяются сразу три ранее рассмотренные команды: .

Давайте сбросим 3-й разряд регистра PORTC и оставим без изменений остальные.

PORTC = 0b00011100;

PORTC &= ~(1<<3);

PORTC == 0b00010100;

Выполним подобные действия для 2-го и 4-го разрядов:

PORTC = 0b00111110;

PORTC &= ~((1<<2) | (1<<4));

PORTC == 0b00101010;

Переключение бита

Кроме установки и сброса также применяется полезная команда, которая переключает отдельный бит на противоположное состояние: единицу в ноль и наоборот. Данная логическая операция находит широкое применение при построении различных световых эффектов, например, таких как новогодняя гирлянда. Рассмотрим на примере PORTA

PORTA = 0b00011111;

PORTA ^= (1<<2);

PORTA == 0b00011011;

Изменим состояние нулевого, второго и шестого битов:

PORTA = 0b00011111;

PORTA ^= (1<<0) | (1<<2) | (1<<6);

PORTA == 0b01011010;

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

Наиболее часто проверка выполняется одним из двух операторов цикла: if и while. С этими операторами мы уже знакомы ранее.

Проверка разряда на наличие логического нуля (сброса) с if

if (0==(PIND & (1<<3)))

Если третий разряд порта D сброшен, то выполняется Код1. В противном случае, выполняется Код2.

Аналогичные действия выполняются при и такой форме записи:

if (~PIND & (1<<3))

Проверка разряда на наличие логической единицы (установки) с if

if (0 != (PIND & (1<<3)))

if (PIND & (1<<3))

Приведенные выше два цикла работаю аналогично, но могут, благодаря гибкости языка программирования C, иметь разную форму записи. Операция!= обозначает не равно. Если третий разряд порта ввода-вывода PD установлен (единица), то выполняется Код1, если нет ‑ Код2.

Ожидание сброса бита с while

while (PIND & (1<<5))

Код1 будет выполняться пока 5-й разряд регистра PIND установлен. При сбросе его начнет выполняться Код2.

Ожидание установки бита с while

Здесь синтаксис языка С позволяет записать код двумя наиболее распространёнными способами. На практике применяются оба типа записи.

Вот ты читаешь сейчас это и думаешь — память, регистры, стек и прочее это хорошо. Но ведь это не пощупать, не увидеть. Разве что в симуляторе, но я и на дельфи с тем же условием могу накодить. Где мясо!!!

В других курсах там, чуть ли не с первых строк, делают что то существенное — диодиком мигают и говорят, что это наш Hello World. А тут? Гыде???

Да-да-да, я тебя понимаю. Более того, наверняка ты уже сбегал к конкурентам и помигал у них диодиком;)))) Ничего, простительно.

Я просто не хотел на этом же мигании дидодиков и остановиться, а для прогресса нужно четкое понимание основ и принципов — мощная теоретическая база. Но вот пришла очередь практики.

О портах было рассказано, шаблон программы у вас уже есть, так что сразу и начнем.

Инструментарий
Работа с портами, обычно, подразумевает работу с битами. Это поставить бит, сбросить бит, инвертировать бит. Да, конечно, в ассемблере есть удобные команды

cbi/sbi, но работают они исключительно в малом адресном диапазоне (от 0 до 1F, поэтому давайте сначала напишем универсальные макросы, чтобы в будущем применять их и не парить мозг насчет адресного пространства.

Макросы будут зваться:

  • SETB byte,bit,temp
  • CLRB byte,bit,temp
  • INVB byte,bit,temp,temp2

Причем при работе с битами младших РВВ (0-1F адрес) то значение параметра TEMP можно и не указывать — он все равно подставляться не будет. За исключением команд инверсии — там промежуточные регистры полюбому нужны будут.

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

  • SETBM byte,bit
  • CLRBM byte,bit
  • INVBM byte,bit

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 < 0x20 SBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ORI R17,1<<@1 OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ORI R17,1<<@1 STS @0,R17 POP R17 .endif .endif .ENDM ;SET BIT with REG .MACRO SETB .if @0 < 0x20 ; Low IO SBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ORI @2,1<<@1 OUT @0,@2 .else ; Memory LDS @2,@0 ORI @2,1<<@1 STS @0,@2 .endif .endif .ENDM ;............................................................. ;Clear BIT with REG .MACRO CLRB .if @0 < 0x20 ; Low IO CBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ANDI @2,~(1<<@1) OUT @0,@2 .else ; Memory LDS @2,@0 ANDI @2,~(1<<@1) STS @0,@2 .endif .endif .ENDM ;Clear BIT with STACK .MACRO CLRBM .if @0 < 0x20 CBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ANDI R17,~(1<<@1) OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ANDI R17,~(1<<@1) STS @0,R17 POP R17 .endif .endif .ENDM ;............................................................. .MACRO INVB .if @0 < 0x40 IN @2,@0 LDI @3,1<<@1 EOR @3,@2 OUT @0,@3 .else LDS @2,@0 LDI @3,1<<@1 EOR @2,@3 STS @0,@2 .endif .ENDM .MACRO INVBM .if @0 < 0x40 PUSH R16 PUSH R17 IN R16,@0 LDI R17,1<<@1 EOR R17,R16 OUT @0,R17 POP R17 POP R16 .else PUSH R16 PUSH R17 LDS R16,@0 LDI R17,1<<@1 EOR R17,R16 STS @0,R17 POP R17 POP R16 .endif .ENDM ;= End macro.inc ========================================

;= Start macro.inc ======================================== ;SET BIT with stack .MACRO SETBM .if @0 < 0x20 SBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ORI R17,1<<@1 OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ORI R17,1<<@1 STS @0,R17 POP R17 .endif .endif .ENDM ;SET BIT with REG .MACRO SETB .if @0 < 0x20 ; Low IO SBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ORI @2,1<<@1 OUT @0,@2 .else ; Memory LDS @2,@0 ORI @2,1<<@1 STS @0,@2 .endif .endif .ENDM ;............................................................. ;Clear BIT with REG .MACRO CLRB .if @0 < 0x20 ; Low IO CBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ANDI @2,~(1<<@1) OUT @0,@2 .else ; Memory LDS @2,@0 ANDI @2,~(1<<@1) STS @0,@2 .endif .endif .ENDM ;Clear BIT with STACK .MACRO CLRBM .if @0 < 0x20 CBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ANDI R17,~(1<<@1) OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ANDI R17,~(1<<@1) STS @0,R17 POP R17 .endif .endif .ENDM ;............................................................. .MACRO INVB .if @0 < 0x40 IN @2,@0 LDI @3,1<<@1 EOR @3,@2 OUT @0,@3 .else LDS @2,@0 LDI @3,1<<@1 EOR @2,@3 STS @0,@2 .endif .ENDM .MACRO INVBM .if @0 < 0x40 PUSH R16 PUSH R17 IN R16,@0 LDI R17,1<<@1 EOR R17,R16 OUT @0,R17 POP R17 POP R16 .else PUSH R16 PUSH R17 LDS R16,@0 LDI R17,1<<@1 EOR R17,R16 STS @0,R17 POP R17 POP R16 .endif .ENDM ;= End macro.inc ========================================

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

Но вернемся к коду,
Мигнем уж светодиодиком то, наконец?

Да не вопрос. На демоплате уже смонтированы светодиоды, почему бы их не заюзать? Они висят на выводах порта PD4,PD5, PD7. Надо только одеть джамперы.

; Internal Hardware Init ====================================== SETB DDRD,4,R16 ; DDRD.4 = 1 SETB DDRD,5,R16 ; DDRD.5 = 1 SETB DDRD,7,R16 ; DDRD.7 = 1 ; End Internal Hardware Init ===================================

Осталось зажечь наши диоды. Зажигаются они записью битов в регистр PORT. Это уже делаем в главной секции программы.

; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 SETB PORTD,7,R16 ; Зажгли LED3 JMP Main ; End Main =====================================================

Компилиуем, можешь в трассировщике прогнать, сразу увидишь как меняются биты. Прошиваем… и после нажатия на RESET и выгрузки bootloader (если конечно у тебя ) увидишь такую картину:


И для версии II


Во! Тока это же скучно. Давай ка ими помигаем.

Заменим всего лишь наши макрокоманды.

; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 JMP Main ; End Main =====================================================

Зажгли, прошили…

А вот фиг — горят оба, но один чуть-чуть тусклей. На самом деле он мерцает, но очень очень быстро. Если ткнуть осциллографом в вывод PD7, то будет видно, что уровень меняется там с бешеной частотой:


Что делать? Очевидно замедлить. Как? Самый простой способ, который практикуют в подавляющем большинстве обучалок и быстрых стартов — тупой задержкой. Т.е. получают код вида:

; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 RCALL Delay JMP Main ; End Main ===================================================== ; Procedure ==================================================== .equ LowByte = 255 .equ MedByte = 255 .equ HighByte = 255 Delay: LDI R16,LowByte ; Грузим три байта LDI R17,MedByte ; Нашей выдержки LDI R18,HighByte loop: SUBI R16,1 ; Вычитаем 1 SBCI R17,0 ; Вычитаем только С SBCI R18,0 ; Вычитаем только С BRCC Loop ; Если нет переноса - переход RET ; End Procedure ================================================

Прошили, запустили… О да, теперь мигание будет заметно.

При параметрах 255.255.255 длительность выдержки на 8Мгц будет около 2.1 секунды. Можно увеличить разрядность задержки еще на несколько байт. Тогда можно хоть час зарядить.

Но этот метод ущербен, сейчас покажу почему.

Давай добавим кнопку. LED3 пусть мигает в инверсии. А мы сделаем так, что когда кнопка нажата у нас горит LED1, а когда отпущена горит LED2.

В качестве кнопки возьмем тактовую A, подключим ее к порту PD6.


Для версии II аналогично. Только кнопку возьмем с группы кнопок и соединим вывод PD6 контроллера с штырем COL1 на кнопочной панели.

Обрати только внимание на джамперы, что стоят на перемычках кнопочного поля. Синие такие. А также на один неприметный черный джамперок, на который указывает правая стрелка. Он соединяет крайне левую колонку кнопок с землей. Набрасывается на пины GND и ROW1. На плате там все подписано.

Проверка кнопки делается командой SBIC, но вначале ее надо инициализировать. Сделать DDR=0, PORT=1 — вход с подтяжкой.

Добавляем в секцию инициализации (Internal Hardware Init) эти строчки:

; Main ========================================================= Main: SBIS PIND,6 ; Если кнопка нажата - переход RJMP BT_Push SETB PORTD,5 ; Зажгем LED2 CLRB PORTD,4 ; Погасим LED1 Next: INVB PORTD,7,R16,R17 ; Инвертировали LED3 RCALL Delay JMP Main BT_Push: SETB PORTD,4 ; Зажгем LED1 CLRB PORTD,5 ; Погасим LED2 RJMP Next ; End Main =====================================================

Ну чо, работает. Кнопочка жмется — диодики меняются. Третий же бодро подмигивает. Но есть западло:

Торомозит программка то! Я кнопочку нажал, а картинка не сменилась, нужно подождать, подержать… Почему? А это из-за нашего быдлокодинга.

Помнишь я тебе говорил, что тупые задержки, в которых МК ничего не делает это адское зло? Вот! Теперь ты в этом убедился сам. Чтож, со злом надо бороться. Как? Ну эт я тоже уже говорил — делать непрерывный цикл с флажками. Хотя бы внести в нашу задержку полезную работу.

Шарманка
Сейчас я тебе покажу как можно сделать цифровую шарманку. Помнишь как она устроена?

Там барабан с торчащими гвоздями и пружинки на разные тона. Гвозди вращаются, дергают пружинки — они звякают. Получается расколбасный музон. А что если нашу шарманку развернуть в ленту. Не правда ли гвозди похожи на единички? ;))))

Лентой будет счетчик, считающий от нуля, скажем, до FF.FF.FF.FF и потом опять до нуля или еще какой величины, сколько надо столько и сделаем. А наши подпрограммы будут играть роль пружинок, цепляясь в нужных местах — сравнивая свою константу с текущим значением счетчика.

Совпало? Делаем «ДРЫНЬ!»

Осталось только прописать на временном цикле нашей шарманки где и что должно запускаться. И тут есть одна очень удобная особенность — для построения циклических последовательностей нам достаточно отлавливать один разряд.

Скажем, считает шарманка от 0 до 1000, а нам надо 10 раз мигнуть диодом. Не обязательно втыкать 10 обработчиков с разными значениями. Достаточно одного, но чтобы он ловил значение **10. Все, остальное нам не важно. И сработает он на 0010, 0110, 0210, 0310, 0410, 0510, 0610, 0710, 0810, 0910. Более частые интервалы также делятся как нам надо, достаточно влезть в другой разряд. Тут надо только не забыть отрезать нафиг старшие разряды, чтобы не мешались.

Приступим. Вначале создадим наш счетчик в сегменте данных:

LDS R16,CCNT LDS R17,CCNT+1 LDS R18,CCNT+2 LDS R19,CCNT+3

Все, теперь в R16 самый младший байт нашего счетчика, а в R19 самый старший.

Регистры можно предварительно затолкать в стек, но я дам тебе лучше другой совет — когда пишешь программу, продумывай алгоритм так, чтобы использовать регистры как сплошной TEMP данные которого актуальны только здесь и сейчас. А что будет с ними в следующей процедуре уже не важно — все что нужно должно будет сохранено в оперативке.

По можно сделать так:

LDI R20,1 ; Нам нужна единичка CLR R15 ; А еще нолик. ADD R16,R20 ; Прибавляем 1 если в регистре 255, то будет С ADC R17,R15 ; Прибавляем 0+С ADC R18,R15 ; Прибавляем 0+С ADC R19,R15 ; Прибавляем 0+С

Пришлось потратить еще два регистра на хранение констант нашего сложения. Все от того, что AVR не умеет складывать регистры с непосредственным числом. Зато умеет вычитать.

Я уже показывал, что R-(-1)=R+1, а ведь никто не запрещает нам этот же прием устроить и тут — сделать сложение через вычитание.

1 2 3 4 SUBI R16,(-1) SBCI R17,(-1) SBCI R18,(-1) SBCI R19,(-1)

SUBI R16,(-1) SBCI R17,(-1) SBCI R18,(-1) SBCI R19,(-1)

Даст нам инкремент четырехбайтного числа R19:R18:R17:R16

А теперь я вам покажу немного целочисленной магии
Почему это будет работать? Смотри сам:

SUBI R16,(-1) это, по факту R16 — 255 и почти при всех раскладах она нам даст нам заём из следующего разряда — С. А в регистре останется то число на которое больше.

Т.е. смотри как работает эта математика, вспомним про число в доп кодах. Покажу на четырехрязрядном десятичном примере. У Нас есть ВСЕГО ЧЕТЫРЕ РАЗРЯДА, ни больше ни меньше. Отрицательное число это 0-1 так? Окей.

1 С 0000-1=9999+C

Т.е. мы как бы взяли как бы из пятиразрядной 1 С 0000 отняли 1, но разрядов то у нас всего четыре! Получили доп код 9999 и флаг заема С (сигнализирующий о том, что был заем)

Т.е. в нашей целочисленной математике 9999=-1:) Проверить легко -1+1 = 0 Верно?

9999+1 = 1 С 0000 Верно! :))) А 1 старшего разряда банально не влезла в разрядность и ушла в флаг переноса C, сигнализирующего еще и о переполнении.

Оки, а теперь возьмем и сделаем R-(-1). Пусть R=4

1 С 0004-9999 = 0005+С

Вот так вот взяли и сложили через вычитание. Просто магия, да? ;)

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

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

Флаг С не выскочит лишь когда у нас дотикает до 255 (9999 в десятичном примере), тогда будет 255-255 = 0 и вскочит лишь Z, но нам он не нужен.

STS CCNT,R16 STS CCNT+1,R17 STS CCNT+2,R18 STS CCNT+3,R19

Код Инкремента четырехбайтной константы в памяти можно свернуть в макрос, чтобы не загромождать код

; Main ========================================================= Main: SETB PORTD,4 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 Next: INCM CCNT JMP Main

Запусти режим отладки и поставь точку останова (F9) на метку Main и загони курсор на первый брейкпоинт.

0хB6(CCNT) 0х9F(CCNT+1) 0х04(CCNT+2) 0x00(CCNT+3)

Осталось теперь только сравнить число с этим слепком.

Как сравнивать? Да довольно просто. Все зависит от того что мы хотим получить. Если ОДНО событие за ВЕСЬ период глобального счетчика нашей шарманки, то тупо, побайтно. В этом случае у тебя диодик моргнет через секунду после старта, а дальше ты будешь ждать пол часа до переполнения всего четырехбайтного счетчика.

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

Младшие байты сравниваем как есть, а самый старший только до его максимального разряда, остальные надо отрезать.

Т.е. самый старший разряд для этого случая это CCNT+2=0х04 если в двоичном представлении то 0х04 = 00000 100 так вот, счетчик у нас четырех разрядный, значит событие с маской

00 04 9F B6 (00000000 00000 100 10011111 10110110)

до переполнения возникнет дофига число раз. Видишь я нули жирным шрифтом выделил. Старший самый у мы вообще сравнивать не будем, а вот пред старший надо через AND по маске 00000111 продавить, чтобы отсечь старшие биты.

Их еще надо заполнить до переполнения и обнуления счетчика. Но если мы их замаскируем то их дальнейшая судьба нас не волнует. Пусть хоть до второго пришествия тикает — нам плевать.

LDS R16,CCNT ; Грузим числа в регистры LDS R17,CCNT+1 LDS R18,CCNT+2 ANDI R18,0x07 ; Накладываем маску CPI R16,0xB6 ; Сравниванем побайтно BRNE NoMatch CPI R17,0x9F BRNE NoMatch CPI R18,0x04 BRNE NoMatch ; Если совпало то делаем экшн Match: INVB PORTD,7,R16,R17 ; Инвертировали LED3 ; Не совпало - не делаем:) NoMatch: Next: INCM CCNT ; Проворачиваем шарманку JMP Main

Во, загрузили теперь мигает. Никаких затупов нет, нигде ничего не подвисает, а главный цикл пролетает со свистом, только успевай барабан шарманки проворачивать:)

Вот только мигает заметно медленней чем мы хотели. Не 1 секунда, а 8. Ну а что ты хотел — добавив процедуру сравнения мы удлиннили цикл еще на несколько команд. И теперь он выполняется не 25 тактов, а 36. Пересчитывай все циферки заново:)))))

Но это еще не самый цимес! Прикол в том, что у тебя часть кода выполняется, а часть нет — команды сравнения и перехода. Поэтому точно высчитать задержку по тактам это проще сразу удавиться — надо по каждой итерации высчитать когда и сколько у тебя будет переходов, сколько они тактов займут…

А если код будет еще больше, то ваще труба и погрешность накапливается с каждой итерацией!

Зато, если добавить код обработки кнопок:

; Main ========================================================= Main: SBIS PIND,6 ; Если кнопка нажата - переход RJMP BT_Push SETB PORTD,5 ; Зажгем LED2 CLRB PORTD,4 ; Погасим LED1 Next: LDS R16,CCNT ; Грузим числа в регистры LDS R17,CCNT+1 LDS R18,CCNT+2 ANDI R18,0x07 CPI R16,0xB6 ; Сравниванем побайтно BRNE NoMatch CPI R17,0x9F BRNE NoMatch CPI R18,0x04 BRNE NoMatch ; Если совпало то делаем экшн Match: INVB PORTD,7,R16,R17 ; Инвертировали LED3 ; Не совпало - не делаем:) NoMatch: NOP INCM CCNT JMP Main BT_Push: SETB PORTD,4 ; Зажгем LED1 CLRB PORTD,5 ; Погасим LED2 RJMP Next ; End Main =====================================================

То увидим, что от тормозов и следов не осталось. Кнопки моментально реагируют на нажатия, а диодик мигает сам по себе. Многозадачность! :)

В общем, шарманка не годится там где нужны точные вычисления. Но если задача стоит из серии «надо периодически дрыгать и не принципиально как точно», то самое то. Т.к. не занимает аппаратных ресурсов вроде таймера.

Например, периодически сканировать клавиатуру, скажем, каждые 2048 оборотов главного цикла. Сам прикинь какое число надо нагрузить на сравнение и какую маску наложить:)

Можешь скачать и поглядеть , протрассировать его, чтобы увидеть как там все вертится.

А для точных вычислений времени существуют таймеры. Но о них разговор отдельный.

Долгое время мы оставляли без внимания микроконтроллеры AVR , и вот пришла пора исправить это недоразумение! Как и для других контроллеров, будем постепенно рассматривать различную периферию AVR’ок, сначала теорию, всякие регистры, ну и под конец небольшие примерчики.

В качестве IDE я использую AVR Studio 5 , шестую версию AVR Studio даже не пробовал, не так часто последнее время мне попадаются задачи для AVR) А вообще неплохо иметь еще и установленную AVR Studio 4, потому что порой случается так, что запрограммировать контроллер из AVR Studio 5 не представляется возможным. Вот совсем недавно я хотел прошить ATMega2560 при помощи программатора STK500 и это оказалось неосуществимо через 5 студию) Хорошо осталась со старых времен AVR Studio 4, и проблема решилась в течение пары минут.

Что тут еще можно сказать?.. Да в принципе, это все, можно переходить к делу;)

Начинать, естественно, будем с GPIO – портов ввода-вывода , потому как без них никуда) И прежде чем описывать регистры, которые управляют работой портов, отмечу несколько «электрических» моментов.

На входе каждой ножки микроконтроллера заботливые разработчики поставили пару диодов, которые должны спасти микроконтроллер в случае превышения допустимого напряжения. НО! На деле все не столь радужно, и если подать на вход микроконтроллера, например, 7.5 Вольт, то контроллеру уже никто и ничто не поможет, проверено на собственном опыте. Поэтому все эксперименты нужно проводить аккуратно)

Теперь к регистрам. Вся работа с портами ввода-вывода в AVR’ках сосредоточена в трех регистрах – DDRx, PORTx, PINx. Символ «x» заменяется соответствующим названием порта (A,B…). То есть, если мы хотим работать с портом A микроконтроллера, то нам нужны регистры DDRA, PORTA, PINA. Если мы хотим работать с пятым пином порта А (РА5), то нас интересует пятый бит упомянутых выше регистров. Как видите, все довольно просто) Осталось лишь разобраться, что же и куда надо записывать, за что отвечает каждый из этих регистров. Итак, начали…

Регистр DDRx.

DDRx отвечает за направление работы соответствующих выводов микроконтроллера. Каждый пин может быть либо входом, либо выходом, третьего не дано. Для того, чтобы настроить вывод на работу в режиме входа в регистр DDR для этого порта нужно записать 0, для выхода – 1. Пусть нам нужно настроить РА6 как вход, а РА3 как выход. Что делаем? Верно, третий бит регистра DDRA выставляем в 1, а в 6 бит все того же регистра DDRA записываем 0. Готово!

Регистр PINx.

В этот регистр мы записать ничего не можем, он предназначен исключительно для чтения данных. В этом регистре информация об уровне сигнала на соответствующем порте. Как мы помним, микроконтроллер – это цифровое устройство, и сигналы на его ножках могут иметь либо высокий уровень (логическая 1), либо низкий (логический 0). Если мы хотим узнать, что у нас там на входе РВ4, то нас интересует четвертый бит регистра PINB.

Регистр PORTx.

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

PORTx = 1 – при такой конфигурации мы получаем вход с подтяжкой вверх (PullUp)

PORTX = 0 – высокоимпедансный вход (Hi-Z) – это значит, что сопротивление порта настолько велико, что его можно считать бесконечным)

Итак, продолжаем с регистром PORTx. Если вывод работает в качестве выхода, а в регистре PORTx единица – то на выводе будет высокий уровень сигнала, аналогично, PORTx = 0 – низкий уровень.

Давайте небольшой пример для иллюстрации 😉

Настроим вывод РС4 на работу в режиме входа с подтяжкой вверх. Для этого в четвертый бит регистра DDRC запишем 0 (режим входа), а в регистре PORTC четвертый бит надо выставить в 1 (подтяжка вверх). Вот и все.

В принципе это все, что касается теории, углубляться дальше не будем. Осталось соблюсти традиции и поиграть с диодиком. Пусть диод подключен к выводу PВ5. Заставим его помигать! И прежде всего создадим проект. Я, как уже говорил, использую AVR Studio 5, а в качестве контроллера выберу мой любимый ATMega88)

// Подключаем нужный файл #include /*******************************************************************/ // Простенькая функция для формирования задержки void delay(unsigned int time ) { unsigned int i = 0 ; for (i = 0 ; i < time ; i++ ) ; } /*******************************************************************/ // Инициализация нашего вывода, он работает на выход void initAll() { DDRB = 0b00100000 ; } /*******************************************************************/ // Собственно тело функции main() int main(void ) { initAll() ; while (1 ) { // Зажигаем диод PORTB = 0b00100000 ; // Отдыхаем delay(50000 ) ; // Гасим диод PORTB = 0b00000000 ; delay(50000 ) ; } } /*******************************************************************/

Вот таким получилось наше первое общение с микроконтроллерами AVR . В скором времени рассмотрим по возможности всю их периферию, а потом можно замутить что-нибудь поинтересней 😉