STM32CubeMX | Modbus RTU 主机协议栈实现
本章博客涉及代码,关注以下公众号,回复关键字mbrtu_master 获取下载链接!
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;
void (*lock)(void);
void (*unlock)(void);
void (*delayms)(uint32_t nms);
void (*timerStop)(void);
void (*timerStart)(void);
uint32_t (*sendData)(const void* buf, uint32_t len);
void (*readCoilsCallback)(uint16_t usStartAddr, uint16_t usNum, const uint8_t* pucBitsOfCoilsState, uint16_t usLen);
void (*readDiscreteInputsCallback)(uint16_t usStartAddr, uint16_t usNum, const uint8_t* pucBitsOfDiscreteInputsState, uint16_t usLen);
void (*readHoldingRegistersCallback)(uint16_t usStartAddr, uint16_t usNum, const uint16_t* pusHoldingRegistersVal, uint16_t usLen);
void (*readInputRegistersCallback)(uint16_t usStartAddr, uint16_t usNum, const uint16_t* pusInputRegistersVal, uint16_t usLen);
}MBRTUMaterTypeDef;
2.2 主机读线圈状态(CMD1)
2.2 主机读离散量输入(CMD2)
2.2 主机读保持寄存器(CMD3)
2.2 主机读输入寄存器(CMD4)
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,
.readCoilsCallback = readCoilsCallback,
.readDiscreteInputsCallback = readDiscreteInputsCallback,
.readHoldingRegistersCallback = readHoldingRegistersCallback,
.readInputRegistersCallback = readInputRegistersCallback,
#ifdef USE_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;
}
static void readCoilsCallback(uint16_t usStartAddr, uint16_t usNum, const uint8_t* pucBitsOfCoilsState, uint16_t usLen)
{
uint8_t ucLoops = (usNum - 1) / 8 + 1;
uint8_t ucState, ucBits;
printf(" Read %d coils starting at start address %d: ", usNum, usStartAddr);
while(ucLoops != 0)
{
ucState = *pucBitsOfCoilsState++;
ucBits = 0;
while(usNum != 0 && ucBits < 8)
{
printf("%d ", ucState & 0X01 ? 1 : 0);
ucState >>= 1;
usNum--;
ucBits++;
}
ucLoops--;
}
printf("\r\n");
}
static void readDiscreteInputsCallback(uint16_t usStartAddr, uint16_t usNum, const uint8_t* pucBitsOfDiscreteInputsState, uint16_t usLen)
{
uint8_t ucLoops = (usNum - 1) / 8 + 1;
uint8_t ucState, ucBits;
printf(" Read %d discrete inputs starting at start address %d: ", usNum, usStartAddr);
while(ucLoops != 0)
{
ucState = *pucBitsOfDiscreteInputsState++;
ucBits = 0;
while(usNum != 0 && ucBits < 8)
{
printf("%d ", ucState & 0X01 ? 1 : 0);
ucState >>= 1;
usNum--;
ucBits++;
}
ucLoops--;
}
printf("\r\n");
}
static void readHoldingRegistersCallback(uint16_t usStartAddr, uint16_t usNum, const uint16_t* pusHoldingRegistersVal, uint16_t usLen)
{
uint16_t val;
printf(" Read %d hold registers starting at start address %d: ", usNum, usStartAddr);
while(usLen--)
{
val = *pusHoldingRegistersVal++;
val = ((val & 0X00FF) << 8) | ((val & 0XFF00) >> 8);
printf("%04X ", val);
}
printf("\r\n");
}
static void readInputRegistersCallback(uint16_t usStartAddr, uint16_t usNum, const uint16_t* pusInputRegistersVal, uint16_t usLen)
{
uint16_t val;
printf(" Read %d input registers starting at start address %d: ", usNum, usStartAddr);
while(usLen--)
{
val = *pusInputRegistersVal++;
val = ((val & 0X00FF) << 8) | ((val & 0XFF00) >> 8);
printf("%04X ", val);
}
printf("\r\n");
}
将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;
}
至此,就移植完毕了,main函数如下:
5、移植测试验证
移植完毕了现在需要测试,测试我选用FreeModbus作为从机,关于FreeModbus从机移植使用可以参考我的另一篇博客:STM32CubeMX | STM32 HAL库移植FreeModbus详细步骤
首先启动从机,然后启动主机,调试打印如下:
|