智能仪表常用协议 ModBus-RTU 详解

Modbus 一个工业上常用的通讯协议、一种通讯约定。Modbus协议包括RTU、ASCII、TCP。其中MODBUS-RTU最常用,比较简单,在单片机上很容易实现。虽然RTU比较简单,但是看协议资料、手册说得太专业了,起初很多内容都很难理解。
    所谓的协议是什么?就是互相之间的约定嘛,如果不让别人知道那就是暗号。现在就来定义一个新的最简单协议。例如,

协议: “A” --“LED灭”
       “B” --“报警”
       “C” --“LED亮”
单片机接收到“A”控制一个LED灭,单片机接收到“B”控制报警,单片机接收到“A”控制一个LED亮。那么当收到对应的信息就执行相应的动作,这就是协议,很简单吧。

  先来简单分析一条MODBUS-RTU报文,例如:01  06  00 01  00 17  98 04 
    01             06            00 01           00 17          98 04 
  从机地址        功能号          数据地址          数据         CRC校验

这一串数据的意思是:把数据 0x0017(十进制23) 写入 1号从机地址 0x0001数据地址。

先弄明白下面的东西。

1、报文
   一个报文就是一帧数据,一个数据帧就一个报文: 指的是一串完整的指令数据,就像上面的一串数据。

2、CRC校验
意义:例如上面的  98 04  是它前面的数据(01 06 00 01 00 17)通过一算法(见附录2,很简单的)计算出来的结果,其实就像是计算累加和那样。(累加和:就是010600010017加起来的值,然后它的算法就是加法)。
作用:在数据传输过程中可能数据会发生错误,CRC检验检测接收的数据是否正确。比如主机发出01 06 00 01 00 17 98 04,那么从机接收到后要根据01 06 00 01 00 17 再计算CRC校验值,从机判断自己计算出来的CRC校验是否与接收的CRC校验(98 04主机计算的)相等,如果不相等那么说明数据传输有错误这些数据不能要。

3、功能号
  意义:modbus 定义。见附录1。
  作用:指示具体的操作。

MODBUS-RTU
一、一个报文分析
先声明下我们的目的,我们是要两个设备通讯,用的是MODBUS协议。上面简单介绍了:“报文”“CRC校验”“功能号”。

在单片机中拿出一部分内存(RAM)进行两个设备通讯,例如:

附件

数组后面的注释,说明
OX[20]   代表是输出线圈,用功能码 0x01,0x05,0x0F 访问, 开头地址是 0 (这个后续说明)
IX[20]    代表是输入线圈,用功能码 0x02 访问,             开头地址是 1 (这个后续说明)
另外两个一样的道理。
注意:所谓的“线圈”“寄存器”就是“位变量”“16位变量”,不要被迷惑。之所以称“线圈”我觉得应该是对于应用的设备,MODBUS协议是专门针对485总线设备(例PLC)开发的。

1、主机对从机写数据操作
如果单片机接收到一个报文那么就对报文进行解析执行相应的处理,如上面报文:
    01             06            00 01           00 17          98 04 
  从机地址        功能号          数据地址          数据         CRC校验

假如本机地址是 1 ,那么单片机接收到这串数据根据数据计算CRC校验判断数据是否正确,如果判断数据无误,则结果是:
            HoldDataReg[1]  =  0x0017;
MODBUS主机就完成了一次对从机数据的写操作,实现了通讯。

2、主机对从机读数据操作
主机进行读HoldDataReg[1] 操作,则报文是:
    01             03            00 01           00 01          D5 CA 
 从机地址        功能号          数据地址      读取数据个数       CRC校验
那么单片机接收到这串数据根据数据计算CRC校验判断数据是否正确,如果判断数据无误,则结果是:返回信息给主机,返回的信息也是有格式的:
返回内容:  
    01         03            02             0017          F8 4A
  从机地址   功能号     数据字节个数    两个字节数据    CRC校验
MODBUS主机就完成了一次对从机数据的读操作,实现了通讯。


二、MODBUS报文模型

以上了解到了MODBUS的一帧报文是如何通讯的,其实每个报文的格式都基本一样的。

附件


                             

这里两个缩略词以前不知道,但是现在要明白指的是什么,“ADU”“PDU”
ADU: 应用数据单元
PDU: 协议数据单元

三、MODBUS数据模型

附件

附件

  


四、MODBUS事务处理
  下列状态图描述了在服务器侧MODBUS事务处理的一般处理过程。
           

附件


五、MODBUS请求与响应
  看MODBUS协议手册,中文第 10 页开始,英文第 24 页开始。手册非常详细举例说明了MODBUS协议各个功能号的请求与响应。 



                                                         modbus协议在单片机上实现过程


MODBUS 任务处理函数

附件


函数中,RcvBuf 为串口接收缓冲区,如果接收了一个报文则,RcvBuf[0] 为从机地址,RcvBuf[0] 为MODBUS功能号。根据功能号做出响应,而具体的操作根据功能号在各自的函数中执行,相当于解析接收到的数据。

附录1:MODBUS-RTU功能码
 最常用功能码:
 下面“线圈”“寄存器”其实分别直的就是“位变量”“16位变量”
        01 (0x01)        读线圈 
        02 (0x02)        读离散量输入
        03 (0x03)        读保持寄存器
        04(0x04)         读输入寄存器
        05 (0x05)        写单个线圈 
        06 (0x06)        写单个寄存器
        15 (0x0F)        写多个线圈 
        16 (0x10)        写多个寄存器

附件


附录2:CRC Generation

附件


Visual C#.NET串口通信及测控应用典型实例.PDF 电子书文字版 非扫描版电子书 带实例源码

Visual  C#.NET串口通信及测控应用典型实例 本书为PDF格式,图文排版,非扫描版本

所以文字和源代码均可直接复制,主要讲解PC串口和单片机和PLC等硬件通讯.

书里有汇编 C C++ C# VB语言等语言的实例.




下载地址:

电子书:链接:https://pan.baidu.com/s/1ZWYz_q5O_DrRMCBgwwmw0w  密码:c272

源码:Visual C#.NET串口通信及测控应用典型实例-源码.rar


Winform异步更新UI界面的方法三:真正线程安全的this.Invoke 匿名委托

this.Invoke可以方便的在子线程中对UI界面进行访问,并且是真正的线程安全的.

单纯这一个委托无法实现回调,也就是绑定完成之后无法提供一个后续的操作

使用场景为需要远程读取或处理数据(耗费时间),然后直接绑定到控件显示,无其他后续操作要求.

本方法使用的时候要注意:

                this.Invoke((Action)delegate
                {
                    //这里只应该放置UI控件访问的代码,其他的数据读取和处理要放到外面,因为实际上这个代码块是在主线程中执行的.                    
                    this.label2.Text = i.ToString();
                });
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace chengchenxu.ActionEMO
{
    public partial class Form1 : Form
    {

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //启动累加线程 不停改变Label2内容
            Thread thread2 = new Thread(Add);
            thread2.IsBackground = true;
            thread2.Start();
        }
        private void Add()
        {
            for (int i = 0; i < 50; i++)
            {
                Thread.Sleep(1000);

                //采用匿名委托方式进行访问UI线程
                this.Invoke((Action)delegate
                {
                    //这里的语句实际上是在主线程执行的
                    //子线程委托主线程进行更新
                    //如果下面这样把for语句挪到this.Invoke内 主界面会卡死
                    //this.Invoke((Action)delegate
                    //{
                    //    for (int i = 0; i < 50; i++)
                    //    {
                    //        Thread.Sleep(1000);
                    //        this.label2.Text = i.ToString();
                    //    }

                    //});
                    this.label2.Text = i.ToString();
                });
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //另起线程3
            Thread thread3 = new Thread(Get);
            thread3.Start();
        }

        private void Get()
        {
            
            try
            {
                //尝试从线程3访问主界面 拒绝访问
                this.label2.Text = "efsadddddddddddddddd";



                //如果这里用上this.Invoke委托,则可以访问.
                //this.Invoke((Action)delegate
                //{
                //    this.label2.Text = "efsadddddddddddddddd";
                //});
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            //从主线程中访问label2是可以访问的,因为他本来就在主线程内
            this.label2.Text = "efsadddddddddddddddd";
            this.label2.ForeColor = Color.Red;
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            
        }
    }
}


DEMO下载:chengchenxu.ActionEMO - 副本.rar


C.Net串口工具源码 串口调试工具 支持16进制发送 可以定时自动发送

软件功能:


使用C#实现PC串口通讯

支持定时发送

支持16进制(hex) ASCII UTF-8 Unicode 四种编码格式

支持配置文件的保存和读取

可以可以显示发送历史

可以统计总的发送字节数和接受字节数

软件界面:


Screen Shot 2018-03-17 at 17.47.04 PM.png


C#源码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.IO.Ports;
using System.Threading;
using System.IO;
using System.Xml.Serialization;

namespace 串口调试工具
{
    public partial class Form1 : Form
    {
        private SerialPort sp = new SerialPort();//链接对象
        Config config = new Config();//配置文件

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //获取串口列表
            string[] comList = SerialPort.GetPortNames();
            if (comList.Length == 0)
            {
                MessageBox.Show("无可用串口");
                return;
            }
            foreach (var com in comList)
            {
                cmbComList.Items.Add(com);
            }


            //添加波特率
            int[] baudRate = { 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 43000, 56000, 57600, 115200 };
            foreach (var br in baudRate)
            {
                cmbBautRade.Items.Add(br);
            }

            //添加数据位
            int[] dataBits = { 8, 7, 6, 5 };
            foreach (var db in dataBits)
            {
                cmbDataBits.Items.Add(db);
            }

            //添加停止位
            int[] stopBits = { 1, 2, 3 };
            foreach (var sb in stopBits)
            {
                cmbStopBits.Items.Add(sb);
            }

            //添加检验方式
            string[] parity = { "None", "Even", "Mark", "Odd", "Space" };
            foreach (var p in parity)
            {
                cmbParity.Items.Add(p);
            }

            //添加常用编码
            string[] encode = { "ASCII","UTF-8","Unicode","Hex"};
            foreach (var en in encode)
            {
                cmbEncodeSend.Items.Add(en);
                cmbEncodeRec.Items.Add(en);
            }

            //加载配置文件
            LoadConfig();
        }

        private void LoadConfig()
        {
            //把配置信息赋值给控件默认值
            cmbComList.SelectedItem = config.PortName;
            if (cmbComList.SelectedIndex == -1) cmbComList.SelectedIndex = 0;
            cmbBautRade.SelectedItem = config.BautRade;
            cmbDataBits.SelectedItem = config.DataBits;
            cmbStopBits.SelectedItem = config.StopBits;
            cmbParity.SelectedItem = config.Parity;
            cmbEncodeRec.SelectedItem = config.EncodeRec;
            cmbEncodeSend.SelectedItem = config.EncodeSend;

            //激活打开按钮
            this.btnOpen.Enabled = true;
        }

        private void btnOpen_Click(object sender, EventArgs e)
        {
            if (((Button)sender).Text == "打开串口")
            {
                //赋值给串口
                sp.PortName = cmbComList.SelectedItem.ToString();
                sp.Parity=(Parity)Enum.Parse(typeof(Parity),cmbParity.SelectedItem.ToString());
                sp.BaudRate = Convert.ToInt32(cmbBautRade.SelectedItem.ToString());
                sp.StopBits = (StopBits)Convert.ToInt32(cmbStopBits.SelectedItem.ToString());
                sp.DataBits = Convert.ToInt32(cmbDataBits.SelectedItem.ToString());

                try
                {
                    sp.Open();

                    //修改控件状态
                    this.btnOpen.Text = "关闭串口";
                    foreach (Control ctr in groupBox1.Controls)
                    {
                        //把下拉框全部禁用
                        ctr.Enabled = false;
                    }

                    //开启新线程,监听收取内容
                    Thread thread = new Thread(receive);
                    thread.Start();
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
            }
            else
            {
                if (this.btnAutoSend.Text == "停止发送")
                {
                    MessageBox.Show("当前定时发送中,请先停止定时发送任务.");
                    return;
                } 
                sp.Close();

                //修改控件状态
                this.btnOpen.Text = "打开串口";
                foreach (Control ctr in this.groupBox1.Controls)
                {
                    ctr.Enabled = true;
                }
            }
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            //窗体关闭时关闭端口
            sp.Close();
        }

        private void btnSend_Click(object sender, EventArgs e)
        {
            Send();
        }

        private void btnAutoSend_Click(object sender, EventArgs e)
        {
            if (this.btnAutoSend.Text == "定时发送")
            {
                //根据打开按钮状态初步检查端口是否已经打开
                if (this.btnOpen.Text == "打开串口") return;

                System.Text.RegularExpressions.Regex reg1 = new System.Text.RegularExpressions.Regex(@"^[0-9]\d*$");
                if (reg1.IsMatch(txtTime.Text.ToString()))
                {
                    //激活Timer定时器
                    this.timer1.Interval = Convert.ToInt32(txtTime.Text.ToString());
                    this.timer1.Start();

                    //修改控件状态
                    this.btnAutoSend.Text = "停止发送";
                    this.btnSend.Enabled = false;

                }
                else
                {
                    MessageBox.Show("发送间隔应为正整数");
                    txtTime.Text = "1000";
                }
            }
            else
            {
                this.btnAutoSend.Text = "定时发送";
                this.btnSend.Enabled = true;
                this.timer1.Stop();
            }
        }

        private void Send()
        {
            if (sp.IsOpen)
            {
                //获取字节
                byte[] buffer = Encode();
                if (buffer.Length == 0) return;

                try
                {
                    sp.Write(buffer, 0, buffer.Length);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }

                //添加发送历史
                this.txtHistory.AppendText(string.Format("[{0}] {1} \r\n", DateTime.Now.ToString("HH:mm:ss.ff"), txtSend.Text));
                //统计发送字节
                this.labSendCount.Text = (Convert.ToInt32(labSendCount.Text) + buffer.Length).ToString();
            }
            else
            {
                this.timer1.Stop();
                MessageBox.Show("串口尚未打开");
                this.btnAutoSend.Text = "定时发送";
            }
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            //定时发送
            Send();
        }

        //字节反编码
        private byte[] Encode()
        {
            byte[] buffer = null;

            switch (this.cmbEncodeSend.SelectedItem.ToString())
            {
                case "Unicode":
                    buffer = Encoding.Unicode.GetBytes(txtSend.Text); 
                    break;
                case "UTF-8":
                    buffer = Encoding.UTF8.GetBytes(txtSend.Text);
                    break;
                case "ASCII":
                    buffer = Encoding.ASCII.GetBytes(txtSend.Text);
                    break;
                case "Hex":
                    buffer = strToToHexByte(txtSend.Text);
                    break;
            }

            return buffer;
        }
        //编码
        private string UnEncode(byte[] buffer)
        {
            string str = string.Empty;

            switch (this.cmbEncodeSend.SelectedItem.ToString())
            {
                case "Unicode":
                    str = new UnicodeEncoding().GetString(buffer);
                    break;
                case "UTF-8":
                    str = new UTF8Encoding().GetString(buffer);
                    break;
                case "ASCII":
                    str = new ASCIIEncoding().GetString(buffer);
                    break;
                case "Hex":
                    str = byteToHexStr(buffer);
                    break;
            }

            return str;
        }
        /// <summary>
        /// 字节数组转16进制字符串
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static string byteToHexStr(byte[] bytes)
        {
            string returnStr = "";
            if (bytes != null)
            {
                for (int i = 0; i < bytes.Length; i++)
                {
                    returnStr += bytes[i].ToString("X2");
                }
            }
            return returnStr;
        }
        /// <summary>
        /// 字符串转16进制字节数组
        /// </summary>
        /// <param name="hexString"></param>
        /// <returns></returns>
        private static byte[] strToToHexByte(string hexString)
        {
            hexString = hexString.Replace(" ", "");
            if ((hexString.Length % 2) != 0)
                hexString += " ";
            byte[] returnBytes = new byte[hexString.Length / 2];
            for (int i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            return returnBytes;
        }

        private void receive()
        {
            //接收信息 先判断是否为打开状态
            while (sp.IsOpen)
            {
                if (sp.BytesToRead == 0) continue;

                //准备接收
                byte[] buffer = new byte[sp.BytesToRead];
                try
                {
                    //接受动作
                    sp.Read(buffer, 0, buffer.Length);

                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }

                //更新UI信息
                this.Invoke((Action)delegate
                {
                    this.txtRec.AppendText(UnEncode(buffer));
                    //是否自动换行
                    if(cbLine.Checked)
                        this.txtRec.AppendText("\r\n");

                    //接收统计信息
                    this.labRecCount.Text = (Convert.ToInt32(this.labRecCount.Text) + buffer.Length).ToString();
                });
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //清空发送历史
            this.txtHistory.Clear();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            //清空接收
            this.txtRec.Clear();
        }

        private void btnLoad_Click(object sender, EventArgs e)
        {
            //读取xml文件 序列化对象
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Filter = "XML(*.xml)|";
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                string filePath = ofd.FileName;
                if (File.Exists(filePath))
                {
                    try
                    {
                        var mySerializer = new XmlSerializer(typeof(Config));
                        using (var sr = new StreamReader(new FileStream(filePath, FileMode.Open, FileAccess.Read)))
                        {
                            config = (Config)mySerializer.Deserialize(sr);
                        }
                        LoadConfig();
                    }
                    catch (Exception ee)
                    {
                        MessageBox.Show(ee.Message);
                    }
                }
                else
                {
                    MessageBox.Show("文件不存在");
                }
            }
        }

        private void btnSave_Click(object sender, EventArgs e)
        {

            FolderBrowserDialog fbd = new FolderBrowserDialog();            
            if (fbd.ShowDialog() == DialogResult.OK)
            {
                //给对象赋值并序列化保存
                config.PortName = cmbComList.SelectedItem.ToString();
                config.Parity = cmbParity.SelectedItem.ToString();
                config.BautRade = Convert.ToInt32(cmbBautRade.SelectedItem.ToString());
                config.StopBits = Convert.ToInt32(cmbStopBits.SelectedItem.ToString());
                config.DataBits = Convert.ToInt32(cmbDataBits.SelectedItem.ToString());

                XmlSerializer xmlFormat=new XmlSerializer(typeof(Config));
                string filePath = fbd.SelectedPath + "\\PortConfig.xml";
                using (Stream stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
                {
                    xmlFormat.Serialize(stream, config);
                }
                MessageBox.Show("成功保存到路径:"+filePath);
            }
        }

        private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            System.Diagnostics.Process.Start("http://www.chengchenxu.com");  
        }
    }
}


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 串口调试工具
{
    [Serializable]
    public class Config
    {
        public string PortName { get; set; }
        public int BautRade { get; set; }
        public int DataBits { get; set; }
        public string Parity { get; set; }
        public string EncodeSend { get; set; }
        public string EncodeRec { get; set; }
        public int StopBits { get; set; }
        public Config()
        {
            //默认值
            BautRade = 9600;
            DataBits = 8;
            Parity = "None";
            EncodeSend = "ASCII";
            EncodeRec = "ASCII";
            StopBits = 1;
        }
        
    }
}


软件下载:

串口调试工具.rar


源码工程文件:

串口调试工具.rar


虚拟端口软件下载:

VirtualSerialPortDriver.rar


未实现的功能:

16进制校验功能,即发送16进制格式的时候检查输入值是否为正确的16进制数字

接受文件的完整性校验功能,有时候一条信息会分两次接受完整


Winform异步更新UI界面的方法二:CheckForIllegalCrossThreadCalls=false 放弃线程安全

在窗体中设置以下属性,可以使窗体放弃线程安全检查,那么结果就是任意线程任意时间均可自由互相访问,例如所有线程都可以直接访问UI控件.

这个方法任何时候均不推荐使用.使用不安全的线程容易让程序发生奇怪并难以调试的BUG.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace chengchenxu.com.CheckForIllegalCrossThreadCalls
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            CheckForIllegalCrossThreadCalls = false; //取消线程安全检查 使UI访问不受限制
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //启用线程 进行累加
            Thread thread = new Thread(Add);
            //设为后台线层 否则关闭窗体线程不会停止 直到执行完毕 这里为了便于调试 
            thread.IsBackground = true;
            thread.Start();           
        }
        private void Add()
        {
            for (int i = 0; i < 1000; i++)
            {
                //可以随意访问UI控件
                this.label2.Text = i.ToString();
                Thread.Sleep(50);
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //开启新线程,访问Label2
            Thread thread2 = new Thread(GetLabelText);
            thread2.Start();
        }
        private void GetLabelText()
        {
            //取得Label2的值
            MessageBox.Show("取得值"+this.label2.Text);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            //主线程直接访问Label2
            MessageBox.Show("取得值" + this.label2.Text);
        }

        
    }
}


