本文介绍如何使用STM32标准外设库的GPIO端口模拟IIC,本例程使用PB6和PB7模拟一路IIC。
本文适合对单片机及C语言有一定基础的开发人员阅读,MCU使用STM32F103VE系列。
1. 简介
IIC (Inter-Integrated Circuit)总线,也可写作I2C,是PHILIPS 公司开发的两线式串行总线,用于多设备之间通讯,分为主机Master和从机Slave,主机和从机可以有多个,但一般情况下只有一个主机,从机之间可以通过地址进行区分,不同种类的设备地址不同,如果同时接入多个相同种类的设备,可以通过片选信号对从机进行选择。通讯只能由主机发起,支持的操作分为读取和写入,即主机读取从机的数据,以及向从机写入数据。
I2C两线分别是时钟线SCL和数据线SDA,其中SCL和SDA均由主机控制,可以设置成开漏输出模式。
2. 协议说明
2.1. 总线传输信号
空闲状态:IIC空闲状态时SCL和SDA均输出高电平,初始状态以及发送结束信号之后均为空闲状态。
开始信号:START,简写S,SCL为高电平,SDA由高电平向低电平跳变。
结束信号:STOP,简写P,SCL为高电平,SDA由低电平向高电平跳变。
从机地址:SLAVE_ADDRESS,每种从机都有一个表示该设备的地址,地址一般为7位,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机,紧跟设备地址的一个数据位用来表示数据传输方向(R/W位),数据方向位为“1”时表示主机由从机读数据,数据方向位为“0”时表示主机向从机写数据。从机接收到匹配的地址后,会返回一个应答(ACK)信号,只有接收到应答信号后,主机才能继续发送或接收数据。
响应信号:ACK/NACK,简写A,响应包括应答(ACK)和非应答(NACK),当数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,即SDA为低电平,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,即SDA为高电平,发送方接收到该信号后会产生一个停止信号,结束信号传输。
主机写入:SCL为高电平时,SDA有效,SDA为输出模式,主机控制SDA输出高电平时表示写入1,SDA输出低电平时表示写入0。
主机读取:SCL为高电平时,SDA有效,SDA为输入模式,主机读取SDA高电平时表示输入1,SDA低电平时表示输入0。此时主机释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号。
2.2. 基本读写过程
写数据:主机先发一个开始信号(S),然后发送从机地址(SLAVE ADDRESS),后续跟上写信号(R/W位为0),然后等待从机的应答信号(ACK位为0),主机向从机传输数据(DATA),数据包为1个字节共8位,从高位到低位依次发送,主机每发送完1个字节数据,都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输 N 个数据,当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。
读数据:主机先发一个开始信号(S),然后发送从机地址(SLAVE ADDRESS),后续跟上读信号(R/W位为1),然后等待从机的应答信号(ACK位为0),主机从从机读取数据(DATA),数据包为1个字节共8位,从高位到低位依次发送,从机每发送完1个字节数据,都要等待主机的应答信号(ACK),重复这个过程,可以从从机读取 N 个数据,当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输,主机再向从机发送一个停止传输信号(P),表示不再传输数据。
读和写数据:除了基本的读写,I2C 通讯还有一种是复合读写模式,该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。
2.3. 速度模式
标准模式传输速率为100kbit/s,即10us可以传输一个bit,如果用GPIO模拟I2C时电平变换时需要增加适当的延时。
3. 初始化
初始化跟普通GPIO类似,只是输出模式设置为开漏输出。
其中SCL始终输出信号,但SDA需要支持输出信号和读取信号,当设置为开漏输出时,如果需要输出信号,则正常输出即可,如果需要读取信号,则MCU将SDA输出高电平,此时如果从机输出低电平,则SDA被拉低,此时MCU可以读到低电平。即GPIO引脚为开漏输出模式时,MCU输出高电平时,即释放了该引脚的控制,此时该引脚的电平取决于从机的输出,且MCU仍可以读取该引脚的电平。
GPIO初始化完成之后,可以将SCL和SDA置为高电平,即释放该引脚的控制,如果总线上有多个主机,则不会干扰其他设备的通讯。
4. 信号模拟
需要按照通讯信号的时序,实现START、STOP、ACK、NACK、Read、Write和WaitAck信号。
完整代码(仅自己编写的部分)
1 #define IIC_SCL_1 GPIO_SetBits(GPIOB, GPIO_Pin_6) /* SCL = 1 */
2 #define IIC_SCL_0 GPIO_ResetBits(GPIOB, GPIO_Pin_6) /* SCL = 0 */
3
4 #define IIC_SDA_1 GPIO_SetBits(GPIOB, GPIO_Pin_7) /* SDA = 1 */
5 #define IIC_SDA_0 GPIO_ResetBits(GPIOB, GPIO_Pin_7) /* SDA = 0 */
6
7 #define IIC_READ_SDA() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) /* 读SDA口线状态 */
8
9 //初始化IIC
10 void IIC_Init(void)
11 {
12 GPIO_InitTypeDef GPIO_InitStructure;
13
14 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
15
16 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
17 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ; //开漏输出
18 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
19 GPIO_Init(GPIOB, &GPIO_InitStructure);
20
21 IIC_Stop();
22 }
23
24 //产生IIC起始信号
25 //SCL为高电平时SDA由高变低
26 /*
27 SCL: ̄ ̄ ̄ ̄ ̄\_
28 SDA: ̄ ̄\____
29 */
30 void IIC_Start(void)
31 {
32 IIC_SDA_1;
33 IIC_SCL_1;
34 delay_us(4);
35 IIC_SDA_0;
36 delay_us(4);
37 IIC_SCL_0;
38 }
39
40 //产生IIC停止信号
41 //SCL为高电平时SDA由低变高
42 //IIC空闲时SCL和SDA均输出高电平,这样不会干扰其他设备的收发
43 /*
44 SCL: ̄ ̄ ̄ ̄
45 SDA:__/ ̄
46 */
47 void IIC_Stop(void)
48 {
49 IIC_SDA_0;
50 IIC_SCL_1;
51 delay_us(4);
52 IIC_SDA_1;
53 }
54
55 //等待应答信号到来
56 //返回值:1,接收应答失败
57 // 0,接收应答成功
58 uint8_t IIC_WaitAck(void)
59 {
60 uint8_t errCount = 0;
61 uint8_t ack = 0;
62
63 IIC_SDA_1;
64 delay_us(4);
65 IIC_SCL_1;
66 delay_us(4);
67
68 while(IIC_READ_SDA())
69 {
70 errCount++;
71 if(errCount > 250){
72 ack = 1;
73 break;
74 }
75 }
76 IIC_SCL_0;
77
78 return ack;
79 }
80
81 //产生应答ACK
82 //SCL为高电平时SDA为低电平表示应答
83 /*
84 SCL:  ̄ ̄\____
85 SDA:_______/ ̄
86 */
87 void IIC_Ack(void)
88 {
89 IIC_SDA_0;
90 delay_us(4);
91 IIC_SCL_1;
92 delay_us(4);
93 IIC_SCL_0;
94 delay_us(4);
95 IIC_SDA_1; //释放SDA
96 }
97
98 //产生非应答NACK
99 //SCL为高电平时SDA为高电平表示非应答
100 /*
101 SCL:  ̄ ̄\__
102 SDA: ̄ ̄ ̄ ̄ ̄ ̄ ̄
103 */
104 void IIC_NAck(void)
105 {
106 IIC_SDA_1;
107 delay_us(4);
108 IIC_SCL_1;
109 delay_us(4);
110 IIC_SCL_0;
111 delay_us(4);
112 }
113
114 //IIC发送一个字节
115 /*
116 SCL:_ _/ ̄\__ _/ ̄ ̄\__ _/ ̄ ̄\__ _/ ̄ ̄\__ _/ ̄ ̄\__ _/ ̄ ̄\__ _/ ̄ ̄\__ _/ ̄ ̄\__
117 SDA:-- ------------ -------------- -------------- -------------- -------------- -------------- -------------- --------------
118 */
119 void IIC_WriteByte(uint8_t txd)
120 {
121 uint8_t i;
122
123 IIC_SCL_0;
124 for(i = 0; i < 8; i++)
125 {
126 (txd & 0x80) ? IIC_SDA_1 : IIC_SDA_0;
127 txd <<= 1;
128
129 delay_us(4);
130 IIC_SCL_1;
131 delay_us(4);
132 IIC_SCL_0;
133 delay_us(4);
134 }
135 IIC_SDA_1;
136 }
137
138 //读1个字节,ack=1时,发送ACK,ack=0,发送NACK
139 /*
140 SCL: ̄ ̄\__ / ̄ ̄\__ / ̄ ̄\__ / ̄ ̄\__ / ̄ ̄\__ / ̄ ̄\__ / ̄ ̄\__ / ̄ ̄\__
141 SDA:========== ============ ============ ============ ============ ============ ============ ============
142 */
143 uint8_t IIC_ReadByte(uint8_t ack)
144 {
145 uint8_t i, rcv = 0;
146
147 for(i = 0; i < 8; i++)
148 {
149 rcv <<= 1;
150 IIC_SCL_1;
151 delay_us(4);
152 if(IIC_READ_SDA()){
153 rcv++;
154 }
155 IIC_SCL_0;
156 delay_us(4);
157 }
158
159 ack ? IIC_Ack() : IIC_NAck();
160
161 return rcv;
162 }