This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

UART and I2C conflict on nrf51 with S130

I am working on application where the nrf51 will read information from several sensors and send the data to a smartphone app via BLE. Among the sensors are one that sends a 23 byte message via UART at 9600 Baud about once a second and an Si7021 temp/humidity sensor over I2C. I am using the S130 Softdevice, and currently am prototyping using the nrf51 DK board.

I have a timer setup to trigger once a second, in order to prepare the sensor data and send it over BLE. This timeout handler routine seems like the natural place to put my blocking I2C code, but if I leave it here, then the application resets about every minute or so. If I dump a bunch of data into the UART, it resets immediately. If I comment out the I2C routine, then I can send the UART as much data as I want with no resets. I am currently circumventing the problem by running the I2C code from the UART handler routine only after having received a complete message, so I know more UART data won't be received while the I2C is running.

I don't like this workaround, and can not explain why I2C and UART read are not coexisting. I have tried to include the most relevant code snippets below; let me know if you need to see more. Thanks!

Addendum 1: I had to set the TWI to ".interrupt_priority = APP_IRQ_PRIORITY_HIGH" to call it from inside the timeout, or else it resets immediately. With APP_IRQ_PRIORITY_HIGH, it takes about a minute to reset with normal sensor input.

Addendum 2: I've spent some time with the debugger now, and determined that reset is triggered from inside the uart handler routine, and appears to be an over run of the 6 byte hardware UART receive buffer. It's ok in my application to miss a message from the sensor occasionally, so I've tried to implement error handling for this case:

case APP_UART_COMMUNICATION_ERROR:
        if (p_event->data.error_communication == UART_ERRORSRC_OVERRUN_Msk)
        {
            // do I need more in here?
            app_uart_flush();
            index = 0;
            uart_receive_state = WAIT_FOR_MSG;
            printf("overrun!\r\n");
        } else {
            APP_ERROR_HANDLER(p_event->data.error_communication);
        }
        break;

This seems to work, but I wonder-

  1. Should I add anything to this to clear the error, the hardware buffer, or anything else?
  2. Seems strange to me that the nrf51 can't keep up with this at 9600 Baud with BLE and some other peripherals- is that surprising? This is a Commercial-Off-The-Shelf sensor with a UART interface, that like very many others, does NOT support hardware flow control.

Original code below:

    static void uart_init(void)
{
    uint32_t                     err_code;
    const app_uart_comm_params_t comm_params =
    {
        //RX_PIN_NUMBER, 
        12,
        TX_PIN_NUMBER,
        RTS_PIN_NUMBER,
        CTS_PIN_NUMBER,
        APP_UART_FLOW_CONTROL_DISABLED,
        false,
        UART_BAUDRATE_BAUDRATE_Baud9600
    };

    APP_UART_FIFO_INIT( &comm_params,
                       UART_RX_BUF_SIZE,
                       UART_TX_BUF_SIZE,
                       uart_event_handle,
                       APP_IRQ_PRIORITY_LOW,  // should this be high?
                       err_code);
    APP_ERROR_CHECK(err_code);
}

void uart_event_handle(app_uart_evt_t * p_event)
{

    switch (p_event->evt_type)
    {
        case APP_UART_DATA_READY:
            UNUSED_VARIABLE(app_uart_get(&this_uart_char));
                        
            switch (uart_receive_state)
            {		
				// parse uart output here and do this upon completion of message
                Si7021_read();                               
            } 
            break;

        case APP_UART_COMMUNICATION_ERROR:
            APP_ERROR_HANDLER(p_event->data.error_communication);
            break;

        case APP_UART_FIFO_ERROR:
            APP_ERROR_HANDLER(p_event->data.error_code);
            break;

        default:
            break;
    }
}

void twi_init (void)
{
    ret_code_t err_code;
    
    const nrf_drv_twi_config_t twi_si_7021_config = {
       .scl                = SI7021_I2C_SCL_PIN,
       .sda                = SI7021_I2C_SDA_PIN,
       .frequency          = NRF_TWI_FREQ_100K,
       .interrupt_priority = APP_IRQ_PRIORITY_LOW // was high
    };
    
    err_code = nrf_drv_twi_init(&m_twi_si_7021, &twi_si_7021_config, NULL, NULL);
    APP_ERROR_CHECK(err_code);
    
    nrf_drv_twi_enable(&m_twi_si_7021);
}

