Jetson Nano与STM32高效串口通信实战

张开发
2026/4/17 23:06:00 15 分钟阅读

分享文章

Jetson Nano与STM32高效串口通信实战
1. Jetson Nano与STM32串口通信基础第一次接触Jetson Nano和STM32的串口通信时我踩了不少坑。记得当时为了调试一个简单的数据收发功能整整花了两天时间。现在回想起来其实只要掌握几个关键点就能快速搭建起稳定的通信链路。在机器人或智能小车项目中Jetson Nano通常负责视觉识别和决策而STM32则专注于电机控制等实时任务。它们之间的通信就像两个人在对话需要约定好语言通信协议和说话方式数据传输格式。串口通信因其简单可靠成为这种场景下的首选方案。硬件连接上Jetson Nano的串口引脚是TXD发送和RXD接收需要与STM32的USART接口交叉连接。我常用的接线方式是Jetson Nano TXD → STM32 RXDJetson Nano RXD → STM32 TXD两边的GND一定要连接这是保证信号稳定的关键波特率设置也很重要115200是个比较通用的选择。太高可能导致通信不稳定太低又会影响实时性。在我的智能小车项目里这个速率既能满足控制指令的传输需求又不会给STM32带来太大负担。2. Jetson Nano端串口配置详解2.1 基础环境搭建在Jetson Nano上使用串口前需要先安装必要的Python库。我推荐使用serial库它简单易用且功能完善。安装命令如下pip install pyserial第一次使用时很多人会遇到权限问题。这是因为Linux系统默认不会给普通用户串口设备的访问权限。解决方法有两种临时方案每次重启后需要重新执行sudo chmod 777 /dev/ttyTHS1永久方案推荐sudo usermod -a -G dialout $USER执行完永久方案后需要重启才能生效。这个命令将当前用户加入dialout组该组默认拥有串口设备的访问权限。2.2 串口初始化代码解析初始化串口的Python代码看似简单但有几个参数需要特别注意import serial ser serial.Serial( port/dev/ttyTHS1, baudrate115200, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout1 )port指定使用的串口设备Jetson Nano的默认串口是/dev/ttyTHS1baudrate波特率必须与STM32端保持一致bytesize数据位长度通常使用8位parity校验位一般设为无校验stopbits停止位常用1位timeout读取超时时间单位为秒在实际项目中我发现timeout设置很关键。太短可能导致数据读取不完整太长又会影响程序响应速度。根据我的经验1秒是个比较折中的值。3. 三种数据打包发送方式对比3.1 struct.pack方法struct.pack是最基础的数据打包方式适合固定格式的小数据包。下面是一个典型的使用示例import struct def send_data(x, y): # 帧头0x2C 0x12数据x,y校验和 checksum (0x2C 0x12 x y) 0xFF data struct.pack(BBhhB, 0x2C, 0x12, x, y, checksum) ser.write(data)这里的格式字符串BBhhB需要特别说明表示小端字节序第一个B表示0x2C占1字节第二个B表示0x12占1字节第一个h表示x占2字节short类型第二个h表示y占2字节最后一个B表示校验和占1字节这种方法的优点是简单直观缺点是每次都要重新打包整个数据对于频繁发送的数据效率不高。3.2 struct.pack_into方法struct.pack_into更适合需要重复发送相似数据的场景它可以直接操作预分配的缓冲区import struct import array buf array.array(B, [0]*8) # 预分配8字节缓冲区 def send_data(x, y): struct.pack_into(BBhh, buf, 0, 0x2C, 0x12, x, y) # 计算校验和 checksum sum(buf[:-1]) 0xFF struct.pack_into(B, buf, 6, checksum) ser.write(buf)这种方法减少了内存分配和释放的开销在需要高频发送数据的场合如机器人控制性能更好。我在一个需要每秒发送50次控制指令的项目中使用这种方法将CPU占用率降低了约30%。3.3 bytearray方法对于简单的数据发送bytearray可能是最直观的选择def send_data(x, y): data bytearray([0x2C, 0x12]) data.extend(x.to_bytes(2, little)) data.extend(y.to_bytes(2, little)) checksum sum(data) 0xFF data.append(checksum) ser.write(data)这种方法的优点是代码可读性强便于调试。我在项目初期经常使用这种方式因为它能很方便地在调试时打印出原始字节数据print(发送数据:, list(data))三种方法的对比总结方法优点缺点适用场景struct.pack简单直接每次重新打包低频简单数据struct.pack_into内存效率高稍复杂高频固定格式数据bytearray灵活易调试手动处理类型转换变长或复杂数据结构4. STM32端数据接收与解析4.1 串口中断配置STM32端的配置同样重要。以下是一个典型的USART初始化代码以HAL库为例UART_HandleTypeDef huart1; void USART1_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huart1) ! HAL_OK) { Error_Handler(); } // 使能接收中断 HAL_UART_Receive_IT(huart1, rx_data, 1); }4.2 中断服务例程实现一个健壮的中断处理程序应该包含帧头检测、长度验证和校验和检查#define MAX_FRAME_LEN 10 uint8_t rx_buf[MAX_FRAME_LEN]; uint8_t rx_index 0; uint8_t frame_ready 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { static uint8_t state 0; // 状态机处理 switch(state) { case 0: // 等待第一个帧头 if(rx_data 0x2C) { rx_buf[0] rx_data; rx_index 1; state 1; } break; case 1: // 等待第二个帧头 if(rx_data 0x12) { rx_buf[1] rx_data; rx_index 2; state 2; } else { state 0; } break; case 2: // 接收数据部分 rx_buf[rx_index] rx_data; if(rx_index MAX_FRAME_LEN) { // 校验和验证 uint8_t checksum 0; for(int i0; iMAX_FRAME_LEN-1; i) { checksum rx_buf[i]; } checksum 0xFF; if(checksum rx_buf[MAX_FRAME_LEN-1]) { frame_ready 1; } state 0; } break; } // 重新使能中断 HAL_UART_Receive_IT(huart, rx_data, 1); }这种状态机的方式虽然代码量稍大但抗干扰能力很强。我在一个存在电磁干扰的工业环境中测试即使有10%的数据错误率系统仍能稳定工作。4.3 数据解析与处理当frame_ready标志置位后就可以在主循环中处理完整的数据帧了void Process_Frame(void) { if(frame_ready) { int16_t x (rx_buf[3] 8) | rx_buf[2]; int16_t y (rx_buf[5] 8) | rx_buf[4]; // 使用x,y数据进行控制 Motor_Control(x, y); frame_ready 0; } }这里需要注意字节序问题。如果Jetson Nano端使用小端格式发送STM32端也需要按照小端格式解析。我在早期项目中就犯过这个错误导致解析出来的数据完全不对。5. 通信稳定性优化技巧5.1 超时重发机制在实际应用中我通常会实现一个简单的超时重发机制。Jetson Nano端发送数据后等待STM32的确认帧如果超时未收到则重发def reliable_send(x, y, max_retry3): for attempt in range(max_retry): send_data(x, y) start_time time.time() while time.time() - start_time 0.1: # 100ms超时 if ser.in_waiting: ack ser.read() if ack b\x55: # 确认帧 return True print(f重试 {attempt 1}/{max_retry}) return False对应的STM32端在成功接收数据后需要发送确认if(frame_ready) { uint8_t ack 0x55; HAL_UART_Transmit(huart1, ack, 1, 100); // ...处理数据... }5.2 数据校验策略除了简单的校验和外在要求更高的场合可以使用CRC校验。下面是一个CRC8的实现示例def crc8(data): crc 0 for byte in data: crc ^ byte for _ in range(8): if crc 0x80: crc (crc 1) ^ 0x07 else: crc 1 crc 0xFF return crc使用时只需要在发送前计算CRC并附加到数据末尾data struct.pack(BBhh, 0x2C, 0x12, x, y) crc crc8(data) ser.write(data bytes([crc]))STM32端也需要实现相同的CRC算法进行验证。这种校验方式能检测出更多类型的数据错误我在传输关键控制指令时都会使用。5.3 流量控制当数据传输量较大时需要考虑流量控制。硬件流控RTS/CTS是最可靠的方式但需要额外的硬件连线。软件流控XON/XOFF也是一种选择ser serial.Serial( port/dev/ttyTHS1, baudrate115200, rtsctsTrue, # 启用硬件流控 dsrdtrTrue )如果没有流控引脚可以实现简单的基于确认的流量控制协议。例如STM32在准备好接收新数据时发送一个就绪信号。6. 常见问题排查指南6.1 数据接收不完整这是最常见的问题之一可能的原因包括波特率不匹配两边设备必须使用相同的波特率缓冲区溢出STM32端处理速度跟不上接收速度中断优先级问题确保串口中断有足够高的优先级我常用的排查步骤是用逻辑分析仪或示波器观察实际传输的信号在STM32端打印接收到的原始数据检查两边的数据打包/解包代码6.2 数据解析错误当数据能收到但解析不正确时通常是因为字节序问题确保两边使用相同的字节序表示小端表示大端数据类型不匹配例如Python端发送int16STM32端却按uint8解析对齐问题某些平台对数据对齐有特殊要求一个有用的调试技巧是在两边都打印出数据的十六进制表示print(发送:, data.hex())printf(接收:); for(int i0; ilen; i) { printf(%02X , rx_buf[i]); } printf(\n);6.3 通信不稳定在电磁环境复杂的场合可以尝试以下改进降低波特率115200降到57600或更低缩短连线长度最好不超过1米使用双绞线减少干扰添加终端电阻特别是长距离传输时在我的一个工业项目中仅仅将波特率从115200降到57600通信成功率就从90%提升到了99.9%。7. 实际项目应用示例7.1 智能小车控制系统在一个基于Jetson Nano和STM32的智能小车项目中我使用了这样的通信协议| 帧头(2B) | 命令(1B) | 左速度(2B) | 右速度(2B) | 校验(1B) |Python端控制代码def set_motor_speeds(left, right): buf bytearray(8) struct.pack_into(2BBhhB, buf, 0, 0xAA, 0x55, # 帧头 0x01, # 命令字 left, right, # 速度值 0) # 校验位占位 # 计算校验和 buf[-1] sum(buf[:-1]) 0xFF ser.write(buf)STM32端解析后直接控制电机if(frame_ready rx_buf[2] 0x01) { int16_t left (rx_buf[4] 8) | rx_buf[3]; int16_t right (rx_buf[6] 8) | rx_buf[5]; TIM1-CCR1 left; // 左电机PWM TIM1-CCR2 right; // 右电机PWM }7.2 机械臂控制方案另一个机械臂项目需要传输更多数据我采用了分帧传输的方式def send_arm_positions(angles): # angles是包含6个关节角度的列表 for i in range(0, 6, 2): # 每次发送2个角度 segment struct.pack(2B2hB, 0xAA, 0x55, i, # 分段索引 angles[i], angles[i1], 0) # 校验位占位 segment[-1] sum(segment[:-1]) 0xFF ser.write(segment) time.sleep(0.01) # 小延迟防止缓冲区溢出STM32端需要重组数据float joint_angles[6]; void Process_Arm_Segment(uint8_t *data) { uint8_t index data[2]; if(index 6) return; int16_t angle1 (data[4] 8) | data[3]; int16_t angle2 (data[6] 8) | data[5]; joint_angles[index] angle1 / 100.0f; // 转换为浮点 joint_angles[index1] angle2 / 100.0f; if(index 4) // 最后一个分段 { Update_Arm_Position(joint_angles); } }这种分帧方式虽然增加了复杂度但解决了单帧数据过长的问题在资源有限的嵌入式系统中很实用。

更多文章