#комбинаторика #H-мост #sensitivity #алгоритмы #математическая логика #FSM #русская смекала

Постановка задачи

Вам с out source производства принесли 600 электронных плат (PCB). Или Вам принесли 5 экземпляров самой первой ревизии PCB прототипа нового сложного электронного продукта прямо с поверхностного монтажа.

Как проверить, что в электронных платах отсутствует брак монтажа: короткие замыкания на GND или VCC, короткие замыкания соседних пинов друг на друга и прочие аппаратные ошибки?

определимся с терминологией

1-- пин (pin) - это металлический проводник, который торчит из микросхемы. В нашем случае микроконтроллера. В современных микроконтроллерах десяти и даже сотни пинов.

2-- граф - это множество вершин и ребер (палочки и кружочки).

3-- ориентированный граф — граф у которого ребра имеют направление

4--конечный автомат -  ориентированный граф. Вершины - это состояния, ребра - события

5--конечный автомат Мили- это конечный автомат у которого действия происходят в момент переходов

6--конечный автомат Мура - это конечный автомат у которого действия происходят в состоянии

7--подтяжка к GND- соединение пина и GND резистором 40kOm

8--подтяжка к VCC- соединение пина и VCC резистором 40kOm

9-- токен - текстовая строчка без пробелов. Служит ссылкой на более длинное предложение

10- bring-up PCB- процесс отладки электронной платы с производства. Выявление неисправностей, несоответствий и их ремонт.

По-хорошему надо тестировать PCB на короткие замыкания и разрыв проводников еще до того как на печатную плату будут припаиваться компоненты. А для этого нужны тестировочные стенды (test jig) под каждую версию PCB. Разработку test jig могут позволить себе далеко не все организации. Как можно в более кустарных условиях протестировать электронную плату на предмет качества пайки?

Подойдем к решению постановки задачи издалека. Существует огромный класс электронных устройств, основой которых является микроконтроллер. Как вы думаете зачем в микроконтроллерах есть такая аппаратная функция как подтяжки напряжения к GND, VCC или вовсе отключенные подтяжки? Иногда это нужно для работы интерфейсов например I2C, 1-Wire, UART Rx, кнопок, но есть и более подходящая работа для подтяжек пинов.

Самое главное достоинство подтяжек напряжения в том, что установкой подтяжки невозможно, что бы то ни было сжечь на электронное плате. Подтяжка всегда подключает резистор очень высокого сопротивления 10kOm-40kOm, поэтому и токи в электронной цепи протекают незначительные (80 uA ... 300 uA).

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

У меня уже был текст про конечный автомат Load-detect для первоначального грубого контроля качества пайки. Вот он https://habr.com/ru/articles/756572/. Однако в том алгоритме было одно существенное ограничение. Тот конечный автомат принципиально не мог определить короткое замыкание между соседними пинами микроконтроллера. А это достаточно частый вид брака в массовом производстве Hi-Tech электроники. Если говорить откровенно, то тот первый Load-detect с FSM на 3 состояния - это уровень простейших многоклеточных. Сегодня же поговорим о настоящем динозавре под названием Cross-Detect.

Вообще я долго не мог понять как назвать весь этот механизм определения брака, чтобы создать для него отдельную папку в репозитории. Были версии назвать это solder-test, assembly-check, wire-verify. Однако название cross-detect лучше всего отражает суть этого алгоритма.

В этом тексте я представил Cross-Detect алгоритм, который помимо замыканий на землю и питание может распознать ещё и короткое замыкание не только между соседними пинами, но и между любыми проводниками на электронной плате (PCB)! Это просто мечта любого человека, который занимается BringUp(ом) электронных плат с производства.

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

Про то как делать распознавание оторванной нагрузки в силовом H-мосте у меня был отдельный культовый текст https://habr.com/ru/articles/709374/. Здесь же в контроле качества пайки PCB ситуация наоборот. Оторванная нагрузка это как раз благоприятный вариант. Тут будет всё как в том тексте про регистрацию обрыва нагрузки с той лишь разницей, что вместо плечей моста будут выступать просто два пина микроконтроллера и появилась еще и подтяжка к земле.

Я описал полный путь от идеи до реализации.

Какие могут быть проблемы в H-мосте?

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

Как же программно выявить брак пайки?

Как водится в программировании, разработка любого программного компонента сводится к тому, что надо реализовать специфический конечный автомат (FSM) для этой задачи. Конечный автомат это золотой шаблон в программировании. Конечные автоматы хороши тем, что процесс разработки FSM лет как сто поставлен на рельcы и состоит из шести чисто формальных фаз.

Фаза 1. Определить входы конечного автомата.

С входами тут всё очень просто. Вход только один. Это событие переполнения таймера окончания переходного процесса. Таймер может быть как аппаратный, так и программный.
Из прошлой статьи (Load-Detect для Проверки Качества Пайки) можно заметить, что при установке подтяжки напряжения реальная длительность переходного процесса редко превышает 3 ms. Но я для надежности поставил 10...100ms.

Фаза 2. Перечислить все возможные состояния конечного автомата.

Тут чистая комбинаторика. Сколько способов установить три различные подтяжки на 2 пина? Согласно правилу произведения, 3*3=9 способов. Значит у нас будет девять состояний.

Фаза 3. Определить действия для данного конечного автомата

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

Фаза 4. Построить таблицу переходов

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

1)для уменьшения энергопотребления микроконтроллера,

2)для уменьшения времени переходного процесса. Один переходной процесс всегда закончится быстрее чем два параллельных переходных процесса.

В двоичной системе счисления такие коды, где меняется только один бит - называются кодами Грея. У нас же тут, по сути, тоже коды Грея, только в третичной системе счисления.

Фаза 5. Построить таблицу выходов (Action Table)

В общем, на каждом переходе конечный автомат делает одно и тоже: перезапускает таймер. Однако на последнем переходе автомат увеличивает счетчик итераций, вычисляет решение, сохраняет решение в отчет и, если надо, выбирает следующую пару пинов для анализа коротких замыканий.

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

Фаза 6. Нарисовать граф конечного автомата

Граф можно нарисовать вручную в программе inkscape (как я и сделал) или сгенерировать на языке генерации авто-документации Graphviz. Тут уж кому как удобнее. Вот такой получился простенький граф (рис.5.)для конечного автомата проверки качества пайки. Тут всё как на ладони: и таблица переходов, и таблица выходов отражены в одной картинке. Как говорит английская народная пословица: "картинка стоит тысячи слов" (А picture is worth a thousand words).

рис.5.
рис.5.

Для чистоты эксперимента в каждый отдельный момент времени работает обработка только одной пары пинов. Конечный автомат cross-detect прокрутится N итераций, вычислит решение для пары пинов и занесет его в матрицу решений.

Конечный автомат Cross-Detect это конечный автомат Мура, так как тут состояние, как раз, напрямую соответствует конфигурации подтяжек напряжения. Однако он же автомат Мили, так как все реальные действия по переключению подтяжек происходят в момент перехода между состояниями.

Фаза 7. Сформировать Look-Up таблицу для принятия решения

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

Left GPIO pin

Right GPIO пин

1

0.0 V

0

2

0.0 V

3.3 V

3

3.3 V

0.0 V

4

3.3 V

3.3 V

При этом конечный автомат сross-detect за одну итерацию пробегает через девять состояний. И в каждом состоянии формально может быть четыре различных исходов измерения GPIO. Таким образом таблица принятия решения получится из 9*4 = 36 строчек.

Задача усложняется тем, что в H-мосте может быть одновременно две неисправности. Например на левом плече короткое замыкание на землю, на правом плече короткое замыкание на питание. Один H-мост и две неисправности.

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

Порядковый номер бита

Назначение
бита

1

0

0

На левом пине КЗ на GND

есть неисправность

нет неисправности

1

На левом пине КЗ на VCC

есть неисправность

нет неисправности

2

На правом пине КЗ на GND

есть неисправность

нет неисправности

3

На правом пине КЗ на VCC

есть неисправность

нет неисправности

4

пины замкнуты друг на друга

есть неисправность

нет неисправности

Что является признаком того, что в H-мосте два пина не соединены перемычкой? Если GPIO на каждом пине показывают разные значение, значит перемычка отсутствует. Очевидно, что если в пайке оказалась клякса из припоя, то на пинах будет одинаковое напряжение.

Как работать с этой таблицей? Очень просто. После одной итерации конечного автомата в программе запускается функция bool IsLeftShortGnd(Node_t Node). Она проверяет, что в состояниях 2, 7 и 8 на левом плече моста всегда GPIO измеряет 0V.

Это и является необходимым и достатоным условием, что левый пин накоротко замкнут на GND.

Аналогично с выявлением короткого замыкания соседних пинов. Если в состояниях 2, 6 на обоих плечах GPIO измеряет 3.3V, а в состояниях 3, 9 GPIO измеряет 0 V, то это верный признак того, что эли два пина соединены перемычкой.

Для полноты картины покажем, что этот автомат также может выявлять короткие замыкания на VCC.

Когда на правом плече включена подтяжка к земле, а GPIO измеряет 3.3 V, то это явный признак, что на правом плече на самом-то деле короткое замыкание на VCC.

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

Фаза 8. Написать программный код на языке программирования Си

Вот код основного драйвера программного компонента cross-detect

Вот код основного драйвера программного компонента cross-detect

#include "cross_detect_drv.h"

#include <stdint.h>
#include <string.h>

#include "cross_detect_diag.h"
#include "data_utils.h"
#include "gpio_drv.h"
#include "gpio_general_diag.h"
#include "log.h"
#include "time_utils.h"

uint64_t cross_detect_period_us = MSEC_2_USEC(20);

static bool CrossDetectIsLeftHi(CrossDetectGpioRead_t read){
    bool res = false;
    switch((uint8_t)read){
        case READ_L1_R0: res = true;break;
        case READ_L1_R1: res = true; break;
        case READ_L0_R0: res = false; break;
        case READ_L0_R1: res = false;  break;
    }
    return res;
}

