Как стать автором
Обновить

История о том, как я угольный котел автоматизировал

Время на прочтение8 мин
Количество просмотров45K
В холодные зимние вечера, когда температура на улице достигала -40 градусов. Я понял, что мне приходится совершать очень много однотипных действий, которые, на первый взгляд, очень просто автоматизировать.

Подробности о том, как не программист угольный котел оживлял ниже.

image
Постановка задачи

Для того, чтобы понять задачу, я в кратце опишу суть работы современных угольных котлов, таких как: Прометей, Bosh или Buderus:

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

По началу показалось все просто: берем, к примеру, привод от центр.замка автомобильной двери или любое похожее приспособление и по таймеру подаем на него питание.

Концепция устройства

Управляющим блоком было решено выбрать — Arduino, за всенародную любовь и большое количество материалов по нему. В интернете был заказан Arduino Uno, а также блок реле на 12В и блок питания на 2,5А.



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

Из металлического уголка под него было подготовлено крепление к полу и сделана съемная насадка на рычаг.



Пришло время переходить к логике:
1. Если приложить мало усилий, то зола недостаточно освободит место для новой партии угля и может приостановить горение.
2. Если приложить усилий больше чем необходимо, то можно выкинуть в отстойник весь уголь.

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

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



с другой стороны:



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



Собираем все на место:


подгоняем все элементы


проверяем экран и управление


собираем все в кучу

