プログラムを書こう!

実務や自作アプリ開発で習得した役に立つソフトウェア技術情報を発信するブログ

MFCでシリアル通信を行う。

この記事は2018年12月11日に投稿しました。

f:id:paveway:20190914064630j:plain

目次

  1. はじめに
  2. MFCでシリアル通信を行う
  3. おわりに

1. はじめに

こんにちは、iOSのエディタアプリPWEditorの開発者の二俣です。
今回は業務で使用しているMFCでシリアル通信を行う方法についてです。

目次へ

2. MFCでシリアル通信を行う

MFCでシリアル通信を行う方法ですが、今回は実装例で示します。

実装例

f:id:paveway:20181210213055p:plain
MFCでシリアル通信を行うサンプルダイアログ

CSampleSerialDlg.h

#pragma once

// CSampleSerialDlg ダイアログ
class CSampleSerialDlg : public CDialogEx
{
// コンストラクション
public:
    CSampleSerialDlg(CWnd* pParent = NULL);    // 標準コンストラクター

// ダイアログ データ
    enum { IDD = IDD_SAMPLESERIAL_DIALOG };

// 実装
protected:
    virtual BOOL OnInitDialog();
    virtual void DoDataExchange(CDataExchange* pDX);   // DDX/DDV サポート

    // メッセージハンドラ
    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnBnClickedBnSendData();
    afx_msg void OnBnClickedBnClear();
    afx_msg void OnBnClickedBnClose();
    afx_msg LRESULT OnCopyData(WPARAM wParam, LPARAM lParam);
    afx_msg LRESULT OnRecvThreadTerminated(WPARAM wParam, LPARAM lParam);

// 値変数
private:
    CString m_sendData;
    CString m_recvData;

// 内部関数
private:
    BOOL InitSerial();
    BOOL UninitSerial();
    BOOL SendData(CString sendData);
    BOOL StartRecvThread();
    static UINT CallRecvThread(LPVOID pParam);
    void RecvThread();

// 内部変数
private:
    HANDLE m_hSerial;
    CWinThread* m_pRecvThread;
    BOOL m_bIsRecvThreadTerminated;
};

CSampleSerialDlg.cpp

#include "stdafx.h"
#include "SampleSerial.h"
#include "SampleSerialDlg.h"
#include "afxdialogex.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// CSampleSerialDlg ダイアログ

// ユーザ定義メッセージ
#define WM_USER_READ_THREAD_TERMINATE  (WM_USER + 100)    // 受信スレッド停止メッセージ

/**
 * @brief コンストラクタ
 */
CSampleSerialDlg::CSampleSerialDlg(CWnd* pParent /*=NULL*/)
    : CDialogEx(CSampleSerialDlg::IDD, pParent)
    , m_sendData(_T(""))
    , m_recvData(_T(""))
    , m_bIsRecvThreadTerminated(FALSE)
{

}

/**
 * @brief ダイアログデータを交換する時に呼び出されます。
 *
 * @param [in] pDX CDataExchangeオブジェクトへのポインタ―
 */
void CSampleSerialDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Text(pDX, IDC_EN_SEND_DATA, m_sendData);
    DDX_Text(pDX, IDC_EN_RECV_DATA, m_recvData);
}

// メッセージマップ
BEGIN_MESSAGE_MAP(CSampleSerialDlg, CDialogEx)
    ON_BN_CLICKED(IDC_BN_SEND_DATA, &CSampleSerialDlg::OnBnClickedBnSendData)
    ON_BN_CLICKED(IDC_BN_CLEAR, &CSampleSerialDlg::OnBnClickedBnClear)
    ON_BN_CLICKED(IDC_BN_CLOSE, &CSampleSerialDlg::OnBnClickedBnClose)
    ON_MESSAGE(WM_COPYDATA, CSampleSerialDlg::OnCopyData)
    ON_MESSAGE(WM_USER_READ_THREAD_TERMINATE, CSampleSerialDlg::OnRecvThreadTerminated)
END_MESSAGE_MAP()

// CSampleSerialDlg メッセージ ハンドラー

/**
 * @brief ダイアログの初期化の時に呼び出されます。
 *
 * @return 処理結果 TRUE:成功/FALSE:失敗
 */
BOOL CSampleSerialDlg::OnInitDialog()
{
    // 親クラスの関数を呼び出します。
    CDialogEx::OnInitDialog();

    // シリアル通信の初期化処理を行ないます。
    if (!InitSerial())
    {
        // シリアル通信の初期化処理に失敗した場合、終了します。
        return FALSE;
    }

    // 受信スレッドを開始します。
    StartRecvThread();

    return TRUE;
}

/**
 * @brief 送信ボタンが押下された時に呼び出されます。
 */
void CSampleSerialDlg::OnBnClickedBnSendData()
{
    // 入力された送信データを取得します。
    UpdateData(TRUE);
    CString sendData = m_sendData.GetString();

    // データを送信します。
    SendData(sendData);
}

/**
 * @brief クリアボタンが押下された時に呼び出されます。
 */
