5 Commits

Author SHA1 Message Date
Dirk Helbig fa94dd871d updated code for nRF Connect SDK v2.8.0 2024-11-29 10:55:13 +01:00
Dirk Helbig ebd9ac4ba9 main: add warning if alternate gpio pin is missing 2024-11-29 10:52:17 +01:00
Dirk Helbig 80c14b0ff6 main: add missing alternate gpio pin config 2024-11-29 10:41:04 +01:00
Dirk Helbig d80b61d5ba add missing overlay entry for alternate gpio 2024-11-26 13:54:09 +01:00
Dirk Helbig 39a16e22c1 main: support for more gpios and timers 2024-11-26 08:09:40 +01:00
3 changed files with 69 additions and 302 deletions
+5 -10
View File
@@ -1,14 +1,9 @@
# nRF5340 Bluetooth Controller with Support for LE ISO Timesync
This fork of the Zephyr HCI UART example adds a custom HCI Command to allow for timesync between the Bluetooth Host
and the Bluetooth Controller. When the HCI LE Read ISO Clock command is received, the Controller toggles a GPIO and
returns its current Bluetooth LE ISO Clock in the Command Complete Event.
This fork of the Zephyr HCI UART example adds a custom HCI Command to allow for timesync between the Bluetooth Host and the Bluetooth Controller. When the HCI LE Read ISO Clock command is received, the Controller toggles a GPIO and returns its current Bluetooth LE ISO Clock in the Command Complete Event.
It has been tested on the nRF5340 Audio DK, but it should work with any nRF5340 dev kit.
## Requirements
- nRF Connect SDK v2.8 or newer
## HCI LE Read ISO Clock Command
- OGF: 0x3f, OCF: 0x200
- Parameters: Flags (1 Octet) unused
@@ -30,25 +25,25 @@ _HCI over UART and timesync not tested._
### HCI over USB CDC
```sh
west build --pristine -b nrf5340_audio_dk/nrf5340/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=usb.overlay -DOVERLAY_CONFIG=overlay-usb.conf
west build -b nrf5340_audio_dk/nrf5340/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=usb.overlay -DOVERLAY_CONFIG=overlay-usb.conf
```
### HCI over UART 0 connected to Virtual UART in J-Link Probe
```sh
west build --pristine -b nrf5340_audio_dk_nrf5340_cpuapp
west build -b nrf5340_audio_dk_nrf5340_cpuapp
```
### HCI over UART 1 connected to Virtual UART in J-Link Probe as well as Arduino Headers
Release build:
```sh
west build --pristine -b nrf5340_audio_dk/nrf5340/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=uart1.overlay
west build -b nrf5340_audio_dk/nrf5340/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=uart1.overlay
```
Debug build:
```sh
west build --pristine -b nrf5340_audio_dk/nrf5340/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=uart1.overlay -DOVERLAY_CONFIG=debug.conf
west build -b nrf5340_audio_dk/nrf5340/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=uart1.overlay -DOVERLAY_CONFIG=debug.conf
```
To use UART 1 via Arduino headers, the virtual UART of the J-Link probe needs to be disabled, e.g. with the JLink Configuration Tool.
-1
View File
@@ -19,7 +19,6 @@ CONFIG_BT_CTLR_ADV_EXT=y
CONFIG_BT_CTLR_ADV_SET=2
CONFIG_BT_CTLR_ADV_ISO_SET=2
CONFIG_BT_CTLR_ADV_ISO_STREAM_COUNT=3
CONFIG_BT_CTLR_SYNC_PERIODIC_ADV_LIST_SIZE=1
# Support two links as a central, or one link as a peripheral
CONFIG_BT_MAX_CONN=8
+64 -291
View File
@@ -37,9 +37,8 @@
#define LOG_MODULE_NAME hci_uart
LOG_MODULE_REGISTER(LOG_MODULE_NAME);
static const struct device *const hci_uart_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_bt_c2h_uart));
static const struct device *const gmap_uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart1));
static const struct device *const hci_uart_dev =
DEVICE_DT_GET(DT_CHOSEN(zephyr_bt_c2h_uart));
static K_THREAD_STACK_DEFINE(tx_thread_stack, CONFIG_BT_HCI_TX_STACK_SIZE);
static struct k_thread tx_thread_data;
static K_FIFO_DEFINE(tx_queue);
@@ -68,30 +67,6 @@ static K_FIFO_DEFINE(uart_tx_queue);
*/
#define H4_DISCARD_LEN 33
#ifdef CONFIG_AUDIO_SYNC_TIMER_USES_RTC
#define TIMESYNC_GPIO DT_NODELABEL(timesync)
#if DT_NODE_HAS_STATUS(TIMESYNC_GPIO, okay)
static const struct gpio_dt_spec timesync_pin = GPIO_DT_SPEC_GET(TIMESYNC_GPIO, gpios);
#else
#error "No timesync gpio available!"
#endif
#define ALTERNATE_TOGGLE_GPIO DT_NODELABEL(alternate_toggle)
static const struct gpio_dt_spec alternate_toggle_pin = GPIO_DT_SPEC_GET(ALTERNATE_TOGGLE_GPIO, gpios);
#endif
// enable to have alternate toggle indicate time from audio in to sdu sync ref in UGT to UGG direction
#define PERIPHERAL_TO_CENTRAL_AUDIO_IN_TO_SDU_SYNC_REF
// Presentation time from Audio to SDU Sync Ref (UGT->UGG) resp. SDU Sync Ref to Audio Out (UGG->UGT)
// HS timer on nRF5340 seems to be off: 60000 ticks = 59372 us
#define PRESENTATION_TIME_US 60634
static uint32_t last_sdu_sync_ref_us;
static uint32_t sdu_interval_us;
static int h4_read(const struct device *uart, uint8_t *buf, size_t len)
{
int rx = uart_fifo_read(uart, buf, len);
@@ -284,15 +259,6 @@ static void tx_thread(void *p1, void *p2, void *p3)
/* Wait until a buffer is available */
buf = k_fifo_get(&tx_queue, K_FOREVER);
#if DT_NODE_HAS_STATUS(TIMESYNC_GPIO, okay)
// toggle GPIO and send 'B' for each TX packet to be able to capture by Logic Analzyer
if (bt_buf_get_type(buf) == BT_BUF_ISO_OUT) {
gpio_pin_toggle_dt( &timesync_pin );
uart_poll_out(gmap_uart_dev, 'B');
}
#endif
/* Pass buffer to the stack */
err = bt_send(buf);
if (err!=BT_HCI_ERR_SUCCESS) {
@@ -396,6 +362,20 @@ static int hci_uart_init(void)
SYS_INIT(hci_uart_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEVICE);
#ifdef CONFIG_AUDIO_SYNC_TIMER_USES_RTC
#define TIMESYNC_GPIO DT_NODELABEL(timesync)
#if DT_NODE_HAS_STATUS(TIMESYNC_GPIO, okay)
static const struct gpio_dt_spec timesync_pin = GPIO_DT_SPEC_GET(TIMESYNC_GPIO, gpios);
#else
#error "No timesync gpio available!"
#endif
#define ALTERNATE_TOGGLE_GPIO DT_NODELABEL(alternate_toggle)
#if DT_NODE_HAS_STATUS(ALTERNATE_TOGGLE_GPIO, okay)
static const struct gpio_dt_spec alternate_toggle_pin = GPIO_DT_SPEC_GET(ALTERNATE_TOGGLE_GPIO, gpios);
#else
#warning "No alternate toggle gpio availabe!"
#endif
#define HCI_CMD_ISO_TIMESYNC (0x200)
@@ -412,38 +392,15 @@ uint8_t hci_cmd_iso_timesync_cb(struct net_buf *buf)
LOG_INF("buf %p type %u len %u", buf, bt_buf_get_type(buf), buf->len);
LOG_INF("buf[0] = 0x%02x", buf->data[0]);
uint32_t timestamp_second_us;
// Lock interrupts to avoid interrupt between time capture and gpio toggle
uint32_t key = arch_irq_lock();
// Get current time
uint32_t timestamp_first_us = audio_sync_timer_capture();
while (1){
// get time again and verify that time didn't jump. Work around:
// https://devzone.nordicsemi.com/f/nordic-q-a/116907/bluetooth-netcore-time-capture-not-working-100-for-le-audio
timestamp_second_us = audio_sync_timer_capture();
int32_t timestamp_delta = (int32_t) (timestamp_second_us - timestamp_first_us);
if (timestamp_delta < 10){
break;
}
timestamp_first_us = timestamp_second_us;
}
rsp = bt_hci_cmd_complete_create(BT_OP(BT_OGF_VS, HCI_CMD_ISO_TIMESYNC), sizeof(*response));
response = net_buf_add(rsp, sizeof(*response));
response->cc.status = BT_HCI_ERR_SUCCESS;
response->timestamp = audio_sync_timer_capture();
#if DT_NODE_HAS_STATUS(TIMESYNC_GPIO, okay)
gpio_pin_toggle_dt( &timesync_pin );
#endif
// Unlock interrupts
arch_irq_unlock(key);
// emit event
rsp = bt_hci_cmd_complete_create(BT_OP(BT_OGF_VS, HCI_CMD_ISO_TIMESYNC), sizeof(*response));
response = net_buf_add(rsp, sizeof(*response));
response->cc.status = BT_HCI_ERR_SUCCESS;
response->timestamp = timestamp_second_us;
if (IS_ENABLED(CONFIG_BT_HCI_RAW_H4)) {
net_buf_push_u8(rsp, H4_EVT);
}
@@ -454,135 +411,56 @@ uint8_t hci_cmd_iso_timesync_cb(struct net_buf *buf)
}
#endif
uint16_t little_endian_read_16(const uint8_t * buffer, int position){
return (uint16_t)(((uint16_t) buffer[position]) | (((uint16_t)buffer[position+1]) << 8));
}
static uint32_t little_endian_read_32(const uint8_t * buffer, int position){
return ((uint32_t) buffer[position]) | (((uint32_t)buffer[position+1]) << 8) | (((uint32_t)buffer[position+2]) << 16) | (((uint32_t) buffer[position+3]) << 24);
}
#if DT_NODE_HAS_STATUS(TIMESYNC_GPIO, okay)
// make sure there's no IRQ between getting the time and the toggle
// verify timestamp as it's 100% coorect
#define LOCK_IRQS_FOR_SYNC_TOGGLE
static uint32_t toggle_and_get_bluetooth_time_us(void) {
uint32_t timestamp_toggle_us;
#ifdef LOCK_IRQS_FOR_SYNC_TOGGLE
// Lock interrupts
uint32_t key = arch_irq_lock();
#endif
while (1){
// Get current time once
timestamp_toggle_us = audio_sync_timer_capture();
// Get current time again
uint32_t timestamp_toggle_us_verify = audio_sync_timer_capture();
// check if time didn't jump
int32_t timestamp_delta = (int32_t) (timestamp_toggle_us_verify - timestamp_toggle_us);
if ((timestamp_delta >= 0) && (timestamp_delta < 10)){
break;
}
}
// Toggle
gpio_pin_toggle_dt( &timesync_pin );
#ifdef LOCK_IRQS_FOR_SYNC_TOGGLE
// Unlock interrupts
arch_irq_unlock(key);
#endif
return timestamp_toggle_us;
}
#endif
#define TIME_TO_WAIT_MS 1000
#define SYNC_TOGGLE_TIMER_INSTANCE_NUMBER 2
static const nrfx_timer_t sync_toggle_timer_instance =
NRFX_TIMER_INSTANCE(SYNC_TOGGLE_TIMER_INSTANCE_NUMBER);
static nrfx_timer_config_t cfg = {.frequency = NRFX_MHZ_TO_HZ(1UL),
.mode = NRF_TIMER_MODE_TIMER,
.bit_width = NRF_TIMER_BIT_WIDTH_32,
.interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY,
.p_context = NULL};
static volatile enum {
ALTERNATE_TOGGLE_STATE_IDLE,
ALTERNATE_TOGGLE_STATE_W2_RISE,
ALTERNATE_TOGGLE_STATE_W2_FALL
} alternate_toggle_state = ALTERNATE_TOGGLE_STATE_IDLE;
static void sync_toggle_timer_isr_handler(nrf_timer_event_t event_type, void *p_context)
{
ARG_UNUSED(p_context);
if(event_type == NRF_TIMER_EVENT_COMPARE1)
{
char * p_msg = p_context;
uint32_t remainder_us = nrf_timer_cc_get(NRF_TIMER2,
NRF_TIMER_CC_CHANNEL1);
static volatile uint32_t alternate_toggle_fall_us;
#if DT_NODE_HAS_STATUS(ALTERNATE_TOGGLE_GPIO, okay)
gpio_pin_toggle_dt( &alternate_toggle_pin );
#endif
static uint32_t get_local_time_us(void) {
nrf_timer_cc_set(NRF_TIMER2, NRF_TIMER_CC_CHANNEL2, 0);
nrf_timer_task_trigger(NRF_TIMER2, nrf_timer_capture_task_get(NRF_TIMER_CC_CHANNEL2));
uint32_t current_time_us = nrf_timer_cc_get(NRF_TIMER2, NRF_TIMER_CC_CHANNEL2);
while (current_time_us == 0) {
current_time_us = nrf_timer_cc_get(NRF_TIMER2, NRF_TIMER_CC_CHANNEL2);
}
return current_time_us;
}
static void alternate_toggle_timer_isr_handler(nrf_timer_event_t event_type, void *p_context){
ARG_UNUSED(p_context);
if(event_type == NRF_TIMER_EVENT_COMPARE1){
switch (alternate_toggle_state) {
case ALTERNATE_TOGGLE_STATE_W2_RISE:
alternate_toggle_state = ALTERNATE_TOGGLE_STATE_W2_FALL;
gpio_pin_set_dt(&alternate_toggle_pin, 1);
nrfx_timer_compare(&sync_toggle_timer_instance, NRF_TIMER_CC_CHANNEL1, alternate_toggle_fall_us, true);
break;
case ALTERNATE_TOGGLE_STATE_W2_FALL:
alternate_toggle_state = ALTERNATE_TOGGLE_STATE_IDLE;
gpio_pin_set_dt(&alternate_toggle_pin, 0);
break;
default:
__ASSERT(0, "Unknown state");
break;
}
}
}
static void alternate_toggle_setup(uint32_t rise_us, uint32_t fall_us) {
alternate_toggle_fall_us = fall_us;
alternate_toggle_state = ALTERNATE_TOGGLE_STATE_W2_RISE;
nrfx_timer_compare(&sync_toggle_timer_instance, NRF_TIMER_CC_CHANNEL1, rise_us, true);
LOG_INF("%d\n", remainder_us );
printf("Timer finished. Context passed to the handler: >%s<", p_msg);
}
}
int main(void)
{
// toggle timer setup
nrfx_err_t ret;
ret = nrfx_timer_init(&sync_toggle_timer_instance, &cfg, alternate_toggle_timer_isr_handler);
if (ret - NRFX_ERROR_BASE_NUM) {
LOG_ERR("nrfx timer init error: %d", ret);
return -ENODEV;
}
IRQ_CONNECT(NRFX_IRQ_NUMBER_GET(NRF_TIMER_INST_GET(SYNC_TOGGLE_TIMER_INSTANCE_NUMBER)), IRQ_PRIO_LOWEST,
NRFX_TIMER_INST_HANDLER_GET(SYNC_TOGGLE_TIMER_INSTANCE_NUMBER), 0, 0);
nrfx_timer_enable(&sync_toggle_timer_instance);
alternate_toggle_state = ALTERNATE_TOGGLE_STATE_IDLE;
// timer setup
nrfx_err_t ret;
ret = nrfx_timer_init(&sync_toggle_timer_instance, &cfg, sync_toggle_timer_isr_handler);
if (ret - NRFX_ERROR_BASE_NUM) {
LOG_ERR("nrfx timer init error: %d", ret);
return -ENODEV;
}
uint32_t desired_ticks = nrfx_timer_ms_to_ticks(&sync_toggle_timer_instance, TIME_TO_WAIT_MS);
nrfx_timer_compare(&sync_toggle_timer_instance, NRF_TIMER_CC_CHANNEL1, desired_ticks, true);
IRQ_CONNECT(NRFX_IRQ_NUMBER_GET(NRF_TIMER_INST_GET(SYNC_TOGGLE_TIMER_INSTANCE_NUMBER)), IRQ_PRIO_LOWEST,
NRFX_TIMER_INST_HANDLER_GET(SYNC_TOGGLE_TIMER_INSTANCE_NUMBER), 0, 0);
nrfx_timer_enable(&sync_toggle_timer_instance);
/* incoming events and data from the controller */
static K_FIFO_DEFINE(rx_queue);
int err;
LOG_DBG("Start");
__ASSERT(hci_uart_dev, "HCI UART device is NULL");
if (!device_is_ready(gmap_uart_dev)) {
printk("GMAP UART device is not ready\n");
return -1;
}
__ASSERT(hci_uart_dev, "UART device is NULL");
/* Enable the raw interface, this will in turn open the HCI driver */
bt_enable_raw(&rx_queue);
@@ -613,6 +491,10 @@ int main(void)
}
}
#if DT_NODE_HAS_STATUS(ALTERNATE_TOGGLE_GPIO, okay)
gpio_pin_configure_dt(&alternate_toggle_pin, GPIO_OUTPUT_INACTIVE);
#endif
#ifdef CONFIG_AUDIO_SYNC_TIMER_USES_RTC
/* Register iso_timesync command */
static struct bt_hci_raw_cmd_ext cmd_list = {
@@ -625,10 +507,6 @@ int main(void)
gpio_pin_configure_dt(&timesync_pin, GPIO_OUTPUT_INACTIVE);
#endif
#if DT_NODE_HAS_STATUS(TIMESYNC_GPIO, okay)
gpio_pin_configure_dt(&alternate_toggle_pin, GPIO_OUTPUT_INACTIVE);
#endif
bt_hci_raw_cmd_ext_register(&cmd_list, 1);
#endif
@@ -640,125 +518,20 @@ int main(void)
NULL, NULL, NULL, K_PRIO_COOP(7), 0, K_NO_WAIT);
k_thread_name_set(&tx_thread_data, "HCI uart TX");
while (1) {
#if 0
while (1) {
uint8_t c = 'A';
uart_fifo_fill(hci_uart_dev, &c, 1);
uart_irq_tx_enable(hci_uart_dev);
k_sleep( K_MSEC(100) );
}
return 0;
#endif
while (1) {
struct net_buf *buf;
buf = net_buf_get(&rx_queue, K_FOREVER);
#if DT_NODE_HAS_STATUS(TIMESYNC_GPIO, okay)
// ISO RX Measurement Code
const uint8_t * packet = buf->data;
if (packet[0] == H4_ISO){
// TODO: check if packets contains timestamp. all Controller -> Host packets have it so far
// get rx timestamp = sdu sync reference: Packet Type (1) | ISO Header (4) | Timestamp (if TS flag is set)
const uint32_t timestamp_sdu_sync_reference_us = little_endian_read_32(packet, 5);
// ignore empty ISO packets
const uint16_t data_total_length = little_endian_read_16(packet, 3);
if (data_total_length > 8) {
// get current Bluetooth time + toggle GPIO
const uint32_t bluetooth_time_us = toggle_and_get_bluetooth_time_us();
// calculate time of toggle relative to sdu sync reference usually
// - negative for UGT->UGG and
// - positive in UGT->UGG direction)
const int32_t delta_us = (int32_t)(bluetooth_time_us - timestamp_sdu_sync_reference_us);
// convert to string and send over UART and RTT
char delta_string[15];
const uint8_t first_payload_byte = packet[13];
snprintf(delta_string, sizeof(delta_string), "R%+06d@%02X!", delta_us,first_payload_byte);
for (size_t i = 0; delta_string[i] != '\0'; i++) {
uart_poll_out(gmap_uart_dev, delta_string[i]);
}
LOG_INF("Toggle %8u - SDU Sync Reference %8u -> delta %s", bluetooth_time_us, timestamp_sdu_sync_reference_us, delta_string);
}
}
if (packet[0] == H4_EVT) {
if (packet[1] == 0x0e) {
uint16_t opcode = little_endian_read_16(packet, 4);
const uint16_t hci_opcode_reset = 0x0c03;;
if (opcode == hci_opcode_reset) {
LOG_INF("HCI Reset");
last_sdu_sync_ref_us = 0;
sdu_interval_us = 0;
}
const uint16_t hci_opcode_le_read_tx_iso_sync = 0x2061;
if (opcode == hci_opcode_le_read_tx_iso_sync) {
const uint8_t * return_params = &packet[6];
// get current Bluetooth time + toggle GPIO
const uint32_t bluetooth_time_us = toggle_and_get_bluetooth_time_us();
// get current local time
const uint32_t local_time_us = get_local_time_us();
// status: 0
uint16_t handle = little_endian_read_16(return_params, 1);
ARG_UNUSED(handle);
// get packet sequence number (assuming counter == packet_sequence_number & 0xff)
const uint16_t packet_sequence_number = little_endian_read_16(return_params, 3);
// get tx timestamp = sdu sync reference: Packet Type (1) | ISO Header (4) | Timestamp (if TS flag is set)
const uint32_t iso_tx_us = little_endian_read_32(return_params, 5);
// get SDU interval based on sdu sync ref interval (round to the next 100 us)
if (last_sdu_sync_ref_us > 0) {
const uint32_t sdu_interval_reported_us = iso_tx_us - last_sdu_sync_ref_us;
sdu_interval_us = ((sdu_interval_reported_us + 50) / 100 ) * 100;
}
last_sdu_sync_ref_us = iso_tx_us;
// calculate time of toggle relative to sdu sync reference (usually negative as the packet is received before it should be played)
const int32_t delta_us = (int32_t)(bluetooth_time_us - iso_tx_us);
// convert to string and send over UART and RTT
char delta_string[15];
snprintf(delta_string, sizeof(delta_string), "T%+06d@%02X!", delta_us,packet_sequence_number & 0xff);
for (size_t i = 0; delta_string[i] != '\0'; i++) {
uart_poll_out(gmap_uart_dev, delta_string[i]);
}
// LOG_INF("Toggle %8u - TX %8u - %02X-> delta %s", bluetooth_time_us, iso_tx_us, packet_sequence_number, delta_string);
#ifdef PERIPHERAL_TO_CENTRAL_AUDIO_IN_TO_SDU_SYNC_REF
// schedule Audio In to Sync Ref for Peripheral to Central direction
if (sdu_interval_us > 0) {
if (alternate_toggle_state == ALTERNATE_TOGGLE_STATE_IDLE) {
// note: for Peripheral to Central, the SDU Sync Ref matches the ISO TX Timestamp
// calculate next audio in time such that:
// - it's in the future
// - audio_in_us + presentatioon time = sdu_sync_ref
uint32_t audio_in_us = iso_tx_us - PRESENTATION_TIME_US;
while (audio_in_us < (bluetooth_time_us + 10000)) {
audio_in_us += sdu_interval_us;
}
// convert into local time reference
const uint32_t rise_us = local_time_us + (audio_in_us - bluetooth_time_us);
const uint32_t fall_us = rise_us + PRESENTATION_TIME_US;
// LOG_INF("bt %u, tx %u, audio in %u // sdu %u // now %u, rise %u",
// bluetooth_time_us, iso_tx_us, audio_in_us,
// sdu_interval_us,
// local_time_us, rise_us);
alternate_toggle_setup(rise_us, fall_us);
}
}
#endif
}
}
}
#endif
buf = k_fifo_get(&rx_queue, K_FOREVER);
err = h4_send(buf);
if (err) {
LOG_ERR("Failed to send");