Связь electron и FT2232 на примере MAX30102

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В данной статья я хочу рассказать, как я получал данные с оптического датчика MAX30102 используя FT2232 и electron.js.

Первым делом я подключил оптический датчик MAX30102 к плате FT2232HL, и соединил вместе выводы ADBUS1 и ADBUS2. Это нужно для того, что бы получить полноценный SDA, так как по отдельности это выводы DI (вход данных) и DO (выход данных).

Подключение MAX30102 к FT2232
Подключение MAX30102 к FT2232

Далее я написал простую библиотеку для работы с i2c. Набор функций у неё небольшой, но хватало для работы

Код библиотеки

Hidden text
const FTDI = require('ftdi-d2xx')

// константы
const SDA_LO_SCL_LO = 0x00
const SDA_IN_SCL_IN = 0x00
const SDA_IN_SCL_OUT = 0x01
const SDA_OUT_SCL_IN = 0x02
const SDA_OUT_SCL_OUT = 0x03
const MSB_RISING_EDGE_CLOCK_BYTE_IN = 0x20
const MSB_FALLING_EDGE_CLOCK_BYTE_OUT = 0x11
const MSB_RISING_EDGE_CLOCK_BIT_IN = 0x22
const MSB_FALLING_EDGE_CLOCK_BIT_OUT = 0x13

class I2C {
    #device = null
    #clock_divider = 0x005F
    #addr = 0xFF
    #buf = []

    /**
     * проверка количества байт приема и чтения
     * @param {int} count количество байт для чтения
     * @returns {buffer} принятые байты
     */
    async read(count) {
        for (let i = 0; i < 1000000; i++) {
            if (this.#device.status.rx_queue_bytes == count) {
                break
            }
        }
        return await this.#device.read(this.#device.status.rx_queue_bytes)
    }

    /**
     * команда старт
     */
    setStart() {
        // SDA 1 SCL 1
        for (let i = 0; i < 6; i++) {
            this.#buf.push(0x80)
            this.#buf.push(SDA_LO_SCL_LO)
            this.#buf.push(SDA_IN_SCL_IN)
        }
        // SDA 0 SCL 1
        for (let i = 0; i < 6; i++) {
            this.#buf.push(0x80)
            this.#buf.push(SDA_LO_SCL_LO)
            this.#buf.push(SDA_OUT_SCL_IN)
        }
        // SDA 0 SCL 0
        for (let i = 0; i < 6; i++) {
            this.#buf.push(0x80)
            this.#buf.push(SDA_LO_SCL_LO)
            this.#buf.push(SDA_OUT_SCL_OUT)
        }
    }

    /**
     * команда стоп
     */
    setStop() {
        // SDA 0 SCL 0
        for (let i = 0; i < 6; i++) {
            this.#buf.push(0x80)
            this.#buf.push(SDA_LO_SCL_LO)
            this.#buf.push(SDA_OUT_SCL_OUT)
        }
        // SDA 0 SCL 1
        for (let i = 0; i < 6; i++) {
            this.#buf.push(0x80)
            this.#buf.push(SDA_LO_SCL_LO)
            this.#buf.push(SDA_OUT_SCL_IN)
        }
        // SDA 1 SCL 1
        for (let i = 0; i < 6; i++) {
            this.#buf.push(0x80)
            this.#buf.push(SDA_LO_SCL_LO)
            this.#buf.push(SDA_IN_SCL_IN)
        }
    }