static bool CrossDetectIsRightHi(CrossDetectGpioRead_t read){
    bool res = false;
    switch((uint8_t)read){
        case READ_L0_R1: res = true; break;
        case READ_L1_R1: res = true; break;
        case READ_L0_R0: res = false; break;
        case READ_L1_R0: res = false; break;
    }
    return res;
}

static bool CrossDetectIsRightLow(CrossDetectGpioRead_t read){
    bool res = false;
    switch((uint8_t)read) {
        case READ_L0_R0: res = true; break;
        case READ_L1_R0: res = true;break;
        case READ_L0_R1: res = false;  break;
        case READ_L1_R1: res = false; break;
    }
    return res;
}

static bool CrossDetectIsLeftLow(CrossDetectGpioRead_t read){
    bool res = false;
    switch((uint8_t)read){
    case READ_L0_R0: res = true; break;
    case READ_L0_R1: res = true; break;
    case READ_L1_R0: res = false; break;
    case READ_L1_R1: res = false; break;
    }
    return res;
}

static bool cross_detect_is_left_gnd(CrossDetectPairInfo_t* pair ){
    bool res = false;
    res = CrossDetectIsLeftLow(pair->measurements[CROSS_DETECT_STATE_LU_RA].read);
    if(res){
        res=CrossDetectIsLeftLow(pair->measurements[CROSS_DETECT_STATE_LU_RU].read);
        if(res){
            res=CrossDetectIsLeftLow(pair->measurements[CROSS_DETECT_STATE_LU_RD].read);
        }
    }

    return res;
}

static bool cross_detect_is_left_vcc(CrossDetectPairInfo_t* pair ){
    bool res = false;
    res = CrossDetectIsLeftHi(pair->measurements[CROSS_DETECT_STATE_LD_RA].read);
    if(res) {
        res = CrossDetectIsLeftHi(pair->measurements[CROSS_DETECT_STATE_LD_RD].read);
        if(res) {
            res=CrossDetectIsLeftHi(pair->measurements[CROSS_DETECT_STATE_LD_RU].read);
        }
    }

    return res;
}

static bool cross_detect_is_right_gnd(CrossDetectPairInfo_t* pair ){
    bool res = false;

    res = CrossDetectIsRightLow(pair->measurements[ CROSS_DETECT_STATE_LD_RU].read);
    if(res){
        res=CrossDetectIsRightLow(pair->measurements[CROSS_DETECT_STATE_LA_RU].read);
        if(res){
            res=CrossDetectIsRightLow(pair->measurements[CROSS_DETECT_STATE_LU_RU].read);
        }
    }
    return res;
}

static bool cross_detect_is_right_vcc(CrossDetectPairInfo_t* pair ){
    bool res = false;

    res = CrossDetectIsRightHi(pair->measurements[CROSS_DETECT_STATE_LU_RD].read);
    if(res) {
        res = CrossDetectIsRightHi(pair->measurements[CROSS_DETECT_STATE_LD_RD].read);
        if(res) {
            res=CrossDetectIsRightHi(pair->measurements[CROSS_DETECT_STATE_LA_RD].read);
        }
    }
    return res;
}

static bool cross_detect_is_cross(CrossDetectPairInfo_t* pair ){
    bool res = false;
    if(READ_L1_R1==pair->measurements[CROSS_DETECT_STATE_LU_RA].read){
        if(CROSS_DETECT_STATE_LU_RA==pair->measurements[CROSS_DETECT_STATE_LU_RA].state){
            if(READ_L1_R1==pair->measurements[CROSS_DETECT_STATE_LA_RU].read){
                if(CROSS_DETECT_STATE_LA_RU==pair->measurements[CROSS_DETECT_STATE_LA_RU].state){
                    res = true;
                }
            }
        }
    }

    if(res){
        res = false;
        if(READ_L0_R0==pair->measurements[CROSS_DETECT_STATE_LA_RD].read){
            if(READ_L0_R0==pair->measurements[CROSS_DETECT_STATE_LD_RA].read){
                res =true;
            }

        }
    }
    return res;
}

static bool cross_detect_run_analitic(CrossDetectHandle_t* const Node) {
    bool res = false;
    LOG_PARN(CROSS_DETECT, "RunAnalytics");
    if(Node) {
        CrossDetectState_t state = 0;
        Node->pair.fault_cnt = 0;


        Node->pair.Fault.fault_code = 0;
        res = cross_detect_is_cross(&Node->pair);
        if(res){
            Node->pair.Fault.cross = 1;
        }else{
            Node->pair.Fault.cross = 0;
        }

        res = cross_detect_is_left_gnd(&Node->pair);
        if(res){
            Node->pair.Fault.left_short_gnd = 1;
        }else{
            Node->pair.Fault.left_short_gnd = 0;
        }

        res = cross_detect_is_right_gnd(&Node->pair);
        if(res){
            Node->pair.Fault.right_short_gnd = 1;
        }else{
            Node->pair.Fault.right_short_gnd = 0;
        }


        res = cross_detect_is_left_vcc(&Node->pair);
        if(res){
            Node->pair.Fault.left_short_vcc = 1;
        }else{
            Node->pair.Fault.left_short_vcc = 0;
        }

        res = cross_detect_is_right_vcc(&Node->pair);
        if(res){
            Node->pair.Fault.right_short_vcc = 1;
        }else{
            Node->pair.Fault.right_short_vcc = 0;
        }

        if(Node->pair.Fault.fault_code != CrossDetectResult[Node->left_num][Node->right_num].FaultPrev.fault_code) {
            if(Node->pair.Fault.fault_code) {
                res = CrossDetectDiagFault(Node);
            }
        }
        CrossDetectResult[Node->left_num][Node->right_num].Fault.fault_code = Node->pair.Fault.fault_code;
        CrossDetectResult[Node->left_num][Node->right_num].FaultPrev.fault_code = Node->pair.Fault.fault_code;
        CrossDetectResult[Node->left_num][Node->right_num].update_cnt++;
        res = true;
    }
    return res;
}

/*  a  b  m     a  b  m
 * (0, 4, 5) ->(1, 0, 5)*/
U16Pair_t calc_next_uniq_u16pair(U16Pair_t pair) {
    U16Pair_t out;
    out = pair;

    if(out.a < out.max ) {
        if(out.b < out.max ) {
            out.b++;
            if(out.max ==out.b){
                out.a++;
                out.b = 0;
            }
        } else {
            out.a++;
            out.b = 0;
        }
    } else {
        out.a = 0;
        out.b = 1;
    }

    if(out.max==out.a){
        out.a=0;
    }

    if(out.a == out.b) {
        if(out.b < (out.max - 1)) {
            out.b++;
        } else {
            out.b = 0;
            if(out.a < (out.max - 1)) {
                out.a++;
            } else {
                out.a = 0;
            }
        }
    }
    return out;
}

static bool cross_detect_set_next_pair(CrossDetectHandle_t* const Node) {
    bool res = false;
    if(Node) {
        Node->pair.spin_cnt = 0;

        uint32_t cnt = cross_detect_get_pin_cnt();
        res = cross_detect_reset_pair(&Node->pair);

        U16Pair_t in_pair = {
            .a = Node->left_num,
            .b = Node->right_num,
            .max = cnt,
        };
        U16Pair_t out = calc_next_uniq_u16pair(in_pair);
        Node->left_num = out.a;
        Node->right_num = out.b;
        LOG_DEBUG(CROSS_DETECT,"%u,%u",Node->left_num ,Node->right_num );

        if(   (cnt*cnt-cnt)<Node->pair_cnt ){
            if(0==(  Node->pair_cnt%(cnt*cnt-cnt)   )  ){
                uint32_t diratin_ms = time_get_ms32() - Node->start_ms;
                LOG_WARNING(CROSS_DETECT,"All %u PairsSolved!, %u ms",cnt*cnt-cnt,diratin_ms);
                Node->start_ms = time_get_ms32();
            }
        }
        Node->pair_cnt++;
        if(Node->left_num != Node->right_num) {
            res = cross_detect_init_pair(&Node->pair, CrossDetectPinConfig[Node->left_num].pad,
                                         CrossDetectPinConfig[Node->right_num].pad);
        }
    }

    return res;
}

static bool cross_detect_cycle_complete(CrossDetectHandle_t* const Node) {
    bool res = false;
    LOG_PARN(CROSS_DETECT, "CycleComplete");
    if(Node) {
        Node->pair.spin_cnt++;
        res = cross_detect_run_analitic(Node);
        if(CROSS_DETECT_TRY_PER_PAIR < Node->pair.spin_cnt) {
            res = cross_detect_set_next_pair(Node);
        }
    }
    return res;
}

static bool cross_detect_next_state(CrossDetectHandle_t* const Node) {
    bool res = true;
    LOG_PARN(CROSS_DETECT, "NextState");
    return res;
}

static bool cross_detect_start(CrossDetectHandle_t* const Node) {
    bool res = true;
    LOG_PARN(CROSS_DETECT, "Start");

    return res;
}

