uart_rx.h

uart_rx.h

/**

******************************************************************************

* @file uart_rx.h

* @brief 串口中断接收、环形缓冲区和简单二进制帧接收。

*

* 本文件是团队自己写的代码,放在 Core/Inc;uart_rx.c 放在 Core/Src。

* CubeMX 重新生成代码不会覆盖这两个文件。

*

* 使用前确认 CubeMX 已配置:USARTx、USARTx global interrupt,且中断服务函数中

* 有 HAL_UART_IRQHandler(&huartx)。本模块自己实现 HAL_UART_RxCpltCallback,

* 工程中不要再定义第二个同名回调函数。

*

* --------------------------------------------------------------------------

* 【串口助手发送多条可读命令:回车换行结尾】

*

* UART_Rx_ReadLine() 用于接收人能看懂的 ASCII 命令。发送端每一条命令都应以

* 换行 '\n' 结束;串口助手一般选择“发送新行”或“CRLF”,即发送 \r\n。

* 本模块会忽略 \r,并把 \n 识别为一条命令结束。因此连续发送:

*

* LEDON\r\nPWM=50\r\n12\r\n

*

* 程序会依次得到三个独立字符串:"LEDON"、"PWM=50"、"12"。不能只使用 if

* 取一次,因为电脑可能一次就发送多条命令;应在 while 内把已完整接收的行全部处理。

*

* main.c 命令识别完整示例(需要在 Includes 区加入 #include <string.h>):

* @code

* char line[32];

*

* // 放在 while(1) 内。每轮会处理接收缓冲区中所有已收完整的命令。

* while (UART_Rx_ReadLine(&uart1_rx, line, sizeof(line)) == UART_RX_STATUS_OK)

* {

* if (strcmp(line, "LEDON") == 0)

* {

* // strcmp 返回 0 表示两个字符串完全相同。

* HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);

* UART_Tx_SendLine(&huart1, "LED is ON", 100U);

* }

* else if (strcmp(line, "LEDOFF") == 0)

* {

* HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);

* UART_Tx_SendLine(&huart1, "LED is OFF", 100U);

* }

* else if (strncmp(line, "PWM=", 4U) == 0)

* {

* // strncmp(..., 4U)==0 表示前四个字符正好是 P、W、M、=。

* // &line[4] 是等号后的第一个字符,例如 "PWM=50" 中的 "50"。

* uint32_t duty;

* if ((UART_Rx_ParseU32(&line[4], &duty) != 0U) && (duty <= 100U))

* {

* // 这里按你的 PWM 驱动实际函数替换,例如设置为 50% 占空比。

* // PWM_Driver_SetDutyPercent(&boost_pwm, (float)duty);

* UART_Tx_SendLine(&huart1, "PWM updated", 100U);

* }

* else

* {

* UART_Tx_SendLine(&huart1, "PWM must be 0~100", 100U);

* }

* }

* else

* {

* UART_Tx_SendLine(&huart1, "Unknown command", 100U);

* }

* }

* @endcode

*

* 比赛时题目会规定命令字和参数格式;只需要把 "LEDON"、"PWM=" 等字符串及其

* 对应处理动作替换成题目要求即可。若题目规定的是 AA 55 等二进制帧,不使用本段

* 的 ReadLine/strcmp 方法,应使用后面的 ReadByte + Protocol_Parser。

* --------------------------------------------------------------------------

*

* 二进制帧固定格式:

* HEADER[0] ... HEADER[N-1] LENGTH DATA[0] ... DATA[LENGTH-1] CHECKSUM

*

* - HEADER:帧头数组,长度 N 可为 1~16 字节,由题目决定。

* - LENGTH:DATA 区的字节数,不包括帧头、长度字节和校验字节。

* - CHECKSUM:SUM8/XOR8 为 1 字节;CRC16-Modbus 为 2 字节(低字节在前)。

* - 校验范围默认是 LENGTH + DATA,不包含帧头和校验字节。

*

* main.c 完整使用示例:

* @code

* // USER CODE BEGIN Includes

* #include "uart_rx.h"

*

* // USER CODE BEGIN PV

* static UART_Rx_t uart1_rx;

* static uint8_t uart1_rx_buffer[128]; // 中断收到的字节先暂存在这里

* static Protocol_Parser_t protocol;

* static Protocol_Frame_t received_frame;

*

* // USER CODE BEGIN 2,必须放在 MX_USART1_UART_Init() 之后:

* UART_Rx_Init(&uart1_rx, &huart1, uart1_rx_buffer, sizeof(uart1_rx_buffer));

* UART_Rx_Start(&uart1_rx); // 开始单字节中断接收

*

* // 题目若定义帧:AA 55 LEN DATA... SUM8,则这样写:

* static const uint8_t frame_header[] = {0xAAU, 0x55U};

* Protocol_Parser_Init(&protocol, frame_header, sizeof(frame_header),

* PROTOCOL_CHECKSUM_SUM8);

*

* // USER CODE BEGIN 3,放进 while(1):

* uint8_t one_byte;

* while (UART_Rx_ReadByte(&uart1_rx, &one_byte) == UART_RX_STATUS_OK)

* {

* if (Protocol_Parser_FeedByte(&protocol, one_byte) == PROTOCOL_STATUS_FRAME_READY)

* {

* Protocol_Parser_TakeFrame(&protocol, &received_frame);

*

* // 例如收到:AA 55 03 11 22 33 69

* // received_frame.length = 7U;

* // received_frame.data[0..6] = {0xAA,0x55,0x03,0x11,0x22,0x33,0x69};

* // 之后直接按数组下标使用,不需要关心命令、载荷等额外结构。

* // 第一个数据的下标 = 帧头长度 + 1 个 LENGTH 字节。

* uint8_t first_data = received_frame.data[sizeof(frame_header) + 1U]; // 0x11

* (void)first_data;

* }

* }

* @endcode

*

* 比赛时只改帧头数组内容、帧头数组长度和校验方式:

* @code

* static const uint8_t frame_header[] = {0x55U, 0xAAU, 0x12U, 0x34U};

* Protocol_Parser_Init(&protocol, frame_header, sizeof(frame_header),

* PROTOCOL_CHECKSUM_XOR8);

* // 可选校验方式:PROTOCOL_CHECKSUM_SUM8、PROTOCOL_CHECKSUM_XOR8、

* // PROTOCOL_CHECKSUM_CRC16_MODBUS、PROTOCOL_CHECKSUM_NONE。

* @endcode

******************************************************************************

*/

