Контроллер дверей

Для решения одной локальной проблемы с дверями, таблом и освещением автомойки запилил подобие небольшого ПЛК, или, скорее, умного реле. Проект выходного дня, так сказать. Ничего особо выдающегося собой не представляет, но все равно выложу, мало ли кому понадобится что-то похожее, а тут все уже готовое. Заказывай в китае платы, да делай. А там только свой код написать, но это же не сложно, да? ;) Тем более я покажу как все работает.

Сама задача была простой, по сигналу контроллера от робомойки надо было переключить табло с «Занято» на «Свободно», включить свет в рабочей зоне, и, главное, разделить двери. У оригинальной мойки дверь одна. В какую заехал из той и выехал. А тут нужна была проходная система с двумя дверями. Поэтому входной импульс надо было первый подать на одни двери, второй на другие. Импульс открывает привод дверей, закрываются они сами, по своим концевикам, когда машина выедет.

Пляшем от корпуса. Нам нужен корпус на DIN рейку. Я взял Gainta D2MG

Чтобы сразу попасть во все размеры, была нагуглена и скачана 3D модель этого корпуса в step формате. Импортировал все в Fusion360 и снял размеры платы. Решено было делать два этажа, соединенных проводом. Внизу будут располагаться релюшки и модуль питания, а вверху мозги.

Силовая часть получилась такой:

Схема крупнее

Все довольно тривиально. Релюшки, да транзисторы. Транзисторы взял BCR112E — они идут уже со встроенными подтягивающими и токоограничивающими резисторами. Меньше деталей на плате будет. Использую их повсеместно, так что у меня их большие запасы.

Вход сделал через опторазвязку. Работает на замыкание. Зажигая светодиод оптопары.

Питается все от AC-DC модуля Tenstar Robot он же Hi-Link. Неплохой блочек, купил на алиэкспрессе пару десятков и с тех пор иногда использую в разных штучны проектах. Нареканий не было. Свои 500мА он честно выдает. Бывает на 3.3, 5 и 12 вольт.

Также есть возможность сделать питание от DC установив туда mini360 DC-DC которых у меня тоже валяется дохрена. При желании его можно отвязать через изолирующий DC-DC от Aimtec, про который я писал несколько месяцев назад. Правда на эти Mini360 есть нарекания. Я не сталкивался с их плохой работой, но коллеги жаловались, что дохнут, не держат влажность, пробиваются. Так что я бы с осторожностью к ним относился.

Релюшки взял пятивольтовые. Одна помощней, В2n на 3А — будет коммутировать свет. А остальные три послабже, будут дергать линии табла и дверей G5V1 5DC.

Получилась вот такая вот платка.

Второй этаж, мозговой, еще проще. Там стоит ATTiny2313 в DIP корпусе :) Пара кнопок, да разъем программирования, индикация с диодиками. Ну и джампер выбора режима. Из защитных элементов только супрессор и немного керамики пикофарадной на линиях, чтобы иголки жрать.

Тиньку, да еще и в DIP, взял потому, что у меня их осталось с одного заказа 10 летней давности полтора десятка. Куда еще их девать? Вот и пихнул в эти изделия. Чтобы не валялись.


Схема крупнее

Развел платку.

Выгрузил из KiCAD в step и пихнул обратно в Fusion360, чтобы посмотреть как оно встанет в корпус. Нет ли каких пересечений.

А после отправил в печать платы в JLCPCB.

Благодаря тому, что платы были срисованы с 3D модели, предоставленной производителем корпуса, то вошли они тик в тик, Прям идеально. Что не может не радовать.

Осталось сколхозить этикетку на морду девайса, тут я просто бумажку заламинировал и приклеил. Выфрезеровать бы, да краской залить. Было бы круче, но Вован, как назло, упер кабель от моего фрезера и так и не вернул!!! Вован, верни кабель! А времени не было ждать.

▌Код
Код устройства простой, но памяти у меня всего 2кб, ОЗУ и того меньше. Писать на ассемблере мне было ну совершенно вломы, стар я стал и ленив. Мой псевдо RTOS диспетчер, что на си, жирноват будет. Поэтому я ничего не стал изобретать, а сунул туда систему на конечных автоматах. А также систему событий на двух проходах, взятую у А. А. Шалыто из его статей о конечных автоматах (нагугливаются легко).

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(void)
{
	InitIO();         // Настраиваем порты ввода вывода. 
	InitMessages();   // Настраиваем систему сообщений 
	InitTime();       // Настраиваем таймер
 
 
// И начинаем запускать все задачи по кругу. В каждой фукнции все делается предельно быстро. Зашел, проверил, вышел. 
	while(1)
	{
	MessagesProcess();
	BlinkerProcess();
	InputProcess();
	logicProcess();
	}
 
return 0;
}

