C# 新手开始编写串口测试工具

前言:

  零基础开始学习使用C#,做一个基于串口的配置工具

  开发环境:VS2010,Framework.NET 4.0,Windows7 64位


一. 实现串口的打开关闭 (2014/06/07)

1.1 窗口绘制


  技巧1:启动软件后COM3和波特率的ComboBox怎么改为不可编辑状态?
ComboBox的DropDownStyle属性改为:DropDownList后控件不能被编辑.


  技巧2:启动软件后串口号的ComboBox如何列举出当系通存在的串口,并显示最大的串口号?
1) 包含.NET的IO类

using System.IO.Ports;      //使用IO口

2) 新建一个枚举串口的函数private bool serial_list(),  注:TS_ComboBox_com即串口号控件, TS_ComboBox_bps即波特率控件

        private bool serial_list()
        {
            //使用系统枚举串口
            string[] port = System.IO.Ports.SerialPort.GetPortNames(); //获取存在的串口字符数组
            string   def_bps = ConfigurationManager.AppSettings["SerialDefBps"];
            TS_ComboBox_com.Items.Clear();                      //清ComboBox
            foreach(string str in port)                         //将串口号显示到ComboBox
            {
                TS_ComboBox_com.Items.Add(str);
            }
            int i = TS_ComboBox_com.Items.Count;
            if(0 != i)
            {
                //有串口则显示最新的一个串口号
                //若配置文件已定义波特率填写配置文件的波特率,若无则使用115200
                TS_ComboBox_com.Text = TS_ComboBox_com.Items[i - 1].ToString();
                if (null != def_bps) TS_ComboBox_bps.Text = def_bps;
                else TS_ComboBox_bps.Text = "115200";
                return true;
            }
            else
            {
                return false;
            }
        }


  技巧3: 怎么将"12345"等字符串转为数值?
程序中会经常用到这些装换,常用的装换有:

            int result = 65535;                     //先定义result未一个已知值
            //使用Convert转换字符串
            result = Convert.ToInt32("");           //发生异常,后续程序无法执行
            result = Convert.ToInt32("12345");      //正常:result=12345    
            result = Convert.ToInt32("123ab");      //发生异常,后续程序无法执行
            result = Convert.ToInt32("ab123");      //发生异常后续程序无法执行
            result = Convert.ToInt32("123 a");      //发生异常后续程序无法执行
                        //使用int.Parse转换字符转
            result = int.Parse("");                 //发生异常,后续程序无法执行
            result = int.Parse("12345");            //正常:result=12345
            result = int.Parse("123ab");            //发生异常,后续程序无法执行
            result = int.Parse("ab123");            //发生异常,后续程序无法执行
            result = int.Parse("123 a");            //发生异常,后续程序无法执行

            //使用int.TryParse转换
            int.TryParse("", out result);           //执行正常:result=0,函数返回false
            int.TryParse("12345", out result);      //执行正常:result=1235,函数返回true
            int.TryParse("123ab", out result);      //执行正常:result=0,函数返回false
            int.TryParse("ab123", out result);      //执行正常:result=0,函数返回false
            int.TryParse("123 a", out result);      //执行正常:result=0,函数返回false
int.TryParse("1234567890123456789",out result);//明显字符串超出int范围,result=0,函数返回

故而这里建议新手朋友们使用TryParse将字符串转为数值, 还可使用如UInt16.TryParse(), UInt64.TryParse(),float.TryParse()等, 举一反三


  技巧4: 如何将数值转为指定格式的字符串,比如数值0x4F转为字符窜"00004F"?

            //使用ToString方法:
            DateTime dt =new DateTime(2003,6,07);
            MessageBox.Show(2.5.ToString("C") + "\n" +      //货币型:  显示"¥2.50"
                            25.ToString("D5") + "\n" +      //十进制:  显示"00025"
                            25000.ToString("E") + "\n" +    //科学计数: 显示"2.500000E+005"
                            25.ToString("F2") + "\n" +      //浮点固定: 显示"25.00"
                            2.5.ToString("G") + "\n" +      //常规型:  显示"2.5"
                            2500000.ToString("N") + "\n" +  //N数字:  显示"2,500,000.00"
                            2500000.ToString("N0") + "\n" + //N数字:  显示"2,500,000"
                            0xF.ToString("X") + "\n" +      //十六进制: 显示"F"
                            0xFF.ToString("X0") + "\n" +    //十六进制: 显示"FF"
                            0xF.ToString("X4") + "\n" +     //十六进制: 显示"000F"
                            0.123.ToString("p")+ "\n" +     //百分比:  显示"12.30%"
                            0.123.ToString("P0")+ "\n" +     //百分比:  显示"12%"
                            1.123.ToString("P3")+ "\n" +     //百分比:  显示"112.230%"
                            dt.ToString("yy.M.d")+"\n" +    //日期:   显示"03.6.7"
                            dt.ToString("yyyy年MM月dd日")+"\n" +   //日期:   显示"2003年6月07日"
                            "");     //十六进制: 显示"000F"
            //使用string.Format方法
            MessageBox.Show(string.Format("the value is {0,4:d}\n", 122) +      //显示:"the value is 122"
                            string.Format("the value is {0,4:f1}\n", 123.45) +  //显示:"the value is 123.4"
                            string.Format("the value is {0,4:f3}\n", 123.45) +  //显示:"the value is 123.450"
                            string.Format("the value is {0,4:f3}\n", 123.45) +  //显示:"the value is 123.450"
                            string.Format("the value is {0,4:X4}\n", 0x4f) +    //显示:"the value is 004F"
                            "");

  技巧5: 如何使用.NET自带的Configuration配置文件功能

