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

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

Комментарии (6)


  1. cavin
    20.01.2017 14:14

    Это конечно прекрасно, а разве библиотеки для esp8266 под pca9685 нету? На atmel-ы точно есть, а по скольку i2c он и на esp работает как i2c, а уж wire.h под esp точно есть.


    1. medvedevia
      20.01.2017 14:36

      есть, но автору нужно чтобы на Lua работало. Отдельный вопрос зачем нужен этот Lua, arduino-вый код выглядит нисколько не сложнее https://github.com/adafruit/Adafruit-PWM-Servo-Driver-Library/blob/master/examples/pwmtest/pwmtest.ino, сам пишу на нем, причем «общаюсь» с устройство в основном udp-дейтаграммами.


      1. dimkabelkov
        20.01.2017 14:48

        Я не сравниваю их сложность, просто была такая проблема с pca9685 и lua


  1. Konachan700
    20.01.2017 17:41
    +1

    Я под линуксом этот чип заводил, вот тут пример кода. Отличный чип для управления сервами, простой и вполне паябельный.

    OUTDRV и OUTNE

    Чип изначально сделан для управления светодиодами. Этот функционал позволяет навесить внешние ключи, если нужно управлять большими токами. Там в даташите вроде как описан сей момент.


  1. osmanpasha
    23.01.2017 16:06

    А какие у ESP8266 проблемы с ШИМом?


    1. dimkabelkov
      23.01.2017 16:18

      10 бит что может быть мало, я понимаю что это может звучать глупо, ну у меня он заводился с рандомной периодичностью, не однократно встречал посты с жалобами на него! Да и куда лучше иметь плату заточенную под задачу!