DEMO:chengchenxu.com.CheckForIllegalCrossThreadCalls.rar


Winform异步更新UI界面的方法一:伪多线程 Timer控件

1 Timer是最简单的实现"类似多线程"的方法

2 Timer只适合执行简单快速的操作

3 Timer是伪多线程,需要定期从主线程(UI线程)中索要执行时间

4 Timer执行时间过长会发生UI卡顿

5 Timer如果每次的执行时间都超过时钟周期,那么会发生周期混乱.


演示代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace ChengChenXu.com.ThreadDemo
{
    public partial class TimerControl : Form
    {
        public TimerControl()
        {
            InitializeComponent();
        }

        private void TimerControl_Load(object sender, EventArgs e)
        {
            //启动Timer1定时器 持续更新时间
            timer1.Start();
        }
        private void timer1_Tick(object sender, EventArgs e)
        {
            //Timer1 周期100毫秒 显示时间 
            //比如本操作需要1毫秒来执行 那么剩余99毫秒将会空闲留给UI 所以UI不会卡死
            this.label2.Text = DateTime.Now.ToString("HH-mm-ss-FF");
        }

       

        private void button1_Click(object sender, EventArgs e)
        {
            //启动Timer2定时器 周期100毫秒
            timer2.Start();
        }

        private void timer2_Tick(object sender, EventArgs e)
        {
            //本操作需要大概需要500毫秒来执行,但是周期只有100毫秒
            //那么将发生线程阻塞,实际表现就是UI会间歇性的卡顿
            //并且周期混乱
            Thread.Sleep(500); //停顿500毫秒

            label4.Text = DateTime.Now.ToString("HH-mm-ss-FF");
        }
    }
}


