Technology Sharing

STM32-I2C

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

This content is based onJiangxie Technology STM32It was sorted out after learning from the video.

1. I2C communication

1.1 Introduction to I2C Communication

  • I2C (Inter IC Bus) is a universal data bus developed by Philips
  • Two communication lines: SCL (Serial Clock) serial clock line, SDA (Serial Data) serial data line
  • Synchronous, half-duplex, single-ended, multi-device
  • With data reply
  • Support bus mounting multiple devices (one master and multiple slaves, multiple masters and multiple slaves)
    • One master and multiple slaves: The microcontroller acts as the host and dominates the operation of the I2C bus. All external modules mounted on the I2C bus are slaves. The slaves can control the I2C bus only after being named by the host and cannot touch the I2C bus without permission to prevent conflicts.
    • Multiple masters and multiple slaves: Any module on the bus can actively jump out and become the master. When there is a bus conflict, the I2C protocol will arbitrate. The winning party will gain control of the bus, and the losing party will automatically become a slave.

image.png

1.2 Hardware Circuit

  • All I2C devices have their SCL and SDA connected together
  • The device's SCL and SDA must be configured in open-drain output mode
  • Add a pull-up resistor to SCL and SDA, the resistance is generally around 4.7KΩ

figure 1figure 2
image.png

  • One master and multiple slaves: The CPU is a single-chip microcomputer, as the master of the bus, including full control of the SCL line. The master is in full control of the SCL line at all times. In addition, in the idle state, the master can actively initiate control of SDA. Only when the slave sends data and the slave responds, the master will transfer the control of SDA to the slave. This is the power of the master.
  • The controlled IC is a slave mounted on the I2C bus, which can be a gesture sensor, OLED, memory, clock module, etc. The slave has relatively little power. For the SCL clock line, it can only passively read at any time, and the slave is not allowed to control the SCL line. For the SDA data line, the slave is not allowed to actively initiate control of SDA. Only after the host sends a command to read the slave, or when the slave responds, can the slave briefly obtain control of SDA.
  • Figure 2: SCL is on the left and SDA is on the right. All data can be input through a data buffer or a Schmitt trigger.
    • Because the input has no effect on the circuit, any device can input at any time.
    • The output adopts the open-drain output configuration. When the output is low level, the switch tube is turned on, and the pin is directly grounded, which is a strong pull-down; when the output is high level, the switch tube is turned off, and the pin is not connected to anything and is in a floating state. In this way, all devices can only output low level but not high level. In order to avoid the floating caused by high level, it is necessary to have an external pull-up resistor for SCL and SDA outside the bus. It is pulled to a high level through a resistor, so it is a weak pull-up. In this way, first, the power supply short circuit phenomenon is completely eliminated to ensure the safety of the circuit; second, the frequent switching of the pin mode is avoided. In the open-drain mode, outputting a high level is equivalent to disconnecting the pin, so before input, a high level can be directly output. Third, this mode has a "line and" phenomenon. As long as any one or more devices output a low level, the bus is at a low level. Only when all devices output a high level, the bus is at a high level. Therefore, I2C can use this phenomenon to perform clock synchronization and bus arbitration in multi-host mode. Therefore, although SCL can use push-pull output in the one-master-multiple-slave mode, it still adopts the open-drain plus pull-up output mode.

1.3 I2C Timing Basic Unit

1.3.1 Starting and ending conditions

  • Starting conditions: During the SCL high level, SDA switches from high level to low level
  • Termination condition: During the SCL high level period, SDA switches from low level to high level

image.png

  • In the start condition:When the I2C bus is in an idle state, both SCL and SDA are in a high-level state, that is, no device touches SCL and SDA. SCL and SDA are pulled up to a high level by external pull-up resistors, and the bus is in a quiet high-level state. When the host needs to send and receive data, it must first break the tranquility of the bus and generate a start condition, that is, SCL is at a high level and does not touch it, and then pull SDA down to generate a falling edge. When the slave captures the SCL high level and SDA falling edge signal, it will reset itself and wait for the host to call. After the falling edge of SDA, the host must pull SCL down again. Pulling down SCL not only occupies the bus, but also facilitates the splicing of the basic units. That is, it will be guaranteed that, in addition to the start and end conditions, the SCL of each timing unit starts and ends with a low level.
  • Termination condition:SCL is released first and rebounds to a high level, and SDA is released again and rebounds to a high level, generating a rising edge, which triggers the termination condition. At the same time, after the termination condition, both SCL and SDA are high levels, returning to the initial calm state.
    The start and stop are generated by the host, and the slave is not allowed to generate the start and stop. Therefore, when the bus is idle, the slave must always let go of both hands and is not allowed to actively jump out and touch the bus.

1.3.2 Sending a Byte

  • Send a byte: During the SCL low level period, the host puts the data bits on the SDA line in sequence (high bit first), and then releases SCL. The slave will read the data bits during the SCL high level period, so no data changes are allowed on SDA during the SCL high level period. The above process is repeated 8 times in sequence to send a byte.

The host puts data at a low level, and reads data from the slave at a high level
image.png
After the start condition, the first byte must also be sent by the host. SCL is low, and the host wants to send 0, so it pulls SDA down to a low level; if it wants to send 1, it lets go, and SDA rebounds to a high level. During the low level of SCL, the level of SDA is allowed to change. After the data is placed, the host lets go of the clock line, and SCL rebounds to a high level. During the high level period, it is when the slave reads SDA, so during the high level period, SDA is not allowed to change. After SCL is at a high level, the slave needs to read SDA as soon as possible. Generally, the slave has completed the reading at the rising edge of SCL. Because the clock is controlled by the host, the slave does not know when the falling edge occurs, so the slave will read the data at the rising edge of SCL. When the host lets go of SCL for a period of time, it can continue to pull SCL down and transmit the next bit. The host also needs to put the data on SDA as soon as possible after the falling edge of SCL. But the host has the dominant power of the clock, so it only needs to put the data on SDA at any time of the low level. After the data is released, the host releases SCL again. When SCL is high, the slave reads this bit. This process is repeated: the host pulls SCL low and puts the data on SDA. The host releases SCL and the slave reads the SDA data. Under the synchronization of SCL, the host sends and the slave receives in turn. After 8 cycles, 8 bits of data are sent, which is a byte.
Since the high bit comes first, the first bit is the highest bit B7 of a byte, and the lowest bit B0 is sent last.

