实验目的与任务
实验目的:
1. 学习对I2C的使用;
2. 掌握KEIL5的仿真与调试。
任务:
1. 根据要求编写程序,并写出原理性注释;
2. 将检查程序运行的结果,分析一下是否正确;
3. 完成所建工程的验证调试。
实验要求
本实验以 EEPROM的读写实验为大家讲解 STM32 的 I2C 使用方法。实验中 STM32 的 I2C 外设采用主模式,分别用作主发送器和主接收器, 通过查询事件的方式来确保正常通讯。
实验内容及步骤
本实验板中的 EEPROM 芯片(型号: AT24C02)的 SCL 及 SDA 引脚连接到了 STM32 对应的 I2C 引脚中,结合上拉电阻,构成了 I2C 通讯总线,它们通过 I2C 总线交互。EEPROM 芯片的设备地址一共有 7 位,其中高 4 位固定为: 1010 b,低 3 位则由 A0/A1/A2信号线的电平决定,见下图,图中的 R/W 是读写方向位,与地址无关。
按照我们此处的连接, A0/A1/A2 均为 0,所以 EEPROM 的 7 位设备地址是: 1010000b ,即 0x50。由于 I2C 通讯时常常是地址跟读写方向连在一起构成一个 8 位数,且当R/W 位为 0 时,表示写方向,所以加上 7 位地址,其值为“0xA0”,常称该值为 I2C 设备的“写地址”;当 R/W 位为 1 时,表示读方向,加上 7 位地址,其值为“0xA1”,常称该值为“读地址”。
EEPROM 芯片中还有一个 WP 引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时,可写入数据,我们直接接地,不使用写保护功能。
1. 软件设计
① 实验新建文件步骤:
运行Keil 5开发环境。由于没有AT24C02模块,本实验使用基于I2C协议的MPU6050姿态传感器代替。为了使工程更加有条理,我们把I2C通讯底层协议和MPU6050功能函数相关的代码独立分开存储,方便以后移植。新建MyI2C.c、MyI2C.h、MPU6050.c、MPU6050.h、MPU6050_Reg.h文件。
② 编程要点:
-
配置通讯使用的目标引脚为开漏模式;
-
使能 I2C 外设的时钟;
-
配置 I2C 外设的模式、地址、速率等参数并使能 I2C 外设;
-
编写基本 I2C 按字节收发的函数;
-
编写读写 EEPROM 存储内容的函数;
-
编写测试程序,对读写数据进行校验。
2. 实验步骤
(1)运行Keil uVision5开发环境,建立一个项目工程。
(2)在工程中添加main.c文件,因需要用到OLED显示屏,所以将之前实验写好的OLED文件移植到该工程中,然后在main.c中调用,如图1所示。
图1 移植程序
(3)在工程中添加I2C底层驱动文件,因此需要创建MyI2C.c文件,编写I2C通讯时序,如图2所示。
图2 编写MyI2C.c代码
(4)编写MyI2C.h程序,方便以后工程文件以移植,使项目工程工具有移植性,如图3所示。
图3 MyI2C.h程序
(5)在工程中添加MPU6050使用的功能函数文件,因此需要先创建W25Q64_Ins.h文件,存放指令码,如图4所示。
图4 编写MPU6050指令码
(6)创建MPU6050.c文件,编写需要使用的功能函数,如图5所示。
图5 编写MPU6050功能函数
(7)编写MPU6050.h程序,方便以后工程文件以移植,使项目工程工具有移植性,如图6所示。
图6 编写MPU6050.h程序
(8)编写main.c程序,读取设备ID,读取六个方向的加速度计数据,如图7所示。
图7 main.c程序
运行并调试成功并无错误和警告。
3. 调试验证及结果
(1)将开发板连接到电脑上,使用跳线在面包板上将MPU6050和单片机连接,使用STLINK将程序烧录到STM32中,如图8所示。
图8 线路连接
(2)程序烧录后,实验现象如图9所示:
图9 实验现象
实验代码分析
(1)MyI2C.c:
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
* @brief 封装写SCL
* @param
* @retval
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
Delay_us(10);
}
/**
* @brief 封装写SDA
* @param
* @retval
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
Delay_us(10);
}
/**
* @brief 封装读SDA
* @param
* @retval
*/
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
/**
* @brief 初始化
* @param
* @retval
*/
void MyI2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//开漏输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}
/**
* @brief I2C起始条件
* @param
* @retval
*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);//拉高,释放
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);//拉低
MyI2C_W_SCL(0);
}
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);//拉低
MyI2C_W_SCL(1);//释放SCL
MyI2C_W_SDA(1);//释放SDA
}
/**
* @brief 发送一个字节
* @param
* @retval
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)
{
MyI2C_W_SDA(Byte & (0x80 >> i));//从最高位开始取
MyI2C_W_SCL(1);//释放SCL
MyI2C_W_SCL(0);//拉低SCL
}
}
/**
* @brief 读一个字节
* @param
* @retval
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1);
for (i = 0; i < 8; i ++)
{
MyI2C_W_SCL(1);
if (MyI2C_R_SDA()) {Byte |= (0x80 >> i);}
MyI2C_W_SCL(0);
}
return Byte;
}
/**
* @brief 发送应答
* @param
* @retval
*/
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
/**
* @brief 接受应答
* @param
* @retval
*/
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;
}
(2)MPU6050.c:
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"
#include "Delay.h"
#define MPU6050_ADDRESS 0xD0
/**
* @brief 指定地址写
* @param
* @retval
*/
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();
}
/**
* @brief 指定地址读
* @param
* @retval
*/
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;
}
/**
* @brief 初始化
* @param
* @retval
*/
void MPU6050_Init(void)
{
MyI2C_Init();
//解除睡眠,选择陀螺仪时钟,6个轴不待机,采样分频为10,滤波参数最大,陀螺仪和加速度计选最大量程
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);//电源管理寄存器1,选择陀螺仪时钟
//Delay_ms(100);
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);//电源管理寄存器2
//Delay_ms(100);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);//采样率分频,10分频
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);//配置寄存器,0000 0110
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);//陀螺仪配置寄存器, 0001 1000
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);//加速度计配置寄存器
}
/**
* @brief
* @param
* @retval
*/
uint8_t MPU6050_ReadID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
/**
* @brief 获取数据寄存器数据
* @param
* @retval
*/
void MPU6050_ReadData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
//加速度
*AccX = (MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H) << 8)| MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccY = (MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccZ = (MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*GyroX = (MPU6050_ReadReg(MPU6050_GYRO_XOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroY = (MPU6050_ReadReg(MPU6050_GYRO_YOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroZ = (MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
}
(3)main函数程序:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;
int main(void)
{
OLED_Init();
MPU6050_Init();
Delay_us(1000);
OLED_ShowString(1, 1, "ID:");
ID = MPU6050_ReadID();
OLED_ShowHexNum(1, 4, ID, 2);
while (1)
{
MPU6050_ReadData(&AX, &AY, &AZ, &GX, &GY, &GZ);
OLED_ShowSignedNum(2, 1, AX, 5);
OLED_ShowSignedNum(3, 1, AY, 5);
OLED_ShowSignedNum(4, 1, AZ, 5);
OLED_ShowSignedNum(2, 9, GX, 5);
OLED_ShowSignedNum(3, 9, GY, 5);
OLED_ShowSignedNum(4, 9, GZ, 5);
}
}
实验总结
通过本次实验,我对STM32的I2C接口有了更深入的理解,掌握了使用I2C进行设备间通信的基本方法和技巧。以下是我的一些心得体会:
仔细阅读官方文档和参考手册对于学习和理解外设的工作原理至关重要。通过深入研究文档,我对I2C接口的功能和寄存器有了更清晰的认识,能够更好地理解和应用相关函数和参数。
在编写代码时,注释的重要性不可忽视。合理的注释可以让代码更易读、易懂,便于他人阅读和维护。我在代码中添加了详细的注释,对每个函数和关键操作进行了解释,使代码更具可读性。
调试是解决问题的关键步骤。在实验过程中,我遇到了一些问题,如通信失败、数据错误等。通过仔细检查代码和调试输出信息,我成功找到并解决了问题。这个过程加深了我对I2C通信的理解,并培养了我解决问题的能力。
在实验中,及时记录和分析实验结果对于理解和掌握知识至关重要。我详细记录了每次实验的结果,分析了数据的正确性和稳定性。这些分析帮助我加深了对I2C通信的认识,并发现了一些潜在问题和改进空间。
总体而言,通过本次实验,我对STM32的I2C接口有了更深入的了解,并且掌握了使用I2C进行设备间通信的基本方法。我相信这些知识和经验将对我今后的学习和项目开发产生积极的影响。我会继续深入学习和探索STM32的其他外设接口,以提升我的嵌入式系统开发能力。
源码:实验4