C#上位机 串口上位机Modbus协议


前言

在上位机和下位机通信时如果只是单纯的发送数据将毫无意义,因为即使收到了数据页只是收到了数据,而不知道这条数据的作用,
所以我们需要制定一个应用层协议使程序在收到数据时按照协议解析而执行对应的操作,Modbus就是一种经常配合串口使用的应用层协议。


一、准备工作

这章主要实现功能为下位机使用单片机,其有两个自定义的Modbus协议中所描述的寄存器,通过上位机控制灯的亮灭和DAC输出电压,并将电压用折线图显示出来。
串口固定为115200波特率、8位数据位、1停止位、无校验。
Modbus实现0x03读指令和0x10写多个寄存器指令。
0x03读指令

地址指令寄存器地址寄存器个数CRC
1Byte1Byte2Byte2Byte2Byte

下位机返回

地址指令数据个数数据CRC
1Byte1Byte1Byte2Byte

0x10写指令

地址指令起始寄存器地址寄存器个数数据个数数据CRC
1Byte1Byte2Byte2Byte1Byte2Byte

下位机返回

地址指令起始寄存器地址寄存器个数CRC
1Byte1Byte2Byte2Byte2Byte

下位机中的寄存器定义

寄存器地址功能说明
0x0000控制获取LED灯亮灭0:灭,1:亮
0x0001获取控制DAC和ADC电压0V-3.3V 扩大100倍传输
0x0002获取按键值只读的

二、界面设计

!在这里插入图片描述

上图为设计完成界面,青色的表示LED灯,青色为灯灭,红色为灯亮,点击控件切换,切换的同时向单片机发送命令,折线图显示实时电压100ms获取一次。
通过向界面拖拽一个定时器控件实现并将其Interval属性设置为100,并绑定事件。
在这里插入图片描述

1、LED灯

LED灯为自定义控件,在项目上右键->添加->用户控件->创建一个名为LED的控件,在创建出的白色画布上双击进入代码界面,编写如下程序,完成后进行一次生成操作就可以在工具箱中看到自定义控件。

public partial class LED : UserControl
    {
        public LED()
        {
            InitializeComponent();
        }

        private void LED_Load(object sender, EventArgs e)
        {
            this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            this.SetStyle(ControlStyles.DoubleBuffer, true);
            this.SetStyle(ControlStyles.ResizeRedraw, true);
            this.SetStyle(ControlStyles.Selectable, true);
            this.SetStyle(ControlStyles.UserPaint, true);
        }

        private Graphics g;


        private SolidBrush onS = new SolidBrush(Color.Red);
        private SolidBrush offS = new SolidBrush(Color.Aqua);

        [Category("Led外观")]
        [Description("Led灯亮时的颜色")]
        public Color OnColor
        {
            get { return onS.Color; }
            set
            {
                if (onS.Color.Equals(value))
                {
                    return;
                }
                onS.Color = value;
                this.Invalidate();
            }
        }

        [Category("Led外观")]
        [Description("Led灯灭时的颜色")]
        public Color OffColor
        {
            get { return offS.Color; }
            set
            {
                if (offS.Color.Equals(value))
                {
                    return;
                }
                offS.Color = value;
                this.Invalidate();
            }
        }

        private bool ledSwitch = false;
        [Category("Led外观")]
        [Description("Led灯状况控制")]
        public bool LedSwitch
        {
            get { return ledSwitch; }
            set
            {
                if (ledSwitch == value)
                    return;
                ledSwitch = value;
                this.Invalidate();
            }
        }

        private void SetGraphics(Graphics g)
        {
            //设置画布的属性
            g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
            g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighSpeed;
        }


        private void Paint1()
        {
            g.FillEllipse(ledSwitch ? onS : offS, new RectangleF(0, 0, Width, Height));
            g.DrawArc(new Pen(ledSwitch ? onS : offS, 4), new RectangleF(0, 0, Width, Height), 0, 360);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            g = e.Graphics;
            SetGraphics(g);//设置画布

            g.FillEllipse(ledSwitch ? onS : offS, new RectangleF(2, 2, Width - 4, Height - 4));
            g.DrawArc(new Pen(ledSwitch ? onS : offS, 4), new RectangleF(2, 2, Width - 4, Height - 4), 0, 360);
        }
    }

