
Во всех современных микроконтроллерах уже давно как (больше 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
https://habr.com/ru/articles/762142/
Контрольные вопросы:
— Как делать тест PCB на пропай?
-- Что такое конечный автомат Мили?
-- Сколько времени длится переходной процесс установки подтяжки напряжения?
