ESP8266 + PCA9685 + LUA

Привіт Хабр! По волі долі мені пощастило вести в одній зі шкіл гурток з робототехніки, тематика роботи зачіпала роботу з сервоприводами.

image

image
Платформа для розробки була обрана esp8266, так як потрібен був wifi, та і ціна у неї прийнятна!

Прошивка використовувалася з LUA, збірка була кастомний (збиралася тут, не забути включити I2C та BIT список підтримуваних бібліотек).

Як ми знаємо сервоприводи управляються за допомогою ШІМ, у esp8266 на борту з ШІМ проблема, але є як мінімум I2C, та й чого вигадувати велосипеди та інші, був знайдений контролер PCA9685 з 12-бітним 16-ти канальним інтерфейсом на борту, + зовнішні харчування, I2C, що ще потрібно для управління сервоприводами, НІЧОГО!

Погугливши знайшов бібліотеки для роботи з PCA9685 на python, arduino, під Lua згадка тільки одне, та й то на рівні «ось працює, можна щось придумати», мене це не влаштувало!

Кому не цікаво опис PCA9685 і він в темі, тому відразу ж ріпа.

Опис контролера для розуміння:

Контролер як ви вже зрозуміли працює за I2C протоколу, суть його роботи у випадку з PCA9685 це передача номера регістра для читання або запису в нього

-- функція з модуля для читання значення регістра
read = function (this, reg)
-- ініціалізуємо I2C
i2c.start(this.ID)
-- кажемо, що хочемо відправити дані по каналу
if not i2c.address(this.ID this.ADDR, i2c.TRANSMITTER) then
return nil
end
-- записуємо номер регістра в канал (адресу регістру, з якого хочемо отримати значення)
i2c.write(this.ID reg)
-- завершуємо роботу по каналу
i2c.stop(this.ID)
-- ініціалізуємо I2C
i2c.start(this.ID)
-- кажемо, що хочемо отримати дані по каналу
if not i2c.address(this.ID this.ADDR, i2c.RECEIVER) then
return nil
end
-- читаємо 1й байт
c = i2c.read(this.ID 1)
-- завершуємо роботу по каналу
i2c.stop(this.ID)
-- повертаємо значення байта
return c:byte(1)
end,

-- функція з модуля для запису значення регістра
write = function (this, reg, ...)
i2c.start(this.ID)
if not i2c.address(this.ID this.ADDR, i2c.TRANSMITTER) then
return nil
end
i2c.write(this.ID reg)
len = i2c.write(this.ID ...)
i2c.stop(this.ID)
return len
end,

Для роботи, нас будуть цікавити тільки 3 регістра, які відповідають за налаштування (0x00, 0x01 і 0xFE), і кілька типів (групування за адресами) регістрів працюють в парі які відповідають за роботу з ШІМ, роботу з додатковими адресами ми тут описувати не будемо!

Детальніше про вміст регістрах, байтах і бітах, як з цим працювати і що це

Правило просте!

1 регістр — 1 байт інформації

Кому не зрозуміло що таке регістри, це той же самий 1 байт який містить адресу деякої ділянки пам'яті, не більше, всі вони представлені в 16-тірічной системі числення, тобто можна перевести в 10-тиричную для загального розуміння!

Так само існують параметри, які приймають два регістри, наприклад 0x06 і 0x07 відповідають в даний момент за точку включення ШІМ на 0 каналі!

Для тих хто не знає що таке біти, скільки їх в байтах, де у нас старші і молодші біти

В 1 байті 8 біт, нумерація права наліво, починаємо з 0, тобто у нас 8 біт, з 0 до 7, старші біти з ліва, молодші права. Якщо у нас якийсь параметр описується 2ма байтами, то ми повинні розуміти який з них відповідає за старші значення а які молодші за!

image
Приклад (коли параметр описується 1 регістром):

У нас є якесь число 45, нам треба його записати в якийсь регістр, що б розуміти що які біти будуть записані давайте переведемо це все в 2-хричную систему і в 16-тиричную

45 → 00101101

Ми отримали набір байт в кількості 8 штук, відповідно ці байти і будуть записані в реєстр за адресою

45 → 0x2D (значення)

Приклад (коли параметр описується 2 регістрами):

Візьмемо число, яке виходить за межу 1 байта, від 256 і вище, ну не більше 12 біт, так як наш контролер 12-тибитный

3271 → 0000110011000111

Як ви бачите ми отримували 2 рази по 8 біт, тобто 16 біт, так як нас цікавить тільки перші 12 біт, то сміливо можемо відкинути останні 4 біта, виходить 110011000111, як ми пам'ятаємо старші біти з ліва, молодші з права, нумерація у нас з права наліво, тобто що б розділити це значення на 2 байти які будуть записані окремо в кожен регістр, нам потрібно розділити ці біти на 2 частини