2、图表

在使用.NET 6或.NET 7时添加图表时可能会找不到图表Chart控件,这时在项目上右键NuGet包,搜索WinForms.DataVisualization,点击安装,然后在项目属性->常规->管理隐式全局使用下的使用框中输入System.Windows.Forms.DataVisualization.Charting,然后点击后面的添加使用,就会变成如下图所示,这时就可以在工具箱中找到Chart控件,放置一个Chart控件在属性中选择Series设置为折线图。
在这里插入图片描述
在这里插入图片描述

三、程序设计

1、串口配置

窗口加载时设置好串口配置

private void Form1_Load(object sender, EventArgs e)
        {
            port.DataBits = 8;//设置数据位数为8位
            port.StopBits = StopBits.One;//设置停止位为1
            port.Parity = Parity.None;//设置为无校验
            port.BaudRate = 115200;//波特率设置为115200
            string[] ports = SerialPort.GetPortNames();//获取已连接的串口
            for (int i = 0; i < ports.Length; i++)
            {
                comboBox1.Items.Add(ports[i]);//添加进下拉框中
            }
            if (ports.Length > 0)
                comboBox1.SelectedIndex = 0;//选中第一个
            port.DataReceived += Port_DataReceived;//绑定接收事件
            port.ErrorReceived += Port_ErrorReceived;//绑定错误事件
        }

2、发送报文

TaskCompletionSourceSemaphoreSlim具体作用可以查看.NET API。

TaskCompletionSource<int> source = new TaskCompletionSource<int>();
        SemaphoreSlim slim = new SemaphoreSlim(1, 1);
        /// <summary>
        /// 异步发送Modbus报文
        /// </summary>
        /// <param name="dat">数据</param>
        /// <param name="len">数据个数</param>
        /// <returns>
        /// 0:成功
        /// 1:串口未打开
        /// 2:等待发送超时
        /// 3:发生了异常
        /// 4:等待接收超时
        /// </returns>
        private async Task<int> ModbusSendAsync(byte[] dat, ushort len)
        {
            if (!port.IsOpen)//先判断串口是否打开
                return 1;
            bool b = await slim.WaitAsync(500);//接下来打代码不能多线程访问
            if (b == false)
            {
                return 2;
            }
            try
            {
                port.Write(dat, 0, len);
                s_flag = true;
                source = new TaskCompletionSource<int>();
                using (var cts = new CancellationTokenSource(250))//如果发送完成250ms内没有接收到返回数据设置一个错误码
                {
                    cts.Token.Register(() =>
                    {
                        source.SetResult(4);
                    });
                    return await source.Task;
                }
            }
            catch
            {
                return 3;
            }
            finally
            {
                r_state = 0;
                s_flag = false;
                slim.Release();
            }
        }

3、CRC校验

 /// <summary>
        /// modbus crc校验
        /// </summary>
        /// <param name="dat"></param>
        /// <param name="len"></param>
        /// <returns></returns>
        private ushort ModebusCRC(byte[] dat, int len)
        {
            ushort val = 0xFFFF;
            ushort poly = 0xA001;

            for (int j = 0; j < len; j++)
            {
                val ^= dat[j];
                for (int i = 0; i < 8; i++)
                {
                    if ((val & 0x01) == 1)
                        val = (ushort)((val >> 1) ^ poly);
                    else
                        val = (ushort)(val >> 1);
                }
            }
            return val;
        }

4、读写寄存器函数

这下面两个函数就是实现0x10和0x03读写寄存器的函数,可以看到在函数内部先是根据Modbus协议的定义组装协议包然后通过之前写的ModbusSendAsync函数发送出去,这两个函数也是异步的。

