当前位置: 首页 > news >正文

原子级操作快速自制modbus协议

原子级操作手把手搞懂modbus协议

文章目录

    • @[toc]
  • 1 modbus协议基础概念
    • 1.1 使用场所
    • 1.2 主从协议站
    • 1.3 modbus帧描述
    • 1.4 数据模式
    • 1.5 modbus状态机
  • 2 modbus协议
    • 2.1 功能码
    • 2.2 公共功能码
    • 2.3 数据域格式
  • 3 modbus从站程序设计
    • 3.1 接口初始化
    • 3.2 数据处理部分
        • 查表法设置超时时间
    • 3.2 主循环查询
    • 3.3 协议解析
      • MODS_01H
        • 向上取整
        • 按位记录数据
        • 错误消息处理
        • CRC校验
      • bsp_PutMsg 双指针环状消息队列

本文主要参考安富莱老师的modbus资料,做了一些自己的思考与感悟,可以方便新手同学的快速入门,建议看完本文后再对原文进行学习彻底搞懂modbus协议,传送门如下:安富莱modbus协议

1 modbus协议基础概念

1.1 使用场所

modbuds主要配合RS485总线使用,主要解决的是主从栈协议的数据收发问题。

通俗理解就是:RS485的数据规定比较简单,只规定了一些电气特征(主要是物理层的一些东西,如高低电平的电压值等) ,

但是对数据层的一些数据没有进行说明,这就导致了以下的一些问题:

  1. 控制器如何获取传感器的数据,怎样能在正确的时间区分不同的传感器数据
  2. 各传感器数据如何上传,只有一条总线,假如存在10个传感器,这些传感器如何上传自己的数据才不会造成线路的拥堵,如何才能区分不同传感器节点的数据

归根到底:传感器节点比较多,但总线只有一条,怎样进行传感器数据的上传才不会造成数据的冲突和总线的拥挤

为了解决这个问题,引入了主从站的概念。

1.2 主从协议站

主从协议栈的特点如下所示:

  1. 同一时刻,总线中只有一个主节点存在;
  2. 总线中最多有247个子节点(GB-TB19582-2008中规定)
  3. 通讯时总是由主节点发起,子节点不主动上传数据,子节点之间不进行互相通讯
  4. 子节点必须有唯一的地址(1-247)

主从协议栈中主要存在两种数据模式:

  • 单播模式:主节点通过特定地址访问特定子节点(发起请求),从结点收到请求后进行应答返回报文;
  • 广播模式:主节点向所有的节点发起请求,从结点无需应答;
    • 广播模式一般用于写请求中,所有子节点原则上必须接受广播模式的写功能;
    • 地址0用于广播数据,子节点地址禁止占用地址0

1.3 modbus帧描述

modbus帧
modbus帧如上所示:
首先是地址域:用于主节点请求特定的节点数据,也用于从节点应答时主节点区分不同的从节点;

功能码和数据域:实现modbus的主要功能(如对 特定节点 写入 特定数据)

CRC:校验码,查看数据是否正确的校验段

1.4 数据模式

一般modbus的数据有两种数据可选:RTU(16进制) 和 ASCII

二者的数据密度比较:

假设表示127这个数,RTU需要 0001 1111(7E)一个字节表示,ASCII需要发送1 2 7三个字节表示,所以RTU的数据密度较高,这样的话就可以节省数据发送及传输的任务量,大大减小总线的负担。

字节(注意这是每个字节的数据格式,每个bit可以代表一个电平,只有二进制的0/1)格式如下所示:
有奇偶校验
无奇偶校验
每字节bit流如上图所示,说明如下:

起始位:bit1

数据位:bit2~bit9

校验位(有的话):bit10

停止位:有校验的话bit11,没有校验的话bit10~bit11

帧数据的格式如下所示:
modbus帧数据
总线报文格式:

数据在总线上发送的时候必须以连续的数据帧格式进行发送,帧内若两个字符之间的数据时间间隔小于1.5字符时间的话。

总线帧数据
总线报文格式:

数据帧的话之间的空闲时间至少需要3.5个字符时间。

总线报文
单帧报文格式

1.5 modbus状态机

modbus协议栈状态机

1、初始态,初始化t3.5(超过3.5个字符传播的时间),如果超过t3.5的话,那就证明时间超时了,就进入空闲态;

2、空闲态:就是总线上没有数据传输的状态

3、发送态:主从栈进行数据发送的状态,发送完成启动t3.5;

4、接收态:接收时启动t1.5和t3.5,接收时会有这两种时间,但二者的话肯定时t1.5先达到,因此t1.5来临的时候,进入一种新的状态,控制和等待状态;

5、控制和等待状态:当t1.5超时之后,有两种可能,一种是这一帧数据不完整,此时的话校验位肯定不对;另一种时数据完整,校验位没问题,再等两个字符时间后到达t3.5后进入空闲状态。

2 modbus协议

2.1 功能码

首先modbus协议对功能码做了严格的定义,有一些是功能码是公共功能码,具体如下所示:
功能码定义
公共功能码是modbus定义好的功能,不能进行修改,用户自定义的功能码只能是65-72100-110的功能码段。

2.2 公共功能码

公共功能码主要功能如下所示:

可以看见数据访问有比特访问16比特访问文件记录访问,还能传文件,感觉挺有意思。

不过常用的功能码有:01 02 03 04 05 15 16
modbus公共功能码定义

2.3 数据域格式

仅对功能码01进行说明,

PS:其实个人感觉安富莱老师的资料中写的非常好了,本文中仅对01进行说明,以便本文观看的连续性,更完整的内容建议读一下原文。

01H:读取线圈的状态

主机查询报文如下所示:主机查询

从机响应的值如下所示:从机响应
对应的线圈状态如下所示:线圈状态

3 modbus从站程序设计

程序设计的流程图如下所示,下面的程序设计也按照下面的流程设计:
modbus从站程序

3.1 接口初始化

首先是程序接口使用的是RS485,因此需要的是初始化485的接口,用于接受和发送数据:

这部分代码的优势并不是很明显,个人只是感觉这一段的串口结构体写的非常好,可以看一下,具体使用的时候可以根据功能的收发进行裁剪。

先进行Bsp_uart_fifo.h重要代码说明

/*  串口485的配置 *//* RS485芯片发送使能GPIO, PB2 */
#define RCC_RS485_TXEN 	RCC_AHB1Periph_GPIOB
#define PORT_RS485_TXEN  GPIOB
#define PIN_RS485_TXEN	 GPIO_Pin_2#define RS485_RX_EN()	PORT_RS485_TXEN->BSRRH = PIN_RS485_TXEN
#define RS485_TX_EN()	PORT_RS485_TXEN->BSRRL = PIN_RS485_TXEN/* 串口3的基本参数 */
#if UART3_FIFO_EN == 1#define UART3_BAUD			9600#define UART3_TX_BUF_SIZE	1*1024#define UART3_RX_BUF_SIZE	1*1024
#endif/* 串口设备结构体,个人感觉安富莱这个结构体做的挺好的,所以在此提一下 */
typedef struct
{USART_TypeDef *uart;		/* STM32内部串口设备指针 */uint8_t *pTxBuf;			/* 发送缓冲区 */uint8_t * ;			/* 接收缓冲区 */uint16_t usTxBufSize;		/* 发送缓冲区大小 */uint16_t usRxBufSize;		/* 接收缓冲区大小 */__IO uint16_t usTxWrite;	/* 发送缓冲区写指针 */__IO uint16_t usTxRead;		/* 发送缓冲区读指针 */__IO uint16_t usTxCount;	/* 等待发送的数据个数 */__IO uint16_t usRxWrite;	/* 接收缓冲区写指针 */__IO uint16_t usRxRead;		/* 接收缓冲区读指针 */__IO uint16_t usRxCount;	/* 还未读取的新数据个数 */void (*SendBefor)(void); 	/* 开始发送之前的回调函数指针(主要用于RS485切换到发送模式) */void (*SendOver)(void); 	/* 发送完毕的回调函数指针(主要用于RS485将发送模式切换为接收模式) */void (*ReciveNew)(uint8_t _byte);	/* 串口收到数据的回调函数指针 */
}UART_T;

Bsp_uart_fifo.c重要代码说明:

  1. 先进行发送和缓存区的全局变量声明

  2. 发送指针和接收指针使用的是相对位置,是数组的索引值,所以写入的时候的时候采用的是以下的方法

    ch = USART_ReceiveData(_pUart->uart);
    _pUart->pRxBuf[_pUart->usRxWrite] = ch;
    if (++_pUart->usRxWrite >= _pUart->usRxBufSize)
    {_pUart->usRxWrite = 0;
    }
    if (_pUart->usRxCount < _pUart->usRxBufSize)
    {_pUart->usRxCount++;
    }
    
  • 初始化代码,隐藏了硬件初始化,中断配置的代码,也没啥好说的,值得注意一点的是全局变量的初始化部分的代码吧
/*  先定义全局变量进行缓存区的定义 */
#if UART3_FIFO_EN == 1static UART_T g_tUart3;static uint8_t g_TxBuf3[UART3_TX_BUF_SIZE];		/* 发送缓冲区 */static uint8_t g_RxBuf3[UART3_RX_BUF_SIZE];		/* 接收缓冲区 */
#endifvoid bsp_InitUart(void)
{UartVarInit();		/* 必须先初始化全局变量,再配置硬件 */InitHardUart();		/* 配置串口的硬件参数(波特率等) */RS485_InitTXE();	/* 配置RS485芯片的发送使能硬件,配置为推挽输出 */ConfigUartNVIC();	/* 配置串口中断 */
}static void UartVarInit(void)
{
#if UART3_FIFO_EN == 1g_tUart3.uart = USART3;						/* STM32 串口设备 */g_tUart3.pTxBuf = g_TxBuf3;					/* 发送缓冲区指针 */g_tUart3.pRxBuf = g_RxBuf3;					/* 接收缓冲区指针 */g_tUart3.usTxBufSize = UART3_TX_BUF_SIZE;	/* 发送缓冲区大小 */g_tUart3.usRxBufSize = UART3_RX_BUF_SIZE;	/* 接收缓冲区大小 */g_tUart3.usTxWrite = 0;						/* 发送FIFO写索引 */g_tUart3.usTxRead = 0;						/* 发送FIFO读索引 */g_tUart3.usRxWrite = 0;						/* 接收FIFO写索引 */g_tUart3.usRxRead = 0;						/* 接收FIFO读索引 */g_tUart3.usRxCount = 0;						/* 接收到的新数据个数 */g_tUart3.usTxCount = 0;						/* 待发送的数据个数 */g_tUart3.SendBefor = RS485_SendBefor;		/* RS485发送数据前的回调函数 */g_tUart3.SendOver = RS485_SendOver;			/* RS485发送完毕后的回调函数 */g_tUart3.ReciveNew = RS485_ReciveNew;		/* RS485接收到新数据后的回调函数 */
#endif
}void RS485_SendBefor(void)
{RS485_TX_EN();	/* 切换RS485收发芯片为发送模式 */
}void RS485_SendOver(void)
{RS485_RX_EN();	/* 切换RS485收发芯片为接收模式 */
}void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen)
{comSendBuf(COM3, _ucaBuf, _usLen);
}
/* 注意硬件配置的时候每个字符是11位的,配置不要出错 */
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_2;
USART_InitStructure.USART_Parity = USART_Parity_No ;
  • 重要的是接受的过程中写的过程

    1. 先写入数据
    2. 若数组的索引出现越界的情况记得要将索引清零
/*接收中断中调用RS485_ReciveNew*/
/*
*********************************************************************************************************
*	函 数 名: UartIRQ
*	功能说明: 供中断服务程序调用,通用串口中断处理函数
*	形    参: _pUart : 串口设备
*	返 回 值: 无
*********************************************************************************************************
*/
static void UartIRQ(UART_T *_pUart)
{/* 处理接收中断  */if (USART_GetITStatus(_pUart->uart, USART_IT_RXNE) != RESET){/* 从串口接收数据寄存器读取数据存放到接收FIFO */uint8_t ch;ch = USART_ReceiveData(_pUart->uart);_pUart->pRxBuf[_pUart->usRxWrite] = ch;if (++_pUart->usRxWrite >= _pUart->usRxBufSize){_pUart->usRxWrite = 0;}if (_pUart->usRxCount < _pUart->usRxBufSize){_pUart->usRxCount++;}/* 回调函数,通知应用程序收到新数据,一般是发送1个消息或者设置一个标记 *///if (_pUart->usRxWrite == _pUart->usRxRead)//if (_pUart->usRxCount == 1){if (_pUart->ReciveNew){_pUart->ReciveNew(ch);}}}
}

3.2 数据处理部分

查表法设置超时时间

首先设置表格,如下所示:

/*
Baud rate	Bit rate	 Bit time	 Character time	  3.5 character times2400	    2400 bits/s	  417 us	      4.6 ms	      16 ms4800	    4800 bits/s	  208 us	      2.3 ms	      8.0 ms9600	    9600 bits/s	  104 us	      1.2 ms	      4.0 ms19200	   19200 bits/s    52 us	      573 us	      2.0 ms38400	   38400 bits/s	   26 us	      286 us	      1.75 ms(1.0 ms)115200	   115200 bit/s	  8.7 us	       95 us	      1.75 ms(0.33 ms) 后面固定都为1750us
*/typedef struct
{uint32_t Bps;uint32_t usTimeOut;
}MODBUSBPS_T;const MODBUSBPS_T ModbusBaudRate[] =
{	{2400,	16000}, /* 波特率2400bps, 3.5字符延迟时间16000us */{4800,	 8000}, {9600,	 4000},{19200,	 2000},{38400,	 1750},{115200, 1750},{128000, 1750},{230400, 1750},
};

然后使用查表法获取超时t3.5时间

/* 根据波特率,获取需要延迟的时间 */
for(i = 0; i < (sizeof(ModbusBaudRate)/sizeof(ModbusBaudRate[0])); i++)
{if(SBAUD485 == ModbusBaudRate[i].Bps){break;}	
}

下面是完整的数据接收函数:

然后函数应该比较易懂,就是接收到消息之后,开启一次t3.5定时,然后将数据添加到RxBuf中。

若t3.5超时的话,可以设置一个标志位或者信号量,然后通知其他线程一帧的数据已经接收完毕:

static void MODS_RxTimeOut(void)
{g_mods_timeout = 1;
}
void MODS_ReciveNew(uint8_t _byte)
{/*3.5个字符的时间间隔,只是用在RTU模式下面,因为RTU模式没有开始符和结束符,两个数据包之间只能靠时间间隔来区分,Modbus定义在不同的波特率下,间隔时间是不一样的,详情看此C文件开头*/uint8_t i;/* 根据波特率,获取需要延迟的时间 */for(i = 0; i < (sizeof(ModbusBaudRate)/sizeof(ModbusBaudRate[0])); i++){if(SBAUD485 == ModbusBaudRate[i].Bps){break;}	}g_mods_timeout = 0;/* 硬件定时中断,定时精度us 硬件定时器1用于MODBUS从机, 定时器2用于MODBUS主机,如果超时的话会调用回调函数MODS_RxTimeOut*/bsp_StartHardTimer(1, ModbusBaudRate[i].usTimeOut, (void *)MODS_RxTimeOut);/* 将数据加入到RxBuf中 */if (g_tModS.RxCount < S_RX_BUF_SIZE){g_tModS.RxBuf[g_tModS.RxCount++] = _byte;}
}

3.2 主循环查询

主循环通过bsp_Idle查询t3.5的标志位(g_mods_timeout)是否超时,如果超时的话,证明一帧数据已经发送完成。

main.c中的程序:

int main()
{...while(1){...bsp_Idle(); /* Modbus解析在此函数里面 */...}
}

modbus_slave.cMODS_Poll函数,主要好的点有以下的点:

  1. 不对的命令直接return进行函数的结束

  2. 巧用goto,如果接收错误的话直接通过指针进行恢复就行了

    void MODS_Poll(void)
    {uint16_t addr;uint16_t crc1;/* 超过3.5个字符时间后执行MODH_RxTimeOut()函数。全局变量 g_rtu_timeout = 1; 通知主程序开始解码 */if (g_mods_timeout == 0)	{return;								/* 没有超时,继续接收。不要清零 g_tModS.RxCount */}g_mods_timeout = 0;	 					/* 清标志 */if (g_tModS.RxCount < 4)				/* 接收到的数据小于4个字节就认为错误,地址(8bit)+指令(8bit)+操作寄存器(16bit) */{goto err_ret;}/* 计算CRC校验和,这里是将接收到的数据包含CRC16值一起做CRC16,结果是0,表示正确接收 */crc1 = CRC16_Modbus(g_tModS.RxBuf, g_tModS.RxCount);if (crc1 != 0){goto err_ret;}/* 站地址 (1字节) */addr = g_tModS.RxBuf[0];				/* 第1字节 站号 */if (addr != SADDR485)		 			/* 判断主机发送的命令地址(SADDR485)是否符合 */{goto err_ret;}/* 分析应用层协议 */MODS_AnalyzeApp();						err_ret:g_tModS.RxCount = 0;					/* 必须清零计数器,方便下次帧同步 */
    }
    

3.3 协议解析

承接前面的解析函数,进行数据分析;

可以看见MODS_AnalyzeApp中对于根据地址找到的相应的消息处理之后主要是两个函数:

static void MODS_AnalyzeApp(void)
{switch (g_tModS.RxBuf[1])				/* 第2个字节 功能码 */{case 0x01:							/* 读取线圈状态(此例程用led代替)*/MODS_01H();bsp_PutMsg(MSG_MODS_01H, 0);	/* 发送消息,主程序处理 */break;case 0x02:							/* 读取输入状态(按键状态)*/...case 0x03:							/* 读取保持寄存器(此例程存在g_tVar中)*/...		case 0x04:							/* 读取输入寄存器(ADC的值)*/...		case 0x05:							/* 强制单线圈(设置led)*/...		case 0x06:							/* 写单个保存寄存器*/ ...			case 0x10:							/* 写多个保存寄存器*/      ...		default:...}
}

MODS_01H

向上取整

numbit需要多少个字节来储存数据,感觉这个方法很巧妙

m = (num + 7) / 8;

按位记录数据

for (i = 0; i < num; i++)
{if (bsp_IsLedOn(i + 1 + reg - REG_D01))		/* 读LED的状态,写入状态寄存器的每一位 */{  status[i / 8] |= (1 << (i % 8));}
}

错误消息处理

static void MODS_SendAckErr(uint8_t _ucErrCode)
{uint8_t txbuf[3];txbuf[0] = g_tModS.RxBuf[0];					/* 485地址 */txbuf[1] = g_tModS.RxBuf[1] | 0x80;				/* 异常的功能码,最高位置1 */txbuf[2] = _ucErrCode;							/* 错误代码(01,02,03,04) */MODS_SendWithCRC(txbuf, 3);
}

CRC校验

这个也是根据查表法获取的CRC校验码,网上资源较多,不进行展示了。

static void MODS_01H(void)
{/*举例:主机发送:11 从机地址01 功能码00 寄存器起始地址高字节13 寄存器起始地址低字节00 寄存器数量高字节25 寄存器数量低字节0E CRC校验高字节84 CRC校验低字节从机应答: 	1代表ON,0代表OFF。若返回的线圈数不为8的倍数,则在最后数据字节未尾使用0代替. BIT0对应第1个11 从机地址01 功能码05 返回字节数CD 数据1(线圈0013H-线圈001AH)6B 数据2(线圈001BH-线圈0022H)B2 数据3(线圈0023H-线圈002AH)0E 数据4(线圈0032H-线圈002BH)1B 数据5(线圈0037H-线圈0033H)45 CRC校验高字节E6 CRC校验低字节例子:01 01 10 01 00 03   29 0B	--- 查询D01开始的3个继电器状态01 01 10 03 00 01   09 0A   --- 查询D03继电器的状态*/uint16_t reg;uint16_t num;uint16_t i;uint16_t m;uint8_t status[10];g_tModS.RspCode = RSP_OK;/* 没有外部继电器,直接应答错误 */if (g_tModS.RxCount != 8){g_tModS.RspCode = RSP_ERR_VALUE;				/* 数据值域错误 */return;}reg = BEBufToUint16(&g_tModS.RxBuf[2]); 			/* 寄存器号 */num = BEBufToUint16(&g_tModS.RxBuf[4]);				/* 寄存器个数 */m = (num + 7) / 8;if ((reg >= REG_D01) && (num > 0) && (reg + num <= REG_DXX + 1)){for (i = 0; i < m; i++){status[i] = 0;}for (i = 0; i < num; i++){if (bsp_IsLedOn(i + 1 + reg - REG_D01))		/* 读LED的状态,写入状态寄存器的每一位 */{  status[i / 8] |= (1 << (i % 8));}}}else{g_tModS.RspCode = RSP_ERR_REG_ADDR;				/* 寄存器地址错误 */}if (g_tModS.RspCode == RSP_OK)						/* 正确应答 */{g_tModS.TxCount = 0;g_tModS.TxBuf[g_tModS.TxCount++] = g_tModS.RxBuf[0];g_tModS.TxBuf[g_tModS.TxCount++] = g_tModS.RxBuf[1];g_tModS.TxBuf[g_tModS.TxCount++] = m;			/* 返回字节数 */for (i = 0; i < m; i++){g_tModS.TxBuf[g_tModS.TxCount++] = status[i];	/* 继电器状态 */}MODS_SendWithCRC(g_tModS.TxBuf, g_tModS.TxCount);}else{MODS_SendAckErr(g_tModS.RspCode);				/* 告诉主机命令错误 */}
}

bsp_PutMsg 双指针环状消息队列

这个函数本身在本项目中作用不大,仅仅是记录一下接收到的消息ID,但是本节所涉及到的双指针环状消息队列的设计和使用比较有意思,展示如下:

先是bsp.h中定义一些基本的量

#define MSG_FIFO_SIZE    40	   		/* 消息个数 */enum 
{MSG_NONE = 0,MSG_MODS_01H,MSG_MODS_02H,MSG_MODS_03H,MSG_MODS_04H,MSG_MODS_05H,MSG_MODS_06H,MSG_MODS_10H,
};/* 按键FIFO用到变量 */
typedef struct
{uint16_t MsgCode;		/* 消息代码 */uint32_t MsgParam;		/* 消息的数据体, 也可以是指针(强制转化) */
}MSG_T;/* 变量 */
typedef struct
{MSG_T Buf[MSG_FIFO_SIZE];	/* 消息缓冲区 */uint8_t Read;					/* 缓冲区读指针1 */uint8_t Write;					/* 缓冲区写指针,是个数buf的个数 */uint8_t Read2;					/* 缓冲区读指针2 */
}MSG_FIFO_T;

然后bsp.c中定义一些常用的操作:

  • 感觉下边的写入挺有意思的,可以参考:
g_tMsg.Buf[g_tMsg.Write].MsgCode = _MsgCode;
  • 读取的时候采用的指针,一定要先初始化,然后使用,避免野指针的产生

    MSG_T *p;
    ...
    p = &g_tMsg.Buf[g_tMsg.Read];
    ..
    p = &g_tMsg.Buf[g_tMsg.Read];if (++g_tMsg.Read >= MSG_FIFO_SIZE)
    {g_tMsg.Read = 0;
    }
/*
*	功能说明: 将1个消息压入消息FIFO缓冲区。
*/
void bsp_PutMsg(uint16_t _MsgCode, uint32_t _MsgParam)
{g_tMsg.Buf[g_tMsg.Write].MsgCode = _MsgCode;   //压栈进来的结构体消息代码g_tMsg.Buf[g_tMsg.Write].MsgParam = _MsgParam;if (++g_tMsg.Write  >= MSG_FIFO_SIZE){g_tMsg.Write = 0;}
}/*
*	功能说明: 从消息FIFO缓冲区读取一个键值。
*/
uint8_t bsp_GetMsg(MSG_T *_pMsg)
{MSG_T *p;//注意只有等于符号,没有大小的关系if (g_tMsg.Read == g_tMsg.Write){return 0;}else{p = &g_tMsg.Buf[g_tMsg.Read];if (++g_tMsg.Read >= MSG_FIFO_SIZE){g_tMsg.Read = 0;}_pMsg->MsgCode = p->MsgCode;_pMsg->MsgParam = p->MsgParam;return 1;}
}/*
*	功能说明: 从消息FIFO缓冲区读取一个键值。使用第2个读指针。可以2个进程同时访问消息区。
*/
uint8_t bsp_GetMsg2(MSG_T *_pMsg)
{MSG_T *p;if (g_tMsg.Read2 == g_tMsg.Write){return 0;}else{p = &g_tMsg.Buf[g_tMsg.Read2];if (++g_tMsg.Read2 >= MSG_FIFO_SIZE){g_tMsg.Read2 = 0;}_pMsg->MsgCode = p->MsgCode;_pMsg->MsgParam = p->MsgParam;return 1;}
}/*
*	功能说明: 清空消息FIFO缓冲区
*/
void bsp_ClearMsg(void)
{g_tMsg.Read = g_tMsg.Write;
}

相关文章:

原子级操作快速自制modbus协议

原子级操作手把手搞懂modbus协议文章目录[toc]1 modbus协议基础概念1.1 使用场所1.2 主从协议站1.3 modbus帧描述1.4 数据模式1.5 modbus状态机2 modbus协议2.1 功能码2.2 公共功能码2.3 数据域格式3 modbus从站程序设计3.1 接口初始化3.2 数据处理部分查表法设置超时时间3.2 主…...

大数据之Apache Doris_亚秒级响应_大数据处理分析_介绍_概述---大数据之Apache Doris工作笔记0001

可以看到这个Doris的介绍 MPP是大规模并行处理 这里的clickhouse ,greenplumn也是mpp,大规模并行处理数据库 应用场景 然后我们看一下doris的架构,可以看到,这里,左侧是数据来源,可以看到这个数据来源有 OLTP数据库,比如mysql,oracle等等这种数据库,还有就是enterprise appli…...

SpringCloud学习笔记 - 分布式系统全局事务管理 - Seata1.5.2+Nacos+OpenFeign

1. Seata 是什么? 由于业务和技术的发展&#xff0c;单体应用被拆分成微服务应用&#xff0c;原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源&#xff0c;业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证&#xff0c; 但是全…...

LeetCode190_190. 颠倒二进制位

LeetCode190_190. 颠倒二进制位 一、描述 颠倒给定的 32 位无符号整数的二进制位。 提示&#xff1a; 请注意&#xff0c;在某些语言&#xff08;如 Java&#xff09;中&#xff0c;没有无符号整数类型。在这种情况下&#xff0c;输入和输出都将被指定为有符号整数类型&…...

atomic 原子操作

atomic 原子操作前言atomic_t定义内核中的实现armv7的实现armv8的实现Exclusive monitor实现所处的位置External exclusive monitorAtomic指令的支持QA前言 修改一个变量会经过读、修改、写的操作序列。但有时该操作序列在执行完毕前会被其他任务或事件打断。 比如在多CPU体系…...

DataGear 制作基于Vue前端框架渲染的数据可视化看板

DataGear 在4.3.0版本新增了dg-dashboard-code特性&#xff0c;并在4.4.0版本进行了改进和增强&#xff0c;结合看板API&#xff0c;可以很方便地制作完全由Vue、React等前端框架渲染的数据可视化看板。 本文基于Vue2、Element UI前端框架的<el-container>、<el-head…...

JavaFX Scene Builder 下载安装

JavaFX Scene Builder 下载安装1. Scene Builder官网下载2. 安装3. Oracle官网下载Scene Builder 是创建漂亮的用户界面&#xff0c;并将您的设计转化为交互式原型。Scene Builder通过创建可以直接在JavaFX应用程序中使用的用户界面&#xff0c;缩小了设计人员和开发人员之间的…...

dva( 轻量级的应用框架 )

dva核心知识与实战运用 dva 首先是一个基于 redux 和 redux-saga 的数据流方案&#xff0c;然后为了简化开发体验&#xff0c;dva 还额外内置了 react-router 和 fetch&#xff0c;所以也可以理解为一个轻量级的应用框架! 介绍 | DvaJS 易学易用&#xff0c;仅有 6 个 api&…...

数据结构:堆的实现与建堆时间复杂度分析

目录 前言 一.堆的介绍 1.堆的本质 2.堆的分类 二.堆的实现(以小根堆为例) 1.关于二叉树的两组重要结论&#xff1a; 2.堆的物理存储结构框架(动态数组的简单构建) 3. 堆元素插入接口(以小根堆为例) 堆尾元素向上调整的算法接口: 4.堆元素插入接口测试 5.堆元素插入…...

对“车辆销售配置器”的认识与理解

概述 中国汽车市场转为存量阶段后&#xff0c;各车企开始从”以产品为中心“转型到”以客户为中心“&#xff0c;产品的个性化配置需求日益丰富。随着竞争的加剧&#xff0c;车企们不仅要提供出色的产品&#xff0c;而且需要提供更加个性化的产品配置和服务&#xff0c;例如&am…...

Linux编译器——gcc/g++(预处理、编译、汇编、链接)

目录 0.程序实现的两大环境 1.gcc如何完成 预处理 编译 汇编 链接 2.动态库与静态库 对比二者生成的文件大小 3. gcc常用选项 0.程序实现的两大环境 任何一个C程序的实现都要经过翻译环境与执行环境。 在翻译环境中又分为4个部分&#xff0c;预编译、编译、汇编与链…...

Java 操作图片进行缩放旋转翻转加水印

1 纯原生手写图片操作工具类 import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.image.BufferedImage; public class RotateImageUtil {public static BufferedImage rotateImage(BufferedImage bu…...

不能去演唱会现场就多听听耳机里的他们,教你用python来实现一个音乐播放器

前言 最近可以说大麦网很知名了&#xff0c;哈哈还有好多想要用Python来搞抢票脚本的 怎么说呢也不是不行&#xff0c;但是咱今天可不是来搞这个的&#xff0c;我可不抢票&#xff0c;抢了都去不了&#xff0c;上班搞钱啊铁铁们 咱就是说去不了现场&#xff0c;就多听听手机…...

CLion Debug 调试 Makefile 构建的 C 语言程序断点不起作用

最近在研究 jattach&#xff0c;打算在本地调试项目&#xff0c;发现 CLion 可以正常编译运行代码&#xff0c;却无法断点 Debug。由于笔者对 C/C 项目不熟悉&#xff0c;在此记录研究过程中遇到的一些基本问题与解决方法。 文章目录解决方式尝试过的手段【未解决】找 Native D…...

·神经网络

目录11神经网络demo112神经网络demo213神经网络demo320tensorflow2.0 安装教程,所有安装工具&#xff08;神经网络&#xff09;21神经网络-线性回归- demo122神经网络-线性回归- demo228神经网络-多层感知- demo1目录11神经网络demo1 package com.example.xxx; import java.ut…...

【Java 多线程学习】

多线程学习多线程1. 并行与并发2.进程和线程3. *****多线程的实现方式3.1 继承Thread类的方式进行实现3.2 实现Runnable接口方式进行实现3.3 利用Callable和Future接口方式实现3.4 设置获取线程名字4.获得线程对象5.线程休眠6.线程调度[线程的优先级]7.后台线程/守护线程多线程…...

【计算机考研408】快速排序的趟数问题 + PAT 甲级 7-2 The Second Run of Quicksort

前言 该题还未加入PAT甲级题库中&#xff0c;可以通过购买2022年秋季甲级考试进行答题&#xff0c;纯考研题改编 快速排序 常考的知识点 快速排序是基于分治法快速排序是所有内部排序算法中平均性能最优的排序算法快速排序是一种不稳定的排序算法快速排序算法中&#xff0c…...

CSS-Grid(网格)布局

前言 之前HTML 页面的布局基本上都是通过 Flexbox 来实现的&#xff0c;能轻松的解决复杂的 Web 布局。 现在又出现了一个构建 HTML 最佳布局体系的新竞争者。就是强大的CSS Grid 布局。 grid和flex区别是什么&#xff1f;适用什么场景&#xff1f; Flexbox 是一维布局系统&am…...

软件测试4

一 form表单标签 1.form表单标签里面就是所有用户填写的表单数据&#xff1b; action“xxx.py”把表单数据提交给哪一个后台程序去处理 method“post” 传递数据时候的方式方法&#xff0c;post代表隐式提交数据、get明文传送数据 2.input标签的type类型 type“text” 普通的输…...

996的压力下,程序员还有时间做副业吗?

996怎么搞副业&#xff1f; 这个问题其实蛮奇怪的&#xff1a;996的压力下&#xff0c;怎么会还想着搞副业呢&#xff1f; 996还想搞副业的原因有哪些&#xff1f; 大家对于996应该都不陌生&#xff0c;总结就是一个字&#xff1a;忙。 996的工作性质就是加班&#xff0c;就…...

每日学术速递3.1

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.Directed Diffusion: Direct Control of Object Placement through Attention Guidance 标题&#xff1a;定向扩散&#xff1a;通过注意力引导直接控制物体放置 作者&#xff1a;…...

金融行业数据模型

一、Teradata FS-LDM Teradata 公司基于金融业务发布的FS-LDM&#xff08;Financial Servies Logical Data Model&#xff09; 十大主题&#xff1a;当事人、产品、协议、事件、资产、财务、机构、地域、营销、渠道。 1、当事人&#xff08;Party&#xff09; 银行所服务的任…...

【面试题】2023前端vue面试题及答案

Vue3.0 为什么要用 proxy&#xff1f;在 Vue2 中&#xff0c; 0bject.defineProperty 会改变原始数据&#xff0c;而 Proxy 是创建对象的虚拟表示&#xff0c;并提供 set 、get 和 deleteProperty 等处理器&#xff0c;这些处理器可在访问或修改原始对象上的属性时进行拦截&…...

(哈希查找)leetcode128. 最长连续序列

文章目录一、题目1、题目描述2、基础框架3、原题链接二、解题报告1、思路分析2、时间复杂度3、代码详解三、本题小知识一、题目 1、题目描述 给定一个未排序的整数数组 nums &#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。…...

js中splice方法和slice方法

splice方法用来操作数组splice(startIndex,deleteNum,item1,....,)此操作会改变原数组。删除数组中元素参数解释&#xff1a;startIndex为起始index索引。deleteNum为从startIndex索引位置开始需要删除的个数。分三种情况&#xff1a;没有传第三个参数的情况下&#xff0c;dele…...

c++ argparse

需求 c程序传参数&#xff0c;像python中argparse一样方便。 方法1 用gflags 参考https://heroacool.blog.csdn.net/?typeblog git clone https://github.com/gflags/gflags cd gflags # 进入项目文件夹 cmake . # 使用 cmake 编译生成 Makefile 文件 make -j 24 # make 编…...

内大892复试真题16年

内大892复试真题16年 1. 输出三个数中较大数2. 求两个数最大公约数与最小公倍数3. 统计字符串中得字符个数4. 输出菱形5. 迭代法求平方根6. 处理字符串(逆序、进制转换)7. 寻找中位数8. 输入十进制输出n进制1. 输出三个数中较大数 问题 代码 #include <iostream>usin…...

面试题 05.02. 二进制数转字符串

二进制数转字符串。给定一个介于0和1之间的实数&#xff08;如0.72&#xff09;&#xff0c;类型为double&#xff0c;打印它的二进制表达式。如果该数字无法精确地用32位以内的二进制表示&#xff0c;则打印“ERROR”。 示例1: 输入&#xff1a;0.625输出&#xff1a;"0…...

MySQL数据更新操作

文章目录前言添加数据插入数据删除数据修改数据前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 数据更新有两种办法&#xff1a; 1&#xff1a;使用数据可视化工具操作 2&#xff1a;SQL语句 添加数据 前面的添加数据命令一次只能插入一条记录。如果想…...

C# 封装

修正bug之前总是要考虑是什么导致了这个bug&#xff0c;并花些时间了解发生了什么。增加打印输出行的语句可能是一个很有效的调试工具。增加语句来打印诊断信息时&#xff0c;要使用Debug.WriteLine。构造器是CLR第一次创建一个新对象实例时调用的方法。字符串插值会让字符串拼…...

c2c网站的类型/百度推广官方网站

skyline是一款不错的三维编辑浏览软件&#xff0c;官方提供的是英文版&#xff0c;目前还没有汉化包&#xff0c;为了使用方便&#xff0c;我们需要汉化一些简单的对话框&#xff0c;本文介绍如何汉化skyline右键菜单。 首先打开skyline的安装目录&#xff0c;在TerraExplorer …...

网站备案 信息查询/seo优化点击软件

Mysql将查询结果集转换为JSON数据前言学生表学生成绩表查询单个学生各科成绩&#xff08;转换为对象JSON串并用逗号拼接&#xff09;将单个学生各科成绩转换为数组JSON串将数组串作为value并设置key两张表联合查询(最终SQL&#xff0c;每个学生各科成绩)最终结果前言 我们经常会…...

网站建设合同中英文/自己有货源怎么找客户

在CSS中background: -moz-linear-gradient 让网站背景渐变的属性&#xff0c;目前火狐3.6以上版本和google浏览器支持这个属性。 background: -moz-linear-gradient(top, #bccfe3 0%, #d2dded 100%); 适合 FF3.6 background: -webkit- gradient(linear, left top, left bott…...

运城网站建设公司有多少/搜索引擎营销案例分析题

Python是一种面向对象的解释型计算机程序设计语言&#xff0c;由荷兰人Guido van Rossum于1989年发明&#xff0c;第一个公开发行版发行于1991年。Python的特点&#xff1a;优雅、明确、简单。Python适合的领域&#xff1a;Web网站和各种网络服务、系统工具和脚本、作为“胶水”…...

门户网站样式/seo任务平台

1.题目描述&#xff1a; 2.算法分析&#xff1a; 首先肯定是定义一个double类型数组存放数据&#xff0c;然后的一个问题是怎么判断浮点数最近的整数的差&#xff0c; 使用round函数即可 floor : 不大于自变量的最大整数 ceil :不小于自变量的最大整数 round:四舍五入到最邻近…...

成都住房和城乡建设局网站/百度统计数据分析

ecshop2.7的版权耍了一下花样&#xff0c;不能按以前的方式清除了&#xff0c;不过只要是源码就有可以清除的办法。其实也很简单&#xff0c;方法如下&#xff1a;themes\default\library\page_footer.lbi 文件(default是你的模板文件夹)&#xff0c;找到下面一行 {foreach f…...