使用PySerial库实现简单的串口程序

前言

这块属于随便扯扯,算是个开发备忘的说明吧。
最近有个课程作业,需要实现惯性导航,那么就先使用PySerial库实现加速度计的数值读取,后续再使用PyQt+多进程+matplotlib实现加速度、角速度曲线的实时绘制,这里主要是实现命令行中串口数据的读写、显示。

下位机传输信息

为了简单,用的Stm32 f103刷arduino的BL,使用Arduino进行开发
总体接线情况是MPU6050的IIC接PB6\PB7,关于Stm32的引脚映射如图。stm32引脚映射
顺便吐槽一下,Stm32使用ardunio开发使用IIC时默认只能用SCL1/SDA1,其他需要声明,谁让咱不会用keil呢
源代码如下,一般也都能找到,详细的解释可以移步这篇博文

#include <Wire.h>
 
// 定义为全局变量,可直接在函数内部使用
float gForceX, gForceY, gForceZ;
float rotX, rotY, rotZ;
long gyroX, gyroY, gyroZ;
long accelX, accelY, accelZ; 

注意!!!!这个网上流传的程序存在Bug!!!!

经过博主一天的调试,发现一个问题,如果全局变量定义如上面代码所述,那么会出问题——你的测量值会超量程,并且X轴角速度的偏差会很大。先后从网上找程序测试、delay秒数调整、位运算测试啥的弄了一天,最后发现原因很无语。由于MPU6050使用两个寄存器存储一个数据,因此数据长度为2个字节,16位,如果使用long/int会使读取数据均为正数,超出量程,必须使用short型变量存储…就是这么简单的问题,解决方案就是修改定义:
short gyroX, gyroY, gyroZ;
short accelX, accelY, accelZ; 
void setup() {
  Serial.begin(9600);//设置波特率为9600
  while(!Serial.available()){
  continue;//等到串口可用进行下一步
  while(Serial.read()>= 0){}//要首先发送一个信息才能实现初始化
  //Serial.println("start communication");
}
  Wire.begin();
  setupMPU();
}
 
void loop() {
  recordAccelRegisters();
  recordGyroRegisters();
  printData();
  delay(500);
}

下面是初始化MPU,寄存器的详细信息可以参考MPU6050的技术手册,这里主要是通过0x6B重置/启动MPU6050,0x68的用途我也不晓得,技术手册说是重置数/模信号路径。
0x1B实现角速度范围的配置,0x1C实现加速度范围的配置,配置方式是向对应地址写入相关字节,0为默认情况,对应正负250度/秒和正负2倍重力加速度。
对于加速度和角速度,MPU6050采用16位有符号整数表示,因此可以通过 2 g ⋆ A C C E L ÷ 32768 2g\star ACCEL\div 32768 2gACCEL÷32768计算加速度,角速度同理.

void setupMPU(){
 // REGISTER 0x6B/REGISTER 107:Power Management 1
 Wire.beginTransmission(0b1101000); 
 // 打开Wire库的传输模式。MPU6050的IIC地址如果AD0是低电平那么是b1101000/0x68;如果是高电平,那么是b1101001/0x69,beginTransmission函数会写入一个IIC开始标志
 Wire.write(0x6B); //Accessing the register 6B/107 - Power Management (Sec. 4.30) 
 Wire.write(0b00000000); //Setting SLEEP register to 0, using the internal 8 Mhz oscillator
 Wire.endTransmission();
 //向0x6B写入一个字节实现MPU6050的Reset

 // REGISTER 0x1b/REGISTER 27:Gyroscope Configuration,寄存器0x1B实现角速度范围的配置
 Wire.beginTransmission(0b1101000); //I2C address of the MPU
 Wire.write(0x1B); //Accessing the register 1B - Gyroscope Configuration (Sec. 4.4) 
 Wire.write(0x00000000); //Setting the gyro to full scale +/- 250deg./s (转化为rpm:250/360 * 60 = 41.67rpm) 最高可以转化为2000deg./s 
 Wire.endTransmission();
 
 // REGISTER 0x1C/REGISTER 28:ACCELEROMETER CONFIGURATION
 Wire.beginTransmission(0b1101000); //I2C address of the MPU
 Wire.write(0x1C); //Accessing the register 1C - Acccelerometer Configuration (Sec. 4.5) 
 Wire.write(0b00000000); //Setting the accel to +/- 2g(if choose +/- 16g,the value would be 0b00011000)
 Wire.endTransmission(); 
}

