Во всех современных микроконтроллерах уже давно как (больше 11 лет) есть подтяжки напряжения на пинах GPIO. Как вы думаете зачем в микроконтроллерах есть функция pull-up/pull-down, если можно просто воспользоваться установкой логического уровня push-pull?

Вы наверное скажете, что подтяжки к питанию нужны для конфигурации пинов шины I2C/1-Wire, нужны для кнопок. Верно! Но это не единственная причина.

Вот типичная ситуация. Вам принесли 6ти слойную электронную плату прямо с производства. Её ещё ни разу не включали. Обычно в таких случаях 90% вероятность, что в PCB есть какие-то аппаратные баги: короткие замыкания на GND, короткие замыкания на VCC или вовсе непропай пинов MCU. Как выявить эти бракованные пины?

Вот тут-то нам и помогут подтяжки к питанию и земле на пинах MCU. Называется эта тема load-detect (LD). У меня уже был текст про load-detect для тестирования силовых высоковольтных H-мостов перед запуском. Вот он: «H‑мост: Load Detect (или как выявлять вандализм)».

Однако load-detect можно реализовать не только на специализированных для этого ASIC(ах), а прямо на пинах микроконтроллера!

LD может выявить короткое замыкание (КЗ) пинов прямо в BGA корпусах, где даже щупом осциллографа к пинам не подлезть! LD это часть прошивки, чисто программный компонент.

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

Обычно LD оформляют как конечный автомат на три состояния. Разработка же конечных автоматов это хорошо формализованный процесс, состоящий из 7 фаз.

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

#

Пояснение

Состояние

1

На пине нет подтяжек напряжения

Pull air

2

На пине подтяжка к GND

Pull GND

3

На пине подтяжка к VCC

Pull VCC

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

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

В частности же время переходного процесса на пине не превышает даже 3 миллисекунд. Получается, что можно обновлять состояние диагностики пина с периодом 4ms*3 = 12 ms или с частотой 83 Hz!

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

Конечный автомат контроля пайки может делать только следующие легальные действия

#

Пояснение действия

Действие

1

Измерить логический уровень на пине

Read GPIO

2

Установить на пине подтяжку к питанию

Set pull Up

3

Установить на пине подтяжку к заземлению

Set pull Down

4

Отключить на пине какие - либо подтяжки

Set pull air

5

Вычислить решение о состоянии пина на основе накопленных измерений

calculate solution

Тут сразу надо отметить что такое вычисление решения. Вот Look Up таблица принятия решения по измерениям автомата Load detect. Как видно после одного цикла измерений согласно комбинаторному правилу перемножения может быть максимум 8 различных вариантов (2*2*2 =2**3=8). В ячейках таблицы измеренные логические уровни GPIO пина на котором работал LoadDetect.

Open load означает, что пин ни к чему не подключен или, если формально, подключен к резистору с бесконечным сопротивлением одним концом и на GND другим. Еще говорят Z-состояние. И то же самое с VCC.

Фаза 4. Составить таблицу переходов для состояний конечного автомата

Как работать с этой таблицей. Если автомат был с состоянии pull-air и сработало прерывание по переполнению таймера, то автомат переходит в состояние pull-down. Если автомат был с состоянии pull-down и сработало прерывание по переполнению таймера, то автомат переходит в состояние pull-up. Если автомат был с состоянии pull-up и сработало прерывание по переполнению таймера, то автомат переходит в состояние pull-air. Это один цикл измерений. Дальше автомат продолжает работать непрерывно новые и новые циклы.

Фаза 5. Нарисовать граф переходов конечного автомата

На самом деле всё, что я тут написал можно объяснить только одной вот этой картинкой графа конечного автомата. Надо пробежаться по всем трём подтяжкам, в каждой подтяжке прочитать состояние пина и найти в подсказке строчку, которая соответствует этим измерениям и покажет в соседней колонке, что, собственно, подключено к этому конкретному пину со стороны улицы. Easy!

Это классический пример конечного автомата Мили так как выходы генерируются на основе входа и состояния.

Фаза 6. написать программный Си-код

Прежде всего LD надо сконфигурировать. Указать с какими пинами ему надо работать а с какими не надо.

#include "load_detect_config.h"

#ifndef HAS_LOAD_DETECT
#error "Add HAS_LOAD_DETECT"
#endif /*HAS_LOAD_DETECT*/

