🚀write in front🚀
🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
💬本系列哔哩哔哩江科大STM32的视频为主以及自己的总结梳理📚
🚀Projeet source code🚀
💾工程代码放在了本人的Gitee仓库:iPickCan (iPickCan) - Gitee.com
引用:
STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili
Keil5 MDK版 下载与安装教程(STM32单片机编程软件)_mdk528-CSDN博客
STM32之Keil5 MDK的安装与下载_keil5下载程序到单片机stm32-CSDN博客
0. 江协科技/江科大-STM32入门教程-各章节详细笔记-查阅传送门-STM32标准库开发_江协科技stm32笔记-CSDN博客
【STM32】江科大STM32学习笔记汇总(已完结)_stm32江科大笔记-CSDN博客
江科大STM32学习笔记(上)_stm32博客-CSDN博客
STM32学习笔记一(基于标准库学习)_电平输出推免-CSDN博客
术语:
|--------------------------------------|---------------------------------------------|
| 英文缩写 | 描述 |
| GPIO:General Purpose Input Onuput | 通用输入输出 |
| AFIO:Alternate Function Input Output | 复用输入输出 |
| AO:Analog Output | 模拟输出 |
| DO:Digital Output | 数字输出 |
| 内部时钟源 CK_INT:Clock Internal | 内部时钟源 |
| 外部时钟源 ETR:External Trigger | 时钟源 External 触发 |
| 外部时钟源 ETR:External Trigger mode 1 | 外部时钟源 External 触发 时钟模式1 |
| 外部时钟源 ETR:External Trigger mode 2 | 外部时钟源 External 触发 时钟模式2 |
| 外部时钟源 ITRx:Internal Trigger inputs | 外部时钟源,ITRx (Internal trigger inputs)内部触发输入 |
| 外部时钟源 TIx:exTernal Input pin | 外部时钟源 TIx (external input pin)外部输入引脚 |
| CCR:Capture/Comapre Register | 捕获/比较寄存器 |
| OC:Output Compare | 输出比较 |
| IC:Input Capture | 输入捕获 |
| TI1FP1:TI1 Filter Polarity 1 | Extern Input 1 Filter Polarity 1,外部输入1滤波极性1 |
| TI1FP2:TI1 Filter Polarity 2 | Extern Input 1 Filter Polarity 2,外部输入1滤波极性2 |
| DMA:Direct Memory Access | 直接存储器存取 |
正文:
0. 概述
从 2024/06/12 定下计划开始学习下江协科技STM32课程,接下来将会按照哔站上江协科技STM32的教学视频来学习入门STM32 开发,本文是视频教程 P2 STM32简介一讲的笔记。
1.🚚MP6050
本节我们来用软件I2C读写MPU6050
接线图:
由于我们这个代码使用的是软件I2C,就是用普通的GPIO口,手动翻转电平实现的协议。它并不需要STM32内部的外设资源支持。所以这里的端口其实可以任意指定,不局限于这两个端口,接在任意的两个普通的GPIO口就可以。
然后我们只需要在程序中配置并操作SCL和SDA对应的端口就行了。这算是软件I2C相比硬件I2C的一大优势,就是端口不受限,可以任意指定。
根据I2C协议的硬件规定,SCL和SDA都应该外挂一个上拉电阻,但是我们的接线这里并没有外挂上拉电阻。是因为上一节我们分析模块电路的时候提到过这个模块内部自带了上拉电阻,所以外部的上拉电阻就不需要接了。
目前这里STM32是主机,MPU6050是从机,是一主一从的模型,当然主机和从机的执行逻辑是完全不同的,我们程序中一般只关注主机端的程序。
这里由于模块内置了下拉电阻 , 所以 引脚 悬空的话就相当于接地。
2.🚚MyI2C.c
由于我们本代码要使用软件I2C,所以I2C的库函数我们就不用看了。软件I2C只需要用GPIO的读写函数就行了。
初始化函数
然后初始化函数中,我们要做两个任务。第一个任务把 SCL 和 SDA 都初始化为开漏输出模式 ( 开漏输出低电平+浮空输入也就是高阻态) 。第二个任务把 SCL 和 SDA 置高电平。
⚠️⚠️⚠️注意:开漏输出并不只能输出,开漏输出模式仍然可以输入。
⚠️⚠️⚠️输入时先输出1,再直接读取输入数据寄存器就行了。
然后接下来我们就来完成I2C的六个时序基本单元。
起始条件
第一个基本单元是起始条件,这里对应写一个函数。
起始条件:SCL高电平期间,SDA从高电平切换到低电平
我们首先把SCL和SDA都确保释放,然后先拉低SDA,再拉低SCL,这样就能产生起始条件了。
在这里我们可以不断的调用SetBits和RetsetBits手动翻转高低电平。但是这样做的话,会在后面的程序中出现非常多的地方来指定这个GPIO端口号。一方面这样做语义并不是很明显,另一方面,如果我们之后需要换一个端口,就需要改动非常多的地方。所以这时我们就需要在上面做个定义,把这个端口号统一替换一个名字,这样无论是语义,还是端口的修改,都会非常方便。给端口号换一个名字,有很多方法都能实现功能。在51单片机中,我们一般使用sbit来定义端口的名称,但是sbit并不是标准C语言的语法,STM32也不支持这样做。这里一种简单的替换方法就是宏定义define。
修改引脚的时候,直接在上面修改一下宏定义,这是一种简单可行的方法,在STM32程序中也是挺常见的一个操作。
进一步的,如果觉得每次都需要定义port和pin比较麻烦,还可以把这整个函数用宏定义进行替换,并且用宏定义替换的函数还可以有参数,叫有参宏。
以我们之前讲过的OLED的程序为例:
在宏定义后面加一个括号,里面写入形参,在实际引用的时候,传入实参。
这样实际上OLED_W_SCL(1)就等价于GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(1));
补充:BitAction是什么意思?
在STM32中,用于强制将特定的操作数转换为一个位值,将一个非零值转换为逻辑高电平(1),将零值转换为逻辑低电平(0)。
在GPI0操作中,可以使用"BitAction"宏定义来设置引脚的状态,例如通过调用GPI0 writeBit()函数来设置引脚的输出状态。
GPI0x表示GPIO端口,GPI0 Pin表示具体的引脚位,而BitAction表示要设置的引脚状态。
但是这种方法在移植到其他库或者其他种类单片机时,很多人都不知道怎么修改。另外还有这种宏定义的方法,如果换到一个主频很高的单片机中,需要对软件的时序进行延时操作的时候也不太方便进一步修改。
所以综合以上缺点 , 在这里 我们 ****就直接一点干脆再套个函数。****如果单片机主频比较快,也非常方便加一些延时,比如每次操作引脚之后,都要延时10us。
后面再调用这个W_SCL,参数给1或0就可以释放或拉低SCL了。
对于STM32F1系列,这里即使不加任何延时,这个引脚翻转速度,MPU6050也能跟得上。但是保险起见,还是延时个十微秒。
如果要把这个程序移植到别的单片机,就可以把这个函数里的操作替换为其他单片机对应的操作。比如SCL是51单片机的P10口,就可以把GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);这句替换为P10=BitValue。
操作SDA的函数
接下来封装一下操作SDA的函数:
读和写不是同一个寄存器,再定义一个函数
有了这三个函数的分装,我们就实现了函数名称、端口号的替换。同时也可以很方便的修改时序的延时。当我们需要替换端口,或者把这个程序移植到别的单片机中时,就只需要对这前四个函数里的操作对应更改。
我们回到这个函数,开始调用以上四个函数。
我们需要先把SCL和SDA都释放,也就是都输出1,然后先拉低SDA。再拉低SCL,这就是起始条件的执行逻辑。
📌📌注意:我们最好把释放SDA的放在前面。
如果起始条件之前,SCL和SDA已经是高电平了 ,先释放哪一个是一样的效果。
📌📌但是后面start还要兼容这里的重复起始条件sr。
如果sr最开始SCL是低电平,SDA电平不敢确定 ,所以保险起见,趁 SCL 是低 电平 时,先确保释放SDA再释放SCL,这时SDA和SCL都是高电平。然后再拉低SDA拉低SCL,这样start就可以兼容起始条件和重复起始条件了。
接下来继续终止条件
终止条件
终止条件:SCL高电平期间,SDA从低电平切换到高电平
果stop开始时SCL和SDA都已经是低电平了,就先释放SCL,再释放SDA就行了。但是在这个时序单元开始时,SDA并不一定是低电平。
所以为了确保之后释放SDA,能产生上升沿,我们要在时序单元开始时先拉低SDA,然后再释放SCL,释放SDA。
然后是发送一个字节
发送一个字节
发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
实际上除了终止条件 SCL 以高 电平 结束 , 所有的单元我们都会保证 SCL 以低 电平 结束 , 这样方便各个单元的拼接。
SCL低电平 变换数据 , 高电平保持数据稳定 , 由于是高位先行 , 所以变换数据的时候 , 按照先放最高位再放次高位 ****,****依次把一个字节的每一位放在SDA线上,每放完一位后执行释放SCL拉低SCL的操作,驱动时钟运转。
Byte & 0x80 就是保留字节的高位,对其他位清0,假设Byte是xxxx xxxxx
由于调用的这个函数中的参数最后会被强制转换成bitAction类型,所以非0即1,所以最终MyI2C_W_SDA(Byte & (0x80 >> i))也相当于传了一个1
接着继续写接收一个字节
接收一个字节
接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
主机需要先释放SDA,释放SDA也相当于切换为输入模式。
SCL低电平变换数据,高电平读取数据,实际上就是一种读写分离的设计,低电平时间定义为写的时间,高电平时间定义为读的时间。
SCL高电平时,SDA下降沿为起始条件,SDA上升沿为终止条件。这个设计也保证了起始和终止的特异性,能够让我们在连续不断的波形中快速的定位起始和终止。因为起始终止和数据传输的波形有本质区别。数据传输 时 SCL 高电平不许动 SDA, 起始终止 条件下是 SCL 高 电平 必须动 SDA
📌📌注意:I2C是在进行通信,通信是有从机的,当主机不断驱动SCL 时钟时 ,从机 就有义务去改变 SDA 的 电平 。所以主机每次循环读取 SDA 的时候 , 这个读取到的数据是 从机 控制的 , 这个数据也正是 从 机想要给我们发送的数据 。
发送应答
然后发送应答和接收应答只要复制发送一个字节和接收一个字节的函数修改一下就可以了。
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
📌📌注意:I2C的引脚 都是开漏输出 + 弱上拉的配置。主机输出 1 并不是强制 SDA 为高 电平, 而是释放 SDA;I2C 通信 时 , 主机释放了 SDA,从机 在此时把 SDA 再拉低的。所以这里即使之前主机把 SDA 置 1 再读取 SDA, 读到的值也可能是 0, 读到 0 代表 从机 给了应答 , 读到 1 代表 从机 没给应答。
测试应答功能
想要测试应答功能时主函数可以这样调用
这样就可以测试从机给不给应答的时序
MyI2C.c
cpp
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "Delay.h"
#define I2C_SCL_GPIO_PIN GPIO_Pin_0
#define I2C_SDA_GPIO_PIN GPIO_Pin_1
#define I2C_GPIO GPIOA
void MyI2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef gpioInitStructure;
gpioInitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
gpioInitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
//初始化为输出高电平
GPIO_SetBits(I2C_GPIO, I2C_SCL_GPIO_PIN);
GPIO_SetBits(I2C_GPIO, I2C_SCL_GPIO_PIN);
}
void MyI2C_W_SCL(uint8_t bitValue)
{
GPIO_WriteBit(I2C_GPIO, I2C_SCL_GPIO_PIN, (BitAction)bitValue);
Delay_us(10); //延时10us,防止翻转过快超过I2C速率
}
void MyI2C_W_SDA(uint8_t value)
{
GPIO_WriteBit(I2C_GPIO, I2C_SDA_GPIO_PIN, (BitAction)value);
Delay_us(10); //延时10us,防止翻转过快超过I2C速率
}
uint8_t MyI2C_R_SDA(void)
{
uint8_t bitValue;
bitValue = GPIO_ReadInputDataBit(I2C_GPIO, I2C_SDA_GPIO_PIN);
Delay_us(10); //延时10us,防止翻转过快超过I2C速率
return bitValue;
}
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); //先拉高SDA总线
MyI2C_W_SCL(1); //再拉高SCL总线
MyI2C_W_SDA(0); //SCL为高电平时,SDA从从高到低表示起始条件
MyI2C_W_SCL(0); //SCL拉低为低电平
}
void MyI2C_Stop(void)
{
MyI2C_W_SCL(0); //SCL拉低
MyI2C_W_SDA(0); //SDA拉低
MyI2C_W_SCL(1); //SCL总线拉为高电平
MyI2C_W_SDA(1); //SCL为高电平时,SDA从低到高表示结束条件
}
void MyI2C_Send_Ack(void)
{
MyI2C_W_SDA(0); //SDA拉低,发送ACK
MyI2C_W_SCL(1); //SCL拉高,通知从机读取数据
MyI2C_W_SCL(0); //SCL拉低
}
void MyI2C_Send_NAck(void)
{
MyI2C_W_SDA(1); //SDA拉低,发送NACK
MyI2C_W_SCL(1); //SCL拉高,通知从机读取数据
MyI2C_W_SCL(0); //SCL拉低
}
uint8_t MyI2C_Recv_Ack(void)
{
uint8_t bitValue;
MyI2C_W_SDA(1); //主机SDA释放总线
MyI2C_W_SCL(1); //SCL拉高
bitValue = MyI2C_R_SDA(); //读取SDA总线
MyI2C_W_SCL(0); //SCL拉低
return bitValue;
}
void MyI2C_Send_Byte(uint8_t data)
{
for(int i=0; i<8; i++)
{
MyI2C_W_SDA(data & (1<<(8-i-1)));
MyI2C_W_SCL(1); //SCL拉高,从机读取SDA总线
MyI2C_W_SCL(0); //SCL拉低
}
}
uint8_t MyI2C_Recv_Byte(void)
{
uint8_t data = 0;
//首先释放SDA总线,防止对从机造成干扰
MyI2C_W_SDA(1);
for(int i=0; i<8; i++)
{
MyI2C_W_SCL(1); //拉高SCL总线
if(MyI2C_R_SDA())
{
data |= (1 << (8-i-1)); //高位优先
}
MyI2C_W_SCL(0); //拉低SCL总线
}
return data;
}
MyI2C.h
cpp
#ifndef __MY_I2C_H__
#define __MY_I2C_H__
void MyI2C_Init(void);
void MyI2C_W_SCL(uint8_t bitValue);
void MyI2C_W_SDA(uint8_t value);
uint8_t MyI2C_R_SDA(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_Send_Ack(void);
void MyI2C_Send_NAck(void);
uint8_t MyI2C_Recv_Ack(void);
void MyI2C_Send_Byte(uint8_t data);
uint8_t MyI2C_Recv_Byte(void);
#endif
Main.c
cpp
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
#include "UART.h"
#include <stdio.h>
#include "Key.h"
#include "String.h"
#include "LED.h"
#include "MyI2C.h"
int main(int argc, char *argv[])
{
uint8_t AckBit = 0;
OLED_Init();
uint8_t MP6060_I2CAddr = 0xD2;
//Delay_s(2);
OLED_ShowString(1, 1, "I2C:");
//OLED_ShowHexNum(1, 5, MP6060_I2CAddr, 2); //1101 0000
OLED_ShowHexNum(1, 5, 0xD2, 2); //1101 0010
MyI2C_Init();
MyI2C_Start();
MyI2C_Send_Byte(MP6060_I2CAddr);
AckBit = MyI2C_Recv_Ack();
OLED_ShowHexNum(2, 1, AckBit,2);
while(1)
{
}
return 1;
}
1101 000是从机的地址,可以理解为是从机的名字,最低位的0是表示"写入操作"
这样运行后显示从机可以给我们应答
使用淘宝购买的19块钱的24MHz 8通道逻辑分析仪抓取下I2C通信寻址的过程,可以看到I2C寻址MP6050的 I2C Addr成功,收到的 ACK应答。
我们接下来讲一下通过AD0引脚改名的功能。
通过AD0引脚改名的功能
我们可以把一根飞线连接AD0引脚和VCC, 这时MPU6050的从机地址就是1101 001了。
这个时候运行就发现从机没有给我们应答了,因为它刚刚改名成1101 001了。
使用淘宝购买的19块钱的24MHz 8通道逻辑分析仪抓取下I2C通信寻址的过程,可以看到I2C寻址MP6050的 I2C Addr成功,收到的 NACK应答。因为它刚刚改名成1101 001了。
这个时候把飞线拔掉,再次运行发现它又可以应答了。
这就是改名的实验现象。目前我们这个芯片只有AD0一个引脚,它就只能拥有两个名字。如果有AD0和AD1两个引脚,就可以拥有总共四个名字。如果有更多的可配置引脚,就有更多的改名机会。当 我们 需要一条总件挂载多个相同型号的设备时 , 就可以利用这个改名的功能 , 避免名字也就是 从机 地址的重复。
再次把飞线插上,然后修成程序使用"1101 001了"寻址一下,确认下是否可以I2C地址寻址成功。使用淘宝购买的19块钱的24MHz 8通道逻辑分析仪抓取I2C通信的时序,可以看到已经收到了I2C从机MP6050的ACK应答。
接下来我们就继续来写建立在myI2C这一模块之上的MPU6050模块
3.🚚MPU6050.c
先初始化MPU6050
模拟指定地址写和指定地址读的时序
然后封装指定地址写和指定地址读的时序。
以上代码跟我们上节讲的指定地址写的这个时序是一样的,可以对照一下每一句代码:
补充:如果想要指定地址写多个字节就用一个for循环将这两句代码框起来多执行几遍
同理,我们按照指定地址读一个字节的时序来完成读的函数
同理,如果想要指定读取多个字节就可以将这两句代码用for循环框起来多执行几遍:
⚠️⚠️⚠️但是要注意,读取最后一个字节给非应答 , 这 之前都 要给 应答。
在这里指定句子读一个字节的时序就完成了,我们就可以进一步来进行测试一下。
以这个MPU6050的这个寄存器为例,这个寄存器是只读寄存器,它的地址是0x75,内容是ID号,默认是0x68
头文件大家都会自己声明了,这个就不用说了,以后都略过
在主函数里可以这样调用:
MP6050.c
cpp
#include "stm32f10x.h" // Device header
#include "MP6050.h"
#include "MyI2C.h"
#include "Delay.h"
#define MP6050_I2CADDR (0x68 << 1)
#define MP6050_I2CRead_DIR 0x01
#define MP6050_I2CWrit_DIR 0x00
void MP6050_Init(void)
{
MyI2C_Init();
}
void MP6050_WriteReg(uint8_t RegAddr, uint8_t data)
{
MyI2C_Start();
MyI2C_Send_Byte(MP6050_I2CADDR); //发送I2C从机地址
MyI2C_Recv_Ack(); //接收应答
MyI2C_Send_Byte(RegAddr); //发送寄存器地址
MyI2C_Recv_Ack(); //接收应答
MyI2C_Send_Byte(data); //发送写数据
MyI2C_Recv_Ack(); //接收应答
MyI2C_Stop();
}
uint8_t MP6050_ReadReg(uint8_t RegAddr)
{
uint8_t data;
MyI2C_Start(); //发送Start
MyI2C_Send_Byte(MP6050_I2CADDR); //发送I2C从机地址,写
MyI2C_Recv_Ack(); //接收应答
MyI2C_Send_Byte(RegAddr); //发送寄存器地址
MyI2C_Recv_Ack(); //接收应答
MyI2C_Start(); //发送ReStart
MyI2C_Send_Byte(MP6050_I2CADDR | MP6050_I2CRead_DIR); //发送I2C从机地址,读
MyI2C_Recv_Ack(); //接收应答
data = MyI2C_Recv_Byte(); //接收数据
MyI2C_Send_NAck(); //发送NACK非应答
MyI2C_Stop();
return data;
}
MP6050.h
cpp
#ifndef __MP6050_H__
#define __MP6050_H__
uint8_t MP6050_ReadReg(uint8_t RegAddr);
void MP6050_WriteReg(uint8_t RegAddr, uint8_t data);
void MP6050_Init(void);
#endif
Main.c
cpp
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
#include "UART.h"
#include <stdio.h>
#include "Key.h"
#include "String.h"
#include "LED.h"
#include "MyI2C.h"
#include "MP6050.h"
#define MP6050_REG_ID 0x75
int main(int argc, char *argv[])
{
uint8_t AckBit = 0;
uint8_t MP6050Id;
OLED_Init();
MP6050_Init();
OLED_ShowString(1, 1, "MP6060 ID:");
MP6050Id = MP6050_ReadReg(MP6050_REG_ID);
OLED_ShowHexNum(2, 1, MP6050Id, 2);
while(1)
{
}
return 1;
}
可以看到读出的ID号是0x68,这说明我们指定地址读一个字节的时序没问题。
使用淘宝购买的19块钱的24MHz 8通道逻辑分析仪抓取I2C通信的时序
接下来验证一下写寄存器的功能。
验证写寄存器的功能
💎💎要想写寄存器,首先需要解除芯片的睡眠模式 , 否则写入是无效的。
睡眠模式是电源管理寄存器1的sleep控制的。我们可以直接把这个寄存器写入0x00,这样就能解除睡眠模式了。
这个寄存器的地址是0x6B
解除了睡眠模式后,我们找一个寄存器写试一下,比如采样率分频寄存器,它的地址是0x19,内容是采样分频,这个值可以先随便给一个,比如0xAA。
到底有没有写入成功?我们在读寄存器把0x19地址下的数据读出来,存到一个变量里
Main.c
cpp
#include "String.h"
#include "LED.h"
#include "MyI2C.h"
#include "MP6050.h"
#define MP6050_REG_ID 0x75
#define MP6050_REG_PWRMNG2 0x6B
#define MP6050_REG_SIMPDIV 0x19
int main(int argc, char *argv[])
{
uint8_t AckBit = 0;
uint8_t MP6050Id;
uint8_t RegVal = 0;
OLED_Init();
MP6050_Init();
OLED_ShowString(1, 1, "MP6060 ID:");
MP6050Id = MP6050_ReadReg(MP6050_REG_ID);
OLED_ShowHexNum(2, 1, MP6050Id, 2);
MP6050_WriteReg(MP6050_REG_PWRMNG2, 0x00); //关闭MP6050睡眠模式
MP6050_WriteReg(MP6050_REG_SIMPDIV, 0xBB); //写MP6050采样分频寄存器
RegVal = MP6050_ReadReg(MP6050_REG_SIMPDIV); //读MP6050采样分频寄存器
OLED_ShowHexNum(4, 1, RegVal, 2);
while(1)
{
}
return 1;
}
实验结果:
说明读写程序都没有问题。
目前我们是把MPU6050当成一个存储器来使用的,写某个存储器,读某个存储器。
其实读写真正的存储器芯片也是完全一样的逻辑。
💎💎寄存器也是一种存储器, 只不过普通的存储器只能写和读里面的数据 , 并没有赋予什么实际意义 , 而 寄存器 的每一位数据都对应着硬件电路的状态 ,寄存器 和外设的硬件电路是可以进行互动的。
完善一下MPU6050初始化函数
接下来我们再完善一下MPU6050初始化函数
初始化之后,我们还要再写入一些寄存器,对MPU6050硬件电路进行初始化配置。
在这里我们一般会用宏定义,先把寄存器的地址都用一个字符串来表示。
要不然每次都查手册比较麻烦,而且光写一个数据的地址也不容易理解。寄存器如果比较少的话,可以直接在这上面进宏定义。如果比较多的话,我们可以再新建一个单独的头文件存,要不然比较占地方。
所以我们在hardware右键再添加一个头文件。
以上宏定义跟手册上是对应的。
有了这个头文件之后,我们要记得在相关的.c文件中包含这个头文件。
MP6050_Reg.h
cpp
#ifndef __MP6050_REG_H__
#define __MP6050_REG_H__
#define MP6050_SMPLRT_DIV 0x19
#define MP6050_CONFIG 0x1A
#define MP6050_GYRO_CONFIG 0x1B
#define MP6050_ACCEL_CONFIG 0x1C
#define MP6050_ACCEL_XOUT_H 0x3B
#define MP6050_ACCEL_XOUT_L 0x3C
#define MP6050_ACCEL_YOUT_H 0x3D
#define MP6050_ACCEL_YOUT_L 0x3E
#define MP6050_ACCEL_ZOUT_H 0x3F
#define MP6050_ACCEL_ZOUT_L 0x40
#define MP6050_TEMP_OUT_H 0x41
#define MP6050_TEMP_OUT_L 0x42
#define MP6050_GYRO_XOUT_H 0x43
#define MP6050_GYRO_XOUT_L 0x44
#define MP6050_GYRO_YOUT_H 0x45
#define MP6050_GYRO_YOUT_L 0x46
#define MP6050_GYRO_ZOUT_H 0x47
#define MP6050_GYRO_ZOUT_L 0x48
#define MP6050_PWM_MGMT_1 0x6B
#define MP6050_PWM_MGMT_2 0x6C
#define MP6050_WHO_AM_I 0x75
#endif
接下来开始配置寄存器
配置寄存器
第一步,配置电源管理寄存器1
设备复位给0不复位,睡眠模式给0解除睡眠,循环模式给0不需要循环,无关位给0即可,温度传感器失能给0 。最后三位选择时钟给000选择内部时钟。但是上节说了它非常建议我们选择陀螺仪时钟,所以我们可以给个001,选择x轴的陀螺仪时钟。当然测试看的话,选择哪个始终影响并不大。所以这个寄存器写入的数据就是0x01。
第二步,配置电源管理寄存器2
前两位是循环模式唤醒频率,给00,即不需要。后六位是每个轴的待机位,全都给0,不需要待机。所以这个寄存器写入的值就是0x00
第三步,配置采样率分频
这八位决定了数据输出的快慢,值越小越快 ,这个可以根据实际需求来,我们给个0x09,也就是十分频。(因为分频系数=1+SMPLRT_DIV(十进制))
第四步,配置寄存器
外部同步全都给0,不需要。最后三位数字低通滤波器,这个也是根据需求来,我们可以给个110,这就是最平滑的滤波。所以这整个计算机的值就是0x06。
第五步,陀螺仪配置寄存器
前面三位是自测使能,这里手册写漏了,我们就不自测了,都给0。满量程选择这个也是根据实际需求来,我们就给11选择最大量程,后面三位无关位,所以这个基准器就是0x18。
第六步,加速度计配置寄存器
在这里自测给000,满量程暂时也给最大量程11,最后高通滤波器我们用不到,给零零。所以这个计算机的值也是0x18。
这样初始化配置就完成了。
💎💎目前的配置主要是解除睡眠、选择陀螺仪时钟、六个轴均不待机、采样分频为10、滤波参数给最大、陀螺仪和加速度计都选择最大量程。
这就是目前给的配置,大家根据实际项目的需求,可以对应更改。
配置完之后,陀螺仪内部就在连续不断的进行数据转换了,输出的数据就存放在这里的数据寄存器里。
获取数据寄存器的值的函数
接下来我们想获取数据的话,只需要再写一个获取数据寄存器的函数即可。
并且这个函数需要返回六个uint16_t的数据,分别表示xyz的加速度值和陀螺仪值。
但是C语言中函数的返回值只能有一个,所以这里就需要一些特殊操作来实现返回六个值的任务,多返回值函数的设计方法有很多
第一种方法
第一种最简单的方法就是在函数外面定义六个全局变量,子函数读到的数据直接写到全局变量里。然后六个全局变量在主函数里进行共享,这样就相当于返回来六个值。这是一种比较简单且直接的方法,比较适合用在规模比较小的项目中。
但这种方法不太利于分装。
第二种方法
第二种进阶一点的方法是用指针进行变量的地址传递来实现多返回值。
第三种方法
然后第三种更进一步更高阶的方法就是用结构体对多个变量进行打包,然后再统一进行传递。这种方法就是STM32的库函数里,这里使用到的,类似于我们初始化GPIO口这里使用 的结构体
这里是结构体打包,输入参数。但是输出参数或者返回值,也可以这样进行打包。
总之,参数的传递方法有很多,一般项目越大,就越要考虑使用这些高级语法,这样更有利于工程的管理。
在这里 , 我 们 就使用第二种方法 , 用指针的地址传递。
所以在函数参数这里写上六个输出参数
我们会在主函数里定义变量,通过指针把主函数变量的地址传递到子函数来。子函数中通过传递过来的地址,操作主函数的变量。
这样子函数结束之后,主函数变量的值就是子函数想要返回的值,这就是使用指针实现函数多返回值的设计。
然后子函数中想要获取数据,我们就要通过MPU6050_ReadReg函数读取数据寄存器。
首先读取加速度静器x轴的高8位,然后再读取加速度寄存器的低8位。
前面定义两个变量,读取高8位的值放在datah中,读取低八位的值,放在dataL中。
之后,高8位左移8位再或上低八位,这就是加速度计x轴的十六位数据,得到十六位数据之后,用指针引用传递进来的地址,把读到的数据通过指针返回回去。这样accx的值就完成了。
然后这里可能有人疑问,这个datah是八位的数据,它再左移八位会不会出问题?这个经过测试是没问题的,因为最终赋值的变量是十六位的。所以八位数据左移之后,会自动进行类型转换,移出去的位并不会丢失。当然如果不放心的话,可以把这两个数据改为十六位的,这样就肯定没问题了。
另外因为手册里说过,这个十六位数据是一个用补码表示的有符号数,所以最终直接赋制给int16_t也是没问题的。
接下来读取后续的数据,同样的操作。
这样这个读取函数就完成了。程序逻辑是分别读取六个轴数据寄存器的高位和低位拼接成十六位的数据,再通过指针变量返回。这里我们是用读取一个寄存器的函数,连续调用了十二次才读取完十二个寄存器。但实际上还有一种更高效的方法,就是使用我们之前提到的I2C读取多个字节的时序,从一个基地址开始,连续读取一片的寄存器。
因为我们这个寄存器的地址是连续的,所以可以从第一个寄存器的地址0x3b开始,连续读取十四个字节,这样就可以一次性的把加速度值、陀螺仪值,当然还包括两个字节的温度值都读出来了。这样在时序上读取效率就会大大提升。有兴趣的话可以自己写程序试一下,这里我们就不写了。
接下来我们来测试一下看看。
测试
主函数这里我们先定义六个变量,这六个值分别用来接收xyz轴的加速度值和陀螺移值
之后,在主循环里调用刚刚写的读取函数,把这6个变量的地址传过去操作。这样就能读取六个轴的数据了,然后用OLED显示一下
代码:
MP6050.c
cpp
#include "stm32f10x.h" // Device header
#include "MP6050.h"
#include "MyI2C.h"
#include "Delay.h"
#include "MP6050_Reg.h"
#include "OLED.h"
#define MP6050_I2CADDR (0x68 << 1)
#define MP6050_I2CRead_DIR 0x01
#define MP6050_I2CWrit_DIR 0x00
void MP6050_Init(void)
{
MyI2C_Init();
MP6050_WriteReg(MP6050_PWM_MGMT_1, 0x01); //关闭MP6050睡眠模式,采样时钟选择内部GyroX轴时钟
MP6050_WriteReg(MP6050_PWM_MGMT_2, 0x00); //MP6050 6轴都使能,不使用Wakeup模式
MP6050_WriteReg(MP6050_SMPLRT_DIV, 0x09); //MP6050采样分频值10分频,实际采样频率=时钟频率/(分频值+1)
MP6050_WriteReg(MP6050_CONFIG, 0x06); //MP6050不使用FSYNC,DLPF数字低通滤波器选择6最平滑滤波
MP6050_WriteReg(MP6050_GYRO_CONFIG, 0x18); //陀螺仪量程选择最大量程
MP6050_WriteReg(MP6050_ACCEL_CONFIG, 0x18); //加速度计量程选择最大量程,不使用高通滤波
}
void MP6050_WriteReg(uint8_t RegAddr, uint8_t data)
{
MyI2C_Start();
MyI2C_Send_Byte(MP6050_I2CADDR); //发送I2C从机地址
MyI2C_Recv_Ack(); //接收应答
MyI2C_Send_Byte(RegAddr); //发送寄存器地址
MyI2C_Recv_Ack(); //接收应答
MyI2C_Send_Byte(data); //发送写数据
MyI2C_Recv_Ack(); //接收应答
MyI2C_Stop();
}
uint8_t MP6050_ReadReg(uint8_t RegAddr)
{
uint8_t data;
MyI2C_Start(); //发送Start
MyI2C_Send_Byte(MP6050_I2CADDR); //发送I2C从机地址,写
MyI2C_Recv_Ack(); //接收应答
MyI2C_Send_Byte(RegAddr); //发送寄存器地址
MyI2C_Recv_Ack(); //接收应答
MyI2C_Start(); //发送ReStart
MyI2C_Send_Byte(MP6050_I2CADDR | MP6050_I2CRead_DIR); //发送I2C从机地址,读
MyI2C_Recv_Ack(); //接收应答
data = MyI2C_Recv_Byte(); //接收数据
MyI2C_Send_NAck(); //发送NACK非应答
MyI2C_Stop();
return data;
}
void MP6050_GetData(struct MP6050_Data_ParaOut *MP6050ParaOut)
{
uint8_t Data_H = 0;
uint8_t Data_L = 0;
Data_H = MP6050_ReadReg(MP6050_ACCEL_XOUT_H);
Data_L = MP6050_ReadReg(MP6050_ACCEL_XOUT_L);
MP6050ParaOut->AccelX = (Data_H << 8) | Data_L; //MP6050加速度计X轴
Data_H = MP6050_ReadReg(MP6050_ACCEL_YOUT_H);
Data_L = MP6050_ReadReg(MP6050_ACCEL_YOUT_L);
MP6050ParaOut->AccelY = (Data_H << 8) | Data_L; //MP6050加速度计Y轴
Data_H = MP6050_ReadReg(MP6050_ACCEL_ZOUT_H);
Data_L = MP6050_ReadReg(MP6050_ACCEL_ZOUT_L);
MP6050ParaOut->AccelZ = (Data_H << 8) | Data_L; //MP6050加速度计Z轴
Data_H = MP6050_ReadReg(MP6050_GYRO_XOUT_H);
Data_L = MP6050_ReadReg(MP6050_GYRO_XOUT_L);
MP6050ParaOut->GyroX = (Data_H << 8) | Data_L; //MP6050角速度计X轴
Data_H = MP6050_ReadReg(MP6050_GYRO_YOUT_H);
Data_L = MP6050_ReadReg(MP6050_GYRO_YOUT_L);
MP6050ParaOut->GyroY = (Data_H << 8) | Data_L; //MP6050角速度计X轴
Data_H = MP6050_ReadReg(MP6050_GYRO_ZOUT_H);
Data_L = MP6050_ReadReg(MP6050_GYRO_ZOUT_L);
MP6050ParaOut->GyroZ = (Data_H << 8) | Data_L; //MP6050角速度计X轴
}
MyI2C.c
cpp
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "Delay.h"
#include "OLED.h"
#define I2C_SCL_GPIO_PIN GPIO_Pin_0
#define I2C_SDA_GPIO_PIN GPIO_Pin_1
#define I2C_GPIO GPIOA
void MyI2C_W_SCL(uint8_t bitValue)
{
GPIO_WriteBit(I2C_GPIO, I2C_SCL_GPIO_PIN, (BitAction)bitValue);
Delay_us(100); //延时10us,防止翻转过快超过I2C速率
}
void MyI2C_W_SDA(uint8_t bitValue)
{
GPIO_WriteBit(I2C_GPIO, I2C_SDA_GPIO_PIN, (BitAction)bitValue);
Delay_us(100); //延时10us,防止翻转过快超过I2C速率
}
uint8_t MyI2C_R_SDA(void)
{
uint8_t bitValue;
bitValue = GPIO_ReadInputDataBit(I2C_GPIO, I2C_SDA_GPIO_PIN);
Delay_us(100); //延时10us,防止翻转过快超过I2C速率
return bitValue;
}
void MyI2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef gpioInitStructure;
gpioInitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出
gpioInitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
//初始化为输出高电平
GPIO_SetBits(I2C_GPIO, I2C_SCL_GPIO_PIN);
GPIO_SetBits(I2C_GPIO, I2C_SCL_GPIO_PIN);
}
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); //先拉高SDA总线
MyI2C_W_SCL(1); //再拉高SCL总线
MyI2C_W_SDA(0); //SCL为高电平时,SDA从从高到低表示起始条件
MyI2C_W_SCL(0); //SCL拉低为低电平
}
void MyI2C_Stop(void)
{
MyI2C_W_SCL(0); //SCL拉低
MyI2C_W_SDA(0); //SDA拉低
MyI2C_W_SCL(1); //SCL总线拉为高电平
MyI2C_W_SDA(1); //SCL为高电平时,SDA从低到高表示结束条件
}
void MyI2C_Send_Ack(void)
{
MyI2C_W_SDA(0); //SDA拉低,发送ACK
MyI2C_W_SCL(1); //SCL拉高,通知从机读取数据
MyI2C_W_SCL(0); //SCL拉低
}
void MyI2C_Send_NAck(void)
{
MyI2C_W_SDA(1); //SDA拉低,发送NACK
MyI2C_W_SCL(1); //SCL拉高,通知从机读取数据
MyI2C_W_SCL(0); //SCL拉低
}
uint8_t MyI2C_Recv_Ack(void)
{
uint8_t bitValue;
MyI2C_W_SDA(1); //主机SDA释放总线
MyI2C_W_SCL(1); //SCL拉高
bitValue = MyI2C_R_SDA(); //读取SDA总线
MyI2C_W_SCL(0); //SCL拉低
return bitValue;
}
void MyI2C_Send_Byte(uint8_t data)
{
MyI2C_W_SCL(0);
for(int i=0; i<8; i++)
{
MyI2C_W_SDA( (data & 1<<(8-i-1)));
MyI2C_W_SCL(1); //SCL拉高,从机读取SDA总线
MyI2C_W_SCL(0); //SCL拉低
}
}
uint8_t MyI2C_Recv_Byte(void)
{
uint8_t data = 0;
//首先释放SDA总线,防止对从机造成干扰
MyI2C_W_SDA(1);
for(int i=0; i<8; i++)
{
MyI2C_W_SCL(1); //拉高SCL总线
if(MyI2C_R_SDA())
{
data |= (1 << (8-i-1)); //高位优先
}
MyI2C_W_SCL(0); //拉低SCL总线
}
return data;
}
Main.c
cpp
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
#include "UART.h"
#include <stdio.h>
#include "Key.h"
#include "String.h"
#include "LED.h"
#include "MyI2C.h"
#include "MP6050.h"
#include "MP6050_Reg.h"
#define MP6050_REG_ID 0x75
#define MP6050_REG_PWRMNG2 0x6B
#define MP6050_REG_SIMPDIV 0x19
int main(int argc, char *argv[])
{
uint8_t AckBit = 0;
uint8_t MP6050Id;
uint8_t RegVal = 0;
struct MP6050_Data_ParaOut MP6050Data = {0};
OLED_Init();
Delay_us(500);
MP6050_Init();
//MyI2C_Send_Byte(0x55);
while(1)
{
MP6050_GetData(&MP6050Data);
OLED_ShowSignedNum(1, 1, MP6050Data.AccelX ,5);
OLED_ShowSignedNum(1, 8, MP6050Data.AccelY ,5);
OLED_ShowSignedNum(2, 1, MP6050Data.AccelZ ,5);
OLED_ShowSignedNum(3, 1, MP6050Data.GyroX ,5);
OLED_ShowSignedNum(3, 8, MP6050Data.GyroY ,5);
OLED_ShowSignedNum(4, 1, MP6050Data.GyroZ ,5);
Delay_ms(100);
}
return 1;
}
结果:
可以看到目前显示的六个数据,并且不断在刷新,晃动传感器数据也都有变化。
然后按照之前我们讲的加速度计和陀螺仪的模型,大概验证一下这些数的含义。
首先这里左边三个数是xyz轴的加速度计,我们按照之前说的一个正方体里面放置一个小球的模型来理解一下。小球压在哪个面上,就产生对应轴的输出。
目前这个芯片是水平放置,对应正方体四个侧面应该不受力,所以这里显示的xy轴数据基本为零。
小球压在底面上产生一个g的重力加速度。这里显示的数据是1943,这个数据对应的重力加速度值,可以算一下:目前初始化配置里我们选择的满量程是最大的16g,所以按比例算下1943/32768=x/16g,解的x就是测量值,结果是0.95g
这里标准的答案应该是一个g,所以测量基本没问题。
加速度计测试
然后再看一下这个板子上标的有x轴和y轴的示意图,可能比较小,大家可以自己看一下,画的是纵向为x轴,横向为y轴,剩下一个z轴是垂直于芯片的个轴
我们这样倾斜就应该是加速度的x轴两个面受力。
第一个数据,上倾,x轴正值
下倾,x轴负值。
而这样倾斜就是外轴的两个面受力
无论怎么倾斜,z轴加速度都是正值,如何让z轴出现负值?
在个正方体和小球的模型中,Z轴代表上下两个面的受力。下面受力是正值,上面受力就是负值
所以要想让z轴输负值得让上面一个面受力。显然我们把这个芯片翻过来,这样z轴就是负值了。
这些就是加速度大小的体现。
陀螺仪测试
然后我们看一下陀螺仪,这里右边三个数据为三个轴的角速度。
我们让面包板平行于桌面转,即是绕z轴旋转陀螺仪,z轴会输出对应的角速度。
平行于桌面逆时针转动,陀螺仪z轴角速度为正
平行于桌面顺时针转动,陀螺仪z轴角速度为负
以面包板横向的中间那条槽为轴,前后翻转,即是绕y轴的转动,陀螺仪y轴数据变化。
以纵向中心轴转,即绕x轴的转动陀螺仪x轴数据变化
具体每个轴旋转的角速度是多少?
也是按照我们刚才说的比例公式计算。
读取的数据 /32768= x / 满 量程, 解得x就是具体的角速度值。
这些就是这个传感器测量各轴姿态数据的实验现象。
目前这个代码的任务也基本完成了。
4.🚚 实验问题记录和经验分享
1.本节I2C软件爱你读取MP6050 接口实验里,遇到了一个奇怪的问题,通过STM32 向GPIO I2C SDA总线上写 '1' 的时候,使用逻辑分析仪抓去通信时序发现此时是'0',是MP6050把电平拉低了,各种诡异的现象,逻辑分析仪还抓取到SDA总线上的突发电平。😭😭😭😭😭😭😭 搞了好几个小时,花了一上午时间,还以为是MP6050硬件有问题,后面对比江科大STM32提供的I2C软件爱你读取源码,才发现是GPIO初始化的时候把GPIO工作模式初始化错了了,错误的写成了'GPIO_Mode_PP'推挽输出模式,而I2C GPIO模拟需要GPIO初始化为开漏输出模式'GPIO_Mode_OD'。
GPIO没有初始化为正确的开漏输出模式,造成了我实验时I2C总线上各种诡异的问题。切记,切记,