简介:这份资源是龚建伟、熊光明编著的《Visual C++/Turbo C串口通信编程实践(第2版)》配套源代码,面向具备一定C/C++基础、希望掌握串口通信开发的高校学生与嵌入式、工控方向程序员。内容围绕VC 6.0与Turbo C环境下的串口编程展开,涵盖通信控件封装、多线程收发、协议解析与调试等典型场景,可帮助读者对照书中实例动手实践,理解串口通信的底层机制与工程实现思路。压缩包共787个文件,约16.57MB,以h头文件、cpp源文件为主,辅以obj、exe、pdb等编译产物,以及ico、bmp、rc等界面资源,另有dsw、dsp、clw等工程与类向导文件,便于直接打开编译运行。目前已有362人学习下载,适合作为课程设计、毕业设计及串口工具开发的参考素材,读者可从中获取完整工程结构、可复用代码模块与调试排错经验。1. 从一份老代码说起:为什么今天还要翻出 VC 6.0 的串口工程前阵子帮一个做工业设备维护的朋友排查问题,他手里一台老工控机跑着 Windows XP,上位机软件是十几年前用 VC 6.0 写的,靠串口跟一块采集板通信。设备还在产线上转,源码却只剩一个压缩包,里面是《Visual C++/Turbo C串口通信编程实践(第2版)》配套的源代码。他问我:这东西现在还有用吗?我的回答是,如果你要维护存量设备、要理解串口通信最底层的收发逻辑,这份代码比很多新框架都直接。它不依赖任何第三方串口库,VC 6.0 部分用 Win32 API 直接操作 COM 口,Turbo C 部分则是在 DOS 环境下直接读写 UART 寄存器。适合两类人:一是需要接手老工控项目的维护开发者,二是想搞懂串口到底怎么收发的学习者。下面我按实际拆包和编译的过程,把这份资源讲清楚。2. 工程结构拆解:VC 6.0 与 Turbo C 两套代码怎么组织2.1 目录布局与文件类型这份源代码包解压后,通常按编译器分成两个大目录,一个对应 Visual C++ 6.0 的 Windows 工程,一个对应 Turbo C 2.0/3.0 的 DOS 工程。VC 目录下是典型的.dsw工作区文件加.dsp工程文件,源码以.cpp和.h为主,资源文件是.rc。Turbo C 目录下则是.prj工程文件和.c源文件,没有资源脚本,因为 DOS 下没有窗口系统。我拿到包后第一件事是看目录名和文件后缀,判断哪些能直接在 IDE 里打开。VC 6.0 的.dsw双击就能启动 IDE,Turbo C 的.prj需要在 DOSBox 里挂载后打开。两个目录的公共部分是一些串口配置的宏定义和校验算法,作者把它们抽成了独立的头文件,两边都引用。文件/目录作用对应编译器*.dsw/*.dspVC 工作区与工程文件Visual C++ 6.0*.cpp/*.hWindows 串口通信源码Visual C++ 6.0*.rc对话框与控件资源Visual C++ 6.0*.prjTurbo C 工程文件Turbo C 2.0/3.0*.cDOS 串口通信源码Turbo C 2.0/3.02.2 VC 6.0 工程的入口与消息循环VC 6.0 的 Windows 串口程序,入口是WinMain,但真正干活的是主对话框的DialogProc。串口数据到达时,系统会向窗口发送WM_COMMNOTIFY消息,代码在消息处理里读取缓冲区。下面这段是典型的串口初始化与消息注册逻辑,我按原工程的结构还原出来:// 打开串口并配置参数 HANDLE hCom = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); if (hCom == INVALID_HANDLE_VALUE) { AfxMessageBox("打开串口失败"); return FALSE; } // 配置波特率、数据位、停止位、校验位 DCB dcb; memset(&dcb, 0, sizeof(dcb)); dcb.DCBlength = sizeof(dcb); GetCommState(hCom, &dcb); dcb.BaudRate = CBR_9600; // 波特率 9600 dcb.ByteSize = 8; // 数据位 8 dcb.Parity = NOPARITY; // 无校验 dcb.StopBits = ONESTOPBIT; // 1 位停止位 SetCommState(hCom, &dcb); // 设置缓冲区大小并注册事件通知 SetupComm(hCom, 1024, 1024); SetCommMask(hCom, EV_RXCHAR); // 只关心接收字符事件这段代码的关键参数是DCB结构体,波特率、数据位、校验位、停止位必须和硬件端完全一致,否则收到的就是乱码。FILE_FLAG_OVERLAPPED表示异步方式,配合WaitCommEvent使用,不会阻塞界面线程。原工程里还封装了一个CSerialPort类,把CreateFile、SetCommState、ReadFile这些 API 包了一层,调用起来更顺手。2.3 Turbo C 工程的寄存器级操作Turbo C 那套代码更底层,直接操作 8250/16550 UART 的寄存器。DOS 下没有驱动层,串口就是几个 I/O 端口。下面这段是初始化 COM1 的典型写法:/* COM1 基地址为 0x3F8 */ #define COM1_BASE 0x3F8 void init_serial(void) { outportb(COM1_BASE + 1, 0x00); /* 关闭中断 */ outportb(COM1_BASE + 3, 0x80); /* 使能 DLAB,准备设置波特率 */ outportb(COM1_BASE + 0, 0x0C); /* 除数低字节,9600 波特率 */ outportb(COM1_BASE + 1, 0x00); /* 除数高字节 */ outportb(COM1_BASE + 3, 0x03); /* 8 位数据,无校验,1 位停止位 */ outportb(COM1_BASE + 2, 0xC7); /* 使能 FIFO,清空缓冲区 */ outportb(COM1_BASE + 4, 0x0B); /* 设置 RTS/DSR 等握手信号 */ }这里每个outportb对应一个寄存器偏移:+0是发送/接收缓冲,+1是中断使能,+2是 FIFO 控制,+3是线路控制,+4是调制解调器控制。DLAB 位(线路控制寄存器最高位)置 1 时,+0和+1才用来写波特率除数。9600 波特率对应的除数是 12,所以低字节写0x0C。这套代码在 DOSBox 里模拟串口时不一定能直接跑通,因为 DOSBox 的串口模拟只支持有限的几种模式,但作为学习寄存器操作的参考非常清晰。3. 跑起来:VC 6.0 与 Turbo C 的编译环境搭建3.1 VC 6.0 工程的编译与串口调试VC 6.0 在 Win7 及以上系统安装时会有兼容性问题,常见做法是装一个虚拟机跑 XP,或者用兼容模式加管理员权限运行。我一般会在虚拟机里装好 VC 6.0 和 SP6 补丁,然后把工程目录挂载进去。打开.dsw后,直接按 F7 编译,如果报afxwin.h找不到,说明 MFC 库没装全,需要勾选安装选项里的 MFC 组件。编译通过后,运行程序会弹出一个对话框,上面有串口选择、波特率设置、发送区和接收区。要验证收发是否正常,可以用一根 USB 转串口线把电脑的两个 COM 口短接,或者用虚拟串口软件建一对互联的端口。我通常用com0com这类工具建一对虚拟串口,一个给程序用,一个给串口助手用,发什么收什么一目了然。提示:如果打开串口时提示“拒绝访问”,先检查是不是被其他程序占用了,比如串口助手没关。另外 USB 转串口线的驱动要装好,设备管理器里能看到 COM 口号才行。3.2 Turbo C 工程的 DOSBox 运行Turbo C 的代码需要在 DOS 环境下编译运行。现在没人真去装 DOS,用 DOSBox 挂载目录就行。把 Turbo C 的安装目录和源码目录都挂载到 DOSBox 里,进入 Turbo C 的BIN目录,运行TC.EXE,打开.prj工程文件,按F9编译,Ctrl+F9运行。这里有个坑:DOSBox 默认的串口模拟只支持serial1=null或serial1=file这种模式,没法直接和物理串口通信。如果只是想看代码逻辑,编译运行看输出就够了;如果要实际收发,得用带串口直通功能的 DOSBox 分支,或者干脆在真实 DOS 机器上跑。我一般建议先在 DOSBox 里把编译跑通,确认代码没有语法错误,再考虑硬件对接。3.3 串口参数配置的对应关系两套代码虽然平台不同,但串口参数的含义是一致的。下面这张表把 VC 里的DCB字段和 Turbo C 里的寄存器值对应起来,改代码时对着看不容易错:参数VC 6.0 DCB 字段Turbo C 寄存器操作常用值波特率BaudRate+0/+1写除数9600 / 115200数据位ByteSize+3低两位8校验位Parity+3第 3 位无校验停止位StopBits+3第 2 位1 位流控fRtsControl+4握手信号无 / RTS/CTS改波特率时,VC 里直接改CBR_9600为CBR_115200就行;Turbo C 里要重新算除数,115200 对应的除数是 1,低字节写0x01,高字节写0x00。除数算错,收发的就是乱码,这是最常见的翻车点。4. 核心代码逐段拆解:收发线程与数据校验4.1 VC 6.0 的接收线程与事件驱动VC 6.0 工程里,串口接收通常放在一个独立线程里,用WaitCommEvent等待EV_RXCHAR事件,然后ReadFile把数据读出来。下面这段是线程函数的骨架:UINT CSerialPort::ReceiveThread(LPVOID pParam) { CSerialPort* pPort = (CSerialPort*)pParam; DWORD dwEventMask = 0; DWORD dwBytesRead = 0; char buffer[1024]; while (pPort->m_bThreadRunning) { // 等待接收字符事件,超时 100ms 便于检查线程退出标志 if (!WaitCommEvent(pPort->m_hCom, &dwEventMask, NULL)) { continue; } if (dwEventMask & EV_RXCHAR) { // 读取串口缓冲区中的数据 if (ReadFile(pPort->m_hCom, buffer, sizeof(buffer), &dwBytesRead, NULL)) { if (dwBytesRead > 0) { // 把数据投递到主窗口,由界面线程更新显示 pPort->PostDataToUI(buffer, dwBytesRead); } } } } return 0; }WaitCommEvent是阻塞的,所以放在线程里不会卡界面。EV_RXCHAR表示接收缓冲区里有新字符。ReadFile读到的字节数放在dwBytesRead里,可能小于缓冲区大小,这是正常的,串口数据是流式的,一次读多少取决于当前缓冲区里有多少。PostDataToUI是自定义函数,用PostMessage把数据指针传给主窗口,避免跨线程直接操作控件。4.2 Turbo C 的查询式接收DOS 下没有线程,Turbo C 的代码用查询方式接收:不断读线路状态寄存器的“数据就绪”位,有数据就取走。下面这段是典型的查询接收循环:int receive_char(void) { /* 等待数据就绪,超时返回 -1 */ unsigned long timeout = 0; while (!(inportb(COM1_BASE + 5) & 0x01)) { if (++timeout > 1000000) { return -1; /* 超时,没有收到数据 */ } } /* 读取接收缓冲寄存器 */ return inportb(COM1_BASE + 0); }inportb(COM1_BASE + 5)读的是线路状态寄存器,最低位0x01表示接收数据就绪。这个循环会一直占着 CPU,所以实际工程里通常会配合中断或者定时器来用,不会死等。原书代码里也有中断方式的版本,用setvect挂接 IRQ4 的中断服务程序,收到数据后置一个标志位,主循环检查标志位再处理。4.3 数据校验与帧解析串口通信最怕数据错位,所以两套代码里都有校验逻辑。常见的是累加和校验或者 CRC16。下面这段是累加和校验的实现,VC 和 Turbo C 里逻辑一样,只是语法略有差别:// 计算从 pData 开始的 len 个字节的累加和,取低 8 位 unsigned char CalcChecksum(const unsigned char* pData, int len) { unsigned int sum = 0; for (int i = 0; i < len; i++) { sum += pData[i]; } return (unsigned char)(sum & 0xFF); }发送时把校验和附在帧尾,接收时重新算一遍对比。如果对不上,说明传输过程中有干扰,这一帧就丢弃。原工程里还定义了帧头帧尾,比如0xAA开头、0x55结尾,中间是数据区和校验和。解析时先找帧头,再按固定长度取数据,最后验校验和。这套逻辑虽然简单,但在工业现场足够用。5. 避坑与排查:串口调试中最容易翻车的几个点5.1 打开串口失败,提示“拒绝访问”现象是CreateFile返回INVALID_HANDLE_VALUE,GetLastError得到 5。原因通常是串口已经被别的程序打开,比如串口助手、调试工具没关干净。解决方法是先在设备管理器里确认串口存在,然后用任务管理器结束可能占用串口的进程,或者换一个 COM 口测试。我习惯在代码里加一个重试机制,打开失败后延时 500ms 再试两次。5.2 收到的数据全是乱码现象是接收区显示一堆问号或方块。原因几乎都是串口参数不匹配:波特率、数据位、校验位、停止位有一项对不上。解决方法是把两端参数打印出来逐项核对,尤其是波特率,9600 和 115200 搞混是最常见的。另外晶振频率不准也会导致波特率偏差,偏差超过 2% 就可能出错,这种情况只能换晶振或者降低波特率。5.3 数据收一半就断了现象是长帧数据收到一半就没了,或者偶尔丢几个字节。原因是接收缓冲区溢出,或者读取不及时。VC 里要把SetupComm的缓冲区设大一点,比如 4096 字节,并且接收线程的优先级适当提高。Turbo C 里查询方式更容易丢数据,建议改用中断方式,或者在查询循环里加inportb读多次,把 FIFO 里的数据都取出来。5.4 编译报错“无法打开预编译头文件”VC 6.0 工程里常见这个报错,原因是stdafx.cpp没有先编译,或者stdafx.h被改动过。解决方法是先单独编译stdafx.cpp生成.pch文件,再编译整个工程。如果还不行,在工程设置里把“使用预编译头”改成“不使用”,虽然编译慢一点,但能绕过这个问题。5.5 DOSBox 里串口中断不触发Turbo C 的中断方式代码在 DOSBox 里跑,中断服务程序可能永远不被调用。原因是 DOSBox 对硬件中断的模拟不完整,IRQ4 的中断向量挂接后不一定能收到。解决方法是先用查询方式验证逻辑,确认代码没问题后再上真实硬件。如果必须在 DOSBox 里测中断,可以改用轮询定时器中断的方式模拟。6. 二次开发:把老串口代码接进现代工程6.1 从 VC 6.0 迁移到 VS 的注意事项如果要把 VC 6.0 的串口代码迁到 Visual Studio 2019/2022,最大的障碍是 MFC 版本差异和字符集。VC 6.0 默认用多字节字符集,新版本默认 Unicode。迁移时要么把工程属性改成“使用多字节字符集”,要么把char改成TCHAR,字符串常量用_T()包起来。串口 API 本身没变,CreateFile、ReadFile、WriteFile在新系统上照样能用。我一般会先把串口操作部分抽成一个独立的.cpp和.h,不依赖 MFC 的CString和CWnd,改成纯 Win32 API 加回调函数。这样这个模块可以同时用在 MFC 工程、Qt 工程甚至控制台程序里。下面是一个简化的接口定义:// 串口回调接口,收到数据时由串口线程调用 class ISerialCallback { public: virtual void OnDataReceived(const unsigned char* data, int len) = 0; }; // 串口类,封装打开、关闭、发送、接收线程 class SerialPort { public: bool Open(const char* portName, int baudRate); void Close(); bool Send(const unsigned char* data, int len); void SetCallback(ISerialCallback* cb); private: HANDLE m_hCom; ISerialCallback* m_pCallback; // ... 线程相关成员 };这样改造后,界面层只负责显示,串口层只负责收发,职责清晰,也方便单元测试。6.2 用现代工具验证串口逻辑老代码没有单元测试,改起来心里没底。我的习惯是先用虚拟串口工具建一对端口,然后写一个简单的 Python 脚本模拟设备端,按协议收发数据。Python 的pyserial库几行代码就能建一个串口服务端:import serial import time # 打开虚拟串口的另一端 ser = serial.Serial('COM2', 9600, timeout=1) while True: # 模拟设备主动上报一帧数据 frame = bytes([0xAA, 0x01, 0x02, 0x03, 0x55]) ser.write(frame) time.sleep(1)用这个脚本配合改造后的串口模块,可以反复验证帧解析、校验和计算是否正确,不用每次都接真实硬件。等逻辑跑通了,再上真实设备联调,效率高很多。6.3 一个具体的改造技巧:把阻塞读改成异步回调VC 6.0 原工程里,接收线程用WaitCommEvent阻塞等待,然后ReadFile同步读。这种模式在数据量不大时没问题,但如果串口数据很密集,线程可能来不及处理。我的做法是把ReadFile也改成异步,用OVERLAPPED结构体加GetOverlappedResult,或者用 I/O 完成端口。这样接收线程只负责投递读请求,数据到了由系统通知,吞吐量能提高不少。具体改法是:在Open时用FILE_FLAG_OVERLAPPED打开串口,然后为每次ReadFile准备一个OVERLAPPED结构体和缓冲区,调用ReadFile后立即返回,用WaitForSingleObject等待OVERLAPPED.hEvent或者用GetQueuedCompletionStatus统一处理。这个改动稍微复杂,但值得做,尤其是波特率上到 115200 以上时,同步读很容易丢数据。从那以后我每次接手老串口代码,都强制先跑一遍虚拟串口回环测试,确认收发和校验逻辑没问题,再动业务代码。这份源代码包虽然老,但把串口通信的底层逻辑讲得很透,VC 和 Turbo C 两套实现对照着看,能少走很多弯路。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份资源是龚建伟、熊光明编著的《Visual C++/Turbo C串口通信编程实践(第2版)》配套源代码,面向具备一定C/C++基础、希望掌握串口通信开发的高校学生与嵌入式、工控方向程序员。内容围绕VC 6.0与Turbo C环境下的串口编程展开,涵盖通信控件封装、多线程收发、协议解析与调试等典型场景,可帮助读者对照书中实例动手实践,理解串口通信的底层机制与工程实现思路。压缩包共787个文件,约16.57MB,以h头文件、cpp源文件为主,辅以obj、exe、pdb等编译产物,以及ico、bmp、rc等界面资源,另有dsw、dsp、clw等工程与类向导文件,便于直接打开编译运行。目前已有362人学习下载,适合作为课程设计、毕业设计及串口工具开发的参考素材,读者可从中获取完整工程结构、可复用代码模块与调试排错经验。1. 从一份老代码说起:为什么今天还要翻出 VC 6.0 的串口工程前阵子帮一个做工业设备维护的朋友排查问题,他手里一台老工控机跑着 Windows XP,上位机软件是十几年前用 VC 6.0 写的,靠串口跟一块采集板通信。设备还在产线上转,源码却只剩一个压缩包,里面是《Visual C++/Turbo C串口通信编程实践(第2版)》配套的源代码。他问我:这东西现在还有用吗?我的回答是,如果你要维护存量设备、要理解串口通信最底层的收发逻辑,这份代码比很多新框架都直接。它不依赖任何第三方串口库,VC 6.0 部分用 Win32 API 直接操作 COM 口,Turbo C 部分则是在 DOS 环境下直接读写 UART 寄存器。适合两类人:一是需要接手老工控项目的维护开发者,二是想搞懂串口到底怎么收发的学习者。下面我按实际拆包和编译的过程,把这份资源讲清楚。2. 工程结构拆解:VC 6.0 与 Turbo C 两套代码怎么组织2.1 目录布局与文件类型这份源代码包解压后,通常按编译器分成两个大目录,一个对应 Visual C++ 6.0 的 Windows 工程,一个对应 Turbo C 2.0/3.0 的 DOS 工程。VC 目录下是典型的.dsw工作区文件加.dsp工程文件,源码以.cpp和.h为主,资源文件是.rc。Turbo C 目录下则是.prj工程文件和.c源文件,没有资源脚本,因为 DOS 下没有窗口系统。我拿到包后第一件事是看目录名和文件后缀,判断哪些能直接在 IDE 里打开。VC 6.0 的.dsw双击就能启动 IDE,Turbo C 的.prj需要在 DOSBox 里挂载后打开。两个目录的公共部分是一些串口配置的宏定义和校验算法,作者把它们抽成了独立的头文件,两边都引用。文件/目录作用对应编译器*.dsw/*.dspVC 工作区与工程文件Visual C++ 6.0*.cpp/*.hWindows 串口通信源码Visual C++ 6.0*.rc对话框与控件资源Visual C++ 6.0*.prjTurbo C 工程文件Turbo C 2.0/3.0*.cDOS 串口通信源码Turbo C 2.0/3.02.2 VC 6.0 工程的入口与消息循环VC 6.0 的 Windows 串口程序,入口是WinMain,但真正干活的是主对话框的DialogProc。串口数据到达时,系统会向窗口发送WM_COMMNOTIFY消息,代码在消息处理里读取缓冲区。下面这段是典型的串口初始化与消息注册逻辑,我按原工程的结构还原出来:// 打开串口并配置参数 HANDLE hCom = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); if (hCom == INVALID_HANDLE_VALUE) { AfxMessageBox("打开串口失败"); return FALSE; } // 配置波特率、数据位、停止位、校验位 DCB dcb; memset(&dcb, 0, sizeof(dcb)); dcb.DCBlength = sizeof(dcb); GetCommState(hCom, &dcb); dcb.BaudRate = CBR_9600; // 波特率 9600 dcb.ByteSize = 8; // 数据位 8 dcb.Parity = NOPARITY; // 无校验 dcb.StopBits = ONESTOPBIT; // 1 位停止位 SetCommState(hCom, &dcb); // 设置缓冲区大小并注册事件通知 SetupComm(hCom, 1024, 1024); SetCommMask(hCom, EV_RXCHAR); // 只关心接收字符事件这段代码的关键参数是DCB结构体,波特率、数据位、校验位、停止位必须和硬件端完全一致,否则收到的就是乱码。FILE_FLAG_OVERLAPPED表示异步方式,配合WaitCommEvent使用,不会阻塞界面线程。原工程里还封装了一个CSerialPort类,把CreateFile、SetCommState、ReadFile这些 API 包了一层,调用起来更顺手。2.3 Turbo C 工程的寄存器级操作Turbo C 那套代码更底层,直接操作 8250/16550 UART 的寄存器。DOS 下没有驱动层,串口就是几个 I/O 端口。下面这段是初始化 COM1 的典型写法:/* COM1 基地址为 0x3F8 */ #define COM1_BASE 0x3F8 void init_serial(void) { outportb(COM1_BASE + 1, 0x00); /* 关闭中断 */ outportb(COM1_BASE + 3, 0x80); /* 使能 DLAB,准备设置波特率 */ outportb(COM1_BASE + 0, 0x0C); /* 除数低字节,9600 波特率 */ outportb(COM1_BASE + 1, 0x00); /* 除数高字节 */ outportb(COM1_BASE + 3, 0x03); /* 8 位数据,无校验,1 位停止位 */ outportb(COM1_BASE + 2, 0xC7); /* 使能 FIFO,清空缓冲区 */ outportb(COM1_BASE + 4, 0x0B); /* 设置 RTS/DSR 等握手信号 */ }这里每个outportb对应一个寄存器偏移:+0是发送/接收缓冲,+1是中断使能,+2是 FIFO 控制,+3是线路控制,+4是调制解调器控制。DLAB 位(线路控制寄存器最高位)置 1 时,+0和+1才用来写波特率除数。9600 波特率对应的除数是 12,所以低字节写0x0C。这套代码在 DOSBox 里模拟串口时不一定能直接跑通,因为 DOSBox 的串口模拟只支持有限的几种模式,但作为学习寄存器操作的参考非常清晰。3. 跑起来:VC 6.0 与 Turbo C 的编译环境搭建3.1 VC 6.0 工程的编译与串口调试VC 6.0 在 Win7 及以上系统安装时会有兼容性问题,常见做法是装一个虚拟机跑 XP,或者用兼容模式加管理员权限运行。我一般会在虚拟机里装好 VC 6.0 和 SP6 补丁,然后把工程目录挂载进去。打开.dsw后,直接按 F7 编译,如果报afxwin.h找不到,说明 MFC 库没装全,需要勾选安装选项里的 MFC 组件。编译通过后,运行程序会弹出一个对话框,上面有串口选择、波特率设置、发送区和接收区。要验证收发是否正常,可以用一根 USB 转串口线把电脑的两个 COM 口短接,或者用虚拟串口软件建一对互联的端口。我通常用com0com这类工具建一对虚拟串口,一个给程序用,一个给串口助手用,发什么收什么一目了然。提示:如果打开串口时提示“拒绝访问”,先检查是不是被其他程序占用了,比如串口助手没关。另外 USB 转串口线的驱动要装好,设备管理器里能看到 COM 口号才行。3.2 Turbo C 工程的 DOSBox 运行Turbo C 的代码需要在 DOS 环境下编译运行。现在没人真去装 DOS,用 DOSBox 挂载目录就行。把 Turbo C 的安装目录和源码目录都挂载到 DOSBox 里,进入 Turbo C 的BIN目录,运行TC.EXE,打开.prj工程文件,按F9编译,Ctrl+F9运行。这里有个坑:DOSBox 默认的串口模拟只支持serial1=null或serial1=file这种模式,没法直接和物理串口通信。如果只是想看代码逻辑,编译运行看输出就够了;如果要实际收发,得用带串口直通功能的 DOSBox 分支,或者干脆在真实 DOS 机器上跑。我一般建议先在 DOSBox 里把编译跑通,确认代码没有语法错误,再考虑硬件对接。3.3 串口参数配置的对应关系两套代码虽然平台不同,但串口参数的含义是一致的。下面这张表把 VC 里的DCB字段和 Turbo C 里的寄存器值对应起来,改代码时对着看不容易错:参数VC 6.0 DCB 字段Turbo C 寄存器操作常用值波特率BaudRate+0/+1写除数9600 / 115200数据位ByteSize+3低两位8校验位Parity+3第 3 位无校验停止位StopBits+3第 2 位1 位流控fRtsControl+4握手信号无 / RTS/CTS改波特率时,VC 里直接改CBR_9600为CBR_115200就行;Turbo C 里要重新算除数,115200 对应的除数是 1,低字节写0x01,高字节写0x00。除数算错,收发的就是乱码,这是最常见的翻车点。4. 核心代码逐段拆解:收发线程与数据校验4.1 VC 6.0 的接收线程与事件驱动VC 6.0 工程里,串口接收通常放在一个独立线程里,用WaitCommEvent等待EV_RXCHAR事件,然后ReadFile把数据读出来。下面这段是线程函数的骨架:UINT CSerialPort::ReceiveThread(LPVOID pParam) { CSerialPort* pPort = (CSerialPort*)pParam; DWORD dwEventMask = 0; DWORD dwBytesRead = 0; char buffer[1024]; while (pPort->m_bThreadRunning) { // 等待接收字符事件,超时 100ms 便于检查线程退出标志 if (!WaitCommEvent(pPort->m_hCom, &dwEventMask, NULL)) { continue; } if (dwEventMask & EV_RXCHAR) { // 读取串口缓冲区中的数据 if (ReadFile(pPort->m_hCom, buffer, sizeof(buffer), &dwBytesRead, NULL)) { if (dwBytesRead > 0) { // 把数据投递到主窗口,由界面线程更新显示 pPort->PostDataToUI(buffer, dwBytesRead); } } } } return 0; }WaitCommEvent是阻塞的,所以放在线程里不会卡界面。EV_RXCHAR表示接收缓冲区里有新字符。ReadFile读到的字节数放在dwBytesRead里,可能小于缓冲区大小,这是正常的,串口数据是流式的,一次读多少取决于当前缓冲区里有多少。PostDataToUI是自定义函数,用PostMessage把数据指针传给主窗口,避免跨线程直接操作控件。4.2 Turbo C 的查询式接收DOS 下没有线程,Turbo C 的代码用查询方式接收:不断读线路状态寄存器的“数据就绪”位,有数据就取走。下面这段是典型的查询接收循环:int receive_char(void) { /* 等待数据就绪,超时返回 -1 */ unsigned long timeout = 0; while (!(inportb(COM1_BASE + 5) & 0x01)) { if (++timeout > 1000000) { return -1; /* 超时,没有收到数据 */ } } /* 读取接收缓冲寄存器 */ return inportb(COM1_BASE + 0); }inportb(COM1_BASE + 5)读的是线路状态寄存器,最低位0x01表示接收数据就绪。这个循环会一直占着 CPU,所以实际工程里通常会配合中断或者定时器来用,不会死等。原书代码里也有中断方式的版本,用setvect挂接 IRQ4 的中断服务程序,收到数据后置一个标志位,主循环检查标志位再处理。4.3 数据校验与帧解析串口通信最怕数据错位,所以两套代码里都有校验逻辑。常见的是累加和校验或者 CRC16。下面这段是累加和校验的实现,VC 和 Turbo C 里逻辑一样,只是语法略有差别:// 计算从 pData 开始的 len 个字节的累加和,取低 8 位 unsigned char CalcChecksum(const unsigned char* pData, int len) { unsigned int sum = 0; for (int i = 0; i < len; i++) { sum += pData[i]; } return (unsigned char)(sum & 0xFF); }发送时把校验和附在帧尾,接收时重新算一遍对比。如果对不上,说明传输过程中有干扰,这一帧就丢弃。原工程里还定义了帧头帧尾,比如0xAA开头、0x55结尾,中间是数据区和校验和。解析时先找帧头,再按固定长度取数据,最后验校验和。这套逻辑虽然简单,但在工业现场足够用。5. 避坑与排查:串口调试中最容易翻车的几个点5.1 打开串口失败,提示“拒绝访问”现象是CreateFile返回INVALID_HANDLE_VALUE,GetLastError得到 5。原因通常是串口已经被别的程序打开,比如串口助手、调试工具没关干净。解决方法是先在设备管理器里确认串口存在,然后用任务管理器结束可能占用串口的进程,或者换一个 COM 口测试。我习惯在代码里加一个重试机制,打开失败后延时 500ms 再试两次。5.2 收到的数据全是乱码现象是接收区显示一堆问号或方块。原因几乎都是串口参数不匹配:波特率、数据位、校验位、停止位有一项对不上。解决方法是把两端参数打印出来逐项核对,尤其是波特率,9600 和 115200 搞混是最常见的。另外晶振频率不准也会导致波特率偏差,偏差超过 2% 就可能出错,这种情况只能换晶振或者降低波特率。5.3 数据收一半就断了现象是长帧数据收到一半就没了,或者偶尔丢几个字节。原因是接收缓冲区溢出,或者读取不及时。VC 里要把SetupComm的缓冲区设大一点,比如 4096 字节,并且接收线程的优先级适当提高。Turbo C 里查询方式更容易丢数据,建议改用中断方式,或者在查询循环里加inportb读多次,把 FIFO 里的数据都取出来。5.4 编译报错“无法打开预编译头文件”VC 6.0 工程里常见这个报错,原因是stdafx.cpp没有先编译,或者stdafx.h被改动过。解决方法是先单独编译stdafx.cpp生成.pch文件,再编译整个工程。如果还不行,在工程设置里把“使用预编译头”改成“不使用”,虽然编译慢一点,但能绕过这个问题。5.5 DOSBox 里串口中断不触发Turbo C 的中断方式代码在 DOSBox 里跑,中断服务程序可能永远不被调用。原因是 DOSBox 对硬件中断的模拟不完整,IRQ4 的中断向量挂接后不一定能收到。解决方法是先用查询方式验证逻辑,确认代码没问题后再上真实硬件。如果必须在 DOSBox 里测中断,可以改用轮询定时器中断的方式模拟。6. 二次开发:把老串口代码接进现代工程6.1 从 VC 6.0 迁移到 VS 的注意事项如果要把 VC 6.0 的串口代码迁到 Visual Studio 2019/2022,最大的障碍是 MFC 版本差异和字符集。VC 6.0 默认用多字节字符集,新版本默认 Unicode。迁移时要么把工程属性改成“使用多字节字符集”,要么把char改成TCHAR,字符串常量用_T()包起来。串口 API 本身没变,CreateFile、ReadFile、WriteFile在新系统上照样能用。我一般会先把串口操作部分抽成一个独立的.cpp和.h,不依赖 MFC 的CString和CWnd,改成纯 Win32 API 加回调函数。这样这个模块可以同时用在 MFC 工程、Qt 工程甚至控制台程序里。下面是一个简化的接口定义:// 串口回调接口,收到数据时由串口线程调用 class ISerialCallback { public: virtual void OnDataReceived(const unsigned char* data, int len) = 0; }; // 串口类,封装打开、关闭、发送、接收线程 class SerialPort { public: bool Open(const char* portName, int baudRate); void Close(); bool Send(const unsigned char* data, int len); void SetCallback(ISerialCallback* cb); private: HANDLE m_hCom; ISerialCallback* m_pCallback; // ... 线程相关成员 };这样改造后,界面层只负责显示,串口层只负责收发,职责清晰,也方便单元测试。6.2 用现代工具验证串口逻辑老代码没有单元测试,改起来心里没底。我的习惯是先用虚拟串口工具建一对端口,然后写一个简单的 Python 脚本模拟设备端,按协议收发数据。Python 的pyserial库几行代码就能建一个串口服务端:import serial import time # 打开虚拟串口的另一端 ser = serial.Serial('COM2', 9600, timeout=1) while True: # 模拟设备主动上报一帧数据 frame = bytes([0xAA, 0x01, 0x02, 0x03, 0x55]) ser.write(frame) time.sleep(1)用这个脚本配合改造后的串口模块,可以反复验证帧解析、校验和计算是否正确,不用每次都接真实硬件。等逻辑跑通了,再上真实设备联调,效率高很多。6.3 一个具体的改造技巧:把阻塞读改成异步回调VC 6.0 原工程里,接收线程用WaitCommEvent阻塞等待,然后ReadFile同步读。这种模式在数据量不大时没问题,但如果串口数据很密集,线程可能来不及处理。我的做法是把ReadFile也改成异步,用OVERLAPPED结构体加GetOverlappedResult,或者用 I/O 完成端口。这样接收线程只负责投递读请求,数据到了由系统通知,吞吐量能提高不少。具体改法是:在Open时用FILE_FLAG_OVERLAPPED打开串口,然后为每次ReadFile准备一个OVERLAPPED结构体和缓冲区,调用ReadFile后立即返回,用WaitForSingleObject等待OVERLAPPED.hEvent或者用GetQueuedCompletionStatus统一处理。这个改动稍微复杂,但值得做,尤其是波特率上到 115200 以上时,同步读很容易丢数据。从那以后我每次接手老串口代码,都强制先跑一遍虚拟串口回环测试,确认收发和校验逻辑没问题,再动业务代码。这份源代码包虽然老,但把串口通信的底层逻辑讲得很透,VC 和 Turbo C 两套实现对照着看,能少走很多弯路。希望帮到你。本文还有配套的精品资源,点击获取