然后在需要使用的cs源文件中添加引用:using System.Configuration; //使用配置文件
假设配置文件中有配置项:

<appSettings>
<!--程序启动模式-->
<add key="RunMode" value="-sa" />
<add key="SerialDefBps" value="460800" />
</appSettings>


在CS源文件中读取将配置文件的配置项:

string   def_bps = ConfigurationManager.AppSettings["SerialDefBps"];

注意:若配置文件中不存在配置项SerialDefBps项,而使用了以上调用,则def_bps的值仍是null


  技巧6: 如何添加/修改/移除配置文件的配置项
1)判断是否存在配置项"RunMode"

if (ConfigurationManager.AppSettings["RunMode"] == null) { }

2) 不同于读取,当需要添加/修改/移除配置文件的配置项时,需要先定义一个配置项操作对象,才能编辑配置项.

Configuration cfa = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
cfa.AppSettings.Settings.Remove("RunMode");             //移除一个配置
cfa.AppSettings.Settings.Add("RunMode", "NEW_TEST");    //新增一个配置
cfa.AppSettings.Settings["RunMode"].Value = "-sa";    //修改一个配置
cfa.Save(ConfigurationSaveMode.Modified);               //保存配置
ConfigurationManager.RefreshSection("appSettings");     //更新配置

 技巧7: 程序目录下相对路径的子文件夹InfTxt内的文件version.txt显示到不可编辑的textBox内.

            //step1: 判断是否存在文件夹,若不存在则创建
            if (!System.IO.Directory.Exists(".\\InfoTxt"))
            {
                System.IO.Directory.CreateDirectory(".\\InfoTxt");
            }

            //step2: 判断文件是否存在,不存在则创建
            if (!File.Exists(".\\InfoTxt\\version.txt"))
            {
                System.IO.File.CreateText(".\\InfoTxt\\version.txt");       
            }

            //step3: 读取文件信息,并将其显示到TextBox
            using (StreamReader sr = new StreamReader(".\\InfoTxt\\version.txt", System.Text.Encoding.Default))
            {
                string TextStr;
                TextStr = sr.ReadToEnd().ToString();
                sr.Close();
                textBoxDescription.Text = TextStr;
            }

顺便textBox的ReadOnly属性设为True时,控件不可编辑.


1.2 打开关闭串口

1) 拖放一个serialport控件到窗体,命名为serialPort.
2) 打开串口处理函数

