Estoy luchando por conseguir que un maestro I2C se comunique de forma fiable con un esclavo, porque también hay un expansor de puertos MCP23017 en el mismo bus. El MCP23017 parece detectar - ¡y ACK! - su dirección, incluso cuando ese patrón de bits aparece casualmente en medio de las conversaciones de otros dispositivos. Cuando hace el ACK espurio, aplasta el bit de datos actual a '0', lo que corrompe el mensaje recibido.
No encuentro ninguna errata publicada al respecto. ¿Esto es real o he metido la pata?
Esto parece muy serio. Estoy descubriendo que hay ciertos mensajes que simplemente no pueden ser enviados correctamente a un esclavo diferente, porque parecen válidos pero contienen bits cero espurios.
Pruebas:
En un experimento, acabamos de conectar un Arduino al MCP23017 usando I2C, y escribimos algo de código para hacer saltar el I2C como si fuera a hablar con un dispositivo diferente (inexistente). A mitad de camino, el MCP23017 salta y corrompe las comunicaciones con su ACK espurio.
Notas:
- Para el experimento, el bus I2C se implementa simplemente usando los pines digitales 4 (SDA) y 5 (SCL) del Arduino, con los pull-ups internos del Arduino. No es perfecto, lo sé, pero habla felizmente con el MCP23017 cuando lo probamos.
- Añadimos una resistencia de 10k al SDA del MCP23017, para que cuando interfiera con el bus, sea una señal visiblemente más débil en el osciloscopio.
- Hicimos un experimento de control para asegurarnos de que la configuración podía hablar correctamente con el MCP23017, y así fue, todo bien. Luego modificamos el código para fingir que hablaba con un dispositivo diferente, y el MCP23017 se entrometió con su ACK espurio a pesar de no estar dirigido.
- Es un MCP23017 bastante antiguo, de 2005.
Aquí está el código - puedes ponerlo en un archivo .ino y subirlo. Añade pequeños retrasos para que las trazas de alcance sean inteligibles.
enum I2cResult {
OK,
WRONG_BIT,
NACK,
WRONG_BIT_NACK
};
void sdaLow(){
pinMode(4, OUTPUT);
digitalWrite(4, LOW); // just make sure that no matter what pinMode does, we get it right...
}
void sdaHigh(){
pinMode(4, INPUT_PULLUP);
}
bool isSdaHigh(){
return digitalRead(4);
}
void sclLow(){
pinMode(5, OUTPUT);
digitalWrite(5, LOW);
}
void sclHigh(){
pinMode(5, INPUT_PULLUP);
}
bool isSclHigh(){
return digitalRead(5);
}
void startBit(){
delayMicroseconds(10);
if(isSdaHigh()){
sclHigh();
delayMicroseconds(20);
sdaLow();
delayMicroseconds(20);
sclLow();
} else {
sclLow();
delayMicroseconds(20);
sdaHigh();
delayMicroseconds(20);
sclHigh();
delayMicroseconds(20);
sdaLow();
delayMicroseconds(20);
sclLow();
}
delayMicroseconds(10);
}
void stopBit(){
delayMicroseconds(10);
if(isSdaHigh()){
sclLow();
delayMicroseconds(20);
sdaLow();
delayMicroseconds(20);
sclHigh();
delayMicroseconds(20);
sdaHigh();
delayMicroseconds(20);
} else {
sclHigh();
delayMicroseconds(20);
sdaHigh();
delayMicroseconds(20);
}
delayMicroseconds(20);
}
//Sends a 1 or 0 based on level - false gives 0, true gives 1.
//returns true if the level on the pin matched the written level
bool sendBit(bool level){
if (level){
sdaHigh();
} else {
sdaLow();
}
delayMicroseconds(25);
sclHigh();
delayMicroseconds(25);
bool v = isSdaHigh();
delayMicroseconds(25);
sclLow();
delayMicroseconds(25);
return v == level;
}
I2cResult sendByte(uint8_t b){
I2cResult result = OK;
for(uint8_t i = 0; i < 8; i++){
if (i == 4){
delayMicroseconds(50); // adds space on scope for readability
}
bool v = (b & (1<<(7-i)));
if(!sendBit(v)){
result = WRONG_BIT;
}
}
delayMicroseconds(50); // adds space on scope for readability
if(sendBit(true)){
if(result == WRONG_BIT){
result = WRONG_BIT_NACK;
}else{
result = NACK;
}
}
return result;
}
//This sends a byte and acks it (simulating a slave receiving the data).
I2cResult sendByteAck(uint8_t b) {
I2cResult result = OK;
for (uint8_t i = 0; i < 8; i++) {
if (i == 4){
delayMicroseconds(50); // adds space on scope for readability
}
bool v = (b & (1<<(7-i)));
if (!sendBit(v)){
result = WRONG_BIT;
}
}
delayMicroseconds(50); // adds space on scope for readability
sendBit(false);
return result;
}
void setup() {
Serial.begin(9600);
Serial.println("hello!");
digitalWrite(4, LOW);
digitalWrite(5, LOW);
pinMode(4, INPUT_PULLUP);
pinMode(5, INPUT_PULLUP);
delay(100);
}
// The MCP23017 is wired to use address 0100 000.
// We then pretend to talk to another device - on address 1100 001, and send it the byte 1000 0001
// This triggers the bug, as, including ack bits, the bus has the following data:
// start|Address|ACK| Data |Ack|stop
// S 1100 0010 0 1000 0001 0 P
// +---++---+^
// with the data the expander sees highlighted (+--+ for the address nibbles, ^ for the ack)
// the expander then writes 0 over the 1 at the end.
void loop() {
startBit();
delayMicroseconds(50);
sendByteAck(0xC2);
delayMicroseconds(150); // adds space on scope for readability
Serial.println(sendByteAck(0x81));
delayMicroseconds(50);
stopBit();
delay(10000);
}
Aquí está el hardware, en una protoboard:
Aquí están las trazas de alcance que muestran el problema - amarillo es el reloj, azul es los datos. Esta imagen muestra el mensaje completo de dos bytes:
Esta imagen de abajo amplía el segundo byte. Observe el '0' debilitado en la línea de datos azul, visible una vez que el Arduino deja de afirmar SDA=0, donde el MCP23017 está afirmando su ACK después de aparentemente ver un fantasma 0100 000W.