转自:http://blog.sina.com.cn/s/blog_61ebc5f30100fpdy.html
1.4 AT24LC08 读写流驱动开发
AT24LC08 是一款 I2C 接口的 EEPROM 芯片,容量为 8Kbit ,内部分 4 个 Page ,每个 Page 有 256B 。访问 AT24LC08 上的地址空间需要 10 位地址线。编写 AT24LC08 芯片的读写驱动需要了解它的读写方式和 I2C 总线的访问时序,这里以 Micro2440 平台为例介绍 AT24LC08 工作过程。该驱动编写的总体流程与 1.3 类似。
1.4.1 A T24LC08 工作原理
AT24LC08 的主要管脚定义如表 1 所示。其中 A2 是和硬件上的连接一致的; A1 、 A0 是用于选择 Page 页; SDA 用于双向的串行数据传输; SCL 是串行时钟线,时钟上升沿时,数据从总线进入 EEPROM ,时钟下降沿时,数据从 EEPROM 传到总线; WP 信号接地,即采用普通的读写方式。
表 1 AT24LC08 主要引脚描述
引脚名称 | 功能 |
A0 A1 A2 | 地址输入 |
SDA | 串行数据 |
SCL | 串行时钟输入 |
WP | 写保护 |
按芯片的工作流程顺序,分别分析器件寻址、写操作、读操作三个过程。
l 器件寻址
AT24LC08 寻址的地址部分包括: 8bit 的器件地址和 8bit 的字地址。器件地址的各 bit 位分布如表 2 所示。前四位是由芯片厂商确定的,为固定值。 A2 必须和硬件的输入管脚一致。 P1 、 P0 为页选地址。 R/W 是读写控制位, 0 表示写操作, 1 表示读操作。
表 2 AT24LC08 器件地址
1 | 0 | 1 | 0 | A2 | P1 | P0 | R/W |
8 位字地址用于寻址当前页中的某个地址空间, 8 位对应 256 个字节。
在芯片读写操作开始时,首先要依次往总线上发送器件地址和字地址,寻址正确才能进行后续的读写操作。
l 写操作
在 Micro2440 开发板上,我们定义 S3C2440 为主设备, AT24LC08 芯片为从设备。主设备会发送 START 信号, STOP 信号;从设备会回复 ACK 信号。
如图 3 所示,当 SCL 为高电平时, SDA 信号被拉低,即出现 START 信号;当 SCL 为高电平时, SDA 信号由低变高,即为 STOP 信号。
图 3 启动、停止条件
当主设备发送完一个字节的数据,从设备会在下一个时钟周期回复一个 ACK ,即一个低电平信号,如图 4 所示。
图 4 输出 ACK 信号
在 AT24LC08 的写操作过程中(以页写模式为例),其操作流程如图 5 所示。首先主设备发出 START 信号,紧接着发送 8 位器件地址和 8 位字地址。器件地址最末位为 WRITE ( 0 )。从设备验证完地址并发回 ACK 信号后,主机就开始发送数据。从设备每收到一个字节的数据就返回一个 ACK 。发送结束后,主机发出 STOP 信号,至此,一个完整的写操作结束。
需要注意的是:页写模式最多支持 16 个字节的写操作。按字地址低四位逐次累加,当累加到 1111 时,下一个时钟周期地址翻转到 0000 ,新写入的数据将覆盖之前的值。
图 5 页写数据顺序
l 读操作
以页读模式为例分析读操作的过程:首先主机发出 START 信号,依次发送器件地址和字地址,此时的读写控制位为 WRITE 。从机收到地址返回 ACK 。然后,主机再发送 START 信号,再传器件地址,此时的读写控制位为 READ 。主机等到从机返回的 ACK ,就开始读数据。读到最后一个字节时,从机返回一个 NACK 信号,主机发出 STOP ,标志读过程结束。
同样,页读模式也支持最多 16 个字节的操作。按字地址的低四位累加,如果溢出就翻转到 0000 地址开始读。
图 6 页读数据顺序
1.4.2 驱动程序架构和访问流程
l 体系结构和访问流程
在流式接口驱动程序中,驱动程序负责把外设抽象成一个文件,而应用程序则使用操作系统提供的文件 API 对外设进行访问。文件 API 被操作系统转发到 FileSys.exe 进程中;然后 FileSys.exe 发现是对设备的操作,就会把执行交给 Device.exe 处理;接着 Device.exe 根据具体的请求,调用流驱动接口函数;最终,驱动程序负责与硬件交互。
图 7 流接口驱动体系结构
具体过程分析如下:
(1) 应用程序须使用该设备,首先调用 CreateFile ( TEXT (“ IIC1 ”)…)打开设备。 CreateFile 函数是在 FileSys.exe 中实现的。但是 FileSys.exe 只作简单判断:如果发现打开的是设备驱动而不是一个文件,那么就重新把主动权交还给设备管理器。
(2) 设备管理器调用驱动程序中的 IIC_Open ()函数打开设备。在 IIC_Open 中,驱动程序可以对硬件进行一些额外的初始化工作,使硬件进入工作状态。
(3) IIC_Open ()函数把打开设备的结果返回给设备管理器。
(4) 设备管理器把 IIC_Open ()返回的结果再传给应用程序中的 CreateFile ()函数调用。
(5) 设备已被成功打开,接下来可对设备进行读写和控制操作。以控制操作为例, CreateFile 函数返回的句柄作为 DeviceIoControl ()的第 1 个参数,向设备发送控制请求。同样, DeviceIoControl ()要 FileSys.exe 转发给设备管理器。
(6) 设备管理器调用驱动程序中的 IIC_IOControl ()函数,与硬件完成交互,读写设备的数据信息,然后返回给设备管理器,再返回给应用程序。
(7) 当应用程序不再使用该设备时,它可调用 CloseHandle ()将设备关闭。此时调用的是驱动程序中的 IIC_Close ()函数。
l 驱动目录组织
在 AT24LC08 读写驱动中,根据访问和交互的对象不同,将驱动程序目录组织如下:
Ø IICBus.c 用于和用户态程序的交互,提供流驱动接口函数;
Ø IIC2440.c 和 S3C2440 的硬件交互,完成寄存器的配置;
Ø 24LC08.c 对 AT24LC08 芯片操作,按照芯片的读写模式配置。
1.4.3 流驱动接口函数的实现
在本驱动中,定义前缀为“ IIC ”, IICBus.c 文件中的流接口驱动函数都以该前缀命名,本驱动中流接口重点实现以下 3 个函数:
BOOL IIC_Init(DWORD dwcontext)
BOOL IIC_Deinit(DWORD hDeviceContext)
BOOL IIC_IOControl(DWORD hOpenContext, DWORD dwCode,
EEPROM_INFO *pBufIn, DWORD dwLenIn,
U8 *pBufOut, DWORD dwLenOut, PDWORD pdwActualOut)
IIC_Init ()函数
该函数在设备加载时被调用,具体代码实现:
BOOL IIC_Init(DWORD dwcontext)
{
RETAILMSG(DBG_OUT,(TEXT("IIC_Init--- /r/n")));
Virtual_Alloc();
Setup_IIC();
return TRUE;
}
Setup_IIC ()函数完成一些端口的初始化,以及 IIC 总线寄存器的初始化配置。
Setup_IIC()
{
RETAILMSG(DBG_OUT,(TEXT("IIC_INTR_Gpio_setting --- /r/n")));
s2440IOP->rGPEUP |= 0xc000; //Pull-up disable
s2440IOP->rGPECON=(s2440IOP->rGPECON&~(3<<30))|(2 << 30); //GPE15=IICSDA.
s2440IOP->rGPECON=(s2440IOP->rGPECON&~(3 << 28))|(2 << 28); //GPE14=IICSCL.
s2440IIC->rIICCON = (1<<7) | (0<<6) | (1<<5) | (0xf); //Tx clock = 0.195MHz
//[Bit7] = Enable ACK, [Bit6] = Prescaler IICCLK=PCLK/16, [Bit5] = Enable interrupt;
//[Bit3:0] = Transmit clock value Tx clock=IICCLK/16
s2440IIC->rIICSTAT = 0x10; //IIC bus data output enable(Rx/Tx)
s2440IIC->rIICADD = 0x10; //2440 slave address = [7:1]
s2440IIC->rIICLC = (1<<2)|(3); // Filter enable, 15 clocks SDA output delay
return TRUE;
}
IIC_Deinit ()函数
该函数在设备卸载时被调用,释放加载时申请的虚拟地址空间,具体代码实现:
BOOL IIC_Deinit(DWORD hDeviceContext)
{
RETAILMSG(DBG_OUT, (TEXT("IIC_INTR_Deinit --- /r/n")));
VirtualFree((void*)s2440IOP, sizeof(IOPreg), MEM_RELEASE);
VirtualFree((void*)s2440IIC, sizeof(PWMreg), MEM_RELEASE);
return TRUE;
}
IIC_IOControl ()函数
该函数用于处理应用程序发送过来的控制命令,控制指令 dwCode 有 IOCTL_IIC_READ 和 IOCTL_IIC_WRITE 两种。当收到读指令 IOCTL_IIC_READ 时,保存读操作的起始地址和读取字节数,然后调用 E2P_Read ()函数读取 AT24LC08 上的数据,将实际读取的字节数保存到 *pdwActualOut 。同样,当收到写指令 IOCTL_IIC_WRITE 时,保存写操作的起始地址和写字节数,然后调用 E2P_PageWrite ()函数往 AT24LC08 上写数据,将实际写入的字节数保存到 *pdwActualOut 。
BOOL IIC_IOControl(DWORD hOpenContext, DWORD dwCode,
EEPROM_INFO *pBufIn, DWORD dwLenIn,
U8 *pBufOut, DWORD dwLenOut, PDWORD pdwActualOut)
{
RETAILMSG(DBG_OUT,(TEXT("IIC: +IIC_IOControl (%d) /r/n"), dwCode));
switch(dwCode){
case IOCTL_IIC_READ:
{
U16 ReadAddress = pBufIn->start_address;
U8 ReadNum = (U8)dwLenOut; //ReadNum 为要读取的字节数
*pdwActualOut = E2P_Read(ReadAddress, ReadNum, pBufOut);
return TRUE;
}
case IOCTL_IIC_WRITE:
{
U16 WriteAddress = pBufIn->start_address;
U8 WriteNum = (U8)dwLenIn;
*pdwActualOut = E2P_Write(WriteAddress, WriteNum, pBufIn->dwData);
return TRUE;
}
}
return TRUE;
}
E2P_Read ()函数和 E2P_Write ()函数都是对 AT24LC08 操作的,在 24LC08.c 中实现,具体代码如下:
U8 E2P_Read(U16 Address, U8 Num, U8 *DataRead)
{
U8 DevAddr_W, DevAddr_R;
U8 PageAddr, WordAddr;
U8 ActualNum;
int i;
if(Address > END_ADDRESS)
{
RETAILMSG(1,(TEXT("IIC: START ADDRESS ERROR!!!")));
return 0;
}
if((Address + Num) > END_ADDRESS) // 超出存储器容量
{
ActualNum =(U8)(END_ADDRESS - Address + 1);
}
else
ActualNum = Num;
memset(&E2pInfo, 0, sizeof(IIC_INFO));
WordAddr = (U8)(Address & 0x00ff); // 获取字地址
PageAddr = (Address >> 8) & 0x03; // 获取 A1, A0 页地址
DevAddr_W = 0xa0 | (PageAddr << 1); //DevAddr = 0x 1 0 1 0 A2 A1 A0 R/W
E2pInfo.iicMode = SETRDADDR;
E2pInfo.iicPt = 0;
E2pInfo.iicData[0] = WordAddr;
E2pInfo.iicDataCount = 1;
E2pInfo.DeviceAddress = DevAddr_W;
IIC_TX(&E2pInfo);
memset(&E2pInfo, 0, sizeof(IIC_INFO));
DevAddr_R = 0xa1 | (PageAddr << 1);
E2pInfo.iicMode = RDDATA;
E2pInfo.iicPt = 0;
E2pInfo.iicDataCount = ActualNum; // 读取的字节数
E2pInfo.DeviceAddress = DevAddr_R;
IIC_RX(&E2pInfo);
for(i = 0; i < ActualNum; i++)
*DataRead++ = *data++;
return ActualNum;
}
E2P_Read ()函数功能如下:
(1) 判断读入的地址参数是否有效,若无效,则退出;
(2) 判断所读的数据长度是否会超出存储器的地址空间,截取实际能读取的数据长度保存;
(3) 将输入的 16 位地址转换成所需的 8 位器件地址和 8 位字地址;
(4) 把读写模式指令、器件地址、字地址、可读取的实际字节数保存到结构体 E2pInfo 中,传递给 IIC_TX ()函数和 IIC_RX ()函数实现 IIC 总线上的数据收发。
(5) 返回读取的实际字节数;
U8 E2P_Write(U16 Address, U8 Num, U8 *DataToWrite)
{
U8 *DataTmp, ActualNum;
U16 Address_End;
U8 FirstSector, SectorNum, EndSector;
int i;
if(Address > END_ADDRESS)
{
RETAILMSG(1,(TEXT("IIC: START ADDRESS ERROR!!!")));
return 0;
}
if((Address + Num) > END_ADDRESS) // 超出存储器容量
{
ActualNum = (U8)(END_ADDRESS - Address + 1);// 获取实际能写入的字符数
}
else
ActualNum = Num;
FirstSector = MAX_BUF - (U8)(Address & 0x000f) ; // 计算第一页能存的字节数
if(ActualNum > FirstSector) // 判断是否能在第一页存完数据
{
SectorNum = (ActualNum - FirstSector) / MAX_BUF;
DataTmp = DataToWrite + FirstSector;
E2P_PageWrite(Address, FirstSector, DataToWrite); // 写第一页数据
for(i = 0; i < SectorNum; i++) // 按每页 16B 写数据
E2P_PageWrite(Address + FirstSector + i* MAX_BUF, MAX_BUF, DataTmp + i*MAX_BUF);
Address_End = Address + FirstSector + SectorNum* MAX_BUF;// 获取末页地址
EndSector = (ActualNum - FirstSector) % MAX_BUF;// 获取末页待写字节数
E2P_PageWrite(Address_End, EndSector, DataTmp + SectorNum*MAX_BUF);
}
else
E2P_PageWrite(Address, ActualNum, DataToWrite); // 写第一页数据
return ActualNum;
}
E2P_Write ()函数功能如下:
(1) 判断写操作起始地址参数是否有效,若无效,则退出;
(2) 判断写数据长度是否会超出存储器的地址空间,截取实际能写的数据长度保存;
(3) 计算起始地址所在页能写的字节数;
(4) 判断实际要写的数据数是否超出第一页,如没有,则直接调用 E2P_PageWrite ()函数完成页写操作;
(5) 若超出,则判断需分几页写,分别调用 E2P_PageWrite ()函数按页写数据;
(6) 返回实际写入的数据字节数。
void E2P_PageWrite(U16 Address, U8 Num, U8 *SectorData)
{
U8 DevAddr_W;
U8 PageAddr, WordAddr;
int i;
memset(&E2pInfo, 0, sizeof(IIC_INFO));
WordAddr = (U8)(Address & 0x00ff); // 获取字地址
PageAddr = (Address >> 8) & 0x03; // 获取 A1, A0 页地址
DevAddr_W = 0xa0 | (PageAddr << 1); //DevAddr = 0x 1 0 1 0 A2 A1 A0 R/W
E2pInfo.iicMode = WRDATA;
E2pInfo.iicPt = 0;
E2pInfo.iicData[0] = WordAddr;
E2pInfo.DeviceAddress = DevAddr_W;
for(i =0; i < Num; i++)
{
E2pInfo.iicData[i+1] = *(SectorData + i);
}
E2pInfo.iicDataCount = 1+Num;
IIC_TX(&E2pInfo);
}
E2P_PageWrite ()函数主要完成地址转换和传递结构体 E2pInfo 的过程,然后调用 IIC_TX ()函数实现 IIC 总线上的数据发送。
IIC_TX ()函数和 IIC_RX ()函数在 IIC2440.c 中实现,代码如下:
void IIC_TX(IIC_INFO *piicinfo)
{
g_iicinfo = piicinfo;
s2440IIC->rIICDS = g_iicinfo->DeviceAddress;
s2440IIC->rIICSTAT = 0xf0; //MasTx,Start
s2440IIC->rIICCON = 0xAf;
//Clearing the pending bit isn't needed because the pending bit has been cleared.
while(g_iicinfo->iicDataCount!=-1)
Run_IICPoll();
return;
}
void IIC_RX(IIC_INFO *piicinfo)
{
g_iicinfo = piicinfo;
s2440IIC->rIICDS = g_iicinfo->DeviceAddress;
s2440IIC->rIICSTAT = 0xb0; //MasRx,Start
s2440IIC->rIICCON = 0xaf; //Resumes IIC operation.
while(g_iicinfo->iicDataCount!=-1)
Run_IICPoll();
data = g_iicinfo->iicData + 1 ;
return;
}
这两个函数都是完成和 S3C2440 的 IIC 寄存器相关的操作,按照 IIC 协议的时序依次往寄存器中器件地址和相关值,然后调用 Run_IICPoll ()函数逐个字节传递数据。
void Run_IICPoll(void)
{
// When using polling mode
if(s2440IIC->rIICCON & 0x10) //Tx/Rx Interrupt Enable
IICPoll();
}
void IICPoll(void)
{
switch(g_iicinfo->iicMode)
{
case RDDATA:
if((g_iicinfo->iicDataCount--)==0)
{
g_iicinfo->iicData[g_iicinfo->iicPt++] = s2440IIC->rIICDS;
s2440IIC->rIICSTAT = 0x90; //Stop MasRx condition
s2440IIC->rIICCON = 0xAf; //Resumes IIC operation.
Delay(10); //Wait until stop condtion is in effect., Too long time...
//The pending bit will not be set after issuing stop condition.
break;
}
g_iicinfo->iicData[g_iicinfo->iicPt++] = s2440IIC->rIICDS;
//The last data has to be read with no ack.
if((g_iicinfo->iicDataCount)==0)
s2440IIC->rIICCON = 0x2f; //Resumes IIC operation with NOACK
else
s2440IIC->rIICCON = 0xAf; //Resumes IIC operation with ACK
break;
case WRDATA:
if((g_iicinfo->iicDataCount--)==0)
{
s2440IIC->rIICSTAT = 0xd0; //stop MasTx condition
s2440IIC->rIICCON = 0xAf; //resumes IIC operation.
Delay(10); // we should adjust this time.
//The pending bit will not be set after issuing stop condition.
break;
}
s2440IIC->rIICDS=g_iicinfo->iicData[g_iicinfo->iicPt++];
Delay(1); //for setup time until rising edge of IICSCL
s2440IIC->rIICCON = 0xAf; //resumes IIC operation.
break;
case SETRDADDR:
if((g_iicinfo->iicDataCount--)==0)
{
break; //IIC operation is stopped because of IICCON[4]
}
s2440IIC->rIICDS = g_iicinfo->iicData[g_iicinfo->iicPt++];
Delay(1); //for setup time until rising edge of IICSCL
s2440IIC->rIICCON = 0xAf; //resumes IIC operation.
break;
default:
break;
}
}
当 S3C2440 的 rIICCON 寄存器中断位使能, Run_IICPoll ()函数调用 IICPoll ()函数。 IICPoll ()函数实现三种模式的数据传输:
( 1 ) SETRDADDR
当执行读操作时,主机往从机写入第一个地址时使用该模式,只写两个字节数据(器件地址和字地址),然后退出。
( 2 ) RDDATA
当执行读操作,主机发完起始地址后,进入 RDDATA 模式,地址依次累加读取 AT24LC08 上的数据,最完最后一个字节时,回复 NACK ,主机收到后发 STOP 信号退出。
( 2 ) WRDATA
当执行写操作时进入 WRDATA 模式,主机往从机发送器件地址和字地址,然后地址依次累加往 AT24LC08 写入数据,最后主机发 STOP 信号退出。
1.4.4 添加 makefile 文件和 Source 文件
(一) makefile 文件
!INCLUDE $(_MAKEENVROOT)/makefile.def
(二) Source 文件
RELEASETYPE=PLATFORM
TARGETNAME=IICBus
TARGETTYPE=DYNLINK
DLLENTRY=DllEntry
TARGETLIBS= /
$(_COMMONSDKROOT)/lib/$(_CPUINDPATH)/coredll.lib /
MSC_WARNING_LEVEL=$(MSC_WARNING_LEVEL) /W3 /WX
INCLUDES= /
$(_TARGETPLATROOT)/inc; /
$(_COMMONOAKROOT)/inc; /
$(_PUBLICROOT)/common/oak/inc;$(_PUBLICROOT)/common/sdk/inc;$(_PUBLICROOT)/common/ddk/inc; /
../../inc /
SOURCES= /
24LC08.c /
IIC2440.c /
IICBus.c /
FILE_VIEW_INCLUDES_FOLDER= /
IIC.h /
IIC2440.h /
24LC08.h /
1.4.5 编写 DLL 的导出函数定义文件
.DEF 文件定义了 DLL 的导出函数列表。在 IIC 中添加一个文本文件,命名为 IICBus.def ,然后在该文件中输入如下内容:
LIBRARY IICBus
EXPORTS
IIC_Init
IIC_Deinit
IIC_Open
IIC_Close
IIC_IOControl
IIC_PowerUp
IIC_PowerDown
IIC_Read
IIC_Write
IIC_Seek
1.4.6 配置注册表
在 platform.reg 中添加如下内容:
[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/IICBus]
"Dll" = "IICBus.dll"
"Prefix" = "IIC"
"Index" = dword:1
"Order" = dword:0
另外,还要在 platform.bib 中添加如下内容:
IICBus.dll $(_FLATRELEASEDIR)/IICBus.dll NK SH