private void TS_Button_open_Click(object sender, EventArgs e)
        {
            //检查是否存在合法的串口
            if ((0 != TS_ComboBox_com.Items.Count) && (null != TS_ComboBox_com.Text))
            {
                try
                {
                    //step1: 打开串口
                    serialPort.PortName = TS_ComboBox_com.Text;
                    serialPort.BaudRate = int.Parse(TS_ComboBox_bps.Text);
                    use_bps = serialPort.BaudRate;  //标记使用波特率
                    serialPort.Open();

                    //step2: 初始化选项卡(我写的初始化代码可能需要几百毫秒执行,让进度条滚动显示一会)
                    ST_ProgressBar.Style = ProgressBarStyle.Marquee;
                    SUpdata_init();
                    SMstCfg_init();
                    ST_ProgressBar.Style = ProgressBarStyle.Continuous;

                    //step3: 启动线程程序 <-------------- 此处新增选项卡的处理
                    if (tabControl_tool.SelectedTab == tabPage1)
                    {
                        my_tab = tabPage1;
                        SUpdata_UartRx_init();
                        //th_uartRx = new Thread(SUpdata_UartRx_Service);
                        //th_uartRx.Start();  //启动线程
                    }
                    else if (tabControl_tool.SelectedTab == tabPage2)
                    {
                        my_tab = tabPage2;
                        SMstCfg_UartRx_init();
                        //th_uartRx = new Thread(SMstCfg_UartRx_Service);
                        //th_uartRx.Start();  //启动线程
                    }
                    else
                    {
                        my_tab = null;
                        MessageBox.Show("未定义控制程序!", "错误!", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }

                    //step4: 标记控件
                    serial_is_open(true);   //打开串口后,打开按键需要失效,关闭按键需要有效等操作由此函数完成
                }
                catch (Exception)
                {
                    serial_is_open(false);
                    MessageBox.Show("串口可能已被占用!\n请先关闭占用串口的应用程序!", "错误:", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
            else
            {
                MessageBox.Show("未检测到有效的串口!\n请连接串口设备后重试!", "提示:", MessageBoxButtons.OK, MessageBoxIcon.Information);
                serial_list();      //枚举串口
            }
        }


3)关闭串口处理函数

        private void TS_Button_close_Click(object sender, EventArgs e)
        {
            serial_is_open(false);
            if (serialPort.IsOpen)      //判断串口是否已打开
            {
                my_tab = null;
                //if( (null != th_uartRx) && (th_uartRx.IsAlive) ) th_uartRx.Abort(); //终止线程
                try
                {
                    //step1: 打开串口
                    serialPort.Close();

                    //step2: 卸载选项卡内容
                    ST_ProgressBar.Style = ProgressBarStyle.Marquee;
                    SUpdata_uninit();
                    SMstCfg_uninit();
                    ST_ProgressBar.Style = ProgressBarStyle.Continuous;

                    //step3: 标记控件
                    serial_is_open(false);
                }
                catch (Exception)
                {
                    serial_is_open(false);
                    MessageBox.Show("尝试关闭串口失败!\n请重新启动应用程序!", "错误:", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
            else
            {
                serial_list();          //枚举串口
                serial_is_open(false);
            }
        }

4) serialPort的接收处理函数

先介绍下串口的接收机制, .NET的串口接收已非常智能了, 完成以上代码后,无需其他更改,打开串口后当接收到一帧数据后, 才会触发serialPort_DataReceived接收事件,
注意: 比如设备连续向上位机发送:
第一次发送: 00 1 1 22 33 44 55 66 77 88 99
第二次发送: AA BB .....(共1024字节,小于接收缓冲)
第三次发送: DD EE FF
则serialPort_DataReceived事件会触发三次,每次使用serialPort.Read()方法读取到的数据和发送的数据相同, 无需特意去做帧尾识别了.
serial的处理代码如下:(实现功能: 将接受到的串口数据,根据激活的选项卡交由不同的服务函数执行)

        private void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            //将数据读出放入数组
            int n = serialPort.BytesToRead;
            byte[] serial_rx_buffer = new byte[n];      //新建一个接受数组
            serialPort.Read(serial_rx_buffer, 0, n);    //将串口接收到的数据写入接收数组 

            //使用处理程序
            if (tabPage1 == my_tab)     SUpdata_UartRx_Service(serial_rx_buffer);
            else if (tabPage2 == my_tab)SMstCfg_UartRx_Service(serial_rx_buffer);
            else
            {
                MessageBox.Show("未定义处理程序,接收到数据:\n" + Serial_Fun.fun_Byte2HexStr(serial_rx_buffer));
            }
        }

5) serialPort数据发送
串口数据发送就非常简单:发送测试代码:

        private void T2_button_next1_Click(object sender, EventArgs e)
        {
            byte[] serial_tx_buffer = {0,1,2,3,4,5,6,7,8,9};
            serialPort.Write("\r\n");
            Thread.Sleep(20);   //ms级延时,注意sleep的精度只有十几毫秒,抖动较大,除非你的win系统啥软件也不装才能达到几毫秒的精度
            serialPort.Write(serial_tx_buffer, 1, 3);   //将会发送3个数据 01 02 03
        }

二. 字符串数据处理 (2014-06-08)

2.1 数组字符串装换

string rxd_str = System.Text.Encoding.Default.GetString( buff);//数组装字符串

byte[] rxd_byte = System.Text.Encoding.Default.GetBytes(rxd_str);//字符转数组

注意:以上方法是等效装换,如Byte[] buff=0,0,32,48,49,50,51,32,00,52,53,32,0,0(共14个元素)转为字符串后,rxd_str的长度为14,但无法显示内容(显示效果等效于14个空格)

解决方法:

    rxd_str = rxd_str.Replace("\0", " ");//将/0替换为空格

    rxd_str = rxd_str.Trim('\0',' ');//分别从头部和尾部开始移除指定的字符直到遇到有效字符位置,使用rxd_str.Trim()不带参数,只会移除空格,带参数能移除指定字符

    最终得到的字符串结果为(长度8)"0123  45"

2.2 字符串分割

实际情况中设备会返回多行ASCII字符数据, 上位机需要解析时必须分割

String.Split 方法有6个重载函数:
1) public string[] Split(params char[] separator)
2) public string[] Split(char[] separator, int count)
3) public string[] Split(char[] separator, StringSplitOptions options)
4) public string[] Split(string[] separator, StringSplitOptions options)
5) public string[] Split(char[] separator, int count, StringSplitOptions options)
6) public string[] Split(string[] separator, int count, StringSplitOptions options)


