赞
踩
这一篇博客是根据自己的需要,以及为了面试岗位的需求,所以专门来再次学习一遍SPI和IIC两个常用的通信协议。这里使用的开发板是STM32F03_MIN
也希望这篇博客能对你有一定的帮助!
I²C ,通常被读作“I方C”,它是一种多主从架构的串行通信总线。在1980年由飞利浦公司设计,用于让主板,嵌入式系统或手机连接低速周边设别。如今在嵌入式领域是非常常见的通信协议。常用于MPU/MCU与外部设备连接通信,数据传输。
IIC由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。每个连接到总线的设备都有一个独立的地址,主机可以通过这个地址来访问不同的设备。因为IIC协议比较简单,常常用GPIO来模拟IIC时序,这种方法称为模拟IIC。如果使用MCU的IIC控制器,设置好IIC控制器,IIC控制器就自动实现协议时序,这种方式称为硬件IIC。因为IIC设备的速率比较低,通常两种方式都可以,模拟IIC方便移植,硬件IIC工作效率相对较高。
关于IIC协议,通过老师传学生足球的例子相信大家可以清晰的理解
把老师看作MCU/MPU,学生看作外设设备。
首先老师将球踢给某学生,即主机发送数据给从机,步骤如下:
接着老师让学生把球传给自己,即从机发送数据给主机,步骤如下:
MCU接收外设设备传输的数据时,首先时MCU发送指令让设备发送数据的信号,设备收到后,响应MCU,同时MCU接收到数据时也要回应外设设备。当数据传输完成,MCU自动结束传输。
从上面的例子可知,都是老师(主机)主导传球,按照规范的流程(通信协议),以保证传球的准确性,收发球的流程总结如下:
① 老师说开始了,表示开始信号(start);
② 老师提醒某个学生要发球,表示发送地址和方向(address/read/write);
③ 该学生回应老师(ack);
④ 老师发球/接球,表示数据的传输;
⑤ 收到球要回应:回应信号(ACK);
⑥ 老师说结束,表示IIC传输结束§。
IIC由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。SDA线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在SCL线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示五有效数据,此时SDA会进行电平切换,为下一次数据表示做准备。如下图

IIC起始信号(S):当SCL高电平时,SDA由高电平向低电平转换;
IIC停止信号(P):当SCL为高电平时,SDA由低电平向高电平转换;

IIC每次传输的8位数据,每次传输后需要从机反馈一个应答位,以确认从机是否正常接收了数据。当主机发送了8位数据后,会再产生一个时钟,此时主机放开SDA的控制,读取SDA电平,在上拉电阻的影响下,此时SDA默认为高,必须从机拉低,以确认收到数据。

IIC完整传输流程如下:
EEPROM的全称是“电可擦除可编程只读寄存器”。通常用于存放用户配置信息数据,比如在开发板首次运行时,需要屏幕校准,校准后的配置信号就可以保存在EEPROM里,开发板断电后不丢失,下次启动,开发板自动读取EEPROM的校准配置信息,就不需要重新校准。
EEPROM和Flash的本质是一样的,Flash包括MCU内部的Flash和外部扩展的Flash,本开发板就有一个SPI接口的外部Flash,SPI后面会进行介绍。从功能上,Flash通常存放运行代码,运行过程中不会修改,而EEPROM存放用户数据,可能会反复的修改。从结构上,Flash按扇区操作,EEPROM通常按字节操作。两者区别这里就不再过多的讲,理解EEPROM在嵌入式中扮演的角色即可。

IIC设备都会有一个设备地址,不同容量的ATC2402,设备地址定义会有所差异,由数据手册可知

