Регістри зсуву та уніполярні крокові двигуни


Довгий пост про аматорську саморобну залізяку. Купа мильних фоток.

There is an English translation of this post.

Є дуже багато різних готових драйверів крокових двигунів і готових для використання motor shield’ов: бери, підключай та використовуй. Але мені захотілося зробити свою плату зі своїми, дуже специфічними приколами.

Ну от. Зробив.

Мета проекту

Прикол перший: я люблю маленькі дешеві уніполярні 5-вольтові крокові двигуни 28BYJ-48. Так, я знаю, уніполярні двигуни неефективні, а 5-вольтові це взагалі щось повільне, малопотужне і не варте уваги. Але уніполярні значно простіші, не вимагають складного драйвера. І взагалі, дорогі потужні біполярні двигуни у важких квадратних корпусах NEMA, які зазвичай використовують в 3D-принтерах, верстатах CNC та інших серйозних штуках — це щось індустріальне, а маленькі «іграшкові» 28BYJ-48 як раз відповідають моєму уявленню про масштаб хобі.

Прикол другий: я не хочу використовувати всюдисущі окремі плати-драйвери керування, зібрані на мікросхемі сімейста ULN2003; я хочу використовувати саму цю мікросхему (в моєму випадку це ULN2003AN) без зайвих дротів та світлодіодів.

Прикол третій: зазвичай motor shield’и роблять для великих плат з великою кількістю GPIO — Arduino Uno, Nano чи взагалі Mega. Натомість я хочу використати регістри зсуву сімейства 74HC595, а саме, у мене були в наявності SN74HC595N.

Поєднати 74HC595 та ULN2003 — наче тривіальна ідея, чи не так?

Для керування регістром зсуву мені достатньо трьох контактів GPIO, а значить, вистачить навіть ESP8266, у якої завжди дефіцит вільних GPIO. Задум такий: два послідовно з’єднаних 74HC595, дві ULN2003, контакти для підключення трьох двигунів 28BYJ-48, контакти для WeMos D1 mini.

Плата не містить жодної радіодеталі, навіть жодного резистора, тільки контакти.

Схема

Взяв у руки Fritzing і став малювати. Вирішив спробувати вмістити все на найпростішу, найдешевшу односторонню макетку з гетинаксу (FR-2) розміром 18 на 24 отвори. Щоб не шкода було, якщо щось криво спаяю. Класичний наскрізний монтаж, найпростіший підхід.

Схема називалася shift_stepper, потім shift_stepper_alt, alt_2, alt_3, alt_4, alt_4a, alt_4b, і нарешті фінальна версія це alt_4c. Нахіба це все було тримати паралельно в системі контроля версій :)

Загальна схема плати

Кольорові дроти на схемі — це, власне, дроти (і майже всі вони зверху). Білі «дроти» це краплі припою, розтягнуті паяльником між окремими металізованими отворами на платі. Я намагався не зловживати цією технікою, але без неї не виходило ніяк.

Нижня половина схеми вочевидь далека від оптимальності. ESP’шка стоїть тупо по центру.

В принципі, в нижній частині схеми все важливе влаштовано більш-менш очевидно: три дроти керування регістром зсуву ST_CP, DS та SH_CP якось там йдуть до контактів D8, D7 та D5, відповідно. (Чому я обрав саме ці контакти? Бо так я можу використовувати апаратний SPI).

Дещо незрозуміле кубло в правому нижньому кутку, там де підключення живлення. Ідея в тому, щоб +5 В основної плати з’єднувалося з контактом 5V на платі D1 mini через джампер (далі по фотках стане ясніше). Ну, це щоб була опція живити ESP’шку окремо, силові ULN2003 окремо.

Файли схеми

Про всяк випадок, викладаю схему для чудової (і жахливої) програми Fritzing.

  • Схема загальна: board.fzz
  • Схема монтажних дротів зверху: board_top.fzz
  • Схема точок пайки знизу, відзеркалена: board_bottom.fzz

Збірка