下边我们通过一些实例来说明下怎么使用(以下string words = "1,2.3,,4";):


1. public string[] Split(params char[] separator)

注:普通分割
string[] split = words.Split(new Char[] { ',' }); //返回:{"1","2.3","","4"}
string[] split = words.Split(new Char[] { ',', '.' });//返回:{"1","2","3","","4"}


2. public string[] Split(char[] separator, int count)

注:按输出个数分割
string[] split = words.Split(new Char[] { ',', '.' }, 2);//返回:{"1","2.3,,4"}
string[] split = words.Split(new Char[] { ',', '.' }, 6);//返回:{"1","2","3","","4"}


3. public string[] Split(char[] separator, StringSplitOptions options)

注:不保留空元素的分割

string[] split = words.Split(new Char[] { ',', '.' }, StringSplitOptions.RemoveEmptyEntries);

//返回:{"1","2","3","4"} 不保留空元素


string[] split = words.Split(new Char[] { ',', '.' }, StringSplitOptions.None);

//返回:{"1","2","3","","4"} 保留空元素


4. public string[] Split(string[] separator, StringSplitOptions options)
string[] split = words.Split(new string[] { ",", "." }, StringSplitOptions.RemoveEmptyEntries);

//返回:{"1","2","3","4"} 不保留空元素


string[] split = words.Split(new string[] { ",", "." }, StringSplitOptions.None);

//返回:{"1","2","3","","4"} 保留空元素


5. public string[] Split(char[] separator, int count, StringSplitOptions options)
string[] split = words.Split(new Char[] { ',', '.' }, 2, StringSplitOptions.RemoveEmptyEntries);

//返回:{"1","2.3,,4"} 不保留空元素
string[] split = words.Split(new Char[] { ',', '.' }, 6, StringSplitOptions.None);//返回:{"1","2","3","","4"} 保留空元素


6. public string[] Split(string[] separator, int count, StringSplitOptions options)
string[] split = words.Split(new string[] { ",", "." }, 2, StringSplitOptions.RemoveEmptyEntries);//返回:{"1","2.3,,4"} 不保留空元素


string[] split = words.Split(new string[] { ",", "." }, 6, StringSplitOptions.None);

//返回:{"1","2","3","","4"} 保留空元


2.3 从字符串中提取有效数据

举例:从以下tst_str字符串中提取出设备的MAC地址的字符串

            //目标字符串(需要提取的结果"8C-61-5F-1F-34-34")
            string tst_str = "  Chip:NetCard  100Mbps Ethernet\r\n" +
                             "\tMAC:    8C-61-5F-1F-34-34\r\n" +
                             "\tIP:     192.168.1.63\r\n" +
                             "\tMask:   255.255.255.0\r\n" +
                             "\tWay:    192.168.1.1\r\n";

            //step1: 设定起始和结束字符串
            //step1: 检测起始字符"MAC:"
            //step2: 检测结束字符从"MAC:"开始,遇到的第一个"\r\n"字符
            //step3: 计算从"MAC:"字符开始的字节位置(idx_start)
            //step4: 计算从"MAC:"开始到"\r\n"之前的字符数(idx_len)
            //step5: 扣除"MAC:"输出子字符串"    8C-61-5F-1F-34-34"
            //step6: 移除结果字符串前后可能存在的空格和制表符
            string str_start="MAC:";    //起始字符串
            string str_endof="\r\n";    //结束字符串
            string out_str="";          //定义结果字符串
            int idx_start = tst_str.IndexOf(str_start);         //检测匹配到的"MAC:"的字节位置
            int idx_len = tst_str.IndexOf(str_endof, idx_start) - idx_start;
            if ((idx_start >= 0) && (idx_len >= str_start.Length))
            {
                out_str = tst_str.Substring(idx_start + str_start.Length, idx_len - str_start.Length);
                out_str = out_str.Trim(' ', '\t');              //去掉头尾可能存在的空格和制表符
                MessageBox.Show(out_str + "\n字符数:" + out_str.Length.ToString());
            }
            else
            {
                MessageBox.Show("未检测到目标字符串!");
            }