/// <summary>
        /// modbus写寄存器
        /// </summary>
        /// <param name="add">从机地址</param>
        /// <param name="reg">寄存器地址</param>
        /// <param name="reg_number">寄存器数量</param>
        /// <param name="dat">寄存器数据</param>
        /// <returns>
        /// 0:成功
        /// </returns>
        private async Task<int> ModebusWriteAsync(byte add, ushort reg, ushort reg_number, byte[] dat)
        {
            s_data[0] = add;
            s_data[1] = 0x10;
            s_data[2] = (byte)((reg >> 8) & 0xff);
            s_data[3] = (byte)(reg & 0xff);
            s_data[4] = (byte)((reg_number >> 8) & 0xff);
            s_data[5] = (byte)(reg_number & 0xff);
            s_data[6] = (byte)reg_number;
            Array.Copy(dat, 0, s_data, 7, reg_number * 2);
            ushort crc = ModebusCRC(s_data, (ushort)(reg_number * 2 + 7));
            s_data[reg_number * 2 + 7] = (byte)(crc & 0xff);
            s_data[reg_number * 2 + 8] = (byte)((crc >> 8) & 0xff);
            int result = await ModbusSendAsync(s_data, (ushort)(reg_number * 2 + 9));
            return result;
        }

        /// <summary>
        /// modbus读寄存器
        /// </summary>
        /// <param name="add">从机地址</param>
        /// <param name="reg">寄存器地址</param>
        /// <param name="reg_number">寄存器数量</param>
        /// <param name="dat">读取的寄存器数据</param>
        /// <returns>
        /// 0:成功
        /// </returns>
        private async Task<int> ModbusReadAsync(byte add, ushort reg, ushort reg_number, byte[] dat)
        {
            s_data[0] = add;
            s_data[1] = 0x03;
            s_data[2] = (byte)((reg >> 8) & 0xff);
            s_data[3] = (byte)(reg & 0xff);
            s_data[4] = (byte)((reg_number >> 8) & 0xff);
            s_data[5] = (byte)(reg_number & 0xff);
            ushort crc = ModebusCRC(s_data, 6);
            s_data[6] = (byte)(crc & 0xff);
            s_data[7] = (byte)((crc >> 8) & 0xff);
            int result = await ModbusSendAsync(s_data, (ushort)(reg_number + 9));
            if (result == 0)
            {
                Array.Copy(r_data, 3, dat, 0, r_data[2]);
            }
            return result;
        }

5、LED状态切换

下面为LED切换事件,在点击界面中的LED控件后自动触发,在其中调用了ModebusWriteAsync向下位机发送了开关LED的命令,也是根据在开始的寄存器定义向下位机的0x0000寄存器写0或1控制。

/// <summary>
        /// 点击的LED灯,进行LED状态切换
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void led1_Click(object sender, EventArgs e)
        {
            if (led1.LedSwitch)
            {
                int result = await ModebusWriteAsync(0x01, 0x0000, 1, new byte[] { 0x00, 0x00 });
                if (result == 0)
                {
                    led1.LedSwitch = false;
                }
                else
                {

                }
            }
            else
            {
                int result = await ModebusWriteAsync(0x01, 0x0000, 1, new byte[] { 0x00, 0x01 });
                if (result == 0)
                {
                    led1.LedSwitch = true;
                }
                else
                {

                }
            }
        }

6、串口接收事件

在串口接收事件中根据Modbus协议内容一步步的判断是否符合条件实现状态转移。

