MITM на шине I2C


9

Я пытался разработать модуль, который позволит мне изменять выбранные ответы подчиненного устройства на шине I2C. Вот исходная конфигурация шины (подтягивания и подключения питания не показаны для ясности:

введите описание изображения здесь На этой шине только 2 устройства, и это только 100 кГц. Контроллер MCU (ведущий I2C) и считыватель RFID-карт (ведомый I2C) NXP PN512. Я не могу изменить прошивку контроллера или изменить транзакции шины I2C. Хорошая часть заключается в том, что контроллер отправляет только 2 типа транзакций:

Master (Write Register) - <s><address+W><register number><data><p> Master (Read Register) - <s><address+W><register number><p><s><address+R><data><p>

То, что я хочу сделать, это заменить выбранные байты данных во время чтения основного регистра моими собственными байтами. Я могу отправить регистрационные номера, которые MCU хочет прочитать, на мой компьютер через UART (921,6 КБод). Я могу обработать их в C / C ++ или Python там. Когда я получу номер регистра, значение которого необходимо заменить, я могу отправить фальшивый байт обратно на мое устройство, и он позаботится о его отправке обратно в контроллер, заменив исходный ответ карты.

Сначала я разделил шину I2C на две шины: введите описание изображения здесь

Я попробовал Arduino Nano и позже CPLD, используя растяжение часов. Аппаратный I2C ATmega328, обращенный к контроллеру MCU, не мог поддерживать, поскольку иногда последовательность запуска генерировалась раньше, чем через 5 мсек после предыдущего цикла остановки. Таким образом, время от времени AVR выполнял транзакцию чтения. CPLD мог справиться со скоростью остановки / запуска, оказалось, что в MCU отключено растяжение шины.

Мне пришла в голову мысль, что я могу «предсказать» чтение основного регистра, обнаружив запись одного байта, так как я уверен, что за ней следует чтение. Кажется, что во время следующей записи адреса цикла чтения у меня было достаточно времени, чтобы ввести байт от ведомого устройства. Это не совсем работает. Вначале операции с шиной казались нормальными (примерно первые 5 секунд), но затем контроллер прекратил все коммуникации на шине, как будто обнаружил, что он не напрямую говорит о считывании меток.

Устройство чтения карт также может генерировать прерывания для мастера. IRQ основаны на таймере или событии. Я связал проблему с задержкой, которую я изначально вводил в автобусе. Я мог ошибаться, но я придумал другой дизайн с «нулевой задержкой». введите описание изображения здесь

Идея состоит в том, что я могу только разорвать линию SDA и оставить линию SCL подключенной между ведущим и ведомым. Таким образом, я все еще могу заменить байты в строке данных в любом направлении. Проект оказался более сложным, так как я должен контролировать направление линии SDA на основе цикла шины. Вот код VHDL, который обрабатывает транзакции шины и отправляет шестнадцатеричные байты по UART на компьютер. Получение байтов с компьютера еще не реализовано:

library ieee; 
use ieee.std_logic_1164.all; 
use ieee.numeric_std.all; 

entity I2C_Sniffer is 
port ( 
 clk : in std_logic;

 scl_master : in std_logic; 
 sda_master : inout std_logic;
 sda_slave  : inout std_logic;

 tx : out std_logic

); 
end entity I2C_Sniffer; 

architecture arch of I2C_Sniffer is
 signal clkDiv: std_logic_vector(7 downto 0) := (others => '0');

 type I2C_STATE is (I2C_IDLE, I2C_MASTER_WRITE, I2C_SLAVE_ACK, I2C_MASTER_READ, I2C_MASTER_ACK);
 signal i2cState: I2C_STATE := I2C_IDLE;

 type I2C_BUS_DIR is (MASTER_TO_SLAVE, SLAVE_TO_MASTER);
 signal i2cBusDir: I2C_BUS_DIR := MASTER_TO_SLAVE;

 signal i2cRxData: std_logic_vector(7 downto 0);
 signal i2cCntr: integer range 0 to 8 := 0;

 signal i2cAddr: std_logic := '1';
 signal i2cCmd: std_logic := '0';

 signal scl_d: std_logic := '1';
 signal scl: std_logic := '1';
 signal sda_d: std_logic := '1';
 signal sda: std_logic := '1';

 --Strobes for SCL edges and Start/Stop bits
 signal start_strobe : std_logic := '0';
 signal stop_strobe : std_logic := '0';
 signal scl_rising_strobe : std_logic := '0';
 signal scl_falling_strobe : std_logic := '0';

 type UART_STATE is (UART_IDLE, UART_START, UART_DATA, UART_STOP);
 signal uartState: UART_STATE := UART_IDLE;

 signal uartTxRdy: std_logic := '0';
 signal uartTxData: std_logic_vector(7 downto 0);
 signal uartCntr: integer range 0 to 8 := 0;

begin

 CLK_DIV: process (clk)
 begin
   if rising_edge(clk) then
     clkDiv <= std_logic_vector(unsigned(clkDiv) + 1);
   end if;
 end process;

I2C_STROBES: process (clk)
begin
  if rising_edge(clk) then
    --Pipelined SDA and SCL signals

    scl_d <= scl_master;
    scl <= scl_d;

    scl_rising_strobe <= '0';
    if scl = '0' and scl_d = '1' then
      scl_rising_strobe <= '1';
    end if;

    scl_falling_strobe <= '0';
    if scl = '1' and scl_d = '0' then
      scl_falling_strobe <= '1';
    end if;

    if i2cBusDir = MASTER_TO_SLAVE then
      sda_d <= sda_master;
      sda <= sda_d;
    else
      sda_d <= sda_slave;
      sda <= sda_d;
    end if;

    start_strobe <= '0';
    if sda_d = '0' and sda = '1' and scl = '1' and scl_d = '1' then
      start_strobe <= '1';
    end if;

    stop_strobe <= '0';
    if sda_d = '1' and sda = '0' and scl = '1' and scl_d = '1' then
      stop_strobe <= '1';
    end if;
  end if;
end process;

BUS_DIR: process(sda_master, sda_slave, i2cBusDir)
begin 
  if i2cBusDir = MASTER_TO_SLAVE then
    sda_slave <= sda_master;
    sda_master <= 'Z';
  else
    sda_master <= sda_slave;
    sda_slave <= 'Z';
  end if;
end process;

I2C: process(clk)
begin
    if rising_edge(clk) then
        uartTxRdy <= '0';

        case i2cState is
            when I2C_IDLE =>
                i2cBusDir <= MASTER_TO_SLAVE;

                if start_strobe = '1' then
                    i2cAddr <= '1';
                    i2cCntr <= 0;
                    i2cState <= I2C_MASTER_WRITE;
                end if;

            -- Master Write (Address/Data)
            when I2C_MASTER_WRITE =>
                i2cBusDir <= MASTER_TO_SLAVE;

                if stop_strobe = '1' then
                    i2cState <= I2C_IDLE;
                        uartTxData <= "00001010";
                        uartTxRdy <= '1';
                end if;

                if scl_rising_strobe = '1' then
                    if i2cCntr <= 7 then
                        i2cRxData(7 - i2cCntr) <= sda;
                        i2cCntr <= i2cCntr + 1;
                    end if;
                end if;

                if i2cCntr = 4 then
                    case i2cRxData(7 downto 4) is
                        when "0000" => uartTxData <= "00110000"; --0
                        when "0001" => uartTxData <= "00110001"; --1
                        when "0010" => uartTxData <= "00110010"; --2
                        when "0011" => uartTxData <= "00110011"; --3
                        when "0100" => uartTxData <= "00110100"; --4
                        when "0101" => uartTxData <= "00110101"; --5
                        when "0110" => uartTxData <= "00110110"; --6
                        when "0111" => uartTxData <= "00110111"; --7
                        when "1000" => uartTxData <= "00111000"; --8
                        when "1001" => uartTxData <= "00111001"; --9
                        when "1010" => uartTxData <= "01000001"; --A
                        when "1011" => uartTxData <= "01000010"; --B
                        when "1100" => uartTxData <= "01000011"; --C
                        when "1101" => uartTxData <= "01000100"; --D
                        when "1110" => uartTxData <= "01000101"; --E
                        when "1111" => uartTxData <= "01000110"; --F
                        when others => uartTxData <= "00111111"; --?
                    end case;
                    uartTxRdy <= '1';
                end if;

                if i2cCntr = 8 then
                    case i2cRxData(3 downto 0) is
                        when "0000" => uartTxData <= "00110000"; --0
                        when "0001" => uartTxData <= "00110001"; --1
                        when "0010" => uartTxData <= "00110010"; --2
                        when "0011" => uartTxData <= "00110011"; --3
                        when "0100" => uartTxData <= "00110100"; --4
                        when "0101" => uartTxData <= "00110101"; --5
                        when "0110" => uartTxData <= "00110110"; --6
                        when "0111" => uartTxData <= "00110111"; --7
                        when "1000" => uartTxData <= "00111000"; --8
                        when "1001" => uartTxData <= "00111001"; --9
                        when "1010" => uartTxData <= "01000001"; --A
                        when "1011" => uartTxData <= "01000010"; --B
                        when "1100" => uartTxData <= "01000011"; --C
                        when "1101" => uartTxData <= "01000100"; --D
                        when "1110" => uartTxData <= "01000101"; --E
                        when "1111" => uartTxData <= "01000110"; --F
                        when others => uartTxData <= "00111111"; --?
                    end case;
                    uartTxRdy <= '1';
                end if;

                if i2cCntr = 8 then
                    if scl_falling_strobe = '1' then
                        i2cState <= I2C_SLAVE_ACK;

                        if i2cAddr = '1' then
                            i2cCmd <= i2cRxData(0);
                            i2cAddr <= '0';
                        end if;
                    end if;
                end if;

            when I2C_SLAVE_ACK =>
                i2cBusDir <= SLAVE_TO_MASTER;

                if scl_falling_strobe = '1' then
                    i2cCntr <= 0;

                    if i2cCmd = '0' then
                        i2cState <= I2C_MASTER_WRITE;
                    else
                        i2cState <= I2C_MASTER_READ;
                    end if;
                end if;

            when I2C_MASTER_READ =>
                i2cBusDir <= SLAVE_TO_MASTER;

                if stop_strobe = '1' then
                    i2cState <= I2C_IDLE;
                        uartTxData <= "00001010";
                        uartTxRdy <= '1';
                end if;

                if scl_rising_strobe = '1' then
                    if i2cCntr <= 7 then
                        i2cRxData(7 - i2cCntr) <= sda;
                        i2cCntr <= i2cCntr + 1;
                    end if;
                end if;

                if i2cCntr = 4 then
                    case i2cRxData(7 downto 4) is
                        when "0000" => uartTxData <= "00110000"; --0
                        when "0001" => uartTxData <= "00110001"; --1
                        when "0010" => uartTxData <= "00110010"; --2
                        when "0011" => uartTxData <= "00110011"; --3
                        when "0100" => uartTxData <= "00110100"; --4
                        when "0101" => uartTxData <= "00110101"; --5
                        when "0110" => uartTxData <= "00110110"; --6
                        when "0111" => uartTxData <= "00110111"; --7
                        when "1000" => uartTxData <= "00111000"; --8
                        when "1001" => uartTxData <= "00111001"; --9
                        when "1010" => uartTxData <= "01000001"; --A
                        when "1011" => uartTxData <= "01000010"; --B
                        when "1100" => uartTxData <= "01000011"; --C
                        when "1101" => uartTxData <= "01000100"; --D
                        when "1110" => uartTxData <= "01000101"; --E
                        when "1111" => uartTxData <= "01000110"; --F
                        when others => uartTxData <= "00111111"; --?
                    end case;
                    uartTxRdy <= '1';
                end if;

                if i2cCntr = 8 then
                    case i2cRxData(3 downto 0) is
                        when "0000" => uartTxData <= "00110000"; --0
                        when "0001" => uartTxData <= "00110001"; --1
                        when "0010" => uartTxData <= "00110010"; --2
                        when "0011" => uartTxData <= "00110011"; --3
                        when "0100" => uartTxData <= "00110100"; --4
                        when "0101" => uartTxData <= "00110101"; --5
                        when "0110" => uartTxData <= "00110110"; --6
                        when "0111" => uartTxData <= "00110111"; --7
                        when "1000" => uartTxData <= "00111000"; --8
                        when "1001" => uartTxData <= "00111001"; --9
                        when "1010" => uartTxData <= "01000001"; --A
                        when "1011" => uartTxData <= "01000010"; --B
                        when "1100" => uartTxData <= "01000011"; --C
                        when "1101" => uartTxData <= "01000100"; --D
                        when "1110" => uartTxData <= "01000101"; --E
                        when "1111" => uartTxData <= "01000110"; --F
                        when others => uartTxData <= "00111111"; --?
                    end case;
                    uartTxRdy <= '1';
                end if;

                if i2cCntr = 8 and scl_falling_strobe = '1' then
                    i2cState <= I2C_MASTER_ACK;
                end if;

            when I2C_MASTER_ACK =>
                i2cBusDir <= MASTER_TO_SLAVE;
                if scl_falling_strobe = '1' then
                    i2cCntr <= 0;
                end if;

                if stop_strobe = '1' then
                    i2cState <= I2C_IDLE;
                    uartTxData <= "00001010"; -- \n
                    uartTxRdy <= '1';
                end if;
        end case;
    end if;
end process;


UART: process (clk, clkDiv(1), uartTxRdy)
begin
    if rising_edge(clk) then
        case uartState is
            when UART_IDLE =>
                if uartTxRdy = '1' then
                    uartState <= UART_START;
                end if;

            when UART_START =>
                if clkDiv(1 downto 0) = "00" then
                    tx <= '0';
                    uartState <= UART_DATA;
                    uartCntr <= 0;
                end if;

            when UART_DATA =>
                if clkDiv(1 downto 0) = "00" then
                    if uartCntr <= 7 then
                        uartCntr <= uartCntr + 1;
                        tx <= uartTxData(uartCntr);
                    else
                        tx <= '1';
                        uartState <= UART_STOP;
                    end if;
                end if;

            when UART_STOP =>
                if clkDiv(1 downto 0) = "00" then
                    tx <= '1';
                    uartState <= UART_IDLE;
                end if;
        end case;
    end if;
  end process;
end architecture arch;

Ниже приведены изменения шины, снятые с CPLD, контролирующим линию SDA.

Регистрация написать:

введите описание изображения здесь

Зарегистрироваться читать:

введите описание изображения здесь

Вы можете увидеть несколько сбоев при изменении направления движения автобусов. Это вызвано различиями во времени между CPLD, меняющим направление шины, и устройством чтения карт, генерирующим ACK. Уровень ACK, по-видимому, стабилен на переднем крае SCL. Насколько я знаю, это все, что вам нужно.

Когда эта вещь установлена, контроллер ведет себя так же, как и в случае разделенных шин, которые в течение нескольких секунд приостанавливают любую работу шины. Я также тестирую этот w Arduino, который имитирует этот MCU и генерирует для меня трафик шины, и похоже, что Arduino время от времени зависает. Так что я думаю, что у меня может быть какая-то проблема с конечным автоматом VHDL, когда при некоторых условиях я застреваю в одном состоянии без выхода. Любые идеи?


Ваш вопрос не очень понятен, для меня в любом случае. Сначала говори, There's only 2 devices on this bus running at 100kHzа потом The hardware I2C was a slave and a bit banged I2C was a master on the card reader bus at 1Mbps. Почему там два автобуса? Зачем нужен скоростной автобус? Предоставьте эскиз вашего первоначального дизайна и постарайтесь уточнить ваш вопрос.
SoreDakeNoKoto

Да жаль. Оригинальная шина имеет только контроллер (ведущий i2c) и считыватель карт / меток RFID (ведомый i2c). Поэтому мне не нужно беспокоиться об адресации I2C, так как это точка-точка (каждый пакет, отправленный ведущим, предназначен для этого ведомого). Мой первый подход состоял в том, чтобы разделить шину на 2 шины и действовать как ведомый i2c на стороне контроллера и мастер на стороне считывателя RFID.
Alexxx

Считыватель RIFD способен работать на более высоких скоростях (1 МГц или даже быстрее), поэтому я подумал, что я могу, пока мы используем это, чтобы я не удерживал шину (растяжение шины) на стороне контроллера слишком долго, пока я читаю данные из считывателя RFID регистр. Кроме того, без растяжения шины, когда я обнаруживаю однобайтовую запись, у меня остается мало времени на следующий цикл чтения, чтобы прочитать байт из считывателя RIFD и отправить его обратно в контроллер.
Alexxx

Под растягиванием шины я подразумеваю растяжение тактовой частоты I2C, когда ведомое устройство удерживает линию SCL на низком уровне, чтобы дать мастеру понять, что он еще не готов к данным. Когда ведомое устройство готово, оно освобождает линию SCL, и мастер продолжает читать биты, отправленные ведомым устройством.
Alexxx

1
Лучше, если вы отредактируете свой вопрос. Вы до сих пор не объяснили, зачем нужны 2 автобуса. Если все, что вам нужно, это прочитать данные с карт-ридера, использующего I2C, то почему бы просто не подключить их к одной шине и не прочитать с нее ваш MCU? Растягивание часов имеет смысл только на стороне раба, обычно, когда он медленно реагирует на хозяина. С этим ничего не поделаешь, если раб не готов. 100-400 кГц обычно достаточно для большинства приложений; единственная причина, по которой вам могут потребоваться более высокие скорости, если вам необходимо выполнить некоторую чувствительную ко времени операцию с данными, которые вы прочитали или аналогичными.
SoreDakeNoKoto

Ответы:


6

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

Согласно твоему описанию, ты не пробовал одну вещь - полную эмуляцию этого устройства для чтения карт. Вы действительно не объяснили, что именно это делает и насколько это сложно, но, судя по тому, что посылает мастер, все не так сложно.

Используйте микроконтроллер с поддержкой аппаратного IIC-слэйва. Это связано с мастером. Прошивка эмулирует кардридер. Поскольку единственное, что когда-либо читает мастер, - это последовательность регистров, другая часть микропрограммы полностью асинхронно связывается со считывателем карт для получения информации и управления ею. Это также означает, что линии сброса и IRQ тоже разделены.

Если все сделано правильно, это должно сработать, так как никакого мошенничества не происходит. Считыватель карт видит контроллер, который посылает ему команды и читает, как именно он должен был использоваться. Это включает в себя реагирование на события IRQ.

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

Это может звучать как большая работа, чем быстрое и грязное защемление другого байта в хакерской шине, но, как вы обнаружили, это не так быстро и может всегда иметь проблемы с синхронизацией. При полной эмуляции все временные ограничения снимаются. Если ваша эмуляция еще не дошла до того, что сделал кард-ридер, то он действует для мастера, как будто этого еще не произошло. По сути, вы делаете вид, что ничего нового не произошло, пока ваша эмуляция не будет готова ответить на событие во всех аспектах.

Это означает, что у вас действительно есть две асинхронные части прошивки: эмуляция IIC считывателя, представленного мастеру, и полный драйвер кард-ридера, который позволяет вам сохранять все его состояние в вашей внутренней памяти.

Так как вы не обманываете, это должно работать, если все сделано правильно. Единственная проблема системного уровня заключается в том, что мастер будет видеть некоторую задержку и вызывать действия устройства чтения карт, чем в существующей системе. Для «кард-ридера» это не имеет большого значения, и, учитывая, что эта задержка в худшем случае, скорее всего, составит 10 секунд. Это, конечно, не должно быть заметно в человеческом масштабе времени.

Обратите внимание, что связь между вашим эмулятором и кард-ридером не ограничивается используемыми в настоящее время 100 кбит / с. Вы должны запустить это так быстро, как позволяет кард-ридер и ваше оборудование. В конце концов, по этой ссылке вы будете хозяином, поэтому у вас есть часы. Опять же, при правильной архитектуре прошивки и асинхронных задачах это не должно иметь значения. Фактически, ваш драйвер, вероятно, будет общаться чаще и получать больше данных от карт-ридера, чем мастер получает от вашего эмулятора.


Спасибо за ответ. Я точно имел в виду некоторые из них, когда впервые заявил, что смотрю на это. Я быстро отказался от этой идеи, поскольку она кажется довольно сложной. Если бы MCU только писал и читал регистры, это было бы легко, но считыватель связывается с RFID, который имеет свой собственный протокол (многобайтовые команды и ответы). Вдобавок к этому MCU устанавливает несколько флагов для IRQ в считывателе и читает статуи. Поэтому казалось, что было бы намного проще выделить только несколько байтов и оставить остальное читателю.
Alexxx

Кроме того, если я разделю всю шину на 2 шины, я действительно смогу общаться с кард-ридером на более высоких скоростях. Однако с последним дизайном, где я обрезал только линию SDA, я должен придерживаться тактирования, предоставляемого MCU на линии SCL, который составляет 100 кГц.
Alexxx

0

Я бы предположил, что вы были на правильном пути с Arduino Nano в качестве MITM, хотя я думаю, что было бы лучше с двумя.

введите описание изображения здесь

NXP-PN512 будет работать на тактовой частоте 3,4 МГц, поэтому я рекомендую вам использовать что-то порядка 1,5 - 2 МГц для правого MCU, разговаривающего с Reader.
Так как левый MCU установлен на 100 кГц, после того, как вы опознали какие-либо байты транзакции (адрес / регистр-WR), вы можете скопировать его по 8-битной параллельной шине (или даже шире) между MCU и отправить команды читателю в меньше чем один такт на медленном канале I2C. Точно так же получение байта от считывателя достигается менее чем за такт на медленной шине, что дает достаточное время для настройки байта ответа.

Здесь я предполагаю, что вам может потребоваться перевести несколько байтов в качестве NFC-идентификатора, а не просто байтовое преобразование (что требует меньше времени).

Основная проблема, которую я вижу, состоит в том, что если вам нужно сериализовать несколько байтов на / с ПК для отображения ваших изменений, синхронизация становится еще более критичной. Если бы был способ встроить свой алгоритм / таблицу изменений отображения в левый MCU, это казалось бы лучшим подходом, хотя решение отображения многобайтовых идентификаторов все еще остается самой сложной задачей.

Если я ошибаюсь и вам просто нужно сопоставить, скажем, один байт идентификатора карты, то это может сработать.

На ранних испытаниях с Arduino вы гарантировали, что все прерывания были отключены (по крайней мере, используется только TWI)? Если вы этого не сделали, то это могло повлиять на ваше время.


1
Я не понимаю, зачем нужны два отдельных микро Множество микросхем могут одновременно обрабатывать две шины IIC. Вам действительно нужно только аппаратное обеспечение, чтобы быть ведомым, хотя использование аппаратного обеспечения может быть удобным, даже если вы ведущий. Взаимодействие между двумя микроэлементами кажется излишне сложным и медленным по сравнению с двумя задачами, выполняющимися в одном и том же микро. Я не вижу проблемы, которую решают два микро.
Олин Латроп

@ Олин Латроп. Это упрощает разработку программного обеспечения. упрощает отладку и т. д. и т. д. Это похоже на то, почему в автомобилях сотни микропроцессоров, а не один (как вы могли бы предположить) большой многопроцессорный процессор. У меня нет абсолютно никаких проблем с использованием нескольких микроконтроллеров, где стоимость в основном ниже, чем у логических микросхем с одной функцией, а функциональность легче определить и разработать. В этом случае в ATMega328 имеется только один вектор прерывания TWI, поэтому поддерживать два канала I2C сложнее. ..Но это конечно личный выбор.
Джек Криси

В этом случае несколько процессоров увеличивают сложность и требуют дополнительной связи. Вам не нужно использовать прерывания или аппаратное обеспечение для IIC, когда вы являетесь мастером шины. Однако существует множество процессоров, которые могут аппаратно обрабатывать две шины IIC независимо друг от друга. Если ATMega не может, и вы хотите использовать два аппаратных IIC, не используйте ATMega.
Олин Латроп

@ Олин Латроп. Давайте согласимся не соглашаться. Бит IMO выше 100 кГц не является стартером. Проблема для OP заключается в том, что стоимость сериализации данных для отправки на ПК для выполнения алгоритма отображения связана с проблемами синхронизации.
Джек Криси

Спасибо за ответ и комментарии. ATMega328 / Arduino нельзя использовать на стороне MCU из-за синхронизации I2C MCU. Этот MCU способен генерировать начальную последовательность быстрее, чем 4.7us после предыдущего останова. Посмотрите на таблицу 32-10 в таблицах данных ATMega328 (параметр tBUF). То, что происходило, было то, что Arduino отслеживал все чтения i2c, которые следовали за записью i2c. Это очевидно известная проблема. Я нашел информацию об этом где-то в Интернете после того, как натянул на это все свои волосы. Вот почему я перешел на CPLD.
Alexxx
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.