High power consumption of nRF52832 using TWI 100KHz ?

Hi, we are using nRF52832 for our project. SDK version is nRF5_SDK_17.1.0_ddde560. SoftDevice is S132.

The project requires TWI to read data from a sensor and send that data via BLE. The sensor will output 9 bytes every 2.5ms, or 3600B/s data rate. Currently, BLE consumes 0.1mA and it matches the estimated number. When data transfer via BLE is going on, our board consumes 3.8mA. We are trying to lower this number by determining if any part of the system consumes more power than usual.

So we first remove the code that sends data via BLE, which means only connect via BLE, and read data via TWI, but do not send it to the peer. We get a consumption of around 3.2mA
Then we remove the code that reads data from sensor via TWI. This time consumption reduces to 1.5mA. The sensor is still in its working state, just the code to read data in the sensor buffer is removed.

That means reading sensor data via TWI consumes 3.2-1.5=1.7mA. This is so high, especially for a quite low data rate. TWI clock is 100KHz. We check the sensor buffer every 60ms to see if any data is available. The buffer check is simply just 2 registers (head & tail) being read from the sensor. From this, we know how many bytes we can read from the sensor, and we read a burst of that many bytes from the sensor.

Is this number normal for TWI usage? Is there any way we can optimize the power consumption when using TWI? Please let us know soon. We really appreciate your speedy help.

Best regards,

Xander

Parents
  • A handy envelope shows 9 bytes or 81 bits at 100kHz is 810uSec active clock and data in 2500uSec, or about 32%. 32% active I2C data and clock with 4k7 pull-ups on SDA and SCK is (say) 400uA assuming something near 50% duty cycle on both. 1k0 pull ups this would be closer to 1mA, with 10k0 pull-ups closer to 200uA. Test this by switching to more power-efficient 400kHz to see if that part of the power reduces by x4 with the same value pull-ups.

    The key to the balance of power is likely to be "TWI"; TWI is inefficient since the CPU is awake more frequently. TWIM using DMA is much better, maybe approaching 9 times better, as although the same number of bytes are processed the 9 sleep/wakeup cycles and interrupt entry/exit cycles (power consumption overhead) are replaced by a single sleep/wakeup cycle and interrupt, particularly when errata workarounds are in effect. That means 9-byte TWIM, not 9 x 1-byte TWIM; 9 x 1-byte TWIM is even worse than 9 x 1-byte TWI and yes that is commonly used.

    Want optimum efficiency? Switch to SPIM (SPI with DMA), should that be an option, which does not require power-hungry pull-ups. I2C/TWI is old-school from the days when power consumption by other components was orders of magnitude higher; yes it's easy but no it's not a low-power solution. Data processing overhead for TWIM and SPIM both with DMA is similar.

  • Test this by switching to more power-efficient 400kHz to see if that part of the power reduces by x4 with the same value pull-ups.

    Yes it does. I changed to TWI at 400KHz and reading sensor data consumes 0.4mA instead of 1.7mA. I still don't understand why it can be this high?

  • 8-bit TWI isn't; it's 9-bit; there is an ACK bit after every 8-bit byte, and 9x9=81 bits. "Data received will be stored in RAM at the address specified in the RXD.PTR register. The TWI master will generate an ACK after all but the last byte received from the slave. The TWI master will generate a NACK after the last byte received to indicate that the read sequence shall stop"

    30k is an unlikely high value for a TWI/I2C/TWIM pull-up; try measuring it and see if that is the actual value.

    DMA with TWI aka TWIM is trivial, and no, fancy stuff is not required. Start with this bare-metal sample and set the number of received bytes to something larger than 1, such as 9 or higher. i2c_master/main.c

    0.4mA instead of 1.7mA? Either the CPU is not staying in sleep or the 30k pull-ups are 3k0 or so. Quite often the CPU is not staying in sleep, and that's another whole can of worms.

  • I have the PCB schematic and we place the PCB assembly from a fab so it shouldn't be wrong, however, I will contact the PCB designer team to confirm if the schematic I'm having is correct. And it is 50K, not even 30K.

    The resistor size is too small, 0201, and I don't have a probe that can measure the resistance of this tiny thing.

    If the CPU isn't staying in sleep, I think it should use a few mA of consumption, and increasing the TWI clock won't reduce 4x the consumption like this, right?

Reply
  • I have the PCB schematic and we place the PCB assembly from a fab so it shouldn't be wrong, however, I will contact the PCB designer team to confirm if the schematic I'm having is correct. And it is 50K, not even 30K.

    The resistor size is too small, 0201, and I don't have a probe that can measure the resistance of this tiny thing.

    If the CPU isn't staying in sleep, I think it should use a few mA of consumption, and increasing the TWI clock won't reduce 4x the consumption like this, right?

