STM32CubeMX | Modbus RTU 主机协议栈实现
目录
示例代码下载:https://gitee.com/jhembedded/HAL_STM32_ModbusMaster_Demo
1、前言
~~~~~~~~ modbus rtu在嵌入式方面非常的常见和使用,嵌入式linux中可以使用libmodbus这个库,但是对于嵌入式单片机,开源的有FreeModbus这个库,但是只是从机,对于modbus rtu主机的实现,网上却找不到开源的库,或者找到了但是不方便移植,使用者想要去使用还要去搞明白是怎么实现的,本博客基于以上原因,实现了一套modbus rtu主机协议栈。
本主机协议栈优点如下:
- 接口明确清晰,使用者无需关心协议栈内部实现
- 面向对象编程思想,使用C语言的struct作为一个modbus rtu主机的控制接口,此方法的好处是可以灵活的实现多个主机,例如:实现一个多主机的modbus pdu。
- 支持RTOS
- 可搭配FreeModbus协议栈无缝使用
- 移植简单、可很方便的移植到其他单片机如GD32、MM32等
- 源码简单、只有一个头文件、一个源文件、一个移植接口示例文件
2、协议栈API介绍
2.1 控制结构
typedef struct
{
//
// 收发数据缓存
//
uint8_t ucBuf[128];
//
// 收发数据状态
//
uint16_t usStatus;
//
// 如果使用了RTOS需要进行互斥,那么需要实现以下两个函数的绑定
//
void (*lock)(void);
void (*unlock)(void);
//
// 微秒延时函数,用于等待超时
//
void (*delayms)(uint32_t nms);
//
// 定时器启动和停止函数
//
void (*timerStop)(void);
void (*timerStart)(void);
//
// 发送数据函数,可以是串口、TCP等
//
uint32_t (*sendData)(const void* buf, uint32_t len);
}MBRTUMaterTypeDef;
2.2 主机读线圈状态(CMD1)
/**
* 主机读取线圈状态
* @param ucSlaveAddress 从机地址
* @param usAddress 要读取的线圈起始地址
* @param usNum 要读取的线圈数量
* @param usTimeout 超时时间,单位毫秒
* @param pucCoilsBuffer 存储读取到的线圈状态,一个字节代表一个线圈状态,值范围:0/1
* @return 0:成功 <0:执行失败
*/
int MBRTUMasterReadCoils(MBRTUMaterTypeDef* psModbus, uint8_t ucSlaveAddress, uint16_t usAddress, uint16_t usNum, uint16_t usTimeout, uint8_t* pucCoilsBuffer)
2.2 主机读离散量输入(CMD2)
/**
* 主机读取离散量输入
* @param ucSlaveAddress 从机地址
* @param usAddress 要读取的离散量起始地址
* @param usNum 要读取的离散量数量
* @param usTimeout 超时时间,单位毫秒
* @param pucDiscBuffer 存储读取到的离散量输入状态,一个字节代表一个离散量的状态,值范围:0/1
* @return 0:成功 <0:执行失败
*/
int MBRTUMasterReadDiscreteInputs(MBRTUMaterTypeDef* psModbus, uint8_t ucSlaveAddress, uint16_t usAddress, uint16_t usNum, uint16_t usTimeout, uint8_t* pucDiscBuffer)
2.2 主机读保持寄存器(CMD3)
/**
* 主机读取保持寄存器
* @param ucSlaveAddress 从机地址
* @param usAddress 要读取的保持寄存器起始地址
* @param usNum 要读取的保持寄存器数量
* @param usTimeout 超时时间,单位毫秒
* @param pusRegBuffer 存储读取到的寄存器值
* @return 0:成功 <0:执行失败
*/
int MBRTUMasterReadHoldingRegisters(MBRTUMaterTypeDef* psModbus, uint8_t ucSlaveAddress, uint16_t usAddress, uint16_t usNum, uint16_t usTimeout, uint16_t* pusRegBuffer)
2.2 主机读输入寄存器(CMD4)
/**
* 主机读取输入寄存器
* @param ucSlaveAddress 从机地址
* @param usAddress 要读取的输入寄存器起始地址
* @param usNum 要读取的输入寄存器数量
* @param usTimeout 超时时间,单位毫秒
* @param pusRegBuffer 存储读取到的寄存器值
* @return 0:成功 <0:执行失败
*/
int MBRTUMasterReadInputRegisters(MBRTUMaterTypeDef* psModbus, uint8_t ucSlaveAddress, uint16_t usAddress, uint16_t usNum, uint16_t usTimeout, uint16_t* pusRegBuffer)
2.2 主机写单个线圈(CMD5)
2.2 主机写单个寄存器(CMD6)
2.2 主机写多个线圈(CMD15)
2.2 主机写多个寄存器(CMD16)
3、移植前的基础工程生成
基础工程这里我使用STM32CubeMX生成,使用的是STM32F103C8单片机,配置步骤如下,首先将时钟配置到72M:
配置串口1用于调试打印,配置串口3用于modbus主机通信:
配置用于检测3.5个字符超时时间的定时器,我配置成了5ms超时。
这里需要跟你实际使用的波特率进行超时时间的计算,以:波特率9600、8bit数据位、1bit停止位,奇校验、无流控为例,那么1s内就可以传输9600bits÷(8+1+1)=960bytes,那么3.5个字节的时间就是1000ms÷960×3.5≈3.65ms,所以,我设置5ms的超时时间是没有问题的。
开启定时器和串口中断,注意:串口的中断要比定时器中断等级高:
最后输出工程就可以了:
4、移植主机协议栈
主机协议栈源码就只有三个文件:
其中,mbrtu_master.h
和mbrtu_master.c
是协议栈实现,无需动,mbrtu_master_example.c
是移植参考示例。
下面讲解一下移植过程。
首先定义一个modbus主机的全局控制结构并初始化:
MBRTUMaterTypeDef MBRTUHandle =
{
.delayms = delayms,
.timerStart = timerStart,
.timerStop = timerStop,
.sendData = sendData,
#ifdef USE_RTOS // 使用了RTOS那么需要实现互斥
.lock = mutex_lock,
.unlock = mutex_unlock,
#endif
};
注意:如果使用了实时系统,需要实现lock和unlock函数。
结构体中的函数实现如下:
#ifdef USE_RTOS
static void mutex_lock(void)
{
}
static void mutex_unlock(void)
{
}
#endif
static void timerStop(void)
{
HAL_TIM_Base_Stop_IT(&htim3);
}
static void timerStart(void)
{
__HAL_TIM_SET_COUNTER(&htim3, 0);
HAL_TIM_Base_Start_IT(&htim3);
}
static void delayms(uint32_t nms)
{
#ifdef USE_RTOS
osDelay(nms);
#else
HAL_Delay(nms);
#endif
}
static uint32_t sendData(const void* buf, uint32_t len)
{
if(HAL_UART_Transmit(&huart3, (uint8_t *)buf, len, 100) != HAL_OK)
{
len = 0;
}
return len;
}
将MBRTUMasterTimerISRCallback
函数放置于定时器中断函数中,对于HAL库那就是这样的:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == htim3.Instance)
{
MBRTUMasterTimerISRCallback(&MBRTUHandle);
}
}
将MBRTUMasterRecvByteISRCallback
函数放置于串口中断函数中,对于HAL库那就是这样的:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == huart3.Instance)
{
MBRTUMasterRecvByteISRCallback(&MBRTUHandle, g_Uart3RxByte);
HAL_UART_Receive_IT(&huart3, &g_Uart3RxByte, 1); // 注册接收
}
}
重定向printf到串口1:
int fputc(int ch, FILE* fp)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100);
return ch;
}
至此,就移植完毕了,测试函数如下:
int ret;
uint8_t ucBuf[10];
uint16_t usBuf[10];
int main_example(void)
{
// 定时器初始化,设置为3.5个字符的超时时间
// Timer_Init();
// 串口初始化,初始化波特率等
// UART_Init();
// 写单个线圈
ret = MBRTUMasterWriteSingleCoil(&MBRTUHandle, 1, 0, 1, 500);
printf(" write single coil %s. \r\n", ret < 0 ? "failed" : "ok");
HAL_Delay(100);
// 写单个寄存器
ret = MBRTUMasterWriteSingleRegister(&MBRTUHandle, 1, 0, 0XAABB, 500);
printf(" write single reg %s. \r\n", ret < 0 ? "failed" : "ok");
HAL_Delay(100);
// 写多个线圈
memset(ucBuf, 0X01, 10);
ret = MBRTUMasterWriteMultipleCoils(&MBRTUHandle, 1, 0, 10, ucBuf, 500);
printf(" write coils %s. \r\n", ret < 0 ? "failed" : "ok");
HAL_Delay(100);
// 写多个寄存器
memset(usBuf, 0XFF, 20);
ret = MBRTUMasterWriteMultipleRegisters(&MBRTUHandle, 1, 0, 10, usBuf, 500);
printf(" write regs %s. \r\n", ret < 0 ? "failed" : "ok");
HAL_Delay(100);
// 读线圈
MBRTUMasterReadCoils(&MBRTUHandle, 1, 0, 10, 500, ucBuf);
printf(" read coils %s. \r\n", ret < 0 ? "failed" : "ok");
HAL_Delay(100);
// 读离散量输入
MBRTUMasterReadDiscreteInputs(&MBRTUHandle, 1, 0, 10, 500, ucBuf);
printf(" read discs %s. \r\n", ret < 0 ? "failed" : "ok");
HAL_Delay(100);
// 读保持寄存器
MBRTUMasterReadHoldingRegisters(&MBRTUHandle, 1, 0, 10, 500, usBuf);
printf(" read hold regs %s. \r\n", ret < 0 ? "failed" : "ok");
HAL_Delay(100);
// 读输入寄存器
MBRTUMasterReadInputRegisters(&MBRTUHandle, 1, 0, 10, 500, usBuf);
printf(" read input regs %s. \r\n", ret < 0 ? "failed" : "ok");
HAL_Delay(100);
return 0;
}
5、移植测试验证
移植完毕了现在需要测试,测试你可以使用MobusSlave软件模拟测试,也可以选用选用FreeModbus作为从机,关于FreeModbus从机移植使用可以参考我的另一篇博客:STM32CubeMX | STM32 HAL库移植FreeModbus详细步骤。