2.4 string.Format格式化输出

在C语言中有sprintf函数非常好用,而C#语言的实现方式是:

            int a = 123, b = 456;
            MessageBox.Show(String.Format("a={0:d4},b={1:d4}",a,b));    //结果:"a=0123,b=0456"
            MessageBox.Show(String.Format("a={0:d},b={1:d}", a, b));    //结果:"a=123,b=456"

再附上零占位符合空格占位符的实现代码

string.Format("{0:0000.00}", 12394.039) 结果为:12394.04
string.Format("{0:0000.00}", 194.039) 结果为:0194.04
string.Format("{0:###.##}", 12394.039) 结果为:12394.04
string.Format("{0:####.#}", 194.039) 结果为:194

2.5 字符串转数值

1) 将"MAC: 8-61-5F-1F-34-34YYY"字符串中提取数值08 61 5F 1F 34 34这6个16进制元素到Byte[]数组:

注:每个16进制元素在字符串中占用1或2个数值字符

原理:从左倒要逐个读取数值字符(0-9,a-f,A-F)将其存入string tmp_str中,遇到下一个非数值字符或读取完毕后将tmp_str转为byte,动态添加到List<Byte>表,最后输出Byte[]数组,不废话放代码:

        public static byte[] fun_HexStr2Bytes(string in_str)
        {
            string tmp_str = null;
            List<byte> byte_buff = new List<byte>();

            //解析字符中存在的数字字符
            foreach (char c in in_str)
            {
                if (((c >= '0') && (c <= '9')) || ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F')))
                {
                    tmp_str += c;
                }
                else if ( (1 == tmp_str.Length) || (2 == tmp_str.Length) )
                {
                    byte tmp_byte = byte.Parse(tmp_str, System.Globalization.NumberStyles.HexNumber);
                    byte_buff.Add(tmp_byte);
                    tmp_str = null; //处理完后清string
                }
                else tmp_str = null;
            }

            //继续解析未处理的数字字符串
            if ( (1 == tmp_str.Length) || (2 == tmp_str.Length) )
            {
                byte tmp_byte = byte.Parse(tmp_str, System.Globalization.NumberStyles.HexNumber);
                byte_buff.Add(tmp_byte);
                tmp_str = null; //处理完后清string
            }

            //返回数值
            if (byte_buff.Count > 0)
            {
                byte[] out_bytes = new byte[byte_buff.Count];
                byte_buff.CopyTo(out_bytes);
                return out_bytes;
            }
            else
            {
                return null;
            }
        }

2)将"IP:    192.168.1.245KKKKK"字符提取出四个数字192 168 1 245到int[]数组

原理和上面的相同,放代码:

        public static int[] fun_DecStr2Ints(string in_str)
        {
            string tmp_str = null;
            List<int> int_buff = new List<int>();

            //解析字符中存在的数字字符
            foreach (char c in in_str)
            {
                if ( (c >= '0') && (c <= '9') )
                {
                    tmp_str += c;
                }
                else if (tmp_str.Length > 0)
                {
                    int tmp_int = int.Parse(tmp_str);
                    int_buff.Add(tmp_int);
                    tmp_str = null; //处理完后清string
                }
                else tmp_str = null;
            }

            //继续解析未处理的数字字符串
            if (tmp_str.Length > 0)
            {
                int tmp_int = int.Parse(tmp_str);
                int_buff.Add(tmp_int);
                tmp_str = null; //处理完后清string
            }

            //返回数值
            if (int_buff.Count > 0)
            {
                int[] out_ints= new int[int_buff.Count];
                int_buff.CopyTo(out_ints);
                return out_ints;
            }
            else
            {
                return null;
            }
        }


三. 文件读写操作 (未完待续)


四. 未完待续

五. 未完待续

六. 未完待续

七. 未完待续

展开阅读全文

没有更多推荐了,返回首页