1.3.3 Receiving a Byte

  • Receive a byte: During the SCL low level period, the slave puts the data bits on the SDA line in sequence (high bit first), and then releases SCL. The host will read the data bits during the SCL high level period, so SDA is not allowed to have data changes during the SCL high level period. The above process is repeated 8 times in sequence to receive a byte (the host needs to release SDA before receiving)

The slave device puts data at a low level, and the host device reads data at a high level
image.png
SDA line: The host must release SDA before receiving. At this time, the slave obtains the control of SDA. If the slave needs to send 0, it pulls SDA low. If the slave needs to send 1, it lets go and SDA rebounds to a high level. Low level converts data, and high level reads data. The solid line indicates the level controlled by the host, and the dotted line indicates the level controlled by the slave. SCL is controlled by the host throughout the process. The SDA host must release it before receiving and let the slave control it. Because the SCL clock is controlled by the host, the data conversion of the slave is basically carried out along the falling edge of SCL, and the host can read at any time when SCL is high.

1.3.4 Sending and Receiving Responses

  • Send Reply:After receiving a byte, the host sends a bit of data in the next clock. Data 0 indicates a response, and data 1 indicates a non-response
  • Receive Response:After the host sends a byte, it receives a bit of data in the next clock to determine whether the slave responds. Data 0 indicates response, and data 1 indicates non-response (the host needs to release SDA before receiving)

image.png
That is, after calling the timing of sending a byte, the timing of receiving the response should be called immediately to determine whether the slave has received the data just given to it. If the slave has received it, then when the host releases SDA at the response bit, the slave should immediately pull down SDA, and then the host reads the response bit during the high level of SCL. If the response bit is 0, it means that the slave has indeed received it.
When receiving a byte, it is necessary to call the send response. The purpose of sending the response is to tell the slave whether you want to continue sending. If the slave sends a data and gets a response from the host, the slave will continue to send. If the slave does not get a response from the host, the slave will think that it has sent a data, but the host ignores it, maybe the host does not want it. At this time, the slave will obediently release SDA and hand over the control of SDA to prevent interference with the subsequent operations of the host.

1.4 I2C Timing

1.4.1 Write to a specified address

  • Write to the specified address
  • For the specified device (Slave Address), write the specified data (Data) at the specified address (Reg Address) (i.e. the register address of the specified device)

image.png
process:
(1) Starting conditions
(2) Send a byte sequence—0xD0 (slave address (7bit) + write (1bit)-0) (1101 0000)
(3) Receive response: RA = 0 (receive response from slave)
(4) Specified address: 0x19 (0001 1001)
(5) Receive response: RA = 0 (receive response from slave)
(6) Write specified data: 0xAA (1010 1010)
(7) Receive response: RA = 0
(8) Stop bit P (termination condition)

  • After the start condition, a byte must be sent. The content of the byte must be the slave address + read/write bit. The slave address is 7 bits, and the read/write bit is 1 bit, which is exactly 8 bits. Sending the slave address is to determine the communication object, and sending the read/write bit is to confirm whether to write or read next. Now the host has sent a data. The content of the byte is converted to hexadecimal, with the high bit first, which is 0xD0. The unit that follows is the response bit (RA) of the receiving slave. After the 8th read/write bit ends and SCL is pulled low, the host must release SDA, and then the response bit RA.
  • The high level after the response bit RA ends is generated by the slave releasing SDA. The slave hands over the control of SDA. Because the slave wants to exchange data as soon as possible when the SCL level is low, the rising edge of SDA and the falling edge of SCL occur almost at the same time.
  • After the response is completed, if you continue to send one byte, the second byte can be sent to the specified device. The slave device can define the purpose of the second byte and subsequent bytes. Generally, the second byte can be a register address or an instruction control word, and the third byte is the content that the host wants to write to the register address (the second byte).
  • P is the stop bit.

The purpose of this data frame is to write the data 0xAA into the register at the internal address 0x19 of the device with the specified slave address 1101000.
0 means: the host will perform a write operation at the next timing;
1 means: the host will perform a read operation in the following sequence;

1.4.2 Current address read

  • Current address read
  • For the specified device (Slave Address), read the slave data (Data) at the address indicated by the current address pointer

image.png
process:
(1) Starting conditions
(2) Timing of sending a byte—0xD1 (slave address (7bit) + read (1bit)-1) (1101 0001)
(3) Receive response: RA = 0 (receive response from slave)
(4) Read slave data: 0x0F (0000 1111)
(7) Send response: SA = 0
(8) Stop bit P (termination condition)

  • The read/write bit is 1, indicating that the next step is to perform a read operation. After the slave responds (RA=0), the data transmission direction will be reversed. The host will hand over the control of SDA to the slave, and the host will call the timing of receiving a byte to perform the receiving operation.
  • In the second byte, the slave is allowed by the host to write to SCL during the low level of SCL, and the host reads SDA during the high level of SCL. Finally, the host reads 8 bits in sequence during the high level of SCL, and receives a byte of data sent by the slave, that is, 0x0F. But which register of the slave is 0x0F? In the read sequence, the I2C protocol stipulates that when the host is addressing, once the read and write flag is 1. The next byte will immediately turn to the read sequence. Therefore, the host has no time to specify which register it wants to read before it starts receiving, so there is no link to specify the address here. In the slave, all registers are allocated to a linear area, and there will be a separate pointer variable indicating one of the registers. This pointer is powered on by default, generally pointing to address 0, and after each byte is written and read, the pointer will automatically increment once and move to the next position. Then, when calling the current address read sequence, the host does not specify which address to read, and the slave will return the value of the register pointed to by the current pointer.

1.4.3 Reading from a specified address

  • Specified address read
  • For the specified device (Slave Address), read the slave data (Data) at the specified address (Reg Address)