Функция MessageProcess обеспечивает взаимодействие между функциями. Суть в том, что сигнал на смену состояния автомата передается не флагом или глобальной переменной, а через переменную события за два прохода. Первый шагом сигнал взводится, второй раз, когда пробежит через все автоматы, встает на изготовку. И на третий раз, когда дойдет до адресата — исполняется. Или сбрасывается, если в этом проходе нет того, кто ждет этот сигнал. Описывается она в отдельных файлах, которые я таскаю из проекта в проект когда использую этот подход.

message.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef MESSAGES_h
#define MESSAGES_h
 
#include <stdint.h>
 
#define MAX_MESSAGES 		5
 
// Прототипы функций. 
void MessagesProcess(void);
void InitMessages(void);
void SendMessage(uint8_t Msg);
uint8_t GetMessage(uint8_t Msg);
 
// Обзываем наши события человеческими именами через enum
typedef enum
{
 InPressed = 0,
 Button1Pressed = 1,
 Button2Pressed
}tMessages;
 
 
#endif

message.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include "message.h"
uint8_t Messages[MAX_MESSAGES]; 	 // Массив ожидающих событий. 
 
void InitMessages(void)		 	// Инициализируем массив, обнуляя все события. 
{
	uint8_t i;
	for(i=0;i<MAX_MESSAGES;i++)
		{
		Messages[i] = 0;
		}
 
}
 
 
 
void SendMessage(uint8_t Msg)	// Шлем событие, записывая в нужную ячейку 1 - инициирующее значение. 
{
	if(Messages[Msg] == 0) Messages[Msg] = 1;
}
 
 
uint8_t GetMessage(uint8_t Msg)	// Чтение события. Если значение 2, то можно запускать. Все кто мог его уже пошевелили и считали. 
{
	if(Messages[Msg] == 2)
	{
	 Messages[Msg] = 0;	// После исполнения обнуляем. 
	 return 1;		// И говорим, что можно выполнять. 
	}
 
	return 0;		// Если же еще не готово, то возвращаем 0. 
}
 
 
// Задача обработки сообщений. Стоит в конце суперцикла и обрабатывает наши сообщения. Если 2, то значит никто не воспользовался и можно обнулить.
// Если же 1, то надо поставить на изготовку в состояние 2, чтобы значение было готово к считыванию. 
 
void MessagesProcess(void)		
{
	uint8_t i;
 
	for(i=0;i<MAX_MESSAGES;i++)
	{
		if(Messages[i] == 2) Messages[i] = 0;
		if(Messages[i] == 1) Messages[i] = 2;
	}
}

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

Для программирования выдержек есть программный таймер. Он реализован на принципе глобального времени. Тикаем раз в 1мс и увеличиваем 64 разрядную переменную. 264мс это примерно пол миллиарда лет до перезагрузки. Нам хватит, надеюсь. :) А дальше, при загадывании таймера, просто прибавляем к текущему времени желаемое и смотрим дожили ли мы до него или нет. Если дожили — меняем состояние автомата.

timers.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef TIMERS_H
#define TIMERS_H
 
#include <stdint.h>
#include <avr/interrupt.h>
 
#define MAX_TIMERS 5
 
 
void InitTime(void);
void TimerProcess(void);
uint64_t GetTime(uint32_t NewTime);
uint8_t TimeIsExpired(uint64_t Time);
 
#endif

timers.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include "timers.h"
 
// У нас есть переменная глобального времени. 
uint64_t GlobalTime; 
 
// Которую мы в прерывании таймера тикаем. 
ISR(TIMER0_COMPA_vect)
{
GlobalTime ++;
}
 
 
 
