AVR. Учебный Курс. Программирование на Си. Часть 4

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

Зашиваю все через AVR Prog в Pinboard и смотрю на поведение LED1 и LED2.

LED1 мигает как и задумано, но стоит мне попытаться зажечь LED2 отправкой с терминала «1», как первый диод гаснет. И наоборот — зажженый диод LED2 гаснет вместе с первым. Бага! Причем жирная такая. Рассмотрим откуда она взялась.

Вот код мигания первым диодом:

1
2
3
4
LED_PORT=1<<LED1;
_delay_ms(1000);
LED_PORT=0<<LED1; 
_delay_ms(1000);

А вот код работы с вторым диодом:

1
2
3
4
5
6
switch(UDR)
	{
	case '1': LED_PORT = 1<<LED2; break;
	case '0': LED_PORT = 0<<LED2; break;
	default: break;
	}

Как видишь, тут мы пишем в один и тот же порт, но вот только биты разные. Но нельзя вот так просто через операцию «=» изменить один бит! (только если мы используем битовые поля, о них я расскажу позже). Так что операция идет с целым байтом, и в LED_PORT поочередно записывается число 00100000 (1<<LED2) и 00010000 (1<<LED1), перезаписывая друг друга. Поэтому когда происходит запись одного значения мы теряем прердыдущее. А 0<<LED2 это по факту просто 0, потому что как ноль по байту не двигай нулем он и останется .

Особенно часто эта ошибка всплывает у начинающих когда они пытаются инициализировать периферию не всю сразу, а по мере надобности. Включил, например, прерывания от таймера 1, а когда активизировал таймер 2, то затер биты таймера1. Как итог — результат не соответствует ожиданиям.

Как быть? Тут нам помогут битовые маски. Помнишь логические операции AND/OR/NOT/XOR?

Вот на их основе и будет работа. Маски накладываются побитно. Так что

  Установка бита Сброс бита Инверсия байта Инверсия отдельных бит
 Исходное значение 10001000 10001000 10001000 10001000
 Операция OR (|) AND (&) NOT (~) XOR (^)
Битовая маска 00010000 01111111 n/a 11000000
 Результат 10011000 00001000 01110111 01001000

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

Вот только не следует путать поразрядные операции (например &) с логическими (&&). Первые действую бит против бита, вторые сразу целиком весь байт, анализируя его по принципу равено он нулю или нет. Если в байте будет хоть одна единица, то он уже не равен нулю.

Так что теперь операция установки бита будет выглядеть так:

1
LED_PORT = LED_PORT | 1<<LED2;

Т.е. берем предыдущее значение порта LED_PORT и накладываем на него маску 1<<LED2 которая установит биты. Результат помещаем обратно где взяли.

Для сброса бита нам маску надо инвертировать, поэтому выглядеть будет так:

1
LED_PORT = LED_PORT & ~(1<<LED2);

Согласно приоритету выполнения операций вначале выполняется выражение в скобках (битмаска), потом оно инвертируется, а затем накладывается на значение порта. И все записывается в прежнее значение.

Ну и для инверсии бита (0 меняется на 1, а 1 на 0) используется такая конструкция:

1
LED_PORT = LED_PORT ^ (1<<LED2);

Также Си допускает сокращенную конструкцию подобной записи логических операций:

1
2
3
LED_PORT &= ~(1<<LED2);
LED_PORT |= 1<<LED2;
LED_PORT ^=1<<LED2;

Так что смело правим нашу прогу получается вот такая конструкция:

1
2
3
4
5
6
switch(UDR)
	{
	case '1': LED_PORT |= 1<<LED2;		break;
	case '0': LED_PORT &= ~(1<<LED2);	break;
	default: break;
	}

и

1
2
3
4
5
6
while(1)
 {
i++;
LED_PORT ^=1<<LED1;
_delay_ms(1000);
 }

Так как нам надо просто мигать, то достаточно инвертировать бит, и код становится еще меньше. Мне тут с галерки подсказывают, что в новых моделях AVR для того, чтобы инвертировать бит достаточно записать в регистр PIN бит на соответствующую ногу. Не знаю, не проверял. А в даташите на атмега 168 (один из новых) регистр PIN обозначен как READ ONLY.

Сами операции выглядят довольно минималистично:
Инверсия через чтение-модификацию-запись