void CSampleSerialDlg::OnBnClickedBnClear()
{
    m_recvData = _T("");
    UpdateData(FALSE);
}

/**
 * @brief 閉じるボタンが押下された時に呼び出されます。
 */
void CSampleSerialDlg::OnBnClickedBnClose()
{
    // シリアル受信スレッドを停止します。
    m_bIsRecvThreadTerminated = TRUE;
}

/**
 * @brief OnCopyDataメッセージが送信された時に呼び出されます。
 *
 * @param [in] wParam パラメータ
 * @param [in] lParam パラメータ
 * @return 処理結果 0:成功/0以外:失敗
 */
LRESULT CSampleSerialDlg::OnCopyData(WPARAM wParam, LPARAM lParam)
{
    // 取得した受信データを受信データ表示用テキストボックスに追加します。
    COPYDATASTRUCT* pData = (COPYDATASTRUCT*)lParam;
    char* pRecvData = (char*)pData->lpData;
    CString recvData;
    recvData.Format(_T("%s"), pRecvData);
    CString data = m_recvData.GetString();
    m_recvData.SetString(data + recvData);
    UpdateData(FALSE);

    return 0;
}

/**
 * @brief 受信スレッドが停止した時に呼び出されます。
 *
 * @param [in] wParam パラメータ(未使用)
 * @param [in] lParam パラメータ(未使用)
 * @return 処理結果 0:成功/0以外:失敗
 *         ※ここでは常に成功にしています。
 */
LRESULT CSampleSerialDlg::OnRecvThreadTerminated(WPARAM wParam, LPARAM lParam)
{
    // シリアル通信の終了処理を行います。
    UninitSerial();

    // ダイアログを閉じます。
    EndDialog(0);

    return 0;
}

// 内部関数

/**
 * @brief シリアル通信の初期化処理を行います。
 *
 * @return 処理結果 TRUE:成功/FALSE:失敗
 */
BOOL CSampleSerialDlg::InitSerial()
{
    // シリアルポートをオープンする。
    m_hSerial = CreateFile(
        _T("COM1"),
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        0);

    // シリアルポートがオープンできない場合
    if (m_hSerial == INVALID_HANDLE_VALUE)
    {
        // 終了します。
        return FALSE;
    }

    // シリアルポートの通信条件を取得します。
    DCB dcb;
    ZeroMemory(&dcb, sizeof(dcb));
    if (!GetCommState(m_hSerial, &dcb))
    {
        // シリアルポートの通信条件が取得できない場合、終了します。
        CloseHandle(m_hSerial);
        m_hSerial = NULL;
        return FALSE;
    }

    // シリアル通信の入出力バッファをクリアします。
    PurgeComm(m_hSerial, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);

    // シリアルポートの通信条件を設定します。
    dcb.fBinary = TRUE;
    dcb.BaudRate = 9600;
    dcb.fParity = FALSE;
    dcb.Parity = NOPARITY;
    dcb.ByteSize = 8;
    dcb.StopBits = ONESTOPBIT;
    dcb.fOutxCtsFlow = TRUE;
    dcb.fOutxDsrFlow = FALSE;
    dcb.fDsrSensitivity = FALSE;
    dcb.fTXContinueOnXoff = FALSE;
    dcb.fOutX = FALSE;
    dcb.fInX = FALSE;
    dcb.fDtrControl = DTR_CONTROL_ENABLE;
    dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
    dcb.fNull = TRUE;

    // シリアル通信の設定を行います。
    if (!SetCommState(m_hSerial, &dcb))
    {
        // シリアル通信の設定でエラーの場合、終了します。
        CloseHandle(m_hSerial);
        m_hSerial = NULL;
        return FALSE;
    }

    // シリアル通信のタイムアウトパラメータを取得します。
    COMMTIMEOUTS timeouts;
    ZeroMemory(&timeouts, sizeof(timeouts));
    if (!GetCommTimeouts(m_hSerial, &timeouts))
    {
        // シリアル通信のタイムアウトパラメータの取得でエラーの場合、終了します。
        CloseHandle(m_hSerial);
        m_hSerial = NULL;
        return FALSE;
    }

    // シリアル通信のタイムアウトパラメータを再設定します。
    timeouts.ReadIntervalTimeout = MAXDWORD;
    timeouts.ReadTotalTimeoutMultiplier = 0;
    timeouts.ReadTotalTimeoutConstant = 100;
    timeouts.WriteTotalTimeoutMultiplier = 0;
    timeouts.WriteTotalTimeoutConstant = 10;
    if (!SetCommTimeouts(m_hSerial, &timeouts))
    {
        // シリアル通信のタイムアウトパラメータの再設定でエラーの場合、終了します。
        CloseHandle(m_hSerial);
        m_hSerial = NULL;
        return FALSE;
    }

    // シリアル通信の拡張機能を設定します。
    EscapeCommFunction(m_hSerial, SETRTS);

    return TRUE;
}