image.png
Start, repeat, then stop
process:
(1) Starting conditions
(2) Send a byte sequence—0xD0 (slave address (7bit) + write (1bit)-0) (1101 0000)
(3) Receive response: RA = 0 (receive response from slave)
(4) Specified address: 0x19 (0001 1001)
(5) Receive response: RA = 0 (receive response from slave)
(6) Repeat the start condition
(7) Timing of sending a byte—0xD1 (slave address (7bit) + read (1bit)-1) (1101 0001)
(8) Receive response: RA = 0
(9) Read slave data: 0xAA (1010 1010)
(10) Send response: SA = 0
(11) Stop bit P (termination condition)

  • The first part is to write to the specified address, but only the address has been specified and there is no time to write; the second part is to read from the current address, because the address has just been specified, so the current address is called to read again.
  • The designated slave address is 1101000, the read/write flag is 0, and a write operation is performed. After the slave responds, another byte (the second byte) is written to specify the address. 0x19 is written into the address pointer of the slave. That is to say, after the slave receives the data, its register pointer points to the position 0x19.
  • Sr is a repeated start condition, which is equivalent to starting another timing sequence. Because the specified read and write flag can only follow the first byte of the start condition, if you want to switch the read and write direction, you can only have another start condition.
  • Then after the start condition, the address is re-set and the read/write flag is specified. At this time, the read/write flag is 1, indicating that it is to be read. Then the host receives a byte, which is the data 0xAA at the address 0x19.

2. MPU6050

2.1 Introduction to MPU6050

  • MPU6050 is a 6-axis attitude sensor that can measure the acceleration and angular velocity parameters of the chip's own X, Y, and Z axes. Through data fusion, the attitude angle (Euler angle) can be further obtained. It is often used in scenarios such as balancing cars and aircraft that need to detect their own attitude.
  • 3-axis accelerometer: measures acceleration on the X, Y, and Z axes
  • 3-axis gyroscope sensor (Gyroscope): measures the angular velocity of the X, Y, and Z axes

image.png

  • Taking the fuselage of an aircraft as an example, the Euler angle is the angle between the fuselage of the aircraft and the initial three axes.
    • airplaneNose down or up, the angle between these axes is calledPitch
    • airplaneThe fuselage rolls left or right, the angle between these axes is calledRoll
    • airplaneKeep the fuselage levelThe nose turns left or right, the angle between these axes is calledYaw
    • The Euler angle indicates the attitude of the aircraft at this moment, whether it is tilted up or down, tilted to the left or to the right.
  • Common data fusion algorithms generally include complementary filtering, Kalman filtering, etc., and attitude solution in inertial navigation.
  • Accelerometer:The dotted line in the middle is the sensing axis. In the middle is a small slider with a certain mass that can slide left and right, with a spring on each side supporting it. When the slider moves, it will drive the potentiometer on it to move. This potentiometer is a voltage divider resistor. By measuring the voltage output by the potentiometer, you can get the acceleration value of the small slider. This accelerometer is actually a spring dynamometer. According to Newton's second law, F = ma. If you want to measure this acceleration a, you can find an object of unit mass and measure the force F. There is an accelerometer on the X, Y, and Z axes respectively. The accelerometer has static stability but not dynamic stability.
  • Gyroscope sensor:In the middle is a rotating wheel with a certain mass. When the rotating wheel rotates at high speed, according to the principle of conservation of angular momentum, the rotating wheel has a tendency to maintain its original angular momentum, and this tendency can keep the direction of the rotation axis unchanged. When the direction of the external object rotates, the direction of the internal rotation axis does not rotate, which will cause an angle deviation at the connection of the balance ring. If a rotating potentiometer is placed at the connection and the voltage of the potentiometer is measured, the angle of rotation can be obtained. The gyroscope should be able to get the angle directly, but the gyroscope of this MPU6050 cannot measure the angle directly. It measures the angular velocity, that is, the angular velocity of the chip rotating around the X-axis, Y-axis and Z-axis. The integral of the angular velocity is the angle, but when the object is stationary, the angular velocity value cannot be completely zeroed due to noise, and then after continuous accumulation of integral, this small noise will cause the calculated angle to drift slowly, that is, the angle obtained by the integral of the angular velocity cannot withstand the test of time, but this angle is fine whether it is stationary or moving, and will not be affected by the movement of the object. The gyroscope has dynamic stability but not static stability.
  • According to the accelerometer has static stability but not dynamic stability, and the gyroscope has dynamic stability but not static stability, these two characteristics can be combined to obtain a statically and dynamically stable attitude angle by taking advantage of their strengths and making up for their weaknesses and performing complementary filtering.

2.2 MPU6050 parameters

  • 16-bit ADC collects the analog signal of the sensor, quantization range: -32768~32767
  • Accelerometer full scale selection: ±2, ±4, ±8, ±16 (g) (1g = 9.8m/s2)
  • Gyroscope full-scale selection: ±250, ±500, ±1000, ±2000 (°/sec, degrees/second, angular velocity unit, how many degrees it rotates per second) (the larger the full-scale selection, the wider the measurement range, the smaller the full-scale selection, the higher the measurement resolution)
  • Configurable digital low-pass filter: Registers can be configured to select low-pass filtering of the output data.
  • Configurable clock source
  • Configurable sampling frequency division: The clock source can provide clock for AD conversion and other internal circuits through the frequency division of the frequency divider. By controlling the frequency division coefficient, the speed of AD conversion can be controlled.
  • I2C slave address: 1101000 (AD0=0) or 1101001 (AD0=1)
    • 110 1000 converted to hexadecimal is 0x68, so some people say that the slave address of MPU6050 is 0x68. But in I2C communication, the upper 7 bits of the first byte are the slave address, and the lowest bit is the read-write bit. So if you think 0x68 is the slave address, when sending the first byte, you must first shift 0x68 left by 1 bit (0x68 << 1), and then bitwise OR the read-write bit, read 1 and write 0.
    • Another way is to use the data after 0x68 is shifted left by 1 bit (0x68 << 1) as the slave address, which is 0xD0. In this way, the slave address of MPU6050 is 0xD0. At this time, when actually sending the first byte, if you want to write, just use 0xD0 as the first byte; if you want to read, use 0xD0 or 0x01 (0xD0 | 0x01), that is, 0xD1 as the first byte. This representation does not require the left shift operation, or this representation integrates the read and write bits into the slave address. 0xD0 is the write address, and 0xD1 is the read address.

2.3 Hardware Circuit

image.png