int r_state = 0;
        int r_len = 0;
        byte[] r_data = new byte[1024];
        /// <summary>
        /// 串口接收事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            if (e.EventType == SerialData.Eof)//关闭串口
            {
                port.Close();
                this.Invoke(new Action(() =>//不能从其他线程修改本线程UI
                {
                    button1.Text = "关闭串口";
                }));
            }
            else//接收到字符
            {
                if (s_flag == false)//在未发送数据的情况下接收到数据直接丢弃
                {
                    port.DiscardInBuffer();
                    return;
                }
                while (port.BytesToRead > 0)
                {
                    byte dat = (byte)port.ReadByte();
                    switch (r_state)
                    {
                        case 0:
                            r_state = 0;
                            r_len = 0;
                            if (dat == s_data[0])//判断地址是否正确
                            {
                                r_data[0] = dat;
                                r_state = 1;
                            }
                            break;
                        case 1:
                            if (dat == s_data[1])//判断功能吗
                            {
                                r_data[1] = dat;
                                if (dat == 0x03)//读指令
                                    r_state = 2;
                                else if (dat == 0x10)
                                    r_state = 5;
                            }
                            else
                                goto case 0;
                            break;
                        case 2:
                            if (dat / 2 == s_data[4] * 256 + s_data[5])
                            {
                                r_data[2] = dat;
                                r_state = 3;
                            }
                            else
                                goto case 0;
                            break;
                        case 3:
                            r_len++;
                            r_data[2 + r_len] = dat;
                            if (r_len == r_data[2])
                            {
                                r_len = 0;
                                r_state = 4;
                            }
                            break;
                        case 4:
                            r_len++;
                            r_data[2 + r_data[2] + r_len] = dat;
                            if (r_len == 2)
                            {
                                ushort crc = ModebusCRC(r_data, 3 + r_data[2]);
                                ushort r_crc = (ushort)(r_data[3 + r_data[2]] + r_data[4 + r_data[2]] * 256);
                                if (crc == r_crc)
                                {
                                    source.SetResult(0);
                                    r_state = 0;
                                }
                                else
                                    goto case 0;
                            }
                            break;

                        case 5:
                            r_len++;
                            if (dat == s_data[1 + r_len])
                            {
                                if (r_len == 4)
                                {
                                    r_len = 0;
                                    r_state = 6;
                                }
                            }
                            else
                                goto case 0;
                            break;
                        case 6:
                            r_len++;
                            r_data[5 + r_len] = dat;
                            if (r_len == 2)
                            {
                                ushort crc = ModebusCRC(r_data, 6);
                                ushort r_crc = (ushort)(r_data[6] + r_data[7] * 256);
                                if (crc == r_crc)
                                {
                                    source.SetResult(0);
                                    r_state = 0;
                                }
                                else
                                    goto case 0;
                            }
                            break;
                    }
                }
            }
        }

7、设置电压

设置电压操作和LED状态切换相同,向0x0001寄存器写入电压值。

/// <summary>
        /// 设置下位机DAC输出电压
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void button2_Click(object sender, EventArgs e)
        {
            float dat;
            try
            {
                dat = float.Parse(textBox1.Text);
                if (dat > 3.3 || dat < 0)
                {
                    MessageBox.Show("电压值应在0V--3.3V之间");
                    return;
                }
            }
            catch
            {
                MessageBox.Show("请输入正确的数值类型");
                return;
            }
            int idat = (int)(dat * 100);
            int result = await ModebusWriteAsync(0x01, 0x0001, 1, new byte[] { (byte)(idat >> 8), (byte)idat });
            if (result == 0)
            {

            }
            else
            {
                MessageBox.Show("设置失败");
            }
        }

8、定时器

在定时器中读取电压值和按键值,并将电压值添加加进图表中。定时器周期为之前设置的500ms。

/// <summary>
        /// 定时器
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void timer1_Tick(object sender, EventArgs e)
        {
            timer1.Stop();
            if (!port.IsOpen)
                return;
            byte[] dat = new byte[4];
            int result = await ModbusReadAsync(0x01, 0x01, 2, dat);
            if (result == 0)
            {
                if (chart1.Series["电压"].Points.Count == 20)
                {
                    chart1.Series["电压"].Points.RemoveAt(0);
                }
                chart1.Series["电压"].Points.AddY(dat[0] * 256 + dat[1]);
                label3.Text = "0x" + (dat[2] * 256 + dat[3]).ToString("X4");
            }

            if (port.IsOpen)
                timer1.Start();
        }

9、其他

/// <summary>
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void comboBox1_DropDown(object sender, EventArgs e)
        {
            string[] ports = SerialPort.GetPortNames();//获取已连接的串口
            comboBox1.Items.Clear();//清除下拉框中的数据
            for (int i = 0; i < ports.Length; i++)
            {
                comboBox1.Items.Add(ports[i]);//添加进下拉框中
            }
            if (ports.Length > 0)
                comboBox1.SelectedIndex = 0;//选中第一个
        }

        /// <summary>
        /// 打开关闭串口
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            Debug.WriteLine(10.ToString("X4"));
            if (button1.Text == "打开串口")
            {
                try
                {
                    port.PortName = comboBox1.Text;//设置端口号
                    port.Open();//打开串口
                    button1.Text = "关闭串口";
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);//弹出框显示出现的错误
                }
            }
            else
            {
                if (port.IsOpen)//判断串口是否打开
                {
                    port.Close();//关闭串口
                }
                button1.Text = "打开串口";
            }
        }

