在嵌入式开发和硬件通信中,串口通信是最基础也是最常用的通信方式之一。无论是单片机与PC通信,还是设备间的数据交换,串口都扮演着重要角色。本文将从零开始,带你快速掌握串口通信的核心原理、配置方法和实战应用,让你在3分钟内搭建起可运行的串口通信系统。
1. 串口通信基础概念
1.1 什么是串口通信
串口通信(Serial Communication)是指数据按位顺序依次传输的通信方式。与并行通信相比,串口通信只需要少数几条线就可以实现系统间的数据交换,具有布线简单、成本低、传输距离远等优点。
串口通信通常包含两种类型:同步串行通信和异步串行通信。在嵌入式领域最常用的是异步串行通信,也就是我们常说的UART(Universal Asynchronous Receiver/Transmitter)。
1.2 串口通信的核心参数
要成功建立串口通信,必须正确配置以下几个关键参数:
- 波特率(Baud Rate):数据传输速率,表示每秒传输的符号数,常见值有9600、115200等
- 数据位(Data Bits):每个数据包包含的数据位数,通常是7或8位
- 停止位(Stop Bits):表示数据包结束的位数,通常是1位或2位
- 校验位(Parity Bit):用于错误检测,可选无校验、奇校验或偶校验
1.3 串口通信的应用场景
串口通信广泛应用于各种场景:
- 单片机与PC机之间的数据传输
- 工业控制设备的通信
- 传感器数据采集
- 嵌入式系统调试和日志输出
- 物联网设备通信
2. 硬件连接与接口标准
2.1 RS-232标准
RS-232是最经典的串口通信标准,使用DB9或DB25接口。虽然现在很多设备已经不再使用传统的RS-232接口,但其通信原理仍然是理解串口通信的基础。
RS-232接口的主要引脚定义:
- TX(引脚3):数据发送端
- RX(引脚2):数据接收端
- GND(引脚5):信号地线
2.2 TTL电平串口
在现代嵌入式系统中,更常用的是TTL电平的串口通信。TTL电平使用0V表示逻辑0,3.3V或5V表示逻辑1,可以直接与单片机GPIO引脚连接。
2.3 USB转串口模块
由于现代电脑大多不再配备原生串口,USB转串口模块成为连接PC与嵌入式设备的重要桥梁。常见的芯片有CH340、CP2102、FT232等。
3. 串口通信协议详解
3.1 数据帧结构
串口通信的数据以帧为单位进行传输,每帧数据包含以下部分:
起始位 | 数据位(7-8位) | 校验位(可选) | 停止位(1-2位)- 起始位:总是逻辑0,表示一帧数据的开始
- 数据位:实际要传输的数据,从低位到高位依次传输
- 校验位:用于简单的错误检测
- 停止位:总是逻辑1,表示一帧数据的结束
3.2 通信流程
串口通信的基本流程如下:
- 发送端将并行数据转换为串行数据
- 添加起始位、校验位和停止位组成数据帧
- 按照设定的波特率逐位发送
- 接收端检测起始位,开始接收数据
- 验证校验位(如果启用),检查数据完整性
- 将串行数据转换为并行数据供处理器使用
4. STM32串口通信实战
4.1 环境准备
- 开发板:STM32F103C8T6(蓝色药丸板)
- 开发环境:STM32CubeIDE
- 串口模块:USB转TTL模块(CH340芯片)
- 连接线:杜邦线若干
4.2 硬件连接
将USB转TTL模块与STM32开发板正确连接:
- TTL模块的TX引脚 → STM32的PA10引脚(USART1_RX)
- TTL模块的RX引脚 → STM32的PA9引脚(USART1_TX)
- TTL模块的GND引脚 → STM32的GND引脚
4.3 STM32CubeMX配置
使用STM32CubeMX进行外设配置:
打开STM32CubeMX,选择对应的STM32型号
在Pinout视图中使能USART1
配置USART1参数:
- Mode: Asynchronous
- Baud Rate: 115200
- Word Length: 8 Bits
- Parity: None
- Stop Bits: 1
- Oversampling: 16
生成代码工程
4.4 核心代码实现
在生成的工程中,添加串口通信代码:
// 文件路径:Core/Src/main.c #include "main.h" #include "string.h" UART_HandleTypeDef huart1; // 重定向printf到串口 int _write(int file, char *ptr, int len) { HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY); return len; } // 串口接收回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // 处理接收到的数据 uint8_t received_data; HAL_UART_Receive_IT(&huart1, &received_data, 1); printf("Received: %c\n", received_data); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); printf("STM32 UART Communication Demo Started!\r\n"); uint8_t receive_buffer; HAL_UART_Receive_IT(&huart1, &receive_buffer, 1); while (1) { // 每隔1秒发送测试数据 printf("Hello from STM32!\r\n"); HAL_Delay(1000); } }4.5 串口初始化配置
// 文件路径:Core/Src/usart.c static void MX_USART1_UART_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(); } }5. Python串口通信上位机开发
5.1 安装pyserial库
pip install pyserial5.2 Python串口通信代码
# 文件路径:serial_monitor.py import serial import time import threading class SerialMonitor: def __init__(self, port='COM3', baudrate=115200): self.ser = serial.Serial( port=port, baudrate=baudrate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1 ) self.running = True def read_serial(self): """读取串口数据的线程函数""" while self.running: if self.ser.in_waiting > 0: data = self.ser.readline().decode('utf-8').strip() if data: print(f"Received: {data}") time.sleep(0.01) def send_data(self, data): """发送数据到串口""" if isinstance(data, str): data = data.encode('utf-8') self.ser.write(data + b'\r\n') print(f"Sent: {data.decode('utf-8')}") def start_monitor(self): """启动串口监视器""" print("Serial Monitor Started!") print("Press Ctrl+C to stop") # 启动读取线程 read_thread = threading.Thread(target=self.read_serial) read_thread.daemon = True read_thread.start() try: while True: # 可以在这里添加发送逻辑 user_input = input("Enter message to send (or 'quit' to exit): ") if user_input.lower() == 'quit': break self.send_data(user_input) except KeyboardInterrupt: print("\nStopping monitor...") finally: self.running = False self.ser.close() if __name__ == "__main__": # 根据实际情况修改端口号 monitor = SerialMonitor(port='COM3', baudrate=115200) monitor.start_monitor()5.3 自动检测可用串口
import serial.tools.list_ports def list_serial_ports(): """列出所有可用的串口""" ports = serial.tools.list_ports.comports() available_ports = [] for port, desc, hwid in sorted(ports): print(f"{port}: {desc} [{hwid}]") available_ports.append(port) return available_ports # 使用示例 if __name__ == "__main__": ports = list_serial_ports() if ports: print(f"可用串口: {ports}") else: print("未找到可用串口")6. 常见问题与解决方案
6.1 通信失败排查步骤
当串口通信出现问题时,可以按照以下步骤排查:
检查硬件连接
- 确认TX、RX交叉连接(设备A的TX接设备B的RX)
- 检查GND是否共地
- 确认电源供电正常
检查参数配置
- 确认双方波特率一致
- 检查数据位、停止位、校验位设置
- 验证流控制设置
软件层面检查
- 确认串口未被其他程序占用
- 检查驱动程序是否正常安装
- 验证代码中的端口号是否正确
6.2 常见错误代码及解决
# 常见的串口错误处理 try: ser = serial.Serial('COM3', 115200, timeout=1) except serial.SerialException as e: print(f"串口打开失败: {e}") # 可能的原因:端口不存在、权限不足、端口被占用6.3 数据丢失或乱码问题
数据丢失的常见原因及解决方案:
- 波特率不匹配:重新校准波特率,确保发送接收方一致
- 缓冲区溢出:增加读取频率或增大缓冲区
- 电磁干扰:使用屏蔽线,缩短传输距离
- 接地问题:确保所有设备共地
7. 串口通信高级应用
7.1 自定义通信协议
在实际项目中,通常需要定义自己的通信协议来确保数据的可靠性:
# 简单的帧结构设计 class SerialProtocol: def __init__(self): self.START_BYTE = 0xAA self.END_BYTE = 0x55 def pack_data(self, command, data): """打包数据帧""" frame = bytearray() frame.append(self.START_BYTE) # 起始字节 frame.append(command) # 命令字 frame.append(len(data)) # 数据长度 frame.extend(data) # 数据内容 # 计算校验和 checksum = sum(frame) & 0xFF frame.append(checksum) # 校验和 frame.append(self.END_BYTE) # 结束字节 return bytes(frame) def unpack_data(self, frame): """解包数据帧""" if (frame[0] != self.START_BYTE or frame[-1] != self.END_BYTE): return None # 帧格式错误 received_checksum = frame[-2] calculated_checksum = sum(frame[:-2]) & 0xFF if received_checksum != calculated_checksum: return None # 校验和错误 return { 'command': frame[1], 'length': frame[2], 'data': frame[3:3+frame[2]] }7.2 多线程串口通信
对于需要同时处理多个任务的应用程序,使用多线程可以提高效率:
import threading import queue import time class ThreadedSerialCommunication: def __init__(self, port, baudrate=115200): self.serial_port = serial.Serial(port, baudrate, timeout=1) self.receive_queue = queue.Queue() self.send_queue = queue.Queue() self.running = True def receiver_thread(self): """接收线程""" while self.running: if self.serial_port.in_waiting > 0: try: data = self.serial_port.readline() if data: self.receive_queue.put(data) except serial.SerialException: break time.sleep(0.01) def sender_thread(self): """发送线程""" while self.running: try: if not self.send_queue.empty(): data = self.send_queue.get() self.serial_port.write(data) except serial.SerialException: break time.sleep(0.01) def start_communication(self): """启动通信线程""" receiver = threading.Thread(target=self.receiver_thread) sender = threading.Thread(target=self.sender_thread) receiver.daemon = True sender.daemon = True receiver.start() sender.start()7.3 数据可视化显示
将串口接收的数据实时显示在图形界面上:
import matplotlib.pyplot as plt import matplotlib.animation as animation from collections import deque class SerialPlotter: def __init__(self, max_points=100): self.max_points = max_points self.data_buffer = deque([0] * max_points, maxlen=max_points) self.fig, self.ax = plt.subplots() self.line, = self.ax.plot(range(max_points), list(self.data_buffer)) def update_plot(self, new_value): """更新绘图数据""" self.data_buffer.append(new_value) self.line.set_ydata(self.data_buffer) return self.line, def start_animation(self): """启动实时动画""" ani = animation.FuncAnimation( self.fig, self.update_plot, interval=50, blit=True ) plt.show()8. 不同平台的串口通信实现
8.1 C# 串口通信实现
using System; using System.IO.Ports; using System.Threading; public class SerialPortExample { private SerialPort serialPort; public SerialPortExample(string portName, int baudRate) { serialPort = new SerialPort(portName, baudRate); serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler); } public void OpenConnection() { try { serialPort.Open(); Console.WriteLine("串口连接成功"); } catch (Exception ex) { Console.WriteLine($"串口连接失败: {ex.Message}"); } } private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { SerialPort sp = (SerialPort)sender; string data = sp.ReadExisting(); Console.WriteLine($"接收到数据: {data}"); } public void SendData(string data) { if (serialPort.IsOpen) { serialPort.WriteLine(data); Console.WriteLine($"发送数据: {data}"); } } public void CloseConnection() { if (serialPort.IsOpen) { serialPort.Close(); } } } // 使用示例 class Program { static void Main() { SerialPortExample example = new SerialPortExample("COM3", 115200); example.OpenConnection(); example.SendData("Hello from C#"); Thread.Sleep(2000); example.CloseConnection(); } }8.2 LabVIEW与单片机串口通信
在LabVIEW中实现串口通信的基本步骤:
前面板设计:
- 添加字符串显示控件(接收数据显示)
- 添加字符串输入控件(发送数据输入)
- 添加数值控件(波特率设置)
- 添加布尔按钮(连接/断开控制)
程序框图设计:
- 使用VISA配置串口函数
- 使用VISA写入函数发送数据
- 使用VISA读取函数接收数据
- 使用While循环实现持续通信
- 添加错误处理机制
8.3 Kotlin Android串口通信
在Android应用中实现串口通信:
class SerialHelper(private val context: Context) { private var serialPort: SerialPort? = null private var inputStream: InputStream? = null private var outputStream: OutputStream? = null private var readThread: Thread? = null private var isReading = false fun openSerialPort(device: File, baudrate: Int): Boolean { return try { serialPort = SerialPort(device, baudrate, 0) inputStream = serialPort!!.inputStream outputStream = serialPort!!.outputStream startReading() true } catch (e: Exception) { Log.e("SerialHelper", "打开串口失败", e) false } } private fun startReading() { isReading = true readThread = Thread { val buffer = ByteArray(1024) while (isReading) { try { val size = inputStream!!.read(buffer) if (size > 0) { val data = String(buffer, 0, size) // 处理接收到的数据 Log.d("SerialHelper", "收到数据: $data") } } catch (e: Exception) { Log.e("SerialHelper", "读取数据异常", e) } } } readThread!!.start() } fun sendData(data: String) { try { outputStream!!.write(data.toByteArray()) outputStream!!.flush() } catch (e: Exception) { Log.e("SerialHelper", "发送数据失败", e) } } fun closeSerialPort() { isReading = false readThread?.interrupt() serialPort?.close() } }9. 性能优化与最佳实践
9.1 波特率选择策略
选择合适的波特率对通信质量至关重要:
- 低速应用(传感器数据):9600-19200 bps
- 中速应用(一般控制):38400-115200 bps
- 高速应用(图像传输):230400-921600 bps
选择原则:在满足数据传输需求的前提下,尽量选择较低的波特率以提高通信稳定性。
9.2 错误检测与纠正
提高通信可靠性的方法:
- 校验和验证:每帧数据添加校验和
- 超时重传:设置合理的超时时间
- 数据确认机制:接收方确认收到数据
- 数据重发机制:发送失败时自动重发
9.3 资源管理最佳实践
# 使用上下文管理器确保资源正确释放 class SafeSerial: def __init__(self, port, baudrate): self.port = port self.baudrate = baudrate self.ser = None def __enter__(self): self.ser = serial.Serial(self.port, self.baudrate, timeout=1) return self.ser def __exit__(self, exc_type, exc_val, exc_tb): if self.ser and self.ser.is_open: self.ser.close() # 使用示例 with SafeSerial('COM3', 115200) as ser: ser.write(b'Hello World') response = ser.readline() print(f"Response: {response}")串口通信作为嵌入式系统中最基础的通信方式,掌握其原理和实现方法对嵌入式开发者至关重要。通过本文的讲解和示例,你应该能够快速搭建起可用的串口通信系统,并根据实际需求进行功能扩展。在实际项目中,记得充分考虑通信的可靠性和错误处理,确保系统的稳定运行。