Understanding NUS throughput using bt_gatt_write and bt_gatt_write_without_response

In my application I had been using the NUS service as a simple way of sending and receiving data between devices in a serial-like manner. One bottleneck I had hit was throughput, as I had only been getting a maximum of about 8 KBps (64 kbps), but in my case a command station needs to download up to 128MB of data which, as you could imagine, would take way too long.

I'm now trying to look at what part of this pipeline is failing, and as a first test I wanted to modify the `throughput` example to use NUS to see what I could get between two Nordic boards (nRF52-DK and nRF5340-DK). With the original (non-NUS) configuration, I can hit pretty close to the expected max, around 1300 kbps. Once I swap things out with the NUS clients, the max achievable is around 260 kbps using the smallest possible connection interval. The rate drops proportional to the increase in connection interval, seeming to indicate I can only send one NUS message per connection event. If I replace the `bt_nus_client_send` function with a direct call to `bt_gatt_write_without_response` I regain the 1300 kbps performance without seeing a strong dependence on connection interval (and very low intervals have slightly reduced throughput, as expected).

In trying to research this problem on this board, I've seen a number of conflicting statements on this matter, although many of the posts are pretty old so a lot could have changed, so I would like to check my knowledge on the expected behavior and understand whether or not it's matching the observed behavior (and, if not, why).

  • Is it expected that using `bt_gatt_write` will only allow one message sent per connection interval? I've seen conflicting statements, even at least one post claiming to see 1300 kbps using the NUS calls, but I haven't reproduced this. If this is not correct, then I'm confused where this dependence on the connection interval is coming from.
  • If it is expected, is 260 kbps about what you would expect for a max throughput? I'm confused why some posts claim a higher throughput is possible, I could be doing something wrong still or this might have been a change to the NUS interface (the post I'm referencing is 2 years old now).
  • Am I correct in understanding `bt_gatt_write_without_response` still receives acknowledgment at some level in the stack, ensuring delivery? It seems like that's the most common opinion from what I've read, so then I wonder if there's any disadvantage to using this call instead of `bt_nus_client_send`. Put another way, I wonder why NUS uses `bt_gatt_write` instead.
  • In either case (`write` or `write_without_response`) what happens if there's no acknowledgement? Does it try to resend the packet some set number of times, or just update an error counter somewhere?

I'm tempted to just make the minor modification of writing without response, but before I do I'd like to better understand the repercussions!

Parents
  • Hi!

    but in my case a command station needs to download up to 128MB of data which, as you could imagine, would take way too long.

    The command station is the peripheral? Normally, I would have imaged a "command station" to be the central(GATT client), and then you have peripherals(GATT server) sending data to the "command station" as notifications.

    Here is an overview over different types of data transfers you can do with BLE:

    With Write command and notifications, you should be able to achieve similar throughput(~1300 kbps). With Write Request, the server need to send a confirmation back, and that slow things down considerably. Even with notifications and write(without response), there is acknowledgment on the link layer. So if a packet was not received, it will be retransmitted. Here is an explanation on that.

  • Thank you the article was helpful. I also took a bit more time to understand the terminology better. You're correct, usually I would be using "bt_nus_send" to send data from the peripheral to another device, as the peripheral hosts the NUS service. I modified the throughput application to benchmark throughput in both directions and I am able to see 1300 kbps in both directions as you suggested, so that's great! Sending to a laptop I'm able to see up to 600 kbps so there's some work to do there, but this was very helpful.

    Now, this issue does still remain if I also want high throughput from the central to the peripheral, which does call into question the use of bt_gatt_write instead of bt_gatt_write_without_response in the nus_client implementation. I'll probably switch to directly calling the latter for tasks that require more throughput (I do need to upload a good bit of configuration data periodically, although nowhere near as much as the telemetry download). I wonder if there's any disadvantage to always using that call in NUS, if not outright replacing the call in nus_client all together?

    So I'll try to answer my own questions here to be complete:

    • Is it expected that using `bt_gatt_write` will only allow one message sent per connection interval? I've seen conflicting statements, even at least one post claiming to see 1300 kbps using the NUS calls, but I haven't reproduced this. If this is not correct, then I'm confused where this dependence on the connection interval is coming from.
      Roughly speaking there will be only one message per interval because it will likely take much longer than the T_IFS for the peer application to acknowledge the message, since this needs to be done at the application level. So one per connection interval is probably best-case.
    • If it is expected, is 260 kbps about what you would expect for a max throughput? I'm confused why some posts claim a higher throughput is possible, I could be doing something wrong still or this might have been a change to the NUS interface (the post I'm referencing is 2 years old now).
      This seems to give with what's indicated in the Nordic BLE bandwidth tables, so probably correct. The mentioned article calls bt_nus_send from the GATT server, which is actually using a notification. I was trying to reproduce this in the opposite direction, where the call to the bt_nus_client_send uses a write request instead of command.
    • Am I correct in understanding `bt_gatt_write_without_response` still receives acknowledgment at some level in the stack, ensuring delivery? It seems like that's the most common opinion from what I've read, so then I wonder if there's any disadvantage to using this call instead of `bt_nus_client_send`. Put another way, I wonder why NUS uses `bt_gatt_write` instead.
      An ACK is still sent, just somewhere else earlier in the network stack (i.e., not all the way at the application level). The write request could be useful if you want to make sure the application is not overwhelmed by data from the client, but it's not clear to me why it should be the default choice. Admittedly, though, my BLE design experience is limited.
    • In either case (`write` or `write_without_response`) what happens if there's no acknowledgement? Does it try to resend the packet some set number of times, or just update an error counter somewhere?
      In either case it will retry anytime it detects dropped packets, independent of the application acknowledgement. That's the main point here, is that the write request and indications require an additional acknowledgement at the application level on top of the packet receipt and integrity check that's done earlier in the network stack.

    Thanks for your help, your response helped me clear up (I hope) some gaps in my understanding of BLE stack.

