Modbus协议是由Modicon开发的,用于工业自动化系统中的通讯协议,Modbus支持串行通讯(RS232, RS485)和以太网通讯。Modbus协议已经成为一个开放的标准,具有简单易懂、灵活性强、易于扩展、开放性和可靠性高等特点。广泛应用于工业自动化控制、智能家居、能源监控、环境监测和智能交通等领域。
Modbus协议根据通讯方式的划分,主要包含如下两大类。
可以看到,modbus的协议部分有RTU,ASCII,TCP三种模式组成,其中RTU和ASCII都是基于串行通讯方式,TCP则是基于网络服务的一主多从结构。另外从角色上来说,Modbus协议也分为主机和从机两部分实现;主机可以是主站也可以是从站,从机则只能是从站。这里关于主机的实现基于libmodbus库实现,从机则是基于freemodbus库实现。
本节目录如下所示。
另外本节也附带相关的代码。
Modbus通讯根据硬件接口不同分为TCP和串行模式,其中串行模式又根据传输格式的不同,有RTU和ASCII两种。看起来很复杂,但具体到软件执行框架和流程是基础一致的。如何解析协议呢,这里参考我曾经的经验。协议是约定了多个设备之间交互的方法,为了保证能够正常的数据通讯,能够处理各类问题;因此定义一些约束,符合约束的设备才能够互相联系,这些约束包含如下所示。
按照这个思路,modbus协议的主从机通讯格式如下。
完整的通讯流程包含主机发送请求步骤和从机响应应答步骤,具体如下。
发送请求步骤如下。
响应应答步骤如下。
可以看到,一次完整的通讯是包含主/从机两部分处理,从流程来说是基本一致的,只是在处理的顺序和数据流向上有区别。
对于协议的理解,最主要是协议如何组包和解包的处理,这部分根据协议文档解析;对于modbus协议,其中modbus串口和modbus TCP的格式有差异;这里分别以modbus RTU和Mobdus TCP来说明。
Modbus RTU的协议格式如下所示。
地址域 | 功能码 | 数据域 | 校验域 |
---|---|---|---|
1字节 | 1字节 | n字节 | 2字节 |
其中Modbus的功能码支持如下所示。
功能码 | 功能说明 |
---|---|
0x01 | 读线圈状态 |
0x02 | 读取离散输入(只读) |
0x03 | 读取保持寄存器 |
0x04 | 读取输入寄存器(只读) |
0x05 | 写单个线圈 |
0x06 | 写单个保持寄存器 |
0x0F | 写多个线圈 |
0x10 | 写多个保持寄存器 |
上面协议格式还好理解,反而功能码在刚开始接触时比较迷惑,这里面线圈,离散,保存,输入寄存器是什么意思,为什么这么命名?其实这是和工业类的需求息息相关的。在工业领域中,类似继电器,LED,蜂鸣器,都是由开关量控制(ON表示打开,OFF表示关闭),这类设备可以通过单个bit控制,统一归纳为线圈管理(coils)。还有一类设备,如传感器,限位开关信号,只有输入开关量,使用只读的离散输入管理。保持寄存器是16位的寄存器,用于存储可读写的数据,通常是模拟量,比如温度、压力、速度等;输入寄存器也是16位寄存器,主要用于存储从外部设备采集到的模拟量数据,比如传感器的测量值。指令根据处理寄存器分类如下所示。
寄存器 | 功能码 |
---|---|
线圈 | 0x01, 0x05, 0x0F |
离散输入 | 0x02 |
保持寄存器 | 0x03, 0x06, 0x10 |
输入寄存器 | 0x04 |
对于Modbus TCP协议,其格式如下所示。
Transaction Identifier | Protocol Identifier | Length | Unit Identifier | Function Code | Data |
---|---|---|---|---|---|
事务处理标识符 | 协议标识符 | 长度 | 单元标识符 | 功能码 | 数据域 |
2 字节 | 2 字节 | 2 字节 | 1 字节 | 1 字节 | n 字节 |
其中功能码和数据域就是和Modbus RTU相同的部分,这里不在说明。讲解完协议,下面进行modbus协议的移植。
modbus_slave实现包含两部分,分为Modbus Serial和Modbus TCP物理层实现。其中协议层主要实现协议部分解析组包的部分,这部分由freemodbus实现。对于用户来说,主要实现两部分,应用层处理寄存器的实现和物理层的操作。
modbus应用层部分则是通用的,主要定义相应的寄存器,并提供读写的处理,这里按照寄存器进行分类处理。
/*
CRC校验: 高位在前, 低位在后
功能码: 01(0x01), 读线圈状态,读取连续的值(每个寄存器代表1bit数据),可读可写
#define MB_FUNC_READ_COILS ( 1 )
eMBException eMBFuncReadCoils( UCHAR * pucFrame, USHORT * usLen )
RTU请求: | 01 | 01 | 00 00 | 00 08 | 3d cc | => | 从设备地址 | 功能码 | 寄存器首地址,实际+1 | 寄存器长度 | CRC校验 |
从机响应: | 01 | 01 | 01 | 10 | 50 44 | => | 从设备地址 | 功能码 | 数据个数 | 寄存器内数据 | CRC校验 |
功能码: 05(0x05), 写单个线圈(0xFF 0x00表示1, 0x00 0x00为0, 其它则错误)
#define MB_FUNC_WRITE_SINGLE_COIL ( 5 )
eMBException eMBFuncWriteCoil( UCHAR * pucFrame, USHORT * usLen )
RTU请求: | 01 | 05 | 00 01 | FF 00 | dd fa | => | 从设备地址 | 功能码 | 寄存器首地址 | 变更数据 | CRC校验 |
从机响应: | 01 | 05 | 00 01 | FF 00 | dd fa | => | 从设备地址 | 功能码 | 寄存器首地址 | 变更数据 | CRC校验 |
功能码: 15(0x0F), 写多个线圈
#define MB_FUNC_WRITE_MULTIPLE_COILS ( 15 )
eMBException eMBFuncWriteMultipleCoils( UCHAR * pucFrame, USHORT * usLen )
RTU请求: | 01 | 0F | 00 00 | 00 04 | 01 | 0x0F | 7f 5e | => | 从设备地址 | 功能码 | 寄存器首地址 | 线圈数目 | 写入字节个数 | 写入字节 | CRC校验 |
从机响应: | 01 | 0F | 00 00 | 00 04 | 54 08 | => | 从设备地址 | 功能码 | 寄存器首地址 | 线圈数目 | CRC校验 |
*/
#define REG_COIL_START 0x0001
#define REG_COIL_NREGS 48
static UCHAR usRegCoilBuf[REG_COIL_NREGS/8] = {0x10, 0xf2, 0x35, 0x00, 0x00, 0x00};
eMBErrorCode eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode )
{
UCHAR *pucStartRegBuffer;
USHORT usRegIndex;
USHORT usCoilnums;
// 检查寄存器范围是否满足要求
if (usAddress < REG_COIL_START
|| usAddress + usNCoils > REG_COIL_START+REG_COIL_NREGS) {
return MB_ENOREG;
}
usRegIndex = usAddress - REG_COIL_START;
usCoilnums = usNCoils;
pucStartRegBuffer = pucRegBuffer;
switch (eMode) {
case MB_REG_WRITE: //写入bit,每次写入8bit,不足写入剩余bit
while( usCoilnums > 0 ) {
xMBUtilSetBits( usRegCoilBuf, usRegIndex, (uint8_t)( usCoilnums > 8 ? 8 : usCoilnums ), *pucStartRegBuffer++ );
if (usCoilnums > 8) {
usCoilnums -= 8;
usRegIndex += 8;
} else {
break;
}
}
coil_hardware_process(usRegCoilBuf, usAddress, usNCoils);
break;
case MB_REG_READ: //读取bit,每次读取8bit,不足8bit剩余位用0填充
while( usCoilnums > 0 ) {
*pucStartRegBuffer++ = xMBUtilGetBits(usRegCoilBuf, usRegIndex, ( uint8_t )( usCoilnums > 8 ? 8 : usCoilnums ) );
if (usCoilnums > 8) {
usCoilnums -= 8;
usRegIndex += 8;
} else {
break;
}
}
break;
}
return MB_ENOERR;
}
/*
功能码: 02(0x02), 读取离散输入(每个寄存器代表1bit数据),只读
#define MB_FUNC_READ_DISCRETE_INPUTS ( 2 )
eMBException eMBFuncReadDiscreteInputs( UCHAR * pucFrame, USHORT * usLen )调用eMBRegDiscreteCB
RTU请求: | 01 | 02 | 00 00 | 00 04 | 25 3a | => | 从设备地址 | 功能码 | 离散寄存器地址 | 离散寄存器长度 | CRC校验 |
从机响应: | 01 | 02 | 01 | 03 | 2a 1c | => | 从设备地址 | 功能码 | 数据个数 | 寄存器内数据 | CRC校验 |
*/
*/
#define REG_DISCRETE_START 0x0001
#define REG_DISCRETE_NREGS 48
static UCHAR usRegDiscreateBuf[REG_DISCRETE_NREGS/8] = {0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde};
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
eMBErrorCode eStatus = MB_ENOERR;
USHORT usRegIndex;
// 检查寄存器范围是否满足要求
if (usAddress < REG_COIL_START
|| (usAddress + usNDiscrete > REG_COIL_START+REG_COIL_NREGS)) {
return MB_ENOREG;
}
usRegIndex = usAddress - REG_COIL_START;
// 读取离散输入寄存器值,每次读取8bit,不足8bit剩余位用0填充
while( usNDiscrete > 0 ) {
*pucRegBuffer++ = xMBUtilGetBits(usRegDiscreateBuf, usRegIndex, (uint8_t)( usNDiscrete > 8 ? 8 : usNDiscrete ) );
if(usNDiscrete > 8) {
usNDiscrete -= 8;
usRegIndex += 8;
} else {
break;
}
}
return eStatus;
}
/*
功能码: 03(0x03), 读取保持寄存器((每个寄存器代表16bit数据)
#define MB_FUNC_READ_DISCRETE_INPUTS ( 2 )
eMBException eMBFuncReadDiscreteInputs( UCHAR * pucFrame, USHORT * usLen )eMBRegHoldingCB
RTU请求: | 01 | 03 | 00 00 | 00 02 | 25 3a | => | 从设备地址 | 功能码 | 保持寄存器地址 | 保持寄存器长度 | CRC校验 |
从机响应: | 01 | 03 | 02 | 0x10 0x00 | 2a 1c | => | 从设备地址 | 功能码 | 数据个数 | 寄存器内数据 | CRC校验 |
功能码: 06(0x06), 写入保持寄存器((每个寄存器代表16bit数据)
#define MB_FUNC_WRITE_REGISTER ( 6 )
eMBException eMBFuncWriteHoldingRegister( UCHAR * pucFrame, USHORT * usLen )
RTU请求: | 01 | 06 | 00 00 | 00 0A | 25 3a | => | 从设备地址 | 功能码 | 保持寄存器地址 | 保持寄存器数据 | CRC校验 |
从机响应: | 01 | 06 | 00 00 | 00 0A | 25 3a | => | 从设备地址 | 功能码 | 数据个数 | 寄存器内数据 | CRC校验 |
*/
#define REG_HOLDING_START 0x0001
#define REG_HOLDING_NREGS 10
static USHORT usRegHoldingStart = REG_HOLDING_START;
static USHORT usRegHoldingBuf[REG_HOLDING_NREGS] = {0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000A};
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
// 检查寄存器范围是否满足要求
if((usAddress < REG_HOLDING_START)
|| ((usAddress+usNRegs) > (REG_HOLDING_START + REG_HOLDING_NREGS))) {
return MB_ENOREG;
}
iRegIndex = (int)(usAddress - usRegHoldingStart);
switch (eMode) {
case MB_REG_READ: //读取寄存器双字节
while (usNRegs > 0) {
*pucRegBuffer++ = (usRegHoldingBuf[iRegIndex] >> 8);
*pucRegBuffer++ = (usRegHoldingBuf[iRegIndex] & 0xFF);
iRegIndex++;
usNRegs--;
}
break;
case MB_REG_WRITE: //写入寄存器双字节
while(usNRegs > 0) {
usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
break;
}
return eStatus;
}
/*
功能码: 04, 读取输入寄存器(每个数据2字节)
#define MB_FUNC_READ_INPUT_REGISTER ( 4 )
eMBException eMBFuncReadInputRegister( UCHAR * pucFrame, USHORT * usLen )eMBRegInputCB
RTU请求: | 01 | 04 | 00 00 | 00 02 | 25 3a | => | 从设备地址 | 功能码 | 离散寄存器地址 | 离散寄存器长度 | CRC校验 |
从机响应: | 01 | 04 | 02 | 0x10 0x00 0x10 0x01 | 2a 1c | => | 从设备地址 | 功能码 | 数据个数 | 寄存器内数据 | CRC校验 |
*/
#define REG_INPUT_START 0x0001
#define REG_INPUT_NREGS 10
static USHORT usRegInputStart = REG_INPUT_START;
static USHORT usRegInputBuf[REG_INPUT_NREGS] = {0x1000, 0x1001, 0x1002, 0x1003};
eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
// 检查寄存器范围是否满足要求
if(usAddress < REG_INPUT_START
|| (usAddress + usNRegs > REG_INPUT_START + REG_INPUT_NREGS)) {
return MB_ENOREG;
}
iRegIndex = ( int )( usAddress - usRegInputStart );
// 读取输入寄存器值双字节
while (usNRegs > 0) {
*pucRegBuffer++ = ( unsigned char )( usRegInputBuf[iRegIndex] >> 8 );
*pucRegBuffer++ = ( unsigned char )( usRegInputBuf[iRegIndex] & 0xFF );
iRegIndex++;
usNRegs--;
}
return eStatus;
}
至此,关于应用层寄存器的处理代码实现完毕,剩余就是初始化协议和执行的代码,这部分可以由线程来处理,具体如下。
// modbus slave初始化和使能实现
static eMBErrorCode modbus_initialize(void)
{
eMBErrorCode eStatus = MB_EINVAL;
#if MODBUS_RUN_MODE == MODBUS_RUN_RTU
eStatus = eMBInit( MB_RTU, MODBUS_DEF_ADDRESS, 0, MODBUS_DEF_UBAUD, MODBUS_DEF_PARITY ); // RTU模式初始化
#elif MODBUS_RUN_MODE == MODBUS_RUN_ASCII
eStatus = eMBInit( MB_ASCII, MODBUS_DEF_ADDRESS, 0, MODBUS_DEF_UBAUD, MODBUS_DEF_PARITY ); // ASCII模式初始化
#elif MODBUS_RUN_MODE == MODBUS_RUN_TCP
eStatus = eMBTCPInit( MODBUS_DEF_TCP_PORT ); // TCP模式初始化
#endif
if ( eStatus != MB_ENOERR ) {
LOG_ERROR(0, "modbus init failed, error code: {}", static_cast<int>(eStatus));
return eStatus;
}
eStatus = eMBEnable( );
if ( eStatus!= MB_ENOERR ){
LOG_ERROR(0, "modbus enable failed, error code: {}", static_cast<int>(eStatus));
return eStatus;
}
return eStatus;
}
int main(int argc, char *argv[])
{
logger_manage::get_instance()->set_log_level(logger_manage::LOGGER_LEVEL::INFO);
//modbus initialize.
if ( modbus_initialize() != MB_ENOERR ) {
LOG_ERROR(0, "modbus initialize failed");
exit(-1);
}
for(;;)
{
( void )eMBPoll( );
}
}
应用层的实现包含寄存器处理,协议初始化、使能和轮询控制,理解了这些就掌握了modbus从机协议层的管理。modbus物理层包含modbus serial接口(RTU、ASCII共用)和modbus tcp接口;其对完整包处理,数据接收的格式都不同,是独立的处理,这里首先以串口接口为例。
根据上面的modbus rtu协议格式,可以看到并没有帧头,帧尾,长度等字段表示完整帧;那么依靠什么来判断呢?实验过单片机空闲中断原理的可以知道,当串口开始收发,一段时间没有收到数据,就认为一次接收结束。串口接口也是类似,接收到数据时,重启定时器,当定时器超时,就认为一次接收结束;至于发送则比较简单,直接调用串口发送接口即可实现;整个移植包含串口收发接口,超时定时器处理;在Linux端因为上层以文件流的方式读取数据,因此以线程的方式控制读写,具体移植内容如下所示。
// 定时器超时处理
void vTimerCallback(int sig)
{
EnterCriticalSection();
pxMBPortCBTimerExpired();
ExitCriticalSection();
}
//初始化定时器,定义超时回调函数
BOOL xMBPortTimersInit( USHORT usTim1Timerout50us )
{
signal(SIGALRM, vTimerCallback);
return TRUE;
}
//使能定时器
void vMBPortTimersEnable( void )
{
struct itimerval tick;
tick.it_value.tv_sec = 0;
tick.it_value.tv_usec = 200;
//After first, the Interval time for clock
tick.it_interval.tv_sec = 0;
tick.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &tick, NULL);
}
//关闭定时器
void vMBPortTimersDisable( void )
{
struct itimerval value;
value.it_value.tv_sec = 0;
value.it_value.tv_usec = 0;
value.it_interval.tv_sec = 0;
value.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &value, NULL);
}
这部分使用系统定时器实现,当然也可以使用rtc,或者注册用定时器的方式来实现都可以。实现逻辑为每次收到定时器时,就重启定时器;当定时器超时时,表示完整一帧数据,此时通知主循环进行协议解析处理,这部分在pxMBPortCBTimerExpired中实现。
发送接口处理如下。
/* ----------------------- Start implementation -----------------------------*/
void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
if (xTxEnable) {
portserial.tx_buffer_size = 0;
portserial.tx_semaphore.signal(); //通知发送线程,开始数据发送
}
if (xRxEnable) {
portserial.rx_buffer_size = 0;
}
}
void
vMBPortClose( void )
{
}
// 发送接口,pxMBFrameCBTransmitterEmpty => xMBPortSerialPutByte
// 将数据写入到发送buffer中
static BOOL
prvvUARTTxReadyISR( void )
{
return pxMBFrameCBTransmitterEmpty( );
}
BOOL xMBPortSerialPutByte( UCHAR ucByte )
{
portserial.tx_buffer[portserial.tx_buffer_size++] = ucByte;
return TRUE;
}
void port_tty_tx_thread(void)
{
while (1) {
if( portserial.tx_semaphore.wait(TIME_ACTION_ALWAYS) ) {
while (!prvvUARTTxReadyISR()) { //调用xMBPortSerialPutByte,将数据写入缓存中
}
#if PORT_RUN_MODE == PORT_SERIAL_MODE
portserial.tty.write(portserial.tx_buffer, portserial.tx_buffer_size);
#else
LOG_INFO(0, "fifo_point_ tx:{}", portserial.tx_buffer_size);
portserial.fifo_point_->write(portserial.tx_buffer, portserial.tx_buffer_size);
#endif
}
}
}
接收接口处理如下。
static void
prvvUARTRxISR( void )
{
pxMBFrameCBByteReceived( );
}
// 从串口读取数据
BOOL xMBPortSerialGetByte( CHAR * pucByte )
{
*pucByte = portserial.rx_buffer[portserial.rx_buffer_size++];
return TRUE;
}
void port_tty_rx_thread(void)
{
ssize_t n_size;
while (1) {
#if PORT_RUN_MODE == PORT_SERIAL_MODE
n_size = portserial.tty.read(portserial.rx_buffer, RX_BUFFER_SIZE); //串口读取数据,写入到缓存portserial.rx_buffer中
#else
n_size = portserial.fifo_point_->read(portserial.rx_buffer, RX_BUFFER_SIZE);
#endif
if (n_size > 0) {
portserial.rx_buffer_size = 0;
for (int index=0; index<n_size; index++) {
prvvUARTRxISR(); //取出缓存数据,写入到modbus协议中
}
} else if ( n_size == 0) {
continue;
} else {
}
}
}
另外还有所有模式下共享的事件队列(port/portevent.cpp),用于告知eMBPoll处理对应事件;完整代码如下所示。
/* ----------------------- Variables ----------------------------------------*/
static EVENT::Thread_Queue<eMBEventType> queue;
/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortEventInit( void )
{
return TRUE;
}
// 发送事件
BOOL
xMBPortEventPost( eMBEventType eEvent )
{
queue.send(eEvent);
return TRUE;
}
// 接收事件
BOOL
xMBPortEventGet( eMBEventType * eEvent )
{
BOOL xEventHappened = FALSE;
eMBEventType event;
if( queue.receive(event, TIME_ACTION_ALWAYS) )
{
*eEvent = event;
xEventHappened = TRUE;
}
return xEventHappened;
}
支持modbus serial协议相关的接口实现完毕,参考代码如下。
modbus tcp协议和serial不同,定义包含数据长度,在接口层可以检测完整帧,然后再投递到应用层;其它部分则一致,主要实现收发接口,具体包含如下步骤。
void mb_tcp_process_task(USHORT port)
{
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_addr_len;
int opt = 1;
int server_fd;
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
LOG_ERROR(0, "can't create socket\n");
return;
}
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 定义服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
do {
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
printf("socket server bind error:%d\n", port);
sleep(5);
continue;
} else {
printf("bind ok, net_port:%d\n", port);
break;
}
}while(1);
// 监听socket
listen(server_fd, 1);
LOG_INFO(0, "server listen success, fd:{}, port:{}", server_fd, port);
while (1) {
client_addr_len = sizeof(client_addr);
porttcp_info.client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (porttcp_info.client_fd < 0) {
LOG_WARN(0, "client accept failed!");
continue;
}
LOG_INFO(0, "client connect success!");
tcp_rx_process(porttcp_info.client_fd);
}
}
static void tcp_rx_process(int client_fd)
{
porttcp_info.rx_frame_size = 0;
while (1) {
porttcp_info.rx_size = recv(client_fd, porttcp_info.rx_buffer, TCP_RX_BUFFER_SIZE, 0);
if (porttcp_info.rx_size < 0) {
break;
} else if (porttcp_info.rx_size == 0) {
break;
} else {
//小于范围才进行拷贝
if (porttcp_info.rx_frame_size + porttcp_info.rx_size < TCP_RX_BUFFER_SIZE) {
memcpy(&porttcp_info.rx_frame_buffer[porttcp_info.rx_frame_size], porttcp_info.rx_buffer, porttcp_info.rx_size);
} else { // 超出范围则直接丢弃
LOG_ERROR(0, "rx_frame_size:{} > rx_size:{}", porttcp_info.rx_frame_size, porttcp_info.rx_size);
porttcp_info.rx_frame_size = 0;
continue;
}
porttcp_info.rx_frame_size += porttcp_info.rx_size;
// 判断完整帧
if (porttcp_info.rx_frame_size >= 6) {
uint16_t len = porttcp_info.rx_frame_buffer[4]*256 + porttcp_info.rx_frame_buffer[5] + 6;
// 长度过长, 数据丢弃
if (len > TCP_RX_BUFFER_SIZE) {
LOG_ERROR(0, "rx_frame_size:{} > TCP_RX_BUFFER_SIZE:{}", len, TCP_RX_BUFFER_SIZE);
porttcp_info.rx_frame_size = 0;
continue;;
}
if (porttcp_info.rx_frame_size >= len) {
xMBPortEventPost(EV_FRAME_RECEIVED);
}
}
}
}
printf("client disconnect!\n");
close(client_fd);
client_fd = -1;
}
// 获取缓存的tcp接收数据帧,提交到应用层
BOOL
xMBTCPPortGetRequest(UCHAR **ppucMBTCPFrame, USHORT *usTCPLength)
{
*ppucMBTCPFrame = &porttcp_info.rx_frame_buffer[0];
*usTCPLength = porttcp_info.rx_frame_size;
porttcp_info.rx_frame_size = 0;
return TRUE;
}
// 调用socket发送接口,响应应答帧
BOOL
xMBTCPPortSendResponse(const UCHAR *pucMBTCPFrame, USHORT usTCPLength)
{
int ret;
BOOL bFrameSent = FALSE;
if (porttcp_info.client_fd >= 0)
{
ret = send(porttcp_info.client_fd, (void *)pucMBTCPFrame, usTCPLength, 0);
if (ret == usTCPLength)
{
bFrameSent = TRUE;
}
}
return bFrameSent;
}
至此关于从机的modbus tcp的代码实现完毕,详细参考代码。
下面开始modbus主机的实现说明,这里主要以modbus tcp协议为例,其他协议实现方式类似。
modbus主机从功能上来说,就实现符合协议格式的组包,发送到从机;接收从机返回,然后解析出数据,进行相应的应用层处理。本篇中以pymodbus和libmodbus为例,讲解modbus主机实现。
pymodbus是python中至此modbus的协议库,可以十分方便进行modbus主机的开发;以modbus tcp为例,主要内容如下所示。
step 1: 安装pymodbus库
# 使用pip3安装pymodbus库
pip3 install pymodbus
# Ubuntu可以使用apt安装pymodbus库
sudo apt install python3-pymodbus
step 2: 使用pymodbus进行modbus主机开发
# 初始化modbus连接接口
client = ModbusClient(SOCK_IPADDRESS, port=SOCK_PORT)
connection = client.connect()
# 读写coils寄存器
# 写入多个bit对应coils寄存器
response = client.write_coils(address=0, values=hex_to_bool_list(usRegCoilBuf, 48))
# 读取多个bit对应coils寄存器
response = client.read_coils(address=0, count=4)
# 写入单个bit对应的coil寄存器
response = client.write_coil(address=0, value=True)
# 读取离散寄存器(只读)
response = client.read_discrete_inputs(address=0, count=16)
# 读写保持寄存器
response = client.write_registers(address=0, values=usRegHoldingBuf)
response = client.read_holding_registers(address=0, count=4)
# 读取输入寄存器(只读)
respone = client.read_input_registers(address=0, count=5)
详细代码可以参考如下。
libmodbus是c语言的modbus协议库,可以方便的进行modbus主机的开发;以modbus tcp为例,主要内容如下所示。
step 1: 安装libmodbus库
# 安装libmodbus库
sudo apt-get install libmodbus-dev
step 2: 使用libmodbus进行modbus主机开发
// 初始化modbus连接接口
modbus_t *ctx = modbus_new_tcp(MODBUS_TCP_IP, MODBUS_TCP_PORT);
if (!ctx) {
fprintf(stderr, "Failed to create TCP context\n");
return -1;
}
if (modbus_set_slave(ctx, MODBUS_TCP_UNIT_ID) == -1) {
fprintf(stderr, "Failed to set slave ID\n");
modbus_free(ctx);
return -1;
}
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 读写coils寄存器
modbus_write_bits(ctx, 0, 48, bitBuffer);
int rc = modbus_read_bits(ctx, 0, 4, usRegCoilReadBuf);
// 读取离散寄存器(只读)
int rc = modbus_read_input_bits(ctx, 0, 16, usRegDiscreateReadBuf)
// 读写保持寄存器
modbus_write_registers(ctx, 0, 10, usRegHoldingBuf);
int rc = modbus_read_registers(ctx, 0, 10, usRegHoldingReadBuf);
// 读取输入寄存器(只读)
int rc = modbus_read_input_registers(ctx, 0, 5, usRegInputReadBuf);
读取数据后,进行相应的处理,就实现了libmodbus主机功能;相应的代码可以参考如下。
至此,关于Modbus主从机的讲解完成。本文主要以协议为基础,freemodbus的移植和应用,libmodbus和pymodbus为核心,讲解modbus需要了解的知识。不过因为篇幅原因,只能抛砖引玉,如果希望彻底掌握modbus协议,还需要自己去移植,调试验证,才能有更深入的了解。
直接开始下一节说明: 使用python进行嵌入式Linux应用层开发