这部分代码实现加速度的读取和处理,加速度的读取通过读取寄存器3B-40的值获得,每个分量对应两个寄存器,关系如表

ACCEL_X[15:8]ACCEL_X[7:0]ACCEL_Y[15:8]ACCEL_Y[7:0]ACCEL_Z[15:8]ACCEL_Z[7:0]
3B3C3D3E3F40

[15:8]为对应的位数,使用位运算将加速度原始数据读出后,按照上一步的公式可以计算出加速度为几G。

void recordAccelRegisters() {
  // REGISTER 0x3B~0x40/REGISTER 59~64
  Wire.beginTransmission(0b1101000); //I2C address of the MPU
  Wire.write(0x3B); //Starting register for Accel Readings
  Wire.endTransmission();
  Wire.requestFrom(0b1101000,6); //Request Accel Registers (3B - 40)
 
  // 使用了左移<<和位运算|。Wire.read()一次读取1bytes,并在下一次调用时自动读取下一个地址的数据
  while(Wire.available() < 6);  // Waiting for all the 6 bytes data to be sent from the slave machine (必须等待所有数据存储到缓冲区后才能读取) 
  accelX = Wire.read()<<8|Wire.read(); //Store first two bytes into accelX (自动存储为定义的long型值)
  accelY = Wire.read()<<8|Wire.read(); //Store middle two bytes into accelY
  accelZ = Wire.read()<<8|Wire.read(); //Store last two bytes into accelZ
  processAccelData();
}
 
void processAccelData(){
  gForceX = accelX / 16384.0;     //float = long / float
  gForceY = accelY / 16384.0; 
  gForceZ = accelZ / 16384.0;
}

这一部分是角速度的读取,原理同加速度

void recordGyroRegisters() {
 // REGISTER 0x43~0x48/REGISTER 67~72
 Wire.beginTransmission(0b1101000); //I2C address of the MPU
 Wire.write(0x43); //Starting register for Gyro Readings
 Wire.endTransmission();
 Wire.requestFrom(0b1101000,6); //Request Gyro Registers (43 ~ 48)
 while(Wire.available() < 6);
 gyroX = Wire.read()<<8|Wire.read(); //Store first two bytes into accelX
 gyroY = Wire.read()<<8|Wire.read(); //Store middle two bytes into accelY
 gyroZ = Wire.read()<<8|Wire.read(); //Store last two bytes into accelZ
 processGyroData();
}

void processGyroData() {
 rotX = gyroX / 131.0;
 rotY = gyroY / 131.0; 
 rotZ = gyroZ / 131.0;
}

关于串口的输出,这里偷个懒,一次直接输出一行,在PySerial那边用readline()读出并转换为列表,丢弃最后一个换行符并转换格式就是我们需要的参数。

void printData() {
 //Serial.print("gryo");
 Serial.print(rotX);//Gyro x
 Serial.print(",");
 Serial.print(rotY);//gryo y
 Serial.print(",");
 Serial.print(rotZ);//gryo z
 Serial.print(",");
 //Serial.print("accel");
 Serial.print(gForceX);//accel x
 Serial.print(",");
 Serial.print(gForceY);
 Serial.print(",");
 Serial.print(gForceZ);
 Serial.print(",\n");
}

PySerial库读取

不知道说啥

这个是一个简单的demo,在实际做项目的时候是使用PyQt做界面,Matplotlib动态绘图,使用多进程交互实现的,这个代码只是简化版,由于整个课程作业还是demo阶段,就放张图看一下显示的效果就好了。
半成品

PySerial的使用

