主机移植
0.下载源码
开源地址:GitHub - loogg/agile_modbus
1.复制源码
1.2、目录结构
名称 | 说明 | |
---|---|---|
doc | 文档 | |
examples | 例子 | 参考示例 |
figures | 素材 | |
inc | 头文件 | 移植需要 |
src | 源代码 | 移植需要 |
util | 提供简单实用的组件 | 移植需要 |
本次移植需要的有
-
参考demo
-
头文件
-
源码
-
从机辅助文件
2.添加源码
3.头文件
4.接口实现
1.串口发送接口
串口的发送时基于发送缓冲为空的基础实现
2.接收一帧完整数据接口
基于RT-Thread前提下,本次移植使用的串口中断方式接收数据,每次收到一个数据就开启一个单次定时器,在定时器未超时之前收到数据立即重置定时器,当定时器超时时表示一帧数据接收完毕,完毕后使用信号量通知主线程
3.本次实现基于GD32F103C8T6+RT-Thread+mdk5
5.主机使用示例
c
#include "agile_modbus.h"
#include <stdio.h>
#include <stdlib.h>
#include "bsp_uart.h"
#include "main.h"
#include "bsp_timer_base.h"
#include "rtthread.h"
static uint8_t ctx_send_buf[AGILE_MODBUS_MAX_ADU_LENGTH];//发送缓存
static uint8_t ctx_read_buf[AGILE_MODBUS_MAX_ADU_LENGTH];//接收缓存
static uint16_t ctx_read_buf_index=0;
static rt_timer_t timer1;
/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
/* 定时器 1 超时函数 */
static void timeout1(void *parameter)
{
//通知数据接收完毕
rt_timer_stop(timer1);
rt_sem_release(dynamic_sem);
}
static void modbus_recv_callback(uint8_t ch)
{
/* 重置定时器 */
if (timer1 != RT_NULL)
{
rt_timer_stop(timer1);
rt_timer_start(timer1);
}
//保存数据
ctx_read_buf[ctx_read_buf_index++%AGILE_MODBUS_MAX_ADU_LENGTH]=ch;
}
void modbus_cycle_entry(void)
{
uint16_t hold_register[10];
uint8_t coils[10];
agile_modbus_rtu_t ctx_rtu;
agile_modbus_t *ctx = &ctx_rtu._ctx;
//rtu初始化
agile_modbus_rtu_init(&ctx_rtu, ctx_send_buf, sizeof(ctx_send_buf), ctx_read_buf, sizeof(ctx_read_buf));
//设置从机地址
agile_modbus_set_slave(ctx, 1);
rt_kprintf("agile_modbus master runing...\n");
//设置串口回调函数
bsp_uart_set_recv_callback(BSP_UART_ID_0,modbus_recv_callback);
/* 创建定时器 1 周期定时器 */
timer1 = rt_timer_create("timer1", timeout1,
RT_NULL, 5,
RT_TIMER_FLAG_PERIODIC);
/* 创建一个动态信号量,初始值是 0 ,用于通知一帧数据接收完毕*/
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
while (1) {
rt_thread_delay(1000*5);
//构建线圈读取指令
int send_len = agile_modbus_serialize_read_bits(ctx, 0, 10);
//清空数据
ctx_read_buf_index=0;
//发送数据
bsp_uart_send_datas(BSP_UART_ID_0,ctx->send_buf, send_len);
//等待数据接收完毕
int result = rt_sem_take(dynamic_sem, 1000);//RT_WAITING_FOREVER
if (RT_EOK ==result)
{
//解析数据
int rc = agile_modbus_deserialize_read_bits(ctx, ctx_read_buf_index, coils);
if (rc >0)
{
rt_kprintf("Coils:[");
for (int i = 0; i < 10; i++)
{
rt_kprintf(" %d", coils[i]);
}
rt_kprintf(" ]\n");
}
else {
rt_kprintf("exp code %d\n",-128 - rc);//得到异常码
}
}
/**************************/
//构建保持寄存器读取指令
send_len = agile_modbus_serialize_read_registers(ctx, 0, 10);
//清空缓存
ctx_read_buf_index=0;
//发送
bsp_uart_send_datas(BSP_UART_ID_0,ctx->send_buf, send_len);
//等待从机回复
result = rt_sem_take(dynamic_sem, 1000);
if (RT_EOK ==result)
{
//解析保持寄存器数据
int rc = agile_modbus_deserialize_read_registers(ctx, ctx_read_buf_index, hold_register);
if (rc >0 )
{
rt_kprintf("Hold Registers:[");
for (int i = 0; i < 10; i++)
{
rt_kprintf(" 0x%04X", hold_register[i]);
}
rt_kprintf(" ]\n");
}
else {
rt_kprintf(" exp code %d\n",-128 - rc);
}
}
}
}
从机移植
从机的移植和主机一样,需要提供串口的发送接口,一帧数据接收接口
从机使用流程
c
#include "agile_modbus.h"
#include <stdio.h>
#include <stdlib.h>
#include "agile_modbus_slave_util.h"
#include "bsp_uart.h"
#include "main.h"
#include "bsp_timer_base.h"
#include "rtthread.h"
//这个定义在bits.c中
extern const agile_modbus_slave_util_map_t bit_maps[1];
//这个定义在input_bits.c中
extern const agile_modbus_slave_util_map_t input_bit_maps[1];
//这个定义在registers.c中
extern const agile_modbus_slave_util_map_t register_maps[1];
//这个定义在input_registers.c中
extern const agile_modbus_slave_util_map_t input_register_maps[1];
//发送缓存
static uint8_t ctx_send_buf[AGILE_MODBUS_MAX_ADU_LENGTH];
//接收缓存
static uint8_t ctx_read_buf[AGILE_MODBUS_MAX_ADU_LENGTH];
//接收数据长度
static uint16_t ctx_read_buf_index=0;
static rt_timer_t timer1;
/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
/* 定时器 1 超时函数 */
static void timeout1(void *parameter)
{
rt_timer_stop(timer1);
rt_sem_release(dynamic_sem);
}
static void modbus_recv_callback(uint8_t ch)
{
/* 启动定时器 1 */
if (timer1 != RT_NULL)
{
rt_timer_stop(timer1);
rt_timer_start(timer1);
}
ctx_read_buf[ctx_read_buf_index++%AGILE_MODBUS_MAX_ADU_LENGTH]=ch;
}
static int addr_check(agile_modbus_t *ctx, struct agile_modbus_slave_info *slave_info)
{
int slave = slave_info->sft->slave;
if ((slave != ctx->slave) && (slave != AGILE_MODBUS_BROADCAST_ADDRESS) && (slave != 0xFF))
return -AGILE_MODBUS_EXCEPTION_UNKNOW;
return 0;
}
const agile_modbus_slave_util_t slave_util = {
bit_maps,
sizeof(bit_maps) / sizeof(bit_maps[0]),
input_bit_maps,
sizeof(input_bit_maps) / sizeof(input_bit_maps[0]),
register_maps,
sizeof(register_maps) / sizeof(register_maps[0]),
input_register_maps,
sizeof(input_register_maps) / sizeof(input_register_maps[0]),
addr_check,
NULL,
NULL};
void rtu_slave_entry(void )
{
agile_modbus_rtu_t ctx_rtu;
agile_modbus_t *ctx = &ctx_rtu._ctx;
agile_modbus_rtu_init(&ctx_rtu, ctx_send_buf, sizeof(ctx_send_buf), ctx_read_buf, sizeof(ctx_read_buf));
agile_modbus_set_slave(ctx, 1);
bsp_uart_set_recv_callback(BSP_UART_ID_0,modbus_recv_callback);
/* 创建定时器 1 周期定时器 */
timer1 = rt_timer_create("timer1", timeout1,
RT_NULL, 10,
RT_TIMER_FLAG_PERIODIC);
/* 创建一个动态信号量,初始值是 0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
rt_kprintf("Running.");
while (1) {
ctx_read_buf_index=0;
int result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);//
if (RT_EOK ==result)
{
int send_len = agile_modbus_slave_handle(ctx, ctx_read_buf_index, 0, agile_modbus_slave_util_callback, &slave_util, NULL);
if(send_len>0)
{
bsp_uart_send_datas(BSP_UART_ID_0,ctx->send_buf, send_len);
}
}
}
}
定义一些从机相关的寄存器
bits.c
c
#include "slave.h"
/*
01指令 读取线圈寄存器
*/
static uint8_t _tab_bits[10] = {0, 0, 0, 1, 0, 1, 0, 1, 1, 0};//0xA801
static int get_map_buf(void *buf, int bufsz)
{
uint8_t *ptr = (uint8_t *)buf;
// pthread_mutex_lock(&slave_mtx);
for (int i = 0; i < sizeof(_tab_bits); i++) {
ptr[i] = _tab_bits[i];
}
// pthread_mutex_unlock(&slave_mtx);
return 0;
}
static int set_map_buf(int index, int len, void *buf, int bufsz)
{
uint8_t *ptr = (uint8_t *)buf;
// pthread_mutex_lock(&slave_mtx);
for (int i = 0; i < len; i++) {
_tab_bits[index + i] = ptr[index + i];
}
// pthread_mutex_unlock(&slave_mtx);
return 0;
}
/*
初始化线圈,
1.设置开始结束地址
2.设置get和set接口
*/
const agile_modbus_slave_util_map_t bit_maps[1] = {
{50, 59, get_map_buf, set_map_buf}};
input_bits.c
c
#include "slave.h"
/*
02指令 读取离散输入寄存器
响应各离散输入寄存器状态,分别对应数据区中的每位值,1 代表 ON;0 代表 OFF。
第一个数据字节的 LSB(最低字节)为查询的寻址地址,其他输入口按顺序在该字节中由低字节向高字节排列,直到填充满 8 位。下一个字节中的 8 个输入位也是从低字节到高字节排列。若返回的输入位数不是 8
的倍数,则在最后的数据字节中的剩余位至该字节的最高位使用 0 填充
*/
static uint8_t _tab_input_bits[10] = {1, 1, 1, 0, 0, 1, 1, 0, 1, 1};//0x6703
static int get_map_buf(void *buf, int bufsz)
{
uint8_t *ptr = (uint8_t *)buf;
// pthread_mutex_lock(&slave_mtx);
for (int i = 0; i < sizeof(_tab_input_bits); i++) {
ptr[i] = _tab_input_bits[i];
}
// pthread_mutex_unlock(&slave_mtx);
return 0;
}
/*
初始化输入离散数据,
1.设置开始结束地址
2.设置get和set接口
*/
const agile_modbus_slave_util_map_t input_bit_maps[1] = {
{10, 19, get_map_buf, NULL}};
register.c
c
#include "slave.h"
/*
03指令 读取保持寄存器
*/
static uint16_t _tab_registers[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
static int get_map_buf(void *buf, int bufsz)
{
uint16_t *ptr = (uint16_t *)buf;
for (int i = 0; i < sizeof(_tab_registers) / sizeof(_tab_registers[0]); i++) {
ptr[i] = _tab_registers[i];
}
return 0;
}
static int set_map_buf(int index, int len, void *buf, int bufsz)
{
uint16_t *ptr = (uint16_t *)buf;
// pthread_mutex_lock(&slave_mtx);
for (int i = 0; i < len; i++) {
_tab_registers[index + i] = ptr[index + i];
}
// pthread_mutex_unlock(&slave_mtx);
return 0;
}
/*
初始化保持寄存器
1.设置开始结束地址
2.设置get和set接口
*/
const agile_modbus_slave_util_map_t register_maps[1] = {
{30, 39, get_map_buf, set_map_buf}};
input_register.c
c
#include "slave.h"
/*
04指令 读取输入寄存器
*/
static uint16_t _tab_input_registers[10] = {0, 1, 2, 3, 4, 9, 8, 7, 6, 5};
static int get_map_buf(void *buf, int bufsz)
{
uint16_t *ptr = (uint16_t *)buf;
//pthread_mutex_lock(&slave_mtx);
for (int i = 0; i < sizeof(_tab_input_registers) / sizeof(_tab_input_registers[0]); i++) {
ptr[i] = _tab_input_registers[i];
}
// pthread_mutex_unlock(&slave_mtx);
return 0;
}
/*
初始化输入寄存器
1.设置开始结束地址
2.设置get和set接口
*/
const agile_modbus_slave_util_map_t input_register_maps[1] = {
{20, 29, get_map_buf, NULL}};
官方Agile Modbus文档
开源地址:GitHub - loogg/agile_modbus
Agile Modbus 即:轻量型 modbus 协议栈
1.1、特性
- 支持 rtu 及 tcp 协议,使用纯 C 开发,不涉及任何硬件接口,可在任何形式的硬件上直接使用。
- 由于其使用纯 C 开发、不涉及硬件,完全可以在串口上跑 tcp 协议,在网络上跑 rtu 协议。
- 支持符合 modbus 格式的自定义协议。
- 同时支持多主机和多从机。
- 使用简单,只需要将 rtu 或 tcp 句柄初始化好后,调用相应 API 进行组包和解包即可。
1.2、目录结构
名称 | 说明 |
---|---|
doc | 文档 |
examples | 例子 |
figures | 素材 |
inc | 头文件 |
src | 源代码 |
util | 提供简单实用的组件 |
1.3、许可证
Agile Modbus 遵循 Apache-2.0
许可,详见 LICENSE
文件。
2、使用 Agile Modbus
帮助文档请查看 doc/doxygen/Agile_Modbus.chm
2.1、移植
-
用户需要实现硬件接口的
发送数据
、等待数据接收结束
、清空接收缓存
函数对于
等待数据接收结束
,提供如下几点思路:-
通用方法
每隔 20 / 50 ms (该时间可根据波特率和硬件设置,这里只是给了参考值) 从硬件接口读取数据存放到缓冲区中并更新偏移,直到读取不到或缓冲区满,退出读取。
本次移植使用的串口中断方式接收数据,每次收到一个数据就开启一个单次定时器,在定时器未超时之前收到数据立即重置定时器,当定时器超时时表示一帧数据接收完毕,完毕后使用信号量通知主线程
这对于裸机或操作系统都适用,操作系统可通过
select
或信号量
方式完成阻塞。 -
串口
DMA + IDLE
中断方式配置
DMA + IDLE
中断,在中断中使能标志,应用程序中判断该标志是否置位即可。但该方案容易出问题,数据字节间稍微错开一点时间就不是一帧了。推荐第一种方案。
-
-
主机:
agile_modbus_rtu_init
/agile_modbus_tcp_init
初始化RTU/TCP
环境agile_modbus_set_slave
设置从机地址清空接收缓存
agile_modbus_serialize_xxx
打包请求数据发送数据
等待数据接收结束
agile_modbus_deserialize_xxx
解析响应数据- 用户处理得到的数据
-
从机:
- 实现
agile_modbus_slave_callback_t
类型回调函数 agile_modbus_rtu_init
/agile_modbus_tcp_init
初始化RTU/TCP
环境agile_modbus_set_slave
设置从机地址等待数据接收结束
agile_modbus_slave_handle
处理请求数据清空接收缓存
(可选)发送数据
- 实现
-
特殊功能码
需要调用
agile_modbus_set_compute_meta_length_after_function_cb
和agile_modbus_set_compute_data_length_after_meta_cb
API 设置特殊功能码在主从模式下处理的回调。-
agile_modbus_set_compute_meta_length_after_function_cb
msg_type == AGILE_MODBUS_MSG_INDICATION
: 返回主机请求报文的数据元长度(uint8_t 类型),不是特殊功能码必须返回 0。msg_type == MSG_CONFIRMATION
: 返回从机响应报文的数据元长度(uint8_t 类型),不是特殊功能码必须返回 1。 -
agile_modbus_set_compute_data_length_after_meta_cb
msg_type == AGILE_MODBUS_MSG_INDICATION
: 返回主机请求报文数据元之后的数据长度,不是特殊功能码必须返回 0。msg_type == MSG_CONFIRMATION
: 返回从机响应报文数据元之后的数据长度,不是特殊功能码必须返回 0。
-
-
agile_modbus_rtu_init
/agile_modbus_tcp_init
初始化
RTU/TCP
环境时需要用户传入发送缓冲区
和接收缓冲区
,建议这两个缓冲区大小都为AGILE_MODBUS_MAX_ADU_LENGTH
(260) 字节。特殊功能码
情况用户根据协议自行决定。但对于小内存 MCU,这两个缓冲区也可以设置小,所有 API 都会对缓冲区大小进行判断:
发送缓冲区设置:如果
预期请求的数据长度
或预期响应的数据长度
大于设置的发送缓冲区大小
,返回异常。接收缓冲区设置:如果
主机请求的报文长度
大于设置的接收缓冲区大小
,返回异常。这个是合理的,小内存 MCU 做从机肯定是需要对某些功能码做限制的。