16 Commits

Author SHA1 Message Date
gaoro-xiao 51b174d330 docs: reorganize project documentation 2026-06-10 11:00:28 +08:00
gaoro-xiao 58361589d8 fix: defer ch390 exti until rtos is ready 2026-04-29 06:06:26 +08:00
gaoro-xiao 0681b8bbe4 fix: set tcp client reconnect interval to three seconds 2026-04-29 04:55:16 +08:00
gaoro-xiao 3cb49fc4f8 docs: document rtos firmware versioning 2026-04-29 04:45:43 +08:00
gaoro-xiao db714471b8 docs: document ch390 pbuf leak fix 2026-04-29 04:36:47 +08:00
gaoro-xiao 8c204aad77 fix: stabilize lwip ethernet recovery 2026-04-29 04:36:29 +08:00
gaoro-xiao a6040e7d68 fix: harden ch390 rx protocol handling 2026-04-29 04:36:17 +08:00
gaoro-xiao 60d2af0a27 fix: stop network tasks cleanly on restart 2026-04-29 04:36:01 +08:00
gaoro-xiao ac0c464910 feat: add delayed uart and runtime mac configuration 2026-04-29 04:35:48 +08:00
gaoro-xiao c9ece65182 fix: serialize ch390 recovery in ethernet poll path 2026-04-23 18:06:03 +08:00
gaoro-xiao c519f90149 fix: harden ch390 recovery primitives 2026-04-23 18:06:03 +08:00
gaoro-xiao d6a1565503 fix: fail fast on app route backpressure 2026-04-23 18:06:02 +08:00
gaoro-xiao ab3b6bfc9a fix: make uart tx enqueue all-or-nothing 2026-04-23 18:06:02 +08:00
gaoro-xiao c1a0822227 fix: add explicit route send failure results 2026-04-23 18:06:02 +08:00
gaoro-xiao 22bc6a7fef fix: abort client close to avoid time-wait reconnect block 2026-04-23 02:55:38 +08:00
gaoro-xiao fe03bee588 docs: document client abort close tradeoff 2026-04-23 02:55:38 +08:00
33 changed files with 2114 additions and 1784 deletions
+26 -13
View File
@@ -2,9 +2,9 @@
## 1. 文档范围
本文档定义 `TCP2UART` 项目的最终 AT 外部协议。
本文档定义 `TCP2UART` 项目的 AT 外部协议。
本文档只描述最终协议模型,不保留任何历史展开式实例字段,不包含测试记录,不讨论旧版兼容命令。
本文档只描述当前代码实现支持的外部命令,不保留历史展开式实例字段,不包含测试记录,不讨论旧版兼容命令。项目结构、任务模型和数据流请阅读 `README.md``项目代码阅读指南.md`
适用对象:
@@ -19,6 +19,13 @@
- 配置口:`USART1`
- 数据口:`USART2``USART3`
### 2.1 固件版本线
- FreeRTOS + lwIP 版本线从 `V2.0.0` 开始。
- 裸机版本线从 `V1.0.0` 开始。
- 当前 FreeRTOS 固件基线 release`TCP2UART RTOS V2.0.0`
- 固件下载:`https://git.furtherverse.com/gaoro-xiao/TCP2UART/releases/tag/V2.0.0`
职责划分:
- `USART1`AT 配置口
@@ -49,12 +56,12 @@ SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
字段定义:
- `SYNC`:帧起始标记,建议固定为 `0x7E`
- `SYNC`:帧起始标记,固定为 `0x7E`
- `LEN_H / LEN_L``PAYLOAD` 长度,高字节在前
- `SRCID`:单字节源端点 ID
- `DSTMASK`:单字节目标端点位图
- `PAYLOAD`:负载数据
- `TAIL`:帧结束标记,建议固定为 `0x7F`
- `TAIL`:帧结束标记,固定为 `0x7F`
规则:
@@ -92,7 +99,7 @@ SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
```text
AT\r\n
AT+MUX?\r\n
AT+NET=192.168.1.100,255.255.255.0,192.168.1.1,02:00:00:00:00:01\r\n
AT+NET=192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00\r\n
```
### 6.2 持久化规则
@@ -121,9 +128,11 @@ MUX = 0
### 7.2 NET 默认值
```text
NET = 192.168.1.100,255.255.255.0,192.168.1.1,02:00:00:00:00:01
NET = 192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00
```
默认 MAC 为全 0,表示 Flash 中不固化板卡 MAC;运行时使用 `CH390D` 内部 MAC。`AT+?``AT+NET?` 回显的是当前生效 MAC。
### 7.3 LINK 默认值
```text
@@ -171,7 +180,7 @@ AT+QUERY\r\n
推荐返回格式:
```text
+NET:IP=192.168.1.100,MASK=255.255.255.0,GW=192.168.1.1,MAC=02:00:00:00:00:01
+NET:IP=192.168.31.100,MASK=255.255.255.0,GW=192.168.31.1,MAC=<当前生效MAC>
+LINK:S1,EN=1,LPORT=8080,RIP=0.0.0.0,RPORT=0,UART=U0
+LINK:S2,EN=0,LPORT=8081,RIP=0.0.0.0,RPORT=0,UART=U1
+LINK:C1,EN=1,LPORT=9001,RIP=192.168.1.200,RPORT=9000,UART=U1
@@ -213,7 +222,7 @@ OK
#### 设置 NET
```text
AT+NET=192.168.1.100,255.255.255.0,192.168.1.1,02:00:00:00:00:01\r\n
AT+NET=192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00\r\n
```
字段顺序:
@@ -231,13 +240,13 @@ AT+NET?\r\n
返回示例:
```text
+NET:IP=192.168.1.100,MASK=255.255.255.0,GW=192.168.1.1,MAC=02:00:00:00:00:01
+NET:IP=192.168.31.100,MASK=255.255.255.0,GW=192.168.31.1,MAC=<当前生效MAC>
OK
```
**MAC 设置说明:**
当MAC设置为全0时,固件将使用硬件MAC地址此时通过AT+?查询到的MAC地址为当前生效的硬件MAC地址。
MAC 设置为全 0 时,固件将使用 `CH390D` 内部 MAC 地址此时 Flash 内仍保存全 0,不会把内部 MAC 写回 Flash;`AT+?``AT+NET?` 查询到的 MAC 地址为当前运行时生效的硬件 MAC 地址。
### 8.5 LINK 类命令
@@ -268,6 +277,9 @@ ROLE,EN,LPORT,RIP,RPORT,UART
- `Server``Client` 共用同一条 `LINK` 记录模型
- `Server``RIP/RPORT` 可作为允许接入的对端约束或预设对端信息
- `Client``RIP/RPORT` 表示远端目标地址与端口
- `Client` 侧当前保留固定 `LPORT` 语义,用于满足部分上位机或现场网络策略对固定源端口的依赖
- 为避免固定 `LPORT` 下频繁重连被 lwIP `TIME_WAIT` 长时间占用阻塞,当前固件对 `Client` 主动断开后的释放路径采用 abortive closeRST)而非优雅 `FIN/ACK` 关闭
- 因此 `Client` 重连场景下,对端可能观察到 `RST` 或“连接被重置”,这属于当前产品约束下的有意设计取舍,不影响 `AT+LINK``LPORT` 的配置语义
#### 查询单条 LINK
@@ -354,7 +366,7 @@ OK: Defaults restored
## 11. 推荐配置流程
```text
AT+NET=192.168.1.123,255.255.255.0,192.168.1.1,02:00:00:00:00:01\r\n
AT+NET=192.168.31.123,255.255.255.0,192.168.31.1,00:00:00:00:00:00\r\n
AT+LINK=S1,1,10001,0.0.0.0,0,U1\r\n
AT+LINK=S2,1,10003,0.0.0.0,0,U1\r\n
AT+LINK=C1,1,20001,192.168.1.201,10002,U0\r\n
@@ -382,9 +394,10 @@ AT+RESET\r\n
1. `AT+SAVE\r\n`
2. `AT+RESET\r\n`
## 13. 相关文件
## 13. 相关文档与文
- 项目入口:`README.md`
- 代码结构和数据流:`项目代码阅读指南.md`
- AT 命令实现:`App/config.c`
- 配置结构与默认值:`App/config.h`
- FreeRTOS 任务定义:`Core/Src/freertos.c`
- 调试指导:`工程调试指南.md`
+8
View File
@@ -28,6 +28,14 @@ extern volatile int32_t g_netif_set_up_err;
extern volatile int32_t g_netif_init_ok;
void app_start_network_tasks(void);
void app_request_network_task_stop(void);
void app_clear_network_task_stop(void);
BaseType_t app_network_task_stop_requested(void);
BaseType_t app_network_tasks_are_stopped(void);
void app_on_network_task_exit(TaskHandle_t task_handle);
void app_request_network_restart(void);
void app_clear_network_restart_request(void);
BaseType_t app_network_restart_requested(void);
#ifdef __cplusplus
}
+121 -11
View File
@@ -13,6 +13,7 @@
#include "route_msg.h"
#include "app_runtime.h"
#include "debug_log.h"
#include "ethernetif.h"
#include "uart_trans.h"
#define CONFIG_RX_BUFFER_SIZE 160u
@@ -22,6 +23,8 @@
static device_config_t g_config;
static volatile bool g_reset_requested;
static volatile bool g_uart1_tx_busy;
static volatile uint32_t g_config_rx_route_fail_count;
static volatile route_send_result_t g_config_rx_route_fail_reason;
static uint8_t g_uart1_rx_buffer[CONFIG_RX_BUFFER_SIZE];
static char g_config_cmd_buffer[CONFIG_CMD_MAX_LEN];
static char g_config_response_buffer[CONFIG_TX_BUFFER_SIZE];
@@ -113,6 +116,19 @@ static int parse_link_uart(const char *value, uint8_t *uart)
return -1;
}
static int parse_uart_name(const char *value, uint8_t *uart_index)
{
if (equals_ignore_case(value, "U0") || equals_ignore_case(value, "UART2")) {
*uart_index = LINK_UART_U0;
return 0;
}
if (equals_ignore_case(value, "U1") || equals_ignore_case(value, "UART3")) {
*uart_index = LINK_UART_U1;
return 0;
}
return -1;
}
static const char *link_uart_to_str(uint8_t uart)
{
return (uart == LINK_UART_U1) ? "U1" : "U0";
@@ -134,6 +150,15 @@ static const char *link_index_to_name(uint32_t index)
}
}
static void config_get_display_mac(uint8_t *mac)
{
if (ethernetif_get_effective_mac(mac) != 0u) {
return;
}
memcpy(mac, g_config.net.mac, sizeof(g_config.net.mac));
}
static int parse_link_name(const char *value, uint32_t *index)
{
if (equals_ignore_case(value, "S1")) {
@@ -229,13 +254,15 @@ static at_result_t handle_summary_query(char *response, uint16_t max_len)
char mask_str[16];
char gw_str[16];
char mac_str[18];
uint8_t display_mac[6];
char rip_str[CONFIG_LINK_COUNT][16];
uint32_t i;
config_ip_to_str(g_config.net.ip, ip_str);
config_ip_to_str(g_config.net.mask, mask_str);
config_ip_to_str(g_config.net.gw, gw_str);
config_mac_to_str(g_config.net.mac, mac_str);
config_get_display_mac(display_mac);
config_mac_to_str(display_mac, mac_str);
for (i = 0; i < CONFIG_LINK_COUNT; ++i) {
config_ip_to_str(g_config.links[i].remote_ip, rip_str[i]);
}
@@ -315,6 +342,14 @@ const device_config_t *config_get(void)
return &g_config;
}
uint32_t config_get_uart_baudrate(uint8_t uart_index)
{
if (uart_index >= CONFIG_UART_COUNT) {
return DEFAULT_UART_BAUDRATE;
}
return g_config.uart_baudrate[uart_index];
}
device_config_t *config_get_mutable(void)
{
return &g_config;
@@ -370,6 +405,39 @@ at_result_t config_process_at_cmd(const char *cmd, char *response, uint16_t max_
snprintf(response, max_len, "+MUX:%u\r\nOK\r\n", g_config.mux_mode);
return AT_OK;
}
if (equals_ignore_case(p, "BAUD?")) {
snprintf(response, max_len,
"+BAUD:U0=%lu,U1=%lu\r\nOK\r\n",
(unsigned long)g_config.uart_baudrate[0],
(unsigned long)g_config.uart_baudrate[1]);
return AT_OK;
}
if (parse_command_with_value(p, "BAUD", &value)) {
char value_copy[48];
char *cursor;
char *token;
uint8_t uart_index;
uint32_t baudrate;
strncpy(value_copy, value, sizeof(value_copy) - 1u);
value_copy[sizeof(value_copy) - 1u] = '\0';
cursor = value_copy;
token = config_next_token(&cursor);
if (token == NULL || parse_uart_name(token, &uart_index) != 0) {
snprintf(response, max_len, "ERROR: Invalid UART\r\n");
return AT_INVALID_PARAM;
}
token = config_next_token(&cursor);
if (token == NULL || parse_u32_value(token, 1200u, 921600u, &baudrate) != 0) {
snprintf(response, max_len, "ERROR: Invalid baudrate\r\n");
return AT_INVALID_PARAM;
}
g_config.uart_baudrate[uart_index] = baudrate;
snprintf(response, max_len, "OK\r\n");
return AT_NEED_REBOOT;
}
if (parse_command_with_value(p, "MUX", &value)) {
uint32_t mux_value;
if (parse_u32_value(value, 0u, 1u, &mux_value) != 0) {
@@ -385,11 +453,13 @@ at_result_t config_process_at_cmd(const char *cmd, char *response, uint16_t max_
char mask_str[16];
char gw_str[16];
char mac_str[18];
uint8_t display_mac[6];
config_ip_to_str(g_config.net.ip, ip_str);
config_ip_to_str(g_config.net.mask, mask_str);
config_ip_to_str(g_config.net.gw, gw_str);
config_mac_to_str(g_config.net.mac, mac_str);
config_get_display_mac(display_mac);
config_mac_to_str(display_mac, mac_str);
snprintf(response, max_len, "+NET:IP=%s,MASK=%s,GW=%s,MAC=%s\r\nOK\r\n", ip_str, mask_str, gw_str, mac_str);
return AT_OK;
}
@@ -570,19 +640,24 @@ void config_uart_idle_handler(void)
uint16_t len = CONFIG_RX_BUFFER_SIZE - dma_counter;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
HAL_StatusTypeDef hal_status;
route_send_result_t route_result;
if (g_uart1_tx_busy) {
return;
}
if (len > 0u && xConfigQueue != NULL) {
(void)route_send_from_isr(xConfigQueue,
0u,
0u,
ROUTE_CONN_UART1,
g_uart1_rx_buffer,
len,
&xHigherPriorityTaskWoken);
route_result = route_send_from_isr(xConfigQueue,
0u,
0u,
ROUTE_CONN_UART1,
g_uart1_rx_buffer,
len,
&xHigherPriorityTaskWoken);
if (route_result != ROUTE_SEND_OK) {
g_config_rx_route_fail_reason = route_result;
g_config_rx_route_fail_count += 1u;
}
}
hal_status = HAL_UART_DMAStop(&huart1);
@@ -625,16 +700,47 @@ static void config_respond_to_uart(route_msg_t *msg, const char *response)
uart_channel_t channel = (msg->src_id == ENDPOINT_UART3) ? UART_CHANNEL_U1 : UART_CHANNEL_U0;
uint8_t frame[ROUTE_MSG_MAX_PAYLOAD + 6u];
uint16_t frame_len = 0u;
uart_trans_send_result_t uart_result;
if (uart_mux_encode_frame(msg->src_id, 0u, (const uint8_t *)response, (uint16_t)strlen(response), frame, &frame_len, sizeof(frame))) {
(void)uart_trans_send_buffer(channel, frame, frame_len);
uart_result = uart_trans_send_buffer(channel, frame, frame_len);
if (uart_result != UART_TRANS_SEND_OK) {
debug_log_printf("[CFG] resp-tx-fail ch=%u rc=%s len=%u\r\n",
(unsigned int)channel,
uart_trans_send_result_to_str(uart_result),
(unsigned int)frame_len);
}
} else {
debug_log_printf("[CFG] resp-enc-fail src=0x%02X len=%u\r\n",
(unsigned int)msg->src_id,
(unsigned int)strlen(response));
}
}
}
static void config_report_route_failures(uint32_t *reported_route_fail_count)
{
uint32_t fail_count;
route_send_result_t fail_reason;
if (reported_route_fail_count == NULL) {
return;
}
fail_count = g_config_rx_route_fail_count;
fail_reason = g_config_rx_route_fail_reason;
if (fail_count != *reported_route_fail_count) {
*reported_route_fail_count = fail_count;
debug_log_printf("[CFG] rx-route-fail rc=%s cnt=%lu\r\n",
route_send_result_to_str(fail_reason),
(unsigned long)fail_count);
}
}
void ConfigTask(void *argument)
{
route_msg_t *msg;
at_result_t result;
uint32_t reported_route_fail_count = 0u;
(void)argument;
debug_log_write("[CFG] task-entry\r\n");
@@ -642,10 +748,14 @@ void ConfigTask(void *argument)
debug_log_write("[CFG] task-ready\r\n");
for (;;) {
if (xQueueReceive(xConfigQueue, &msg, portMAX_DELAY) != pdPASS) {
config_report_route_failures(&reported_route_fail_count);
if (xQueueReceive(xConfigQueue, &msg, pdMS_TO_TICKS(50)) != pdPASS) {
continue;
}
config_report_route_failures(&reported_route_fail_count);
if (msg->len >= sizeof(g_config_cmd_buffer)) {
msg->len = sizeof(g_config_cmd_buffer) - 1u;
}
+2 -1
View File
@@ -66,7 +66,7 @@ typedef struct {
#define DEFAULT_NET_IP {192, 168, 31, 100}
#define DEFAULT_NET_MASK {255, 255, 255, 0}
#define DEFAULT_NET_GW {192, 168, 31, 1}
#define DEFAULT_NET_MAC {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}
#define DEFAULT_NET_MAC {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
#define DEFAULT_UART_BAUDRATE 115200u
#define DIAG_CH390_RAW_POLL 0
@@ -86,6 +86,7 @@ int config_save(void);
void config_set_defaults(void);
const device_config_t *config_get(void);
device_config_t *config_get_mutable(void);
uint32_t config_get_uart_baudrate(uint8_t uart_index);
at_result_t config_process_at_cmd(const char *cmd, char *response, uint16_t max_len);
void ConfigTask(void *argument);
void config_uart_idle_handler(void);
+85 -32
View File
@@ -12,6 +12,22 @@ typedef struct {
static route_slot_t g_route_slots[ROUTE_MSG_POOL_SIZE];
const char *route_send_result_to_str(route_send_result_t result)
{
switch (result) {
case ROUTE_SEND_OK:
return "ok";
case ROUTE_SEND_INVALID_INPUT:
return "invalid";
case ROUTE_SEND_POOL_EXHAUSTED:
return "pool";
case ROUTE_SEND_QUEUE_FULL:
return "queue";
default:
return "unknown";
}
}
void route_msg_init(void)
{
memset(g_route_slots, 0, sizeof(g_route_slots));
@@ -108,15 +124,15 @@ void route_msg_free_from_isr(route_msg_t *msg)
taskEXIT_CRITICAL_FROM_ISR(saved_interrupt_status);
}
static bool route_prepare(route_msg_t *msg,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len)
static route_send_result_t route_prepare(route_msg_t *msg,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len)
{
if (msg == NULL || data == NULL || len == 0u || len > ROUTE_MSG_MAX_PAYLOAD) {
return false;
return ROUTE_SEND_INVALID_INPUT;
}
msg->src_id = src_id;
@@ -124,51 +140,88 @@ static bool route_prepare(route_msg_t *msg,
msg->conn_type = conn_type;
msg->len = len;
memcpy(msg->data, data, len);
return true;
return ROUTE_SEND_OK;
}
bool route_send(QueueHandle_t queue,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len,
TickType_t wait_ticks)
static route_send_result_t route_validate_args(QueueHandle_t queue,
const uint8_t *data,
uint16_t len)
{
route_msg_t *msg = route_msg_alloc(wait_ticks);
if (queue == NULL || data == NULL || len == 0u || len > ROUTE_MSG_MAX_PAYLOAD) {
return ROUTE_SEND_INVALID_INPUT;
}
if (!route_prepare(msg, src_id, dst_mask, conn_type, data, len)) {
return ROUTE_SEND_OK;
}
route_send_result_t route_send(QueueHandle_t queue,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len,
TickType_t wait_ticks)
{
route_send_result_t result;
route_msg_t *msg;
result = route_validate_args(queue, data, len);
if (result != ROUTE_SEND_OK) {
return result;
}
msg = route_msg_alloc(wait_ticks);
if (msg == NULL) {
return ROUTE_SEND_POOL_EXHAUSTED;
}
result = route_prepare(msg, src_id, dst_mask, conn_type, data, len);
if (result != ROUTE_SEND_OK) {
route_msg_free(msg);
return false;
return result;
}
if (xQueueSend(queue, &msg, wait_ticks) != pdPASS) {
route_msg_free(msg);
return false;
return ROUTE_SEND_QUEUE_FULL;
}
return true;
return ROUTE_SEND_OK;
}
bool route_send_from_isr(QueueHandle_t queue,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len,
BaseType_t *xHigherPriorityTaskWoken)
route_send_result_t route_send_from_isr(QueueHandle_t queue,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len,
BaseType_t *xHigherPriorityTaskWoken)
{
route_msg_t *msg = route_msg_alloc_from_isr(xHigherPriorityTaskWoken);
route_send_result_t result;
route_msg_t *msg;
if (!route_prepare(msg, src_id, dst_mask, conn_type, data, len)) {
result = route_validate_args(queue, data, len);
if (result != ROUTE_SEND_OK) {
return result;
}
msg = route_msg_alloc_from_isr(xHigherPriorityTaskWoken);
if (msg == NULL) {
return ROUTE_SEND_POOL_EXHAUSTED;
}
result = route_prepare(msg, src_id, dst_mask, conn_type, data, len);
if (result != ROUTE_SEND_OK) {
route_msg_free_from_isr(msg);
return false;
return result;
}
if (xQueueSendFromISR(queue, &msg, xHigherPriorityTaskWoken) != pdPASS) {
route_msg_free_from_isr(msg);
return false;
return ROUTE_SEND_QUEUE_FULL;
}
return true;
return ROUTE_SEND_OK;
}
+22 -14
View File
@@ -29,6 +29,13 @@ typedef enum {
ROUTE_CONN_C2
} route_conn_type_t;
typedef enum {
ROUTE_SEND_OK = 0,
ROUTE_SEND_INVALID_INPUT,
ROUTE_SEND_POOL_EXHAUSTED,
ROUTE_SEND_QUEUE_FULL
} route_send_result_t;
typedef struct {
uint8_t src_id;
uint8_t dst_mask;
@@ -42,20 +49,21 @@ route_msg_t *route_msg_alloc(TickType_t wait_ticks);
route_msg_t *route_msg_alloc_from_isr(BaseType_t *xHigherPriorityTaskWoken);
void route_msg_free(route_msg_t *msg);
void route_msg_free_from_isr(route_msg_t *msg);
bool route_send(QueueHandle_t queue,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len,
TickType_t wait_ticks);
bool route_send_from_isr(QueueHandle_t queue,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len,
BaseType_t *xHigherPriorityTaskWoken);
const char *route_send_result_to_str(route_send_result_t result);
route_send_result_t route_send(QueueHandle_t queue,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len,
TickType_t wait_ticks);
route_send_result_t route_send_from_isr(QueueHandle_t queue,
uint8_t src_id,
uint8_t dst_mask,
uint8_t conn_type,
const uint8_t *data,
uint16_t len,
BaseType_t *xHigherPriorityTaskWoken);
#ifdef __cplusplus
}
+67 -2
View File
@@ -16,6 +16,64 @@
#include "app_runtime.h"
#include "debug_log.h"
#define CH390_RESTART_HOLD_DOWN_MS 500u
#define NETWORK_TASK_DELETE_SETTLE_MS 50u
#define CH390_EXPECTED_VENDOR_ID 0x1C00u
#define CH390_EXPECTED_PRODUCT_ID 0x9151u
static void net_poll_wait_for_network_tasks_stop(void)
{
while (app_network_tasks_are_stopped() == pdFALSE) {
vTaskDelay(pdMS_TO_TICKS(20));
}
}
static BaseType_t net_poll_restart_network_stack(const device_config_t *cfg)
{
#if !DIAG_CH390_RAW_POLL
ip4_addr_t ipaddr;
ip4_addr_t netmask;
ip4_addr_t gateway;
uint16_t vendor_id;
uint16_t product_id;
uint8_t revision;
IP4_ADDR(&ipaddr, cfg->net.ip[0], cfg->net.ip[1], cfg->net.ip[2], cfg->net.ip[3]);
IP4_ADDR(&netmask, cfg->net.mask[0], cfg->net.mask[1], cfg->net.mask[2], cfg->net.mask[3]);
IP4_ADDR(&gateway, cfg->net.gw[0], cfg->net.gw[1], cfg->net.gw[2], cfg->net.gw[3]);
#endif
ethernetif_force_link_down();
g_netif_ready = pdFALSE;
app_request_network_task_stop();
net_poll_wait_for_network_tasks_stop();
vTaskDelay(pdMS_TO_TICKS(NETWORK_TASK_DELETE_SETTLE_MS));
vTaskDelay(pdMS_TO_TICKS(CH390_RESTART_HOLD_DOWN_MS));
#if DIAG_CH390_RAW_POLL
ethernetif_diag_ch390_init();
#else
ethernetif_force_full_recovery(&ipaddr, &netmask, &gateway, cfg->net.mac);
vendor_id = ethernetif_ch390_get_vendor_id();
product_id = ethernetif_ch390_get_product_id();
revision = ethernetif_ch390_get_revision();
if ((vendor_id != CH390_EXPECTED_VENDOR_ID) || (product_id != CH390_EXPECTED_PRODUCT_ID)) {
debug_log_printf("[NET] restart-recovery id-warn vid=0x%04X pid=0x%04X rev=0x%02X free=%lu min=%lu\r\n",
(unsigned int)vendor_id,
(unsigned int)product_id,
(unsigned int)revision,
(unsigned long)xPortGetFreeHeapSize(),
(unsigned long)xPortGetMinimumEverFreeHeapSize());
}
#endif
app_clear_network_task_stop();
g_netif_ready = pdTRUE;
app_start_network_tasks();
app_clear_network_restart_request();
return pdTRUE;
}
void NetPollTask(void *argument)
{
const device_config_t *cfg;
@@ -98,6 +156,11 @@ void NetPollTask(void *argument)
debug_log_write("[NET] loop-enter\r\n");
loop_logged = pdTRUE;
}
if (app_network_restart_requested() != pdFALSE) {
(void)net_poll_restart_network_stack(cfg);
}
(void)xSemaphoreTake(xNetSemaphore, pdMS_TO_TICKS(2));
#if DIAG_CH390_RAW_POLL
@@ -120,8 +183,10 @@ void NetPollTask(void *argument)
}
}
#else
ethernetif_poll();
ethernetif_check_link();
if (g_netif_ready != pdFALSE) {
ethernetif_poll();
ethernetif_check_link();
}
#endif
}
}
+116 -17
View File
@@ -5,6 +5,8 @@
#include "queue.h"
#include "lwip/api.h"
#include "lwip/ip_addr.h"
#include "lwip/tcp.h"
#include "lwip/tcpip.h"
#include "app_runtime.h"
#include "config.h"
@@ -12,7 +14,56 @@
#include "ethernetif.h"
#include "route_msg.h"
#define TCP_CLIENT_CONNECT_TIMEOUT_MS 5000
#define TCP_CLIENT_CONNECT_TIMEOUT_MS 500
#define TCP_CLIENT_RECONNECT_INTERVAL_MS 3000u
#define TCP_CLIENT_STOP_POLL_MS 50u
static BaseType_t tcp_client_stop_requested(void)
{
return (app_network_task_stop_requested() != pdFALSE) ? pdTRUE : pdFALSE;
}
static BaseType_t tcp_client_delay_with_stop(uint32_t delay_ms)
{
uint32_t remaining_ms = delay_ms;
while (remaining_ms > 0u) {
uint32_t slice_ms = (remaining_ms > TCP_CLIENT_STOP_POLL_MS) ? TCP_CLIENT_STOP_POLL_MS : remaining_ms;
if (tcp_client_stop_requested() != pdFALSE) {
return pdFALSE;
}
vTaskDelay(pdMS_TO_TICKS(slice_ms));
remaining_ms -= slice_ms;
}
return (tcp_client_stop_requested() == pdFALSE) ? pdTRUE : pdFALSE;
}
static void tcp_client_abort_and_delete(struct netconn *conn, uint8_t link_index)
{
struct tcp_pcb *pcb;
if (conn == NULL) {
return;
}
pcb = conn->pcb.tcp;
if (pcb != NULL) {
LOCK_TCPIP_CORE();
pcb = conn->pcb.tcp;
if (pcb != NULL) {
tcp_abort(pcb);
conn->pcb.tcp = NULL;
conn->state = NETCONN_NONE;
debug_log_printf("[CLI] idx=%u abort-close\r\n", (unsigned int)link_index);
}
UNLOCK_TCPIP_CORE();
}
netconn_delete(conn);
}
static err_t tcp_client_worker(struct netconn *conn, uint8_t link_index)
{
@@ -22,30 +73,51 @@ static err_t tcp_client_worker(struct netconn *conn, uint8_t link_index)
uint8_t src_endpoint = config_link_index_to_endpoint(link_index);
err_t err;
route_msg_t *tx_msg;
route_send_result_t route_result;
netconn_set_recvtimeout(conn, 10);
for (;;) {
if (tcp_client_stop_requested() != pdFALSE) {
return ERR_CLSD;
}
err = netconn_recv(conn, &buf);
if (err == ERR_OK) {
do {
void *data;
uint16_t len;
netbuf_data(buf, &data, &len);
(void)route_send(xTcpRxQueue,
src_endpoint,
uart_endpoint,
(link_index == CONFIG_LINK_C1) ? ROUTE_CONN_C1 : ROUTE_CONN_C2,
(const uint8_t *)data,
len,
pdMS_TO_TICKS(10));
route_result = route_send(xTcpRxQueue,
src_endpoint,
uart_endpoint,
(link_index == CONFIG_LINK_C1) ? ROUTE_CONN_C1 : ROUTE_CONN_C2,
(const uint8_t *)data,
len,
pdMS_TO_TICKS(10));
if (route_result != ROUTE_SEND_OK) {
debug_log_printf("[CLI] idx=%u rx-route-fail rc=%s len=%u\r\n",
(unsigned int)link_index,
route_send_result_to_str(route_result),
(unsigned int)len);
netbuf_delete(buf);
return ERR_CLSD;
}
} while (netbuf_next(buf) >= 0);
netbuf_delete(buf);
} else if (err != ERR_TIMEOUT) {
} else if (err == ERR_TIMEOUT) {
if (tcp_client_stop_requested() != pdFALSE) {
return ERR_CLSD;
}
} else {
return err;
}
while (xQueueReceive(xLinkTxQueues[link_index], &tx_msg, 0) == pdPASS) {
if (tcp_client_stop_requested() != pdFALSE) {
route_msg_free(tx_msg);
return ERR_CLSD;
}
err = netconn_write(conn, tx_msg->data, tx_msg->len, NETCONN_COPY);
route_msg_free(tx_msg);
if (err != ERR_OK) {
@@ -69,28 +141,44 @@ static void tcp_client_task(uint8_t link_index)
first_connect_deferred = (link_index == CONFIG_LINK_C1) ? 1u : 0u;
for (;;) {
if (tcp_client_stop_requested() != pdFALSE) {
break;
}
while ((g_netif_ready == pdFALSE) || (ethernetif_link_is_up() == 0u)) {
if (tcp_client_stop_requested() != pdFALSE) {
goto exit_task;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
cfg = config_get();
if (cfg->links[link_index].enabled == 0u) {
vTaskDelay(pdMS_TO_TICKS(500));
if (tcp_client_stop_requested() != pdFALSE) {
break;
}
if (tcp_client_delay_with_stop(500u) == pdFALSE) {
break;
}
continue;
}
delay_ms = (cfg->reconnect_interval_ms == 0u) ? 3000u : cfg->reconnect_interval_ms;
delay_ms = TCP_CLIENT_RECONNECT_INTERVAL_MS;
if (first_connect_deferred != 0u) {
first_connect_deferred = 0u;
debug_log_write("[CLI] C1 first-connect defer\r\n");
vTaskDelay(pdMS_TO_TICKS(delay_ms));
if (tcp_client_delay_with_stop(delay_ms) == pdFALSE) {
break;
}
continue;
}
conn = netconn_new(NETCONN_TCP);
if (conn == NULL) {
vTaskDelay(pdMS_TO_TICKS(delay_ms));
if (tcp_client_delay_with_stop(delay_ms) == pdFALSE) {
break;
}
continue;
}
@@ -102,7 +190,9 @@ static void tcp_client_task(uint8_t link_index)
(int)err,
(unsigned int)cfg->links[link_index].local_port);
netconn_delete(conn);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
if (tcp_client_delay_with_stop(delay_ms) == pdFALSE) {
break;
}
continue;
}
}
@@ -140,10 +230,19 @@ static void tcp_client_task(uint8_t link_index)
}
}
netconn_close(conn);
netconn_delete(conn);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
tcp_client_abort_and_delete(conn, link_index);
if (tcp_client_stop_requested() != pdFALSE) {
break;
}
if (tcp_client_delay_with_stop(delay_ms) == pdFALSE) {
break;
}
}
exit_task:
netconn_thread_cleanup();
app_on_network_task_exit(xTaskGetCurrentTaskHandle());
vTaskDelete(NULL);
}
void TcpCliTask_C1(void *argument)
+86 -14
View File
@@ -11,7 +11,33 @@
#include "debug_log.h"
#include "route_msg.h"
static void tcp_server_worker(struct netconn *conn, uint8_t link_index)
#define TCP_SERVER_ACCEPT_TIMEOUT_MS 100
#define TCP_SERVER_STOP_POLL_MS 50u
static BaseType_t tcp_server_stop_requested(void)
{
return (app_network_task_stop_requested() != pdFALSE) ? pdTRUE : pdFALSE;
}
static BaseType_t tcp_server_delay_with_stop(uint32_t delay_ms)
{
uint32_t remaining_ms = delay_ms;
while (remaining_ms > 0u) {
uint32_t slice_ms = (remaining_ms > TCP_SERVER_STOP_POLL_MS) ? TCP_SERVER_STOP_POLL_MS : remaining_ms;
if (tcp_server_stop_requested() != pdFALSE) {
return pdFALSE;
}
vTaskDelay(pdMS_TO_TICKS(slice_ms));
remaining_ms -= slice_ms;
}
return (tcp_server_stop_requested() == pdFALSE) ? pdTRUE : pdFALSE;
}
static err_t tcp_server_worker(struct netconn *conn, uint8_t link_index)
{
struct netbuf *buf;
const device_config_t *cfg = config_get();
@@ -19,37 +45,60 @@ static void tcp_server_worker(struct netconn *conn, uint8_t link_index)
uint8_t src_endpoint = config_link_index_to_endpoint(link_index);
err_t err;
route_msg_t *tx_msg;
route_send_result_t route_result;
netconn_set_recvtimeout(conn, 10);
for (;;) {
if (tcp_server_stop_requested() != pdFALSE) {
return ERR_CLSD;
}
err = netconn_recv(conn, &buf);
if (err == ERR_OK) {
do {
void *data;
uint16_t len;
netbuf_data(buf, &data, &len);
(void)route_send(xTcpRxQueue,
src_endpoint,
uart_endpoint,
(link_index == CONFIG_LINK_S1) ? ROUTE_CONN_S1 : ROUTE_CONN_S2,
(const uint8_t *)data,
len,
pdMS_TO_TICKS(10));
route_result = route_send(xTcpRxQueue,
src_endpoint,
uart_endpoint,
(link_index == CONFIG_LINK_S1) ? ROUTE_CONN_S1 : ROUTE_CONN_S2,
(const uint8_t *)data,
len,
pdMS_TO_TICKS(10));
if (route_result != ROUTE_SEND_OK) {
debug_log_printf("[SRV] idx=%u rx-route-fail rc=%s len=%u\r\n",
(unsigned int)link_index,
route_send_result_to_str(route_result),
(unsigned int)len);
netbuf_delete(buf);
return ERR_CLSD;
}
} while (netbuf_next(buf) >= 0);
netbuf_delete(buf);
} else if (err != ERR_TIMEOUT) {
} else if (err == ERR_TIMEOUT) {
if (tcp_server_stop_requested() != pdFALSE) {
return ERR_CLSD;
}
} else {
break;
}
while (xQueueReceive(xLinkTxQueues[link_index], &tx_msg, 0) == pdPASS) {
if (tcp_server_stop_requested() != pdFALSE) {
route_msg_free(tx_msg);
return ERR_CLSD;
}
err = netconn_write(conn, tx_msg->data, tx_msg->len, NETCONN_COPY);
route_msg_free(tx_msg);
if (err != ERR_OK) {
return;
return err;
}
}
}
return err;
}
static void tcp_server_task(uint8_t link_index)
@@ -61,31 +110,49 @@ static void tcp_server_task(uint8_t link_index)
netconn_thread_init();
for (;;) {
if (tcp_server_stop_requested() != pdFALSE) {
break;
}
while (g_netif_ready == pdFALSE) {
if (tcp_server_stop_requested() != pdFALSE) {
goto exit_task;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
cfg = config_get();
if (cfg->links[link_index].enabled == 0u) {
vTaskDelay(pdMS_TO_TICKS(500));
if (tcp_server_stop_requested() != pdFALSE) {
break;
}
if (tcp_server_delay_with_stop(500u) == pdFALSE) {
break;
}
continue;
}
listener = netconn_new(NETCONN_TCP);
if (listener == NULL) {
vTaskDelay(pdMS_TO_TICKS(500));
if (tcp_server_delay_with_stop(500u) == pdFALSE) {
break;
}
continue;
}
netconn_set_recvtimeout(listener, TCP_SERVER_ACCEPT_TIMEOUT_MS);
if (netconn_bind(listener, IP_ADDR_ANY, cfg->links[link_index].local_port) != ERR_OK ||
netconn_listen(listener) != ERR_OK) {
netconn_delete(listener);
vTaskDelay(pdMS_TO_TICKS(500));
if (tcp_server_delay_with_stop(500u) == pdFALSE) {
break;
}
continue;
}
for (;;) {
if (cfg->links[link_index].enabled == 0u) {
if (tcp_server_stop_requested() != pdFALSE || cfg->links[link_index].enabled == 0u) {
break;
}
if (netconn_accept(listener, &newconn) == ERR_OK) {
@@ -98,6 +165,11 @@ static void tcp_server_task(uint8_t link_index)
netconn_close(listener);
netconn_delete(listener);
}
exit_task:
netconn_thread_cleanup();
app_on_network_task_exit(xTaskGetCurrentTaskHandle());
vTaskDelete(NULL);
}
void TcpSrvTask_S1(void *argument)
+243 -57
View File
@@ -32,10 +32,27 @@ typedef struct {
volatile uint16_t tx_tail;
volatile uint16_t tx_dma_len;
volatile uint8_t tx_busy;
volatile uint8_t tx_kick_fail_logged;
} uart_channel_ctx_t;
static uart_channel_ctx_t g_channels[UART_CHANNEL_MAX];
const char *uart_trans_send_result_to_str(uart_trans_send_result_t result)
{
switch (result) {
case UART_TRANS_SEND_OK:
return "ok";
case UART_TRANS_SEND_INVALID_INPUT:
return "invalid";
case UART_TRANS_SEND_RING_FULL:
return "full";
case UART_TRANS_SEND_KICK_FAILED:
return "kick";
default:
return "unknown";
}
}
static uint16_t ring_used(uint16_t head, uint16_t tail, uint16_t size)
{
return (head >= tail) ? (head - tail) : (size - tail + head);
@@ -66,20 +83,21 @@ static void process_rx_snapshot(uart_channel_t channel)
}
}
static void kick_tx(uart_channel_t channel)
static uart_trans_send_result_t kick_tx(uart_channel_t channel)
{
uart_channel_ctx_t *ctx = &g_channels[channel];
uint16_t available;
uint16_t chunk;
uint16_t tail;
uint16_t i;
if (ctx->tx_busy != 0u) {
return;
return UART_TRANS_SEND_OK;
}
available = ring_used(ctx->tx_head, ctx->tx_tail, UART_TX_RING_BUFFER_SIZE);
if (available == 0u) {
return;
return UART_TRANS_SEND_OK;
}
chunk = available;
@@ -87,16 +105,28 @@ static void kick_tx(uart_channel_t channel)
chunk = UART_TX_DMA_BUFFER_SIZE;
}
tail = ctx->tx_tail;
for (i = 0; i < chunk; ++i) {
ctx->tx_dma_buffer[i] = ctx->tx_ring[ctx->tx_tail];
ctx->tx_tail = (uint16_t)((ctx->tx_tail + 1u) % UART_TX_RING_BUFFER_SIZE);
ctx->tx_dma_buffer[i] = ctx->tx_ring[tail];
tail = (uint16_t)((tail + 1u) % UART_TX_RING_BUFFER_SIZE);
}
if (HAL_UART_Transmit_DMA(ctx->huart, ctx->tx_dma_buffer, chunk) != HAL_OK) {
ctx->tx_dma_len = 0u;
if (ctx->tx_kick_fail_logged == 0u) {
debug_log_printf("[UART] kick-fail ch=%u len=%u\r\n",
(unsigned int)channel,
(unsigned int)chunk);
ctx->tx_kick_fail_logged = 1u;
}
return UART_TRANS_SEND_KICK_FAILED;
}
ctx->tx_tail = tail;
ctx->tx_dma_len = chunk;
ctx->tx_busy = 1u;
if (HAL_UART_Transmit_DMA(ctx->huart, ctx->tx_dma_buffer, chunk) != HAL_OK) {
ctx->tx_busy = 0u;
}
ctx->tx_kick_fail_logged = 0u;
return UART_TRANS_SEND_OK;
}
static uint16_t uart_ring_available(uart_channel_t channel)
@@ -143,6 +173,7 @@ static void uart_route_raw_channel(uart_channel_t channel)
uint16_t len;
uint8_t uart_endpoint = (channel == UART_CHANNEL_U1) ? ENDPOINT_UART3 : ENDPOINT_UART2;
uint32_t i;
route_send_result_t route_result;
len = uart_ring_read(channel, buffer, sizeof(buffer));
if (len == 0u) {
@@ -154,38 +185,143 @@ static void uart_route_raw_channel(uart_channel_t channel)
continue;
}
(void)route_send(xLinkTxQueues[i],
uart_endpoint,
config_link_index_to_endpoint((uint8_t)i),
(channel == UART_CHANNEL_U1) ? ROUTE_CONN_UART3 : ROUTE_CONN_UART2,
buffer,
len,
pdMS_TO_TICKS(10));
route_result = route_send(xLinkTxQueues[i],
uart_endpoint,
config_link_index_to_endpoint((uint8_t)i),
(channel == UART_CHANNEL_U1) ? ROUTE_CONN_UART3 : ROUTE_CONN_UART2,
buffer,
len,
pdMS_TO_TICKS(10));
if (route_result != ROUTE_SEND_OK) {
debug_log_printf("[UART] raw-route-fail idx=%u rc=%s len=%u\r\n",
(unsigned int)i,
route_send_result_to_str(route_result),
(unsigned int)len);
}
}
}
static void uart_send_tcp_msg_to_uarts(route_msg_t *msg)
static uart_trans_send_result_t uart_send_tcp_msg_chunk(route_msg_t *msg,
uint16_t offset,
uint16_t *accepted_len)
{
uint8_t frame[ROUTE_MSG_MAX_PAYLOAD + 6u];
uint16_t frame_len = 0u;
uint16_t remaining;
uint16_t chunk_len;
uint8_t uart_mask;
uart_trans_send_result_t uart_result;
if ((msg->dst_mask & ENDPOINT_UART2) != 0u) {
if (config_get()->mux_mode == MUX_MODE_FRAME) {
if (uart_mux_encode_frame(msg->src_id, ENDPOINT_UART2, msg->data, msg->len, frame, &frame_len, sizeof(frame))) {
(void)uart_trans_send_buffer(UART_CHANNEL_U0, frame, frame_len);
}
} else {
(void)uart_trans_send_buffer(UART_CHANNEL_U0, msg->data, msg->len);
}
if (accepted_len == NULL || msg == NULL || offset >= msg->len) {
return UART_TRANS_SEND_INVALID_INPUT;
}
if ((msg->dst_mask & ENDPOINT_UART3) != 0u) {
*accepted_len = 0u;
uart_mask = (uint8_t)(msg->dst_mask & (ENDPOINT_UART2 | ENDPOINT_UART3));
if ((msg->dst_mask != uart_mask) ||
(uart_mask != ENDPOINT_UART2 && uart_mask != ENDPOINT_UART3)) {
return UART_TRANS_SEND_INVALID_INPUT;
}
remaining = (uint16_t)(msg->len - offset);
if (uart_mask == ENDPOINT_UART2) {
if (config_get()->mux_mode == MUX_MODE_FRAME) {
if (uart_mux_encode_frame(msg->src_id, ENDPOINT_UART3, msg->data, msg->len, frame, &frame_len, sizeof(frame))) {
(void)uart_trans_send_buffer(UART_CHANNEL_U1, frame, frame_len);
chunk_len = remaining;
if (chunk_len > (uint16_t)(UART_TX_RING_BUFFER_SIZE - 1u - 6u)) {
chunk_len = (uint16_t)(UART_TX_RING_BUFFER_SIZE - 1u - 6u);
}
} else {
(void)uart_trans_send_buffer(UART_CHANNEL_U1, msg->data, msg->len);
if (!uart_mux_encode_frame(msg->src_id, ENDPOINT_UART2, &msg->data[offset], chunk_len, frame, &frame_len, sizeof(frame))) {
return UART_TRANS_SEND_INVALID_INPUT;
}
uart_result = uart_trans_send_buffer(UART_CHANNEL_U0, frame, frame_len);
if (uart_result != UART_TRANS_SEND_OK) {
return uart_result;
}
*accepted_len = chunk_len;
return UART_TRANS_SEND_OK;
}
chunk_len = remaining;
if (chunk_len > (uint16_t)(UART_TX_RING_BUFFER_SIZE - 1u)) {
chunk_len = (uint16_t)(UART_TX_RING_BUFFER_SIZE - 1u);
}
uart_result = uart_trans_send_buffer(UART_CHANNEL_U0, &msg->data[offset], chunk_len);
if (uart_result != UART_TRANS_SEND_OK) {
return uart_result;
}
*accepted_len = chunk_len;
return UART_TRANS_SEND_OK;
}
if (config_get()->mux_mode == MUX_MODE_FRAME) {
chunk_len = remaining;
if (chunk_len > (uint16_t)(UART_TX_RING_BUFFER_SIZE - 1u - 6u)) {
chunk_len = (uint16_t)(UART_TX_RING_BUFFER_SIZE - 1u - 6u);
}
if (!uart_mux_encode_frame(msg->src_id, ENDPOINT_UART3, &msg->data[offset], chunk_len, frame, &frame_len, sizeof(frame))) {
return UART_TRANS_SEND_INVALID_INPUT;
}
uart_result = uart_trans_send_buffer(UART_CHANNEL_U1, frame, frame_len);
if (uart_result != UART_TRANS_SEND_OK) {
return uart_result;
}
*accepted_len = chunk_len;
return UART_TRANS_SEND_OK;
}
chunk_len = remaining;
if (chunk_len > (uint16_t)(UART_TX_RING_BUFFER_SIZE - 1u)) {
chunk_len = (uint16_t)(UART_TX_RING_BUFFER_SIZE - 1u);
}
uart_result = uart_trans_send_buffer(UART_CHANNEL_U1, &msg->data[offset], chunk_len);
if (uart_result != UART_TRANS_SEND_OK) {
return uart_result;
}
*accepted_len = chunk_len;
return UART_TRANS_SEND_OK;
}
static void uart_try_advance_pending_tcp_msg(route_msg_t **pending_tcp_msg,
uint16_t *pending_tcp_offset,
uart_trans_send_result_t *pending_tcp_result)
{
route_msg_t *msg;
uart_trans_send_result_t uart_result;
uint16_t accepted_len;
if (pending_tcp_msg == NULL || pending_tcp_offset == NULL || pending_tcp_result == NULL) {
return;
}
msg = *pending_tcp_msg;
if (msg == NULL) {
return;
}
for (;;) {
accepted_len = 0u;
uart_result = uart_send_tcp_msg_chunk(msg, *pending_tcp_offset, &accepted_len);
if (uart_result != UART_TRANS_SEND_OK) {
if (uart_result != *pending_tcp_result) {
debug_log_printf("[UART] tcp-pend src=0x%02X dst=0x%02X off=%u rc=%s\r\n",
(unsigned int)msg->src_id,
(unsigned int)msg->dst_mask,
(unsigned int)(*pending_tcp_offset),
uart_trans_send_result_to_str(uart_result));
*pending_tcp_result = uart_result;
}
break;
}
*pending_tcp_offset = (uint16_t)(*pending_tcp_offset + accepted_len);
*pending_tcp_result = UART_TRANS_SEND_OK;
if (*pending_tcp_offset >= msg->len) {
route_msg_free(msg);
*pending_tcp_msg = NULL;
*pending_tcp_offset = 0u;
break;
}
}
}
@@ -194,18 +330,26 @@ static void uart_route_mux_frame(uart_channel_t source_channel, const uart_mux_f
{
const device_config_t *cfg = config_get();
uint32_t i;
uint8_t endpoint;
uint8_t source_conn = (source_channel == UART_CHANNEL_U1) ? ROUTE_CONN_UART3 : ROUTE_CONN_UART2;
uint8_t out_frame[ROUTE_MSG_MAX_PAYLOAD + 6u];
uint16_t out_frame_len = 0u;
route_send_result_t route_result;
uart_trans_send_result_t uart_result;
if (frame->dst_mask == 0u) {
(void)route_send(xConfigQueue,
frame->src_id,
0u,
source_conn,
frame->payload,
frame->payload_len,
pdMS_TO_TICKS(10));
route_result = route_send(xConfigQueue,
frame->src_id,
0u,
source_conn,
frame->payload,
frame->payload_len,
pdMS_TO_TICKS(10));
if (route_result != ROUTE_SEND_OK) {
debug_log_printf("[UART] mux-cfg-fail rc=%s len=%u\r\n",
route_send_result_to_str(route_result),
(unsigned int)frame->payload_len);
}
return;
}
@@ -213,21 +357,41 @@ static void uart_route_mux_frame(uart_channel_t source_channel, const uart_mux_f
if (cfg->links[i].enabled == 0u) {
continue;
}
uint8_t endpoint = config_link_index_to_endpoint((uint8_t)i);
endpoint = config_link_index_to_endpoint((uint8_t)i);
if ((frame->dst_mask & endpoint) != 0u) {
(void)route_send(xLinkTxQueues[i], frame->src_id, endpoint, source_conn, frame->payload, frame->payload_len, pdMS_TO_TICKS(10));
route_result = route_send(xLinkTxQueues[i], frame->src_id, endpoint, source_conn, frame->payload, frame->payload_len, pdMS_TO_TICKS(10));
if (route_result != ROUTE_SEND_OK) {
debug_log_printf("[UART] mux-route-fail idx=%u rc=%s len=%u\r\n",
(unsigned int)i,
route_send_result_to_str(route_result),
(unsigned int)frame->payload_len);
}
}
}
if ((frame->dst_mask & ENDPOINT_UART2) != 0u && source_channel != UART_CHANNEL_U0) {
if (uart_mux_encode_frame(frame->src_id, ENDPOINT_UART2, frame->payload, frame->payload_len, out_frame, &out_frame_len, sizeof(out_frame))) {
(void)uart_trans_send_buffer(UART_CHANNEL_U0, out_frame, out_frame_len);
uart_result = uart_trans_send_buffer(UART_CHANNEL_U0, out_frame, out_frame_len);
if (uart_result != UART_TRANS_SEND_OK) {
debug_log_printf("[UART] mux-u0-tx-fail rc=%s len=%u\r\n",
uart_trans_send_result_to_str(uart_result),
(unsigned int)out_frame_len);
}
} else {
debug_log_printf("[UART] mux-u0-enc-fail len=%u\r\n", (unsigned int)frame->payload_len);
}
}
if ((frame->dst_mask & ENDPOINT_UART3) != 0u && source_channel != UART_CHANNEL_U1) {
if (uart_mux_encode_frame(frame->src_id, ENDPOINT_UART3, frame->payload, frame->payload_len, out_frame, &out_frame_len, sizeof(out_frame))) {
(void)uart_trans_send_buffer(UART_CHANNEL_U1, out_frame, out_frame_len);
uart_result = uart_trans_send_buffer(UART_CHANNEL_U1, out_frame, out_frame_len);
if (uart_result != UART_TRANS_SEND_OK) {
debug_log_printf("[UART] mux-u1-tx-fail rc=%s len=%u\r\n",
uart_trans_send_result_to_str(uart_result),
(unsigned int)out_frame_len);
}
} else {
debug_log_printf("[UART] mux-u1-enc-fail len=%u\r\n", (unsigned int)frame->payload_len);
}
}
}
@@ -241,13 +405,6 @@ int uart_trans_init(void)
return 0;
}
int uart_trans_config(uint8_t uart_index, uint32_t baudrate)
{
UART_HandleTypeDef *huart = (uart_index == LINK_UART_U1) ? &huart3 : &huart2;
huart->Init.BaudRate = baudrate;
return (HAL_UART_Init(huart) == HAL_OK) ? 0 : -1;
}
int uart_trans_start_all(void)
{
uint32_t i;
@@ -273,22 +430,44 @@ int uart_trans_start_all(void)
return 0;
}
bool uart_trans_send_buffer(uart_channel_t channel, const uint8_t *data, uint16_t len)
uart_trans_send_result_t uart_trans_send_buffer(uart_channel_t channel, const uint8_t *data, uint16_t len)
{
uart_channel_ctx_t *ctx = &g_channels[channel];
uart_channel_ctx_t *ctx;
uart_trans_send_result_t uart_result;
uint16_t original_head;
uint16_t written = 0u;
if (data == NULL || len == 0u) {
return false;
if (channel >= UART_CHANNEL_MAX || data == NULL || len == 0u || len >= UART_TX_RING_BUFFER_SIZE) {
return UART_TRANS_SEND_INVALID_INPUT;
}
while (written < len && ring_free(ctx->tx_head, ctx->tx_tail, UART_TX_RING_BUFFER_SIZE) > 0u) {
ctx = &g_channels[channel];
if (ctx->huart == NULL) {
return UART_TRANS_SEND_INVALID_INPUT;
}
taskENTER_CRITICAL();
original_head = ctx->tx_head;
if (ring_free(ctx->tx_head, ctx->tx_tail, UART_TX_RING_BUFFER_SIZE) < len) {
taskEXIT_CRITICAL();
return UART_TRANS_SEND_RING_FULL;
}
while (written < len) {
ctx->tx_ring[ctx->tx_head] = data[written++];
ctx->tx_head = (uint16_t)((ctx->tx_head + 1u) % UART_TX_RING_BUFFER_SIZE);
}
kick_tx(channel);
return (written == len);
uart_result = kick_tx(channel);
if (uart_result != UART_TRANS_SEND_OK) {
ctx->tx_head = original_head;
taskEXIT_CRITICAL();
return uart_result;
}
taskEXIT_CRITICAL();
return UART_TRANS_SEND_OK;
}
void uart_trans_notify_rx_from_isr(uart_channel_t channel, BaseType_t *xHigherPriorityTaskWoken)
@@ -412,8 +591,11 @@ void UartRxTask(void *argument)
uint32_t notify_value;
BaseType_t notified;
route_msg_t *msg;
route_msg_t *pending_tcp_msg = NULL;
uint16_t pending_tcp_offset = 0u;
uart_mux_frame_t frame;
const device_config_t *cfg;
uart_trans_send_result_t pending_tcp_result = UART_TRANS_SEND_OK;
(void)argument;
if (uart_trans_start_all() != 0) {
@@ -439,9 +621,13 @@ void UartRxTask(void *argument)
g_channels[UART_CHANNEL_U1].tx_busy = 0u;
}
while (xQueueReceive(xTcpRxQueue, &msg, 0) == pdPASS) {
uart_send_tcp_msg_to_uarts(msg);
route_msg_free(msg);
uart_try_advance_pending_tcp_msg(&pending_tcp_msg, &pending_tcp_offset, &pending_tcp_result);
while (pending_tcp_msg == NULL && xQueueReceive(xTcpRxQueue, &msg, 0) == pdPASS) {
pending_tcp_msg = msg;
pending_tcp_offset = 0u;
pending_tcp_result = UART_TRANS_SEND_OK;
uart_try_advance_pending_tcp_msg(&pending_tcp_msg, &pending_tcp_offset, &pending_tcp_result);
}
cfg = config_get();
+9 -2
View File
@@ -16,6 +16,13 @@ typedef enum {
UART_CHANNEL_MAX
} uart_channel_t;
typedef enum {
UART_TRANS_SEND_OK = 0,
UART_TRANS_SEND_INVALID_INPUT,
UART_TRANS_SEND_RING_FULL,
UART_TRANS_SEND_KICK_FAILED
} uart_trans_send_result_t;
typedef struct {
uint8_t src_id;
uint8_t dst_mask;
@@ -29,9 +36,9 @@ typedef struct {
#define UART_TX_RING_BUFFER_SIZE 256u
int uart_trans_init(void);
int uart_trans_config(uint8_t uart_index, uint32_t baudrate);
int uart_trans_start_all(void);
bool uart_trans_send_buffer(uart_channel_t channel, const uint8_t *data, uint16_t len);
const char *uart_trans_send_result_to_str(uart_trans_send_result_t result);
uart_trans_send_result_t uart_trans_send_buffer(uart_channel_t channel, const uint8_t *data, uint16_t len);
void uart_trans_notify_rx_from_isr(uart_channel_t channel, BaseType_t *xHigherPriorityTaskWoken);
void uart_trans_tx_cplt_handler(uart_channel_t channel);
void UartRxTask(void *argument);
-66
View File
@@ -1,66 +0,0 @@
# TCP2UART 当前交接 Prompt
## 1. 用途
本文件不再承担“项目从零编码任务说明”的职责,而是作为**当前工程的交接入口 Prompt**使用。
长期有效的工程知识、调试经验和现状说明,已经分别固化到其它文档,不再全部堆在本文件中。
---
## 2. 接手后先读什么
请按以下顺序阅读:
1. `交接清单.md` —— 当前状态、接下来要做什么、怎么做
2. `工程调试指南.md` —— 已固化的调试经验与当前工程真实边界
3. `项目技术实现.md` —— 架构、任务模型、协议模型
4. `项目需求说明.md`
5. `AT固件使用手册.md`
---
## 3. 当前工程一句话状态
当前项目已从早期 bring-up 阶段推进到 full-task 运行期调试阶段;`DIAG_TASK_ISOLATION=1` 稳定,`DIAG_TASK_ISOLATION=0` 仍会卡死,但故障边界已被多轮 discriminator 推进到 enabled 的 `netconn_*` 路径。当前在 `STM32F103RCT6` 上的 RAM/heap 余量过低,已被认定为调试噪声的主要来源之一,因此推荐下一阶段先切到 pin2pin 的 `STM32F103RDT6` 再继续分析。
---
## 4. 下一位 agent 的当前目标
请不要把当前任务理解成“立刻继续加逻辑修补”。当前更重要的是:
1. 完成 `STM32F103RCT6 -> STM32F103RDT6` 目标切换
2. 使用真实 Keil 日志重新确认构建成功
3. 在新器件上复测当前代码基线
4. 比较:
- 故障是否消失
- 是否明显后移
- 是否仍停在相同 enabled path
5. 只有拿到新器件上的第一轮 RTT / heap / HWM 证据后,再决定下一步最小化改动
---
## 5. 工作约束
1. 构建真值以 `MDK-ARM/build_capture.txt``TCP2UART.build_log.htm``.map` 为准
2. 不要再把 viewer 当作当前构建真值
3. 不要忽视 `DIAG_TASK_ISOLATION=1 正常、=0 异常` 这个前提
4. 在新器件的第一轮复测前,避免继续做大范围代码改动
5. 每次只做一个能明显改变故障边界的最小改动,并保留 RTT 证据
---
## 6. 可直接复制给下一位 agent 的起始 Prompt
```text
请先阅读:`交接清单.md`、`工程调试指南.md`、`项目技术实现.md`。
当前项目是 STM32F103 + FreeRTOS + lwIP + CH390 的 TCP↔UART 透传工程。此前在 `STM32F103RCT6` 上调试时,`DIAG_TASK_ISOLATION=1` 稳定,`DIAG_TASK_ISOLATION=0` 仍会卡死,但故障边界已被多轮 discriminator 推进到 enabled 的 `netconn_*` 路径。当前最关键的资源事实是:在 `RCT6` 上 full-task 创建完四个 TCP 任务后,FreeRTOS heap 只剩约 944 bytes,静态 RAM 也已逼近物理上限,因此当前推荐先切换到 pin2pin 的 `STM32F103RDT6`,保持现有代码基线基本不变,先完成第一轮换片复测,再根据新器件上的 RTT、free/min heap 和 enabled `S1/C1` 行为决定下一步。
你的当前目标不是立刻修完所有问题,而是:
1. 完成 `RCT6 -> RDT6` 目标切换;
2. 用真实 Keil 日志确认构建通过;
3. 在新器件上复测当前代码,判断故障是否消失、后移或保持原状;
4. 仅在拿到新器件上的第一轮 RTT 后,再继续做最小化的下一步判别。
```
+1
View File
@@ -47,6 +47,7 @@ void MX_USART2_UART_Init(void);
void MX_USART3_UART_Init(void);
/* USER CODE BEGIN Prototypes */
void USART_SetConfiguredBaudrates(uint32_t usart2_baudrate, uint32_t usart3_baudrate);
/* USER CODE END Prototypes */
+69
View File
@@ -37,6 +37,8 @@ static TaskHandle_t xTcpCliTaskC1Handle = NULL;
static TaskHandle_t xTcpCliTaskC2Handle = NULL;
static TaskHandle_t xDefaultTaskHandle = NULL;
static BaseType_t xNetworkTasksStarted = pdFALSE;
static volatile BaseType_t xNetworkTaskStopRequested = pdFALSE;
static volatile BaseType_t xNetworkRestartRequested = pdFALSE;
void app_start_network_tasks(void)
{
@@ -49,6 +51,11 @@ void app_start_network_tasks(void)
return;
}
if (xNetworkTaskStopRequested != pdFALSE) {
debug_log_write("[NET] start-network-tasks stop-pending\r\n");
return;
}
cfg = config_get();
debug_log_printf("[NET] start-network-tasks enter free=%lu min=%lu\r\n",
@@ -107,6 +114,68 @@ void app_start_network_tasks(void)
#endif
}
void app_request_network_task_stop(void)
{
xNetworkTaskStopRequested = pdTRUE;
}
void app_clear_network_task_stop(void)
{
xNetworkTaskStopRequested = pdFALSE;
}
BaseType_t app_network_task_stop_requested(void)
{
return xNetworkTaskStopRequested;
}
BaseType_t app_network_tasks_are_stopped(void)
{
return (xTcpSrvTaskS1Handle == NULL &&
xTcpSrvTaskS2Handle == NULL &&
xTcpCliTaskC1Handle == NULL &&
xTcpCliTaskC2Handle == NULL) ? pdTRUE : pdFALSE;
}
void app_on_network_task_exit(TaskHandle_t task_handle)
{
taskENTER_CRITICAL();
if (task_handle == xTcpSrvTaskS1Handle) {
xTcpSrvTaskS1Handle = NULL;
} else if (task_handle == xTcpSrvTaskS2Handle) {
xTcpSrvTaskS2Handle = NULL;
} else if (task_handle == xTcpCliTaskC1Handle) {
xTcpCliTaskC1Handle = NULL;
} else if (task_handle == xTcpCliTaskC2Handle) {
xTcpCliTaskC2Handle = NULL;
}
if (xTcpSrvTaskS1Handle == NULL &&
xTcpSrvTaskS2Handle == NULL &&
xTcpCliTaskC1Handle == NULL &&
xTcpCliTaskC2Handle == NULL) {
xNetworkTasksStarted = pdFALSE;
}
taskEXIT_CRITICAL();
}
void app_request_network_restart(void)
{
xNetworkRestartRequested = pdTRUE;
}
void app_clear_network_restart_request(void)
{
xNetworkRestartRequested = pdFALSE;
}
BaseType_t app_network_restart_requested(void)
{
return xNetworkRestartRequested;
}
static void StartDefaultTask(void *argument)
{
BaseType_t iwdg_ready = pdFALSE;
+8 -2
View File
@@ -76,9 +76,15 @@ void MX_GPIO_Init(void)
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* EXTI interrupt init*/
/* EXTI interrupt init
* Keep CH390 INT masked during early boot. PB0 may already be asserted at
* power-on, while the FreeRTOS semaphore is not created until
* MX_FREERTOS_Init(). The network driver enables EXTI0 after CH390 and the
* RTOS objects are ready.
*/
HAL_NVIC_DisableIRQ(EXTI0_IRQn);
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
HAL_NVIC_SetPriority(EXTI0_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
+9 -2
View File
@@ -68,6 +68,7 @@ void MX_FREERTOS_Init(void);
/* USER CODE BEGIN PFP */
static void CH390_HardwareReset(void);
static void LED_Init(void);
static void ApplyConfiguredUartBaudrates(void);
void Debug_TrapWithRttHint(const char *tag);
/* USER CODE END PFP */
@@ -109,6 +110,12 @@ void LED_Toggle(void)
HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
}
static void ApplyConfiguredUartBaudrates(void)
{
USART_SetConfiguredBaudrates(config_get_uart_baudrate(LINK_UART_U0),
config_get_uart_baudrate(LINK_UART_U1));
}
/* USER CODE END 0 */
/**
@@ -143,6 +150,8 @@ int main(void)
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
config_init();
ApplyConfiguredUartBaudrates();
MX_USART2_UART_Init();
MX_USART3_UART_Init();
MX_SPI1_Init();
@@ -155,8 +164,6 @@ int main(void)
/* CH390 硬件复位 */
CH390_HardwareReset();
/* Initialize configuration from Flash (fallback to defaults on invalid data) */
config_init();
debug_log_boot("config-ready");
/* USER CODE END 2 */
+6 -2
View File
@@ -2,6 +2,7 @@
#include "stm32f1xx_it.h"
#include "FreeRTOS.h"
#include "semphr.h"
#include "task.h"
#include "app_runtime.h"
@@ -101,8 +102,11 @@ void EXTI0_IRQHandler(void)
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
xSemaphoreGiveFromISR(xNetSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
if ((xNetSemaphore != NULL) &&
(xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)) {
xSemaphoreGiveFromISR(xNetSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
}
+11 -2
View File
@@ -22,6 +22,9 @@
/* USER CODE BEGIN 0 */
static uint32_t g_usart2_baudrate = 115200u;
static uint32_t g_usart3_baudrate = 115200u;
/* USER CODE END 0 */
UART_HandleTypeDef huart1;
@@ -76,7 +79,7 @@ void MX_USART2_UART_Init(void)
/* USER CODE END USART2_Init 1 */
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.BaudRate = g_usart2_baudrate;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
@@ -105,7 +108,7 @@ void MX_USART3_UART_Init(void)
/* USER CODE END USART3_Init 1 */
huart3.Instance = USART3;
huart3.Init.BaudRate = 115200;
huart3.Init.BaudRate = g_usart3_baudrate;
huart3.Init.WordLength = UART_WORDLENGTH_8B;
huart3.Init.StopBits = UART_STOPBITS_1;
huart3.Init.Parity = UART_PARITY_NONE;
@@ -396,4 +399,10 @@ void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
/* USER CODE BEGIN 1 */
void USART_SetConfiguredBaudrates(uint32_t usart2_baudrate, uint32_t usart3_baudrate)
{
g_usart2_baudrate = usart2_baudrate;
g_usart3_baudrate = usart3_baudrate;
}
/* USER CODE END 1 */
+100 -6
View File
@@ -12,6 +12,25 @@
#include "CH390.h"
#include "CH390_Interface.h"
#define CH390_EPCR_POLL_LIMIT 100000u
static int ch390_wait_epcr_ready(void)
{
uint32_t poll_count = CH390_EPCR_POLL_LIMIT;
while ((ch390_read_reg(CH390_EPCR) & 0x01u) != 0u)
{
if (poll_count == 0u)
{
ch390_write_reg(CH390_EPCR, 0x00u);
return -1;
}
--poll_count;
}
return 0;
}
void ch390_probe_rx_header(uint8_t *head)
{
if (head == 0)
@@ -22,6 +41,46 @@ void ch390_probe_rx_header(uint8_t *head)
ch390_read_mem(head, 4);
}
int ch390_peek_packet(uint8_t *rx_status, uint16_t *rx_len)
{
uint8_t nsr;
uint8_t header[4];
uint16_t mrr;
if (rx_status != 0)
{
*rx_status = 0u;
}
if (rx_len != 0)
{
*rx_len = 0u;
}
nsr = ch390_read_reg(CH390_NSR);
if ((nsr & NSR_RXRDY) == 0u)
{
return 0;
}
mrr = (uint16_t)ch390_read_mrrl() | ((uint16_t)ch390_read_mrrh() << 8);
ch390_read_mem(header, 4);
ch390_write_reg(CH390_MRRL, (uint8_t)(mrr & 0xffu));
ch390_write_reg(CH390_MRRH, (uint8_t)((mrr >> 8) & 0xffu));
if (rx_status != 0)
{
*rx_status = header[1];
}
if (rx_len != 0)
{
*rx_len = (uint16_t)header[2] | ((uint16_t)header[3] << 8);
}
return 1;
}
/**
* @name ch390_receive_packet
* @brief Receive packet
@@ -33,6 +92,7 @@ void ch390_probe_rx_header(uint8_t *head)
uint32_t ch390_receive_packet(uint8_t *buff, uint8_t *rx_status)
{
uint8_t nsr;
uint8_t ready;
uint16_t rx_len = 0;
uint8_t ReceiveData[4];
@@ -48,6 +108,18 @@ uint32_t ch390_receive_packet(uint8_t *buff, uint8_t *rx_status)
return 0;
}
(void)ch390_read_mrcmdx();
ready = ch390_read_mrcmdx1();
if (ready == 0u)
{
return 0;
}
if (ready != CH390_PKT_RDY)
{
ch390_rx_reset();
return 0;
}
ch390_read_mem(ReceiveData, 4);
if (rx_status != 0)
@@ -56,15 +128,17 @@ uint32_t ch390_receive_packet(uint8_t *buff, uint8_t *rx_status)
}
rx_len = (uint16_t)ReceiveData[2] | ((uint16_t)ReceiveData[3] << 8);
if (((ReceiveData[1] & 0x3Fu) != 0u) ||
(rx_len < 14u) ||
if ((ReceiveData[0] != CH390_PKT_RDY) ||
((ReceiveData[1] & 0x3Fu) != 0u) ||
(rx_len < (uint16_t)(14u + CH390_PKT_CRC_LEN)) ||
(rx_len > CH390_PKT_MAX))
{
ch390_rx_reset();
return 0;
}
ch390_read_mem(buff, rx_len);
return rx_len;
return (uint32_t)(rx_len - CH390_PKT_CRC_LEN);
}
/**
@@ -125,6 +199,17 @@ void ch390_drop_packet(uint16_t len)
ch390_write_reg(CH390_MRRH, (mdr >> 8) & 0xff);
}
void ch390_rx_reset(void)
{
uint8_t rcr = ch390_read_reg(CH390_RCR);
ch390_write_reg(CH390_RCR, (uint8_t)(rcr & (uint8_t)(~RCR_RXEN)));
ch390_write_reg(CH390_MPTRCR, MPTRCR_RST_RX);
ch390_write_reg(CH390_NSR, NSR_RXOV);
ch390_write_reg(CH390_ISR, (uint8_t)(ISR_ROS | ISR_ROO | ISR_PR));
ch390_write_reg(CH390_RCR, (uint8_t)(rcr | RCR_RXEN));
}
/**
* @name ch390_read_phy
* @brief Read PHY register
@@ -135,7 +220,10 @@ uint16_t ch390_read_phy(uint8_t reg)
ch390_write_reg(CH390_EPAR, CH390_PHY | reg);
// Chose PHY, send read command
ch390_write_reg(CH390_EPCR, EPCR_ERPRR | EPCR_EPOS);
while(ch390_read_reg(CH390_EPCR) & 0x01);
if (ch390_wait_epcr_ready() != 0)
{
return 0xFFFFu;
}
// Clear read command
ch390_write_reg(CH390_EPCR, 0x00);
return (ch390_read_reg(CH390_EPDRH) << 8) |
@@ -155,7 +243,10 @@ void ch390_write_phy(uint8_t reg, uint16_t value)
ch390_write_reg(CH390_EPDRH, ((value >> 8) & 0xff)); // High byte
// Chose PHY, send write command
ch390_write_reg(CH390_EPCR, 0x0A);
while(ch390_read_reg(CH390_EPCR) & 0x01);
if (ch390_wait_epcr_ready() != 0)
{
return;
}
// Clear write command
ch390_write_reg(CH390_EPCR, 0x00);
}
@@ -173,7 +264,10 @@ void ch390_write_eeprom(uint8_t reg, uint16_t value)
ch390_write_reg(CH390_EPDRH, ((value >> 8) & 0xff)); // High byte
// Chose EEPROM, send write command
ch390_write_reg(CH390_EPCR, EPCR_ERPRW);
while(ch390_read_reg(CH390_EPCR) & 0x01);
if (ch390_wait_epcr_ready() != 0)
{
return;
}
// Clear write command
ch390_write_reg(CH390_EPCR, 0x00);
}
+20
View File
@@ -150,6 +150,8 @@ enum ch390_phy_mode
#define CH390_BCASTCR 0x53
#define CH390_INTCKCR 0x54
#define CH390_MPTRCR 0x55
#define MPTRCR_RST_TX (1<<1)
#define MPTRCR_RST_RX (1<<0)
#define CH390_MLEDCR 0x57
#define CH390_MRCMDX 0x70
#define CH390_MRCMDX1 0x71
@@ -302,6 +304,8 @@ enum ch390_phy_mode
#define CH390_RLENCR 0x52
#define CH390_BCASTCR 0x53
#define CH390_MPTRCR 0x55
#define MPTRCR_RST_TX (1<<1)
#define MPTRCR_RST_RX (1<<0)
#define CH390_MRCMDX 0xF0
#define CH390_MRCMDX1 0xF1
#define CH390_MRCMD 0xF2
@@ -356,6 +360,7 @@ enum ch390_phy_mode
#define CH390_PKT_NONE 0x00 /* No packet received */
#define CH390_PKT_RDY 0x01 /* Packet ready to receive */
#define CH390_PKT_ERR 0xFE /* Un-stable states */
#define CH390_PKT_CRC_LEN 4u /* Ethernet FCS stored in RX SRAM */
#define CH390_PKT_MAX 1536 /* Received packet max size */
#define CH390_PKT_MIN 64
@@ -657,6 +662,21 @@ int ch390_runtime_link_up_from_status(const struct ch390_runtime_status *status)
*/
void ch390_probe_rx_header(uint8_t *head);
/**
* @name ch390_peek_packet
* @brief Peek current RX header without consuming the packet.
* @param rx_status - Output abnormal status while receiving packet
* @param rx_len - Output packet length from RX header
* @return 0: no packet pending 1: header sampled
*/
int ch390_peek_packet(uint8_t *rx_status, uint16_t *rx_len);
/**
* @name ch390_rx_reset
* @brief Repair RX datapath after overflow/corruption without full chip reset.
*/
void ch390_rx_reset(void);
/**
* @name ch390_runtime_receive_packet
* @brief Runtime RX entry point for packet receive
+6 -13
View File
@@ -188,6 +188,8 @@ void ch390_interrupt_init(void)
/* EXTI0 is configured in CubeMX for PB0 */
/* NVIC priority should be >= configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY */
/* for FreeRTOS compatibility */
HAL_NVIC_DisableIRQ(EXTI0_IRQn);
__HAL_GPIO_EXTI_CLEAR_IT(CH390_INT_PIN);
HAL_NVIC_SetPriority(EXTI0_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
@@ -283,25 +285,16 @@ uint8_t ch390_read_reg(uint8_t reg)
static uint8_t ch390_read_rx_reg(uint8_t reg)
{
uint8_t tx_buf[3];
uint8_t rx_buf[3];
tx_buf[0] = OPC_MEM_DMY_R;
tx_buf[1] = reg;
tx_buf[2] = 0x00u;
uint8_t value;
CH390_SPI_ATOMIC_ENTER();
ch390_cs(0);
if (HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 3, SPI_TIMEOUT) != HAL_OK)
{
ch390_cs(1);
CH390_SPI_ATOMIC_EXIT();
return 0u;
}
ch390_spi_exchange_byte(reg | OPC_REG_R);
value = ch390_spi_dummy_read();
ch390_cs(1);
CH390_SPI_ATOMIC_EXIT();
return rx_buf[2];
return value;
}
uint8_t ch390_read_mrcmdx(void)
+4 -4
View File
@@ -30,28 +30,28 @@ uint8_t ch390_read_reg(uint8_t reg);
/**
* @name ch390_read_mrcmdx
* @brief Read MRCMDX via memory-dummy-read opcode
* @brief Read MRCMDX receive-ready latch
* @return Register value
*/
uint8_t ch390_read_mrcmdx(void);
/**
* @name ch390_read_mrcmdx1
* @brief Read MRCMDX1 via memory-dummy-read opcode
* @brief Read MRCMDX1 receive-ready latch
* @return Register value
*/
uint8_t ch390_read_mrcmdx1(void);
/**
* @name ch390_read_mrrl
* @brief Read MRRL via memory-dummy-read opcode
* @brief Read MRRL receive memory pointer register
* @return Register value
*/
uint8_t ch390_read_mrrl(void);
/**
* @name ch390_read_mrrh
* @brief Read MRRH via memory-dummy-read opcode
* @brief Read MRRH receive memory pointer register
* @return Register value
*/
uint8_t ch390_read_mrrh(void);
+5 -5
View File
@@ -7,8 +7,8 @@
*
* Key design decisions:
* - netconn API for thread-safe multi-connection TCP
* - tcpip_thread handles all lwIP core operations
* - LWIP_TCPIP_CORE_LOCKING=1 allows direct send from application tasks
* - tcpip_thread handles netconn API and timers
* - core locking lets the poll task process RX packets synchronously
* - Conservative memory footprint: target ~16KB for lwIP
*/
@@ -27,9 +27,9 @@
#define LWIP_NETCONN 1
#define LWIP_NETIF_API 1
/* Core locking: allows netconn_write/recv from any task without going through mbox */
/* Core locking: process netif RX synchronously instead of consuming TCPIP_MSG_INPKT slots. */
#define LWIP_TCPIP_CORE_LOCKING 1
#define LWIP_TCPIP_CORE_LOCKING_INPUT 0
#define LWIP_TCPIP_CORE_LOCKING_INPUT 1
/* Critical section protection */
#define SYS_LIGHTWEIGHT_PROT 1
@@ -54,7 +54,7 @@
#define MEM_SIZE (7 * 1024)
/* Number of pbufs in pool.
* 10 pools for 4 concurrent connections with some headroom. */
* RX is processed synchronously under the core lock, so a small pool is sufficient. */
#define PBUF_POOL_SIZE 8
/* Size of each pbuf in pool (must hold one Ethernet frame) */
@@ -23,7 +23,23 @@ struct ethernetif {
err_t ethernetif_init(struct netif *netif);
void ethernetif_input(struct netif *netif);
void lwip_netif_init(const ip4_addr_t *ipaddr, const ip4_addr_t *netmask, const ip4_addr_t *gw);
void ethernetif_diag_ch390_init(void);
void ethernetif_diag_poll_status(void);
void ethernetif_check_link(void);
void ethernetif_apply_runtime_netif_config(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac);
void ethernetif_force_full_recovery(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac);
void ethernetif_force_link_down(void);
uint16_t ethernetif_ch390_get_vendor_id(void);
uint16_t ethernetif_ch390_get_product_id(void);
uint8_t ethernetif_ch390_get_revision(void);
uint8_t ethernetif_ch390_health_ok(void);
uint8_t ethernetif_get_effective_mac(uint8_t *mac);
uint8_t ethernetif_link_is_up(void);
void ethernetif_poll(void);
+4 -1
View File
@@ -36,6 +36,7 @@ err_t ethernet_output(struct netif *netif,
{
struct pbuf *q;
struct eth_hdr *ethhdr;
err_t err;
LWIP_ASSERT("netif != NULL", netif != NULL);
LWIP_ASSERT("p != NULL", p != NULL);
@@ -56,7 +57,9 @@ err_t ethernet_output(struct netif *netif,
SMEMCPY(&ethhdr->src, src, sizeof(struct eth_addr));
ethhdr->type = lwip_htons(eth_type);
return netif->linkoutput(netif, q);
err = netif->linkoutput(netif, q);
pbuf_free(q);
return err;
}
err_t ethernet_input(struct pbuf *p, struct netif *netif)
+502 -31
View File
@@ -41,12 +41,59 @@ static SemaphoreHandle_t spi_mutex = NULL;
static uint8_t s_rx_buffer[CH390_PKT_MAX];
static uint8_t s_tx_buffer[CH390_PKT_MAX];
static uint8_t s_garp_sent = 0u;
static uint8_t s_effective_mac[ETHARP_HWADDR_LEN];
static uint8_t s_effective_mac_valid = 0u;
#define CH390_TX_TIMEOUT_RESTART_THRESHOLD 6u
#define CH390_EXPECTED_VENDOR_ID 0x1C00u
#define CH390_EXPECTED_PRODUCT_ID 0x9151u
#define ETH_RX_LOG_INITIAL_COUNT 3u
#define ETH_RX_LOG_EVERY_COUNT 64u
#define ETH_RECOVERY_NONE 0u
#define ETH_RECOVERY_RX 1u
#define ETH_RECOVERY_FULL 2u
struct ethernetif_recovery_state
{
uint8_t pending;
uint8_t in_progress;
uint8_t tx_timeout_streak;
uint32_t tx_timeout_count;
uint32_t rx_overflow_count;
uint32_t rx_bad_count;
uint32_t rx_recovery_count;
uint32_t full_recovery_count;
};
static struct ethernetif_recovery_state s_recovery = {0};
/* Forward declarations */
static err_t low_level_init(struct netif *netif);
static err_t low_level_output(struct netif *netif, struct pbuf *p);
static struct pbuf *low_level_input(struct netif *netif);
static void ethernetif_update_link(uint8_t link_status);
static uint32_t ethernetif_record_counter(uint32_t *counter);
static uint32_t ethernetif_record_event_and_request(uint32_t *counter, uint8_t recovery_type);
static uint8_t ethernetif_claim_recovery(void);
static void ethernetif_finish_recovery(void);
static uint8_t ethernetif_run_pending_recovery(void);
static uint8_t ethernetif_should_log_counter(uint32_t count);
static void ethernetif_clear_tx_timeout_streak(void);
static uint32_t ethernetif_note_tx_timeout_streak(void);
static uint8_t ethernetif_mac_is_all_zero(const uint8_t *mac);
static void ethernetif_store_effective_mac(const uint8_t *mac);
static void ethernetif_apply_configured_or_internal_mac_locked(const uint8_t *configured_mac);
static void ethernetif_prepare_runtime_netif_locked(struct netif *netif);
static void ethernetif_apply_runtime_netif_config_locked(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac);
static void ethernetif_perform_full_recovery(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac,
uint8_t *link_status);
/*---------------------------------------------------------------------------
* Low Level Hardware Functions
@@ -58,7 +105,6 @@ static void ethernetif_update_link(uint8_t link_status);
*/
static err_t low_level_init(struct netif *netif)
{
struct ethernetif *ethernetif = netif->state;
const device_config_t *cfg = config_get();
/* Create SPI mutex */
@@ -88,24 +134,9 @@ static err_t low_level_init(struct netif *netif)
ch390_read_reg(CH390_BCASTCR),
ch390_read_reg(CH390_MAR + 7));
/* Apply configured MAC address to CH390 before reading it back into lwIP */
ch390_set_mac_address((uint8_t *)cfg->net.mac);
ethernetif_apply_configured_or_internal_mac_locked(cfg->net.mac);
/* Set MAC hardware address length */
netif->hwaddr_len = ETHARP_HWADDR_LEN;
/* Get MAC address from CH390 */
ch390_get_mac(netif->hwaddr);
/* Maximum transfer unit */
netif->mtu = 1500;
/* Device capabilities */
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET;
/* Initialize state */
ethernetif->rx_len = 0;
ethernetif->rx_status = 0;
ethernetif_prepare_runtime_netif_locked(netif);
/* Enable CH390 interrupt */
ch390_interrupt_init();
@@ -113,6 +144,408 @@ static err_t low_level_init(struct netif *netif)
return ERR_OK;
}
static uint32_t ethernetif_record_counter(uint32_t *counter)
{
uint32_t value;
taskENTER_CRITICAL();
++(*counter);
value = *counter;
taskEXIT_CRITICAL();
return value;
}
static uint32_t ethernetif_record_event_and_request(uint32_t *counter, uint8_t recovery_type)
{
uint32_t value;
taskENTER_CRITICAL();
++(*counter);
if (recovery_type == ETH_RECOVERY_FULL)
{
app_request_network_restart();
}
else if (recovery_type > s_recovery.pending)
{
s_recovery.pending = recovery_type;
}
value = *counter;
taskEXIT_CRITICAL();
return value;
}
static void ethernetif_clear_tx_timeout_streak(void)
{
taskENTER_CRITICAL();
s_recovery.tx_timeout_streak = 0u;
taskEXIT_CRITICAL();
}
static uint32_t ethernetif_note_tx_timeout_streak(void)
{
uint32_t streak;
taskENTER_CRITICAL();
++s_recovery.tx_timeout_count;
if (s_recovery.tx_timeout_streak < 0xFFu)
{
++s_recovery.tx_timeout_streak;
}
streak = s_recovery.tx_timeout_streak;
taskEXIT_CRITICAL();
return streak;
}
static uint8_t ethernetif_claim_recovery(void)
{
uint8_t recovery_type = ETH_RECOVERY_NONE;
taskENTER_CRITICAL();
if ((s_recovery.in_progress == 0u) && (s_recovery.pending != ETH_RECOVERY_NONE))
{
recovery_type = s_recovery.pending;
s_recovery.pending = ETH_RECOVERY_NONE;
s_recovery.in_progress = 1u;
}
taskEXIT_CRITICAL();
return recovery_type;
}
static void ethernetif_finish_recovery(void)
{
taskENTER_CRITICAL();
s_recovery.in_progress = 0u;
taskEXIT_CRITICAL();
}
static uint8_t ethernetif_should_log_counter(uint32_t count)
{
if (count <= ETH_RX_LOG_INITIAL_COUNT)
{
return 1u;
}
return ((count % ETH_RX_LOG_EVERY_COUNT) == 0u) ? 1u : 0u;
}
static uint8_t ethernetif_run_pending_recovery(void)
{
ip4_addr_t ipaddr;
ip4_addr_t netmask;
ip4_addr_t gateway;
uint8_t recovery_type;
uint8_t link_status = 0u;
uint32_t count;
recovery_type = ethernetif_claim_recovery();
if (recovery_type == ETH_RECOVERY_NONE)
{
return ETH_RECOVERY_NONE;
}
if (recovery_type == ETH_RECOVERY_FULL)
{
const device_config_t *cfg = config_get();
IP4_ADDR(&ipaddr, cfg->net.ip[0], cfg->net.ip[1], cfg->net.ip[2], cfg->net.ip[3]);
IP4_ADDR(&netmask, cfg->net.mask[0], cfg->net.mask[1], cfg->net.mask[2], cfg->net.mask[3]);
IP4_ADDR(&gateway, cfg->net.gw[0], cfg->net.gw[1], cfg->net.gw[2], cfg->net.gw[3]);
ethernetif_clear_tx_timeout_streak();
ethernetif_perform_full_recovery(&ipaddr, &netmask, &gateway, cfg->net.mac, &link_status);
}
else
{
if (spi_mutex != NULL)
{
xSemaphoreTake(spi_mutex, portMAX_DELAY);
}
ch390_rx_reset();
if (spi_mutex != NULL)
{
xSemaphoreGive(spi_mutex);
}
}
if (recovery_type == ETH_RECOVERY_FULL)
{
count = ethernetif_record_counter(&s_recovery.full_recovery_count);
debug_log_printf("[ETH] rec full n=%lu link=%u\r\n",
(unsigned long)count,
(unsigned int)link_status);
}
else
{
count = ethernetif_record_counter(&s_recovery.rx_recovery_count);
if (ethernetif_should_log_counter(count) != 0u)
{
debug_log_printf("[ETH] rec rx n=%lu\r\n", (unsigned long)count);
}
}
ethernetif_finish_recovery();
return recovery_type;
}
static void ethernetif_perform_full_recovery(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac,
uint8_t *link_status)
{
uint8_t local_link_status = 0u;
if (spi_mutex != NULL)
{
xSemaphoreTake(spi_mutex, portMAX_DELAY);
}
ch390_spi_init();
ch390_hardware_reset();
ch390_default_config();
ethernetif_apply_configured_or_internal_mac_locked(mac);
ch390_set_phy_mode(CH390_AUTO);
ch390_rx_enable(1);
ethernetif_prepare_runtime_netif_locked(&ch390_netif);
ethernetif_apply_runtime_netif_config_locked(ipaddr,
netmask,
gw,
(s_effective_mac_valid != 0u) ? s_effective_mac : NULL);
local_link_status = (uint8_t)ch390_get_link_status();
if (spi_mutex != NULL)
{
xSemaphoreGive(spi_mutex);
}
ch390_interrupt_init();
if (link_status != NULL)
{
*link_status = local_link_status;
}
ethernetif_update_link(local_link_status);
}
static uint8_t ethernetif_mac_is_all_zero(const uint8_t *mac)
{
uint32_t i;
if (mac == NULL)
{
return 1u;
}
for (i = 0u; i < ETHARP_HWADDR_LEN; ++i)
{
if (mac[i] != 0u)
{
return 0u;
}
}
return 1u;
}
static void ethernetif_store_effective_mac(const uint8_t *mac)
{
if (mac == NULL)
{
s_effective_mac_valid = 0u;
return;
}
MEMCPY(s_effective_mac, mac, ETHARP_HWADDR_LEN);
s_effective_mac_valid = 1u;
}
static void ethernetif_apply_configured_or_internal_mac_locked(const uint8_t *configured_mac)
{
uint8_t runtime_mac[ETHARP_HWADDR_LEN];
if (ethernetif_mac_is_all_zero(configured_mac) == 0u)
{
ch390_set_mac_address((uint8_t *)configured_mac);
ethernetif_store_effective_mac(configured_mac);
return;
}
ch390_get_mac(runtime_mac);
ethernetif_store_effective_mac(runtime_mac);
debug_log_printf("[ETH] use internal mac=%02X:%02X:%02X:%02X:%02X:%02X\r\n",
(unsigned int)runtime_mac[0],
(unsigned int)runtime_mac[1],
(unsigned int)runtime_mac[2],
(unsigned int)runtime_mac[3],
(unsigned int)runtime_mac[4],
(unsigned int)runtime_mac[5]);
}
static void ethernetif_prepare_runtime_netif_locked(struct netif *netif)
{
struct ethernetif *ethernetif;
if (netif == NULL)
{
return;
}
netif->hwaddr_len = ETHARP_HWADDR_LEN;
ch390_get_mac(netif->hwaddr);
ethernetif_store_effective_mac(netif->hwaddr);
netif->mtu = 1500;
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET;
ethernetif = (struct ethernetif *)netif->state;
if (ethernetif != NULL)
{
ethernetif->rx_len = 0u;
ethernetif->rx_status = 0u;
}
}
static void ethernetif_apply_runtime_netif_config_locked(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac)
{
err_t err;
if (mac != NULL)
{
MEMCPY(ch390_netif.hwaddr, mac, ETHARP_HWADDR_LEN);
s_garp_sent = 0u;
}
if (ipaddr != NULL && netmask != NULL && gw != NULL)
{
err = netifapi_netif_set_addr(&ch390_netif, ipaddr, netmask, gw);
if (err != ERR_OK)
{
debug_log_printf("[ETH] set-addr fail err=%d\r\n", (int)err);
}
}
err = netifapi_netif_set_default(&ch390_netif);
if (err != ERR_OK)
{
debug_log_printf("[ETH] resync-default fail err=%d\r\n", (int)err);
}
err = netifapi_netif_set_up(&ch390_netif);
if (err != ERR_OK)
{
debug_log_printf("[ETH] resync-up fail err=%d\r\n", (int)err);
}
}
void ethernetif_apply_runtime_netif_config(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac)
{
ethernetif_apply_runtime_netif_config_locked(ipaddr, netmask, gw, mac);
}
void ethernetif_force_full_recovery(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac)
{
uint8_t link_status = 0u;
uint32_t count;
ethernetif_clear_tx_timeout_streak();
ethernetif_perform_full_recovery(ipaddr, netmask, gw, mac, &link_status);
count = ethernetif_record_counter(&s_recovery.full_recovery_count);
debug_log_printf("[ETH] rec full n=%lu link=%u\r\n",
(unsigned long)count,
(unsigned int)link_status);
}
void ethernetif_force_link_down(void)
{
ethernetif_update_link(0u);
}
uint8_t ethernetif_ch390_health_ok(void)
{
return ((ethernetif_ch390_get_vendor_id() == CH390_EXPECTED_VENDOR_ID) &&
(ethernetif_ch390_get_product_id() == CH390_EXPECTED_PRODUCT_ID)) ? 1u : 0u;
}
uint8_t ethernetif_get_effective_mac(uint8_t *mac)
{
if ((mac == NULL) || (s_effective_mac_valid == 0u))
{
return 0u;
}
MEMCPY(mac, s_effective_mac, ETHARP_HWADDR_LEN);
return 1u;
}
uint16_t ethernetif_ch390_get_vendor_id(void)
{
uint16_t vendor_id;
if (spi_mutex != NULL)
{
xSemaphoreTake(spi_mutex, portMAX_DELAY);
}
vendor_id = ch390_get_vendor_id();
if (spi_mutex != NULL)
{
xSemaphoreGive(spi_mutex);
}
return vendor_id;
}
uint16_t ethernetif_ch390_get_product_id(void)
{
uint16_t product_id;
if (spi_mutex != NULL)
{
xSemaphoreTake(spi_mutex, portMAX_DELAY);
}
product_id = ch390_get_product_id();
if (spi_mutex != NULL)
{
xSemaphoreGive(spi_mutex);
}
return product_id;
}
uint8_t ethernetif_ch390_get_revision(void)
{
uint8_t revision;
if (spi_mutex != NULL)
{
xSemaphoreTake(spi_mutex, portMAX_DELAY);
}
revision = ch390_get_revision();
if (spi_mutex != NULL)
{
xSemaphoreGive(spi_mutex);
}
return revision;
}
/**
* @brief Transmit a packet via CH390
* @param netif Network interface structure
@@ -125,6 +558,7 @@ static err_t low_level_output(struct netif *netif, struct pbuf *p)
uint16_t offset;
uint16_t tx_len;
int tx_rc;
uint32_t tx_timeout_streak;
(void)netif;
/* Take SPI mutex */
@@ -179,10 +613,18 @@ static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
LINK_STATS_INC(link.drop);
LINK_STATS_INC(link.err);
debug_log_write("[ETH] tx timeout\r\n");
tx_timeout_streak = ethernetif_note_tx_timeout_streak();
debug_log_printf("[ETH] tx timeout n=%lu streak=%lu\r\n",
(unsigned long)s_recovery.tx_timeout_count,
(unsigned long)tx_timeout_streak);
if (tx_timeout_streak >= CH390_TX_TIMEOUT_RESTART_THRESHOLD)
{
app_request_network_restart();
}
return ERR_TIMEOUT;
}
ethernetif_clear_tx_timeout_streak();
LINK_STATS_INC(link.xmit);
return ERR_OK;
@@ -266,7 +708,6 @@ static struct pbuf *low_level_input(struct netif *netif)
else
{
/* No memory - drop packet */
ch390_drop_packet(ethernetif->rx_len);
LINK_STATS_INC(link.memerr);
LINK_STATS_INC(link.drop);
}
@@ -428,7 +869,7 @@ void ethernetif_diag_ch390_init(void)
ch390_spi_init();
ch390_hardware_reset();
ch390_default_config();
ch390_set_mac_address((uint8_t *)cfg->net.mac);
ethernetif_apply_configured_or_internal_mac_locked(cfg->net.mac);
ch390_interrupt_init();
g_netif_init_ok = 1;
@@ -457,8 +898,12 @@ void ethernetif_diag_poll_status(void)
*/
static void ethernetif_update_link(uint8_t link_status)
{
uint8_t old_link_status;
LOCK_TCPIP_CORE();
old_link_status = netif_is_link_up(&ch390_netif) ? 1u : 0u;
if (link_status)
{
if (!netif_is_link_up(&ch390_netif))
@@ -481,6 +926,22 @@ static void ethernetif_update_link(uint8_t link_status)
}
UNLOCK_TCPIP_CORE();
if (old_link_status != ((link_status != 0u) ? 1u : 0u))
{
debug_log_printf("[ETH] link %s ip=%u.%u.%u.%u mac=%02X:%02X:%02X:%02X:%02X:%02X\r\n",
(link_status != 0u) ? "up" : "down",
(unsigned int)ip4_addr1_16(netif_ip4_addr(&ch390_netif)),
(unsigned int)ip4_addr2_16(netif_ip4_addr(&ch390_netif)),
(unsigned int)ip4_addr3_16(netif_ip4_addr(&ch390_netif)),
(unsigned int)ip4_addr4_16(netif_ip4_addr(&ch390_netif)),
(unsigned int)ch390_netif.hwaddr[0],
(unsigned int)ch390_netif.hwaddr[1],
(unsigned int)ch390_netif.hwaddr[2],
(unsigned int)ch390_netif.hwaddr[3],
(unsigned int)ch390_netif.hwaddr[4],
(unsigned int)ch390_netif.hwaddr[5]);
}
}
/**
@@ -491,6 +952,8 @@ void ethernetif_poll(void)
struct ch390_runtime_status runtime_status;
struct pbuf *p;
err_t input_err;
uint32_t rx_overflow_count;
uint8_t recovery_type;
uint8_t rx_budget = 4u;
/* Take SPI mutex */
@@ -508,19 +971,25 @@ void ethernetif_poll(void)
xSemaphoreGive(spi_mutex);
}
/* Handle link change */
if ((runtime_status.int_status & ISR_LNKCHG) != 0u)
/* Handle RX overflow */
if ((runtime_status.int_status & (ISR_ROS | ISR_ROO)) != 0u)
{
LINK_STATS_INC(link.err);
rx_overflow_count = ethernetif_record_event_and_request(&s_recovery.rx_overflow_count, ETH_RECOVERY_RX);
if (ethernetif_should_log_counter(rx_overflow_count) != 0u)
{
debug_log_printf("[ETH] rx ovf n=%lu\r\n", (unsigned long)rx_overflow_count);
}
}
recovery_type = ethernetif_run_pending_recovery();
/* Handle link change unless a full recovery already resynchronized link state. */
if ((recovery_type != ETH_RECOVERY_FULL) && ((runtime_status.int_status & ISR_LNKCHG) != 0u))
{
ethernetif_update_link((uint8_t)ch390_runtime_link_up_from_status(&runtime_status));
}
/* Handle RX overflow */
if ((runtime_status.int_status & ISR_ROS) != 0u)
{
/* RX overflow - packets might be corrupted */
LINK_STATS_INC(link.err);
}
/* Always attempt to drain RX FIFO so packet handling does not depend only on ISR_PR timing. */
while (rx_budget > 0u)
{
@@ -539,6 +1008,8 @@ void ethernetif_poll(void)
--rx_budget;
}
(void)ethernetif_run_pending_recovery();
if (rx_budget == 0u)
{
taskYIELD();
+14
View File
@@ -62,6 +62,20 @@ void ethernetif_diag_poll_status(void);
* @note Call this from the LwIP task periodically or on interrupt
*/
void ethernetif_check_link(void);
void ethernetif_apply_runtime_netif_config(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac);
void ethernetif_force_full_recovery(const ip4_addr_t *ipaddr,
const ip4_addr_t *netmask,
const ip4_addr_t *gw,
const uint8_t *mac);
void ethernetif_force_link_down(void);
uint16_t ethernetif_ch390_get_vendor_id(void);
uint16_t ethernetif_ch390_get_product_id(void);
uint8_t ethernetif_ch390_get_revision(void);
uint8_t ethernetif_ch390_health_ok(void);
uint8_t ethernetif_get_effective_mac(uint8_t *mac);
/**
* @brief Query whether physical Ethernet link is currently up
+105
View File
@@ -0,0 +1,105 @@
# TCP2UART 项目说明
`TCP2UART` 是一个基于 `STM32F103RCT6 + CH390D` 的 TCP 与双串口透传固件。系统运行在 `FreeRTOS` 上,网络协议栈使用 `lwIP NO_SYS=0 + netconn API`,通过 `SEGGER RTT` 输出调试日志,工程入口为 Keil MDK 项目 `MDK-ARM/TCP2UART.uvprojx`
本仓库当前文档已经收敛为两类:
1. 面向开发者的代码阅读与系统理解文档。
2. 面向上位机、测试和联调人员的 AT 命令接口文档。
## 硬件与软件基线
| 项目 | 内容 |
|------|------|
| 主控 | `STM32F103RCT6` |
| 以太网芯片 | `CH390D` |
| 配置串口 | `USART1` |
| 数据串口 | `USART2``USART3` |
| RTOS | `FreeRTOS` |
| TCP/IP | `lwIP``NO_SYS=0`,启用 `netconn` API |
| 调试输出 | `SEGGER RTT` |
| 构建工程 | `MDK-ARM/TCP2UART.uvprojx` |
`USART1` 只承担 AT 配置面。`USART2/USART3` 是业务数据口,可工作在普通透传模式或 MUX 帧模式。网络侧提供 2 路 TCP Server 实例和 2 路 TCP Client 实例,统一使用 `LINK` 配置模型管理。
## 当前文档地图
| 文档 | 读者 | 内容 |
|------|------|------|
| `README.md` | 所有人 | 项目总览、文档入口、快速理解路径 |
| `项目代码阅读指南.md` | 固件开发、调试、交接人员 | 代码目录、启动流程、任务模型、数据流、阅读顺序、调试线索 |
| `AT固件使用手册.md` | 上位机、测试、联调人员 | AT 命令、参数模型、默认值、配置流程、常见错误 |
旧的需求说明、技术实现、调试交接、Prompt 和单点问题复盘文档已经合并到上述文档中。临时调试结论不再作为根目录入口,避免把某一轮现场状态误认为长期项目事实。
## 一句话架构
```text
远端 TCP 连接
|
v
TcpSrvTask_S1/S2 或 TcpCliTask_C1/C2
|
v
route_msg_t + FreeRTOS Queue
|
v
UartRxTask / uart_trans
|
v
USART2 / USART3
```
反向数据流也走同一套路由对象:数据口收到字节后由 `UartRxTask` 判断普通透传或 MUX 帧,再投递到对应 `xLinkTxQueues[]`,最终由 TCP 任务调用 `netconn_write()` 发回网络。
## 快速阅读顺序
首次接手建议按下面顺序阅读:
1. `README.md`:确认硬件、软件、文档入口。
2. `项目代码阅读指南.md` 的“总体架构”和“业务数据流示例”:先建立整体图。
3. `Core/Src/main.c`:理解上电初始化顺序。
4. `Core/Src/freertos.c`:理解队列、信号量和任务创建。
5. `App/config.c``AT固件使用手册.md`:理解配置模型和 AT 命令。
6. `App/tcp_server.c``App/tcp_client.c``App/uart_trans.c``App/route_msg.c`:理解 TCP 与 UART 如何互相转发。
7. `App/task_net_poll.c``Drivers/CH390/*``Drivers/LwIP/src/netif/ethernetif.c`:需要定位网络底层问题时再深入。
## 核心配置模型
固件内部把所有链路抽象为 4 条 `LINK`
| 角色 | 内部索引 | 端点编码 | 含义 |
|------|----------|----------|------|
| `S1` | `CONFIG_LINK_S1` | `0x10` | TCP Server 1 |
| `S2` | `CONFIG_LINK_S2` | `0x20` | TCP Server 2 |
| `C1` | `CONFIG_LINK_C1` | `0x01` | TCP Client 1 |
| `C2` | `CONFIG_LINK_C2` | `0x02` | TCP Client 2 |
| `UART2` / `U0` | `LINK_UART_U0` | `0x04` | 数据串口 0 |
| `UART3` / `U1` | `LINK_UART_U1` | `0x08` | 数据串口 1 |
每条 `LINK` 记录包含:启用状态、本地端口、远端 IP、远端端口、绑定的数据串口。外部配置命令见 `AT固件使用手册.md`
## 常用开发入口
| 目的 | 文件 |
|------|------|
| 上电初始化 | `Core/Src/main.c` |
| 任务和队列创建 | `Core/Src/freertos.c` |
| AT 命令与 Flash 参数 | `App/config.c``App/flash_param.c` |
| 路由消息池 | `App/route_msg.c` |
| UART DMA/IDLE、普通透传、MUX 帧 | `App/uart_trans.c` |
| TCP Server | `App/tcp_server.c` |
| TCP Client | `App/tcp_client.c` |
| 网络初始化和轮询 | `App/task_net_poll.c` |
| CH390 与 lwIP netif | `Drivers/CH390/*``Drivers/LwIP/src/netif/ethernetif.c` |
| RTT 日志封装 | `Core/Src/debug_log.c` |
## 调试提示
1. 启动阶段先看 RTT 中的 `debug_log_boot()` 里程碑,例如 `hal-init``clock-config``peripherals-ready``tasks-created``scheduler-start`
2. 网络阶段重点看 `NetPollTask``tcpip-init``netif-init``netif-ready``post-ready free/min heap` 日志。
3. TCP 或 UART 透传异常时,优先检查 `route_send()` 返回的 `pool``queue``invalid`,它们能区分消息池耗尽、队列满和参数错误。
4. HardFault、MemManage、BusFault、UsageFault 会进入 `Debug_TrapWithRttHint()`,应先保留 RTT 现场再修改代码。
5. `STM32F103RCT6` RAM 余量有限;如果 full-task 模式下问题呈现强资源相关特征,应优先核对堆、栈、水位和 lwIP 池配置,不要只看业务逻辑。
更详细的阅读和调试路线见 `项目代码阅读指南.md`
-208
View File
@@ -1,208 +0,0 @@
# TCP2UART 调试交接清单
## 1. 文档目的
本清单用于把当前 `TCP2UART` 工程的调试状态、已验证结论、后续动作建议一次性交接给下一位 agent 或开发者。
这份文档本身就可以当作下一位 agent 的工作 prompt 使用。
---
## 2. 先读哪些文档
接手本工程后,推荐按以下顺序阅读:
1. `交接清单.md` —— 当前状态、下一步、禁止回退到哪些旧假设
2. `工程调试指南.md` —— 已固化的调试经验与真实工程边界
3. `项目技术实现.md` —— 架构、任务模型、协议模型
4. `项目需求说明.md` —— 用户需求与协议要求
5. `AT固件使用手册.md` —— AT 命令与配置面
---
## 3. 当前工程状态(交接时刻)
### 3.1 当前平台与构建状态
1. 当前 Keil 工程目标仍是 `STM32F103RC`
2. 当前代码可以真实构建通过
3. 当前构建真值应查看:
- `MDK-ARM/build_capture.txt`
- `MDK-ARM/TCP2UART/TCP2UART.build_log.htm`
- `MDK-ARM/TCP2UART/TCP2UART.map`
4. 最近一次 Keil 构建示例:
- `Code=84560`
- `RW-data=432`
- `ZI-data=47056`
- `0 Error(s), 0 Warning(s)`
### 3.2 当前调试结论摘要
1. `DIAG_TASK_ISOLATION=1` 稳定
2. `DIAG_TASK_ISOLATION=0` 仍会卡死
3. 卡死边界已经从更早的启动阶段被推进到更靠后的 enabled `netconn_*` 路径
4.`RCT6` 上,四个 TCP task 创建后 `FreeRTOS heap` 仅剩约 `944 bytes`
5. 这说明当前 `RCT6` 上的资源余量已经严重干扰调试判断
6. 因此当前推荐策略是:**先换到 pin2pin 的 `STM32F103RDT6`,再继续 full-task 调试**
### 3.3 已做过并有信息量的改动/观察
以下工作已经做过,不要在没有新理由的情况下重复一遍:
1. 清理与恢复 `DIAG_TASK_ISOLATION=0`
2. 用真实 Keil 日志替代 viewer 作为构建真值
3. staged creation:将四个 TCP task 延后到 `netif-ready` 后创建
4. `lwIP netif` 初始化、post-init、post-ready 关键 RTT 日志
5. CH390 TX bounded wait / timeout 观察
6. TCP task 栈从 `256` 提高到 `384 words`
7. TCP task 入口 `hwm` 日志
8.`C1` 增加 one-shot first-connect defer discriminator
这些动作都让故障边界后移了,但仍未在 `RCT6` 上把问题彻底消灭。
---
## 4. 当前最可信的判断
当前最可信的判断不是“某一行代码单点必错”,而是:
1. `RCT6` 上的静态 RAM 占用与 FreeRTOS heap 余量都已经接近极限
2. disabled 的 task 可以持续打印,说明调度器与基础日志路径未整体死亡
3. enabled 的 `S1 / C1` 一旦进入真实 `netconn_*` 路径,就更容易触发新的运行期问题
4. 因此如果继续在 `RCT6` 上做更多 discriminator,很容易一直被资源边界噪声带偏
换句话说,**先把“内存极限平台”这个干扰项拿掉,再看逻辑问题还剩多少,是当前性价比最高的路线。**
---
## 5. 下一位 agent 现在应该做什么
### 5.1 第一目标
先完成 `STM32F103RCT6 -> STM32F103RDT6` 的工程切换,然后在**尽量不再改业务逻辑**的前提下复现当前版本。
### 5.2 为什么先这样做
因为下一位 agent 最先需要回答的,不是“立刻怎么修”,而是:
1. 换片后 full-task 模式是否还挂
2. 如果还挂,挂点是否后移
3. 换片后 `free/min heap` 是否显著改善
4. enabled `S1 / C1` 是否能进入更深的 `netconn_*` 路径
---
## 6. 下一位 agent 的推荐工作步骤
### Step 1:切换目标器件到 `STM32F103RDT6`
建议动作:
1. 更新 Keil target 里的 device 选择
2. 对齐 Flash / RAM 容量描述
3. 确认 linker / scatter / startup 相关目标描述与新器件一致
4. 再次真实构建,确认 `0 Error(s), 0 Warning(s)`
### Step 2:保持当前代码基线,直接上板复测
要求:
1. 不要一换片就继续改业务逻辑
2. 使用当前代码基线直接验证
3. 仍以 `build_capture.txt` 和 RTT 为主证据
### Step 3:收集第一轮换片后的关键证据
至少记录:
1. `netif-ready` 后是否仍卡死
2. `free/min heap` 变化
3. enabled `S1 / C1` 的新 RTT 行为
4. `C1 first-connect defer` 是否仍然影响故障边界
5. LED 心跳和 IWDG 表现是否变化
### Step 4:根据换片结果分流
#### 情况 A:换片后明显稳定 / 不再挂
说明:
1. RAM 压力是主要阻碍项之一
2. 后续需要回头做“资源预算收敛”,而不是继续把当前临时参数直接当最终方案
#### 情况 B:换片后仍挂,但更晚 / 更深
说明:
1. 当前逻辑问题仍在
2. 但已经去掉了最主要的资源噪声
3. 这时再围绕 `S1 / C1` 的真实 `netconn_new / bind / connect / listen / accept` 做最小日志/判别,会更有信息量
#### 情况 C:换片后几乎同点位仍挂
说明:
1. 主问题不再是单纯 RAM
2. 应优先检查 enabled 路径的 API 使用、阻塞行为、线程间交互与资源释放
---
## 7. 不要重复的方向
下一位 agent 接手后,除非有新证据,否则不要优先回到以下旧方向:
1. 单纯怀疑 startup/init 早期 bring-up
2. 把 viewer 当构建真值
3. 继续只靠加大 TCP task 栈来解释所有现象
4. 默认把 CH390 TX timeout 当成当前一号嫌疑
5.`RCT6` 上继续大量增加日志、队列、栈或临时 buffer
---
## 8. 关键文件
### 构建/目标
1. `MDK-ARM/TCP2UART.uvprojx`
2. `MDK-ARM/build_capture.txt`
3. `MDK-ARM/TCP2UART/TCP2UART.build_log.htm`
4. `MDK-ARM/TCP2UART/TCP2UART.map`
### 任务与运行期
1. `Core/Inc/FreeRTOSConfig.h`
2. `Core/Src/freertos.c`
3. `App/task_net_poll.c`
4. `App/tcp_server.c`
5. `App/tcp_client.c`
### 网络与驱动
1. `Drivers/LwIP/src/netif/ethernetif.c`
2. `Drivers/CH390/CH390.c`
3. `Drivers/CH390/CH390.h`
### 文档
1. `工程调试指南.md`
2. `项目技术实现.md`
3. `交接清单.md`
4. `CODING_PROMPT.md`
---
## 9. 可直接给下一位 agent 的 Prompt
下面这段文字可以直接作为下一位 agent 的起始 prompt
```text
请先阅读:`交接清单.md`、`工程调试指南.md`、`项目技术实现.md`。
当前项目是 STM32F103 + FreeRTOS + lwIP + CH390 的 TCP↔UART 透传工程。此前在 `STM32F103RCT6` 上调试时,`DIAG_TASK_ISOLATION=1` 稳定,`DIAG_TASK_ISOLATION=0` 仍会卡死,但故障边界已被多轮 discriminator 推进到 enabled 的 `netconn_*` 路径。当前最关键的资源事实是:在 `RCT6` 上 full-task 创建完四个 TCP 任务后,FreeRTOS heap 只剩约 944 bytes,静态 RAM 也已逼近物理上限,因此当前推荐先切换到 pin2pin 的 `STM32F103RDT6`,保持现有代码基线基本不变,先完成第一轮换片复测,再根据新器件上的 RTT、free/min heap 和 enabled `S1/C1` 行为决定下一步。
你的当前目标不是立刻修完所有问题,而是:
1. 完成 `RCT6 -> RDT6` 目标切换;
2. 用真实 Keil 日志确认构建通过;
3. 在新器件上复测当前代码,判断故障是否消失、后移或保持原状;
4. 仅在拿到新器件上的第一轮 RTT 后,再继续做最小化的下一步判别。
```
-450
View File
@@ -1,450 +0,0 @@
# TCP2UART 调试指导
## 1. 适用范围
本指导面向当前 `TCP2UART` 工程,覆盖以下四类调试场景:
1. `STM32F103RCT6 + CH390D` 的基础 bring-up
2. `SEGGER RTT`、异常陷阱与 FreeRTOS 任务运行状态确认
3. `USART1` 配置口、`USART2/USART3` 数据口与 `MUX / NET / LINK[idx]` 协议联调
4. `TCP Server / TCP Client / UART` 三层数据通路联调与问题隔离
本指导默认基线如下:
1. 当前工程采用 `FreeRTOS` 任务调度架构
2. `CH390` 运行时访问由 `xSpiMutex` 保护,`NetworkTask` 持有主要访问权
3. 调试输出统一使用 `SEGGER RTT`
4. 当前应用层协议模型已经收敛到 `MUX / NET / LINK[idx]`
5. 当前代码应以 `MDK-ARM` 工程构建结果为准
---
## 2. 当前工程边界与真实状态
在进入现场调试前,先统一以下工程边界:
1. 当前项目的主要软件路径已经切换为:
- `NET`:网络基础参数
- `LINK[idx]`:链路配置记录
- `MUX`:数据口承载模式
2. 对外 AT 配置面应只围绕以下命令展开:
- `AT` / `AT+?` / `AT+QUERY`
- `AT+MUX` / `AT+NET` / `AT+LINK`
- `AT+SAVE` / `AT+RESET` / `AT+DEFAULT`
3. 已有结论表明:
- MCU 启动、RTT、FreeRTOS 调度、TIM4 心跳路径可工作
- `CH390D` 基础寄存器读写与 `lwIP netif` 基本链路已经打通过一次
- 真实硬件侧曾定位到 `CH390D` 供电滤波电容虚焊问题
4. 当前调试重点是:
- FreeRTOS 任务是否正常创建与调度
- `MUX / NET / LINK[idx]` 协议是否与代码一致
- UART / TCP / CH390 三层通路是否协同稳定
- 参数保存、复位和恢复流程是否可靠
---
## 3. 代码入口与调试责任边界
### 3.1 启动与 FreeRTOS 入口
以下代码路径是 bring-up 的第一现场:
1. `Core/Src/main.c`
- `main()`:总启动入口
- `SystemClock_Config()`:时钟初始化
- `MX_FREERTOS_Init()`FreeRTOS 任务创建(在 `freertos.c` 中实现)
2. `Core/Src/freertos.c`
- `StartDefaultTask()`:默认任务(LED 心跳 + 看门狗)
- `MX_FREERTOS_Init()`:用户任务创建入口
3. `Core/Src/stm32f1xx_it.c`
- 故障与中断入口
- `TIM4_IRQHandler`HAL 时间基准
- `USART1/2/3``EXTI0`、DMA 回调等联调关键入口
### 3.2 CH390 责任边界
当前 CH390 调试必须遵守以下责任边界:
1. `Drivers/CH390/CH390_Interface.c`GPIO / SPI / 寄存器与内存事务
2. `Drivers/CH390/CH390.c`:芯片级 helper
3. `Drivers/CH390/ch390_runtime.c`:唯一的运行时拥有者
4. `Drivers/LwIP/src/netif/ethernetif.c`netif glue 与轮询桥接
5. SPI 访问由 `xSpiMutex` 保护,避免多任务竞争
### 3.3 配置口与业务口边界
1. `USART1`AT 配置口,接收 `AT` 命令
2. `USART2 / USART3`:数据口,普通透传或 MUX 承载
---
## 4. 当前硬件与调试工具基线
### 4.1 核心硬件对象
1. MCU`STM32F103RCT6`256KB Flash / 48KB SRAM
2. 以太网芯片:`CH390D`
3. 配置串口:`USART1`
4. 数据串口:`USART2 / USART3`
5. 调试输出:`SEGGER RTT`
### 4.2 构建与下载基线
1. `MDK-ARM/TCP2UART.uvprojx`
2. 启动文件:`startup_stm32f103xe.s`
3. 目标器件:`STM32F103RC`
4. 预处理器宏:`USE_HAL_DRIVER, STM32F103xE`
### 4.3 常用调试工具
1. `Keil MDK-ARM`
2. `ST-Link / J-Link`
3. `SEGGER RTT Viewer`
4. `PowerShell`
5. `tools/start_tcp_debug_server.ps1`
6. `tools/tcp_debug_server.py`
---
## 5. FreeRTOS 专项调试
### 5.1 任务状态检查
使用 `vTaskList` 获取所有任务运行状态:
```c
char buf[512];
vTaskList(buf);
SEGGER_RTT_WriteString(0, buf);
```
输出格式:
```text
任务名 状态 优先级 剩余栈 编号
NetworkTask R 4 120 1
UartTask B 4 200 2
ConfigTask B 3 150 3
RouteTask R 3 180 4
DefaultTask B 1 80 5
IDLE R 0 100 6
Tmr Svc B 2 90 7
```
状态码:`R`=Ready, `B`=Blocked, `S`=Suspended, `D`=Deleted, `I`=Invalid
### 5.2 栈溢出检测
已启用 `configCHECK_FOR_STACK_OVERFLOW = 2`,溢出时自动调用:
```c
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
SEGGER_RTT_printf(0, "STACK OVERFLOW: %s\n", pcTaskName);
__BKPT(0);
}
```
### 5.3 堆内存失败检测
已启用 `configUSE_MALLOC_FAILED_HOOK`,分配失败时自动调用:
```c
void vApplicationMallocFailedHook(void)
{
SEGGER_RTT_printf(0, "MALLOC FAILED: Free heap = %u\n", xPortGetFreeHeapSize());
__BKPT(0);
}
```
### 5.4 常见 FreeRTOS 调试陷阱
1. **优先级反转**:使用 Mutex(含优先级继承)而非 Binary Semaphore 保护共享资源
2. **死锁**:多 Mutex 场景确保所有任务按相同顺序获取
3. **中断优先级**FreeRTOS 可管理的 ISR 优先级必须 >= `configMAX_SYSCALL_INTERRUPT_PRIORITY`(本工程 5
4. **栈不足**:每个任务定期调用 `uxTaskGetStackHighWaterMark(NULL)` 检查剩余栈
5. **禁止在中断中调用阻塞 API**:必须使用 `FromISR` 后缀版本
---
## 6. 启动阶段调试顺序
建议按 P0 ~ P5 顺序推进,不要跳层。
### 6.1 P0:确认最小基础条件
1. `MDK-ARM` 可构建并产出新的 `axf/hex/map`
2. 板卡可正常下载与复位
3. RTT 可连接并看到启动输出
4. FreeRTOS 任务创建成功,`DefaultTask` LED 心跳可工作
5. `TIM4` 1ms tick 正常运行
### 6.2 P1:确认 FreeRTOS 调度正常
上电或复位后,优先确认:
1. `StartDefaultTask` 是否进入运行
2. `vTaskList` 输出是否显示所有预期任务
3. `xPortGetFreeHeapSize()` 返回值是否合理
4.`STACK OVERFLOW``MALLOC FAILED` 输出
### 6.3 P2:确认 CH390 初始化链路
启动阶段应重点关注 `NetworkTask` 中初始化日志:
1. `ETH init: gpio`
2. `ETH init: spi`
3. `ETH init: reset`
4. `ETH init: probe`
5. `ETH init: default`
6. `ETH init: mac`
7. `ETH init: done`
### 6.4 P3:确认 TCP 链路
1. lwIP `tcpip_thread` 是否正常运行
2. TCP Server 是否在指定端口监听
3. TCP Client 是否成功连接远端
### 6.5 P3.5:确认 ARP / ICMP 基础网络可达
在继续 TCP 联调前,建议先把 `ARP + ping(ICMP)` 跑通。对于当前 `CH390 + lwIP + FreeRTOS` 架构,这一步不是可选项,而是 TCP 可达之前必须成立的网络基线。
#### 6.5.1 推荐最小验证顺序
1. 先确认板卡 IP、掩码、MAC 与 PC 所在网段一致
2. 上电后先观察 RTT,确认 `ETH init: done` 已出现
3. 在 PC 侧执行一次 `ping <板卡IP>`,同时开启 Wireshark 抓包
4. 先看是否出现发往板卡 IP 的 ARP request,再看设备是否回 ARP reply
5. ARP 正常后,再看是否出现 ICMP echo request / echo reply 成对出现
#### 6.5.2 当前工程推荐观察点
如果网络基础链路有疑问,建议按以下分层观察,不要只看某一层:
1. **raw RX 层**CH390 是否确实收到了以太网帧
2. **Ethernet demux 层**`ethernet_input()` 是否识别到 `ETHTYPE_ARP` / `ETHTYPE_IP`
3. **协议处理层**`etharp_input()` / `ip_input()` 是否真正进入
4. **协议发包层**:lwIP 是否已经生成待发送的 ARP reply / ICMP reply
5. **驱动发送层**`low_level_output()` / CH390 TX 是否真正把帧送出
这次 bring-up 证明,`raw RX 正常` 并不等于 `lwIP 已真正处理该帧`。如果只看到底层收到了包,就直接假设协议栈一定会回复,通常会把排查方向带偏。
#### 6.5.3 这次 ARP / ICMP bring-up 的关键结论
本轮调试中,最终根因位于:
- `Drivers/LwIP/src/netif/ethernet.c`
问题本质是:
1. `ethernet_input()``ETHTYPE_ARP` 分支中,曾直接调用 `etharp_input(p, netif)`
2.`etharp_input()` 要求 `p->payload`**ARP 头** 开始,而不是从 Ethernet 头开始
3. 因此如果没有先执行:
```c
pbuf_remove_header(p, SIZEOF_ETH_HDR)
```
则 ARP 包虽然被收到了,但会在 `etharp_input()` 的早期校验中被静默丢弃,最终表现为:
1. Wireshark 能看到 PC 发来的 ARP request
2. 板子侧底层收包计数在增长
3. 但设备始终不回 ARP reply
4. ping 也自然不会成功
当前应保留的正确处理方式如下:
```c
case ETHTYPE_ARP:
if (netif->flags & NETIF_FLAG_ETHARP) {
if (pbuf_remove_header(p, SIZEOF_ETH_HDR)) {
pbuf_free(p);
return ERR_OK;
}
etharp_input(p, netif);
} else {
pbuf_free(p);
}
return ERR_OK;
```
#### 6.5.4 为什么这类问题容易漏看
这类问题常见但隐蔽,原因通常有三点:
1. 根因非常小,外在表现却像“整个发送链路都坏了”
2. 多个低层信号可能同时正常,容易误导为 SPI / TX / CH390 初始化问题
3. `rx ok``ARP 帧计数在涨``链路已 up` 都不代表协议层一定接受了该帧
因此,后续遇到“收得到包但就是不回”的问题时,优先检查:
1. 传给上层协议处理函数时,`pbuf->payload` 是否已经对齐到正确协议头
2. glue-layer 是否和 lwIP 原生调用约定一致
3. 观察点是否已经覆盖到 `demux -> protocol handler -> linkoutput` 这一整条链
#### 6.5.5 建议保留的最小验收标准
在认定网络基线“已经打通”之前,至少应满足:
1. Keil 工程可稳定构建通过
2. 上电后可稳定看到网络初始化完成日志
3. Wireshark 中能看到设备对本机 ARP request 做出 reply
4. PC 对设备 `ping` 时,能看到 ICMP echo request / reply 成对出现
5. RTT 中无 `STACK OVERFLOW``MALLOC FAILED`、异常 trap 等故障信号
---
## 7. MUX / NET / LINK[idx] 联调指导
### 7.1 协议总则
与裸机版本完全一致,参见 `AT固件使用手册.md`
### 7.2 推荐最小 MUX 联调顺序
1. 先在 `MUX=0` 下跑通原始透传
2. 再切换 `MUX=1`
3. 先发一个控制帧,确认 `DSTMASK=0x00` 路径可通
4. 再发一个单目标数据帧
5. 最后验证多目标位图转发
---
## 8. 异常、卡死与假死排查
### 8.1 看到 `TRAP:` 时怎么做
1. 先记录 RTT 中的 trap 标签
2. 立刻用调试器查看当前 PC / LR / 调用栈
3. 结合 `Core/Src/stm32f1xx_it.c` 中对应 handler 定位异常类型
### 8.2 FreeRTOS 任务卡死时怎么做
1. 使用 `vTaskList` 检查各任务状态
2. 如果某个任务始终 `B`(Blocked),检查其等待的队列/信号量
3. 检查是否有 Mutex 被持有但从未释放
4. 使用调试器暂停,查看各任务的调用栈
### 8.3 常见 FreeRTOS 陷阱
1. 在 ISR 中误调用阻塞 API(如 `xQueueSend` 而非 `xQueueSendFromISR`
2. 中断优先级低于 `configMAX_SYSCALL_INTERRUPT_PRIORITY` 但调用了 FreeRTOS API
3. Mutex 持有期间任务被删除导致 Mutex 永不释放
4. 栈溢出导致邻近变量被破坏
---
## 9. 常见误区
1. 不要继续沿用"CH390 恒为全 `0xFF`"过时结论
2. 不要在多个任务中直接访问 CH390 SPI(必须通过 Mutex 保护)
3. 不要在没有芯片脚侧证据前,只凭 GPIO 判断总线正常
4. 不要在基础寄存器读写尚不可信时,直接调高层业务
5. 不要在 ISR 中执行复杂 SPI 事务或调用阻塞 API
6. 不要忽视 `configCHECK_FOR_STACK_OVERFLOW` 报告
7. 不要把“底层已经收到 ARP 包”等同于“lwIP 一定已经正确处理 ARP 包”
8. 不要忽略 glue-layer 对 `pbuf->payload` 起始位置的约定,特别是 `Ethernet header -> ARP/IP header` 的切换
9. 不要在 ARP / ICMP 还没闭环前,就直接怀疑 TCP Server / TCP Client 逻辑
10. 不要在没有抓包和分层观测点的情况下,只凭单一日志就断言故障位于 TX 或 SPI 层
---
## 10. 推荐配套阅读
1. `AT固件使用手册.md`
2. `项目技术实现.md`
3. `项目需求说明.md`
4. `Keil工程配置说明.txt`
---
## 11. 近期调试经验固化(2026-04
本节用于固化这一轮 `FreeRTOS + lwIP + CH390` 联调过程中已经验证过的经验,后续调试默认以本节为前提,不要反复回到已排除的旧假设。
### 11.1 构建结果的真值来源
当前工程的构建真值必须以 `Keil` 实际构建日志为准,而不是辅助 viewer。
推荐优先级如下:
1. `MDK-ARM/build_capture.txt`
2. `MDK-ARM/TCP2UART/TCP2UART.build_log.htm`
3. `MDK-ARM/TCP2UART/TCP2UART.map`
说明:
1. `keil-build-viewer.exe` 只能辅助观察内存占用或旧构建快照
2. viewer 未刷新时,不代表当前代码没有变化,往往只是最新构建没成功或 viewer 数据滞后
3. 后续任何“编译通过 / RAM 变化 / 目标器件切换”的判断,都必须引用以上三类 Keil 真实产物
### 11.2 近期已验证的事实
以下结论已经被本轮调试反复验证:
1. `DIAG_TASK_ISOLATION=1` 时,系统可以稳定运行
2. `DIAG_TASK_ISOLATION=0` 时,full-task 模式仍会卡死
3. 启动阶段 `lwip_netif_init()`、deferred `xTaskCreate()``netif-ready` 等关键日志已经真实跑通
4. 之前通过加日志、分阶段创建 TCP 任务、调整栈大小后,卡死边界会继续后移,但问题不会完全消失
5. 这说明当前问题不是单一固定点故障,而是“逻辑路径 + 极低资源余量”共同作用的结果
### 11.3 已排除或降级优先级的方向
以下方向目前不再应作为一号假设:
1. **startup/init 失败**
- 当前日志已能稳定走到 `netif-ready`
- 因此主要问题不再是最初的 bring-up 链路本身
2. **单纯 CH390 TX 无限等待是当前主因**
- 已为 TX 路径增加过 bounded wait / timeout 观察点
- 最新故障没有先落到 `[ETH] tx timeout` 分支
3. **只靠继续增大 TCP task 栈就能解决问题**
- `256 -> 384 words` 后,故障边界虽然继续后移,但系统仍会卡死
- 栈确实曾是问题的一部分,但不是唯一剩余问题
### 11.4 当前最重要的资源压力结论
当前 `STM32F103RCT6`(48KB SRAM)上的真实资源压力已经高到会直接干扰调试判断:
1. 真实构建中,`ZI-data` 已接近物理 RAM 上限(约 95%+)
2.`DIAG_TASK_ISOLATION=0` 下,创建完四个 TCP 任务后,`xPortGetFreeHeapSize()` 只剩约 `944 bytes`
3. 这个余量不足以让后续 `netconn_*`、semaphore、mailbox、任务栈回旋空间保持清晰边界
4. 因此当前在 `RCT6` 上继续做 discriminator 时,结果会持续混入“资源边界噪声”
这也是为什么本轮调试后半段已经把重点从“继续在 RCT6 上做更多小修补”切换到“先换更大 RAM 的 pin2pin 器件再继续分析”。
### 11.5 当前推荐的硬件调试策略
当前推荐的下一阶段策略如下:
1. 先从 `STM32F103RCT6` 切换到 pin2pin 的 `STM32F103RDT6`
2. 在切换器件后,尽量保持当前代码基线不做大改
3. 先复测当前版本的 RTT 与运行边界,看故障是否:
- 消失
- 明显后移
- 仍然停在相同 enabled path
4. 再根据新器件上的表现,区分:
- 资源压力主导
- 逻辑 / 时序 / API 使用问题仍然存在
### 11.6 换片后第一轮调试目标
切换到 `RDT6` 后,第一轮调试不追求立刻修复,而是优先回答下面几个问题:
1. 当前 full-task 模式是否仍然卡死
2. `free/min heap` 是否明显高于 `RCT6` 版本
3. enabled 的 `S1 / C1` 是否能够继续进入更深的 `netconn_new / bind / connect / listen / accept` 路径
4. 之前为了定位问题而加入的 discriminator(例如 `C1 first-connect defer`)是否仍然影响故障边界
如果这些问题不先回答,后续继续改代码的结论可信度会很差。
### 11.7 当前建议保留的调试原则
后续 agent 或开发者继续接手本工程时,建议遵守以下原则:
1. 不要在 `RCT6` 上继续大量加日志、加栈、加 queue 深度后再试图解释现象
2. 不要把 viewer 当作当前构建真值
3. 不要忽略 `DIAG_TASK_ISOLATION=1 正常、=0 异常` 这个前提
4. 不要一次性修改 `C1/S1/CH390/lwIP` 多个方向,避免再次失去因果关系
5. 每次只做一个能明显改变故障边界的最小改动,并用 RTT + Keil build 结果交叉验证
+449
View File
@@ -0,0 +1,449 @@
# TCP2UART 项目代码阅读指南
## 1. 文档目的
本文面向接手 `TCP2UART` 的固件开发、联调和调试人员,目标是说明当前代码如何组织、启动后如何运行、TCP 与 UART 数据如何流动,以及应该按什么顺序阅读源码。
本文只描述当前工程的长期有效结构。历史调试交接文档中的某些“下一步动作”属于当时现场状态,不再作为当前项目入口。
## 2. 系统边界
### 2.1 硬件边界
- MCU`STM32F103RCT6`
- 以太网芯片:`CH390D`
- 配置口:`USART1`
- 数据口:`USART2``USART3`
- 调试输出:`SEGGER RTT`
### 2.2 软件边界
- 工程:`MDK-ARM/TCP2UART.uvprojx`
- 调度:`FreeRTOS`
- 网络:`lwIP NO_SYS=0 + netconn API`
- 网络输入:`tcpip_thread` + `ethernetif` + CH390 驱动
- 业务链路:2 路 TCP Server2 路 TCP Client2 路 UART 数据口
- 配置协议:`AT``MUX``NET``LINK``BAUD``SAVE``RESET``DEFAULT`
### 2.3 目录结构
```text
App/
app_runtime.h 全局任务、队列、信号量声明
config.c/.h AT 命令、运行配置、默认值、Flash 保存
flash_param.c/.h Flash 参数读写与 CRC
route_msg.c/.h 固定消息池和路由消息封装
task_net_poll.c/.h lwIP/CH390 初始化、netif ready、网络重启
tcp_server.c/.h S1/S2 TCP Server 任务
tcp_client.c/.h C1/C2 TCP Client 任务
uart_trans.c/.h USART2/3 业务数据接收、发送、MUX 编解码
Core/Inc, Core/Src/
main.c 上电入口和外设初始化顺序
freertos.c FreeRTOS 队列、信号量、任务创建
stm32f1xx_it.c 中断入口,尤其是 UART IDLE 和 CH390 EXTI
usart.c/dma.c/... STM32CubeMX 生成的外设初始化
debug_log.c/.h RTT 日志和异常提示
Drivers/CH390/
CH390.c/.h 芯片级寄存器/辅助操作
CH390_Interface.c SPI/GPIO 与 CH390 事务封装
Drivers/LwIP/
src/include/arch/lwipopts.h 当前 lwIP 配置
src/netif/ethernetif.c CH390 与 lwIP netif 胶水层
port/sys_arch.c lwIP 在 FreeRTOS 上的 sys_arch 适配
```
## 3. 总体架构
```text
+------------------------------------------------------+
| AT / Control Plane |
| USART1 IDLE DMA -> ConfigTask -> config_process_at_cmd|
+------------------------------------------------------+
| Configuration Model |
| MUX / NET / LINK[S1,S2,C1,C2] / BAUD |
+------------------------------------------------------+
| Routing Layer |
| route_msg fixed pool + xTcpRxQueue + xLinkTxQueues[] |
+------------------------------------------------------+
| Data Tasks |
| UartRxTask + TcpSrvTask_S1/S2 + TcpCliTask_C1/C2 |
+------------------------------------------------------+
| Network Runtime |
| NetPollTask + tcpip_thread + ethernetif + CH390 |
+------------------------------------------------------+
| HAL / DMA / IRQ |
| USART1/2/3 DMA+IDLE, SPI1, EXTI0, TIM4 timebase |
+------------------------------------------------------+
```
这套架构的核心思想是:TCP 任务和 UART 任务不直接互相调用,而是通过 `route_msg_t` 和 FreeRTOS 队列传递数据。这样可以把“从哪里来”“发到哪里去”“数据多长”统一表示,便于普通透传和 MUX 模式共用同一套路由机制。
## 4. 启动流程
启动入口在 `Core/Src/main.c``main()`
1. `HAL_Init()` 初始化 HAL、Flash 接口和基础 tick。
2. `debug_log_init()` 初始化 RTT 日志,并输出 `hal-init`
3. `SystemClock_Config()` 配置系统时钟。
4. 初始化外设:`MX_GPIO_Init()``MX_DMA_Init()``MX_USART1_UART_Init()`
5. `config_init()` 从 Flash 读取配置;读取失败则 `config_set_defaults()`
6. `ApplyConfiguredUartBaudrates()` 根据配置设置 `USART2/USART3` 波特率。
7. 初始化 `USART2``USART3``SPI1`
8. 初始化 LED 并执行 `CH390_HardwareReset()`
9. `osKernelInitialize()` 初始化 RTOS 内核。
10. `MX_FREERTOS_Init()` 创建队列、信号量和基础任务。
11. `osKernelStart()` 启动调度器。
调试启动问题时,优先按 RTT boot 日志确认流程卡在哪个里程碑。
## 5. FreeRTOS 对象和任务
`Core/Src/freertos.c` 是理解运行时结构的关键文件。
### 5.1 全局对象
| 对象 | 类型 | 用途 |
|------|------|------|
| `xNetSemaphore` | Binary Semaphore | `EXTI0` 通知网络轮询任务处理 CH390 事件 |
| `xTcpRxQueue` | Queue | TCP 任务收到网络数据后投递给 `UartRxTask` |
| `xConfigQueue` | Queue | `USART1` AT 命令或 MUX 控制帧投递给 `ConfigTask` |
| `xLinkTxQueues[4]` | Queue | UART 收到的数据投递给指定 S1/S2/C1/C2 TCP 任务 |
| `route_msg` pool | 固定池 | 避免每包动态分配,最大载荷见 `ROUTE_MSG_MAX_PAYLOAD` |
### 5.2 基础任务
`MX_FREERTOS_Init()` 固定创建:
| 任务 | 入口 | 职责 |
|------|------|------|
| `defaultTask` | `StartDefaultTask()` | LED 心跳、看门狗 |
| `NetPoll` | `NetPollTask()` | 初始化 lwIP/netif,轮询或响应 CH390 中断,网络 ready 后启动 TCP 任务 |
| `UartRx` | `UartRxTask()` | 处理 USART2/3 RX,执行普通透传或 MUX 路由 |
| `Config` | `ConfigTask()` | 处理 AT 命令并回复 |
`DIAG_TASK_ISOLATION` 打开时,`UartRx``Config` 会被隔离,这只用于调试,不代表正常产品形态。
### 5.3 网络任务
网络任务不是在 `MX_FREERTOS_Init()` 里立即全部创建,而是在 `NetPollTask()` 完成 `tcpip_init()``lwip_netif_init()` 并设置 `g_netif_ready = pdTRUE` 后,由 `app_start_network_tasks()` 按配置创建:
| 任务 | 条件 | 职责 |
|------|------|------|
| `TcpSrvS1` | `LINK[S1].enabled` | 监听 S1 本地端口,收发 Server 数据 |
| `TcpSrvS2` | `LINK[S2].enabled` | 监听 S2 本地端口,收发 Server 数据 |
| `TcpCliC1` | `LINK[C1].enabled` | 主动连接 C1 远端,断线重连 |
| `TcpCliC2` | `LINK[C2].enabled` | 主动连接 C2 远端,断线重连 |
这种延迟创建能避免 TCP 任务在 netif 尚未就绪时进入 `netconn_*` 路径。
## 6. 配置模型
配置结构定义在 `App/config.h``device_config_t`
### 6.1 端点编码
| 端点 | 代码宏 | 编码 |
|------|--------|------|
| `C1` | `ENDPOINT_C1` | `0x01` |
| `C2` | `ENDPOINT_C2` | `0x02` |
| `UART2` / `U0` | `ENDPOINT_UART2` | `0x04` |
| `UART3` / `U1` | `ENDPOINT_UART3` | `0x08` |
| `S1` | `ENDPOINT_S1` | `0x10` |
| `S2` | `ENDPOINT_S2` | `0x20` |
`SRCID` 是单一源端点;`DSTMASK` 是目标端点位图。`DSTMASK=0x00` 专用于系统控制帧,最终进入 AT 解析路径。
### 6.2 LINK 模型
4 条链路固定为:
| 角色 | 内部索引 | 默认作用 |
|------|----------|----------|
| `S1` | `CONFIG_LINK_S1` | TCP Server 1 |
| `S2` | `CONFIG_LINK_S2` | TCP Server 2 |
| `C1` | `CONFIG_LINK_C1` | TCP Client 1 |
| `C2` | `CONFIG_LINK_C2` | TCP Client 2 |
每条 `LINK` 包含:
```text
EN,LPORT,RIP,RPORT,UART
```
`UART``U0``U1`,分别对应 `USART2``USART3`
### 6.3 默认配置
当前默认值由 `config_set_defaults()` 写入:
- `MUX=0`,即普通透传模式。
- `NET=192.168.31.100,255.255.255.0,192.168.31.1,00:00:00:00:00:00`
- `UART2/USART2``UART3/USART3` 默认波特率为 `115200`
- `reconnect_interval_ms=3000`
完整 AT 命令格式以 `AT固件使用手册.md` 为准。
## 7. AT 配置数据流
AT 配置口固定使用 `USART1`
```text
上位机 AT 文本
-> USART1 DMA 接收
-> USART1 IDLE 中断
-> config_uart_idle_handler()
-> route_send_from_isr(xConfigQueue, ROUTE_CONN_UART1, ...)
-> ConfigTask
-> config_process_at_cmd()
-> config_respond_to_uart()
-> USART1 DMA 发送响应
```
`config_process_at_cmd()` 当前支持:
- `AT`
- `AT+?` / `AT+QUERY`
- `AT+SAVE`
- `AT+RESET`
- `AT+DEFAULT`
- `AT+MUX?` / `AT+MUX=0|1`
- `AT+NET?` / `AT+NET=IP,MASK,GW,MAC`
- `AT+LINK?` / `AT+LINK=ROLE,...` / `AT+LINK=ROLE`
- `AT+BAUD?` / `AT+BAUD=U0|U1,baudrate`
修改网络、链路、MUX 或波特率后,代码会返回 `AT_NEED_REBOOT``ConfigTask` 会追加提示:`Use AT+SAVE then AT+RESET to apply changes`
## 8. 业务数据流示例:无协议透传
本节用一个最常见场景说明数据如何流动。
### 8.1 场景配置
假设现场需要“电脑 TCP 客户端连到设备,数据直接从 `USART2` 输出;`USART2` 收到的数据再原样回到 TCP 客户端”。可以配置:
```text
AT+MUX=0
AT+LINK=S1,1,8080,0.0.0.0,0,U0
AT+SAVE
AT+RESET
```
含义:
- 普通透传模式,不使用 MUX 帧。
- 启用 `S1` TCP Server。
- `S1` 监听本地 `8080` 端口。
- `S1` 绑定 `U0`,也就是 `USART2`
### 8.2 TCP 到 UART
当远端 TCP 客户端连接 `S1:8080` 并发送字节,例如 `01 02 03`
```text
远端 TCP 客户端
-> CH390D 收包
-> ethernetif / lwIP
-> TcpSrvTask_S1
-> tcp_server_worker()
-> netconn_recv()
-> route_send(xTcpRxQueue, src=S1, dst=UART2, data=01 02 03)
-> UartRxTask
-> uart_trans_send_buffer(UART_CHANNEL_U0, data)
-> USART2 DMA TX
-> 外部串口设备收到 01 02 03
```
关键代码位置:
- `App/tcp_server.c``tcp_server_worker()``netconn_recv()``netbuf`,再投递到 `xTcpRxQueue`
- `App/uart_trans.c``UartRxTask()``xTcpRxQueue` 取消息,按 `dst_mask` 发送到 `USART2/USART3`
### 8.3 UART 到 TCP
当外部串口设备向 `USART2` 发送 `A1 B2 C3`
```text
外部串口设备
-> USART2 DMA RX
-> USART2 IDLE 中断
-> uart_trans_notify_rx_from_isr(UART_CHANNEL_U0)
-> UartRxTask
-> uart_route_raw_channel(U0)
-> 查找所有 enabled 且绑定 U0 的 LINK
-> route_send(xLinkTxQueues[S1], src=UART2, dst=S1, data=A1 B2 C3)
-> TcpSrvTask_S1
-> xQueueReceive(xLinkTxQueues[S1])
-> netconn_write()
-> lwIP / CH390D
-> 远端 TCP 客户端收到 A1 B2 C3
```
普通透传模式下,`uart_route_raw_channel()` 会把一个 UART 上收到的数据复制给所有“已启用且绑定该 UART”的链路。因此如果 `S1``C2` 都绑定 `U0` 且都启用,`USART2` 的一段输入会分别投递到 `xLinkTxQueues[S1]``xLinkTxQueues[C2]`
## 9. MUX 模式数据流
MUX 模式由 `AT+MUX=1` 开启。帧格式在 `App/uart_trans.c` 中由 `uart_mux_try_extract_frame()``uart_mux_encode_frame()` 实现:
```text
SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
0x7E | len high | len low | source | destinations | bytes | 0x7F
```
处理规则:
1. `DSTMASK=0x00`:系统控制帧,`PAYLOAD` 作为 AT 文本进入 `xConfigQueue`
2. `DSTMASK` 包含 `S1/S2/C1/C2`:投递到对应 `xLinkTxQueues[]`
3. `DSTMASK` 包含另一个 UART:编码成新的 MUX 帧并转发到另一个数据口。
4. TCP 到 UART 时,如果目标是 UART 且当前处于 MUX 模式,会带上源端点并编码成 MUX 帧输出。
MUX 模式适合多个 TCP 实例共享一个数据口,或者上位机需要明确指定数据发往哪个逻辑端点的场景。
## 10. 网络初始化和 CH390 路径
网络运行入口是 `App/task_net_poll.c``NetPollTask()`
1. 调用 `tcpip_init(NULL, NULL)` 创建 lwIP 内核线程。
2.`config_get()` 中的 `NET` 参数构造 `ipaddr/netmask/gateway`
3. 调用 `lwip_netif_init()` 初始化 netif 和 CH390 glue。
4. 初始化成功后设置 `g_netif_ready = pdTRUE`
5. 调用 `app_start_network_tasks()` 创建启用的 TCP Server/Client 任务。
6. 主循环中等待 `xNetSemaphore` 或周期轮询,驱动 `ethernetif_poll()`,并响应网络重启请求。
底层路径主要在:
- `Drivers/CH390/CH390_Interface.c`:SPI、CS、寄存器和 FIFO 访问。
- `Drivers/CH390/CH390.c`:芯片级 helper。
- `Drivers/LwIP/src/netif/ethernetif.c`CH390 与 lwIP netif 的桥接。
- `Drivers/LwIP/src/include/arch/lwipopts.h`lwIP 池、线程、core locking 配置。
当前关键 lwIP 配置包括:
- `NO_SYS=0`
- `LWIP_NETCONN=1`
- `LWIP_TCPIP_CORE_LOCKING=1`
- `LWIP_TCPIP_CORE_LOCKING_INPUT=1`
- `PBUF_POOL_SIZE=8`
- `MEMP_NUM_PBUF=8`
- `MEMP_NUM_TCPIP_MSG_INPKT=8`
- `LWIP_NETCONN_SEM_PER_THREAD=1`
## 11. 推荐阅读路线
### 11.1 只想理解系统怎么跑
1. `README.md`
2. 本文第 3、4、5、8 节
3. `Core/Src/main.c`
4. `Core/Src/freertos.c`
### 11.2 要改 AT 命令或默认参数
1. `AT固件使用手册.md`
2. `App/config.h`:结构体、默认值、端点编码。
3. `App/config.c`:解析、保存、响应。
4. `App/flash_param.c`Flash 存储。
### 11.3 要改 TCP 或串口透传
1. 本文第 8、9 节。
2. `App/route_msg.c`:先理解消息生命周期。
3. `App/uart_trans.c`UART RX/TX、普通透传、MUX。
4. `App/tcp_server.c``App/tcp_client.c`:网络收发。
### 11.4 要查网络底层问题
1. `App/task_net_poll.c`
2. `Drivers/LwIP/src/netif/ethernetif.c`
3. `Drivers/CH390/CH390_Interface.c`
4. `Drivers/LwIP/src/include/arch/lwipopts.h`
5. RTT 日志和抓包结果一起看,不要只看单侧现象。
## 12. 调试指南
### 12.1 启动阶段
先看 RTT boot 日志:
```text
hal-init
clock-config
peripherals-ready
config-ready
uart-trans-init
tasks-created
freertos-init
scheduler-start
```
如果停在 `scheduler-start` 前,优先看外设初始化、CH390 reset、RTOS 对象创建断言。如果进入任务后异常,再看 `NetPollTask``ConfigTask``UartRxTask` 的 task-entry 日志。
### 12.2 网络阶段
关键日志点:
- `[NET] tcpip-init enter/exit`
- `[NET] netif-init enter/exit`
- `[NET] post-init ok=... hwm=... free=... min=...`
- `[NET] start-network-tasks call`
- `[NET] netif-ready`
如果 netif 未 ready,不要先查 TCP 业务任务;应先查 CH390、SPI、netif 初始化和 lwIP 配置。
### 12.3 路由阶段
`route_send_result_to_str()` 的返回值很重要:
| 返回 | 含义 | 常见方向 |
|------|------|----------|
| `invalid` | 参数或长度非法 | 检查 `dst_mask`、payload 长度 |
| `pool` | `route_msg` 固定池耗尽 | 检查消费者任务是否卡住、队列是否积压 |
| `queue` | 目标队列满 | 检查对应 TCP/UART 任务是否还在运行 |
### 12.4 资源约束
`STM32F103RCT6` 的 SRAM 余量有限,而 FreeRTOS、lwIP、多个 TCP 任务、UART ring buffer 和消息池都会消耗 RAM。遇到 full-task 模式下的非确定性问题时,应同时记录:
- `xPortGetFreeHeapSize()`
- `xPortGetMinimumEverFreeHeapSize()`
- `uxTaskGetStackHighWaterMark()`
- lwIP pbuf/memp 池配置
- 哪些 `LINK` 被启用
如果问题随任务数量、池大小或启用链路数量明显移动,先按资源问题分析,不要急着给业务路径加补丁。
### 12.5 历史 CH390/lwIP pbuf 泄漏教训
历史上曾出现“设备能成功 ping 固定次数,随后不再回应”的问题。现象随 `PBUF_POOL_SIZE` 从 8 改到 16 而从 8 次移动到 16 次,最终定位到 lwIP 输出路径中 pbuf 引用计数未释放这一类问题。
这个案例的长期价值不是记住某个临时修改,而是调试方法:
1. 如果失败次数精确跟池大小相关,优先怀疑引用计数或释放路径。
2. 扩大池只能延迟问题,不能当根修复。
3. 抓包、RTT、lwIP 统计和代码引用计数要一起看。
## 13. 修改代码时的边界
1. 不要绕过 `route_msg` 和队列直接让 TCP/UART 任务互相调用。
2. ISR 中只做通知或投递,不做阻塞等待。
3. `netconn_*` 只在网络任务语境中使用,注意每个线程的 `netconn_thread_init()` / `netconn_thread_cleanup()`
4. 改 AT 命令时同步更新 `AT固件使用手册.md`
5. 改 MUX 帧格式或端点编码时,同步检查 `App/config.h``App/uart_trans.c``AT固件使用手册.md`
6. 改 lwIP 池大小时,同步记录 RAM、heap、水位和实际业务链路数量。
## 14. 术语速查
| 术语 | 含义 |
|------|------|
| `U0` | `USART2` 数据口 |
| `U1` | `USART3` 数据口 |
| `S1/S2` | TCP Server 实例 |
| `C1/C2` | TCP Client 实例 |
| `MUX=0` | 普通透明透传 |
| `MUX=1` | 带 `SRCID/DSTMASK` 的帧化透传 |
| `xTcpRxQueue` | TCP 到 UART 的队列 |
| `xLinkTxQueues[]` | UART 到 TCP 的队列数组 |
| `xConfigQueue` | AT 命令队列 |
| `NetPollTask` | 网络初始化、轮询和恢复任务 |
-638
View File
@@ -1,638 +0,0 @@
# TCP2UART 项目技术实现
## 一、文档目的
本文档描述 `TCP2UART` 项目基于 `STM32F103RCT6 + FreeRTOS` 的最终内部实现口径。
本文档只围绕最终协议模型展开:
- `MUX`:串口承载层
- `NET`:全局网络配置层
- `LINK[idx]`:实例配置与连接管理层
不再保留历史 `S1... / C1...` 外部字段模型。
## 二、当前工程基础
当前工程基础约束如下:
1. MCU`STM32F103RCT6`256KB Flash / 48KB SRAM
2. 网络芯片:`CH390D`
3. 软件架构:`FreeRTOS + lwIP NO_SYS=0`
4. 协议栈:`lwIP socket/netconn API`
5. 调试输出:`SEGGER RTT`
6. 使用 `FreeRTOS` 任务调度
7. 不实现 DHCP
## 三、总体架构
```text
+--------------------------------------------------+
| AT / Control Plane |
| USART1 AT parser + MUX control frame parser |
+--------------------------------------------------+
| Configuration Model |
| MUX / NET / LINK[idx] |
+--------------------------------------------------+
| FreeRTOS Tasks |
| NetworkTask / UartTask / ConfigTask / RouteTask |
+--------------------------------------------------+
| Inter-Task Communication |
| Queue / Semaphore / Mutex / StreamBuffer |
+--------------------------------------------------+
| lwIP TCP/IP Stack (NO_SYS=0) |
| tcpip_thread + socket/netconn + sys_arch |
+--------------------------------------------------+
| Driver Layer |
| CH390 / lwIP netif / UART DMA+IDLE / HAL |
+--------------------------------------------------+
```
## 四、FreeRTOS 任务设计(路径 Anetconn + 多 TCP 任务)
### 4.1 架构路线
本项目采用 **路径 A**`NO_SYS=0 + netconn API + 每个 TCP 连接独立任务`
核心决策:
1. lwIP 以 `NO_SYS=0` 模式运行,`tcpip_thread` 由 lwIP 自动创建
2. TCP 连接使用 `netconn` 阻塞 API`netconn_accept` / `netconn_recv` / `netconn_write`
3. 每个 TCP Server 和 Client 实例各占一个独立任务
4. 任务间通过 Queue 传递指针 + 元数据描述符,实现零拷贝
### 4.2 任务列表(共 9 个任务 + 1 个 lwIP 自建)
| 任务名 | 优先级 | 栈(words) | 模式 | 职责 |
|--------|--------|-----------|------|------|
| `tcpip_thread` | 6 (最高) | 512 | 阻塞 | lwIP 内核线程(自动创建) |
| `NetPollTask` | 5 | 384 | 事件驱动 | `ethernetif_poll` + 链路检测 |
| `TcpSrvTask_S1` | 4 | 384 | 阻塞 | `netconn_accept` + S1 收发 |
| `TcpSrvTask_S2` | 4 | 384 | 阻塞 | `netconn_accept` + S2 收发 |
| `TcpCliTask_C1` | 4 | 256 | 阻塞 | `netconn_connect` + C1 收发 |
| `TcpCliTask_C2` | 4 | 256 | 阻塞 | `netconn_connect` + C2 收发 |
| `UartRxTask` | 4 | 384 | 事件驱动 | UART DMA/IDLE 接收 + MUX 帧提取 |
| `ConfigTask` | 2 | 256 | 阻塞 | AT 命令解析与响应 |
| `DefaultTask` | 1 | 128 | 周期 | LED 心跳 + IWDG 喂狗 |
说明:
- `tcpip_thread` 是 lwIP 自建的内核线程,处理所有协议栈内部事件
- 所有 `netconn_*` API 通过 `tcpip_thread` 消息机制实现线程安全
- `LWIP_TCPIP_CORE_LOCKING=1` 允许应用任务直接调用 `netconn_write` 而不经邮箱中转
- `tcpip_thread` 优先级最高(6),确保 TCP ACK 和超时处理不被延迟
### 4.3 零拷贝路由消息设计
```c
/* 路由消息描述符 - Queue 传递的是此结构的指针,不拷贝负载数据 */
typedef struct {
uint8_t src_id; /* 源端点 ID */
uint8_t dst_mask; /* 目标端点位图 */
uint16_t len; /* 数据长度 */
uint8_t conn_type; /* 连接标识:LINK_S1/S2/C1/C2 */
uint8_t *data; /* 指向预分配静态缓冲区 */
} route_msg_t;
```
静态缓冲池(预分配,避免动态分配):
```c
#define ROUTE_BUF_COUNT 4
#define ROUTE_BUF_SIZE 512
static uint8_t g_route_buf_pool[ROUTE_BUF_COUNT][ROUTE_BUF_SIZE];
static volatile uint8_t g_route_buf_used[ROUTE_BUF_COUNT];
```
- 发送方:从池中获取空闲缓冲区,拷贝数据,填充 `route_msg_t`Queue 发送指针
- 接收方:从 Queue 取 `route_msg_t*`,处理数据后标记缓冲区为可用
- 无动态分配,无堆碎片
### 4.4 任务间通信机制
```text
UART ISR ──[TaskNotify]──> UartRxTask ──[Queue*]──> TcpSrvTask / TcpCliTask
│ ▲
├──[Queue*]──────────────>─┘
DSTMASK=0 └──[Queue]──> ConfigTask
TcpSrvTask / TcpCliTask ──[Queue*]──> UartRxTask (UART TX 方向)
EXTI0 ISR ──[BinarySem]──> NetPollTask ──> ethernetif_poll ──> tcpip_thread
```
通信对象:
| 对象 | 类型 | 生产者 | 消费者 | 用途 |
|------|------|--------|--------|------|
| `xNetSemaphore` | Binary Semaphore | EXTI0 ISR | NetPollTask | CH390 中断通知 |
| `xUartRxNotify` | TaskNotification | UART IDLE ISR | UartRxTask | UART 接收通知 |
| `xTcpRxQueue` | Queue (16, route_msg_t*) | TCP 任务 | UartRxTask | TCP→UART 数据 |
| `xUartTxQueue` | Queue (8, route_msg_t*) | UartRxTask | TCP 任务 | UART→TCP 数据 |
| `xConfigQueue` | Queue (8, char*) | UartRxTask | ConfigTask | AT 文本 |
说明:
- TCP→UART 方向:TCP 任务调用 `netconn_recv` 获取数据,构造 `route_msg_t` 指针投递到 `xTcpRxQueue`UartRxTask 取出后写入 UART DMA
- UART→TCP 方向:UartRxTask 从 UART DMA 读取数据,构造 `route_msg_t` 指针投递到 `xUartTxQueue`,对应 TCP 任务取出后调用 `netconn_write` 发送
- 路由逻辑内联在 UartRxTask 和各 TCP 任务中,不设独立 RouteTask
### 4.5 NetPollTask 实现
```c
void NetPollTask(void *argument)
{
/* 初始化 CH390 + lwIP netif */
ethernetif_init(&ch390_netif);
/* 等待链路就绪后启动 TCP 任务 */
/* ... */
for (;;) {
xSemaphoreTake(xNetSemaphore, pdMS_TO_TICKS(2));
ethernetif_poll(&ch390_netif);
ethernetif_check_link(&ch390_netif);
/* sys_check_timeouts() 由 tcpip_thread 自动执行,此处不需要调用 */
}
}
```
### 4.6 TcpSrvTask 实现模板
```c
void TcpSrvTask_S1(void *argument)
{
struct netconn *conn = netconn_new(NETCONN_TCP);
netconn_bind(conn, IP_ADDR_ANY, cfg->links[0].lport);
netconn_listen(conn);
for (;;) {
struct netconn *newconn;
if (netconn_accept(conn, &newconn) == ERR_OK) {
/* 在本任务内处理唯一客户端 */
tcp_server_worker(newconn, LINK_S1);
netconn_close(newconn);
netconn_delete(newconn);
}
}
}
```
### 4.7 TcpCliTask 实现模板
```c
void TcpCliTask_C1(void *argument)
{
for (;;) {
struct netconn *conn = netconn_new(NETCONN_TCP);
ip_addr_t remote_ip;
IP_ADDR4(&remote_ip, cfg->links[2].rip[0], ...);
if (netconn_connect(conn, &remote_ip, cfg->links[2].rport) == ERR_OK) {
tcp_client_worker(conn, LINK_C1);
}
netconn_close(conn);
netconn_delete(conn);
vTaskDelay(pdMS_TO_TICKS(cfg->links[2].reconnect_interval));
}
}
```
### 4.8 UartRxTask 实现
```c
void UartRxTask(void *argument)
{
for (;;) {
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10));
/* 处理 UART2/UART3 DMA 接收数据 */
/* MUX=0: 构造 route_msg_t 指针投递到 xUartTxQueue */
/* MUX=1: 提取 MUX 帧,DSTMASK=0 投 xConfigQueue,否则投 xUartTxQueue */
/* 从 xTcpRxQueue 取数据,写入 UART DMA 发送 */
}
}
```
### 4.9 ConfigTask 实现
```c
void ConfigTask(void *argument)
{
for (;;) {
char *cmd;
xQueueReceive(xConfigQueue, &cmd, portMAX_DELAY);
config_process_at_cmd(cmd);
/* 通过 UART1 发送响应 */
}
}
```
## 五、最终协议实现模型
### 5.1 MUX 帧承载层
数据口启用 MUX 后,统一处理如下帧:
```text
SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
```
实现职责:
1. 识别帧边界
2. 解析长度字段
3. 提取 `SRCID`
4. 解析 `DSTMASK`
5. 按控制帧或数据帧分流
### 5.2 控制帧与数据帧分离
控制规则固定如下:
- `DSTMASK = 0x00`:系统控制帧
- `DSTMASK != 0x00`:业务数据帧
系统控制帧处理要求:
1. `PAYLOAD` 解释为 AT 文本
2. AT 文本必须以 `\r\n` 结束
3. 控制帧投递到 `ConfigTask`
业务数据帧处理要求:
1. `SRCID` 表示单一源端点
2. `DSTMASK` 表示目标端点集合
3. `RouteTask` 根据 `DSTMASK` 做多目标分发
### 5.3 统一端点编码
内部与外部文档统一使用以下端点编码:
| 端点 | 编码 |
|------|------|
| `C1` | `0x01` |
| `C2` | `0x02` |
| `UART2` | `0x04` |
| `UART3` | `0x08` |
| `S1` | `0x10` |
| `S2` | `0x20` |
实现要求:
- `SRCID` 为单值
- `DSTMASK` 为位图
- `DSTMASK=0x00` 仅保留为控制帧
## 六、配置层设计
### 6.1 MUX 记录
`MUX` 为全局记录,仅控制设备数据口是否进入 MUX 承载模式。
取值:
- `0`:普通透传
- `1`MUX 透传
### 6.2 NET 记录
`NET` 为全局静态网络记录:
```text
IP,MASK,GW,MAC
```
说明:
- 设备只有一张网卡,因此不为每个实例单独配置本地 IP
- 当前实现目标中不包含 DHCP
### 6.3 LINK 记录
`LINK[idx]` 为统一实例记录:
```text
EN,LPORT,RIP,RPORT,UART
```
固定索引映射:
- `0 = S1`
- `1 = S2`
- `2 = C1`
- `3 = C2`
字段职责:
- `EN`:实例启用状态
- `LPORT`:本地端口
- `RIP / RPORT`:对端地址与端口
- `UART`:对应业务数据口
说明:
- `Server``Client` 共享同一记录结构
- `Server``RIP / RPORT` 可作为对端约束或预设
- `Client``RIP / RPORT` 表示远端目标
## 七、模块职责
### 7.1 配置模块 `config.c/.h`
最终职责:
1. 解析 `AT+MUX`
2. 解析 `AT+NET`
3. 解析 `AT+LINK`
4. 加载与保存配置
5. 处理 `SAVE / RESET / DEFAULT`
### 7.2 UART 透传模块 `uart_trans.c/.h`
最终职责:
1. 保持 `USART2 / USART3``DMA + IDLE` 接收发送基线
2.`MUX=0` 时执行普通透传
3.`MUX=1` 时执行 MUX 帧收发
4. 将控制帧与业务数据帧分流
### 7.3 TCP Server / Client 模块(需重写)
`tcp_server.c` / `tcp_client.c` 基于 lwIP RAW API 回调模式,需重写为 netconn 阻塞模式:
1. 删除所有 `tcp_pcb` / `tcp_recv` / `tcp_accept` 回调代码
2. 改用 `netconn_new` / `netconn_bind` / `netconn_listen` / `netconn_accept`Server
3. 改用 `netconn_new` / `netconn_connect`Client
4. 收发改为 `netconn_recv` / `netconn_write` 阻塞调用
5. 内部 ring buffer 可取消(netconn 内部已有 pbuf 缓冲)
6. 每个 TCP 任务内直接处理路由,通过 Queue 指针传递数据
### 7.4 FreeRTOS 初始化 `freertos.c`
CubeMX 生成的 FreeRTOS 初始化文件,职责:
1. 定义默认任务 `StartDefaultTask`
2. 用户在 `MX_FREERTOS_Init` 中创建自定义任务
### 7.5 FreeRTOS 配置 `FreeRTOSConfig.h`
关键配置项:
| 配置项 | 值 | 说明 |
|--------|-----|------|
| `configUSE_PREEMPTION` | 1 | 抢占式调度 |
| `configTICK_RATE_HZ` | 1000 | 1ms tick |
| `configMINIMAL_STACK_SIZE` | 128 | 最小栈(words |
| `configTOTAL_HEAP_SIZE` | 10240 | FreeRTOS 堆大小 |
| `configMAX_PRIORITIES` | 7 | 最大优先级数 |
| `configUSE_MUTEXES` | 1 | 启用互斥锁 |
| `configUSE_COUNTING_SEMAPHORES` | 1 | 启用计数信号量 |
| `configUSE_RECURSIVE_MUTEXES` | 1 | 启用递归互斥锁 |
| `configCHECK_FOR_STACK_OVERFLOW` | 2 | 栈溢出检测方式 2 |
| `configUSE_MALLOC_FAILED_HOOK` | 1 | 内存分配失败钩子 |
| `configSUPPORT_DYNAMIC_ALLOCATION` | 1 | 动态内存分配 |
## 八、lwIP 配置(NO_SYS=0 + netconn
### 8.1 lwIP 线程模型
由于采用 `NO_SYS=0`lwIP 将运行以下线程:
1. `tcpip_thread`(优先级 6,栈 512 words):lwIP 核心线程,处理所有协议栈内部事件
2. 应用任务通过 `netconn` API 与 lwIP 交互,由 `tcpip_thread` 消息机制保证线程安全
3. `LWIP_TCPIP_CORE_LOCKING=1`:允许应用任务在持有核心锁时直接调用 `netconn_write`,无需通过邮箱中转
### 8.2 lwIP 关键配置项(lwipopts.h
| 配置项 | 值 | 说明 |
|--------|-----|------|
| `NO_SYS` | 0 | 启用 OS 抽象 |
| `LWIP_NETCONN` | 1 | 启用 netconn API |
| `LWIP_SOCKET` | 0 | 不使用 socket API,节省 RAM |
| `LWIP_TCPIP_CORE_LOCKING` | 1 | 允许直接发送,减少延时 |
| `MEM_SIZE` | 8192 | lwIP 堆大小 |
| `PBUF_POOL_SIZE` | 10 | pbuf 池数量 |
| `MEMP_NUM_NETCONN` | 8 | 2监听 + 4连接 + 2余量 |
| `MEMP_NUM_NETBUF` | 8 | netconn 缓冲 |
| `MEMP_NUM_TCP_PCB` | 4 | TCP 控制块 |
| `MEMP_NUM_TCP_PCB_LISTEN` | 2 | TCP 监听 |
| `MEMP_NUM_TCP_SEG` | 24 | TCP 段 |
| `MEMP_NUM_TCPIP_MSG_API` | 8 | API 消息池 |
| `MEMP_NUM_TCPIP_MSG_INPKT` | 8 | 入包消息池 |
| `TCP_MSS` | 536 | 保守 MSS |
| `TCP_SND_BUF` | 8×MSS=4288 | 发送缓冲 |
| `TCP_WND` | 8×MSS=4288 | 接收窗口 |
| `TCPIP_THREAD_STACKSIZE` | 512 | tcpip_thread 栈 |
| `TCPIP_THREAD_PRIO` | 6 (最高) | tcpip_thread 优先级 |
| `LWIP_DHCP` | 0 | 不使用 DHCP |
| `LWIP_UDP` | 0 | 不使用 UDP |
### 8.3 sys_arch 移植层
`Drivers/LwIP/port/sys_arch.c` 需实现 lwIP 到 FreeRTOS 的适配:
1. `sys_thread_new`:创建 lwIP 线程(即 `tcpip_thread`
2. `sys_mbox_*`:消息邮箱(基于 FreeRTOS Queue,容量 `TCPIP_MBOX_SIZE=8`
3. `sys_sem_*`:信号量(基于 FreeRTOS Semaphore
4. `sys_mutex_*`:互斥锁(基于 FreeRTOS Mutex
5. `sys_arch_protect / unprotect`:临界区保护(基于 `vPortEnterCritical / vPortExitCritical`
### 8.4 lwIP 初始化流程
```c
/* 在 MX_FREERTOS_Init 或 NetPollTask 中调用 */
void lwip_init_task(void)
{
/* tcpip_thread 在 lwip_init() 或第一个 sys_thread_new 时自动启动 */
tcpip_init(NULL, NULL);
/* 等待 tcpip_thread 就绪 */
/* 添加网络接口 */
netif_add(&ch390_netif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);
netif_set_default(&ch390_netif);
netif_set_up(&ch390_netif);
}
```
## 九、中断与 HAL 时间基准
### 9.1 HAL 时间基准
FreeRTOS 下 `SysTick` 被 FreeRTOS 占用,HAL 时间基准改用 `TIM4`
- `TIM4` 配置为 1ms 中断(72MHz / (71+1) / (999+1) = 1kHz
- `HAL_InitTick` 使用 `TIM4` 而非 `SysTick`
- `uwTick``TIM4_IRQHandler` 中递增
### 9.2 中断优先级规划
| 中断 | 优先级 | 说明 |
|------|--------|------|
| `SysTick` | 15(最低) | FreeRTOS tick |
| `PendSV` | 15(最低) | FreeRTOS 上下文切换 |
| `SVCall` | 0 | FreeRTOS 服务调用 |
| `TIM4` | 0 | HAL 时间基准 |
| `EXTI0` | 5 | CH390 中断 |
| `DMA1_Ch2~7` | 5 | UART DMA |
| `USART1/2/3` | 5 | UART 中断 |
| `SPI1` | 5 | SPI 中断 |
FreeRTOS 可管理的中断优先级必须 >= `configMAX_SYSCALL_INTERRUPT_PRIORITY`(本工程为 5)。
## 十、内存预算(路径 A 精确估算)
`STM32F103RCT6` 为目标(48KB SRAM):
### 10.1 RAM 预算
| 项目 | 大小 | 说明 |
|------|------|------|
| 启动栈 (MSP) | 2,048 B | `startup_stm32f103xe.s` 定义 0x800 |
| FreeRTOS 堆 (heap_4) | 10,240 B | `configTOTAL_HEAP_SIZE` |
| 任务栈 (9 任务) | 13,312 B | 3,328 words × 4 (见上表) |
| lwIP 堆 (MEM_SIZE) | 8,192 B | |
| PBUF 池 (10 个) | ~6,000 B | 10 × ~600B |
| MEMP 池 | ~4,000 B | netconn/netbuf/tcpip_msg/pcb/seg |
| UART DMA+Ring 缓冲 | 2,304 B | 2 通道 × (256+256+512+384)/2 |
| 路由缓冲池 (4×512B) | 2,048 B | 零拷贝指针传递 |
| 配置结构 | 1,024 B | |
| **合计** | **~49,168 B** | |
| **余量 (RCT6 48KB)** | **~-928 B** | ⚠️ 超出,需优化或换 RDT6 |
| **余量 (RDT6 64KB)** | **~15,264 B** | ✅ 充裕 |
### 10.2 优化空间(RCT6 下)
若坚持使用 RCT6,可通过以下措施压缩到 48KB 以内:
| 优化项 | 节省 |
|--------|------|
| 取消 TCP 任务的 ring buffernetconn 内部有 pbuf 缓冲) | -2,048 B |
| `configTOTAL_HEAP_SIZE` 降至 8KB | -2,048 B |
| `MEM_SIZE` 降至 6KB | -2,048 B |
| TcpCliTask 栈降至 192 words × 2 | -512 B |
| **优化后合计** | **~42,504 B** |
| **RCT6 余量** | **~5,464 B** |
### 10.3 备选 MCU
当 RAM 最终不够用时,切换为 `STM32F103RDT6`pin-to-pin 替代):
| 项目 | RCT6 | RDT6 |
|------|------|------|
| Flash | 256 KB | 384 KB |
| SRAM | 48 KB | 64 KB |
| 引脚 | LQFP64 | LQFP64(完全兼容) |
| 启动文件 | `startup_stm32f103xe.s` | `startup_stm32f103xe.s` |
| 宏定义 | `STM32F103xE` | `STM32F103xE` |
| Flash 算法 | `STM32F10x_HD` | `STM32F10x_HD`(相同) |
| SRAM 大小 | `0xC000` | `0x10000` |
| Flash 大小 | `0x40000` | `0x60000` |
### 10.4 Flash 预算
| 项目 | 估计值 | 说明 |
|------|--------|------|
| FreeRTOS 内核 | ~8 KB | 含 CMSIS-RTOS V2 |
| HAL 驱动 | ~20 KB | GPIO/UART/SPI/DMA/IWDG/TIM |
| lwIP 协议栈 | ~50 KB | core + api + ipv4 + netif + sys_arch |
| CH390 驱动 | ~4 KB | |
| 应用代码 | ~20 KB | config/uart_trans/tcp_server/tcp_client |
| **合计** | ~102 KB | RCT6 预留 154 KBRDT6 预留 282 KB |
## 十一、硬件资源
### 11.1 MCU
- 型号:`STM32F103RCT6`
- Flash`256 KB`
- SRAM`48 KB`
- 主频:`72 MHz`
### 11.2 主要外设
- `SPI1`:连接 `CH390D`
- `USART1`:配置串口
- `USART2`:数据透传串口
- `USART3`:数据透传串口
- `DMA1`3 路 UART 收发 DMA
- `EXTI0`CH390 中断输入
- `IWDG`:独立看门狗
- `TIM4`HAL 时间基准(替代 SysTick
### 11.3 引脚分配
| 引脚 | 功能 | 用途 |
|------|------|------|
| PA2 | USART2_TX | 数据透传串口 |
| PA3 | USART2_RX | 数据透传串口 |
| PA4 | SPI1_NSS | CH390D 片选 |
| PA5 | SPI1_SCK | CH390D SPI 时钟 |
| PA6 | SPI1_MISO | CH390D SPI 数据输入 |
| PA7 | SPI1_MOSI | CH390D SPI 数据输出 |
| PA9 | USART1_TX | 配置串口 |
| PA10 | USART1_RX | 配置串口 |
| PB0 | EXTI0 | CH390D INT |
| PB1 | GPIO_Output | CH390D RESET |
| PB10 | USART3_TX | 数据透传串口 |
| PB11 | USART3_RX | 数据透传串口 |
| PC13 | GPIO_Output | 状态 LED |
| PD0/PD1 | HSE | 8MHz 外部晶振 |
## 十二、实现边界
1. 保持单网卡静态网络模型
2. 不实现 DHCP
3. 不实现旧 `S1... / C1...` 外部协议字段
4. 不在文档中保留兼容层描述
5. 所有 AT 文本控制统一要求 `\r\n` 结束
6. FreeRTOS 堆管理使用 `heap_4.c`
7. HAL 时间基准使用 `TIM4` 而非 `SysTick`
## 十三、文档一致性要求
后续实现、联调、测试与代码注释必须遵守以下统一口径:
1. 对外协议只使用 `MUX / NET / LINK`
2. 控制帧只使用 `DSTMASK=0x00`
3. MUX 帧格式固定为 `SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL`
4. AT 手册、需求说明、技术实现三份文档不得再出现历史展开式字段
## 十四、路径 A 实现清单
### 14.1 必须重写的模块
| 模块 | 原实现 | 目标实现 | 说明 |
|------|--------|----------|------|
| `tcp_server.c/.h` | lwIP RAW API 回调 | netconn 阻塞任务 | `tcp_new``netconn_new`,回调 → 阻塞循环 |
| `tcp_client.c/.h` | lwIP RAW API 回调 | netconn 阻塞任务 | 同上 |
| `sys_arch.c/.h` | NO_SYS=1 空壳 | FreeRTOS 移植层 | `sys_mbox``sys_sem``sys_mutex``sys_thread` |
| `lwipopts.h` | NO_SYS=1 | NO_SYS=0 + netconn | 已更新 |
| `main.c` | while(1) 轮询 | FreeRTOS 任务创建 | `App_Poll()` → 各任务函数 |
| `stm32f1xx_it.c` | 裸机 ISR | FreeRTOS ISR | `xxFromISR` API |
### 14.2 可复用(需适配)的模块
| 模块 | 适配内容 |
|------|----------|
| `uart_trans.c/.h` | 添加 `xTaskNotifyFromISR` 替代 poll 模式 |
| `config.c/.h` | 添加 `xQueueReceive` 替代 `config_poll()` |
| `flash_param.c/.h` | 无需修改 |
| `CH390` 驱动 | `ethernetif_poll` 改为 NetPollTask 调用,SPI 加 Mutex 保护 |
| `ethernetif.c` | 添加 `netif_add``tcpip_input` 回调 |
### 14.3 无需修改的模块
| 模块 | 说明 |
|------|------|
| `FreeRTOSConfig.h` | 已更新(任务优先级宏、堆大小) |
| `startup_stm32f103xe.s` | 已适配 RCT6 |
| `TCP2UART.ioc` | 已适配 RCT6 + FreeRTOS + TIM4 |
| `MDK-ARM/TCP2UART.uvprojx` | 已适配 RCT6 + xE 宏 |
| MUX 帧编解码 | 协议逻辑与 RTOS 无关 |
### 14.4 新增模块
| 模块 | 职责 |
|------|------|
| `route_msg.c/.h` | 零拷贝路由消息池管理 |
| `task_tcp_server.c/.h` | netconn Server 任务模板 |
| `task_tcp_client.c/.h` | netconn Client 任务模板 |
| `task_net_poll.c/.h` | CH390 poll + link check 任务 |
-191
View File
@@ -1,191 +0,0 @@
# TCP2UART 项目需求说明
## 一、项目目标
本项目基于 `STM32F103RCT6``CH390D` 实现一台多实例 TCP 与双串口数据透传设备。
最终对外协议模型固定为:
1. `MUX`:控制串口侧是否采用 MUX 承载
2. `NET`:全局静态网络配置
3. `LINK[idx]`:按实例索引组织的链路配置
系统必须支持:
- `2` 路 TCP Server 实例
- `2` 路 TCP Client 实例
- `UART1` 作为 AT 配置口
- `UART2 / UART3` 作为业务数据口
## 二、硬件与软件边界
### 2.1 硬件边界
- 主控:`STM32F103RCT6`256KB Flash / 48KB SRAM
- 备选主控:`STM32F103RDT6`384KB Flash / 64KB SRAM),pin-to-pin 兼容,当 RAM 不够时直接替换
- 以太网芯片:`CH390D`
- 网卡数量:`1`
- 配置口:`UART1`
- 数据口:`UART2``UART3`
### 2.2 软件边界
- 执行模型:`FreeRTOS`
- 网络协议栈:`lwIP NO_SYS=0 + netconn API`(线程安全,每连接独立任务)
- 调试输出:`SEGGER RTT`
- 采用 `FreeRTOS` 任务调度
- TCP 连接使用 `netconn` 阻塞 API`netconn_accept` / `netconn_recv` / `netconn_write`
- 每条 TCP 连路(S1/S2/C1/C2)独立一个任务
- 不包含 DHCP 协议支持
## 三、最终协议需求
### 3.1 MUX 帧格式
所有 MUX 数据承载必须使用如下格式:
```text
SYNC | LEN_H | LEN_L | SRCID | DSTMASK | PAYLOAD | TAIL
```
要求:
- `DSTMASK != 0x00`:业务数据帧
- `DSTMASK = 0x00`:系统控制帧
- 系统控制帧承载 AT 文本命令
- AT 文本命令必须以 `\r\n` 结尾
### 3.2 统一端点编码
系统必须使用统一端点编码,同时覆盖 UART 与 TCP 逻辑实例:
| 端点 | 编码 |
|------|------|
| `C1` | `0x01` |
| `C2` | `0x02` |
| `UART2` | `0x04` |
| `UART3` | `0x08` |
| `S1` | `0x10` |
| `S2` | `0x20` |
要求:
- `SRCID` 为单值
- `DSTMASK` 为位图
- `DSTMASK=0x00` 仅保留给系统控制帧
## 四、AT 接口需求
### 4.1 命令分类
AT 协议必须收敛为以下三类命令:
1. `AT+MUX`
2. `AT+NET`
3. `AT+LINK`
不再保留历史展开式实例字段命令。
### 4.2 MUX 命令需求
- `AT+MUX=0/1`:设置全局 MUX 模式
- `AT+MUX?`:查询当前 MUX 模式
### 4.3 NET 命令需求
`NET` 必须统一表达以下静态网络参数:
```text
IP,MASK,GW,MAC
```
说明:
- 设备只有一张网卡,因此本地 IP 不按实例拆分
- DHCP 不属于协议需求范围
### 4.4 LINK 命令需求
`LINK[idx]` 必须统一表达如下字段:
```text
EN,LPORT,RIP,RPORT,UART
```
要求:
- `idx` 固定映射四个实例:`0=S1``1=S2``2=C1``3=C2`
- `Server``Client` 共用同一条 `LINK` 配置模型
- `LPORT` 必须可配置
- `RIP / RPORT` 必须可配置
- `UART` 必须可配置
## 五、功能需求
### 5.1 TCP 功能
- 支持 `2` 路 Server
- 支持 `2` 路 Client
- 每个实例通过 `LINK[idx]` 配置其本地端口、对端地址、对端端口和串口路由
### 5.2 串口透传功能
- `UART2 / UART3` 支持普通透传模式与 MUX 透传模式
- 当需要多实例共享数据口时,必须启用 MUX 模式
- 业务数据流向由 `SRCID / DSTMASK` 决定
### 5.3 系统控制功能
- 系统控制帧由 `DSTMASK=0x00` 表示
- 系统控制帧进入 AT 解析路径
- 控制文本必须以 `\r\n` 结束
### 5.4 参数保存功能
- 参数修改后支持 `SAVE`
- 支持 `RESET` 后按保存配置启动
- 支持恢复默认配置
## 六、FreeRTOS 任务架构需求
### 6.1 任务划分
系统至少应包含以下 FreeRTOS 任务:
| 任务 | 优先级 | 职责 |
|------|--------|------|
| tcpip_thread | 6 (最高) | lwIP 内核线程(自动创建) |
| NetPollTask | 5 | CH390 事件轮询 + 链路检测 |
| TcpSrvTask_S1 | 4 | S1 netconn_accept + 收发 |
| TcpSrvTask_S2 | 4 | S2 netconn_accept + 收发 |
| TcpCliTask_C1 | 4 | C1 netconn_connect + 收发 |
| TcpCliTask_C2 | 4 | C2 netconn_connect + 收发 |
| UartRxTask | 4 | UART DMA/IDLE 接收 + MUX 帧提取 + 路由 |
| ConfigTask | 2 | AT 命令解析与响应 |
| DefaultTask | 1 | LED 心跳 + 看门狗 |
### 6.2 任务间通信
- 使用 `Queue` 传递指针 + 元数据描述符(零拷贝路由消息)
- 使用 `Binary Semaphore` 同步 CH390 中断事件
- 使用 `TaskNotification` 通知 UART IDLE 事件
- 预分配静态缓冲池,避免动态分配
## 七、非功能需求
1. 满足 `STM32F103RCT6``256KB Flash / 48KB SRAM` 约束,若 RAM 不足可切换 `STM32F103RDT6`pin-to-pin64KB SRAM
2. 工程可在 `MDK-ARM` 下构建
3. 调试输出统一使用 `SEGGER RTT`
4. 不引入 DHCP、DNS、UDP 等当前非目标协议
5. FreeRTOS 堆使用 `heap_4.c`,总堆大小建议 `10KB`
6. 所有任务栈通过 `uxTaskGetStackHighWaterMark` 监控
## 八、验收口径
验收时以下几点必须同时成立:
1. 文档只使用 `MUX / NET / LINK` 作为最终协议模型
2. 文档不再出现历史 `S1... / C1...` 外部字段
3. 串口控制文本统一规定为 `\r\n` 结束
4. MUX 帧格式与端点编码在需求、手册、技术实现三份文档中表述一致
5. FreeRTOS 任务无死锁、无栈溢出、无优先级反转