文章参考:基于STM32f407的TSL2561模块的使用、TSL2561 STM32驱动程序、STM32系统学习——I2C (读写EEPROM)、STM32的I2C框图详解及通讯过程、I2C总线读写、I2C协议小结、I2C总线之(一)---概述、I2C总线之(二)---时序、I2C总线之(三)---以C语言理解IIC、IIC 时序分析,stm32软件模拟驱动编写
硬件:STM32F107VC( Cortex-M3)
软件:Keil μVision4,JLINK
一、TSL256x
1. TSL256x简介
TSL256x是TAOS公司推出的一种高速、低功耗、宽量程、可编程灵活配置的光强度数字转换芯片。该芯片可广泛应用于各类显示屏的监控,目的是在多变的光照条件下,使得显示屏提供最佳的显示亮度并尽可能降低电源功耗,还可以用于街道光照控制、安全照明等众多场合。
该芯片有两种不同的封装但是都是六引脚。封装图和六个引脚的含义如下:
ADDR引脚连接不同可选择不同的器件地址,有接地,接Vdd,以及浮空三种接法。器件访问地址与引脚2电平的对应关系如下:
2. TSL256x的内部结构和工作原理
TSL256x是第二代周围环境光强度传感器,其内部结构如下图所示。
通道0和通道1是两个光敏二极管,其中通道0对可见光和红外线都敏感,而通道1仅对红外线敏感。积分式A/D转换器对流过光敏二极管的电流进行积分,并转换为数字量,在转换结束后将转换结果存入芯片内部通道0和通道1各自的寄存器中。当一个积分周期完成之后,积分式A/D转换器将自动开始下一个积分转换过程。微控制器和TSL2560可通过标准的SMBus(System Management Bus) V1.1或V2.0实现,TSL2561则可通过I2C总线协议访问。
对TSL256x的控制是通过对其内部的16个寄存器的读写来实现的,TSL2561内部寄存器地址及作用如下表:
2.1 命令寄存器(COMMAND Register)
在MaskWord.h里定义了相关的command register。
/*******************************************************
About COMMAND Register
*********************************************************/
#define IntClearClrMask 0xBF
#define AddressClrMask 0xF0
command寄存器主要用来设置要配置的寄存器地址,其中高四位为控制位,低四位就是寄存器地址。
2.2 控制寄存器(Control Register)
在MaskWord.h里定义了相关的Control Register。
/********************************************************
About Control Register
*********************************************************/
#define PowerUp 0x03
#define PowerDown 0x00
Control寄存器主要用来控制模块的上电和断电。其值为0x03时上电;为0x00时为断电。
2.3 时间寄存器(Timing Register)
在MaskWord.h里定义了相关的Timing Register。
/*********************************************************
About Timing Register
*********************************************************/
#define GainClrMask 0xEF
#define GainSetLow 0x00
#define GainSetHigh 0x10
#define ManualMask 0xF7
#define ManualSet 0x08
#define ManualClr 0x00
#define IntegMask 0xFC
//最后两位设置积分时间,NOMINAL INTEGRATION TIME 13.7ms 101ms 402ms#define IntegScal0034 0x00 //积分时间13.7ms
#define IntegScal0252 0x01 //积分时间101ms
#define IntegScal1000 0x02 //积分时间402ms
TIMING寄存器主要用于选择增益和积分时间。
2.4 中断控制寄存器(Interrupt Control Register)
/*About Interrupt Control Register*/
#define IntrMask 0xCF
#define IntrDisabled 0x00
#define IntrLevelInt 0x10
#define IntrSMBAlert 0x20
#define IntrTestMode 0x30
#define PersistMask 0xF0
#define EveryADCCycle 0x00
#define AnyOutsideValue 0x01
#define TowPeriods 0x02
#define ThreePeriods 0x03
#define FourPeriods 0x04
#define FivePeriods 0x05
#define SixePeriodS 0x06
#define SevenPeriods 0x07
#define EightPeriods 0x08
#define NinePeriods 0x09
#define TenPeriods 0x0A
#define ElevenPeriods 0x0B
#define TwelvePeriods 0x0C
#define ThirteenPeriods 0x0D
#define FourteenPeriods 0x0E
#define FifteenPeriods 0x0F
2.5 ID寄存器(ID Register)
/*About ID Register*/
#define PARTNOMask 0x0F
#define REVNOMask 0xF0
3. TSL256x应用设计
TSL256x的访问遵循标准的SMBus和I2C协议。例如,TSL2561能够通过I2C总线访问,所以硬件接口电路很简单。假如所选用的微控制器带有I2C总线控制器,则将该总线的时钟线和数据线直接和TSL2561的I2C总线的SCL和SDA分别相连;假如微控制器内部没有上拉电阻,则还需要再用2个上拉电阻接到总线上。假如微控制器不带I2C总线控制器,则将TSL2561的I2C总线的SCL和SDA和普通I/O口连接即可;但编程时需要模拟I2C总线的时序来访问TSL2561,INT引脚接微控制器的外部中断。芯片接口如图:
微控制器可以通过I2C总线协议对TSL2561进行读写。写数据时,先发送器件地址,然后发送要写的数据。TSL2561的写过程如下:先发送一组器件地址;然后写命令码,命令码是指接下来写寄存器的地址00h-0fh和写寄存器的方式,是以字节、字或块为大单位进行写操作;最后发送要写的数据,根据命令码规定写寄存器的方式,连续发送要写的数据,内部写寄存器会自动加1。流程如下:
当积分式A/D转换器转换完成后,能够从通道0寄存器和通道1寄存器读取相应的值CH0和CH1,但是要以Lux(流明)为单位,还要根据CH0和CH1进行计算。对于CHIPSCALE封装,计算公式能够查看相应的芯片资料。对于TMB封装,假设光强为E(单位为Lux),则计算公式如下:
- 0 < CH1/CH0 ≤ 0.5 E=0.0304×CH0 - 0.062×CH0 ×(CH1/CH0)/4
- 0.5< CH1/CH0 ≤ 0.61 E=0.0224×CH0 - 0.031×CH1
- 0.61 < CH1/CH0 ≤ 0.8 E=0.0128×CH0 - 0.0153×CH1
- 0.8 < CH1/CH0 ≤ 1.3 E=0.00146×CH0 - 0.00112×CH1
- 1.3 < CH1/CH0 E=0
对应的代码calclux.c。
二、I2C概述
1. I2C简介
I2C(芯片间)总线接口连接微控制器和串行I2C总线。它提供多主机功能,控制所有I2C总线特定的时序、协议、仲裁和定时。支持标准和快速两种模式,同时与SMBus 2.0兼容。I2C总线采用了器件地址的硬件设置方法,通过软件寻址完全避免了器件的片选线寻址方法,从而使硬件系统具有最简单而灵活的扩展方法。 I2C接口的标准传输速率为100Kbit/s,快速传输可400Kbit/s,目前还增加了高速模式,最高传输速率可达3.4Mbit/s。
I2C模块有多种用途,包括CRC码的生成和校验、SMBus(系统管理总线—System ManagementBus)和PMBus(电源管理总线—Power Management Bus)。
I2C用于连接集成电路和功能模块,在它们之间交互数据或控制信息。很多设备如:键盘和LED控制器,以及存储设备EEPROM和FLASH都配备了I2C 总线接口。根据特定设备的需要,可以使用DMA以减轻CPU的负担。
I2C模块接收和发送数据,并将数据从串行转换成并行,或并行转换成串行。可以开启或禁止中断。接口通过数据引脚(SDA)和时钟引脚(SCL)连接到I2C总线。允许连接到标准(高达100kHz)或快速(高达400kHz)的I2C总线。
2. I2C的输出级
I²C 只是用两条双向的线,一条 Serial Data Line (SDA) ,另一条Serial Clock (SCL)。
SCL:上升沿将数据输入到每个EEPROM器件中;下降沿驱动EEPROM器件输出数据。(边沿触发)
SDA:双向数据线,为OD门,与其它任意数量的OD与OC门成"线与"关系。
每一个I2C总线器件内部的SDA、SCL引脚电路结构都是一样的,引脚的输出驱动与输入缓冲连在一起。其中输出为漏极开路的场效应管,输入缓冲为一只高输入阻抗的同相器,这种电路具有两个特点:
1)由于SDA、SCL为漏极开路结构(OD),因此它们必须接有上拉电阻,阻值的大小常为 1k8, 4k7 和10k ,但1k8 时性能最好;当总线空闲时,两根线均为高电平。连到总线上的任一器件输出的低电平,都将使总线的信号变低,即各器件的SDA及SCL都是线"与"关系。
2)引脚在输出信号的同时还将引脚上的电平进行检测,检测是否与刚才输出一致,为"时钟同步"和"总线仲裁"提供了硬件基础。
3. 主设备与从设备、发送器和接收器
系统中的所有外围器件都具有一个7位的"从器件专用地址码",其中高4位为器件类型,由生产厂家制定,低3位为器件引脚定义地址,由使用者定义。主控器件通过地址码建立多机通信的机制,因此I2C总线省去了外围器件的片选线,这样无论总线上挂接多少个器件,其系统仍然为简约的二线结构。终端挂载在总线上,有主端和从端之分,主端必须是带有CPU的逻辑模块,在同一总线上同一时刻使能有一个主端,可以有多个从端,从端的数量受地址空间和总线的最大电容 400pF的限制。
- 主端主要用来驱动SCL line;产生时钟,产生起始和停止信号。
- 从设备(从机)对主设备(主机)产生响应;
二者都可以传输数据,但是从设备(从机)不能发起传输,且传输是受到主设备(主机)控制。
发送器是本次传输中发送数据到总线的器件;接收器是本次传输中从总线接收数据的器件。主设备(主机)通常是微控制器,从设备(从机)是被主机寻址的器件,可以是发送器或接收器。所以接口运行的模式有四种:从发送器模式、从接收器模式、主发送器模式、主接收器模式。
4. "时钟同步"和"总线仲裁"
时钟同步:
仲裁过程中,实际由于电气特性,已经对各个不同主设备的时钟进行同步处理,所以各个设备使用的时钟都是一样的、同步后的SCL。同步规则就是:SCL线上时钟低电平时间由各器件中时钟最长的低电平时间决定,而时钟高电平时间则由高电平时间最短的器件决定。总的原则是:低电平否决高电平。
总线仲裁:
当两个设备同时发出起始位进行数据传输时,相互竞争的设备使它们的时钟保持同步。没有检测到冲突之前,每个设备都认为只有自己在使用总线。每个设备都有仲裁功能,它们都去检测SDA线,检测到自己发送的数据和总线上数据不匹配的设备就失去仲裁能力。
如果两个或更多的设备发送的第一个字节的内容相同,那么仲裁就发生在随后传输中。失去仲裁的主设备转变成从接收模式。
注意:数据冲突是因为逻辑低电平可以把逻辑高电平否决。在同一时刻如果两设备一个发高电平一个发低电平,那么总线上是低电平。
5. I2C函数
三、I2C总线的接口时序和代码
STM32 芯片有多个I2C 外设,它们的I2C 通讯信号引出到不同的GPIO 引脚上,使用时必须配置到这些指定的引脚,使用的硬件:STM32F107VC( Cortex-M3),则引脚如表。
I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
1. 起始和停止
在数据传送过程中,必须确认数据传输的开始和结束,这通过起始和结束信号识别。
起始位:SCL=1时,SDA上有下降沿
停止位:SCL=1时,SDA上有上升沿
所以只有当SCL为低时,才允许SDA改变数据,不然会被误判为停止位而造成错误。
1.1 I2C_Trans_Start
void I2C_Trans_Start(void){
TSL2561_DAT_OUT; //PB6推挽输出, PB7推挽输出,即SCL、SDA设置为输出
TSL2561_DAT_H; //PB7推挽输出,PB7=1,即SDA置高电平
TSL2561Delay1(); //延迟
TSL2561_CLK_H; //PB6 = 1,即SCL置高电平
Delay(8); //本为5...//1.3us; //t(BUF),at least 1.3us needed
TSL2561_DAT_L; //PB7推挽输出,PB7=0,即SDA置低电平
Delay(5); //本为3...//0.827us; //t(HDSTA),at least 0.6us needed
TSL2561_CLK_L; //PB6 = 0,即SCL置低电平
//拉低SCL,因为只有在SCL为低电平期间,SDA上的高低电平状态才允许变化
Delay(8); //t(Low),at least 1.3us needed;
}
起始位:SCL(PB6)=1时,SDA(PB7)上有下降沿
1.2 I2C_Trans_Stop
void I2C_Trans_Stop(void){
TSL2561_DAT_OUT; //PB6,PB7推挽输出,即SCL、SDA设置为输出
TSL2561_CLK_H; //PB6=1,即SCL置高电平
Delay(8);//5...//t(SUSTO), at least 0.6us needed ////////////////////////
TSL2561_DAT_H; //PB7=1,即SDA置高电平,拉高SDA实现终止信号
}
I2C_Trans_Stop函数调用之前,PB6=PB7=0,即SCL、SDA都是低电平。
停止位:SCL(PB6)=1时,SDA(PB7)上有上升沿。
2. 数据有效性
I2C使用 SDA信号线来传输数据,使用 SCL信号线进行数据同步。SDA数据线在 SCL的每个时钟周期传输一位数据。I2C读写每一位数据都是在SCL为高时完成,读写每一位数据的时候SCL都有一个高脉冲的过程,也就是SCL为高时读写SDA的值,这意味着在SCL拉高之前,SDA必须为一稳定值,不然读写就不准确 。只有当SCL为低时,才允许SDA改变数据,不然会被误判为停止位而造成错误。
3. 响应
I2C 的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。
3.1 主机写完数据等待从机应答
数据传送格式:每一个字节必须保证是8位长度。数据传送时,先传高位,每个被传送的字节后面必须跟随一位应答位(ACK)。可以向从机传输 N 个数据,这个 N 没有大小限制。当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。
I2C总线数据传送时,每传送一个字节数据后都必须有应答信号(ACK)。 I2C因为有两种寻址模式,所以它具体的格式有两种,这里主要按分类部分来描述数据格式。7位寻址模式如下:
10位寻址模式如下:
主控器接收数据时,如果要结束通信时,将在停止位之前发送非应答信号。回应ACK是个低电平脉冲,并且这是由从器件的硬件来完成的,不需要主器件来软件操作,只需要等待。
在主机向从机传送每发送8个BIT数据时,应答位由从机产生,主机检测从机产生的应答信号。
//功能:接收从机应答信号,读取SDA,看是否有应答信号ACK(负脉冲)
TSL2561_DAT_IN; //PB6推挽输出,PB7浮空输入,即SCL设置为输出,SDA设置为输入 TSL2561_CLK_H; //PB6=1,即SCL置高电平
Delay(8);//5...//0.828us;//t(HIGH), at least 0.6us needed
//检测从机是否产生负脉冲
if(TSL2561_DATA==1/*c*/)
{
c=1;//非应答信号
}
else {
c=0; //应答信号,正常情况下
}
TSL2561_CLK_L; //PB6=0,即SCL置低电平
Delay(11);//8...//t(LOW), at least 1.3us needed
if(c) return 0; //no ack, failed
return 1;//success!
3.2 主机读完数据向从机发送应答信号
主机读数据时有这样的规定:当主机每读一个数据,主机都要回复应答信号。当最后一个字节数据读完后,主机要返回以“非应答”信号,并终止信号读出操作,这里的应答信号和非应答信号作用是给从机判断是否继续读取数据。由下图可以看出应答信号是在SDA低电平期间,SCL由低置高再置低。非应答信号是在SDA位高电平期间,SCL由低置高再置低。
//主机读完数据向从机发送应答信号
void I2C_Ack_L(void){
TSL2561_DAT_OUT;//PB6推挽输出, PB7推挽输出,即SCL、SDA设置为输出
TSL2561_DAT_L; //PB7=0,即SDA置低电平
TSL2561_CLK_H; //PB6=1,即SCL置高电平
Delay(8); //5...
TSL2561_CLK_L;//PB6 = 0,即SCL置低电平
Delay(11);//8...
}
//主机读完数据向从机发送非应答信号
void I2C_Ack_H(void){
TSL2561_DAT_OUT;//PB6推挽输出, PB7推挽输出,即SCL、SDA设置为输出
TSL2561_DAT_H; //PB7=1,即SDA置高电平
TSL2561_CLK_H; //PB6=1,即SCL置高电平
Delay(8);//5...
TSL2561_CLK_L;//PB6 = 0,即SCL置低电平
Delay(11);//8...
}
4. 数据读写
主机发送起始位,这会通知总线上的所有设备传输开始了,接下来主机发送设备地址(从机地址,SLAVE_ADDRESS,7位或10位)。在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。与这一地址匹配的设备slave将继续这一传输过程,而其它设备slave将会忽略接下来的传输并等待下一次传输的开始。
在从机地址位之后,是传输方向的选择位(R/W)。该位为 0 时,表示后面的数据传输方向是由主机传输至从机,即主机向从机写数据。该位为 1时,则相反。
从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号,只有接收到应答信号后,主机才能继续发送或接收数据。
主机寻址到从机后,发送它所要读取或写入的从机的内部寄存器地址; 之后,发送数据。数据发送完毕后,发送停止位。
4.1 主机向从机发送数据
SDA线:
SDA+SCL:
过程如下:
- 发送起始位
- 发送从机的地址和读/写选择位;释放总线,等到从机拉低总线进行应答;如果从机接收成功,则进行应答;若没有握手成功或者发送的数据错误时从机不产生应答,此时要求重发或者终止。
- 主机发送想要写入的内部寄存器地址;从机对其发出应答;
- 发送数据
- 发送停止位
对应的代码如下:
// Address是从机地址;CommandCode是Command寄存器,包含寄存器地址;DataNumToBeSend是要发送的数据个数,DataBufferTx是要发送的数据
int I2C_SendData_TSL2561(unsigned char Address, unsigned char CommandCode, int DataNumToBeSend, unsigned char DataBufferTx[]){
int i=0;
//发送起始位
I2C_Trans_Start();
//发送从机的地址和读/写选择位
if(!I2C_Write_Byte(Address<<1))
return 0;//No ACK
//发送想要写入的内部寄存器地址,CommandCode高四位为控制位,低四位为要写入数据的地址,即寄存器地址
if(!I2C_Write_Byte(CommandCode))
return 0;
//发送数据
while(i<DataNumToBeSend){
if(!I2C_Write_Byte(DataBufferTx[i++]))
return 0;
}
//发送停止位
I2C_Trans_Stop();
return 1;
}
4.1.1 Address是从机地址
从给的数据手册可以看出有一个引脚ADDR SEL用来设置芯片地址,当引脚接地时从机地址为0x29,浮空时从机地址0x39,接高电平时为0x49。
在Address.h里定义了从机地址。
#define SlaveAddressGND 0x29
#define SlaveAddressFloat 0x39
#define SlaveAddressVDD 0x49
在TSL2561PowerUp()函数里,Address是SlaveAddressVDD,定义是#define SlaveAddressVDD 0x49。用Address<<1表示了7位的从机地址7'b1001001和一位的读写选择位1'b0(写操作)。
4.1.2 CommandCode是Command寄存器
command寄存器主要用来设置要配置的寄存器地址,其中高四位为控制位,低四位就是寄存器地址。
每个寄存器地址和几个常用的寄存器地址宏定义如下。
在Address.h里定义了寄存器地址。
/********************************************************************************
Register Address Define
*********************************************************************************/
/*#define COMMAND */
#define CtrlRegAdd 0x00
#define TimingRegAdd 0x01
#define ThreshLowLowRegAdd 0x02
#define ThreshLowHighRegAdd 0x03
#define ThreshHighLowRegAdd 0x04
#define ThreshHighHighRegAdd 0x05
#define InterruptRegAdd 0x06
/*#define 0x07*/
#define CRCRegAdd 0x08
/*#define 0x09*/
#define IDRegAdd 0x0A
#define BlockWriteAdd 0x0B
#define Data0LowRegAdd 0x0C
#define Data0HighRegAdd 0x0D
#define Data1LowRegAdd 0x0E
#define Data1HighRegAdd 0x0F
在TSL2561PowerUp()函数里,CommandCode是PowerUpCode,定义是#define PowerUpCode 0x80|CtrlRegAdd。控制位是0x8,表示选择Command寄存器,寄存器地址是#define CtrlRegAdd 0x00,表示是控制寄存器,控制芯片是否工作。
4.1.3 写数据
int I2C_Write_Byte(unsigned char b){
int i=0;
unsigned char c=0;
TSL2561_DAT_OUT; //PB6,PB7推挽输出,即SCL、SDA设置为输出
for(i=0;i<8;i++){
if(b&0x80) //先传高位再传低位
{
TSL2561_DAT_H; //PB7=1,即SDA置高电平
}
else {
TSL2561_DAT_L;//PB7=0,即SDA置低电平 }
b=b<<1;
TSL2561_CLK_H; //PB6 = 1,即SCL置高电平,拉高SCL,此时SDA上的数据稳定
Delay(8);//5...//t(HIGH), at least 0.6us needed
TSL2561_CLK_L; //PB6 = 0,即SCL置低电平
//拉低SCL,SDA上的数据才允许变化,为下次数据传输做好准备
Delay(11);//8...//1.6us//t(LOW), at least 1.3us needed
}
//功能:接收从机应答信号,读取SDA,看是否有应答信号ACK(负脉冲)
TSL2561_DAT_IN; //PB6推挽输出,PB7浮空输入,即SCL设置为输出,SDA设置为输入
TSL2561_CLK_H; //PB6=1,即SCL置高电平
Delay(8);//5...//0.828us;//t(HIGH), at least 0.6us needed
//检测从机是否产生负脉冲
if(TSL2561_DATA==1/*c*/) //读PB7即SDA的值
{
c=1;//非应答信号
}
else {
c=0; //应答信号,正常情况下
}
TSL2561_CLK_L; //PB6=0,即SCL置低电平
Delay(11);//8...//t(LOW), at least 1.3us needed
if(c) return 0; //no ack, failed
return 1;//success!
}
4.2 主机从从机读取数据
读的过程比较复杂,在从slave读出数据前,你必须先要告诉它哪个内部寄存器是你想要读取的,因此必须先对其进行写入(dummy write)。
过程如下:
- 发送起始位;
- 发送slave地址+write bit set;
- 发送内部寄存器地址;
- 重新发送起始位,即restart;
- 重新发送slave地址+read bit set;
- 读取数据
- 主机接收器在接收到最后一个字节后,也不会发出ACK信号。于是,从机发送器释放SDA线,以允许主机发出P信号结束传输。
- 发送停止位
对应的代码如下:
// Address是从机地址;CommandCode是Command寄存器,包含寄存器地址;DataNumToBeSend是要接收的数据个数,DataBufferTx是要接收的数据
int I2C_ReceivData(unsigned char Address, unsigned char CommandCode, int DataNumReceived, unsigned char DataBufferRx[]){
int i=0;
I2C_Trans_Start(); //发送起始位
if(!I2C_Write_Byte(Address<<1)) // 发送从机地址+写操作
return 0;//No ACK
if(!I2C_Write_Byte(CommandCode))//发送要读取的内部寄存器地址
return 0;
I2C_Trans_Start(); //重新发送起始位
if(!I2C_Write_Byte((Address<<1)|0x01)) // 发送从机地址+读操作
return 0;//No ACK
//读取数据
while(i<DataNumReceived){
DataBufferRx[i++]=I2C_Read_Byte();
I2C_Ack_L(); //主机读完数据向从机发送应答信号 }
I2C_Trans_Stop();//发送停止位
return 1;
}
4.2.1 Address和CommandCode
Address和CommandCode的具体定义看5.1.1和5.1.2。
这里的Address是SlaveAddressVDD,定义是#define SlaveAddressVDD 0x49。
用Address<<1表示了7位的从机地址7'b1001001和一位的读写选择位1'b0(写操作)。用(Address<<1)|0x01)表示了7位的从机地址7'b1001001和一位的读写选择位1'b1(读操作)。
这里CommandCode是PowerUpCode,定义是#define Data0LowRegCode 0xC0|Data0LowRegAdd //reading Data and Clear Interrupt。控制位是0xc,表示选择Command寄存器并清除中断,寄存器地址是#define Data0LowRegAdd 0x0C,表示是数据寄存器,指的是通道0低字节。
4.2.2 读数据
将SDA的数据放入temp里,每读一次SDA,temp就左移一位,直至读到8位数据,输出temp。
unsigned char I2C_Read_Byte(void){
int i=0;
unsigned char temp=0;
TSL2561_DAT_IN; //PB6推挽输出,PB7浮空输入,即SCL设置为输出,SDA设置为输入
for(i=0;i<8;i++){
temp=temp<<1;
TSL2561_CLK_H; //PB6 = 1,即SCL置高电平
//SCL上升沿时,I2C设备将数据放在SDA线上,并在SCL高电平期间数据已经稳定,可以接收
Delay(8);//5...
//读PB7即SDA的值
if(TSL2561_DATA==0x01){
temp|=1; //sda=1,temp|=sda
}
TSL2561_CLK_L; //PB6=0,即SCL置低电平,拉低SCL,使发送端可以把数据放在SDA上
Delay(11);//8...
}
return temp;
}
五、代码解析
在tsl2561.h里定义了部分函数的寄存器操作。
1. TSL2561_CLK_H
#define TSL2561_CLK_H GPIOB->BSRR = GPIO_Pin_6 //set PB6 = 1
GPIO_BSRR,高16位控制对应引脚输出低电平:写入1,对应引脚输出低电平,写入0,没有任何作用;低16位控制对应引脚输出高电平:写入1,对应引脚输出高电平;写入0,没有任何作用。
GPIOB->BSRR = GPIO_Pin_6,就是GPIOB->BSRR [6]=1,BS6=1,即PB6=1。
2. TSL2561_CLK_L
#define TSL2561_CLK_L GPIOB->BRR = GPIO_Pin_6 //set PB6 = 0
GPIO_BRR,32位寄存器,其中高16位保留,低16位的每1位写入1,对应引脚输出低电平,写入0,没有任何作用。
GPIOB->BRR = GPIO_Pin_6,即GPIOB_BRR[6]=1,BR6=1,即PB6=0。
3. TSL2561_DAT_H
//PB7输出,将相关位置位。output, set PB7 =1
#define TSL2561_DAT_H
GPIOB->CRL |= 0x30000000; //CNF7[1:0]=00,通用推挽输出模式;MODE7[1:0]=11:输出模式,最大速度50MHz,也就是PB7推挽输出,最大速度50MHz
GPIOB->BSRR = GPIO_Pin_7 //PB7 =1
GPIO_CRL用于控制GPIO的低8个引脚(Pin7-Pin0)。 其中MODE对应GPIO的最大输出速度,CNF对应GPIO的工作(配置)模式。
GPIOB->CRL |= 0x30000000; 最终结果GPIOB->CRL[31:28]=0x3,即MODE7[1:0]=11,输出模式,最大输出速度为50MHz,CNF7[1:0]=00,通用推挽输出模式,即PB7推挽输出,最大输出速度为50MHz。
GPIOB->BSRR = GPIO_Pin_7,就是GPIOB->BSRR [7]=1,BS7=1,即PB7=1。
4. TSL2561_DAT_L
//PB7输出,将相关的位复位。output, set PB7 =0
#define TSL2561_DAT_L
GPIOB->CRL |= 0x30000000; //CNF7[1:]=00,通用推挽输出模式;MODE7[1:0]=11:输出模式,最大速度50MHz,也就是PB7推挽输出,最大速度50MHz
GPIOB->BRR = GPIO_Pin_7 //PB7 =0
GPIOB->BRR = GPIO_Pin_7,即GPIOB_BRR[7]=1,BR7=1,即PB7=0。
5. TSL2561_DATA
//read PB7 vlaue
#define TSL2561_DATA
((GPIOB->IDR & GPIO_Pin_7) >> 7) //读PB7的值
GPIO_IDR,32位寄存器,其中高16位保留,低16位的每1位用于存放对应引脚的电平状态, 0:对应引脚输入低电平, 1:对应引脚输入高电平。
((GPIOB->IDR & GPIO_Pin_7) >> 7)=IDR7,即读IDR7的值,为PB7的状态。
6. TSL2561_DAT_IN
//PB7 input, PB6 output
#define TSL2561_DAT_IN
GPIOB->CRL |= 0x40000000;
GPIOB->CRL &= 0x4FFFFFFF; //CNF7[1:]=01,浮空输入模式;MODE7[1:0]=00:输入模式,也就是PB7浮空输入
GPIOB->CRL |= 0x03000000;
GPIOB->CRL &= 0xF3FFFFFF////CNF6[1:0]=00,通用推挽输出模式;MODE6[1:0]=11:输出模式,最大速度50MHz,也就是PB6推挽输出,最大速度50MHz
GPIOB->CRL |= 0x40000000; GPIOB->CRL &= 0x4FFFFFFF;最终结果GPIOB->CRL[31:28]=0x4,即MODE7[1:0]=00,输入模式,CNF7[1:0]=01,浮空输入模式,即PB7浮空输入。
GPIOB->CRL |= 0x03000000; GPIOB->CRL &= 0xF3FFFFFF;最终结果GPIOB->CRL[27:24]=0x3,即MODE6[1:0]=11,输出模式,最大输出速度为50MHz,CNF6[1:0]=00,通用推挽输出模式,PB6推挽输出,最大输出速度为50MHz。
7. TSL2561_DAT_OUT
//PB7 output, PB6 output
#define TSL2561_DAT_OUT
GPIOB->CRL |= 0x30000000;
GPIOB->CRL &= 0x3FFFFFFF; //CNF7[1:0]=00,通用推挽输出模式;MODE7[1:0]=11:输出模式,最大速度50MHz,也就是PB7推挽输出,最大速度50MHz
GPIOB->CRL |= 0x03000000;
GPIOB->CRL &= 0xF3FFFFFF //CNF6[1:0]=00,通用推挽输出模式;MODE6[1:0]=11:输出模式,最大速度50MHz,也就是PB6推挽输出,最大速度50MHz
GPIOB->CRL |= 0x30000000; GPIOB->CRL &= 0x3FFFFFFF;最终结果GPIOB->CRL[31:28]=0x3,即MODE7[1:0]=11,输出模式,最大输出速度为50MHz,CNF7[1:0]=00,通用推挽输出模式,即PB7推挽输出,最大输出速度为50MHz。
GPIOB->CRL |= 0x03000000; GPIOB->CRL &= 0xF3FFFFFF;最终结果GPIOB->CRL[27:24]=0x3,即MODE6[1:0]=11,输出模式,最大输出速度为50MHz,CNF6[1:0]=00,通用推挽输出模式,PB6推挽输出,最大输出速度为50MHz。
8. TSL2561Delay1
__asm void TSL2561Delay1(){
NOP;
bx lr;
}
NOP,这个指令在汇编中的作用是空指令,意味着什么都不做的意思,一般用来控制CPU的时间周期,达到时钟延时的效果,NOP也就占用一个时钟周期。
bx lr的作用等同于mov pc,lr,即跳转到lr中存放的地址处。lr就是连接寄存器(Link Register, LR),在ARM体系结构中LR的特殊用途有两种:一是用来保存子程序返回地址;二是当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。(见BX LR)
9. Delay()
void Delay(int Nus){
while(Nus--){
TSL2561Delay1();
}
}
Delay(Nus)括号里的参数Nus,表示延迟Nus个周期。
六、实验
6.1 程序说明
该示例程序实现了如何通过IO口模拟串行数据时序,来读取光强度传感器的数值,读取到的数值被转换成流明值,显示在LCD屏幕上。
6.2 配置
连接JP18的两个管脚,确保I2C_SCK跳线被短接;将CN15处的传感器换成光强度度传感器;查看LCD所显示的光强的大小,而且可以在不同的光强下查看数值的大小的变化。
6.3 流程图
/* Includes ------------------------------------------------------------------*/
#include "stm32f10x.h"
#include "stm3210c_eval_lcd.h"
#include "stm32_eval.h"
#include "tsl2561.h"
#include <stdio.h>
/* Private variables ---------------------------------------------------------*/
ErrorStatus HSEStartUpStatus;
/* Private function prototypes -----------------------------------------------*/
void RCC_Configuration(void);
void GPIO_Configuration(void);
int main(void)
{
/* System clocks configuration ---------------------------------------------*/
RCC_Configuration();
/* GPIO configuration ------------------------------------------------------*/
GPIO_Configuration();
STM3210C_LCD_Init();
LCD_Clear(White);
/* Set the LCD Text Color */
LCD_SetTextColor(Black);
printf(" STM3210C-EVAL \n");
printf(" Light sensor \n");
TSL2561PowerUp();
TSL2561SetTimmingReg(GainSetHigh, ManualClr,IntegScal1000);
TSL2561SetInterruptCtrlReg(IntrLevelInt, EveryADCCycle);
while (1)
{
TSL2561ReadINT2();
}
}
void RCC_Configuration(void)
{
/* RCC system reset(for debug purpose) */
RCC_DeInit();
/* Enable HSE */
RCC_HSEConfig(RCC_HSE_ON);
/* Wait till HSE is ready */
HSEStartUpStatus = RCC_WaitForHSEStartUp();
if(HSEStartUpStatus == SUCCESS)
{
/* Enable Prefetch Buffer */
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
/* Flash 2 wait state */
FLASH_SetLatency(FLASH_Latency_2);
/* HCLK = SYSCLK */
RCC_HCLKConfig(RCC_SYSCLK_Div1);
/* PCLK2 = HCLK */
RCC_PCLK2Config(RCC_HCLK_Div1);
/* PCLK1 = HCLK/2 */
RCC_PCLK1Config(RCC_HCLK_Div2);
/* ADCCLK = PCLK2/4 */
RCC_ADCCLKConfig(RCC_PCLK2_Div4);
#ifndef STM32F10X_CL
/* PLLCLK = 8MHz * 7 = 56 MHz */
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_7);
#else
/* Configure PLLs *********************************************************/
/* PLL2 configuration: PLL2CLK = (HSE / 5) * 8 = 40 MHz */
RCC_PREDIV2Config(RCC_PREDIV2_Div5);
RCC_PLL2Config(RCC_PLL2Mul_8);
/* Enable PLL2 */
RCC_PLL2Cmd(ENABLE);
/* Wait till PLL2 is ready */
while (RCC_GetFlagStatus(RCC_FLAG_PLL2RDY) == RESET)
{}
/* PLL configuration: PLLCLK = (PLL2 / 5) * 7 = 56 MHz */
RCC_PREDIV1Config(RCC_PREDIV1_Source_PLL2, RCC_PREDIV1_Div5);
RCC_PLLConfig(RCC_PLLSource_PREDIV1, RCC_PLLMul_7);
#endif
/* Enable PLL */
RCC_PLLCmd(ENABLE);
/* Wait till PLL is ready */
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET)
{
}
/* Select PLL as system clock source */
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
/* Wait till PLL is used as system clock source */
while(RCC_GetSYSCLKSource() != 0x08)
{
}
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_GPIOB |RCC_APB2Periph_GPIOC , ENABLE );
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 |RCC_APB1Periph_TIM3 | RCC_APB1Periph_TIM4 | RCC_APB1Periph_SPI2, ENABLE );
}
}
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t* file, uint32_t line)
{
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* Infinite loop */
while (1)
{
}
}
#endif
6.4 函数解析
6.4.1 TSL2561PowerUp()
int TSL2561PowerUp(void){
DataBufferTx[0]=PowerUp;
return I2C_SendData_TSL2561(SlaveAddressVDD, PowerUpCode, 1, DataBufferTx);
}
涉及的参数是
#define PowerUp 0x03
#define SlaveAddressVDD 0x49
#define PowerUpCode 0x80|CtrlRegAdd
#define CtrlRegAdd 0x00
6.4.2 TSL2561SetTimmingReg(GainSetHigh, ManualClr,IntegScal1000)
/**********************************************************************************
GAIN: should be GainSetLow or GainSetHigh
Manual: should be ManualSet or ManualClr
INTEG: should be IntegScal0034, IntegScal0252 or IntegScal1000
**********************************************************************************/
int TSL2561SetTimmingReg(u8 GAIN, u8 Manual, u8 INTEG){
u8 temp=0;
temp&=GainClrMask&ManualMask&IntegMask;
temp|=GAIN|Manual|INTEG;
DataBufferTx[0]=temp;
return I2C_SendData_TSL2561(SlaveAddressVDD, TimingRegCode, 1, DataBufferTx);
}
涉及的参数
#define GainClrMask 0xEF
#define ManualMask 0xF7
#define IntegMask 0xFC
#define GainSetHigh 0x10
#define ManualClr 0x00
#define IntegScal1000 0x02
#define SlaveAddressVDD 0x49#define TimingRegCode 0x80|TimingRegAdd
#define TimingRegAdd 0x01
6.4.3 TSL2561SetInterruptCtrlReg(IntrLevelInt, EveryADCCycle)
/*************************************************************************************
InterruptLogic:
should be one of these: IntrDisabled,IntrLevelInt,IntrSMBAlert,
IntrTestMode
InterruptRate:
should be one of these: EveryADCCycle,AnyOutsideValue,TowPeriods,
ThreePeriods,FourPeriods,FivePeriods,
SixePeriodS,SevenPeriods,EightPeriods,
NinePeriods,TenPeriods,ElevenPeriods,
TwelvePeriods,ThirteenPeriods,FourteenPeriods,
FifteenPeriods
**************************************************************************************/
int TSL2561SetInterruptCtrlReg(unsigned char InterruptLogic, unsigned char InterruptRate){
unsigned char temp=0;
temp&=IntrMask;
temp&=PersistMask;
temp|=InterruptLogic|InterruptRate;
DataBufferTx[0]=temp;
return I2C_SendData_TSL2561(SlaveAddressVDD, InterruptRegCode, 1, DataBufferTx);
}
涉及的参数
#define IntrMask 0xCF
#define PersistMask 0xF0
#define IntrLevelInt 0x10
#define EveryADCCycle 0x00
#define SlaveAddressVDD 0x49
#define InterruptRegCode 0x80|InterruptRegAdd
#define InterruptRegAdd 0x06
6.4.4 TSL2561ReadINT2()
void TSL2561ReadINT2(void){
u16 LightIntensity0=0xFFFF;
u16 LightIntensity1=0xFFFF;
unsigned long Lux=0xFFFFFFFF;
TSL2561ReadADC(0, &LightIntensity0); //读取通道0寄存器值
TSL2561ReadADC(1, &LightIntensity1); //读取通道1寄存器值
//计算流明值
Lux=CalculateLux(GainSetHigh>>4, IntegScal1000, LightIntensity0, LightIntensity1, T); Xaddr=0;
Yaddr=Line5;
printf("The Light Intensity is: %d lux\n", Lux);
xDelay(0x01ffff);
}
涉及的参数
#define GainSetHigh 0x10
#define IntegScal1000 0x02
#define T 0x0000
6.4.5 TSL2561ReadADC()
在读数据的时候,因为TSL2561的ADC的数据寄存器分为DATE0_LOW,DATE0_HIGH,DATE1_LOW,DATE1_HIGH,一共两路,每路16位。因此一次读数据要有两次的I2C读操作,并把DATE_HIGH移到高位。
/**********************************************************************************
ChannelNum: shoule be 0 or 1
*LightIntensity : fetch the Channel Number;
**********************************************************************************/
//读取通道寄存器值
int TSL2561ReadADC(int ChannelNum, u16 *LightIntensity){
if(ChannelNum==0){
if(I2C_ReceivData(SlaveAddressVDD, Data0LowRegCode, 2, DataBufferRx)){
*LightIntensity=(DataBufferRx[1]<<8)|DataBufferRx[0];
return 1;
}
return 0;//No Ack;
}
if(ChannelNum==1){
if(I2C_ReceivData(SlaveAddressVDD, Data1LowRegCode, 2, DataBufferRx)){
*LightIntensity=(DataBufferRx[1]<<8)|DataBufferRx[0];
return 1;
}
return 0;//No ACK;
}
return 2;//Wrong ChannelNum;
}