Код
/*
Prometey shaker
17.11.2014
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#define SHAKERSTEPUPPIN 4 // Пин для кнопки Шаг++
#define SHAKERSTEPDOWNPIN 5 // Пин для кнопки Шаг--
#define MANUALSHAKE 6 // Ручной запуск/Меню ОК
#define POWERRELE 8 // Питания на Реле
int eppromaddr = 0;
int SHAKERSTEP = 300; // Шаг в секундах с которым будет увеличиваться/уменьшаться период встряхивания.
int sensorPin0 = A1; // select the input pin for the potentiometer
float stepValue = 0.0986328125; // step value per one of 0..1023 ((50A * 2 + 1)/1024)
int ZeroLevel = 514; // Zero level. Нулевоз значение АЦП для датчика тока (высчитывается в setup)
float CurrentLevel = 3; // Пороговое значение для тока (задается программно)
byte CURRSHAKE, LEFTSHAKE = 2; // Пин для активации встряхивателя вращение по часовой стрелки + текущий пин для встряхивания
byte RIGHTSHAKE = 3; // Пин для активации встряхивателя вращение против часовой стрелки
byte DOVOD = 12; // Пин для активации доводчика (Прометей)
int buttonState = 0; // Состояние нажатой кнопки
boolean buttonPressed = false;
boolean showMenu = false;
byte menuItem = 0;
volatile long mks100;
volatile long ms10;
volatile int cntr;
long tmillis,tms10=0;
unsigned long shaketimer, shaketime = 0;
byte stopshake; // Время работы шейкера
byte stpdvd; // Время работы доводчика (Прометей)
boolean flip = false; // Для отслеживания активности шейкера — стоит ли вызывать функцию проверки тока и включать реверс.
boolean flipD = false; // Для отслеживания активности доводчика — стоит ли вызывать функцию включения доводчика (Прометей)
byte shakerstepcount = 0;
long temp = 0; // временная переменная для отслеживания времени
boolean tempcurr = 0; // временная переменная для вывода сообщения об измерении тока
boolean isinit = true;
LiquidCrystal_I2C lcd(0x27,16,2);
void setup() {
Serial.begin(9600);
// инициализация портов для кнопок
pinMode(SHAKERSTEPUPPIN, INPUT);
digitalWrite(SHAKERSTEPUPPIN, HIGH);
pinMode(SHAKERSTEPDOWNPIN, INPUT);
digitalWrite(SHAKERSTEPDOWNPIN, HIGH);
pinMode(MANUALSHAKE, INPUT);
digitalWrite(MANUALSHAKE, HIGH);
lcd.init(); // Инициализация lcd
lcd.backlight(); // Включаем подсветку
lcd.print(«Version 3.0»);
lcd.setCursor(0, 1);
lcd.print(«13.11.2014»);
delay(1000);
lcd.clear();
pinMode(sensorPin0, INPUT); // Curent sensor
digitalWrite(sensorPin0, HIGH); // Включаем внутренний подтягивающий резистор
shaketimer = SHAKERSTEP;
mks100 = 0; // счетчик сотен микросекунд, переполнение счетчика примерно через 5 суток
ms10 = 0; // счетчик десятков миллисекунд, переполнение счетчика примерно через 16 месяцев
cntr = 0;
flip = 0;
flipD = 0; // просто добавил это здесь (Прометей)
pinMode(LEFTSHAKE, OUTPUT); // Подготавливаем порты для активации реле. Реле инвертное, срабатывает при подачи GND на контрольные пины.
pinMode(RIGHTSHAKE, OUTPUT); // Подготавливаем порты для активации реле. Реле инвертное, срабатывает при подачи GND на контрольные пины.
CURRSHAKE = LEFTSHAKE;
digitalWrite(LEFTSHAKE, HIGH); // Подготавливаем порты для активации реле. Реле инвертное, срабатывает при подачи GND на контрольные пины.
digitalWrite(RIGHTSHAKE, HIGH); // Подготавливаем порты для активации реле. Реле инвертное, срабатывает при подачи GND на контрольные пины.
pinMode(SHAKERSTEPUPPIN, INPUT); // Подготавливаем порты для кнопки
digitalWrite(SHAKERSTEPUPPIN, HIGH);
pinMode(SHAKERSTEPDOWNPIN, INPUT); // Подготавливаем порты для кнопки
digitalWrite(SHAKERSTEPDOWNPIN, HIGH);
pinMode(POWERRELE, OUTPUT); // Подаем питание на реле (Прометей)
digitalWrite(POWERRELE, HIGH);
pinMode(DOVOD, OUTPUT); // Подготавливаем порт на доводчик (Прометей)
digitalWrite(DOVOD, HIGH);
// Включаем нужный нам режим таймера/счетчика — нормальный
TCCR2A = 0; //нормальный режим (по умолчанию 1 — ШИМ с коррекцией фазы?)
// Предделитель таймера/счетчика настраиваем на 16 — // это позволит «тикать» таймером каждую микросекунду
// (в предположении, что сердце микроконтроллера стучит с
// частотой 16.000.000 ударов в секунду)
TCCR2B = 2; // 010 — fclk/8 (по умолчанию 100 — fclk/64)
//TCCR2B = 7; // 111 — fclk/1024 (по умолчанию 100 — fclk/64)
TCNT2=59;//55;
TIMSK2 |= (1 << TOIE2); // разрешаем прерывание таймера/счетчика 2 по переполнению
}
ISR(TIMER2_OVF_vect) {
// прежде всего взводим счетчик
TCNT2=59;//55;
// прошли очередные 100 мксек — увеличиваем счетчик сотен микросекунд
mks100++;
// if(mks100%100==0) ms10++;
cntr++;
// прошли очередные 10 мсек? — увеличиваем счетчик десятков миллисекунд
if(cntr>99) {
ms10++;
cntr = 0;
}
}
float getCurrent() {
int sensorRead = 0;
for (int i=0; i <= 4; i++){
sensorRead+=analogRead(sensorPin0);
}
sensorRead = sensorRead / 5;
// lcd.setCursor(8,1);
// lcd.print(sensorRead);
// lcd.print(" ");
return (abs(sensorRead — ZeroLevel)) * stepValue;
// return abs(analogRead(sensorPin0) — ZeroLevel) * stepValue;
}
void current_check() {
float CurrentValue = getCurrent();
if (CurrentValue >= CurrentLevel) {
stop_shake();
if (CURRSHAKE == LEFTSHAKE) {
CURRSHAKE = RIGHTSHAKE;
} else {
CURRSHAKE = LEFTSHAKE;
}
start_shake();
}
// Проверяем значение датчика тока. Если значение больше порогового (расчетное 5А),
// то деактивируем шейкер, меняем CURRSHAKE на противоположное (LEFTSHAKE, RIGHTSHAKE)
// и активируем по-новой.
}
void start_shake() {
digitalWrite(CURRSHAKE, LOW); // Подаем сигнал на активацию шейкера
shaketime = 0;
flip = true;
delay(500);
}
void stop_shake() {
digitalWrite(CURRSHAKE, HIGH); // Подаем сигнал на деактивацию шейкера
flip = false;
// stopshake = 0;
}
void startdovod() {
digitalWrite(DOVOD, LOW); // Подаем сигнал на активацию доводчика (Прометей)
flipD = true;
}
void stopdovod() {
digitalWrite(DOVOD, HIGH); // Подаем сигнал на деактивацию доводчика (Прометей)
flipD = false;
}
void lcdTimer() {
lcd.setCursor(0, 0);
lcd.print(«shake:»);
lcd.setCursor(6, 0);
lcd.print(SHAKERSTEP/60 — shaketime/60);
lcd.print("/");
lcd.print(SHAKERSTEP/60);
lcd.print(" ");
}
void lcdCurrent() {
lcd.setCursor(0, 1);
lcd.print(«C:»);
lcd.setCursor(2, 1);
lcd.print(getCurrent(), 1);
lcd.print("/");
lcd.print(CurrentLevel, 1);
}
int buttonRead() {
int readState = 0;
int returnCode = 0;
readState = !digitalRead(SHAKERSTEPUPPIN);
returnCode = readState;
readState = !digitalRead(SHAKERSTEPDOWNPIN) * 2;
returnCode+=readState;
readState = !digitalRead(MANUALSHAKE) * 4;
returnCode+=readState;
if (returnCode > 0) { delay(200); }
//lcd.setCursor(0,0);lcd.print(returnCode);
return returnCode;
}
void lcdMenu() {
lcd.setCursor(0, 0);
lcd.print(«1.Timer set»);
lcd.setCursor(0, 1);
lcd.print(«2.Current set»);
lcd.setCursor(0, 0);
}
void lcdCurrentMenu() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(«Current lvl:»);
lcd.print(CurrentLevel,1);
}
void lcdTimerMenu() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(«Timer set:»);
lcd.print(SHAKERSTEP/60);
}
void buttonProcced(byte buttonState) {
// Ручной shake
if ((buttonState == 4)&&(!showMenu)) {
start_shake();
}
// Ручной dovod
if ((buttonState == 3)&&(!showMenu)) {
startdovod();
}
// вызов меню
if (buttonState == 3) {
menuItem = 1;
lcd.clear();
showMenu = !showMenu;
if (showMenu) {
lcd.blink();
lcdMenu();
}
if (!showMenu) {
byte lowByte = ((SHAKERSTEP >> 0) & 0xFF);
byte highByte = ((SHAKERSTEP >> 8) & 0xFF);
EEPROM.write(eppromaddr, lowByte);
EEPROM.write(eppromaddr+1, highByte);
EEPROM.write(eppromaddr+2, CurrentLevel*10);
lcd.noBlink();
lcdTimer();
lcdCurrent();
}
}
// кнопка вниз в основном меню
if ((showMenu)&&(buttonState == 2)&&(menuItem < 3)) {
menuItem = 2;
lcd.setCursor(0, 1);
}
// кнопка вверх в основном меню
if ((showMenu)&&(buttonState == 1)&&(menuItem < 3)) {
menuItem = 1;
lcd.setCursor(0, 0);
}
// кнопка вниз в меню тока
if ((menuItem == 2)&&(buttonState == 4)) {
menuItem = 4;
buttonState = 0;
lcdCurrentMenu();
}
// кнопка вверх в меню тока
if ((menuItem == 4)&&(buttonState == 2)) {
CurrentLevel-=0.5;
if (CurrentLevel<1) {CurrentLevel=8;}
lcdCurrentMenu();
}
// кнопка вниз в меню тока
if ((menuItem == 4)&&(buttonState == 1)) {
CurrentLevel+=0.5;
if (CurrentLevel>=8) {CurrentLevel=1;}
lcdCurrentMenu();
}
// кнопка Ок в меню тока
if ((menuItem == 4)&&(buttonState == 4)) {
buttonState = 0;
menuItem = 1;
lcd.clear();
lcdMenu();
}
// кнопка Ок — выбор меню таймера
if ((menuItem == 1)&&(buttonState == 4)) {
menuItem = 3;
buttonState = 0;
lcdTimerMenu();
}
// кнопка вниз в меню таймера
if ((menuItem == 3)&&(buttonState == 2)) {
SHAKERSTEP-=300;
if (SHAKERSTEP <= 0) {SHAKERSTEP = 3600;}
// if (SHAKERSTEP > 3600) {SHAKERSTEP = 300;}
lcdTimerMenu();
}
// кнопка вверх в меню таймера
if ((menuItem == 3)&&(buttonState == 1)) {
SHAKERSTEP+=300;
// if (SHAKERSTEP > 0) {SHAKERSTEP = 3600;}
if (SHAKERSTEP > 3600) {SHAKERSTEP = 300;}
lcdTimerMenu();
}
// книпка Ок в меню таймера
if ((menuItem == 3)&&(buttonState == 4)) {
menuItem = 1;
lcd.clear();
lcdMenu();
}
}
void inits() {
ZeroLevel = 0;
for (int i=0; i <= 9; i++){
ZeroLevel+=analogRead(sensorPin0);
}
ZeroLevel = ZeroLevel/10;
lcd.print(«zerolevel=»);lcd.print(ZeroLevel);
// stepValue =;
lcd.setCursor(0,1);
lcd.print(«stepvalue=»);lcd.print(stepValue);
delay(1000);
lcd.clear();
byte lowByte = EEPROM.read(eppromaddr);
byte highByte = EEPROM.read(eppromaddr + 1);
SHAKERSTEP = ((lowByte << 0) & 0xFF) + ((highByte << 8) & 0xFF00);
CurrentLevel = EEPROM.read(eppromaddr+2)/10;
lcdTimer();
lcdCurrent();
isinit = false;
}
void loop() {
if (isinit) { inits(); }
buttonState = buttonRead();
if (buttonState > 0) { buttonProcced(buttonState); }
if (ms10>tms10) {
tms10 = ms10;
if (tms10%1000==0) { // выполнение каждые 10 сек
shaketime += 10;
if (!showMenu) {
lcdTimer();
lcdCurrent();
}
}

if ((flip)&&(tms10%100==0)) { // выполнение каждую 1 сек
stopshake += 1;
}

if ((flipD)&&(tms10%100==0)) { // выполнение каждую 1 сек
stpdvd += 1;
}
if (stopshake>=3) { // сколько секунд работать
stop_shake();
stopshake = 0;
// активируем доводчик (Прометей)
startdovod();
}
if ((flipD)&&(stpdvd>=4)) {
stopdovod();
}
if (shaketime>=SHAKERSTEP) {
start_shake();
}
}
if (flip) {
current_check();
}
}

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

Отладка

Но отладить логику оказалось немного сложнее. Система делалась под два котла: «Прометей» и «Buderus». С виду они похожи, но система колосников совершенно разная на практике. «Buderus», допускает частое и продолжительное шевеление колосниками, в отличие от «Прометея», который легко выкидывает содержимое топки. Также в «Прометее» необходимо обязательно вернуть в обратное положение рычаг колосников.

Поэтому в коде для «Прометея» было решено задействовать еще одну возможность автомобильного стеклоочистителя — третий контакт, который возвращает дворники на место.

Система работает второй год. Когда на улице -40 реально выручает, а то приходилось несколько раз за ночь вставать и дергать этот рычаг. Код программы далеко не идеальный, но к сожалению я не программист, как и тот человек который это придумал и начал воплощать первый вариант. Если будет конструктивная критика — то можете помочь его оптимизировать для тех, кому он может пригодиться.
Теги:
Хабы:
+30
Комментарии14

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн