一.FlyMcu串口下载程序
将串口和stlink接到电脑,将boot0跳线帽向右移一个位置,按下复位,在程序魔法棒->OutPut选项卡里勾选create Hex File编译程序生成hex文件,在FlyMcu中选择串口和对应HEX文件开始编程,在写入成功后将boot0跳线帽复位。
两个问题:
1.BOOT引脚是干嘛的,为什么这样配置,BootLoader是干嘛的,串口下载原理是什么?
串口下载的原理就是将程序通过串口传输给STM32并存放在Flash区,但是串口下载本身就是一段程序,更新过程中就会被顶替也就无法完成程序更新,而存储在ROM区系统存储器中的BootLoader就是ST公司写好的一段自举程序,用于辅助完成串口下载和程序更新,而更改跳线帽所插位置就是启动BootLoader的方式之一
2.每次下载程序都要拔插跳线帽有没有解决方法?
可以在串口的RTS和DTR口上接一个控制电路来控制BOOT0的值,祥光电路搜索STM32一键下载电路,当具有一键下载电路时就不需要多次改变跳线帽了
在没有一键下载电路的情况下,开启BootLoader后可以将FlyMcu中的编程后执行勾选并将编程到Flash时写选项字节取消勾选,也可以直接运行程序,但是这样程序是一次性的,复位后停止运行
点击软件中的读FLASH按钮可以将芯片中的程序读取出来,所以以后自己做产品一定要开启读保护,点击清除芯片按钮会将芯片中的代码擦除,点击读器件信息就会将芯片的序列号等等信息读出
点击设定选项字节等的按钮会跳出一个窗口:
在这个界面中就可以设置读保护是否允许读出,注意如果选择阻止读出则在向芯片下载程序时就会失败,需要将其设置成允许读出,在取消读保护的同时会清空芯片程序,达成保护程序的目的
选项字节中的参数不会随程序的更新而更新,选项字节也可以用上位机进行直接修改,可以用于某些用户自主配置的参数。
二.I2C协议
1.I2C协议规则
1)简介
I2C总线是由Philips公司开发的一种通用数据总线
两根通信线:SCL(Serial Clock)同步、SDA(Serial Data)半双工
带数据应答,支持总线挂载多设备(一主多从、多主多从),一主多从指单片机作为主机主导I2C总线的运行,挂载在I2C总线上的所有设备都是从机,只有主机允许才可以控制I2C总线;多主多从是指多个主机,任何一个模块都可以作为主机,当冲突时对其进行仲裁,谁胜利谁成为主机
2)硬件电路
主机永远拥有SCL时钟线的控制权,在空闲时可以主动操控SDA线,也可以将SDA线的控制权交给从机;从机权力较小,永远只能被动的读取SCL线,不允许控制,从机不允许主动发起对SDA的控制,只有在主机发送从机读取的命令后或者从机应答的时候从机才能短暂的取得SDA的控制权
在线路中如果出现一边输出高电平一边输出低电平的情况就会发生电源短路,为了避免这种情况,I2C总线要求设置为开漏输出(只能输出低电平或者无输出)并接入一个上拉电阻(在不输出的情况下自然置为一个弱上拉的高电平),有效避免了电源短路的问题,并且只要有一个设备输出低电平总线输出就是低电平,而只有所有设备都不输出才输出高电平,I2C可以利用这个特征进行多主机模式下的时钟同步和总线仲裁
3)I2C时序基本单元
注:无论起始还是终止都是由主机产生的,从机不能产生
在起始位后,发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
应答机制的设计:
4)I2C完整时序
I2C总线上每个从机都确定一个唯一的设备地址,主机在起始条件之后会先发送一个字节确认相应的设备地址,相应的设备响应之后的读写操作,MPU6050地址为1101000
第一个时序:指定地址写
对于指定设备的指定地址下写入指定数据
该时序下每个红线间的意义为:起始位 | 发送确认从机地址的字节(高七位表示从机地址,低1位表示读写位,0表示之后主机进行写操作,1相反) | 从机应答位 | 第二个字节(根据不同的设备定义用途,MPU6050定义为寄存器地址) | 从机应答位 | 第三个字节,也就是想要写入的数据 |从机应答位 | 终止位
第二个时序:当前地址读
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
该时序下每个红线间的意义为:起始位 | 发送确认从机地址的字节,最后一位为1表示读取 | 从机应答位 | 第二个字节(调用接收一个字节的时序,接收数据) | 从机应答位 (如果想要停止一定要发送非应答) | 终止位
在该时序下确定寄存器地址的方法是:在从机中所有寄存器都被分配到一个线性区域中,并会有一个单独的指针变量默认指向0地址,每写入或读出一个字节后指针就会自增,当调用当前地址读的时序时就会读取当前指针指向位置
第三个时序:指定地址读
对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
该时序下从中间红线分割,前半部分为指定地址写在写之前的时序,后半部分为当前地址读的时序,因为在前半部分中指定了地址,所以可以读出指定地址的数据
进阶时序
如果想实现在指定位置开启按顺序写入多个字节的功能,则只需要将写入的字节部分多重复几次即可,但是记得每次写入后地址指针都会自增,也就是说第一次写入在0x19,第二次就在0x1A中,读的时序也是类似的改变效果
2.I2C外设
1)MPU6050
简介
MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角(也称欧拉角,以飞机为例,其相对于平衡状态的三轴角度就叫欧拉角,因为任何一种传感器都无法获得准确欧拉角所以需要多传感器配合),常应用于平衡车、飞行器等需要检测自身姿态的场景
3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度,加速度计具有静态稳定性,没有动态稳定性
3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度,陀螺仪具有动态稳定性,没有静态稳定性
两个传感器的特性正好相反所以取长补短,进行互补滤波,就能得到动态静态都稳定的姿态角了
在6轴基础上再集成一个三轴的磁场传感器(测量xyz轴的磁场强度)则成为9轴姿态传感器;再继承一个气压传感器(一般用于测量高度信息所以只有一轴),那就成为10轴传感器
几个重要参数
在这个芯片中传感器测量出的数据是电压数据也就是模拟量,所以芯片内置了ADC转换器,而16位ADC采集传感器的模拟信号,量化范围:-32768~32767(2^16)
加速度计满量程选择:±2、±4、±8、±16(g),根据量化范围对应具体值量程,AD值与加速度是线性的
陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)
可配置的数字低通滤波器,低通滤波可以使数据变得更加平缓
可配置的时钟源和可配置的采样分频
I2C从机地址:1101000(AD0=0)、1101001(AD0=1),从机地址有两种表示方式,第一种就是直接将7位2进制转换为16进制,我们以1101000这个地址为例转换后即为0x68,使用这种方式在时序中发送第一个字节时需要将其向左移一位然后再或上读写位;第二种方式是直接将原地址向左移一位后的地址,即0xD0作为地址,这样在使用的时候就可以直接或上读写位作为字节发送。
硬件电路
在仅有6轴的情况下无法保证一定是平衡的,欧拉角不够稳定,所以需要接入磁力计或者气压计来增加准确度,而XCL、XDA这两个引脚就是用来外接磁力计或者气压计使用的,在接入磁力计或者气压计后MPU6050的主机接口可以直接访问这些扩展芯片的数据,将数据读取进MPU6050中,在芯片中有DMP单元进行数据融合和姿态解算,但是如果不需要姿态解算功能的话其实也可以直接接在SCL和SDA这条总线上
三.软件I2C读写MPU6050代码设计
程序设计思路
1.首先创建I2C的两个文件,在I2C.c中配置GPIO口并将六个时序基本单元写好(起始,终止,发送字节,接收字节,发送应答,接收应答),
2.再建立MPU6050的两个模块,基于I2C.c实现指定地址读、指定地址写、写寄存器对芯片进行配置和读寄存器得到传感器数据
3.最后在主函数中调用MPU的模块,初始化、拿到数据、显示数据
详细程序
MyI2C.c中的配置
#include "stm32f10x.h" // Device header
#include "Delay.h"
//因为后续程序中要多次改变引脚电平,因此在此处使用宏定义改变为名称,改换引脚更方便
//#define SCL_Port GPIOB
//#define SCL_Pin GPIO_Pin_10
//#define SDA_Pin GPIO_Pin_11
//此处封装函数来写入SCL电平方便后续调用
void MyI2C_W_SCL(uint8_t BitValue){
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
Delay_us(10);//对于主频较高的单片机需要在每次配置后加个延时
}
//此处封装函数来写入SDA电平方便后续调用
void MyI2C_W_SDA(uint8_t BitValue){
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
Delay_us(10);
}
//此处封装函数来读取SDA电平方便后续调用
uint8_t MyI2C_R_SDA(void){
uint8_t BitValue;
BitValue=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
//此处封装函数来读取SCL电平方便后续调用
uint8_t MyI2C_R_SCL(void){
uint8_t BitValue;
BitValue=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10);
Delay_us(10);
return BitValue;
}
void MyI2C_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_N;
GPIO_N.GPIO_Mode=GPIO_Mode_Out_OD;
GPIO_N.GPIO_Pin=GPIO_Pin_10 | GPIO_Pin_11;
GPIO_N.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_N);
GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
}
//先拉高两个线,再按照时序拉低,注意要先拉高SDA因为初始状态未知,可能会达成终止位的条件
void MyI2C_Start(void){
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
//先拉低两个线,再按照时序拉高,注意要先拉低SDA因为初始状态未知,可能会达成起始位的条件
//因为所有单元都以SCL低电平为结束所以不需要拉低SCL
void MyI2C_Stop(void){
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
//发送一个字节
void MyI2C_SendByte(uint8_t Byte){
uint8_t i;
for(i=0;i<8;i++){
MyI2C_W_SDA(Byte & (0x80>>i));//与(0x80>>i)相与从最高位开始发送
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
//接收一个字节
uint8_t MyI2C_ReceiveByte(void){
uint8_t i,Byte=0x00;
MyI2C_W_SDA(1);//主机放手回到自动高电平,后续由从机决定高低电平
//数据从低位开始进每次都向左移一位再进新数据,移动8次接收完毕
for(i=0;i<8;i++){
MyI2C_W_SCL(1);
Byte=(Byte<<1)+ MyI2C_R_SDA();
MyI2C_W_SCL(0);
}
return Byte;
}
//发送应答
void MyI2C_SendAck(uint8_t AckBit){
MyI2C_W_SDA(AckBit);//主机将要发送的应答位置于SDA
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
//接收应答
uint8_t MyI2C_ReceiveAck(void){
uint8_t AckBit;
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit=MyI2C_R_SDA();//主机读取应答位
MyI2C_W_SCL(0);
return AckBit;
}
MPU6050.c中的配置:
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"
#define MPU6050_Address 0xD0 //宏定义MPU地址
//封装按地址写时序
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data){
MyI2C_Start();
MyI2C_SendByte(MPU6050_Address);
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();
MyI2C_SendByte(Data);
MyI2C_ReceiveAck();
MyI2C_Stop();
}
//封装按地址读时序
uint8_t MPU6050_ReadReg(uint8_t RegAddress){
uint8_t Data;
MyI2C_Start();
MyI2C_SendByte(MPU6050_Address);
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();
MyI2C_Start();
MyI2C_SendByte(MPU6050_Address | 0x01);
MyI2C_ReceiveAck();
Data=MyI2C_ReceiveByte();
MyI2C_SendAck(1);//停止接收
MyI2C_Stop();
return Data;
}
void MPU6050_Init(void){
MyI2C_Init();
//初始化配置:
//配置电源管理寄存器1
//此处配置为:不复位,接触睡眠,不需要循环,温度传感器不失能,使用陀螺仪x轴时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);
//配置电源管理寄存器1
//此处配置为:不需要循环模式唤醒频率,所有轴不需要待机
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
//配置采样率分频
//此处配置为:决定数据输出快慢,数越小越快,此处为10分频
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
//配置配置寄存器
//此处配置为:不需要外部同步,低通滤波器最平滑滤波
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
//配置陀螺仪配置寄存器
//此处配置为:不自测,满量程选择最大量程
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);
//配置加速度计配置寄存器
//此处配置为:不自测,满量程选择最大量程,不使用高通滤波器
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
}
//获取数据寄存器的函数,此处使用指针地址传递,还可以使用结构体传递的方法
//指针法的原理是在主函数中定义变量将地址传入函数,在函数中直接对地址进行改写
void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ )
{
uint8_t DataH,DataL;//定义变量分别存放高八位和第八位的值
DataH=MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//读取加速度计x轴高八位数据
DataL=MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取加速度计x轴低八位数据
*AccX=(DataH<<8|DataL);
DataH=MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL=MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY=(DataH<<8|DataL);
DataH=MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL=MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ=(DataH<<8|DataL);
DataH=MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);//读取陀螺仪x轴高八位数据
DataL=MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);//读取陀螺仪x轴低八位数据
*GyroX=(DataH<<8|DataL);
DataH=MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL=MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY=(DataH<<8|DataL);
DataH=MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL=MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ=(DataH<<8|DataL);
}
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
宏定义对应寄存器地址的头文件MPU6050_Reg.h:
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H
#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75
#endif
主函数部分:
#include "MPU6050.h"
uint8_t ID;
int16_t AX,AY,AZ,GX,GY,GZ;
int main(void)
{
OLED_Init();
MPU6050_Init();
OLED_ShowString(4,1,"ID:");
ID=MPU6050_GetID();
OLED_ShowHexNum(4,4,ID,2);
while(1)
{
MPU6050_GetData(&AX,&AY,&AZ,&GX,&GY,&GZ);
OLED_ShowSignedNum(1,1,AX,5);
OLED_ShowSignedNum(2,1,AY,5);
OLED_ShowSignedNum(3,1,AZ,5);
OLED_ShowSignedNum(1,8,GX,5);
OLED_ShowSignedNum(2,8,GY,5);
OLED_ShowSignedNum(3,8,GZ,5);
}
}
本章作业
尝试使用结构体的方式传递循环方式读取出来的参数