1) 1100 → 0x0C (старші 4 біта)
2) 11000111 → 0xC7 (молодші 8 біт)

Реалізація даного поділу в Lua виконується з допомогою бітових операцій

-- бітовий зсув в право
bit.rshift(3271, 8)

-- 00001100 11000111 -> 00001100

-- на виході ми отримуємо
-- 00001100

-- побітове І
bit.band(3271, 0xFF)

-- 00001100 11000111
-- 11111111

-- на виході ми отримуємо
-- 00000000 11000111

Детальніше про параметри:

Як писалося вище ми будемо розглядати роботу з 3мя регістрами

3) 0xFE — відповідає за частоту ШІМ (PRE_SCALE)

Для встановлення частоти ШІМ використовується джерело тактирования, внутрішній джерело тактирования працює на частоті 25MHz, значення яке передається в регістр необхідно розрахувати за формулою, а потім записати в регістр

Розрахунок значення PRE_SCALE

\begin{eqnarray}
PRE\_SCALE &=& round( \frac{F_{osc}}{4096 * F_{pwm}} ) — 1
\end{eqnarray}

Fosc = 25 000 000
Fpwm = бажана частота ШІМ
4096 — кількість значень, що містяться в 12 біт

Тобто для встановлення частоти в 50Hz

\begin{eqnarray}
PRE\_SCALE &=& round( \frac{25000000}{4096 * 50} ) — 1 = 121
\end{eqnarray}

Необхідно записати в регістр 0xFE значення 121 (0x79)

Розрахунок значення Fpwm

\begin{eqnarray}
F_{pwm} &=& \frac{F_{osc}}{4096 * (PRE\_SCALE + 1)}
\end{eqnarray}

\begin{eqnarray}
F_{pwm} &=& \frac{25000000}{4096 * (121 + 1)} = 50
\end{eqnarray}

getFq = function(this)
local fq = this:read(this.PRE_SCALE)
return math.floor(25000000 / ( fq + 1) / 4096)
end,
setFq = function(this, fq)
local fq = math.floor(25000000 / ( fq * 4096 ) - 1)
local oldm1 = this:read(0x00);
this:setMode1(bit.bor(oldm1, this.SLEEP))
this:write(this.PRE_SCALE, fq)
this:setMode1(oldm1)
return nil
end

Функції для роботи з регістрами 0x00 і 0x01

getMode1 = function(this)
return this:read(0x00)
end,
setMode1 = function(this, data)
return this:write(0x00, data)
end,

getMode2 = function(this)
return this:read(0x01)
end,
setMode2 = function(this, data)
return this:write(0x01, data)
end,

getChan = function(this, chan)
return 6 + chan * 4
end,

1) 0x00 параметри

7 біт RESTART
6 біт EXTCLK
5 біт AI
4 біт SLEEP
3 біт — SUB1*
2 біт — SUB2*
1 біт — SUB3*
0 біт ALLCALL

RESTART — встановлює прапор перезавантаження
EXTCLK — використовує, — 1 зовнішній, 0 внутрішній джерело тактирования
AI — включає (1) або вимикає (0) автоинкремент регістра при запису даних у регістр, тобто можна передати відразу ж 2 біта поспіль з адресою першого регістр, причому 2 байт запишеться на адресу регістра + 1
SLEEP — переклад контролера в режим енергозбереження (1), та назад (0)
ALLCALL — дозволяє (1) модулю реагувати на адреси загального виклику (робота з ШІМ), 0 в протилежному випадку

* — не розглядаємо

-- MODE 1

reset = function(this)
local mode1 = this:getMode1()
mode1 = bit.set(mode1, 7)
this:setMode1(mode1)
mode1 = bit.clear(mode1, 7)
this:setMode1(mode1)
end,

getExt = function(this)
return bit.isset(this:getMode1(), 6)
end,
setExt = function(this, ext)
local mode1 = this:getMode1()
if (ext) then
mode1 = bit.clear(mode1, 6)
else
mode1 = bit.set(mode1, 6)
end
this:setMode1(mode1)
end,

getAi = function(this)
return bit.isset(this:getMode1(), 5)
end,
setAi = function(this, ai)
local mode1 = this:geMode1()
if (ai) then
mode1 = bit.clear(mode1, 5)
else
mode1 = bit.set(mode1, 5)
end
this:setMode1(mode1)
end,

