MITM no barramento I2C


9

Eu tenho tentado projetar um módulo que permita modificar respostas de escravos selecionadas em um barramento I2C. Aqui está a configuração do barramento original (as conexões pull-ups e de energia não são mostradas para maior clareza:

insira a descrição da imagem aqui Existem apenas 2 dispositivos neste barramento e são apenas 100kHz. Um controlador MCU (mestre I2C) e o leitor de cartão RFID (escravo I2C) NXP PN512. Não consigo modificar o firmware do controlador ou alterar as transações de barramento I2C. A parte boa é que o Controller envia apenas 2 tipos de transações:

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

O que eu quero fazer é substituir os bytes de dados selecionados durante a leitura do registro mestre pelos meus próprios bytes. Posso enviar os números de registro que o MCU deseja ler para o meu PC pelo UART (921.6kbaud). Eu posso processá-los em C / C ++ ou Python lá. Quando recebo o número de registro cujo valor precisa ser substituído, posso enviar um byte falso de volta ao meu dispositivo e ele cuidará de enviá-lo de volta ao controlador, substituindo a resposta do cartão original.

No começo, dividi o barramento I2C em dois: insira a descrição da imagem aqui

Eu tentei o Arduino Nano e depois um CPLD usando o alongamento do relógio. O hardware ATmega328 I2C voltado para o controlador MCU não pôde acompanhar, pois às vezes a sequência de início era gerada antes de 5us após o ciclo de parada anterior. Então, de vez em quando, o AVR fazia NAK uma transação de leitura. O CPLD podia lidar com a velocidade de parada / partida, pois o alongamento do barramento estava desativado no MCU.

Tive uma idéia de que eu posso "prever" a leitura do registro mestre detectando uma gravação de um byte, pois tenho certeza de que é seguida por uma leitura. Parece que tive tempo suficiente durante a gravação do endereço do ciclo de leitura a seguir para obter o byte do escravo. Isso não deu certo. As transações do barramento pareciam boas no início (aproximadamente 5 primeiros segundos), mas o controlador estava interrompendo todas as comunicações no barramento como se detectasse que não está falando diretamente com a leitura de tags.

O leitor de cartão também pode gerar interrupções para o mestre. Os IRQs são baseados em um cronômetro ou evento. Atribuí o problema ao atraso que eu estava introduzindo inerentemente no ônibus. Eu poderia estar errado, mas criei outro design de "atraso zero". insira a descrição da imagem aqui

A idéia é que eu só posso interromper a linha SDA e deixar a linha SCL conectada entre o mestre e o escravo. Dessa forma, ainda posso substituir bytes na linha de dados em qualquer direção. O design provou ser mais complicado, pois tenho que controlar a direção da linha SDA com base no ciclo do barramento. Aqui está o código VHDL que lida com as transações de barramento e envia bytes hexadecimais pelo UART para o computador. O recebimento de bytes do computador ainda não foi implementado:

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;

Abaixo estão as transações de ônibus capturadas com o CPLD controlando a linha SDA.

Registrar escrever:

insira a descrição da imagem aqui

Registre-se lido:

insira a descrição da imagem aqui

Você pode ver algumas falhas quando a direção do ônibus muda. Isso é causado pelas diferenças de tempo entre o CPLD mudar a direção do barramento e o leitor de cartão que gera um ACK. O nível de ACK parece estável na borda ascendente do SCL. Tanto quanto sei, é tudo o que você precisa.

Com isso, o controlador se comporta da mesma maneira que com os barramentos divididos suspendendo qualquer atividade de barramentos em alguns segundos. Também testo o w Arduino que zomba desse MCU e gera tráfego de ônibus para mim e parece que o Arduino também congela de vez em quando. Então, acho que posso ter algum tipo de problema com a máquina de estado VHDL, em que, sob algumas condições, fico presa em um estado sem saída. Alguma ideia?


Sua pergunta não é muito clara, para mim de qualquer maneira. Primeiro você diz There's only 2 devices on this bus running at 100kHze depois The hardware I2C was a slave and a bit banged I2C was a master on the card reader bus at 1Mbps. Por que existem dois ônibus? Por que a necessidade do ônibus de alta velocidade? Faça um esboço do seu design inicial e tente esclarecer sua pergunta.
SoreDakeNoKoto

Sim, desculpe-me. O barramento original possui apenas o controlador (mestre i2c) e o leitor de cartão / etiqueta RFID (escravo i2c). Portanto, eu realmente não preciso me preocupar com o endereçamento I2C, pois é ponto a ponto (todos os pacotes enviados pelo mestre são para esse escravo). Minha primeira abordagem foi dividir o barramento em 2 e atuar como escravo i2c no lado do controlador e mestre no lado do leitor de RFID.
Alexxx #

O leitor RIFD é capaz de velocidades mais rápidas (1MHz ou até mais rápido), então pensei que poderíamos usá-lo para não segurar o barramento (alongamento de barramento) no lado do controlador por muito tempo enquanto leio dados do leitor RFID registro. Além disso, sem o barramento esticar quando detecto uma gravação de um único byte, tenho pouco tempo durante o próximo ciclo de leitura para ler o byte do leitor RIFD e enviá-lo de volta ao controlador.
Alexxx #

Ao esticar o barramento, quero dizer o relógio I2C estendendo-se onde o escravo mantém a linha SCL baixa para informar ao mestre que ainda não está pronto para os dados. Quando o escravo estiver pronto, ele libera a linha SCL e o mestre continua a ler os bits enviados pelo escravo.
Alexxx #

1
É melhor se você editar sua pergunta. Você ainda não explicou por que são necessários 2 ônibus. Se tudo o que você precisa é ler dados do leitor de cartão que utiliza o I2C, por que não conectá-los no mesmo barramento e fazer com que o seu MCU leia a partir dele? O alongamento do relógio só faz sentido do lado do escravo, geralmente quando é lento para responder a um mestre. Não há nada que você possa fazer se o escravo não estiver pronto. 100-400kHz é geralmente suficiente para a maioria das aplicações; o único motivo pelo qual você pode querer velocidades mais rápidas se precisar executar alguma operação sensível ao tempo nos dados que leu ou similares.
SoreDakeNoKoto

Respostas:


6

Acho que tentar hackers cutsey como você está pedindo problemas, com exatamente o tipo de sintomas nos quais você está se deparando. Você está basicamente tentando trapacear e espero que não seja pego.

A única coisa que você não tentou, de acordo com sua descrição, é uma emulação completa dessa coisa de leitor de cartão. Você realmente não explicou exatamente o que faz e como é complicado, mas, a julgar pelo que o mestre está enviando, não é tão complicado.

Use um microcontrolador com capacidade de escravo IIC de hardware. Isso está conectado ao mestre. O firmware emula o leitor de cartão. Como a única coisa que o mestre lê é uma sequência de registros, a outra parte do firmware se comunica completamente de forma assíncrona com o leitor de cartão para obter informações e controlá-lo. Isso também significa que as linhas de redefinição e IRQ também são separadas.

Se bem feito, isso tem que funcionar, pois não há trapaça. O leitor de cartão vê um controlador enviando comandos e fazendo leituras exatamente como ele deveria ser usado. Isso inclui responder a eventos de IRQ.

O mestre acha que está falando diretamente com um leitor de cartão real, porque você emula todas as suas operações exatamente como a coisa real, incluindo o comportamento de redefinição e IRQ.

Isso pode parecer mais trabalhoso do que algum bloqueio rápido e sujo de um byte diferente no hack do barramento, mas como você descobriu, isso não é tão rápido e pode sempre ter alguns problemas de tempo. Com uma emulação completa, todas as restrições de tempo são levantadas. Se sua emulação ainda não alcançou algo que o leitor de cartão fez, ela age com o mestre como se ainda não tivesse acontecido. Você basicamente finge que nada de novo aconteceu até que sua emulação esteja pronta para responder ao evento em todos os aspectos.

Isso significa que você realmente tem duas partes assíncronas do firmware: a emulação da IIC do leitor apresentada ao mestre e um driver completo do leitor de cartão que permite manter todo o seu estado ativo na memória interna.

Como você não está trapaceando, isso deve funcionar se for feito corretamente. O único problema no nível do sistema é que haverá algum atraso no mestre vendo e causando ações do leitor de cartão do que o sistema existente. Isso não soa muito para um "leitor de cartão" e, considerando esse atraso, provavelmente seria 10s de milissegundos na pior das hipóteses. Certamente não deve ser perceptível em uma escala de tempo humana.

Observe que a comunicação entre o emulador e o leitor de cartão não se limita aos 100 kbits / s usados ​​atualmente. Você deve executá-lo o mais rápido que o leitor de cartão e seu hardware permitirem. Afinal, nesse link você será o mestre, e será o dono do relógio. Novamente, com arquitetura de firmware adequada e tarefas assíncronas, isso não deve importar. De fato, seu driver provavelmente se comunicará com mais frequência e obterá mais dados do leitor de cartão do que o mestre obtém do emulador.


Obrigado pela resposta. Eu exatamente tinha alguns em mente quando afirmei olhar para isso. Abandonei rapidamente a ideia, pois isso parece ser bastante complicado. Se o MCU estivesse apenas escrevendo e lendo os registros, seria fácil, mas o leitor se comunica com um RFID que possui seu próprio protocolo (comandos e respostas de vários bytes). Além disso, o MCU está configurando algumas sinalizações para o IRQ no leitor e lendo estátuas. Portanto, parecia muito mais fácil direcionar apenas alguns bytes e deixar o resto para o leitor.
Alexxx #

Além disso, se eu dividir o barramento inteiro em 2, posso conversar com o leitor de cartões em velocidades mais rápidas. No entanto, com o design mais recente, em que apenas cortei a linha SDA, tenho que manter o tempo fornecido pelo MCU na linha SCL, que é 100KHz.
Alexxx #

0

Eu sugiro que você esteja no caminho certo com um Arduino Nano como o MITM, embora eu ache que seria melhor com dois.

insira a descrição da imagem aqui

O NXP-PN512 funcionará na velocidade de clock de 3,4 Mhz, por isso sugiro que você use algo da ordem de 1,5 - 2 MHz para o MCU do lado direito conversando com o Reader.
Como o MCU esquerdo está definido em 100 kHz, depois de reconhecer os bytes de transação (endereço / registrador-WR), você pode copiá-lo em um barramento paralelo de 8 bits (ou mais amplo) entre os MCUs e enviar os comandos ao leitor em menos de uma hora no canal I2C de baixa velocidade. Igualmente, o recebimento de um byte do leitor é alcançado em menos de um relógio no barramento lento, proporcionando tempo suficiente para configurar o byte de resposta.

Estou assumindo aqui que você pode realmente precisar converter vários bytes como um ID NFC e não apenas uma conversão de byte a byte (que requer menos tempo).

O principal problema que eu veria então é que, se você precisar serializar vários bytes de / para o PC para mapear suas alterações, o tempo se tornará ainda mais crítico. Se houvesse uma maneira de criar seu algoritmo / tabela de alteração de mapeamento no MCU do lado esquerdo, isso pareceria uma abordagem melhor, embora resolver um mapeamento de identificador de vários bytes ainda seja o maior desafio.

Se eu estiver errado e você apenas precisar mapear, digamos, um único byte de identificador de cartão, isso poderá funcionar.

Nos seus primeiros testes com o Arduino, você garantiu que todas as interrupções foram desativadas (pelo menos apenas o TWI está sendo usado)? Caso contrário, isso pode ter interferido no seu tempo.


1
Não vejo por que dois micros separados são necessários. Muitos micros podem lidar com dois barramentos da IIC simultaneamente. Você realmente só precisa de hardware para ser escravo, embora o uso de hardware possa ser conveniente, mesmo quando for o mestre. A comunicação entre dois micros parece desnecessariamente complexa e lenta, em comparação com duas tarefas executadas no mesmo micro. Não vejo o problema que dois micros resolvem.
Olin Lathrop

@Olin Lathrop. Simplifica o desenvolvimento de software. torna a depuração muito mais simples, etc. É como por que os carros têm centenas de microprocessadores em vez de um (mais simples como você pode sugerir) grande processador multiprocesso. Não tenho absolutamente nenhum problema em usar vários MCUs, em que o custo é principalmente inferior ao dos chips lógicos de função única e a funcionalidade é mais fácil de definir e desenvolver. Nesse caso, existe apenas um vetor de interrupção TWI no ATMega328, portanto, é mais difícil suportar dois canais de I2C. ..Mas certamente é uma escolha pessoal.
Jack Creasey

Nesse caso, vários processadores aumentam a complexidade e introduz a necessidade de comunicação extra. Você não precisa usar interrupções ou hardware para a IIC quando você é o mestre do barramento. No entanto, existem muitos processadores que podem lidar com dois barramentos da IIC independentemente em hardware. Se o ATMega não puder e você desejar usar duas IIC de hardware, não use um ATMega.
amigos estão dizendo sobre

@Olin Lathrop. Vamos concordar em discordar. O IMO bit bashing acima de cem kHz é um não iniciador. O problema para o OP é que o custo de serializar dados para enviar a um PC para executar o algoritmo de mapeamento está repleto de problemas de tempo.
Jack Creasey

Obrigado pela resposta e comentários. O ATMega328 / Arduino não pode ser usado no lado do MCU devido ao tempo do MCU I2C. Este MCU é capaz de gerar uma sequência de início mais rápida que 4.7us após uma parada anterior. Veja a tabela 32-10 nas folhas de dados do ATMega328 (parâmetro tBUF). O que estava acontecendo era que o Arduino estava NACKing em todas as leituras do i2c que se seguiram à gravação do i2c. Aparentemente, é um problema conhecido. Encontrei informações sobre isso em algum lugar on-line depois de puxar todo o meu cabelo por causa disso. Foi por isso que mudei para o CPLD.
Alexxx #
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.