全部博文(776)
分类: WINDOWS
2010-09-20 23:08:48
目录:
1. 异步非阻塞串口通讯的优点
2. 异步非阻塞串口通讯的基本原理
3. 异步非阻塞串口通讯的基础知识
4. 异步非阻塞串口通讯的实现步骤
2005.01.05
一,异步非阻塞串口通讯的优点
读写串行口时,既可以同步执行,也可以重叠(异步)执行。
在同步执行时,函数直到操作完成后才返回。这意味着在同步执行时线程会被阻塞,从而导致效率下降。
在重叠执行时,即使操作还未完成,调用的函数也会立即返回。费时的I/O操作在后台进行,这样线程就可以干别的事情。
例如,线程可以在不同的句柄上同时执行I/O操作,甚至可以在同一句柄上同时进行读写操作。"重叠"一词的含义就在于此。
二,异步非阻塞串口通讯的基本原理
首先,确定要打开的串口名、波特率、奇偶校验方式、数据位、停止位,传递给CreateFile()函数打开特定串口;
其次,为了保护系统对串口的初始设置,调用 GetCommTimeouts()得到串口的原始超时设置;
然后,初始化DCB对象,调用SetCommState() 设置DCB,调用SetCommTimeouts()设置串口超时控制;
再次,调用SetupComm()设置串口接收发送数据的缓冲区大小,串口的设置就基本完成,之后就可以启动读写线程了。
三,异步非阻塞串口通讯的基础知识
下面来介绍并举例说明一下编写异步非阻塞串口通讯的程序中将会使用到的几个关键函数
CreateFile()
功能:打开串口设备
函数原型
HANDLE CreateFile(
LPCTSTR lpFileName, // 串口名称字符串;如: "COM1" 或 "COM2"
DWORD dwDesiredAccess, // 设置读写属性(访问模式 );一般为 GENERIC_READ|GENERIC_WRITE,
DWORD dwShareMode, // 共享模式;"必须"为 0, 即不能共享
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性;一般为NULL
DWORD dwCreationDistribution, // 创建方式,串口设置必须设置此值; 在这里"必须"为 OPEN_EXISTING
DWORD dwFlagsAndAttributes, // 文件属性和标志;在这里我们设置成FILE_FLAG_OVERLAPPED ,实现异步I/O
HANDLE hTemplateFile // 临时文件的句柄,通常为NULL
);
说明:
如果调用成功,那么该函数返回文件的句柄,如果调用失败,则函数返回INVALID_HANDLE_VALUE。
Forexample:
Handle m_hComm = CreateFile(com1,GENERIC_READ||GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,0);
CloseHandle();
功能:关闭串口
BOOL CloseHandle(
HANDLE hObject // handle to object to close
)
这个,我想就不多说了吧!
GetCommState()
功能:获得串口状态
BOOL GetCommState(
HANDLE hFile, // handle of communications device
LPDCB lpDCB // address of device-control block structure
);
SetCommState()
功能:设置串口状态
BOOL SetCommState(
HANDLE hFile, // handle of communications device
LPDCB lpDCB // address of device-control block structure
);
说明:
在打开通信设备句柄后,常常需要对串行口进行一些初始化工作。这需要通过一个DCB结构来进行。DCB结构包含了诸如波特率、每个字符的数据位数、奇偶校验和停止位数等信息。在查询或配置置串行口的属性时,都要用DCB结构来作为缓冲区。
调用GetCommState函数可以获得串口的配置,该函数把当前配置填充到一个DCB结构中。一般在用CreateFile打开串行口后,可以调用 GetCommState函数来获取串行口的初始配置。要修改串行口的配置,应该先修改DCB结构,然后再调用SetCommState函数用指定的 DCB结构来设置串行口
Forexample:
DCB dcb;
memset(&dec,0,dizeof(dcb));
if(!GetCommState(HComm,&dcb))//获取当前DCB配置
return FALSE;
dcb.BaudRate = CBR_9600;//修改数据传输率
............
if(SetCommState(hComm,&dcb))//设置新参数
...... //错误处理
BuildCommDCB()
功能:初始化DCB结构
BOOL BuildCommDCB(
LPCTSTR lpDef, // pointer to device-control string
LPDCB lpDCB // pointer to device-control block
);
Forexample:
DCB dcb;
memset(&dcb,0,sizeof(dcb));
dcb.DCBlength = sizeof(dcb);
if(!BuildCommDCb("9600,n,8,1",&dcb))//"baud=9600 parity=N data=8 stop=1"
{
...... //参数修改错误
return FALSE;
}
else
{
...... //己准备就绪
}
说明:功能同上面的例子。
SetupComm()
功能:设置I/O缓冲区的大小
函数原型:
BOOL SetupComm(
HANDLE hFile, // handle to communications device
DWORD dwInQueue, // size of input buffer
DWORD dwOutQueue // size of output buffer
);
说明:
除了在DCB中的设置外,程序一般还需要设置I/O缓冲区的大小和超时。Windows用I/O缓冲区来暂存串行口输入和输出的数据,如果通信的速率较高,则应该设置较大的缓冲区。调用SetupComm函数可以设置串行口的输入和输出缓冲区的大小。
先介绍一个结构:COMMTIMEOUTS
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout; // 读间隔超时
DWORD ReadTotalTimeoutMultiplier; // 读时间系数
DWORD ReadTotalTimeoutConstant; // 读时间常量
DWORD WriteTotalTimeoutMultiplier; // 写时间系数
DWORD WriteTotalTimeoutConstant; // 写时间常量
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
再介绍两个函数
GetCommTimeouts
功能:读取TimeOut的值
函数原型:
BOOL GetCommTimeouts(
HANDLE hFile, // handle of communications device
LPCOMMTIMEOUTS lpCommTimeouts // address of comm. time-outs structure
);
SetCommTimeouts
功能:设置TimeOUt的值
函数原型:
BOOL SetCommTimeouts(
HANDLE hFile, // handle of communications device
LPCOMMTIMEOUTS lpCommTimeouts // address of communications time-out structure
);
这里顺便介绍一下TimeOut机制的两个性质:
超时函数
说明:
在用ReadFile和WriteFile读写串行口时,需要考虑超时问题。如果在指定的时间内没有读出或写入指定数量的字符,那么ReadFile或 WriteFile的操作就会结束。要查询当前的超时设置应调用GetCommTimeouts函数,该函数会填充一个COMMTIMEOUTS结构。调用SetCommTimeouts可以用某一个COMMTIMEOUTS结构的内容来设置超时。
有两种超时:间隔超时和总超时。间隔超时是指在接收时两个字符之间的最大时延,总超时是指读写操作总共花费的最大时间。写操作只支持总超时,而读操作两种超时均支持。用COMMTIMEOUTS结构可以规定读/写操作的超时,该结构的定义为: COMMTIMEOUTS结构的成员都以毫秒为单位。总超时的计算公式是:
总超时=时间系数×要求读/写的字符数 + 时间常量
例如,如果要读入10个字符,那么读操作的总超时的计算公式为:
读总超时=ReadTotalTimeoutMultiplier×10 + ReadTotalTimeoutConstant
可以看出,间隔超时和总超时的设置是不相关的,这可以方便通信程序灵活地设置各种超时。
如果所有写超时参数均为0,那么就不使用写超时。如果ReadIntervalTimeout为 0,那么就不使用读间隔超时,如果 ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都为0,则不使用读总超时。如果读间隔超时被设置成MAXDWORD并且两个读总超时为0,那么在读一次输入缓冲区中的内容后读操作就立即完成,而不管是否读入了要求的字符。
在用重叠方式读写串行口时,虽然ReadFile和WriteFile在完成操作以前就可能返回,但超时仍然是起作用的。在这种情况下,超时规定的是操作的完成时间,而不是ReadFile和WriteFile的返回时间。
Forexample:
COMMTIMEOUTS timeOver;
memset(&&timeOver.0.sizeof(timeOver));
DWORDtimeMultiplier,timeConstant;
timeOver.ReadTotalTimeoutMultiplier=timeMultiplier;
timeOver.ReadTotalTimeoutConstant=timeConstant;
SetCommTimeouts(hComport,&&timeOver);
ReadFile()
功能:读取数据
函数原型:
BOOL ReadFile(
HANDLE hFile, // 串口名称字符串(文件句柄 )
LPVOID lpBuffer, // 读缓冲区
DWORD nNumberOfBytesToRead, // 要求读入的字节数
LPDWORD lpNumberOfBytesRead, // 实际读入的字节数
LPOVERLAPPED lpOverlapped // 指向一个OVERLAPPED结构
); //若返回TRUE则表明操作成功
Forexample:
char *pReciveBuf;
DWORD nWantRead = 100,
nReadRead;
LPOVERLAPPED m_OverlappedRead;
BOOL bReadStatus = ReadFile( m_hComm, preciveBuf,nWantRead, &&nReadRead, &&m_OverlappedRead );
WriteFile()
功能:来将资料写入Serial port.
函数原型:
BOOL WriteFile(
HANDLE hFile, // handle to file to write to
LPCVOID lpBuffer, // pointer to data to write to file
DWORD nNumberOfBytesToWrite, // number of bytes to write
LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
LPOVERLAPPED lpOverlapped // pointer to structure needed for overlapped I/O
);
说明:
ReadFile函数只要在串行口输入缓冲区中读入指定数量的字符,就算完成操作。
而WriteFile函数不但要把指定数量的字符拷入到输出缓冲中,而且要等这些字符从串行口送出去后才算完成操作。
当ReadFile 和WriteFile返回FALSE时,不一定就是操作失败,线程应该调用GetLastError函数分析返回的结果。例如,在重叠操作时如果操作还未完成函数就返回,那么函数就返回FALSE,而且GetLastError函数返回ERROR_IO_PENDING。
如果GetLastError函数返回ERROR_IO_PENDING,则说明重叠操作还为完成,线程可以等待操作完成。
有两种等待办法:一种办法是用象WaitForSingleObject这样的等待函数来等待OVERLAPPED结构的hEvent成员,
可以规定等待的时间,在等待函数返回后,调用GetOverlappedResult。
另一种办法是调用GetOverlappedResult函数等待,如果指定该函数的bWait参数为TRUE,
那么该函数将等待OVERLAPPED结构的hEvent 事件。
GetOverlappedResult可以返回一个OVERLAPPED结构来报告包括实际传输字节在内的重叠操作结果。
如果规定了读/写操作的超时,那么当超过规定时间后,hEvent成员会变成有信号的。因此,在超时发生后,WaitForSingleObject和 GetOverlappedResult都会结束等待。WaitForSingleObject的dwMilliseconds参数会规定一个等待超时,该函数实际等待的时间是两个超时的最小值。注意GetOverlappedResult不能设置等待的时限,因此如果hEvent成员无信号,则该函数将一直等待下去
ClearCommError()
功能: 从字面上的意思看来, 它是用来清除错误情况用的, 但是实际上它还可以拿来取得目前通讯设备的一些信息.
函数原型:
BOOL ClearCommError(
HANDLE hFile, // handle to communications device
LPDWORD lpErrors, // pointer to variable to receive error codes
LPCOMSTAT lpStat // pointer to buffer for communications status
);
说明:
在调用ReadFile和WriteFile之前,线程应该调用ClearCommError函数清除错误标志。
该函数负责报告指定的错误和设备的当前状态。
PurgeComm()
功能:终止目前正在进行的读或写的动作
函数原型:
BOOL PurgeComm(
HANDLE hFile, // handle of communications resource
DWORD dwFlags // action to perform
);
参数说明:
HANDLE hFile,//串口名称字符串
dwFlags 共有四种 flags:
PURGE_TXABORT: 终止目前正在进行的(背景)写入动作
PURGE_RXABORT: 终正目前正在进行的(背景)读取动作
PURGE_TXCLEAR: flush 写入的 buffer
PURGE_TXCLEAR: flush 读取的 buffer
调用PurgeComm函数可以终止正在进行的读写操作,该函数还会清除输入或输出缓冲区中的内容。
GetCommMask()
功能:得到设置的通信事件的掩码
函数原型:
BOOL GetCommMask(
HANDLE hFile, // handle of communications device
LPDWORD lpEvtMask // address of variable to get event mask
);
SetCommMask()
功能:设置想要得到的通信事件的掩码
函数原型:
BOOL SetCommMask(
HANDLE hFile, // handle of communications device
DWORD dwEvtMask // mask that identifies enabled events
);
说明:
可设置的通信事件标志(即SetCommMask()函数所设置的掩码)
可以有EV_BREAK、EV_CTS、EV_DSR、 EV_ERR、EV_RING、EV_RLSD、EV_RXCHAR、EV_RXFLAG、EV_TXEMPTY。
注:若对端口数据的响应时间要求较严格,可采用事件驱动I/O读写,Windows定义了9种串口通信事件,较常用的有:
EV_RXCHAR: 接收到一个字节,并放入输入缓冲区。
EV_TXEMPTY: 输出缓冲区中的最后一个字符发送出去。
EV_RXFLAG: 接收到事件字符(DCB结构中EvtChar成员),放入输入缓冲区。
下面是MSDN上的解释:
EV_BREAK A break was detected on input.
EV_CTS The CTS (clear-to-send) signal changed state.
EV_DSR The DSR (data-set-ready) signal changed state.
EV_ERR A line-status error occurred. Line-status errors are CE_FRAME, CE_OVERRUN, and CE_RXPARITY.
EV_RING A ring indicator was detected.
EV_RLSD The RLSD (receive-line-signal-detect) signal changed state.
EV_RXCHAR A character was received and placed in the input buffer.
EV_RXFLAG The event character was received and placed in the input buffer. The event character is specified in the device's DCB structure, which is applied to a serial port by using the SetCommState function.
EV_TXEMPTY The last character in the output buffer was sent.
WaitCommEvent()
功能:等待设定的通讯事件的发生
函数原型:
BOOL WaitCommEvent(
HANDLE hFile, // handle of communications device
LPDWORD lpEvtMask, // address of variable for event that occurred
LPOVERLAPPED lpOverlapped, // address of overlapped structure
);
说明:
WaitCommEvent() 会一直 block(阻塞) 到你所设定的通讯事件发生为止.
所以当 WaitCommEvent() 返回时, 你可以由 lpEvtMask 取得究竟是那一事件发生, 再来决定要如何处理.
WaitForSingleObject()
功能:保证线程同步的等待函数
函数原型:
DWORD WaitForSingleObject(HANDLE hHandle,//同步对象的句柄
DWORD dwMilliseconds//以毫秒为单位的超时间隔,如果设为INFINITE,则超时间隔是无限的
);
说明:
返回值 含义
WAIT_FAILED 函数失败
WAIT_OBJECT_0 指定的同步对象处于有信号的状态
WAIT_ABANDONED 拥有一个mutex的线程已经中断了,但未释放该MUTEX
WAIT_TIMEOUT 超时返回,并且同步对象无信号
WaitForMultipleObjects()
功能:可以同时监测多个同步对象
函数原型:
DWORD WaitForMultipleObjects(DWORD nCount,//句柄数组中句柄的数目
CONST HANDLE *lpHandles,//代表一个句柄数组
BOOL bWaitAll, //说明了等待类型(),如果为TRUE,那么函数在所有对象都有信号后才返回,
//如果为FALSE,则只要有一个对象变成有信号的,函数就返回
DWORD dwMilliseconds//以毫秒为单位的超时间隔
);
说明:
返回值 含义
WAIT_OBJECT_0到WAIT_ OBJECT_0+nCount-1 若bWaitAll为TRUE,则返回值表明所有对象都是有信号的。
如果bWaitAll为FALSE,则返回值减去WAIT_OBJECT_0就是数组中有信号对 象的最小索引。
WAIT_ABANDONED_0 到WAIT_ ABANDONED_ 0+nCount-1 若bWaitAll为TRUE,则返回值表明所有对象都有信号,但有一个mutex被 放弃了。若bWaitAll为FALSE,则返回值减去WAIT_ABANDONED_0就是被放弃 mutex在对象数组中的索引。
WAIT_TIMEOUT 超时返回
只一个框架性流程而矣............
实现重叠模型的步骤
下面就结合俺写的一个Console程序简单示例进行说明:
【第一步】打开串口
HANDLE m_hCom = CreateFile("com1",GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (m_hCom == INVALID_HANDLE_VALUE)
{
cout<<"CreateFile fail!"<
return -1;
}
cout<<"CreateFile OK!"<< p="" />
【第二步】设置缓冲区大小
if(!SetupComm(m_hCom,2048,2048))
{
cout<<"SetupComm fail! Close Comm!"<
CloseHandle(m_hCom);
return -1;
}
cout<<"SetupComm OK!"<< p="" />
【第三步】设置超时
COMMTIMEOUTS TimeOuts;
memset(&TimeOuts,0,sizeof(TimeOuts));
TimeOuts.ReadIntervalTimeout = MAXDWORD;
TimeOuts.ReadTotalTimeoutConstant = 0;
TimeOuts.ReadTotalTimeoutMultiplier = 0;
TimeOuts.WriteTotalTimeoutConstant = 2000;
TimeOuts.WriteTotalTimeoutMultiplier = 50;
SetCommTimeouts(m_hCom,&TimeOuts);
【第四步】设置串口参数
DCB dcb;
if (!GetCommState(m_hCom,&dcb))
{
cout<<"GetCommState fail! Comm close"<
CloseHandle(m_hCom);
return -1;
}
cout<<"GetCommState OK!"<
dcb.DCBlength = sizeof(dcb);
if (!BuildCommDCB("9600,n,8,1",&dcb))//填充DCB的数据传输率、奇偶校验类型、数据位、停止位
{
//参数修改错误,进行错误处理
cout<<"BuileCOmmDCB fail,Comm close!"<
CloseHandle(m_hCom);
return -1;
}
if(SetCommState(m_hCom,&dcb))
{
cout<<"SetCommState OK!"<
}
【第五步】建立并初始化重叠结构
OVERLAPPED wrOverlapped;
ZeroMemory(&wrOverlapped,sizeof(wrOverlapped));
if (wrOverlapped.hEvent != NULL)
{
ResetEvent(wrOverlapped.hEvent);
wrOverlapped.hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
}
【第六步】封装数据(按照自己的格式封装需要发送的数据,此步可以省略)
typedef enum
{
HEAT_BEAT, //心跳数据
NET_STATE,//网络状态数据
PACKET //正常数据包
//支持可扩展性......
}ProtocolType;
typedef enum
{
Train_No,//无线车次信息
Attemper_Command,//调度命令信息
Revert_Command,//调度命令回执信息
Replay_Command,//重发的调度命令信息
KGL_SING //开关量数据
//支持可扩展性......
}PacketDataType;
//串口数据结构
typedef struct SerialNetProto
{
unsigned long PacketSize; //包总长度,不包括本身字段
ProtocolType NetState; //协议包类型
PacketDataType DataType; //数据类型
unsigned long SourcedAddr; //数据包源地址
unsigned long DestinationAddr; //数据包目的地址
unsigned long DataLength; //包的数据段长度
unsigned long Offset; // 数据在整个包中的偏移地址
}PacketHead;
int DataLen = 100;
char *pBuf = new char[DataLen];
strcpy(pBuf,"Hello World!");
DataLen = strlen(pBuf);
PacketHead Myhead;
Myhead.DestinationAddr = 11;
Myhead.SourcedAddr = 10;
Myhead.DataType = Attemper_Command;
Myhead.DataLength = DataLen;
Myhead.NetState = PACKET;
Myhead.PacketSize = sizeof(PacketHead) - sizeof(unsigned long);
Myhead.Offset = sizeof(Myhead.DestinationAddr) +sizeof(Myhead.SourcedAddr) + sizeof(Myhead.DataType) +sizeof(Myhead.DataLength) + sizeof(Myhead.NetState) + sizeof(Myhead.PacketSize);
char *pSendBuffer = new char[sizeof(Myhead)+DataLen+ 4];//发送的数据
memcpy(pSendBuffer,"##",2);//包头标志
memcpy(pSendBuffer+2,(char*)&Myhead,sizeof(Myhead));//包头
memcpy(pSendBuffer+2+sizeof(Myhead),pBuf,DataLen);//数据
memcpy(pSendBuffer+2+sizeof(Myhead)+DataLen,"@@",2);//包尾标志
【第七步】发送数据
DWORD dwError;
//DWORD dwWantSend = 100;
DWORD dwRealSend = 0;
char* pReadBuf = NULL;
if (ClearCommError(m_hCom,&dwError,NULL))
{
PurgeComm(m_hCom,PURGE_TXABORT | PURGE_TXCLEAR);
cout<<"PurgeComm OK!"<
}
if (!WriteFile(m_hCom,pSendBuffer,sizeof(Myhead)+DataLen+ 4,&dwRealSend,&wrOverlapped))
{
if (GetLastError() == ERROR_IO_PENDING)
{
while (!GetOverlappedResult(m_hCom,&wrOverlapped,&dwRealSend,FALSE))
{
if (GetLastError() == ERROR_IO_INCOMPLETE)
{
//cout<<"写未完成,继续!"<
continue;
}
else
{
cout<<"发生错误,尝试恢复!"<
ClearCommError(m_hCom,&dwError,NULL);
break;
}
}
}
}
【第八步】数据接收
DWORD dwError;
DWORD dwWantRead = 100;
DWORD dwRealRead = 0;
char* pReadBuf = new char[100];
if (ClearCommError(m_hCom,&dwError,NULL))
{
PurgeComm(m_hCom,PURGE_TXABORT | PURGE_TXCLEAR);
cout<<"PurgeComm OK!"<
}
if(!ReadFile(m_hComm,pReadBuf,dwWantRead,&RealRead,&wrOverlapped))
{
if(dwError = GetLastError()==ERROR_IO_PENDING)
{
While(GetOverlappedResult(m_hComm,&wrOverlapped,&dwRealRead,FALSE))
{
//对接收到的数据进行数据解析,处理
//【第九步】............
cout<<"dwRealRead = "<
}
}
}
【第九步】数据解析(数据解包处理)
#define MAX_SERIAL_BUFFER 4096
BOOL CanGetFullFrame(char* pReadBuf,int& dwRealRead)
{
static char Buf[MAX_SERIAL_BUFFER*2];//自定义一个数据缓冲区
static unsigned long nFrameStart = 0;//数据祯的开始位置
static unsigned long nFrameEnd = 0;//数据祯的结束位置
static unsigned long nCurrectPos = 0;//指针当前位置
char *pdest = NULL;
if (pReadBuf && (dwRealRead!= 0))
{
memcpy(&Buf[nCurrectPos],pReadBuf,dwRealRead);
nCurrectPos = nCurrectPos + dwRealRead;//更新当前位置
}
//查找数据祯的开始标志
pdest = (char*)Find(Buf,"##",MAX_SERIAL_BUFFER*2,2);
if (pdest)
{
nFrameStart = unsigned long(pdest - Buf);//找到数据祯的开始位置
}
else//没有找到开始祯标志"##"
{
Buf[0] = Buf[nCurrectPos];//丢弃数据
nFrameStart = 0;
return FALSE;
}
//查找数据祯的结尾标志
pdest = (char*)Find(Buf,"@@",MAX_SERIAL_BUFFER*2,2);
if (pdest)
{
nFrameEnd = unsigned long (pdest - Buf+2);
dwRealRead= nFrameEnd - nFrameStart;
memcpy(pReadBuf,&Buf[nFrameStart],dwRealRead);
nFrameStart = nFrameEnd;//指向下一帧的开始位置
nCurrectPos = nCurrectPos - dwRealRead;//修正nCurrentPos值
memcpy(Buf,&Buf[nFrameEnd],nCurrectPos);//向前移动数据
return TRUE;
}
else
{
return FALSE;
}
}
//一个在内存块中查找指定字符串的函数
void* Find(const char *pSour,const char *pDest,int SourLen,int DestLen)
{
int i = 0, j = 0;
while(i < SourLen && j < DestLen)
{
if(*(pSour + i) == *(pDest + j))
{
i++;
j++;
}
else
{
i =i - j + 1;
j = 0;
}
}
if(j == DestLen)
{
return (void*)(pSour + (i - DestLen));
}
else
{
return NULL;
}
}
【第十步】重新投递Overlapped,略......
traceback:
http://blog.csdn.net/yanbilian/archive/2007/11/13/1881836.aspx