// Таймер настраиваем так, чтобы он тикал 1000раз в секунду. 
void InitTime(void)
{
	GlobalTime = 0;
	TCCR0A = 1<<WGM01;
	TCCR0B = 0<<CS02 | 1<< CS01 | 1<< CS00;  
	OCR0A = 125;
	TIMSK |= 1<<OCIE0A;
	sei();
}
 
 
// Когда надо запрограммировать таймер мы просто берем значение переменной времени в текущий момент.
// Но сделать это надо  Атомарно. Чтобы прерывание не вмешалось. Ведь переменная у нас аж 64 разрядная!!!
// Поэтому мы ее читаем в буфер при запрещенных прерываниях, а затем спокойно прибавляем к ней задержку
// И отдаем время "когда сработать". Как будильник завести. На часах 10.00, а надо кашу варить 15 минут?
// Окей ,запоминаем, что в 10.15 надо выключить кастрюльку. 
 
uint64_t GetTime(uint32_t NewTime)
{
	uint64_t buffer;
 
	cli();
	buffer = GlobalTime;
	sei();
 
	return buffer + NewTime;
}
 
// Проверяем текущее время. Даем функции то значение времени какое мы запомнили, а она скажет настало
// еще время или нет. Разумеется чтение переменной глобального времени надо делать атомарно. 
 
uint8_t TimeIsExpired(uint64_t Time)
{
	uint64_t buffer;
 
	cli();
	buffer = GlobalTime;
	sei();
 
	if (buffer > Time) return 1;
	return 0;
}

Так работает автоматная диспетчерская и ее таймер. А теперь пример реализации остального, на ее основе.

BlinkerProcess служит для тупого мигания лампочкой. Я обычно делаю такую фиговину и через нее сообщаю, что прога работает. Можно какие-то ошибки выводить морганиями ,отладку вести. В общем, удобная полезняшка.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void BlinkerProcess(void)
{
static uint8_t state=0;
static uint64_t Time=0;
 
	switch(state)
	{
		case 0: 		// ON
		{
			LED1ON;		// Макрос включения светодиода. Просто запись в порт. 
			state = 1;		
			Time = GetTime(100);	// Ставим таймер на 100мс. 
			break;
		}
 
		case 1:			// И ждем пока таймер не сработает. 
		{
			if (TimeIsExpired(Time)) state = 2;
			break;
		}
 
		case 2:
		{
			LED1OFF;		// Выключаем светодиод
			state = 3;		
			Time = GetTime(1000);	// Ставим таймер на 1000мс.
			break;
		}
 
		case 3:				// И ждем пока таймер не сработает. 
		{
			if (TimeIsExpired(Time)) state = 0;
			break;
		}
	}
}

InputProcess
Обрабатывает входящие сигналы и выдает события. Опрос кнопок и входных сигналов идет с защитой от дребезга. Просто состояние должно быть установившимся заданное количество циклов. Иначе не считается. :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
 
void InputProcess(void)
{
static uint8_t BounceCounterIN=0;
static uint8_t BounceCounterBT1=0;
static uint8_t BounceCounterBT2=0;
 
static uint8_t state=0;
static uint64_t Time=0;
 
	switch(state)
	{
	case 0: 		// Стадия проверки.
		{
 
			if (GetIN()) // Читаем данные из порта. Если замкнуто... 
			{
				BounceCounterIN++; // Увеличиваем счетчик антидребезга. 
 
				if(BounceCounterIN>5) // Если счетчик достиг 5, то значит это точно замыкание. 
				{
					BounceCounterIN = 0;	// Сбрасываем счетчик антидребезга. 
					SendMessage(InPressed);	// Шлем сообщение, в логику, что у нас есть входной сигнал.
				}
			}
			else  // Если же, вдруг, сигнал отпустился и до 5 не дотикало, значит помеха какая то. Обнуляем  счетчик антидребезга. 
			{
				BounceCounterIN = 0;
			}		
 
 
			if (GetB1())  // Аналогично обрабатываем кнопку В1
			{
				BounceCounterBT1++;
 
				if(BounceCounterBT1>5) 
				{
					BounceCounterBT1 = 0;
					SendMessage(Button1Pressed);
 
				}
			}
			else 
			{
				BounceCounterBT1 = 0;
			}
 
 
			if (GetB2())  // Аналогично обрабатываем кнопку В2
			{
				BounceCounterBT2++;
 
				if(BounceCounterBT2>5) 
				{
					BounceCounterBT2 = 0;
					SendMessage(Button2Pressed);
				}
			}
			else 
			{
				BounceCounterBT2 = 0;
			}
 
 
		state = 1;	 	// Переходим в стадию ожидания. 		
		Time = GetTime(10);	// Взводим таймер на 10мс, интервал между опросами.
		break;
		}
 
	case 1:				// Wait
		{
		if (TimeIsExpired(Time)) state = 0;	// Ждем пока не натикает 10мс. 
		break;
		}
 
	default: state = 0; break;
 
	}
}