C#串口通讯教程 简化一切 只保留核心功能 这可能是最易于理解的一篇教程

串口的定义,请自行了解.

C#操作串口通讯在.Net强大类库的支持下,只需要三个步骤:

1 创建

2 打开 

3 发送/接受


1 创建:


1 串口通讯需用用到的命名空间如下:

using System.IO.Ports;

2 因为全局使用,所以声明为全局变量

private SerialPort spSend = new SerialPort();

3 指定串口名称

spSend.PortName = "COM1";
//继续根据需要指定端口的波特率,校验位等信息
//在例子中我们只指定名称,其他的一概不管.


2 打开:

spSend.Open();


3 发送/接收

byte[] data = Encoding.ASCII.GetBytes("要发送的信息");
spSend.Write(data, 0, data.Length);
byte[] data = new byte[spSend.BytesToRead];
spSend.Read(data, 0, data.Length);
String str = new ASCIIEncoding().GetString(data);//收取到的信息


好了,核心代码就是这么简单,下面看完整实例,

界面:

Screen Shot 2018-03-15 at 11.00.20 AM.png

控件名称:下拉框ComList  打开按钮btnOpen 发送框 txtSend 发送按钮btnSend 接收框txtInfo 另外还有一个定时器Timer1


完整源码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports;//需要的命名空间
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace ChengChenXu.com.COMDemo
{
    public partial class Form1 : Form
    {
        private SerialPort spSend = new SerialPort(); //全局变量
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //获取本机串口列表
            string[] comList = SerialPort.GetPortNames();
            if (comList.Length == 0)  MessageBox.Show("本机没有任何串口");
            //绑定串口列表到下拉列表,设置第一项为默认值   
            foreach (var com in comList)
            {
                ComList.Items.Add(com);
            }
            ComList.SelectedIndex =  0;

            //启动定时器,用来接受信息,没有使用多线程,更易于理解
            timer1.Start();
        }
        private void btnOpen_Click(object sender, EventArgs e)
        {
            if (ComList.Items.Count == 0)
            {
                MessageBox.Show("没有发现串口");
                return;
            }

            //判断是打开操作还是关闭操作
            if (btnOpen.Text == "打开串口")
            {
                if (!spSend.IsOpen)
                {
                    //设置端口名称
                    //这里我们仅仅设置端口的名称,其他的全部用默认.
                    spSend.PortName = ComList.SelectedItem.ToString();

                    try
                    {
                        //打开串口
                        spSend.Open();
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(ex.Message);
                    }
                    //更新控件状态
                    this.btnOpen.Text = "关闭串口";
                    this.ComList.Enabled = false;
                }
            }
            else if(btnOpen.Text=="关闭串口")
            {
                //关闭串口
                spSend.Close();

                btnOpen.Text = "打开串口";
                ComList.Enabled = true;
            }
        }

        private void btnSend_Click(object sender, EventArgs e)
        {
            //发送数据
            //准备数据 这里我们只实现发送ASCII码 其他的可以先转化为byte[]再发送
            byte[] data = Encoding.ASCII.GetBytes(txtSend.Text);

            if (spSend.IsOpen)
            {
                try
                {
                    //发送动作 参数三个分别为数据 起始偏移位置 长度
                    spSend.Write(data, 0, data.Length);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
            }
            else
            {
                MessageBox.Show("端口还未打开");
            }

        }

        private void Receive()
        {
            //接收信息 先判断是否为打开状态
            if (spSend.IsOpen)
            {
                //准备接收
                byte[] data = new byte[spSend.BytesToRead];
                //接受动作
                spSend.Read(data, 0, data.Length);
                //把接收到的信息转成字符串显示到控件里
                this.txtInfo.Text += new ASCIIEncoding().GetString(data);
            }
        }
        private void timer1_Tick(object sender, EventArgs e)
        {
            //用定时器来定期执行接收动作 间隔100毫秒
            Receive();
        }
    }
}


