这块属于随便扯扯,算是个开发备忘的说明吧。
最近有个课程作业,需要实现惯性导航,那么就先使用PySerial库实现加速度计的数值读取,后续再使用PyQt+多进程+matplotlib实现加速度、角速度曲线的实时绘制,这里主要是实现命令行中串口数据的读写、显示。
为了简单,用的Stm32 f103刷arduino的BL,使用Arduino进行开发
总体接线情况是MPU6050的IIC接PB6\PB7,关于Stm32的引脚映射如图。
顺便吐槽一下,Stm32使用ardunio开发使用IIC时默认只能用SCL1/SDA1,其他需要声明,谁让咱不会用keil呢
源代码如下,一般也都能找到,详细的解释可以移步这篇博文
#include
// 定义为全局变量,可直接在函数内部使用
float gForceX, gForceY, gForceZ;
float rotX, rotY, rotZ;
long gyroX, gyroY, gyroZ;
long accelX, accelY, accelZ;
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 2g⋆ACCEL÷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] |
---|---|---|---|---|---|
3B | 3C | 3D | 3E | 3F | 40 |
[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");
}
这个是一个简单的demo,在实际做项目的时候是使用PyQt做界面,Matplotlib动态绘图,使用多进程交互实现的,这个代码只是简化版,由于整个课程作业还是demo阶段,就放张图看一下显示的效果就好了。
首先要导入,要注意的就是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)