Как не тратить время на велосипеды, а просто написать тесты на python и наслаждаться стабильно работающими тестовыми станциями на производстве электроники? Конечно, используя open source решение HardPy.

Разбор использования функций HardPy - открытого фреймворка для создания тестовых станций для производства электроники на Python на примере тестирования и прошивки отладочной платы Nucleo-F401.

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

Разберём на простейшем примере, как с помощью HardPy сделать это быстро и надёжно. Задача - показать основные шаги разработки программного обеспечения для тестирования.

Требуемое железо, документация, полезные ссылки

Исходники этого проекта с подробными инструкциями можно посмотреть на github.

  • HardPy GitHub

  • HardPy Documentation

  • Telegram

  • Статья написана для пакета hardpy версии 0.4.0 и для использования в Linux (Mint, Ubuntu). Под Windows HardPy тоже работает, но некоторые функции по запуску и адреса портов будут отличаться.

  • Для запуска примера нужна отладочная плата Nucleo-F401RE.

  • Если у вас нет платы Nucleo-F401RE - можно запустить другой пример, Examples GitHub, Examples documentation.

Тестовый стенд

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

  • какое устройство мы тестируем - (DUT - Device under test, тестируемое устройство)

  • что мы хотим протестировать - список функций

  • как мы это будем тестировать - тестовый план

  • чем мы это тестируем - приборы, структурная схема стенда

DUT

В качестве тестируемого устройства выступает отладочная плата Nucleo-F401RE . Nucleo - аскетичные отладки, которые предлагают минимум функций по классной цене. Треть платы занимает программатор ST-LINK, мы будем считать его прибором, а не частью проверяемой платы.

Nucleo-F401RE functions
Nucleo-F401RE functions

Определяем объём тестирования

Какие функциональные блоки мы хотим проверить у этой платы:

  • Питание

  • Кнопка (1)

  • LED (1)

  • Цифровые входы-выходы, выведенные на разъёмы (2 шт.)

  • MCU

Для имитации реального мира привнесём имитатор дефекта на плате - jumper. Он позволит создать (дефекта нет) или разорвать соединение (дефект есть) между выводами микроконтроллера.

Мы тестируем маленькую партию, поэтому автоматизировать детектирование работы светодиода и активацию кнопки не будем, задействуем для этого глаза и руки тестировщика. Чтобы снизить влияние человеческого фактора, будем варьировать задания и проверять их исполнение человеком вместе с DUT.

Теперь, когда мы точно знаем, что мы тестируем и в каком объёме, можно приступать к разработке тест-плана.

Тестовый план

  1. Проверить наличие приборов;

  2. Проверить наличие прошивки для DUT;

  3. Прошить DUT;

  4. Прочитать серийный номер DUT;

  5. Проверить наличие джампера;

  6. Проверить светодиод, поморгать светодиодом несколько раз, спросить у пользователя количество морганий;

  7. Проверить User Button, попросить пользователя нажать кнопку произвольное количество раз (вариация) на кнопку. Просим ввести это количество в форму.

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

Вычислитель тестовой станции

Любой компьютер с Linux. На Windows пример запустить тоже можно,но не стоит.

Схема стенда

Зафиксируем состав стенда и все связи между компонентами на структурной схеме.

Stand structure
Stand structure

Для тестирования DUT нам понадобится единственный прибор - программатор ST-LINKV2. По счастливой случайности он есть на нашей плате Nucleo, так что осталось просто подключить плату к компьютеру по USB и приготовить джампер

Программа тестовой станции

Пишем драйвера

Для работы программы с DUT и приборами нужны драйвера. У нас один прибор и один модуль DUT.

ST-LINK driver

К счастью, многое уже написано за нас, и можно просто использовать PyOCD для прошивки микроконтроллера STM32 с помощью ST-LINKV2.

DUT driver

Для нашего DUT никто готовых драйверов не написал, так что пишем сами.
Драйвер достаточно простой и подключается к устройству по серийному порту к DUT на скорости 115200 бод. Драйвер запрашивает у DUT серийный номер, статус наличия джампера и количество нажатий на кнопку.


from serial import Serial
from struct import unpack
  

class DutDriver(object):
    def __init__(self, port: str, baud: int) -> None:
        self.serial = self._connect(port, baud)
        self.SERIAL_NUMBER_STR_LEN = 11
        self.SIMPLE_REPLY_LEN = 1

    def write_bytes(self, data: str) -> None:
        self.serial.write(data.encode())

    def read_bytes(self, size: int) -> bytes:
        return self.serial.read(size)

    def reqserial_num(self) -> str:
        self.write_bytes("0")
        reply = self.read_bytes(self.SERIAL_NUMBER_STR_LEN)
        return reply.decode("ascii")[:-1]

    def req_jumper_status(self) -> int:
        self.write_bytes("1")
        reply = self.read_bytes(self.SIMPLE_REPLY_LEN)
        return unpack("<b", reply)[0]

    def req_button_press(self) -> int:
        self.write_bytes("2")
        reply = self.read_bytes(self.SIMPLE_REPLY_LEN)
        return unpack("<b", reply)[0]

    def _connect(self, port: str, baud: int) -> Serial | None:
        try:
            return Serial(port=port, baudrate=baud)
        except IOError as exc:
            print(f"Error open port: {exc}")
            return None

