ESP8266 + PCA9685 + LUA

в 10:23, , рубрики: esp8266, Lua, nodeMCU, pca9685, программирование микроконтроллеров

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

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

Автор: dimkabelkov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js