AT24C02的容量为2K,对应上图中的第一行,高四位固定为“ 1010”,中间三位由A2、 A1、 A0引脚的电平决定,比如A2~0引脚全接地,则值为“000”,最后的最低位为读写位, 0代表写命令, 1代表读命令。A2、 A1、 A0引脚电平需要由原理图决定,假设全接电源地,则如果需要向AT24C02写数据,则发送地址“ 1010 0000”,如果需要向AT24C02读数据,则发送地址“ 1010 0001”。
假设开发板有多个AT24C02挂在同一I²C总线上,通过这个规则,只需设计电路时,让A2、 A1、 A0引脚电平不同,即可区分两个AT24C02。
对于容量再大一点的AT24Cxx系列,比如AT24C04,器件地址由A2、 A1引脚决定,数据空间有P0决定。比如对AT24C04的02K空间操作,则P0为0,对2K4K空间操作,则P0为1。
AT24Cxx支持字节写模式和页写模式。字节写模式是一个地址一个数据的写;页写模式是连续写数据,一个地址多个数据的写,但是页写模式不能自动跨页,如果超出一页,超出的数据会覆盖原先写入的数据。
如下图为AT24Cxx字节写模式的时序
在MCU发出开始信号( Start)后,发出8 Bit的设备地址信息(图中读写位为低电平,即写数据),待收到AT24Cxx应答信号后,再发出要写的数据地址,再次等待AT24Cxx应答,最后发出8 Bit数据写数据,待AT24Cxx应答后,发出停止信号( Stop),完成一次单字
节写数据。
AT24C02容量为2K。因此数据地址范围为0x00~0xFF,即0 ~ 255,每个数据地址每次写1Byte,即8bit,也就刚好2048bit。对于1K容量的产品,数据地址范围为0x00 ~ 0x7F,最高位不会用到,因此下图中数据地址的最高位用“*”表示,意思就是不用关心,不会用

AT24Cxx的页写模式时序,如下图,与字节写模式的差异在于,不是只发送以Byte数据,而是任意多个,需注意,该模式不能跨页写,遇到跨页时,需要重新发送完整的时序。
值得一提的是, 《AT24Cxx.pdf》 里提到每次写完之后,再到下次写之前,需要间隔5ms时间, 以确保上次写操作在芯片内部完成

AT24Cxx支持当前地址读模式、随机地址读模式和顺序地址读模式。
当前地址读模式:就是在上一次读/写操作之后的最后位置,继续读出数据,比如上次读/写在地址n,接下来可以直接从n+1除读出数据;
随机地址读模式:是指定数据地址,然后读出数据;
顺序读模式:是连续读出多个数据。
在当前地址读模式下,无须发送数据地址,数据地址为上一次读/写操作之后的位置,如下所示
注意:主机接收到数据后,无需产生应答信号。
在随机地址读模式下,需要先发送设备地址,待读的数据地址,接着再重新发出信号,设备地址,读出数据

在顺序读模式下,需要先从当前地址读模式或随机地址读模式启动,随后可以连续读多个数据,时序如下

U6为AT24C02芯片,它的A0、 A1、 A2都接地,因此该设备地址为“ 1010 000X”,当读该设备时, X为1,写该设备时, X为0。
U4的7脚为写保护引脚( Write Protect, WP),当该引脚为高,则禁止写AT24C02,这里直接拉低WP,任何时候都可直接写AT24C02。
此外, I2C的两个脚SCL和SDA都进行了上拉处理,从而保证I2C总线空闲时, 两根线都必须为高电平。
如果没有上拉,在主机发送完数据后,放开SDA,此时SDA的电平状态不确定,可能为高,也可能为低,无法确定是从机拉低给出应答信号。
通过GPIO模拟I2C总线时序,对EEPROM设备AT24C02进行读写操作。
i2c.h

