feat: 添加协议处理和校验和计算功能,更新 UART 相关代码

This commit is contained in:
2026-04-02 01:52:28 +08:00
parent 8d2a0ea0c8
commit a53aa38ed3
11 changed files with 390 additions and 40 deletions

View File

@@ -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

View File

@@ -1,5 +1,9 @@
#include "bsp_uart.h"
#include "checksum.h"
#include "elog.h"
#include "protocol.h"
#include <stdint.h>
#include <string.h>
/**
* @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);
}
}

24
Core/Bsp/checksum.c Normal file
View File

@@ -0,0 +1,24 @@
#include <stdint.h>
/**
* @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);
}

17
Core/Bsp/checksum.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef CHECKSUM_H
#define CHECKSUM_H
#include <stdint.h>
/**
* @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

126
Core/Bsp/protocol.c Normal file
View File

@@ -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 <stdio.h>
#include <string.h>
/* 定义日志 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 */
}

16
Core/Bsp/protocol.h Normal file
View File

@@ -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 */

View File

@@ -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 */

130
README.md
View File

@@ -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)

View File

@@ -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

BIN
image-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -1 +1 @@
0ИеИе
0hhh