1
2
3
4
46:       LED_PORT ^=1<<LED1;
+00000060:   B382        IN        R24,0x12       In from I/O location
+00000061:   2784        EOR       R24,R20        Exclusive OR
+00000062:   BB82        OUT       0x12,R24       Out to I/O location

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

1
2
17:       	case '1': LED_PORT |= 1<<LED2;		break;
+00000041:   9A95        SBI       0x12,5         Set bit in I/O register
1
2
 	case '0': LED_PORT &= ~(1<<LED2);	break;
+00000043:   9895        CBI       0x12,5         Clear bit in I/O register

Но этот прикол нашли все кто внимательно в код позырил. А есть более тонкие глюки. В частности я тут «бездумно» скопипастил код инициализации USART из другого проекта.

1
2
3
4
5
6
7
8
9
10
11
#define XTAL 8000000L
#define baudrate 9600L
#define bauddivider (XTAL/(16*baudrate)-1)
#define HI(x) ((x)>>8)
#define LO(x) ((x)& 0xFF)
 
UBRRL = LO(bauddivider);
UBRRH = HI(bauddivider);
UCSRA = 0;
UCSRB = 1<<RXEN|1<<TXEN|1<<RXCIE|1<<TXCIE;
UCSRC = 1<<URSEL|1<<UCSZ0|1<<UCSZ1;

Казалось бы все хорошо, работает. Где тут бага?

А все дело в прерывании. Мы создали обработчик для USART_RXC_vect, а для TX ничего не писали, хотя вектор прерывания от передатчика включен. Да, в данный момент нам это не мешает — т.к. мы ничего не передаем, но программы имеют тенденцию усложняться и кто знает во что это выльется. А что будет если произойдет прерывание которого нет? А давай поглядим на вектора:

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
+00000000:   940C002A    JMP       0x0000002A     Jump
+00000002:   940C0034    JMP       0x00000034     Jump
+00000004:   940C0034    JMP       0x00000034     Jump
+00000006:   940C0034    JMP       0x00000034     Jump
+00000008:   940C0034    JMP       0x00000034     Jump
+0000000A:   940C0034    JMP       0x00000034     Jump
+0000000C:   940C0034    JMP       0x00000034     Jump
+0000000E:   940C0034    JMP       0x00000034     Jump
+00000010:   940C0034    JMP       0x00000034     Jump
+00000012:   940C0034    JMP       0x00000034     Jump
+00000014:   940C0034    JMP       0x00000034     Jump
+00000016:   940C0036    JMP       0x00000036     Jump
+00000018:   940C0034    JMP       0x00000034     Jump
+0000001A:   940C0034    JMP       0x00000034     Jump
+0000001C:   940C0034    JMP       0x00000034     Jump
+0000001E:   940C0034    JMP       0x00000034     Jump
+00000020:   940C0034    JMP       0x00000034     Jump
+00000022:   940C0034    JMP       0x00000034     Jump
+00000024:   940C0034    JMP       0x00000034     Jump
+00000026:   940C0034    JMP       0x00000034     Jump
+00000028:   940C0034    JMP       0x00000034     Jump
+0000002A:   2411        CLR       R1             Clear Register
+0000002B:   BE1F        OUT       0x3F,R1        Out to I/O location
+0000002C:   E5CF        LDI       R28,0x5F       Load immediate
+0000002D:   E0D4        LDI       R29,0x04       Load immediate
+0000002E:   BFDE        OUT       0x3E,R29       Out to I/O location
+0000002F:   BFCD        OUT       0x3D,R28       Out to I/O location
+00000030:   940E004A    CALL      0x0000004A     Call subroutine
+00000032:   940C006B    JMP       0x0000006B     Jump
+00000034:   940C0000    JMP       0x00000000     Jump

Как видишь, все пустые вектора ведут на метку 00000034, где нас ждет

1
+00000034:   940C0000    JMP       0x00000000     Jump

Это переход на вектор сброса.

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

Хотя такой маневр компилятора относительно неопределенных векторов у меня вызывает недоумение. Дело в том, что при писании прог на ассемблере принято глушить вектора не переходами в ноль, а командами RETI — в этом случае прерывание как вызывалось так и убралось. И программа потеряла только несколько тактов, а не работосопособность.

С другой стороны вешать туда сброс это максимизация ошибки. Т.е. большую багу когда не работает вообще все отловить легче.

