前面的文章分析了串口的一些基本知识,在工业应用中,串口通信比较常用的协议就是Modbus RTU,freemodbus是一款微型modbus协议栈,之前对各种单片机、小型处理器支持的比较好,从V1.6版本开始,对Linux也支持了,下面先简单的分析总结下freemodbus的工作流程:
我们知道Modbus通信的重点一方面是数据解析,另一方面就是串口的 不定长 数据接收,因为modbus的命令还是有很多种的,不同的命令,长度都是不同的,尤其是“写”多个寄存器命令,直接跟要写的寄存器数量成正比,这就要求串口能够接收不定长数据。之前的文章介绍过各种方式,既要保证实时性,又要保证数据准确性,最好的效果就是,一旦串口接收到最后一个字符后,很快就触发modbus处理。
freemodbus的接收处理方式采用的是 定时器计时,当串口有数据接收时,触发定时器开始计时,每接收一个字节,重置定时器,这样能保证在接收数据的过程中,定时器是不会溢出的,这一点与 前面文章讲的 termios结构体中的 VTIME是一样的道理。等到串口接收全部数据后,由于没有对定时器进行重置,所以定时器就会按照设定的溢出时间(一般是3.5个字符时间),溢出,在串口溢出中断服务程序中,将要更新 状态机FSM,而eMBPoll函数则会进行modbus协议的解析。
在非Linux的系统中,上面说的定时器是通过 实际的 定时器外围来实现的,当然串口的 收发都是采用 指定外围的发送和接收中断来实现的。而在Linux中,由于多级分层的编程思想,应用程序的程序员基本上是可以不接触具体的硬件的,尤其是linux下的timeval 结构体,本身就能实现 微秒 级的 时间获取,所以这里就不再需要 再使用一个 定时器 外围了,而是虚拟了一个 计时 函数,也能实现定时器的效果。而串口的发送,自然是使用 write函数。
在freemodbus LINUX中,串口的接收,作者也是采用定时器 计时实现的,termios中的VMIN和VTIME都是设置为0,这就意味 着串口只要有数据,read就会改变 阻塞状态,同时配合select机制,实现串口的接收,freemodbus是 典型使用状态机FSM的案例,把串口 数据流细分成 n多个状态,这样做的优点就是 程序代码非常稳健,当然缺点就是 代码量 有点大,上手稍微优点难。 不过结合上面的 接收原理理解起来不难。
下面说一下移值步骤:
1.修改 配置地址 串口参数 等,
位置:demo.c
L112
原:
else if( eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN ) != MB_ENOERR )
修改为 else if( eMBInit( MB_RTU, 0x01, 0, 9600, MB_PAR_NONE ) != MB_ENOERR )
之所以这么改的原因是,modbus通信,不适合波特率过高,为了传输质量。
2、删掉 设备信息配置,不需要,
位置:demo
L117...
else if( eMBSetSlaveID( 0x34, TRUE, ucSlaveID, 3 ) != MB_ENOERR )
{
fprintf( stderr, "%s: can't set slave id!\n", PROG );
iExitCode = EXIT_FAILURE;
}
3、修改 串口 初始化中的 打开串口的 “路径”字符串
位置:port/portserial.c
L101
原:
snprintf( szDevice, 16, "/dev/ttyS%d", ucPort );
改:
snprintf( szDevice, 16, "/dev/ttySP%d", ucPort );
3、修改 定时器 超时统计函数,这里应该是个bug,
位置:port/porttimer.c
L73-74
原:
ulDeltaMS = ( xTimeCur.tv_sec - xTimeLast.tv_sec ) * 1000L +
( xTimeCur.tv_usec - xTimeLast.tv_usec ) * 1000L;
改:
ulDeltaMS = ( xTimeCur.tv_sec - xTimeLast.tv_sec ) * 1000L +
( xTimeCur.tv_usec - xTimeLast.tv_usec ) / 1000L;
4、修改寄存器起始地址及 数量
位置:demo.c
L38-41
原:
#define REG_INPUT_START 1000
#define REG_INPUT_NREGS 4
#define REG_HOLDING_START 2000
#define REG_HOLDING_NREGS 130
该:
#define REG_INPUT_START 1
#define REG_INPUT_NREGS 200
#define REG_HOLDING_START 1
#define REG_HOLDING_NREGS 200
即输入和保持寄存器起始地址都为1,数量为200个。
重新编写Makefile如下
# 交叉环境变量
CROSS = arm-fsl-linux-gnueabi-
# gcc 变量
CC = $(CROSS)gcc
# lib
LIBS = -pthread
# strip函数-优化代码
STRIP = $(CROSS)strip
# CFLAGS-系统变量,编译器参数
CFLAGS = -g -O2 -Wall
# 头文件路径
INC = -I. -Iport -I../../modbus/rtu \
-I../../modbus/ascii -I../../modbus/include
# 目标
TARGET = modbusrtu
# 源文件
SRC = demo.c port/portevent.c port/portother.c \
port/portserial.c port/porttimer.c \
../../modbus/mb.c \
../../modbus/rtu/mbrtu.c ../../modbus/rtu/mbcrc.c \
../../modbus/ascii/mbascii.c \
../../modbus/functions/mbfunccoils.c \
../../modbus/functions/mbfuncdiag.c \
../../modbus/functions/mbfuncdisc.c \
../../modbus/functions/mbfuncholding.c \
../../modbus/functions/mbfuncinput.c \
../../modbus/functions/mbfuncother.c \
../../modbus/functions/mbutils.c
# 目标文件
OBJS = $(SRC:.c=.o)
# 链接为可执行文件
$(TARGET): $(OBJS)
$(CC) $^ -o $@ $(LIBS)
.PHONY: clean
clean:
rm -f $(OBJS)
install : $(TARGET) clean
@echo compile....
cp $(TARGET) /var/tftpboot
@echo end..
%.o : %.c
$(CC) $(CFLAGS) $(INC) -o $@ -c $<
执行make install 会生成 modbusrtu 执行文件, 将其copy到 EasyARM287开发板下,修改文件权限为 可执行,然后执行,才是开发板就是Modbus从设备,通信地址为 1,串口参数为 9600-8-N-1,在Windows下运行 Modbus Poll软件,通信界面如下:
由于没有对 输入和 保持寄存器赋值,所以都是0,不过测试通信正常。
代码git地址如下:
https://github.com/YorkJia/modbusRTU-Linux.git