树莓派ZeroW串口通讯
电脑系统:WIN10
树莓派型号:Zero W
树莓派系统:Raspbian,2018-11-13-raspbian-stretch-lite.img
Python 2.7.3
软件&工具:sscom5.13.1(串口助手),串口通讯线,串口驱动(电脑用)
硬件配置
使用树莓派的GPIO15、GPIO16作为串口的TxD和RxD,另外还有附近的5V、GND。准备好串口通讯线,电脑上装好驱动。
树莓派CPU的内部有两个串口,一个是硬件串口,一个是迷你串口(mini-uart)。硬件串口的速率是稳定的,而迷你串口没有时钟源,由内核提供时钟参考源,由于内核本身的频率是变化的,导致迷你串口的速率不稳定。
系统默认把硬件串口分配给了蓝牙模块,迷你串口分配给了GPIO(GPIO15、GPIO16),这将影响串口通讯的可靠性,所以在安装树莓派系统的时候,交换了两个串口的分配:
pi@raspberrypi:~ $ ls -l /dev #查看设备信息
ttyAMA0就是GPIO上的那个串口,系统默认它是作为console使用的,不过在安装树莓派系统的时候已经把这个配置给删掉了,现在可以作为普通的串口使用。
数据收发
先实现基本的数据收发功能,主要是为了测试接收和发送的通路是否正常。
新建一个测试文件 test_serial.py:
pi@raspberrypi:~ $ sudo nano test_serial.py
右键复制下面的内容,保存退出:
1 # -*- coding:utf-8 -*- 2 import serial 3 import time 4 5 def main(): 6 ser = serial.Serial("/dev/ttyAMA0", 19200) 7 ser.flushInput() 8 ser.flushOutput() 9 10 while True: 11 recv_num = ser.inWaiting() 12 13 if recv_num > 0: 14 recv_buffer = ser.read(recv_num) 15 ser.flushInput() 16 17 ser.flushOutput() 18 ser.write('received ' + str(recv_num) + ' bytes >> ') 19 ser.write(recv_buffer) 20 21 time.sleep(0.1) 22 23 if __name__ == '__main__': 24 main()
这段程序首先是打开串口0,波特率19200。然后在while循环中等待接收数据,接收到数据以后,把这个数据以及它的字节数发送出来。
每0.1s扫描一次,测试数据最好稍微短一点,否则就肯定被截断了。
运行 test_serial.py:
pi@raspberrypi:~ $ python test_serial.py
如果中途需要退出,比如去重新编辑文件,可以按Ctrl+Z
连接串口通讯线,打开串口助手(sscom),选择扫描到的COM口,设置波特率为19200,不勾选HEX显示、HEX发送,然后点击打开串口。
串口助手发送:hello_serial
串口助手接收:received 12 bytes >> hello_serial
串口助手接收到的就是树莓派串口发送的,说明硬件连接正常,串口发送接收功能正常。
串口通讯
本项目中的串口通讯包含两种模式(互斥,默认为数据模式):
- 管理模式下,树莓派接收配置参数,发送自身状态;
- 数据模式下,树莓派接收设备的广播数据
管理模式
设计了4个管理指令,均为12字节,不够的就用*补足,可以根据实际的需要随意定义,指令的长度也可以不一样:
ADMIN_QUERY* | 查询状态:内部控制参数,磁盘信息,数据文件数量等 |
ADMIN_CONFIG | 配置内部控制参数:树莓派的编号,通讯协议,WIFI用户名和密码等 |
ADMIN_CLEAR* | 清空磁盘中的所有数据库文件 |
ADMIN_EXIT** | 退出管理模式 |
上电后一段时间内(可以设置为1-5分钟),树莓派接收到前3条指令后,均进入管理模式,树莓派在这个模式下每隔1s发送一次自身的状态。树莓派收到退出指令或者一定时间限制后,自动退出管理模式。
广播包约0.5~2s发一帧,每帧数据约50~400字节,数据帧之间的空闲时间≥100ms。树莓派接收到广播数据帧后将其存在一个int数组里,其他程序后面会来处理这个数组。
新建一个测试文件 test_serial_comn.py:
pi@raspberrypi:~ $ sudo nano test_serial_comn.py
右键复制下面的内容,保存退出:
1 # -*- coding:utf-8 -*- 2 import serial 3 import time 4 import binascii 5 6 DB_COMN_LENGTH = 400 7 COMN_LENTH_MIN = 50 8 COMN_LENTH_MAX = 400 9 10 def DeviceConfigSet(str_config): 11 print('DeviceConfigSet() function run...') 12 13 def DeviceClear(): 14 print('DeviceClear() function run...') 15 16 def GetDeviceStatus(): 17 print('GetDeviceStatus() function run...') 18 return 'defined_device_status_string...' 19 20 def main(): 21 global DB_COMN_LENGTH 22 global COMN_LENTH_MIN 23 global COMN_LENTH_MAX 24 25 # initialize 26 print('>> initialize...') 27 #### 28 29 is_data_received = False 30 is_admin_mode_enabled = False 31 32 in_admin_count = 0 33 update_status_interval = 0 34 35 comn_data = [0 for i in range(DB_COMN_LENGTH)] 36 recv_num_last = 0 37 38 ser = serial.Serial("/dev/ttyAMA0",19200) 39 ser.flushInput() 40 ser.flushOutput() 41 42 print('>> wait for data or command...') 43 44 while True: 45 recv_num = ser.inWaiting() 46 if recv_num > 0: 47 time.sleep(0.01) 48 recv_num_real = ser.inWaiting() 49 while recv_num_real > recv_num: 50 recv_num = recv_num_real 51 time.sleep(0.01) 52 recv_num_real = ser.inWaiting() 53 54 recv = ser.read(recv_num_real) 55 56 if in_admin_count < 3600 and 'ADMIN_CONFIG' == recv[0:12]: 57 print('>> command received...') 58 print(recv) 59 is_admin_mode_enabled = True 60 DeviceConfigSet(recv) 61 62 elif in_admin_count < 3600 and 'ADMIN_QUERY*' == recv[0:12]: 63 print('>> command received...') 64 print(recv) 65 is_admin_mode_enabled = True 66 67 elif in_admin_count < 3600 and 'ADMIN_CLEAR*' == recv[0:12]: 68 print('>> command received...') 69 print(recv) 70 is_admin_mode_enabled = True 71 DeviceClear() 72 73 elif True == is_admin_mode_enabled and 'ADMIN_EXIT**' == recv[0:12]: 74 print('>> command received...') 75 print(recv) 76 is_admin_mode_enabled = False 77 78 elif recv_num_real >= COMN_LENTH_MIN and recv_num_real <= COMN_LENTH_MAX: 79 is_data_received = True 80 for i in range(0, recv_num_real): 81 comn_data[i] = int(binascii.b2a_hex(recv[i]), 16) 82 for i in range(recv_num_real, recv_num_last): 83 comn_data[i] = 0 84 recv_num_last = recv_num_real 85 print('>> data received...') 86 print(comn_data) 87 88 else: 89 print('>> invalid data received!!!...') 90 print(recv) 91 92 ser.flushInput() 93 94 # save data records if needed 95 #### 96 97 if in_admin_count < 3600: 98 in_admin_count = in_admin_count + 1 99 else: 100 is_admin_mode_enabled = False 101 102 if True == is_admin_mode_enabled: 103 update_status_interval = update_status_interval + 1 104 if update_status_interval >= 20: 105 update_status_interval = 0 106 ser.flushOutput() 107 ser.write(GetDeviceStatus()) 108 109 # system tick 110 time.sleep(0.05) 111 112 if __name__ == '__main__': 113 main()
代码说明
>> 切割通讯帧
通过帧之间的空闲时间来区分两个帧。软件正常每隔50ms扫描一次串口,看是否收到新的数据,如果收到了新的数据,认为新的一帧开始了,软件会每隔10ms扫描一次串口,直到在上个10ms内没有接受到新的数据,认为这一帧结束(扫描间隔要考虑波特率,19200对应的1个字节持续时间约为0.625ms)。一帧接收完成后,读取出全部接收到的数据,进入下一步处理。
主要是用这段代码来实现的:
1 recv_num = ser.inWaiting() 2 if recv_num > 0: 3 time.sleep(0.01) 4 recv_num_real = ser.inWaiting() 5 while recv_num_real > recv_num: 6 recv_num = recv_num_real 7 time.sleep(0.01) 8 recv_num_real = ser.inWaiting() 9 10 recv = ser.read(recv_num_real)
>> 有效数据帧
为了考虑到采集系统的通用性,用数据帧的长度来进行过滤,给定一个有效数据帧的长度范围,超出的都认为是无效数据帧。
当然也可以把通讯协议做进去,可以配置参数里可以配置通讯协议类型,0默认是无协议,其他的自行定义。
>> 数据格式转换
串口接收到的数据 recv = ser.read(recv_num_real),read函数说明如下:
https://pythonhosted.org/pyserial/pyserial_api.html
依次打印recv[0]和recv:
1 print('>> recv[0]...') 2 print(recv[0]) 3 print('>> recv...') 4 print(recv) 5 print('>> data received...') 6 print(comn_data)
串口助手发送的第一个字节是99(10进制),recv[0]打印出来是c,这个是ascii格式的:
要把ascii格式的数据转换为int格式的数据:
1 import binascii 2 3 comn_data[i] = int(binascii.b2a_hex(recv[i]), 16)
通过binascii.b2a_hex()函数来接收串口数据recv[i],把这个字节转换为16进制的数据;
通过int(x [,base])把刚刚得到的16进制数据转换为int格式的数据。
运行 test_serial_comn.py,显示初始化完成,等待接收数据或指令:
pi@raspberrypi:~ $ python test_serial_comn.py
测试管理指令
树莓派收到[ADMIN_QUERY*]指令后,进入管理模式,开始每隔1s发送一次自身的状态;
树莓派收到[ADMIN_CLEAR*]指令后,进入管理模式,调用DeviceClear()函数清空磁盘中的数据文件,并开始每隔1s发送一次自身的状态;
树莓派收到[ADMIN_CONFIG]指令后,进入管理模式(之前已经在了),调用DeviceConfigSet()函数配置参数,并开始每隔1s发送一次自身的状态;
树莓派收到[ADMIN_EXIT]非法指令,提示收到非法指令;
测试广播数据
测试包长度为222字节,16进制显示:
63 78 12 10 05 32 01 00 00 00 00 00 00 10 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2E 0A 27 00 00 00 00 00 5A 00 26 00 5A 00 26 00 5A 00 26 00 00 00 00 11 F8 08 FC 0C D0 05 5A 08 5C 03 84 04 9C 01 F4 00 00 00 00 00 52 00 26 10 68 08 FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2A 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 28 00 00 00 00 00 00 00 00 00 00 03 E8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 11 F8 08 FC 11 F8 08 FC
连续测试10个包,发送间隔500ms,主要是看一下接收到的数据包是否完整(软件中写了数组长度为400字节,不够的就补零)。