/**
 * @brief シリアル通信の終了処理を行います。
 *
 * @return 処理結果 TRUE:成功/FALSE:失敗
 *         ※ここでは常に成功としています。
 */
BOOL CSampleSerialDlg::UninitSerial()
{
    // シリアルのハンドルをクローズします。
    CloseHandle(m_hSerial);
    m_hSerial = NULL;

    return TRUE;
}

/**
 * @brief データを送信します。
 *
 * @param [in] sendData 送信するデータ
 * @return 処理結果 TRUE:成功/FALSE:失敗
 */
BOOL CSampleSerialDlg::SendData(CString sendData)
{
    // 送信するデータをCString型からchar型に変換します。
    int writeLen = sendData.GetLength();
    char* buf = new char[writeLen + 1];
    strcpy_s(buf, writeLen + 1, (char*)sendData.GetBuffer());

    // データを全て送信するまで繰り返します。
    int totalWriteBytes = 0;
    do
    {
        int writeBytes = 0;
        BOOL bRet = WriteFile(m_hSerial, buf, writeLen - totalWriteBytes, (LPDWORD)&writeBytes, NULL); 
        
        // エラーまたはデータを送信できなかった場合
        if (!bRet || (writeBytes == 0))
        {
            // 終了します。
            break;
        }
        totalWriteBytes += writeBytes;
    }
    while (totalWriteBytes != writeLen);
    return TRUE;
}

/**
 * @brief 受信スレッドを開始します。
 *
 * @return 処理結果 TRUE:成功/FALSE:失敗
 */
BOOL CSampleSerialDlg::StartRecvThread()
{
    // 受信スレッドを生成します。
    m_pRecvThread = AfxBeginThread(CallRecvThread, (LPVOID)this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL);
    
    // 受信スレッドが生成できない場合
    if (!m_pRecvThread)
    {
        // エラーで終了します。
        return FALSE;
    }
    
    // 受信スレッドを開始します。
    m_pRecvThread->m_pMainWnd = this;
    m_pRecvThread->m_bAutoDelete = TRUE;
    m_pRecvThread->ResumeThread();
    return TRUE;
}

/**
 * @brief 受信スレッド本体を呼び出します。
 *
 * @param [in] pParam パラメータ
 * @return 処理結果 0:成功/0以外:失敗
 */
UINT CSampleSerialDlg::CallRecvThread(LPVOID pParam)
{
    // 受信処理はCSampleSerialDlgのメンバ関数で行います。
    CSampleSerialDlg* pDlg = dynamic_cast<CSampleSerialDlg*>(reinterpret_cast<CWnd*>(pParam));
    if (pDlg)
    {
        pDlg->RecvThread();
    }
    return 0;
}

/**
 * @brief 受信スレッド本体
 *        10ms間隔でポーリングします。
 */
void CSampleSerialDlg::RecvThread()
{
    // 受信スレッド停止フラグが未設定の間、繰り返します。
    do
    {
        // 10ms待ちます。
        Sleep(10);

        // 受信データを1バイト読み出します。
        char achar = 0;
        DWORD dwRead = 0;
        BOOL bRet = ReadFile(m_hSerial, (LPVOID)&achar, 1, &dwRead, NULL);

        // 受信データの読み出しでエラーの場合
        if (!bRet)
        {
            // エラーをクリアして、終了します。
            ULONG portError = 0;
            COMSTAT cs;
            ZeroMemory(&cs, sizeof(cs));
            ClearCommError(m_hSerial, &portError, &cs);
            break;
        }

        // 受信データがある場合
        if (dwRead > 0)
        {
            // 受信データをUIに反映します。
            char recvData[1];
            recvData[0] = achar;
            COPYDATASTRUCT data;
            memset(&data, 0, sizeof(data));
            data.dwData = 0;
            data.lpData = recvData;
            data.cbData = sizeof(recvData);
            this->SendMessage(WM_COPYDATA, 0, (LPARAM)&data);
        }
    }
    while (!m_bIsRecvThreadTerminated);

    // スレッド停止処理を呼び出します。
    this->PostMessage(WM_USER_READ_THREAD_TERMINATE);
}

目次へ

3. おわりに

別の人が作ったコードを元に、シリアル通信部分のみ抜き出しました。
個人的には受信処理はポーリングではなく、イベント待ちなどがいいのですが、イベント待ちのサンプルは今後試したいと思います。

紹介している一部の記事のコードはGitlabで公開しています。
興味のある方は覗いてみてください。

目次へ


私が勤務しているニューラルでは、主に組み込み系ソフトの開発を行っております。
弊社製品のハイブリッドOS Bi-OSは高い技術力を評価されており、特に制御系や通信系を得意としています。
私自身はiOSモバイルアプリウィンドウズアプリを得意としております。
ソフトウェア開発に関して相談などございましたら、お気軽にご連絡ください。

また一緒に働きたい技術者の方も随時募集中です。
興味がありましたらご連絡ください。

EMAIL : info-nr@newral.co.jp / m-futamata@newral.co.jp
TEL : 042-523-3663
FAX : 042-540-1688

目次へ