#include "data_utils.h"
#include "gpio_drv.h"
#include "log.h"


const LoadDetectPinConfig_t LoadDetectPinConfig[] = {
    {.num = 1, .pin_num = 1, .pad={.port=0, .pin=8}, .valid=true,},
    {.num = 1, .pin_num = 2, .pad={.port=0, .pin=16}, .valid=true,},
   ....
    {.num = 1, .pin_num = 22, .pad={.port=1, .pin=12}, .valid=true,},

};

LoadDetectPinInfo_t LoadDetectPinInstance[] = {
        {.num = 1, .pin_num = 1,  .valid=true,},
        {.num = 1, .pin_num = 2,  .valid=true,},
...
        {.num = 1, .pin_num = 22,  .valid=true,},
};

const LoadDetectConfig_t LoadDetectConfig[] = {
    {.num = 1, .name="MCUgpio", .valid=true,  .gpio_class=GPIO_CLASS_MCU, },
};

LoadDetectHandle_t LoadDetectInstance[] = {
    {.num = 1, .valid=true, },
};

uint32_t load_detect_get_cnt(void) {
    uint32_t cnt = 0;
    uint32_t cnt_conf = ARRAY_SIZE(LoadDetectConfig);
    uint32_t cnt_ints = ARRAY_SIZE(LoadDetectInstance);
    if(cnt_conf == cnt_ints) {
        cnt = cnt_ints;
    }
    return cnt;
}

uint32_t load_detect_get_pin_cnt(void) {
    uint32_t cnt = 0;
    uint32_t cnt_conf = ARRAY_SIZE(LoadDetectPinConfig);
    uint32_t cnt_ints = ARRAY_SIZE(LoadDetectPinInstance);
    if(cnt_conf == cnt_ints) {
        cnt = cnt_ints;
    }else{
    	LOG_ERROR(LOAD_DETECT,"PinConfigMisMatch ConfPins%u!=RamPins%u",cnt_conf,cnt_ints);
    }
    return cnt;
}

Вот API. Как и любой программный компонент его надо проинициализировать load_detect_init() , затем прокручивать load_detect_proc() где-то с супер цикле.

#ifndef LOAD_DETECT_DRIVER_H
#define LOAD_DETECT_DRIVER_H

#include <stdbool.h>
#include <stdint.h>

#include "load_detect_config.h"
#include "load_detect_types.h"

bool load_detect_init(void);
bool load_detect_proc(void);

#endif /* LOAD_DETECT_DRIVER_H  */

А это код самого драйвера load-detect. Как видите все функции тривиальные и помещаются на один экран.

#include "load_detect_drv.h"

#include <stdint.h>

#include "gpio_drv.h"
#include "log.h"
#include "time_utils.h"

LoadDetectHandle_t* LoadDetectGetNode(uint8_t num) {
    LoadDetectHandle_t *LdNode = NULL;
    uint32_t i = 0;
    uint32_t cnt = load_detect_get_cnt();
    for (i = 0; i < cnt; i++) {
        if (num == LoadDetectInstance[i].num) {
            if (LoadDetectInstance[i].valid) {
                LdNode = &LoadDetectInstance[i];
                break;
            }
        }
    }
    return LdNode;
}

const LoadDetectConfig_t* LoadDetectGetConfNode(uint8_t num) {
    const LoadDetectConfig_t *LDConfig = NULL;
    uint32_t i = 0;
    uint32_t cnt = load_detect_get_cnt();
    for (i = 0; i < cnt; i++) {
        if (num == LoadDetectConfig[i].num) {
            if (LoadDetectConfig[i].valid) {
                LDConfig = &LoadDetectConfig[i];
                break;
            }
        }
    }
    return LDConfig;
}

const LoadDetectPinConfig_t* LoadDetectGetPinConfNode(uint8_t pin_num) {
    const LoadDetectPinConfig_t *PinConfig = NULL;
    uint32_t i = 0;
    uint32_t cnt = load_detect_get_pin_cnt();
    for (i = 0; i < cnt; i++) {
        if (pin_num == LoadDetectPinConfig[i].pin_num) {
            if (LoadDetectPinConfig[i].valid) {
                PinConfig = &LoadDetectPinConfig[i];
                break;
            }
        }
    }
    return PinConfig;
}

