Предельная скорость USB на STM32F103, чем она обусловлена?

    У данной статьи тяжёлая история. Мне надо было сделать USB-устройства, не выполняющие никакой функции, но работающие на максимальной скорости. Это были бы эталоны для проверки некоторых вещей. HS-устройство я сделал на базе ПЛИС и ULPI, загрузив туда «прошивку» на базе проекта Daisho. Для FS-устройства, разумеется, была взята «голубая пилюля». Скорость получалась смешная. Прямо скажем, черепашья скорость.



    Но я же крутой, у меня теперь есть USB-анализатор (его разработка была описана в блоке из нескольких предыдущих статей). Посмотрев логи, я понял, что похоже, знаю, почему скорость невозможно поднять, даже если STM32F103 обслуживает исключительно USB и ничего больше. Даже начал писать о результатах… Но потом решил, что кому-то это будет не интересно, а кто-то скажет, что и так это знал.

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

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

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

    Подготовка проекта STM32


    Создаём проект


    Итак. Чтобы все могли повторить мои действия, я скачал самую свежую на момент написания статьи версию CubeMX – 6.2. Правда, они выходят с такой частотой, что на момент, когда всё будет выложено на Хабр, всё может уже измениться. Но так или иначе. Скачиваем, устанавливаем.

    Создаём новый проект для STM32F103C8Tx. Добавляем туда USB-Device.



    Теперь, когда есть USB, добавляем CDC-устройство. При этом заменим ему VID и PID. Дело в том, что у меня есть inf Файл, который ставит драйвер winusb именно на эту пару идентификаторов. При работе через драйвер usbser скорость была ещё ниже. Я решил исключить всё, что может влиять. Буду замерять скорость без каких-либо прослоек.



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



    После всего этого (добавили USB и добавили RCC) можно и тактовые частоты настроить, но сначала спасём себя от самоотключающегося блока отладки. Он спрятан надёжно! Вот так сейчас всё выглядит по умолчанию:



    А вот так – надо



    Прекрасно! Теперь можно настроить тактовые частоты. Я всегда это делаю опытным путём. Системная частота должна стать 72 МГц, а частота USB – 48 МГц. Это я в состоянии запомнить. Остальное каждый раз заново вывожу.



    Ну всё. Для тестового проекта настроек, вроде, достаточно. Заполняем свойства проекта и сохраняем. Лично я – в формате MDK ARM. Он же Кейл. Мне так проще.

    Надеюсь, я ничего не забыл сделать. Я специально показываю все шаги, чтобы внимательная общественность проверила меня.

    Донастраиваем проект в среде разработки


    В Кейле я убеждаюсь, что стоит максимальный уровень оптимизации, и дополнительно ставлю оптимизацию по времени. Тогда функции будут по максимуму… Не люблю англицизмы, но инлайниться они будут. А у нас вопросы быстродействия под конец рассуждений выйдут на первое место, так что автоматические инлайны – это то, что нам нужно.



    Дописываем код проекта


    Наш проект должен просто принимать данные из USB и… И всё! Принимать, принимать, принимать! Не будем тратить время на какую-то обработку этих данных. Просто приняли и забыли, приняли и забыли. Обработчик события «данные приняты» в типовом CDC проекте живёт здесь:



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

    Подготовка проекта под Windows


    Вариант честной работы с UART мы опустим. Дело в том, что совсем скоро мы будем искать причины тормозов. А вдруг они вызваны драйвером usbser.sys? Нет. Мы возьмём проверенный временем драйвер winusb и будем работать с ним через библиотеку libusb. Кому нравится Linux – сможет работать через эту же библиотеку там. Мы тренировались работать с нею в этой статье. А в этой – учились работать с нею в асинхронном режиме.

    Сначала я вёл работу через блокирующие функции, так как их написать было проще. Мало того, черновые замеры, которые я делал ещё до начала работы над текстом, были вполне красивые. Это ещё не всё, первая метрика, снятая для статьи, тоже была прекрасна и полностью отражала черновые результаты! Потом что-то случилось. График стал каким-то удивительным, правда, чуть ниже я эту удивительность объясню. При работе с блоками меньше чем 64 байта программа съедала 25% процессорного времени, а именно на блоке 64 байта был излом. Мне казалось, что кто-то обязательно напишет в комментариях, что сделай я всё на асинхронных функциях, всё станет намного лучше. В итоге, я взял и всё переписал на асинхронный вариант. Процент потребления процессорного времени на малых блоках действительно изменился. Теперь программа потребляет 28% вместо двадцати пяти… Цифры скоростей же не изменились… Но асинхронная работа более правильная сама по себе, так что я покажу именно её. Вся теория уже рассматривалась мною в тех статьях про libusb.

    Я завожу всё ту же вспомогательную структуру:

        struct asyncParams
        {
            uint8_t* pData;
            uint32_t dataOffset;
            uint32_t dataSizeInBytes;
            uint32_t transferLen;
            uint32_t actualTranfered;
    
            QElapsedTimer timer;
            quint64       timerAfter;
    
        };
    
        asyncParams m_asyncParams;
    

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

    Ну, и указатели на объекты «передача» имеются, куда же без них:

        static const int m_nTransfers = 32;
        libusb_transfer* m_transfers [m_nTransfers];
     

    Функция обратного вызова отличается от описанной в предыдущих статьях как раз тем, что она считывает показание таймера, если передавать больше нечего. Это произойдёт не единожды, а для каждой из передач (тридцати двух в случае мелких блоков, если блоки крупные – их будет меньше, но всё равно не одна). Но на самом деле, это не страшно. Мы это значение будем анализировать только после последнего вызова этой функции. В остальном – там всё то же, что и раньше, так что просто покажу код, не объясняя его. Объяснения все были в предыдущих статьях.

    void MainWindow::WriteDataTranfserCallback(libusb_transfer *transfer)
    {
        MainWindow* pClass = (MainWindow*) transfer->user_data;
        switch (transfer->status )
        {
        case LIBUSB_TRANSFER_COMPLETED:
            pClass->m_asyncParams.actualTranfered += transfer->length;
            // Still need transfer data
            if (pClass->m_asyncParams.dataOffset < pClass->m_asyncParams.dataSizeInBytes)
            {
                transfer->buffer = pClass->m_asyncParams.pData+pClass->m_asyncParams.dataOffset;
                pClass->m_asyncParams.dataOffset += pClass->m_asyncParams.transferLen;
                libusb_submit_transfer(transfer);
            } else
            {
                pClass->m_asyncParams.timerAfter = pClass->m_asyncParams.timer.nsecsElapsed();
            }
            break;
    /*    case LIBUSB_TRANSFER_CANCELLED:
        {
            pClass->m_cancelCnt -= 1;
        }*/
        default:
            break;
        }
    
    }
    

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

    Ну, а параметр blockSize у функции – это я в своих статьях уже набил оскомину высказыванием, что при работе с USB скорость зависит от размера блока. До определённого значения она ниже нормальной. Это связано с тем, что хост посылает пакеты медленнее, чем их может обработать устройство. Поэтому я всегда строю графики и смотрю, где они входят в насыщение. Сегодня я буду делать то же самое. Правда, сегодня график в дополнение к банальному росту, имеет непривычную для меня форму, что и сподвигло меня на переделку программы с блокирующего на асинхронный режим. Итак, функция, измеряющая скорость, выглядит так:

    Её текст я скрыл под катом.
    quint64 MainWindow::MeasureSpeed2(uint32_t totalSize, uint32_t blockSize, uint32_t avgCnt)
    {
        std::vector<qint64> gist;
        gist.resize(avgCnt);
    
        QByteArray data;
        data.resize(totalSize);
    
        m_asyncParams.dataSizeInBytes = totalSize;
    
        m_asyncParams.transferLen = blockSize;
    
        uint32_t nTranfers = m_nTransfers;
        if (totalSize/blockSize < nTranfers)
        {
            nTranfers = totalSize/blockSize;
        }
    
        for (uint32_t i=0;i<avgCnt;i++)
        {
    
            m_asyncParams.dataOffset = 0;
            m_asyncParams.actualTranfered = 0;
            m_asyncParams.pData = (uint8_t*)data.constData();
            // Готовим структуры для передач
            for (uint32_t i=0;i<nTranfers;i++)
            {
                m_transfers[i] = libusb_alloc_transfer(0);
                libusb_fill_bulk_transfer (m_transfers[i],m_usb.m_hUsb,0x02,//,0x01,
                                           m_asyncParams.pData+m_asyncParams.dataOffset,m_asyncParams.transferLen,WriteDataTranfserCallback,
                                                // No need use timeout! Let it be as more as possibly
                                           this,0x7fffffff);
                m_asyncParams.dataOffset += m_asyncParams.transferLen;
            }
    
            m_asyncParams.timerAfter = 0;
            m_asyncParams.timer.start();
    
            for (uint32_t i=0;i<nTranfers;i++)
            {
                int res = libusb_submit_transfer(m_transfers[i]);
                if (res != 0)
                {
                    qDebug() << libusb_error_name(res);
                }
            }
    
            timeval tv;
            tv.tv_sec = 0;
            tv.tv_usec = 500000;
            while (m_asyncParams.actualTranfered < totalSize)
            {
                libusb_handle_events_timeout (m_usb.m_ctx,&tv);
            }
    
            quint64 size = totalSize;
            size *= 1000000000;
            gist [i] = size/m_asyncParams.timerAfter;
        }
    
        for (uint32_t i = 0;i<nTranfers;i++)
        {
            libusb_free_transfer(m_transfers[i]);
            m_transfers[i] = 0;
        }
    
        qint64 avgSpeed = 0;
        for (uint32_t i=0;i<avgCnt;i++)
        {
            avgSpeed += gist [i];
        }
        avgSpeed /= avgCnt;
        if (avgCnt < 4)
        {
            return avgSpeed;
        }
        for (uint32_t i=0;i<avgCnt;i++)
        {
            if (gist [i] < (avgSpeed * 3)/4)
            {
                gist [i] = 0;
            }
            if (gist [i] > (avgSpeed * 5)/4)
            {
                gist [i] = 0;
            }
        }
        avgSpeed = 0;
        int realAvgCnt = 0;
        for (uint32_t i=0;i<avgCnt;i++)
        {
            if (gist[i]!= 0)
            {
                avgSpeed += gist [i];
                realAvgCnt += 1;
            }
        }
        if (realAvgCnt == 0)
        {
            return 0;
        }
        return avgSpeed/realAvgCnt;
    }
    


    Сборку статистики в файл csv я делаю так:

    void MainWindow::on_m_btnWriteStatistics_clicked()
    {
        QFile file ("speedMEasure.csv");
        if (!file.open(QIODevice::WriteOnly))
        {
            QMessageBox::critical(this,"Error","Cannot create csv file");
            return;
        }
    
        QTextStream out (&file);
    
        QApplication::setOverrideCursor(Qt::WaitCursor);
        for (int blockSize=0x8;blockSize<=0x20000;blockSize *= 2)
        {
            quint64 speed = MeasureSpeed(0x100000,blockSize,10);
            out << blockSize << "," << speed << Qt::endl;
        }
    
        out.flush();
        file.close();
        QApplication::restoreOverrideCursor();
    }
    

    Первый результат


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



    Где-то после размера блока 4 килобайта, скорость упирается в 560 килобайт в секунду. Давайте я грубо умножу это на 8. Получаю условные 4.5 мегабита в секунду. Условность состоит в том, что на самом деле, там ещё бывают вставные биты, да и на пакеты оверхед имеется. Но всё равно, это отстоит очень далеко от 12 мегабит в секунду, положенных на скорости Full Speed (кстати, именно поэтому на вступительном рисунке стоит знак «120», он символизирует данный теоретический предел).

    Почему результат именно такой


    Будучи человеком вооружённым (не зря же я всех полгода пытал статьями про изготовление анализатора), я взял и рассмотрел детали трафика. И вот что у меня получилось (данные я чуть обрезал, там реально всегда уходит по 64 байта):



    То же самое текстом.
    *** +4 us
    OUT Addr: 16 (0x10), EP: 1
    E1 90 40 
    
    *** +3 us
    DATA0
    C3 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD …
    
    *** +45 us
    ACK
    D2 
    
    *** +4 us
    OUT Addr: 16 (0x10), EP: 1
    E1 90 40 
    
    *** +3 us
    DATA1
    4B 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD …
    
    *** +45 us
    NAK
    5A 
    
    *** +4 us
    OUT Addr: 16 (0x10), EP: 1
    E1 90 40 
    
    *** +3 us
    DATA1
    4B 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD…
    
    *** +45 us
    ACK
    D2 
    
    *** +5 us
    OUT Addr: 16 (0x10), EP: 1
    E1 90 40 
    
    *** +3 us
    DATA0
    C3 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD…
    
    *** +45 us
    NAK
    5A
    


    И так до бесконечности на всех участках, что я смог осмотреть глазами, возвращается то ACK, то NAK. Причём в режиме FS каждая пачка данных передаётся целиком. Принялась она или нет, а всё равно передаётся целиком. Хорошо, что это не роняет всю шину USB, так как до последнего хаба данные бегут на скорости HS в виде SPLIT транзакции. А дальше – уже хаб мелко шинкует её на пакеты по 64 байта и пытается отослать на скорости FS.

    Подобные чередования ACK-NAK я нашёл и в сети, когда пытался проверить, у одного ли меня такой результат. Нет, не у одного.

    Причины падения скорости, честно говоря, написаны прямо под носом. А именно – в комментариях к функции, где нам следует вставлять обработку данных. Вот их текст:



    Нам честно говорят, что пока мы находимся в этой функции, будет посылаться NAK. Вышли из неё – снова начинается работа шины.

    А почему при блоке 64 байта скорость выше? Я долго думал, и нашёл следующее объяснение: 64 байта – это тот размер блока, после которого при работе с FS-устройствами через HS-хабы начинается использование SPLIT-транзакций. Что это такое – можно посмотреть в стандарте, там этому посвящён не один десяток страниц. Но если коротко: до хаба запрос идёт на скорости HS, а уже хаб обеспечивает снижение скорости и нарезание данных на FS-блоки. Основная шина при этом не тормозит.

    Выше мы видели, что уже через 5 микросекунд после прихода примитива ACK, пошёл следующий пакет, который не был обработан контроллером. А что будет, если мы будем работать блоками по 64 байта? Я начну с примитива SOF.

    Смотреть код.
    *** +1000 us
    SOF 42.0 (0x2a)
    A5 2A 50 
    
    *** +3 us
    OUT Addr: 29 (0x1d), EP: 1
    E1 9D F0 
    
    *** +3 us
    DATA0
    C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00… 
    
    *** +45 us
    ACK
    D2 
    
    *** +37 us
    OUT Addr: 29 (0x1d), EP: 1
    E1 9D F0 
    
    *** +3 us
    DATA1
    4B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00…
    
    *** +45 us
    ACK
    D2 
    
    *** +43 us
    OUT Addr: 29 (0x1d), EP: 1
    E1 9D F0 
    
    *** +3 us
    DATA0
    C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00…
    
    *** +45 us
    ACK
    D2
    


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

    По этой же причине, если мы воткнём между материнской платой и устройством дешёвый USB2-хаб, марку которого я не скажу, так как сильно поругался с магазином, владеющим именем бренда, но судя по ID, чип там VID_05E3&PID_0608, то статистика окажется намного лучше, чем при прямом подключении к материнке:



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

    Пробуем двухбуферную систему


    Будучи опытным программистом для микроконтроллеров, я знаю, что обычно такая проблема решается путём применения двухбуферной схемы. Пока обрабатывается один буфер, данные передаются во второй. А какая схема используется здесь? Ответ на мой вопрос мы получим из следующего кода:

    USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev)
    {
    …
      /* USER CODE BEGIN EndPoint_Configuration */
      HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
      HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
      /* USER CODE END EndPoint_Configuration */
      /* USER CODE BEGIN EndPoint_Configuration_CDC */
      HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0xC0);
      HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0x110);
      HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x82 , PCD_SNG_BUF, 0x100);
    

    Тут, везде написано PCD_SNG_BUF. Вообще, в статье про DMA я уже рассуждал, что если разработчики этой библиотеки что-то не используют, значит и не стоит этого использовать. Но всё же, я попробовал заменить SNG_BUF на DBL_BUF. Результат остался прежним. Тогда я нашёл в сети следующее утверждение:

    The USB peripheral's HAL driver (PCD) has a known limitation: it directly maps EPnR register numbers with endpoint addresses. This works if all OUT and IN endpoints with the same number (e.g. 0x01 and 0x81) are the same type, and not double buffered. However, isochronous endpoints have to be double buffered, therefore you have to use an endpoint number that's unused in the other direction. E.g. in your case, set AUDIO_OUT_EP1 to 0x01, and AUDIO_IN_EP1 to 0x82. (Or you can drop this USB stack altogether as I did.)

    Попробовал разнести точки – результат тот же. В общем, анализ показал, что для точек типа BULK это если и возможно, то только путём переписывания MiddleWare. Короче, не зря там везде одиночные буферы выбраны. Разработчики знали, что делают.

    Тормоза в обработчике прерывания


    Теперь подойдём к проблеме с уже имеющимися данными, но с другой стороны. Выше в комментариях мы видели, что пока мы находимся в обработчике прерывания, система будет слать NAK. А как велик этот обработчик?

    Немного потрассируем код. Давайте я поставлю точку останова в функции CDC_Receive_FS, в которую мы попадаем ради каждого блока в 64 байта или меньше. Ну, потому что транзакции, больше чем на 64 байта, будут разрезаны на пакеты такого размера. Вот такой у нас получается стек возвратов в обработчик прерываний. А это ещё часть функций заинлайнилась!



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

    Но и это ещё не всё. В функции PCD_EP_ISR_Handler (кстати, в стеке возвратов её не видно, но она есть) мы изымаем данные из FIFO в буфер. В дальнейшем, если бы у нас был свой код, мы бы ещё раз скопировали данные из системного буфера к себе. Имейте в виду, что логичнее сразу задавать указатель на свой массив, двигаясь по нему, благо такая возможность имеется.

    Но и это ещё не всё! Ради всё тех же каждых 64 байт, мы вызываем функцию USBD_CDC_SetRxBuffer(), а затем — USBD_CDC_ReceivePacket(), которая также расслоится на кучку вызовов, каждый из которых всего лишь чуть-чуть перетасовывает данные. Оцените стек возвратов, когда мы находимся в самом глубоком слое!



    А если вы попробуете рассмотреть код функции USB_EPStartXfer(), то вам совсем станет грустно. А ведь всё это безобразие вызывается ради каждого блока в 64 байта. Вот так 64 байта пробежало – начинается чехарда с многократной (правда, в нашем случае с отсутствующим функционалом — однократной) пересылкой этих данных, а также с вызовом кучи слоёв. Некоторые из них тратят массу тактов процессора на свою важную работу, но некоторые – просто слои. Они просто зачем-то перекладывают байтики из структуры в структуру и передают управление дальше. А такты на это – всё равно расходуются!

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

    Проверяем теорию осциллографом


    Имея стек возвратов, а также известные времянки посылки транзакций, мы можем проверить всё с другой стороны, чтобы быть уверенными, что нигде не ошиблись. Давайте я на входе в прерывание буду взводить какой-либо порт, а на выходе – сбрасывать. Как всегда, я буду делать это через библиотеку mcucpp Константина Чижова.

    Объявим, скажем, ножку Pa0 для этой цели:

    #include <iopins.h>
    …
    typedef Mcucpp::IO::Pa0 oscOut;							//PC13
    

    За что люблю эту библиотеку, так за её простоту. Инициализация ножки выглядит так:

    	oscOut::ConfigPort::Enable();
    	oscOut::SetDirWrite();
    

    Включили тактирование аппаратных блоков, назначили ножку на выход. Собственно, всё.

    И добавим пару строк в функцию обработки прерывания (первая взведёт ножку в единицу, вторая – сбросит в ноль):



    То же самое текстом.
    void USB_LP_CAN1_RX0_IRQHandler(void)
    {
      /* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 0 */
    	oscOut::Set();
      /* USER CODE END USB_LP_CAN1_RX0_IRQn 0 */
      HAL_PCD_IRQHandler(&hpcd_USB_FS);
      /* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 1 */
    	oscOut::Clear();
      /* USER CODE END USB_LP_CAN1_RX0_IRQn 1 */
    }
    


    Типичный период следования прерываний – от 70 до 100 микросекунд. При этом в прерывании мы находимся чуть меньше чем 19 микросекунд. На первом рисунке показан случай, когда данные идут помедленнее (расстояния велики), на втором – побыстрее (расстояния меньше). Обе осциллограммы сняты при подключении через хаб.





    А вот – хорошая и не очень хорошая ситуации, снятые при прямом подключении к материнке. Собственно, на плохом варианте видно, что расстояния удваиваются. Один из пакетов уходит с NAKом, в прерывание мы не попадаем.





    Осталось понять, как эти прерывания располагаются относительно USB-примитивов. В этом нам поможет Reference Manual. Момент прихода прерывания я выделил.



    Вот теперь всё сходится. После формирования ACKа мы не готовы принимать новые пакеты на протяжении 18 микросекунд. Когда они приходят через 3-5 микросекунд (а именно это мы видим в текстовом логе анализатора выше для плохого случая), контроллер их просто игнорирует, посылая NAK. Когда через 30-40 (что мы наблюдаем в текстовом логе для случая хорошего, хотя, точно подойдёт любое значение, больше чем 19) – обрабатывает.

    Плюс из текстовых логов мы видим, что влево от прерывания около пятидесяти микросекунд занимает сама OUT-транзакция на аппаратном уровне. Кстати. У нас же скорость FS. Такие сигналы можно ловить обычным осциллографом. Давайте я добавлю активность на шине USB в виде голубого луча. Что получим?

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



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




    Причём 64 байта, с учётом, что я передаю все нули, а значит там может быть вставленный бит – это примерно 576 бит. При частоте 12 МГц их передача займёт 48 миллисекунд. То есть, когда между пакетами примерно по 50 миллисекунд, мы имеем дело с пределом скорости. Тут даже NAKов нет.




    NAKи – вот они, когда хост пытается что-то передавать, пока мы заняты, что совпадает с выводами, сделанными при анализе текстовых логов. Вот тут прямо видны два пакета, первый из которых начался, когда аппаратура ещё не готова его принимать.




    Всё спасла бы двойная буферизация, но она для BULK случая, как я понял из описаний в сети, не работает, так как на аппаратном уровне заложено, что пока находимся в обработчике прерывания – надо слать NAK и всё тут. Только в логике изохронных транзакций этого нет. Поняв всё это, я сумел сформулировать правильный поисковый запрос и нашёл интересную китайскую статью.

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

    Неожиданное следствие из снятой осциллограммы


    И раз уж я снял осциллограммы, предлагаю посмотреть на процесс, когда пакеты идут по шине, с точки зрения не оптимизатора, а простого программиста. Вот осциллограмма. Единичное значение – контроллер находится в обработчике прерывания. Все остальные задачи в это время не могут работать. Каково?



    А при коротких пакетах, бывает и такое:



    Правда, это только во время передачи данных. Если передача не идёт, мы видим только SOFы (на предыдущей осциллограмме короткая иголка слева – это как раз SOF). Но когда идёт обмен данными, процентов 20 времени контроллер не готов нас обслуживать. Он находится в контексте прерывания. Так что если и обслужит, то только прерывания с бОльшим приоритетом.

    Выводы


    Ну что, пришла пора делать выводы. Как видим, контроллер STM32F103C8T6 не может выжать всю производительность даже из шины USB 2.0 FS.

    Хорошо это или плохо? Ни то, ни другое. Есть тысяча и одна задача, где не надо гнаться за производительностью USB, и этот копеечный контроллер прекрасно с ними справляется. Вот там его и надо использовать. (Дополнение: пока статья лежала «в столе», на Хабре появилась статья, что уже не копеечный. Цена у местных поставщиков, согласно той статье, выросла в 10 раз. Надеюсь, это временное явление.)

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

    А для производительных вещей я в ближайшее время собираюсь изучить контроллер, у которого USB обрабатывается по стандарту EHCI. Там всё на дескрипторах. Заполнил адрес, длину… Когда пришло прерывание – данные уже готовы… Надеюсь… Если будет что-то интересное – сделаю статью. А здесь сам подход к обработке приходящих данных (они помещаются в выделенную память, а затем – программно изымаются оттуда, причём в контексте прерывания) не даёт развить высоких скоростей. По крайней мере, на кристалле F103.

    Следующий вывод: добавленный в систему дешёвый USB-хаб даёт неожиданный прирост производительности. Это связано с тем, что он шлёт пакеты с паузами 20-25 микросекунд (в статье подтверждающий лог не приводится для экономии места, но его можно скачать здесь для самостоятельного изучения). Получаем грубо 20 микросекунд задержки, 50 микросекунд передачи. Итого 5/7 от полной производительности. Как раз 700-800 килобайт в секунду при теоретическом максимуме 1000-1100. Так что любой FS-контроллер, включённый через этот хаб, не сможет выдать больше.

    Дальше: видно, что, когда по USB передаются данные, контроллер довольно большой процент времени находится в обработчике прерывания USB. Это также надо иметь в виду, проектируя систему. Прерываниям UART, SPI и прочим, где данные ждать невозможно, а обработка будет быстрой, стоит задавать приоритет выше, чем у прерывания USB. Ну, или использовать DMA.

    И, наконец, мы выяснили, что для FS устройств на базе STM32 нет чёткого критерия оптимальной работы со стороны прикладной программы. Одна и та же система, в которую то добавляли, то исключали внешний USB-хаб, работала с максимальной производительностью либо при длине блока 64 байта (без хаба), либо более четырёх килобайт (с хабом). При разработке прикладных программ для PC, требующих высокой производительности, следует учитывать и этот аспект. Вплоть до калибровки параметров под конкретную конфигурацию оборудования.

    Дополнение: не только в Китае дорабатывают библиотеки


    В комментариях к статье пользователь EddyEm оставил суровый комментарий, что не надо пользоваться библиотеками Кубика. Ну, это я и в основном тексте говорил, но чем пользоваться? Есть у меня привычка, просматривать профили резких комментаторов. И вот, среди его высказываний под другими статьями я нашёл ссылку на такой проект: stm32samples/F1-nolib/CDC_ACM at master · eddyem/stm32samples · GitHub. Он как раз под пилюлю, и не использует сторонних библиотек. Чуть доработал, выкинув всю полезную работу (мои тесты же тоже просто гоняют пакеты, не пользуясь их содержимым), и получил такой результат:



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

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



    При прямой работе с разъёмом на материнской плате, получаем такую красоту:



    Собственно, мы видим, что полоса используется полностью.

    Затем пользователь COKPOWEHEU прислал ссылку на свою библиотеку usb/5.CDC_L1 at main · COKPOWEHEU/usb · GitHub. Правда, она для L1, так что её в прямом виде быстро проверить не удастся.

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

    Послесловие. А что там в режиме HS?


    Ради интереса приведу результаты, снятые для HS-устройства на базе ПЛИС. Второй столбец – скорость при прямом подключении к материнской плате, третий – через тот самый дешёвый хаб. Мне уже интересно, почему там хаб даёт такую просадку, но с этим я разберусь как-нибудь в другой раз.



    А пока статья лежала «в столе», я взял типовой пример CDC-прошивки от NXP, немного доработал его (без доработки он «зависнет» при односторонней передаче), залил в плату Teensy 4.1 и снял метрики там. У него контроллер EHCI и скорость HS.



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

    usb_status_t USB_DeviceCdcAcmRecv(class_handle_t handle, uint8_t ep, uint8_t *buffer, uint32_t length)
    {
    …
        if (1U == cdcAcmHandle->bulkOut.isBusy)
        {
            return kStatus_USB_Busy;
        }
        cdcAcmHandle->bulkOut.isBusy = 1U;
    …
    

    Правда, там, за счёт резервов аппаратуры, удалось с минимальной доработкой улучшить графики до таких:



    Но это уже тема для другой статьи.
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 45

      +2
      Где-то после размера блока 4 килобайта, скорость упирается в 5.6 килобайт в секунду. Давайте я грубо умножу это на 8. Получаю условные 4.5 мегабита в секунду.

      5.6КиБ * 8 = 44,5Киб. Кибибайты при умножении должны превратиться в кибибиты. 44,5 кибибита это примерно 0,0434 мибибита. Понятно, то там NRZI да ещё и приправлен битстаффингом, но не на три порядка же.
        +1
        Спасибо! Действительно что-то я неверно перевёл байты в таблице в килобайты в тексте. 56X_XXX байт. Примерно 560 килобайт в секунду, конечно же.

        560 * 8 = 4 480 килобит, грубо 4.5 мегабита.

        Сейчас поправлю текст. Ещё раз спасибо.
          0
          А, если их сотни, тогда да, всё нормально. Не за что.
          PS
          560 * 8 = 4 480 килобит, грубо 4.5 мегабита.

          А если точно: 4480 кибибита / 1024 = 4,375 мибибита.
            0
            Когда ориентируемся на частоту генератора — я уже привык на реальные тысячи умножать. Потому что там все эти биты в секунду от кварца пляшут. Так что все программистские вычисления тут только всё запутают.
        +1

        Было бы интересно сравнить с ChibiOS, так как там свой USB-стек, не завязаный на кривоубогенький STM-овский HAL.

          0
          Может когда-нибудь руки и дойдут… Или кто ещё сделает это. А я сейчас весь в играх с Teensy 4.1. Последние графики сняты на ней. Скоро планирую результаты своих осмотров той платы опубликовать. Спасибо Заказчику Тинсевого проекта, разрешил все находки в публикацию пускать.
          –3
          Статья ни о чем!
          ТС наваял USB на основе калокуба и пытается доказать, что в проблемах виновато железо, а не рукожопые разработчики калокуба!
          Реальные тесты нужно проводить: на «голом» CMSIS со своей реализацией USB. Тогда и всплывут реальные ограничения железа.
            0
            Сложный вопрос. Давным-давно, лет 5 назад, играл я в CMSIS RTOS. В те времена её вызовы проваливались в ту же самую MiddleWare, что и у Кубика используется. Дрйверы для ОС же поставляли те же самые STшники. Возможно, с тех пор что-то изменилось, но мне это было уже не интересно.

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

            В общем, единственные, кто целенаправленно давил эту проблему — те самые китайские товарищи. Но тот самый USB 2.0 хаб испортил бы результаты и им… И это в статье доказано.
              0
              Речь за предыдущий StdPeriph супротив новомодного HAL?
              0
              Порылся по Вашему профилю. Нашёл Вашу USB библиотеку на Гитхабе. Выглядит забавно. Настолько забавно (в хорошем смысле этого слова), чтобы показать всем. Будем измеряяяять. По результатам — отпишусь.
                +1
                В общем, всё здорово!

                Это было (для меня) нелегко, но я справился со сборкой Вашего проекта /F1-nolib/CDC_ACM под Windows. Результат при прямом подключении к материнке — просто замечательный. Через хаб — предсказуемо такой же, какой был.

                image

                Сделаю несколько замеров — дополню статью ссылкой на российского автора полезной библиотеки в Вашем лице…
                  +1
                  Можете заодно и мою проверить: github.com/COKPOWEHEU/usb/tree/main/5.CDC_L1 (для F1 надо библиотеку из Core_F1 скопировать) если желание будет. Тоже на регистрах, без Куба. Ну и двойная буферизация имеется.
                    +1
                    Огромное спасибо!
                    Больше библиотек, хороших и разных!
                0

                Это было давно, я тестировал на том же камне f103 без cdc, в bulk режиме. И если мне не изменяет память я получал скорость 1МБ/сек. Использовал этот пример

                https://github.com/geoffreymbrown/stm32usb-bulk

                  0
                  В readme сказано, что это для F3. ld файл тоже для большой памяти, в makefile всё тоже для f3.
                    0

                    Ну понятно что написано. Но я портировал. На hal это по-моему вообще ни чего делать не пришлось

                  0
                  Хм. Признаюсь, не поверил статье. взял голубую таблетку из запасов, сделал проект (stm32cubeide под linux) — CDC com-порт, запустил копирование туда данных — и таки получил 12 Mbps (ну точнее 11.8), но правда после копирования туда более 100 Мбайт… Воля ваша, что-то у вас не то — Cube генерит нормальный код. Может, у вас измерение не так происходит?

                  И вот еще что — вначале скорость была 400-500 кбайт (ну то есть 2 Mbps), но постепенно (!) разогналась.

                  И да, если хотите чтобы компилятор делал самый лучший код — ставьте просто оптимизация на размер…
                    0
                    Оптимизация на размер именно в Кейле даёт массу CALLов вместо автоматических инлайнов. Так что она точно на скорость в лучшую сторону не может повлиять.

                    Что до постепенного разгона — вопрос интересный. По крайней мере, я замерял для нескольких мегабайт.
                      0
                      обычно я инлайны не использую… Но если хотите, можно поставить нужные флаги. Проверено практикой — код полученный -Os работает на 10-15% быстрее и сам по себе компактнее (практически не поддается сжатию), наверно из-за того и быстрее.

                      Замерьте после 50 Мбайт — думаю, сможете разогнать.
                        0
                        Кстати, а измеряет Ваша программа или чужая? Просто подумалось, а не вещественные ли числа там делятся? Если они, то при росте порядка, идёт загрубление, что может приводить к интересным результатам.

                        А так — выше я в комментариях отписался (и новый раздел ближе к концу текста вставил), как удалось разогнать на любых объёмах. Тот же метод, что и у китайцев, но не абстрактное «Мы сделали», а скачана конкретная библиотека от отечественного автора. И получившиеся осциллограммы соответствуют предсказанным. Ну, и с USB 2.0 хабом, предсказуемо ничего не улучшилось.
                      0

                      Интересно, как вы это сделали, если стандарт USB 2.0 приводит максимальную пропускную способность для Bulk и 64-байтных пакетов равную 1216000, что есть 9.728 Mbit/s. Что-то тут не сходится...

                      0
                      Классная статья, хотелось бы узнать что там у вас за контроллер с EHCI, и вообще из дешевых вариантов контроллеров какой выбрать для FS USB?
                        0
                        Ну, дешёвый контроллер с FS — это всё тот же F103. Он замечательный по большинству параметров. А дикая скорость нужна не всегда. Чуть выше я в комментарии привёл график, полученный с использованием библиотеки EddyEm — с нею можно получить полноценную скорость. Я верил, что в комментариях мне подкинут идею (правда, для этого пришлось пошерстить его комментарии к чужим статьям, чтобы найти ссылку на эту библиотеку). Чуть позже дополню статью ссылкой на эту библиотеку.

                        EHCI — вот эта плата, процессор NXP IMXRT1062. Скоро выложу несколько статей со своими первыми впечатлениями о работе с нею. Но по цене это совсем разные весовые категории… И отладочного порта на этой плате нет. А так — забавная штучка.
                          0

                          Он замечательный по большинству параметров

                          Особенно по цене сейчас)

                            0
                            1) HAL зло. Там тьма кода для покрытия всех возможных вариантов аргументов. Куча данных перекладывается с места на место (из структуры в структуру). inline помогает очень слабо.
                            2) HAL зло, написанное криворукими программистами, которые даже не пытаются сделать совместимость чипов на уровне софта, хотя на уровне железа STM такую совместимость декларирует (можно заменить один контроллер на более мощный той-же распиновки).
                            Возьмите 2 HAL разных контроллеров, совместимых по выводам и периферии — ВСЕ переименовано. Чтобы сменить контроллер, приходиться тупо проходить по коду и менять имена констант.
                            3) HAL зло, написанное криворукими программистами, которые получают оплату за кол-во изменившегося кода от версии к версии. и STM на это пофиг.
                            Берем 2 HAL разных версий: и видим например в коде настройки пинов замену for (int i...) цикл на int i =0; while (i<)… цикл.
                            Итог: совместимости нет. Оптимизации нет. Святая вера в оптимизации компилятора это бред. Если Вам нужна производительность, надо писать короткий и ПРАВИЛЬНЫЙ код, который будет оптимизироваться. А надеяться что компилятор заинлайнит стек из 30 вызовов — это бред.
                            П.С. Прошу прощения. Накипело. STM делает прекрасные контроллеры и полное Г. в библиотеках. Может соберемся и сделаем open-source CMSIS+HAL спроектированное и реализованное прямыми руками?
                              +1
                              STM делает прекрасные контроллеры

                              Не обольщайтесь. Скажем, стек USB там проектировали те еще наркоманы:
                              1. Регистр USB_EPxR: часть битов доступна только на чтение (это нормально). Часть — на чтение и запись (тоже пока нормально). Часть игнорирует запись единицы с сбрасывается записью нуля. А часть игнорирует ноль и перебрасывает значение при записи единицы.
                              2. Размер буфера под конечные точки нигде в инклюдах не прописан — только если догадаться, что он не часть оперативки и залезть в memory map (ссылок туда из соответствующего раздела тоже нет)
                              3. Организация буфера конечных точек: 256 штук 16-битных слов с 32-битной адресацией. Хочешь копировать побайтно — шиш тебе, перекодируй на лету.
                              4. Двойная буферизация. Она как бы есть — в изохронных и bulk точках. При этом переключение буферов на передачу осуществляется битом DTOG_RX. Буферов на передачу битом на чтение!
                                0
                                ИМХО В сравнении с ценой/возможностями альтернативы нет. STM покрывает 90% требований. Маленькие. Быстрые. Мощные. Дешевые. Куча периферии.
                                  0
                                  Возможно. Но и косяков хватает. Ну и не очень очевидно что с ними в ближайшем будущем случится.
                                    0
                                    В своё время, конкретно F103C8T6 на сайте обещали не снимать с производства в ближайшие 10 лет. Так что если сама фирма не развалится, то должно быть всё хорошо. Будем хотя бы на это надеяться.

                                    Но как человек, живший при дефиците, я несколько десятков Пилюль купил давно. Для дома, для семьи… Уж больно они замечательные.
                                      0
                                      А вот тут можно чуть подробнее? Я видимо эту новость пропустил. Спасибо.
                                        0
                                        Если вы про «неизвестно что будет», то я всего лишь про бардак с ценами. Никакой секретной информации у меня нет. А что до косяков, так в F103-их RTС ущербный, порты криво настраиваются, про USB отдельную нецензурную песню складывать надо
                                          +1
                                          Да, но остальные или хуже, или дороже… Понятно, что они не вечны… Но вот прямо сейчас, если бы не дефицит на фоне пандемии, они популярны. А лет 10 назад я Мегу восьмую любил. Причём писал на ассемблере для неё.

                                          У NXP библиотеки лучше сделаны. Да и сами они неплохи. Но ценаааааа… Я просто сейчас Teensy 4.1 по проектной необходимости изучаю. Но когда надо круть — можно и переплатить. Когда не надо — зачем переплачивать?
                                            0
                                            Да, ATmega8 гениальный контроллер. Небольшой корпус, в котором все нужное есть.
                                            Что до f103 — может пора уже искать им замену? Правда, я тоже не знаю на что.
                                          0
                                          Вы про 10 лет? Вот ссылка

                                          Слева сверху зелёненькая надпись Active. Чуть правее от неё монетка десятикопеечная. Наведите на неё курсор…
                                            0
                                            Спасибо. Я пока не вижу проблемы. В программе пролонгации на 10 лет 100500 контроллеров. Что не так?
                                              0
                                              Не так совсем другое. Вот это не так. Остальное — просто замечательно. И эти 10 копеек наоборот внушают оптимизм. что всё наладится.
                                                0
                                                Понял. Принял. Спасибо.
                                      0
                                      1. Все эти режимы сделаны намеренно и нужны в разных ситуациях.
                                      2. Так он не фиксирован. Догадываться не надо, в Reference Manual написано про эту память.
                                      3. Не очень удобно, но исправлено в более новых контроллерах. Впрочем, DMA должен уметь делать такую конвертацию.
                                      4. Только DTOG_RX для такого endpoint называется SW_BUF, если Reference Manual почитать. А в остальном — почему бы и нет? Поскольку такой endpoint не может одновременно работать на чтение и на запись, часть битов не имеет смысла и может в теории использоваться как угодно.
                                        0
                                        Все эти режимы сделаны намеренно и нужны в разных ситуациях.
                                        Что им мешало сделать инверсию не по 1, а по 0 или наоборот чтобы запись 1 сбрасывала другой бит? Претензия-то не к тому, что разные способы доступа, а что маски кривые накладывать приходится
                                        Так он не фиксирован. Догадываться не надо, в Reference Manual написано про эту память.
                                        Написано. Вот только ссылки туда из соответствующего раздела нет. И в инклюдниках ее размер не задефайнен.
                                        Не очень удобно, но исправлено в более новых контроллерах. Впрочем, DMA должен уметь делать такую конвертацию.
                                        DMA умеет. Про новые контроллеры ничего сказать не могу.
                                        Только DTOG_RX для такого endpoint называется SW_BUF, если Reference Manual почитать.
                                        Так кто ж спорит что они это задокументировали. Но зачем было заставлять дергать бит приема при передаче? Если уж «часть битов можно использовать как угодно», так и привязали бы бит передачи к передаче.
                                      +1
                                      Для всего, кроме USB Middleware я использую mcucpp Константина Чижова. Она довольно здорово оптимизирует код, а также делает простой работу со сложными блоками (я использовал GPIO, таймеры, в том числе, в режиме ШИМ, UART, SPI). При этом ноги перекидывать при переразводке платы — сплошное удовольствие.

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

                                      USB — приходилось пользоваться тем, что есть… Но буду изучать библиотеки, с которыми познакомился сегодня. Сейчас вставлю дополнение в текст…
                                        +1
                                        А что мешает опубликовать статьи? Вы же не пишете что это ваша разработка…
                                        А то по вашей логике получается, что мы должны писать статьи только про свой код. Про linux/windows/appache/e.t.c. низя… ШТА? О_О А как учиться?
                                          0
                                          делает простой работу со сложными блоками (я использовал GPIO
                                          GPIO как раз штука простая, не знаю зачем в том же HAL или Arduino ее настолько изуродовали. К примеру, в моем варианте делается примерно так:
                                          #define LED_RED B, 10, GPIO_PP_VS
                                          GPIO_config( LED_RED );
                                          GPO_ON( LED_RED );
                                          GPO_OFF( LED_RED );
                                            0
                                            Как это делается в mcucpp — есть в тексте статьи.

                                            Где-то объявляем
                                            typedef Mcucpp::IO::Pa0 oscOut;	

                                            Потом можно всегда поименять на другое

                                            Затем — где надо, инициализируем
                                            	oscOut::ConfigPort::Enable();
                                            	oscOut::SetDirWrite();
                                            

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

                                            Ну, и работаем
                                            oscOut::Set();
                                            oscOut::Clear();
                                            

                                            Обратите внимание, что никаких переменных не заводится. Там работа через статические функции идёт. Как макросы, только легко читаются реализующие функции, и всё строго типизировано. И при вводе текста часто подсказки IntelliSense хорошие выдаёт, не надо всё помнить.

                                            Оптимизируются так, что руками на ассемблере лучше не написать. Я проверял.
                                              0
                                              Это что, С++ с его нечитаемыми шаблонами? Уж лучше макросы.
                                                0
                                                Вот конкретно у товарища Чижова всё идеально читается. Намного легче, чем макросы. Везде надо знать меру. И он её знает.

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

                                      Самое читаемое