Послідовність монтажу така: спочатку, на першому кроці, мені треба розмістити монтажний дріт з верхньої сторони плати (там де нема металізованих ділянок для пайки). В якості монтажного дроту я часто використовую жили, висмикнуті зі старого шматка ethernet’а (він також чудово підходить для безпаєчних макетних плат).

Тож я беру дріт, кусачки і починаю нарізати шматки потрібної довжини, рахуючи клітинки.

Для зручності маю окремий варіант схеми, на якій є тільки ці верхні дроти:

Плата, вид зверху, монтажні дроти

Схема чітка. В реальності виходить косо, криво, як попало:

Плата, вид зверху, свіжовстановлені монтажні дроти

На майбутнє: схема все-ж вийшла так собі. Якщо придивитися, то на схемі є два місця (L01 та E01), де в одну дірку мають увійти аж три жили монтажних дротів. Дві мідні жили з ethernet’а лізуть без проблем, а три вже не лізуть. Довелося розколупати ці дірки поширше. (Знаю як в наступній версії трохи змінити схему і з мінімальними зусиллями уникнути таких «потрійних» точок).

Окей. Продовжую. Далі я просто припаюю ці дроти до відповідних контактних площадок. На наступному фото точки пайки не видно (вони на іншому боці), але про пайку можна здогадатися, причому не тільки по плямам каніфолі:

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

Вочевидь, краще використовувати правильний монтажний дріт в термостійкій ізоляції :)

Поки паяв, постійно перевіряв себе, прозвонював схему. Сама перша плата вже на цьому кроці зійшла з дистанції: виявилося, що гетинакс буває бракованим. (Її фото я вирішив не постити, ну її до біса). Окей, я просто взяв іншу макетку. Плюси і мінуси найдешевшої фігні.

На наступному етапі треба встановити «панельки» для мікросхем в корпусах DIP-16 і різноманітні контакти для всього іншого. Найскладніше з контактами для двигунів: у мене не знайшлося правильного роз’єма XH з ключом (це мало бути щось типу XH2.54-5P), тож використовую що попало, звичайну «гребінку» на 5 контактів. Експлуатація вимагатиме уважності і обережності, щоб випадково не підключити двигун не тим боком.

Ось так виглядає плата з усіма контактами:

Плата зі встановленими контактами

Для зручності цього етапу, я створив ще одну допоміжну схему: що, власне, з чим треба буде спаяти. Відносно основної схеми вона горизонтально відзеркалена (тобто вона така, як я фактично бачу задню сторону плати).

Кольорове — це дріт, біле — це краплі припою:

Плата, точки пайки, вид знизу

В кінці кінців, щось таке стрьомненьке якось потроху виходить:

Задня сторона плати

Так, багато лажі. Бачу, що точки E01 та B04 погано пропаяні. Також бачу всяку неакуратність: припой потрапив на деякі місця, які не використовуються (зокрема E02, L08, X18). Але я в процесі все це ретельно прозвонював, перевіряв і воно відповідає плану.

І взагалі, я тільки вчуся паяти.

Ось так виглядає плата в зборі зі встановленими мікросхемами і платою WeMos D1 mini.

Плата зі встановленими компонентами

Софт

Два 8-бітних послідовно з’єднаних регістри зсуву мають 16 виводів. Біти 0 та 8 складно застосувати, бо вони як раз посередині щільного кубла дротів. Доведеться ними пожертвувати.

Інші 14 виводів прямо і рівно йдуть на входи ULN2003, у кожної з яких саме 7 входів і 7 виходів. Але використовуються лише 12 з цих 14 виходів, бо саме стільки треба для керування трьома кроковими двигунами. Ще два контакти залишаються незадіяними. З міркувань зручності монтажу це біти 5 та 11.

Нехай у нас є три крокових двигуни: A, B та C. Кожен має по 4 обмотки: A1–A4, B1–B4, C1–C4.

біт 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
обмотка C4 C3 C2 C1 B4 B3 B2 B1 A4 A3 A2 A1

Нормальний софт я ще не написав, лише нашвидкоруч щось просте зліпив на MicroPython’і: модуль для керування регістрами зсуву, модуль для контроля кроковими двигунами і проста демонстрація роботи цих модулей.