static const CrossDetectStateInfo_t StateTableLookUpTable[] = {
    [CROSS_DETECT_STATE_LA_RA] =
        {
            .pull_left = GPIO__PULL_AIR,
            .pull_right = GPIO__PULL_AIR,
            .action = cross_detect_start,
            .state_new = CROSS_DETECT_STATE_LU_RA,
        }, /*Left Pull air,Right pull air*/
    [CROSS_DETECT_STATE_LU_RA] =
        {
            .pull_left = GPIO__PULL_UP,
            .pull_right = GPIO__PULL_AIR,
            .action = cross_detect_next_state,
            .state_new = CROSS_DETECT_STATE_LD_RA,
        }, /*Left pull up,Right pull air*/
    [CROSS_DETECT_STATE_LD_RA] =
        {
            .pull_left = GPIO__PULL_DOWN,
            .pull_right = GPIO__PULL_AIR,
            .action = cross_detect_next_state,
            .state_new = CROSS_DETECT_STATE_LD_RD,
        }, /*Left pull down,Right pull air*/
    [CROSS_DETECT_STATE_LD_RD] =
        {
            .pull_left = GPIO__PULL_DOWN,
            .pull_right = GPIO__PULL_DOWN,
            .action = cross_detect_next_state,
            .state_new = CROSS_DETECT_STATE_LD_RU,
        }, /*Left pull down,Right pull down*/
    [CROSS_DETECT_STATE_LD_RU] =
        {
            .pull_left = GPIO__PULL_DOWN,
            .pull_right = GPIO__PULL_UP,
            .action = cross_detect_next_state,
            .state_new = CROSS_DETECT_STATE_LA_RU,
        }, /*Left pull down,Right pull up*/
    [CROSS_DETECT_STATE_LA_RU] =
        {
            .pull_left = GPIO__PULL_AIR,
            .pull_right = GPIO__PULL_UP,
            .action = cross_detect_next_state,
            .state_new = CROSS_DETECT_STATE_LU_RU,
        }, /*Left pull air,Right pull up*/
    [CROSS_DETECT_STATE_LU_RU] =
        {
            .pull_left = GPIO__PULL_UP,
            .pull_right = GPIO__PULL_UP,
            .action = cross_detect_next_state,
            .state_new = CROSS_DETECT_STATE_LU_RD,
        }, /*Left pull up,Right pull up*/
    [CROSS_DETECT_STATE_LU_RD] =
        {
            .pull_left = GPIO__PULL_UP,
            .pull_right = GPIO__PULL_DOWN,
            .action = cross_detect_next_state,
            .state_new = CROSS_DETECT_STATE_LA_RD,
        }, /*Left pull up,Right pull down*/
    [CROSS_DETECT_STATE_LA_RD] =
        {
            .pull_left = GPIO__PULL_AIR,
            .pull_right = GPIO__PULL_DOWN,
            .action = cross_detect_cycle_complete,
            .state_new = CROSS_DETECT_STATE_LA_RA,
        }, /*Left pull air,Right pull down*/
};

static bool cross_detect_state_set(CrossDetectPairInfo_t* const pair, CrossDetectState_t new_state) {
    bool res = false;
    if(pair) {
        uint8_t ok = 0;
        res = gpio_set_pull(pair->left.byte, StateTableLookUpTable[new_state].pull_left);
        if(res) {
            ok++;
        }

        res = gpio_set_pull(pair->right.byte, StateTableLookUpTable[new_state].pull_right);
        if(res) {
            ok++;
        }

        if(2 == ok) {
            pair->state = new_state;
            res = true;
        } else {
            LOG_ERROR(CROSS_DETECT, "SetStateErr %u=%s", new_state, CrossDetectState2Str(new_state));
            res = false;
        }
    }
    return res;
}


const CrossDetectSolutionInfo_t CrossDetectSolutionInfo[36] = {
    {
        /*1*/ .state = CROSS_DETECT_STATE_LA_RA,
        .read = READ_L0_R0,
        .left_short_gnd = CONF_NO,
        .right_short_gnd = CONF_NO,
        .left_short_vcc = CONF_NO,
        .right_short_vcc = CONF_NO,
        .cross = CONF_NO,
        .open = CONF_NO,
        .error = CONF_NO,
        .ok = CONF_YES,
    },

    {
        /*2*/ .state = CROSS_DETECT_STATE_LU_RA,
        .read = READ_L0_R0,
        .left_short_gnd = CONF_YES,
        .left_short_vcc = CONF_NO,
        .right_short_gnd = CONF_NO,
        .right_short_vcc = CONF_NO,
        .cross = CONF_MAYBE,
        .open = CONF_MAYBE,
        .error = CONF_YES,
        .ok = CONF_NO,
    },

    {
        /*3*/ .state = CROSS_DETECT_STATE_LD_RA,
        .read = READ_L0_R0,
        .left_short_gnd = CONF_MAYBE,
        .left_short_vcc = CONF_NO,
        .right_short_gnd = CONF_MAYBE,
        .right_short_vcc = CONF_NO,
        .cross = CONF_MAYBE,
        .open = CONF_MAYBE,
        .error = CONF_NO,
        .ok = CONF_YES,
    },

    {
        /*4*/ .state = CROSS_DETECT_STATE_LD_RD,
        .read = READ_L0_R0,
        .left_short_gnd = CONF_MAYBE,
        .left_short_vcc = CONF_NO,
        .right_short_gnd = CONF_MAYBE,
        .right_short_vcc = CONF_NO,
        .cross = CONF_MAYBE,
        .open = CONF_MAYBE,
        .error = CONF_NO,
        .ok = CONF_YES,
    },

    {
        /*5*/ .state = CROSS_DETECT_STATE_LD_RU,
        .read = READ_L0_R0,
        .left_short_gnd = CONF_MAYBE,
        .left_short_vcc = CONF_NO,
        .right_short_gnd = CONF_YES,
        .right_short_vcc = CONF_NO,
        .cross = CONF_MAYBE,
        .open = CONF_MAYBE,
        .error = CONF_YES,
        .ok = CONF_NO,
    },

    {
        /*6*/ .state = CROSS_DETECT_STATE_LA_RU,
        .read = READ_L0_R0,
        .left_short_gnd = CONF_MAYBE,
        .left_short_vcc = CONF_NO,
        .right_short_gnd = CONF_YES,
        .right_short_vcc = CONF_NO,
        .cross = CONF_MAYBE,
        .open = CONF_MAYBE,
        .error = CONF_YES,
        .ok = CONF_NO,
    },

    {/*7*/ .state = CROSS_DETECT_STATE_LU_RU,
     .read = READ_L0_R0,
     .left_short_gnd = CONF_YES,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_YES,
     .right_short_vcc = CONF_NO,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*8*/ .state = CROSS_DETECT_STATE_LU_RD,
     .read = READ_L0_R0,
     .left_short_gnd = CONF_YES,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_NO,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*9*/ .state = CROSS_DETECT_STATE_LA_RD,
     .read = READ_L0_R0,
     .left_short_gnd = CONF_MAYBE,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_NO,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_NO,
     .ok = CONF_YES},

    {/*10*/ .state = CROSS_DETECT_STATE_LA_RA,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_MAYBE,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*11*/ .state = CROSS_DETECT_STATE_LU_RA,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_YES,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*12*/ .state = CROSS_DETECT_STATE_LD_RA,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_MAYBE,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*13*/ .state = CROSS_DETECT_STATE_LD_RD,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_MAYBE,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_YES,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*14*/ .state = CROSS_DETECT_STATE_LD_RU,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_MAYBE,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_NO,
     .ok = CONF_YES},

    {/*15*/ .state = CROSS_DETECT_STATE_LA_RU,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_MAYBE,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_NO,
     .ok = CONF_YES},

    {/*16*/ .state = CROSS_DETECT_STATE_LU_RU,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_YES,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*17*/ .state = CROSS_DETECT_STATE_LU_RD,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_YES,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_YES,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*18*/ .state = CROSS_DETECT_STATE_LA_RD,
     .read = READ_L0_R1,
     .left_short_gnd = CONF_MAYBE,
     .left_short_vcc = CONF_NO,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_YES,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*19*/ .state = CROSS_DETECT_STATE_LA_RA,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*20*/ .state = CROSS_DETECT_STATE_LU_RA,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_NO,
     .ok = CONF_YES},

    {/*21*/ .state = CROSS_DETECT_STATE_LD_RA,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_YES,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*22*/ .state = CROSS_DETECT_STATE_LD_RD,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_YES,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*23*/ .state = CROSS_DETECT_STATE_LD_RU,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_YES,
     .right_short_gnd = CONF_YES,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*24*/ .state = CROSS_DETECT_STATE_LA_RU,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_YES,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*25*/ .state = CROSS_DETECT_STATE_LU_RU,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_YES,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*26*/ .state = CROSS_DETECT_STATE_LU_RD,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_NO,
     .ok = CONF_YES},

    {/*27*/ .state = CROSS_DETECT_STATE_LA_RD,
     .read = READ_L1_R0,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_NO,
     .cross = CONF_NO,
     .open = CONF_YES,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*28*/ .state = CROSS_DETECT_STATE_LA_RA,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*29*/ .state = CROSS_DETECT_STATE_LU_RA,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_YES,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*30*/ .state = CROSS_DETECT_STATE_LD_RA,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_YES,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*31*/ .state = CROSS_DETECT_STATE_LD_RD,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_YES,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_YES,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*32*/ .state = CROSS_DETECT_STATE_LD_RU,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_YES,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*33*/ .state = CROSS_DETECT_STATE_LA_RU,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_YES,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*34*/ .state = CROSS_DETECT_STATE_LU_RU,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_MAYBE,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_NO,
     .ok = CONF_YES},

    {/*35*/ .state = CROSS_DETECT_STATE_LU_RD,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_NO,
     .right_short_vcc = CONF_YES,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},

    {/*36*/ .state = CROSS_DETECT_STATE_LA_RD,
     .read = READ_L1_R1,
     .left_short_gnd = CONF_NO,
     .left_short_vcc = CONF_MAYBE,
     .right_short_gnd = CONF_MAYBE,
     .right_short_vcc = CONF_YES,
     .cross = CONF_MAYBE,
     .open = CONF_MAYBE,
     .error = CONF_YES,
     .ok = CONF_NO},
};

uint32_t cross_detect_get_lut_size(void) {
    uint32_t cnt = ARRAY_SIZE(CrossDetectSolutionInfo);
    return cnt;
}

static const CrossDetectSolutionInfo_t* GetSolNode(const CrossDetectStateMeasure_t* const measure_node) {
    const CrossDetectSolutionInfo_t* LutNode = NULL;
    uint32_t cnt = cross_detect_get_lut_size( );
    uint32_t i = 0;
    for(i = 0; i < cnt; i++) {
        if(CrossDetectSolutionInfo[i].state == measure_node->state) {
            if(CrossDetectSolutionInfo[i].read == measure_node->read) {
                LutNode = &CrossDetectSolutionInfo[i];
                break;
            }
        }
    }
    return LutNode;
}