Children
  • 50k pull-up is highly unlikely to work at all at 400kHz, and would be most doubtful at 100kHz with potential failures in the field, unless the nRF52832 internal 13k pull-up is enabled. As an aside, no designer would choose 50k for an I2C/TWI pull-up; 50k sounds more like a push-button, sensor enable pin or reset pin pull-up value.

    The sleep issue may be that the CPU stays awake for the 9 TWI transfers but sleeps outside that time; just a guess. Predicting code issues is difficult unless you post some of the code, or indeed the schematic.

  • I asked my PCB design team and because of the tiny PCB size, from the simulation with 400KHz I2C, they claim they can go up to 500K ohm pull-up if I want.
    From the firmware side, I don't explicitly pull up any I2C pin.

    Sure below is the code involved in the TWI transfer with sensors.

    Firstly, this is the timer handler that is triggered every 60ms

    static void poll_sensor_timeout_handler(void *p_context){
        UNUSED_PARAMETER(p_context);
        // clear buffer
        for (int i = 0; i < I2C_MAX_BUFFER_SIZE; i++) {
            sensor_data[i] = 0;
        }
        // read data from sensor
        package_len = sensor_checkNewData();
        // send data via BLE to smartphone
        if (package_len > 0){
            ble_data_char_update(sensor_data);
        }
    }

    Here is the code that read sensor data via TWI.

    uint16_t sensor_checkNewData(){
        static sensor_reg addr_reg  = sensor_FIFODATA;
        // static uint8_t data_reg[I2C_MAX_BUFFER_SIZE];
        uint8_t readPointer = sensor_getReadPointer();
        uint8_t writePointer = sensor_getWritePointer();
    
        int numberOfSamplesAvailable = 0;
        
        // new data available when FIFO isn't full (readP # writeP) or when (readP = writeP && A_FULL_INT = high)
        if (readPointer != writePointer || ((readPointer = writePointer) && sensor_getINT_A_FULL())){
            int return_byte = 0;
            //Calculate the number of readings we need to get from sensor
            numberOfSamplesAvailable = writePointer - readPointer;
            if (numberOfSamplesAvailable < 0) numberOfSamplesAvailable += 32; //Wrap condition
    
            //We now have the number of readings, now calc bytes to read
            int bytesToRead = numberOfSamplesAvailable * activeDevices * 3;    
            if (numberOfSamplesAvailable > sample_count_per_MTU)
                {
                    //If bytesToRead is 32 this is bad because we read 6 bytes at a time
                    //32 % 6 = 2 left over. We don't want to request 32 bytes, we want to request 30.
                    //32 % 9 (Red+IR+ECG) = 5 left over. We want to request 27.
    
                    bytesToRead = sample_count_per_MTU * SAMPLE_BYTE_LENGTH;
                }
            return_byte = bytesToRead;
            //Get ready to read a burst of data from the FIFO register, max burst length: 32*3*3=288 bytes
            nrf_twi_mngr_transfer_t const transfers[] = {sensor_READ(&addr_reg, sensor_data, bytesToRead)};
            nrf_twi_mngr_perform(sensor_p_nrf_twi_mngr, sensor_p_twi_config, transfers, 2, NULL);
            return return_byte;
        } else {
            return 0;
        }
    }

    Below are functions that read read/write pointer of sensor internal buffer

    uint8_t sensor_getWritePointer(){
        static sensor_reg addr_reg  = sensor_FIFOWRITEPTR;
        static uint8_t data_reg = 0;
        sensor_read_8(&addr_reg, &data_reg);
        return data_reg;
    }
    
    uint8_t sensor_getReadPointer(){
        static sensor_reg addr_reg  = sensor_FIFOREADPTR;
        static uint8_t data_reg = 0;
        sensor_read_8(&addr_reg, &data_reg);
        return data_reg;
    }

    sensor_read_8 is a helper function to read a single 8-bit register from the sensor

    /**
     * @brief Macro for creating a read transfer from sensor.
     *
     * @param[in] p_reg_addr Pointer to the address of register to read from.
     * @param[in] p_buffer   Pointer to store the read data
     * @param[in] byte_cnt   How many byte to read
     */
    #define sensor_READ(p_reg_addr, p_buffer, byte_cnt) \
        NRF_TWI_MNGR_WRITE(sensor_ADDRESS, p_reg_addr, 1,        NRF_TWI_MNGR_NO_STOP), \
        NRF_TWI_MNGR_READ (sensor_ADDRESS, p_buffer,   byte_cnt, 0)
    
    void sensor_read_8(sensor_reg *p_reg_addr, uint8_t *p_buffer){
        nrf_twi_mngr_transfer_t const transfers[] = {sensor_READ(p_reg_addr, p_buffer, 1)};
        nrf_twi_mngr_perform(sensor_p_nrf_twi_mngr, sensor_p_twi_config, transfers, 2, NULL);
    }
    

    Below is the TWI settings in main.c

    #define TWI_INSTANCE_ID 0
    #define MAX_PENDING_TRANSACTIONS 5
    NRF_TWI_MNGR_DEF(m_nrf_twi_mngr, MAX_PENDING_TRANSACTIONS, TWI_INSTANCE_ID);
    nrf_drv_twi_config_t const twi_config = {
        .scl                = I2C_SCL_PIN,
        .sda                = I2C_SDA_PIN,
        .frequency          = NRF_DRV_TWI_FREQ_400K,
        .interrupt_priority = APP_IRQ_PRIORITY_LOW_MID,
        .clear_bus_init     = false
    };

  • The code you list shows that there is no sleep until the separate TWI transfers complete so as I suggested "The sleep issue may be that the CPU stays awake for the 9 TWI transfers but sleeps outside that time" explaining the cause of the high current and why it reduces when using 400kHz. Switch to TWIM; search the devzone for an example, I see one here twim-errata-109 but there may be better examples; I don't have time to search, perhaps a Nordic engineer could suggest one.

    I2C/TWI/TWIM has a specification Tr which is the rise time of the clock signal SCK; this may be quoted in the datasheet or inferred from (Tclock-(Thigh+Tlow))/2 = Tr = Tf. You don't mention the part number, but Tr is typically 300nSec but may be significantly shorter. Pin capacitance of the nRF52832 is some 3pF, sensor say 3pF also and some distributed capacitance traces and pins to GND giving a minimum 10pF or so. 50k and 10pF violates Tr and may lead to failures in the field, 500k would clearly be unusable. On one battery management IC in a recent design with a short Tr the only way to satisfy Tr was to use a pull-up of 910 Ohm. For a medical or safety device a failure caused by a known Tr or other violation could lead to legal action against the designerin the event of equipment malfunction

    For lowest power in extreme cases, should that be the goal, a simple bare-metal design would minimise CPU on time further; keep in mind nRFx examples are designed to protect the novice user from doing things wrong during development and are not really targeted for final designs.

  • I just asked my coworker to measure the resistance and they are indeed 51K pull up. I will ask them what make they think they can use up to 500K pull up for I2C.

    So I'm planning to use non-blocking TWI with nrf_twi_mngr_schedule(). Is it the same with TWIM? Or I should use TWIM?

    I'm implementing non-blocking TWI in conjunction with BLE and it is very unstable now. It will crash after about 10 seconds of running of collecting TWI data and send it via BLE. First thing is that I have set .interrupt_priority = APP_IRQ_PRIORITY_LOW_MID for TWI, as LOW and MID both crashes the app immediately once TWI transaction is going on. 

    This is the timer handler that triggers every 60ms

    static void poll_sensor_timeout_handler(void *p_context){
        UNUSED_PARAMETER(p_context);
        sensor_checkNewData();    
    }

    This is the function that will read a burst of data via TWI non-blocking mode

    void sensor_checkNewData(){
        static sensor_reg addr_reg  = sensor_FIFODATA;
        uint8_t readPointer = sensor_getReadPointer();
        uint8_t writePointer = sensor_getWritePointer();
        static twi_buffer small_buffer;
        static int numberOfSamplesAvailable = 0;
        
        // new data available when FIFO isn't full (readP # writeP) or when (readP = writeP && A_FULL_INT = high)
        if (readPointer != writePointer || ((readPointer = writePointer) && sensor_getINT_A_FULL())){
            //Calculate the number of readings we need to get from sensor
            numberOfSamplesAvailable = writePointer - readPointer;
            if (numberOfSamplesAvailable < 0) numberOfSamplesAvailable += 32; //Wrap condition
    
            //We now have the number of readings, now calc bytes to read
            int bytesToRead = numberOfSamplesAvailable * activeDevices * 3;    
            if (bytesToRead > I2C_MAX_BUFFER_SIZE)
            {
                //If bytesToRead is 32 this is bad because we read 6 bytes (Red+IR * 3 = 6) at a time
                //32 % 6 = 2 left over. We don't want to request 32 bytes, we want to request 30.
                //32 % 9 (Red+IR+ECG) = 5 left over. We want to request 27.
    
                bytesToRead = I2C_MAX_BUFFER_SIZE - (I2C_MAX_BUFFER_SIZE % (activeDevices * 3)); //Trim toGet to be a multiple of the samples we need to read
            }
            small_buffer.byte_cnt = bytesToRead;
            //Get ready to read a burst of data from the FIFO register, max burst length: 32*3*3=288 bytes
            nrf_twi_mngr_transfer_t const transfers[] = {sensor_READ(&addr_reg, small_buffer.data, bytesToRead)};
            const nrf_twi_mngr_transaction_t p_transaction = {
                .callback = twi_callback,
                .p_user_data = &small_buffer,
                .p_transfers = transfers,
                .number_of_transfers = sizeof(transfers) / sizeof(transfers[0]),
                .p_required_twi_cfg = NULL
            };
            APP_ERROR_CHECK(nrf_twi_mngr_schedule(sensor_p_nrf_twi_mngr, &p_transaction));
        } 
    }

    This is the TWI callback that I want to read the TWI buffer and store it in a bigger buffer, which is for packaging data for BLE transaction.

    void twi_callback(ret_code_t result, void *p_user_data){
        if (result == NRF_SUCCESS){
            twi_buffer* buffer = (twi_buffer*)(p_user_data);
            if (sensor_FIFO.count + buffer->byte_cnt > sensor_BUFFER_SIZE_BYTES){
                // not enough space, need to release buffer immediately
                char data[negotiated_MTU];
                for (uint8_t i = 0; i < negotiated_MTU; i++) {
                    data[i] = sensor_FIFO.data[sensor_FIFO.head];
                    sensor_FIFO.head = (sensor_FIFO.head + 1) % sensor_BUFFER_SIZE_BYTES;
                }
                sensor_FIFO.count -= negotiated_MTU;
                send_BLE_data(data);
            }
            for (int i = 0; i< buffer->byte_cnt; i++){
                sensor_FIFO.data[sensor_FIFO.tail] = buffer->data[i];
                sensor_FIFO.tail = (sensor_FIFO.tail + 1) % sensor_BUFFER_SIZE_BYTES;
                sensor_FIFO.count ++;
            }
            if (sensor_FIFO.count >= negotiated_MTU){
                char data[negotiated_MTU];
                for (uint8_t i = 0; i < negotiated_MTU; i++) {
                    data[i] = sensor_FIFO.data[(sensor_FIFO.head+i) % sensor_BUFFER_SIZE_BYTES];
                }
                sensor_FIFO.count -= negotiated_MTU;
                send_BLE_data(data);
            }
        }
    }
    

    Below are struct of all buffers being used in above code.

    typedef struct{
        uint8_t data[I2C_MAX_BUFFER_SIZE];
        uint8_t byte_cnt;
    } twi_buffer;   // This is 1D buffer to store TWI data from sensor burst
    
    typedef struct{
        uint8_t data[sensor_BUFFER_SIZE_BYTES];
        uint16_t head;
        uint16_t tail;
        uint16_t count;
    } sensor_buffer; //This is our circular buffer of readings from the sensor
    

    I still don't understand why it crashes occasionally then. I have tried removing callback from TWI non-block, but it still crashes.

    const nrf_twi_mngr_transaction_t p_transaction = {
        .callback = NULL,
        .p_user_data = NULL,
        .p_transfers = transfers,
        .number_of_transfers = sizeof(transfers) / sizeof(transfers[0]),
        .p_required_twi_cfg = NULL
    };

    I also remove APP_ERROR_CHECK from max86150_checkNewData() but it still crashes so TWI queue should be fine.

    I also ensures there are no other TWI transaction going on in the app. Also it usually takes about 20 seconds from the moment I start the poll_sensor_timer until the app crashes, so I will check if there is any other background task that interferes. Previously I used blocking TWI and there wasn't any issue like this

  • Hi 

    I agree with Hugh that 51K, and definitely 500K, sounds excessive. Good for power consumption, but not strong enough to ensure sufficiently fast rise times. 

    Regarding your other issues, have you tried to scope the bus to see what is going on? 

    In particular, how long will each transaction typically take, and what is the worst case time?
    The fact that the length of the transaction changes from time to time makes the system a bit unpredictable, and you need to ensure that the worst case time of your transaction is shorter than the 60ms interval. 

    Another thing worth checking is if the start of each transaction is spaced 60ms apart as expected, or if there is a lot of jitter in the start time. It is possible you could have other interrupts in the system that will delay the execution of the timer, which can cause issues if the total transaction time is close to the interval time. 

    Have you tried to increase the interval just to see if the crashes go away? 
    If so it could point to an issue with the timing as mentioned above. 

    Also, looking at your code I see you call send_BLE_data(..) from the twi_callback directly, which could be problematic. Ideally you want to buffer the data in the callback, but have the actual BLE transmission occur in thread/main context to ensure you are not blocking other interrupts in the system while this is taking place. 

    Best regards
    Torbjørn

Related