Файл myShiftRegister.py

Спочатку я зробив bit-bang версію (емулювати SPI можна на будь-яких GPIO), а потім став використовувати апаратний інтерфейс SPI.

SPI взагалі швидкий. Апаратний SPI в ESP8266 може працювати зі швидкістю до 80 МГц.

З іншого боку, самі регістри зсуву сімейства SN74HC595 на напрузі 5 В можуть працювати зі швидкістю до 25 МГц. Я це з’ясував не одразу, а тільки тоді, коли став шукати помилки :) бо на швидкості втричі вищій за дозволену специфікацією вони лише трішечки почали глючити.

import machine

# WeMos board: D7 (GPIO13), SPI MOSI
# 74HC595: DS (Serial data input)
dataPin = machine.Pin(13, machine.Pin.OUT)

# WeMos board: D5 (GPIO14), SPI SCLK
# 74HC595: SH_CP (Shift register clock pin)
clockPin = machine.Pin(14, machine.Pin.OUT)

# WeMos board: D8 (GPIO15), SPI CS
# 74HC595: ST_CP (Storage register clock pin)
latchPin = machine.Pin(15, machine.Pin.OUT)

# Can I use hardware SPI? Yes.
spi = machine.SPI(1, baudrate=20000000, polarity=0, phase=0)
# Beware: if I define the hardware SPI, I will be not able to use bitbang version

# Or I can use SoftSPI
# Dunno why I cannot just say miso=None there
#spi = machine.SoftSPI(baudrate=100000, polarity=0, phase=0, sck=clockPin, mosi=dataPin, miso=machine.Pin(12))

def write74HC595_spi(data, num_bits=8):
    spi.write(data.to_bytes(num_bits >> 3, 'big'))
    latchPin.value(1)
    latchPin.value(0)

# And that's mostly for debugging
def write74HC595_bitbang(data, num_bits=8):
    for i in range(num_bits-1, -1, -1):
        #print((data >> i) & 1, end='')
        dataPin.value((data >> i) & 1)
        clockPin.value(1)
        clockPin.value(0)
    latchPin.value(1)
    latchPin.value(0)
    #print('')

write74HC595 = write74HC595_spi
#write74HC595 = write74HC595_bitbang

# On start, set all that to zeroes
clockPin.value(0)
latchPin.value(0)
write74HC595(0, 16)

Файл myStepper.py

Тут все більш-менш традиційно, все майже як з Arduino, тільки вивід через регістр зсуву.

Я використовую послідовність full-step, найкращу і найпростішу. Коли двигун неактивний, вимикаю у нього всі обмотки.

Випадково помітив, що мої крокові двигуни з двох різних партій (можливо, навіть різних виробників), які відрізняються послідовністю обмоток. Одна і та сама керівна послідовність рухає вал двигуна за годинниковою стрілкою чи проти годинникової стрілки, в залежності від того, з якої партії цей двигун. І схоже, що це розповсюджена проблема. На майбутнє треба це врахувати.

from myShiftRegister import write74HC595

class MyStepper:
    outputBits = 0                  # part of 74HC595 output
    stepActual = 0
    stepTarget = 0
    _delta = 0
    
    def __init__(self, stepBitMask):
        assert len(stepBitMask) == 4
        self.stepBitMask = stepBitMask

    def target(self, target):
        self.stepTarget = target
        self._delta = 1 if self.stepActual < self.stepTarget else -1

    def update(self):
        if (self.stepActual == self.stepTarget):
            self.outputBits = 0     # turn the coils off after last step
            return

        if (self.outputBits):       # if the coils is already turned on
            self.stepActual += self._delta
        # else: turn on the coils at last position, wait one step cycle

        self.outputBits = self.stepBitMask[self.stepActual & 3]

# my 2x SN74HC595 + 2x ULN2003AN scheme, version 4c:
#
#  bit: 15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
# coil: C4 C3 C2 C1 -- B4 B3 -- B2 B1 -- A4 A3 A2 A1 --

