Управление RGB светодиодами через блок UDB микроконтроллеров PSoC фирмы Cypress



    Введение


    Я давно хотел изучить методику программирования блоков UDB в контроллерах PSoC фирмы Cypress, но всё руки как-то не доходили. И вот, возникла задачка, на которой это можно было сделать. Разбираясь с материалами из сети, я понял, что практические рекомендации по работе с UDB ограничиваются теми или иными вариациями счётчиков и ШИМов. Все авторы почему-то делают свои вариации этих двух канонических примеров, поэтому описание чего-то иного вполне может быть интересно читателям.

    Итак. Возникла задачка динамически управлять длинной линейкой из RGB светодиодов WS2812B. Классические подходы к этому делу известны. Можно взять банальную Arduino, но там вывод идёт программно, поэтому пока данные выводятся — всё остальное простаивает, иначе собьются временные диаграммы. Можно взять STM32 и выводить данные либо через DMA в ШИМ, либо через DMA в SPI. Методики известны. Я даже, в своё время, лично через SPI уже линейкой из шестнадцати диодов управлял. Но накладные расходы велики. Один бит данных в светодиодах занимает 8 бит в памяти для случая с ШИМ и от 3 до 4 бит (зависит от крутости PLL в контроллере) для SPI. Пока светодиодов мало, это не страшно, но если их, скажем, пара сотен, то 200 * 24 = 4800 бит = 600 байт полезных данных должны физически храниться в буфере, объёмом более 4 килобайт для ШИМ-варианта или более 2 килобайт для SPI-варианта. Для динамической индикации буферов должно быть несколько, а у STM32F103 ОЗУ на всё про всё 20 килобайт. Не то, чтобы мы упёрлись в нереализуемую задачу, но повод для проверки, можно ли это реализовать на PSoC без необходимости расходования лишнего ОЗУ, вполне весомый.

    Ссылки на теорию


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

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

    Примерно то же самое, но нарезанное на мелкие куски, можно посмотреть здесь . У меня видео не воспроизводилось, но его можно скачать и посмотреть локально. Среди прочего, там имеется и канонический пример реализации ШИМ.

    Поиск готовых решений


    Чтобы не изобретать велосипед (а наоборот – изучать методику на чужом опыте), я порылся по сети в поисках готовых решений для управления RGB светодиодами. Самое популярное решение — StripLightLib.cylib. Но у него уже много лет в планах числится «Добавить поддержку DMA». А хочется испытать именно решение, никак не зависящее от центрального процессора. Хочется запустить процесс и забыть о нём, сосредоточившись на подготовке следующего кадра.

    Решение, соответствующее моим желаниям, нашлось по адресу https://github.com/PolyVinalDistillate/PSoC_DMA_NeoPixel.

    Там всё реализовано на UDB (а ведь светодиоды – это всего лишь повод, цель – изучить UDB). Там есть поддержка DMA. И проект там явно красиво организован.

    Проблемы выбранного за основу решения


    Как устроена «прошивка» в проекте PSoC_DMA_NeoPixel, желающие могут посмотреть после прочтения статьи. Это позволит закрепить материал. Пока лишь я скажу, что сначала упростил логику оригинальной микропрограммы без уменьшения потребляемых ресурсов (зато её стало проще понимать). Затем начал экспериментировать с заменой логики автомата, что сулило выигрыш в ресурсах, но нарвался на серьёзную проблему. Так и этак решал — не устраняется она! И стали терзать меня смутные сомнения, нет ли той же проблемы у английского автора? Его демка очень красиво мигает светодиодами. Но что, если мы заменим красивое заполнение на «все единицы» и проконтролируем вывод не глазами, а осциллографом?
    Вот так, максимально грубо (можно даже сказать «брутально») формируем данные:

            memset (pPixelArray,0xff,sizeof(pPixelArray));
            //Call NeoPixel update function (non blocking) to trigger DMA pixel update
            NP_Update();

    И вот такую картинку наблюдаем на осциллографе:



    У первого бита ширина отличается от всех остальных. Я просил посылать все единицы, а уходят не все. Среди них затесался ноль! Меняем развёртку:



    Ширина отличается у каждого восьмого бита.

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

    Практическая часть


    Теперь начинаем практиковаться. Прощупаем основные аспекты разработки микропрограмм для UDB. Рассмотрим взаимосвязи и основные приёмы. Для этого открываем мой вариант проекта. В левом блоке хранится информация о рабочих файлах. По умолчанию открыта вкладка Source. Главный исходник проекта — файл main.c. Собственно, в группе Source Files других рабочих файлов и нет.



    Группа Generated Source содержит библиотечные функции. Их лучше не править. После каждого изменения «прошивки» UDB эта группа будет генерироваться заново. Итак, где в этой идиллии размещено описание кода для UDB? Чтобы его увидеть, надо переключиться на вкладку Components:



    Автор оригинального проекта сделал двухуровневый набор компонентов. На верхнем уровне лежит схема NeoPixel_v1_2.cysch. Это видно из основной схемы:



    Компонент выглядит следующим образом:



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



    «Прошивка» UDB


    Открываем тот компонент (файл с расширением .cyudb). Открывшийся рисунок просто огромен. Начинаем разбираться, что там к чему.



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

    1. Стартовая часть (всегда 1)
    2. Часть с данными
    3. Стоповая часть (всегда 0)

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

    Состояние покоя (Idle). В нём автомат пребывает, пока в FIFO не пришли новые данные.



    Из учебных видео мне было не совсем понятно, как состояния автомата связаны с АЛУ. Авторы пользуются связью, как чем-то само собой разумеющимся, но я, как новичок, не сразу смог её разглядеть. Давайте сразу разберёмся детально. На рисунке выше видно, что состояние Idle кодируется значением 1'b0. Правильнее будет 3'b000, но редактор всё равно всё переделает. Входы блока Datapath описываются вот так:



    Если по ним дважды щёлкнуть, то появится более детальный вариант:



    Это значит, что нулевому биту адреса инструкции АЛУ соответствует нулевой бит переменной, задающей состояние автомата. Первому — первый, второму — второй. При желании, битам адреса инструкции АЛУ можно сопоставлять любые переменные и даже выражения (в оригинальном варианте второму биту адреса инструкции АЛУ сопоставлялось именно выражение, причём в текущей версии оно явно не используется, но как выносящий мозг пример — очень наглядно, потом можете глянуть).

    Итак. При текущей настройке входов, какой у автомата двоичный код состояния, такая инструкция АЛУ и используется. Когда мы находимся в состоянии Idle, имеющем код 000, используется нулевая инструкция. Вот она:



    Я уже умею по этой записи понимать, что это банальный NOP. Но вы можете дважды щёлкнуть по ней и прочесть полный вариант:



    Везде вписаны NOPы. Регистры ничем не заполняются.

    Теперь разберёмся, что это за загадочный флаг !NoData, заставляющий автомат покинуть состояние покоя. Это выход из блока Datapath. Всего можно описать до шести выходов. Просто Datapath может вырабатывать намного больше флагов, но трассировочных ресурсов на всех не хватит, поэтому надо выбрать, какие шесть (или менее) нам реально нужны. Вот список на рисунке:



    Если по нему дважды щёлкнуть — раскроются подробности:



    Вот так выглядит полный список флагов, которые можно было бы вывести:



    Выбрав требуемый флаг, следует присвоить ему имя. С этого момента в системе имеется флаг. Как видно, флаг NoData — это имя для цепи F0 block status (empty). То есть признак, что во входном буфере нет данных. А !NoData, соответственно, его инверсия. Признак наличия данных. Как только данные попадут в FIFO (программно или при помощи DMA), флаг будет сброшен (а его инверсия взведена), и на следующем такте автомат выйдет из состояния покоя и перейдёт в состояние GetData.



    Как видим, из этого состояния автомат выйдет безусловно, пробыв в нём ровно один такт. На графе переходов для этого состояния не обозначено никаких действий. Но всегда надо смотреть, что при этом сделает АЛУ. Код состояния 1'b1, то есть, 3'b001. Смотрим соответствующий адрес в АЛУ:



    Что-то есть. Не имея опыта чтения того, что здесь написано, раскрываем, дважды щёлкнув по соответствующей ячейке:



    Отсюда следует, что само АЛУ по-прежнему не выполняет никаких действий. Но в регистр A0 будет помещено содержимое FIFO0, то есть, данные, поступающие от программы или блока DMA. Забегая вперёд, я скажу, что A0 используется как регистр сдвига, из которого байт будет выходить в последовательном виде. В регистр A1 будет помещено значение регистра D1. Вообще, все регистры D обычно заполняются программно до начала активной работы аппаратуры. Потом, при рассмотрении API, мы увидим, что в этот регистр кладётся число тактов автомата, задающее длительность трети бита. Итак. В A0 попало сдвигаемое значение, а в A1 — значение длительности стартовой части бита. И на следующем такте автомат безусловно перейдёт в состояние Constant1.



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



    А вот так — ноль:



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

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



    И под состоянием на графе имеется такой текст:



    Не пугайтесь символа «Равно». Это особенности редактора. В результирующем Verilog коде (автоматически созданным этой же системой) будет стрелка:

    Constant1 : 
            begin
                CurrentBit <= (1);
                if (( CycleTimeout ) == 1'b1)
                begin
                    MainState <= Setup1 ;
                end
            end

    Значение, защёлкнутое в этом триггере, является выходным сигналом всего нашего блока:



    То есть, когда автомат вошёл в состояние Constant1, на выход разрабатываемого нами блока попадёт единица. Теперь смотрим, как запрограммировано АЛУ для адреса 3'b010:



    Раскрываем этот элемент:



    Из регистра A1 вычитается единица. Выходное значение АЛУ попадает в регистр A1. Выше мы рассматривали, что A1 — это счётчик тактовых импульсов, используемый для задания длительности выходного импульса. Напомню, что он загрузился из D1 на прошлом шаге.
    Какое условие выхода из состояния? CycleTimeOut. Оно описано среди выходов следующим образом:



    Итак, сводим логику воедино. В прошлом состоянии в регистр A1 попало содержимое заранее заполненного программой регистра D1. На этом шаге автомат переводит триггер CurrentBit в единицу, а в АЛУ регистр A1 уменьшается на каждом такте. Когда A1 станет равен нулю, будет автоматически взведён флаг, которому автор дал имя CycleTimeout, в результате чего автомат перейдёт в состояние Setup1.

    Состояние Setup1 готовит данные для передачи полезного импульса.



    Смотрим на инструкцию АЛУ по адресу 3'b011. Я сразу раскрою её:



    Казалось бы, у АЛУ нет никаких действий. Операция же NOP. И выход АЛУ никуда не попадает. Но это не так. Чрезвычайно важным действием является сдвиг данных в АЛУ. Дело в том, что бит переноса среди выходов связан с нашей цепью ShiftOut:



    И в результате этой операции сдвига само сдвинутое значение никуда не попадёт, но цепь ShiftOut примет значение старшего бита регистра A0. То есть, тех данных, которые следует передавать. Под состоянием графа видно, что это значение, вышедшее из АЛУ в цепь ShiftOut, будет защёлкнуто в триггер CurrentBit. Давайте я ещё раз покажу рисунок, чтобы не отматывать статью вверх:



    Начинается передача второй части бита – непосредственного значения 0 или 1.

    Возвращаемся к инструкции для АЛУ. Кроме того, что уже сказано, там видно, что попутно в регистр A1 снова будет положено содержимое регистра D1, чтобы снова можно было отмерять длительность второй трети импульса.

    Состояние DataStage очень похоже на состояние Constant1. Автомат просто вычитает единицу из A1 и выходит в следующее состояние по достижении нуля. Давайте я даже покажу это вот так:



    и вот так:



    Затем идёт состояние Setup2, суть которого мы уже тоже знаем.



    В этом состоянии триггер CurrentBit сбрасывается в ноль (так как будет передаваться третья треть импульса, стоповая часть, а она всегда нулевая). АЛУ же загружает содержимое D1 в A1. Можно даже намётанным глазом увидеть это в краткой записи:



    Состояние Constant0 полностью идентично состояниям Constant1 и DataStage. Вычитаем единицу из A1. Когда значение достигло нуля, выходим в состояние ShiftData:





    Состояние ShiftData более сложное. В соответствующей инструкции для АЛУ выполняются следующие действия:



    Регистр A0 сдвигается на 1 бит, и результаты помещаются назад в A0. В A1 же снова кладётся содержимое D1, чтобы начать отмерять стартовую треть для следующего бита данных.

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



    Если передан не последний бит (о том, как формируется этот флаг, чуть ниже), то передаём единицу для следующего бита текущего байта.

    Если передан последний бит и в FIFO уже нет данных, идём в состояние покоя.

    Наконец, если передан последний бит, но в FIFO имеются данные, идём на выборку и передачу очередного байта.

    Теперь о счётчике битов. В АЛУ есть только два аккумулятора: A0 и A1. Они уже заняты под регистр сдвига и счётчик задержки, соответственно. Поэтому счётчик битов используется внешний.



    Дважды щёлкнем по нему:



    Значение при загрузке равно шести. Загружается оно по флагу LoadCounter, описанному в секции переменных:



    То есть, когда берётся очередной байт данных, попутно загружается эта константа.

    Когда автомат попадает в состояние ShiftData, счётчик уменьшает значение. При достижении нулевого значения, взводится выход TerminalCount, подключённый к цепи нашей семы FinalBit. Именно эта цепь задаёт, пойдёт автомат передавать следующий бит текущего байта или передавать новый байт (ну, или ждать новую пачку данных).

    Собственно, из логики — всё. Как формируется сигнал SpaceForData, задающий состояние выхода Hungry (информирующий блок DMA, что можно передавать очередные данные), читателям предлагается отследить самостоятельно.

    Программная поддержка


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



    С этого уровня идёт управление как библиотечным блоком DMA, так и всеми частями, входящими в UDB-шную часть. Для реализации API автор оригинала добавил заголовочный и программный файлы:



    Формат тела этих файлов навевает тоску. Всему виной любовь разработчиков PSoC Designer к «чистым Сям». Отсюда ужасные макросы и километровые имена. Классовая организация на C++ пришлась бы здесь как нельзя кстати. По крайней мере, мы это проверили при реализации своей ОСРВ МАКС: получилось красиво и удобно. Но здесь можно много рассуждать, а пользоваться придётся тем, что нам спущено сверху. Я только коротко покажу, как выглядит функция API, содержащая эти самые макросы:

    
    volatile void* `$INSTANCE_NAME`_Start(unsigned int nNumberOfNeopixels, void* pBuffer, double fSpeedMHz)
    {
        //work out cycles required at specified clock speed...
        `$INSTANCE_NAME`_g_pFrameBuffer = NULL;
        if((0.3/(1.0/(fSpeedMHz))) > 255) return NULL;
        
        unsigned char fCyclesOn = (unsigned char)(0.35/(1.0/(fSpeedMHz)));
        
        `$INSTANCE_NAME`_g_nFrameBufferSize = nNumberOfNeopixels*3;
        //Configure for 19.2 MHz operation
        `$INSTANCE_NAME`_Neo_BITCNT_Start();        //Counts bits in a byte
        //Sets bitrate frequency in number of clocks. Must be larger than largest of above two counter periods
        CY_SET_REG8(`$INSTANCE_NAME`_Neo_DPTH_D1_PTR, fCyclesOn+1);
        //Setup a DMA channel
        `$INSTANCE_NAME`_g_nDMA_Chan = `$INSTANCE_NAME`_DMA_DmaInitialize(`$INSTANCE_NAME`_DMA_BYTES_PER_BURST,
    
    `$INSTANCE_NAME`_DMA_REQUEST_PER_BURST, 
    
    HI16(`$INSTANCE_NAME`_DMA_SRC_BASE), 
    
    HI16(`$INSTANCE_NAME`_DMA_DST_BASE));
        
        if(pBuffer == NULL)
    ...

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

    После генерации кода (описанной ниже) этот файл будет храниться вот здесь:



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

    Инициализация затрагивает все части системы. Там присутствует инициализация семибитного счётчика, входящего в состав UDB-системы:

        NP_Neo_BITCNT_Start();        //Counts bits in a byte

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

    
        unsigned char fCyclesOn = (unsigned char)(0.35/(1.0/(fSpeedMHz)));
        CY_SET_REG8(NP_Neo_DPTH_D1_PTR, fCyclesOn+1);
    

    Настройка блока DMA занимает бОльшую часть этой функции. В качестве источника используется буфер, а в качестве приёмника — FIFO0 блока UDB (в километровой записи — NP_Neo_DPTH_F0_PTR). У автора часть этой настройки находилась в функции передачи данных. Но, на мой взгляд, делать все вычисления ради каждой передачи, слишком расточительно. Особенно, если учесть, что одно из действий внутри функции выглядит весьма и весьма объёмно.

    
        //work out cycles required at specified clock speed...
        NP_g_pFrameBuffer = NULL;
        
        NP_g_nFrameBufferSize = nNumberOfNeopixels*3;
    
        //Setup a DMA channel
        NP_g_nDMA_Chan = NP_DMA_DmaInitialize(NP_DMA_BYTES_PER_BURST, 
    NP_DMA_REQUEST_PER_BURST, HI16(NP_DMA_SRC_BASE), HI16(NP_DMA_DST_BASE));
    
    	...    
    
        NP_g_nDMA_TD = CyDmaTdAllocate();
        CyDmaTdSetConfiguration(NP_g_nDMA_TD, NP_g_nFrameBufferSize, CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT);
        CyDmaTdSetAddress(NP_g_nDMA_TD, LO16((uint32)NP_g_pFrameBuffer), LO16((uint32)NP_Neo_DPTH_F0_PTR));
        CyDmaChSetInitialTd(NP_g_nDMA_Chan, NP_g_nDMA_TD);
    

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

    
    void NP_Update()
    {
        if(NP_g_pFrameBuffer)
        {
            CyDmaChEnable(NP_g_nDMA_Chan, 1);
        }
    }
    

    Здесь явно не хватает функциональности для работы с несколькими буферами (чтобы обеспечивать двойную буферизацию), но в целом, обсуждение функциональности API выходит за рамки темы статьи. Сейчас главное — показать, как к разработанной микропрограммной части добавить программную поддержку. Теперь мы знаем, как это делается.

    Генерация проекта


    Итак, вся микропрограммная часть готова, API добавлено, что делать дальше? Выбирать пункт меню Build->Generate Application.



    Если всё пройдёт успешно, можно открыть вкладку Results и посмотреть файл с расширением rpt.



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





    Когда я сравниваю приведённые результаты с теми, которые были в оригинальном проекте, на душе становится теплее.

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



    Заключение


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

    На самом деле, это только часть найденной нестандартной информации. В частности, из большинства материалов может показаться, что UDB хорошо работает только с последовательными данными, но это не так. Найден Application Note, из которого кратко видно, как можно гонять и параллельные данные. Можно было бы рассмотреть конкретные примеры, базирующиеся на этих сведениях (правда, затмить FX2LP, другой контроллер от Cypress, не удастся: у PSoC скорость шины USB ниже).

    У меня в голове крутятся идеи, как решить давно мучающую меня проблему «прошивки» 3D-принтера. Там прерывания, обслуживающие шаговые двигатели, пожирают просто безумный процент процессорного времени. Вообще, про прерывания и процессорное время я много рассуждал в статье про ОСРВ МАКС. Есть прикидки, что для обслуживания шаговых двигателей можно вынести все времянки полностью на откуп UDB, оставив процессору чисто вычислительную задачу без опасения, что он не успеет это сделать в выделенный временной слот.

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

    Похожие публикации

    Комментарии 11
      +2

      Неплохо бы в начале статьи рассказать для непосвященных, что такое UDB.

        –1
        Это сделано в разделе «Ссылки на теорию»

        Просто статья получилась и так громоздкая. А рассказать, что такое UDB — займёт ещё столько же места. По первой ссылке из того раздела суть UDB подробно показывается.
          +1
          Не люблю видеотуториалы и не понимаю устный английский.
          Это разве не встроенный в микроконтроллер CPLD (блок программируемой логики)?
            0
            Блок UDB (Universal Digital Block) содержит:

            1) Две ПЛМ (PLD по-английски, но ПЛМ К556РТ1 и РТ2 даже наша промышленность выпускала)
            2) Регистры управления и статуса, либо реализуемый на тех же ресурсах семиразрядный счётчик
            3) Блок DataPath, состоящий из пары аккумуляторов, пары регистров данных, пары FIFO, простенького АЛУ и кучки компараторов.

            Подробнее — тут www.cypress.com/file/41531/download

            То есть, в PSoC нет CPLD. Там есть много PLD, но они образуют не классическую CPLD, а нечто особенное (набор UDB, в состав которых они входят).
              0
              Понял, спасибо :)
              И не так уж много места заняло краткое пояснение ;)
        0
        В чем причина такого звона на осциллограммах? Влияние щупов?
          0
          Особенности конструкции макетки. Макетка — CY8CKIT-059. У неё USB разъём без оплётки. Земля идёт по тоненькому проводочку. А крокодилом взять не за что — пришлось за корпус ЭВМ щуп землить. Со всеми вытекающими. Когда потом понадобилось снимать более скоростные осциллограммы (играл в управление шаговыми двигателями — проверял, вставляется ли такт при определённых условиях, будет интерес к теме — опишу результаты в отдельной статье) — чтобы уменьшить звон, подключил ещё кабель Micro USB в надежде, что через него контакт земли станет лучше. Не помогло. Оказалось. что у того разъёма оплётка не соединена с землёй тоже. В общем, земля у измеряемой макетки отвратительная. Но для данной задачи — суть и так видна.
          0
          Для STM32 кроме хранения изображения в готовом для вывода в интерфейс RGB-светодиодов виде, возможен и другой подход — хранение графических данных в традиционном компактном формате 8R-8G-8B или 5R-6G-5B, и преобразование их на лету, при выводе на светодиоды. DMA-контроллер STM32 в циклическом режиме может генерировать прерывания по окончанию передачи половины буфера, и всего буфера. В обработчике этих прерываний можно заполнять половину буфера DMA, преобразовывая компактный RGB в данные для выбранного периферийного устройства. В качестве такого устройства кроме таймера и SPI можно использовать и UART. Если, к примеру, выдавать данные в SPI, кодируя 1 бит светодиодов 4-мя битами SPI, и сделать буфер DMA на 64 cветодиода, то прерывания по выдаче половины буфера будут идти с интервалом около 1 мс. Если развернуть цикл перекодировки скажем 1 байта на STM32F103, работающем на 72МГц, — можно получить загрузку процессора около 5% во время выдачи кадра, а сама выдача кадра размером 200 пикселей с частотой 50Гц займёт лишь 29% времени, то есть средняя загрузка процессора этой задачей — около 2%. Размер буфера DMA при этом — 768 байт, независимо от размера кадра. Можно сделать буфер и меньше, тогда загрузка процессора немного вырастет.
            0
            В целом, тоже неплохо. Но я в тексте отметил. что «Не то, чтобы нерешаемая задача, просто хороший повод изучить UDB». А так — спасибо, запомнил. Хотя бы потому, что STM32F103 — дешевле и проще доставаем. Но использование UDB для некоторых других задач открывает новые горизонты, а на светодиодах просто удобно набивать руку.
            0
            Так а где то что показывает осциллограф для вашего варианта? Ожидал увидеть это в завершении статьи.
              +1
              Если бы я в статье улучшал временные характеристики — это было бы нужно. Но увы, я наоборот взял вариант с идеальными времянками и сделал их посредственными, но приемлемыми, о чём честно сообщил в тексте (зато устранил плюху и уменьшил потребляемые ресурсы, на чём и сосредоточился в рассказе). Так что считаю, что в статье им не место. Со всеми подробностями и пояснениями они ещё пару экранов займут, а текст и так огромный. Читатель просто бросит его.

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

              Вот я посылаю константу 0xF0 с широкой развёрткой



              На ней я курсорами намерял, что

              T1H = 760 нс при норме 750-1050
              T1L = 412 нс при норме 200-500

              T0H = 377 нс при норме 200-500
              T0L = 807 нс при норме 750-1050

              Вот другая развёртка, чтобы проверить, что все биты целы

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

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