This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

SPI & Bluetooth best practices

Hi All,

I'm a little rusty on my embedded C and was wondering if I could get a few broad pointers on the best way to organize this research system I'm putting together.

The system is basic: I have an NRF52840 that talks to a custom IC via SPI and then sends whatever it reads out to an NRF52 dongle via bluetooth. If this was one way then it would be especially straight forward but unfortunately, the NRF52840 can also receive commands from the dongle (over bluetooth) that may or may not trigger specific SPI transactions. It seems to me that the best way to organize the NRF52840 (that's talking to the custom IC) as the peripheral and the dongle as the central device. This way the peripheral can use an interrupt to query the IC and then notify the central device with a new packet.

I'm having a little more trouble with the other main function though (dongle sending commands to the peripheral) though since it's really important that this bluetooth connection runs as fast as possible without dropping any packets.

Is it fair to have a main while loop on all my devices that spins (just checking if there's been an incoming command) when they're in their main interrupt-driven operating mode. Then if there has been a command, they break out of their interrupt-driven mode and work on whatever the command is? I worry that this constant spinning may burn unnecessary power and also may slow down the main BLE operation (which needs to run as fast as possible!). Are there other alternatives that may allow the bluetooth and spi to operate as fast as possible in parallel?

Thanks so much for any advice/help. If anything was unclear please let me know and I can clarify. I tried to distill everything to not be too confusing but I may have distilled too much.

- Ryan

Parents
  • Hi Karl, 

    This was incredibly helpful! I'll play with the SPIM peripheral/drivers and take a look at the SAADC example! 

    I have one more question regarding best practices - this time with my 'main mode of operation'. Eventually, I will have a timer triggering a SPI transaction every 500us. The resulting data from this SPI transaction will then be beamed via bluetooth.

    To make things simpler for now, I am leaving the SPI out and just using my 500us timer to increment a counter. I then have an if statement in my main while loop that sends the updated counter to another device over bluetooth. Is this the best way to organize fast transactions through bluetooth? I must be doing something wrong because I'm losing a lot of data (I suspect the bluetooth transactions are taking too long - or the cpu is getting halted for too long so the counter increments more than once between bluetooth transfers. I tried putting the ble_nus_data_send() command in my timer's interrupt handler but that just caused nrf52840 to hang and become unresponsive.

    I'm currently building off of the ble_app_uart examples.

    Thanks again for all of your help!

    Ryan

Reply
  • Hi Karl, 

    This was incredibly helpful! I'll play with the SPIM peripheral/drivers and take a look at the SAADC example! 

    I have one more question regarding best practices - this time with my 'main mode of operation'. Eventually, I will have a timer triggering a SPI transaction every 500us. The resulting data from this SPI transaction will then be beamed via bluetooth.

    To make things simpler for now, I am leaving the SPI out and just using my 500us timer to increment a counter. I then have an if statement in my main while loop that sends the updated counter to another device over bluetooth. Is this the best way to organize fast transactions through bluetooth? I must be doing something wrong because I'm losing a lot of data (I suspect the bluetooth transactions are taking too long - or the cpu is getting halted for too long so the counter increments more than once between bluetooth transfers. I tried putting the ble_nus_data_send() command in my timer's interrupt handler but that just caused nrf52840 to hang and become unresponsive.

    I'm currently building off of the ble_app_uart examples.

    Thanks again for all of your help!

    Ryan

Children
  • Hello,

    ryerye120 said:
    This was incredibly helpful!
    ryerye120 said:
    Thanks again for all of your help!

    No problem at all, Ryan - I am happy to hear that you found my comment helpful!

    ryerye120 said:
    To make things simpler for now, I am leaving the SPI out and just using my 500us timer to increment a counter. I then have an if statement in my main while loop that sends the updated counter to another device over bluetooth. Is this the best way to organize fast transactions through bluetooth?

    In general, the best way to do this is to handle it as part of an event handler, rather than having it happen in the main loop. This way you can easily control its priority, and compartmentalize the code better (not have everything running in the main loop).
    Additionally, for power saving, you should only have the SYSTEM_ON sleep (idle_state_handler function in the BLE Peripheral examples) happen in the main loop, since this will make your system go to SYSTEM_ON sleep whenever there are no other work to be done. 
    I would therefore recommend that you either have the transfers happens as the handler to a TIMER CC event, or similar.

    ryerye120 said:
    I must be doing something wrong because I'm losing a lot of data (I suspect the bluetooth transactions are taking too long - or the cpu is getting halted for too long so the counter increments more than once between bluetooth transfers. I tried putting the ble_nus_data_send() command in my timer's interrupt handler but that just caused nrf52840 to hang and become unresponsive.

    Could you possibly show some code of how you are doing this? Are you getting any error codes returned from your call to queue the data for sending? You could see the likely returned error codes in the sd_ble_gatts_hvx API reference documentation.
    Since data is never lost in the link it is likely that you are dropping the data in the case that it fails to queue - for example if the HVN queue is already full. This could happen if you are queueing notifications faster than you are sending them, or if your hvn queue is not big enough to accommodate all the notifications generated between each connection event.
    What connection parameters are you using for your application, and how big is your BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT?

    Best regards,
    Karl

  • Karl!

    Hi Karl, 

    I've been able to get something to work but it still can't run at max speed. Per your suggestion, I've set up the Bluetooth transfer inside the timer's interrupt handler. I fixed one problem - I had a 'while (ble_ready == success)' gating the ble_nus_data_send - removing that helped things a little. I figure this is okay because I'm under the assumption the timer will always leave enough time for the ble_nus_data_send to do its thing. My timer's interrupt handler is below:

     

    static void trx_timer_handler(void * p_context)
    {
      // fire off a SPI transaction
    
      // For now, just send an incrementing counter over BLE
      if (test_counter == 15) {
        test_counter = 0;
      } else {
        test_counter = test_counter + 1;
      }
      
    
      //app_fifo_put(&EE_rx_fifo, test_counter);
    
      if ((ble_ready == NRF_SUCCESS)) { // & (app_fifo_get(&EE_rx_fifo, EE_transfer_buffer) == NRF_SUCCESS)){
        // uint16_t size2 = sprintf(m_tx_buffer, "%d \r\n", *EE_transfer_buffer);
        uint16_t size2 = sprintf(m_tx_buffer, "%d \r\n", test_counter);
        ble_ready = ble_nus_data_send(&m_nus, (uint8_t *) m_tx_buffer, &size2, m_conn_handle);
      }
    }

    This works if I slow the timer down - if it triggers once every 3ms I don't drop any packets. Unfortunately, when I speed the timer up to trigger every 500us (which is my target), I start "dropping" packets. I suspect this is because I'm effectively trying to queue notifications faster than I can send them.

    I'm using most of the default connection parameters from the ble_app_uart example! The only ones I changed were the min and max_conn_interval:

    #define MIN_CONN_INTERVAL               MSEC_TO_UNITS(7.5, UNIT_1_25_MS)             /**< Minimum acceptable connection interval (20 ms), Connection interval uses 1.25 ms units. */
    #define MAX_CONN_INTERVAL               MSEC_TO_UNITS(7.5, UNIT_1_25_MS)             /**< Maximum acceptable connection interval (75 ms), Connection interval uses 1.25 ms units. */

    Are there any other connection parameters you think I should update to speed up the bluetooth speed? I can't find any BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT in the project or ble_gatt.h. Is this something I need to implement? Is there an example that implements this? I'm currently using NRF5 sdk 17 with the nrf52840 DK.

    As always, Karl, thank you so much for your help.

  • Hello again,

    ryerye120 said:
    As always, Karl, thank you so much for your help.

    It is no problem at all, Ryan, I am happy to help!

    ryerye120 said:
    I'm under the assumption the timer will always leave enough time for the ble_nus_data_send to do its thing.

    The ble_nus_data_send function does not actually send the data directly, but it queues the notification for sending (placing it in the hvn queue). The queue is FIFO, and the notification will be sent as soon as there is room for it in an upcoming connection event.
    - This is why it is important that your queue size is at minimum large enough to hold all notifications you intend to generate over the span of your connection interval. I say at minimum because you also need to account for possible retransmission due to corrupted packets or otherwise lost packets (data is never lost in the link since the un-acknowledged packet will be retransmitted).

    ryerye120 said:
    I've set up the Bluetooth transfer inside the timer's interrupt handler.

    The ble_nus_data_send call could also happen as part of the SPI event handler, so that you do not have to wait in the timer interrupt for the SPI transaction to complete before you can queue the data for transfer, for instance.

    ryerye120 said:
    The only ones I changed were the min and max_conn_interval:

    This is indeed how you change the connection interval preferences of the peripheral - but keep in mind that it is always the central that actually determines what connection parameters will be used. The peripheral may only send connection parameter update requests in order to ask the central to change to parameters within its preferences, but the central is free to reject these requests.
    If the central rejects the peripheral can either accept the parameters used by the central, or terminate the link. Many applications such as nRF Connect for smartphone etc. will accommodate any preferences, but some native applications might not in order to save power on the central device. Certain phone OS's especially limit this, in order to avoid excess power consumption.

    ryerye120 said:
    I can't find any BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT in the project or ble_gatt.h.

    Does it not exist in your sdk_config.h file? The sdk_config is used to set all these types of defines and configurations for your project in the nRF5 SDK.

    ryerye120 said:
    Are there any other connection parameters you think I should update to speed up the bluetooth speed?

    You could take a look at the SoftDevice's throughput documentation for this - it details the parameters used to achieve the highest connection throughput. Have you calculated what throughput you will need for your application? Is there a constraint on the latency of the transfers?

    Best regards,
    Karl

  • Hello again, Ryan

    I just noticed that you do not immediately check the error code returned from ble_nus_data_send. I strongly recommend that you always pass the returned error code directly to an APP_ERROR_CHECK in order to be alerted to any possible failings by the function. There might be an unexpected condition or situation that makes a function call fail, and checking the returned error code is the only way you may know that this has happened.

    This is likely also the reason why you might be seeing 'lost data' - if the call to queue data for sending fails, and your application proceeds as if it succeeded, it will discard the data and move on to the next.
    The returned error codes will also let you know exactly how to remedy the issue (by looking up the returned error code in the function's API reference) if you have DEBUG defined in your preprocessor defines, like shown in the included image:

    This will make a detailed error message be outputted by your logger whenever a non-NRF_SUCCESS error code is passed to an APP_ERROR_CHECK.

    Best regards,
    Karl

  • Hi Karl,

    The queue is FIFO, and the notification will be sent as soon as there is room for it in an upcoming connection event.

    Ahhh this makes a lot of sense. This seems like a likely issue because BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT isn't defined in my sdk_config.h either. I'm assuming this is a peripheral device thing since it has to do with notifications (which is what I'm working on right now).


    You could take a look at the SoftDevice's throughput documentation for this - it details the parameters used to achieve the highest connection throughput. Have you calculated what throughput you will need for your application? Is there a constraint on the latency of the transfers?

    Yes! Ultimately I'll need to hit about 1Mbps so my goal is to transfer about 500 bits every 500 microsec (which means I should be transferring about 1000 bits total in every millisecond -> 1000kbps). From my understanding/corroborated by the link, I need to make sure my central device sets the PHY to 2Mbps. It should be that way already - is there a way to ask the peripheral to print what the PHY setting is via a debug statement?


    I just noticed that you do not immediately check the error code returned from ble_nus_data_send. I strongly recommend that you always pass the returned error code directly to an APP_ERROR_CHECK in order to be alerted to any possible failings by the function.

    Ah great point! I've added an app error check and the pertinent code is now:

      if ((ble_ready == NRF_SUCCESS)) { // & (app_fifo_get(&EE_rx_fifo, EE_transfer_buffer) == NRF_SUCCESS)){
        // uint16_t size2 = sprintf(m_tx_buffer, "%d \r\n", *EE_transfer_buffer);
        uint16_t size2 = sprintf(m_tx_buffer, "%d \r\n", test_counter);
        ble_ready = ble_nus_data_send(&m_nus, (uint8_t *) m_tx_buffer, &size2, m_conn_handle);
        APP_ERROR_CHECK(ble_ready);
      }
     

    As you expected, the device starts to hang and I get thrown into an error. Unfortunately, I'm having a really hard time finding a stack trace that gives me more info. This is what I see in segger:

    I figure the real error I need to fix is 0x00730075 but I can't figure out what this error is. So far I've looked through sdk_errors.h, nrf_error.h, and google and haven't been able to understand. Clearly, I don't understand the error hierarchy - is there a more detailed guide somewhere describing these? I've been trying to go through the appropriate nrf52 s140 documentation infocenter but if anything I'm more confused.

    As a side note:

    The ble_nus_data_send call could also happen as part of the SPI event handler, so that you do not have to wait in the timer interrupt for the SPI transaction to complete before you can queue the data for transfer, for instance.

    That's encouraging to hear. When I add the SPI driver into this project, I'll shift everything just to the spi trx' interrupt handler!

Related