Пишем тестовый план на pytest

pytest - открытый инструмент, который позволяет легко писать небольшие, читаемые тесты и может масштабироваться для поддержки сложного функционального тестирования приложений и библиотек. Подробнее про функционал pytest можно почитать в документации проекта

В процессе отладки кода можно удобно запускать его прямо в своём IDE, HardPy для этого не нужен.

Прошивка MCU

Напишем тест для прошивки DUT. Пример будет состоять из 2 файлов: conftest.py и test_1_fw.py. В файле conftest.py положим инициализацию драйвера DUT, а в test_1_fw.py будет лежать сам тест. Подробнее про conftest.py можно почитать в документации к pytest. Все файлы примера должны лежать в одной папке, у нас это будет папка tests.

conftest.py

В conftest.py создаем экземпляр драйвера, передаем ему порт и скорость в 115200 бод (задана в DUT). Порт на разных ПК может отличаться, поэтому может потребоваться редактирование этой переменной.
Для работы с портом надо добавиться в группу dialout. После этого надо перелогиниться в систему.

sudo usermod -aG dialout имя_пользователя
# conftest.py

import pytest
from dut_driver import DutDriver

@pytest.fixture(scope="session")
def device_under_test():
    dut = DutDriver("/dev/ttyACM0", 115200)
    yield dut

test_1_fw.py

Тестирование в рамках файла test_1_fw.py заключается в проверке наличия прошивки, проверке подключения DUT к ПК, прошивке DUT. В папку с тестом надо положить скомпилированную прошивку DUT, dut-nucleo.hex, можно собрать её самостоятельно из исходников, либо воспользоваться уже скомпилированной версией.

# test_1_fw.py

from glob import glob
from pathlib import Path

import pytest
from pyocd.core.helpers import ConnectHelper
from pyocd.flash.file_programmer import FileProgrammer

def firmware_find():
    fw_dir = Path(Path(__file__).parent.resolve() / "dut-nucleo.hex")
    return glob(str(fw_dir))

def test_fw_exist():
    assert firmware_find() != []
  
def test_check_connection(device_under_test):
    assert device_under_test.serial is not None, "DUT not connected"

def test_fw_flash():
    fw = firmware_find()[0]
    hardpy.set_message(f"Flashing file {fw}...", "flash_status")

    with ConnectHelper.session_with_chosen_probe(
        target_override="stm32f401retx"
    ) as session:
        # Load firmware into device.
        FileProgrammer(session).program(fw)

Для работы тестов нам потребуется установить через pip несколько пакетов python.

pyserial==3.5
pytest==8.1.1
pyocd==0.36.0

Рекомендуется устанавливать их в виртуальное окружение venv или conda.

  • pyserial используется для взаимодействия с DUT по серийному порту

  • pyocd используется для обновления прошивки DUT через встроенный ST-link.

Для корректной работы pyocd также потребуется установить пакет поддержки stm32f401retx.
Для этого нужно вызвать команды:

pyocd pack update
pyocd pack install stm32f401retx

Можно запускать тесты из папки tests командой:

pytest .

Получаем сообщение об успешном тестировании:

collected 3 items                                                                                                                                 

test_1_fw.py ...                                                                                                                            [100%]

======= 3 passed in 7.27s =======

Таким образом, у нас получилось написать простой тест план, обновляющий прошивку DUT с помощью pytest.

В процессе разработки устройств можно использовать pytest для тестовых скриптов, которые будут проверять конкретные сценарии работы разрабатываемого устройства. В идеале это может быть встроено в процесс CI/CD, HIL.

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

Добавляем HardPy

Теперь в тесты можно добавить HardPy, установив его через pip. (Для HardPy версии 0.4.0 версия python должна быть не ниже 3.10, а версия pytest не ниже 7)

pip install hardpy

Улучшаем наш файл test_1_fw.py:

# test_1_fw.py

from glob import glob
from pathlib import Path

import hardpy
import pytest

from pyocd.core.helpers import ConnectHelper
from pyocd.flash.file_programmer import FileProgrammer

pytestmark = pytest.mark.module_name("Testing preparation")

def firmware_find():
    fw_dir = Path(Path(__file__).parent.resolve() / "dut-nucleo.hex")
    return glob(str(fw_dir))

