学习IIC通讯协议之前,我们回忆一下之前学习的第一个通信协议USART串口。
串口通信协议的特点 是串行 的、 异步 的、 半双工 通信。
【问】串口通信有几根线?
两根:TX和RX,一条收,一条发。
本章学习的IIC通信协议的特点是串行的、同步的、半双工的。
IIC是同步的,说明它必须有一条时钟线SCL,另外数据线有几条呢?
只有一条SDA数据线,所以决定了他是半双工的,来看一下图示:
如图:设备有主机STM32和若干从机,每个连接到总线的从机都有唯一的地址,任何器件也都可以作为主机,但是同一时刻只能拥有一个主机。
【问】之前在GPIO模式,讲到开漏输出的时候说到IIC特别适合开漏输出,为什么呢?
这是由IIC总线内部结构决定的,总线内部使用漏极开漏输出驱动器,所以SDA和SCL两根线都使用一个上拉电阻(上图),使用的时候就拉低为低电平,但是不能被驱动为高电平,平时为高阻态。
PS:这个上拉值有一个典型值4.7KΩ。
【问】那么IIC的通信过程是什么样的??
一般分为四个部分:
我觉得大家可以思考一下,如果只给你两根线,一根时钟线一根数据线,条件是如此的艰难,那么要实现主机对从从机,或者是从机对主机的选择,以及数据的发送,数据的接收,甚至还需要应答机制,不然对方怎么知道数据收没收到??你会怎么做????

数据帧格式,对应上方的通信过程。



最后详细叙述一下通讯过程:
1.开始:主设备把SDA从高拉低,再把SCL从高拉低,对总线上的从机说:我要开始和你们中的某个人通信了。
2.主设备发送要与之通信的从机的7位地址(一般是7位),第八位是读写位(读1写0)
3.总线上的从设备把主设备发送的地址与自己的地址比较,如果匹配:从设备将SDA拉低一位表示应答(此时从设备获得SDA的控制权,之前SDA都是主设备控制的)。如果不拉低,SDA为高就表示非应答。
4.主设备发送(或接收)数据给从设备。SCL为高,读(写)数据,SCL为低,准备下一位的数据。
5.数据传输完毕,从设备返回一个应答给主设备
6.停止:主设备把SCL从低拉高,再把SDA从低拉高,表示停止通信。
了解了通信协议,我们具体怎么使用呢?
这里我们使用软件模拟IIC通信协议,软件模拟I2C不需要额外的硬件支持,只需要使用微控制器的GPIO引脚和软件实现即可。此外灵活性和可移植性非常高。
我们一步步来,这章使用的是GD32F1103,但其实和STM32是完全一样的。
首先我们要控制SDA和SCL的电平,就要封装一个写引脚SDA和SCL 1或0 的函数:
(IIC的引脚都宏定义了,函数基本和STM32一样的)

【起始条件】SDA从高拉低,再把SCL从高拉低
【结束条件】SCL从低拉高,再把SDA从低拉高
【应答】从设备将SDA拉低表示应答

【非应答】从设备将SDA拉高表示非应答

【主机发送一个字节数据给从机】

【主机读从机一个字节数据】

【主机读应答】
这个读SDA也就是把 gpio_input_bit_get 封装了一下:
那么软件模拟IIC已经被我们封装好了,接下来做实验。
首先硬件连接: PB6 -- SCL PB7 -- SDA
实验目的: 温度寄存器的值拿出来转化为浮点型的数值,通过串口发送到主机

第一步不用想也是初始化
- #define I2C_SOFT_RCU RCU_GPIOB
- #define I2C_SOFT_PORT GPIOB
- #define I2C_SOFT_SCL_PIN GPIO_PIN_6
- #define I2C_SOFT_SDA_PIN GPIO_PIN_7
-
- //初始化函数
- void my_i2c_init(void){
- //打开IIC时钟
- rcu_periph_clock_enable(I2C_SOFT_RCU);
-
- //初始化GPIO
- gpio_init(I2C_SOFT_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ, I2C_SOFT_SCL_PIN|I2C_SOFT_SDA_PIN);
-
- //GPIO引脚置1(高阻态)
- gpio_bit_set(I2C_SOFT_PORT, I2C_SOFT_SCL_PIN|I2C_SOFT_SDA_PIN);
- }
首先封装一个函数,用来向IIC总线上写入器件地址以及要用到的温度寄存器地址
- uint8_t lm75a_write_addr(uint8_t id_rw, uint8_t reg_addr){
- my_i2c_start();
- my_i2c_send_byte(id_rw);
- my_i2c_read_ack();
- my_i2c_send_byte(reg_addr);
- my_i2c_read_ack();
-
- return 0;
- }
然后就是读温度寄存器的值。
- #define LM75A_I2C_ADDR 0x9E //LM75A的从机地址
- #define LM75A_TEMP_REG 0x00 //温度寄存器的指针地址
-
- #define IIC_WRITE 0
- #define IIC_READ 1
-
- /***
- 功能:读温度寄存器的值
- 输入:
- uint8_t lm75a_id: lm75a的iic从机地址
- uint8_t reg:要操作的寄存器的指针
- uint8_t *p:读取结果存放的位置
- uint8_t len:寄存器的字节长度(1 or 2)
- 返回:无
- *****/
- void lm75a_read_reg(uint8_t lm75a_id, uint8_t reg, uint8_t *p, uint8_t len){
- //向iic总线上写入器件地址、指针字节
- lm75a_write_addr(lm75a_id|IIC_WRITE, reg);
- my_i2c_start();
- my_i2c_send_byte(lm75a_id|IIC_READ);
- my_i2c_read_ack();
-
- uint8_t i;
-
- for(i = 0; i < len; i++){
- *p++ = my_i2c_read_byte();
- if(i != (len-1))
- my_i2c_ack();
- }
- my_i2c_nack();
-
- my_i2c_stop();
- }
详细如下图:

- // 读温度传感器的温度寄存器的值并转换为温度值
- float lm75a_get_temp(void){
- float temp_result;
- //读温度寄存器值
- uint8_t byte_data[2];
- lm75a_read_reg(LM75A_I2C_ADDR, LM75A_TEMP_REG, byte_data, 2);
-
- //将温度寄存器值转为温度值
- uint16_t temp_reg = byte_data[0]<<3 | byte_data[1]>>5;
-
- if((temp_reg & 0x0400) == 0){
- temp_result = temp_reg * 0.125;
- }else{
- temp_reg = (~((temp_reg&0x03ff)-1)) & 0x03ff; //补码到原码转换
- temp_result = temp_reg * (-0.125);
- }
-
- return temp_result;
- }

查阅数据手册,得到温度寄存器和温度值的转换方法:
判断这个寄存器的第十位是0还是1,对应着不同的计算方法:
- lm75a_init();
-
- while(1){
- temp_result = lm75a_get_temp();
- sprintf(temp_string, "temperature is: %.3f C.\n", temp_result);
- usart0_send_string((uint8_t *)temp_string);
- delay_1ms(1000); //等待1s
- }