CrossDetectHandle_t* CrossDetectGetNode(uint8_t num) {
    CrossDetectHandle_t* Node = NULL;
    uint32_t i = 0;
    uint32_t cnt = cross_detect_get_cnt();
    for(i = 0; i < cnt; i++) {
        if(num == CrossDetectInstance[i].num) {
            if(CrossDetectInstance[i].valid) {
                Node = &CrossDetectInstance[i];
                break;
            }
        }
    }
    return Node;
}

const CrossDetectConfig_t* CrossDetectGetConfNode(uint8_t num) {
    const CrossDetectConfig_t* Config = NULL;
    uint32_t i = 0;
    uint32_t cnt = cross_detect_get_cnt();
    for(i = 0; i < cnt; i++) {
        if(num == CrossDetectConfig[i].num) {
            if(CrossDetectConfig[i].valid) {
                Config = &CrossDetectConfig[i];
                break;
            }
        }
    }
    return Config;
}

bool cross_detect_init_pair(CrossDetectPairInfo_t* const pair, Pad_t left, Pad_t right) {
    bool res = false;
    if(pair) {
        if(left.byte != right.byte) {
            pair->left = left;
            pair->right = right;
            pair->solution = CROSS_DETECT_SOL_UNDEF;
            //pair->prev_solution = CROSS_DETECT_SOL_UNDEF;
            pair->state = CROSS_DETECT_STATE_LA_RA;
            pair->spin_cnt = 0;
            pair->err_cnt = 0;
            pair->time_start = 0;
            pair->pause_ms = 0;
            pair->init = true;
            CrossDetectDiagPair(pair);
            res = cross_detect_state_set(pair, CROSS_DETECT_STATE_LA_RA);
        }
    }
    return res;
}

bool cross_detect_reset_pair(const CrossDetectPairInfo_t* const pair){
    bool res = false;
    res = gpio_set_pull(pair->left.byte, GPIO__PULL_AIR);
    res = gpio_set_pull(pair->right.byte, GPIO__PULL_AIR);
    return res;
}

bool cross_detect_enable(uint8_t num, bool on_off) {
    bool res = false;
    CrossDetectHandle_t* Node = CrossDetectGetNode(num);
    if(Node) {
        Node->on = on_off;
        res = true;
    }
    return res;
}

static bool cross_detect_init_pin(const CrossDetectPinConfig_t* const PinConfig) {
    bool res = false;
    if(PinConfig) {
            uint8_t ok = 0;
            LOG_WARNING(CROSS_DETECT, "InitPad: %s In PullAir", GpioPad2Str(PinConfig->pad.byte));

            res = gpio_set_dir(PinConfig->pad.byte, GPIO_DIR_IN);
            if(res) {
                ok++;
            } else {
                LOG_ERROR(CROSS_DETECT, "Pad: %s SetDirIn Err", GpioPad2Str(PinConfig->pad.byte));
            }

            res = gpio_set_pull(PinConfig->pad.byte, GPIO__PULL_AIR);
            if(res) {
                ok++;
            } else {
                LOG_ERROR(CROSS_DETECT, "Pad: %s SetPullAir Err", GpioPad2Str(PinConfig->pad.byte));
            }

#ifdef HAS_NRF5340
            res = gpio_set_pin_mcu(PinConfig->pad, NRF_GPIO_PIN_MUX_APP);
            if(res) {
                ok++;
            } else {
                LOG_ERROR(CROSS_DETECT, "Pad: %s SetAppCore Err", GpioPad2Str(PinConfig->pad.byte));
            }
#endif

            if(3 == ok) {
                res = true;
            } else {
                res = false;
            }

    }

    return res;
}

bool cross_detect_init_pins(uint8_t num) {
    bool res = false;
    uint32_t pin_cnt = cross_detect_get_pin_cnt();
    LOG_WARNING(CROSS_DETECT, "LD%u Init %u Pins", num, pin_cnt);
    uint32_t i;
    uint32_t ok = 0;
    for(i = 0; i < pin_cnt; i++) {
        if(num == CrossDetectPinConfig[i].num) {
            res = cross_detect_init_pin(&CrossDetectPinConfig[i]);
            if(res) {
                ok++;
                LOG_DEBUG(CROSS_DETECT, "InitPin %s Ok", GpioPadToStr(CrossDetectPinConfig[i].pad));
            } else {
                LOG_ERROR(CROSS_DETECT, "InitPinErr %d", num);
            }
        }
    }
    if(0 < ok) {
        res = true;
    }
    return res;
}


bool cross_detect_init_one(uint8_t num) {
    bool res = false;
    LOG_WARNING(CROSS_DETECT, "Init %d", num);
    const CrossDetectConfig_t* Config = CrossDetectGetConfNode(num);
    if(Config) {
        CrossDetectHandle_t* Node = CrossDetectGetNode(num);
        if(Node) {
            Node->on = true;
            Node->init = true;
            Node->valid = true;
            Node->pair_cnt = 0;
            Node->left_num = 0;
            Node->right_num = 1;
            Node->start_ms = time_get_ms32();
            //size_t size = sizeof(CrossDetectResult_t) * CROSS_DETECT_PIN_CNT * CROSS_DETECT_PIN_CNT;
            //LOG_INFO(CROSS_DETECT, "ZeroSize %u byte of result array", size);
            uint32_t l, r;
            uint32_t cnt = cross_detect_get_pin_cnt();
            for(l=0;l<cnt;l++){
                for(r=0;r<cnt;r++){
                    CrossDetectResult[l][r].Fault.fault_code=0;
                    CrossDetectResult[l][r].FaultPrev.fault_code=0;
                }
            }

            res = cross_detect_init_pins(num);
            if(res) {
                LOG_INFO(CROSS_DETECT, "%u InitPinsOk", num);
            } else {
                LOG_ERROR(CROSS_DETECT, "%u InitPinsErr", num);
            }


            res = cross_detect_init_pair(&Node->pair, CrossDetectPinConfig[Node->left_num].pad,
                                         CrossDetectPinConfig[Node->right_num].pad);
            if(res){

            }else{
                LOG_ERROR(CROSS_DETECT, "InitPair %u,%u Err",Node->left_num, Node->right_num);
            }
        } else {
            LOG_ERROR(CROSS_DETECT, "%u NodeErr", num);
        }
    } else {
        LOG_ERROR(CROSS_DETECT, "%u ConfErr", num);
    }
    return res;
}

bool cross_detect_init(void) {
    bool res = false;
    set_log_level(CROSS_DETECT, LOG_LEVEL_DEBUG);
    uint32_t cnt = cross_detect_get_cnt();
    uint32_t ok = 0;
    LOG_WARNING(CROSS_DETECT, "Init Cnt %d", cnt);

    uint32_t i = 0;
    for(i = 1; i <= cnt; i++) {
        res = cross_detect_init_one(i);
        if(res) {
            ok++;
            LOG_INFO(CROSS_DETECT, "LD%u InitOk", i);
        } else {
            LOG_ERROR(CROSS_DETECT, "LD%u InitErr", i);
        }
    }

    if(ok) {
        res = true;
        LOG_INFO(CROSS_DETECT, "Init %u Ok", ok);
    } else {
        res = false;
        LOG_ERROR(CROSS_DETECT, "InitErr");
    }

    set_log_level(CROSS_DETECT, LOG_LEVEL_INFO);
    return res;
}

static bool cross_detect_calc_fault(CrossDetectPairInfo_t* const pair, CrossDetectState_t state) {
    bool res = false;
    uint32_t i = 0;
    /*Are any faults found in this state?*/
    for(i = 0; i < 9; i++) {
        if(pair->measurements[i].state == state) {
            const CrossDetectSolutionInfo_t* LutNode = GetSolNode(&pair->measurements[i]);
            if(LutNode) {
                //if(CONF_YES == LutNode->cross) {
                //    pair->Fault.cross = 1;
                //}
                if(CONF_YES == LutNode->left_short_gnd) {
                    pair->Fault.left_short_gnd = 1;
                }
                if(CONF_YES == LutNode->left_short_vcc) {
                    pair->Fault.left_short_vcc = 1;
                }
                if(CONF_YES == LutNode->right_short_gnd) {
                    pair->Fault.right_short_gnd = 1;
                }
                if(CONF_YES == LutNode->right_short_vcc) {
                    pair->Fault.right_short_vcc = 1;
                }
                res = true;
            }else{
                LOG_ERROR(CROSS_DETECT,"UndefCase State %u, Read %u",state,pair->measurements[i].read);
            }
            break;
        }
    }
    return res;
}

static bool cross_detect_measure(CrossDetectPairInfo_t* const pair) {
    bool res = false;
    if(pair) {
        LOG_PARN(CROSS_DETECT, "MeasureGPIOin %s", CrossDetectState2Str(pair->state));

        GpioLogicLevel_t logic_left=GPIO_LVL_UNDEF;
        GpioLogicLevel_t logic_right=GPIO_LVL_UNDEF;
        res = gpio_get_state(pair->left.byte, &logic_left);
        res = gpio_get_state(pair->right.byte, &logic_right);

        if(GPIO_LVL_HI == logic_left) {
            if(GPIO_LVL_HI == logic_right) {
                pair->measurements[pair->state].read = READ_L1_R1;
            } else {
                pair->measurements[pair->state].read = READ_L1_R0;
            }
        } else {
            if(GPIO_LVL_HI == logic_right) {
                pair->measurements[pair->state].read = READ_L0_R1;
            } else {
                pair->measurements[pair->state].read = READ_L0_R0;
            }
        }

        pair->measurements[pair->state].state = pair->state;
    }
    return res;
}

