Modbus 调试
背景
大量的工业设备基本都支持 RS485 串行协议,包括绝大部分工业传感器,在 RS485 的基础上,支持 ModbusRTU 协议,所以有必要对Modbus进行支持。
Freemodbus
Freemodbus 是 armink 大神移植的 Freemodbus 协议栈。同时支持主机和从机的功能。FreeModbus 是一款开源的 Modbus 协议栈,但是只有从机开源,主机源码是需要收费的。同时网上也没有发现比较好的开源的 Modbus 主机协议栈,所以才开发这款支持主机模式的 FreeModbus 协议栈。本版 FreeModbus版本号更改为V1.6,特性如下:
- 新增加的主机源码与原有从机的风格及接口保持一致;
- 支持主机与从机在同一协议栈运行;
- 支持实时操作系统及裸机移植;
- 为应用提供多种请求模式,用户可以选择阻塞还是非阻塞模式,自定义超时时间等,方便应用层灵活调用;
- 支持所有常用的Modbus方法。
获取方式
使用 Freemodbus 软件包 需要在 RT-Thread 的包管理器中选择它,具体路径如下:
RT-Thread online packages
IoT - internet of things --->
[*] FreeModbus: Modbus master and slave stack --->
[*] Master mode --->
[*] Slave mode --->
配置 Freemodbus
使用 Env 工具配置 Freemodbus 软件包,使能 Master 模式也就是主机模式,一般的传感器设备都是从机,所以这里使用主机模式去测试。配置从机的地址,也就是 RS485 地址,配置使用 uart2,配置波特率为9600,使能 sample 示例。
修改测试函数
打开 sample_mb_master.c
文件,这个是示例代码。
测试函数如下:
static int mb_master_samlpe(int argc, char **argv)
{
static rt_uint8_t is_init = 0;
rt_thread_t tid1 = RT_NULL, tid2 = RT_NULL;
if (is_init > 0)
{
rt_kprintf("sample is running\n");
return -RT_ERROR;
}
tid1 = rt_thread_create("md_m_poll", mb_master_poll, RT_NULL, 512, MB_POLL_THREAD_PRIORITY, 10);
if (tid1 != RT_NULL)
{
rt_thread_startup(tid1);
}
else
{
goto __exit;
}
tid2 = rt_thread_create("md_m_send", read_thread_entry, RT_NULL, 512, MB_SEND_THREAD_PRIORITY, 10);
if (tid2 != RT_NULL)
{
rt_thread_startup(tid2);
}
else
{
goto __exit;
}
is_init = 1;
return RT_EOK;
__exit:
if (tid1)
rt_thread_delete(tid1);
if (tid2)
rt_thread_delete(tid2);
return -RT_ERROR;
}
MSH_CMD_EXPORT(mb_master_samlpe, run a modbus master sample);
首先运行md_m_poll
进程,入口函数是mb_master_poll
static void mb_master_poll(void *parameter)
{
eMBMasterInit(MB_RTU, PORT_NUM, PORT_BAUDRATE, PORT_PARITY);
eMBMasterEnable();
while (1)
{
eMBMasterPoll();
rt_thread_mdelay(MB_POLL_CYCLE_MS);
}
}
此函数调用eMBMasterInit
方法初始化 Modbus 主机协议栈,主机涉及到的一些硬件就在这个时候做了初始化,然后调用eMBMasterEnable
方法启动Modbus主机。轮询的周期由MB_POLL_CYCLE_MS
定义,轮询周期决定了命令的相应时间。
随后,运行了md_m_send
线程,这里我修改了入口函数为read_thread_entry
,因为我需要做的是读取测试,自带的代码中是写入(随时时间参数)测试。
static void read_thread_entry(void *parameter)
{
eMBMasterReqErrCode error_code = MB_MRE_NO_ERR;
rt_uint16_t error_count = 0;
while (1)
{
/* Test Modbus Master */
error_code = eMBMasterReqReadHoldingRegister( SLAVE_ADDR,
MB_SEND_REG_START,
1,
500 );
error_code = eMBMasterReqReadHoldingRegister( 2,
MB_SEND_REG_START,
1,
500 );
printf("sensor1: %d sensor2: %d \t",usMRegHoldBuf[0][0],usMRegHoldBuf[1][0]);
printf("error_code : %d \n",error_code);
/* Record the number of errors */
if (error_code != MB_MRE_NO_ERR)
{
error_count++;
}
}
}
此函数调用了eMBMasterReqReadHoldingRegister
来读取保持寄存器,函数原型如下,其它 API 函数可详见仓库 readme 文档。
eMBMasterReqErrCode eMBMasterReqReadHoldingRegister( UCHAR ucSndAddr,
USHORT usRegAddr,
USHORT usNRegs,
LONG lTimeOut );
参数 | 描述 |
---|---|
ucSndAddr | 请求的从机地址,0代表广播。 |
usRegAddr | 读寄存器的地址 |
usRegData | 读寄存器的数量 |
lTimeOut | 请求超时时间。支持永久等待,使用操作系统的永久等待参数即可。 |
完成读取后我直接用printf
显示出来,方便测试。根据 readme 的描述,读取的数据是同意存放在数据缓存区的,代码中已经提供了默认的缓存区,也可以在 Modbus 数据处理回调接口中处理自定义的缓存区。
数据缓冲区
数据缓冲区定义的位置位于 FreeModbus\port\user_mb_app_m.c
文件顶部,共计 4种 数据类型。 FreeModbus从机默认使用 一维数组 作为缓存区数据结构,主机可以存储所有网内从机的数据,所以主机采用 二维数组 对所有从机节点数据进行存储。二维数组的列号代表寄存器、线圈及离散量地址,行号代表从机节点ID,但需要做减一处理,例如usMRegHoldBuf[2][1]
代表从机ID为 3,保持寄存器地址为 1 的从机数据。
Modbus 数据处理回调接口
Modbus 一共有4种不同的数据类型,所有的 Modbus 功能都围绕这些数据类型进行操作。由于不同的用户数据缓冲区结构可能有所不同,那么对应的 Modbus 数据处理方式也就存在差异,所以用户需要把每种数据类型对应的操作,按照自己的数据缓冲区结构进行定制实现。 所有的 Modbus 数据处理回调接口如下:
接口 | 功能描述 |
---|---|
eMBMasterRegInputCB | 输入寄存器回调接口 |
eMBMasterRegHoldingCB | 保持寄存器回调接口 |
eMBMasterRegCoilsCB | 线圈回调接口 |
eMBMasterRegDiscreteCB | 离散输入回调接口 |
所以我预计读取从机地址1、2的两个设备的各自的第一个保持寄存器。
测试
如果手头没有 Modbus 设备实物,可以使用 Modbus Slave 这个软件来模拟,功能非常齐全,价格是99刀/单个授权,我仅用于研究用途,就使用了研究用途版本。
连接USB-TTL, TTL 端连接单片机的 uart2,打开软件后点击 Conection,设置串口参数,OK 即可。
默认打开了一个从机,点击 File -> New 新建一个,默认 F = 03 即为保持寄存器,分别双击设置保存寄存器0的值为 1234 和 5678。
打开终端,输入测试命令。
可以看到已经成功读取保存寄存器。
可以实时地对保存寄存器的值进行修改