PinoutFunction
VCC、GNDpower supply
SCL、SDAI2C Communication Pins
XCL、XDAHost I2C communication pin
AD0The lowest bit of the slave address
INTInterrupt signal output
  • LDO: Low dropout linear regulator, 3.3V voltage regulation.
  • SCL and SDA: They are the pins for I2C communication. The module has built-in two 4.7K pull-up resistors, so when wiring, just connect SDA and SCL to the GPIO port directly, and no external pull-up resistors are needed.
  • XCL, XDA: Host I2C communication pins, these two pins are designed to expand the chip function. Usually used for external magnetometers or barometers. When these expansion chips are connected, the host interface of MPU6050 can directly access the data of these expansion chips and read the data of these expansion chips into MPU6050. MPU6050 has a DMP unit for data fusion and attitude solution.
    AD0 pin: It is the lowest bit of the slave address. If it is connected to a low level, the 7-bit slave address is 1101000; if it is connected to a high level, the 7-bit slave address is 1101001. There is a resistor in the circuit diagram, which is weakly pulled down to a low level by default, so if the pin is left floating, it is a low level. If you want to connect it to a high level, you can directly lead AD0 to VCC and strongly pull it up to a high level.
  • INT: interrupt output pin. You can configure some events inside the chip to trigger the output of the interrupt pin, such as data ready, I2C host error, etc.
  • The chip also has built-in free fall detection, motion detection, zero motion detection, etc. These signals can trigger the INT pin to generate a level jump, and the interrupt signal can be configured if necessary.
  • The power supply of MPU6050 chip is 2.375-3.46V, which is a 3.3V powered device and cannot be directly connected to 5V. Therefore, a 3.3V regulator is added, and the input voltage VCC_5V can be between 3.3V and 5V, and then the 3.3V regulator outputs a stable 3.3V voltage to power the chip. As long as there is power at the 3.3V end, the power indicator light will be on.

2.4 MPU6050 Block Diagram

image.png

  • CLKIN and CLKOUT are the clock input pin and clock output pin, but we generally use the internal clock.
  • Gray part: It is the sensor inside the chip, including the accelerometer of XYZ axis and the gyroscope of XYZ axis.
  • There is also a built-in temperature sensor that can be used to measure the temperature.
  • These sensors are essentially equivalent to variable resistors. After voltage division, they output analog voltages, which are then converted to digital using the ADC. After the conversion is complete, the data from these sensors are uniformly placed in the data register, and the value measured by the sensor can be obtained by reading the data register. The conversion inside this chip is fully automatic.
  • Each sensor has a self-test unit, which is used to verify the chip. When the self-test is started, the chip will simulate an external force on the sensor. This external force causes the sensor data to be larger than usual. Self-test process: You can enable self-test first, read the data, then enable self-test again, read the data, and subtract the two data. The data obtained is called the self-test response. For this self-test response, the manual gives a range. If it is within this range, it means that the chip is fine.
  • Charge Pump: It is a charge pump or charging pump. A charge pump is a voltage boost circuit.
  • The CPOUT pin requires an external capacitor.
  • Interrupt status register: can control which internal events are output to the interrupt pin,
  • FIFO: First-in, first-out register, which can cache data streams.
  • Configuration register: can configure each internal circuit
  • Sensor register: that is, data register, which stores the data of each sensor.
  • Factory Calibrated: This means that the internal sensors are calibrated.
  • Digital Motion Processor: DMP for short, is a hardware algorithm for attitude calculation inside the chip. It can perform attitude calculation in conjunction with the official DMP library.
  • FSYNC: frame synchronization.

3. 10-1 Software I2C Read and Write MPU6050

3.1 Hardware Connection

Through software I2C communication, read and write the registers inside the MPU6050 chip, write to the configuration register, and then configure the plug-in module. Read the data register to get the data of the plug-in module. The read data will be displayed on the OLED. The top data is the device ID number. The ID number of this MPU6050 is fixed to 0x68. Below, the 3 on the left are the output data of the acceleration sensor, which are the acceleration of the X-axis, Y-axis, and Z-axis, respectively. The 3 on the right are the output data of the gyroscope sensor, which are the angular velocity of the X-axis, Y-axis, and Z-axis, respectively.
SCL is connected to the PB10 pin of STM32, and SDA is connected to the PB11 pin. Since the level is flipped by software here, you can connect any two GPIO ports.

3.2 Operation Results

IMG_20240406_155156.jpg

3.3 Code Flow

STM32 is the host and MPU6050 is the slave, which is a master-slave mode.

  1. Build the .c and .h modules of the I2C communication layer
    1. Write the GPIO initialization of I2C bottom layer
    2. 6 basic timing units: start, stop, send a byte, receive a byte, send response, receive response
  2. Build the .c and .h modules of MPU6050
    1. Based on the I2C communication module, it can realize the specified address reading, the specified address writing, and then write registers to configure the chip and read registers to obtain sensor data.
  3. main.c
    1. Call the MPU6050 module, initialize, get data, and display data

3.4 Code

  1. I2C Code:
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}

uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

void MyI2C_Init(void)
{
/*
软件I2C初始化:
	1. 把SCL和SDA都初始化为开漏输出模式;
	2. 把SCL和SDA置高电平;
输入时,先输出1,再直接读取输入数据寄存器就行了;
初始化结束后,调用SetBits,把GPIOB的Pin_10和Pin_11都置高电平,
此时I2C总线处于空闲状态
*/	
	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);
	
}

/*
起始条件:SCL高电平期间,SDA从高电平切换到低电平。
如果起始条件之前,SDA和SCL都已经是高电平了,那先释放哪一个是一样的效果。
但是这个Start还要兼容重复起始条件Sr,Sr最开始,SCL是低电平,SDA电平不敢确定,
所以为保险起见,在SCL低电平时,先确保释放SDA,再释放SCL。
这时SDA和SCL都是高电平,然后再拉低SDA、拉低SCL。
这样这个Start就可以兼容起始条件和重复起始条件了。
*/
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}

/*
终止条件:SCL高电平期间,SDA从低电平切换到高电平
如果Stop开始时,SCL和SDA都已经是低电平了,那就先释放SCL,再释放SDA。
但在这个时序单元开始时,SDA并不一定是低电平,所以为了确保之后释放
SDA能产生上升沿,要在时序单元开始时,先拉低SDA,然后再释放SCL、释放SDA。
*/
void MyI2C_Stop(void)// 终止条件
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

/*
发送一个字节:发送一个字节时序开始时,SCL是低电平。
除了终止条件SCL以高电平结束,所有的单元都会保证SCL以低电平结束。
SCL低电平变换数据;高电平保持数据稳定。由于是高位先行,所以变换数据的时候,
按照先放最高位,再放次高位,...,最后最低位的顺序,依次把每一个字节的每一位放在SDA线上,
每放完一位后,执行释放SCL,拉低SCL的操作,驱动时钟运转。
程序:趁SCL低电平,先把Byte的最高位放在SDA线上,
*/

void MyI2C_SendByte(uint8_t Byte) // 发送一个字节
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));// 右移i位
	    MyI2C_W_SCL(1);
	    MyI2C_W_SCL(0);
	}
}

/*
接收一个字节:时序开始时,SCL低电平,此时从机需要把数据放到SDA上,
为了防止主机干扰从机写入数据,主机需要先释放SDA,释放SDA相当于切换为输入模式,
那在SCL低电平时,从机会把数据放到SDA上,如果从机想发1,就释放SDA,想发0,就拉低SDA,
主机释放SCL,在SCL高电平期间,读取SDA,再拉低SCL,低电平期间,从机就会把下一位数据放到SDA上,重复8次,
主机就能读到一个字节了。
SCL低电平变换数据,高电平读取数据,实际上是一种读写分离的操作,低电平时间定义为写的时间,高电平时间定义为读的时间,

*/
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() == 1) // 如果if成立,接收的这一位为1,
	    {
		    Byte |= (0x80 >> i);   // 最高位置1
	    }
        MyI2C_W_SCL(0);	
	}
	return Byte;
}
/*
问题:反复读取SDA,for循环中又没写过SDA,那SDA读出来应该始终是一个值啊?
回答:I2C是在进行通信,通信是有从机的,当主机不断驱动SCL时钟时,
从机就有义务去改变SDA的电平,所以主机每次循环读取SDA的时候,
这个读取到的数据是从机控制的,这个数据也正是从机想要给我们发送的数据,
所以这个时序叫做接收一个字节。
*/

void MyI2C_SendAck(uint8_t AckBit) // 发送应答
{
	// 函数进来,SCL低电平,主机把AckBit放到SDA上,
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);  // 从机读取应答
	MyI2C_W_SCL(0);  // 进入下一个时序单元
	
}

uint8_t MyI2C_ReceiveAck(void) // 接收应答
{
	// 函数进来,SCL低电平,主机释放SDA,防止从机干扰
	uint8_t AckBit;
	MyI2C_W_SDA(1);  // 主机释放SDA
	MyI2C_W_SCL(1);  // SCL高电平,主机读取应答位
	AckBit = MyI2C_R_SDA(); 
	MyI2C_W_SCL(0);	 // SCL低电平,进入下一个时序单元
	return AckBit;
}

/*问题:在程序里,主机先把SDA置1了,然后再读取SDA,
这应答位肯定是1啊,
回答:第一,I2C的引脚是开漏输出+弱上拉的配置,主机输出1,
并不是强制SDA为高电平,而是释放SDA,
第二,I2C是在通信,主机释放了SDA,从机是有义务在此时把SDA再拉低的,
所以,即使主机把SDA置1了,之后再读取SDA,读到的值也可能是0,
读到0,代表从机给了应答,读到1,代表从机没给应答,这就是接收应答的流程。


*/

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  1. MPU6050 code:
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

// 宏定义: 寄存器的名称   对应的地址

#define	MPU6050_SMPLRT_DIV		0x19  // 采样率分频
#define	MPU6050_CONFIG			0x1A  // 配置外部帧同步(FSYNC)引脚采样和数字低通滤波器(DLPF)设置
#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  // 电源管理寄存器1
#define	MPU6050_PWR_MGMT_2		0x6C  // 电源管理寄存器2
#define	MPU6050_WHO_AM_I		0x75  // 用于验证设备身份

#endif

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

// 宏定义:从机地址
#define MPU6050_ADDRESS  0xD0

// 指定地址写
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);// 发送从机地址后,接收应答
	MyI2C_ReceiveAck();// 寻址找到从机之后,继续发送下一个字节
	MyI2C_SendByte(RegAddress); // 指定寄存器地址,存在MPU6050的当前地址指针里,用于指定具体读写哪个寄存器
	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); // 指定地址:就是设置了MPU6050的当前地址指针
	MyI2C_ReceiveAck();
	// 转入读的时序,重新指定读写位,就必须重新起始
	MyI2C_Start();// 重复起始条件
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);// 指定从机地址和读写位,0xD0是写地址,或上0x01变为0xD1,读写位为1,接下来要读从机的数据
	MyI2C_ReceiveAck(); // 接收应答后,总线控制权就正式交给从机了,从机开始发送一个字节
	Data = MyI2C_ReceiveByte();// 主机接收一个字节,该函数返回值就是接收到的数据
	// 主机接收一个字节后,要给从机发送一个应答
	MyI2C_SendAck(1);// 参数为0,就是给从机应答,参数给1,就是不给从机应答
	// 如果想继续读多个字节,就要给应答,从机收到应答之后,就会继续发送数据,如果不想继续读了,就不能给从机应答了。
	// 主机收回总线的控制权,防止之后进入从机以为你还想要,但你实际不想要的冲突状态,
	// 这里,只需要读取1个字节,所以就给1,不给从机应答,
	MyI2C_Stop();
	return Data;
}

void MPU6050_Init(void)
{
	MyI2C_Init();
	// 写入一些寄存器对MPU6050硬件电路进行初始化配置
	// 电源管理寄存器1:设备复位:0,不复位;睡眠模式:0,解除睡眠:循环模式:0,不循环;无关位i:0;温度传感器失能:0,不失能;最后三位选择时钟:000,选择内部时钟,001,选择x轴的陀螺仪时钟,
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);// 解除睡眠,选择陀螺仪时钟
	// 电源管理寄存器2:前两位,循环模式唤醒频率:00,不需要;后6位,每一个轴的待机位:全为0,不需要待机;
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); // 均不待机
	// 采样率分频:该8位决定了数据输出的快慢,值越小越快
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);// 采样分频:10分频
	// 配置寄存器:外部同步:全为0,不需要;数字低通滤波器:110,最平滑的滤波
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);// 滤波参数给最大
	// 陀螺仪配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位无关位:为0
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);// 陀螺仪和加速度计都选最大量程
	// 加速度计配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位高通滤波器:用不到,为000
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
		
}

// 获取芯片的ID号
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

// 获取寄存器数据的函数,返回6个int16_t的数据,分别表示XYZ的加速度值和陀螺仪值
// 指针地址传递的方法,返回多值
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;
	// 加速度计X
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL; // 高8位左移8位,再或上低8位,得到加速度计X轴的16位数据
	// 加速度计Y
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	// 加速度计Z
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	// 陀螺仪X
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	// 陀螺仪Y
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	// 陀螺仪Z
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyI2C.h"
#include "MPU6050.h"

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;// 接收XYZ轴的加速度值和陀螺仪值



int main(void)
{
	OLED_Init();
//	MyI2C_Init();
	MPU6050_Init();
//	
	OLED_ShowString(1,1,"ID:");
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1, 4, ID, 2);
	
//	// 指定地址写
//	MyI2C_Start(); // 产生起始条件,开始一次传输
//	// 主机首先发送一个字节,内容是从机地址+读写位,进行寻址
//	MyI2C_SendByte(0xD0);  // 1101 000 0,0代表即将进行写入操作
//	// 发送一个字节后,要接收一下应答位,看看从机有没有收到刚才的数据
//	uint8_t Ack = MyI2C_ReceiveAck();
//	// 接收应答之后,要继续发送一个字节,写入寄存器地址
//	MyI2C_Stop();
//	
//	OLED_ShowNum(1, 1, Ack, 3);
	
//	// 指定地址读
//	uint8_t ID = MPU6050_ReadReg(0X75);// 返回值是0x68
//	OLED_ShowHexNum(1, 1, ID, 2);
	
//	// 指定地址写,需要先解除睡眠模式,否则写入无效
//	// 睡眠模式是电源管理寄存器1的这一位SLEEP控制的,把该寄存器写入0x00,解除睡眠模式
//	// 该寄存器地址是0x6B
//	MPU6050_WriteReg(0x6B, 0x00);
//	// 采样率分频寄存器,地址是0x19,值的内容是采样分频
//	MPU6050_WriteReg(0x19, 0xAA);
//	
//	uint8_t ID = MPU6050_ReadReg(0X19);
//	OLED_ShowHexNum(1, 1, ID, 2);//显示0x19地址下的内容,应该是0xAA
	
	while(1)
	{
		MPU6050_GetData(&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, 8, GX, 5);
		OLED_ShowSignedNum(3, 8, GY, 5);
		OLED_ShowSignedNum(4, 8, GZ, 5);
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

4. I2C peripherals

4.1 Introduction to I2C Peripherals

  • The STM32 has an integrated hardware I2C transceiver circuit, which can automatically perform functions such as clock generation, start and end condition generation, response bit transmission and reception, and data transmission and reception by hardware, thus reducing the burden on the CPU.
  • Support multi-host model
  • Support 7-bit/10-bit address mode
  • Supports different communication speeds, standard speed (up to 100 kHz), fast speed (up to 400 kHz)
  • Support DMA
  • Compatible with SMBus protocol
  • STM32F103C8T6 hardware I2C resources: I2C1, I2C2

4.2 I2C Block Diagram

image.png

  • On the left are the communication pins: SDA and SCL; SMBALERT is for SMBus;
    Generally, the pins of peripherals are connected to the outside world by using the multiplexing mode of the GPIO port. (See the table)
  • The above is the data control part: SDA. The core part of data transmission and reception is the data register DR (DATA REGISTER) and the data shift register. When data needs to be sent, a byte of data can be written to the data register DR. When there is no data shift in the shift register, the value of the data register will be further transferred to the shift register. During the shifting process, the next data can be directly placed in the data register and wait. Once the previous data shift is completed, the next data can be seamlessly connected and continue to be sent. When the data is transferred from the data register to the shift register, the TXE bit of the status register will be set to 1, indicating that the transmit register is empty.
  • Receive: The input data is shifted from the pin to the shift register one by one. When a byte of data is received, the data is transferred from the shift register to the data register as a whole, and the flag RXNE is set, indicating that the receive register is not empty. At this time, the data can be read from the data register. As for when to receive and when to send, the corresponding bit of the control register needs to be written to operate. The start condition, end condition, response bit, etc. are completed through data control.
  • The comparator and address register are used in slave mode.
  • SCL: Clock control is used to control the SCL line. Write the corresponding bit in the clock control register, and the circuit will perform the corresponding function. Control logic circuits, write control registers to control the entire circuit. Read the status register to know the working status of the circuit.
  • When sending and receiving a lot of bytes, DMA can be used to improve efficiency.

4.3 I2C Basic Structure

image.png

  • SDA: Since I2C is high-bit first, this shift register is shifted to the left. When sending, the high bit is shifted out first, and then the second high bit. One SCL clock shifts once, and after 8 shifts, 8 bytes can be placed on the SDA line from high to low. When receiving, the data is shifted in from the right through the GPIO port, and finally shifted 8 times, one byte is received. The output data is output to the port through the GPIO port. The input data is input to the shift register through the GPIO port.
  • The GPIO port needs to be configured in multiplexed open-drain output mode; multiplexing means that the state of the GPIO port is controlled by the on-chip peripherals, and open-drain output is the port configuration required by the I2C protocol. Even in open-drain output mode, the GPIO port can also be input.
  • SCL: The clock controller controls the clock line through GPIO.
    image.png

4.4 Host sends

image.png
When the STM32 wants to execute a write to a specified address, it needs to follow the transmitter transmission sequence diagram.

  • 7-bit address: The byte after the start condition is the addressing
  • 10-bit address: The two bytes after the start condition are addressing. The first byte is the frame header, and the content is 5-bit flag 11110 + 2-bit address + 1 read-write bit; the second byte is a pure 8-bit address.
  • 7-bit flow: start, slave address, response, data, response, data, response... stop
  1. After initialization, the bus defaults to idle state, and the STM defaults to slave mode. In order to generate a start condition, the STM32 needs to write to the control register (CR1) and write 1. Then the STM32 switches from slave mode to master mode.

image.png

  1. The EV5 event can be regarded as a flag bit. SB is a bit in the status register, which indicates the status of the hardware. SB=1 means that the start condition has been sent.

image.png

  1. Then you can send a one-byte slave address. The slave address needs to be written into the data register DR. After writing into DR, the hardware circuit will automatically transfer the address byte to the shift register, and then send the byte to the I2C bus. After that, the hardware will automatically receive the response and judge. If there is no response, the hardware will set the response failure flag, and then the flag can apply for an interrupt to remind us.
  2. When addressing is completed, an EV6 event occurs and the ADDR flag is 1, which indicates the end of address sending in master mode.

image.png

  1. The EV8_1 event is when the TxE flag is 1, the shift register is empty, and the data register is empty. We need to write to the data register DR to send data. After writing to DR, since the shift register is empty, DR will immediately transfer to the shift register for sending. The EV8 event will be performed. The shift register is not empty and the data register is empty, which means the shift register is sending data. So here in the process, the timing of data 1 is generated. At this moment, data 2 will be written into the data register and wait. After receiving the response bit, the data bit will be transferred to the shift register for sending. At this time, the state is that the shift register is not empty and the data register is empty, so the EV8 event occurs again.
  2. After that, data 2 is being sent, but this time the next data has already been written to the data register and is waiting. Once the EV8 event is detected, the next data can be written.
  3. When the data to be sent is written, there is no new data written into the data register. When the current data shift of the shift register is completed, the shift register is empty and the data register is also empty, that is, the EV8_2 event. TxE=1 means the shift register is empty and the data register is empty. BTF: byte transmission end flag. When sending, when a new data is to be sent and the data register has not been written with new data. When EV8_2 is detected, the termination condition Stop can be generated. To generate the termination condition, obviously, there should be a corresponding bit in the control register to control it. In this way, the timing of the transmission is over.

4.5 Host Reception

image.png
7-bit master reception: start, slave address + read, receive response, receive data, send response... receive data, non-response, stop

  1. First, write the Start bit of the control register to generate a start condition, and then wait for the EV5 event (indicating that the start condition has been sent).
  2. After that, addressing is performed and a response is received. When finished, an EV6 event is generated (indicating that addressing is completed).
  3. Data 1 indicates that data is being input through the shift register.
  4. EV6_1 indicates that the data is still shifting. After receiving the response, it means that the shift register has successfully shifted in a byte of data 1. At this time, the shifted byte is transferred to the data register as a whole, and the RxNE flag is set at the same time, indicating that the data register is not empty, that is, a byte of data has been received. This state is the EV7 event, RxNE=1, and reading the DR register clears the event, which means that the data has been received. When we read the data, the event disappears.
  5. Of course, when data 1 has not been read out, data 2 can be directly shifted into the shift register. After that, the shift of data 2 is completed, data 2 is received, EV7 event is generated, data 2 is read out, and EV7 event disappears.
  6. When no more reception is needed, the ACK bit control register must be set to 0 in advance when the last timing unit occurs, and the termination condition request, that is, the EV7_1 event, will be set. After that, a non-response NA will be given. Since the STOP bit is set, a termination condition is generated.

4.6 Software/Hardware Waveform Comparison

image.png

image.png

5. 10-2 Hardware I2C reading and writing MPU6050

5.1 I2C Library Functions


void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 生成起始条件、终止条件
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 配置CR1的ACK这一位,0:无应答,1:应答
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 发送数据,把Data数据直接写入到DR寄存器
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
// 读取DR,接收数据
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);

// Address参数也是通过DR发送的,但在发送之前,设置了Address最低位的读写位,
// I2C_Direction不是发送,是把Address的最低位置1(读),否则最低位清0(写)
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

5.2 Hardware I2C Read and Write MPU6050 Implementation

5.2.1 Hardware Connection

SCL is connected to the PB10 pin of STM32, and SDA is connected to the PB11 pin. Since the level is flipped by software here, you can connect any two GPIO ports.
The data on the top of the OLED is the device ID number, and the ID number of this MPU6050 is fixed to 0x68. Below, the 3 on the left are the output data of the acceleration sensor, which are the acceleration of the X-axis, Y-axis, and Z-axis, and the 3 on the right are the output data of the gyroscope sensor, which are the angular velocity of the X-axis, Y-axis, and Z-axis.

5.2.2 Operation Results

IMG_20240406_172128.jpg

5.2.3 Code Implementation Process

  1. Configure I2C peripherals, initialize I2C peripherals, and replace MyI2C_Init
    (1) Turn on the clock of the I2C peripheral and the corresponding GPIO port,
    (2) Initialize the GPIO port corresponding to the I2C peripheral to multiplexed open-drain mode
    (3) Use the structure to configure the entire I2C
    (4) I2C_Cmd, enable I2C
  2. Control peripheral circuits to achieve the timing of writing to the specified address and replace WriteReg
  3. Control peripheral circuits to achieve the timing of reading the specified address and replace ReadReg

5.2.4 Code

  1. MPU6050 code:
#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"

/*
1. 配置I2C外设,对I2C外设进行初始化,替换MyI2C_Init
   (1)开启I2C外设和对应GPIO口的时钟,
   (2)把I2C外设对应的GPIO口初始化为复用开漏模式
   (3)使用结构体,对整个I2C进行配置
   (4)I2C_Cmd,使能I2C
2. 控制外设电路,实现指定地址写的时序,替换WriteReg
3. 控制外设电路,实现指定地址读的时序,替换ReadReg
*/


// 宏定义:从机地址
#define MPU6050_ADDRESS  0xD0
 
// 超时退出
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t TimeOut;
	TimeOut = 10000;
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) 
	{   
		TimeOut --;
		if (TimeOut == 0)
		{
			break;// 跳出循环,直接执行后面的程序
		}
	}
}

// 指定地址写:发送器传送时序
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);// 发送从机地址后,接收应答
//	MyI2C_ReceiveAck();// 寻址找到从机之后,继续发送下一个字节
//	MyI2C_SendByte(RegAddress); // 指定寄存器地址,存在MPU6050的当前地址指针里,用于指定具体读写哪个寄存器
//	MyI2C_ReceiveAck();
//	MyI2C_SendByte(Data);// 指定写入指定寄存器地址下的数据
//	MyI2C_ReceiveAck();
//	MyI2C_Stop();
	
	I2C_GenerateSTART(I2C2, ENABLE); // 起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //EV5事件
	
	// 发送从机地址,接收应答。该函数自带了接收应答,如果应答错误,硬件会通过标志位和中断来提示我们
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //EV6事件
	
	// 直接写入DR,发送数据
	I2C_SendData(I2C2, RegAddress);
	// 写入了DR,DR立刻转移到移位寄存器进行发送,EV8事件出现的非常快,基本不用等。因为有两级缓存,
	// 第一个数据写进DR了,会立刻跑到移位寄存器,这时不用等第一个数据发完,第二个数据就可以写进去等着了。
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //EV8事件
	
	I2C_SendData(I2C2, Data);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //EV8_2事件
	
	I2C_GenerateSTOP(I2C2, ENABLE);
}

