随着近年来物联网行业的迅速发展,工业物联网领域也成为了最大子领域之一。另外的领域包括运输业物联网、基础设施物联网、消费者物联网。
受制于体积、功耗、成本等因素,一部分设备无法直接接入物联网服务。对于这种设备,目前行业的解决方案通常是单独设置一个网关设备,无法直接接入网络的设备通过有线连接到网关,通过一定的协议将数据通过网关转发到上层网络。
这种连接方式和协议一起叫做现场总线(Field bus),现场总线的协议和一般的单片机常用UART、I2C、SPI、SDMMC等协议不同。现场总线协议要求更高的容错纠错率、抗干扰和易部署性,通常现场总线的长度都在几十米以上,普通的UART、I2C、SPI协议无法在这么长的长度上进行工作。
常用的线程总线协议有:
1.Modbus
2.CAN (常用于汽车)
3.Foundation Fieldbus
如果通过OSI七层网络模型来说的话,Modbus协议仅仅位于第二层:数据链路层。
因为处于的层次非常低,几乎不涉及到其他协议(实际上还涉及到串口UART协议,后面会讲),所以Modbus协议非常的单纯,几乎只是把物理层的电信号进行了一下封装。
当然这也不意味着Modbus协议就非常简单,实际上,如果你大学是学计算机专业的,一定学过一门叫做计算机网络的课,这门课通常的课时是50个小时左右,而这门课的内容大部分是在讲处于应用层的TCP/UDP协议。所以理论上来讲,学习Modbus协议至少也要20个小时的课时吧。(所以前两遍学不会也不要慌张,很正常O(∩_∩)O哈哈~)
废话少说,言归正传。看一下下面这张图:
上图表示的是:Modbus线上的信号是0/1的形式,Modbus协议会将多个0/1信号进行划分和组合,形成一个Modbus帧。一个Modbus帧就是一次Modbus请求或者Modbus回应。
实际上,Modbus还利用了串口UART协议,可以理解为Modbus是建立在UART协议之上的协议。但是由于其实在是不复杂,所以我个人认为还是把其定义为数据链路层比较OK。因此Modbus中也有波特率、数据位、校验位、停止位的概念,Modbus协议只取数据位作为数据。 由于串口协议数据位通常是8位,所以Modbus帧的数据划分也是以8位、16位这样进行划分的。
Modbus和http协议很像,一次请求,一次回应。
只能主机向从机发送一次Modbus请求,然后从机响应一次Modbus回应。
从机不能主动向主机发送任何信息。
Modbus协议还定义了寄存器的概念。Modbus主机向从机发送的查询请求帧的数据部分,并不能像tcp/http协议那样自定义数据内容,而是只能是固定的格式:16位寄存器起始地址➕16位寄存器数量。
同时从机也只会固定的按照这种格式去解析来自主机的请求。解析请求之后,一般会提供给开发者一个这样的回调函数:
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
开发者需要手动重写这个函数,来返回给主机期望的数据。
总而言之,可以理解Modbus请求还在应用层帮我们定义了一层寄存器的概念,所有Modbus数帧都必须按照寄存器的概念来发送/响应数据。这样加强了Modbus协议的通用性,同时我们在开发时也不用自己去想应用层的协议了。
主机向一台设备地址为3的从机读取输入寄存器0~输入寄存器10的数据。波特率9600,数据位8,校验位无,停止位1:
首先主机会向从机发送一个起始信号,起始信号持续一定时间(3.5个字符周期)
然后开始发送数据,通过UART串口,依次发送以下电信号:
(注意串口是高位在前低位在后,最后的1代表1位停止位)
11000001 (数据为0x03) // 代表设备地址为3
00100001 (数据为0x04) // 代表功能码为0x04,读输入寄存器
00000001 (数据为0x00) //
00000001 (数据为0x00) // 和上一数据一起代表寄存器起始地址为0
00000001 (数据为0x00) //
01010001 (数据为0x0A) // 和上一数据一起代表寄存器数量为10
111101111 (数据为0xEF) //
100011101 (数据为0x71) // 和上一数据一起代表CRC校验码
(转换为16进制顺序为:03 04 00 00 00 0A EF 71)
你甚至可以通过串口直接发送 0x03 0x04 0x00 0x00 0x00 0x0A 0xEF 0x71,但是实际上你会发现不行,因为Modbus协议在各种地方还做了一些时序的限制,比如3.5字符周期的起始信号、1.5字符周期的数据间隔等。
3.从机收到之后,会对数据进行校验,校验的内容包括但不限于:设备地址是否是本机地址、CRC校验码是否正确等
4.校验如果通过,则会回复给主机相应的数据,数据格式和上面大同小异。这里就不累述了(写起太累啦)。
看了我上面的介绍,你是不是会觉得好像自己手写一个Modbus请求也不是特别困难,百八十行代码就能解决。确实是这样,但是=你还需要考虑超时、接收数据CRC校验、总线冲突等一系列问题,所以再加上这些内容就不只几百行代码能搞定了,所以我们还是使用现成的Modbus协议栈。一般来说Modbus协议都跑在RTOS操作系统之上。
目前有的Modbus协议栈有,在github上搜一搜:
https://github.com/search?l=C&o=desc&q=modbus&s=stars&type=Repositories
搜出来start多且是用C语言写的有如下几个
接下来我们选择 FreeModbus_Slave-Master-RTT-STM32 进行开发。上面有说到,FreeModbus_Slave-Master-RTT-STM32 的作者是rtthread的主要作者,所以和rtthread有不潜的py关系,甚至直接提供了一套在rtthread上的移植。
使用的开发板为淘宝f103开发板
:淘宝链接
HardFault_Handler、PendSV_Handler、SysTick_Handler
三个中断函数由于Modbus协议基于RT-thread,所以需要先稍稍修改一下RT-thread:
board.c/rt_hw_board_init
函数中对MCU进行初始化,更改的后的rt_hw_board_init函数如下:#include "main.h"
extern void SystemClock_Config(void);
extern void MX_GPIO_Init(void);
extern void MX_USART1_UART_Init(void);
extern UART_HandleTypeDef huart1;
/* 调试串口1接收数据的消息队列buffer */
static uint8_t consoleInputBuffer[256];
struct rt_messagequeue consoleInputMQ;
void rt_hw_board_init()
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
/* 使用串口1作为调试串口,初始化一个消息队列保存串口1接收到的数据,并手动开启串口中断 */
rt_err_t error = rt_mq_init(&consoleInputMQ,"consoleInputMQ",consoleInputBuffer,
1,sizeof(consoleInputBuffer),RT_IPC_FLAG_FIFO);
RT_ASSERT(error == RT_EOK);
SET_BIT(huart1.Instance->CR1, USART_CR1_PEIE | USART_CR1_RXNEIE);
/* System Clock Update */
SystemCoreClockUpdate();
/* System Tick Configuration */
_SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
/* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
}
2.增加一个打印输出的函数 rt_hw_console_output
,位置随意,代码如下:
extern UART_HandleTypeDef huart1;
void rt_hw_console_output(const char *str)
{
rt_size_t i = 0, size = 0;
char a = '\r';
__HAL_UNLOCK(&huart1);
size = rt_strlen(str);
for (i = 0; i < size; i++)
{
if (*(str + i) == '\n')
{
HAL_UART_Transmit(&huart1, (uint8_t *)&a, 1, 50);
}
HAL_UART_Transmit(&huart1, (uint8_t *)(str + i), 1, 50);
}
}
3.增加一个接收输入的函数rt_hw_console_getchar
,位置随意,代码如下:
char rt_hw_console_getchar(void){
char ch = 0;
rt_mq_recv(&consoleInputMQ, &ch, 1, 500);
return ch;
}
4.串口1的中断函数中:
#include "rtthread.h"
extern struct rt_messagequeue consoleInputMQ;
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
if(huart1.Instance->SR & USART_SR_RXNE)
{
uint8_t data = (uint8_t)(huart1.Instance->DR & (uint8_t)0x00FF);
rt_mq_send(&consoleInputMQ, &data, 1);
}
/* USER CODE END USART1_IRQn 0 */
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
5.rtconfig.h
中取消注释event和messagequeue:
/*rtconfig.h*/
// Using Event
// Using Event
#define RT_USING_EVENT
//
// Using Message Queue
// Using Message Queue
#define RT_USING_MESSAGEQUEUE
//
//
6.在main.c
中增加一个main函数:
int main(void) {
while(1) {
rt_thread_mdelay(500);
__NOP();
}
}
最后编译运行,在串口输出中应该能看到如下输出:
先点击这里,添加一个Group
按F2对Group重命名为MODBUS
往MODBUS组里面添加源文件
由于这里我们仅仅需要实现MODBUS主机功能,所以只需要添加10个源文件:
"./FreeModbus/modbus/mb_m.c"
"./FreeModbus/modbus/rtu/mbcrc.c"
"./FreeModbus/modbus/rtu/mbrtu_m.c"
"./FreeModbus/modbus/functions/mbfuncother.c"
"./FreeModbus/modbus/functions/mbfuncinput_m.c"
"./FreeModbus/modbus/functions/mbutils.c"
"./FreeModbus/port/rtt/port.c"
"./FreeModbus/port/rtt/portevent_m.c"
"./FreeModbus/port/rtt/portserial_m.c"
"./FreeModbus/port/rtt/porttimer_m.c"
在option-include Paths中添加三个路径:
"-IFreeModbus/port",
"-IFreeModbus/modbus/include",
"-IFreeModbus/modbus/rtu"
由于官方对于rtthread的移植默认采用的是rtthread的bsp框架,bsp框架接入比较复杂,暂且不讨论,这里我们需要把bsp的函数替换成我们的函数,位置在 portserial_m.c
中,
主要是把rtthread bsp相关的代码注释掉,替换成手动调用HAL库,整个文件改动比较大,注释部分为原有代码,见下面:
/*
* FreeModbus Libary: RT-Thread Port
* Copyright (C) 2013 Armink
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* File: $Id: portserial_m.c,v 1.60 2013/08/13 15:07:05 Armink add Master Functions $
*/
#include "port.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
// #include "rtdevice.h"
// #include "bsp.h"
#include "main.h"
#if MB_MASTER_RTU_ENABLED > 0 || MB_MASTER_ASCII_ENABLED > 0
/* ----------------------- Static variables ---------------------------------*/
ALIGN(RT_ALIGN_SIZE)
/* software simulation serial transmit IRQ handler thread stack */
static rt_uint8_t serial_soft_trans_irq_stack[512];
/* software simulation serial transmit IRQ handler thread */
static struct rt_thread thread_serial_soft_trans_irq;
/* serial event */
static struct rt_event event_serial;
/* modbus master serial device */
// static rt_serial_t *serial;
/**/
extern UART_HandleTypeDef huart2;
static uint8_t uartRecvBuffer[64];
static uint8_t * uartRecvBufferPtr = uartRecvBuffer;
/* ----------------------- Defines ------------------------------------------*/
/* serial transmit event */
#define EVENT_SERIAL_TRANS_START (1<<0)
/* ----------------------- static functions ---------------------------------*/
static void prvvUARTTxReadyISR(void);
static void prvvUARTRxISR(void);
static rt_err_t serial_rx_ind(rt_size_t size);
static void serial_soft_trans_irq(void* parameter);
extern void MX_USART2_UART_Init(void);
/* ----------------------- Start implementation -----------------------------*/
BOOL xMBMasterPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits,
eMBParity eParity)
{
// /**
// * set 485 mode receive and transmit control IO
// * @note MODBUS_MASTER_RT_CONTROL_PIN_INDEX need be defined by user
// */
// rt_pin_mode(MODBUS_MASTER_RT_CONTROL_PIN_INDEX, PIN_MODE_OUTPUT);
// /* set serial name */
// if (ucPORT == 1) {
// #if defined(RT_USING_UART1) || defined(RT_USING_REMAP_UART1)
// extern struct rt_serial_device serial1;
// serial = &serial1;
// #endif
// } else if (ucPORT == 2) {
// #if defined(RT_USING_UART2)
// extern struct rt_serial_device serial2;
// serial = &serial2;
// #endif
// } else if (ucPORT == 3) {
// #if defined(RT_USING_UART3)
// extern struct rt_serial_device serial3;
// serial = &serial3;
// #endif
// }
// /* set serial configure parameter */
// serial->config.baud_rate = ulBaudRate;
// serial->config.stop_bits = STOP_BITS_1;
// switch(eParity){
// case MB_PAR_NONE: {
// serial->config.data_bits = DATA_BITS_8;
// serial->config.parity = PARITY_NONE;
// break;
// }
// case MB_PAR_ODD: {
// serial->config.data_bits = DATA_BITS_9;
// serial->config.parity = PARITY_ODD;
// break;
// }
// case MB_PAR_EVEN: {
// serial->config.data_bits = DATA_BITS_9;
// serial->config.parity = PARITY_EVEN;
// break;
// }
// }
// /* set serial configure */
// serial->ops->configure(serial, &(serial->config));
// /* open serial device */
// if (!serial->parent.open(&serial->parent,
// RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX )) {
// serial->parent.rx_indicate = serial_rx_ind;
// } else {
// return FALSE;
// }
/* 手动调用HAL库 初始化串口,并开启串口中断 */
MX_USART2_UART_Init();
SET_BIT(huart2.Instance->CR1, USART_CR1_PEIE | USART_CR1_RXNEIE);
/* software initialize */
rt_event_init(&event_serial, "master event", RT_IPC_FLAG_PRIO);
rt_thread_init(&thread_serial_soft_trans_irq,
"master trans",
serial_soft_trans_irq,
RT_NULL,
serial_soft_trans_irq_stack,
sizeof(serial_soft_trans_irq_stack),
10, 5);
rt_thread_startup(&thread_serial_soft_trans_irq);
return TRUE;
}
void vMBMasterPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{
rt_uint32_t recved_event;
if (xRxEnable)
{
/* enable RX interrupt */
// serial->ops->control(serial, RT_DEVICE_CTRL_SET_INT, (void *)RT_DEVICE_FLAG_INT_RX);
// /* switch 485 to receive mode */
// rt_pin_write(MODBUS_MASTER_RT_CONTROL_PIN_INDEX, PIN_LOW);
HAL_NVIC_EnableIRQ(USART2_IRQn);
}
else
{
/* switch 485 to transmit mode */
// rt_pin_write(MODBUS_MASTER_RT_CONTROL_PIN_INDEX, PIN_HIGH);
// /* disable RX interrupt */
// serial->ops->control(serial, RT_DEVICE_CTRL_CLR_INT, (void *)RT_DEVICE_FLAG_INT_RX);
HAL_NVIC_DisableIRQ(USART2_IRQn);
}
if (xTxEnable)
{
/* start serial transmit */
rt_event_send(&event_serial, EVENT_SERIAL_TRANS_START);
}
else
{
/* stop serial transmit */
rt_event_recv(&event_serial, EVENT_SERIAL_TRANS_START,
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, 0,
&recved_event);
}
}
void vMBMasterPortClose(void)
{
// serial->parent.close(&(serial->parent));
}
BOOL xMBMasterPortSerialPutByte(CHAR ucByte)
{
// serial->parent.write(&(serial->parent), 0, &ucByte, 1);
HAL_UART_Transmit(&huart2, (uint8_t*)&ucByte, 1, 1);
return TRUE;
}
BOOL xMBMasterPortSerialGetByte(CHAR * pucByte)
{
// serial->parent.read(&(serial->parent), 0, pucByte, 1);
*pucByte = *uartRecvBufferPtr--;
return TRUE;
}
/*
* Create an interrupt handler for the transmit buffer empty interrupt
* (or an equivalent) for your target processor. This function should then
* call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that
* a new character can be sent. The protocol stack will then call
* xMBPortSerialPutByte( ) to send the character.
*/
void prvvUARTTxReadyISR(void)
{
pxMBMasterFrameCBTransmitterEmpty();
}
/*
* Create an interrupt handler for the receive interrupt for your target
* processor. This function should then call pxMBFrameCBByteReceived( ). The
* protocol stack will then call xMBPortSerialGetByte( ) to retrieve the
* character.
*/
void prvvUARTRxISR(void)
{
pxMBMasterFrameCBByteReceived();
}
/**
* Software simulation serial transmit IRQ handler.
*
* @param parameter parameter
*/
static void serial_soft_trans_irq(void* parameter) {
rt_uint32_t recved_event;
while (1)
{
/* waiting for serial transmit start */
rt_event_recv(&event_serial, EVENT_SERIAL_TRANS_START, RT_EVENT_FLAG_OR,
RT_WAITING_FOREVER, &recved_event);
/* execute modbus callback */
prvvUARTTxReadyISR();
}
}
/**
* This function is serial receive callback function
*
* @param dev the device of serial
* @param size the data size that receive
*
* @return return RT_EOK
*/
static rt_err_t serial_rx_ind(rt_size_t size) {
prvvUARTRxISR();
return RT_EOK;
}
/* 手动声明串口中断函数 */
void USART2_IRQHandler(void) {
uint32_t isrflags = READ_REG(huart2.Instance->SR);
if((isrflags & USART_SR_RXNE) != RESET ) {
*++uartRecvBufferPtr = huart2.Instance->DR;
serial_rx_ind(0);
}
}
#endif
还需要在 mbconfig.h
中关闭没有用到的功能函数,这里我们只用到了input功能。所以把input后面的宏定义值都改为0.
#define MB_FUNC_OTHER_REP_SLAVEID_BUF ( 32 )
/*! \brief If the Report Slave ID function should be enabled. */
#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED ( 1 )
/*! \brief If the Read Input Registers function should be enabled. */
#define MB_FUNC_READ_INPUT_ENABLED ( 1 )
/*! \brief If the Read Holding Registers function should be enabled. */
#define MB_FUNC_READ_HOLDING_ENABLED ( 0 )
/*! \brief If the Write Single Register function should be enabled. */
#define MB_FUNC_WRITE_HOLDING_ENABLED ( 0 )
/*! \brief If the Write Multiple registers function should be enabled. */
#define MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED ( 0 )
/*! \brief If the Read Coils function should be enabled. */
#define MB_FUNC_READ_COILS_ENABLED ( 0 )
/*! \brief If the Write Coils function should be enabled. */
#define MB_FUNC_WRITE_COIL_ENABLED ( 0 )
/*! \brief If the Write Multiple Coils function should be enabled. */
#define MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED ( 0 )
/*! \brief If the Read Discrete Inputs function should be enabled. */
#define MB_FUNC_READ_DISCRETE_INPUTS_ENABLED ( 0 )
/*! \brief If the Read/Write Multiple Registers function should be enabled. */
#define MB_FUNC_READWRITE_HOLDING_ENABLED ( 0 )
最后还需要在 port.h
中修改一下:
//#include
#include "main.h"
在main.c
中添加测试代码:
/* 启动两个线程,一个线程用于MODBUS轮询 */
#include "mb.h"
#include "mb_m.h"
#include "user_mb_app.h"
/* MODBUS需要额外使用一个线程*/
static uint8_t modbusStatck[256] = {
0};
static struct rt_thread modbusThread;
USHORT usMRegInStart = M_REG_INPUT_START;
USHORT usMRegInBuf[MB_MASTER_TOTAL_SLAVE_NUM][M_REG_INPUT_NREGS];
void modbusSend(void* param) {
while(1) {
eMBMasterPoll();
rt_thread_mdelay(500);
}
}
int main(void) {
eMBMasterInit(MB_RTU, 1, 9600, 0);
eMBMasterEnable();
rt_err_t ret = rt_thread_init(&modbusThread, "modbus", modbusSend, NULL, modbusStatck,
sizeof(modbusStatck), 9, 10);
if(ret!=RT_EOK) {
Error_Handler();
}
rt_thread_startup(&modbusThread);
while(1) {
rt_thread_mdelay(3000);
eMBMasterReqReadInputRegister(1, 0, 10, 1000);
rt_kprintf("MODBUS read input regitster 0 value:%d", usMRegInBuf[0][0]);
}
}
/* MODBUS读取回调函数 */
eMBErrorCode eMBMasterRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
eMBErrorCode eStatus = MB_ENOERR;
USHORT iRegIndex;
USHORT * pusRegInputBuf;
USHORT REG_INPUT_START;
USHORT REG_INPUT_NREGS;
USHORT usRegInStart;
pusRegInputBuf = usMRegInBuf[ucMBMasterGetDestAddress() - 1];
REG_INPUT_START = M_REG_INPUT_START;
REG_INPUT_NREGS = M_REG_INPUT_NREGS;
usRegInStart = usMRegInStart;
/* it already plus one in modbus function method. */
usAddress--;
if ((usAddress >= REG_INPUT_START)
&& (usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS))
{
iRegIndex = usAddress - usRegInStart;
while (usNRegs > 0)
{
pusRegInputBuf[iRegIndex] = *pucRegBuffer++ << 8;
pusRegInputBuf[iRegIndex] |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}
然后找个MODBUS从机模拟器:个人推荐这个 https://github.com/ClassicDIY/ModbusTool
硬件连接如下最终项目代码:https://github.com/jiladahe1997/CSDN_Modbus_port_demo