LoadDetectPinInfo_t* LoadDetectGetPinNode(uint8_t pin_num) {
    LoadDetectPinInfo_t *PinNode = NULL;
    uint32_t i = 0;
    uint32_t pin_cnt = load_detect_get_pin_cnt();
    for (i = 0; i < pin_cnt; i++) {
        if (pin_num == LoadDetectPinInstance[i].pin_num) {
            if (LoadDetectPinInstance[i].valid) {
                PinNode = &LoadDetectPinInstance[i];
                break;
            }
        }
    }
    return PinNode;
}

static bool load_detect_init_pin(const LoadDetectPinConfig_t* const PinConfig,LoadDetectPinInfo_t* const  PinNode) {
    bool res = false;
    if(PinConfig) {
        if(PinNode) {
            uint32_t ok = 0 ;
            LOG_WARNING(LOAD_DETECT, "InitPad: %s In PullAir", GpioPad2Str(PinConfig->pad.byte));
            PinNode->num = PinConfig->num;
            PinNode->valid = PinConfig->valid;
            PinNode->pad = PinConfig->pad;
            PinNode->pin_num = PinConfig->pin_num;
            PinNode->on_off = true;

            PinNode->state = LOAD_DETECT_OUT_UNDEF;
            PinNode->prev_state = LOAD_DETECT_OUT_UNDEF;
            PinNode->llevel_at_pullair = GPIO_LVL_UNDEF;
            PinNode->llevel_at_pulldown = GPIO_LVL_UNDEF;
            PinNode->llevel_at_pullup = GPIO_LVL_UNDEF;

            res = gpio_set_dir(  PinConfig->pad.byte, GPIO_DIR_IN) ;
            if(res) {
                ok++;
            } else {
                LOG_ERROR(LOAD_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(LOAD_DETECT, "Pad: %s SetPullAir Err", GpioPad2Str(PinConfig->pad.byte));
            }

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

    return res;
}

bool load_detect_init_pins(uint8_t num) {
    bool res = false;
    uint32_t pin_cnt = load_detect_get_pin_cnt();
    LOG_WARNING(LOAD_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 == LoadDetectPinConfig[i].num) {
            res= load_detect_init_pin(&LoadDetectPinConfig[i],&LoadDetectPinInstance[i]);
            if (res) {
                ok++;
                LOG_DEBUG(LOAD_DETECT, "InitPin %s Ok", GpioPad2Str(LoadDetectPinInstance[i].pad.byte));
            } else {
                LOG_ERROR(LOAD_DETECT, "InitPinErr %d", num);
            }
        }
    }
    if(0<ok){
        res = true;
    }
    return res;
}

bool load_detect_init_one(uint8_t num) {
    bool res = false;
    LOG_WARNING(LOAD_DETECT, "Init %d", num);
    const LoadDetectConfig_t *Config = LoadDetectGetConfNode(num);
    if (Config) {
        LoadDetectHandle_t *Node = LoadDetectGetNode(num);
        if (Node) {
            Node->gpio_class = Config->gpio_class;
            Node->init_done = true;
            Node->on_off = true;
            Node->valid = true;
            Node->state =  GPIO__PULL_AIR;
            Node->spin_cnt = 0;
            res = load_detect_init_pins(num);
            if(res){
                LOG_INFO(LOAD_DETECT, "%u InitPinsOk",num);
            }else{
                LOG_ERROR(LOAD_DETECT, "%u InitPinsErr",num);
            }
        } else {
            LOG_ERROR(LOAD_DETECT, "%u NodeErr",num);
        }
    } else {
        LOG_ERROR(LOAD_DETECT, "%u ConfErr",num);
    }
    return res;
}

bool load_detect_init(void) {
    bool res = false;
    log_level_set(LOAD_DETECT, LOG_LEVEL_DEBUG);
    uint32_t cnt = load_detect_get_cnt();
    uint32_t ok = 0;
    LOG_WARNING(LOAD_DETECT, "Init Cnt %d", cnt);

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

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

    log_level_set(LOAD_DETECT, LOG_LEVEL_INFO);
    return res;
}


static bool load_detect_set_mcu_ll(LoadDetectHandle_t *Node, GpioPullMode_t pull_mode) {
    bool res = false;
    uint32_t i = 0;
    uint32_t ok = 0;
    uint32_t cnt = load_detect_get_pin_cnt();
    for (i = 1; i <= cnt; i++) {
        LoadDetectPinInfo_t *PinNode = LoadDetectGetPinNode(i);
        if (PinNode) {
            if (PinNode->num == Node->num) {
                res = gpio_set_pull(PinNode->pad.byte, pull_mode);
                if (res) {
                    ok++;
                }
            }
        }
    }

    res = (ok == cnt) ? true : false;

    return res;
}

static bool load_detect_pin_update(LoadDetectHandle_t *Node,
        LoadDetectPinInfo_t *PinNode, GpioLogicLevel_t logic_level) {
    bool res = false;
    LOG_DEBUG(LOAD_DETECT, "Update: %u %s %s %s" , Node->num,GpioPad2Str(PinNode->pad.byte), GpioPull2Str(Node->state),GpioLevel2Str(logic_level));
    switch (Node->state) {
    case GPIO__PULL_AIR: {
        PinNode->llevel_at_pullair = logic_level;
        res = true;
    }
        break;
    case GPIO__PULL_DOWN: {
        PinNode->llevel_at_pulldown = logic_level;
        res = true;
    }
        break;
    case GPIO__PULL_UP: {
        PinNode->llevel_at_pullup = logic_level;
        res = true;
    }
        break;
    default:
        break;
    }
    return res;
}

static bool load_detect_measure_mcu_ll(LoadDetectHandle_t *Node) {
    bool res = false;
    LOG_DEBUG(LOAD_DETECT, "ProcMeasureMcu:%u", Node->num);
    uint32_t i = 0;
    uint32_t cnt = load_detect_get_pin_cnt();
    for (i = 1; i <= cnt; i++) {
        LoadDetectPinInfo_t *PinNode = LoadDetectGetPinNode(i);
        if (PinNode) {
            if (PinNode->num == Node->num) {
                GpioLogicLevel_t logic_level = GPIO_LVL_UNDEF;
                res = gpio_get_state(PinNode->pad.byte, &logic_level);
                if (res) {
                    res = load_detect_pin_update(Node, PinNode, logic_level);
                }
            }
        }
    }

    return res;
}

static bool load_detect_set_pull_ll(LoadDetectHandle_t *Node, GpioPullMode_t pull_mode) {
    bool res = false;
    switch (Node->gpio_class) {
    case GPIO_CLASS_MCU:
        res = load_detect_set_mcu_ll(Node, pull_mode);
        break;
    case GPIO_CLASS_DW1000:
        res = false;
        break;
    case GPIO_CLASS_DW3000:
        res = false;
        break;
    default:
        LOG_ERROR(LOAD_DETECT, "UndefGPIO");
        break;
    }
    return res;
}

static bool load_detect_measure(LoadDetectHandle_t *Node) {
    bool res = false;
    LOG_DEBUG(LOAD_DETECT, "ProcMeasure:%u", Node->num);
    switch (Node->gpio_class) {
    case GPIO_CLASS_MCU:
        res = load_detect_measure_mcu_ll(Node);
        break;
    case GPIO_CLASS_DW1000:
        res = false;
        break;
    case GPIO_CLASS_DW3000:
        res = false;
        break;
    default:
        LOG_ERROR(LOAD_DETECT, "UndefGPIOclass");
        break;
    }
    return res;
}

static bool load_detect_calc_pin_solution(LoadDetectHandle_t *Node, LoadDetectPinInfo_t* PinNode){
    bool res = false;
    if(Node){
        LOG_DEBUG(LOAD_DETECT, "CalcSolution:%u", Node->num);
        if(PinNode) {
            if(PinNode->num == Node->num){
                switch((uint8_t)PinNode->llevel_at_pullup){
                    case GPIO_LVL_LOW: {
                        PinNode->state = LOAD_DETECT_OUT_SHORT_GND;
                        res = true;
                    }break;
                    case GPIO_LVL_HI: {
                        res = true;

                    }break;
                }

                switch((uint8_t)PinNode->llevel_at_pulldown){
                    case GPIO_LVL_LOW: {
                        res = true;
                    }break;
                    case GPIO_LVL_HI: {
                        PinNode->state = LOAD_DETECT_OUT_SHORT_VCC;
                        res = true;

                    }break;
                }

                if(GPIO_LVL_LOW==PinNode->llevel_at_pulldown) {
                    if(GPIO_LVL_HI==PinNode->llevel_at_pullup){
                        PinNode->state =  LOAD_DETECT_OUT_OPEN;
                        res = true;
                    }
                }

                if(PinNode->prev_state!=PinNode->state){
                    LOG_WARNING(LOAD_DETECT,"Pad %s NewState %s->%s",GpioPad2Str(PinNode->pad.byte),LoadDetectOut2Str(PinNode->prev_state),LoadDetectOut2Str(PinNode->state));
                }
                PinNode->prev_state = PinNode->state;
            }
        }
    }
    return res;
}

static bool load_detect_calc_solution(LoadDetectHandle_t *Node){
    bool res = false;
    uint32_t pin_cnt = load_detect_get_pin_cnt();
    LOG_DEBUG(LOAD_DETECT, "CalcSolution:%u for %u pins", Node->num, pin_cnt);
    Node->spin_cnt++;
    uint32_t i = 0 ;
    uint32_t ok = 0 ;
    for(i=0; i<pin_cnt; i++) {
        res = load_detect_calc_pin_solution(Node,&LoadDetectPinInstance[i]);
        if(res){
            ok++;
        }
    }

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

static bool load_detect_proc_air_ll(LoadDetectHandle_t *const Node) {
    bool res = false;
    LOG_DEBUG(LOAD_DETECT, "ProcAir:%u", Node->num);
    if (ONE_STATE_TIME_OUT_MS < Node->pause_ms) {
        load_detect_measure(Node);
        Node->state = GPIO__PULL_DOWN;
        Node->time_start = time_get_ms();
        LOG_DEBUG(LOAD_DETECT, "SwitchState Air->Down");
        res = load_detect_set_pull_ll(Node, GPIO__PULL_DOWN);
    }

    return res;
}

static bool load_detect_proc_down_ll(LoadDetectHandle_t *Node) {
    bool res = false;
    LOG_DEBUG(LOAD_DETECT, "ProcDown:%u", Node->num);
    if (ONE_STATE_TIME_OUT_MS < Node->pause_ms) {
        load_detect_measure(Node);
        Node->state = GPIO__PULL_UP;
        Node->time_start = time_get_ms();
        LOG_DEBUG(LOAD_DETECT, "SwitchState Down->Up");
        res = load_detect_set_pull_ll(Node, GPIO__PULL_UP);
    }

    return res;
}

static bool load_detect_proc_up_ll(LoadDetectHandle_t *Node) {
    bool res = false;
    LOG_DEBUG(LOAD_DETECT, "ProcUp:%u", Node->num);
    if (ONE_STATE_TIME_OUT_MS < Node->pause_ms) {
        load_detect_measure(Node);
        Node->state = GPIO__PULL_AIR;
        Node->time_start = time_get_ms();
        LOG_DEBUG(LOAD_DETECT, "PullState:Up->Air");
        res = load_detect_set_pull_ll(Node, GPIO__PULL_AIR);

        res=load_detect_calc_solution(Node);
    }

    return res;
}

/*UP->AIR->Down->Up*/
bool load_detect_proc_one(uint8_t num) {
    bool res = false;
    uint32_t up_time = time_get_ms();
    LOG_DEBUG(LOAD_DETECT, "Proc:%u UpTime %u ms", num,up_time);
    LoadDetectHandle_t *Node = LoadDetectGetNode(num);
    if (Node) {
        if (Node->on_off) {
            Node->pause_ms = up_time - Node->time_start;
            LOG_DEBUG(LOAD_DETECT, "Proc Cnt:%u Pause %u ms", num, Node->pause_ms);
            switch (Node->state) {
            case GPIO__PULL_AIR:
                res = load_detect_proc_air_ll(Node);
                break;
            case GPIO__PULL_DOWN:
                res = load_detect_proc_down_ll(Node);
                break;
            case GPIO__PULL_UP:
                res = load_detect_proc_up_ll(Node);
                break;
            default:
                Node->state = GPIO__PULL_AIR;
                res = false;
                break;
            }
        }
    } else {
        LOG_ERROR(LOAD_DETECT, "NodeErr %u",num);
    }
    return res;
}

bool load_detect_proc(void) {
    bool res = false;
    uint8_t ok = 0;
    uint8_t cnt = load_detect_get_cnt();
    LOG_DEBUG(LOAD_DETECT, "Proc Cnt:%u", cnt);
    for (uint32_t i = 1; i <= cnt; i++) {
        res = load_detect_proc_one(i);
        if (res) {
            ok++;
        }
    }

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

    return res;
}

Как видите драйвер load-detect разом командует сразу всеми N пинами из конфига, подобно тому как в армии лейтенант командует сразу всем взводом (ровняясь! смирно! Напра-аааа-во !). Поэтому все N пинов переключают свои подтяжки синхронно.

Фаза 7. отладить конечный автомат

Для того чтобы просмотреть отчет работы LD вам в прошивку надо добавить интерфейс командной строки поверх UART-CLI. Что это и зачем есть отдельный текст
https://habr.com/ru/articles/694408/
Иначе без UART-CLI Вы просто никогда не узнаете, где, собственно, обнаружились короткие замыкания. В прошивке отчет LD выглядит как ASCII табличка, где каждому GPIO пину поставлено в соответствие состояние его нагрузки: short GND/VCC или оpen-load.

По-хорошему для верификации микроконтроллерных плат с производства должна быть собрана отдельная прошивка (сборка), которая прокручивает шестерни механизма load-detect. В этой сборке должны быть такие компоненты как TIMER, GPIO, UART, CLI, LD, LED. Эту сборку обычно называют BoardName_IO_Bang. Получается так, что плата как бы изнутри тестирует сама себя.

Лично мне load-detect однажды очень помог найти один чрезвычайно красивый аппаратный баг в первой ревизии одной новой платы. Схемотехники для мультиплексора RS2058 при трассировке взяли для схемотехники распиновку от корпуса MSOP10, а PCB поставили корпус QFN. Пины для MSOP10 и QFN, естественно, не совпадают по номерам. В результате мультиплексор RS2058 не пропускал сигнал и вообще не управлялся.

Вывод

При помощи манипуляции подтяжками напряжений на пинах микроконтроллера и измерений в нужный момент логического уровня на GPIO пине можно запросто определять такие высокоуровневые события как короткое замыкание на GND/VCC или отсутствие нагрузки (подключено бесконечное сопротивление).

Добавляйте в свои прошивки компонент load-detect. Это позволит Вам делать bring-up новых электронных плат легко и эффективно.

Акроним

Расшифровка

GPIO

general-purpose input/output

LD

Load Detect

GND

заземление (Ground)

MCU

MicroController Unit

API

Application programming interface

VCC

Voltage at the Common Collector (Supply voltage )

I2C

Inter-Integrated Circuit

FSM

finite-state machine

PCB

printed circuit board

КЗ

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

BGA

Ball grid array

ASIC

Application-Specific Integrated Circuit

Links

load detect one pin look up table

H-мост: Load Detect (или как выявлять вандализм)

Почему Нам Нужен UART-Shell? (или Добавьте в Прошивку Гласность)

https://habr.com/ru/articles/681280/

https://en.wikipedia.org/wiki/Mealy_machine

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

— Как делать тест PCB на пропай?

-- Что такое конечный автомат Мили?

-- Сколько времени длится переходной процесс установки подтяжки напряжения?

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


  1. Nick_Shl
    25.08.2023 01:15
    +5

    Как вы думаете зачем в микроконтроллерах есть функция Pull_Up/Pull_Down, если можно просто воспользоваться установкой логического уровня Push-Pull?

    Good luck, как говорится, установить логический уровень Push(Vcc) и подцепить на этот пин кнопочку замыкающую эту линию на землю ????

    Шутка конечно, внутренние подтягивающие резисторы хоть и можно так использовать, но лучше использовать внешние. Потому что встроенные высокоомные, легче ловят помеху и скорее всего не обеспечивают wetting current.

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


  1. aumi13
    25.08.2023 01:15

    Вот типичная ситуация. Вам принесли 6ти слойную плату с производства. Её ещё ни разу не включали. Обычно в таких случаях 90% вероятность, что в PCB есть какие-то аппаратные баги: короткие замыкания на GND, короткие замыкания на VCC или вовсе не пропай.

    какая глупость. работаю на производстве, подобные проблеммы с платами или с пайкой возникают в 0.01% случаев.


    1. aabzel Автор
      25.08.2023 01:15
      +2

      Производство- то может быть и отличное.

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

      А такого понятия как hardware review в русском языке не существует.


      1. VT100
        25.08.2023 01:15
        +2

        Любые схемотехники.


      1. olartamonov
        25.08.2023 01:15

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

        И с учётом, что 99,9 % аппаратных багов этот ваш LD не поймает, время на его написание и запуск не окупится примерно никогда.


  1. Polarisru
    25.08.2023 01:15
    +1

    Идея интересная, даже, возможно, попробую ее реализовать, но в смысл пришлось очень активно вчитываться, и все эти диаграммы, таблицы и прочее не помогают совершенно, только запутывают, все же гораздо проще на самом деле. И зачем эти 3.3/0 в таблице, если можно было занести HI/LO? И да, если речь идет не о замыкании на землю/питание, а о сопле между двумя соседними контактами, то отловить проблему, анализируя лишь один пин, практически невозможно.


    1. aabzel Автор
      25.08.2023 01:15

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

      Для этого 2 соседних пина надо рассматривать как H-мост и применять алгоритм LD для H-моста.

      https://habr.com/ru/articles/709374/


    1. aabzel Автор
      25.08.2023 01:15

       И зачем эти 3.3/0 в таблице, если можно было занести HI/LO?

      3,3 V это конкретнее. Это физика.
      Я встречал российские микроконтроллеры у которых Lo по докам считалось 3,3V.


    1. aabzel Автор
      25.08.2023 01:15
      +1

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

      В BGA корпусах у каждого пина 6 соседних пинов!

      На каждой паре пинов надо проделать load-detect для H-моста.

      Получается 6 измерений на пин!

      В худшем случае это займёт
      (94[пинов])*(6[пинов соседей])*(4 [ms])*(4 [состояния]) = 9024ms = 9 секунд на плату.

      Тогда можно также выявить и короткие замыкания между пинами!


  1. le2
    25.08.2023 01:15
    +4

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

    В остальном бесполезно. Во-первых, подтяжки появились в контроллерах относительно недавно и они есть не на всех пинах и, соответственно не на всех чипах. Во-вторых если вам приносят платы с производства это значит что все довольно печально у вас на предприятии.
    У больших мальчиков, при условии что чипы это поддерживают, используется периферийное сканирование IEEE 1149.1. Летающие щупы и прочие дорогущие штуки.
    Но это дорого. При массовом производстве - копеечные игольчатые стенды и оснастки для тестирования. Обычно оснастки проектируются параллельно с разработкой.
    Заглядывать под BGA не нужно. У сертифицированного производства всегда есть рентген. И качество производства это их проблемы.
    Обычно, специальной прошивки не требуется, это просто отдельный режим, который включается при замыкании определенных ножек.
    Итого, секрет успеха - стратегию тестирования на производстве нужно разрабатывать сразу при проектировании устройства, а не выдумывать "когда принесут".


    1. aumi13
      25.08.2023 01:15
      +1

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

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

      хотя зачастую любые стройные теории разбиваюца о криворукие практики.


      1. le2
        25.08.2023 01:15
        +1

        в этом деле точно не нужно изобретать универсальное решение, а исходить из конкретного изделия.
        Я был на некоторых заводах в России и Китае. Видел много оснасток. Самая крутая была на производстве неких промышленных компьютеров (в России) за 1 млн евро - большой станок, как часть линии конвейера. Где не только подключалось ложе гвоздей сверху и снизу но и автоматически втыкались все разъемы для компьютера. Но это не значит что это решение подходит для всех случаев - банально дорого, это оправдано только для тех ребят с миллиоными тиражами по году.
        На работе заказал в digilent.com - USB-осциллограф. Включил - не работает. Техподдержка пыталась помочь, а потом забанила, чтобы я не портил статистику на форуме. Через несколько месяцев решил починить - починил - непропай разъема USB. То есть эти красавцы запрограммировали, проверили (судя по точкам на плате) но даже не включали!


    1. aabzel Автор
      25.08.2023 01:15
      +1

      Летающие щупы и прочие дорогущие штуки.

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


    1. aabzel Автор
      25.08.2023 01:15

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

      А вот это как раз признак отчаянного колхоза.
      Промышленный способ посылать команды это UART-CLI (Shell), а не совать джамперы на GPIO.


      1. le2
        25.08.2023 01:15
        +3

        Производство больших партий несколько отличается. Наступает непреодолимое желание акционеров двигать конвейер со скоростью, например, одно изделие в 30 секунд. Отсюда нужно параллелить посты для программирования и проверки, а также резко сокращать время проверки. То есть практически это может быть 10 оснасток и (3 оснастки в подменном фонде на полке, для оперативной замены). Если в приборе 6 плат, то 60 оснасток...
        То есть "разные методы тестирования... дополняют" и "отчаяный колхоз" "посылать команды" это хорошо когда объемы производства смехотворные и можно дергать разработчика.

        Но когда платы сами себя проверяют без компьютера это очень круто. Прошивать также лучше без компьютера через недорогой jtag-программатор с кнопкой.
        При наличии сети сервисных центров это также вызывает восторг сервисменов.
        Да, рабочих нельзя смущать приборами и излишней индикацией. Оснастка должна показывать зеленую или красную лампочку. (расширенный режим диагностики отдельно для инженера)
        Однажды своё производство заказало разработке сделать стенд для параллельного программирования и проверки десятка плат. Стендов должно было быть десяток. Разработка выполнила эту работу качественно с подключением аналитиков и тестировщиков - время работы 6 минут. Производство работу отклонило - запросили 30 секунд. Подключилось руководство - ни в чем себе не отказывайте, но сделайте как просят - 6 минут никуда не годится. В итоге наступили на горло собственной песне и сделали как хотят. Вот это - суровая правда жизни. Никому не интересно, чтобы это было по учебнику, согласно буржуинскому стандарту или как иначе. Управляют качеством всегда акционеры.


    1. aabzel Автор
      25.08.2023 01:15

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

      В надежных системах в прошивке generic в принципе не должно быть лишнего кода. Это требование ISO26262.


    1. aabzel Автор
      25.08.2023 01:15

       Во-первых, подтяжки появились в контроллерах относительно недавно и они есть не на всех пинах и, соответственно не на всех чипах.

      С 2012 года работаю программистом МК. Подтяжки были всегда и везде (у STM32, CC26xx, NRF5340, SPC5x, MDR32, ESP32, LPC4xxx).

      11 лет это не "недавно". Это "уже давно как".


      1. Polarisru
        25.08.2023 01:15
        +1

        Справедливости ради, у AVR есть только Pull-Up, а у контроллеров с ядром 8051 от SiLabs этот Pull-Up еще и неконфигурируемый, его можно включить только для всех ножек.


        1. aabzel Автор
          25.08.2023 01:15
          -1

          , у AVR есть только Pull-Up,

          Что ж.
          По крайней мере можно будет Load detect (ом) определять КЗ на GND.


          А если схемотехники поставят высокомный резистор на каждую ножку, то еще и KЗ на VСС.


        1. aabzel Автор
          25.08.2023 01:15

          SiLabs этот Pull-Up еще и неконфигурируемый, его можно включить только для всех ножек.

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

          Как видите драйвер load-detect разом командует сразу всеми N пинами из конфига, подобно тому как в армии лейтенант командует сразу всем взводом (ровняясь! смирно! Напра-аааа-во !). Поэтому все N пинов переключают свои подтяжки синхронно.


          1. Polarisru
            25.08.2023 01:15
            +1

            Поэтому все N пинов переключают свои подтяжки синхронно

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


            1. aabzel Автор
              25.08.2023 01:15

              Куда ожидаемее были бы контакты с соседними пинами, но так это вообще невозможно отследить.

              Для этого 2 соседних пина надо рассматривать как H-мост и применять алгоритм LD для H-моста.

              https://habr.com/ru/articles/709374/


  1. VT100
    25.08.2023 01:15
    +3

    Либо описание хромает, либо автомат не дописан.
    Откуда в 4-й строке таблицы 3,3 В для Z-состояния и почему вердикт "open load"? Это может быть и внешний "pull up". В общем случае — можно оценивать ещё время от перехода уверенного чтения уровня к неуверенному, которое зависит от наличия гистерезиса входного буфера, которое… и вот уже граф автомата не помещается в ПЗУ.
    Если есть JTAG — то его использовать. Если нет — можно подумать и о таком тесте. Или о "гвоздях" и пр. В каждом случае — будут разные приоритеты.