四、下位机单片机程序设计

由于手里只有一块STM32F429的开发板,所以就这上面进行编写下位机程序。

1、串口配置

串口配置为115200波特率,并开启接收和发送完成中断。

void Modbus_Init()
{
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

	GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	USART_InitStructure.USART_BaudRate = 115200;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;

	USART_Init(USART1, &USART_InitStructure);
	USART_Cmd(USART1, ENABLE);
	USART_ClearFlag(USART1, USART_FLAG_TC | USART_FLAG_TXE);
	USART_ClearFlag(USART1, USART_FLAG_RXNE);

	USART_ITConfig(USART1, USART_IT_TC, ENABLE);   //使能发送完成中断
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能接收中断

	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x03;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

2、串口接收中断处理


uint8_t ModbusReceFlag = 0;

void USART1_IRQHandler(void)
{
	static uint8_t r_state = 0;
	static uint16_t r_len = 0;
	uint16_t crc;
	if (USART_GetITStatus(USART1, USART_IT_TC))
	{ //触发发送中断
		if (TX_BufNum != 0)
		{														 //发送缓冲区有未发送的数据
			USART_SendData(USART1, UART1_SendBuf[TX_TailIndex]); //根据尾指针取出待发送数据 发送
			TX_BufNum--;										 //缓冲区个数减1
			if (++TX_TailIndex >= TX_BufSize)					 //调整尾指针
				TX_TailIndex = 0;
		}
		USART_ClearITPendingBit(USART1, USART_IT_TC);
	}
	else if (USART_GetITStatus(USART1, USART_IT_RXNE))
	{
		uint8_t dat = USART1->DR;
		switch (r_state)
		{
		case 0:
		reset_rece:
			r_state = 0;
			r_len = 0;
			if (dat == 0x01)
			{
				ModbusReceBuf[0] = dat;
				r_state = 1;
			}
			break;
		case 1:
			ModbusReceBuf[1] = dat;
			if (dat == 0x03)
			{
				r_state = 2;
			}
			else if (dat == 0x10)
			{
				r_state = 3;
			}
			else
			{
				goto reset_rece;
			}
			break;
		case 2:
			r_len++;
			ModbusReceBuf[1 + r_len] = dat;
			if (r_len == 6)
			{
				crc = ModebusCRC(ModbusReceBuf, 6);
				if (crc == ModbusReceBuf[6] + ModbusReceBuf[7] * 256)
				{
					ModbusReceFlag = 1;
				}
				else
				{
					goto reset_rece;
				}
				r_state = 0;
			}
			break;

		case 3:
			r_len++;
			ModbusReceBuf[1 + r_len] = dat;
			if (r_len == 5)
			{
				r_len = 0;
				r_state = 4;
			}
			break;
		case 4:
			r_len++;
			ModbusReceBuf[6 + r_len] = dat;
			if (r_len == ModbusReceBuf[6] + 2)
			{
				crc = ModebusCRC(ModbusReceBuf, 7 + ModbusReceBuf[6]);
				if (crc == ModbusReceBuf[7 + ModbusReceBuf[6]] + ModbusReceBuf[8 + ModbusReceBuf[6]] * 256)
				{
					ModbusReceFlag = 1;
				}
				else
				{
					goto reset_rece;
				}
				r_state = 0;
			}
			break;
		}
	}
}

3、发送函数

uint16_t ModebusCRC(uint8_t *dat, int len)
{
	uint16_t val = 0xFFFF;
	uint16_t poly = 0xA001;

	for (int j = 0; j < len; j++)
	{
		val ^= dat[j];
		for (int i = 0; i < 8; i++)
		{
			if ((val & 0x01) == 1)
				val = (val >> 1) ^ poly;
			else
				val = val >> 1;
		}
	}
	return val;
}

uint8_t ModbusSendChar(char dat)
{
	uint16_t cnt = 0;
	while (TX_BufNum == TX_BufSize)
	{ //缓冲区满等待1秒钟
		if (++cnt >= 1000)
			return 1;
	}																			  //缓冲区满等待
	if ((TX_BufNum == 0) && (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == SET)) //缓冲区没有数据并且串口空闲
	{
		USART_SendData(USART1, dat); //直接发送数据
	}
	else //如果缓冲区还没有发送完存入缓冲区
	{
		UART1_SendBuf[TX_HeadIndex] = dat; //根据头指针 存入待发送数到缓冲区
		TX_BufNum++;					   //发送缓冲区加1
		if (++TX_HeadIndex >= TX_BufSize)  //头指针加1
			TX_HeadIndex = 0;			   //回到头部
	}
	return 0;
}

uint8_t ModbusSend(uint8_t *dat, uint16_t len)
{
	uint16_t crc = ModebusCRC(dat, len);
	dat[len] = crc;
	dat[len + 1] = crc >> 8;
	for (uint16_t i = 0; i < len + 2; i++)
	{
		ModbusSendChar(dat[i]);
	}
}

4、ADC、DAC初始化

ADC采集使用DMA将数据传输到一个缓存区中,之后使用时将缓存区相加取平均值。

uint16_t ADC1_BUFFER[32];

void ADC1_DMA_INIT(void)
{
	ADC_InitTypeDef ADC_InitStructure;
	ADC_CommonInitTypeDef ADC_CommonInitStructure;
	DMA_InitTypeDef DMA_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2 | RCC_AHB1Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	DMA_DeInit(DMA2_Stream4);
	DMA_InitStructure.DMA_Channel = DMA_Channel_0;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
	DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)ADC1_BUFFER;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
	DMA_InitStructure.DMA_BufferSize = 32; //设置缓冲区大小
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						//存储器自动增量模式
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //半字传输
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环模式
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
	DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
	DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
	DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
	DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
	DMA_Init(DMA2_Stream4, &DMA_InitStructure);

	DMA_Cmd(DMA2_Stream4, ENABLE);

	ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div8;
	ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
	ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_20Cycles;
	ADC_CommonInit(&ADC_CommonInitStructure);

	ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 12位AD
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;		   //使能扫描模式
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;	   //使能连续转换
	ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_NbrOfConversion = 1; //转换通道为2
	ADC_Init(ADC1, &ADC_InitStructure);

	ADC_DMACmd(ADC1, ENABLE);

	ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_480Cycles);

	ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE);

	ADC_Cmd(ADC1, ENABLE);

	ADC_SoftwareStartConv(ADC1);
}

