STM32F407 —— 硬件 I2C 驱动的步骤与应用
此篇文章将从理论到实践,对 I2C 通信方式进行理论介绍与实际应用。理论部分主要讲述 I2C 总线的物理结构与其协议中的相关规定及几种不同的通信过程。实践部分将以我们各种开发场合常用到的一个显示模块——4线 0.96寸 OLED 显示屏和本人正在进行的大创项目中用到的 MS5837 压力传感器为例,对在 STM32F407 上如何用 I2C 协议驱动模块进行介绍。这是本人(仅仅是一位刚入门嵌入式的平平无奇的大学生)在 CSDN 的第一篇博文,旨在自己总结相关知识,如有错误,欢迎各位技术大牛批评指正!本人VX:Cyy15880234628。如果能帮到他人,那再好不过啦!
一、I2C相关知识
1、物理层
(1) 定义
IIC 是 Inter-Integrated Circuit 的缩写,即两线式串行总线,有数据线 SDA 和时钟线 SCL 构成的串行总线,可进行数据的收发。总线结构如图所示:
(2) 通信方式
由定义可知,其数据线只有一条,所以 I2C 通信采用的是半双工通信,即数据在数据线上可以进行双向传输,但是不能同时收发。(可以想象成一根水管,水可以从左端流向右端,也可以从右端流向左端,但不能同时即从左流向右,又从右流向左,不可兼得哦!)
2、协议层
在协议层中,我们先对通信过程中所用到的几个名词所对应的相关规定做个了解。
(我也只懂这么多,哈哈哈哈,毕竟 I2C 协议不是一天两天读得完的。)
(1) 空闲状态
SDA 与 SCL 两条信号线同时处于高电平,这时规定总线处于空闲状态。
(2) 起始信号与停止信号的定义
起始信号: SCL 为高电平时,SDA 由高电平跳变为低电平;
停止信号: SCL 为高电平时,SDA 由低电平跳变为高电平。
光看文字解释似乎很抽象,来,咱们直接上图(本人自己画的丑图)!
start 表示起始信号,stop 表示停止信号,其实起始信号与停止信号的定义就是用 SDA 与 SCL 的不同组成来表示罢了。(当然,我对这俩规定的理解是,可以把SDA 想象成一个开关的刀,下,即合上,开始传输;上,即断开,停止传输。)
(3) 应答信号
发送器发生一个字节,在第九个时钟脉冲期间释放总线,接收器反馈一个应答信号。而应答信号又可分为两种,①有效应答(ACK):应答信号为低电平;②无效应答(NACK):应答信号为高电平。
(4) 数据传输
每位数据都有一个时钟脉冲来同步控制,即在 SCL 串行时钟配合下,SDA 逐位地串行传送每一位数据,传输是边沿出发的。
(5) 数据有效性
怎样的数据在传输过程中才算有效呢?在时钟信号高电平期间,数据线上的数据保持稳定。(或许又不好理解,那再上图!)
3、I2C 基本读写过程
主要包括三种通信过程:主机写数据到从机、主机读取从机数据、复合通信(既读又写)。在应用中,主要是前两种通信过程,复合通信很少见,这里我也只介绍前两种,后面驱动实战部分也是对这两种分别举例展示。
(1) 主机写数据到从机
通信过程如下图所示:
(有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。)
首先,主机产生一个起始信号,开始传输数据。接着,主机向从机发送地址位和读写位(0),确定是哪个从机要接收主机发送的数据。之后就开始从主机向从机发送有效数据,从机根据接收到的数据返回有效应答或无效应答。最后主机产生一个停止信号,表示此次传输结束。
整个过程就是这样的,我不知道自己是否表述清楚,其实大家也可以类比送快递的一个过程,要知道送去谁家,送什么东西,签收时给一个反馈,在淘宝确认收货表示派送成功,过程终止。这样可能形象一点。
(2) 主机读从机数据
通信过程如下图所示:(有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。)
大体过程与主机写数据到从机上差不多,大家自行类比理解哈,要注意的点就是数据和应答信号是谁发给谁。
二、实战应用
1、I2C 驱动 4线 0.96寸 OLED 显示屏
这个模块相信大家并不陌生,小型而又低功耗,运用范围十分广泛。现在关于这个模块在STM32F1系列上的实现源码很多,但在STM32F4系列上的实现源码很少,还是有很多人不懂得移植或者移植不成功,也当作是一个半开源了吧。
话不多说,开始讲思路!
其实就是按照前面所讲的对应的通信过程和相关协议规定来配置,再配合OLED的数据手册,编写移植显示的函数即可。
驱动该模块的流程图就是主机写数据到从机的通信过程,再放一遍图吧(把良心打在公屏上)
通信过程的配置就按照下面这部分代码配置即可,注释个人觉得挺清楚的了。
/*********************OLED写数据************************************/
void OLED_WrDat(unsigned char IIC_Data)
{
IIC_Start(); //起始信号
IIC_Send_Byte(0x78); //确定从机地址
IIC_Wait_Ack();
IIC_Send_Byte(0x40); //确定读写位,写为0
IIC_Wait_Ack(); //从机应答
IIC_Send_Byte(IIC_Data); //开始写数据到从机上
IIC_Wait_Ack(); //从机应答
IIC_Stop(); //停止信号
}
起始信号的定义:
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA=1;
IIC_SCL=1;
delay_us(2);
IIC_SDA=0; //START:when CLK is high,DATA change form high to low
delay_us(2);
IIC_SCL=0; //钳住I2C总线,准备发送或接收数据
}
停止信号的定义:
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL=0;
IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
delay_us(2);
IIC_SCL=1;
IIC_SDA=1;//发送I2C总线结束信号
delay_us(2);
}
等待应答信号:
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
IIC_SDA=1;delay_us(1);
IIC_SCL=1;delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;//时钟输出0
return 0;
}
有效应答与无效应答的定义:
//产生ACK应答
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(4);
IIC_SCL=1;
delay_us(4);
IIC_SCL=0;
}
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(4);
IIC_SCL=1;
delay_us(4);
IIC_SCL=0;
}
根据数据手册又可编写一些显示函数,如下:
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 Char_Size)
{
unsigned char c=0,i=0;
c=chr-' ';//得到偏移后的值
if(x>128-1){x=0;y=y+2;}
if(Char_Size ==16)
{
OLED_Set_Pos(x,y);
for(i=0;i<8;i++)
OLED_WrDat(F8X16[c*16+i]);
OLED_Set_Pos(x,y+1);
for(i=0;i<8;i++)
OLED_WrDat(F8X16[c*16+i+8]);
}
else {
OLED_Set_Pos(x,y);
for(i=0;i<6;i++)
OLED_WrDat(F6x8[c][i]);
}
}
//m^n函数
u32 oled_pow(u8 m,u8 n)
{
u32 result=1;
while(n--)result*=m;
return result;
}
//显示2个数字
//x,y :起点坐标
//len :数字的位数
//size:字体大小
//mode:模式 0,填充模式;1,叠加模式
//num:数值(0~4294967295);
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size2)
{
u8 t,temp;
u8 enshow=0;
for(t=0;t<len;t++)
{
temp=(num/oled_pow(10,len-t-1))%10;
if(enshow==0&&t<(len-1))
{
if(temp==0)
{
OLED_ShowChar(x+(size2/2)*t,y,' ',size2);
continue;
}else enshow=1;
}
OLED_ShowChar(x+(size2/2)*t,y,temp+'0',size2);
}
}
//显示一个字符号串
void OLED_ShowString(u8 x,u8 y,u8 *chr,u8 Char_Size)
{
unsigned char j=0;
while (chr[j]!='\0')
{ OLED_ShowChar(x,y,chr[j],Char_Size);
x+=8;
if(x>120){x=0;y+=2;}
j++;
}
}
//------------显示中文------------------
void OLED_ShowCHinese(u8 x,u8 y,u8 no)
{
u8 t,adder=0;
OLED_Set_Pos(x,y);
for(t=0;t<16;t++)
{
OLED_WrDat(Hzk[2*no][t]);
adder+=1;
}
OLED_Set_Pos(x,y+1);
for(t=0;t<16;t++)
{
OLED_WrDat(Hzk[2*no+1][t]);
adder+=1;
}
}
当然,要使用该模块之前要记得对其进行初始化哦!初始化函数如下:
/*********************OLED初始化************************************/
void OLED_Init(void)
{
delay_ms(500);//初始化之前的延时很重要!
OLED_WrCmd(0xae);//--turn off oled panel
OLED_WrCmd(0x00);//---set low column address
OLED_WrCmd(0x10);//---set high column address
OLED_WrCmd(0x40);//--set start line address Set Mapping RAM Display Start Line (0x00~0x3F)
OLED_WrCmd(0x81);//--set contrast control register
OLED_WrCmd(Brightness); // Set SEG Output Current Brightness
OLED_WrCmd(0xa1);//--Set SEG/Column Mapping 0xa0左右反置 0xa1正常
OLED_WrCmd(0xc8);//Set COM/Row Scan Direction 0xc0上下反置 0xc8正常
OLED_WrCmd(0xa6);//--set normal display
OLED_WrCmd(0xa8);//--set multiplex ratio(1 to 64)
OLED_WrCmd(0x3f);//--1/64 duty
OLED_WrCmd(0xd3);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)
OLED_WrCmd(0x00);//-not offset
OLED_WrCmd(0xd5);//--set display clock divide ratio/oscillator frequency
OLED_WrCmd(0x80);//--set divide ratio, Set Clock as 100 Frames/Sec
OLED_WrCmd(0xd9);//--set pre-charge period
OLED_WrCmd(0xf1);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
OLED_WrCmd(0xda);//--set com pins hardware configuration
OLED_WrCmd(0x12);
OLED_WrCmd(0xdb);//--set vcomh
OLED_WrCmd(0x40);//Set VCOM Deselect Level
OLED_WrCmd(0x20);//-Set Page Addressing Mode (0x00/0x01/0x02)
OLED_WrCmd(0x02);//
OLED_WrCmd(0x8d);//--set Charge Pump enable/disable
OLED_WrCmd(0x14);//--set(0x10) disable
OLED_WrCmd(0xa4);// Disable Entire Display On (0xa4/0xa5)
OLED_WrCmd(0xa6);// Disable Inverse Display On (0xa6/a7)
OLED_WrCmd(0xaf);//--turn on oled panel
OLED_Fill(0x00); //初始清屏
OLED_Set_Pos(0,0);
}
这样,OLED 显示屏就配置好了,我调试过了,亲测可用。
2、I2C 驱动 MS5837 压力传感器
由于大创需要,这个传感器可是我找了半天才找到符合需求的模块。也许大家用不到,但其驱动原理与其他模块是一样的。
该模块的驱动与OLED类似,只需要改写下地址位和读写位即可。
unsigned long MS583703BA_getConversion(uint8_t command)
{
unsigned long conversion = 0;
u8 temp[3];
IIC_Start();
IIC_Send_Byte(0xEC); //写地址
IIC_Wait_Ack();
IIC_Send_Byte(command); //写转换命令
IIC_Wait_Ack();
IIC_Stop();
delay_ms(10);
IIC_Start();
IIC_Send_Byte(0xEC); //写地址
IIC_Wait_Ack();
IIC_Send_Byte(0); // start read sequence
IIC_Wait_Ack();
IIC_Stop();
IIC_Start();
IIC_Send_Byte(0xEC+0x01); //进入接收模式
IIC_Wait_Ack();
temp[0] = IIC_Read_Byte(1); //带ACK的读数据 bit 23-16
temp[1] = IIC_Read_Byte(1); //带ACK的读数据 bit 8-15
temp[2] = IIC_Read_Byte(0); //带NACK的读数据 bit 0-7
IIC_Stop();
conversion = (unsigned long)temp[0] * 65536 + (unsigned long)temp[1] * 256 + (unsigned long)temp[2];
return conversion;
}
关于 I2C 相关内容我就介绍这么多,也许文中有很多表述不清楚,还请大家指出或者联系我,大家一起进步,一起向嵌入式攻城狮迈进!!!