Продовжимо вивчати керування двигунами постійного струму за допомогою Raspberry Pi, але тепер додаємо в код Python класи, щоб зробити його більш ефективним.
Використання подвійного H-моста на L293D для керування двома двигунами
Зазвичай є потреба мати принаймні два двигуни постійного струму: для переміщення лівого і правого набору коліс. Двигуни будуть використовуватися для переміщення платформи вперед і назад, а також, щоб повертати її вліво і вправо.
Для цього, перейдемо від управління одним двигуном постійного струму до двох незалежних двигунів постійного струму. Управління двома двигунами постійного струму буде виконане за допомогою розглянутої раніше ІС L293D подвійного H-моста.
Спочатку подивимося на схему під’єднань виводів GPIO до L293D:
Для керування напрямком двигуна 1 GPIO 4 і GPIO 17
Для керування широтно-імпульсною модуляцією ( ШІМ - PWM): GPIO 18 (спільно для обох двигунів)
Для підключення двигуна 1 постійного струму: M1+ і M1-
Під’єднання джерел живлення: Battery+ і 5v Pi
Земля: GND і GND
Зеленим кольором позначено те, що додано для того, щоб отримати керування другим двигуном постійного струму, спільно використовуючи драйвер двигуна на подвійному H-мосту L293D.
Для керування напрямком двигуна 2: GPIO 23 і GPIO 24
Для підключення двигуна 2 постійного струму: M2+ і M2-
Ми використовуємо один модуль PWM для управління швидкістю обертання двигунів, бо вони обидва повинні обертатися з однаковою швидкістю. Спільне використання GPIO 18 для обох двигунів працює нормально. Недоліком використання модуля PWM є те, що він тільки один. Ми не можемо використовувати його як для обох двигунів постійного струму, так і для сервоприводів.
Давайте тепер подивимося на код. Для того, щоб мати ефективний код для двигунів, створимо клас Motor - тоді ми зможемо створити екземпляри для кожного з двох двигунів з одним визначенням класу Motor:
class Motor(object):
def __init__(self, in1_pin, in2_pin):
self.in1_pin = in1_pin #1
self.in2_pin = in2_pin
GPIO.setup(self.in1_pin, GPIO.OUT) #2
GPIO.setup(self.in2_pin, GPIO.OUT)
def clockwise(self): #3
GPIO.output(self.in1_pin, True)
GPIO.output(self.in2_pin, False)
def counter_clockwise(self): #4
GPIO.output(self.in1_pin, False)
GPIO.output(self.in2_pin, True)
def stop(self): #5
GPIO.output(self.in1_pin, False)
GPIO.output(self.in2_pin, False)
1. Створення методу конструктора класу __init__, який розглянемо нижче
2. Встановлення вибраних виводів RPi як вихідних
3. Функція для формування сигналів обертання двигуна за годинниковою стрілкою
4. Функція для формування сигналів обертання двигуна проти годинникової стрілки
5. Функція для формування сигналів зупинки двигуна
Перш, ніж продовжити проект з керування двома двигунами, спробуємо розібратися з класами в Python.
Класи в Python
Є одна річ в програмуванні, про яку ви повинні знати: програмісти люблять бути лінивими. Їх філософія: якщо щось було зроблено кимсь раніше, то чому вони повинні робити це знову?
Наприклад, функції в Python. Ви записуєте свій код як функцію і повторно використовуєте для всіх випадків. Можете звернутися до функції в будь-якому місці свого коду і комп'ютер завжди буде знати, що ви маєте на увазі.
Звичайно, функції мають свої обмеження. Функції не зберігають інформацію, наприклад, про зроблені зміни - щоразу, коли функція запускається, вона починає заново. Тим не менш, деякі функції та змінні пов'язані між собою дуже тісно, і повинні часто взаємодіяти один з одним.
Ці та інші проблеми вирішує так зване об'єктно-орієнтоване програмування (ООП). Воно співставляє функції і змінні разом таким чином, що вони можуть бачити одне одного і працювати разом, бути відтвореними і змінюватися в міру необхідності, а не тоді, коли нікому не потрібні.
Для цього використовують "клас".
Що ж таке клас? Думайте про клас, як про план. Це не щось саме по собі - він просто описує, як щось зробити. Ви можете створити безліч об'єктів з цього плану, відомих, як екземпляри.
Так звані "класи" дуже легко створити з оператором class.
Створення класу
Приклад 1 - Визначення класу
# Визначення класу
class class_name:
[твердження 1]
[твердження 2]
[твердження 3]
[і т.д.]
Майже незрозуміло? Це нормально, тому приклад, який створює клас Shape:
Приклад 2 - Приклад класу Shape
#Приклад класу
class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
self.description = "Форма ще не описана"
self.author = "Ніхто не стверджував, що вже зробив цю
форму"
def area(self):
return self.x * self.y
def perimeter(self):
return 2 * self.x + 2 * self.y
def describe(self, text):
self.description = text
def authorName(self, text):
self.author = text
def scaleSize(self, scale):
self.x = self.x * scale
self.y = self.y * scale
Створене є описом форми (тобто змінні) і які операції ви можете робити з формою (тобто функції). Це дуже важливо - ви зробили не фактичну форму, а просто описали, що форма є. Форма має ширину (х), висоту (у), а також площу і периметр (area(self) і perimeter(self)). Жоден код не запускається, коли визначаєте клас - ви просто створюєте функції і змінні.
Описаний метод називається конструктором класу і в мові програмування Python, носить ім'я __init__ (напочатку і наприкінці стоїть по два знаки підкреслення.)
Першим параметром, як і у будь-якого іншого методу, у __init__ є параметр self, на місце якого підставляється об'єкт в момент його створення. Другий і наступні (якщо є) параметри замінюються аргументами, що передалися в конструктор під час виклику класу.
В даному випадку функція __init__ запускається, коли ми створюємо примірник Shape - тобто, коли ми створюємо фактичну форму, на відміну від «плану», який тут маємо, __init__ запускається.
Змінна self є примірником самого об'єкта. Більшість об'єктно-орієнтованих мов передають це як прихований параметр методів, визначених на об'єкті. Python робить не так: ви повинні заявити про це відкрито.
self - це звертання до речей класу зсередини нього. self буде першим параметром в будь-якій функції, визначеній всередині класу. Будь-яка функція або змінна, створена на першому рівні відступу (тобто рядки коду, які починаються праворуч через один TAB, після того, як поставили class Shape), автоматично вводяться в self. Для того, щоб отримати доступ до цих функцій і змінних з інших місць усередині класу, їх імена повинні мати попереду self (наприклад, self.variable_name).
Це чудово, що ми створили клас, але як його тепер використати? Нижче приклад, як ми можемо створити примірник класу. Наголошуємо, що код з прикладу 2 повинен вже бути запущений:
Приклад 3 – Створення примірника класу
rectangle = Shape(100, 45)
Ми створюємо примірник класу, даючи спочатку назву (в даному випадку, Shape), а потім, в дужках, значення для переходу до функції __init__. Функція ініціалізації запускається (з використанням параметрів, які передали в дужках), а потім викладає примірник цього класу, який в даному випадку, призначений для імені прямокутника.
Уявіть наш примірник класу, тобто прямокутник, як самодостатню сукупність змінних і функцій. Так само, як ми використовували self для доступу до функцій і змінних примірника класу всередині нього, ми використовуємо ім'я, яке присвоїли йому зараз (Shape) для доступу до функцій і змінних примірника класу поза його межами.
Розглянемо два класи: в одному використаємо конструктор, а в іншому обійдемося без нього. Потрібно створити два атрибути об'єкта.
Приклад 4 – Використання конструктора
class YesInit:
def __init__(self,one,two):
self.fname = one
self.sname = two
obj1 = YesInit("Peter","Ok")
print (obj1.fname, obj1.sname)
Приклад 5 – Клас без конструктора
class NoInit:
def names(self,one,two):
self.fname = one
self.sname = two
obj1 = NoInit()
obj1.names("Peter","Ok")
print (obj1.fname, obj1.sname)
Виведення інтерпретатора в обох випадках:
Peter Ok
В обох програмах у об'єкта з'являються два атрибути: fname і sname. Однак, в першому випадку вони не започатковані при створенні об'єкта і повинні передаватися в дужках при виклику класу. Якщо якісь атрибути повинні бути присутніми у об'єктів класу обов'язково, то використання методу __init__ - ідеальний варіант. У другій програмі (без використання конструктора) атрибути створюються шляхом виклику методу names після створення об'єкта. В даному випадку виклик методу names необов'язковий, тому об'єкти можуть існувати без атрибутів fname і sname.
Зазвичай метод __init__ передбачає передачу аргументів при створенні об'єктів, однак аргумент може бути не переданий. Наприклад, якщо в прикладі вище створити об'єкт так: obj1 = YesInit(), тобто не передати класу аргументи, то станеться помилка. Щоб уникнути подібних ситуацій, можна в методі __init__ назначати параметрам значення за замовчуванням. Якщо при виклику класу були задані аргументи для даних параметрів, то добре - вони і будуть використовуватися, якщо ні - ще краще - в тілі методу будуть використані значення за замовчуванням:
Приклад 6 – Використання значень за замовчуванням
class YesInit:
def __init__(self,one="noname",two="nonametoo"):
self.fname = one
self.sname = two
obj1 = YesInit("Sasha","Tu")
obj2 = YesInit()
obj3 = YesInit("Spartak")
obj4 = YesInit(two="Harry")
print (obj1.fname, obj1.sname)
print (obj2.fname, obj2.sname)
print (obj3.fname, obj3.sname)
print (obj4.fname, obj4.sname)
Виведення інтерпретатора:
Sasha Tu
noname nonametoo
Spartak nonametoo
noname Harry
В даному випадку, другий об'єкт створюється без передачі аргументів, тому в методі __init__ використовуються значення за замовчуванням ( "noname" і "nonametoo"). При створенні третього і четвертого об'єктів передаються по одному аргументу. Якщо вказується значення не першого аргументу, то слід явно вказати ім'я параметра (четвертий об'єкт).
Метод __init__ може містити параметри як без значень за замовчуванням, так і зі значеннями за замовчуванням. В такому випадку, параметри, аргументи яких повинні бути обов'язково вказані при створенні об'єктів, вказуються першими, а параметри зі значеннями за замовчуванням – після них.
Ще кілька прикладів для засвоєння.
Приклад 7 - Програма «Безкорисні кнопки - 2»
Програма «Безкорисні кнопки - 2» - це «Безкорисні кнопки» з попереднього заняття, переписана в об’єктному стилі. З точки зору користувача, все залишилось попереднім, але внутрішня структура програми все ж зазнала деяких змін
Імпорт модуля tkinter
Не дивлячись на значні нововведення в структурі програми, модуль графічного інтерфейсу, як і раніше, завантажується на самому початку:
# Безкорисні кнопки - 2
#Демонструє створення класу в віконному додатку на основі tkinter
from tkinter import *
Оголошення класу Application
Тепер створимо новий клас Application, похідний від Frame:
class Application(Frame):
""" GUI-додаток з трьома кнопками. """
Замість того, щоб створювати примірник класу Frame, будемо інстанціювати клас Аррlication, об’єкт якого вже буде мати всі кнопки. Такий підхід зручний тому, що об’єкт Аррlication - це всього лише особливого роду рамка.
Оголошення методу-конструктора
В класі Аррlication оголошуємо конструктор:
def __init__(self, master):
""" Ініціалізує рамку. """
super(Application,self).__init__(master)
self.grid()
self.create_widgets()
В цьому коді спочатку викликаємо конструктор надкласу. Об’єкту Аррlication передається ім’я батьківського елементу (вікна); в цих умовах об’єкт веде себе, як звичайна рамка. Потім викликаємо метод об’єкту create_widgets(), оголошений далі.
Оголошення методу, який створює елементи управління
Метод create_widgets(), який створює всі три кнопки на рамці:
def create_widgets(self):
""" Створює три безкорисні кнопки. """
# перша кнопка
self.bttn1 = Button(self, text = "Я нічого не роблю!")
self.bttn1.grid()
# друга кнопка
self.bttn2 = Button(self)
self.bttn2.grid()
self.bttn2.configure(text ="І я також!")
# третя кнопка
self.bttn3 = Button(self)
self.bttn3.grid()
self.bttn3["text"] ="І я!"
Цей код дуже схожий на ті інструкції, за допомогою яких ми створювали кнопки в першій версії програми «Безкорисні кнопки». Важлива відмінність полягає в тому, що bttn1, bttn2 і bttn3 - атрибути об’єкта Аррlication. Ще одна відмінність така: як батьківський елемент для кнопок назначений self, отже, вони будуть «прикріплені» до об’єкту Аррlication.
Створення об’єкту класу Application
В основній частині програми створюємо базове вікно, присвоюємо йому заголовок і назначаємо відповідні розміри:
# основна частина
root = Tk()
root.title("Бeзкорисні кнопки - 2")
root.geometry("200x85")
Слідом за цим інстанцируємо клас Аррliсаtion з батьківським елементом root:
арр = Application(root)
Даний код створює об’єкт Аррlication у вікні root. Метод-конструктор цього об’єкту викликає метод create_widgets(), який, в свою чергу, створює всередині об’єкта Аррlication три кнопки. В конці, як зазвичай, запускається цикл подій, який починає роботу GUI:
root.mainloop()
Зв’язування елементів управління з обробниками подій
GUI-додатки, розглянуті раніше, практично безкорисні. Це тому, що в них на активізацію користувачем елементів управління не реагує жоден код. Отже, якщо раніше наші елементи були схожі на обезструмлені люстри без лампочок, то тепер «підключимо» їх і заставимо ефективно працювати. В термінах GUI-програмування зараз займимося тим, що будемо писати обробники подій і зв’язувати їх з подіями.
Приклад 8 - Програма «Лічильник клацань»
В програмі «Лічильник клацань» є працююча кнопка: на ній відображається, скільки разів користувач її натиснув. Якщо говорити технічно, зв’язаний з цією кнопкою обробник щоразу при події (клацанні) збільшує кількість натискань на одиницю і змінює текст на кнопці, тобто тоді, коли користувач натискає кнопку
Налаштування програми
Традиційний перший крок - загрузити модуль графічного інтерфейсу:
# Лічильник натискань
# Демонструє зв’язування подій з обробниками
from tkinter import *
Слідом за цим оголошується Аррlication:
class Application(Frame):
""" GUI-додаток, який підраховує кількість натискань кнопки. """
def __init__(self, master):
""" Ініціалізує рамку. """
super(Application,self).__init__(master)
self.grid()
self.bttn_clicks = 0# кількість натискань
self.create_widget()
Більшою частиною цей код уже знайомий. З нового в ньому тільки один рядок, self.bttn_clicks = 0, який створює у об’єкта атрибут, який стежить за кількістю натискань кнопки.
Зв’язування обробника з подією
В методі create_widget() створимо єдину кнопку:
def create widget(self):
""" Створює кнопку, на якій відображається кількість зроблених натискань. """
self.bttn = Button(self)
self.bttn["text"]= "Кількість кліків: 0"
self.bttn["command"] = self.update_count
self.bttn.grid()
Параметр coпmand елемента посилається на метод update_count(). Тому, коли користувач натискає кнопку, викликається даний метод. Як говорять програмісти, ми «зв’язали» подію (натискання кнопки) з обробником події (методом update_count()). Параметр command будь-якого елементу управлення взагалі призначений для того, щоб при його активації викликати метод-обробник.
Створення обробника події
Тепер напишемо метод update_count(), який буде обробляти натискання кнопки:
def update_count(self):
""" Збільшує кількість натискань на одиницю і відображає її. """
self.bttn_clicks += 1
self.bttn["text"] = "Кількість кліків: " + str(self.bttn_clicks)
Цей метод збільшує загальну кількість зроблених користувачем натискань кнопки, а потім змінює текст на кнопці так, щоб відображалось це нове число. Проста механіка, але завдяки їй кнопка стала хоча б трохи корисною.
Обгортка програми
Основна частина коду вже відома:
# основна частина
root = Tk()
root.title("Kількість кліків")
root.geometry("200x50")
арр = Application(root)
root.mainloop()
Створили базове вікно root, призначили його заголовок і встановили розміри. Потім створили екземпляр класу Аррlicatiоп всередині батьківського елементу root. Нарешті, запуск циклу події базового вікна виводить GUI на екран.
Знову про керування двигунами
У підручнику Adafruit, є допоміжна функція, яка називається set(), яку залишимо поза класом. Її можна включити в клас пізніше, якщо так буде зручніше.
Функція set():
def set(property, value):
try:
f = open("/sys/class/rpi-pwm/pwm0/" + property, 'w')
f.write(value)
f.close()
except:
print("Error writing to: " + property + " value: " + value)
Нижче наведений весь зібраний код програми rover.py для керування двигунами:
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
left_in1_pin = 4
left_in2_pin = 17
right_in1_pin = 23
right_in2_pin = 24
class Motor(object):
def __init__(self, in1_pin, in2_pin):
self.in1_pin = in1_pin
self.in2_pin = in2_pin
GPIO.setup(self.in1_pin, GPIO.OUT)
GPIO.setup(self.in2_pin, GPIO.OUT)
def clockwise(self):
GPIO.output(self.in1_pin, True)
GPIO.output(self.in2_pin, False)
def counter_clockwise(self):
GPIO.output(self.in1_pin, False)
GPIO.output(self.in2_pin, True)
def stop(self):
GPIO.output(self.in1_pin, False)
GPIO.output(self.in2_pin, False)
def set(property, value):
try:
f = open("/sys/class/rpi-pwm/pwm0/" + property, 'w')
f.write(value)
f.close()
except:
print("Error writing to: " + property + " value: " + value)
try:
set("delayed", "0")
set("frequency", "500")
set("active", "1")
left_motor = Motor(left_in1_pin, left_in2_pin)
right_motor = Motor(right_in1_pin, right_in2_pin)
direction = None
while True:
cmd = input("Command, f/r/o/p/s 0..9, E.g. f5 :")
# if enter was pressed with no value, just stick with the current value
if len(cmd) > 0:
direction = cmd[0]
if direction == "f":
left_motor.clockwise()
right_motor.clockwise()
elif direction == "r":
left_motor.counter_clockwise()
right_motor.counter_clockwise()
elif direction == "o": #opposite1
left_motor.counter_clockwise()
right_motor.clockwise()
elif direction == "p": #opposite2
left_motor.clockwise()
right_motor.counter_clockwise()
else:
left_motor.stop()
right_motor.stop()
# only need to adjust speed if we want to
if len(cmd) > 1:
speed = int(cmd[1]) * 11
set("duty", str(speed))
except KeyboardInterrupt:
left_motor.stop()
right_motor.stop()
print ("\nstopped")
Для запуску:
$ sudo python rover.py
Команди: f/r/o/p/s 0..9, наприклад, f5 :f
f == вперед (forward)
r == назад (reverse)
o == змінити напрям (opposite directions)
p == змінити напрям (opposite directions)
s == зупинити (stop)
Повне швидке зупинення двигунів: CTRL+C
За матеріалами: roverpi.blogspot.com