void DAC_INIT(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	DAC_InitTypeDef DAC_InitType;

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); //使能GPIOA时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);	  //使能DAC时钟

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;   //模拟输入
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉
	GPIO_Init(GPIOA, &GPIO_InitStructure);		   //初始化

	DAC_InitType.DAC_Trigger = DAC_Trigger_None;						 //不使用触发功能 TEN1=0
	DAC_InitType.DAC_WaveGeneration = DAC_WaveGeneration_None;			 //不使用波形发生
	DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; //屏蔽、幅值设置
	DAC_InitType.DAC_OutputBuffer = DAC_OutputBuffer_Disable;			 // DAC1输出缓存关闭 BOFF1=1
	DAC_Init(DAC_Channel_1, &DAC_InitType);								 //初始化DAC通道1

	DAC_Cmd(DAC_Channel_1, ENABLE); //使能DAC通道1

	DAC_SetChannel1Data(DAC_Align_12b_R, 0); // 12位右对齐数据格式设置DAC值
}

5、Modbus寄存器功能支持

typedef struct
{
	uint16_t reg;
	uint8_t (*write)(uint16_t);
	uint16_t (*read)(void);
} ModbusReg_TypeDef;

uint8_t Modbus_Write_0x0000(uint16_t dat)
{
	if (dat == 0)
		GPIO_SetBits(GPIOB, GPIO_Pin_0);
	else
		GPIO_ResetBits(GPIOB, GPIO_Pin_0);
	return 0;
}

uint8_t Modbus_Write_0x0001(uint16_t dat)
{
	double temp = dat;
	uint16_t val;
	temp /= 100;
	temp = temp * 4096 / 3.3;
	val = (uint16_t)temp;
	if (val > 4096)
		val = 4096;
	DAC_SetChannel1Data(DAC_Align_12b_R, val); // 12位右对齐数据格式设置DAC值
}

uint16_t Modbus_Read_0x0000(void)
{
	return GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0) ? 0 : 1;
}

