Add Phase 1 UART service foundation

Add versioned NVS-backed configuration, buffered UART1 I/O, modem
monitoring, counters, and serial console controls. Coordinate UART1
ownership with Phase 0 diagnostics and document loopback verification.
This commit is contained in:
2026-08-22 23:23:13 +02:00
parent 126314a277
commit 535c27350d
13 changed files with 1961 additions and 30 deletions
+720
View File
@@ -0,0 +1,720 @@
#include "serial_service.h"
#include <stdatomic.h>
#include <string.h>
#include "board_pins.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_check.h"
#include "esp_log.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/stream_buffer.h"
#include "freertos/task.h"
#include "rs232_hw_test.h"
#include "rs232_port_owner.h"
#define SERIAL_UART_RX_RING_SIZE 8192
#define SERIAL_UART_EVENT_QUEUE_SIZE 64
#define SERIAL_RX_STREAM_SIZE 16384
#define SERIAL_TX_STREAM_SIZE 8192
#define SERIAL_IO_CHUNK_SIZE 256
#define SERIAL_TASK_STACK_SIZE 4096
#define SERIAL_TASK_PRIORITY 10
#define SERIAL_TASK_IDLE_POLL_MS 50
#define SERIAL_TASK_TX_POLL_MS 10
#define SERIAL_STOP_TIMEOUT_MS 1000
static const char *TAG = "serial_service";
static SemaphoreHandle_t s_state_mutex;
static SemaphoreHandle_t s_task_stopped;
static StreamBufferHandle_t s_rx_stream;
static StreamBufferHandle_t s_tx_stream;
static QueueHandle_t s_uart_event_queue;
static TaskHandle_t s_event_task;
static portMUX_TYPE s_counter_lock = portMUX_INITIALIZER_UNLOCKED;
static serial_config_t s_config;
static serial_modem_state_t s_modem_state;
static serial_service_counters_t s_counters;
static bool s_initialized;
static atomic_bool s_running;
static atomic_bool s_stop_requested;
static atomic_size_t s_tx_task_pending;
static bool s_session_active;
static bool s_static_mode_safe;
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
{
TickType_t ticks = pdMS_TO_TICKS(milliseconds);
return (milliseconds > 0 && ticks == 0) ? 1 : ticks;
}
static void add_counter(uint64_t *counter, uint64_t amount)
{
taskENTER_CRITICAL(&s_counter_lock);
*counter += amount;
taskEXIT_CRITICAL(&s_counter_lock);
}
static serial_modem_state_t read_modem_state(void)
{
return (serial_modem_state_t) {
/* MAX3243 receiver outputs are low when modem-control inputs assert. */
.dcd = gpio_get_level(RS232_DCD_GPIO) == 0,
.dsr = gpio_get_level(RS232_DSR_GPIO) == 0,
.cts = gpio_get_level(RS232_CTS_GPIO) == 0,
.ri = gpio_get_level(RS232_RI_GPIO) == 0,
.valid = gpio_get_level(RS232_VALID_GPIO) != 0,
};
}
static void poll_modem_state(void)
{
serial_modem_state_t current = read_modem_state();
taskENTER_CRITICAL(&s_counter_lock);
if (current.dcd != s_modem_state.dcd) {
++s_counters.dcd_transitions;
}
if (current.dsr != s_modem_state.dsr) {
++s_counters.dsr_transitions;
}
if (current.cts != s_modem_state.cts) {
++s_counters.cts_transitions;
}
if (current.ri != s_modem_state.ri) {
++s_counters.ri_transitions;
}
if (current.valid != s_modem_state.valid) {
++s_counters.valid_transitions;
}
s_modem_state = current;
taskEXIT_CRITICAL(&s_counter_lock);
}
static bool configured_dtr_active(void)
{
switch (s_config.dtr_behavior) {
case SERIAL_CONFIG_DTR_ACTIVE:
return true;
case SERIAL_CONFIG_DTR_ON_CONNECT:
return s_session_active;
case SERIAL_CONFIG_DTR_INACTIVE:
default:
return false;
}
}
static esp_err_t prepare_gpio_for_uart(void)
{
s_static_mode_safe = false;
/* Keep every RS-232 driver disabled while GPIO-matrix routing changes. */
ESP_RETURN_ON_ERROR(gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0), TAG, "Shut down MAX3243");
const gpio_config_t shutdown_config = {
.pin_bit_mask = 1ULL << RS232_FORCE_OFF_N_GPIO,
.mode = GPIO_MODE_INPUT_OUTPUT_OD,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&shutdown_config), TAG, "Configure OFF GPIO");
/*
* TX and RTS start in their inactive logic-1 state. DTR follows policy,
* where logic 0 is the asserted RS-232 modem-control state.
*/
ESP_RETURN_ON_ERROR(gpio_set_level(RS232_TX_GPIO, 1), TAG, "Set TX idle latch");
ESP_RETURN_ON_ERROR(gpio_set_level(RS232_RTS_GPIO, 1), TAG, "Set RTS idle latch");
ESP_RETURN_ON_ERROR(
gpio_set_level(RS232_DTR_GPIO, configured_dtr_active() ? 0 : 1),
TAG,
"Set DTR policy latch");
const gpio_config_t output_config = {
.pin_bit_mask = (1ULL << RS232_TX_GPIO) |
(1ULL << RS232_RTS_GPIO) |
(1ULL << RS232_DTR_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&output_config), TAG, "Configure RS-232 outputs");
const gpio_config_t input_config = {
.pin_bit_mask = (1ULL << RS232_RX_GPIO) |
(1ULL << RS232_CTS_GPIO) |
(1ULL << RS232_DSR_GPIO) |
(1ULL << RS232_DCD_GPIO) |
(1ULL << RS232_RI_GPIO) |
(1ULL << RS232_VALID_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
return gpio_config(&input_config);
}
static void enqueue_received_data(const uint8_t *data, size_t size)
{
size_t accepted = xStreamBufferSend(s_rx_stream, data, size, 0);
add_counter(&s_counters.rx_bytes, size);
if (accepted < size) {
add_counter(&s_counters.rx_dropped_bytes, size - accepted);
}
}
static void drain_uart_receive_ring(size_t suggested_size)
{
uint8_t buffer[SERIAL_IO_CHUNK_SIZE];
size_t remaining = suggested_size;
while (!s_stop_requested) {
size_t request = sizeof(buffer);
if (remaining > 0 && remaining < request) {
request = remaining;
}
int count = uart_read_bytes(RS232_UART_PORT, buffer, request, 0);
if (count <= 0) {
break;
}
enqueue_received_data(buffer, (size_t)count);
if (remaining > 0) {
if ((size_t)count >= remaining) {
remaining = 0;
} else {
remaining -= (size_t)count;
}
}
/* For buffer-full recovery, continue until ring and stashed data are drained. */
if (suggested_size > 0 && remaining == 0) {
break;
}
}
}
static void handle_uart_event(const uart_event_t *event)
{
switch (event->type) {
case UART_DATA:
drain_uart_receive_ring(event->size);
break;
case UART_BUFFER_FULL:
add_counter(&s_counters.buffer_full_events, 1);
drain_uart_receive_ring(0);
break;
case UART_FIFO_OVF:
add_counter(&s_counters.fifo_overflows, 1);
uart_flush_input(RS232_UART_PORT);
xQueueReset(s_uart_event_queue);
break;
case UART_FRAME_ERR:
add_counter(&s_counters.frame_errors, 1);
break;
case UART_PARITY_ERR:
add_counter(&s_counters.parity_errors, 1);
break;
case UART_BREAK:
case UART_DATA_BREAK:
add_counter(&s_counters.breaks, 1);
break;
default:
break;
}
}
static void serial_event_task(void *context)
{
(void)context;
uint8_t pending[SERIAL_IO_CHUNK_SIZE];
size_t pending_size = 0;
size_t pending_offset = 0;
while (!s_stop_requested) {
if (pending_offset == pending_size) {
pending_size = xStreamBufferReceive(s_tx_stream, pending, sizeof(pending), 0);
pending_offset = 0;
s_tx_task_pending = pending_size;
}
if (pending_offset < pending_size) {
/*
* uart_tx_chars is nonblocking and therefore remains safe when CTS
* is deasserted indefinitely. Unsent bytes stay in this task's
* local pending buffer until hardware FIFO space is available.
*/
int sent = uart_tx_chars(
RS232_UART_PORT,
(const char *)(pending + pending_offset),
pending_size - pending_offset);
if (sent > 0) {
pending_offset += (size_t)sent;
s_tx_task_pending = pending_size - pending_offset;
add_counter(&s_counters.tx_sent_to_uart_bytes, (uint64_t)sent);
}
}
TickType_t event_wait = milliseconds_to_ticks(
pending_offset < pending_size || xStreamBufferBytesAvailable(s_tx_stream) > 0
? SERIAL_TASK_TX_POLL_MS
: SERIAL_TASK_IDLE_POLL_MS);
uart_event_t event;
if (xQueueReceive(s_uart_event_queue, &event, event_wait) == pdTRUE) {
handle_uart_event(&event);
}
/* Event-queue notifications can be dropped; the ring length is authoritative. */
drain_uart_receive_ring(0);
poll_modem_state();
}
size_t discarded = (pending_size - pending_offset) +
xStreamBufferBytesAvailable(s_tx_stream);
if (discarded > 0) {
add_counter(&s_counters.tx_dropped_bytes, discarded);
}
s_tx_task_pending = 0;
s_event_task = NULL;
xSemaphoreGive(s_task_stopped);
vTaskDelete(NULL);
}
static esp_err_t restore_static_mode_or_fault(void)
{
esp_err_t result = rs232_hw_test_init();
s_static_mode_safe = result == ESP_OK;
if (result != ESP_OK) {
gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
ESP_LOGE(TAG, "Static GPIO restoration failed; MAX3243 disabled and port faulted");
}
return result;
}
static esp_err_t cleanup_failed_start(
bool driver_installed,
bool restore_static_mode)
{
esp_err_t result = gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
if (driver_installed) {
esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT);
if (delete_error != ESP_OK) {
s_running = false;
s_stop_requested = true;
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
ESP_LOGE(TAG, "Could not delete UART1 after failed start; port faulted");
return delete_error;
}
}
s_uart_event_queue = NULL;
s_running = false;
s_stop_requested = false;
if (restore_static_mode) {
esp_err_t restore_error = restore_static_mode_or_fault();
if (result == ESP_OK) {
result = restore_error;
}
}
return result;
}
static esp_err_t start_locked(bool restore_static_on_failure)
{
if (s_running) {
return ESP_ERR_INVALID_STATE;
}
uart_config_t uart_config;
ESP_RETURN_ON_ERROR(serial_config_to_uart_config(&s_config, &uart_config), TAG, "Convert serial config");
bool driver_installed = false;
esp_err_t err = prepare_gpio_for_uart();
if (err == ESP_OK) {
err = uart_driver_install(
RS232_UART_PORT,
SERIAL_UART_RX_RING_SIZE,
0,
SERIAL_UART_EVENT_QUEUE_SIZE,
&s_uart_event_queue,
0);
driver_installed = err == ESP_OK;
}
if (err == ESP_OK) {
err = uart_param_config(RS232_UART_PORT, &uart_config);
}
if (err == ESP_OK) {
err = uart_set_line_inverse(RS232_UART_PORT, 0);
}
bool hardware_flow = s_config.flow_control == SERIAL_CONFIG_FLOW_CONTROL_RTS_CTS;
if (err == ESP_OK) {
err = uart_set_pin(
RS232_UART_PORT,
RS232_TX_GPIO,
RS232_RX_GPIO,
hardware_flow ? RS232_RTS_GPIO : UART_PIN_NO_CHANGE,
hardware_flow ? RS232_CTS_GPIO : UART_PIN_NO_CHANGE);
}
if (err == ESP_OK) {
err = uart_set_rx_full_threshold(RS232_UART_PORT, 64);
}
if (err != ESP_OK) {
esp_err_t cleanup_error = cleanup_failed_start(driver_installed, restore_static_on_failure);
return cleanup_error == ESP_OK ? err : cleanup_error;
}
if (xStreamBufferReset(s_rx_stream) != pdPASS ||
xStreamBufferReset(s_tx_stream) != pdPASS) {
cleanup_failed_start(driver_installed, restore_static_on_failure);
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_task_stopped, 0);
s_stop_requested = false;
s_tx_task_pending = 0;
err = gpio_set_level(RS232_FORCE_OFF_N_GPIO, 1);
if (err != ESP_OK) {
esp_err_t cleanup_error = cleanup_failed_start(driver_installed, restore_static_on_failure);
return cleanup_error == ESP_OK ? err : cleanup_error;
}
vTaskDelay(pdMS_TO_TICKS(20));
serial_modem_state_t initial_modem_state = read_modem_state();
taskENTER_CRITICAL(&s_counter_lock);
s_modem_state = initial_modem_state;
taskEXIT_CRITICAL(&s_counter_lock);
if (xTaskCreate(
serial_event_task,
"serial_uart",
SERIAL_TASK_STACK_SIZE,
NULL,
SERIAL_TASK_PRIORITY,
&s_event_task) != pdPASS) {
esp_err_t cleanup_error = cleanup_failed_start(driver_installed, restore_static_on_failure);
return cleanup_error == ESP_OK ? ESP_ERR_NO_MEM : cleanup_error;
}
s_running = true;
ESP_LOGI(
TAG,
"UART1 started: baud=%lu, data-bits=%s, parity=%s, stop-bits=%s, flow=%s, DTR=%s",
(unsigned long)s_config.baud_rate,
serial_config_data_bits_to_string(s_config.data_bits),
serial_config_parity_to_string(s_config.parity),
serial_config_stop_bits_to_string(s_config.stop_bits),
serial_config_flow_control_to_string(s_config.flow_control),
serial_config_dtr_behavior_to_string(s_config.dtr_behavior));
return ESP_OK;
}
static esp_err_t stop_locked(bool restore_static_mode)
{
if (!s_running) {
return ESP_OK;
}
s_stop_requested = true;
if (xSemaphoreTake(s_task_stopped, pdMS_TO_TICKS(SERIAL_STOP_TIMEOUT_MS)) != pdTRUE) {
ESP_LOGE(TAG, "UART service task did not quiesce within %d ms", SERIAL_STOP_TIMEOUT_MS);
return ESP_ERR_TIMEOUT;
}
size_t unread_rx = xStreamBufferBytesAvailable(s_rx_stream);
if (unread_rx > 0) {
add_counter(&s_counters.rx_dropped_bytes, unread_rx);
}
esp_err_t result = gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT);
if (delete_error != ESP_OK) {
s_running = false;
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
ESP_LOGE(TAG, "Could not delete UART1; MAX3243 remains disabled and port is faulted");
return delete_error;
}
s_uart_event_queue = NULL;
s_running = false;
s_stop_requested = false;
if (xStreamBufferReset(s_rx_stream) != pdPASS ||
xStreamBufferReset(s_tx_stream) != pdPASS) {
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
return ESP_ERR_INVALID_STATE;
}
if (restore_static_mode) {
esp_err_t restore_error = restore_static_mode_or_fault();
if (result == ESP_OK) {
result = restore_error;
}
}
ESP_LOGI(TAG, "UART1 stopped%s", restore_static_mode ? "; GPIOs restored to static idle mode" : " for reconfiguration");
return result;
}
esp_err_t serial_service_init(const serial_config_t *initial_config)
{
if (s_initialized) {
return ESP_ERR_INVALID_STATE;
}
ESP_RETURN_ON_ERROR(serial_config_validate(initial_config), TAG, "Validate initial config");
s_state_mutex = xSemaphoreCreateMutex();
s_task_stopped = xSemaphoreCreateBinary();
s_rx_stream = xStreamBufferCreate(SERIAL_RX_STREAM_SIZE, 1);
s_tx_stream = xStreamBufferCreate(SERIAL_TX_STREAM_SIZE, 1);
if (s_state_mutex == NULL || s_task_stopped == NULL ||
s_rx_stream == NULL || s_tx_stream == NULL) {
if (s_state_mutex != NULL) {
vSemaphoreDelete(s_state_mutex);
}
if (s_task_stopped != NULL) {
vSemaphoreDelete(s_task_stopped);
}
if (s_rx_stream != NULL) {
vStreamBufferDelete(s_rx_stream);
}
if (s_tx_stream != NULL) {
vStreamBufferDelete(s_tx_stream);
}
s_state_mutex = NULL;
s_task_stopped = NULL;
s_rx_stream = NULL;
s_tx_stream = NULL;
return ESP_ERR_NO_MEM;
}
s_config = *initial_config;
s_static_mode_safe = true;
s_initialized = true;
return ESP_OK;
}
esp_err_t serial_service_start(void)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
esp_err_t result = rs232_port_claim(RS232_PORT_OWNER_SERVICE);
if (result == ESP_OK) {
result = start_locked(true);
if (result != ESP_OK &&
rs232_port_get_owner() == RS232_PORT_OWNER_SERVICE &&
!uart_is_driver_installed(RS232_UART_PORT)) {
if (s_static_mode_safe) {
rs232_port_release(RS232_PORT_OWNER_SERVICE);
} else {
gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
}
}
}
xSemaphoreGive(s_state_mutex);
return result;
}
esp_err_t serial_service_stop(void)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
esp_err_t result = stop_locked(true);
if (!s_running &&
!uart_is_driver_installed(RS232_UART_PORT) &&
rs232_port_get_owner() == RS232_PORT_OWNER_SERVICE) {
if (s_static_mode_safe) {
esp_err_t release_error = rs232_port_release(RS232_PORT_OWNER_SERVICE);
if (result == ESP_OK) {
result = release_error;
}
} else {
gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
}
}
xSemaphoreGive(s_state_mutex);
return result;
}
bool serial_service_is_running(void)
{
return atomic_load(&s_running);
}
esp_err_t serial_service_apply_config(const serial_config_t *config)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
ESP_RETURN_ON_ERROR(serial_config_validate(config), TAG, "Validate new config");
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
serial_config_t previous = s_config;
bool restart = s_running;
esp_err_t result = ESP_OK;
if (restart) {
result = stop_locked(false);
}
if (result == ESP_OK) {
s_config = *config;
if (restart) {
result = start_locked(false);
if (result != ESP_OK &&
rs232_port_get_owner() == RS232_PORT_OWNER_SERVICE) {
esp_err_t original_error = result;
ESP_LOGW(TAG, "New configuration failed; restoring previous UART configuration");
s_config = previous;
esp_err_t rollback_error = start_locked(false);
if (rollback_error != ESP_OK) {
ESP_LOGE(TAG, "Could not restore previous UART configuration: %s", esp_err_to_name(rollback_error));
if (!uart_is_driver_installed(RS232_UART_PORT)) {
esp_err_t restore_error = restore_static_mode_or_fault();
if (restore_error == ESP_OK) {
rs232_port_release(RS232_PORT_OWNER_SERVICE);
}
} else {
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
}
}
result = original_error;
}
}
} else if (!uart_is_driver_installed(RS232_UART_PORT) &&
rs232_port_get_owner() == RS232_PORT_OWNER_SERVICE) {
/* A failed stop that removed UART1 releases ownership only after safe restoration. */
esp_err_t restore_error = restore_static_mode_or_fault();
if (restore_error == ESP_OK) {
rs232_port_release(RS232_PORT_OWNER_SERVICE);
}
}
xSemaphoreGive(s_state_mutex);
return result;
}
esp_err_t serial_service_get_config(serial_config_t *config)
{
if (!s_initialized || config == NULL) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
*config = s_config;
xSemaphoreGive(s_state_mutex);
return ESP_OK;
}
size_t serial_service_read(uint8_t *data, size_t size)
{
if (!s_initialized || data == NULL || size == 0) {
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
size_t received = 0;
if (s_running && !s_stop_requested) {
received = xStreamBufferReceive(s_rx_stream, data, size, 0);
}
xSemaphoreGive(s_state_mutex);
return received;
}
size_t serial_service_write(const uint8_t *data, size_t size)
{
if (!s_initialized || data == NULL || size == 0) {
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
size_t accepted = 0;
if (s_running && !s_stop_requested) {
accepted = xStreamBufferSend(s_tx_stream, data, size, 0);
add_counter(&s_counters.tx_queued_bytes, accepted);
if (accepted < size) {
add_counter(&s_counters.tx_dropped_bytes, size - accepted);
}
}
xSemaphoreGive(s_state_mutex);
return accepted;
}
size_t serial_service_rx_available(void)
{
if (!s_initialized) {
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
size_t available = xStreamBufferBytesAvailable(s_rx_stream);
xSemaphoreGive(s_state_mutex);
return available;
}
size_t serial_service_tx_pending(void)
{
if (!s_initialized) {
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
size_t pending = xStreamBufferBytesAvailable(s_tx_stream) +
atomic_load(&s_tx_task_pending);
xSemaphoreGive(s_state_mutex);
return pending;
}
esp_err_t serial_service_set_session_active(bool active)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
s_session_active = active;
esp_err_t result = ESP_OK;
if (s_running && s_config.dtr_behavior == SERIAL_CONFIG_DTR_ON_CONNECT) {
result = gpio_set_level(RS232_DTR_GPIO, active ? 0 : 1);
}
xSemaphoreGive(s_state_mutex);
return result;
}
void serial_service_get_modem_state(serial_modem_state_t *state)
{
if (state == NULL) {
return;
}
taskENTER_CRITICAL(&s_counter_lock);
*state = s_modem_state;
taskEXIT_CRITICAL(&s_counter_lock);
}
void serial_service_get_counters(serial_service_counters_t *counters)
{
if (counters == NULL) {
return;
}
taskENTER_CRITICAL(&s_counter_lock);
*counters = s_counters;
taskEXIT_CRITICAL(&s_counter_lock);
}
void serial_service_clear_counters(void)
{
taskENTER_CRITICAL(&s_counter_lock);
memset(&s_counters, 0, sizeof(s_counters));
taskEXIT_CRITICAL(&s_counter_lock);
}