Reply
  • Thank you the article was helpful. I also took a bit more time to understand the terminology better. You're correct, usually I would be using "bt_nus_send" to send data from the peripheral to another device, as the peripheral hosts the NUS service. I modified the throughput application to benchmark throughput in both directions and I am able to see 1300 kbps in both directions as you suggested, so that's great! Sending to a laptop I'm able to see up to 600 kbps so there's some work to do there, but this was very helpful.

    Now, this issue does still remain if I also want high throughput from the central to the peripheral, which does call into question the use of bt_gatt_write instead of bt_gatt_write_without_response in the nus_client implementation. I'll probably switch to directly calling the latter for tasks that require more throughput (I do need to upload a good bit of configuration data periodically, although nowhere near as much as the telemetry download). I wonder if there's any disadvantage to always using that call in NUS, if not outright replacing the call in nus_client all together?

    So I'll try to answer my own questions here to be complete:

    • Is it expected that using `bt_gatt_write` will only allow one message sent per connection interval? I've seen conflicting statements, even at least one post claiming to see 1300 kbps using the NUS calls, but I haven't reproduced this. If this is not correct, then I'm confused where this dependence on the connection interval is coming from.
      Roughly speaking there will be only one message per interval because it will likely take much longer than the T_IFS for the peer application to acknowledge the message, since this needs to be done at the application level. So one per connection interval is probably best-case.
    • If it is expected, is 260 kbps about what you would expect for a max throughput? I'm confused why some posts claim a higher throughput is possible, I could be doing something wrong still or this might have been a change to the NUS interface (the post I'm referencing is 2 years old now).
      This seems to give with what's indicated in the Nordic BLE bandwidth tables, so probably correct. The mentioned article calls bt_nus_send from the GATT server, which is actually using a notification. I was trying to reproduce this in the opposite direction, where the call to the bt_nus_client_send uses a write request instead of command.
    • Am I correct in understanding `bt_gatt_write_without_response` still receives acknowledgment at some level in the stack, ensuring delivery? It seems like that's the most common opinion from what I've read, so then I wonder if there's any disadvantage to using this call instead of `bt_nus_client_send`. Put another way, I wonder why NUS uses `bt_gatt_write` instead.
      An ACK is still sent, just somewhere else earlier in the network stack (i.e., not all the way at the application level). The write request could be useful if you want to make sure the application is not overwhelmed by data from the client, but it's not clear to me why it should be the default choice. Admittedly, though, my BLE design experience is limited.
    • In either case (`write` or `write_without_response`) what happens if there's no acknowledgement? Does it try to resend the packet some set number of times, or just update an error counter somewhere?
      In either case it will retry anytime it detects dropped packets, independent of the application acknowledgement. That's the main point here, is that the write request and indications require an additional acknowledgement at the application level on top of the packet receipt and integrity check that's done earlier in the network stack.

    Thanks for your help, your response helped me clear up (I hope) some gaps in my understanding of BLE stack.

Children
No Data
Related