16-CubeMx+Keil+Proteus仿真STM32 - I2C
本文例子参考《STM32单片机开发实例——基于Proteus虚拟仿真与HAL/LL库》
源代码:https://github.com/LanLinnet/STM32F103R6
项目要求
掌握\(I^2C\)的通讯方法和时序,通过串口发送数据,单片机接收并存入AT24C02首地址中。按下按键BTN,单片机将存放在AT24C02首地址中的数据取出并通过串口发送。串口通信参数:波特率为19200bits/s;无校验。
硬件设计
-
在第一节的基础上,在Proteus中添加电路如下图所示。其中我们添加了一个I2C通信的外设:EEPROM芯片
AT24C02
(在Proteus中为FM24C02
)。
此外,还添加了\(I^2C\)总线调试工具I2C DEBUGGER
,用于读取\(I^2C\)输入输出的数据。
串口和按键的相关电路可以参考第13节。COMPIM设置如下图所示。
-
\(I^2C\):
1)简介:\(I^2C\)(Inter-Integrated Circuit)总线是由Philips公司提出的一种两线式串行总线。\(I^2C\)总线属于多主总线,每个节点都可以设置唯一的地址,向总线发送数据的设备作为发送器,从总线接收数据的设备作为接收器。
2)\(I^2C\)总线:由时钟信号线SCL和双向数据线SDA组成。
3)通信时序:\(I^2C\)总线的通信时序分为发送器启动/停止通信、数据位传送、接收器返回响应信号三种。-
发送器启动/停止通信:SCL保持高电平期间,SDA产生下降沿,即通信启动信号;SCL保持高电平期间,SDA产生上升沿,即通信停止信号。
-
数据位传送:数据发送器在启动通信之后,便向\(I^2C\)总线发送数据,发送数据字节长度为1字节,发送顺序高位在前,低位在后,逐位发送。如下图所示,在SCL处于高电平期间,SDA必须保持稳定,SDA低电平代表数据0,高电平代表数据1;只有在SCL处于低电平期间,SDA才能改变电平状态。
-
接收器返回响应信号:数据发送器每发送1个字节,数据接收器都必须返回1位响应信号,响应信号若为低电平则规定为应答响应位(ACK),表示数据接收器接收该字节数据成功;反之,则称为非应答响应位(NACK),表示数据接收器接收该字节数据失败。
如果数据接收器是主机,则在它收到最后一字节数据后,返回一个非应答位,通知数据发送器结束数据发送,接着主机向\(I^2C\)总线发送一个停止通信信号,结束通信过程。
-
-
AT24C02
1)简介:AT24Cxx是美国Atmel公司出品的单行\(EEPROM\)系列芯片,xx表示不同的容量。如02表示该芯片的总容量为2kbits(256字节)。
2)引脚:AT24C02芯片引脚如下图所示,引脚功能如下表所示。
其中,1-3引脚参与构成AT24C02在\(I^2C\)总线上的地址。如图1K/2K的地址所示,地址高4位固定为1010B,低4位的最低位在总线“写”指令中固定为0,在总线“读”指令中固定为1,其余3位就由1-3引脚决定。
3)读写时序:AT24C02的读写方式有写入字节、写入页、读当前地址、随机读取和连续读取5种方式,下面我们介绍本项目中使用的两种。-
写入字节时序(Byte Write):写入字节即向AT24C02写入1字节,由下面8步组成。
①主机发送启动通信(Start)信号
②发送器件(芯片)地址(Device Address)
③产生应答响应(ACK)
④发送字地址(Word Address)
⑤产生应答响应(ACK)
⑥发送数据(Data)
⑦产生应答响应(ACK)
⑧发送停止通信(Stop)信号
-
随机读取时序(Random Read):随机读取即从AT24C02读取1字节,由下面11步组成。
①主机发送启动通信(Start)信号
②发送器件(芯片)地址(Device Address)
③产生应答响应(ACK)
④发送字地址(Word Address)
⑤产生应答响应(ACK)
⑥再次发送启动通信(Start)信号
⑦发送器件(芯片)地址(Device Address)
⑧产生应答响应(ACK)
⑨读取数据(Data)
⑩发送非应答响应(No ACK)
⑪发送停止通信(Stop)信号
-
-
打开CubeMX,建立工程。设置PB6、PB7为
GPIO_Output
,PC0为GPIO_Input
,点击“Categories”中的“GPIO”的“User Label”设置如下图所示。
这里要注意,STM32F103R6自带一个\(I^2C\)总线通信模块,但是为了便于移植,我们这里采用GPIO引脚PB6、PB7模拟\(I^2C\)总线的时序。
随后进行串口设置,如下图所示,这里就不赘述了,具体可以参考第13节。
-
点击“Generator Code”生成Keil工程。
软件编写
-
考虑到代码的可移植性,这里将\(I^2C\)总线时序模拟和AT24C02操作代码分别写入头文件“vI2C.h”“AT24C02.h”中。我们可以先在
...\Core\Src
文件夹中建立这两个头文件,此时Keil可能找不到对应文件,可以直接将文件拽入Keil中进行编辑,然后再在“main.c”文件中进行include。 -
点击“Open Project”在Keil中打开工程,打开“vI2C.h”,添加代码如下。
//I2C总线时序模拟 #ifndef VI2C_H_ #define VI2C_H_ #include "main.h" //延时1μs void delay_us(uint16_t n) { uint16_t i = n*8; //8MHz,周期为1/8μs while(i--); } //设置数据线模式: I-输入 O-输出 void Pin_vSDA_Mode(char status) { GPIO_InitTypeDef GPIO_InitStruct = {0}; HAL_GPIO_WritePin(GPIOB, vSDA_Pin, GPIO_PIN_SET); GPIO_InitStruct.Pin = vSDA_Pin; GPIO_InitStruct.Pull = GPIO_PULLUP; if(status == 'I') //输入 { GPIO_InitStruct.Mode = GPIO_MODE_INPUT; } else if(status == 'O') //输出 { GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; } HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } //时钟线输出 void vSCL_Out(uint8_t dat) { switch(dat) { case 0: HAL_GPIO_WritePin(GPIOB, vSCL_Pin, GPIO_PIN_RESET); break; default: HAL_GPIO_WritePin(GPIOB, vSCL_Pin, GPIO_PIN_SET); break; } } //写数据线 void vSDA_Out(uint8_t dat) { switch(dat) { case 0: HAL_GPIO_WritePin(GPIOB, vSDA_Pin, GPIO_PIN_RESET); break; default: HAL_GPIO_WritePin(GPIOB, vSDA_Pin, GPIO_PIN_SET); break; } } //读数据线 uint8_t vSDA_In() { GPIO_PinState PinState; uint8_t rt; PinState = HAL_GPIO_ReadPin(GPIOB, vSDA_Pin); switch(PinState) { case GPIO_PIN_RESET: rt = 0; break; default: rt = 1; break; } return rt; } //启动I2C通信 void I2C_Start() { Pin_vSDA_Mode('O'); vSDA_Out(1); delay_us(6); //至少延时4.7μs vSCL_Out(1); delay_us(6); //至少延时4.7μs vSDA_Out(0); //下降沿 delay_us(6); //至少延时4.7μs vSCL_Out(0); } //停止I2C通信 void I2C_Stop() { Pin_vSDA_Mode('O'); vSDA_Out(0); delay_us(6); //至少延时4.7μs vSCL_Out(1); delay_us(6); //至少延时4.7μs vSDA_Out(1); //上升沿 delay_us(6); //至少延时4.7μs } //发送应答-低电平 void I2C_Ack() { Pin_vSDA_Mode('O'); vSDA_Out(0); delay_us(6); //至少延时4.7μs vSCL_Out(1); delay_us(6); //至少延时4.7μs vSCL_Out(0); delay_us(6); //至少延时4.7μs vSDA_Out(1); delay_us(6); //至少延时4.7μs } //写1字节数据 void I2C_WtByte(uint8_t Dat) { uint8_t i, tmp; Pin_vSDA_Mode('O'); for(i = 0; i < 8; i++) { tmp = Dat & (0x80>>i); //高位在前,低位在后,逐位发送 vSCL_Out(0); delay_us(6); (tmp == 0) ? (vSDA_Out(0)) : (vSDA_Out(1)); delay_us(6); vSCL_Out(1); delay_us(6); } vSCL_Out(0); delay_us(6); vSDA_Out(1); delay_us(6); } //读1字节数据 uint8_t I2C_RdByte() { uint8_t Dat = 0, tmp, i; Pin_vSDA_Mode('I'); vSCL_Out(0); delay_us(6); for(i = 0; i < 8; i++) { vSCL_Out(1); delay_us(6); tmp = vSDA_In(); Dat = Dat << 1; //读1位左移1位 Dat = Dat | tmp; delay_us(6); vSCL_Out(0); delay_us(6); } return Dat; } #endif /* VI2C_H_ */
打开“AT24C02.h”,添加代码如下。
//AT24C02操作 #ifndef AT24C02_H_ #define AT24C02_H_ #define AT24C02_ADDR 0xa0 #include "main.h" #include "vI2C.h" //写入1字节 void AT24C02_Write(uint8_t DatAddr, uint8_t Dat) { I2C_Start(); //主机发送启动通信信号 I2C_WtByte(AT24C02_ADDR + 0); //发送器件(芯片)地址 I2C_Ack(); //产生应答响应 I2C_WtByte(DatAddr); //发送字地址 I2C_Ack(); //产生应答响应 I2C_WtByte(Dat); //发送数据 I2C_Ack(); //产生应答响应 I2C_Stop(); //发送停止通信信号 } //读取1字节 uint8_t AT24C02_Read(uint8_t DatAddr) { uint8_t Dat; I2C_Start(); //主机发送启动通信信号 I2C_WtByte(AT24C02_ADDR + 0); //发送器件地址 I2C_Ack(); //产生应答响应 I2C_WtByte(DatAddr); //发送字地址 I2C_Ack(); //产生应答响应 I2C_Start(); //再次发送启动通信信号 I2C_WtByte(AT24C02_ADDR + 1); //发送器件地址 I2C_Ack(); //产生应答响应 Dat = I2C_RdByte(); //读取数据 I2C_Stop(); //产生非应答信号,发送停止通信信号 return Dat; } #endif /* AT24C02_H_ */
-
随后我们需要在main.c文件中的最前面引入我们自定义的头文件
/* USER CODE BEGIN Includes */ #include "vI2C.h" //引用I2C总线时序模拟头文件 #include "AT24C02.h" //引用AT24C02操作头文件 /* USER CODE END Includes */
在main函数中定义一些全局变量
/* USER CODE BEGIN PV */ uint8_t RcvDat[1]; //存放接收数据数组 uint8_t SndDat[1]; //存放发送数据数组 uint8_t rf = 0; //接收完成标志位 /* USER CODE END PV */
进行串口相关操作
/* USER CODE BEGIN 2 */ HAL_UART_Receive_IT(&huart1, RcvDat, 1); //串口1接收中断 /* USER CODE END 2 */
/* USER CODE BEGIN WHILE */ while (1) { if(rf==1) //若接收完成 { rf = 0; //清0标志位 AT24C02_Write(0, RcvDat[0]); //写入1字节 HAL_UART_Receive_IT(&huart1, RcvDat, 1); //每次接收前需调用一次 } else if(HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET) //若按下按键 { SndDat[0] = AT24C02_Read(0); //读1字节数据,并存入数组 HAL_UART_Transmit(&huart1, SndDat, 1, 0xffff); //串口1发送1字节,超时65535ms while(HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET); //直到按键松开 } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */
/* USER CODE BEGIN 4 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) //串口接收完毕回调函数 { if(huart == &huart1) { rf = 1; //若接收完成,则标志位置1 } } /* USER CODE END 4 */
联合调试
-
点击运行,生成HEX文件。
-
在Proteus中加载相应HEX文件,点击运行。
-
打开串口调试助手“XCOM”,选择
COM4
,设置相应的波特率、停止位、数据位、奇偶校验等,勾选“16进制显示”和“16进制发送”,点击“打开串口”。在发送框输入“CD”,点击“发送”。在Proteus中我们可以看到“I2C Debug”接收到数据“CD”。按下按键,同时再观察串口调试助手“XCOM”,可以看到接收窗口收到数据“CD”。