Add UART multiplexer and INA226 monitoring
This commit is contained in:
@@ -24,3 +24,104 @@ config WATCH_WS2812_RMT_RESOLUTION
|
||||
10 МГц = 0.1 мкс на імпульс.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "INA226 моніторинг"
|
||||
|
||||
config WATCH_INA226_ENABLED
|
||||
bool "Увімкнути моніторинг INA226"
|
||||
default y
|
||||
help
|
||||
Якщо увімкнено, ESP32-S3 опитує датчики INA226 для вимірювання
|
||||
напруги, струму та потужності кожного каналу.
|
||||
|
||||
config WATCH_INA226_I2C_PORT
|
||||
int "I2C порт"
|
||||
range 0 1
|
||||
default 0
|
||||
|
||||
config WATCH_INA226_I2C_SDA
|
||||
int "GPIO SDA"
|
||||
range 0 48
|
||||
default 6
|
||||
|
||||
config WATCH_INA226_I2C_SCL
|
||||
int "GPIO SCL"
|
||||
range 0 48
|
||||
default 7
|
||||
|
||||
config WATCH_INA226_I2C_FREQ_HZ
|
||||
int "Швидкість I2C (Гц)"
|
||||
range 10000 1000000
|
||||
default 400000
|
||||
|
||||
config WATCH_INA226_SHUNT_MILLIOHM
|
||||
int "Опір шунта (мОм)"
|
||||
range 1 500
|
||||
default 10
|
||||
|
||||
config WATCH_INA226_CURRENT_LSB_uA
|
||||
int "Крок струму (мкА/LSB)"
|
||||
range 10 10000
|
||||
default 100
|
||||
|
||||
config WATCH_INA226_SAMPLE_INTERVAL_MS
|
||||
int "Інтервал опитування (мс)"
|
||||
range 50 5000
|
||||
default 500
|
||||
|
||||
config WATCH_INA226_ADDR
|
||||
hex "Адреса INA226 (загальна шина)"
|
||||
default 0x40
|
||||
|
||||
endmenu
|
||||
|
||||
menu "UART мультиплексор"
|
||||
|
||||
config WATCH_UART_MUX_ENABLED
|
||||
bool "Увімкнути взаємодію з Raspberry Pi через UART"
|
||||
default y
|
||||
|
||||
config WATCH_UART_MUX_CHANNELS
|
||||
int "Кількість каналів (Raspberry Pi)"
|
||||
range 1 8
|
||||
default 5
|
||||
|
||||
config WATCH_UART_PORT
|
||||
int "Номер UART"
|
||||
range 0 2
|
||||
default 1
|
||||
|
||||
config WATCH_UART_BAUD
|
||||
int "Швидкість UART, біт/с"
|
||||
default 115200
|
||||
|
||||
config WATCH_UART_TX_GPIO
|
||||
int "GPIO TX"
|
||||
default 17
|
||||
|
||||
config WATCH_UART_RX_GPIO
|
||||
int "GPIO RX"
|
||||
default 16
|
||||
|
||||
config WATCH_UART_MUX_SEL_A0
|
||||
int "GPIO A0"
|
||||
default 9
|
||||
|
||||
config WATCH_UART_MUX_SEL_A1
|
||||
int "GPIO A1"
|
||||
default 10
|
||||
|
||||
config WATCH_UART_MUX_SEL_A2
|
||||
int "GPIO A2"
|
||||
default 11
|
||||
|
||||
config WATCH_UART_MUX_DEFAULT_READ_LEN
|
||||
int "Типова довжина читання (байт)"
|
||||
default 128
|
||||
|
||||
config WATCH_UART_HEARTBEAT_TIMEOUT_SEC
|
||||
int "Тайм-аут heartbeat (сек.)"
|
||||
range 5 600
|
||||
default 60
|
||||
|
||||
endmenu
|
||||
|
||||
49
README.md
49
README.md
@@ -6,18 +6,31 @@ watch-watch — вбудована система на ESP32-S3 для нагл
|
||||
- **Керування каналами живлення**: 5 незалежних ліній `EN` (GPIO 2, 4, 5, 18, 19), які можна увімкнути, вимкнути або перемкнути з коду чи CLI.
|
||||
- **Послідовний автотест**: у `app_main` реалізовано базову логіку — канали вмикаються по черзі з інтервалом 4 с, що дозволяє перевірити всі DC/DC.
|
||||
- **Світлодіодний індикатор стану**: п’ять WS2812 (GPIO 8) показують роботу каналів — активний канал підсвічується яскраво-зеленим, увімкнені/вимкнені відображаються зеленим/синім, помилки — червоним.
|
||||
- **Моніторинг навантаження**: датчики INA226 вимірюють напругу, струм та потужність кожного каналу, інформація потрапляє в лог і CLI.
|
||||
- **UART взаємодія з Raspberry Pi**: один UART через мультиплексор (A0/A1/A2) ділиться між п’ятьма Pi, дозволяючи надсилати службові повідомлення або обмінюватися даними.
|
||||
- **Нативний USB-CLI**: ESP32-S3 підключається до Raspberry Pi 5 по USB і стає CDC ACM пристроєм; командний інтерфейс дозволяє керувати каналами та дивитись стан у реальному часі.
|
||||
- **Модульна архітектура**: окремі компоненти `dcdc_controller` і `usb_cdc_cli` спрощують розширення (телеметрія, автоматизація, протоколи зв’язку).
|
||||
|
||||
## Структура проєкту
|
||||
```
|
||||
├── CMakeLists.txt
|
||||
├── Kconfig.projbuild # меню і параметри WATCH_WS2812_*
|
||||
├── README.md
|
||||
├── sdkconfig # збережені налаштування menuconfig
|
||||
├── dependencies.lock
|
||||
├── main
|
||||
│ ├── main.c // базова логіка, ініціалізація модулів
|
||||
│ ├── dcdc_controller.c/.h // API керування DC/DC каналами
|
||||
│ ├── usb_cdc_cli.c/.h // TinyUSB CLI для Raspberry Pi 5
|
||||
│ └── idf_component.yml // залежність на espressif/esp_tinyusb
|
||||
├── sdkconfig // конфігурація (опції TinyUSB увімкнені)
|
||||
└── README.md // цей файл
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── idf_component.yml # залежності: esp_tinyusb, led_strip
|
||||
│ ├── main.c # головний цикл, тестова логіка DC/DC
|
||||
│ ├── dcdc_controller.c/.h # керування GPIO EN
|
||||
│ ├── usb_cdc_cli.c/.h # CLI по USB CDC
|
||||
│ ├── ws2812_status.c/.h # індикація стану на WS2812
|
||||
│ ├── ina226_monitor.c/.h # вимірювання напруги/струму/потужності
|
||||
│ └── uart_mux.c/.h # UART взаємодія з 5 Raspberry Pi
|
||||
├── managed_components
|
||||
│ ├── espressif__esp_tinyusb # бібліотека TinyUSB від Espressif
|
||||
│ └── espressif__led_strip # драйвер керування WS2812 (RMT/SPI)
|
||||
└── .vscode / .devcontainer / .clangd # допоміжні файли середовища розробки
|
||||
```
|
||||
|
||||
## GPIO-призначення каналів
|
||||
@@ -39,6 +52,25 @@ watch-watch — вбудована система на ESP32-S3 для нагл
|
||||
- Колірні алгоритми можна кастомізувати у `main/ws2812_status.c`.
|
||||
- GPIO, кількість діодів та тактову частоту RMT можна змінити через `idf.py menuconfig` (розділ *Налаштування watch-watch*).
|
||||
|
||||
## Моніторинг живлення (INA226)
|
||||
- Один INA226 підключений до загальної шини живлення (I2C порт 0, GPIO 6/7; адреса налаштовується параметром `WATCH_INA226_ADDR`).
|
||||
- Модуль `ina226_monitor` вимірює сумарну напругу, струм і розраховує потужність — ці значення фіксуються в логах і використовуються для телеметрії.
|
||||
- Команда CLI `sense` показує поточні показники для всієї системи (канал не вказується, бо датчик один).
|
||||
- Конфігурація I2C, адреси, шунта та кроку струму знаходиться в `menuconfig → INA226 моніторинг`.
|
||||
|
||||
## UART взаємодія та heartbeat
|
||||
- Загальний UART (типово UART1, GPIO17/16) підключений до аналогового мультиплексора, лінії адреси `A0/A1/A2` (GPIO 9/10/11) вибирають одну з Raspberry Pi.
|
||||
- Модуль `uart_mux` серіалізує доступ до UART, надає API для `uart_mux_write/read` і періодично опитує UART на наявність heartbeat.
|
||||
- Після кожного вимірювання INA226 ESP32-S3 відправляє поточну телеметрію (`PWR <V> <I>`) до активної Raspberry Pi.
|
||||
- Якщо heartbeat від Pi не надходить протягом `CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC` (за замовчуванням 60 с), відповідний канал живлення вимикається й знову вмикається для примусового перезапуску.
|
||||
- Команди CLI `uart send` / `uart read` дозволяють вручну надсилати/читати повідомлення, а в `app_main` можна реалізувати власні протоколи синхронізації.
|
||||
|
||||
## UART взаємодія з Raspberry Pi
|
||||
- Шина UART (типово UART1, TX=GPIO17, RX=GPIO16) підключена до аналогового мультиплексора з адресними лініями A0/A1/A2 (GPIO 9/10/11), що дозволяє вибирати одну з 5 Raspberry Pi.
|
||||
- Модуль `uart_mux` гарантує серійний доступ: перед операцією він виставляє двійковий код каналу на A0-A2 та блокує UART м’ютексом.
|
||||
- У `app_main` після вимірювань кожному Pi відправляється телеметрія (`CHx <V>V <I>mA`), а через CLI можна виконати `uart send <n> <msg>` або `uart read <n> [len]`.
|
||||
- Усі параметри (порт, швидкість, GPIO) доступні в `menuconfig → UART мультиплексор`.
|
||||
|
||||
## USB CDC CLI
|
||||
Після підключення ESP32-S3 до Raspberry Pi 5 з’являється USB-пристрій (CDC ACM). У CLI доступні команди:
|
||||
|
||||
@@ -49,6 +81,11 @@ watch-watch — вбудована система на ESP32-S3 для нагл
|
||||
| `enable <n>` | увімкнути канал `n` (0..4) |
|
||||
| `disable <n>` | вимкнути канал `n` |
|
||||
| `toggle <n>` | перемкнути канал `n` |
|
||||
| `sense` | виміряти загальну напругу/струм/потужність |
|
||||
| `uart send <n> <msg>` | надіслати повідомлення у Raspberry Pi `n` |
|
||||
| `uart read <n> [len]` | прочитати відповідь від Raspberry Pi `n` |
|
||||
| `uart send <n> <msg>` | відправити текст у Raspberry Pi №n |
|
||||
| `uart read <n> [len]` | прочитати дані з цільового Pi |
|
||||
|
||||
CLI з’являється після того, як Raspberry Pi встановить DTR (наприклад, через `screen`, `picocom` або власний скрипт).
|
||||
|
||||
|
||||
@@ -2,4 +2,6 @@ idf_component_register(SRCS "main.c"
|
||||
"dcdc_controller.c"
|
||||
"usb_cdc_cli.c"
|
||||
"ws2812_status.c"
|
||||
"ina226_monitor.c"
|
||||
"uart_mux.c"
|
||||
INCLUDE_DIRS ".")
|
||||
|
||||
172
main/ina226_monitor.c
Normal file
172
main/ina226_monitor.c
Normal file
@@ -0,0 +1,172 @@
|
||||
#include "ina226_monitor.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include "driver/i2c.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifndef CONFIG_WATCH_INA226_ENABLED
|
||||
#define CONFIG_WATCH_INA226_ENABLED 0
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_INA226_I2C_PORT
|
||||
#define CONFIG_WATCH_INA226_I2C_PORT 0
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_INA226_I2C_SDA
|
||||
#define CONFIG_WATCH_INA226_I2C_SDA 6
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_INA226_I2C_SCL
|
||||
#define CONFIG_WATCH_INA226_I2C_SCL 7
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_INA226_I2C_FREQ_HZ
|
||||
#define CONFIG_WATCH_INA226_I2C_FREQ_HZ 400000
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_INA226_CURRENT_LSB_uA
|
||||
#define CONFIG_WATCH_INA226_CURRENT_LSB_uA 100
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_INA226_SHUNT_MILLIOHM
|
||||
#define CONFIG_WATCH_INA226_SHUNT_MILLIOHM 10
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_INA226_ADDR
|
||||
#define CONFIG_WATCH_INA226_ADDR 0x40
|
||||
#endif
|
||||
|
||||
#define INA226_REG_CONFIG 0x00
|
||||
#define INA226_REG_BUS 0x02
|
||||
#define INA226_REG_CURRENT 0x04
|
||||
#define INA226_REG_CALIBRATION 0x05
|
||||
|
||||
#define INA226_CONFIG_AVG_16 (0x04 << 9)
|
||||
#define INA226_CONFIG_VBUS_1100US (0x04 << 6)
|
||||
#define INA226_CONFIG_VSH_1100US (0x04 << 3)
|
||||
#define INA226_CONFIG_MODE_SHUNT_BUS_CONT 0x07
|
||||
|
||||
#if CONFIG_WATCH_INA226_ENABLED
|
||||
static const char *TAG = "ina226";
|
||||
static bool s_initialized;
|
||||
static float s_current_lsb_ma;
|
||||
static uint8_t s_address = CONFIG_WATCH_INA226_ADDR;
|
||||
static ina226_reading_t s_last_reading;
|
||||
#endif
|
||||
|
||||
esp_err_t ina226_monitor_init(void)
|
||||
{
|
||||
#if !CONFIG_WATCH_INA226_ENABLED
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#else
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
i2c_config_t config = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = CONFIG_WATCH_INA226_I2C_SDA,
|
||||
.scl_io_num = CONFIG_WATCH_INA226_I2C_SCL,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.master.clk_speed = CONFIG_WATCH_INA226_I2C_FREQ_HZ,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(i2c_param_config(CONFIG_WATCH_INA226_I2C_PORT, &config), TAG, "i2c config failed");
|
||||
ESP_RETURN_ON_ERROR(i2c_driver_install(CONFIG_WATCH_INA226_I2C_PORT, config.mode, 0, 0, 0),
|
||||
TAG, "i2c driver install failed");
|
||||
|
||||
double current_lsb_a = ((double)CONFIG_WATCH_INA226_CURRENT_LSB_uA) / 1000000.0;
|
||||
double shunt_ohms = ((double)CONFIG_WATCH_INA226_SHUNT_MILLIOHM) / 1000.0;
|
||||
double calibration = 0.00512 / (current_lsb_a * shunt_ohms);
|
||||
if (calibration > 0xFFFF) calibration = 0xFFFF;
|
||||
uint16_t calibration_value = (uint16_t)calibration;
|
||||
s_current_lsb_ma = (float)current_lsb_a * 1000.0f;
|
||||
|
||||
const uint16_t config_value = INA226_CONFIG_AVG_16 |
|
||||
INA226_CONFIG_VBUS_1100US |
|
||||
INA226_CONFIG_VSH_1100US |
|
||||
INA226_CONFIG_MODE_SHUNT_BUS_CONT;
|
||||
|
||||
uint8_t payload_cfg[3] = { INA226_REG_CONFIG, (uint8_t)(config_value >> 8), (uint8_t)(config_value & 0xFF) };
|
||||
ESP_RETURN_ON_ERROR(i2c_master_write_to_device(CONFIG_WATCH_INA226_I2C_PORT,
|
||||
s_address, payload_cfg, sizeof(payload_cfg),
|
||||
pdMS_TO_TICKS(100)),
|
||||
TAG, "config write failed");
|
||||
|
||||
uint8_t payload_cal[3] = { INA226_REG_CALIBRATION, (uint8_t)(calibration_value >> 8),
|
||||
(uint8_t)(calibration_value & 0xFF) };
|
||||
ESP_RETURN_ON_ERROR(i2c_master_write_to_device(CONFIG_WATCH_INA226_I2C_PORT,
|
||||
s_address, payload_cal, sizeof(payload_cal),
|
||||
pdMS_TO_TICKS(100)),
|
||||
TAG, "calibration write failed");
|
||||
|
||||
s_last_reading = (ina226_reading_t){0};
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG, "INA226 ініціалізовано (адреса 0x%02X)", s_address);
|
||||
return ESP_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ina226_monitor_ready(void)
|
||||
{
|
||||
#if CONFIG_WATCH_INA226_ENABLED
|
||||
return s_initialized;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t ina226_monitor_channel_count(void)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
#if CONFIG_WATCH_INA226_ENABLED
|
||||
static esp_err_t ina226_read_register(uint8_t reg, uint16_t *out_value)
|
||||
{
|
||||
uint8_t value[2];
|
||||
esp_err_t err = i2c_master_write_read_device(CONFIG_WATCH_INA226_I2C_PORT,
|
||||
s_address, ®, 1, value, sizeof(value),
|
||||
pdMS_TO_TICKS(100));
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
*out_value = ((uint16_t)value[0] << 8) | value[1];
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
esp_err_t ina226_monitor_sample(ina226_reading_t *out_reading)
|
||||
{
|
||||
#if !CONFIG_WATCH_INA226_ENABLED
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#else
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
uint16_t bus_raw = 0;
|
||||
uint16_t current_raw = 0;
|
||||
|
||||
ESP_RETURN_ON_ERROR(ina226_read_register(INA226_REG_BUS, &bus_raw), TAG, "bus read failed");
|
||||
ESP_RETURN_ON_ERROR(ina226_read_register(INA226_REG_CURRENT, ¤t_raw), TAG, "current read failed");
|
||||
|
||||
float voltage_v = (float)bus_raw * 1.25f / 1000.0f;
|
||||
float current_ma = (int16_t)current_raw * s_current_lsb_ma;
|
||||
float power_mw = voltage_v * current_ma;
|
||||
|
||||
s_last_reading = (ina226_reading_t){
|
||||
.voltage_v = voltage_v,
|
||||
.current_ma = current_ma,
|
||||
.power_mw = power_mw,
|
||||
};
|
||||
if (out_reading) {
|
||||
*out_reading = s_last_reading;
|
||||
}
|
||||
return ESP_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
const ina226_reading_t *ina226_monitor_get_last(void)
|
||||
{
|
||||
#if CONFIG_WATCH_INA226_ENABLED
|
||||
return &s_last_reading;
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
18
main/ina226_monitor.h
Normal file
18
main/ina226_monitor.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef struct {
|
||||
float voltage_v;
|
||||
float current_ma;
|
||||
float power_mw;
|
||||
} ina226_reading_t;
|
||||
|
||||
esp_err_t ina226_monitor_init(void);
|
||||
bool ina226_monitor_ready(void);
|
||||
size_t ina226_monitor_channel_count(void);
|
||||
esp_err_t ina226_monitor_sample(ina226_reading_t *out_reading);
|
||||
const ina226_reading_t *ina226_monitor_get_last(void);
|
||||
28
main/main.c
28
main/main.c
@@ -5,6 +5,8 @@
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "dcdc_controller.h"
|
||||
#include "ina226_monitor.h"
|
||||
#include "uart_mux.h"
|
||||
#include "usb_cdc_cli.h"
|
||||
#include "ws2812_status.h"
|
||||
|
||||
@@ -25,6 +27,19 @@ void app_main(void)
|
||||
ESP_LOGW(TAG, "WS2812 статусний індикатор недоступний");
|
||||
}
|
||||
|
||||
if (ina226_monitor_init() == ESP_OK) {
|
||||
ESP_LOGI(TAG, "INA226 моніторинг активовано");
|
||||
ina226_monitor_sample(NULL);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Моніторинг навантаження недоступний");
|
||||
}
|
||||
|
||||
if (uart_mux_init() == ESP_OK) {
|
||||
ESP_LOGI(TAG, "UART мультиплексор активовано");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "UART мультиплексор недоступний");
|
||||
}
|
||||
|
||||
if (usb_cdc_cli_init() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "USB CDC CLI недоступний");
|
||||
} else {
|
||||
@@ -48,6 +63,19 @@ void app_main(void)
|
||||
ws2812_status_mark_active(ch);
|
||||
vTaskDelay(on_time);
|
||||
|
||||
ina226_reading_t reading = {0};
|
||||
if (ina226_monitor_sample(&reading) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Живлення: %.2f В, %.1f мА, %.1f мВт",
|
||||
reading.voltage_v, reading.current_ma, reading.power_mw);
|
||||
}
|
||||
|
||||
if (uart_mux_ready()) {
|
||||
char msg[64];
|
||||
int len = snprintf(msg, sizeof(msg), "PWR %.2fV %.0fmA\r\n",
|
||||
reading.voltage_v, reading.current_ma);
|
||||
uart_mux_write(ch, (const uint8_t *)msg, len, pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
prev_channel = ch;
|
||||
}
|
||||
}
|
||||
|
||||
233
main/uart_mux.c
Normal file
233
main/uart_mux.c
Normal file
@@ -0,0 +1,233 @@
|
||||
#include "uart_mux.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "dcdc_controller.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifndef CONFIG_WATCH_UART_MUX_CHANNELS
|
||||
#define CONFIG_WATCH_UART_MUX_CHANNELS 5
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC
|
||||
#define CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC 60
|
||||
#endif
|
||||
|
||||
#if CONFIG_WATCH_UART_MUX_ENABLED
|
||||
|
||||
#define UART_MUX_MAX_CHANNELS CONFIG_WATCH_UART_MUX_CHANNELS
|
||||
|
||||
static const char *TAG = "uart_mux";
|
||||
|
||||
static const gpio_num_t s_select_pins[3] = {
|
||||
CONFIG_WATCH_UART_MUX_SEL_A0,
|
||||
CONFIG_WATCH_UART_MUX_SEL_A1,
|
||||
CONFIG_WATCH_UART_MUX_SEL_A2,
|
||||
};
|
||||
|
||||
static SemaphoreHandle_t s_mutex;
|
||||
static size_t s_active_channel = SIZE_MAX;
|
||||
static bool s_initialized;
|
||||
static int64_t s_last_heartbeat_us[UART_MUX_MAX_CHANNELS];
|
||||
static TaskHandle_t s_watchdog_task;
|
||||
|
||||
static esp_err_t uart_mux_select_locked(size_t channel)
|
||||
{
|
||||
if (channel >= UART_MUX_MAX_CHANNELS) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (channel == s_active_channel) {
|
||||
return ESP_OK;
|
||||
}
|
||||
for (int bit = 0; bit < 3; ++bit) {
|
||||
int level = (channel >> bit) & 0x1;
|
||||
ESP_RETURN_ON_ERROR(gpio_set_level(s_select_pins[bit], level), TAG,
|
||||
"GPIO set failed");
|
||||
}
|
||||
s_active_channel = channel;
|
||||
s_last_heartbeat_us[channel] = esp_timer_get_time();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void uart_mux_watchdog_task(void *arg)
|
||||
{
|
||||
const TickType_t poll_interval = pdMS_TO_TICKS(1000);
|
||||
const TickType_t read_timeout = pdMS_TO_TICKS(10);
|
||||
const int64_t timeout_us = (int64_t)CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC * 1000000LL;
|
||||
uint8_t buffer[CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN];
|
||||
|
||||
while (true) {
|
||||
vTaskDelay(poll_interval);
|
||||
int64_t now = esp_timer_get_time();
|
||||
for (size_t ch = 0; ch < UART_MUX_MAX_CHANNELS; ++ch) {
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(20)) == pdTRUE) {
|
||||
if (uart_mux_select_locked(ch) == ESP_OK) {
|
||||
int read = uart_read_bytes(CONFIG_WATCH_UART_PORT,
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
read_timeout);
|
||||
if (read > 0) {
|
||||
s_last_heartbeat_us[ch] = now;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
|
||||
if (dcdc_get_state(ch) && s_last_heartbeat_us[ch] > 0 &&
|
||||
(now - s_last_heartbeat_us[ch]) > timeout_us) {
|
||||
ESP_LOGW(TAG, "Heartbeat каналу %u втрачено, перезавантаження...", (unsigned)ch);
|
||||
dcdc_disable(ch);
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
dcdc_enable(ch);
|
||||
s_last_heartbeat_us[ch] = esp_timer_get_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CONFIG_WATCH_UART_MUX_ENABLED
|
||||
|
||||
esp_err_t uart_mux_init(void)
|
||||
{
|
||||
#if !CONFIG_WATCH_UART_MUX_ENABLED
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#else
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pin_bit_mask = 0,
|
||||
};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
io_conf.pin_bit_mask = 1ULL << s_select_pins[i];
|
||||
ESP_RETURN_ON_ERROR(gpio_config(&io_conf), TAG, "GPIO config failed");
|
||||
ESP_RETURN_ON_ERROR(gpio_set_level(s_select_pins[i], 0), TAG, "GPIO init level failed");
|
||||
}
|
||||
s_active_channel = 0;
|
||||
|
||||
uart_config_t uart_cfg = {
|
||||
.baud_rate = CONFIG_WATCH_UART_BAUD,
|
||||
.data_bits = UART_DATA_8_BITS,
|
||||
.parity = UART_PARITY_DISABLE,
|
||||
.stop_bits = UART_STOP_BITS_1,
|
||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||
.source_clk = UART_SCLK_APB,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(uart_param_config(CONFIG_WATCH_UART_PORT, &uart_cfg), TAG, "UART config failed");
|
||||
ESP_RETURN_ON_ERROR(uart_set_pin(CONFIG_WATCH_UART_PORT,
|
||||
CONFIG_WATCH_UART_TX_GPIO,
|
||||
CONFIG_WATCH_UART_RX_GPIO,
|
||||
UART_PIN_NO_CHANGE,
|
||||
UART_PIN_NO_CHANGE),
|
||||
TAG, "UART pin assign failed");
|
||||
ESP_RETURN_ON_ERROR(uart_driver_install(CONFIG_WATCH_UART_PORT,
|
||||
CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN * 2,
|
||||
CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN * 2,
|
||||
0, NULL, 0),
|
||||
TAG, "UART driver install failed");
|
||||
|
||||
s_mutex = xSemaphoreCreateMutex();
|
||||
ESP_RETURN_ON_FALSE(s_mutex, ESP_ERR_NO_MEM, TAG, "mutex alloc failed");
|
||||
|
||||
int64_t now = esp_timer_get_time();
|
||||
for (size_t ch = 0; ch < UART_MUX_MAX_CHANNELS; ++ch) {
|
||||
s_last_heartbeat_us[ch] = now;
|
||||
}
|
||||
|
||||
if (xTaskCreate(uart_mux_watchdog_task, "uart_mux_wd", 4096, NULL, 5, &s_watchdog_task) != pdPASS) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG, "UART мультиплексор активовано, каналів: %d", UART_MUX_MAX_CHANNELS);
|
||||
return ESP_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool uart_mux_ready(void)
|
||||
{
|
||||
#if CONFIG_WATCH_UART_MUX_ENABLED
|
||||
return s_initialized;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t uart_mux_channel_count(void)
|
||||
{
|
||||
return CONFIG_WATCH_UART_MUX_CHANNELS;
|
||||
}
|
||||
|
||||
esp_err_t uart_mux_write(size_t channel, const uint8_t *data, size_t length, TickType_t timeout)
|
||||
{
|
||||
#if !CONFIG_WATCH_UART_MUX_ENABLED
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#else
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (channel >= UART_MUX_MAX_CHANNELS) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!data || length == 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (xSemaphoreTake(s_mutex, timeout) != pdTRUE) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
esp_err_t err = uart_mux_select_locked(channel);
|
||||
if (err == ESP_OK) {
|
||||
int written = uart_write_bytes(CONFIG_WATCH_UART_PORT, (const char *)data, length);
|
||||
if (written < 0 || (size_t)written != length) {
|
||||
err = ESP_FAIL;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_mutex);
|
||||
return err;
|
||||
#endif
|
||||
}
|
||||
|
||||
esp_err_t uart_mux_read(size_t channel, uint8_t *buffer, size_t buffer_size, size_t *out_length, TickType_t timeout)
|
||||
{
|
||||
#if !CONFIG_WATCH_UART_MUX_ENABLED
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#else
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (channel >= UART_MUX_MAX_CHANNELS || !buffer || buffer_size == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (xSemaphoreTake(s_mutex, timeout) != pdTRUE) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
esp_err_t err = uart_mux_select_locked(channel);
|
||||
if (err == ESP_OK) {
|
||||
int read = uart_read_bytes(CONFIG_WATCH_UART_PORT, buffer, buffer_size, timeout);
|
||||
if (read < 0) {
|
||||
err = ESP_FAIL;
|
||||
} else {
|
||||
if (out_length) {
|
||||
*out_length = (size_t)read;
|
||||
}
|
||||
if (read > 0) {
|
||||
s_last_heartbeat_us[channel] = esp_timer_get_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_mutex);
|
||||
return err;
|
||||
#endif
|
||||
}
|
||||
12
main/uart_mux.h
Normal file
12
main/uart_mux.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
esp_err_t uart_mux_init(void);
|
||||
bool uart_mux_ready(void);
|
||||
size_t uart_mux_channel_count(void);
|
||||
esp_err_t uart_mux_write(size_t channel, const uint8_t *data, size_t length, TickType_t timeout);
|
||||
esp_err_t uart_mux_read(size_t channel, uint8_t *buffer, size_t buffer_size, size_t *out_length, TickType_t timeout);
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "tinyusb.h"
|
||||
#include "tusb_cdc_acm.h"
|
||||
#include "ws2812_status.h"
|
||||
#include "ina226_monitor.h"
|
||||
#include "uart_mux.h"
|
||||
|
||||
#define CLI_LINE_MAX_LEN 128
|
||||
#define CLI_QUEUE_LEN 8
|
||||
@@ -25,6 +27,9 @@
|
||||
#ifndef CONFIG_TINYUSB_CDC_RX_BUFSIZE
|
||||
#define CONFIG_TINYUSB_CDC_RX_BUFSIZE 64
|
||||
#endif
|
||||
#ifndef CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN
|
||||
#define CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN 128
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
size_t length;
|
||||
@@ -147,6 +152,84 @@ static void usb_cli_handle_switch(const char *cmd, char *args)
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_cli_print_measurement(size_t channel, const ina226_reading_t *reading)
|
||||
{
|
||||
usb_cli_printf("\r\nCH%u: %.2f В, %.1f мА, %.1f мВт",
|
||||
(unsigned)channel, reading->voltage_v, reading->current_ma, reading->power_mw);
|
||||
}
|
||||
|
||||
static void usb_cli_handle_sense(char *args)
|
||||
{
|
||||
if (!ina226_monitor_ready()) {
|
||||
usb_cli_printf("\r\nМоніторинг INA226 недоступний\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
ina226_reading_t reading = {0};
|
||||
if (ina226_monitor_sample(&reading) == ESP_OK) {
|
||||
usb_cli_print_measurement(0, &reading);
|
||||
} else {
|
||||
usb_cli_printf("\r\nНе вдалося зчитати дані з INA226\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_cli_handle_uart(char *args)
|
||||
{
|
||||
if (!uart_mux_ready()) {
|
||||
usb_cli_printf("\r\nUART мультиплексор недоступний\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
char *ctx = NULL;
|
||||
char *action = strtok_r(args, " ", &ctx);
|
||||
if (!action) {
|
||||
usb_cli_printf("\r\nUART usage: uart send <channel> <text> | uart read <channel> [len]\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
char *channel_str = strtok_r(NULL, " ", &ctx);
|
||||
dcdc_channel_t channel;
|
||||
if (!usb_cli_parse_channel(channel_str, &channel)) {
|
||||
usb_cli_printf("\r\nНевірний номер каналу\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcasecmp(action, "send") == 0) {
|
||||
char *payload = ctx;
|
||||
if (!payload || *payload == '\0') {
|
||||
usb_cli_printf("\r\nНемає даних для відправлення\r\n");
|
||||
return;
|
||||
}
|
||||
size_t len = strlen(payload);
|
||||
if (uart_mux_write(channel, (const uint8_t *)payload, len, pdMS_TO_TICKS(200)) == ESP_OK) {
|
||||
usb_cli_printf("\r\nВідправлено %u байт\r\n", (unsigned)len);
|
||||
} else {
|
||||
usb_cli_printf("\r\nПомилка відправлення UART\r\n");
|
||||
}
|
||||
} else if (strcasecmp(action, "read") == 0) {
|
||||
char *len_str = ctx ? strtok_r(NULL, " ", &ctx) : NULL;
|
||||
size_t req_len = len_str ? strtoul(len_str, NULL, 10) : CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN;
|
||||
if (req_len == 0) {
|
||||
req_len = CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN;
|
||||
}
|
||||
if (req_len > 256) {
|
||||
req_len = 256;
|
||||
}
|
||||
uint8_t buffer[256];
|
||||
size_t received = 0;
|
||||
if (uart_mux_read(channel, buffer, req_len, &received, pdMS_TO_TICKS(200)) == ESP_OK && received > 0) {
|
||||
usb_cli_printf("\r\nCH%u UART (%u байт): ", (unsigned)channel, (unsigned)received);
|
||||
for (size_t i = 0; i < received; ++i) {
|
||||
usb_cli_write_raw((char *)&buffer[i], 1);
|
||||
}
|
||||
} else {
|
||||
usb_cli_printf("\r\nДані відсутні\r\n");
|
||||
}
|
||||
} else {
|
||||
usb_cli_printf("\r\nНевірна дія '%s'\r\n", action);
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_cli_process_line(char *line)
|
||||
{
|
||||
char *trimmed = usb_cli_trim(line);
|
||||
@@ -167,13 +250,20 @@ static void usb_cli_process_line(char *line)
|
||||
" status - стан всіх каналів\r\n"
|
||||
" enable <n> - увімкнути канал n\r\n"
|
||||
" disable <n> - вимкнути канал n\r\n"
|
||||
" toggle <n> - перемкнути канал n\r\n");
|
||||
" toggle <n> - перемкнути канал n\r\n"
|
||||
" sense [n] - показати напругу/струм/потужність (опц. канал)\r\n"
|
||||
" uart send <n> <msg> - відправити повідомлення Pi n\r\n"
|
||||
" uart read <n> [len] - прочитати дані з Pi n\r\n");
|
||||
} else if (strcasecmp(cmd, "status") == 0) {
|
||||
usb_cli_print_status();
|
||||
} else if (strcasecmp(cmd, "enable") == 0 ||
|
||||
strcasecmp(cmd, "disable") == 0 ||
|
||||
strcasecmp(cmd, "toggle") == 0) {
|
||||
usb_cli_handle_switch(cmd, save_ptr);
|
||||
} else if (strcasecmp(cmd, "sense") == 0) {
|
||||
usb_cli_handle_sense(save_ptr);
|
||||
} else if (strcasecmp(cmd, "uart") == 0) {
|
||||
usb_cli_handle_uart(save_ptr);
|
||||
} else {
|
||||
usb_cli_printf("\r\nНевідома команда '%s'\r\n", cmd);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user