串口通信是嵌入式系统中最常用的通信方式之一,特别是在STM32这类微控制器上。它就像两个人用对讲机通话,一方发送数据,另一方接收数据。在实际项目中,我经常用串口来传输传感器数据,比如电机转速、温度值等。
STM32的串口外设非常灵活,支持多种配置。最基本的设置包括波特率、数据位、停止位和校验位。这里有个小技巧:波特率设置要确保发送端和接收端一致,否则就像两个说不同语言的人对话,完全无法理解对方。常用的波特率有9600、115200等,在电机控制这类实时性要求高的场景,我通常选择115200。
初始化串口的代码看起来是这样的:
c复制void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置TX引脚(PA9)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置RX引脚(PA10)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 串口参数配置
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 使能串口
USART_Cmd(USART1, ENABLE);
}
在实际项目中,我发现很多人会忽略硬件流控的设置。如果通信距离较长或者环境干扰大,建议启用RTS/CTS硬件流控,可以有效避免数据丢失。我曾经在一个工业现场调试,就是因为没开硬件流控,导致数据时不时出错,排查了好久才发现这个问题。
在电机控制系统中,我们通常需要发送多种数据,比如位置、速度等。直接发送原始二进制数据虽然效率高,但可读性差,调试困难。我更喜欢用字符串格式,就像把数据打包成快递,加上清晰的标签。
常用的字符串格式化函数是sprintf,它就像个聪明的包装工,能把各种数据类型整齐地打包成字符串。比如要发送电机位置和速度:
c复制float motor_position = 123.456;
float motor_velocity = 78.90;
char data_str[64];
sprintf(data_str, "POS:%-8.4f,VEL:%-8.4f\n", motor_position, motor_velocity);
Usart_SendString(USART1, data_str);
这里有几个实用技巧:
在真实项目中,我建议使用更结构化的格式,比如CSV(逗号分隔值)。这样既保持可读性,又方便后期处理。例如:
c复制sprintf(data_str, "%.4f,%.4f,%.4f,%.4f\n",
motor_position, sensor_position,
motor_velocity, sensor_velocity);
我曾经踩过一个坑:没有预留足够的缓冲区大小。当数据突然变大时,sprintf会溢出,导致系统崩溃。所以一定要确保字符数组足够大,或者使用更安全的snprintf函数。
接收数据就像在嘈杂的派对上听清朋友说话,需要一些技巧。STM32通常使用中断方式接收数据,这样可以及时响应,不占用CPU时间。
一个健壮的接收程序需要考虑以下几点:
下面是一个典型的接收中断服务程序框架:
c复制#define MAX_RX_LEN 128
char USART_RX_BUF[MAX_RX_LEN];
volatile uint16_t USART_RX_STA = 0;
void USART1_IRQHandler(void)
{
uint8_t Res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res = USART_ReceiveData(USART1);
// 简单帧判断:以'\r\n'结尾
if(USART_RX_STA < MAX_RX_LEN)
{
if(Res == '\r')
{
// 等待'\n'
}
else if(Res == '\n')
{
// 完成接收
USART_RX_BUF[USART_RX_STA] = '\0'; // 字符串终结符
ProcessReceivedData(USART_RX_BUF);
USART_RX_STA = 0;
}
else
{
USART_RX_BUF[USART_RX_STA++] = Res;
}
}
else
{
// 缓冲区溢出
USART_RX_STA = 0;
}
}
}
在实际项目中,我遇到过电磁干扰导致数据错误的情况。后来我增加了奇偶校验位,效果立竿见影。比如使用简单的累加和校验:
c复制uint8_t CalculateChecksum(const char *data, uint8_t len)
{
uint8_t sum = 0;
for(uint8_t i=0; i<len; i++)
{
sum += data[i];
}
return sum;
}
接收到的字符串需要转换成数值才能用于控制算法。这就好比把菜谱上的文字描述变成实际的烹饪操作。C语言提供了atoi、atof等函数,但在嵌入式系统中,我们经常需要更定制化的转换。
下面是一个支持正负号和小数点的字符串转浮点数函数:
c复制float StringToFloat(const char *str)
{
float result = 0.0;
float fraction = 0.1;
int8_t sign = 1;
uint8_t decimal_flag = 0;
// 处理符号位
if(*str == '-')
{
sign = -1;
str++;
}
else if(*str == '+')
{
str++;
}
// 转换整数和小数部分
while(*str != '\0')
{
if(*str == '.')
{
decimal_flag = 1;
}
else if(*str >= '0' && *str <= '9')
{
if(!decimal_flag)
{
result = result * 10 + (*str - '0');
}
else
{
result += (*str - '0') * fraction;
fraction *= 0.1;
}
}
str++;
}
return sign * result;
}
在电机控制项目中,数值转换的效率和精度都很重要。我有几点经验分享:
我曾经优化过一个转换函数,通过查表法将转换时间缩短了60%。关键是要根据具体应用场景选择最合适的方案。
让我们看一个完整的电机控制系统数据通信实例。假设我们需要传输以下数据:
首先定义通信协议:
发送端代码:
c复制void SendMotorData(float pos, float enc_pos, float speed, uint8_t status)
{
char buf[64];
uint8_t checksum = 0;
uint8_t len = sprintf(buf, "$%.2f,%.2f,%.2f,%d",
pos, enc_pos, speed, status);
// 计算校验
for(uint8_t i=1; i<len; i++) // 跳过'$'
{
checksum ^= buf[i];
}
// 添加校验和帧尾
sprintf(buf+len, ",%02X\r\n", checksum);
Usart_SendString(USART1, buf);
}
接收端解析代码:
c复制void ParseMotorData(const char *data)
{
// 示例数据: "$123.45,123.40,3000.00,5,3A\r\n"
char *ptr;
float motor_pos, enc_pos, speed;
uint8_t status, checksum, calc_checksum=0;
// 检查帧头
if(data[0] != '$') return;
// 计算校验(跳过帧头和最后的校验部分)
uint8_t i;
for(i=1; data[i] && data[i]!=','; i++); // 找到第一个逗号
for(; data[i] && data[i+3]!='\r'; i++)
{
if(data[i] != ',') calc_checksum ^= data[i];
}
// 提取校验值
sscanf(data+i+1, "%02X", &checksum);
// 校验比对
if(calc_checksum != checksum) return;
// 解析数据
ptr = strtok((char*)data+1, ",");
motor_pos = atof(ptr);
ptr = strtok(NULL, ",");
enc_pos = atof(ptr);
ptr = strtok(NULL, ",");
speed = atof(ptr);
ptr = strtok(NULL, ",");
status = atoi(ptr);
// 使用解析后的数据...
MotorControl(motor_pos, speed);
}
在这个案例中,我添加了异或校验来提高通信可靠性。实际测试发现,在工业环境中,这样的校验机制可以过滤掉99%以上的干扰错误。同时,使用固定的浮点精度(如%.2f)可以减少数据传输量,提高效率。
在STM32串口通信开发中,我遇到过各种各样的问题。这里分享几个典型问题及其解决方法:
问题1:数据接收不完整
症状:只能收到部分数据,或者数据被截断
可能原因:
问题2:数据偶尔出错
症状:大部分数据正常,偶尔出现乱码
可能原因:
问题3:通信一段时间后死机
症状:系统运行一段时间后串口不工作
可能原因:
调试串口通信时,我有几个常用技巧:
曾经有个项目,串口通信总是随机出错。后来用逻辑分析仪发现是电源噪声导致的,在串口线上加了个小电容就解决了。这说明硬件问题有时也会表现为软件故障,调试时要全面考虑。
当系统需要高速传输大量数据时,普通的串口通信方式可能成为瓶颈。经过多个项目的积累,我总结出以下优化技巧:
DMA传输
使用DMA可以大幅减轻CPU负担,特别是在高速通信时。配置步骤:
示例代码:
c复制void USART1_DMA_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 发送DMA配置
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = TX_BUF_SIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
// 使能DMA
DMA_Cmd(DMA1_Channel4, ENABLE);
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
}
双缓冲区技术
对于实时性要求高的应用,可以采用双缓冲区:
数据压缩
对于需要传输大量数据的应用,可以考虑简单的数据压缩算法,如:
在最近的一个四轴飞行器项目中,我使用DMA+双缓冲区技术,将串口通信的CPU占用率从15%降到了不到2%,同时提高了数据传输的实时性。这让我深刻体会到,好的优化可以带来质的飞跃。