【蓝桥杯】【嵌入式组别】第八节:EEPROM
EEPROM
- IIC协议简介:
- **主设备访问从设备的一般过程**
- **START和STOP条件**
- 数据有效性和字节格式
- 确认(ACK)和不确认(NACK)
- IIC 数据传输
- 向从器件写入数据
- 从从机读取数据
- AT24C02芯片(EEPROM)
- 程序设计:
IIC协议:
PCF8591:集成了AD/DA功能。这个芯片是通过IIC协议与单片机通讯的。
AT24C02:EEPROM芯片,也是通过IIC协议与单片机通讯的。
IIC协议简介:
I2C总线是标准双向接口,控制器/处理器作为主机与从设备通信。 除非已由主设备寻址,否则从设备不能主动传输数据。 I2C总线上的每个器件都有一个特定的器件地址,以区分同一I2C总线上的多个器件。 许多从设备在启动时将需要配置以设置设备的行为。 这通常在主器件访问具有唯一寄存器地址的从器件内部寄存器映射时完成。 设备可以有一个或多个寄存器,用于存储,写入或读取数据。
物理I2C接口由串行时钟(SCL)和串行数据(SDA)线组成。 SDA和SCL线都必须通过上拉电阻连接到VCC。 上拉电阻的大小由I2C总线上的电容量决定(更多详细信息,请参考I2C上拉电阻计算的文章([SLVA689])。 只有在总线空闲时才可以启动数据传输。 如果在STOP条件后SDA和SCL线均为高电平,则总线被视为空闲。
主设备访问从设备的一般过程
假设主设备想要向从设备发送数据:
- 主发送器发送START条件并寻址从接收器
- 主发送器将数据发送到从接收器
- 主发送器以STOP条件终止传输
如果主设备想要从从设备接收/读取数据:
- 主发送器发送START条件并寻址从接收器
- 主接收器发送请求读取的寄存器
- 主接收器从从发送器接收数据
- 主发送器以STOP条件终止传输
START和STOP条件
与主设备的I2C通信由主设备发送START条件启动,并由主设备发送STOP条件终止。 当SCL为高电平时,SDA线上由高到低的转换定义了START条件。 当SCL为高电平时,SDA线上由低到高的转换定义了STOP条件。
重复START条件类似于START条件,用来在不使总线空闲的状态下重新开始传输。 它看起来与START条件相同,但与START条件不同,因为它发生在STOP条件之前(当总线不空闲时)。 这对于主设备希望开始新通信但是不希望总线在STOP条件下空闲时非常有用,否则这可能导致主设备失去对另一个设备的控制权(在多主环境中)。
数据有效性和字节格式
在SCL的每个时钟脉冲期间传输一个数据位。 一个字节由SDA线上的8位组成。 字节可以是器件地址,寄存器地址,也可以是写入从器件或从器件读取的数据。 数据首先传输最高位(MSB)。 在START和STOP条件之间,可以从主向从传输任意数量的数据字节。 SDA线上的数据必须在时钟周期的高电平期间保持稳定,因为当SCL为高电平时数据线的变化被解释为控制命令(START或STOP).
确认(ACK)和不确认(NACK)
每个数据字节(包括地址字节)后跟一个来自接收方的ACK位。 ACK位表示接收器与发送器通信,该字节已成功接收,并且可以发送另一个字节。
在接收方发送ACK之前,发送方必须释放SDA线。 要发送ACK位,接收器应在ACK/NACK相关时钟周期(第9周期)的低相位期间下拉SDA线,以便在ACK的高相位期间SDA线稳定为低电平。
当在ACK/NACK相关时钟周期期间SDA线保持高电平时,这被解释为NACK。 有几个条件会导致生成NACK:
- 通信方无法接收或发送,因为它正在执行某些实时功能,并且尚未准备好开始与主站通信。
- 在传输期间,接收方获取它不理解的数据或命令。
- 在传输期间,接收方不能再接收任何数据字节。
- 主接收器完成读取数据并通过NACK向从设备指示。
IIC 数据传输
数据必须是向从设备发送或从从设备接收,但实现此目的的方式是通过从或从从设备中的寄存器读取或写入。
寄存器是从机存储器中的位置,包含信息,无论是配置信息,还是要发送回主机的一些采样数据。 主设备必须将信息写入这些寄存器,以指示从设备执行任务。
虽然在I2C从设备中通常有寄存器,但请注意并非所有从设备都有寄存器。 有些器件很简单,只包含1个寄存器,可以通过在从地址之后立即发送寄存器数据直接写入,而不是寻址寄存器。
单寄存器设备的示例是8位I2C开关,其通过I2C命令控制。
由于它有1位来启用或禁用通道,因此只需要1个寄存器,并且主器件仅在从地址之后写入寄存器数据,跳过寄存器编号。
向从器件写入数据
要在I2C总线上写入,主器件将在总线上发送带有从器件地址的起始条件,并且最后一位设置为0的(R/W位),这表示写入。 从器件发送应答位后,主器件将发送它希望写入的寄存器的寄存器地址。 从机将再次确认,让主机知道它准备好了。 在此之后,主设备将开始将数据发送到从设备的寄存器,直到主设备发送了所需的所有数据(有时这只是一个字节),主设备将以STOP条件终止传输。
从从机读取数据
从从机中读取与写作非常相似,但需要一些额外的步骤。 为了从从机读取,主机必须首先向从机指示它希望从哪个寄存器读取。 这是由主设备以与写入类似的方式开始传输,通过发送R/W位等于0(表示写入)的地址,然后是希望从中读取的寄存器地址。 一旦从机确认该寄存器地址,主机将再次发送START条件,然后是从机地址,R/W位设置为1(表示读取)。 这次,从器件将确认读取请求,主器件释放SDA总线,但将继续向从器件提供时钟。 在通讯的这一部分期间,主设备将成为主接收器,从设备将成为从发送器。
主机将继续发送时钟脉冲,但将释放SDA线,以便从机可以传输数据。 在每个数据字节结束时,主设备将向从设备发送ACK,让从设备知道它已准备好接收更多数据。 一旦主设备收到了预期的字节数,它就会发送一个NACK,向从设备发出信号以停止通信并释放总线。然后主机将以STOP条件结束通讯。
AT24C02芯片(EEPROM)
主要特性就是掉电不丢失,内部是MOS管的存储结构。(Flash也是掉电不丢失的存储结构)
电路图如下:
8脚是VCC,4脚是GND,就是接电源的引脚,接好后芯片就可以工作了。
然后是IIC的通讯管脚,SCL和SDA分别接的是单片机的P20口和P21口。通过两个上拉电阻上拉到VCC,因为IIC从器件和主机都是要通过开漏模式进行通讯的,开漏的话必须通过上拉电阻的VCC才能输出高电平。
那么E1,E2,E3引脚分别有什么用呢?
IIC设备都有一个器件地址,24C02就只看上图的第一行就可以了,因为他的容量就是1K/2K规格的。
可以看到前7位里面有4位已经是固定的,剩下三位就是电路原理图中的A0,A1,A2。那么电路原理图中把A0,A1,A2分别都接在GND上,那么这个地址就定下来了。前七位就是1010000.
最后一位是来定写操作还是读操作的。如果是0就是往芯片中进行写操作,如果是1就是进行读操作。
所以AT24C02的器件地址:1010000(R/W(写是低电平有效))
- 0xA0代表向AT24C02写数据
- 0xA1代表读AT24C02的数据
WC引脚是一个写保护引脚,当WP为高电平时,AT24C02无法写入数据。
AT24C02芯片特点
• Low-voltage and Standard-voltage Operation
– 2.7 (VCC = 2.7V to 5.5V)
– 1.8 (VCC = 1.8V to 5.5V)
• Internally Organized 128 x 8 (1K), 256 x 8 (2K), 512 x 8 (4K),
1024 x 8 (8K) or 2048 x 8 (16K)
• Two-wire Serial Interface
• Schmitt Trigger, Filtered Inputs for Noise Suppression
• Bidirectional Data Transfer Protocol
• 100 kHz (1.8V) and 400 kHz (2.7V, 5V) Compatibility
• Write Protect Pin for Hardware Data Protection
• 8-byte Page (1K, 2K), 16-byte Page (4K, 8K, 16K) Write Modes
• Partial Page Writes Allowed
• Self-timed Write Cycle (5 ms max)
• High-reliability
– Endurance: 1 Million Write Cycles
– Data Retention: 100 Years
• Automotive Devices Available
• 8-lead JEDEC PDIP, 8-lead JEDEC SOIC, 8-lead Ultra Thin Mini-MAP (MLP 2x3), 5-lead
SOT23, 8-lead TSSOP and 8-ball dBGA2 Packages
• Die Sales: Wafer Form, Waffle Pack and Bumped Wafers
翻译版:
[AT24002特性]
1.低压和标压版本;
2.AT24002 :256*8 (2K)
3.IIC协议;
4.通讯降噪:
5.双向传输协议;
6.400KHz通讯速率;
7.硬件写保护;
8.写入周期(5ms)
9.高可靠性:1百万次写入,数据可存100年!
程序设计:
- 复制【资源数据包】 里的i2c-halc和h文件到【编程工程】
- 在main.c调用的IIC部分IO初始化代码;PB6,PB7)
- 在i2c.c文件里编程:读写EEPROM函数;
- 在main.c 调用EEPROM Write和EEPROM Read函数,完成EEPROM读写程序
i2c.c:
#include "i2c.h"
#define DELAY_TIME 20
/**
* @brief SDA线输入模式配置
* @param None
* @retval None
*/
void SDA_Input_Mode()
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
GPIO_InitStructure.Pin = GPIO_PIN_7;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief SDA线输出模式配置
* @param None
* @retval None
*/
void SDA_Output_Mode()
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
GPIO_InitStructure.Pin = GPIO_PIN_7;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief SDA线输出一个位
* @param val 输出的数据
* @retval None
*/
void SDA_Output( uint16_t val )
{
if ( val )
{
GPIOB->BSRR |= GPIO_PIN_7;
}
else
{
GPIOB->BRR |= GPIO_PIN_7;
}
}
/**
* @brief SCL线输出一个位
* @param val 输出的数据
* @retval None
*/
void SCL_Output( uint16_t val )
{
if ( val )
{
GPIOB->BSRR |= GPIO_PIN_6;
}
else
{
GPIOB->BRR |= GPIO_PIN_6;
}
}
/**
* @brief SDA输入一位
* @param None
* @retval GPIO读入一位
*/
uint8_t SDA_Input(void)
{
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_SET){
return 1;
}else{
return 0;
}
}
/**
* @brief I2C的短暂延时
* @param None
* @retval None
*/
static void delay1(unsigned int n)
{
uint32_t i;
for ( i = 0; i < n; ++i);
}
/**
* @brief I2C起始信号
* @param None
* @retval None
*/
void I2CStart(void)
{
SDA_Output(1);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SDA_Output(0);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C结束信号
* @param None
* @retval None
*/
void I2CStop(void)
{
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output(0);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SDA_Output(1);
delay1(DELAY_TIME);
}
/**
* @brief I2C等待确认信号
* @param None
* @retval None
*/
unsigned char I2CWaitAck(void)
{
unsigned short cErrTime = 5;
SDA_Input_Mode();
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
while(SDA_Input())
{
cErrTime--;
delay1(DELAY_TIME);
if (0 == cErrTime)
{
SDA_Output_Mode();
I2CStop();
return ERROR;
}
}
SDA_Output_Mode();
SCL_Output(0);
delay1(DELAY_TIME);
return SUCCESS;
}
/**
* @brief I2C发送确认信号
* @param None
* @retval None
*/
void I2CSendAck(void)
{
SDA_Output(0);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C发送非确认信号
* @param None
* @retval None
*/
void I2CSendNotAck(void)
{
SDA_Output(1);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C发送一个字节
* @param cSendByte 需要发送的字节
* @retval None
*/
void I2CSendByte(unsigned char cSendByte)
{
unsigned char i = 8;
while (i--)
{
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output(cSendByte & 0x80);
delay1(DELAY_TIME);
cSendByte += cSendByte;
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
}
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C接收一个字节
* @param None
* @retval 接收到的字节
*/
unsigned char I2CReceiveByte(void)
{
unsigned char i = 8;
unsigned char cR_Byte = 0;
SDA_Input_Mode();
while (i--)
{
cR_Byte += cR_Byte;
SCL_Output(0);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
cR_Byte |= SDA_Input();
}
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output_Mode();
return cR_Byte;
}
//
void I2CInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
GPIO_InitStructure.Pin = GPIO_PIN_7 | GPIO_PIN_6;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}
i2c.h:
#ifndef __I2C_H
#define __I2C_H
#include "main.h"
void I2CStart(void);
void I2CStop(void);
unsigned char I2CWaitAck(void);
void I2CSendAck(void);
void I2CSendNotAck(void);
void I2CSendByte(unsigned char cSendByte);
unsigned char I2CReceiveByte(void);
void I2CInit(void);
#endif
比赛时需要根据上面的给出的例程和前面讲的IIC协议在i2c.c
文件中来写读写EEPROM的函数(写完后记得在i2c.h
中进行声明):
//写24C02
void EEPROM_Write(u8 add,u8 dat)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(add);
I2CWaitAck();
I2CSendByte(dat);
I2CWaitAck();
I2CStop();
HAL_Delay(5);
}
//读24C02
u8 EEPROM_Read(u8 add)
{
u8 dat;
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(add);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
dat = I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return(dat);
}
然后我们就可以在main.c
文件中调用读写函数对EEPROM进行测试:
注意EEPROM的地址范围是0~0xff,也就是0-255。写入的数值范围也是0-255.(因为写入的数最高为位数是8位,所以就是最大0xff,也就是255)
main.c如下:
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2021 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
#include "led.h"
#include "key.h"
#include "i2c.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
//Led执行程序
__IO uint32_t ledTick =0,keyTick=0;
u8 led_ctrl=0xff;
void LED_Process(void)
{
if(uwTick-ledTick<500)return;
ledTick=uwTick;
LED_Control(led_ctrl);
led_ctrl=~led_ctrl;
}
void KEY_Process(void)
{
if(uwTick-keyTick<10)return;
keyTick=uwTick;
Key_Read();
// if(Trg&0x01)
// {
// LED_Control(0x01);
// }
if(Trg)
{
LED_Control(Trg);
}
}
void LCD_Process(void)
{
u8 display_buf[20];
//[问题]长数据对端数据的覆盖问题
sprintf((char*)display_buf,"%d",4000);
LCD_DisplayStringLine(Line0,display_buf);
sprintf((char*)display_buf,"%d",10);
LCD_DisplayStringLine(Line0,display_buf);
//解决方案:加空格,针对字符串
LCD_DisplayStringLine(Line2,"hello");
LCD_DisplayStringLine(Line2,"h ");
//解决方案:格式化输出,针对数据
sprintf((char*)display_buf,"%5d",5000);//默认5位,显示右对齐
LCD_DisplayStringLine(Line3,display_buf);
sprintf((char*)display_buf,"%5d",10);
LCD_DisplayStringLine(Line3,display_buf);
sprintf((char*)display_buf,"%-5d",10);//左对齐
LCD_DisplayStringLine(Line4,display_buf);
sprintf((char*)display_buf,"%05d",500);//前面补0
LCD_DisplayStringLine(Line5,display_buf);
sprintf((char*)display_buf,"%5.2f",3.1415926);//显示小鼠,总长是5位,小数点算一位
LCD_DisplayStringLine(Line6,display_buf);
sprintf((char*)display_buf,"%x",15);
LCD_DisplayStringLine(Line7,display_buf);//%x显示16进制,%o显示8进制
sprintf((char*)display_buf,"%c",'a');
LCD_DisplayStringLine(Line8,display_buf);//%s字符串,%c是字符
sprintf((char*)display_buf,"%d %%",10);
LCD_DisplayStringLine(Line9,display_buf);//输出百分号:%
}
u8 val_24c02=0;
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
LCD_Init();
LED_Control(0x00);
//LCD_Process();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
LCD_Clear(Blue);
LCD_SetBackColor(Blue);
LCD_SetTextColor(White);
LCD_Process();
I2CInit();
EEPROM_Write(0x10,0x55);
val_24c02=EEPROM_Read(0x10);
u8 display_buf[20];
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//LED_Process();
sprintf((char*)display_buf,"EEPROM:%d",val_24c02);
LCD_DisplayStringLine(Line1,display_buf);//输出百分号:%
KEY_Process();
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV2;
RCC_OscInitStruct.PLL.PLLN = 20;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_3) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/