如何测试

串口通讯既然是通讯那么肯定是需要两方参与的,如何在单机进行测试呢?下面给出几个方法:

1 方法一 把电脑串口的2 3针链接起来,那么接收方和发送方可以为同一个端口.因为2针负责发送,3针负责接收,连接起来即可形成回路

2 使用两台电脑,用串口线相连

3 使用虚拟串口软件,最简单易用,这里我们采用这个方法进行测试.

首先软件下载:VirtualSerialPortDriver 下载地址: VirtualSerialPortDriver.rar

这个是一个收费软件,半个月的试用期,需要的话可以搜索下是否有破解版


安装好之后打开软件,右侧选择好两个准备互联的串口然后点击Add pair即可. 我选择的是COM9和COM10 可以看到左边Virtual ports下面已经有了COM9和COM10了 他们已经可以实现通讯了

Screen Shot 2018-03-15 at 11.16.35 AM.png


把DEMO编译好之后,直接运行两个实例: 一个选择COM9 一个选择COM10 然后都打开串口

Screen Shot 2018-03-15 at 11.18.31 AM.png


现在已经可以互相发送信息了 

由COM9发出的Send for COM9已经发送到COM10

COM9也已经接收到了COM10发出的信息Send for COM10

Screen Shot 2018-03-15 at 11.20.56 AM.png


本例只用了最简单的例子来演示串口通讯过程,简化一切功能,只为更好理解.

源码以及DEMO: ChengChenXu.com.COMDemo.rar


Newtonsoft.Json 简单实用方法 C# Json序列化和反序列化解析操作工具类

Newtonsoft.Json是一个功能多,效率高的Json工具

官方网站:https://www.newtonsoft.com/json

GitHun:https://github.com/JamesNK/Newtonsoft.Json


安装一:

工具-库程序包管理器-库程序包管理器控制台,输入:

Install-Package Newtonsoft.Json

安装二:

项目-右键引用-管理NuGet程序包 可以联机搜索或者搜索已经安装的程序集引入即可


安装三:直接引入DLL文件, 下载地址:

Newtonsoft.Json.rar

版本6.0.0.0


对象的序列化和反序列化:


首先定义一个实体类,只有简单的几个属性

    /// <summary>
    /// 新闻类
    /// </summary>
    public class NewArticle
    {
        //ID
        public int Id { get; set; }
        //标题
        public string Title { get; set; }
        //内容
        public string Content { get; set; }
        //新闻所属分类
        public ArticleClass ArticleClass { get; set; }


    }

    public class ArticleClass
    {
        //分类ID
        public int Cid { get; set; }
        //分类名称
        public string CName { get; set; }
    }


序列化和反序列化演示:

        static void Main(string[] args)
        {
            //实例化一个NewArticle对象
            NewArticle na = new NewArticle
            {
                Id = 1,
                Title = "Newtonsoft.Json的用法简介",
                Content = "Newtonsoft.Json的用法简介的详细文章内容",
                ArticleClass = new ArticleClass { 
                    Cid=1,
                    CName="技术分享"
                }
            };

            //序列化和反序列化:这里主要利用了Newtonsoft.Json.JsonConvert类的SerializeObject(object o)和DeserializeObject<T>(string jsonString)方法

            //序列化为Json字符串
            string jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(na);
            Console.WriteLine("序列化对象");
            Console.WriteLine(jsonString);
            //输出:{\"Id\":1,\"Title\":\"Newtonsoft.Json的用法简介\",\"Content\":\"Newtonsoft.Json的用法简介的详细文章内容\",\"ArticleClass\":{\"Cid\":1,\"CName\":\"技术分享\"}}

            Console.WriteLine("反序列化对象");

            //反序列化:这里利用了泛型方法
            NewArticle na2 = Newtonsoft.Json.JsonConvert.DeserializeObject<NewArticle>(jsonString);

            Console.WriteLine("ID:{0}\nTitle:{1}\nContent:{2}",na2.Id,na.Title,na2.Content);

            Console.ReadKey();

        }