uint16_t Modbus_Read_0x0001(void)
{
	uint32_t dat = 0;
	for (uint16_t i = 0; i < 32; i++)
	{
		dat += ADC1_BUFFER[i];
	}

	return (uint16_t)(dat / 32.0f * (3.3f / 4096.0f) * 100);
}

uint16_t Modbus_Read_0x0002(void)
{
	return 0x5550 | GPIO_ReadInputDataBit(GPIOH, GPIO_Pin_2);
}

ModbusReg_TypeDef ModbusReg[] =
	{
		{0x0000, Modbus_Write_0x0000, Modbus_Read_0x0000},
		{0x0001, Modbus_Write_0x0001, Modbus_Read_0x0001},
		{0x0002, 0, Modbus_Read_0x0002},
		{0xFFFF, 0, 0}};

6、主函数

uint8_t time_1ms_flasg = 0;

/**
 * @brief  Main program
 * @param  None
 * @retval None
 */
int main(void)
{

	uint16_t reg;
	uint16_t reg_num;
	ModbusReg_TypeDef *reg_func;
	uint16_t len;
	uint16_t cnt_500ms = 0;
	uint16_t val = 0;
	
	GPIO_InitTypeDef GPIO_InitStructure;
	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOH, ENABLE);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
	GPIO_Init(GPIOH, &GPIO_InitStructure);
	
	RCC_GetClocksFreq(&RCC_Clocks);
	SysTick_Config(RCC_Clocks.HCLK_Frequency / 1000);
	Modbus_Init();
	DAC_INIT();
	ADC1_DMA_INIT();
	
	/* Infinite loop */
	while (1)
	{
		if (ModbusReceFlag)
		{
			ModbusReceFlag = 0;
			reg = ModbusReceBuf[2] * 256 + ModbusReceBuf[3];
			reg_num = ModbusReceBuf[4] * 256 + ModbusReceBuf[5];
			for (int i = 0;; i++)
			{
				if (reg == ModbusReg[i].reg)
				{
					reg_func = &ModbusReg[i];
					if (ModbusReceBuf[1] == 0x03)
					{
						ModbusSendBuf[0] = 0x01;
						ModbusSendBuf[1] = 0x03;
						ModbusSendBuf[2] = reg_num * 2;
						len = 3 + reg_num * 2;
						for (int j = 0; j < reg_num; j++)
						{
							if (reg_func->reg == 0xFFFF)
							{
								ModbusSendBuf[3 + j * 2] = 0;
								ModbusSendBuf[4 + j * 2] = 0;
							}
							else if (reg_func->read != 0)
							{
								val = reg_func->read();
								ModbusSendBuf[3 + j * 2] = val >> 8;
								ModbusSendBuf[4 + j * 2] = val;
							}
							else
							{
								ModbusSendBuf[3 + j * 2] = 0;
								ModbusSendBuf[4 + j * 2] = 0;
							}
							reg_func++;
						}
					}
					else if (ModbusReceBuf[1] == 0x10)
					{
						memcpy(ModbusSendBuf, ModbusReceBuf, 6);
						len = 6;
						for (int j = 0; j < reg_num; j++)
						{
							if (reg_func->reg == 0xFFFF)
							{
							}
							else if (reg_func->write != 0)
							{
								val = ModbusReceBuf[7 + j * 2] * 256 + ModbusReceBuf[8 + j * 2];
								reg_func->write(val);
							}
							reg_func++;
						}
					}
					ModbusSend(ModbusSendBuf, len);
					break;
				}
				else if (ModbusReg[i].reg == 0xFFFF)
				{
					break;
				}
			}
		}
		if (time_1ms_flasg)
		{
			time_1ms_flasg = 0;
			if (++cnt_500ms >= 500)
			{
				cnt_500ms = 0;
				GPIO_ToggleBits(GPIOB, GPIO_Pin_1);
			}
		}
	}
}

五、效果

可以看到在设置为3V时通过协议查询到的电压值同样变成了3V,在窗口中的LED为青色时开发板上的红色LED不亮,在点击窗口中的LED变为红色后开发板上的红色LED也亮了起来,在按下按键后按键值的标签同样变为了0x5550。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


  • 13
    点赞
  • 97
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值