Работа с регистрами портов, как сделать правильную библиотеку.

Тема в разделе "Флудилка", создана пользователем Alex19, 21 июл 2015.

  1. Alex19

    Alex19 Гуру

    День добрый.
    Для работы с портами использую библиотеку FastIO.h (https://github.com/ErikZalm/Marlin-non-gen6/blob/master/Marlin/fastio.h), так же есть не плохая библиотека CyberLib (http://www.cyber-place.ru/showthread.php?t=550), ее внимательно не изучал.

    Обе они построены на макроподстановках, первая еще на объедении двух лексем.

    К примеру данный код, ни когда не скомпилируется.
    Код (Text):
    void checkPressButtonInversePin(uint8_t* btnArray, uint8_t bitNumber, uint8_t pinNumber)
    {
      if (MC_BIT_IS_SET(*btnArray, bitNumber))
      {      
            MC_WRITE_PIN(pinNumber, !MC_READ_PIN(pinNumber));
      }
    }
    Причина тут.
    Код (Text):
    #define MC_READ_PIN(IO) ((bool)(DIO ## IO ## _RPORT & MC_MASK_PIN(DIO ## IO ## _PIN)))
    Как в прочем и в MC_WRITE_PIN, там тоже используется объедении двух лексем (##).

    А хочется простой аналог digitalWrite, digitalRead с передачей простого int - номера пина ардуины. Это дает возможность, как передавать в функцию, так и работать в циклах.

    Первое что пришло мне в голову сделать функцию, с простым switch.
    Код (Text):
    void readPin(uint8_t pinNumber)
    {
      switch(pinNumber)
      {
        case 0:
          MC_READ_PIN(0);
          break;
        // И т.д.
      }
    }
    Минус, придется в 2 двух местах, 1 библиотеки контролировать сами пины. В switch и в списке #define со списком пинов. Или другие варианты со switch, переписав сами функции.

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

    Unixon не согласился с мои взглядом и вероятно он прав учитывая его опыт (да и тогда, просто не понял о чем он говорит, а может и сейчас не понимаю:)).

    Привожу цитату из темы - http://forum.amperka.ru/threads/тест-скорости-high-low.5431/#post-44397
    Сама библиотека avrpins.h - https://github.com/felis/USB_Host_Shield_2.0/blob/master/avrpins.h.

    Сделано конечно красиво (С++, ООП в хорошем примере), но большое количество классов, по сути каждый пин, это класс. Почему он будет быстрее макроподстановок?
    Да и размер скетча определенно будет больше.

    Много написал (увы не талант:(), как более правильно, интересует скорость и размер скетча, сделать функции работы с портами, по аналогии с digitalWrite, digitalRead с передачей простого int?
    Не интересует код, интересует принцип. Классов не боюсь как и ООП (хоть и минимальный опыт работы с ними).
     
  2. geher

    geher Гуру

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

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

    Если исходить из критериев скорости и размера, то, наверное, придется нарисовать что-то с директивами условной компиляции, в котором для каждого варианта платы предусмотрена своя ветка, обрамленная директивами
    #if,,, и #endif
    Категории плат можно с некоторой точностью различать по наличию определений SERIAL_PORT_USBVIRTUAL (для леонардоподобных), SERIAL_PORT_HARDWARE3 (для меги). Те, у которых таких определений нет - считать уноподобными.

    Внутри категории платы можно попробовать различать по количеству пинов (NUM_DIGITAL_PINS и NUM_ANALOG_INPUTS)
     
    Alex19 нравится это.
  3. Alex19

    Alex19 Гуру

    Большое спасибо за консультацию.

    Примерно, так себе и представлял. Вы подтвердили мои догадки и исправили ошибки в моем понимании. Мне больше нравится подход С++ (есть небольшой опыт с C#), но есть страх, что я не влезу ни по памяти, ни по размеру скетчей. Поэтому пока останусь на простом СИ, а уже после того, как закончу проект попробую переписать некоторые методы с помощью С++.

    Мне больше понравился, такой вариант, основанный на определении конкретного чипа
    Код (Text):
    //***********************  Определяем тип ардуины  **************************//
    #if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega2560__)

      // Частота процессора.
      #define F_CPU 16000000UL                    

      // Определяем тип ардуины.
      #if defined(__AVR_ATmega328P__)
        // Если ArduinoProMini.
        #define ARDUINO_PRO_MINI
      #endif
      #if defined(__AVR_ATmega32U4__)
        // Если ArduinoProMicro.
        #define ARDUINO_PRO_MICRO
      #endif
      #if defined(__AVR_ATmega2560__)
        // Если ArduinoMega.
        #define ARDUINO_MEGA_2560
      #endif
    #else
      #error This board not support!
    #endif
    //***********************  /Определяем тип ардуины  *************************//
    Как я понял можно еще определять и типы платформ
    Код (Text):
    ifdef __AVR__
    Код (Text):
    ifdef __arm__
    UPD.
    Тут пока не уверен, есть же inline, но он увеличивает размер кода. Придется выбирать.
     
    Последнее редактирование: 21 июл 2015
  4. nailxx

    nailxx Официальный Нерд Администратор

    ООП, макросы ни причём, если мы говорим о быстродействии. Нужно понимать какие конструкции в какой машинный код превращаются. Вот смотрите, вы хотите использовать FastIO, чтобы быстро обращаться к портам. И тут же предлагаете использовать адовый switch, который не то, чтобы убъёт всё быстродействие, а сделает ваш подход много медленнее штатного digitalRead.

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

    Уверен, вы знаете, что на микроконтроллерах AVR состояние регистров — это просто один из байтов в памяти. К нему можно обратиться по имени PINA, PINB, PINC, etc, в зависимости от того какой из регистров вы имеете в виду. Хотите считать быстро?

    Код (Text):
    uint8_t myBlazingFastData = PINA;
    Всё, быстрее этого нельзя. Даже на ассемблере. FastIO — это просто фантик, который сводится к этой конструкции.

    Теперь мы вспоминаем, что есть Arduino с её собственной нумерацией пинов. Это уровень абстрации. Его нужно привести обратно к AVR’ному виду. Можно это развернуть в compile-time (что и делает FastIO), можно в runtime, если ваши входные параметры — переменные, о значениях которых, компилятор может только догадываться. Последний случай — это штатный digitalRead и то, что вы пытаетесь добиться.

    Давайте глянем на штатный digitalRead?

    Код (Text):

    int digitalRead(uint8_t pin)
    {
        uint8_t timer = digitalPinToTimer(pin);
        uint8_t bit = digitalPinToBitMask(pin);
        uint8_t port = digitalPinToPort(pin);

        if (port == NOT_A_PIN) return LOW;

        // If the pin that support PWM output, we need to turn it off
        // before getting a digital reading.
        if (timer != NOT_ON_TIMER) turnOffPWM(timer);

        if (*portInputRegister(port) & bit) return HIGH;
        return LOW;
    }
     
    Ага, тут какие-то digitalPinTo… Смотрим что это

    Код (Text):

    #define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )
    #define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )
    #define digitalPinToTimer(P) ( pgm_read_byte( digital_pin_to_timer_PGM + (P) ) )
     
    Макросы. Если развернёте дальше, то увидите, что это обычный lookup, который ведёт к исконным PINA, PINB и еже. В общем, необходимый уровень зла, связанный с тем, что распиновка Arduino ≠ распиновке микроконтроллера. Но это самый эффективный lookup, который может быть. Быстрее не сделаете.

    Но да, здесь что-то про таймеры, что-то про NOT_A_PIN, что-то про ветвление. Допустим, вам всё это не нужно: вас не интересует состояние PWM, вы уверены, что не промажете индексом, вам не нужен результат канонически приведённый к bool. Хорошо, отбрасываем ненужное:

    Код (Text):

    uint8_t digitalReadFast(uint8_t pin)
    {
        uint8_t bit = digitalPinToBitMask(pin);
        uint8_t port = digitalPinToPort(pin);
        return *portInputRegister(port) & bit;
    }
     
    Ну и если вы настаиваете на том, что это должно работать без пенальти на вызов функции (4 такта навскидку), в ущерб размеру скетча, заявите об этом компилятору:

    Код (Text):

    uint8_t digitalReadFast(uint8_t pin)  __attribute__((always_inline))
    {
        uint8_t bit = digitalPinToBitMask(pin);
        uint8_t port = digitalPinToPort(pin);
        return *portInputRegister(port) & bit;
    }
     
    Вот мы и получили самый скоростной способ прочитать порт Arduino. С записью всё то же самое.
     
    Alex19 нравится это.
  5. Alex19

    Alex19 Гуру

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

    Не подумал, просто решал задачу в лоб.

    Да я представляю работу FastIO.

    Спасибо за подробное разъяснение, я как и любой любитель захотел все и сразу:(. Пока останусь на простой обертке FastIO.

    У меня к Вам пару вопросов, буду благодарен за ответы.
    1.
    Как понимать?
    Предполагаю, что для этого требуется знания ассемблера и понимание архитектуры конкретного чипа, но так ли это.
    2. __attribute__((always_inline)), как я понимаю это команда компилятора, где можно ознакомится со всеми командами компилятора?

    UPD. Ступил с вопросом 2. Сейчас использую Arduino IDE для компиляции, там используется avr-gcc. О нем, без труда можно найти всю информацию - https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html.
     
    Последнее редактирование: 22 июл 2015
  6. Megakoteyka

    Megakoteyka Оракул Модератор

    Alex19 нравится это.
  7. geher

    geher Гуру

    На самом деле можно выжать еще немного ускорения.
    В частности, можно избавиться от pgm_read_byte, перенеся массивы с номерами портов и номеров бит там в ОЗУ и исключив обращение к флэшу (progmem). Для этого нужно убрать соответствующую спецификацию в объявлении массива.
    Но, естественно, за это придется заплатить небольшой потерей доступного ОЗУ (несколько десятков байт).

    Кстати, преобразование пинов в пары порт-бит можно сделать на уровне макроопределений.
    что-то вроде
    #define PIN1 PORTD,2
    И скармливать это в виде единственного параметра для макросов FastIO.
     
    Alex19 нравится это.
  8. nailxx

    nailxx Официальный Нерд Администратор

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

    Есть книга Hacker's Delight, она в своё время мне просто вывернула мозг, показав какие трюки можно творить на уровне битов-байтов. Очень полезно. Погуглите, весьма может быть, что найдёте её в PDF на первой же странице выдачи.

    Ну и далее, ATmega datasheet.

    Посмотреть какой ассемблер получается в итоге из вашего скетча на C++ можно с помощью утилиты arv-objdump:

    Код (Text):
    avr-objdump -S /path/to/your/build/directory/firmware.elf > asm_of_my_sketch.S
     
    Alex19 нравится это.
  9. Alex19

    Alex19 Гуру

    Извините за паузу, работа и разбирал ответы, не все было сразу понятно.

    Спасибо, ссылка действительно полезная. Там человек серьезно подошёл к данному вопросу, стараясь попробовать различные варианты. Конечное решение на С++ очень похоже на библиотеку avrpins.h. Лично мне, будет очень любопытно разобраться с его решением.

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

    Спасибо, за идею. Не знал о таком способе, даже не понимал, как это работает, на запятые в СИ не похоже. Через 30 минут нашел ответ.

    Код (Text):
    #define RECEIVE(first, second) (first*4+second*5)
    #define RECEIVE2(x) RECEIVE(x)
    #define A 3,4
    RECEIVE2(A);
    Взято от сюда - http://www.cyberforum.ru/cpp-beginners/thread42490.html.

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

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