匿名对象的序列化和反序列化

有时候我们仅想使用Json传递一组临时数据给Js进行处理,后台不需要对数据再反序列化,可以利用匿名对象.

删除掉上面代码的Article和ArticleClass类 现在Main方法改为:

        static void Main(string[] args)
        {
           
            var  na = new
            {
                Id = 1,
                Title = "Newtonsoft.Json的用法简介",
                Content = "Newtonsoft.Json的用法简介的详细文章内容",
                ArticleClass = new {
                    Cid = 1,
                    CName = "技术分享"
                }
            };


            //序列化和反序列化:这里主要利用了Newtonsoft.Json.JsonConvert类的SerializeObject(object o)和DeserializeObject<T>(string jsonString)方法

            //序列化为Json字符串
            string jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(na);
            Console.WriteLine("序列化对象");
            Console.WriteLine(jsonString);
            //输出:{\"Id\":1,\"Title\":\"Newtonsoft.Json的用法简介\",\"Content\":\"Newtonsoft.Json的用法简介的详细文章内容\",\"ArticleClass\":{\"Cid\":1,\"CName\":\"技术分享\"}}

            Console.WriteLine("反序列化对象");

            //反序列化:这里利用了非泛型方法 因为匿名类没有固定类型
            var na2 = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonString);

            //匿名类取值需要利用反射,所以需要反序列化的对象一般都不要用匿名对象
            Console.ReadKey();

        }

当运行到最后的时候,可以查看Na2的值:

Screen Shot 2018-03-12 at 12.28.41 PM.png

说明已经反序列化成功了.对于匿名对象想取得属性的话需要用到反射,所以需要反序列化的类一般要实际定义,不要使用匿名对象.

.NET CLR via C#(第4版)PDf电子书下载 .net C#经典名著 深入剖析C#内部机制 值得一读

赵姐夫推荐的本书读法:

细读:都要读懂,要都理解了,读不懂反复读,找额外资料读。
通读:大致都了解可以干嘛,尽量看懂。
粗读:随手翻下,读不懂可以跳过,时不时回头看看。

以第4版为例:

Ch1通读。
Ch2和3粗读。
Ch4到19:细读,全是基础内容。
Ch20细读,最后两节(CER和Code Contract)可以粗读。
Ch21细读,讲GC的,比较重要。
Ch22粗读。
Ch23到25通读。
Ch26细读。
Ch27到30通读。

本资源为PDF扫描版,有条件直接上纸质书,(吐槽:Kindle的专业书籍真的太少了,6寸屏幕看PDF又太吃力.)




265M 文件较大 转存到百度网盘 如果连接失效 可以留言 我会及时更新:

链接:https://pan.baidu.com/s/1YwVUKs4FTFw6DdAQoIUcJA  


利用反射和泛型把Model对象按行储存进数据库以及按行取出然后转换成Model 类实例 MVC网站通用配置项管理

该类在MVC中可以方便管理配置信息,可以直接把Model储存进数据库或者从数据库去除数据转为Model.

1 何为配置项目?

比如网站的名称,页脚信息,meta中的KeyWord信息等,如果不想硬编码到网页里,就需要使用配置文件进行储存,通常都是储存到数据库中.使用的时候读取出来,也方便修改.

2 MVC中对于数据的编辑一般是Model建模,然后View调用强类型,使用诸如@Html.TextBoxFor(m=>m.Name)之类的方式,取值时可以直接取到Model,不用再根据Request.From来一个一个的去赋值.

3 MVC的特性提供了强大的数据自检能力,如果Model中属性为Int类型,那么输入的时候如果不是数字则会直接提示类型错误.该特性支持正则表达式,可以说不用写一句js代码就可以完成数据的服务器端和客户端双重验证,十分强大.

4 本类只有两个方法,一个Load 一个Save,顾名思义,一个读取一个储存.参数都采用了泛型.你可以创建一个ConfigWebSIteModel基本设置类,

然后再创建一个ConfigSeo类,来分别管理不同的配置项目,可以一并储存到一个数据表中.

        T Load<T>();
        void Save<T>(T t);

不用的类中的属性不可以重复,否则会覆盖,比如ConfigWebSiteModel中有个ConfigWebSiteModel.Name 那么 ConfigSeo中就不能在出现Name属性,否则会覆盖掉,出错.


核心代码:

    using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.Text;
using ChengChenXu.Blog.Models;
using System.Reflection;
using System.Data.SqlClient;

