pebble/src/fw/drivers/accessory.c

577 lines
20 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "drivers/accessory.h"
#include "board/board.h"
#include "console/console_internal.h"
#include "console/serial_console.h"
#include "console/prompt.h"
#include "drivers/dma.h"
#include "drivers/gpio.h"
#include "drivers/exti.h"
#include "drivers/periph_config.h"
#include "drivers/uart.h"
#include "kernel/util/stop.h"
#include "mcu/interrupts.h"
#include "os/mutex.h"
#include "os/tick.h"
#include "services/common/new_timer/new_timer.h"
#include "services/common/system_task.h"
#include "system/logging.h"
#include "system/passert.h"
#include "kernel/util/delay.h"
#include "kernel/util/sleep.h"
#include "util/attributes.h"
#include "util/likely.h"
#include "util/size.h"
#include "FreeRTOS.h" /* FreeRTOS Kernel Prototypes/Constants. */
#include "semphr.h"
#define STM32F4_COMPATIBLE
#define STM32F7_COMPATIBLE
#include <mcu.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
//! The default baudrate for the accessory UART.
#define DEFAULT_BAUD AccessoryBaud115200
//! How long each interval should be in milliseconds.
#define ACCESSORY_STOP_INTERVAL_PERIOD_MS (250)
//! How many intervals we should wait outside of stop mode when we first see any noise on the
//! serial port.
#define ACCESSORY_INITIAL_STOP_INTERVALS (500 / ACCESSORY_STOP_INTERVAL_PERIOD_MS)
//! How many intervals we should wait outside of stop mode when we first see valid data on the
//! serial port.
#define ACCESSORY_VALID_DATA_STOP_INTERVALS (3000 / ACCESSORY_STOP_INTERVAL_PERIOD_MS)
//! How many bytes of send history to keep. This needs to be 3 bytes because the TX buffer will be
//! moved into the shift register (with a new byte being loaded into the buffer) before we receive
//! the byte we previously sent. So, when we receive a byte, we will have sent 2 more bytes by then.
#define SEND_HISTORY_LENGTH (1)
//! Within accessory_send_stream(), how long we wait for a byte to be sent before timing-out.
#define SEND_BYTE_TIMEOUT_MS (100)
//! We DMA into this buffer as a circular buffer
#define RX_BUFFER_LENGTH (200)
static uint8_t DMA_BSS s_rx_buffer[RX_BUFFER_LENGTH];
//! The current baud rate
static uint32_t s_baudrate;
//! Whether or not the accessory power is enabled
static bool s_power_enabled;
//! Whether or not we are in input mode (receiving)
static bool s_input_enabled;
//! We'll store up to the last 3 bytes which were sent for detecting bus contention
typedef struct {
uint8_t data;
bool has_data;
} SendHistory;
static volatile SendHistory s_send_history;
//! Flag which states whether or not we've detected bus contention since last disabling input
static volatile bool s_bus_contention_detected;
//! Whether or not we sent data since disabling input
static bool s_sent_data;
//! The callback for a stream being sent via accessory_send_stream()
static volatile AccessoryDataStreamCallback s_stream_cb;
//! Context passed to accessory_send_stream()
static void *s_stream_context;
//! Semaphore used for accessory_send_stream()
static SemaphoreHandle_t s_send_semaphore;
//! Mutex used for accessory_block() / accessory_unblock()
static PebbleRecursiveMutex *s_blocked_lock;
//! Used to track whether or not the accessory_send_stream callback sent a new byte via
//! accessory_send_byte()
static volatile bool s_did_send_byte;
//! Whether or not we should use DMA for receiving
static bool s_use_dma;
//! Whether or not DMA is enabled
static bool s_dma_enabled;
//! Used by accessory_send_stream() to track whether or not we've sent a byte recently
static volatile bool s_has_sent_byte;
//! We need to disable stop mode in order to receive data on the accessory connector. To do this,
//! we set up an exti that kicks us out of stop mode when data is seen. Then, we schedule a timer
//! to check for additional data being seen on the connector. If we go 5 seconds without seeing
//! data, we can go back into stop mode.
static struct {
//! If the accessory connector is currently active...
bool active;
//! The timer that will fire once a second while we're active
TimerID timer;
//! How many intervals have gone by without data being seen
int intervals_without_data;
//! How many intervals we should wait for without data before going back into stop mode
int max_intervals_without_data;
//! If we saw data on the connector since the last time the timer fired.
bool data_seen_this_interval;
} s_stop_mode_monitor;
static bool prv_rx_irq_handler(UARTDevice *dev, uint8_t data, const UARTRXErrorFlags *err_flags);
static bool prv_tx_irq_handler(UARTDevice *dev);
static void prv_lock(void) {
if (mcu_state_is_isr()) {
// assume we're in an ISR for the UART and don't need to worry about being blocked
return;
}
mutex_lock_recursive(s_blocked_lock);
}
static void prv_unlock(void) {
if (mcu_state_is_isr()) {
// assume we're in an ISR for the UART and don't need to worry about being blocked
return;
}
mutex_unlock_recursive(s_blocked_lock);
}
static void prv_enable_dma(void) {
PBL_ASSERTN(!s_dma_enabled);
s_dma_enabled = true;
uart_start_rx_dma(ACCESSORY_UART, s_rx_buffer, sizeof(s_rx_buffer));
}
static void prv_disable_dma(void) {
if (!s_dma_enabled) {
return;
}
s_dma_enabled = false;
uart_stop_rx_dma(ACCESSORY_UART);
}
//! The interval timer callback.
static void prv_timer_interval_expired_cb(void *data) {
if (!s_stop_mode_monitor.data_seen_this_interval) {
// The accessory connector didn't have any data since the last time this callback fired.
++s_stop_mode_monitor.intervals_without_data;
if (s_stop_mode_monitor.intervals_without_data >=
s_stop_mode_monitor.max_intervals_without_data) {
// Enough intervals have passed and we should now turn stop mode back on.
stop_mode_enable(InhibitorAccessory);
s_stop_mode_monitor.active = false;
s_stop_mode_monitor.intervals_without_data = 0;
s_stop_mode_monitor.max_intervals_without_data = 0;
new_timer_stop(s_stop_mode_monitor.timer);
}
} else {
// Data was seen, reset the interval counter
s_stop_mode_monitor.intervals_without_data = 0;
}
// Regardless of what happened, this interval is over and should be reset
s_stop_mode_monitor.data_seen_this_interval = false;
}
static void prv_start_timer_cb(void *context) {
new_timer_start(s_stop_mode_monitor.timer, ACCESSORY_STOP_INTERVAL_PERIOD_MS,
prv_timer_interval_expired_cb, NULL, TIMER_START_FLAG_REPEATING);
}
//! Callback run whenever the EXTI fires
static void prv_exti_cb(bool *should_context_switch) {
if (!s_stop_mode_monitor.active) {
// First time seeing data, let's go active
s_stop_mode_monitor.active = true;
s_stop_mode_monitor.intervals_without_data = 0;
s_stop_mode_monitor.max_intervals_without_data = ACCESSORY_INITIAL_STOP_INTERVALS;
stop_mode_disable(InhibitorAccessory);
// Need to flip tasks because we can't start a timer from an interrupt
system_task_add_callback_from_isr(prv_start_timer_cb, NULL, should_context_switch);
}
s_stop_mode_monitor.data_seen_this_interval = true;
}
//! The UART peripheral only runs if the accessory is not in stop mode. We listen to the txrx
//! pin on the accessory connector and if we see anything we'll disable stop mode for a few
//! seconds to see if anyone has something to say.
static void prv_initialize_exti(void) {
s_stop_mode_monitor.timer = new_timer_create();
gpio_input_init(&BOARD_CONFIG_ACCESSORY.int_gpio);
exti_configure_pin(BOARD_CONFIG_ACCESSORY.exti, ExtiTrigger_Falling, prv_exti_cb);
exti_enable(BOARD_CONFIG_ACCESSORY.exti);
}
static void prv_initialize_uart(uint32_t baudrate) {
#if RECOVERY_FW
// In PRF / MFG, we'll have a strong (2k) external pull-up, so we should always be open-drain
const bool is_open_drain = true;
#else
// If we raise the baud rate above 115200 we need to configure as push-pull to ensure we are
// sufficiently driving the line. Ideally, the accessory would have a strong-enough pull-up, but
// now that we've released use of the accessory port via the smartstrap APIs, we can't easily
// change this.
const bool is_open_drain = (baudrate <= 115200);
#endif
s_baudrate = baudrate;
if (is_open_drain) {
uart_init_open_drain(ACCESSORY_UART);
} else {
uart_init(ACCESSORY_UART);
}
uart_set_rx_interrupt_handler(ACCESSORY_UART, prv_rx_irq_handler);
uart_set_tx_interrupt_handler(ACCESSORY_UART, prv_tx_irq_handler);
uart_set_baud_rate(ACCESSORY_UART, s_baudrate);
uart_set_rx_interrupt_enabled(ACCESSORY_UART, true);
}
static void prv_initialize_hardware(void) {
periph_config_acquire_lock();
gpio_output_init(&BOARD_CONFIG_ACCESSORY.power_en, GPIO_OType_PP, GPIO_Speed_2MHz);
gpio_output_set(&BOARD_CONFIG_ACCESSORY.power_en, false); // Turn power off
accessory_set_baudrate(DEFAULT_BAUD);
periph_config_release_lock();
prv_initialize_exti();
}
static void prv_set_baudrate(uint32_t baudrate, bool force_update) {
if ((baudrate != s_baudrate) || force_update) {
PBL_LOG(LOG_LEVEL_DEBUG, "Changing accessory connector baud rate to %"PRIu32, baudrate);
prv_initialize_uart(baudrate);
if (s_dma_enabled) {
// we need to reset DMA after resetting the UART
prv_disable_dma();
prv_enable_dma();
}
}
}
void accessory_init(void) {
s_send_semaphore = xSemaphoreCreateBinary();
xSemaphoreGive(s_send_semaphore);
s_blocked_lock = mutex_create_recursive();
prv_initialize_hardware();
accessory_set_power(false);
accessory_enable_input();
}
void accessory_block(void) {
prv_lock();
accessory_send_stream_stop();
uart_deinit(ACCESSORY_UART);
}
void accessory_unblock(void) {
// We want to restore the previous baudrate, but clear s_baudrate in order to force a complete
// re-init of the peripheral.
prv_set_baudrate(s_baudrate, true /* force_update */);
prv_unlock();
}
void accessory_send_byte(uint8_t data) {
// NOTE: this may be run within an ISR
prv_lock();
s_has_sent_byte = true;
s_did_send_byte = true;
PBL_ASSERTN(!s_input_enabled);
while (!(uart_is_tx_ready(ACCESSORY_UART))) continue;
// this section needs to be atomic since the UART IRQ also modifies these variables
portENTER_CRITICAL();
if (s_send_history.has_data) {
// The send buffer is full. This means that the receive interrupt hasn't fired to clear the
// buffer which indicates that there is bus contention preventing a stop bit from occuring.
s_bus_contention_detected = true;
} else {
s_send_history.data = data;
s_send_history.has_data = true;
}
portEXIT_CRITICAL();
uart_write_byte(ACCESSORY_UART, data);
s_sent_data = true;
prv_unlock();
}
void accessory_send_data(const uint8_t *data, size_t length) {
// NOTE: this may be run within an ISR
prv_lock();
// When sending data, we need to temporarily disable input, as there's only one data line for
// both directions and any data we send on that line will also be interpreted as data we can
// read. This means there's a bit of overhead for sending data as we have to also make sure
// we don't accidentally read it back. If you're going to be sending a large amount of data,
// calling accessory_disable_input before will give you a nice speed boost as we don't have
// to wait for it to be safe to turn the input back on after each byte.
const bool temporarily_disabled = s_input_enabled;
if (UNLIKELY(temporarily_disabled)) {
accessory_disable_input();
}
for (size_t i = 0; i < length; ++i) {
accessory_send_byte(data[i]);
}
if (UNLIKELY(temporarily_disabled)) {
accessory_enable_input();
}
prv_unlock();
}
bool accessory_send_stream(AccessoryDataStreamCallback stream_callback, void *context) {
bool success = true;
prv_lock();
PBL_ASSERTN(xSemaphoreTake(s_send_semaphore, portMAX_DELAY) == pdPASS);
PBL_ASSERTN(stream_callback != NULL);
PBL_ASSERTN(!s_input_enabled);
if (s_dma_enabled) {
uart_clear_rx_dma_buffer(ACCESSORY_UART);
}
s_stream_context = context;
s_stream_cb = stream_callback;
s_has_sent_byte = false;
uart_set_tx_interrupt_enabled(ACCESSORY_UART, true);
// Block until the sending is complete, but timeout if we aren't able to send a byte for a while.
while (xSemaphoreTake(s_send_semaphore, milliseconds_to_ticks(SEND_BYTE_TIMEOUT_MS)) != pdPASS) {
if (!s_has_sent_byte) {
// we haven't sent a byte in the last timeout period, so time out the whole send
s_stream_cb = NULL;
s_stream_context = NULL;
success = false;
PBL_LOG(LOG_LEVEL_ERROR, "Timed-out while sending");
break;
}
s_has_sent_byte = false;
}
xSemaphoreGive(s_send_semaphore);
prv_unlock();
return success;
}
void accessory_send_stream_stop(void) {
prv_lock();
if (s_stream_cb) {
// wait for any in-progress write to finish
PBL_ASSERTN(xSemaphoreTake(s_send_semaphore, portMAX_DELAY) == pdPASS);
xSemaphoreGive(s_send_semaphore);
}
uart_set_tx_interrupt_enabled(ACCESSORY_UART, false);
s_stream_cb = NULL;
s_stream_context = NULL;
prv_unlock();
}
void accessory_disable_input(void) {
// NOTE: This function may be called from an ISR
prv_lock();
PBL_ASSERTN(s_input_enabled);
s_input_enabled = false;
s_send_history.has_data = false;
s_bus_contention_detected = false;
prv_unlock();
}
void accessory_enable_input(void) {
// NOTE: This function may be called from an ISR
if (s_input_enabled) {
return;
}
prv_lock();
if (s_sent_data) {
// wait for the TC flag to be set
uart_wait_for_tx_complete(ACCESSORY_UART);
// wait a little for the lines to settle down
const uint32_t us_to_wait = (1000000 / s_baudrate) * 2;
delay_us(us_to_wait);
s_sent_data = false;
}
// Read data and throw it away to clear the state. We don't want to handle data we received
// while input was disabled
uart_read_byte(ACCESSORY_UART);
s_input_enabled = true;
prv_unlock();
}
void accessory_use_dma(bool use_dma) {
prv_lock();
s_use_dma = use_dma;
if (s_use_dma) {
prv_enable_dma();
} else {
prv_disable_dma();
}
prv_unlock();
}
bool accessory_bus_contention_detected(void) {
return s_bus_contention_detected;
}
static uint32_t prv_get_baudrate(AccessoryBaud baud_select) {
const uint32_t BAUDS[] = { 9600, 14400, 19200, 28800, 38400, 57600, 62500, 115200, 125000, 230400,
250000, 460800, 921600 };
_Static_assert(ARRAY_LENGTH(BAUDS) == AccessoryBaudInvalid,
"bauds table doesn't match up with AccessoryBaud enum");
return BAUDS[baud_select];
}
void accessory_set_baudrate(AccessoryBaud baud_select) {
prv_lock();
PBL_ASSERTN(baud_select < AccessoryBaudInvalid);
prv_set_baudrate(prv_get_baudrate(baud_select), false /* !force_update */);
prv_unlock();
}
void accessory_set_power(bool on) {
if (on == s_power_enabled) {
return;
}
PBL_LOG(LOG_LEVEL_DEBUG, "Setting accessory power %s", on?"on":"off");
s_power_enabled = on;
gpio_output_set(&BOARD_CONFIG_ACCESSORY.power_en, on);
}
bool accessory_is_present(void) {
accessory_set_power(true);
gpio_input_init_pull_up_down(&BOARD_CONFIG_ACCESSORY.int_gpio, GPIO_PuPd_DOWN);
// budget for a capacitance up to ~1uF and a resistance of 10kOhm
psleep(10);
bool result = (gpio_input_read(&BOARD_CONFIG_ACCESSORY.int_gpio) == SET);
gpio_input_init(&BOARD_CONFIG_ACCESSORY.int_gpio);
return result;
}
// ISRs
////////////////////////////////////////////////////////////////////
static bool prv_rx_irq_handler(UARTDevice *dev, uint8_t data, const UARTRXErrorFlags *err_flags) {
bool should_context_switch = false;
// We've now seen valid data on the serial port, make sure we stay out of stop mode for a
// longer period of time.
s_stop_mode_monitor.max_intervals_without_data = ACCESSORY_VALID_DATA_STOP_INTERVALS;
if (s_input_enabled) {
// we are receiving data from the accessory
if (!err_flags->framing_error) {
should_context_switch = accessory_manager_handle_character_from_isr((char)data);
} else if (data == 0x00) {
should_context_switch = accessory_manager_handle_break_from_isr();
}
} else {
// we are receiving data we just sent since the RX/TX lines are tied together
if (s_send_history.has_data) {
if (s_send_history.data != data) {
// The byte we are receiving doesn't match the next byte in the send queue.
s_bus_contention_detected = true;
}
s_send_history.has_data = false;
} else {
// We received a byte without sending and the input is not enabled. This typically indicates
// a race condition between when we disable input and start sending, or between when we
// finish sending and enable input. Either way, we can't trust this data so treat it as bus
// contention.
s_bus_contention_detected = true;
}
}
if (s_stream_cb) {
// enable the TXE interrupt for sending the next byte
uart_set_tx_interrupt_enabled(dev, true);
}
return should_context_switch;
}
static bool prv_tx_irq_handler(UARTDevice *dev) {
bool should_context_switch = false;
if (s_stream_cb && !s_send_history.has_data) {
s_did_send_byte = false;
if (s_stream_cb(s_stream_context)) {
// the callback MUST send a byte in order for this interrupt to trigger again
PBL_ASSERTN(s_did_send_byte);
} else {
// we're done sending
portBASE_TYPE was_higher_task_woken = pdFALSE;
xSemaphoreGiveFromISR(s_send_semaphore, &was_higher_task_woken);
should_context_switch = (was_higher_task_woken != pdFALSE);
uart_set_tx_interrupt_enabled(dev, false);
s_stream_cb = NULL;
s_stream_context = NULL;
}
} else {
// we haven't yet received back the byte we sent
uart_set_tx_interrupt_enabled(dev, false);
}
return should_context_switch;
}
// Commands
////////////////////////////////////////////////////////////////////
void command_accessory_power_set(const char *on) {
if (!strcmp(on, "on")) {
accessory_set_power(true);
} else if (!strcmp(on, "off")) {
accessory_set_power(false);
} else {
prompt_send_response("Usage: accessory power (on|off)");
}
}
static volatile int32_t s_num_test_bytes;
static bool prv_test_send_stream(void *context) {
accessory_send_byte((uint8_t)s_num_test_bytes);
if (accessory_bus_contention_detected()) {
return false;
}
return (--s_num_test_bytes > 0);
}
void command_accessory_stress_test(void) {
if (s_baudrate != prv_get_baudrate(DEFAULT_BAUD)) {
prompt_send_response("FAILED: accessory port is busy");
return;
}
// send 1 second worth of data
s_num_test_bytes = 46080;
accessory_use_dma(true);
accessory_set_baudrate(AccessoryBaud460800);
accessory_disable_input();
const bool success = accessory_send_stream(prv_test_send_stream, NULL);
accessory_enable_input();
accessory_set_baudrate(DEFAULT_BAUD);
accessory_use_dma(false);
char buffer[50];
if (!success) {
prompt_send_response_fmt(buffer, sizeof(buffer), "FAILED: send timed-out");
} else if (s_num_test_bytes == 0) {
prompt_send_response_fmt(buffer, sizeof(buffer), "PASS!");
} else {
prompt_send_response_fmt(buffer, sizeof(buffer), "FAILED: %"PRId32" bytes left!",
s_num_test_bytes);
}
}