i2c.c
#include <stdio.h> #include "i2c.h" #include "main.h" #include "timer.h" #include "usart.h" #define I2C_Delay() us_timer_delay(5) //初始化模拟I2C的引脚为输出状态且SCL/SDA都初始为高电平 void I2C_Init(void){ GPIO_InitTypeDef GPIO_InitStruct={0}; SCL_PIN_CLK_EN(); SDA_PIN_CLK_EN(); GPIO_InitStruct.Mode=GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull=GPIO_NOPULL; GPIO_InitStruct.Pin=SCL_PIN; GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(SCL_PORT,&GPIO_InitStruct); GPIO_InitStruct.Pin=SDA_PIN; HAL_GPIO_Init(SDA_PORT,&GPIO_InitStruct); SCL_H(); SDA_H(); } //配置SDA的引脚为输出。 static void I2C_SDA_OUT(void){ GPIO_InitTypeDef GPIO_InitStruct={0}; GPIO_InitStruct.Mode=GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull=GPIO_PULLUP; GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Pin=SDA_PIN; HAL_GPIO_Init(SDA_PORT,&GPIO_InitStruct); } //配置SDA引脚为输入模式 static void I2C_SDA_IN(void){ GPIO_InitTypeDef GPIO_InitStruct={0}; GPIO_InitStruct.Mode=GPIO_MODE_INPUT; GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Pin=SDA_PIN; HAL_GPIO_Init(SDA_PORT,&GPIO_InitStruct); } //开始信号 void I2C_Start(void){ I2C_SDA_OUT();//设置为输出模式 SCL_H();//时钟线输出为高 I2C_Delay();//延迟5ms SDA_H();//数据线输出为高 I2C_Delay(); SDA_L();//数据线输出低,由高到底表示开始信号 I2C_Delay(); } //结束信号 void I2C_Stop(void){ I2C_SDA_OUT();//输出模式 SDA_L(); I2C_Delay(); SCL_H(); I2C_Delay(); SDA_H(); I2C_Delay(); } //发出应答信号函数 void I2C_ACK(void){ I2C_SDA_OUT();//设置为接收模式 SCL_L();//时钟线输出为低 I2C_Delay(); SDA_L(); I2C_Delay(); SCL_H(); I2C_Delay(); SCL_L(); I2C_Delay(); } //发出非应答信号 void I2C_NACK(void){ I2C_SDA_OUT(); SCL_L(); I2C_Delay(); SDA_H(); I2C_Delay(); SCL_H(); I2C_Delay(); SCL_L(); I2C_Delay(); } //等待从机的应答信号 uint8_t I2C_GetACK(void){ uint8_t time=0; I2C_SDA_IN();//设置为输入模式 SCL_L(); I2C_Delay(); SDA_H(); I2C_Delay(); SCL_H(); I2C_Delay(); while(SDA_INPUT()){ time++; if(time>250){ SCL_L(); return 1; } } SCL_L(); return 0; } //发送一个字节的数据 void I2C_SendBYTE(uint8_t data){ uint8_t cnt=0; I2C_SDA_OUT(); for(cnt=0;cnt<8;cnt++){ SCL_L(); I2C_Delay(); if(data & 0x80){ SDA_H(); }else{ SDA_L(); } data<<=1; SCL_H(); I2C_Delay(); } SCL_L();//发完数据 I2C_Delay(); I2C_GetACK(); } //读取一个字节的数据 uint8_t I2C_ReadBYTE(uint8_t ack){ uint8_t cnt=0; uint8_t data=0xFF;//确定data的值 SCL_L(); I2C_Delay(); for(cnt=0;cnt<8;cnt++){ SCL_H(); //SCL高,读取数据 I2C_Delay(); data<<=1; if(SDA_INPUT()){ data |=0x01; } SCL_L(); I2C_Delay(); } //发送应答信号,低为应答,高为非应答 if(ack==0){ I2C_ACK(); } else{ I2C_NACK(); } return data; }
eeprom.h
#ifndef _EEPROM_H #define _EEPROM_H #include "stm32f1xx_hal.h" #define EEPROM_DEV_ADDR (0xA0) #define EEPROM_WR (0x00) #define EEPROM_RD (0x01) #define EEPROM_WORD_ADDR_SIZE (0x08) void EEPROM_WriteByte(uint16_t addr,uint8_t data); void EEPROM_ReadByte(uint16_t addr,uint8_t *pdata); void EEPROM_Write_NByte(uint16_t addr , uint8_t *pdata,uint16_t size); void EEPROM_Read_NByte(uint16_t addr,uint8_t *pdata,uint16_t size); #endif
eerom.c
#include "eeprom.h" #include <stdio.h> #include"gpio.h" #include "usart.h" #include "i2c.h" #include "timer.h" //EEPROM写一个字节 void EEPROM_WriteByte(uint16_t addr,uint8_t data){ //开始信号 I2C_Start(); //发送要写的设备地址 I2C_SendBYTE(EEPROM_DEV_ADDR | EEPROM_WR); //要写的数据地址 if(EEPROM_WORD_ADDR_SIZE==0x08){ I2C_SendBYTE((uint8_t)(addr & 0x00FF)); } else{ I2C_SendBYTE((uint8_t)(addr>>8)); I2C_SendBYTE((uint8_t)(addr & 0x00FF)); } //写数据 I2C_SendBYTE(data); //停止信号 I2C_Stop(); } //读EEPROM的一个字节 void EEPROM_ReadByte(uint16_t addr,uint8_t *pdata){ //开始信号 I2C_Start(); //设备地址 I2C_SendBYTE(EEPROM_DEV_ADDR | EEPROM_WR); //读取的数据的地址 if(EEPROM_WORD_ADDR_SIZE==0x08){ I2C_SendBYTE((uint8_t)(addr & 0x00FF)); }else{ I2C_SendBYTE((uint8_t)(addr>>8)); I2C_SendBYTE((uint8_t)(addr & 0x00ff)); } //开始传输 I2C_Start(); //设备的地址且设置为读模式 I2C_SendBYTE(EEPROM_DEV_ADDR | EEPROM_RD); //读取数据,因为读取数据不需要回应信号 *pdata=I2C_ReadBYTE(NACK); //结束 I2C_Stop(); } //写N个字节 void EEPROM_Write_NByte(uint16_t addr , uint8_t *pdata,uint16_t size){ uint16_t i=0; for(i=0;i<size;i++){ EEPROM_WriteByte(addr,pdata[i]); addr++; HAL_Delay(10);//每次期间需要延迟5ms,这里延迟10ms防止出错 } } //连续读数据 void EEPROM_Read_NByte(uint16_t addr,uint8_t *pdata,uint16_t size){ uint16_t i=0; for(i=0;i<size;i++){ EEPROM_ReadByte(addr,&pdata[i]); addr++; } }
timer.h
#ifndef _TIMER_H
#define _TIMER_H
#include "stm32f1xx_hal.h"
void us_timer_delay(uint16_t t);
#endif
timer.c
#include "timer.h"
void us_timer_delay(uint16_t t){
uint16_t counter=0;
while(t--){
counter=10;
while(counter--){
}
}
}
main.c
/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2022 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "usart.h" #include "gpio.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "eeprom.h" #include<stdio.h> #include "i2c.h" #include "timer.h" #include "string.h" /* 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); static void MX_NVIC_Init(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ char tx_buffer[20]={"Hello IIC"}; char rx_buffer[20]={0}; /* 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(); MX_USART1_UART_Init(); /* Initialize interrupts */ MX_NVIC_Init(); /* USER CODE BEGIN 2 */ printf("*****************\r\n"); printf("i2c test\r\n"); printf("*****************\r\n"); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if(get_KeyValue()){ //读写字符串并打印 EEPROM_Write_NByte(0,(uint8_t *)tx_buffer,sizeof(tx_buffer)); HAL_Delay(1); EEPROM_Read_NByte(0,(uint8_t *)rx_buffer,sizeof(rx_buffer)); HAL_Delay(1); printf("EEPROM Write :%s\r\n",rx_buffer); printf("EEPROM Read :%s\r\n",rx_buffer); //清空接收buffer memset(rx_buffer,0,sizeof(rx_buffer)); } } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses 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_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } } /** * @brief NVIC Configuration. * @retval None */ static void MX_NVIC_Init(void) { /* EXTI0_IRQn interrupt configuration */ HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); /* USART1_IRQn interrupt configuration */ HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); } /* 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 */ __disable_irq(); while (1) { } /* 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, ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ } #endif /* USE_FULL_ASSERT */

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。