/*UP->AIR->Down->Up*/
bool cross_detect_proc_one(uint8_t num) {
    bool res = false;
    LOG_PARN(CROSS_DETECT, "Proc:%u", num);
    CrossDetectHandle_t* Node = CrossDetectGetNode(num);
    if(Node) {
        if(Node->on) {
            /*Measure GPIO logic levels */
            res = cross_detect_measure(&Node->pair);
            if(res) {
                LOG_PARN(CROSS_DETECT, "MeasureGPIOOk");
            } else {
                Node->pair.err_cnt++;
                LOG_ERROR(CROSS_DETECT, "MeaseireGPIOErr");
            }

            CrossDetectState_t new_state = StateTableLookUpTable[Node->pair.state].state_new;
            CrossDetectHandler_t action = StateTableLookUpTable[Node->pair.state].action;
            if(action) {
                res = action(Node);
            }

            res = cross_detect_state_set(&Node->pair, new_state);
            if(res) {
                LOG_PARN(CROSS_DETECT, "SetStateOk %u=%s", new_state, CrossDetectState2Str(new_state));
            } else {
                LOG_ERROR(CROSS_DETECT, "SetStateErr %u=%s", new_state, CrossDetectState2Str(new_state));
            }
        } else {
            LOG_DEBUG(CROSS_DETECT, "Off %u", num);
        }
    } else {
        LOG_ERROR(CROSS_DETECT, "NodeErr %u", num);
    }
    return res;
}

bool cross_detect_proc(void) {
    bool res = false;
    uint8_t ok = 0;
    uint8_t cnt = cross_detect_get_cnt();
    LOG_PARN(CROSS_DETECT, "Proc Cnt:%u", cnt);
    for(uint32_t i = 1; i <= cnt; i++) {
        res = cross_detect_proc_one(i);
        if(res) {
            ok++;
        }
    }

    if(ok) {
        res = true;
    } else {
        res = false;
    }

    return res;
}

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

Фаза 9. Отладка

Хорошо. Код мы написали. Но как получить отчет работы программного компонента cross-detect?

Обычно для тестов на пропай для каждой версии платы собирают отдельную сборку прошивки с суффиксом IO-Bang. Эта прошивка как раз и содержит компонент cross-detect. Также там есть UART-CLI для связи c внешним миром, а все пины сконфигурированы на вход.

Вот тут-то нам и пригодится интерфейс командной строки поверх UART. Про него есть отдельный текст https://habr.com/ru/articles/694408/. Соединяем плату и LapTop переходником USB-UART, открываем программу TeraTerm, нажимаем help и видим набор доступных команд компонента cross-detect.

Так как этот конечный автомат разрабатывался прежде всего для определения замыканий соседних пинов, то вот это первым делом и проверим. Как видно прошивка в run-time распознала установленную проводную перемычку P0.04-P0.05. По сути получилась прозвонка цепи подобно тому как это происходит в DMM.

Вот отчет в TeraTerm о том, что пин P0.24 замкнул на GND.

Вот регистрация короткого замыкания на VCC

Этот конечный автомат проверил 6 проводников каждый с каждым за 22 секунды.

Достоинства cross-detect

1++ Использование этого способа тестирования практически ничего не стоит. Всё можно написать с нуля за 3 дня.

2++Это чисто софтверный способ тестирования. Из оборудование нужен только копеечный переходник USB-UART.

3++ Программный компонент cross-detect позволяет определять все 4 типа возможных неисправностей:

Пример неисправности

1

короткое замыкание на GND

2

короткое замыкание на VCC

3

короткое замыкание любых двух проводников на плате друг на друга

4

обрыв нагрузки между пинами

4++ Сross-detect может выявить короткое замыкание (КЗ) пинов прямо в BGA корпусах, где даже щупом осциллографа или pogo pins к пинам не подлезть! С BGA у Вас только два варианта: просвечивать плату с BGA рентгеновским сканером (X-ray) и глазами искать КЗ между пинами, либо прогонять сross-detect.

Что можно ещё улучшить?
На самом деле исходный код cross-detect вовсе не обязательно исполнять на target устройстве. Например на ARM Cortex-M4 или ARM Cortex-M33. cross-detect - просто алгоритм. Можно cоединить PC и Target PCB интерфейсом JTAG через микросхему переходник USB-JTAG (FT232H), написать на DeskTop консольное приложение симулятор прошивки с CLIшкой в stdin/stdout (или GUI) и прокручивать этот же самый конечный автомат опроса GPIO прямо на LapTop(е) NetTop(e).
По сути надо заменить CMSIS на код драйвера переходника USB-JTAG, чтобы читать и писать физическую память микроконтроллера. Дело в том, что через интерфейс JTAG можно получить тотальный доступ ко всем внутренностям микроконтроллера или микропроцессора (GPIO, SPI, PLL, RCC, UART и прочее).

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

Если говорить метафорами, то получается микроконтроллер-марионетка. Вместо куклы микроконтроллер, вместо ниточек 4 провода JTAG. А кукловод это Host PC.

По сути те же утилиты для прошивки по JTAG (например STM32 ST-LINK Utility.exe) так и работают. Они по JTAG подключаются к карте памяти микроконтроллера, обращаются к контроллеру Flash памяти, снимают защиту на запись, записывают по частям бинарь, включают обратно защиту на запись Flash и перезагружают ядро микроконтроллера.

Плюсы теста на пропай через JTAG