LogicProcess
Главная логика выглядит примерно так, на примере мигалки, этой же платы. Просто набор case с состояниями и переход в следующее если было событие.
В коде логики работа с этим выглядит примерно так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 
.... кусочек выдранный из logicProcess(); 
 
	switch(state)
	{
		case 0:	// Empty State 
		{
			RL1OFF		// Door OFF
			RL3OFF;		// Door OFF
 
			RL2OFF;		// Light OFF;
			RL4OFF; 	// Table OFF
			LED2OFF;	// LED 2 OFF
 
 
			if(GetMessage(Button1Pressed))		// Если нажата кнопка
				{
					Time = GetTime(1000);	// Программируем таймер на 1000мс
					state = 4;		// И уходим ждать в стадию 4. 
					break;
				}
 
 
			if(GetMessage(InPressed))	state = 1;	// Если замкнули контакт идем на стадию 1... 
			break;
		}
 
.... 
....
 
		case 4:	// Wait 3s
		{
			if (TimeIsExpired(Time)) state = 5;		// Ждем пока таймер дотикает, а потом выходим в стадию 5. 
			break;
		}

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

Архив с проектом (код и печатные платы со схемой в KiCad)

Спасибо!!! Вы потрясающие! Всего за месяц мы собрали нужную сумму в 500000 на хоккейную коробку для детского дома Аистенок. Из которых 125000+ было от вас, читателей EasyElectronics!!! Были даже переводы на 25000+ и просто поток платежей на 251 рубль. Это невероятно круто!!! Сейчас идет заключение договора и подготовка к строительству!

А я встрял на три года, как минимум, ежемесячной пахоты над статьями :)))))))))))) Спасибо вам за такой мощный пинок!!!

17 thoughts on “Контроллер дверей”

    1. Есть плюсы и минусы. Но новый игл мне не понравился сразу. Там еще была непонятная чехарда с библиотеками. Онлайновые, оффлайновые… постоянно менялось. После я не следил. Импорт в ф360 работал очень странно. Ща стало еще странней. В ф360 появился свой «игл» встроенный.

  1. Красиво. За что люблю 21 век — любому человеку с руками и мозгом доступно мелкосерийное производство изделий промышленного уровня. Никакой кустарщины. Но кабель для фрезера надо вернуть!

  2. Не нашёл куда ещё написать, поэтому сюда.
    Огромное спасибо за ваши статьи, они просто супер! Очень помогают в том, что-бы разобраться в непонятных вещах. Сначало читаю всякие «шибко вумные» материалы, а потом ваши статьи, и все сразу встаёт на свои места…
    А теперь просьба: можете рассказать про квадратурный демодулятор/модулятор? И что такое мультиплексирование? И… А потом… И ещё… (так закатал губу и пошёл разбираться с очередной непонятной штукой)

      1. 25.12.21 вышла новая 6.0.0 — посмотрел, поправил один из проектов — интересные нововведения сделали. Рекомендую посмотреть.

    1. оххх… вроде бы в Компоненты и Технологии был цикл из десятка статей. А так да, плодовитый товарищ. В каждой бочке затычка :)

  3. А поделитесь опытом, плиз, как так точно рассчитать и разметить отверстия под кнопку и светодиоды, чтобы они туда так ровно и точно заходили. Тоже хочу попробовать спроектировать некий девайс, но как это делается — пока не научился.

    1. Так по 3Д модели все измерил с точностью до нанометра ,а потом аккуратно штангелем разметил, накернил и просверлил.

  4. Дилетантский вопрос. Есть модель корпуса – известны размеры стоек, которые должны проходить сквозь плату. Вопрос: если диаметр стойки x мм, то какое ответное отверстие заложить при разводке платы х+0.1 мм, или х+0.2 мм, или еще какой вариант и как влияет металлизация отверстия (сколько добавить к диаметру). И аналогичный вариант с выводными деталями, если известен диаметр вывода, то сколько добавлять к диаметру металлизированного отверстия, чтобы деталь входила, и припой потом лишний не утекал.

Добавить комментарий

Ваш e-mail не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.