Improve Bluetooth credit handling and Switch 2 connection timing
This commit is contained in:
parent
fa38123873
commit
7c1c37a84e
13 changed files with 408 additions and 71 deletions
|
|
@ -3061,6 +3061,7 @@ void test_switch2_radio_policy(bool individual) {
|
|||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto other_ble = device(3, true, UNI_BT_CONN_PROTOCOL_BLE);
|
||||
negotiated_intervals[other_ble.conn.handle] = 24;
|
||||
ready_switch2(left);
|
||||
require(negotiated_intervals[left.conn.handle] == 6,
|
||||
"a single Switch2 link must retain fast scheduling");
|
||||
|
|
@ -3075,7 +3076,7 @@ void test_switch2_radio_policy(bool individual) {
|
|||
platform_on_device_connected(&classic);
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6 &&
|
||||
negotiated_intervals[other_ble.conn.handle] == 6,
|
||||
negotiated_intervals[other_ble.conn.handle] == 24,
|
||||
"Classic setup must not slow Joy-Cons or retime unrelated BLE");
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic controller must complete setup alongside the pair");
|
||||
|
|
@ -3105,10 +3106,19 @@ void test_switch2_radio_policy(bool individual) {
|
|||
"peer-negotiated slow Joy-Con links must reconcile to the fast default");
|
||||
platform_on_device_disconnected(&right);
|
||||
right = switch2_device(1, UNI_SW2_PRO_PID);
|
||||
negotiated_intervals[right.conn.handle] = 24;
|
||||
ready_switch2(right);
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 24,
|
||||
"Joy-Con preference must not remove the existing Switch2 Pro coexistence policy");
|
||||
negotiated_intervals[right.conn.handle] == 6 &&
|
||||
negotiated_intervals[other_ble.conn.handle] == 24,
|
||||
"Switch2 Pro must join at the fast interval without retiming unrelated BLE");
|
||||
platform_on_device_disconnected(&left);
|
||||
left = switch2_device(0, UNI_SW2_PRO_PID);
|
||||
negotiated_intervals[left.conn.handle] = 24;
|
||||
ready_switch2(left);
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6,
|
||||
"multiple Switch2 Pro links must remain fast alongside Classic");
|
||||
}
|
||||
|
||||
void test_switch2_radio_settling() {
|
||||
|
|
@ -3119,39 +3129,50 @@ void test_switch2_radio_settling() {
|
|||
platform_on_device_connected(&classic);
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic-first connection must become ready");
|
||||
ready_switch2(left);
|
||||
negotiated_intervals[left.conn.handle] = 24;
|
||||
negotiated_intervals[right.conn.handle] = 24;
|
||||
defer_interval_updates = true;
|
||||
now_ms = UINT32_MAX - 10;
|
||||
ready_switch2(left);
|
||||
platform_on_device_connected(&right);
|
||||
require(interval_requests[right.conn.handle] == 0,
|
||||
"pending Switch2 setup must retain ownership of its initial interval");
|
||||
require(platform_on_device_ready(&right) == UNI_ERROR_SUCCESS,
|
||||
"second Switch2 connection must become ready");
|
||||
platform_on_device_disconnected(&right);
|
||||
// The surviving controller completes the old update after losing its mate.
|
||||
for (auto* half : {&left, &right}) {
|
||||
negotiated_intervals[half->conn.handle] = pending_intervals[half->conn.handle];
|
||||
}
|
||||
now_ms += 50;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
negotiated_intervals[left.conn.handle] = pending_intervals[left.conn.handle];
|
||||
require(negotiated_intervals[left.conn.handle] == 6,
|
||||
"late coexistence update must not strand a solo at slow intervals across clock wrap");
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
defer_interval_updates = false;
|
||||
reject_interval_updates = true;
|
||||
ready_switch2(right);
|
||||
const unsigned attempts = interval_requests[left.conn.handle];
|
||||
require(pending_intervals[left.conn.handle] == 6 &&
|
||||
pending_intervals[right.conn.handle] == 6,
|
||||
"both slow Switch2 Pro links must request the fast interval");
|
||||
const unsigned deferred_attempts = interval_requests[left.conn.handle];
|
||||
now_ms += 999;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
require(interval_requests[left.conn.handle] == attempts,
|
||||
"rejected negotiation must not flood the HCI command queue");
|
||||
require(interval_requests[left.conn.handle] == deferred_attempts &&
|
||||
negotiated_intervals[left.conn.handle] == 24,
|
||||
"an accepted asynchronous request must be allowed to settle across clock wrap");
|
||||
platform_on_device_disconnected(&right);
|
||||
negotiated_intervals[left.conn.handle] = pending_intervals[left.conn.handle];
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
interval_requests[left.conn.handle] == deferred_attempts,
|
||||
"a late fast-interval acknowledgement must survive another controller leaving");
|
||||
defer_interval_updates = false;
|
||||
reject_interval_updates = true;
|
||||
negotiated_intervals[left.conn.handle] = 24;
|
||||
right = switch2_device(1, UNI_SW2_PRO_PID);
|
||||
negotiated_intervals[right.conn.handle] = 24;
|
||||
ready_switch2(right);
|
||||
const unsigned left_attempts = interval_requests[left.conn.handle];
|
||||
const unsigned right_attempts = interval_requests[right.conn.handle];
|
||||
now_ms += 999;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
require(interval_requests[left.conn.handle] == left_attempts &&
|
||||
interval_requests[right.conn.handle] == right_attempts,
|
||||
"rejected negotiations must not flood the HCI command queue");
|
||||
reject_interval_updates = false;
|
||||
++now_ms;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24,
|
||||
"transiently rejected negotiation must recover without reconnect or pairing");
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6,
|
||||
"transiently rejected fast intervals must recover without reconnect or pairing");
|
||||
}
|
||||
|
||||
void test_switch2_mate_reconnect() {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@
|
|||
#define SWITCH_PICO_CREDIT_BATCH_TEST_CONFIG_H
|
||||
|
||||
#define ENABLE_CLASSIC
|
||||
#define ENABLE_BLE
|
||||
#define ENABLE_LE_CENTRAL
|
||||
#define ENABLE_LE_PERIPHERAL
|
||||
#define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL
|
||||
#define ENABLE_BTSTACK_ASSERT
|
||||
#define HAVE_MALLOC
|
||||
#define HCI_OUTGOING_PRE_BUFFER_SIZE 4
|
||||
#define MAX_NR_HCI_CONNECTIONS 4
|
||||
#define HCI_ACL_PAYLOAD_SIZE 1021
|
||||
#define HCI_HOST_ACL_PACKET_LEN 1021
|
||||
#define HCI_HOST_ACL_PACKET_NUM 3
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ static unsigned wire_count;
|
|||
static uint8_t wire[32][64];
|
||||
static int wire_size[32];
|
||||
static void (*during_send)(void);
|
||||
static uint8_t wire_type[32];
|
||||
static bool complete_inline;
|
||||
static uint8_t *last_transport_packet;
|
||||
|
||||
static void transport_sent(void){
|
||||
uint8_t sent[] = {HCI_EVENT_TRANSPORT_PACKET_SENT, 0};
|
||||
receive_packet(HCI_EVENT_PACKET, sent, sizeof(sent));
|
||||
}
|
||||
|
||||
noreturn void btstack_assert_failed(const char *file, uint16_t line){
|
||||
fprintf(stderr, "BTstack assertion: %s:%u\n", file, line);
|
||||
|
|
@ -60,8 +68,13 @@ static int can_send(uint8_t packet_type){
|
|||
}
|
||||
|
||||
static int send_packet(uint8_t type, uint8_t *packet, int size){
|
||||
assert(type == HCI_COMMAND_DATA_PACKET);
|
||||
assert(type == HCI_COMMAND_DATA_PACKET || type == HCI_ACL_DATA_PACKET);
|
||||
assert(wire_count < 32 && size <= 64);
|
||||
// CYW43 writes its header before the packet and reads word-rounded data.
|
||||
assert(((uintptr_t)packet & 3u) == 0);
|
||||
memset(packet - HCI_OUTGOING_PRE_BUFFER_SIZE, 0xa5, HCI_OUTGOING_PRE_BUFFER_SIZE);
|
||||
wire_type[wire_count] = type;
|
||||
last_transport_packet = packet;
|
||||
memcpy(wire[wire_count], packet, (size_t) size);
|
||||
wire_size[wire_count++] = size;
|
||||
if (during_send != NULL){
|
||||
|
|
@ -69,6 +82,7 @@ static int send_packet(uint8_t type, uint8_t *packet, int size){
|
|||
during_send = NULL;
|
||||
callback();
|
||||
}
|
||||
if (complete_inline) transport_sent();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +110,7 @@ static void working(void){
|
|||
// Fixture bypasses controller initialization, leaving the production receive/run paths intact.
|
||||
hci_stack->state = HCI_STATE_WORKING;
|
||||
hci_stack->gap_tasks_classic = 0;
|
||||
hci_stack->le_scanning_param_update = false;
|
||||
hci_stack->num_cmd_packets = 1;
|
||||
hci_register_acl_packet_handler(on_acl);
|
||||
hci_register_sco_packet_handler(on_sco);
|
||||
|
|
@ -106,6 +121,8 @@ static void begin(uint32_t now, bool asynchronous){
|
|||
transport_ready = true;
|
||||
transport.can_send_packet_now = asynchronous ? can_send : NULL;
|
||||
during_send = NULL;
|
||||
complete_inline = false;
|
||||
last_transport_packet = NULL;
|
||||
wire_count = acl_delivered = sco_delivered = 0;
|
||||
btstack_run_loop_init(&run_loop);
|
||||
btstack_memory_init();
|
||||
|
|
@ -310,6 +327,125 @@ static void test_lifecycle(void){
|
|||
assert(wire_count == after_power_transition);
|
||||
finish();
|
||||
}
|
||||
|
||||
#ifdef SWITCH_PICO_HCI_CREDIT_BUFFER
|
||||
static void test_credits_bypass_fragmented_acl(unsigned completion_mode){
|
||||
begin(100, completion_mode != 0);
|
||||
complete_inline = completion_mode == 2;
|
||||
add_connection(0x41, BD_ADDR_TYPE_ACL);
|
||||
add_connection(0x42, BD_ADDR_TYPE_LE_PUBLIC);
|
||||
hci_stack->acl_packets_total_num = 1;
|
||||
hci_stack->acl_data_packet_length = 8;
|
||||
hci_reserve_packet_buffer();
|
||||
uint8_t *packet = hci_get_outgoing_packet_buffer();
|
||||
little_endian_store_16(packet, 0, 0x2041);
|
||||
little_endian_store_16(packet, 2, 16);
|
||||
for (unsigned i = 0; i < 16; ++i) packet[4 + i] = (uint8_t)(0x80 + i);
|
||||
assert(hci_send_acl_packet_buffer(20) == ERROR_CODE_SUCCESS);
|
||||
assert(wire_count == 1 && wire_type[0] == HCI_ACL_DATA_PACKET);
|
||||
|
||||
if (completion_mode == 1) transport_ready = false;
|
||||
acl(0x42);
|
||||
acl(0x42);
|
||||
if (completion_mode == 1){
|
||||
advance(2);
|
||||
assert(wire_count == 1);
|
||||
transport_ready = true;
|
||||
transport_sent();
|
||||
}
|
||||
assert(wire_count == 2 && wire_type[1] == HCI_COMMAND_DATA_PACKET);
|
||||
expect_credits(1, 0x42, 2);
|
||||
if (completion_mode == 1) transport_sent();
|
||||
// A credit completion must not release the still-prepared ACL continuation.
|
||||
assert(!hci_can_send_command_packet_now());
|
||||
|
||||
uint8_t completed[] = {HCI_EVENT_NUMBER_OF_COMPLETED_PACKETS, 5, 1, 0x41, 0, 1, 0};
|
||||
receive_packet(HCI_EVENT_PACKET, completed, sizeof(completed));
|
||||
assert(wire_count == 3 && wire_type[2] == HCI_ACL_DATA_PACKET);
|
||||
assert(wire_size[0] == 12 && wire_size[2] == 12);
|
||||
assert(little_endian_read_16(wire[2], 0) == 0x1041);
|
||||
for (unsigned i = 0; i < 8; ++i){
|
||||
assert(wire[0][4 + i] == 0x80 + i);
|
||||
assert(wire[2][4 + i] == 0x88 + i);
|
||||
}
|
||||
if (completion_mode == 1) transport_sent();
|
||||
assert(hci_can_send_command_packet_now());
|
||||
finish();
|
||||
}
|
||||
|
||||
static void test_credit_buffer_async_ownership(void){
|
||||
begin(100, true);
|
||||
add_connection(0x41, BD_ADDR_TYPE_ACL);
|
||||
hci_reserve_packet_buffer();
|
||||
acl(0x41);
|
||||
advance(2);
|
||||
assert(wire_count == 1);
|
||||
uint8_t *in_flight = last_transport_packet;
|
||||
uint8_t saved[8];
|
||||
memcpy(saved, in_flight, sizeof(saved));
|
||||
acl(0x41);
|
||||
advance(2);
|
||||
assert(wire_count == 1 && memcmp(saved, in_flight, sizeof(saved)) == 0);
|
||||
// Even a transport that reports ready must not permit overlapping sends.
|
||||
assert(!hci_can_send_prepared_acl_packet_now(0x41));
|
||||
transport_sent();
|
||||
assert(wire_count == 2);
|
||||
expect_credits(1, 0x41, 1);
|
||||
transport_sent();
|
||||
assert(!hci_can_send_command_packet_now());
|
||||
hci_release_packet_buffer();
|
||||
assert(hci_can_send_command_packet_now());
|
||||
finish();
|
||||
}
|
||||
|
||||
static void sleep_during_send(void){
|
||||
hci_power_control(HCI_POWER_SLEEP);
|
||||
}
|
||||
|
||||
static void test_credit_completion_during_sleep(void){
|
||||
begin(100, false);
|
||||
add_connection(0x41, BD_ADDR_TYPE_ACL);
|
||||
acl(0x41);
|
||||
during_send = sleep_during_send;
|
||||
advance(2);
|
||||
assert(wire_count == 1);
|
||||
expect_credits(0, 0x41, 1);
|
||||
// Cancelling batching must not strand ownership of a completed synchronous send.
|
||||
assert(hci_can_send_command_packet_now());
|
||||
finish();
|
||||
|
||||
begin(100, true);
|
||||
add_connection(0x41, BD_ADDR_TYPE_ACL);
|
||||
hci_reserve_packet_buffer();
|
||||
acl(0x41);
|
||||
advance(2);
|
||||
hci_power_control(HCI_POWER_SLEEP);
|
||||
transport_sent();
|
||||
// A late credit completion after sleep still must not release another packet.
|
||||
assert(!hci_can_send_command_packet_now());
|
||||
hci_release_packet_buffer();
|
||||
assert(hci_can_send_command_packet_now());
|
||||
finish();
|
||||
}
|
||||
|
||||
static void test_credit_buffer_power_cycle(void){
|
||||
begin(100, true);
|
||||
add_connection(0x41, BD_ADDR_TYPE_ACL);
|
||||
acl(0x41);
|
||||
advance(2);
|
||||
assert(wire_count == 1);
|
||||
// A wedged transport never completes the credit send; shutdown closes it.
|
||||
transport_ready = false;
|
||||
hci_power_control(HCI_POWER_OFF);
|
||||
advance(1001);
|
||||
transport_ready = true;
|
||||
hci_power_control(HCI_POWER_ON);
|
||||
assert(wire_count == 2);
|
||||
assert(wire_type[1] == HCI_COMMAND_DATA_PACKET);
|
||||
assert(little_endian_read_16(wire[1], 0) == HCI_OPCODE_HCI_RESET);
|
||||
finish();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
int main(void){
|
||||
|
|
@ -320,6 +456,12 @@ int main(void){
|
|||
test_sco_and_malformed_acl();
|
||||
test_synchronous_callbacks();
|
||||
test_lifecycle();
|
||||
#ifdef SWITCH_PICO_HCI_CREDIT_BUFFER
|
||||
for (unsigned mode = 0; mode < 3; ++mode) test_credits_bypass_fragmented_acl(mode);
|
||||
test_credit_buffer_async_ownership();
|
||||
test_credit_completion_during_sleep();
|
||||
test_credit_buffer_power_cycle();
|
||||
#endif
|
||||
#else
|
||||
begin(100, false);
|
||||
add_connection(0x41, BD_ADDR_TYPE_ACL);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,14 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batching", [False, True], ids=["default", "batched"])
|
||||
def test_btstack_credit_batch_native(tmp_path: Path, batching: bool) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("batching", "dedicated"),
|
||||
[(False, False), (True, False), (True, True)],
|
||||
ids=["default", "batched", "dedicated"],
|
||||
)
|
||||
def test_btstack_credit_batch_native(
|
||||
tmp_path: Path, batching: bool, dedicated: bool
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
sdk = Path(
|
||||
os.environ.get("PICO_SDK_PATH", root / "build" / "_deps" / "pico_sdk-src")
|
||||
|
|
@ -58,6 +64,7 @@ def test_btstack_credit_batch_native(tmp_path: Path, batching: bool) -> None:
|
|||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
*(["-DSWITCH_PICO_HCI_CREDIT_BATCH=1"] if batching else []),
|
||||
*(["-DSWITCH_PICO_HCI_CREDIT_BUFFER=1"] if dedicated else []),
|
||||
f"-I{root / 'tests' / 'btstack_credit_batch_native_stubs'}",
|
||||
f"-I{patched_source}",
|
||||
f"-I{source}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue