Load-Detect для Проверки Качества Пайки

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

Вот тут-то нам и помогут подтяжки к питанию и земле на пинах MCU. Называется эта тема load-detect (LD). У меня уже был текст про load-detect для тестирования силовых высоковольтных H-мостов перед запуском. Вот он https://habr.com/ru/articles/709374/

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

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

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

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

#

Пояснение

Состояние

1

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

Pull air

2

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

Pull GND

3

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

Pull VCC

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

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

Фаза 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**3 = 8). В ячейках таблицы измеренные логические уровни GPIO пина на котором работал LoadDetect.

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

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

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

Фаза 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

#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_enable(uint8_t num, bool enable);
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 пинов переключают подтяжки синхронно.

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

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

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

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

Вывод

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

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

Акроним

Расшифровка

GPIO

general-purpose input/output

LD

load detect

GND

заземление

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

Links

https://docs.google.com/spreadsheets/d/1prvCcVHzQ8rhGC7CaeIyoBzeyAq_lFkMaNI8ickKX1g/edit#gid=0

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы делаете load-detect для контроля пайки в новых электронных платах?
33.33% да 1
66.67% нет 2
Проголосовали 3 пользователя. Воздержавшихся нет.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы делаете отдельные тестировочные прошивки для контроля качества изготовления электронных плат?
33.33% да 1
66.67% нет 2
Проголосовали 3 пользователя. Воздержавшихся нет.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы поняли из этого текста механизм работы load-detect?
33.33% да 1
66.67% нет 2
Проголосовали 3 пользователя. Воздержавшихся нет.
Источник: https://habr.com/ru/articles/756572/


Интересные статьи

Интересные статьи

Как правильнее всего измерять качество машинного перевода? Многие слышали о BLEU, но на самом деле метрик много. В этой статье расскажем, какие существуют метрики, как они эволюционировали и какие сег...
Одна из основных проблем человека, который занимается машинным обучением - данные. Исследователи сталкиваются с плохим качеством данных и/или их отсутствием. Рассмотрим способы улучшение метрик класси...
Есть принцип «выбирай любые два понятия из "быстро, качественно и дёшево"». Да, он верен для проектных историй, но не верен для разработки. В разработке, как процессе, нет никакого треугольника качест...
Моё знакомство с Open XML SDK началось с того, что мне понадобилась библиотека для создания документов Word с некоторой отчётностью. После работы с Word API более 7 лет, захотелось попр...
Перед записью на новый курс Machine Learning Advanced мы тестируем будущих студентов, чтобы определить уровень их готовности и понять, что именно им необходимо предложить для подгот...