2

I have an application polling 4 ADCs by I2C. The application shall also write on some ADC registers. In order to test it, I'd like to use a single Arduino UNO, enabling it for receiving I2C messages over 4 different addresses by masking the TWAMR register. In this way I can have 4 ADCs "emulated" on a single Arduino.

When the application asks for a read to Arduino at some address, I'm able to get the value of the required address by reading the TWDR register:

void requestEvent(){
  byte addr = TWDR >> 1;
  if (0x4A == addr){ // do something }
  else if (0x4B == addr){ // do something }
  // ...
}

void setup(){
  // ...
  Wire.onRequest(requesEvent);
}

However, when the application requests to write something to a certain ADC (with its own address), I'm not able to get the address value in the onReceive(int) callback:

void receiveEvent(int howMany){
  byte addr = TWDR >> 1; // returns the first byte AFTER the address in the I2C message
}

Am I doing something wrong? Is there a way to reach my goal?

Thanks.

Luca

Edit:

Reading the ATmega datasheet (sec. 22.9.4, p. 241), the problem with reading the TWDR register is that it contains the last byte received, that in slave receiver mode is not the address but the last data sent by the master.

Anyway, is there any workaround for this problem?

8
  • This seems unnecessarily complicated to me. You say you are polling, so why not just send a request to 0x4A, and wait for a response, then 0x4B, and wait for a response, and so on?
    – Nick Gammon
    Commented Oct 30, 2023 at 8:39
  • Actually I've written something incorrect. The application is not only polling. but also writing to some registers of ADC in order to configure it. In particular, the procedure for each ADC is: 1) write to ADC config register to trigger a conversion; 2) poll it continuously until the conversion is completed; 3) request the measure when the conversion is completed. I'm not sure why the application is written in that way, I took it as is. Probably there are some possible improvements on this side, but I'm working on the other end, that is a simulator to test the application
    – lumaca96
    Commented Oct 30, 2023 at 10:50
  • It still seems simpler to me to do a round-robin approach. First, tell each ADC to start the conversion (ie. Start A, Start B, Start C, Start D), then (Poll A, Poll B, Poll C, Poll D), until some or all have finished the conversion, then (Read A, Read B, Read C, Read D). It seems too complex to me to be fiddling around with hardware registers when it isn't necessary.
    – Nick Gammon
    Commented Oct 31, 2023 at 4:31
  • I'm not sure if I've understood your answer. If your advise is to change the design of the device that my arduino is communicating with, unfortunately I can't control it.
    – lumaca96
    Commented Nov 3, 2023 at 17:13
  • I'm not advocating any hardware changes. Why not just send the commands to one ADC after another? If you ask a particular ADC for its response, and wait for it, then surely you know which ADC replied?
    – Nick Gammon
    Commented Nov 4, 2023 at 3:17

1 Answer 1

1

Reading the TWDR register in the onReceive() callback doesn't give you the address because of the time, that it is called at. The onReceive() callback is called when all bytes have already been received and put in the Wire libraries internal buffer. Thus in that callback TWDR will still hold the last received data byte.

The onRequest() callback on the other hand is called right after the address was received. Thus TWDR still holds the received address.

You will need to modify the Wire library to achieve your goal. Similar to the onRequest() callback you can introduce a third kind of callback function, which will be executed after receiving the slave address to save the received address for later use (the address matching is still done via hardware, so you still need to mask the address via TWAMR).

You can find the files for the Wire library inside your used core (for me the Uno uses the path ~/.arduino15/packages/arduino/hardware/avr/1.8.5/libraries/Wire). Look at line 581 of src/utility/twi.c. This is inside the TWI ISR, the case clause TW_SR_SLA_ACK, which will be executed on address receive/match. Here you can insert your callback function.

If you want to do this in a similar handy way, that the Wire library does with the other callbacks, then you need to change a few other things:

  1. Insert a function pointer for this at the start of the twi.c file, right besides the other function pointers:

     static void (*twi_onSlaveAddressMatch)(uint8_t);
    
  2. Add the execution of that function to the case in the ISR described above and provide the content of TWDR (shifted to eliminate the direction bit) as a parameter:

     twi_onSlaveAddressMatch(TWDR >> 1);
    
  3. Add a function to set the callback function pointer in twi.c:

     void twi_attachSlaveAddressMatchEvent( void (*function)(uint8_t) ){
         twi_onSlaveAddressMatch = function;
     }
    
  4. Add the callback set function as a function prototype to twi.h:

     void twi_attachSlaveAddressMatchEvent( void (*function)(uint8_t) );
    
  5. In Wire.h add a function pointer for the new callback and the corresponding service function inside the private: section:

     static void (*user_onSlaveAddressMatch)(uint8_t);
     static void onSlaveAddressMatchService(uint8_t);
    
  6. In Wire.h add a function to set the user callback function to the public: section:

     void onSlaveAddressMatch( void (*)(uint8_t) );
    
  7. In Wire.cpp add the implementation of the service function:

     void TwoWire::onSlaveAddressMatchService(uint8_t address){
         if(!user_onSlaveAddressMatch){
             return;
         }
         user_onSlaveAddressMatch(address);
     }
    
  8. In Wire.cpp add the implementation of the callback setter function:

     void TwoWire::onSlaveAddressMatch( void (*function)(uint8_t) ) {
         user_onSlaveAddressMatch = function;
     }
    
  9. In Wire.cpp, TwoWire::begin(void) add the initialization of the callback service function:

     twi_attachSlaveAddressMatchEvent(onSlaveAddressMatchService);
    

Now you should be able to set a callback for the address receive in your main code and save the address for later use:

uint8_t received_address = 0;

void getAddress(uint8_t address){
    received_address = address;
}

void setup(){
    ...
    Wire.onSlaveAddressMatch(getAddress);
}

In the onReceive() callback you can then check for that global variable.

Note: I have not tested or compiled this. I just looked at the logic of the Wire library and copied the way it already defines its existing callback functions.

2
  • Hi @chrisl, thanks for the reply. I asked here as my last hope not to rebuild the core libraries. I expected that. Another workaround to solve my issue could be to add the following lines in twi.c (in case TW_SR_SLA_ACK): if (twi_sr_store_address){ twi_rxBuffer[twi_rxBufferIndex++] = TWDR >> 1; } where twi_sr_store_address is just a boolean that can be set through Wire. This way the Wire.read() method now will return also the address byte. Do you think this solution could work as well as yours? If not, why? I'm just asking to learn. Thanks again for the help, Luca
    – lumaca96
    Commented Oct 29, 2023 at 21:43
  • Sure, that would also work. Though I think using a callback function is a cleaner solution. For both solutions you need to modify the library.
    – chrisl
    Commented Oct 29, 2023 at 23:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.