Pull to refresh

Cross-Detect для Проверки Качества Пайки в Электронных Цепях

Level of difficultyMedium
Reading time27 min
Views5.7K

#комбинаторика #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.

Тут SMD компоненты мешают выявить короткое замыкание между пинами.
Тут SMD компоненты мешают выявить короткое замыкание между пинами.

Что можно ещё улучшить?
На самом деле исходный код cross-detect вовсе не обязательно исполнять на target устройстве. Например на ARM Cortex-M4 или ARM Cortex-M33. Сross-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. А кукловод - это Windows-процесс на Host PC. Easy.

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

Суть этой технологии изложена в культовом тексте
Как перестать писать прошивки для микроконтроллеров и начать жить https://habr.com/ru/articles/433504/

Плюсы теста на пропай через 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/

Как перестать писать прошивки для микроконтроллеров и начать жить https://habr.com/ru/articles/433504/

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

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

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

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

Only registered users can participate in poll. Log in, please.
Вы реализовывали в прошивке cross-detect?
15.63% да5
84.38% нет27
32 users voted. 7 users abstained.
Only registered users can participate in poll. Log in, please.
Вы поняли механизм работы автомата cross-detect?
74.07% да20
25.93% нет7
27 users voted. 9 users abstained.
Only registered users can participate in poll. Log in, please.
Вы составляете тестировочные прошивки для контроля качества пайки?
28.13% да9
71.88% нет23
32 users voted. 8 users abstained.
Only registered users can participate in poll. Log in, please.
Вы писали программу для PC для тестов на пропай для управление GPIO через интерфейс JTAG?
12.5% да2
87.5% нет14
16 users voted. 1 user abstained.
Only registered users can participate in poll. Log in, please.
У Вас бывает потребность делать тест качества сборки/пайки электронных плат?
81.82% да9
18.18% нет2
11 users voted. Nobody abstained.
Tags:
Hubs:
Total votes 17: ↑12 and ↓5+11
Comments152

Articles