@pytest.mark.case_name("Availability of firmware")
def test_fw_exist():
    assert firmware_find() != []

@pytest.mark.case_name("Check DUT connection")  
def test_check_connection(device_under_test):
    assert device_under_test.serial is not None, "DUT not connected"

@pytest.mark.dependency("test_1_fw::test_check_connection")
@pytest.mark.case_name("Flashing firmware")
def test_fw_flash():
    fw = firmware_find()[0]
    hardpy.set_message(f"Flashing file {fw}...", "flash_status")

    with ConnectHelper.session_with_chosen_probe(
        target_override="stm32f401retx"
    ) as session:
        # Load firmware into device.
        FileProgrammer(session).program(fw)

    hardpy.set_message(f"Successfully flashed file {fw}", "flash_status")
    assert True

В файле добавились:

  • Импорт пакета hardpy;

  • Сообщения оператору, через функцию set_message();

  • Понятные оператору названия тестов и группы тестов: case_name и module_name в терминологии hardpy.

  • Маркер зависимости dependency теста test_fw_flash от теста test_check_connection. В случае, если тест test_fw_flash провалится, тест test_check_connection не будет запускаться.

На этом этапе добавляем в папку с тестами файл pytest.ini, который настроит лог и включит hardpy при запуске pytest через регистрацию плагина addopts = --hardpy-pt.

[pytest]
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)s] %(message)s
log_cli_date_format = %H:%M:%S
addopts = --hardpy-pt

Запускаем базу данных

Результаты тестирования надо сохранять. Для этого HardPy использует базу данных CouchDB. Важно, что версия CouchDB должна быть не ниже 3.2.

Создаем папку database и добавляем туда файл couchdb.ini, хранящий настройки базы.

[chttpd]
enable_cors=true

[cors]
origins = *
methods = GET, PUT, POST, HEAD, DELETE
credentials = true
headers = accept, authorization, content-type, origin, referer, x-csrf-token

Для запуска воспользуемся инструментом docker compose. Создадим файл docker-compose.yaml со следующим содержимым:

version: "3.8"

services:
  couchserver:
    image: couchdb:3.3.2
    ports:
      - "5984:5984"
    environment:
      COUCHDB_USER: dev
      COUCHDB_PASSWORD: dev
    volumes:
      - ./dbdata:/opt/couchdb/data
      - ./couchdb.ini:/opt/couchdb/etc/local.ini

И запустим базу через команду docker compose up.

Интерфейс базы данных доступен по адресу http://127.0.0.1:5984/_utils  с логином и паролем, которые были указаны в docker-compose.yaml - dev/dev.

Запускаем панель оператора

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

hardpy-panel .

либо командой:

hardpy-panel <путь до папки tests>

если запуск происходит не из папки с тестами.

В итоге в терминале мы должны увидеть сообщение о том, что все 3 теста cколлекционированы.

configfile: pytest.ini
plugins: hardpy-0.4.0, anyio-4.4.0
collected 3 items                                                                                                                        

<Dir tests>
  <Module test_1_fw.py>
    <Function test_fw_exist>
    <Function test_check_connection>
    <Function test_fw_flash>

======================================================= 3 tests collected in 0.37s =======================================================

А дальше можно открыть панель оператора в браузере по адресу http://localhost:8000/

HardPy operator panel
HardPy operator panel

По кнопке Start запускаются тесты, результаты - в базе. В целом, это уже полноценная тестовая станция для производства, только тестовое покрытие пока слабое.

Увеличим объем тестов

Добавим 2 модуля test_2_base_functions.py и test_3_user_button.py. Содержание тестов разбирать не будем, за исключением моментов использования hardpy.

Проверка серийного номера и наличия перемычки

  • Считываем серийный номер, проверяем его.

  • Проверяем наличие перемычки на DUT.

# test_2_base_functions.py

import pytest
import hardpy

from dut_driver import DutDriver

pytestmark = pytest.mark.module_name("Base functions")

pytestmark = [
    pytest.mark.module_name("Base functions"),
    pytest.mark.dependency("test_1_fw::test_fw_flash"),
]

@pytest.mark.case_name("DUT info")
def test_serial_num(device_under_test: DutDriver):
    serial_number = device_under_test.reqserial_num()
    hardpy.set_dut_serial_number(serial_number)
    assert serial_number == "test_dut_1"

    info = {
        "name": serial_number,
        "batch": "batch_1",
    }
    hardpy.set_dut_info(info)

@pytest.mark.case_name("LED")
def test_jumper_closed(device_under_test: DutDriver):
    assert device_under_test.req_jumper_status() == 0

Используются 2 функции:

  • set_dut_serial_number - записывает в базу данных серийный номер DUT.

  • set_dut_info - записывает в базу данных словарь с любой информацией об устройстве. В нашем случае мы записываем ещё раз серийный номер и номер партии.