motor1 = MyStepper([ 0b1100 << 1, 0b0110 << 1, 0b0011 << 1, 0b1001 << 1 ])
motor2 = MyStepper([ 0b11000 << 6, 0b01010 << 6, 0b00011 << 6, 0b10001 << 6 ])
motor3 = MyStepper([ 0b1100 << 12, 0b0110 << 12, 0b0011 << 12, 0b1001 << 12 ])

def oneStep():
    motor1.update()
    motor2.update()
    motor3.update()
    bitmask = motor1.outputBits | motor2.outputBits | motor3.outputBits
    write74HC595(bitmask, 16)

main.py

Не придумав нічого цікавішого, аніж просто прокрутити всі три двигуни. Тут і зараз одночасно я застосовую не більше двох двигунів, просто з обережності. За умови належного живлення можна використовувати і три одразу.

import myStepper, time

myStepper.motor1.target(1024)
for x in range(770):
    myStepper.oneStep()
    time.sleep_ms(3)

myStepper.motor2.target(1024)
for x in range(770):
    myStepper.oneStep()
    time.sleep_ms(3)

myStepper.motor3.target(1024)
for x in range(1026):
    myStepper.oneStep()
    time.sleep_ms(3)

Воно крутиться! :)

Невелике відео демонстрації роботи на YouTube: https://youtu.be/qPV8w360FCU

Далі треба буде написати все це як слід. Це ж ESP8266, в неї є Wi-Fi на борту. За планом, наступна версія прошивки буде спілкуватися з зовнішнім світом по MQTT.

Можливі модифікації

Звісно, схему можна вдосконалити. Тут я занотую пару простих можливих варіантів розширення вже наявної схеми.

Як використати ще два біти на виході ULN2003

Здається, біти 5 та 11 не так вже й складно застосувати для чогось ще. Тісно, але наче можна.

Плата, модифікація на +2 біти

Тут на фото насправді контакти не припаяні, це просто «примірка», щоб оцінити картину:

Плата з додатковими контактами

Стандартні конектори DuPont і контакти крокового двигуна наче мають одночасно поміститися:

Плата з додатковими контактами, щось підключене

Чому б ні? Можна підключити світлодіод, або активний динамік (buzzer), або ще щось. Соленоїд. Лазер. Два лазери :)

Як підключити ще щось до самої ESP8266

Шина I²C, кінцевий вимикач, PWM для сервопривода? Місце є, вільні GPIO є, щось додати можна.

Звісно, це «щось» має підключатися просто, без мороки. Тобто якщо це буде шина I²C, то це має бути зручний 4-контактний роз’єм, наприклад такий: VCC (ймовірно 3.3 В), GND, SCL, SDA.

Якщо робити підключення сервоприводу, то, відповідно, для нього має бути стандартний трьохконтактний роз’єм (GND, +5V, PWM). Треба лише продумати, що з чим спаяти.

Тут на фото теж лише «примірка», навіть без схеми, лише щоб оцінити місце на платі:

Ідея розташування додаткових контактів біля плати WeMos D1 mini

Окремо хочу сказати за кінцеві вимикачі. Часто вони використовуються, щоб при включенні якогось принтера-плоттера-верстата прошивка могла знайти початок координат (бо невідомо, в якому фізичному положенні система знаходилася у момент увімкнення).

Скільки треба кінцевиків для трьох моторів, які керують трьома незалежними осями? Думаю, що для зручності їх може бути скільки завгодно, наприклад по одному вимикачу на кожну вісь; але підключити їх треба паралельно до однієї GPIO. Просто виставлення нуля кожної координати треба робити по черзі.

Алгоритм такий: рухаємо одну вісь, поки не спрацює кінцевик, потім трошки відступаємо допоки його не відпустить, повторюємо дії для наступної вісі. Все! :) Ми на початку координат.

Висновки

Це моя перша «перманентна» плата.

Так, це надзвичайно проста схема. Але я цю штуку придумав, перевірив на безпаєчній макетці, намалював реалістичну схему, спаяв, написав прошивку, розібрався з глюками. Виявилося, що воно дійсно працює і дійсно робе саме те, що планувалося.

Я цілком задоволений. Мені подобається моє хобі.