getSleep = function(this)
return bit.isset(this:getMode1(), 4)
end,
setSleep = function(this, sleep)
local mode1 = this:geMode1()
if (sleep) then
mode1 = bit.clear(mode1, 4)
else
mode1 = bit.set(mode1, 4)
end
this:setMode1(mode1)
end,

getAC = function(this)
return bit.isset(this:getMode1(), 0)
end,
setAC = function(this, ac)
local mode1 = this:geMode1()
if (ac) then
mode1 = bit.clear(mode1, 0)
else
mode1 = bit.set(mode1, 0)
end
this:setMode1(mode1)
end,

2) 0x01 параметри

7 біт — не використовується
6 біт — не використовується
5 біт — не використовується
4 біт INVRT
3 біт OCH
2 біт OUTDRV
1, 0 біт OUTNE

INVRT — інвертування сигнали на виході, (0) — інверсія вимкнено, (1) — інверсія включено
OCH — метод застосування значення для ШІМ по каналу I2C (1 ASK, 0 — за STOP)
OUTDRV — можливість підключення зовнішніх драйверів (1), без зовнішніх драйверів (0)
OUTNE — тип підключення зовнішнього драйвера (0 — 3)

-- MODE 2

getInvrt = function(this)
return bit.isset(this:getMode2(), 4)
end,
setInvrt = function(this, invrt)
local mode2 = this:geMode2()
if (invrt) then
mode2 = bit.clear(mode1, 4)
else
mode2 = bit.set(mode1, 4)
end
this:setMode2(mode2)
end,

getInvrt = function(this)
return bit.isset(this:getMode2(), 4)
end,
setInvrt = function(this, invrt)
local mode2 = this:geMode2()
if (invrt) then
mode2 = bit.clear(mode2, 4)
else
mode2 = bit.set(mode2, 4)
end
this:setMode2(mode2)
end,

getOch = function(this)
return bit.isset(this:getMode2(), 3)
end,
setOch = function(this, och)
local mode2 = this:geMode2()
if (och) then
mode2 = bit.clear(mode2, 3)
else
mode2 = bit.set(mode2, 3)
end
this:setMode2(mode2)
end,

getOutDrv = function(this)
return bit.isset(this:getMode2(), 2)
end,
setOutDrv = function(this, outDrv)
local mode2 = this:geMode2()
if (outDrv) then
mode2 = bit.clear(mode2, 2)
else
mode2 = bit.set(mode2, 2)
end
this:setMode2(mode2)
end,

getOutNe = function(this)
return bit.band(this:getMode2(), 3)
end,
setOutNe = function(this, outne)
local mode2 = this:geMode2()
this:setMode2(bit.bor(mode2, bit.band(outne, 3)))
end,

getMode2Table = function(this)
return {
invrt = this:getInvrt(),
och = this:getOch(),
outDrv = this:getOutDrv(),
outNe = this:getOutNe(),
}
end,

Робота з ШІМ

Контролер має 16 каналів, для кожного каналу виділено по 4 адреси, з яких 2 на включення і 2 на відключення

Приклад:

0 канал

Регістри на включення
0x06 (L, молодші 8 біт)
0x07 (H, старші 4 біта)

Регістри на вимикання
0x08 (L, молодші 8 біт)
0x09 (H, старші 4 біта)

відповідно +4 до кожного адресою регістру це адреса регістра певного типу на певному каналі

Функції для роботи з ШІМ

-- CNAHEL

setOn = function(this, chan, data)
this:write(this:getChan(chan), bit.band(data, 0xFF))
this:write(this:getChan(chan) + 1, bit.rshift(data, 8))
end,

setOff = function(this, chan, data)
this:write(this:getChan(chan) + 2, bit.band(data, 0xFF))
this:write(this:getChan(chan) + 3, bit.rshift(data, 8))
end,

setOnOf = function(this, chan, dataStart, dataEdn)
this:setOn(chan, dataStart)
this:setOff(chan, dataEdn)
end,

Відповідно простий приклад для роботи з модулем

-- підключаємо модуль
require('pca9685')

-- ініціалізуємо об'єкт, вказуючи номер i2c та адресу пристрою
pca = pca9685.create(0, 0x40)

-- вказуємо GPIO c SDA і SCL
pca:init(1, 2)

-- задаємо параметри для роботи
pca:setMode1(0x01)
pca:setMode2(0x04)

-- визначаємо частоту
pca:setFq(50)

-- задаємо значення для ШІМ вказуючи номер каналу
pca:setOnOf(0, 200, 600)

P.s. Буду радий будь-яких уточнень і зауважень, буду особливо вдячний за більш докладне роз'яснення про OUTDRV і OUTNE, так як я так і не зміг знайти більш простого пояснення
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.