namespace ChengChenXu.Blog.DAL.SqlServer
{
    public class ConfigModelDAL:IConfigModelDAL
    {
        private readonly string tableName = "blog_Config";//表名
        private readonly string columnKey = "c_Key";//key列名
        private readonly string columnValue = "c_Value";//Value列名
        private readonly string columnType = "c_Type";//Type列名
       


        /// <summary>
        /// 加载
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T Load<T>()
        {
            //通过sqlhelper获取datatable
            string sql = "select * from " + tableName;
            DataTable dt = SqlHelper.ExecuteDataTable(sql);

            //不存在记录
            if (dt.Rows.Count == 0) return default(T);

            //表行转换成列 ,临时表
            DataTable temp = new DataTable();
            foreach (DataRow dr in dt.Rows)
            {
                //添加一列,设置列的数据类型
                DataColumn dc = new DataColumn();
                dc.ColumnName = dr[columnKey].ToString();
                //根据字符串设置数据类型
                dc.DataType = System.Type.GetType(dr[columnType].ToString());
                temp.Columns.Add(dc);

                //如果时第一列,添加一行
                int index = temp.Columns.Count - 1;
                if (temp.Rows.Count == 0) temp.Rows.Add();

                //如果不是第一例,则行必定已经存在,直接赋值
                temp.Rows[0][index] = dr[columnValue];
            }

            if (temp.Columns.Count == 0) return default(T);

            //把临时表转换成Model并返回
            return temp.Rows[0].ToModel<T>();
        }

        /// <summary>
        /// 保存
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        public void Save<T>(T t)
        {
            //利用反射获取对象所有属性
            string attributeName = String.Empty;
            PropertyInfo[] propertys = t.GetType().GetProperties();

            //获取数据库配置表放到内存中,对比数据是否已经存在
            DataTable dt = new DataTable();
            if (propertys.Length > 0)
            {
                dt = SqlHelper.ExecuteDataTable("select * from "+tableName+"");
                //给表设置主键,方便查找.
                dt.PrimaryKey=new[] {(dt.Columns[columnKey])};
            }

            //依次保存对象属性到数据库
            foreach (PropertyInfo pi in propertys)
            {
                //获取属性值
                var a = pi.GetValue(t, null);
                //值为NULL跳出,不保存,进入下个循环
                if (a == null)
                {
                    SqlHelper.ExecuteNonQuery("delete from "+tableName+" where "+columnKey+" ='"+pi.Name+"' ");
                    continue;
                }

                //准备sql参数
                SqlParameter[] parameters = SqlHelper.CreatParameters(
                    new string[] { "Key", "Value" ,"Type"},
                    new object[] { pi.Name, a, a.GetType().ToString() }
                    );

                //查找属性是否已经存在于数据库中
                if(dt.Rows.Contains(pi.Name))
                {
                    //存在 更新属性
                    SqlHelper.ExecuteNonQuery(
                        "update " + tableName + " set " + columnValue + " = @Value , " + columnType + " = @Type where " + columnKey + " = @Key",
                        parameters
                        );
                }
                else
                {
                    //不存在 插入属性
                    SqlHelper.ExecuteNonQuery(
                        "insert into " + tableName + " (" + columnKey + "," + columnValue + "," + columnType + ") values (@key,@value,@type) ",
                        parameters
                        );
                }
            }
        }
    }
}


该类用到了两个外部类,一个是SqlHelper 就是普通的数据库辅助类,只用到了根据Sql语句和参数进行查询,更新,插入的功能,可以替换为自己的Helper类或者直接在此类中完成数据库操作

另外一个是把DataRow转换为Model对象的类,这个类是一个扩展方法,引用之后就可以直接对DataRows实例进行ToModel操作了.

return temp.Rows[0].ToModel<T>();

Sqlhelper类不再贴出,可以自己查找.扩展方法见本文:http://www.chengchenxu.com/Article/10/

本博客源码中也使用了此类,可以关注后期整理好源码后会开源,可以参考用法.

使用方法很简单:

1 这四个属性对应数据库中的表名以及列名,可以自定义,例如下图这样.

        private readonly string tableName = "blog_Config";//表名
        private readonly string columnKey = "c_Key";//key列名
        private readonly string columnValue = "c_Value";//Value列名
        private readonly string columnType = "c_Type";//Type列名


Screen Shot 2018-03-10 at 15.26.58 PM.png

key要设置为主键,类型都为varchar,长度视情况而定.


2 数据库链接字符串都是sqlHelper类中定义,SqlHelper类参考文章:http://www.chengchenxu.com/Article/11/sqlhelper


3 创建一个Model

public class ConfigSeoModel
{
        [Display(Name = "Meta关键字")]
        public string KeyWord { get; set; }
        [Display(Name = "Meta描述")]
        public string Description { get; set; }
}

//
ConfigModelDAL dal=new ConfigModelDAL();

//new 一个Model

ConfigSeoModel model=new ConfigSeoModel();
model.KeyWord="关键字";
model.Description = "描述"

//完成保存
dal.Save<ConfigSeoModel>(model);

 
//读取
ConfigSeoModel model = dal.Load<ConfigModel>();


首页 1 2 3 尾页