Еще есть в этом коде еще мощные вилы связанные с неатомарностью (атомарный доступ — в одну команду, без возможности прерывания) доступа к регистрам периферии и памяти. Но об этом я расскажу в следующий раз.

Анонс:
Прерывание + доступ к памяти = головная боль быдлокодера.
Архитектура построения программы контроллера — то о чем дружно забыли составители курсов.
Отладочные приемы или как эффективно травить клопов в матрасе

90 thoughts on “AVR. Учебный Курс. Программирование на Си. Часть 4”

  1. Вот кусок документации на мегу:

    Writing a logic one to PINxn toggles the value of PORTxn, independent on the value of
    DDRxn. Note that the SBI instruction can be used to toggle one single bit in a port.

    Только что попробовал, работает!

    1. сорри, на 168 мегу.

      Соответственно, код

      PORTD=1<<3;
      DDRD=1<<3;

      for (;;)
      {
      PIND=1<<3;
      _delay_ms(1000);
      }

      заставляет мигать светодиодом.

      1. Ой какая прелесть! Выходит теперь у нас есть CPL за одну инструкцию! УРА! А то я все сокрушался что после перехода на AVR с С51 мне не хватало этой команды :)

        1. Кстати, если уж речь зашла о DDRx, PINx, PORTx. Думаю, новичкам было бы полезно осознать разницу между последними двумя регистрами и операциями над ними. По опыту, людям не всегда это очевидно из чтения даташитов.

  2. Кстати, если нажать в AVR studio клавишу F12, то она очистит папку, в которую билдит проект от всего, созданного при компиляции, типа elf, hex, lss, map, o. Насколько я понял, работает только после постановки GCC.

  3. Дело в том, что при писании прог на ассемблере принято глушить вектора не переходами в ноль, а командами RETI — в этом случае прерывание как вызывалось так и убралось. И программа потеряла только несколько тактов, а не работосопособность.
    Был у мя один баг на эту тему. Я как то наивно думал что RETI является панацеей! А вот и нет. Иницилизации USART тоже делал копипастом, есссессно включил и приём, и передачу. Отправлял непрерывно данные с ацп и всё было прекрасно, пока не дотронулся до мк. Хрясь и COM порт замолчал! Чё за … Ушло почти 3 часа!!! Оказалось, что пока не произвёл чтение UDR флаг RX не сбрасывается. Картина маслом:
    Наводка на RX-> в UDR мусор-> прерывание от UDR приём-> reti (чтение нет,а значит флаг не сброшен)-> прерывание от UDR приём-> reti-> прерывание от UDR приём-> reti и так пока ацп (который встаёт позже чем USART и поэтому тапки полюбому USART_а) не сорвёт стек!! А я тем временем убиваюсь говой об стол, понимая что это не бок блока питания, а моя ошибка в коде))))

    1. Вот дерьмо. Точняк. Я как то и забыл, что прерывание UDR не сбрасывается при уходе на обработчик и надо читать регистр. Однако тут вылазит максимизация ошибки — такой глюк куда заметней чем спонтанный ресет :)

      1. Так как такую заглушку для уарта сделать, то есть я так понимаю надо какбы считать в NULL из UDR? Ну на асм’е наверное в какой-нибудь регистр и потом о нём можно спокойно забыть, а вот на Си… интересно… Переменную ввести чтоль

    1. Знаю. Нахер его. Мало того, что это лишняя сущность, так еще совершенно не очевиден (я когда увидел его в первые долго тупил думал это какая то функция) и не определен в других компиляторах.

      Опять же инверсию им делать чтобы выключить бит так это еще одно лог условие, а тут только нолик вписать.

      1. Что за ерунда, я думал 0<<LED2 это у Вас «намеренная ошибка».

        «LED_PORT=0<<LED1» делает совсем не то, что Вы подразумеваете; ноль сколько ни сдвигай в любую сторону всё равно ноль получишь. Правильно пишут «LED_PORT &= ~(1 << LED1)» либо «LED_PORT &= ~_BV (LED1)». А исходная строка тупо выключит все биты в DDR.

        BV расшифровывается как bit value. Лично я даже в документацию не смотрел, сразу всё понятно было.

        1. Нет разумеется нулем не сбросить. Но, например, в такой ситуации:

          1
          
          UCSRB = 1<<RXEN|1<<TXEN|1<<RXCIE|1<<TXCIE;

          можно взять и записать в одно движение:

          1
          
          UCSRB = 1<<RXEN|1<<TXEN|1<<RXCIE|0<<TXCIE;

          выключив тем самым бит из инициализации. Скажем для отладки. При этом сама запись остается на месте и ее можно вернуть просто поменяв 0 на 1.

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

          1. Да, оно, конечно так про замену 1 на 0 и обратно. Но это в таких длинных конструкциях хорошо, когда очевидно, что пишется целое слово/байт, составленное из таких-то полей, каждое из которых имеет такое-то значение. А в процитированном куске, от которого мне тоже чуть плохо не стало, запись целого слова/байта _неочевидна_. Особенно для новичков, для которых тут вроде как всё пишется.

            А между тем

            LED_PORT=0<<LED1; /* При чтении подразумеваем «сбросить LED1 в 0» */

            это просто

            LED_PORT=0; /* На самом деле — обнулить ВСЕ биты */

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

    2. Не стоит привыкать к использованию неизвестно кем и для чего введенных служебных макросов (а подчеркивание в названии говорит именно о «служебности»), не описанных ни в C99, ни в POSIX’е.

      «Привыкнешь, и жизнь твоя не будет стоить и ломаного цента» (c) :)

      В смысле переносимость таких программ нулевая. Для мелкого кодинга под микроконтроллер оно, может, ещё и сойдёт (хотя при переходе на другой МК тоже может выйти боком), а при «большом» программировании на Си уж точно не стоит этим пользоваться.

      1. Порылся тут в сети. Ну да, так и есть. Макрос _BV() — это персональное изобретение AVR GCC для своих узко-хозяйственных целей. При переходе с AVR на что-нибудь другое, получите массу приятных ощущений от необходимости всё это безобразие переправлять.

        Если уж так сильно рябит в глазах от битовых сдвигов, гораздо лучше определить свой собственный макрос, аналогичный этому _BV(), и таскать его за своим проектом в отдельном заголовочном файле:

        — mytools.h —
        #ifndef __MY_TOOLS_H__
        #define __MY_TOOLS_H__
        /* Set bit macro, use as val |= _SET(bit) */
        #define _SET(x) (1 << (x))
        /* Clear bit macro, use as val &= _CLR(bit) */
        #define _CLR(x) (~_SET(x))
        #endif
        ——————

        1. 1. Я не собираюсь переходить с gcc на что-нибудь другое.
          2. Задефайнить макрос _BV — дело пяти секунд. #ifndef _BV и так далее.
          3. Используя собственные корявые макросы точно так же рискуете наползти на чужой макрос при смене компилятора.

  4. На Си тоже можно задать вектор прерывания по умолчанию

    ISR (BADISR_vect)
    {
    reti();
    }

    маны бывают иногда полезными

  5. Кста, DI, а можешь статейку про разводку печатных плат написать?
    Думаю эта тема интересна многим )

    1. Я просто не знаю что там писать. Ну это как художника попросить научить рисовать :) Тут только один совет — садиться и разводить. Начинать с простых, потом усложнять.

      1. Я так понимаю, все очень от софта еще зависит. Я пробовал с CircuitMaker+TraxMaker — получается, что надо самому располагать компоненты + две трети дорог рисовать вручную. Может и руки кривые.

        1. Да это все трассировщики такие. Даже профессиональные, типа PCADа, простые схемы, которые я разведу полностью на одной стороне, без протаскивания дорожек промеж ног и без перемычек, в большинстве случаев не могут развести без двух сторон и металлизации, или понакрутят петель черезо всю плату. А «оптимизация» в них по всяким критериям еще больше все запутывает. Часто в них изменение вручную пары дорожек позволяет сделать разводку более оптимальной. Разводку надо «чувствовать», а это приходит только с опытом. Чем больше занимаешься, тем лучше получается. Сейчас хоть это намного проще делается на компьютере, а в 70х годах приходилось делать на клетчатой бумаге, стирая иногда из за одной дорожки уже поллиста разведенных… И производительность процесса сильно зависела от качества бумаги, карандаша и резинки. Чтобы бумага не лохматилась и хорошо стиралась, чтобы карандаш писал четко, но легко стирался, чтобы резинка не пересыхала, стирала хорошо карандаш, не размазывая, и не портя бумагу…
          Раньше на плате приходилось ставить до полусотни микросхем, от 14 до 40 ног у каждой, и кучу других деталей. Сейчас редко больше десятка, а то и вообще на одной. Развести их — не проблема. Зато и ног у некоторых корпусов стало до двух сотен и более. Правда, я считаю, что для дома можно обойтись и без них. Незачем конкурировать с автоматизированным производством, мучаясь с ними кустарными способами.

        2. Вручную надо разводить, так как частенько необходимо окружить вниманием анал. цепи или развести вход с выходами. ИМХО …

      2. Имелось ввиду введение — какой пакет нравится и почему, к какому проще найти дополнительные библиотеки компонентов, и базовые вещи — к примеру: как задать количество слоев, подкорректировать творчество автотрассировщика, развести полностью вручную и т.д.

        То, что непосредственно разводка — дело серьезное — это понятно )

        1. Мне на 99% хватает Sprint Layout’a материнские платы я не развожу поэтому вся мощь пикадов всяких мне не нужна.

              1. Я когда увидел, что в Лайауте — ужаснулся :)
                Чем удобен РCAD? Составил принципиальную схему в модуле Shematics, сделал netlist, импортировал его в файл PCB и спокойно разводишь руками.
                Все связи между элементами, футпринты элементов (контактные площадки, отверстия) показаны и ошибиться невозможно (это актуально даже для относительно несложных плат).
                Можно задать ширину проводника и расстояние между проводниками для любой цепи (оч. удобно для питания, аналоговых линий и т.п.). Очень мощный модуль проверки ошибок (в Лайауте тоже есть — DRC, но попроще).
                Главный минус PCAD, ИМХО, — библиотеки элементов, на создание которых надо убить кучу времени. И, конечно, сложность интерфейса и куча настроек. Но оно того стоит, очень рекомендую изучить.

                1. Понимаешь, я черчу много разных схем, причем футпринт под каждый компонент приходится рисовать заново, т.к. хер найдешь готовый. А схемы редко сложные, поэтому я в 99% никогда не создаю принципиалку и херачу сразу же PCB так что я в любом случае не косячу. Я в принципе спокойно рисовал схемы и в игле и в пикаде, но забил на эти проги потому что пока сделаешь всю подготовку (все принты нарисуешь) я уже в лайоуте разведу схему на три раза.

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

    1. Можно конечно. Для операционок реального времени, например, а особенно с виртуальными машинами, прерывания — это вообще зло. Для микроконтроллеров, подозреваю, тоже могут быть злом. Потому что приход прерывания — штука недетерминированная и может попортить вообще всю малину, если придёт не вовремя. Запрещать/разрешать прерывания каждый раз — это лишние (минимум) две инструкции к тому же. Да и оверхед на сохранение/восстановление контекста может тоже мешать.

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

  7. Слушай, DI. Когда-то ты юзал avrlib. Я даже видел ветку на казусе. Как тебе удалось пользоватся avrlib winavr и avrstudio? Ты юзал спец makefile или чтото прописал в студии?

    1. а ты про библиотеку с кучей всякого кода на все случаи жизни?

      Да я просто кидаю копию хидера и сишника в свой проект и подключаю его словно собственный исходник.

  8. DI HALT , здравствуйте! Можно с этой главы поподробней, особенно про то где и как я оказываюсь после обработки прерывания. У меня что-то не получается начать с того места, окуда я ушла на прерывание.
    P.S. Спасибо! Очень интересно читать))

    1. Ну давайте сначала определимся на каком языке пишем. На асме или на Си. Хотя работает конечно же все одинаково.

      Терминология:
      Аппаратное — делает процессор под действием внешних сигналов
      ПОльзователь — то что мы вписываем в программу сами. Текст программы заставляет делать так.

      Путь такой (в скобочках действие):
      Фоновая программа -(Прерывание!)-(аппаратный аналог CALL)- вектор прерывания -(пользовательский JMP)- процедура обработки прерывания — (пользователь делает RET)- Фоновая программа.

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

      Если же брать чисто сишную абстрацкию, то обработка прерывания выглядит так:

      Мы где то в дебрях main{} — (Прерывание!) — мы оказываемся в ISR{} — как закончили обоработку то выходим обратно в main{} ровно в то же место откуда пришли.

      1. Пишу я на си. Я правильно понимаю, должно быть так:

        инклуды

        Фунцкции
        {
        }
        Прерывания
        {
        }
        main()
        {
        инициализация;

        Главный БЕСКОНЕЧНЫЙ цикл ( While (1) )
        {
        собственно программа пошла
        1…..
        2….
        тут мы уходим на обработку прерывания (например, пришел байт), а после того, как подпрограмма обработки прерывания отработана, возвращаемся на 3 и идем дальше на 4 и т.д. ?
        3….
        4…. и т.д.
        }
        }

        Я завтра попробую еще, но так у меня не получалось.

  9. Может быть, еще имеет смысл привести пример работы с таймером по прерываниям? ИМХО, во-первых, начинающему важно как можно быстрее получить результат с минимальными усилиями, а тут нужно правильно подключить контроллер к компу, разобраться с терминалом… Путь длиннее, граблей больше. А таймер — вот он, тут уже, и светодиод от него вовсю мигает :). Опять же, показать, как правильно организовывать в программе задержки вместо _delay_us и _delay_ms (на результаты работы которых прерывания будут оказывать влияние, кстати о птичках)

  10. Отличный курс. Ди Хальт в тебе пропадает преподаватель :). Сам я правда использую CVAVR, там что бы выставить в 1 какой-нибудь бит какого-нибудь порта не нужно городить конструкции с битовыми масками вроде

    LED_PORT = LED_PORT | 1<<LED2;

    а достаточно просто написать :

    PORTB.1=1; // к примеру выводим 1 в бит 1 порта B. При этом другие биты порта остаются нетронутыми

    Проверка состояния бита — аналогично.

    if (PINB.1=1) или просто if (PINB.1)

    if (PINB.1=0) или еще проще if (!PINB.1)

    Но в ОБЩЕМ подход Ди Хальта является более грамотным, так-как пишется компиляторо-НЕЗАВИСИМЫЙ код.
    Просто в моем случае нужно было как-можно быстрее получить работающий код, а за ним и устройство. На стандарты языка СИ и кроссплатформенность время не тратилось.
    А так, все-таки полезно знать как написать грамотно код, не привязанный к определенному компилятору.
    Автору большой РЕСПЕКТ !

    1. >Отличный курс. Ди Хальт в тебе пропадает преподаватель :)

      А я тут чем занимаюсь? ;)))) А попреподавать на реальном классе мне уже довелось. Удовольствие ниже среднего, впрочем, может с местом не поперло.

    2. А эта конструкция работатет только с портами IO? А просто с переменной в памяти не прокатит? Или там придется битовое поле делать?

      1. Эта конструкция — упрощение в компиляторе CVAVR (вроде компиляторы Микроэлектроники тоже ее содержат)- позволяющее работать с битами регистров непосредственно. Через конструкцию
        REGISTR.bit

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

      2. Сделай так (в маны по контроллеру не смотрю, пишу пример от балды, нужно поправить в соответствии с действительностью, в т.ч. и порядок битов в соответствии с endianness AVR’а):

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        
        typedef union {
             char all; /* Assuming that port is 8-bit wide */
             struct {
                  char b0:1;
                  char b1:1;
                  char b2:1;
                  char b3:1;
                  char b4:1;
                  char b5:1;
                  char b6:1;
                  char b7:1;
             } bit;
        } ledport;
         
        ...
         
        main()
        {
        leport *port1 = LED_PORT;
        ...
        port1->all = 0xFF; /* Set all bits to 1 */
        port1->bit.b4 = 0; /* Clear bit 4 */
        ...
        }
          1. Чё-то после форматирования кода выше, в нём баги появились :)
            Стрелки не правильно обработались и почему-то стали двойными.
            Должно быть -> (минус, больше), а не ->> (минус, больше, больше)

            1. Вот те, к которым можно по адресу обращаться — они и называются «memory-mapped».
              А бывают в природе (не факт, что в АВРе) еще регистры, существующие в отдельном адресном пространстве. К таким можно обращаться только специальными командами (на x86 — inb, outb и т.п.; на ppc — всякие mfdcr, mtdcr и прочая). Аппаратная реализация может быть разная, но, как правило, на шине адреса просто присутствует линия типа обращения (память или i/o), хотя могут быть и вообще отдельные шины. Так вот приведенный выше метод подходит только для memory-mapped регистров.

              Ты вот что учти, я под АВР не программировал никогда. И вообще под микроконтроллеры.
              Поэтому мои советы — они достаточно обобщенные и их надо самостоятельно адаптировать под конкретную архитектуру. :)

              Ну а если говорить об i/o регистрах (не memory-mapped), то для них тоже можно придумать какую-нибудь красивую оболочку, но уже менее симпатичную. Например так:

              ——————
              #define IOSETB(r, b) do {\
              char val;\
              val = inb(r); /* Assuming that inb() is a function to read from an 8-bit reg */\
              val |= 1 << (b);\
              outb(r, outb); /* Assuming that outb() is a function to write an 8-bit reg */\
              } while(0)

              #define IOCLRB(r, b) do {\
              char val;\
              val = inb(r); /* Assuming that inb() is a function to read from an 8-bit reg */\
              val &= ~(1 << (b));\
              outb(r, outb); /* Assuming that outb() is a function to write an 8-bit reg */\
              } while(0)
              ——————

              Пара комментариев для не-программистов и новичков.
              1. Обратите внимание, что при описании макросов все их аргументы я всегда беру в скобки. Это _необходимо_ делать всегда во избежание неоднозначностей при обработке макроса Си-препроцессором. Дело в том, что препроцессор подставляет значения аргументов в макросе точно так, как они были переданы, без каких-либо вычислений над ними. Поэтому, если аргументы внутри макроса не заключать в скобки, то например, если порт SOME_PORT управляет несколькими идентичными устройствами одновременно, и каждое под каждое из них отведено DEV_SIZE битов, то конструкция ~(1 << b) при использовании макроса IOCLRB вот таким образом: «IOCRLB(SOME_REG, START_BIT + DEV_SIZE*devn)» будет преобразована препроцессором фактически вот в такую конструкцию:

              ~(1 << START_BIT + DEV_SIZE*devn)

              А это при значениях START_BIT = 1, DEV_SIZE=2, devn=3 даст нам, в соответствии с приоритетами операций, вот такой результат:

              ~(1 << 1 + 2*3) == ~(2+6) == ~8

              что сильно отличается от желаемого

              ~(1 << (1+2*3)) == ~(1 << 7) == ~128.

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

              SOMEMACRO(i++);

              Почему? Потому что в итоге, если внутри макроса аргумент используется более 1 раза, вы рискуете на выходе из макроса получить увеличение переменной i больше, чем на 1. :)

              2. Конструкция «do { … } while(0)», использованная мной в макросах выше — это не бессмыслица, как может показаться на первый взгляд. Ведь, действительно, зачем использовать цикл, условие которого таково, что он будет исполнен лишь однажды? А смысл в том, что объявленный таким образом многострочный макрос можно без зазрения совести использовать как полноценную функцию типа void. То есть ставить после выражений if и while, не заключая в дополнительные фигурные скобки, и при этом оставаться уверенным, что будет под if или while будет исполнен _весь_ макрос, а не только его первая строчка, как было бы, если бы он был объявлен без «do {..} while(0)».

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

              1. А, ну просто у AVR все регистры IO имеют адрес в памяти, а младшие 3F регистра еще имеют и адрес в пространстве IO по которым к ним обращаются битовые операции. Поэтому в АВР тут можно и так и так.

                1. —
                2. Компилятор, конешн, ее сожрет и вырежет (при включенном оптимизаторе), но что мешает использовать этот макрос не как фунцкию. а именно как макрос, без ; в конце, просто заключив его в фигурный блок (прямо в тексте макроса)?

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

                1. Можно и так, но лично у меня строки без ; в конце вызывают аллергию :)
                  Хотя, оно и с ; пойдёт, кстати. Но в учебниках рекомендуют именно do {…} while(0) делать. Чё-то я сейчас не соображу, в чём подводные камни простых фигурных скобок могут быть.

                  Кстати, а может расскажешь, как сразу писать код отформатированным тут?

                  1. А! Вот! Знаю в чём подводный камень. Надо или всегда использовать такие макросы без точки-запятой, что выглядит некрасиво, или будут ошибки компиляции в случаях типа

                    if()
                    MACRO();
                    else

  11. действия типа 1<<7 работает только с регистрами? С переменными такое будет работать? Как вообще изменить конкретный бит в переменной не меняя соседние? Спасибо.

  12. Привет DI, после полтора года ковыряния ассемблера и понимания как внутри работает МК, решил что пора заняться изучением СИ. Сейчас сижу грызу твои статейки по СИ и другие сайты тоже поковыриваю. У меня есть платка с ATmega16 и разведенными сведодиодами. Так типа мини пинбоарда, когда-то на ней осваивал асму. Сейчас решил под нее написать такую-же прогу мигания диодами тока на СИ. Все работает чики-пики пока не включаю sei(); При компиляции ни каких варнингов и ошибок. Заливаю и при определенной комбинации переключения диодов все останавливается намертво. Реакции со стороны мк ноль. Даже на ресет не сбрасывается. Все прерывания отключены, обработчики прерываний еще отсутствуют. Убираю sei(); проблема исчезает. Играл с оптимизацией не помогает. Причем клинит строго при одной комбинации. volatile тоже вставлял, бесполезняк. Не знаешь что за ерунда? Куда хоть рыть?

    1. Рой в прерывания и его вектора. У тебя косяк похоже где то там. Разрешил какое то не то прерывание вот и вклинило. А Вот то что на ресет не реагирует это странно.

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

        1. Не, ну думай логически. ДО SEI у тебя все ок. После — жопа. Значит где то разрешено прерывание на который нет обработчика.

          1. А кстати слегка поколупываю 5 студию, уже вышла официальная версия. Всяких там кнопочек и пимпочек налепили кучу и окон разных открывается пачка целая, какие то вещи стали менее удобны какие то более удобны. например в портянке где пишется код, сам код теперь можно сворачивать, раньше приходилось длинные куски в файлы прятать чтоб перед глазами не мельтешил, а сейчас нажал и опа кусок кода свернулся. А вот переключение с десятичной в шеснадцатеричную системы сделали неудобно. Каждый регистр отделно нужно переключать это убиться можно. А ведь рано или поздно 4 версия студии канет в небытие да и они ее поддерживать перестанут. по любому через пару тройку лет придется на 5 версию переползти. Вот такие дела.

  13. Здравствуйте у меня возник такой вопрос
    как из вот этого

    ISR(USART_RXC_vect)
    {
    switch(UDR)
    {
    case ‘1’: LED_PORT = 1<<LED2; break;
    case ‘0’: LED_PORT = 0<<LED2; break;
    default: break;
    }
    }
    выйти со сброшенным флагом глоб.прерывания
    или скажем выходить из прерывания с флагом i или без него в зависимости от
    условия в теле прерывания

    1. Из прерывания можно смело выходить через ret тогда флаг i не вернется. Но это надо делать ассемблерными вставками. И создавать обработчик без пролога и эпилога (есть там такой модификатор только не помню как пишется, вроде бы native) но надо еще и регистры юзаемые сейвить самостоятельно в стеке на пару со срегом.

  14. здравствуйте прошу прощения что вопрос немного не потеме
    почему функция SUM добавляется в код веть она даже не используется

    //WinAVR-20100110//AVR Studio 4.16 Build628
    #include
    unsigned char sum (unsigned char i,unsigned char u);
    int main( void )
    {
    while (1){PORTB=1,PORTB=0;}
    return 0;
    }//main

    unsigned char sum (unsigned char i,unsigned char u)
    {//sum
    PORTB=i;
    PORTB=(i-u);
    PORTB=(i+u);
    PORTB=(i/u);
    PORTB=(i+u);
    PORTB==(i-u);
    PORTB-=(i+u);
    PORTB*=(i/u);
    PORTB/=(i+u);
    return i+u;
    }//sum

    1. Си это не паскаль. Если вы функцию обьявили и написали, то она будет включена в код. Даже если нигде не применяется. Это наверняка можно исправить ключами линкера и компилятора. Предлагаю вам самому покурить документацию. Ее там на целую книгу :)

  15. Если принимается по uart 1 байт, что с ним делать — понятно.
    А если принимается несколько байт подряд(например, как предложения NMEA0183), то как их принять и обработать.

  16. Программировал не в Atmel Studio, а на Linux с makefile. Подскажите пожалуйста, как отправить бит через терминал в UDR?

Добавить комментарий

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha