STM32CubeMX系列07——IIC通信(AT24C02、OLED屏幕)

====>>> 文章汇总(有代码汇总) <<<====

1. 准备工作

1.1. 所用硬件

STM32F103 普中-准端-Z100,主控 STM32F103ZET6.

1.2. IIC简介

IIC(Inter Integrated Circuit)

  1. 由 PHILIPS 公司开发
  2. 需要两根线(数据线SDA时钟SCL
  3. 双向传送
  4. 高速IIC 可达 400kbps 以上

三种类型信号(看看就行,这里用的硬件IIC,也不用自己写)

  • 开始信号: SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据;
  • 结束信号: SCL 为高电平时, SDA 由低电平向高电平跳变,结束传送数据;
  • 应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号, CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。

起始信号是必需的,结束信号和应答信号,都可以不要。

1.2. 生成工程

1.2.1. 创建工程选择主控

在这里插入图片描述

1.2.2. 系统配置

配置时钟源
在这里插入图片描述
配置debug模式(如果需要ST-Link下载及调试可以勾选)
在这里插入图片描述
配置时钟树(可以直接在HCLK那里输入72,然后敲回车会自动配置)
在这里插入图片描述

1.2.3. 配置工程目录

在这里插入图片描述
在这里插入图片描述

1.2.4. IIC配置

在这里插入图片描述
然后生成代码。

扫描二维码关注公众号,回复: 15552771 查看本文章

2. 读写EEPROM实验(AT24C02)

EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。

2.1. AT24C02简介

2.1.1. AT24C02原理图

在这里插入图片描述

  • A0,A1,A2:硬件地址引脚
  • WP:写保护引脚,接高电平只读,接地允许读和写
  • SCL和SDA:IIC总线

24C02后面的 02 表示的是可存储 2Kbit 的数据,转换为字节的存储量为2*1024/8 = 256byte;256个字节一共分为32页,每页8个字节。

2.1.2. 设备地址

在AT24C02的参考手册第9页 可以看到下图
在这里插入图片描述
AT24C设备地址为如下:

  • 前四位固定 1010
  • A2、A1、A0为由管脚电平决定。此处原理图都接地,默认为000。
  • 最后一位表示读操作还是写操作。读地址为0xA1,写地址为0xA0。

结论:

  • 写24C02的时候,从器件地址为10100000(0xA0)
  • 读24C02的时候,从器件地址为10100001(0xA1)

片内地址寻址:

芯片寻址可对 内部256个字节 中的任一个进行 读/写操作,其寻址范围为00~FF,共256个寻址单位。

2.2. 读写时序

写一个字节
在这里插入图片描述
从时序图上可以看出(上面是MCU的信号,下面是存储芯片的信号),写一个字节数据的操作顺序为:

  1. MCU 先给芯片发送一个开始信号(START)
  2. 开始信后之后的第一个字节,发送要写入的设备地址(DEVICE ADDRESS)(注意,因为总线上可能由多个设备,是根据设备地址选择不同的设备的),然后发送写数据命令(0xA0),然后等待应答信号(ACK)
  3. 发送数据的存储地址。一共有256个字节的存储空间,地址从0x00~0xFF,想存到哪个地址,就发哪个地址
  4. 发送要存储的数据,发送完成之后MCU会收到应答信号
  5. 数据发送完成之后,发送结束信号(STOP)停止总线。

读一个字节
在这里插入图片描述
从时序图上可以看出(上面是MCU的信号,下面是存储芯片的信号),读一个字节数据的操作顺序为:

  1. MCU 先给芯片发送一个开始信号(START)
  2. 开始信号之后的第一个字节,发送要读取的设备地址(DEVICE ADDRESS)(注意,因为总线上可能由多个设备,是根据设备地址选择不同的设备的),然后发送写数据命令(0xA0),并发送要读取的数据地址(WORD ADDRESS),然后等待应答信号(ACK)
  3. 再次发送开始信号(START)
  4. 开始信号之后的第一个字节,发送要读取的设备地址(DEVICE ADDRESS),发送读取数据命令(0xA1)
  5. 此时,24C02会自动给MCU发送数据。
  6. MCU发送结束信号(STOP)停止总线。

写一页数据
在这里插入图片描述
时序图和写单个字节差不多,只是每个字节写完之后存储器都会给MCU发送应答信号,之后继续发送下一个字节,写完之后,MCU发送停止信号即可。

256个字节一共分为32页,每页8个字节。AT24C02页写入只支持8个byte,所以需要分32次写入。如果按照上述时序连续写入8个字节后,会重复的继续往该页写数据。(当然也可以 往256个地址中分别写入一个字节。。。)

连续读数据
在这里插入图片描述
时序图和都单个字节差不多,存储器给MCU发送完每个字节,MCU要发送应答信号给存储器,直到MCU发送停止信号。且读数据没有8个字节的限制。

2.3. 代码实现

虽然时序看起来很复杂,但是不用担心,很多都已经有实现了。
在生成的工程中,打开stm32f1xx_hal.h,可以看到已经生成了轮询,中断和DMA三种控制方式的代码。
在这里插入图片描述
我们只看轮询的,其他的也都差不多,只是应用场景不一样。

// 作为主机 发送数据
// 参数:iic接口、设备地址、发送的数据、数据长度、超时时间
HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// 作为主机 接收数据
// 参数:iic接口、设备地址、存储读取到的数据、数据长度、超时时间
HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// 作为从机 发送数据
// 参数:iic接口、设备地址、发送的数据、数据长度、超时时间
HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// 作为从机 接收数据
// 参数:iic接口、设备地址、存储读取到的数据、数据长度、超时时间
HAL_I2C_Slave_Receive(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// 直接发送两个字节数据(就用于我们现在的情况,发送命令 + 发送数据)
// 参数:iic接口、设备地址、发送的数据1、发送的数据2、数据长度、超时时间
HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// 直接发送两个字节数据,并接受数据(就用于我们现在的情况,发送命令 + 接收数据)
// 参数:iic接口、设备地址、发送的数据1、发送的数据2、存储读取到的数据、数据长度、超时时间
HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// 查询设备是否就绪
HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout);

我们可以直接看HAL_I2C_Mem_WriteHAL_I2C_Mem_Read,刚好可以满足我们这里需要发送 指令 + 地址 的情况,其在发送或者读取数据的过程中,地址还可以自己增加,很方便。

i2c.h中声明如下代码

/* USER CODE BEGIN Prototypes */

#define    	AT24C02_ADDR_WRITE  		0xA0	// 写命令
#define    	AT24C02_ADDR_READ   		0xA1	// 读命令
uint8_t At24c02_Write_Byte(uint16_t addr, uint8_t* dat);
uint8_t At24c02_Read_Byte(uint16_t addr, uint8_t* read_buf);
uint8_t At24c02_Write_Amount_Byte(uint16_t addr, uint8_t* dat, uint16_t size);
uint8_t At24c02_Read_Amount_Byte(uint16_t addr, uint8_t* recv_buf, uint16_t size);

/* USER CODE END Prototypes */

i2c.c中添加如下代码

/* USER CODE BEGIN 1 */

#include <string.h>

/**
 * @brief        AT24C02任意地址写一个字节数据
 * @param        addr —— 写数据的地址(0-255)
 * @param        dat  —— 存放写入数据的地址
 * @retval       成功 —— HAL_OK
*/
uint8_t At24c02_Write_Byte(uint16_t addr, uint8_t* dat)
{
    
    
	HAL_StatusTypeDef result;
	result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, 1, 0xFFFF);
	HAL_Delay(5);	// 写一个字节,延迟一段时间,不能连续写
    return result;
}

/**
 * @brief        AT24C02任意地址读一个字节数据
 * @param        addr —— 读数据的地址(0-255)
 * @param        read_buf —— 存放读取数据的地址
 * @retval       成功 —— HAL_OK
*/
uint8_t At24c02_Read_Byte(uint16_t addr, uint8_t* read_buf)
{
    
    
    return HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_READ, addr, I2C_MEMADD_SIZE_8BIT, read_buf, 1, 0xFFFF);
}

/**
 * @brief        AT24C02任意地址连续写多个字节数据
 * @param        addr —— 写数据的地址(0-255)
 * @param        dat  —— 存放写入数据的地址
 * @retval       成功 —— HAL_OK
*/
uint8_t At24c02_Write_Amount_Byte(uint16_t addr, uint8_t* dat, uint16_t size)
{
    
    
    uint8_t i = 0;
    uint16_t cnt = 0;        // 写入字节计数
	HAL_StatusTypeDef result;	// 返回是否写入成功

    /* 对于起始地址,有两种情况,分别判断 */
    if(0 == addr % 8)
    {
    
    
        /* 起始地址刚好是页开始地址 */
        /* 对于写入的字节数,有两种情况,分别判断 */
        if(size <= 8)
        {
    
    
            // 写入的字节数不大于一页,直接写入
			result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, size, 0xFFFF);
            HAL_Delay(20);	// 写完八个字节(最多八个字节),延迟久一点
			return result;
        }
        else
        {
    
    
            // 写入的字节数大于一页,先将整页循环写入
            for(i = 0; i < size/8; i++)
            {
    
    
                HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], 8, 0xFFFF);
                // 一次写入了八个字节,延迟久一点
				HAL_Delay(20);	// 写完八个字节,延迟久一点
				addr += 8;
                cnt += 8;
            }
            // 将剩余的字节写入
			result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], size - cnt, 0xFFFF);
            HAL_Delay(20);	// 写完八个字节(最多八个字节),延迟久一点
			return result;
        }
    }
    else
    {
    
    
        /* 起始地址偏离页开始地址 */
        /* 对于写入的字节数,有两种情况,分别判断 */
        if(size <= (8 - addr%8))
        {
    
    
            /* 在该页可以写完 */
			result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, size, 0xFFFF);
			HAL_Delay(20);	// 写完八个字节(最多八个字节),延迟久一点
            return result;
        }
        else
        {
    
    
            /* 该页写不完 */
            // 先将该页写完
            cnt += 8 - addr%8;
            HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, cnt, 0xFFFF);
            HAL_Delay(20);	// 写完八个字节(最多八个字节),延迟久一点
			addr += cnt;

            // 循环写整页数据
            for(i = 0;i < (size - cnt)/8; i++)
            {
    
    
                HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], 8, 0xFFFF);
                HAL_Delay(20);	// 写完八个字节,延迟久一点
				addr += 8;
                cnt += 8;
            }
            // 将剩下的字节写入
			result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], size - cnt, 0xFFFF);
            HAL_Delay(20);	// 写完八个字节(最多八个字节),延迟久一点
			return result;
        }            
    }
}

/**
 * @brief        AT24C02任意地址连续读多个字节数据
 * @param        addr —— 读数据的地址(0-255)
 * @param        dat  —— 存放读出数据的地址
 * @retval       成功 —— HAL_OK
*/
uint8_t At24c02_Read_Amount_Byte(uint16_t addr, uint8_t* recv_buf, uint16_t size)
{
    
    
    return HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_READ, addr, I2C_MEMADD_SIZE_8BIT, recv_buf, size, 0xFFFF);
}

/* USER CODE END 1 */

main.c中添加如下代码

int main(void)
{
    
    
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_USART1_UART_Init();

	// 单个字节 读写测试
	uint8_t simple_write_dat = 0xa5;	// 一个字节
	uint8_t simple_recv_buf = 0;

	if(HAL_OK == At24c02_Write_Byte(10, &simple_write_dat)){
    
    
		printf("Simple data write success \r\n");
	} else {
    
    
		printf("Simple data write fail \r\n");
	}
	
	HAL_Delay(50);      // 写一次和读一次之间需要短暂的延时
	
	if(HAL_OK == At24c02_Read_Byte(10, &simple_recv_buf)){
    
    
		printf("Simple data read success, recv_buf = 0x%02X \r\n", simple_recv_buf);
	} else {
    
    
		printf("Simple data read fail \r\n");
	}
	printf("--------------------- \r\n");
	// 单个字节读写 测试结束
	
	// 浮点数 读写测试
	union float_union{
    
    
		float float_write_dat;		// 浮点数占4个字节
		double double_write_dat;	// 双精度浮点数占8个字节
		uint8_t buf[8];				// 定义 8个字节 的空间
	};
	union float_union send_float_data;	// 用来发送
	union float_union rev_float_data;	// 用来接收
	
	// 先测试第一个 浮点数
	send_float_data.float_write_dat = 3.1415f;
	if(HAL_OK == At24c02_Write_Amount_Byte(20, send_float_data.buf, 4)){
    
    
		printf("Float data write success \r\n");
	} else {
    
    
		printf("Float data write fail \r\n");
	}
	HAL_Delay(50);
	if(HAL_OK == At24c02_Read_Amount_Byte(20, rev_float_data.buf, 4)){
    
    
		// 默认输出六位小数
		printf("Float data read success, recv_buf = %f \r\n", rev_float_data.float_write_dat);
	} else {
    
    
		printf("Float data read fail \r\n");
	}
	// 测试第二个 双精度浮点数
	send_float_data.double_write_dat = 3.1415f;
	if(HAL_OK == At24c02_Write_Amount_Byte(20, send_float_data.buf, 8)){
    
    
		printf("Double data write success \r\n");
	} else {
    
    
		printf("Double data write fail \r\n");
	}
	HAL_Delay(50);
	if(HAL_OK == At24c02_Read_Amount_Byte(20, rev_float_data.buf, 8)){
    
    
		// 最多15位小数
		printf("Double data read success, recv_buf = %.15f \r\n", rev_float_data.double_write_dat);
	} else {
    
    
		printf("Double data read fail \r\n");
	}
	printf("--------------------- \r\n");
	// 浮点数读写测试 测试结束
  
	// 连续数据读写测试
	uint8_t write_dat[22] = {
    
    0};		// 22个字节
	uint8_t recv_buf[22] = {
    
    0};
	
	printf("正在往数组中填充数据... \r\n");
	for(int i = 0; i < 22; i++){
    
    
		write_dat[i] = i;
		printf("%02X ", write_dat[i]);
	}
	printf("\r\n 数组中数据填充完毕... \r\n");
	
	if(HAL_OK == At24c02_Write_Amount_Byte(0, write_dat, 22)){
    
    
		printf("24c02 write success \r\n");
	} else {
    
    
		printf("24c02 write fail \r\n");
	}
	
	HAL_Delay(50);      // 写一次和读一次之间需要短暂的延时
	
	if(HAL_OK == At24c02_Read_Amount_Byte(0, recv_buf, 22)){
    
    
		printf("read success \r\n");
		for(int i = 0; i < 22; i++) {
    
    
			printf("0x%02X ", recv_buf[i]);
		}
	} else {
    
    
		printf("read fail\r\n");
	}
	// 连续数据读写 测试结束
  while (1)
  {
    
    
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }

因为每次发送或者接收只能按照一个字节的单位进行,因此对于 uint8_t 类型的整数没什么问题。但是对于浮点数等,占用多个字节的,就可以通过共用体的方式进行。
此方法在许多应用场景中都有应用,比如串口发送浮点数,也可以用这样的方式进行。

效果验证
编译、烧录
用串口助手观察现象。
在这里插入图片描述

3. 0.96寸OLED显示实现

3.1. OLED简介(SSD1306)

四线OLED采用IIC通信。该模块集成了 SSD1306存储芯片

从模块数据手册可以看到下图。GDDRAM是一个位映射静态RAM,保存要显示的位模式。RAM的大小为128 x 64位,RAM分为八页,从第0页到第7页,用于单色128x64点阵显示器。
在这里插入图片描述
存储大小刚好对应了我们屏幕的分辨大小(128*64)。简单来说,就是只要在对应的存储位上存储有效电平,对应的像素点就可以亮起来。

3.2. 代码实现

具体的显示方式和上个章节的差不多,这里直接放代码,抄来用即可。
因为这里的代码比较多,因此我们另外建个文件夹放我们的代码。

  1. 在工程目录下,创建icode文件夹,用来存放我们自己的代码。
  2. icode文件夹下,创建OLED文件夹,存放oled相关代码。
  3. OLED文件夹下, 创建如下几个文件:oled.coled.holedfont.hbmp.h

oled.c:源文件
oled.h:头文件
oledfont.h:字库文件
bmp.h:图片库文件

在这里插入图片描述
把源文件添加进去
在这里插入图片描述
把头文件路径添加进去
在这里插入图片描述

代码如下(我总觉得我手上的东西有问题,等有个靠谱的再写把)
oled.h


oled.c


oledfont.h


bmp.h


猜你喜欢

转载自blog.csdn.net/weixin_46253745/article/details/127834961