这几天一直在学习iic从理论到实操,尝试这自己写iic的stm32驱动程序,今天终于成功的实现使用iic协议发送数据和读数据。驱动程序涉及stm32的位带操作,关于位带操作我写在上一篇博客:stm32位带操作。整个工程托管在github:点击打开链接
一、要点
1.iic总线只有两根双向信号线。一根是数据线SDA,另一根是时钟线SCL,所有操作都靠这两根线完成。
2.iic总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。参考图1可以看出SDA高低电平的变化都是在SCL低电平期间。
图一
二、时序图详解
要写的驱动程序包括起始信号、终止信号、主机读取数据的应答信号、主机读取最后一个数据的非应答信号、数据发送、数据读取。
1.起动和停止驱动程序
SCL信号线为高电平期间,SDA线由高电平向低电平变化表示起始信号,如图所示:
图二
/*
函数名:iic_start
功 能:启动iic,启动方式,在SCL高电平期间将SDA由高置低
参 数:无
返回值:无
*/
void iic_start(void)
{
IIC_SDA_OUT();//设置SDA口为输出
SDA_PIN_OUT=1;//因为启动要求从高到低变化所以我们先把SDA置高
delay_us(5); //
SCL_PIN_OUT=1;//SDA要在SCL高电平期间变化,由图中阴影部分可以看出
delay_us(5);
SDA_PIN_OUT=0;//SDA置低
delay_us(5);
}
/*
函数名:iic_stop
功 能:停止传输数据,实现方式在SCL高电平期间将SDA由低置高
参 数:无
返回值:无
*/
void iic_stop(void)
{
IIC_SDA_OUT();//因为stm32IO有个输出方向问题,所以要先设置IO口输出方向,本代码采用位带操作
SDA_PIN_OUT=0;//终止信号要求SDA在SCL高电平期间由低电平置高电平,因此先把SDA拉低
delay_us(5);
SCL_PIN_OUT=1;//SCL置高电平
delay_us(5);
SDA_PIN_OUT=1;//拉高SDA实现终止信号
delay_us(5);
}
2.等待从机应答驱动代码
数据传送格式:每一个字节必须保证时8位长度。数据传送时,先传高位,每个被传送的字节后面必须跟随一位应答位,在主机向从机传送数据时应答位由从机产生,主机检测从机产生的应答信号,如果从机应答时间超时,主机默认数据传输完成,本段代码就是检测从机的应答信号。
图三
/*
函数名:iic_ack
功 能:接收从机应答信号,释放掉总线读取SDA置看是否有负脉冲,
当一段时间无应答默认接收完毕
参 数:无
返回值:无
*/
u8 iic_wait_ack(void)
{
u8 i=0;
IIC_SDA_IN(); //SDA设置为输入
SDA_PIN_OUT=1;delay_us(5); //SCL和SDA置高同时释放总线,留给从机产生应答信号。
SCL_PIN_OUT=1;delay_us(5);
while(SDA_PIN_IN) //检测从机是否产生负脉冲,或者从机超时,主机默认从机成功接收数据
{
i++;
if(i>250)
{
iic_stop(); //如果i>255则产生非应答信号,iic停止
return 1;
}
}
SCL_PIN_OUT=0;//时钟输出0
return 0;
}
3.主机读数据产生的应答信号或非应答信号
主机读数据时有这样的规定:当主机每读一个数据主机都要回复应答信号。当最后一个字节数据读完后,主机要返回以“非应答”,并终止信号读出操作,这里的应答信号和非应答信号作用是给从机判断是否继续读取数据。由图可以看出应答信号是在SDA低电平期间,SCL由低置高再置低。非应答信号是在SDA位高电平期间SCL由低置高再置低。
图4
/*
函数名:iic_ask
功 能:产生ask应答
参 数:无
返 回:无
*/
void iic_ack(void)
{
IIC_SDA_OUT();
SCL_PIN_OUT=0;
delay_us(5);
SDA_PIN_OUT=0;//产生一正个脉冲
delay_us(5); //
SCL_PIN_OUT=1;//
delay_us(5);
SCL_PIN_OUT=0;
}
/*
函数名:iic_nask
功 能:产生非应答信号
参 数:无
返 回:无
*/
void iic_nack(void)
{
IIC_SDA_OUT();
SCL_PIN_OUT=0;
delay_us(5);
SDA_PIN_OUT=1;
delay_us(5);
SCL_PIN_OUT=1;
delay_us(5);
SCL_PIN_OUT=0;
}
4.字节传送
字节传送分为发送和读取,主要根据图三的时序图进行编写,代码如下:
/*
函数名:iic_bit_write
功 能:传送一个字节
参 数:u8
返回值:无
*/
void iic_byte_write(u8 buf)
{
u8 i;
IIC_SDA_OUT();
for(i=1;i<=8;i++)
{
SCL_PIN_OUT=0;
delay_us(5);
SDA_PIN_OUT=(buf>>(8-i))&0x01;//先传高位再传地位
delay_us(5);
SCL_PIN_OUT=1;
delay_us(5);
}
SCL_PIN_OUT=0;
}
/*
函数名:iic_bit_read
功 能:主机读取一个字节,ask参数主要用来发送信号给从机判断主机还需不需要继续读取字节,如果主机发送应答信号表示继续读取,反之则说明已经读完最后一个字节。
参 数:ask
返回值:
*/
u8 iic_byte_read(unsigned char ask)
{
unsigned char i,buf=0;
IIC_SDA_IN();
for(i=0;i<8;i++)
{
SCL_PIN_OUT=0;
delay_us(5);
SCL_PIN_OUT=1;
buf <<=1;
if(SDA_PIN_IN)
buf++;
delay_us(5);
}
if(!ask)
iic_nack();
else
iic_ack();
return buf;
}
5.综合调试
(1)写操作:在main()函数中实现写一个字节到从机中,然后在都出来用串口打印。从机使用eeprom AT24c系列,单片机在写操作时,应该先发送器件地址,入下图所示,其中器件地址为7位第8位为数据传送方向设置,途中非影音部分代表从机向主机传送数据的过程,A表应答的意思,S为启动信号,传送数据时,单片机首先发送一个字节的被写入器件的存储区首地址,收到存储器器件的应答后,单片机就逐个发送数据字节,但每个字节都要等待应答。当要写入的数据传送完成后,单片机应发送终止信号以结束写入操作。
图5
(2)读操作
单片机先发送该器件的7位地址码和写方向位“0”(“伪写”),发送完后释放SDA线并在SCL线上产生第9个时钟信号。被选中的存储器器件在确认是自己的地址后,在SDA线上产生一个应答信号作为回应。然后,再发一个字节的要读出器件的存储区的首地址,收到应答后,单片机要重复一次起始信号并发出器件地址和读方向位(“1”),收到器件应答后就可以读出数据字节,每读出一个字节,单片机都要回复应答信号。当最后一个字节数据读完后,单片机应返回以“非应答”(高电平),并发出终止信号以结束读出操作
图6
int main(void)
{
u8 iic_buf=0;
USART1_Config();
delay_init();
iic_init();
iic_start();
iic_byte_write(0xa0);//写器件地址
iic_wait_ack();
iic_byte_write(3); //写存储器地址
iic_wait_ack();
iic_byte_write('a'); //写存储的数据
iic_wait_ack();
iic_stop(); //停止总线
delay_ms(10);
iic_start(); //读刚才写的数据
iic_byte_write(0xa0); //先写器件地址
iic_wait_ack();
iic_byte_write(3); //在写要读取的存储器地址
iic_wait_ack();
iic_start(); //重启一次iic
iic_byte_write(0xa1); //把传递方向设为读取,前7位为器件地址,最后一位为判断传递方向
iic_wait_ack();
iic_buf=iic_byte_read(0);//读取数据
iic_stop();
while(1)
{
printf("%c",iic_buf); //这里利用stm32串口直接打印
delay_ms(500);
};
}
整个工程亲测可用。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。