本帖最后由 机器人爱好者1991 于 2023-10-19 16:40 编辑
单片机的串口在调试中非常重要。串口可以用于打印单片机的变量数值,方便排查问题。在调试中都是非常重要的工具,可以显著的提高调试效率。
一、引脚分配。
串口用到了stm32重要的外设资源,uart。对于这个板子是没有串口转换芯片的,直接就是TX、RX的引脚。原理图的CN9标注USART的地方,4和6引脚,分别是PD6(RX)和PD5(TX)引脚。我们就选择这个。
二、串口的概念
串口是一种通信接口,用于在设备之间传输数据。在STM32中,串口主要分为UART和USART两种类型。
UART是通用异步收发传输器(Universal Asynchronous Receiver Transmitter)的缩写,它是一种简单的串口通信方式。UART只支持异步通信,也就是数据的传输不需要时钟信号,而是通过起始位、数据位、校验位和停止位来进行数据的传输。
USART是通用同步异步收发传输器(Universal Synchronous/Asynchronous Receiver Transmitter)的缩写,它是一种更为复杂的串口通信方式。USART既支持异步通信,也支持同步通信。异步通信与UART相似,而同步通信则需要使用外部时钟信号来同步数据的传输。
总的来说,UART和USART都是串口通信的方式,UART是一种简单的异步通信方式,而USART则是一种更为复杂的通信方式,支持异步和同步通信。在选择使用哪种串口通信方式时,需要根据具体的应用需求和外部设备的要求来决定。
三、串口的配置
配置相对简单,就是设置Mode为异步,打开串口2的全局中断,设置引脚,设置比特率等信息。
四、串口的发送
代码主要用于在STM32微控制器上配置和管理USART2的UART通信。 MX_USART2_UART_Init 函数用于初始化USART2的通信参数,包括波特率、数据位、停止位等,然后通过调用HAL_UART_Init函数完成初始化。
HAL_UART_MspInit 函数负责配置与USART2相关的GPIO引脚和中断,包括使能USART2时钟、配置GPIO引脚为复用模式、设置中断优先级等。在STM32微控制器中,一个GPIO引脚可以有多种不同的功能,比如作为普通数字输入/输出、串行通信接口(如USART、SPI、I2C)的数据线,或者其他功能。这种切换功能的能力称为引脚的复用功能。Alternate 字段是一个用来指定GPIO引脚复用功能的设置。在这里,GPIO_AF7_USART2 指定了将这些引脚配置为与USART2通信相关的功能。
最后,HAL_UART_MspDeInit 函数用于反初始化USART2,包括禁用USART2时钟、释放GPIO引脚配置以及禁用中断。这些函数协同工作以确保USART2 UART通信正确初始化和反初始化。
中断发送。while(huart2.gState != HAL_UART_STATE_READY);这是一个 while 循环,用于等待USART2的发送状态变为 HAL_UART_STATE_READY,即等待数据发送完成。在这个循环中,程序会不断检查 huart2.gState 的状态,如果不等于 HAL_UART_STATE_READY,就会一直循环等待。一旦 huart2.gState 变为 HAL_UART_STATE_READY,表示数据发送完成,循环结束,程序继续执行后续操作。
HAL_UART_Transmit(&huart2, (uint8_t*)send_buffer, strlen(send_buffer), HAL_MAX_DELAY);
while(huart2.gState != HAL_UART_STATE_READY);
五、串口的接收
阻塞接收。定义一个数组 uint8_t RxBuffer[10] = {0};直接在while(1)里面,设置接收的长度以及超时时间1s。跳出的条件是超时或者是接收完10个字节。
if(HAL_UART_Receive(&huart1,RxBuffer,sizeof(RxBuffer),1000) == HAL_OK)
{
HAL_UART_Transmit(&huart1,RxBuffer,sizeof(RxBuffer),100);
}
中断接收。在UI界面开启中断与设置中断函数。设置中断接收HAL_UART_Receive_IT(&huart2, (uint8_t *)&usart2_rx_buffer, 1);和中断处理函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)进行组合。
uint8_t RxBuffer[8] = {0,1,2,3,4,5,6,7};
void MX_USART2_UART_Init(void)
{
/* USER CODE BEGIN USART2_Init 2 */
HAL_UART_Receive_IT(&huart2, RxBuffer, sizeof(RxBuffer));
/* USER CODE END USART2_Init 2 */
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart2)
{
HAL_UART_Transmit(&huart2, RxBuffer, sizeof(RxBuffer), 100);
memset(RxBuffer, 0, sizeof(RxBuffer));
HAL_UART_Receive_IT(&huart2, RxBuffer, sizeof(RxBuffer));
}
}
我这里出现的问题是大于sizeof(RxBuffer)之后,就不进入中断了。实际想接收8个字符/次,上位机发送9个字符之后,整个串口中断就进去不了了,缓冲区阻塞了。最后,将RxBuffer的size设置成了1,会好一些;又将波特率从115200变成9600就更好了。这个问题以后有时间再详细探究。
DMA接收。DMA的接收有时间再补,随便写可能会有遗漏。
六、通信协议数据的发送接收
6.1 接收字符的处理
因为我发送的0x01的时间和0x02的速度,消息不定长,我们采用的是每次接收一个字符。然后进行拼接。不只是单片机,我的上位机也使用了一次一个字符进行拼接的策略。我定义了一个struct,struct里面是rx_buffer来存储接收缓冲;rx_index表示接收第几个字节了;frame_started类似于一个bool数值,表示帧接收完帧头否。
typedef struct
{
uint8_t rx_buffer[256]; // 接收缓冲
uint8_t rx_index; // 接收缓冲区索引
uint8_t frame_started; // 帧接收完帧头否
} rx_data_t;
下面的接收流程,就是一个个的字符接收,然后拼成完整的一帧。主要功能是根据特定的帧头和帧尾标识,进行 CRC 校验,最后解析接收到的数据帧。
这里面分为帧头的检测、帧尾的检测、CRC16-Modbus的校验,完整数据的输入解析。如果索引为0且接收到的字符等于帧头1(0x55),则重置接收,将接收索引设置为1,并将接收到的字符存入接收缓冲区。如果索引为1且接收到的字符等于帧头2(0xaa),则设置frame_started标志表示有新的数据帧开始,将接收索引设置为2,并将接收到的字符存入接收缓冲区。如果索引大于等于2且frame_started标志为真,则将接收到的字符存入接收缓冲区,并递增接收索引。判断是否接收到了帧尾。如果接收索引大于等于6且接收缓冲区倒数第二个字符等于帧尾1,并且倒数第一个字符等于帧尾2,则执行以下操作:调用函数计算接收缓冲区中数据部分的 CRC16 校验值,函数解析接收到的数据帧。最后,重置frame_started标志、接收索引和接收缓冲区,以准备接收下一帧数据。
void recv_uchar(const uint8_t *buffer, const int16_t length)
{
uint8_t crc_low, crc_high;
rx_buffer_temp[0]=buffer[0];
// 1. 如果,索引为0而且收到等于帧头1的字符,就得重置接收
if (rx_data.rx_index == 0 && rx_buffer_temp[0] == 0x55)
{
rx_data.rx_index = 1;
rx_data.rx_buffer[0] = rx_buffer_temp[0];
}
// 2. 如果,索引为1而且收到等于帧头2的字符,设置个 frame_started 来表示有新数据了
else if (rx_data.rx_index == 1 && rx_buffer_temp[0] == 0xaa)
{
rx_data.rx_index = 2;
rx_data.frame_started = 1;
rx_data.rx_buffer[1] = rx_buffer_temp[0];
}
// 3. 对数组[2]以后的接收处理
else if (rx_data.rx_index >= 2 && rx_data.frame_started)
{
rx_data.rx_buffer[rx_data.rx_index++] = rx_buffer_temp[0];
// 4. 判断是否收到了帧尾
if (rx_data.rx_index >= 6 && rx_data.rx_buffer[rx_data.rx_index - 2] == frame_footer1 &&
rx_data.rx_buffer[rx_data.rx_index - 1] == frame_footer2)
{
// 5. crc16校验
calculate_crc(&rx_data.rx_buffer[5], rx_data.rx_buffer[4], &crc_low, &crc_high);
if (crc_low == rx_data.rx_buffer[rx_data.rx_index - 4] &&
crc_high == rx_data.rx_buffer[rx_data.rx_index - 3])
{
// printf("CRC success\n");
parse_serial_data(rx_data.rx_buffer, rx_data.rx_index);
}
else
{
printf("CRC failed\n");
}
rx_data.frame_started = 0;
rx_data.rx_index = 0;
memset(rx_data.rx_buffer,0,sizeof(rx_data.rx_buffer));
}
}
else
{
;
}
}
对于速度数据的处理,就是按照小端格式就行赋值即可。
/*解析返回的速度*/
void recv_velocity_cmd(const uint8_t *buffer, const int16_t length, int16_t recv_velocity[2])
{
if (0x06 != buffer[2] || 0x02 != buffer[3])
{
return;
}
recv_velocity[0] = (int16_t *)(buffer[5] + ((int16_t)(buffer[6]) << 8));
recv_velocity[1] = (int16_t *)(buffer[7] + ((int16_t)(buffer[8]) << 8));
printf("left_velocity=%d,right_velocity=%d\n", recv_velocity[0], recv_velocity[1]);
send_velocity[0]=recv_velocity[0];
send_velocity[1]=recv_velocity[1];
}
/*解析返回的时间*/
void recv_time_cmd(const uint8_t *buffer, const int16_t length, int16_t recv_beijing_time[6])
{
if (0x06 != buffer[2] || 0x01 != buffer[3])
{
return;
}
recv_beijing_time[0] = (int16_t *)(buffer[5] + ((int16_t)(buffer[6]) << 8));
recv_beijing_time[1] = (int16_t *)(buffer[7] + ((int16_t)(buffer[8]) << 8));
recv_beijing_time[2] = (int16_t *)(buffer[9] + ((int16_t)(buffer[10]) << 8));
recv_beijing_time[3] = (int16_t *)(buffer[11] + ((int16_t)(buffer[12]) << 8));
recv_beijing_time[4] = (int16_t *)(buffer[13] + ((int16_t)(buffer[14]) << 8));
recv_beijing_time[5] = (int16_t *)(buffer[15] + ((int16_t)(buffer[16]) << 8));
printf("year=%d,month=%d,day=%d,hour=%d,minute=%d,second=%d\n", recv_beijing_time[0],recv_beijing_time[1],recv_beijing_time[2],recv_beijing_time[3],recv_beijing_time[4],recv_beijing_time[5]);
}
6.2 字符命令的发送
按照0-1为帧头,0x06表示写入,0x01表示时间,下一个字节是数据的长度,下面的按照小端格式对年月日时分秒,进行每个变量2个字节的占位进行赋值。接线来进行CRC16的校验填充,最后加两个帧尾。
void send_current_time(const int16_t beijing_time[6])
{
uint8_t write_data[21];
uint8_t crc_low, crc_high;
int16_t send_index = 0;
write_data[send_index++] = frame_header1;
write_data[send_index++] = frame_header2;
write_data[send_index++] = 0x06; // 写入
write_data[send_index++] = 0x01; // 时间
write_data[send_index++] = 12; // 数据长度
// 将年、月、日、时、分、秒按小端格式存储到数组中
write_data[send_index++] = beijing_time[0] & 0xFF;
write_data[send_index++] = beijing_time[0] >> 8;
write_data[send_index++] = beijing_time[1] & 0xFF;
write_data[send_index++] = beijing_time[1] >> 8;
write_data[send_index++] = beijing_time[2] & 0xFF;
write_data[send_index++] = beijing_time[2] >> 8;
write_data[send_index++] = beijing_time[3] & 0xFF;
write_data[send_index++] = beijing_time[3] >> 8;
write_data[send_index++] = beijing_time[4] & 0xFF;
write_data[send_index++] = beijing_time[4] >> 8;
write_data[send_index++] = beijing_time[5] & 0xFF;
write_data[send_index++] = beijing_time[5] >> 8;
// 计算CRC16校验
calculate_crc(&write_data[5], write_data[4], &crc_low, &crc_high);
// 将CRC校验值赋值给write_data[16]和write_data[17]
write_data[send_index++] = crc_low;
write_data[send_index++] = crc_high;
write_data[send_index++] = frame_footer1;
write_data[send_index++] = frame_footer2;
HAL_UART_Transmit(&huart2,write_data,send_index,100);
}
6.3 串口2的一些配置
当然得使能一下串口2的printf。
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
PUTCHAR_PROTOTYPE
{
// 将字符发送到UART2
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
串口2的初始化。
typedef struct
{
uint8_t rx_buffer[256]; // 接收缓冲
uint8_t rx_index; // 接收缓冲区索引
uint8_t frame_started; // 帧接收完帧头否
} rx_data_t;
rx_data_t rx_data; // 接收相关数据的结构体实例
uint8_t rx_buffer_temp[1] = {0};
void MX_USART2_UART_Init(void)
{
/* USER CODE BEGIN USART2_Init 2 */
HAL_UART_Receive_IT(&huart2, rx_buffer_temp, sizeof(rx_buffer_temp));
rx_data.rx_index=0;
rx_data.frame_started=0;
memset(rx_data.rx_buffer,0,sizeof(rx_data.rx_buffer));
/* USER CODE END USART2_Init 2 */
}
串口2的单个字符接收函数。
// 接收数据回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
uint8_t crc_low, crc_high;
if (huart == &huart2)
{
rx_buffer_temp[0]=huart->Instance->DR;
// 1. 如果,索引为0而且收到等于帧头1的字符,就得重置接收
if (rx_data.rx_index == 0 && rx_buffer_temp[0] == 0x55)
{
rx_data.rx_index = 1;
rx_data.rx_buffer[0] = rx_buffer_temp[0];
// printf("recv header1\n");
}
// 2. 如果,索引为1而且收到等于帧头2的字符,设置个 frame_started 来表示有新数据了
else if (rx_data.rx_index == 1 && rx_buffer_temp[0] == 0xaa)
{
rx_data.rx_index = 2;
rx_data.frame_started = 1;
rx_data.rx_buffer[1] = rx_buffer_temp[0];
// printf("recv header2\n");
}
// 3. 对数组[2]以后的接收处理
else if (rx_data.rx_index >= 2 && rx_data.frame_started)
{
rx_data.rx_buffer[rx_data.rx_index++] = rx_buffer_temp[0];
// 4. 判断是否收到了帧尾
if (rx_data.rx_index >= 6 && rx_data.rx_buffer[rx_data.rx_index - 2] == frame_footer1 &&
rx_data.rx_buffer[rx_data.rx_index - 1] == frame_footer2)
{
// printf("recv footer\n");
// HAL_UART_Transmit(&huart2, rx_data.rx_buffer, rx_data.rx_index, HAL_MAX_DELAY);
// 5. crc16校验
calculate_crc(&rx_data.rx_buffer[5], rx_data.rx_buffer[4], &crc_low, &crc_high);
// HAL_UART_Transmit(&huart2, &rx_data.rx_buffer[5], rx_data.rx_buffer[4], HAL_MAX_DELAY);
// HAL_UART_Transmit(&huart2, crc_low, 1, HAL_MAX_DELAY);
// HAL_UART_Transmit(&huart2, crc_high, 1, HAL_MAX_DELAY);
if (crc_low == rx_data.rx_buffer[rx_data.rx_index - 4] &&
crc_high == rx_data.rx_buffer[rx_data.rx_index - 3])
{
printf("CRC success\n");
;
recv_data_parse(rx_data.rx_buffer, rx_data.rx_index);
}
else
{
printf("CRC failed\n");
recv_data_parse(rx_data.rx_buffer, rx_data.rx_index);
// HAL_UART_Transmit(&huart2, rx_data.rx_buffer, rx_data.rx_index, HAL_MAX_DELAY);
// HAL_UART_Transmit(&huart2, &rx_data.rx_buffer[5], rx_data.rx_buffer[4], HAL_MAX_DELAY);
// HAL_UART_Transmit(&huart2, crc_low, 1, HAL_MAX_DELAY);
// HAL_UART_Transmit(&huart2, crc_high, 1, HAL_MAX_DELAY);
// HAL_UART_Transmit(&huart2, rx_data.rx_buffer[rx_data.rx_index - 4], 1, HAL_MAX_DELAY);
// HAL_UART_Transmit(&huart2, rx_data.rx_buffer[rx_data.rx_index - 3], 1, HAL_MAX_DELAY);
}
rx_data.frame_started = 0;
rx_data.rx_index = 0;
memset(rx_data.rx_buffer,0,sizeof(rx_data.rx_buffer));
}
}
else
{
;
}
memset(rx_buffer_temp, 0, sizeof(rx_buffer_temp));
HAL_UART_Receive_IT(&huart2, rx_buffer_temp, sizeof(rx_buffer_temp));
}
}
6.3 测试
在windows上打开串口8,按下按键,会发送相应的指令到下位机。下位机将收到的指令recv数组赋值给send数组,发送到上位机,进行解析。
windows下的上位机,出现了一些汉字显示的问题,删除汉字,使用下面的指令编译了一下。
C:\mingw64\bin\x86_64-w64-mingw32-gcc -o serial.exe serial_server.c -lpthread