一、前言
串行通信接口,通常簡(jiǎn)稱為“串口”,是一種數(shù)據(jù)傳輸方式,其中信息以連續(xù)的比特流形式發(fā)送,每個(gè)比特在不同的時(shí)間點(diǎn)被傳輸。這與并行通信形成對(duì)比,在并行通信中,多個(gè)比特同時(shí)通過多個(gè)線路傳輸。串口通信因其簡(jiǎn)單的硬件需求和廣泛的應(yīng)用場(chǎng)景而受到青睞,尤其是在遠(yuǎn)程通信、設(shè)備控制、數(shù)據(jù)采集等領(lǐng)域。
串口通信在現(xiàn)代技術(shù)中的應(yīng)用場(chǎng)景極為廣泛,從個(gè)人電腦連接外設(shè)(如鼠標(biāo)、鍵盤)到工業(yè)自動(dòng)化系統(tǒng)中的傳感器網(wǎng)絡(luò),從移動(dòng)設(shè)備的數(shù)據(jù)同步到實(shí)驗(yàn)室設(shè)備的控制,都能見到其身影。在嵌入式系統(tǒng)開發(fā)中,單片機(jī)與PC機(jī)或其他設(shè)備之間的通信經(jīng)常采用串口,因?yàn)槠湟子趯?shí)現(xiàn)且成本低廉。
在Windows環(huán)境下使用C語(yǔ)言進(jìn)行串口編程,主要涉及到對(duì)Windows API函數(shù)的調(diào)用。Windows提供了豐富的API用于串口通信,包括CreateFile
、SetupComm
、PurgeComm
、SetCommState
、SetCommTimeouts
、ReadFile
、WriteFile
等,這些函數(shù)分別用于打開串口、設(shè)置串口參數(shù)、讀寫串口數(shù)據(jù)以及控制串口的輸入輸出緩沖區(qū)等。
下面示例,展示如何使用C語(yǔ)言和Windows API打開指定的串口并進(jìn)行通信:
#include <windows.h>
#include <stdio.h>
int main() {
HANDLE hComm;
DCB dcbSerialParams = {0};
COMMTIMEOUTS timeouts;
// 打開串口
hComm = CreateFile("COM3", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hComm == INVALID_HANDLE_VALUE) {
printf("無法打開串口。n");
return -1;
}
// 設(shè)置串口參數(shù)
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
GetCommState(hComm, &dcbSerialParams);
dcbSerialParams.BaudRate = CBR_9600; // 設(shè)置波特率
dcbSerialParams.ByteSize = 8; // 設(shè)置字節(jié)大小
dcbSerialParams.StopBits = ONESTOPBIT; // 設(shè)置停止位
dcbSerialParams.Parity = NOPARITY; // 設(shè)置校驗(yàn)位
SetCommState(hComm, &dcbSerialParams);
// 設(shè)置超時(shí)時(shí)間
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 500;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 500;
SetCommTimeouts(hComm, &timeouts);
// 發(fā)送數(shù)據(jù)
char data[] = "Hello from PC!";
DWORD dwWritten;
WriteFile(hComm, data, strlen(data), &dwWritten, NULL);
// 接收數(shù)據(jù)
char buffer[256];
DWORD dwRead;
ReadFile(hComm, buffer, sizeof(buffer), &dwRead, NULL);
buffer[dwRead] = '?'; // 確保字符串以空字符結(jié)尾
printf("Received: %sn", buffer);
// 關(guān)閉串口
CloseHandle(hComm);
return 0;
}
這段代碼展示了如何打開一個(gè)串口(例如COM3),設(shè)置其通信參數(shù),然后向串口發(fā)送數(shù)據(jù),并從串口接收數(shù)據(jù)。通過這樣的程序設(shè)計(jì),可以實(shí)現(xiàn)PC機(jī)與單片機(jī)或其他串口設(shè)備之間的雙向通信,為數(shù)據(jù)交換、設(shè)備控制等應(yīng)用提供基礎(chǔ)。
串口通信是連接不同設(shè)備之間的一種基本而強(qiáng)大的手段,尤其在嵌入式系統(tǒng)領(lǐng)域。掌握Windows環(huán)境下的串口編程,對(duì)于從事相關(guān)領(lǐng)域的開發(fā)者來說至關(guān)重要。
二、實(shí)操代碼
2.1 串口編程的函數(shù)詳解
在Windows環(huán)境下進(jìn)行串口編程時(shí),主要依賴于Windows API中的一系列函數(shù)。這些函數(shù)允許你控制串口的打開、配置、讀寫操作以及錯(cuò)誤處理。下面是幾個(gè)關(guān)鍵函數(shù)的詳細(xì)說明,包括它們的功能、參數(shù)含義和用法:
1. CreateFile
功能:打開或創(chuàng)建一個(gè)指定的設(shè)備或文件。
語(yǔ)法:
HANDLE CreateFile(
LPCWSTR lpFileName, // 指定文件名或設(shè)備名
DWORD dwDesiredAccess, // 請(qǐng)求的訪問類型
DWORD dwShareMode, // 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全屬性
DWORD dwCreationDisposition, // 創(chuàng)建或打開的處置
DWORD dwFlagsAndAttributes, // 文件屬性
HANDLE hTemplateFile // 模板文件句柄
);
用法:
- 通常用于打開串口設(shè)備,如
CreateFile(TEXT("COM1"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
2. CloseHandle
功能:關(guān)閉一個(gè)已打開的設(shè)備或文件句柄。
語(yǔ)法:
BOOL CloseHandle(
HANDLE hObject // 要關(guān)閉的句柄
);
用法:
- 在完成串口操作后調(diào)用以釋放資源,如
CloseHandle(hComm);
3. GetCommState
功能:獲取串口當(dāng)前的通信狀態(tài)。
語(yǔ)法:
BOOL GetCommState(
HANDLE hFile, // 串口句柄
LPDCB lpDCB // 指向DCB結(jié)構(gòu)體的指針
);
用法:
- 用于獲取串口的當(dāng)前配置,如波特率、數(shù)據(jù)位數(shù)等。
4. SetCommState
功能:設(shè)置串口的通信狀態(tài)。
語(yǔ)法:
BOOL SetCommState(
HANDLE hFile, // 串口句柄
LPDCB lpDCB // 指向DCB結(jié)構(gòu)體的指針
);
用法:
- 用于設(shè)置串口的配置參數(shù),如波特率、數(shù)據(jù)位、停止位和奇偶校驗(yàn)。
5. PurgeComm
功能:清除串口的輸入輸出緩沖區(qū)。
語(yǔ)法:
BOOL PurgeComm(
HANDLE hFile, // 串口句柄
DWORD dwMask // 指定要清除的緩沖區(qū)
);
用法:
- 用于清除串口的輸入或輸出緩沖區(qū),避免數(shù)據(jù)殘留。
6. ReadFile
功能:從串口讀取數(shù)據(jù)。
語(yǔ)法:
BOOL ReadFile(
HANDLE hFile, // 串口句柄
LPVOID lpBuffer, // 數(shù)據(jù)緩沖區(qū)
DWORD nNumberOfBytesToRead, // 要讀取的字節(jié)數(shù)
LPDWORD lpNumberOfBytesRead, // 實(shí)際讀取的字節(jié)數(shù)
LPOVERLAPPED lpOverlapped // 異步讀取時(shí)的重疊結(jié)構(gòu)
);
用法:
- 用于從串口讀取數(shù)據(jù)到緩沖區(qū)中。
7. WriteFile
功能:向串口寫入數(shù)據(jù)。
語(yǔ)法:
BOOL WriteFile(
HANDLE hFile, // 串口句柄
LPCVOID lpBuffer, // 數(shù)據(jù)緩沖區(qū)
DWORD nNumberOfBytesToWrite, // 要寫入的字節(jié)數(shù)
LPDWORD lpNumberOfBytesWritten, // 實(shí)際寫入的字節(jié)數(shù)
LPOVERLAPPED lpOverlapped // 異步寫入時(shí)的重疊結(jié)構(gòu)
);
用法:
- 用于向串口發(fā)送數(shù)據(jù)。
8. SetCommTimeouts
功能:設(shè)置串口的超時(shí)值。
語(yǔ)法:
BOOL SetCommTimeouts(
HANDLE hFile, // 串口句柄
LPCOMMTIMEOUTS lpCommTimeouts // 指向COMMTIMEOUTS結(jié)構(gòu)體的指針
);
用法:
- 用于設(shè)置讀寫操作的超時(shí)時(shí)間,防止無限期等待。
9. GetLastError
功能:獲取上一次調(diào)用失敗的錯(cuò)誤代碼。
語(yǔ)法:
DWORD GetLastError(void);
用法:
- 當(dāng)API函數(shù)調(diào)用失敗時(shí),可以調(diào)用此函數(shù)獲取具體的錯(cuò)誤代碼,幫助診斷問題。
以上函數(shù)是進(jìn)行串口編程時(shí)最常用的,它們共同提供了串口設(shè)備的完整控制能力。在實(shí)際編程中,你需要根據(jù)具體的應(yīng)用需求選擇合適的函數(shù)組合,以實(shí)現(xiàn)串口的高效穩(wěn)定通信。
2.2 掃描當(dāng)前系統(tǒng)可用串口端口
在Windows環(huán)境下,使用C語(yǔ)言來枚舉所有可用的串口,可以通過調(diào)用Windows API函數(shù)來實(shí)現(xiàn)。
以下代碼,會(huì)打印出系統(tǒng)上所有可用的串口名稱:
#include <windows.h>
#include <stdio.h>
#include <string.h>
// 定義一個(gè)結(jié)構(gòu)體存儲(chǔ)串口信息
typedef struct _SERIAL_INFO {
DWORD dwSize;
HANDLE hFile;
DWORD dwDeviceType;
DWORD dwReserved;
DWORD dwProviderSubType;
DWORD dwServiceCharacteristics;
DWORD dwVendorGuidData;
DWORD dwDriverVersion;
DWORD dwDriverDate;
DWORD dwHardwareIndex;
DWORD dwConfigFlags;
DWORD dwNumParameters;
DWORD dwNumProperties;
} SERIAL_INFO;
// 定義一個(gè)結(jié)構(gòu)體存儲(chǔ)串口屬性
typedef struct _SERIAL_PROPERTY_KEY {
DWORD dwPropertyKey;
DWORD dwPropertyType;
DWORD dwReserved;
} SERIAL_PROPERTY_KEY;
int main() {
DWORD dwSize = 0;
DWORD dwRetVal = 0;
HANDLE hComm = NULL;
SERIAL_INFO SerialInfo;
SERIAL_PROPERTY_KEY SerialPropKey;
TCHAR szPortName[MAX_PATH];
DWORD dwBufferSize = 0;
DWORD dwBytesReturned = 0;
DWORD dwError = 0;
// 獲取所需的SERIAL_INFO結(jié)構(gòu)體大小
dwRetVal = QueryDosDevice(NULL, NULL, 0);
if (dwRetVal == 0) {
dwSize = GetLastError();
SerialInfo.dwSize = dwSize;
} else {
printf("QueryDosDevice failed with error: %ldn", GetLastError());
return -1;
}
// 枚舉所有的串口
for (int i = 1; i <= 256; i++) {
wsprintf(szPortName, TEXT("COM%d"), i);
dwRetVal = QueryDosDevice(szPortName, NULL, 0);
if (dwRetVal != 0) {
continue; // 如果返回非零,則跳過,表示端口不存在或不可用
}
dwError = GetLastError();
if (dwError != ERROR_INSUFFICIENT_BUFFER) {
continue; // 如果錯(cuò)誤不是緩沖區(qū)不足,則跳過
}
// 如果是緩沖區(qū)不足,則獲取正確的緩沖區(qū)大小
dwBufferSize = dwError;
if (dwBufferSize > 0) {
SerialInfo.dwSize = dwBufferSize;
dwRetVal = QueryDosDevice(szPortName, (LPTSTR)&SerialInfo, dwBufferSize);
if (dwRetVal != 0) {
// 成功獲取串口信息,嘗試打開串口
hComm = CreateFile(szPortName,
GENERIC_READ | GENERIC_WRITE,
0, NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hComm != INVALID_HANDLE_VALUE) {
// 打印可用的串口號(hào)
wprintf(L"Found COM port: %sn", szPortName);
// 清理資源
CloseHandle(hComm);
}
}
}
}
return 0;
}
這個(gè)代碼片段會(huì)遍歷從COM1到COM256的所有可能的串口號(hào),嘗試打開每一個(gè)串口,如果成功打開,則表明該串口是可用的,并將串口號(hào)打印出來。
2.3 創(chuàng)建串口程序與單片機(jī)進(jìn)行數(shù)據(jù)互發(fā)通信
下面是一個(gè)使用C語(yǔ)言在Windows環(huán)境下進(jìn)行串口編程的例子,演示了如何與單片機(jī)進(jìn)行數(shù)據(jù)互發(fā)通信。
創(chuàng)建一個(gè)程序,打開串口,設(shè)置波特率為115200,然后接收從單片機(jī)發(fā)送來的數(shù)據(jù),將其打印出來,并將同樣的數(shù)據(jù)返回給單片機(jī)。
#include <windows.h>
#include <stdio.h>
#include <string.h>
int main() {
HANDLE hComm;
DCB dcbSerialParams = {0};
COMMTIMEOUTS timeouts;
// 打開串口
hComm = CreateFile("COM3", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hComm == INVALID_HANDLE_VALUE) {
printf("無法打開串口。n");
return -1;
}
// 設(shè)置串口參數(shù)
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
if (!GetCommState(hComm, &dcbSerialParams)) {
printf("無法獲取串口狀態(tài)。n");
CloseHandle(hComm);
return -1;
}
dcbSerialParams.BaudRate = CBR_115200; // 設(shè)置波特率為115200
dcbSerialParams.ByteSize = 8; // 設(shè)置數(shù)據(jù)位為8位
dcbSerialParams.StopBits = ONESTOPBIT; // 設(shè)置停止位為1位
dcbSerialParams.Parity = NOPARITY; // 設(shè)置無校驗(yàn)位
if (!SetCommState(hComm, &dcbSerialParams)) {
printf("無法設(shè)置串口參數(shù)。n");
CloseHandle(hComm);
return -1;
}
// 設(shè)置超時(shí)時(shí)間
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 500;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 500;
if (!SetCommTimeouts(hComm, &timeouts)) {
printf("無法設(shè)置串口超時(shí)時(shí)間。n");
CloseHandle(hComm);
return -1;
}
// 循環(huán)讀取和回顯數(shù)據(jù)
char buffer[256];
DWORD dwRead, dwWritten;
while (1) {
memset(buffer, 0, sizeof(buffer));
if (!ReadFile(hComm, buffer, sizeof(buffer)-1, &dwRead, NULL)) {
printf("讀取數(shù)據(jù)失敗。n");
break;
}
if (dwRead > 0) {
printf("接收到: %sn", buffer);
if (!WriteFile(hComm, buffer, dwRead, &dwWritten, NULL)) {
printf("寫入數(shù)據(jù)失敗。n");
break;
}
}
}
// 清理資源
CloseHandle(hComm);
return 0;
}
在這個(gè)例子中,使用CreateFile
函數(shù)打開串口,然后通過GetCommState
和SetCommState
函數(shù)設(shè)置串口的波特率、數(shù)據(jù)位、停止位和校驗(yàn)位。接著,使用SetCommTimeouts
函數(shù)設(shè)置讀寫操作的超時(shí)時(shí)間,以防在沒有數(shù)據(jù)的情況下無限等待。
接下來,進(jìn)入一個(gè)無限循環(huán),使用ReadFile
函數(shù)從串口讀取數(shù)據(jù)。如果讀取成功,將接收到的數(shù)據(jù)打印出來,并使用WriteFile
函數(shù)將同樣的數(shù)據(jù)返回到串口,實(shí)現(xiàn)回顯功能。