首先要导入,要注意的就是serial.tools.list_ports需要单独导入,否则会报错(我也不知道为什么)。

import serial
import serial.tools.list_ports

先实现一个简单的串口选择函数PortChoose(),使用serial.tools.list_ports.comports()函数获取可用串口并且将其转换为列表port_list,列表里每个元素都是一个Port对象(好像?)。如果在Windows下,这个对象的device属性是形如‘COM4’的字符串,所以我们的输入就是COMX再与其对比即可。

def PortChoose():
    port_list = list(serial.tools.list_ports.comports())#获取可用串口
    print(port_list)
    if len(port_list) == 0:
        print('无可用串口')
        return None
    else:
        for i in range(0,len(port_list)):
            print(port_list[i])
        port = input("请输入所需串口")
        for i in range(0,len(port_list)):
            if port == port_list[i].device:
                return port
        return None

然后是串口的写入,这里因为是arduino,只支持ASCII码,所以直接编码为ASCII,serial.write()函数会返回成功写入的字节数。

def PortWrite(ser,text):
    result = ser.write(text.encode("ascii"))  # 写数据,返回写入字节数
    return result

DReadPort函数实现数据的读取,这个函数对我们的程序没啥意义,准确的说是一开始不知道怎么考虑数据格式的时候copy的。关于0x55和0xaa,我也不知道有什么特殊意义(爬。

def DReadPort(ser):
    # 循环接收数据,此为死循环,可用线程实现
    readstr = ""
    if ser.in_waiting:
        readbuf = ser.read(ser.in_waiting)
        if readbuf[0] == 0x55 and readbuf[1] == 0xaa:
            readstr = readbuf
        else:
            readstr = readstr + readbuf
    return readstr

最后一个就是实现串口连接和读写了,因为stm32设置的波特率为9600,所以就直接设定为9600。serial.Serial()类会打开串口、创建串口对象,里面的timeout属性就是从串口读取数据时,如果一直没有,那么等待timeout秒后就结束等待,可以实现非阻塞的读取数据。
关于Serial类的read()和readline()方法,前者需要指定字节数,而后者在读到/n时就结束此次读取,为了省事,就直接readline了。其实还可以用读取ser.in_waiting的字节实现完全读取数据,但是这种方法可能会一直读下去,也可能乱序,所以放弃。

def PortConnect(n):
    #n为数据一行的个数
    port = PortChoose()#获取单片机端口
    bps = 9600 #设定波特率
    dltime = 5 #延时设置,None:永远等待操作,0为立即返回请求结果,其他值为等待超时时间(单位为秒)
    try:
        ser=serial.Serial(port,bps,timeout=dltime)#打开串口,创建串口对象
        print("串口详情参数:", ser)
        print(ser.port,ser.baudrate)#输出串口号和波特率
        PortWrite(ser,'initial')
        #print(ser.read())#读一个字节
        #print(ser.read(10).decode("gbk"))#读十个字节
        #print(ser.readline().decode("gbk"))#读一行
        #print(ser.readlines())#读取多行,返回列表,必须匹配超时(timeout)使用
        #print(ser.in_waiting)#获取输入缓冲区的剩余字节数
        #print(ser.out_waiting)#获取输出缓冲区的字节数
        while True:
            #if ser.in_waiting:
            #    temp=ser.read(ser.in_waiting ).decode("ascii")#将缓冲区的数据全部读出,这种操作一般会有一些问题
            #data = ser.readlines(6)#读取bytes weishu
            #s = ser.read(10)#从端口读10个字节
            temp = ser.readline().decode('ascii') #是读一行,以/n结束,要是没有/n就一直读,阻塞
            #for i in range(0,5):
            #    print(data[i].decode('ascii'))
            data = temp.split(",")
            for i in range(0,n):
                data[i]= float(data[i])
            del(data[n])
            print(data)
        print("---------------")
        ser.close()#关闭串口
    except Exception as e:
        print("---异常---:",e)

©️2020 CSDN 皮肤主题: 1024 设计师:上身试试 返回首页