VC++ 串口开、关、读、写操作及注意事项

最近帮朋友做一款工具,设计到对操作系统串口的操作,虽然这个东西已经是历史产物了,但是还有很多设备再用,索性从网络上找了一些代码最终完成这个小功能。下面资料将介绍串口在打开、关闭、读和写的时候一些注意事项以及参数的配置(代码中有详细注释。)

串口的开关

在串口打开的时候,我们要对串口做一些基础的初始化,比如波特率、数据位、校验位、停止位几个参数,他们分别被声明在 WinBase.h 头文件中。

打开串口的代码如下:

bool SerialPortManager::Open(ReceiveDataCallback cb/* = nullptr*/)
{
    if (serial_handle_ != NULL)
    {
        return false;
    }

    if (cb != nullptr)
    {
        cb_ = cb;
    }

    serial_handle_ = CreateFile(com_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
    if (serial_handle_ == INVALID_HANDLE_VALUE)
    {
        return false;
    }

    // 获取旧的 dcb 数据
    DCB dcb;
    dcb.DCBlength = sizeof(DCB);
    if (!GetCommState(serial_handle_, &dcb))
    {
        Close();
        return false;
    }

    // 修改 dcb 数据然后设置端口属性
    // CBR_115200;
    dcb.ByteSize = byte_size_;
    dcb.BaudRate = baud_rate_;
    dcb.StopBits = stop_bits_;
    dcb.Parity = parity_;
    dcb.fBinary = TRUE;
    dcb.fParity = TRUE;
    if (!SetCommState(serial_handle_, &dcb))
    {
        Close();
        return false;
    }

    // 设置读写缓冲区大小
    SetupComm(serial_handle_, 1024, 1024);

    // 清空数据
    PurgeComm(serial_handle_, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);

    // 设置超时 10 秒
    COMMTIMEOUTS to;
    memset(&to, 0, sizeof(to));
    to.ReadIntervalTimeout = 1000;
    to.ReadTotalTimeoutMultiplier = 500;
    to.ReadTotalTimeoutConstant = 5000; //设定写超时
    to.WriteTotalTimeoutMultiplier = 500;
    to.WriteTotalTimeoutConstant = 2000;
    SetCommTimeouts(serial_handle_, &to);

    PostReadThread();

    QLOG_APP(L"Serial port device is ready, serial: {0}, baud rate: {1}, byte size: {2}, parity: {3}, stop bits: {4}.")
        << com_ << baud_rate_ << byte_size_ << parity_ << stop_bits_;

    return true;
}

其中除了打开串口时传递的参数外,还包含了一些串口处理数据超时、读写缓冲区大小等属性,需要用到的根据自己的环境来配置。 串口的关闭很简单,只需要关闭掉 CreateFile 返回的句柄就可以了,这里不多介绍。

串口读写

串口的读写可以同步也可以异步,但是同步方式会造成一个问题就是当你调用了 ReadFile 在等待串口数据时,再去调用 WriteFile 就会被阻塞,因为 ReadFile 一直没有返回。所以我还是推荐大家用异步方式来读写串口,代码如下:

void SerialPortManager::ReadSerialPortThread()
{
    QLOG_APP(L"PostReadThread is running....");
    while (TRUE)
    {
        if (!serial_handle_)
        {
            QLOG_ERR(L"Failed to read data from serial port, serial port handle is null.");
            break;
        }

        // 计算最小需要读取的数据量
        DWORD read_size = 1024;
        // read_size = min(read_size, (DWORD)com_stat.cbInQue);

        // 开始异步读取
        OVERLAPPED over_lapped;
        memset(&over_lapped, 0, sizeof(OVERLAPPED));
        over_lapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

        std::shared_ptr<BYTE> buffer;
        buffer.reset(new BYTE[read_size]);
        BOOL bReadStatus = ReadFile(serial_handle_, buffer.get(), read_size, &read_size, &over_lapped);
        if (!bReadStatus) // 如果 ReadFile 函数返回 FALSE
        {
            DWORD last_error = GetLastError();
            if (last_error == ERROR_IO_PENDING)
            {
                QLOG_APP(L"Read file return ERROR_IO_PENDING..");
                BOOL bRet = GetOverlappedResult(serial_handle_, &over_lapped, &read_size, TRUE);
                if (bRet)
                {
                    // 返回 true 代表读取到了数据
                    QLOG_APP(L"Read data {0}") << nbase::UTF8ToUTF16((char*)buffer.get());
                    cb_((char*)buffer.get());
                    PurgeComm(serial_handle_, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
                }
                else
                {
                    // 返回 false 可能是句柄已经被 close 了
                    QLOG_APP(L"GetOverlappedResult returned false");
                }

                continue;
            }

            QLOG_ERR(L"Failed to read data from serial prot, error code = {0}") << last_error;
        }
    }

    QLOG_APP(L"PostReadThread is quit....");
}
bool SerialPortManager::WriteData(const std::string& data)
{
    QLOG_APP(L"Begin to write data [{0}] to serial port.") << data;

    DWORD bytes_written = data.size() + 1;
    OVERLAPPED over_lapped;
    memset(&over_lapped, 0, sizeof(OVERLAPPED));
    over_lapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    BOOL write_stat = WriteFile(serial_handle_, data.c_str(), bytes_written, &bytes_written, &over_lapped);
    if (!write_stat)
    {
        DWORD last_error = GetLastError();
        if (last_error == ERROR_IO_PENDING)
        {
            WaitForSingleObject(over_lapped.hEvent, 2000);
            return true;
        }

        return false;
    }

    QLOG_APP(L"Finished to write data.");
    return true;
}

读因为是异步操作,我们需要传一个 OVERLAPPED 结构体到 ReadFile 的最后一个参数。写也是一样,我们可以用 GetOverlappedResultWaitForSingleObject 来等待操作事件完成(记得要初始化 OVERLAPPED 否则会报错的)。两种方式读写我都做了演示,可以根据自己的需求改造。 读写操作的时候可以获取当前返回值判断是不是 ERROR_IO_PENDING 来确定是不是有数据还没有读取完成。

总结

串口的读写其实还是相对简单的,上面代码基本上把可能出现问题的点都体现出来了,最后再来罗列一下注意事项。

  • 打开串口时要根据硬件情况初始化串口参数(在 WinBase.h 中有声明)
  • 设置串口的缓冲区和超时
  • 异步去读写串口通过返回值判断是否读写成功
  • 不要忘记初始化 OVERLAPPAD 结构
  • 读取完成后 PurgeComm 串口

Qt 注册自定义数据类型提供信号和槽函数传递参数

Qt 信号和槽函数参数只能是基于 Qt 的基础类型的,比如 QString、int、bool 等,如果想传递自定义类型默认情况下是行不通的。要想在 Qt 的信号和槽函数之间传递自定义类型,可以先将自己的自定义类型注册一下,使用如下代码:

Q_DECLARE_METATYPE(nim::DocTransInfo)

nim::DocTransInfo 为你的自定义数据结构,我这里是一个结构体。当需要传递这个数据时,不是直接使用,而是用 QVariant 来包装一下,信号和槽函数则直接使用 QVariant 类型的数据作为参数传递。首先我们连接信号和槽:

connect(this, SIGNAL(AddDocItemSignalNew(QVariant)), this, SLOT(AddDocItemNew(QVariant)), Qt::QueuedConnection);

信号和槽函数的声明如下:

signals:
    void AddDocItemSignalNew(QVariant variant);
private slots:
    void AddDocItemNew(QVariant variant);

当要发起信号的时候,把我们要传递的这个数据包装到 QVariant 中,然后传递给信号函数就可以了。如下所示:

QVariant data;
data.setValue(file_info);
emit AddDocItemSignalNew(data);

槽函数接受到信号时可以像如下方法一样解析参数出来使用:

void MainForm::AddDocItemNew(QVariant variant)
{
    nim::DocTransInfo doc_info = variant.value<nim::DocTransInfo>();
    // ....... 其他代码
}

这样包装后,我们就可以使用 Qt 的信号和槽功能来传递自定义数据结构了。

利用 NvAPI 设置数字振动数值

开始是一位朋友有这个需求,他给了我一个英伟达官网的开发包,名字是:R410-developer.zip(诸位可以自己到英伟达官网下载),里面提供了一些示例,包含自定义分辨率、显示器颜色设置等,但是显示器色彩设置的例子一致没有跑通,而且我也没有找到哪个参数是可以设置数字振动值的,所以憋屈了很多天。但直到看到了一个 AHK 版本实现的设置工具通过代码发现,其实实现方法是通过 nvapi.dll 动态库导出的一个查询函数地址的方法,将指定接口导出来执行具体业务。再加上在 github 上搜索的各类示例,最终实现了这个功能,下面详细介绍实现步骤。

继续阅读

svn 项目迁移到 git 保留 commit 记录

最近迁移了两个 SVN 项目到 Gitlab 上,唯一的目的就是想把提交记录保存下来,至于谁提交的无所谓,只希望能在看不懂一些代码的时候能追溯回去看看当时为什么这样修改。随后查看了很多资料,初试的时候遇到了很多问题,最终使用如下命令完美解决:

git svn clone https://mysvn/demo --no-metadata --no-minimize-url demo

参考资料:https://blog.csdn.net/xueshanhaizi/article/details/54929365

用于 Windows 下的日志跟踪分析工具(Tail for Windows)

在 Linux 下做开发和调试任务的时候,有些情况会动态去跟踪一些日志的变化来调试问题。Linux 下使用 tail -f 就可以达到需求了,但 Windows 下一直没有找到类似的好用工具,在 github 上也有一些开源项目,不是项目相对陈旧界面丑陋,就是功能不完善不能让人专注于分析日志。索性自己做了一个,预览图如下:

继续阅读

Cmder git 中文显示乱码解决方案

Cmder 是 Windows 下替换原有 cmd 的绝佳工具,但是与默认的 cmd 一样,都与 git 命令行存在一些兼容性问题,比如中文乱码问题。在 Cmder 安装目录下的 config/user-profile.cmd 中添加如下代码,可以解决中文乱码的问题。

set LANG=zh_CN.UTF8

添加后的配置文件

添加后重新打开 Cmder 的效果

将 Cmder 加入到右键菜单,到 Cmder 目录下执行如下命令即可:

Cmder.exe /REGISTER ALL

添加后的效果:

另外你还可以修改终端起始符号 λ 改成 $,修改 vendor/clink.lua 文件,将

"\x1b[1;32;40m{cwd} {git}{hg}{svn} \n\x1b[1;39;40m{lamb} \x1b[0m"

替换为 (实际就是将 {lamb} 替换为 $ )

"\x1b[1;32;40m{cwd} {git}{hg}{svn} \n\x1b[1;39;40m$ \x1b[0m"

参考:https://blog.csdn.net/qq_33304418/article/details/79918123

Windows 编译 openssl-1.0.2p(libeay32.lib、ssleay32.lib)静态库 32/64、debug/release、MT/MTd

最近要用到 libeay32.lib、ssleay32.lib 两个静态库文件,因为以前项目中其中一个文件在 64 位编译时选择的不是 MTd,而导致我引入该库以后提示运行时库和其他库声明冲突,其实实际原因就是生成选项不一样。最终我还是决定自己编译 openssl 的库来使用。

下载所需工具

准备工作

安装 Perl 和 NASM,默认下一步下一步就可以了。Perl 安装的时候记得勾选将执行程序添加到系统环境变量中。NASM 安装时没有选项,需要在完成后要将执行程序添加到系统的环境变量中。如下图所示:

解压 openssl-1.0.2p.tar.gz 到任意目录,比如 D:\openssl-1.0.2p

开始编译

打开 VS 的命令行工具(我这里安装的是 VS2013),所以目录在 C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts 下,如果想编译 32 位的静态库就使用 VS2013 x86 本机工具命令提示,如果想编译 64 位,就使用 VS2013 x64 本机工具命令提示

打开后切换到 D:\openssl-1.0.2p 目录,执行如下命令生成 makefile 文件。

perl configure VC-WIN32

如果是生成 64 位则执行

perl configure VC-WIN64A

成功后如下所示:

如果你要编译 debug 版本,则修改 ms/do_nasm.bat 文件,将原来

perl util\mkfiles.pl >MINFO
perl util\mk1mf.pl nasm VC-WIN32 >ms\nt.mak
perl util\mk1mf.pl dll nasm VC-WIN32 >ms\ntdll.mak
perl util\mk1mf.pl nasm BC-NT >ms\bcb.mak

perl util\mkdef.pl 32 libeay > ms\libeay32.def
perl util\mkdef.pl 32 ssleay > ms\ssleay32.def

修改为:

perl util\mkfiles.pl >MINFO
perl util\mk1mf.pl nasm debug VC-WIN32 >ms\nt.mak
perl util\mk1mf.pl dll nasm debug VC-WIN32 >ms\ntdll.mak
perl util\mk1mf.pl nasm BC-NT >ms\bcb.mak

perl util\mkdef.pl 32 libeay > ms\libeay32.def
perl util\mkdef.pl 32 ssleay > ms\ssleay32.def

就是将第二行和第三行编译选项增加了 debug。修改完成后执行 ms/do_nasm.bat

ms\do_nasm.bat

运行后结果如下:

修改完成执行如下命令开始编译(如果想编译成 dll,则执行 nmake -f ms\ntdll.mak,编译前要修改 ms\ntdll.mak 将 CFLAG 的 /MD 属性修改为 /MT,与你调用项目匹配):

nmake -f ms\nt.mak

如果没有错误,几分钟后编译后的文件就会生成于 D:\openssl-1.0.2p\out32 目录下。

Qt 用画笔对图片实现马赛克效果

最近接了一个用 Qt 做跨平台截图工具的任务,主要功能有截图、绘制图案、马赛克、毛玻璃、文字能效果,其中马赛克功能时参考网上的文献并自己研究制作出来的,这里特意给大家分享一下。有需要的朋友可以作为借鉴。

实现原理

首先要开始实现之前,我们首先要来说一下我自己实现马赛克功能的原理。

  1. 为了可以执行撤销操作,我们不能直接对图片进行修改。将图片附加到窗口上以后,需要在窗口上覆盖一层透明的窗口,在这个窗口使用画笔等工具来绘制马赛克,覆盖后面的图片来实现一个视觉效果,最终保存的时候将图片和透明窗口上绘制的数据合并得到一个绘制了马赛克效果的图片。
  2. 对窗口进行区块划分,比如当我们想让每个马赛克的块大小是 10px,那么我们以图片左上角为 0,0 点,给图片划分成一块一块 10px*10px 的小块。
  3. 当鼠标点击透明窗口的某个区域时要到实际后面图片的响应坐标取这个坐标对应像素的颜色值(QColor),如果条件允许可以将这个坐标周围的色值也都取出来做一个颜色混合得到平均的颜色色彩。
  4. 计算当前坐标对应的 10px10px 小块的起始点坐标,然后根据已经取出来的颜色绘制一个 10px10px 像素的方块。
  5. 鼠标移动过程中时时计算坐标,如果当前鼠标所在坐标已经绘制了一个图形那么不再绘制,如果没有绘制则重复 3、4 步。

大体步骤就是这样的,接下来我们来拆分每一个步骤的实现方式。

实现细节

这个透明窗口派生于 QWidget 类,在构造函数中设置窗口为透明的,这样在我们将绘制的马赛克和图片混合的时候就不会有窗口背景色了。

// 让窗口背景透明,在获取窗口绘制的数据时不显示背景色
setAttribute(Qt::WA_TranslucentBackground, true);

对窗口划分区域并根据鼠标得出马赛克绘制方块。在绘制前你要监听鼠标在窗口上按下、弹起、移动等消息,在鼠标按下前,我们要先初始化一个 image 对象来提供我们获取颜色,并根据图片大小初始化一个坐标数组,比如我们有一个 100*100 的图片,想让马赛克的大小是 10 像素,那么我们就要将这个图片宽和高都划分为 10 个 10 像素的小方块。记录下所有图片的像素坐标点。

// 将截取的未知转为 iamge 对象,用来获取坐标像素的颜色
image_ = originPainting_->toImage();
// 初始化每个像素点的坐标位置,鼠标移动过程中记录坐标并通过数组下标
// 直接访问到数据查看某个区块是否已经被绘制上了马赛克。
pointArray.reset(new QVector<QVector<bool>>);
pointArray->resize(parentWidget()->width());
for (int i = 0; i < parentWidget()->width(); i++) {
    // 高度像素
    (*pointArray)[i].resize(parentWidget()->height());
}
for (int i = 0; i < parentWidget()->width(); i++) {
    for (int j = 0; j < parentWidget()->height(); j++) {
        (*pointArray)[i][j] = false;
    }
}

在按下的时候根据背景图像取像素的点,计算并记录像素坐标点归属那一块我们划分出来的小方块:

int x = endPoint_.x() - endPoint_.x() % mosaicSize_;
int y = endPoint_.y() - endPoint_.y() % mosaicSize_;

if (x < 0) x = 0;
if (y < 0) y = 0;

// 防止越界崩溃
if (x > pointArray->size() - mosaicSize_ ||
    y > (*pointArray)[0].size() - mosaicSize_) {
    break;
}

if ((*pointArray)[x][y] == false) {
    (*pointArray)[x][y] = true;

    MosaicData mosaic;

    // 为适配 retina 屏幕从全屏界面中取像素颜色
    QColor color = image_.pixel(realX, realY);

    QPen pen;
    pen.setColor(color);

    mosaic.color_ = color;
    mosaic.pen_ = pen;
    mosaic.point_ = { x, y };

    drawMosaic_.push_back(mosaic);
}

鼠标移动的时候同样走上面的流程,记录下鼠标移动的所有的坐标。此时在透明窗口的 paintEvent 中,你已经可以根据 drawMosaic_ 里面记录的坐标开始绘制一个一个 mosaicSize_ 大小的小方块了。最后在保存图片的时候,你可以先将透明窗口图片保存为一个 pixmap,然后将 pixmap 合并到图片中就可以了。

QRect imageRect = CommonHelper::getRetinaRect(currentRect_);

// 创建一副空背景图片
QImage resultImg = QImage(imageRect.width(), imageRect.height(), QImage::Format_ARGB32);
std::shared_ptr<QPainter> painter;
painter.reset(new QPainter(&resultImg));

// 先绘制截图的内容
painter->drawPixmap(QRect(0, 0, imageRect.width(), imageRect.height()), *originPainting_, imageRect);

// 再绘制图形数据
for (auto window : drawItemList_) {
    // 跳过空文本窗体
    if (window->getDrawModel() == DrawUnit::MODEL::Text && window->getDrawText().size() == 0) {
        continue;
    }

    // 获取窗口数据到 pixmap 中
    QPixmap pixmap = window->grab();
    // 获取窗口实际大小
    QRect windowRect = window->rect();
    // retina 兼容
    QRect windowImageRect = CommonHelper::getRetinaRect(windowRect);

    painter->drawPixmap(QRect(currentRect_.width() - windowRect.width(), currentRect_.height() - windowRect.height(),
        windowImageRect.width(), windowImageRect.height()), pixmap, windowImageRect);

    window->close();
}

return resultImg;

最终效果