树莓派4B串口通信实战:从蓝牙占用解除到PC双向通信(附完整代码)
树莓派4B串口通信实战:从蓝牙占用解除到PC双向通信(附完整代码)
在嵌入式开发领域,串口通信就像工程师的"母语"——简单、直接、无处不在。作为一款性价比极高的微型计算机,树莓派4B的串口功能却被蓝牙模块"鸠占鹊巢",这让不少开发者第一次尝试串口通信时就碰了壁。本文将带你完整走通树莓派4B串口通信的全流程,从硬件配置到代码实现,解决那些官方文档没告诉你的实战细节。
1. 硬件准备与环境配置
1.1 解除蓝牙对串口的占用
树莓派4B的硬件设计有个"历史遗留问题"——默认将硬件串口分配给了蓝牙模块。这就像把会议室钥匙交给了错误的部门,我们需要先收回使用权:
# 首先备份原始配置 sudo cp /boot/config.txt /boot/config.txt.bak sudo cp /boot/cmdline.txt /boot/cmdline.txt.bak # 编辑config.txt文件 sudo nano /boot/config.txt在文件末尾添加这两行关键配置:
enable_uart=1 dtoverlay=disable-bt接着处理cmdline.txt文件,删除所有涉及console=serial0,115200的内容。完成后重启系统:
sudo reboot注意:修改前建议先
ls -l /dev/serial*查看当前串口映射,修改后该命令显示的设备名通常会从/dev/ttyAMA0变为/dev/ttyS0
1.2 硬件接线方案
正确的接线是通信的基础,树莓派4B的GPIO引脚排列如下表所示:
| 树莓派引脚 | 功能 | USB-TTL模块连接 |
|---|---|---|
| GPIO14 (TX) | 发送 | RX (接收) |
| GPIO15 (RX) | 接收 | TX (发送) |
| GND | 地线 | GND |
常见错误排查:
- 数据反接:TX对TX会导致通信完全失败
- 波特率不匹配:双方必须设置相同波特率
- 电源干扰:避免使用同一电源给树莓派和转换模块供电
2. 内核级串口配置详解
2.1 用户权限管理
默认情况下,普通用户无权访问串口设备,需要将用户加入dialout组:
sudo usermod -a -G dialout $USER验证权限是否生效:
ls -l /dev/ttyAMA0 # 正确输出应包含'dialout'组权限 crw-rw---- 1 root dialout 204, 64 May 15 10:30 /dev/ttyAMA02.2 波特率与缓冲区设置
树莓派4B的UART控制器支持多种波特率配置,通过stty命令可以动态调整:
stty -F /dev/ttyAMA0 115200查看当前串口参数:
stty -F /dev/ttyAMA0 -a关键参数说明:
cs8:8位数据位-parenb:无奇偶校验-cstopb:1位停止位-crtscts:禁用硬件流控
3. C语言通信实战代码
3.1 基础通信框架
使用wiringPi库实现最简单的回显功能:
#include <wiringPi.h> #include <wiringSerial.h> #include <stdio.h> int main() { int fd; if(wiringPiSetup() == -1) { fprintf(stderr, "GPIO初始化失败\n"); return 1; } if((fd = serialOpen("/dev/ttyAMA0", 115200)) < 0) { fprintf(stderr, "无法打开串口设备\n"); return 1; } printf("串口通信已启动,输入q退出...\n"); while(1) { if(serialDataAvail(fd)) { char c = serialGetchar(fd); printf("收到: %c (0x%02x)\n", c, c); serialPutchar(fd, c); // 回传接收到的字符 if(c == 'q') break; } } serialClose(fd); return 0; }编译命令:
gcc -o serial_test serial_test.c -lwiringPi3.2 数据帧协议实现
实际项目中通常需要自定义通信协议,以下示例实现简单的帧结构:
| 字节位置 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头 |
| 1 | 数据长度N | 有效数据长度 |
| 2~N+1 | 有效数据 | 实际传输内容 |
| N+2 | 校验和 | 前面所有字节的异或 |
对应代码实现:
#define FRAME_HEADER 0xAA #define MAX_DATA_LEN 128 typedef struct { unsigned char header; unsigned char length; unsigned char data[MAX_DATA_LEN]; unsigned char checksum; } SerialFrame; int receiveFrame(int fd, SerialFrame *frame) { // 等待帧头 while(serialDataAvail(fd) && serialGetchar(fd) != FRAME_HEADER); if(!serialDataAvail(fd)) return -1; frame->header = FRAME_HEADER; delay(1); // 给后续数据到达留出时间 if(serialDataAvail(fd) < 2) return -1; frame->length = serialGetchar(fd); if(frame->length > MAX_DATA_LEN) return -1; unsigned char sum = frame->header ^ frame->length; for(int i=0; i<frame->length; i++) { frame->data[i] = serialGetchar(fd); sum ^= frame->data[i]; } frame->checksum = serialGetchar(fd); return (sum == frame->checksum) ? 0 : -1; }4. Windows端调试技巧
4.1 常用串口工具对比
| 工具名称 | 优点 | 缺点 |
|---|---|---|
| Tera Term | 轻量级,支持宏录制 | 界面较老旧 |
| Putty | 开源免费,支持多种协议 | 功能相对简单 |
| CoolTerm | 跨平台,数据可视化 | 高级功能需要付费 |
| Serial Port Utility | 专业级数据分析工具 | 资源占用较大 |
4.2 数据乱码解决方案
当出现乱码时,按以下步骤排查:
波特率验证:
# Python简易波特率测试脚本 import serial ser = serial.Serial('COM3', 115200, timeout=1) ser.write(b'AT\r\n') print(ser.readline())线路干扰处理:
- 缩短连接线长度(建议<30cm)
- 在TX/RX线上串联100Ω电阻
- 在GND与信号线间并联100pF电容
逻辑分析仪抓包:
# 使用sigrok-cli工具 sigrok-cli -d fx2lafw --channels D0,D1 -o capture.sr
5. 高级应用:多线程串口管理
对于需要同时处理收发数据的场景,建议采用多线程架构:
#include <pthread.h> pthread_mutex_t serial_mutex = PTHREAD_MUTEX_INITIALIZER; void* receiveThread(void* arg) { int fd = *(int*)arg; while(1) { pthread_mutex_lock(&serial_mutex); if(serialDataAvail(fd)) { // 处理接收数据 } pthread_mutex_unlock(&serial_mutex); usleep(10000); } return NULL; } void* sendThread(void* arg) { int fd = *(int*)arg; while(1) { // 构造发送数据 pthread_mutex_lock(&serial_mutex); // 发送操作 pthread_mutex_unlock(&serial_mutex); sleep(1); } return NULL; } int main() { // ...初始化代码... pthread_t tid_recv, tid_send; pthread_create(&tid_recv, NULL, receiveThread, &fd); pthread_create(&tid_send, NULL, sendThread, &fd); // ...主线程逻辑... }在实际项目中,我曾用这种架构实现了树莓派与多个传感器的稳定通信,关键点在于:
- 使用互斥锁保护串口资源
- 接收线程采用非阻塞方式轮询
- 设置合理的线程优先级
