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