// 指定地址读:接收器传送序列
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);
//	MyI2C_ReceiveAck();
//	MyI2C_SendByte(RegAddress); // 指定地址:就是设置了MPU6050的当前地址指针
//	MyI2C_ReceiveAck();
//	// 转入读的时序,重新指定读写位,就必须重新起始
//	MyI2C_Start();// 重复起始条件
//	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);// 指定从机地址和读写位,0xD0是写地址,或上0x01变为0xD1,读写位为1,接下来要读从机的数据
//	MyI2C_ReceiveAck(); // 接收应答后,总线控制权就正式交给从机了,从机开始发送一个字节
//	Data = MyI2C_ReceiveByte();// 主机接收一个字节,该函数返回值就是接收到的数据
//	// 主机接收一个字节后,要给发送从机一个应答
//	MyI2C_SendAck(1);// 参数为0,就是给从机应答,参数给1,就是不给从机应答
//	MyI2C_Stop();
	
	
	I2C_GenerateSTART(I2C2, ENABLE); // 起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //EV5事件
	
	// 发送从机地址,接收应答。该函数自带了接收应答,如果应答错误,硬件会通过标志位和中断来提示我们
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //EV6事件
	
	// 直接写入DR,发送数据
	I2C_SendData(I2C2, RegAddress);
	// 写入了DR,DR立刻转移到移位寄存器进行发送,EV8事件出现的非常快,基本不用等。因为有两级缓存,
	// 第一个数据写进DR了,会立刻跑到移位寄存器,这时不用等第一个数据发完,第二个数据就可以写进去等着了。
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //EV8_2事件
	
	I2C_GenerateSTART(I2C2, ENABLE);// 重复起始条件
	
	// 主机接收
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //EV5事件
	// 接收地址
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); // 函数内部就自动将该地址MPU6050_ADDRESS的最低位置1
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //EV6事件
	
	// 在最后一个数据之前就要把应答位ACK置0,同时把停止条件生成位STOP置1
	I2C_AcknowledgeConfig(I2C2, DISABLE);
	I2C_GenerateSTOP(I2C2, ENABLE);
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); //EV7事件
	// 等EV7事件产生后,一个字节的数据就已经在DR里面了。
	// 读取DR就可拿出该字节
	Data = I2C_ReceiveData(I2C2); // 返回值就是DR的数据
	// 在接收函数的最后,要恢复默认的ACK = 1。
	// 默认状态下ACK就是1,给从机应答,在收最后一个字节之前,临时把ACK置0,给非应答,
	// 所以在接收函数的最后,要恢复默认的ACK = 1,这个流程是为了方便指定地址收多个字节。
	I2C_AcknowledgeConfig(I2C2, ENABLE);
	
	return Data;
}

void MPU6050_Init(void)
{
	
//	MyI2C_Init();
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // 模式
	I2C_InitStructure.I2C_ClockSpeed = 50000; // 时钟速度,最大400kHz的时钟频率
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;	// 时钟占空比,只有在时钟频率大于100kHz,也就是进入到快速状态时才有用,小于100kHz,占空比是固定的1:1,
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // STM32作为从机,可以响应几位的地址
	I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 自身地址1,也是作为从机使用,
	I2C_Init(I2C2, &I2C_InitStructure); 
	
	I2C_Cmd(I2C2,ENABLE);
	
	// 写入一些寄存器对MPU6050硬件电路进行初始化配置
	// 电源管理寄存器1:设备复位:0,不复位;睡眠模式:0,解除睡眠:循环模式:0,不循环;无关位i:0;温度传感器失能:0,不失能;最后三位选择时钟:000,选择内部时钟,001,选择x轴的陀螺仪时钟,
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);// 解除睡眠,选择陀螺仪时钟
	// 电源管理寄存器2:前两位,循环模式唤醒频率:00,不需要;后6位,每一个轴的待机位:全为0,不需要待机;
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); // 均不待机
	// 采样率分频:该8位决定了数据输出的快慢,值越小越快
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);// 采样分频:10分频
	// 配置寄存器:外部同步:全为0,不需要;数字低通滤波器:110,最平滑的滤波
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);// 滤波参数给最大
	// 陀螺仪配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位无关位:为0
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);// 陀螺仪和加速度计都选最大量程
	// 加速度计配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位高通滤波器:用不到,为000
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
		
}

// 获取芯片的ID号
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

// 获取寄存器数据的函数,返回6个int16_t的数据,分别表示XYZ的加速度值和陀螺仪值
// 指针地址传递的方法,返回多值
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;
	// 加速度计X
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL; // 高8位左移8位,再或上低8位,得到加速度计X轴的16位数据
	// 加速度计Y
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	// 加速度计Z
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	// 陀螺仪X
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	// 陀螺仪Y
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	// 陀螺仪Z
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  1. main.c
#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;// 接收XYZ轴的加速度值和陀螺仪值



int main(void)
{
	OLED_Init();
	MPU6050_Init();
	
	OLED_ShowString(1,1,"ID:");
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1, 4, ID, 2);
	
//	// 指定地址写
//	MyI2C_Start(); // 产生起始条件,开始一次传输
//	// 主机首先发送一个字节,内容时从机地址+读写位,进行寻址
//	MyI2C_SendByte(0xD0);  // 1101 000 0,0代表即将进行写入操作
//	// 发送一个字节后,要接收一下应答位,看看从机有没有收到刚才的数据
//	uint8_t Ack = MyI2C_ReceiveAck();
//	// 接收应答之后,要继续发送一个字节,写入寄存器地址
//	MyI2C_Stop();
//	
//	OLED_ShowNum(1, 1, Ack, 3);
	
//	// 指定地址读
//	uint8_t ID = MPU6050_ReadReg(0X75);// 返回值是0x68
//	OLED_ShowHexNum(1, 1, ID, 2);
	
//	// 指定地址写,需要先解除睡眠模式,否则写入无效
//	// 睡眠模式是电源管理寄存器1的这一位SLEEP控制的,把该寄存器写入0x00,解除睡眠模式
//	// 该寄存器地址是0x6B
//	MPU6050_WriteReg(0x6B, 0x00);
//	// 采样率分频寄存器,地址是0x19,值的内容是采样分频
//	MPU6050_WriteReg(0x19, 0xAA);
//	
//	uint8_t ID = MPU6050_ReadReg(0X19);
//	OLED_ShowHexNum(1, 1, ID, 2);//显示0x19地址下的内容,应该是0xAA
	
	
	
	while(1)
	{
		MPU6050_GetData(&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, 8, GX, 5);
		OLED_ShowSignedNum(3, 8, GY, 5);
		OLED_ShowSignedNum(4, 8, GZ, 5);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57