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


Платформа для разработке была выбрана esp8266, так как нужен был wifi, да и цена у нее приемлемая!
Прошивка использовалась с LUA, сборка была кастомная (собиралась тут, не забыть включить I2C и BIT в список поддерживаемых библиотек).
Как мы знаем сервоприводы управляются с помощью ШИМ, у esp8266 на борту с ШИМ проблема, но есть как минимум I2C, да и чего придумывать велосипеды и прочие, был найден контроллер PCA9685 с 12-битным 16-ти канальным интерфейсом на борту, + внешние питание, I2C, что еще нужно для управления сервоприводами, НИЧЕГО!
Погуглив нашел библиотеки для работы с PCA9685 на python, arduino, под Lua упоминание только одно, и то на уровне «вот работает, можно что-то придумать», меня это не устроило!
Кому не интересно описание PCA9685 и он в теме, тому сразу же репа.
Описание контроллера для понимания:
Контроллер как вы уже поняли работает по I2C протоколу, суть его работы в случае с PCA9685 это передача номера регистра для чтения или записи в него
Для работы, нас будут интересовать только 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 выполняется с помощью битовых операций
Подробнее о параметрах:
Как писалось выше мы будем рассматривать работу с 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}
Функции для работы с регистрами 0x00 и 0x01
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 в обратном случае
* — не рассматриваем
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)
Работа с ШИМ
Контроллер имеет 16 каналов, для каждого канала выделено по 4 адреса, из которых 2 на включения и 2 на отключение
Пример:
0 канал
Регистры на включение
0x06 (L, младшие 8 бит)
0x07 (H, старшие 4 бита)
Регистры на выключение
0x08 (L, младшие 8 бит)
0x09 (H, старшие 4 бита)
соответственно +4 к каждому адресу регистру это адрес регистра определенного типа на определенном канале
Функции для работы с ШИМ
Соответственно простой пример для работы с модулем
P.S. Буду рад любым уточнениям и замечаниям, буду особенно благодарен за более подробное разъяснение про OUTDRV и OUTNE, так как я так и не смог найти более простого объяснения


Платформа для разработке была выбрана 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, так как я так и не смог найти более простого объяснения