#ifndef UART_RX_H

#define UART_RX_H

#ifdef __cplusplus

extern "C" {

#endif

#include "usart.h"

typedef enum

{

UART_RX_STATUS_OK = 0,

UART_RX_STATUS_EMPTY,

UART_RX_STATUS_INCOMPLETE,

UART_RX_STATUS_INVALID_ARG,

UART_RX_STATUS_NOT_INITIALIZED,

UART_RX_STATUS_BUSY,

UART_RX_STATUS_HAL_ERROR,

UART_RX_STATUS_BUFFER_SMALL

} UART_Rx_Status_t;

/* UART 中断接收对象。应用层只需定义变量并传给 Init,不要手动修改成员。 */

typedef struct

{

UART_HandleTypeDef *huart;

uint8_t *buffer;

uint16_t capacity;

volatile uint16_t read_index;

volatile uint16_t write_index;

uint8_t rx_byte;

volatile uint32_t overflow_count;

volatile uint32_t error_count;

uint8_t initialized;

uint8_t running;

} UART_Rx_t;

UART_Rx_Status_t UART_Rx_Init(UART_Rx_t *device, UART_HandleTypeDef *huart,

uint8_t *buffer, uint16_t capacity);

UART_Rx_Status_t UART_Rx_Start(UART_Rx_t *device);

UART_Rx_Status_t UART_Rx_Stop(UART_Rx_t *device);

uint16_t UART_Rx_Available(const UART_Rx_t *device);

UART_Rx_Status_t UART_Rx_ReadByte(UART_Rx_t *device, uint8_t *byte);

/**

* @brief 从串口环形缓冲区取出一整行 ASCII 文本,并转换成 C 字符串。

* @param device 已 Init 且 Start 的 UART 接收对象,例如 &uart1_rx。

* @param line 用户提供的 char 数组,用于保存文本,例如 char line[32]。

* @param line_capacity line 数组总容量,必须写 sizeof(line),不要手写数字。

* @return UART_RX_STATUS_OK:已得到一行;其他常见返回值见下方。

*

* 本函数适用于“串口助手文本发送”,不是十六进制二进制帧解析器。

* 例如电脑发送 hello 并按回车,串口收到的原始字节是:

* 0x68 0x65 0x6C 0x6C 0x6F 0x0D 0x0A

* 本函数返回后 line 内容是 "hello";末尾的 \r 和 \n 被自动丢弃,且自动补 \0。

*

* 返回值含义:

* - UART_RX_STATUS_OK:已经收到 \n,line 内是一条完整字符串。

* - UART_RX_STATUS_EMPTY:目前没有收到任何字节。

* - UART_RX_STATUS_INCOMPLETE:收到了一部分文本,但尚未收到 \n;下次 while 循环再调用。

* - UART_RX_STATUS_BUFFER_SMALL:完整一行长度超过 line 数组容量;该行被丢弃。

*

* main.c 使用例子:

* @code

* char line[32];

* if (UART_Rx_ReadLine(&uart1_rx, line, sizeof(line)) == UART_RX_STATUS_OK)

* {

* // 电脑文本发送 hello + 回车:line 为 "hello"。

* // 电脑文本发送 12 + 回车:line[0]='1',line[1]='2',不是数值 12。

* UART_Tx_SendLine(&huart1, line, 100U);

* }

* @endcode

*

* 若题目给的是 AA 55 LEN DATA CHECKSUM 这类二进制报文,请使用

* UART_Rx_ReadByte() + Protocol_Parser_FeedByte(),不要使用本函数。

*/

UART_Rx_Status_t UART_Rx_ReadLine(UART_Rx_t *device, char *line, uint16_t line_capacity);

/**

* @brief 将一行纯十进制 ASCII 文本转换为可计算的 uint32_t 无符号整数。

* @param text 以 \0 结尾的字符串,通常来自 UART_Rx_ReadLine(),例如 "12"。

* @param value 转换成功后写入数值,例如 "12" 转为 12U。

* @return 1=转换成功;0=字符串为空、含非数字字符或数值超过 uint32_t 范围。

*

* 此函数只接受十进制非负整数,允许首尾空格和开头的 + 号:

* "12"、" 12 "、"+12" 均成功;"12.5"、"-12"、"hello" 均失败。

*

* main.c 使用例子:

* @code

* char line[32];

* uint32_t number;

* if (UART_Rx_ReadLine(&uart1_rx, line, sizeof(line)) == UART_RX_STATUS_OK)

* {

* if (UART_Rx_ParseU32(line, &number) != 0U)

* {

* number = number + 1U; // 电脑发 12,number 变成 13

* UART_Tx_SendU32(&huart1, number, 100U);

* }

* }

* @endcode

*/

uint8_t UART_Rx_ParseU32(const char *text, uint32_t *value);

/**

* @brief 将一行 ASCII 小数文本转换为可计算的 float 浮点数。

* @param text 以 \0 结尾的字符串,通常来自 UART_Rx_ReadLine();例如 "3.287"。

* @param value 转换成功后写入浮点结果的变量地址,例如 &dac_pin_voltage。

* @return 1U:转换成功;0U:字符串格式错误或参数为 NULL,value 保持原值。

*

* 支持:首尾空格、可选的 + / - 号、整数部分和小数部分,例如 "12"、"3.287"、

* "-0.5"、".25"。不支持科学计数法(例如 "1e-3")、逗号小数点("3,2")

* 和单位("3.3V")。发送端每条数据仍须以 \r\n 结尾。

*

* main.c 使用案例:电脑发送 3.287 并勾选“发送新行(CRLF)”后,变量保存为 3.287f。

* @code

* char line[32];

* float dac_pin_voltage;

*

* if (UART_Rx_ReadLine(&uart1_rx, line, sizeof(line)) == UART_RX_STATUS_OK)

* {

* if (UART_Rx_ParseFloat(line, &dac_pin_voltage) != 0U)

* {

* // 例如:3.287 + 1.0 = 4.287

* dac_pin_voltage = dac_pin_voltage + 1.0f;

*

* // 参数依次为:串口、浮点值、小数位数、超时 ms。

* UART_Tx_SendFloat(&huart1, dac_pin_voltage, 3U, 100U);

* UART_Tx_SendLine(&huart1, "", 100U);

* }

* else

* {

* UART_Tx_SendLine(&huart1, "Float format error", 100U);

* }

* }

* @endcode

*/

uint8_t UART_Rx_ParseFloat(const char *text, float *value);

UART_Rx_Status_t UART_Rx_Clear(UART_Rx_t *device);

uint32_t UART_Rx_GetOverflowCount(const UART_Rx_t *device);

uint32_t UART_Rx_GetErrorCount(const UART_Rx_t *device);

/* 单帧 DATA 最大长度。完整数组还需容纳帧头、长度和最多两个校验字节。 */

#define PROTOCOL_MAX_HEADER_LENGTH 16U /* 题目帧头超过 16 字节时,只改这个数值。 */

#define PROTOCOL_MAX_DATA_LENGTH 128U

#define PROTOCOL_MAX_FRAME_LENGTH (PROTOCOL_MAX_HEADER_LENGTH + 1U + PROTOCOL_MAX_DATA_LENGTH + 2U)

typedef enum

{

PROTOCOL_CHECKSUM_NONE = 0, /* 无校验:帧尾没有校验字节。 */

PROTOCOL_CHECKSUM_SUM8, /* LENGTH 和 DATA 累加,保留低 8 位。 */

PROTOCOL_CHECKSUM_XOR8, /* LENGTH 和 DATA 逐字节异或。 */

PROTOCOL_CHECKSUM_CRC16_MODBUS/* CRC 初值 FFFF,低字节先到。 */

} Protocol_Checksum_t;

typedef enum

{

PROTOCOL_STATUS_OK = 0,

PROTOCOL_STATUS_FRAME_READY, /* 一帧已校验完成,可调用 TakeFrame。 */

PROTOCOL_STATUS_INVALID_ARGUMENT,

PROTOCOL_STATUS_NOT_READY,

PROTOCOL_STATUS_LENGTH_ERROR,

PROTOCOL_STATUS_CHECKSUM_ERROR,

PROTOCOL_STATUS_BUSY

} Protocol_Status_t;

/* 这就是给业务程序用的完整帧数组。data[0] 永远是帧头的第一个字节。 */

typedef struct

{

uint16_t length; /* data[] 实际有效字节数。 */

uint8_t data[PROTOCOL_MAX_FRAME_LENGTH];/* 完整帧:帧头、长度、数据、校验。 */

} Protocol_Frame_t;

/* 解析器内部状态。只需定义变量和调用 API,不需直接修改成员。 */

typedef struct

{

uint8_t header[PROTOCOL_MAX_HEADER_LENGTH];

uint8_t header_length;

uint8_t header_index;

Protocol_Checksum_t checksum_type;

uint8_t state;

uint8_t data_length;

uint8_t received_data_count;

uint16_t checksum;

uint8_t received_crc_low;

Protocol_Frame_t completed_frame;

uint8_t frame_ready;

uint8_t initialized;

uint32_t frame_count;

uint32_t length_error_count;

uint32_t checksum_error_count;

} Protocol_Parser_t;

/* 设置帧头数组、帧头长度和校验方式;格式始终为 HEADER LENGTH DATA CHECKSUM。 */

Protocol_Status_t Protocol_Parser_Init(Protocol_Parser_t *parser,

const uint8_t *header, uint8_t header_length,

Protocol_Checksum_t checksum_type);

/* 每从 UART 环形缓冲区读到一个字节,就调用一次。 */

Protocol_Status_t Protocol_Parser_FeedByte(Protocol_Parser_t *parser, uint8_t byte);

uint8_t Protocol_Parser_IsFrameReady(const Protocol_Parser_t *parser);

/* 把已完成的一帧复制到 frame->data[],并允许接收下一帧。 */

Protocol_Status_t Protocol_Parser_TakeFrame(Protocol_Parser_t *parser,

Protocol_Frame_t *frame);

#ifdef __cplusplus

}

#endif

#endif /* UART_RX_H */