Проверка User button

Просим пользователя понажимать на User button, а потом ввести количество нажатий.

# test_3_user_button.py

import pytest
import hardpy
from hardpy.pytest_hardpy.utils.dialog_box import (
    DialogBoxWidget,
    DialogBoxWidgetType,
    DialogBox,
)

from dut_driver import DutDriver

pytestmark = [
    pytest.mark.module_name("User interface"),
    pytest.mark.dependency("test_1_fw::test_fw_flash"),
]

@pytest.mark.case_name("User button")
def test_user_button(device_under_test: DutDriver):
    hardpy.set_message(f"Push the user button")
    keystroke = device_under_test.req_button_press()
    dbx = DialogBox(
        dialog_text=(
            f"Enter the number of times the button has "
            f"been pressed and press the Confirm button"
        ),
        title_bar="User button",
        widget=DialogBoxWidget(DialogBoxWidgetType.NUMERIC_INPUT),
    )
    user_input = int(hardpy.run_dialog_box(dbx))
    assert keystroke == user_input, (
        f"The DUT counted {keystroke} keystrokes"
        f"and the user entered {user_input} keystrokes"
    )

Используется возможность вызова диалоговых окон:

  • Класс DialogBox - описывает содержимое диалогового окна. В нашем случае это окно для ввода числовых значений.

  • run_dialog_box - вызывает диалоговое на стороне панели оператора.

    HardPy operator dialog
    HardPy operator dialog

Собираем данные

Запись отчетов в базу данных

Для записи финальных отчетов нужно дополнить файл conftest.py действиями по окончании тестирования.
Во время тестирования отчет всегда пишется в CouchDB, в базу runstore, но мы добавим сохранение всех отчетов по окончании тестирования в базу report.
Отчет хранится в формате json документа согласно схеме, описанной в документации.

Сама база доступна по адресу http://127.0.0.1:5984/_utils/#

# conftest.py

import pytest
from hardpy import (
    CouchdbLoader,
    CouchdbConfig,
    get_current_report,
)

from dut_driver import DutDriver

@pytest.fixture(scope="session")
def device_under_test():
    dut = DutDriver("/dev/ttyACM0", 115200)
    yield dut

def finish_executing():
    report = get_current_report()
    if report:
        loader = CouchdbLoader(CouchdbConfig())
        loader.load(report)

@pytest.fixture(scope="session", autouse=True)
def fill_actions_after_test(post_run_functions: list):
    post_run_functions.append(finish_executing)
    yield

В файл были добавлены:

  • Функция fill_actions_after_test, которая заполняет список post_run_functions функций, которые нужно выполнить по окончании тестирования.

  • Функция finish_executing - описывает действия по считыванию актуального отчета из runstore и записи его в базу report.

    Отчёт о тестировании в базе данных
    Отчёт о тестировании в базе данных

Панель оператора

Как это всё видит оператор стенда:

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

Заключение

Сегодня мы легко и просто сделали тестовую станцию, написав только минимум кода, который является специфичным для DUT. Стенд готов к производству.

Спасибо соавтору @ilya_alexandrov !

P.S.
А где же онлайн сбор и анализ данных о тестировании и удалённое управление тестовым парком? Скоро всё будет, пишите, если хотите поучаствовать в тестировании ранних версий.

Комментарии (5)


  1. engin
    17.08.2024 11:11

    Ребята, такой подход к тестированию конкретной отладочной платы рулит, в том случае когда производство ее на потоке рассчитано как минимум на 3 - 5 лет без каких либо изменений в ее процессорной архитектуре. Опять же такую "станцию" никак нельзя строить на опенсорсе, или сразу с себя снимать ответственность перед клиентом за ее стабильность и функционал. Обычно производители такие тесты хранят как зеницу ока, т.к. это часть их конкуренции в условиях агрессивного рынка.
    Что касается среды исполнения тест бенча, применительно к фикстуре испытуемой платы (разработчики платы обязаны в процессе ее разводки размещать тест поинты по по всем точкам, подлежащим контролю, с целью выявления отклонений от рабочих параметров).
    В статье есть ряд тестов, которые перенасыщают алгоритм теста.
    Так к примеру:

    • Проверить наличие приборов;

    • Проверить наличие прошивки для DUT;

    • Прошить DUT;

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


    1. juramehanik
      17.08.2024 11:11

      Никак нельзя строить на опенсурсе - шта? Питон нельзя использовать? Он тоже опенсурсный сам по себе если что.


      1. engin
        17.08.2024 11:11

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

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


        1. juramehanik
          17.08.2024 11:11

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


          1. engin
            17.08.2024 11:11

            Как не тратить время на велосипеды, а просто написать тесты на python и наслаждаться стабильно работающими тестовыми станциями на производстве электроники? Конечно, используя open source решение HardPy.

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