    /**
     * соеденение с чипом и настройка работы по шине i2c
     * @param {byte} addr адрес i2c устройства
     * @returns {boolean} при успешном соединении вернет true
     */
    async open(addr) {
        if (this.#device) {
            return false
        }
        this.#device = await FTDI.openDevice("A")
        if (!this.#device) {
            return false
        }
        this.#device.resetDevice()
        await this.read(this.#device.status.rx_queue_bytes)
        this.#device.setLatencyTimer(16)
        this.#device.setUSBParameters(65535, 65535)
        this.#device.setTimeouts(1000, 1000)
        this.#device.setBitMode(0x00, FTDI.FT_BITMODE_RESET)
        // переход в режим MPSSE
        this.#device.setBitMode(0x00, FTDI.FT_BITMODE_MPSSE)
        // отправка тестового байта 0xAA
        await this.#device.write(Uint8Array.from([0xAA]))
        let response = await this.read(2)
        if (response[0] != 0xFA || response[1] != 0xAA) {
            this.close()
            return false
        }
        // отправка тестового байта 0xAB
        await this.#device.write(Uint8Array.from([0xAB]))
        response = await this.read(2)
        if (response[0] != 0xFA || response[1] != 0xAB) {
            this.close()
            return false
        }
        // настройка фазы и полярности тактовых импульсов SCL
        await this.#device.write(Uint8Array.from([0x8A, 0x97, 0x8C]))
        // настройка частоты тактовых импульсов SCL
        await this.#device.write(Uint8Array.from([0x80, 0x03, 0x13, 0x86, (this.#clock_divider & 0xFF), ((this.#clock_divider >> 8) & 0xFF)]))
        await this.#device.write(Uint8Array.from([0x85]))
        this.#addr = addr
        return true
    }

    /**
     * отсоедение от чипа
     * @returns {boolean} при успешном отсоединении вернет true
     */
    close() {
        if (this.#device) {
            this.#device.close()
            this.#device = null
            return true
        }
        return false
    }

    /**
     * данные на запись в линию i2c байта
     * @param {byte} val значение байта  
     */
    writeByte(val) {
        this.#buf.push(0x80)
        this.#buf.push(SDA_LO_SCL_LO)
        this.#buf.push(SDA_OUT_SCL_OUT)

        this.#buf.push(MSB_FALLING_EDGE_CLOCK_BYTE_OUT)
        this.#buf.push(0x00)
        this.#buf.push(0x00)
        this.#buf.push(val)

        this.#buf.push(0x80)
        this.#buf.push(SDA_LO_SCL_LO)
        this.#buf.push(SDA_IN_SCL_OUT)

        this.#buf.push(MSB_RISING_EDGE_CLOCK_BIT_IN)
        this.#buf.push(0x00)

        this.#buf.push(0x87)
    }

    /**
     * данные на чтение из линии i2c байта
     * @param {boolean} ask  
     */
    readByte(ask = false) {
        this.#buf.push(0x80)
        this.#buf.push(SDA_LO_SCL_LO)
        this.#buf.push(SDA_IN_SCL_OUT)

        this.#buf.push(MSB_RISING_EDGE_CLOCK_BYTE_IN)
        this.#buf.push(0x00)
        this.#buf.push(0x00)

        this.#buf.push(0x80)
        this.#buf.push(SDA_LO_SCL_LO)
        this.#buf.push(SDA_OUT_SCL_OUT)

        this.#buf.push(MSB_FALLING_EDGE_CLOCK_BIT_OUT)
        this.#buf.push(0x00)
        this.#buf.push(ask ? 0x00 : 0xFF)

        this.#buf.push(0x80)
        this.#buf.push(SDA_LO_SCL_LO)
        this.#buf.push(SDA_IN_SCL_OUT)

        this.#buf.push(0x87)
    }

    /**
     * чтение регистра устройства
     * @param {byte} addr адрес регистра
     * @param {int} count количество байт для чтения
     * @returns {array|null} массив байт или null, в случае неудачи
     */
    async readRegister(addr, count = 1) {
        if (!this.#device) {
            return null
        }
        this.#buf = []
        this.setStart()
        this.writeByte((this.#addr << 1) | 0)
        this.writeByte(addr)
        this.setStart()
        this.writeByte((this.#addr << 1) | 1)
        for (let i = 0; i < count; i++){
            this.readByte(i != count - 1)
        }
        this.setStop()
        await this.#device.write(Uint8Array.from(this.#buf))
        let result = [...await this.read(count + 3)]
        // проверка принятых данных
        if (!result || (result[0] & 1) != 0 || (result[1] & 1) != 0 || (result[2] & 1) != 0) {
            return null
        }
        // отсекаем данные от writeByte
        result.splice(0, 3)
        return result
    }

    /**
     * запись байта в регистр
     * @param {byte} addr адрес регистра
     * @param {byte} val значение для записи
     * @returns {boolean} вернет true при успешной записи
     */
    async writeRegister(addr, val) {
        if (!this.#device) {
            return false
        }
        this.#buf = []
        this.setStart()
        this.writeByte((this.#addr << 1) | 0)
        this.writeByte(addr)
        this.writeByte(val)
        this.setStop()
        await this.#device.write(Uint8Array.from(this.#buf))
        const result = await this.read(3)
        // проверка данных
        if ((result[0] & 1) != 0 || (result[1] & 1) != 0 || (result[2] & 1) != 0) {
            return false
        }
        return true
    }
    
}

module.exports = I2C

После того как библиотека была протестирована на работоспособность я написал код для работы с оптическим датчиком MAX30102. Окно программы разделил на две части: в правой половине находится карта регистров, в левой поле для вывода графической информации. В карте регистров есть представление регистра как целого байта, так и отдельных битов. Возможность считывания и записи в регистры данных. Для проверки работы всей связки я считал значение регистра по адресу 0xFF (Part ID) и получил значение 21 (0x15), следовательно, вся связка работает.

Далее нужно было изучить даташит на MAX30102. Как его настраивать для работы, как организован буфер. Основные настройки выглядят так:

Имя

Рег.

Деф.

Описание

0x02

192

Прерывание и разрешение на работу

Mode Configure

0x09

3

Режим работы одновременно красного и инфракрасного канала

SpO2 Configuration

0x0A

39

Настройки для режима SpO2

LED Pulse Amplitude

0x0C

36

Амплитуда красного канала, чем больше число, тем ярче свечение

LED Pulse Amplitude

0x0D

36

Амплитуда инфракрасного канала, чем больше число, тем ярче свечение

Proximity Mode LED Pulse Amplitude

0x10

127

Мощность светодиода при приближении

Код работы с датчиком MAX3102

Hidden text
const $ = require("jquery")
const I2C = require("ftdi-i2c")

const i2c = new I2C()
let x = 0
let run = false
let red_avr = 0
let red_n = 0
let red_sr = 0
let ir_avr = 0
let ir_sr = 0

/**
 * Опрос датчика MAX3102 и вычитание постоянной составляющей
 */
setInterval(async () => {
    const canvas = document.getElementById("canvas")
    const ctx = document.getElementById("canvas").getContext("2d")
    if (i2c && run) {
        // количество байт, которые прочитали
        const rd = await i2c.readRegister(0x06)
        // количество байт, которые успели записаться в буфер
        const wr = await i2c.readRegister(0x04)
        const count = (wr - rd) & 31
        if (count > 0) {
            const result = await i2c.readRegister(0x07, 6 * count)
            for(let i = 0; i < count; i++) {
                let red = (result[i*6] << 16) | (result[1 + i*6] << 8) | (result[2 + i*6] << 0)
                let ir = (result[3 + i*6] << 16) |(result[4 + i*6] << 8) | (result[5 + i*6] << 0)
                red_avr += red
                ir_avr += ir
                red_n++
                ctx.fillStyle = "#ff2626"
                // вывод зеленых точек, канал IR
                ctx.beginPath()
                ctx.arc(x, (red - red_sr) / 50 + canvas.height / 2, 1, 0, Math.PI * 2, true)
                ctx.fill()
                // вывод красных точек, канал RED
                ctx.fillStyle = "#26ff26"
                ctx.beginPath()
                ctx.arc(x, (ir - ir_sr) / 50 + canvas.height / 2, 1, 0, Math.PI * 2, true)
                ctx.fill()
                if (x++ > canvas.width) {
                    red_sr = red_avr / red_n
                    ir_sr = ir_avr / red_n
                    red_avr = 0
                    red_n = 0
                    ir_avr = 0
                    x = 0
                    ctx.clearRect(0, 0, canvas.width, canvas.height)
                }
            }
        }
    }  
}, 200)

/**
 * Перевод битов в байт
 */
function setByte(self) {
    const parent = $(self).parent("td").parent("tr")
    let byte = 0
    for (let i = 0; i < 8; i++) {
        byte |= ((parent.find("td:eq(" + (i + 1) + ")").find("input").val() || 0) << (7 - i))
    }
    parent.find("td:eq(10)").find("input").val(byte)
}

/**
 * Перевод байта в биты
 */
function setBit(self) {
    const parent = $(self).parent("td").parent("tr")
    const val = (parent.find("td:eq(10)").find("input").val() || 0x00)
    parent.find("td:eq(1)").find("input").val(((val & 1 << 7) != 0) ? 1 : 0)
    parent.find("td:eq(2)").find("input").val(((val & 1 << 6) != 0) ? 1 : 0)
    parent.find("td:eq(3)").find("input").val(((val & 1 << 5) != 0) ? 1 : 0)
    parent.find("td:eq(4)").find("input").val(((val & 1 << 4) != 0) ? 1 : 0)
    parent.find("td:eq(5)").find("input").val(((val & 1 << 3) != 0) ? 1 : 0)
    parent.find("td:eq(6)").find("input").val(((val & 1 << 2) != 0) ? 1 : 0)
    parent.find("td:eq(7)").find("input").val(((val & 1 << 1) != 0) ? 1 : 0)
    parent.find("td:eq(8)").find("input").val(((val & 1 << 0) != 0) ? 1 : 0)
}

$(document).ready(() => {
    $("#open").click(async function() {
        const is_open = $(this).html() == "OPEN"
        if (is_open) {
            if (await i2c.open(0x57)) {
                $(this).html("CLOSE")
            }
        } else {
            if (await i2c.close()) {
                $(this).html("OPEN")
            }
        }
        
    })
    /**
     * Установка значений как пример
     */
    $("#def").click(async () => {
        run = false
        $("#run").html("RUN")
        await i2c.writeRegister(0x02, 0xC0)
        await i2c.writeRegister(0x03, 0x00)
        await i2c.writeRegister(0x04, 0x00)
        await i2c.writeRegister(0x05, 0x00)
        await i2c.writeRegister(0x06, 0x00)
        await i2c.writeRegister(0x08, 0x1f)
        await i2c.writeRegister(0x09, 0x03)
        await i2c.writeRegister(0x0A, 0x27)
        await i2c.writeRegister(0x0C, 0x24)
        await i2c.writeRegister(0x0D, 0x24)
        await i2c.writeRegister(0x10, 0x7f)
    })
    /**
     * запуск чтения из буфера
     */
    $("#run").click(function() {
        if (run) {
            run = false
            $(this).html("RUN")
        } else {
            run = true
            $(this).html("STOP")
        }
    })

    /**
     * запись и чтение данных в/из регистр(а)
     */
    $("button.rw").on("click", async function () {
        run = false
        $("#run").html("RUN")
        const parent = $(this).parent("td").parent("tr")
        const addr = parent.find("td:eq(9)").html().trim()
        const is_r = $(this).html().trim() == 'R'
        if (is_r) {
            const val = await i2c.readRegister(Number(addr))
            parent.find("td:eq(10)").find("input").val(val[0])
            setBit(this)
        } else {
            const val = Number(parent.find("td:eq(10)").find("input").val() || 0x00)
            await i2c.writeRegister(Number(addr), val)
        }
    })
    $("input.byte").on("change", function() {
        setBit(this)
    })
    $("input.byte").on("keyup", function(e) {
        setBit(this)
    })
    $("input.bit").on("change", function() {
        $(this).val($(this).val() == 1 ? 1 : 0)
        setByte(this)
    })
    $("input.bit").on("keyup", function(e) {
        if (e.keyCode != 8) {
            $(this).val($(this).val() == 1 ? 1 : 0)
            setByte(this)
        }
    })
})

В итоге я получил такой результат

Результат работы программы
Результат работы программы

Для запуска нужно скачать пример ftdi-electron

cd ftdi-electron
npm i
npm start

Код для работы с ftdi, на основе него была написана библиотека ftdi-i2c. Поставьте лайк, человеку будет приятно

Код моего проекта

Источник: https://habr.com/ru/articles/759038/


Интересные статьи

Интересные статьи

Двенадцать лет назад 13 марта 2010 года в Орле состоялось по тем меркам уникальное событие, которое привлекло внимание не только радиолюбителей, но остальных жителей города. Впервые в Орле был пр...
Добрый день. Меня зовут Тимур и я программист. Сегодня я предлагаю посмотреть как можно подрихтовать исходники chromium-а, собрать свой вариант браузера и подтянуть это добро в electron. Эта ст...
В статье речь идет о Ebiten 1.10.Ebiten — это хорошо продуманная библиотека для создания 2D-игр, написанная Хадзиме Хошем на языке Go. С ее помощью созданы движки ряда мобильных и десктопных игр, как ...
Работая над своим стартовым шаблоном для Electron приложений я решил полностью отказаться от CommonJS модулей и использовать исключительно ECMAScript модули.Спойлер: ниче...
В начале 2020 года МТС первым из российских операторов запустил сотовую связь в Антарктиде. Чтобы добраться до ледяного континента, специалисты компании прошли обучение для моряков и выдержали же...