最近同事要再版多年前出版的《单片机教程》,当年帮助编写了与PC机通信一节。
顺手封装了一个类,整理如下:
本例题要求在PC机与单片机间通过串行通信实现从单片机向PC机的数据发送,具体要求是:首先由PC机向单片机发送一个“S”的ASCII码作为联络信号,单片机收到“S”后,发一个“A”的ASCII码作为应答信号。PC机收到“A”后,向单片机发送一个“F”的ASCII码命令信号,单片机收到“F”后,开始从内存50H取数据,并连续发送10个字节的数据,最后发送这10个数据的累加和校验。PC机在收到发送过来的数据和累加和后,与自己的累加和相比较,相同则发一个“J”的ASCII码作为应答信号,表示本次通信结束;不同则发一个“C”的ASCII码作为应答信号,表示本次通信失败,要求重新发送。单片机发送三次之后如果仍然不对,则作错误处理
在软件设计时一定要注意单片机与PC机之间应该遵守相同的通信协议,其主要包括波特率、传输帧格式、校验位等。除此之外,如果要实现PC机与多个单片机的通信,PC机还应该向单片机发送欲寻单片机的地址编码,而单片机中要编写地址识别程序段。
本例题的通信协议约定如下:
波特率:2400b/s;
帧格式:1位起始位,8位数据位,1位停止位,无奇偶校验;
传送方式:PC机采用中断方式接收,单片机也采用中断方式接收;
数据长度:一个字节
校验方式:累加和校验;
握手方式:软件握手
1.PC机的通信软件设计
实现PC机串口通信的软件在此采用VC++6.0语言编程,VC提供了一组系统函数用于支持Window平台下的串口通信,在msdn帮助文档中提供了TTY方式通信的例子,我们以这组通信函数为基础,实现了一个用于串口通信的类CCom,此通信类能够与指定串口关联,向串口发送数据,并且可以检测串口,一旦有数据到来,就会以中断方式将数据读入用户缓冲并向主窗口发送消息,利用它可以方便地编制串口应用程序。
源程序清单如下:
通信类头文件Com.h
#define BUFLEN 1024 //缓冲长度
#define WM_COMM_READ WM_USER+1000 //用户自定义的数据读入消息
class CCom : public CObject
{
public:
BOOL VerifyRbuf(); //校验和检查
void dowithRbuf(); //通信协议应答
void Write(char cmd); //发送命令字
void WriteFormat(char * sbuf,BYTE length); //打包发送数据
void Close(); //关闭串口
void Read(); //从串口读数据
void Open(UINT com); //打开串口
HANDLE m_hCom; //串口句柄
CWinThread * m_hThread; //线程句柄
BOOL m_bRun; //是否运行标志
UINT m_com; //串口编号
char m_rbuf[BUFLEN]; //输入缓冲
int m_rbuflen; //输入数据长度
char m_sbuf[BUFLEN]; //输出缓冲
DWORD dwLength; //实际读入(发送)的数据长度
OVERLAPPED osWrite, osRead; //读写操作结果
CCom(); //构造函数
virtual ~CCom(); //析构函数
};
通信类实现Com.cpp
UINT CommWatchProc(VOID* pcom);
CCom::CCom()
{
m_bRun = FALSE;
m_hThread = NULL;
m_rbuflen = 0;
}
CCom::~CCom()
{
}
void CCom::Open(UINT com) //初始化串口,参数com为打开的串口编号
{
memset(&osRead,0,sizeof(OVERLAPPED));
memset(&osWrite,0,sizeof(OVERLAPPED));
//创建读操作系统事件
osRead.hEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
//创建写操作系统事件
osWrite.hEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
if(osRead.hEvent == NULL || osWrite.hEvent == NULL)
return; //创建事件失败返回
m_com = com; //记住串口编号
CString str;
str.Format("COM%d",m_com);
m_hCom =CreateFile((LPCTSTR)str, //打开指定的串口
GENERIC_READ | GENERIC_WRITE, // 允许读和写
0, // 此项必须为0,即独占方式
NULL, // 默认安全属性
OPEN_EXISTING, //仅当串口设备存在,打开该串口
FILE_ATTRIBUTE_NORMAL |FILE_FLAG_OVERLAPPED,
// 使用异步方式读写,
NULL ); //模板文件句柄,用于串口读写时必须设置为NULL
ASSERT(m_hCom!=INVALID_HANDLE_VALUE); //检测打开串口操作是否成功
SetCommMask(m_hCom, EV_RXCHAR ); //设置事件驱动的类型
SetupComm( m_hCom, 1024,512); //设置输入、输出缓冲区的大小
PurgeComm( m_hCom, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR
| PURGE_RXCLEAR ); //清输入、输出缓冲区
DCB dcb; // 定义数据控制块结构
GetCommState(m_hCom, &dcb ); //读串口原来的参数设置
dcb.BaudRate =2400; //设置波特率为2400
dcb.ByteSize =8; //数据位8位
dcb.Parity = NOPARITY; //无奇偶校验位
dcb.StopBits = ONESTOPBIT; //1位停止位
dcb.fParity = FALSE; //不进行奇偶校验
SetCommState(m_hCom, &dcb ); //串口参数设置
m_bRun = TRUE; //启动串口读检测
m_hThread = AfxBeginThread(CommWatchProc,this, THREAD_PRIORITY_NORMAL, 0,
CREATE_SUSPENDED,NULL);
m_hThread->m_bAutoDelete=FALSE;
m_hThread->ResumeThread();
}
void CCom::Read() //从串口读入数据,存放到用户自定义缓冲rbuf,并向主窗口发送消息
{
DWORD nBytesRead,dwEvent,dwError;
COMSTAT cs;
while(m_bRun) //是否停止检测
{
if(WaitCommEvent(m_hCom,&dwEvent,NULL)) //等待数据到来
{
if((dwEvent & EV_RXCHAR) == EV_RXCHAR) //是否为数据到来事件
{
ClearCommError(m_hCom,&dwError,&cs); //获得数据长度
if(cs.cbInQue != 0)
{ //读数据
ReadFile(m_hCom,m_rbuf + m_rbuflen,cs.cbInQue,&nBytesRead,&osRead);
m_rbuflen += cs.cbInQue; //设置读入数据长度
//向主窗口发送接收到数据的消息 ::SendMessage(AfxGetMainWnd()->m_hWnd,WM_COMM_READ,(UINT)this,NULL);
}
}
}
}
PurgeComm(m_hCom,PURGE_RXCLEAR); //清输入、输出缓冲区
}
void CCom::Close() //关闭串口
{
m_bRun = FALSE; //置停止检测标志
if(m_hThread)
{
WaitForSingleObject(m_hThread->m_hThread, 1000);//INFINITE); // 等待子线程停止
m_hThread = NULL;
}
CloseHandle(m_hCom); //释放串口句柄
CloseHandle(osRead.hEvent); //释放读事件句柄
CloseHandle(osWrite.hEvent); //释放写事件句柄
}
void CCom::Write(char cmd) //发送命令字
{
WriteFile(m_hCom,&cmd,1,&dwLength,&osWrite);
}
//向串口发送数据,sbuf为数据缓冲,length为数据长度
void CCom::WriteFormat(char * sbuf,BYTE length)
{
m_sbuf[0] = length; //设置数据长度字
char sum = 0;
for(int i=1; iRead();
return TRUE;
}
在windows下将上述通信类加入到应用程序中的过程如下:
定义通信类对象,调用open方法初始化串口。
在你的主窗口类中定义通信类对象: CCom m_com;
在主窗口类的初始化函数中打开串口:m_com.Open(1);
向串口发送数据: m_com.Write('S');
在主窗口消息处理函数中处理接收到的数据;
为主窗口类增加消息响应函数:
afx_msg void OnCommRead(WPARAM wParam,LPARAM lParam);
在主窗口类的实现中增加消息映射(在END_MESSAGE_MAP()宏之前)
ON_MESSAGE(WM_COMM_READ,OnCommRead)
该消息响应函数实现如下:(其中CMainWnd为你的主窗口类)
void CMainWnd::OnCommRead(WPARAM wParam, LPARAM lParam)
{
m_com.dowithRbuf();
}
2.单片机的通信软件设计
单片机的通信软件适用于51系列的任何一种型号。单片机的发送和接收采用中断程序。准备发送的数据存放在以内存50H为首地址的连续10个单元中。
汇编语言程序清单如下:
ORG 0000H
LJMP MAIN
ORG 0023H ;串行中断入口
LJMP RECEIVE ;转中断程序
ORG 0030H
MAIN: MOV SP,#70H ;设置堆栈
MOV IE,#10010000B;CPU开串行中断
MOV SCON,#0C0H ;设置串行口方式3
MOV TMOD,#21H ;设置定时器1为方式2
MOV TH1,#0F4H ;设置波特率2400HZ
MOV TL1,#0F4H
SETB TR1 ;启动定时器1
SETB REN ;允许串行接收
SJMP $ ;等待PC机发送联络信号
;串行中断程序
RECEIVE:CLR EA ;关中断
PUSH ACC
PUSH PSW
MOV PSW,#08H
MOV A,SBUF ;接收一个数据
CLR RI
CJNE A,#53H,OUT2;是否收到询问信号“S”
MOV A,#41H ;发送回答信号“A”
LCALL SIOO
LJMP OUT1
OUT2:CJNE A,#46H,OUT1;是否收到联络信号“F”
SEND:LCALL SENDT ;发送数据
L1:JBC RI,L2 ;等待接收PC机信号
SJMP L1
L2:MOV A,SBUF
CJNE A,#04AH,OUT3 ;是否收到联络信号“J”,收到则跳出中断
SJMP OUT1
OUT3:CJNE A,#43H,OUT1;是否收到联络信号“C”,收到则重新发送数据
SJMP SEND
OUT1:POP PSW
POP ACC
SETB EA ;开中断
RETI
;发送50H单元起始10个字节数据子程序
SENDT: MOV R2,#00H
MOV R4,#10 ;发送数据长度
MOV A,R4
LCALL SIOO
MOV R1,#50H ;数据首址
ST: MOV A,@R1 ;取数据
LCALL SIOO ;调发送一个字节数据的子程序
ADD A,R2
MOV R2,A
INC R1
DJNZ R4,ST
MOV A,R2 ;发送校验和
LCALL SIOO
RET
;发送一个字节数据的子程序
SIOO:CLR ES
MOV SBUF,A
JNB TI,$ ;判一帧是否发送完
CLR TI
SETB ES
RET
C语言程序清单如下:
#include //库文件定义
unsigned char SendBUF[10] _at_ 0x50; //要发送的数据
unsigned char send_temp; //发送数据状态
void Send_data(unsigned char send_byte); //发送数据子函数
void main (void)
{
SP = 0x70; //设置堆栈
IE = 0x80; // CPU开串行中断
SCON = 0xc0; //设置串行口方式3
TMOD = 0x21; //设置定时器1为方式2
TH1 = 0xF4; //设置波特率2400HZ
TL1 = 0xF4;
TR1 = 1; //启动定时器1
REN = 1; //启动定时器1
ES = 1; //串行口开中断
send_temp = 0;
while (1);
}
uart_pro(void) interrupt 5 //串行中断程序
{
unsigned char get_data,i,sum;
EA = 0;
get_data = SBUF; //接收一个数据
RI = 0;
switch (send_temp) //判断状态
{
case 0:
{
if(get_data =='S') //是否收到询问信号“S”
{
Send_data('A'); //发送回答信号“A”
send_temp ++;
}else send_temp = 0;
break;}
case 1:
{
if(get_data =='F') //是否收到联络信号“F”
{
sum = 0;
for (i=0;i<10;i++)
{
Send_data(SendBUF[i]); //发送数据
sum += SendBUF[i]; //数据求和
}
Send_data(sum); //发送和
send_temp ++;
}else send_temp = 0;
break;}
case 2:
{
if(get_data =='J') //是否收到联络信号“J”
{ send_temp =0; }
if(get_data =='C') //收到联络信号“C , 则重新发送数据
{
for (i=0;i<10;i++)
{
Send_data(SendBUF[i]); //发送数据
sum += SendBUF[i]; //数据求和
}
Send_data(sum); //发送和
}
break;}
default :send_temp = 0;break;
}
}
void Send_data(unsigned char send_byte) //发送一个字节数据的子程序
{
ES = 0;
SBUF = send_byte;
while(TI ==0); //判一帧是否发送完
TI = 0;
ES = 1;
}