1++Утилиту Firmware Simulator можно писать абсолютно на любом моднявом языке программирования (Python, С#, C++, Go, Java, C).

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

3++Также вы сможете протестировать пины самого UART до того как его впервые включат.

4++УJTAG высокая битовая скорость 30MHz. Это в 30--60 раз выше чем по UART

Вывод

Если говорить метафорично, то cross-detect это такие гномы, которые бегают по электронной плате и находят брак в производстве PCB. Вообще подтяжки напряжения просто идеально подходят для распознавания коротко-замкнутых проводов.

По сути плата тестирует сама себя изнутри.

Теперь и вы умеете пользоваться компонентом cross-detect и можете учить других. Надеюсь этот текст поможет программистам микроконтроллеров быстро и эффективно находить PCB брак в прототипах электронных плат.

Как видите благодаря этому остроумному алгоритму можно софтом определить замыкания на плате!

Словарь для понимания текста и комментариев

Акроним

Расшифровка

GPIO

General-purpose input/output

V

Volts

ISR

Interrupt Service Routine

CMSIS

Common Microcontroller Software Interface Standard

CLI

command-line interface

ОТК

Отдел технического контроля

UART

universal asynchronous receiver / transmitter

PCB

printed circuit board

КЗ

короткое замыкание

BGA

Ball grid array

DFM

design for manufacturability

FSM

Finite-state machine

MPSSE

Multi-Protocol Synchronous Serial Engine

EoL 

End-of-life

MCAL

MicroController Absorption Layer

DMM

digital multimeter

МК

Микроконтроллер

MCU

Micro Controller Unit

GCC

GNU Compiler Collection, GNU-GNU’s Not UNIX

UNIX/UNICS

Uniplexed Information and Computing System

ЛУТ

Лазерно-утюжная технология изготовления печатных плат

IO

Input/Output

JTAG

Joint Test Action Group

NDA

Non-disclosure agreement

SWD

Serial Wire Debug

ПО

Программное обеспечение

ОС

operating system

ЭВМ

Электронная вычислительная машина

Links

Load-Detect для Проверки Качества Пайки https://habr.com/ru/articles/756572/

H-мост: Load Detect (или как выявлять вандализм) https://habr.com/ru/articles/709374/

Все таблички из текста можно просмотреть тут https://docs.google.com/spreadsheets/d/1Bs4YaRxqCRQDz73HKJhUfD-evO-U6rQLS0fySpSOWG8/edit#gid=2020010813

Что такое UART-CLI https://habr.com/ru/articles/694408/

Контрольные вопросы:

1--Как выявить короткое замыкание между соседними пинами в уже припаянной BGA микросхеме?

2--Электронная плата с производства не работает. Что Вы предпримете чтобы выявить причину поломки?

3--Чем конечный автомат Мура отличается от конечного автомата Мили?

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


  1. HEXFFFFFFFF
    23.09.2023 15:07
    +10

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

    Гораздо эффективней (и я всегда так делаю) написать тестовую прошивку которая проверяет всю переферию. Это может быть отдельная программа , а может быть модуль в основном софте запусаемый каким то хитрым образом. Он же может использоватся и при последующим ремонте.


    1. aabzel Автор
      23.09.2023 15:07
      +1

      Этот программный компонент реально работает и уже несколько раз помог найти КЗ в платах.


    1. aabzel Автор
      23.09.2023 15:07
      +1

      Гораздо эффективней (и я всегда так делаю) написать тестовую прошивку которая проверяет всю переферию. 

      Вот вам пример из жизни. При инициализации шины I2S микроконтроллер перезагружается. На другой плате этого не происходит. Как вы поймете какой из 5х проводов шины I2S коротит на UART TX?


      1. cujos
        23.09.2023 15:07
        +5

        а зачем определять? выглядит как брак

        ремонтировать после EoL немного не корректно, нужно работать с отделом качества контрактников
        кмк вещь полезная, чтобы браковать, но зачем понимать какой из проводов куда коротит?
        проверил функционал - брак/не брак и достаточно


        1. aabzel Автор
          23.09.2023 15:07
          +1

          а зачем определять? Зачем понимать какой из проводов куда коротит?

          Разработчику надо не только выявить факт брака, но также надо распознать причину брака.

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



          1. cujos
            23.09.2023 15:07
            +1

            причина в некачественной пайке, этим должен заниматься тот, кто паял
            как я уже сказал осуществлять ремонт после того, как он прошел контроль не корректно
            разве что на единичных сборках/мелких сериях/прототипах, у меня вот лежит куча с браком, раньше процент был больше, со временем уменьшается, но стабильно пара процентов уходит: убивает что-то статикой, непропай, повышенное потребление, выходит за расчет аналоговых параметров - не важно
            разбираться, а уж тем более менять что-то после автоматического монтажа - бред
            долго, дорого и не качественно


            1. aabzel Автор
              23.09.2023 15:07
              +1

              причина в некачественной пайке, этим должен заниматься тот, кто паял

              Ну вот включил программист-микроконтрллеров плату и плата не работает. Разработчик идет в цэх, подходит к монтажнице и говорит:

              -плата которую ты собрала не работает

              монтажница в ответ:
              --Ок, что именно не работает и что мне надо поменять?

              Разработчик прикидывается валенком и говорит:
              --Да я особо не разбирался. Не знаю. Смотрю не работает и принес тебе.

              Так что-ли надо работать по Вашему?


              1. iig
                23.09.2023 15:07
                +2

                монтажница в ответ:
                --Ок, что именно не работает и что мне надо поменять?

                Что может поменять монтажница в ручной пайка BGA?


                1. aabzel Автор
                  23.09.2023 15:07
                  -1

                  Вот cross-detect как раз и локализует аппаратный баг.
                  Далее будет уже 1, 2 варианта решения аппаратной ошибки.


            1. aabzel Автор
              23.09.2023 15:07
              +1

              как я уже сказал осуществлять ремонт после того, как он прошел контроль не корректно

              Вот нет никакого контроля. Плату спаяли тётушки бальзаковского возраста, визуально глазками посмотрели на PCB (ничего такого не заметили) и передали программисту-микроконтроллеров.

              Программист-микроконтроллеров прошил плату релизной прошивкой и видит, что плата не работает.

              Что делать?


              1. cujos
                23.09.2023 15:07
                +4

                отдавать на нормальный монтаж


                1. aabzel Автор
                  23.09.2023 15:07

                  Ошибки не только в монтаже бывают, но и в самом дизайне PCB тоже.

                  Вот Вам яркий пример.
                  Схемотехники для RS2058 взяли для Э3 распиновку от корпуса MSOP10, а на PCB поставили корпус QFN.

                  В результате мультиплексор не пропускает сигнал так как распиновка в MSOP10 и QFN не совпадает на 90%.

                  Никто на этаже не мог понять что не так.

                  Зато мой еще прошлый примитивный load-detect
                  https://habr.com/ru/articles/756572/
                  выявил место проблемы на первой же секунде своей работы.


                  1. cujos
                    23.09.2023 15:07
                    +2

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


                    1. aabzel Автор
                      23.09.2023 15:07
                      +2

                      их нужно до серии выявлять


                      Для серии обязательно надо строить полноценные test jig(и). Типа таких
                      https://thirdpin.io/stendy_testirovaniya_elektroniki_i_pechatnyh_plat

                      Cross-detect хорош на стадии проверки первого, второго прототипа PCB. Когда еще нет ни одной test jig и окончательного решения о запуске массового производства.


                    1. aumi13
                      23.09.2023 15:07
                      +1

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


                  1. Karlson_rwa
                    23.09.2023 15:07
                    +2

                    Схемотехники для RS2058 взяли для Э3 распиновку от корпуса MSOP10, а на PCB поставили корпус QFN.

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


                    Никто на этаже не мог понять что не так.

                    Бегите из этого места как можно скорее. :)


                    1. aabzel Автор
                      23.09.2023 15:07
                      -1

                      Бегите из этого места как можно скорее. :)

                      Куда бежать? В РФ везде так.


                      1. Karlson_rwa
                        23.09.2023 15:07
                        +2

                        Куда бежать? В РФ везде так.

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


              1. randomsimplenumber
                23.09.2023 15:07
                +2

                Плату спаяли тётушки бальзаковского возраста

                Бальзак, "Тридцатилетняя женщина". Что за эйджизм ;)

                Так то у вас получился аналог автотестов, но для железа. Если тесты прошли - это ничего не значит. Если упали - значит ошибки точно есть.


      1. iig
        23.09.2023 15:07
        +1

        Как вы поймете какой из 5х проводов шины I2S коротит на UART TX?

        Элементарно, Ватсон ;)
        Тот который рядом.


        Заголовок спойлера

        Напряжометр с пищалкой в помощь ;)


        1. aabzel Автор
          23.09.2023 15:07

          Это потом выяснилось что I2S c UART коротит.
          Cross-deteсt как раз показал.

          Сначала вообще не ясно было что не так. Тем же DMM прозванивать - это бы целый день заняло а, то и неделю.

          А сross-deteсt все нежелательные перемычки за минуту найдет


          1. iig
            23.09.2023 15:07
            +1

            целый день заняло а, то и неделю

            Нет смысла проверять КЗ любых 2 точек на плате. Имеет смысл проверять близко расположенные, и непосредственно связанные с МК. Вряд ли это займет весь день.
            Хотя, конечно, автоматизированное тестирование найдет ошибку бесплатно (если ошибка действительно связана с КЗ ножек МК).


        1. aabzel Автор
          23.09.2023 15:07

          Тот который рядом.

          У программистом MCU на компе нет топологии всех 4х внутренних слоев PCB, чтобы понять кто там рядом проложен, а кто нет.

          Да и тест падов на PCB тоже нет, чтобы приложить электроды DMM для прозвонки.


          1. iig
            23.09.2023 15:07
            +1

            Если закорочены внутренние слои, тут, пожалуй, какие вопросы к монтажникам?


            1. aabzel Автор
              23.09.2023 15:07

              Программисты тем более вообще имеют право ничего не знать о топологии.


            1. aabzel Автор
              23.09.2023 15:07

              Тогда плату надо тестировать еще до того как на нее компоненты будут припаяны. Нужно для каждой PCB test jig строить.


              1. iig
                23.09.2023 15:07
                +1

                Вроде как производство печатных плат достаточно стабильно. Вы же не ЛУТ практикуете дла 4-слойных плат?


                1. aabzel Автор
                  23.09.2023 15:07

                  Пробовал ЛУТ для однослойки. Слишком много мороки и брака.


              1. segment
                23.09.2023 15:07
                +1

                Ээ, так и тестируют платы, если того требует сложность платы. Для этого есть тот же flying probe.


                1. aabzel Автор
                  23.09.2023 15:07

                  В Росcии есть хоть одна фирма, которая разработала собственное оборудование для flying probe ?


                  1. segment
                    23.09.2023 15:07
                    +1

                    Мне кажется тут не так важно есть ли собственное оборудование или нет, тк в рф оно вряд ли появится в скором времени. Тот же Резонит предоставляет электроконтроль через fly probe уже давно.


    1. aabzel Автор
      23.09.2023 15:07
      -1

      Пытаться софтом определить замыкания на плате бесполезное занятие.

      Видимо Вы ещё очень мало времени занимаететь программированием микроконтроллеров.


    1. aabzel Автор
      23.09.2023 15:07

      Зачем так сложно о простом?

      Где Вы тут сложность-то увидели? Конечный автомат на 9 состояний для Вас сложно?


    1. aabzel Автор
      23.09.2023 15:07
      +1

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

      Что дешевле: строить для каждой платы тестировочный стенд с pogo pins

      https://thirdpin.io/stendy_testirovaniya_elektroniki_i_pechatnyh_plat


      или залить прошивку IoBang с Cross-detect(ом)?


      1. segment
        23.09.2023 15:07
        +2

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


        1. aabzel Автор
          23.09.2023 15:07
          +1

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

          Как Вы собираетесь pogo pin(ами) от test jig подбираться пинам BGA микросхем , чтобы выявить короткое замыкание на соседних пинах?



          1. segment
            23.09.2023 15:07
            +2

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


            1. aabzel Автор
              23.09.2023 15:07

              И зачем заводить какой-то софт, который сможет всего лишь частично определить проблемное место?

              Чтобы в следующей ревизии оптимизировать трассировку PCB.


              1. segment
                23.09.2023 15:07
                +3

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


          1. Perycalypsis
            23.09.2023 15:07
            +3

            Давно изобретен автоматический визуальный и рентген контроль на производстве пп и монтаже. Если требования DFM выполнены, то все работает. После приемки нужен только функциональный контроль - работает или нет через тестовую прошивку или часть загрузчика. Брак в помойку или возврат на производство, когда накопится. А при отладке пары плат это делается ручками и глазами + рентген для бга.

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


          1. Karlson_rwa
            23.09.2023 15:07
            +4

            от test jig подбираться пинам BGA микросхем, чтобы выявить короткое замыкание на соседних пинах?

            Для этого есть JTAG.


      1. a9d
        23.09.2023 15:07
        +1

        собрать тестинг джиг стоит где-то 150$. Без него один хрен в серию ничего не запустить. На производстве же завернут и пошлют лесом


    1. aabzel Автор
      23.09.2023 15:07

      Зачем так сложно о простом?


      Вот Вам механизм попроще Load-Detect для Проверки Качества Пайки
      https://habr.com/ru/articles/756572/
      но он не умеет определять короткие замыкания между дорожками PCB. Зато просто.


  1. aystarik
    23.09.2023 15:07
    +3

    а почему не JTAG + boundary scan?


    1. aabzel Автор
      23.09.2023 15:07
      -2

      Есть где методичка как пользоваться этим JTAG + boundary scan ? Какое нужно оборудование, какой софтвер?

      Ну поставит программатор в boundary scan на пине P0.04 3.3V, а дальше-то что?


      boundary scan применим только тогда когда у тебя на плате 2+ микросхем с JTAG интерфейсом.
      А в микроконтроллерных проектах только одна JTAG микросхема (микроконтроллер).


      Можно что угодно ставить на GPIO через JTAG, а толу-то?

      И вообще на большинстве электронных плат с MCU на разъёмы выводят не JTAG, а двухпроводной интерфейс SWD.


      1. aystarik
        23.09.2023 15:07
        +2

        1. aystarik
          23.09.2023 15:07
          +1

          https://github.com/viveris/jtag-boundary-scanner --вот здесь уже софт какой-то есть...


          1. aystarik
            23.09.2023 15:07
            +1

            1. aabzel Автор
              23.09.2023 15:07
              -3



              Лучше бы господа Кауркин Максим Николаевич , Стариковский Алексей Юрьевич , Гурин Константин Львович написали бы пост на habr про свой уникальный опыт управления процессором по JTAG чем заполнять эти мнимые конторские свидетельства.

              Тогда бы их труд хоть кто-то реально прочитал и прокомментировал.

              Наверное еще текст своих исходников на миллиметровой бумаге печатали чтобы их рассмотрели.


              1. aystarik
                23.09.2023 15:07
                +7

                Стариковский Алексей Юрьевич перед вами, спрашивайте, что вас интересует...


                1. aabzel Автор
                  23.09.2023 15:07

                  --Какое устройство выступало в роли переходника с USB-JTAG? Подойдет ST-Link V2 ISOL?

                  --Какой был программный API для управления отладчиком на стороне PC приложения?

                  --Как обрабатывались аппаратные прерывания по переполнению таймера на target?


                  1. aystarik
                    23.09.2023 15:07
                    +2

                    FTDI FT232, собственная библиотека для MPSSE. ST-LINK -- не подойдет, он на stm32f1/stm32f7


                    1. aystarik
                      23.09.2023 15:07
                      +2

                      еще был lua интерфейс для скриптов для DFT, но это уже в это свидетельсво не попало


                    1. aabzel Автор
                      23.09.2023 15:07
                      -1

                      Я открыл спеку на FT232 , но там нет даже слов таких как JTAG.


                      1. aystarik
                        23.09.2023 15:07
                        +4

                        https://ftdichip.com/products/ft232hq/ не все ft232 одинаково полезны


                      1. aystarik
                        23.09.2023 15:07
                        +2

                        https://aliexpress.ru/item/1005004864054361.html -- вот платка готовая


                1. aabzel Автор
                  23.09.2023 15:07
                  +1

                  Почему нельзя было воспользоваться для этого не JTAG, а UART?

                  Можно же написать прошивку для процессора с UART-CLI, где будет всего 2 команды: чтение физического адреса и запись физического адреса.

                  Залить прошивку в ROM процессора.

                  Далее соединить PC и PCB переходником USB-UART и с тем же успехом утилитой на PC читать и писать регистры. Интерпретировать их по datasheet и отрисовывать красивую GUI.


                  Переходники USB-UART они же дешевле чем переходники USB-JTAG.


                  1. aystarik
                    23.09.2023 15:07
                    +5

                    потому, что это "взрослый" чип, у него память -- это DDR3 снаружи, а через JTAG мы могли получить доступ ко всему, даже при полностью мертвом процессоре. FT232 стоит не сильно дороже FT230, а если вместе с платой, то разницы уже не видно. JTAG можно до 30+МГц разогнать, UART так не умеет.


                    1. aabzel Автор
                      23.09.2023 15:07

                      Получается что если делать то же только для MCU, то можно на PC написать внешнюю прошивку размером с гигабайт в бинаре, которая никогда бы не поместилась бы во on-chip Flash память.

                      Вот только не ясно откуда микроконтроллер будет исполнять код прерываний (внешнее прерывание по нажатию кнопки) если прошивка внешняя и крутится на PC.
                      Из on-chip RAM памяти что-ли?

                      Тогда PC еще должен будет исполнять роль и компоновщика и интерпретатора одновременно.


                      1. aystarik
                        23.09.2023 15:07
                        +2

                        вы зачем-то мешаете в одну кучу отладку переферии и проверку внешних соединений с отладкой программы на ядре контроллера. Первое можно делать внешней программой через прямой доступ к регистрам устройств и к ножкам по boundary-scan. отладку программы в ядре (прерывания) уже можно отлаживать с участием собственно ядра... И откуда берутся "гигабайты в бинаре", сколько функциональности хотите, столько и будет размера...


                      1. aabzel Автор
                        23.09.2023 15:07

                        Ясно.
                        Берем прошивку. Меняем CMSIS на API управления USB-JTAG и прокручиваем всё тот же cross-dectct.
                        Только не на Target, а на PC.


                      1. aystarik
                        23.09.2023 15:07
                        +3

                        Нет, проблемы с пайкой все еще проще делать через boundary-scan :)


                      1. aabzel Автор
                        23.09.2023 15:07
                        +1

                         boundary-scan позволяет устанавливать подтяжки к VCC/GND


                    1. aabzel Автор
                      23.09.2023 15:07
                      +1

                      что это "взрослый" чип

                      Даже в CPU процессорах Cortex-A есть несколько десятков килобайт для on-chip BOOT ROM(а). Например 1892ВМ14Я


                      1. aystarik
                        23.09.2023 15:07
                        +2

                        и? в Байкал-Т тоже был BOOT ROM MONITOR, доступный через консоль. Только вот что делать, если консоль не отвечает?


                      1. aabzel Автор
                        23.09.2023 15:07

                        Значат что-то с проводами, либо прошивка на target зависла.

                        Мне в микроконтроллерах JTAG нужен только для пошаговой отладки, в случае если CLI не отвечает на команды из TeraTerm.

                        Беру очередной MCU, настраиваю на нем UART-Shell, а далее отладка идет только по UART-CLI

                        У меня есть отдельный текст про то, почему нужна консоль в UART
                        https://habr.com/ru/articles/694408/


                      1. aystarik
                        23.09.2023 15:07
                        +6

                        настраиваю на нем UART-Shell

                        т.е. вам нужно в слепую подать питание на HSE, PLL, GPIO, UART, выставить частоту PLL, wait-states для FLASH, ну и бауд-рэйт UART... ошибка в одной операции -- и у вас пересборка-перезаливка. Альтернатива -- все это понастраивать снаружи и заливать уже только рабочий вариант...


                      1. aabzel Автор
                        23.09.2023 15:07

                        Это надо преподавать в институтах.


                      1. aabzel Автор
                        23.09.2023 15:07

                         все это понастраивать снаружи и заливать уже только рабочий вариант.

                        Вот только как отладить на PC программные компоненты MCU, которые вызывают внешние прерывания?

                        В микроконтроллерах у каждого блока (GPIO RCC Timer SysTick Flash UART DMA) есть целая куча всяческих прерываний на каждый чих.

                        И исполняться ISR прерывания должны либо из on-chip RAM либо из on-chip Flash.


                    1. aabzel Автор
                      23.09.2023 15:07

                      Если говорить метафорами, то получается микроконтроллер-марионетка.
                      Вместо куклы микроконтроллер, вместо ниточек 4 провода JTAG. А кукловод это Host PC.


                1. aabzel Автор
                  23.09.2023 15:07
                  -1

                  То есть у вас на PC работает одновременно GdbServer и ваша GUI утилита.

                  GdbServer общается по JTAG c Target, а ваша утилита по TCP сокету общается с GdbServer .

                  GUI утилита управляет GPIO через GdbServer сервер. Так?


                  1. aystarik
                    23.09.2023 15:07
                    +2

                    "собственная библиотека для MPSSE" -- где вы увидели слово GdbServer/TCP сокет? GUI через библиотеку управляет через JTAG регистрами устройств. Никакого GDBserver.


                    1. aabzel Автор
                      23.09.2023 15:07

                      Что-то типа этого ?




                      1. aystarik
                        23.09.2023 15:07
                        +2

                        без винды и сима, а так да, похоже


      1. makkarpov
        23.09.2023 15:07
        +1

        JTAG/SWD дают прямой доступ к памяти МК, и, как следствие, ко всем его регистрам. Не очень быстрый, но доступ. Не только к GPIO, а вообще ко всей периферии. Хоть GPIO ставьте, хоть АЦП считывайте, хоть через I2C с остальной платой разговаривайте. Сам boundary scan (в своем изначальном смысле) - это исключительно JTAG, но тут он и не нужен.

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


        1. aabzel Автор
          23.09.2023 15:07

          JTAG/SWD дают прямой доступ к памяти МК, и, как следствие, ко всем его регистрам. Не очень быстрый, но доступ. Не только к GPIO, а вообще ко всей периферии. Хоть GPIO ставьте, хоть АЦП считывайте, хоть через I2C с остальной платой разговаривайте.

          Это что мне надо на PC писать отдельный симулятор прошивки под Win и весь MCAL, чтобы через JTAG GPIO считывать/прописывать?

          Да это же жесть полнейшая. Cross-Detect в 100 крат проще получается.



          1. makkarpov
            23.09.2023 15:07
            +2

            Ну, приведенное вам по ссылке коммерческое ПО именно так и работает.

            Учитывая, что в приведенном вами фрагменте "прошивки" 10% логики и 90% простыни из копипаста - уверен, на каком-нибудь python это получилось бы куда проще как минимум из-за замены C высокоуровневым языком, а так же доступности полноценной ОС. Для того, чтобы через SWD дергать ножками GPIO, усилий вообще не требуется (помимо поиска библиотеки для используемого отладчика).


            1. aabzel Автор
              23.09.2023 15:07
              -1

               уверен, на каком-нибудь python это получилось бы куда проще как минимум из-за замены C высокоуровневым языком, а так же доступности полноценной ОС. Для того, чтобы через SWD дергать ножками GPIO, усилий вообще не требуется (помимо поиска библиотеки для используемого отладчика).


              В Роcсии никто так не делает!

              Писать на Python симулятор прошивки который работает на Windows. При этом поддерживать Real-Time SWD link с target(ом) чтобы на реальной железке отражались изменения на PCB.

              Вы попробуйте убедить Python разработчика чтобы он перешел из Back-end разработки на Python за 300kRUR заниматься таким мартышкиным трудом в embedded за 50 kRUR.

              Это просто смешно!


              1. segment
                23.09.2023 15:07
                +4

                Смешно то, что Вы считаете, что никто так не делает. Вам все правильно разъясняют, и python был приведен как пример, можете брать тот же C/C++.


                1. aystarik
                  23.09.2023 15:07

                  1. aabzel Автор
                    23.09.2023 15:07
                    -5


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

                    Лучше бы они пост на habr написали про свой опыт управления процессором по JTAG.

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


                    1. aystarik
                      23.09.2023 15:07

                      Нет, C++/Qt. даже git и gcc использовали, представляете?


                  1. aabzel Автор
                    23.09.2023 15:07
                    +1

                    В 2013 в военном НИИ меня тоже вовлекли в процесс получения государственной регистрации моей программы для ЭВМ. Я тогда обрадовался. Но зря.

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

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

                    В итоге я понял одну вещь.
                    Лучшее что можно сделать со своей хорошей программой - это написать по нее пост на habr.


                    1. aystarik
                      23.09.2023 15:07
                      +1

                      NDA никто не отменял... А еще и секретность временами приплетают... лучше всего иметь код на каком-нибудь github, там разговор предметнее получается.


          1. kipar
            23.09.2023 15:07
            +1

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


            1. aabzel Автор
              23.09.2023 15:07

              Нужно еще регистр установки подтяжек читать писать.
              А вот выход ножек трогать нельзя. Это же push-pull.

              В случае КЗ на GND push-pull (ом) можно нечаянно сжечь плату.


            1. aabzel Автор
              23.09.2023 15:07
              +1

               Можно читать по jtag регистр состояния ножек и писать в регистр выхода ножек.

              Осталось только понять какой там API, чтобы из консольного windows приложения на Си читать регистры MCU по JTAG.

              И нужеy ли параллельно работающий GDBServer (ST-LINK_gdbserver.exe) для этого?


        1. aabzel Автор
          23.09.2023 15:07

          JTAG/SWD дают прямой доступ к памяти МК, и, как следствие, ко всем его регистрам. Не очень быстрый, но доступ. Не только к GPIO, а вообще ко всей периферии. Хоть GPIO ставьте, хоть АЦП считывайте, хоть через I2C с остальной платой разговаривайте.

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

          А как тогда обрабатывать внешние прерывания (например по SysTick таймеру или GPIO, I2C, UART, DMA, SPI)? Тоже на Host(e)?


          1. makkarpov
            23.09.2023 15:07
            +1

            Вам нужно работоспособность SysTick таймера протестировать (вдруг чип бракованный подсунули?) или корректность сборки платы?

            Если вдруг вам часто подсовывают чипы, где сломан SysTick таймер - можете залить тестовый код в RAM и оттуда выполнить. Но это мягко говоря нецелевое применение обсуждаемой технологии.


    1. aabzel Автор
      23.09.2023 15:07
      -2

      а почему не JTAG + boundary scan?


      Обыкновенный инженер может проверить что-то только одним единственным способом, хороший - тремя способам.


      1. aystarik
        23.09.2023 15:07
        +12

        Вы привели один способ и всячески отбиваетесь от других способов... Вы к какой категории себя после этого относите?


        1. Kudriavyi
          23.09.2023 15:07
          +4

          Я наблюдал за автором статьи и не рекомендую вам с ним спорить - затея абсолютно бесполезная. Он пропагандирует отладку через uart cli и других способов не приемлет от слова совсем. А кто этого не понимает - тот чернь неученая. Так же уже писали, что блок-схемы автора перегружены и в них без стакана не разберёшься, но это тоже все потому, что никто не понимает истины

          Причём я не хочу очернять подходы автора данной статьи, но у него совершенно отсутствует критика к себе, что сильно снижает рейтинги /то есть одобрение читающих/


      1. makkarpov
        23.09.2023 15:07
        +2

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

        У меня, например, нет готового UART CLI (и никогда не появится, потому что он мне нафиг не нужен, это пустой расход как человеческих ресурсов, так и машинных). Думаю, что у большинства читателей тоже.

        Специально для вас прикладываю скриптик в 40 строк, который читает и пишет любые регистры на реальном МК. Даже светодиодик загорается, лично проверил. Никаких проблем добавить туда хоть CLI, хоть HTML вообще нет - и для этого не надо писать никаких драйверов UART-а, достаточно простого println.

        Душат змею без регистрации и SMS
        import time
        import swd
        
        
        class GpioTest:
            RCC_AHB2ENR1 = 0x46020C8C
            RCC_AHB2ENR1_GPIOBEN = 1 << 1
        
            GPIOB = 0x42020400
        
            GPIO_MODER = 0x00
            GPIO_ODR = 0x14
        
            MODE_OUTPUT = 0x1
        
            def __init__(self):
                self.swd = swd.Swd()
                self.cortex = swd.CortexM(self.swd)
        
            def _modify_reg(self, addr: int, b_reset: int, b_set: int):
                self.swd.set_mem32(addr, (self.swd.get_mem32(addr) & ~b_reset) | b_set)
        
            def _set_mode(self, gpio: int, bit: int, mode: int):
                self._modify_reg(gpio + GpioTest.GPIO_MODER, 3 << (2 * bit), mode << (2 * bit))
        
            def _set_pin(self, gpio: int, bit: int, value: bool):
                self._modify_reg(gpio + GpioTest.GPIO_ODR, 1 << bit, int(value) << bit)
        
            def run(self):
                self.cortex.reset_halt()
                while not self.cortex.is_halted():
                    time.sleep(0.1)
        
                self._modify_reg(GpioTest.RCC_AHB2ENR1, 0, GpioTest.RCC_AHB2ENR1_GPIOBEN)
                self._set_mode(GpioTest.GPIOB, 7, GpioTest.MODE_OUTPUT)
                self._set_pin(GpioTest.GPIOB, 7, True)
        
        
        if __name__ == '__main__':
            GpioTest().run()
        


        1. aabzel Автор
          23.09.2023 15:07
          +1

          У меня, например, нет готового UART CLI

          Если кому-то еще нужен код UART-CLI пишете в личку свои email/TG и я пришлю свою версию UART-CLI сорцов. Уже пару человек обращались. Выслал без проблем.


        1. aabzel Автор
          23.09.2023 15:07

          del


        1. aabzel Автор
          23.09.2023 15:07
          +1

          Зачем вы пишите этот тестировочный код на Python?
          Не лучше ли бы написать его на С или С++? Тогда бы код был переносимым.

          Вы могли бы запускать тест GPIO так на target(е) так и на PC через переходник USB/JTAG, просто заменив CMSIS на драйвер переходника USB/JTAG одной строчкой в makefile.


          1. makkarpov
            23.09.2023 15:07

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


            1. aabzel Автор
              23.09.2023 15:07

               Код написан на Python из-за того, что на таргете его запускать априори не планируется по самой постановке задачи.

              Писать код для JTAG адаптера на Python это мартышкин труд!
              Такие вещи надо на C или C++ делать, чтобы и разработчики прошивки и тестировщики могли использовать одну общую протестированную пере используемую кодовую базу.

              Тем более стандарт автомобильного ПО ISO26262 запрещает использовать в разработке языки без строгой типизации типов данных (таких как Python) для всех ASIL.


    1. aabzel Автор
      23.09.2023 15:07
      +1

      а почему не JTAG + boundary scan?

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


  1. RV3EFE
    23.09.2023 15:07
    -1

    Хорошая статья. Интересный подход, который имеет право на жизнь. Видно, что автор участвует и имеет опыт в разработке и производстве серийного оборудования, с подходом экономичного производство( не путать с бережливым (5s)), а не как 90% разработчиков, только прототипов, которые идут в ящик или производятся штучно.


    1. aabzel Автор
      23.09.2023 15:07

      Спасибо за оценку.


  1. MagNitronik
    23.09.2023 15:07
    +2

    В качестве учебной задачи очень неплохо. Для производства конечно не подходит. Огорчает, что автор не разобрался с Boundary-scan. Существуют серийные образцы jtag- сканеров, в ПО которых загружается схемотехника платы, что дает возможность учитывать поведение других компонентов платы.


    1. aabzel Автор
      23.09.2023 15:07
      -4

      Огорчает, что автор не разобрался с Boundary-scan. Существуют серийные образцы jtag- сканеров, в ПО которых загружается схемотехника платы, что дает возможность учитывать поведение других компонентов платы.

      Огорчает то, что Вы @MagNitronik



      не написали по это текст на habr, раз у Вас есть экспертиза в этой теме.


      1. MagNitronik
        23.09.2023 15:07
        +3

        Это мало кому интересно. У тех, кто купил проф. Оборудование есть на него мануал.

        Вас обижает, что вас не поняли?


        1. aabzel Автор
          23.09.2023 15:07

          Вас обижает, что вас не поняли?

          Вовсе нет. Я вижу по голосованию, что большинство поняли. Значит со своей стороны я все вполне корректно изложил.


          1. mlnw
            23.09.2023 15:07
            +1

            "Понял" не значит "принял". Лучше б запилили опрос, кому-то это вообще нужно?


            1. aabzel Автор
              23.09.2023 15:07

              добавил


              1. segment
                23.09.2023 15:07
                +1

                Как этот вопрос относится к обсуждаемой теме? Ответ на этот вопрос звучит так: "да, потребность есть, поэтому заказываю электротест и на производстве используется test jig оснастка".


      1. devprodest
        23.09.2023 15:07
        +2

        Не всё упирается в статьи на Хабре: одним это нафиг не интересно писать, у других нет времени.

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

        Статья же переносит проблемы с больной головы на здоровую.


        1. aabzel Автор
          23.09.2023 15:07

          Не всё упирается в статьи на Хабре: одним это нафиг не интересно писать, у других нет времени.

          Написание постов на habr это своего рода вариант для сохранения ценной документации по проекту для тех компаний у которых нет денег на Atlassian Confluence(а).

          Мне часто приходилось самому читать свои же старые тексты на habr так как там была инструкция того что приходится делать снова и снова в разное время.
          https://habr.com/ru/articles/709932/
          https://habr.com/ru/articles/695978/
          https://habr.com/ru/articles/694708/
          https://habr.com/ru/articles/683762/
          https://habr.com/ru/articles/735422/
          https://habr.com/ru/articles/726352/
          https://habr.com/ru/articles/682498/
          https://habr.com/ru/articles/673522/

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


          1. devprodest
            23.09.2023 15:07
            +1

            Для хранения информации о проекте есть куча бесплатных википодобных систем.

            К тому же не всё можно и нужно постить в публичных местах.

            Я вас не отговариваю, пишите если нравится.


  1. vit496
    23.09.2023 15:07
    +2

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


  1. mlnw
    23.09.2023 15:07

    С 5В МК, работающим с 3.3В периферией в режиме открытого коллектора, такая эквилибристика с подтяжками просто пожжет периферию.