文章目录
- @[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 双指针环状消息队列
文章目录
- @[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的数据规定比较简单,只规定了一些电气特征(主要是物理层的一些东西,如高低电平的电压值等) ,
但是对数据层的一些数据没有进行说明,这就导致了以下的一些问题:
- 控制器如何获取传感器的数据,怎样能在正确的时间区分不同的传感器数据
- 各传感器数据如何上传,只有一条总线,假如存在10个传感器,这些传感器如何上传自己的数据才不会造成线路的拥堵,如何才能区分不同传感器节点的数据
归根到底:传感器节点比较多,但总线只有一条,怎样进行传感器数据的上传才不会造成数据的冲突和总线的拥挤
为了解决这个问题,引入了主从站的概念。
1.2 主从协议站
主从协议栈的特点如下所示:
- 同一时刻,总线中只有一个主节点存在;
- 总线中最多有247个子节点(GB-TB19582-2008中规定)
- 通讯时总是由主节点发起,子节点不主动上传数据,子节点之间不进行互相通讯
- 子节点必须有唯一的地址(1-247)
主从协议栈中主要存在两种数据模式:
- 单播模式:主节点通过特定地址访问特定子节点(发起请求),从结点收到请求后进行应答返回报文;
- 广播模式:主节点向所有的节点发起请求,从结点无需应答;
- 广播模式一般用于写请求中,所有子节点原则上必须接受广播模式的写功能;
- 地址0用于广播数据,子节点地址禁止占用地址0
1.3 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
帧数据的格式如下所示:
总线报文格式:
数据在总线上发送的时候必须以连续的数据帧格式进行发送,帧内若两个字符之间的数据时间间隔小于1.5字符时间的话。
总线
报文格式:
数据帧的话之间的空闲时间至少需要3.5个字符时间。
1.5 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-72
和100-110
的功能码段。
2.2 公共功能码
公共功能码主要功能如下所示:
可以看见数据访问有
比特访问
,16比特访问
和文件记录访问
,还能传文件,感觉挺有意思。不过常用的功能码有:
01
02
03
04
05
15
16
2.3 数据域格式
仅对功能码01进行说明,
PS:其实个人感觉安富莱老师的资料中写的非常好了,本文中仅对01进行说明,以便本文观看的连续性,更完整的内容建议读一下原文。
01H:读取线圈的状态
主机查询报文如下所示:
从机响应的值如下所示:
对应的线圈状态如下所示:
3 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
重要代码说明:
先进行发送和缓存区的全局变量声明
发送指针和接收指针使用的是相对位置,是数组的索引值,所以写入的时候的时候采用的是以下的方法
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 == 1
static UART_T g_tUart3;
static uint8_t g_TxBuf3[UART3_TX_BUF_SIZE]; /* 发送缓冲区 */
static uint8_t g_RxBuf3[UART3_RX_BUF_SIZE]; /* 接收缓冲区 */
#endif
void bsp_InitUart(void)
{
UartVarInit(); /* 必须先初始化全局变量,再配置硬件 */
InitHardUart(); /* 配置串口的硬件参数(波特率等) */
RS485_InitTXE(); /* 配置RS485芯片的发送使能硬件,配置为推挽输出 */
ConfigUartNVIC(); /* 配置串口中断 */
}
static void UartVarInit(void)
{
#if UART3_FIFO_EN == 1
g_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 ;
-
重要的是接受的过程中写的过程
- 先写入数据
- 若数组的索引出现越界的情况记得要将索引清零
/*接收中断中调用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 times 2400 2400 bits/s 417 us 4.6 ms 16 ms 4800 4800 bits/s 208 us 2.3 ms 8.0 ms 9600 9600 bits/s 104 us 1.2 ms 4.0 ms 19200 19200 bits/s 52 us 573 us 2.0 ms 38400 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.c
中MODS_Poll
函数,主要好的点有以下的点:
不对的命令直接
return
进行函数的结束巧用
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
向上取整
num
个bit
需要多少个字节
来储存数据,感觉这个方法很巧妙
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; }