void Si7021_read( void )
{
    ret_code_t err_code;
    static uint16_t m_sample;
	uint8_t reg;
    uint16_t this_sample;
	
	reg = SI7021_HUMD_MEASURE_HOLD;
    err_code = nrf_drv_twi_tx(&m_twi_si_7021, SI7021_ADDR, &reg, sizeof(reg), true);
    APP_ERROR_CHECK(err_code);

    /* Read 2 bytes from the specified address. */
    err_code = nrf_drv_twi_rx(&m_twi_si_7021, SI7021_ADDR, (uint8_t*)&m_sample, sizeof(m_sample));
    APP_ERROR_CHECK(err_code);
    
    this_sample = (m_sample>>8) | (m_sample<<8);
    
	humidity = (float)this_sample * 125 / 65536.0 - 6;
		
	reg = SI7021_TEMP_PREV;
    err_code = nrf_drv_twi_tx(&m_twi_si_7021, SI7021_ADDR, &reg, sizeof(reg), true);
    APP_ERROR_CHECK(err_code);

    /* Read 2 bytes from the specified address. */
    err_code = nrf_drv_twi_rx(&m_twi_si_7021, SI7021_ADDR, (uint8_t*)&m_sample, sizeof(m_sample));
    APP_ERROR_CHECK(err_code);
    
    this_sample = (m_sample>>8) | (m_sample<<8);

    temperature = (float)this_sample * 175.72 / 65536.0 - 46.85;    
    
}

static void timers_init(void)
{
    uint32_t err_code;

    // Initialize timer module.
    APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_OP_QUEUE_SIZE, false);

    // Create timers.

    err_code = app_timer_create(&m_sensor_timer_id,
                                APP_TIMER_MODE_REPEATED,
                                sensor_timeout_handler);
    APP_ERROR_CHECK(err_code);
}

static void sensor_timeout_handler(void * p_context)
{
    // if I do this here, the firmware resets if the UART is receiving much data
    //Si7021_read();

    // at this point, get data together from sensors and send over BLE ( S130 softdevice ) 	
}
  • If you get overrun error it indicates that you are blocking the uart interrupt for more than 6 bytes (the error will happen if the UART peripheral FIFO is full and a start bit is detected on the bus). With 9600 baud this is (using 8N2 -> 10 bauds per byte) 60 / 9600 * 1000 = 6.25ms. It will actually be a bit more since there is some time between every byte.

    The TWI transaction you are doing within the interrupt may take too long time and be the reason you get overrun errors. I suggest that you try to use app_scheduler to run the code from main context. I recommend to read this tutorial on app_scheduler. You can also set up app_timer to execute the handlers in main context using app_scheduler.

    Generally speaking you should spend as little time as possible inside interrupts, or else things like this can happen.

  • Thanks, Ole, I implemented app_scheduler for the entire timeout using the convenient app_timer_appsh library, and now I can have the TWI code running in the timeout routine (now running in main context,) and dump a ton of text to the UART without overrun. I want to do some more testing on this approach, but I'll come back and ratify your answer if it looks ok.

    I'd still like to handle any potential UART over runs gracefully- my code above for the error handler seems to work, any feedback as to any other housekeeping I need to do at that point?

    Thanks!

  • Right now, I have left the above UART handler as is without using the scheduler to send it to main context. I expect that, even though the blocking TWI commands are going to a higher interrupt context behind the scenes, UART handler can now jump in any time between these blocking commands, which is probably the right behavior.

    Is there any sense in sending the UART handler to main context using scheduler? It appears that app_timer_appsh is sending the entire timeout routine to main context as one event, so that would defeat the purpose, right? Or is there some way to make scheduler do the right thing?

  • You don’t need to send the uart handler (I assume you mean the code inside the handler) to main context using scheduler, unless you do a lot of stuff inside the handler of course, then you should put that code in a function and send it to main context using the scheduler.

    About the overrun error handler you should not call app_uart_flush(), this will flush the software fifo (created in app_uart_fifo.c) not the UART peripheral fifo. You can call app_uart_flush() if you get fifo error (APP_UART_FIFO_ERROR), but in that case you should consider allocating a bigger fifo (UART_RX_BUF_SIZE).

Related