学习板:STM32F103ZET6
在接下来的几篇博客中,总结一下STM32通信方面的几大块内容。由于板子用到24C02这个外设,故IIC的内容以这个外设的参考文档为主,一步步的用代码解释各类时序。
一个建议:代码中各函数的延时一定要自己去调试,初次写代码时延时可以稍微长一点,保证通信能够完成,然后逐渐减小延时,提高通信速度。
通过本博写的代码,完全脱离STM32寄存器的束缚,代码可以移植的51、F4、Arduino等各类单片机。
通过这种完全靠延时调试时序来完成通信的方法,以后也会应用在其他实验中,如OV摄像头、LCD显示,之后有时间会慢慢总结这部分内容。
IIC(Inter Integrated Circuit),顾名思义,是集成电路总线。它是一种串行通信总线。注意读法:“I方C”,平方的“方”,IIC有时候也写成I2C
IIC 用于连接微控制器及其外围设备,它由一条双向传输的数据线SDA和一条时钟线SCL组成。数据线SDA同一时间只能发送或接收,时钟线SCL只能输出,故是半双工通信。
SDA输出时一般采用开漏输出,即需要外接上拉电阻输出1,输出0时直接接GND。当然根据外围电路设计灵活变通,推挽输出也是可以实现的。
这部分内容开始前,先对GPIO和一些基本函数编写。
首先GPIO的配置,定义一个IIC初始化函数,主要对PB6和PB7的初始化,设置为推挽输出即可。
代码:
void IIC_Init()
{
// PB6 SCL
//PB7 SDA
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);
}
传输过程中,由于SDA是双向传输的,所以要时刻改变PB7的模式是输出还是输入,故写俩个设置PB7的函数:
void SDA_IN(void)
{
// PB6 SCL
//PB7 SDA
GPIOB->CRL&=0x0fffffff;
GPIOB->CRL|=0x80000000;
}
void SDA_OUT(void)
{
GPIOB->CRL&=0x0fffffff;
GPIOB->CRL|=0x30000000;//推挽输出
//GPIOB->CRL|=0x70000000;//开漏输出
SCL只有输出,之前IIC_Init()函数中已经设置,不用再设置,程序运行全过程都是输出。
为了程序中方便SDA和SCL置高低电平,采用宏定义:
#define IIC_SDA PBout(7)
#define IIC_SCL PBout(6)
#define IIC_READ_SDA PBin(7)
上图说明了数据传输的有效性,得到以下信息:
①SCL一个时钟周期,传输一个数
IIC通信时每8位为一个传输周期,每个SCL时钟周期传输1位,传输8次完成一个字节传输
②传输完1位数据后,需要将SCL时钟置低电平,即在SCL时钟低电平期间才能进行每1位数据的更替。
③之所以要在SCL时钟低电平期间进行数据更新,是因为在SCL高电平时,如果SDA的传输数据发生跳变,会产生开始信号或停止信号,导致通信出错。
(1)开始
传输开始时,表示SDA为输出模式(只有读时才是输入),先将SDA与SCL置高电平,然后先将SDA置低电平,注意这里SDA从1跳变到0时,图中是一条倾斜的直线,以后碰上这种倾斜直线都要去延时,再将SCL置低电平。触发一个开始信号。
思路:
SDA设置为输出——>SDA与SCL都置1——>SDA置0(+延时)——>SCL置0(+延时)
代码:
void IIC_Start(void)
{
SDA_OUT();//设置为SDA输出
IIC_SDA=1;
IIC_SCL=1; //SDA与SCL都设置为高电平
delay_us(2);//---------------这里延时,待调
IIC_SDA=0;//在SCL高电平期间SDA由高到低跳变,表示开始传送
delay_us(2);
IIC_SCL=0; //注意这里必须置0,否则数据传输时,容易触发开始和停止信号
delay_us(2);
}
(2)停止
先将SDA设置为输出模式,根据上图,将SDA与SCL都设置为低电平,然后SCL置1,SDA再置1 。
思路:
SDA设置为输出模式——>SDA、SCL置0——>SCL置1(+延时)——>SDA置1(+延时)
代码:
void IIC_Stop(void)
{
SDA_OUT();//设置为SDA输出
IIC_SCL=0; //SDA与SCL都设置为低电平
IIC_SDA=0;
delay_us(2);//---------------这里延时,待调
IIC_SCL=1;
delay_us(2);
IIC_SDA=1; //SDA在SCL高电平期间从低电平跳变到高电平
delay_us(2);
}
IIC在每个SCL周期传输1位数,传输8次完成1bit数的传输,完成后会产生一个应答信号。所以SCL的第9个周期为应答信号。
注意下图所示,首先SCL与SDA都置0,然后SCL置高电平,一端时间后,SCL置0,SDA置1(注意应答信号结束时,一定要先将SCL置0再将SDA置1,即使他们是同时发生的。时刻注意只有SCL低电平时SDA才能跳变!!!)
思路:
SDA与SCL都置0(+延时)——>SCL置1、SDA仍保持0(+延时)——>SCL置0、SDA置1(可+延时)
void IIC_Ack()
{
SDA_OUT();
IIC_SCL=0;
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
IIC_SDA=1;
delay_us(2);
}
不产生应答信号函数就是破坏这个应答时序,首先SCL是不能破坏的,因为要识别信号必须要有SCL时钟信号,所以要破坏SDA时序,只需SDA全程高电平即可
代码:
void IIC_NAck(void)
{
SDA_OUT();
IIC_SCL=0;
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
还有就是应答信号等待,发送1bit的数或者接收1bit的数,需要等待发送、接收的完成,判断完成的条件可以根据应答信号来完成。所以写一个等待应答信号的函数。
等待应答的思想就是判断SDA在一定时间内是否跳变为低电平,再跳变为高电平,即下图中标注2。而SCL是通过软件置高低电平实现,只是用来传输数据。为了检测出SDA的电平跳变,要把SDA设置为输入。
还是按照上图的时序,先将SDA设置为低电平(上图红框中的标注1,注意标注2的SDA跳变比SCL跳变早一点点),然后等待SDA低电平的到来,在while循环中,如果SDA一直处于高电平,一段时间后,没有等到低电平的到来,表示没有等到应答,结束本函数,并返回1 。如果等到SDA低电平的到来,跳出while循环(标注2的位置),然后将SCL置高电平,延时后(标注3,SDA=0、SCL=1),再将SDA置1、SCL置0。
代码:
u8 IIC_Wait_Ack()//等待应答,成功返回0,失败返回1
{
u8 temp=0;
SDA_IN();
IIC_SDA=1;
delay_us(2);// 这个延时必须调!!!
while(IIC_READ_SDA)
{
temp++;
if(temp>200)//待调
{
IIC_Stop();
return 1;
}
}
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
IIC_SDA=1;
return 0;
}
上述代码的倒数第二行:IIC_SDA=1;
可以不用设置,因为已经等待到SDA低电平了,表示已经得到应答,不过对照时序,加上这句比较严谨。
根据应答信号的时序,写一个发送1bit数的函数,之后给24C02写数据、从24C02读数据,都可以调用该函数
从上面这个时序可以看到,开始后(注意之前写的开始信号函数,结束后SDA与SCL都是低电平),先将SCL置低电,利用循环来发送每一位数。从高位开始发送,如果高位是1,则SDA输出1,高位是0,SDA为0。发送完成后,整个数左移1位,此时高7位变为高8位。然后时钟SCL置1,开始发送,一段时间后,SCL置0,完成一个SCL周期的1位数发送。循环8次,完成1bit数发送。
代码:
void IIC_Send_Byte(u8 data)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//注意数据的每一位传输时,数据更换一定要在SCL低电平期间,否则触发开始和停止
for(t=0;t<8;t++)
{
if(data&0x80)//最高位为1,每次都传输最高位
IIC_SDA=1;
else
IIC_SDA=0;//此时高位数据被发送到传输线上
data<<=1;
delay_us(2);
IIC_SCL=1;//原来SCL=0,这里需要给它一个时钟脉冲
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
还是上面的那个时序图,每一个SCL高电平,接收一位数,接收1bit的数,需要8次循环。先将SCL置0,一段时间后置1,此时接收到传输线上的1位数据。接收的数据也是从高位开始接收,故每次循环数据需要左移一位。
代码:
u8 IIC_Read_Byte(u8 ack)//ack=1,表示发送应答,ack=0,表示不发送应答
{
u8 t,receive=0;
SDA_IN();
for(t=0;t<8;t++)
{
IIC_SCL=0;
delay_us(2);//这个延时期间来的信号
IIC_SCL=1;
receive<<=1;
if(IIC_READ_SDA)
receive++; //接收到了1
//receive<<=1;写前面,如第一个数为1,写后面后变为10,是错的
delay_us(2);
}
if(!ack)
IIC_NAck();
else
IIC_Ack();
return receive;
}
根据24C02参考手册的前言部分,可知24C02为2Kbit,即存储2048个8位数据。
我们用到的设备地址:
设备地址中的A2、A1、A0,与外部电路有关系;WP为写保护,低电平有效,外部电路中接GND ;VCC为3.3V供电(个人习惯,信号类器件供电都会外接一个旁路电容)。SDA、SCL接芯片任意俩个IO就行。
如外部电路中A2、A1、A0都接GND,则设备地址为:1010 000R/W 。
参考手册:
所以进行读操作时第8位(即最低位)为1,进行写操作时第8位为0 。
我的板子A2、A1、A0都接GND,故:
读操作时设备地址:10100001 即0xA1
写操作时设备地址:10100000 即0xA0
24C02中,一个储存单元储存8位的数,字地址从0开始依次递增,24C02最大储存2Kbit数,所以字地址为0~2047,表示储存的单元序号。
注意一个非常有意思的东西:这个字地址是自动归零的。如存储时,从2048字地址开始存储,实际上是从字地址0开始储存;从2049字地址开始储存,实际上是从字地址1开始储存; 同理,读的时候也一样,读字地址2048的内容,其实是在读字地址0的内容…。
根据上图的时序,可以得到给24C02写一个字节的思路:
开始信号——>写设备地址——>等待应答——>写字地址——>等待应答——>写数据——>等待应答——>停止信号
注意停止后,一定要延时一段较长的时间,让数据完全储存到24C02中
代码:
void HG24C02_WriteOneByte(u16 WordAddress ,u8 Data)
{
IIC_Start();
IIC_Send_Byte(0xA0);//写操作时地址为0xA0
IIC_Wait_Ack();
IIC_Send_Byte(WordAddress);
IIC_Wait_Ack();
IIC_Send_Byte(Data);
IIC_Wait_Ack();
IIC_Stop();
delay_ms(5);//注意这里延时!一定要调试
}
根据上图时序,可以得到24C02读一个字节的思路:
开始信号——>写设备地址——>等待应答——>写字地址——>等待应答——>开始信号——>读设备地址——>等待应答——>写数据——>(不应答)停止信号
代码:
u8 HG24C02_ReadOneByte(u16 WordAddress)
{
u8 temp=0;
IIC_Start();
IIC_Send_Byte(0xA0);//写地址
IIC_Wait_Ack();
IIC_Send_Byte(WordAddress);
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0xA1);//读地址
IIC_Wait_Ack(); //一定要等待
temp=IIC_Read_Byte(0);//不需要应答
IIC_Stop();
delay_ms(5);//注意这里延时!一定要调试
return temp;
}
对于16位、32位数,可以先存高8位,再存中间8位,最后存低8位。也可以从低位开始存。只需注意,从高8位开始存时,读取的时候也必须从高8位开始读。
以低8位优先储存为例,每次字地址++后,数据需要右移8位,将中间8位变为低8位。下面代码中与0xff的与运算是必要的,必须将除了低8位的其它位都置0 。
代码:
void HG24C02_WriteLenByte(u16 WordAddress,u32 Data,u8 Len)
{
u8 t;
for(t=0;t<Len;t++)
HG24C02_WriteOneByte(WordAddress+t,(Data>>(8*t))&0xff);
}
上面写数据的函数中,低8位优先写,所以在24C02中,低字地址存储数据的低8位,高字地址储存数据的高8位,所以需要从高字地址开始8位—8位的去读取,然后左移8位继续取下一个8位。
代码:
u32 HG24C02_ReadLenByte(u16 WordAddress ,u8 Len)
{
u8 t;
u32 temp=0;
for(t=0;t<Len;t++)
{
temp<<=8;
temp+=HG24C02_ReadOneByte(WordAddress+Len-t-1);
}
return temp;
}
传递过来一个空数组,读取一系列数据后,存放在数组里面返回,直接调用上面写的 《9、24C02读字节》的函数即可。
注意这个数组一定要是全局的,在函数外面定义,否则作用域会出错
代码:
void IIC_Sequential_Read(u16 WordAddress,u8 *arr1,u16 Len)//顺序读
{
u8 t;
for(t=0;t<Len;t++)
arr1[t]=HG24C02_ReadOneByte(WordAddress+t);
}
传递过来一个带数据的数组,将数组中的值写入24C02指定地址区域。直接调用《8、24C02写字节》函数
void IIC_Sequential_Write(u16 WordAddress,u8 *arr2 ,u16 Len)//顺序写
{
u8 t;
for(t=0;t<Len;t++)
HG24C02_WriteOneByte(WordAddress+t,arr2[t]);
}
开始信号——>读设备地址——>等待应答——>读数据——>(不需要应答)停止信号。
代码:
u8 Current_Address_Read()
{
u8 temp;
IIC_Start();
IIC_Send_Byte(0xA1);//读地址
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop();
return temp;
}
将数组buffer[]={"Hello World!"}
内的数据存储在24C02中,然后读取24C02数据,并在串口中打印出来。
(1)iic.h代码
#ifndef _IIC_
#define _IIC_
#include "stm32f10x.h"
void IIC_Init(void);
void SDA_IN(void);
void SDA_OUT(void);
void IIC_Start(void);
void IIC_Stop(void);
void IIC_Ack(void);
u8 IIC_Wait_Ack(void);
void IIC_NAck(void);
void IIC_Send_Byte(u8);
u8 IIC_Read_Byte(u8);
void HG24C02_WriteOneByte(u16,u8 );
u8 HG24C02_ReadOneByte(u16);
void HG24C02_WriteLenByte(u16,u32,u8);
u32 HG24C02_ReadLenByte(u16 ,u8);
void IIC_Sequential_Read(u16,u8 *,u16);
void IIC_Sequential_Write(u16,u8 *,u16);
u8 Current_Address_Read();
#endif
(2)iic.c代码
#include "iic.h"
#include "stm32f10x.h"
#include "delay.h"
#include "usart.h"
#define IIC_SDA PBout(7)
#define IIC_SCL PBout(6)
#define IIC_READ_SDA PBin(7)
void IIC_Init() //IIC初始化
{
// PB6 SCL
//PB7 SDA
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);
}
void SDA_IN(void) //设置SDA线为输入模式
{
// PB6 SCL
//PB7 SDA
GPIOB->CRL&=0x0fffffff;
GPIOB->CRL|=0x80000000;
}
void SDA_OUT(void) //设置SDA线为输出模式
{
GPIOB->CRL&=0x0fffffff;
GPIOB->CRL|=0x30000000;//推挽输出
//GPIOB->CRL|=0x70000000;//开漏输出
}
void IIC_Start(void) //开始信号
{
SDA_OUT();//设置为SDA输出
IIC_SDA=1;
IIC_SCL=1; //SDA与SCL都设置为高电平
delay_us(2);//---------------这里延时,待调
IIC_SDA=0;//在SCL高电平期间SDA由高到低跳变,表示开始传送
delay_us(2);
IIC_SCL=0; //注意这里必须置0,否则数据传输时,容易触发开始和停止信号
delay_us(2);
}
void IIC_Stop(void) //结束信号
{
SDA_OUT();//设置为SDA输出
IIC_SCL=0; //SDA与SCL都设置为低电平
IIC_SDA=0;
delay_us(2);//---------------这里延时,待调
IIC_SCL=1;
delay_us(2);
IIC_SDA=1; //SDA在SCL高电平期间从低电平跳变到高电平
delay_us(2);
}
void IIC_Ack() //应答信号
{
SDA_OUT();
IIC_SCL=0;
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
IIC_SDA=1;
delay_us(2);
}
u8 IIC_Wait_Ack()//等待应答,成功返回0,失败返回1
{
u8 temp=0;
SDA_IN();
IIC_SDA=1;
delay_us(2);// 这个延时必须调!!!
while(IIC_READ_SDA)
{
temp++;
if(temp>200)//待调
{
IIC_Stop();
return 1;
}
}
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
IIC_SDA=1;
return 0;
}
void IIC_NAck(void) //不需要应答
{
SDA_OUT();
IIC_SCL=0;
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
void IIC_Send_Byte(u8 data) //IIC发送一个字节的数
{
u8 t;
SDA_OUT();
IIC_SCL=0;//注意数据的每一位传输时,数据更换一定要在SCL低电平期间,否则触发开始和停止
for(t=0;t<8;t++)
{
if(data&0x80)//最高位为1,每次都传输最高位
IIC_SDA=1;
else
IIC_SDA=0;//此时高位数据被发送到传输线上
data<<=1;
delay_us(2);
IIC_SCL=1;//原来SCL=0,这里需要给它一个时钟脉冲
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
//ack=1,表示发送应答,ack=0,表示不发送应答
u8 IIC_Read_Byte(u8 ack) //IIC读一个字节的数
{
u8 t,receive=0;
SDA_IN();
for(t=0;t<8;t++)
{
IIC_SCL=0;
delay_us(2);//这个延时期间来的信号
IIC_SCL=1;
receive<<=1;
if(IIC_READ_SDA)
receive++; //接收到了1
//receive<<=1;写前面,如第一个数为1,写后面后变为10,是错的
delay_us(2);
}
if(!ack)
IIC_NAck();
else
IIC_Ack();
return receive;
}
void HG24C02_WriteOneByte(u16 WordAddress ,u8 Data) //24C02储存一个字节的数
{
IIC_Start();
IIC_Send_Byte(0xA0);//写操作时地址为0xA0
IIC_Wait_Ack();
IIC_Send_Byte(WordAddress);
IIC_Wait_Ack();
IIC_Send_Byte(Data);
IIC_Wait_Ack();
IIC_Stop();
delay_ms(5);//注意这里延时!一定要调试
}
u8 HG24C02_ReadOneByte(u16 WordAddress) //24C02取一个字节的数
{
u8 temp=0;
IIC_Start();
IIC_Send_Byte(0xA0);//写地址
IIC_Wait_Ack();
IIC_Send_Byte(WordAddress);
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0xA1);//读地址
IIC_Wait_Ack(); //一定要等待
temp=IIC_Read_Byte(0);//不需要应答
IIC_Stop();
delay_ms(5);//注意这里延时!一定要调试
return temp;
}
void HG24C02_WriteLenByte(u16 WordAddress,u32 Data,u8 Len) //24C02写16位、32位的一个数
{
u8 t;
for(t=0;t<Len;t++)
HG24C02_WriteOneByte(WordAddress+t,(Data>>(8*t))&0xff);
}
//注意存储32位、16位数时,先存储低位再储存高位,所以读取时,地址要减,从后面往前读
u32 HG24C02_ReadLenByte(u16 WordAddress ,u8 Len) //24C02读取16位、32位的一个数
{
u8 t;
u32 temp=0;
for(t=0;t<Len;t++)
{
temp<<=8;
temp+=HG24C02_ReadOneByte(WordAddress+Len-t-1);
}
return temp;
}
void IIC_Sequential_Read(u16 WordAddress,u8 *arr1,u16 Len)//顺序读
{
u8 t;
for(t=0;t<Len;t++)
arr1[t]=HG24C02_ReadOneByte(WordAddress+t);
}
void IIC_Sequential_Write(u16 WordAddress,u8 *arr2 ,u16 Len)//顺序写
{
u8 t;
for(t=0;t<Len;t++)
HG24C02_WriteOneByte(WordAddress+t,arr2[t]);
}
u8 Current_Address_Read() //读当前字地址
{
u8 temp;
IIC_Start();
IIC_Send_Byte(0xA1);//读地址
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop();
return temp;
}
(3)main.c代码
#include "stm32f10x.h"
#include "iic.h"
#include "delay.h"
#include "usart.h"
u8 buffer[]={
"Hello World!"};
#define SIZE sizeof(buffer)
u8 arr[SIZE];
int main(void)
{
IIC_Init();
delay_init();
uart_init(115200);
IIC_Sequential_Write(0,buffer,SIZE);
IIC_Sequential_Read(0,arr,SIZE);
printf("初始数组:%s\r\n",buffer);
printf("读取数组:%s\r\n",arr);
while(1)
{
}
}