Автоматизация аппаратного тестирования Embedded Систем

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

    Итак, что имеем:

    1. Десятки Embedded устройств в которых нужно проводить тестирование новой версии FirmWare (если быть точнее — ежедневная сборка прошивки)
    2. В виду особенностей процедуры загрузки FW может потребоваться необходимость сбросить питание (т.н. режим загрузки прошивки в режиме Power On Capture)
    3. Хотелось бы в некоторые конкретные моменты времени по ходу выполнения тестового скрипта имитировать нажатия на механические кнопки размещенные на отладочной плате Embededed системы

    Зачем может быть нужен пункт 3? В моем случае задача следующая — в случае возникновения критического Exception выполнения кода система «встает» в полумертвое состояние из которого она не выйдет пока ей не сбросят питание (еще одна причина для управления питанием). Но самое интересное в том что если в таком режиме нажать специально предназначенную для этого механическую кнопку — в последовательный порт будет выдан т.н. Exception лог — отладочная информация по которой можно будет попробовать понять в каком месте по коду произошло исключение.

    Вот именно по этой причине для эффективной автоматизации тестовой установки возникла необходимость иметь возможность ресетить питание устройства и имитировать нажатия механических кнопок на отладочной плате тем самым «спасая» очень важную информацию по Exception от скорой ее утери т.к. тестовый скрипт рано или поздно поняв что ответа от устройства нет попробует ее реанимировать сбросив питание.

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

    Анализ схемы отладочной платы показал что кнопка просто замыкает GPIO линию подтянутую к +3.3 В на GND. Значит если «подпаяться» к кнопке и с помощью реле замыкать GPIO линию на землю — то будет то что нужно.

    Далее встал вопрос выбора устройства / модуля для управления к которому выдвигались следующие требования:

    • Максимальное число реле (нужно по 2 шт на каждое устройство, а устройств десятки)
    • Доступ по Ethernet
    • Управление URL командами
    • Возможность копировать и масштабировать систему

    По традиции остановились на модуле Etherent реле Laurent-128:



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

    Схема подключения и связи отладочной платы тестируемого устройства и модуля управления показана на рисунке ниже:



    «Подпайка» к контактам механической кнопки на примере одного из тестируемых устройств выглядит так:



    Ура! Мехническая часть сделана. Осталось дело за малым — дописать код тестовых процедур так что бы в случае необходимости (загрузка образа прошивки после сброса питания или запись отладки по нажатию кнопки) подать команду на включение / выключение нужного реле.

    Синтаксис URL команд управления простой и очевидный. Например, для того чтобы включить реле под номер 4 нужно использовать следующий HTTP адреc:

    http://192.168.0.101/cmd.cgi?psw=Laurent&cmd=REL,4,1

    А вот и простая функция на Perl которая вызывается из главной тестовой процедуры если нужно «дернуть» реле:

    #---------------------------------------------------------------#
    # FUNCTION:: click rele
    # PARAM1: Laurent IP adress
    # PARAM2: RELE ID
    # PARAM3: 0 / 1  - what to do with rele
    #---------------------------------------------------------------#
    sub func_click_pwr_rele {
      my ( $_IN_IP, $_IN_RELE, $_IN_VALUE ) = @_;
      my $url;
    
      $url = "http://".$_IN_IP."/cmd.cgi?cmd=REL,".$_IN_RELE.",".$_IN_VALUE; 
      
      my $content = get $url;
      if( defined $content ) {
      }
      else {
        func_put_reslog( "ERROR! Can't manage RELE at adress $url", "BAD" );    
      }
    }
    

    Отмечу что система работает очень надежно без сбоев и зависаний. Laurent-128 и ранее использовавшиеся Laurent-112 (на 12 реле) отработали по паре лет без сбоев и остановок с учетом ежедневной работы.

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

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +2

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

        0

        То, чего все больше всего боялись — свершилось. Continuous Integration добралась до FW.
        Все таки ежедневные сборки прошивки — это пока еще дичайшая дичь. Чтобы полностью проверить прошивку, надо бы тестовый стенд по сложности в 2 раза сложнее, чем подопытное устройство.

          +1
          Чтобы полностью проверить прошивку, надо бы тестовый стенд по сложности в 2 раза сложнее, чем подопытное устройство.


          Ок. А разве CI на стенде запрещает обычную человеческую проверку?
          И даже стенд из ардуины помогает найти некоторые косяки.
            0
            Ну чего всего в два раза то? У нас например в стенде на пяток устройств — кнопки жимкают 16-канальные i2c PWM контроллеры, питаловом управляют они-же и USB хабы поддерживающие управление питанием портов, 7-ми-сегментники и светодиоды трекаются видеокамерой и все это счастье обмазано JS-фреймворком на промисах, разруливающий свистопляску номеров TTYUSB_N по USB-топологии, выставляющим примитивы похожие на selenium — waitForUARTtext, waitForPixel, powerCycle, buttonPress… Cбоку CI конвеер собирает из С кода + React-вебморды, после вебпака гзипуемого и транслируемого в C-литералы. Все это крееепко сложнее ESP8266+PIC прибора который тестируется.

            P.S. есть идеи выложить фремворк в паблик, но как-то не заморачивался думая что мало кто в эмбеде по CI упарывается
              0
              Вообще тоже интресно посмотреть технологию и фреймворк. Сами нечто похожее делаем, только на базе Миландра
                0
                ок, начну чуть издалека, сорри за фуражку Капитана Очевидности во многих моментах, имхо на них стоит сделать акцент.

                «Железячные тестовые стенды» и тестируемые приборы, в отличии от «тестирования кодом кода», часто не совсем детерминированные — взаимные наводки, плохие контакты, помехи, коты и всякое такое, поэтому:

                — принцип KISS на первом плане — чем меньше мы навесим тестовой обвязки — тем лучше, стараемся максимально снять инфы по визуальному каналу (вебкамера смотрит на экран тестируемых приборов и их светодиоды), там где можно тестировать демоборды или что-то с питанием от USB — берем USB хаб управляющий питанием по каждому порту (например Dlink H7 в сером корпусе) — $20 за 7 каналов управления питанием и проброса UART-или-что-там-на-борде в систему

                — никаких плат разложенных по столам/полкам стендов — во первых коты, во вторых маппинг экранов/светодиодов в пиксели видеокамеры будет слетать. Например, собираем тестовые «лотки» из корпусов от старых CD приводов, и вставляем их в старый ATX корпус, где напротив них стационарно висит вебкамера (см фото). На веб-камеру стоит нацепить полярик, и не особо совать все это под солнечный свет (цвета пикселей будут уходить)

                — только качественные и короткие USB провода

                Фото
                image
                image


                О фреймворке (точнее сборки из NodeJS+Redis c легаси и велосипедами) — он состоит из серверной части и самих тестов. Серверная часть выставляет веб-морду, из которой можно запустить любой тест и смотреть его прогресс и видеокамеру. (планировалась еще интерактивность, пробросить кнопки прибора в кнопки на морде, но руки пока не дошли). Так-же серверная часть обрабатывает вебкамеру, захватывая кадры через OpenCV, выделяя интересующую область и публикуя ее в Redis-канал для приема на стороне теста, и в вебсокеты UI клиентам.

                На стороне теста «фреймворк» выставляет Promise обертки над всякими командами «поуправлять питанием портов USB хаба» и над i2c периферией, которая «нажимает кнопки» 16-канальным PWM драйвером, им-же эмулирует аналоговые сигналы (например, один из наших приборов {реле защиты} измеряет напряжение в сети и от него все действия, PWM хоть и точностью +-3 вольта, но эмулит пере/недо-напряжения)

                Пример теста (неканонично, это все делалось для внутр. пользования)
                const {
                    wait ,
                    waitForUart ,
                    waitForPixel ,
                    timeLabel ,
                    infoLabel ,
                    buttonPress ,
                    buttonUnpress,
                    sendPWM ,
                    powerCycle
                } = require('./test-framework');
                
                const Stage = require('./stage-config');
                
                const PowerLedX = 34;
                const PowerLedY = 236;
                
                const PowerReadyLedX = 53;
                const PowerReasyLedY = 274;
                
                Promise.resolve(true)
                    //
                    // turn on PWM driver
                    .then(timeLabel("init-section","START"))
                    .then(infoLabel("включаем PWM,устанавливаем напряжение входа VP16"))
                    .then(powerCycle(true,Stage.PWM))
                    .then(wait(500))
                    // set input voltage at VP16
                    .then(sendPWM(0,220/Stage.VP16.UCONST,Stage.VP16.UINP))
                    .then(wait(500))
                    .then(timeLabel("init-section","PASS"))
                    .catch(timeLabel("init-section","FAIL"))
                    .then(infoLabel("включаем VP16"))//
                    .then(wait(500))
                    .then(timeLabel("pwr-ready-test","START"))
                    .then(powerCycle(true,Stage.VP16))
                    //.then(powerCycle(true,Stage.TR16))
                    .then(infoLabel("ждем готовности по напряжению"))
                    .then(waitForPixel(PowerReadyLedX,PowerReasyLedY,(r,g,b,a)=>(r>200 || ((r > (g+b)*2)&&(r>30)) )))
                    .then(timeLabel("pwr-ready-test","PASS"))
                    .catch(timeLabel("pwr-ready-test","FAIL"))
                    .then(wait(2000))
                    .then(timeLabel("pwr-button-on-test","START"))
                    .then(infoLabel("нажимаем среднюю кнопку"))
                    .then(buttonPress(Stage.VP16.BUTTON2))
                    .then(waitForPixel(PowerLedX,PowerLedY,(r,g,b,a)=>r>230))
                    .then(timeLabel("pwr-button-on-test","PASS"))
                    .catch(timeLabel("pwr-button-on-test","FAIL"))
                    .then(infoLabel("отпускаем среднюю кнопку"))
                    .then(buttonUnpress(Stage.VP16.BUTTON2))
                    .then(wait(2000))
                    .then(timeLabel("pwr-overvoltage-test","START"))
                    .then(infoLabel("имитируем овервольтаж"))
                    .then(sendPWM(0,310/Stage.VP16.UCONST,Stage.VP16.UINP))
                    .then(waitForPixel(PowerLedX,PowerLedY,(r,g,b,a)=>r<230))
                    .then(timeLabel("pwr-overvoltage-test","PASS"))
                    .catch(timeLabel("pwr-overvoltage-test","FAIL"))
                    .then(wait(2000))
                    .then(infoLabel("ждем восстановления"))
                    .then(wait(500))
                    .then(timeLabel("pwr-overvoltage-return-test","START"))
                    .then(sendPWM(0,220/Stage.VP16.UCONST,Stage.VP16.UINP))
                    .then(waitForPixel(PowerLedX,PowerLedY,(r,g,b,a)=>r>230))
                    .then(timeLabel("pwr-overvoltage-return-test","PASS"))
                    .catch(timeLabel("pwr-overvoltage-return-test","FAIL"))
                    .then(wait(2000))
                    .then(infoLabel("выключаем приборы"))
                    .then(powerCycle(false,Stage.VP16))
                    .then(wait(500))
                    .then(powerCycle(false,Stage.TR16))
                    .then(wait(500))
                    .then(powerCycle(false,Stage.PWM))
                    .then(wait(1000))
                    //.then(powerCycle(false,Stage.TR16))
                    .then(timeLabel("final-section","PASS"))
                    .catch(timeLabel("final-section","FAIL"))
                    .then(()=>process.exit());
                



                И пример конфига для него:
                const Stage = {
                    "PWM" :  {
                        id: "PWM",
                        usb: "8.7",
                        power_hub_port: 7,
                        serial: undefined
                    },
                
                    "VP16": {
                        id: "VP16",
                        usb: "8.1",
                        power_hub_port: 1,
                        serial: undefined,
                        // voltage calibration
                        UCONST : 0.2980,
                
                        // pwm shield port map
                        UINP    : 0, // U input emulation
                        BUTTON1 : 2, // buttons...
                        BUTTON2 : 1, //
                        BUTTON3 : 3, //
                    },
                    "TR16": {
                        id: "TR16",
                        usb: "8.6",
                        power_hub_port: 6,
                        serial: undefined,
                    },
                
                    "FLASHER" :  {
                        id: "FLASHER",
                        usb: "8.2",
                        power_hub_port: 2,
                        serial: undefined
                    },
                
                };
                
                module.exports = Stage;
                



                Тест можно запускать из-под UI вручную, можно из cmd-line для интеграции пайплайнов да и просто с IDE по SSH (у меня CLion скриптом собирает бинарь, scp его на стенд, ssh туда, выгрузка на плату и запуск теста при необходимости — получается эмбед писать с ноута удаленно от стенда и проводов). При запуске из командной строки вывод и вэбка все-равно транслируются в UI для стороннего контроля.

                Крутится все это на i5 под убунтой, жрет CPU 10..15% (в основном вэбка и ее данные по шине Redis, там не особо оптимизировать успел)

                В целом получается что-то типа сильно упрощенного Redd от EasyLy ( habr.com/ru/post/440156 ) только «мясом вовнутрь» и за шапку сухарей.
                  0
                  Спасибо, за детальный ответ. его можно и расписать в виде отдельной статьи.
                    0
                    Увы, статьей аккаунт не позволяет. По свободе заделаю git-репозиторий с кодом и примером «как помигать светодиодом» под автотестами.
              +1
              Ну уж скажите, дичь. Давайте я расскажу вам, как это все живет у нас.

              Для начала масштаб проблемы. У нас пяток классов устройств, у каждого — своя система. Одновременно для каждой системы ведется разработка над текущим релизом, следующим мажорным релизом и опционально следующим мажорным релизом + 1. Помимо этого есть минорные релизы, над котороыми работа может начаться одновременно с мажорным. Плюс специальные ветки для бета-версий, фиксов безопасности и отдельные ветки для фабрик, чтобы не показывать им весь софт до релиза. Это все дает примерно с десяток активных каналов разработки для каждого класса устройств. Примерно каждый год выходит несколько новых моделей почти во всех классах, поддержка старых осуществляется в течение нескольких лет. Это дает до нескольких десятков поддерживаемых моделей устройств на класс. У каждой системы есть несколько вариантов, для пользователей, для фабричного тестирования, для разработчиков, etc. Итого мы ежедневно получаем примерно 30-50 сборок систем в день и примерно 100 моделей устройств, на которых нужно проверить, что интересующие нас варианты систем хотя бы базово работают. Сейчас у нас больше 250 устройств, которые висят подключенными на почти трех десятках серверов и работают в CI. Осложняется дело необходимым на все это временем. Сборка всех изменений за сутки занимает примерно 6-8 часов, сборка всего — 13 часов. Еще 2-4 часа на то, чтобы смастерить образы системы для всех вариантов и для всех устройств. Отдельная история про построение OTA патчей и загрузку всего этого дела в архив, это идет в фоне и может занимать до суток. erase-install на одно устройство занимает примерно полчаса. Минимальный набор тестов еще минут 15.

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

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

              Ну и да, мы единственная команда, которая проверяет все поддерживаемые устройства. Правда и набор тестов у нас скудный. Чтобы его пройти образ должен успешно установиться на устройство, не жрать 100% cpu, не крешиться в цикле, а процесс UI должен запуститься и не падать. Мы буквально отвечаем на вопрос «а пригоден ли билд к дальнейшему тестированию?», зато для всего.

              У других команд есть всевозможные автотесты на функциональность, они работают на том же технологическом стеке, но на другом пулле устройств и они проверяют, как правило, одну модель для каждого SoC. Полностью же покрыть весь софт автотестами не реально — слишком быстро происходят изменения. В ручную тестируется просто большая часть системы.
                0
                Мы буквально отвечаем на вопрос «а пригоден ли билд к дальнейшему тестированию?», зато для всего.

                В том-то и дело, что при этом по аналогии с софтом может возникнуть ложная уверенность, что раз CI "дала добро", можно смело начинать выкатывать OTA на пользовательские устройства, хотя это на самом деле только самый базовый уровень тестирования и в реале экономит не так уж много времени, если думать об "успешно установиться на устройство, не жрать 100% cpu, не крешиться в цикле, а процесс UI должен запуститься и не падать" еще на этапе архитектуры, а не кодирования.

                  0
                  ложная уверенность — плохо, но это к теории тестирования)) для OTA имхо как минимум один тест — «способен ли новый билд посредством OTA откатиться на старый» должен быть автоматизирован в пайплайне
                    0
                    — «способен ли новый билд посредством OTA откатиться на старый» должен быть автоматизирован в пайплайне

                    И вот у меня почему-то на 99% уверенности, что комментатор выше это не тестирует на своей CI ферме. :-)

                      0
                      У нас в принципе нет механизмов даунгрейда версии. Нужен даунгрейд — бэкап данных/erase-install/восстановление бэкапа. Ну и все прекрасно понимают, что то что мы тестируем — позволяет поймать то, что билд совсем сломан, а не проверить то, что он хороший. Учитывая, что главные каналы обновления получают свежие сборки один раз в день, это просто ответ на вопрос, могут ли разработчики продолжать работать на сегодняшнем билде или лучше остаться на предыдущем.
                        0
                        Тут дело не в даунгрейде версии, а в работоспособности OTA в новой версии — не сможем откатиться на старую (пусть и под новым номером) — значит багфикс и новую версию тоже не втянем, канал ОТА лег, уже проданные приборы — кирпичи.
                          0
                          Вот это, кстати, интересный кейс. Было бы интересно его тестировать. В текущем варианте мы этого не можем сделать, так как чтобы протестировать OTA, надо собрать еще одну новую версию и сделать патчи с текущей версии на новую. Я полагаю, этот кейс обязательно входит в финальное ручное тестирование апдейтов, которые уходят пользователям.
              0
              Тема автоматизации тестирования весьма интересная, но ожида прочитать что-то большее, чем примитивное заыкание 2х реде для имитации кнопки и сброса питания. Раз уж такая тема, никому не приходилось реализовывать USB хост для FTDI на стороне МК? Попадался драйвер для этого дела под USB стек для ST. Но больше интересует, есть ли в наличии внешние микрухи, чтобы просто подключить к МК по уарту и не париться с реализацией всего этого?
                0

                Мне кажется все проще. Подключите любой МК аля ардуины с USB и UART на борту к вашему МК. В подключаемом — простая программа проброса данных с порта на порт

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

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