Привет! По воли судьбы мне посчастливилось вести в одной из школ кружок по робототехнике, тематика работы затрагивала работу с сервоприводами.
Платформа для разработке была выбрана 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мя байтами, то мы должны понимать какой из них отвечает за старшие биты а какой за младшие!
Пример (когда параметр описывается 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, так как я так и не смог найти более простого объяснения
Автор: dimkabelkov