diff --git a/CMakeLists.txt b/CMakeLists.txt index bcfc9f2..5f20010 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Core/Bsp/easylogger/src/elog.c ${CMAKE_CURRENT_SOURCE_DIR}/Core/Bsp/easylogger/src/elog_utils.c ${CMAKE_CURRENT_SOURCE_DIR}/Core/Bsp/easylogger/port/elog_port.c + ${CMAKE_CURRENT_SOURCE_DIR}/Core/Bsp/checksum.c + ${CMAKE_CURRENT_SOURCE_DIR}/Core/Bsp/protocol.c ) # Add include paths diff --git a/Core/Bsp/bsp_uart.c b/Core/Bsp/bsp_uart.c index 73d65eb..b3ab987 100644 --- a/Core/Bsp/bsp_uart.c +++ b/Core/Bsp/bsp_uart.c @@ -1,5 +1,9 @@ #include "bsp_uart.h" +#include "checksum.h" #include "elog.h" +#include "protocol.h" +#include +#include /** * @brief 使用 UART1 和 DMA 发送数据到 ESP12F 模块(用于 TCP 透传) @@ -7,28 +11,27 @@ * @return HAL_StatusTypeDef: HAL_OK 表示成功,其他值表示失败 */ HAL_StatusTypeDef ESP12F_TCP_SendMessage(const char *data) { - if (data == NULL) { - return HAL_ERROR; // 参数无效 - } + if (data == NULL) { + return HAL_ERROR; // 参数无效 + } - // 计算数据大小 - uint16_t size = strlen(data); - if (size == 0) { - return HAL_ERROR; // 数据为空 - } + // 计算数据大小 + uint16_t size = strlen(data); + if (size == 0) { + return HAL_ERROR; // 数据为空 + } - // 启动 DMA 传输 - return HAL_UART_Transmit_DMA(&huart1, (uint8_t *)data, size); + // 启动 DMA 传输 + return HAL_UART_Transmit_DMA(&huart1, (uint8_t *)data, size); } - /* External DMA handle */ extern DMA_HandleTypeDef hdma_usart1_rx; /* DMA 接收缓冲区及状态变量 */ -uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE]; -volatile uint16_t uart1_rx_len = 0; /* 记录最近一次接收到的数据包长度 */ -volatile uint8_t uart1_rx_flag = 0; /* 接收完成标志位:1表示有一帧新数据 */ +uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE]; +volatile uint16_t uart1_rx_len = 0; /* 记录最近一次接收到的数据包长度 */ +volatile uint8_t uart1_rx_flag = 0; /* 接收完成标志位:1表示有一帧新数据 */ /** * @brief 初始化 UART1 并启动 DMA 接收 @@ -38,14 +41,13 @@ volatile uint8_t uart1_rx_flag = 0; /* 接收完成标志位:1表示 * @return 无 * @note 需要在调用此函数前确保 UART1 和 DMA 已经正确初始化 */ -void BSP_UART1_Init(void) -{ +void BSP_UART1_Init(void) { /* 停止并重置 DMA 状态 */ - HAL_UART_DMAStop(&huart1); + HAL_UART_DMAStop(&huart1); uart1_rx_len = 0; uart1_rx_flag = 0; memset(uart1_rx_buf, 0, sizeof(uart1_rx_buf)); - + /* 启动 DMA 接收,将接收到的数据存储到 uart1_rx_buf 缓冲区 */ HAL_UART_Receive_DMA(&huart1, uart1_rx_buf, UART1_RX_BUF_SIZE); @@ -66,7 +68,7 @@ void UART_IDLE_Callback(UART_HandleTypeDef *huart) { /* 清除空闲中断标志位(HAL库宏) */ __HAL_UART_CLEAR_IDLEFLAG(huart); - + /* 停止 DMA 以便安全读取计数器并更新状态 */ HAL_UART_DMAStop(huart); @@ -75,13 +77,13 @@ void UART_IDLE_Callback(UART_HandleTypeDef *huart) { if (recv_len > 0 && recv_len < UART1_RX_BUF_SIZE) { uart1_rx_len = recv_len; - uart1_rx_buf[recv_len] = '\0'; /* 添加字符串结束符,方便后续字符串处理 */ - uart1_rx_flag = 1; /* 置位标志,通知应用层新消息到达 */ - - /* 仅供调试:在中断中打印接收到的数据(注意:printf 可能会影响实时性) */ - elog_raw("UART1 Received: %s\r\n", (char *)uart1_rx_buf); + uart1_rx_buf[recv_len] = '\0'; /* 添加字符串结束符,方便后续字符串处理 */ + uart1_rx_flag = 1; /* 置位标志,通知应用层新消息到达 */ + + // 调用协议处理函数,通过消息队列发送接收到的数据 + Protocol_HandleMessage(uart1_rx_buf, recv_len); } /* 重新启动新一轮的 DMA 接收 */ HAL_UART_Receive_DMA(&huart1, uart1_rx_buf, UART1_RX_BUF_SIZE); -} \ No newline at end of file +} \ No newline at end of file diff --git a/Core/Bsp/checksum.c b/Core/Bsp/checksum.c new file mode 100644 index 0000000..333d7ff --- /dev/null +++ b/Core/Bsp/checksum.c @@ -0,0 +1,24 @@ +#include + +/** + * @brief 计算校验和 + * @param buf: 数据缓冲区指针 (即你的 uart1_rx_buf) + * @param start_pos: 校验数据的起始下标 (通常跳过帧头 "LOGI:") + * @param length: 需要校验的数据长度 (不包含校验位本身和帧尾) + * @return uint8_t: 计算得出的校验和 + * + * 算法说明: 将所有字节相加,取低8位 (相当于 % 256) + */ +uint8_t Calculate_CheckSum(uint8_t *buf, uint16_t start_pos, uint16_t length) +{ + uint32_t sum = 0; // 使用32位防止累加溢出,虽然uint8累加也不会溢出单片机寄存器 + uint16_t i; + + for (i = 0; i < length; i++) + { + sum += buf[start_pos + i]; + } + + // 返回低8位,相当于 sum % 256 + return (uint8_t)(sum & 0xFF); +} \ No newline at end of file diff --git a/Core/Bsp/checksum.h b/Core/Bsp/checksum.h new file mode 100644 index 0000000..f6ee6e6 --- /dev/null +++ b/Core/Bsp/checksum.h @@ -0,0 +1,17 @@ +#ifndef CHECKSUM_H +#define CHECKSUM_H + +#include + +/** + * @brief 计算校验和 + * @param buf: 数据缓冲区指针 (即你的 uart1_rx_buf) + * @param start_pos: 校验数据的起始下标 (通常跳过帧头 "LOGI:") + * @param length: 需要校验的数据长度 (不包含校验位本身和帧尾) + * @return uint8_t: 计算得出的校验和 + * + * 算法说明: 将所有字节相加,取低8位 (相当于 % 256) + */ +uint8_t Calculate_CheckSum(uint8_t *buf, uint16_t start_pos, uint16_t length); + +#endif // CHECKSUM_H \ No newline at end of file diff --git a/Core/Bsp/protocol.c b/Core/Bsp/protocol.c new file mode 100644 index 0000000..5c44509 --- /dev/null +++ b/Core/Bsp/protocol.c @@ -0,0 +1,126 @@ +/** + * @file protocol.c + * @brief 协议处理函数实现 + * @details 该文件包含了协议解析和处理的相关函数,主要用于处理从 ESP12F + * 模块接收到的数据。 + * @author Beihong Wang + * @date 2026-04-01 + */ + +#include "protocol.h" +#include "bsp_uart.h" +#include "checksum.h" +#include "cmsis_os.h" +#include "elog.h" +#include +#include + + +/* 定义日志 TAG */ +#define Protocol_TAG "Protocol" + +/* 引用在 freertos.c 中定义的消息队列句柄 */ +extern osMessageQueueId_t CmdQueueHandle; +/** + * @brief 协议处理函数 + * @details 严格按照协议文档:校验范围 = 帧头 + 命令 + 数据 + * 即从下标 0 开始,一直加到最后一个冒号之前 + */ +void Protocol_HandleMessage(uint8_t *data, uint16_t len) { + if (data == NULL) + return; + + // 1. 基础检查:长度必须足够,且必须以 '#' 结尾 + if (len < 10 || data[len - 1] != '#') { + elog_w(Protocol_TAG, "协议错误:长度不足或帧尾错误 (len: %d)", len); + return; + } + + // 2. 寻找校验位前的分隔符 + // 协议格式:LOGI:CMD:DATA:CS# + // 我们需要找到最后一个冒号 ':' 的位置,它前面是数据,后面是校验位 + int last_colon_pos = -1; + for (int i = 0; i < len; i++) { + if (data[i] == ':') { + last_colon_pos = i; + } + } + + // 如果找不到冒号,说明格式错误 + if (last_colon_pos == -1 || last_colon_pos < 5) { + elog_w(Protocol_TAG, "协议错误:找不到分隔符 ':' 或位置非法"); + return; + } + + // 3. 提取接收到的校验位 (从 ASCII 转为 Hex 数值) + // 校验位紧跟在 last_colon_pos 之后,长度为 2 字节 + char recv_cs_hex_str[3] = {0}; + // 防止越界 + if (last_colon_pos + 3 >= len) { + elog_w(Protocol_TAG, "协议错误:校验位数据越界"); + return; + } + + recv_cs_hex_str[0] = data[last_colon_pos + 1]; + recv_cs_hex_str[1] = data[last_colon_pos + 2]; + + unsigned int received_checksum = 0; + sscanf(recv_cs_hex_str, "%02X", &received_checksum); + + // 4. 计算本地校验和 + // 【核心修改点】 + // 严格按照协议文档:从下标 0 开始,长度为 last_colon_pos + // 也就是计算 "LOGI:SP:080" 的累加和 + uint8_t calculated_checksum = + Calculate_CheckSum(data, 0, (uint16_t)last_colon_pos); + + // 5. 对比校验和 + if (calculated_checksum == (uint8_t)received_checksum) { + elog_i(Protocol_TAG, "✅ 校验通过!执行指令: %s", (char *)data); + + /* 提取有效载荷发送到消息队列 */ + char cmd_payload[16] = {0}; + // 将 "LOGI:" 之后到最后一个冒号之前的内容作为指令 + uint16_t payload_len = last_colon_pos - 5; + uint16_t copy_len = (payload_len > 15) ? 15 : payload_len; + if (copy_len > 0) { + memcpy(cmd_payload, &data[5], copy_len); + } + + osStatus_t status = osMessageQueuePut(CmdQueueHandle, cmd_payload, 0, 0); + if (status != osOK) { + elog_e(Protocol_TAG, "Protocol: Queue put failed: %d", status); + } + } else { + elog_w(Protocol_TAG, "❌ 校验失败!计算值: 0x%02X, 接收值: 0x%02X", + calculated_checksum, (uint8_t)received_checksum); + + // 辅助调试:打印实际参与计算的数据段 + char debug_buf[32] = {0}; + if (last_colon_pos < 32) { + memcpy(debug_buf, data, last_colon_pos); + elog_i(Protocol_TAG, " -> 单片机正在计算这段数据的校验和: [%s]", + debug_buf); + elog_i(Protocol_TAG, " -> 请检查上位机是否也是按照此范围计算累加和"); + } + } +} + +#define CarCtrlTask_TAG "CarCtrlTask" + +void CarCtrl_Task(void *argument) { + /* USER CODE BEGIN CarCtrl_Task */ + char cmd_payload[16]; + /* Infinite loop */ + for (;;) { + /* 从消息队列中获取数据,阻塞等待 */ + if (osMessageQueueGet(CmdQueueHandle, cmd_payload, NULL, osWaitForever) == + osOK) { + elog_i(CarCtrlTask_TAG, "CarCtrl: Received command (ASCII: %s) from queue", + cmd_payload); + /* 可以在这里添加根据 cmd_payload 控制小车的逻辑 */ + } + // osDelay(1); // osMessageQueueGet 已经是阻塞的,不需要额外的 osDelay + } + /* USER CODE END CarCtrl_Task */ +} \ No newline at end of file diff --git a/Core/Bsp/protocol.h b/Core/Bsp/protocol.h new file mode 100644 index 0000000..2ccad7c --- /dev/null +++ b/Core/Bsp/protocol.h @@ -0,0 +1,16 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +#include "main.h" + +/** + * @brief 协议处理函数,解析接收到的数据并通过消息队列发送命令 + * @param data: 接收到的原始数据指针 + * @param len: 数据长度 + */ +void Protocol_HandleMessage(uint8_t *data, uint16_t len); + + +void CarCtrl_Task(void *argument); + +#endif /* PROTOCOL_H */ diff --git a/Core/Src/freertos.c b/Core/Src/freertos.c index 5d67b56..71ec707 100644 --- a/Core/Src/freertos.c +++ b/Core/Src/freertos.c @@ -29,6 +29,7 @@ #include "bsp_beep.h" #include "usart.h" #include "elog.h" +#include "protocol.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -57,6 +58,18 @@ const osThreadAttr_t initTask_attributes = { .stack_size = 128 * 4, .priority = (osPriority_t) osPriorityNormal, }; +/* Definitions for CarCtrlTask */ +osThreadId_t CarCtrlTaskHandle; +const osThreadAttr_t CarCtrlTask_attributes = { + .name = "CarCtrlTask", + .stack_size = 256 * 4, + .priority = (osPriority_t) osPriorityNormal, +}; +/* Definitions for CmdQueue */ +osMessageQueueId_t CmdQueueHandle; +const osMessageQueueAttr_t CmdQueue_attributes = { + .name = "CmdQueue" +}; /* Private function prototypes -----------------------------------------------*/ /* USER CODE BEGIN FunctionPrototypes */ @@ -73,6 +86,7 @@ PUTCHAR_PROTOTYPE { /* USER CODE END FunctionPrototypes */ void StartDefaultTask(void *argument); +void CarCtrl_Task(void *argument); void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */ @@ -103,6 +117,10 @@ void MX_FREERTOS_Init(void) { /* start timers, add new ones, ... */ /* USER CODE END RTOS_TIMERS */ + /* Create the queue(s) */ + /* creation of CmdQueue */ + CmdQueueHandle = osMessageQueueNew (16, 16, &CmdQueue_attributes); + /* USER CODE BEGIN RTOS_QUEUES */ /* add queues, ... */ /* USER CODE END RTOS_QUEUES */ @@ -111,6 +129,9 @@ void MX_FREERTOS_Init(void) { /* creation of initTask */ initTaskHandle = osThreadNew(StartDefaultTask, NULL, &initTask_attributes); + /* creation of CarCtrlTask */ + CarCtrlTaskHandle = osThreadNew(CarCtrl_Task, NULL, &CarCtrlTask_attributes); + /* USER CODE BEGIN RTOS_THREADS */ /* add threads, ... */ /* USER CODE END RTOS_THREADS */ @@ -134,24 +155,35 @@ void StartDefaultTask(void *argument) /* Infinite loop */ for(;;) { - const char *message = "Hello, ESP12F! This is a test message."; - HAL_StatusTypeDef status = ESP12F_TCP_SendMessage(message); - if (status == HAL_OK) { - HAL_GPIO_WritePin(RUN_LED_GPIO_Port, RUN_LED_Pin, GPIO_PIN_SET); - BEEP_On(); - osDelay(50); - BEEP_Off(); - osDelay(20); - - } else { - HAL_GPIO_WritePin(RUN_LED_GPIO_Port, RUN_LED_Pin, GPIO_PIN_RESET); - } + + HAL_GPIO_TogglePin(RUN_LED_GPIO_Port, RUN_LED_Pin); + osDelay(1000); } /* USER CODE END StartDefaultTask */ } +/* USER CODE BEGIN Header_CarCtrl_Task */ +/** +* @brief Function implementing the CarCtrlTask thread. +* @param argument: Not used +* @retval None +*/ +/* USER CODE END Header_CarCtrl_Task */ +__weak void CarCtrl_Task(void *argument) +{ + /* USER CODE BEGIN CarCtrl_Task */ + + /* Infinite loop */ + for(;;) + { + + osDelay(1); // osMessageQueueGet 已经是阻塞的,不需要额外的 osDelay + } + /* USER CODE END CarCtrl_Task */ +} + /* Private application code --------------------------------------------------*/ /* USER CODE BEGIN Application */ diff --git a/README.md b/README.md index 8907520..a29d8c9 100644 --- a/README.md +++ b/README.md @@ -177,3 +177,133 @@ void UART_IDLE_Callback(UART_HandleTypeDef *huart) { const char *message = "Hello, ESP12F! This is a test message."; HAL_StatusTypeDef status = ESP12F_TCP_SendMessage(message); ``` + + +这些是上位机发送的指令: +按照这些来写代码 + +这是一个基于 TCP/IP 局域网通信的物流小车控制指令协议设计方案。 + +为了方便调试和开发,本协议采用 **ASCII 文本格式**(类似于 Modbus ASCII 或简单的串口透传格式),而不是二进制格式。这样你可以直接使用网络调试助手(如 NetAssist)手动发送字符串来测试小车,而无需编写专门的上位机软件。 + +--- + +### 📡 通信基础参数 + +- **通信方式**:TCP Client (上位机) 连接 TCP Server (单片机/小车) +- **数据格式**:ASCII 字符串 +- **换行符**:建议使用 `\r\n` (回车+换行) 作为每条指令的结束标志,以便单片机解析。 +- **字节序**:N/A (文本协议不涉及大小端问题) + +--- + +### 📦 指令帧结构 + +每条指令由以下几个部分组成,字段之间用英文冒号 `:` 分隔: + +`[帧头][命令字][数据内容][校验和][帧尾]` + +- **帧头**:固定为 `LOGI` (代表 Logistics),用于快速识别有效数据包。 +- **命令字**:2位字符,代表具体操作(如 `GS` 代表去站点)。 +- **数据内容**:具体的参数,长度可变。 +- **校验和**:2位十六进制数,用于验证数据完整性(防止丢包或乱码)。 +- **帧尾**:固定为 `#`。 + +--- + +### 📝 具体控制指令定义 + +以下是针对你提出的四个需求(去站点、停止、启动、速度)的具体指令格式。 + +#### 1. 去往指定站点 +- **功能**:指示小车移动到编号为 N 的站点。 +- **指令格式**:`LOGI:GS:NNN:CS#` +- **参数说明**: + - `GS`: 命令字 (Go to Station)。 + - `NNN`: 站点编号,3位数字,不足补0。例如:1号站写为 `001`,12号站写为 `012`。 + - `CS`: 校验和。 +- **示例**: + - 去往 **5号站点**:`LOGI:GS:005:15#` (假设校验和计算结果为15) + +#### 2. 启动运行 +- **功能**:让处于停止或待机状态的小车开始执行任务或继续运行。 +- **指令格式**:`LOGI:ST:RUN:CS#` +- **参数说明**: + - `ST`: 命令字 (Start/Status)。 + - `RUN`: 固定参数,表示启动。 +- **示例**: + - 启动小车:`LOGI:ST:RUN:2A#` + +#### 3. 紧急停止/暂停 +- **功能**:立即停止小车的运动,通常用于急停或到达站点后的确认暂停。 +- **指令格式**:`LOGI:ST:STOP:CS#` +- **参数说明**: + - `STOP`: 固定参数,表示停止。 +- **示例**: + - 停止小车:`LOGI:ST:STOP:32#` + +#### 4. 设置运行速度 +- **功能**:动态调整小车的行驶速度。 +- **指令格式**:`LOGI:SP:VVV:CS#` +- **参数说明**: + - `SP`: 命令字 (Set Speed)。 + - `VVV`: 速度值,3位数字 (000-100),代表百分比或具体PWM占空比等级。 + - `000`: 停止 + - `050`: 50% 速度 + - `100`: 全速 +- **示例**: + - 设置速度为 **80%**:`LOGI:SP:080:04#` + +--- + +### 🧮 校验和算法 + +为了防止无线信号干扰导致指令错误,我们需要一个简单的校验和。 + +- **算法**:将 **帧头** 到 **数据内容** 结束的所有字符的 ASCII 码值相加,然后对 256 取余,最后转换为 2位十六进制字符串。 +- **公式**:`Sum = (Byte1 + Byte2 + ... + ByteN) % 256` + +**举例计算 (去往 1 号站点):** +1. 原始字符串:`LOGI:GS:001` +2. ASCII 码值相加: + - 'L'(76) + 'O'(79) + 'G'(71) + 'I'(73) + ':'(58) + 'G'(71) + 'S'(83) + ':'(58) + '0'(48) + '0'(48) + '1'(49) + - 总和 = 614 +3. 取余:`614 % 256 = 102` +4. 转十六进制:`102` -> `66` +5. 最终发送指令:`LOGI:GS:001:66#` + +--- + +### 💬 小车回复机制 (可选但推荐) + +单片机执行指令后,应向上位机返回执行结果,以便上位机显示状态。 + +**回复格式**:`[命令字]:[状态码]:[描述]#` + +- **状态码定义**: + - `OK`: 指令接收正确并执行。 + - `ERR`: 指令格式错误或校验失败。 + - `BUSY`: 小车正在忙,无法执行新指令。 + +**示例回复**: +- 成功去往站点:`GS:OK:Arrived#` +- 速度设置成功:`SP:OK:SpeedSet#` +- 校验错误:`CMD:ERR:CheckSum#` + +--- + +### 📌 总结清单 + +你可以直接将下表发给单片机开发人员: + +| 功能 | 指令模板 | 示例 (假设校验和为 XX) | 说明 | +| :--- | :--- | :--- | :--- | +| **去站点** | `LOGI:GS:NNN:XX#` | `LOGI:GS:003:XX#` | NNN为3位站点号 | +| **启动** | `LOGI:ST:RUN:XX#` | `LOGI:ST:RUN:XX#` | 开始运动 | +| **停止** | `LOGI:ST:STOP:XX#` | `LOGI:ST:STOP:XX#` | 立即停止 | +| **设速度** | `LOGI:SP:VVV:XX#` | `LOGI:SP:050:XX#` | VVV为0-100 | + + +我创建了 处理解析指令的任务和传递消息的消息队列。 +![指令处理任务和队列](image-6.png) + diff --git a/f103_car.ioc b/f103_car.ioc index 8baecb1..e31d925 100644 --- a/f103_car.ioc +++ b/f103_car.ioc @@ -40,8 +40,9 @@ Dma.USART2_TX.2.PeriphInc=DMA_PINC_DISABLE Dma.USART2_TX.2.Priority=DMA_PRIORITY_LOW Dma.USART2_TX.2.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority FREERTOS.FootprintOK=true -FREERTOS.IPParameters=Tasks01,FootprintOK -FREERTOS.Tasks01=initTask,24,128,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL +FREERTOS.IPParameters=Tasks01,FootprintOK,Queues01 +FREERTOS.Queues01=CmdQueue,16,16,1,Dynamic,NULL,NULL +FREERTOS.Tasks01=initTask,24,128,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;CarCtrlTask,24,256,CarCtrl_Task,As weak,NULL,Dynamic,NULL,NULL File.Version=6 GPIO.groupedBy=Group By Peripherals KeepUserPlacement=false diff --git a/image-6.png b/image-6.png new file mode 100644 index 0000000..752ec58 Binary files /dev/null and b/image-6.png differ diff --git a/物流小车/esp_12f/TCPUDPDbg/config/lastsend.data b/物流小车/esp_12f/TCPUDPDbg/config/lastsend.data index 6b4ac24..211494f 100644 --- a/物流小车/esp_12f/TCPUDPDbg/config/lastsend.data +++ b/物流小车/esp_12f/TCPUDPDbg/config/lastsend.data @@ -1 +1 @@ -0ո \ No newline at end of file +0hhh \ No newline at end of file