串口编程学习报告
引言:
串口是计算机上非常通用设备通信的协议,串行接口可以接收来自CPU的并行数据字符转换为连续的串行数据流发送出去,同时可将接收的串行数据流转换为并行数据字符供给CPU的器件,在日常生活中应用广泛,因此有必要对串口进行深入学习,首先谈到需要了解的硬件和软件方面的知识,然后可以整体的了解从硬件到软件数据收发的过程.
一.硬件部分
1、串口通信是一位一位的传输的,但是计算机处理数据是并行数据,所以当数据由计算机送至数据发送器时,首先用移位寄存器把串行数据转换为并行数据才能送入计算机处理。RS232C是用正负电压表示逻辑状态与TTL高低电压表示逻辑状态的规定不一样在这里就需要硬件部分有ETA与TTL电平转换电路这样才能和计算机和TTL的器件连接,并且下位机还需要有可编程的串口芯片这样可以通过编程控制我们需要达到的成效.
2、串口要遵守串口之间的协议,在物理方面就要求有相同的波特率来控制数据的发送速度,因此需要有波特率发生器. 具体的要多大的波特率可以根据需求通过控制寄存器来控制发生的波特率
3、根据同步/异步方式,有时要要求上位机与下位机时间同步,需要一个授时器授时.
4、每个下位机都有它自己的地址,这时当上位机要寻找这个下位机时就需要寻找它的地址,这里要一个地址译码器。
5、对于数据流的控件,可以是硬件流控制(RTC/CTS DTR/CTS等)也可以是软件流控制(XON/XOFF),不过现在大多都是用软件控制。
6、普遍情况下电脑都有一个RS232的串口,所以我们一般都采用RS232串口线,通常情况下无论使用的是九针的还是二十五针的只要接通2,3针就可以进行简单的收发数据测试DB-25,DB-9接口如下图。
7、串口都有一定的存贮功能,这些可以由下位机单片机上的RAM,移位寄存器或其它的外部存贮来实现的.
二.软件部分
WINDOWS API已经提供给我们通用接口方法程序,只要在适当的时候调用就可以完成自己想要的功能,在写一个串口程序中(打开/关闭,配置,读写)需要如下的API提供的方法:
1、打开串口:
Win32系统把文件的概念进行了扩展,无论是文件、通信设备、命名管道、邮件槽、磁盘、还是控制台,都是用API函数CreateFile来打开或创建的。该函数的原型为:
private static extern int CreateFile(
string lpFileName,
uint dwDesiredAccess,
int dwShareMode,
int lpSecurityAttributes,
int dwCreationDistribution,
int dwFlagsAndAttributes,
int hTemplateFile);
lpFileName:将要打开的串口逻辑名,如“COM1”; dwDesiredAccess:指定串口访问的类型,可以是读取、写入或二者并列;
dwShareMode:指定共享属性,由于串口不能共享,该参数必须置为0;
lpSecurityAttributes:引用安全性属性结构,缺省值为NULL; dwCreationDistribution:创建标志,对串口操作该参数必须置为OPEN_EXISTING;
dwFlagsAndAttributes:属性描述,用于指定该串口是否进行异步操作,该值为FILE_FLAG_OVERLAPPED,表示使用异步的I/O;该值为0,表示同步I/O操作;
hTemplateFile:对串口而言该参数必须置为NULL;
2、关闭串口:
利用API函数关闭串口非常简单,只需使用CreateFile函数返回的句柄作为参数调用CloseHandle即可:
BOOL CloseHandle(
Int hObject; //handle to object to close
);
3、配置串口:
(1)缓冲区大小设置:打开串口后,可以对I/O口的缓冲区的大小进行设置,虽然Windows用I/O缓冲区来暂存串口输入和输出的数据。如果通信的速率较高,则应该设置较大的缓冲区。调用SetupComm函数可以设置串行口的输入和输出缓冲区的大小。
BOOL SetupComm(
Int hFile, // 通信设备的句柄
Int dwInQueue, // 输入缓冲区的大小(字节数)
Int dwOutQueue // 输出缓冲区的大小(字节数)
);
(2)超时设置:在用ReadFile和WriteFile读写串行口时,需要考虑超时问题。超时的作用是在指定的时间内没有读入或发送指定数量的字符,ReadFile或WriteFile的操作仍然会结束。要查询当前的超时设置应调用GetCommTimeouts函数,该函数会填充一个COMMTIMEOUTS结构。调用SetCommTimeouts可以用某个COMMTIMEOUTS结构的内容来设置超时。
读写串口的超时有两种情况:间隔超时和总超时。间隔超时是指在接收时两个字符之间的最大时延。总超时是指读写操作总共花费的最大时间。写操作只支持总超时,而读操作两种超时均支持。用COMMTIMEOUTS结构可以规定读写操作的超时。
COMMTIMEOUTS结构的定义为:
typedef struct _COMMTIMEOUTS {
int ReadIntervalTimeout; //读间隔超时
int ReadTotalTimeoutMultiplier; //读时间系数
int ReadTotalTimeoutConstant; //读时间常量
int WriteTotalTimeoutMultiplier; // 写时间系数
int WriteTotalTimeoutConstant; //写时间常量
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
COMMTIMEOUTS结构的成员都以毫秒为单位。总超时的计算公式是:
总超时=时间系数×要求读/写的字符数+时间常量
如果所有写超时参数均为0,那么就不使用写超时。如果ReadIntervalTimeout为0,那么就不使用读间隔超时。如果ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 都为0,则不使用读总超时。如果读间隔超时被设置成MAXDWORD并且读时间系数和读时间常量都为0,那么在读一次输入缓冲区的内容后读操作就立即返回,而不管是否读入了要求的字符。
(3)对串口的一些属性配置我们会用到DCB, DCB结构包含了诸如波特率、数据位数、奇偶校验和停止位数等信息。在查询或配置串口的属性时,都要用DCB结构来作为缓冲区一般用CreateFile打开串口后,可以调用GetCommState函数来获取串口的初始配置。要修改串口的配置,应该先修改DCB结构,然后再调用SetCommState函数设置串口:
BOOL GetCommState(
Int hFile, //标识通讯端口的句柄
RefDCB lpDCB //指向一个设备控制块(DCB结构)的指针
);
SetCommState函数设置COM口的设备控制块:
BOOL SetCommState(
int hFile,
refDCB lpDCB
);
我们用到的DCB部分结构如下:
typedef struct _DCB{
………
//波特率,指定通信设备的传输速率。这个成员可以是实际波特率值或者下面的常量值之一:
Public int BaudRate;
Public int fParity; // 指定奇偶校验使能。若此成员为1,允许奇偶校验检查
…
BYTE ByteSize; // 通信字节位数,4—8
Public byte Parity; //指定奇偶校验方法。此成员可以有下列值:
//EVENPARITY 偶校验 NOPARITY 无校验
//MARKPARITY 标记校验 ODDPARITY 奇校验
Public byte StopBits; //指定停止位的位数。此成员可以有下列值:
//ONESTOPBIT 1位停止位 TWOSTOPBITS 2位停止位
ONE5STOPBITS 1.5位停止位
………
} DCB;
4、读/写串口:
(1)读串口:
BOOL ReadFile(
Int hFile, //串口的句柄
// 读入的数据存储的地址,
// 即读入的数据将存储在以该指针的值为首地址的一片内存区
Byte[] lpBuffer,
int nNumberOfBytesToRead, // 要读入的数据的字节数
// 指向一个DWORD数值,该数值返回读操作实际读入的字节数
Ref int lpNumberOfBytesRead,
// 重叠操作时,该参数指向一个OVERLAPPED结构,同步操作时,该参数为NULL。
Ref OVERLAPPED lpOverlapped
);
OVERLAPPED的结构如下(该结构最重要的成员是hEvent。hEvent是读写事件。当串口使用异步通讯时,函数返回时操作可能还没有完成,程序可以通过检查该事件得知是否读写完毕。):
typedef struct _OVERLAPPED {
public int Internal;
public int InternalHigh;
public int Offset;
public int OffsetHigh;
public int hEvent;
} OVERLAPPED;
(2)写串口:
BOOL WriteFile(
int hFile, //串口的句柄
// 写入的数据存储的地址,
// 即以该指针的值为首地址的nNumberOfBytesToWrite
// 个字节的数据将要写入串口的发送数据缓冲区。
Byte[] lpBuffer,
int nNumberOfBytesToWrite, //要写入的数据的字节数
// 指向指向一个DWORD数值,该数值返回实际写入的字节数
Ref int lpNumberOfBytesWritten,
// 重叠操作时,该参数指向一个OVERLAPPED结构,
// 同步操作时,该参数为NULL。
Ref OVERLAPPED lpOverlapped
);
(3)清除缓存或内在空间方法:
BOOL PurgeComm(
int hFile, //串口句柄
uint dwFlags // 需要完成的操作
);
参数dwFlags指定要完成的操作,可以是下列值的组合:
PURGE_TXABORT 中断所有写操作并立即返回,即使写操作还没有完成。
PURGE_RXABORT 中断所有读操作并立即返回,即使读操作还没有完成。
PURGE_TXCLEAR 清除输出缓冲区
PURGE_RXCLEAR 清除输入缓冲
综上:在了解了以上的基本API方法的用法和作用后,现在来假设上位机发命令从下位机读取一条数据的例子。首先假设一下我们的串口和通信协议如下:
串口协议:
Portname(com3),BaudRate = 9600,ByteSize = 8,Parity = 0
通信协议: 数据头+内容+数据尾(BG+…+END)
Readtime:下位机就可以传回(BG20121020END)
这里假设下位机只有一个,但是每个下位机地址是唯一的,所以这里的大致流程可以是这样的:
(1)首先打开串口,下面程序中的open()。
(2)由上位机发送一个相应的下位机地址【下面程序的writefile(),发送到串口】,下位机可以获取串口数据,根据发来的地址匹配,如果地址相符就打开自己的串口,并发一个连接成功胡消息回来,【上位机可以用readfile()来读取显示】确定上位机和下位机已经可以通信。
(3)这时就可以从发送窗口发送命令,如readtime,当下位机接到命令后,就会根据相应的命令查找相应的数据然后发送回去。
以上的收发都是用的基本的readfile()和writefile(),包括serialport类都是由这些最基本的API封闭而成的,只是他的方式更多,使用更方便.因为这里没有联到下位机,所以就简单的进行了一下收发显示,效果如图:
程序如下:
namespace API_串口
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
}
public string Port = "COM3:";
///波特率9600
public int BaudRate = 9600;
///数据位4-8
public byte ByteSize = 8; //4-8
///奇偶校验0-4=no,odd,even,mark,space
public byte Parity = 0; //0-4=no,odd,even,mark,space
///停止位
public byte StopBits = 0; //0,1,2 = 1, 1.5, 2
///超时长
public int ReadTimeout = 200;
///串口是否已经打开
public bool Opened = false;
/// COM口句柄
private int hComm = -1;
#region "API相关定义"
private const string DLLPATH = "kernel32.dll";
/// WINAPI常量,写标志
private const uint GENERIC_READ = 0x80000000;
/// WINAPI常量,读标志
private const uint GENERIC_WRITE = 0x40000000;
/// WINAPI常量,打开已存在
private const int OPEN_EXISTING = 3;
/// WINAPI常量,无效句柄
private const int INVALID_HANDLE_VALUE = -1;
private const int PURGE_RXABORT = 0x2;// 中断所有读操作并立即返回,即使读操作还没有完成
private const int PURGE_RXCLEAR = 0x8;//清除输入缓冲区
private const int PURGE_TXABORT = 0x1;// 中断所有写操作并立即返回,即使写操作还没有完成
private const int PURGE_TXCLEAR = 0x4;//清除输出缓冲区
///设备控制块结构体类型
[StructLayout(LayoutKind.Sequential)]
public struct DCB
{
/// DCB长度
public int DCBlength;
///指定当前波特率
public int BaudRate;
///标志位
public uint flags;
///未使用,必须为0
public ushort wReserved;
///指定在XON字符发送这前接收缓冲区中可允许的最小字节数
public ushort XonLim;
///指定在XOFF字符发送这前接收缓冲区中可允许的最小字节数
public ushort XoffLim;
///指定端口当前使用的数据位
public byte ByteSize;
///指定端口当前使用的奇偶校验方法,可能为:EVENPARITY,MARKPARITY,NOPARITY,ODDPARITY 0-4=no,odd,even,mark,space
public byte Parity;
///指定端口当前使用的停止位数,可能为:ONESTOPBIT,ONE5STOPBITS,TWOSTOPBITS 0,1,2 = 1, 1.5, 2
public byte StopBits;
///指定用于发送和接收字符XON的值 Tx and Rx XON character
public byte XonChar;
///指定用于发送和接收字符XOFF值 Tx and Rx XOFF character
public byte XoffChar;
///本字符用来代替接收到的奇偶校验发生错误时的值
public byte ErrorChar;
///当没有使用二进制模式时,本字符可用来指示数据的结束
public byte EofChar;
///当接收到此字符时,会产生一个事件
public byte EvtChar;
///未使用
public ushort wReserved1;
}
///串口超时时间结构体类型
[StructLayout(LayoutKind.Sequential)]
private struct COMMTIMEOUTS
{
public int ReadIntervalTimeout;
public int ReadTotalTimeoutMultiplier;
public int ReadTotalTimeoutConstant;
public int WriteTotalTimeoutMultiplier;
public int WriteTotalTimeoutConstant;
}
///溢出缓冲区结构体类型
[StructLayout(LayoutKind.Sequential)]
private struct OVERLAPPED
{
public int Internal;
public int InternalHigh;
public int Offset;
public int OffsetHigh;
public int hEvent;
}
///打开串口
///<param name="lpFileName">要打开的串口名称</param>
///<param name="dwDesiredAccess">指定串口的访问方式,一般设置为可读可写方式</param>
///<param name="dwShareMode">指定串口的共享模式,串口不能共享,所以设置为0</param>
///<param name="lpSecurityAttributes">设置串口的安全属性,WIN9X下不支持,应设为NULL</param>
///<param name="dwCreationDisposition">对于串口通信,创建方式只能为OPEN_EXISTING</param>
///<param name="dwFlagsAndAttributes">指定串口属性与标志,设置为FILE_FLAG_OVERLAPPED(重叠I/O操作),指定串口以异步方式通信</param>
///<param name="hTemplateFile">对于串口通信必须设置为NULL</param>
[DllImport(DLLPATH)]
private static extern int CreateFile(string lpFileName, uint dwDesiredAccess, int dwShareMode,
int lpSecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, int hTemplateFile);
///得到串口状态
///<param name="hFile">通信设备句柄</param>
///<param name="lpDCB">设备控制块DCB</param>
[DllImport(DLLPATH)]
private static extern bool GetCommState(int hFile, ref DCB lpDCB);
///建立串口设备控制块(嵌入版没有)
///<param name="lpDef">设备控制字符串</param>
///<param name="lpDCB">设备控制块</param>
//[DllImport(DLLPATH)]
//private static extern bool BuildCommDCB(string lpDef, ref DCB lpDCB);
///设置串口状态
///<param name="hFile">通信设备句柄</param>
///<param name="lpDCB">设备控制块</param>
[DllImport(DLLPATH)]
private static extern bool SetCommState(int hFile, ref DCB lpDCB);
///读取串口超时时间
///<param name="hFile">通信设备句柄</param>
///<param name="lpCommTimeouts">超时时间</param>
[DllImport(DLLPATH)]
private static extern bool GetCommTimeouts(int hFile, ref COMMTIMEOUTS lpCommTimeouts);
///设置串口超时时间
///<param name="hFile">通信设备句柄</param>
///<param name="lpCommTimeouts">超时时间</param>
[DllImport(DLLPATH)]
private static extern bool SetCommTimeouts(int hFile, ref COMMTIMEOUTS lpCommTimeouts);
///读取串口数据
///<param name="hFile">通信设备句柄</param>
///<param name="lpBuffer">数据缓冲区</param>
///<param name="nNumberOfBytesToRead">多少字节等待读取</param>
///<param name="lpNumberOfBytesRead">读取多少字节</param>
///<param name="lpOverlapped">溢出缓冲区</param>
[DllImport(DLLPATH)]
private static extern bool ReadFile(int hFile, byte[] lpBuffer, int nNumberOfBytesToRead,
ref int lpNumberOfBytesRead, ref OVERLAPPED lpOverlapped);
///写串口数据
///<param name="hFile">通信设备句柄</param>
///<param name="lpBuffer">数据缓冲区</param>
///<param name="nNumberOfBytesToWrite">多少字节等待写入</param>
///<param name="lpNumberOfBytesWritten">已经写入多少字节</param>
///<param name="lpOverlapped">溢出缓冲区</param>
[DllImport(DLLPATH)]
private static extern bool WriteFile(int hFile, byte[] lpBuffer, int nNumberOfBytesToWrite,
ref int lpNumberOfBytesWritten, ref OVERLAPPED lpOverlapped);
[DllImport(DLLPATH, SetLastError = true)]
private static extern bool FlushFileBuffers(int hFile);
[DllImport(DLLPATH, SetLastError = true)]
private static extern bool PurgeComm(int hFile, uint dwFlags);
///关闭串口
///<param name="hObject">通信设备句柄</param>
[DllImport(DLLPATH)]
private static extern bool CloseHandle(int hObject);
///得到串口最后一次返回的错误
[DllImport(DLLPATH)]
private static extern uint GetLastError();
#endregion
///设置DCB标志位
如果没有用到标志位,这里的这个函数也可以不用.
internal void SetDcbFlag(int whichFlag, int setting, DCB dcb)
{
uint num;
setting = setting << whichFlag;
if ((whichFlag == 4) || (whichFlag == 12))
{
num = 3;
}
else if (whichFlag == 15)
{
num = 0x1ffff;
}
else
{
num = 1;
}
dcb.flags &= ~(num << whichFlag);
dcb.flags |= (uint)setting;
}
///建立与串口的连接
public int Open()
{
DCB dcb = new DCB();
COMMTIMEOUTS ctoCommPort = new COMMTIMEOUTS();
// 打开串口
hComm = CreateFile(Port, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);
if (hComm == INVALID_HANDLE_VALUE)
{
return -1;
}
// 设置通信超时时间
GetCommTimeouts(hComm, ref ctoCommPort);
ctoCommPort.ReadTotalTimeoutConstant = ReadTimeout;
ctoCommPort.ReadTotalTimeoutMultiplier = 0;
ctoCommPort.WriteTotalTimeoutMultiplier = 0;
ctoCommPort.WriteTotalTimeoutConstant = 0;
SetCommTimeouts(hComm, ref ctoCommPort);
//设置串口参数
GetCommState(hComm, ref dcb);
dcb.DCBlength = Marshal.SizeOf(dcb);
dcb.ByteSize = (byte)ByteSize;
dcb.StopBits = StopBits;
dcb.Parity = (byte)Parity;
dcb.BaudRate = BaudRate;
dcb.flags = 0;
/*SetDcbFlag(0, 1, dcb); //二进制方式
SetDcbFlag(1, (Parity == 0) ? 0 : 1, dcb);
SetDcbFlag(2, 0, dcb); //不用CTS检测发送流控制
SetDcbFlag(3, 0, dcb); //不用DSR检测发送流控制
SetDcbFlag(4, 0, dcb); //禁止DTR流量控制
SetDcbFlag(6, 0, dcb); //对DTR信号线不敏感
SetDcbFlag(9, 1, dcb); //检测接收缓冲区
SetDcbFlag(8, 0, dcb); //不做发送字符控制
SetDcbFlag(10, 0, dcb); //是否用指定字符替换校验错的字符
SetDcbFlag(11, 0, dcb); //保留NULL字符
SetDcbFlag(12, 0, dcb); //允许RTS流量控制
SetDcbFlag(14, 0, dcb); //发送错误后,继续进行下面的读写操作
//--------------------------------
dcb.wReserved = 0; //没有使用,必须为0
dcb.XonLim = 0; //指定在XOFF字符发送之前接收到缓冲区中可允许的最小字节数
dcb.XoffLim = 0; //指定在XOFF字符发送之前缓冲区中可允许的最小可用字节数
dcb.XonChar = 0; //发送和接收的XON字符
dcb.XoffChar = 0; //发送和接收的XOFF字符
dcb.ErrorChar = 0; //代替接收到奇偶校验错误的字符
dcb.EofChar = 0; //用来表示数据的结束
dcb.EvtChar = 0; //事件字符,接收到此字符时,会产生一个事件
dcb.wReserved1 = 0; //没有使用 */
if (!SetCommState(hComm, ref dcb))
{
return -2;
}
Opened = true;
return 0;
}
///关闭串口,结束通讯
public new void Close()
{
if (hComm != INVALID_HANDLE_VALUE)
{
CloseHandle(hComm);
}
}
///读取串口返回的数据
public byte[] Read()
{
byte[] bytData={};
if (hComm != INVALID_HANDLE_VALUE)
{
OVERLAPPED ovlCommPort = new OVERLAPPED();
int BytesRead = 0;
int NumBytes = 10;
bytData = new byte[NumBytes];
ReadFile(hComm, bytData, NumBytes, ref BytesRead, ref ovlCommPort);
byte [] outbytes = new byte[BytesRead];
Array.Copy(bytData,outbytes,BytesRead);
//在这里可以加上根据通信协议判断接收的数据是否完整,格式是否正确,并做出相应处理.
return outbytes;
}
else
{
return null;
}
}
///向串口写数据
///<param name="WriteBytes">数据数组</param>
public void Write( byte[] WriteBytes ,int intSize)
{
if (hComm != INVALID_HANDLE_VALUE)
{
OVERLAPPED ovlCommPort = new OVERLAPPED();
int BytesWritten = 0;
WriteFile(hComm, WriteBytes, intSize, ref BytesWritten, ref ovlCommPort);
}
else
{
MessageBox.Show("false");
}
}
///清除接收缓冲区
public void ClearReceiveBuf()
{
if (hComm != INVALID_HANDLE_VALUE)
{
PurgeComm(hComm, PURGE_RXABORT | PURGE_RXCLEAR);
}
}
///清除发送缓冲区
public void ClearSendBuf()
{
if (hComm != INVALID_HANDLE_VALUE)
{
PurgeComm(hComm, PURGE_TXABORT | PURGE_TXCLEAR);
}
}
private void button1_Click(object sender, EventArgs e)
{
Open();
}
private void button2_Click(object sender, EventArgs e)
{
byte[] str=System.Text.Encoding.Default.GetBytes(textBox1.Text);
int s=str.Length;
Write(str,s);
byte[] receives = Read();
string receive = System.Text.Encoding.Default.GetString(receives);
richTextBox1.AppendText( receive + "\n\t");
ClearSendBuf();
ClearReceiveBuf();
}
}
}
结束语:可以利用上面的程序封装成像serialport哪样的类,实现自己所想要实现的功能,在这里为了让串口实现,只采用了同步I/O,为了提高效率,不出现阻塞,最好采用异步I/O和多线程.