🚀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博客
STM32 MCU学习资源-CSDN博客
stm32学习笔记-作者: Vera工程师养成记
stem32江科大自学笔记-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.🚚I2C硬件库函数
stm32中I2C库函数介绍(stm32f10x_i2c.h)
下面这些是关于I2C的库函数,这里我会挑选最常见的以及本期要用到的来去进行详细讲解。
1.初始化
在学过前面这么多结构体初始化的函数,对于结构体的初始化我们已经再属性不过了,下面是I2C外设的结构体初始化的函数。
void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
一般情况我们都是自己去定义配置结构体再初始化的,下面我们看一个定义好结构体初始化的示例:
I2C_InitTypeDef I2C_initstruct;
I2C_initstruct.I2C_Mode = I2C_Mode_I2C; //选择模式,这里选定使用I2C模式
I2C_initstruct.I2C_ClockSpeed = 100000; //选择时钟的频率,不得大于400kHz
I2C_initstruct.I2C_DutyCycle = I2C_DutyCycle_2;//占空比选择,低电平:高电平,如果当前是标准速度的话,这个是没用的的,如果在快速时钟的时候,这个才会起作用
I2C_initstruct.I2C_Ack = I2C_Ack_Enable; //选择是否给应答
I2C_initstruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//stm32作为从机的时候的响应地址位数
I2C_initstruct.I2C_OwnAddress1 = 0x01; //stm32作为从机的时候地址,我们这里是主机,所以这里随便设一个就行了
I2C_Init(I2C2, &I2C_initstruct);
2.使能操作
我们本期不需要用DMA到使能,所以这里就不过多讲解。
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
3.生成起始位和结束位标志
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
手册解释如下:
4.发送I2C从机地址
下面这个函数其实就是前面学习软件I2C的“点名”操作,硬件也是一样的,同样都是需要进行点名指定的I2C从机来进行通讯。如果有多个从机,主机会执行下面这个函数后会给每一个从机发送Address的数据,然后从机接收到了后就对比自己的IP数据,如果一样的话那么就开始跟主机通讯,其他从机保持沉默。
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
5.发送数据和接收数据
下面这个是发送数据:
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
下面可以看到把数据放入到数据寄存器DR的操作后,然后剩下就是进入到发送数据的流程。
下面这个是接受数据:
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
我们可以看到这个函数的返回值是DR寄存器里面的数据。
6.发送应答位
下面这个函数是用来发送应答位的,当主机接收到了数据之后会向从机发送应答情况,如果应答位是ENABLE表示接收完成返回一个应答反之就是无应答返回 ,手册解释如下
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
7.状态检测
下面这个函数是用来检测每次操作一个步骤之后的状态的,比如发送了一个字节后的数据,然后就会进入一个EVx的状态,这里就需要去执行这个状态是否完成,然后再执行下一步,这是一种缓冲机制以保证数据收发的准确性。在上一期我们也是详细讲解过了这个过程了的。
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
下面是这个函数的相关参数:
2.🚚硬件I2C读取MPU6050
实验现象如下,其实跟前面软件I2C读取MPU6050现象是一样的,只是方法不同。(软件I2C读取MPU6050链接:stm32入门-----软件I2C读写MPU6050-CSDN博客)
1.电路连线图
查STM32F102T8C6引脚定义表,看到I2C1 PB6, PB7引脚已经给 OLED液晶显示屏使用,本节我们使用I2C2的PB10,PB11引脚。
2.主要工程文件
主要工程文件如下,这里我们就没有像软件那部分一样需要建立一个MyI2C的文件来去执行收发数据的底层,这里我们是使用硬件外设配置库函数来去实现I2C通讯的,底层是由硬件来自动执行的,所以就不需要MyI2C这个文件了。
其中MPU6050.c文件是用来配置I2C外设以及MPU6050相关模块的文件,MPU6050_reg.h文件是存放MPU6050相关寄存器数据的文件
MPU6050_reg.h文件数据如下:
#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
这里我们主要去讲解MPU6050.c这个文件里面的内容,这个是配置I2C外设的主要文件。
3.MPU6050.c代码剖析
(1)检测步骤超时操作
在进入到检测状态的函数的时候,难免会出现意外超时的情况,这里我们就需要去对这个bug进行处理,比如如果超时的情况就放弃这个字节读取或写入的操作。
//检测事件后的超时操作
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT) {
uint32_t time_out = 100000;
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) {
time_out--;
if (!time_out) {
break;
}
}
}
(2)指定地址写
代码是对应下面这个序列图进行写的,序列图跟代码的步骤是一样的,我们这里使用的是7位。
void MP6050_WriteReg(uint8_t RegAddr, uint8_t data)
{
//起始条件
I2C_GenerateSTART(I2C2, ENABLE); //产生Start起始位
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //等待EV5事件
//发送I2C从机地址
I2C_Send7bitAddress(I2C2, MP6050_I2CADDR, I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS); //等待EV6事件
//发送寄存器地址
I2C_SendData(I2C2, RegAddr);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS); //等待EV8事件
//发送数据
I2C_SendData(I2C2, data);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS); //等待EV8事件
//发送停止位
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS); //等待EV8_2事件
I2C_GenerateSTOP(I2C2, ENABLE);
}
(3)指定地址读
uint8_t MP6050_ReadReg(uint8_t RegAddr)
{
uint8_t data = 0;
//起始条件
I2C_GenerateSTART(I2C2, ENABLE); //产生Start起始位
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //等待EV5事件
//发送I2C从机地址
I2C_Send7bitAddress(I2C2, MP6050_I2CADDR, I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS); //等待EV6事件
//发送寄存器地址
//这里需要等待EV8_2事件等待数据传输完成,TRANSMITTED 而不是 TRANSMITTING 正在传输
I2C_SendData(I2C2, RegAddr);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS); //等待EV8_2事件
//重复起始条件
I2C_GenerateSTART(I2C2, ENABLE); //产生Start起始位
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //等待EV5事件
//发送I2C从机地址
I2C_Send7bitAddress(I2C2, MP6050_I2CADDR, I2C_Direction_Receiver);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS); //等待EV6事件
//只接收一个字节必须在EV6事件之后清除ACK位和应答位
I2C_AcknowledgeConfig(I2C2, DISABLE); //设置NACK非应答位
I2C_GenerateSTOP(I2C2, ENABLE); //停止条件
//接收数据
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS); //等待EV7事件
data = I2C_ReceiveData(I2C2);
//回复默认的ACK=1,方便连续接收
I2C_AcknowledgeConfig(I2C2, ENABLE);
return data;
}
(4)初始化配置
(5)获取MPU6050寄存器数据
4.主函数代码
这部分其实跟软件I2C读写MPU6050的是一样的。
MP6050.c
#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)
{
//使能RCC外设时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//配置GPIO
GPIO_InitTypeDef gpioInitStructure;
gpioInitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //复用开漏输出模式
gpioInitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; //使用GPIOB_Pin10, Pin11
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpioInitStructure);
//配置I2C外设
I2C_InitTypeDef I2C_InitStruct;
I2C_StructInit(&I2C_InitStruct);
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; //I2C工作方式
I2C_InitStruct.I2C_ClockSpeed = 100*1000; //I2C时钟频率,不超过400KHz
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_16_9; //I2C时钟占空比
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; //I2C应答
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //I2C从机7位模式
I2C_InitStruct.I2C_OwnAddress1 = 0x01; //I2C从机模式使用,自身地址寄存器
I2C_Init(I2C2, &I2C_InitStruct);
//使能I2C外设
I2C_Cmd(I2C2, ENABLE);
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); //加速度计量程选择最大量程,不使用高通滤波
MP6050_ReadReg(MP6050_WHO_AM_I);
}
void MP6050_WriteReg(uint8_t RegAddr, uint8_t data)
{
//起始条件
I2C_GenerateSTART(I2C2, ENABLE); //产生Start起始位
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //等待EV5事件
//发送I2C从机地址
I2C_Send7bitAddress(I2C2, MP6050_I2CADDR, I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS); //等待EV6事件
//发送寄存器地址
I2C_SendData(I2C2, RegAddr);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS); //等待EV8事件
//发送数据
I2C_SendData(I2C2, data);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS); //等待EV8事件
//发送停止位
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS); //等待EV8_2事件
I2C_GenerateSTOP(I2C2, ENABLE);
}
uint8_t MP6050_ReadReg(uint8_t RegAddr)
{
uint8_t data = 0;
//起始条件
I2C_GenerateSTART(I2C2, ENABLE); //产生Start起始位
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //等待EV5事件
//发送I2C从机地址
I2C_Send7bitAddress(I2C2, MP6050_I2CADDR, I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS); //等待EV6事件
//发送寄存器地址
//这里需要等待EV8_2事件等待数据传输完成,TRANSMITTED 而不是 TRANSMITTING 正在传输
I2C_SendData(I2C2, RegAddr);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS); //等待EV8_2事件
//重复起始条件
I2C_GenerateSTART(I2C2, ENABLE); //产生Start起始位
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //等待EV5事件
//发送I2C从机地址
I2C_Send7bitAddress(I2C2, MP6050_I2CADDR, I2C_Direction_Receiver);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS); //等待EV6事件
//只接收一个字节必须在EV6事件之后清除ACK位和应答位
I2C_AcknowledgeConfig(I2C2, DISABLE); //设置NACK非应答位
I2C_GenerateSTOP(I2C2, ENABLE); //停止条件
//接收数据
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS); //等待EV7事件
data = I2C_ReceiveData(I2C2);
//回复默认的ACK=1,方便连续接收
I2C_AcknowledgeConfig(I2C2, ENABLE);
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轴
}
Main.c
#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();
MP6050_Init();
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;
}
实验结果:
使用淘宝购买的19块钱的24MHz 8通过逻辑分析仪抓取下硬件I2C读取时的通信时序。
可以看到硬件I2C的时序,SCLK时钟特别规整,SDA的变化也紧跟着SCK的下降沿。
3🚚实验问题记录和经验分享
本节硬件I2C实验遇到了一个问题,就是I2C_ReceiveData()读取出来的数据总是固定的值 '0xD',反复排查才发现是因为在读取I2C DR接收寄存器之前没有等待EV7事件。如果不等待EV7事件就直接读取I2C DR寄存器,根据STM32的主接收时序此时DR寄存器的值还是上一次的值,I2C数据还没有开始接收,所以此时立即读I2C DR寄存器就总是读取到的上一次值。
正确的做法是,接收时先等待EV7事件确认I2C 已经接收到数据,再读取I2C DR寄存器,此时读取到的才是正确的接收数据。