本帖最后由 望风阁 于 2024-5-15 16:58 编辑
一、外设介绍
1.1、MODBUS 标准简介
Modbus 是由 Modicon(现为施耐德电气公司的一个品牌)在 1979 年发明的,是全球第一个真正用于工业现场的总线协议。
ModBus 网络是一个工业通信系统,由带智能终端的可编程序控制器和计算机通过公用线路或局部专用线路连接而成。其系统结构既包括硬件、亦包括软件。它可应用于各种数据采集和过程监控。
为更好地普及和推动 Modbus 在基于以太网上的分布式应用,目前施耐德公司已将 Modbus 协议的所有权移交给 IDA(Interface for Distributed Automation,分布式自动化接口)组织,并成立了Modbus-IDA 组织,为 Modbus 今后的发展奠定了基础。
在中国,Modbus 已经成为国家标准。 标准编号:GB/T19582-2008 标准名称:《基于 Modbus 协议的工业自动化网络规范》 分 3 个部分:
《GB/T 19582.1-2008 第 1 部分:Modbus 应用协议》
《GB/T 19582.2-2008 第 2 部分:Modbus 协议在串行链路上的实现指南》
《GB/T 19582.3-2008 第 3 部分: Modbus 协议在 TCP/IP 上的实现指南》
本教程仅涉及第 1 部分和第 2 部分,串行链路仅介绍 RS485 网络。
1.2、MODBUS 协议概述
按照 7 层 OSI 通信模型,Modbus 标准包括应用层、数据链路层、物理层。
Modbus 串行链路协议是一个主/从协议。该协议位于 OSI 模型的第二层。 一个主从类型的系统有一个向某个“子”节点发出显式命令并处理响应的节点(主节点)。典型的子节点在没有收到主节点的请求时并不主动发送数据,也不与其它子节点通信。 在物理层,Modbus 串行链路系统可以使用不同的物理接口(RS485、RS232)。最常用的是TIA/EIA-485 (RS485) 两线制接口。 1.3、Modbus 主站/从站协议原理 Modbus 串行链路协议是一个主-从协议。在同一时刻,只有一个主节点连接于总线,一个或多个子节点 (最大编号为 247 ) 连接于同一个串行总线。Modbus 通信总是由主节点发起。子节点在没有收到来自主节点的请求时,从不会发送数据。子节点之间从不会互相通信。主节点在同一时刻只会发起一个Modbus 事务处理。 主节点以两种模式对子节点发出 Modbus 请求:
单播模式 主节点以特定地址访问某个子节点,子节点接到并处理完请求后,子节点向主节点返回一个报文(一个 '应答')。在这种模式,一个 Modbus 事务处理包含 2 个报文:一个来自主节点的请求,一个来自子节点的应答。 每个子节点必须有唯一的地址 (1 到 247),这样才能区别于其它节点被独立的寻址。
广播模式 主节点向所有的子节点发送请求。对于主节点广播的请求没有应答返回。广播请求一般用于写命令。所有设备必须接受广播模式的写功能。地址 0是专门用于表示广播数据的。
0 | 1 ~ 247 | 248 ~ 255 | 广播地址 | 子节点单独地址 | 保留 |
Modbus 主节点没有地址,只有子节点必须有一个地址。 该地址必须在 Modbus 串行总线上唯一。
在 Modbus 串行链路,地址域只含有子节点地址。 如前文所述,合法的子节点地址为十进制 0 – 247。 每个子设备被赋予 1 – 247 范围中的地址。 主节点通过将子节点的地址放到报文的地址域对子节点寻址。当子节点返回应答时,它将自己的地址放到应答报文的地址域以让主节点知道哪个子节点在回答。
功能码指明服务器要执行的动作。功能码后面可跟有表示含有请求和响应参数的数据域。
错误检验域是对报文内容执行 "冗余校验" 的计算结果。根据不同的传输模式 (RTU or ASCII) 使用两种不同的计算方法。
1.5、RTU 传输模式
有两种串行传输模式被定义: RTU 模式 和 ASCII 模式。 它定义了报文域的位内容在线路上串行的传送。它确定了信息如何打包为报文和解码。 Modbus 串行链路上所有设备的传输模式 (和串行口参数) 必须相同。尽管在特定的领域 ASCII 模式是要求的,但达到 Modbus 设备之间的互操作性只有每个设备都有相同的模式: 所有设备必须实现 RTU 模式。ASCII 传输模式是选项,鉴于篇幅限制本次移植只接受RTU模式。
RTU 模式每个字节 ( 11 位 ) 的格式为 :
编码系统: 8–位二进制,报文中每个 8 位字节含有两个 4 位十六进制字符(0–9, A–F)
每字节的 bit 流: 1 起始位
8 数据位, 首先发送最低有效位
1 位作为奇偶校验
1 停止位
偶校验是要求的, 其它模式 ( 奇校验, 无校验 ) 也可以使用。 为了保证与其它产品的最大兼容性,同时支持无校验模式是建议的。默认校验模式模式 必须为偶校验。 注 : 使用无校验要求 2 个停止位,此bit流可参考串口通讯。
帧检验域:循环冗余校验 (CRC), 2 字节。
帧描述 :Modbus RTU 帧总长度最大为 256 字节。帧结构如下图
1.5.1、Modbus 报文 RTU 帧 由发送设备将 Modbus 报文构造为带有已知起始和结束标记的帧。这使设备可以在报文的开始接收新帧,并且知道何时报文结束。不完整的报文必须能够被检测到而错误标志必须作为结果被设置。 在 RTU 模式,报文帧由时长至少为 3.5 个字符时间的空闲间隔区分。在后续的部分,这个时间区间被称作 t3.5。
下图表示了对 RTU 传输模式状态图的描述。 "主节点" 和 "子节点" 的不同角度均在相同的图中表示:
上面状态图的一些解释:
从 "初始" 态到 “空闲” 态转换需要 t3.5 定时超时: 这保证帧间延迟
“空闲” 态是没有发送和接收报文要处理的正常状态。
在 RTU 模式, 当没有活动的传输的时间间隔达 3.5 个字符长时,通信链路被认为在 “空闲” 态。
当链路空闲时, 在链路上检测到的任何传输的字符被识别为帧起始。 链路变为 "活动" 状态。 当链路上没有字符传输的时间间个达到 t3.5 后,被识别为帧结束。
检测到帧结束后,完成 CRC 计算和检验。然后,分析地址域以确定帧是否发往此设备,如果不是,则丢弃此帧。 为了减少接收处理时间,地址域可以在一接到就分析,而不需要等到整个帧结束。这样,CRC 计算只需要在帧寻址到该节点 (包括广播帧) 时进行。
1.5.2、CRC 校验
在 RTU 模式包含一个对全部报文内容执行的,基于循环冗余校验 (CRC - Cyclical Redundancy Checking) 算法的错误检验域。CRC 域检验整个报文的内容。不管报文有无奇偶校验,均执行此检验。
CRC 包含由两个 8 位字节组成的一个 16 位值。 CRC 域作为报文的最后的域附加在报文之后。计算后,首先附加低字节,然后是高字节。CRC 高字节为报文发送的最后一个子节。
附加在报文后面的 CRC 的值由发送设备计算。接收设备在接收报文时重新计算 CRC 的值,并将计算结果于实际接收到的 CRC 值相比较。如果两个值不相等,则为错误。 CRC 的计算, 开始对一个 16 位寄存器预装全 1。 然后将报文中的连续的 8 位子节对其进行后续的计算。只有字符中的 8 个数据位参与生成 CRC 的运算,起始位,停止位和校验位不参与 CRC 计算。
CRC 的生成过程中, 每个 8–位字符与寄存器中的值异或。然后结果向最低有效位(LSB)方向移动(Shift) 1 位,而最高有效位(MSB)位置充零。 然后提取并检查 LSB:如果 LSB 为 1, 则寄存器中的值与一个固定的预置值异或;如果 LSB 为 0, 则不进行异或操作。
这个过程将重复直到执行完 8 次移位。完成最后一次(第 8 次)移位及相关操作后,下一个 8 位字节与寄存器的当前值异或,然后又同上面描述过的一样重复 8 次。当所有报文中子节都运算之后得到的寄存器忠的最终值,就是 CRC。
二、外设规格参数
2.1 功能码分类 MODBUS功能码有三类 。它们分别是:公共功能码、用户定义功能码、保留功能码。
公共功能码
是较好地被定义的功能码,
保证是唯一的,
MODBUS 组织可改变的,
公开证明的,
具有可用的一致性测试,
MB IETF RFC 中证明的,
包含已被定义的公共指配功能码和未来使用的未指配保留供功能码。
用户定义功能码
有两个用户定义功能码的定义范围,即 65 至 72 和十进制 100 至 110。
用户没有 MODBUS 组织的任何批准就可以选择和实现一个功能码
不能保证被选功能码的使用是唯一的。
如果用户要重新设置功能作为一个公共功能码,那么用户必须启动 RFC,以便将改变引入公共分类中,并且指配一个新的公共功能码。
保留功能码
一些公司对传统产品通常使用的功能码,并且对公共使用是无效的功能码。
如果我们需要实现自定义的一些功能码,必须在 65-72 或 100-110 中进行选择。
2.2 公共功能码定义
MODBUS 数据模型有四种,通过不同的功能码来读写这些数据对象。
对象类别 | 对象类型 | 访问类型 | 内容 | 离散量输入 | 单个比特 | 只读 I/O | 系统提供这种类型数据 | 线圈 | 单个比特 | 读写 | 通过应用程序改变这种类型数据 | 输入寄存器 | 16-比特字 | 只读 I/O | 系统提供这种类型数据 | 保持寄存器 | 16-比特字 | 读写 | 通过应用程序改变这种类型数据 | 功能码定义如下:
最常用的功能码有:01、02、03、04、05、15、16。
2.3 读线圈寄存器 01H
描述: 读 MODBUS 从机线圈寄存器当前状态。
举例: 例如从机地址为 11H,线圈寄存器的起始地址为 0013H,结束地址为 0037H。该次查询总共访 问 37 个线圈寄存器。
响应:响应负载中的各线圈状态与数据内容每位相对应。1 代表 ON,0 代表 OFF。若返回的线圈数不 为 8 的倍数,则在最后数据字节未尾使用 0 代替。
线圈 0013H 到线圈 001AH 的状态为 CDH,二进制值为 11001101,该字节的最高字节为线圈 001AH , 最 低 字 节 为 线 圈 0013H 。 线 圈 001AH 到 线 圈 0013H 的 状 态 分 别 为ON-ON-OFF-OFF-ON-ON-OFF-ON
最后一个数据字节中,线圈 0033H 到线圈 0037 状态为 1BH(二进制 00011011),线圈 0037H是左数第 4 位,线圈 0033H 为该字节的最低字节,线圈 0037H 至线圈 0033H 的状态分别为ON-ON-OFF-ON-ON,剩余 3 位使用 0 填充。
2.4、写多个线圈寄存器 0FH 描述:写多个线圈寄存器。若数据区的某位值为“1”表示被请求的相应线圈状态为 ON,若某位值为“0”,则为状态为 OFF。
举例: 从机地址为 11H,线圈寄存器的起始地址为 0013H,线圈寄存器的结束地址为 001CH。总共访问 10 个寄存器。寄存器内容如下表所示。
传输的第一个字节 CDH 对应线圈为 0013H 到 001AH,LSB(最低位)对应线圈 0013H,传输 第二个字节为 01H,对应的线圈为 001BH 到 001CH,LSB 对应线圈 001CH,其余未使用位使用 0 填充。
响应:
其余读写操作类似,不再赘述。
三、移植过程 3.1、资料介绍
本次移植原本以为需要完全写代码实现,在整理资料的过程中意外发现已经有很多开源代码已经实现,本着站在巨人肩上的原则,开始借鉴移植到M61上来。
XTinyModbus
===
一个应用于嵌入式方面的小型Modbus协议栈,采用C语言编写,支持master与slave,支持离散映射,支持非阻塞与阻塞读写模式,并提供完整示例,可以移植到不同的处理器,使用简单,移植方便。开源项目中给出在STM32F1系列芯片中实现MODBUS通讯的一个移植示例,如果需要移植到其它芯片,需要对MD_RTU_Serial.c进行修改,实现串口函数的移植,以及需要在中断函数中调用串口与定时器的函数。为最大程度的保留原有代码的完整性,本次移植修改主要集中在Uart.c和tim3.c两个文件中,实现串口收发和定时器功能。
3.2、移植至工程
查看源码发现Modbus的实现是通过构建上文提到的标准帧结构,再通过串口发送接收来实现通讯的,再由定时器实现帧间控制。首先将源码调加到新工程,再解决M61的串口初始化、接受中断实现,以及定时时间设定即可。
3.2.1、添加新工程
在SDK同级目录新建文件夹,命名modbus_rs485用于存放整个工程,再新建Modbus文件夹用于存放待移植源码。为方便起见,我们直接复制sdk中的一个example中的一个demo,再重命名为modbus_rs485即可。新工程中原有的4个文件需要修改,以便编译下载的时候不会命名错误。
在CMakeLists.txt中修改工程名为新的工程名,在此以工程文件夹同名modbus_rs485,同时添加if(CONFIG_MODBUS) endif()语句段用于添加需要包含的源文件和头文件
修改编译结果的bin文件命名。
修改sdk所在目录。
激活CMakeLists.txt中的判断设定。到此新工程建立完成,开始驱动移植。
将源码中的文件夹按图所示复制到我们的Modbus文件夹下,其中APP文件夹放源码example下的*.app.*问件。user存放适用于我们M61的串口和定时器代码。同在在CMakeLists.txt中关联好新添加的源文件和头文件。代码如下,Modbus文件夹内容最后会随附件上传。
- if(CONFIG_MODBUS)
- # target_sources(app PRIVATE Modbus/app/MDS_RTU_App.c)
- target_sources(app PRIVATE Modbus/app/MDM_RTU_App.c)
- target_sources(app PRIVATE Modbus/app/MDS_RTU_App_1.c)
- target_sources(app PRIVATE Modbus/ModbusBase/MD_RTU_CRC16.c)
- target_sources(app PRIVATE Modbus/ModbusBase/MD_RTU_MapTable.c)
- target_sources(app PRIVATE Modbus/ModbusBase/MD_RTU_Queue.c)
- target_sources(app PRIVATE Modbus/ModbusRTUMaster/MDM_RTU_Fun.c)
- target_sources(app PRIVATE Modbus/ModbusRTUMaster/MDM_RTU_RW_Man.c)
-
- target_sources(app PRIVATE Modbus/ModbusRTUMaster/MDM_RTU_User_Fun.c)
- target_sources(app PRIVATE Modbus/ModbusRTUSlave/MDS_RTU_Fun.c)
- target_sources(app PRIVATE Modbus/ModbusRTUSlave/MDS_RTU_User_Fun.c)
- target_sources(app PRIVATE Modbus/Port/MD_RTU_SysInterface.c)
- target_sources(app PRIVATE Modbus/Port/MDM_RTU_Serial.c)
- target_sources(app PRIVATE Modbus/Port/MDS_RTU_Serial_1.c)
- # target_sources(app PRIVATE Modbus/Port/MDS_RTU_Serial.c)
- target_sources(app PRIVATE Modbus/user/tim3.c)
- target_sources(app PRIVATE Modbus/user/usart3.c)
- sdk_add_include_directories(Modbus/app)
- sdk_add_include_directories(Modbus)
- sdk_add_include_directories(Modbus/ModbusBase)
- sdk_add_include_directories(Modbus/ModbusRTUMaster)
- sdk_add_include_directories(Modbus/ModbusRTUSlave)
- sdk_add_include_directories(Modbus/Port)
- sdk_add_include_directories(Modbus/Serial)
- sdk_add_include_directories(Modbus/user)
- endif()
复制代码 3.2.2、适应性移植 3.2.2.1、串口
源码例程是是stm32平台,移植到M61需要修改串口、定时器、bsp的板级头文件。在uart3.c(命名uart3是因为避免改动头文件关联关系,并不是使用M61的串口3,虽然也没有串口3),完成串口初始化、接受中断等函数,代码如下:
- #include "usart3.h"
- #include "Sys_Config.h"
- #if MD_USD_SALVE
- #include "MDS_RTU_Serial_1.h"
- #else
- #include "MDM_RTU_Serial.h"
- #include "MD_RTU_SysInterface.h"
- #include "MDM_RTU_Fun.h"
- #endif
- // uartx = bflb_device_get_by_name(DEFAULT_TEST_UART);
- struct bflb_device_s *uartx;
- void RS485RWConvInit(void)
- {
- // GPIO_InitTypeDef GPIO_InitStructure;
- // RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC| RCC_APB2Periph_AFIO, ENABLE);
- // BKP_TamperPinCmd(DISABLE);
- // BKP_ITConfig(DISABLE);
- // GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
- // GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
- // GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
- // GPIO_Init(GPIOC, &GPIO_InitStructure);
- // GPIO_ResetBits(GPIOC,GPIO_Pin_13);
- }
- void uart_isr(int irq, void *arg)
- {
- uint32_t intstatus = bflb_uart_get_intstatus(uartx);
- if (intstatus & UART_INTSTS_RTO){
- uint8_t data;
- while (bflb_uart_rxavailable(uartx)) {
- data = bflb_uart_getchar(uartx);
- printf("0x%02x\r\n", data);
- }
- // uint8_t data = bflb_uart_getchar(uartx);
- bflb_uart_int_clear(uartx, UART_INTCLR_RTO);
- // printf("data is %d",data);
- printf("get over\n");
- #if !MD_RTU_USED_OS
- #if MD_USD_SALVE
- MDSSerialRecvByte_1(data);
- #else
- #if MDM_USD_USART3
- MDMSerialRecvByte(data);
- printf("RecvByteget over\n");
- #endif
- #endif
- #else
- extern Modbus_RTU modbus_RTU;
- MD_RTU_MsgPut((PModbusBase)(&modbus_RTU), MD_RTU_MSG_HANDLE_ARG(&modbus_RTU),(void*)(data),0);
- #endif
- }
- }
- void init_usart3(u32 baudRate){
-
- board_uartx_gpio_init();
- uartx = bflb_device_get_by_name(DEFAULT_TEST_UART);
- struct bflb_uart_config_s cfg;
- cfg.baudrate = baudRate;
- cfg.data_bits = UART_DATA_BITS_8;
- cfg.stop_bits = UART_STOP_BITS_1;
- cfg.parity = UART_PARITY_NONE;
- cfg.flow_ctrl = 0;
- cfg.tx_fifo_threshold = 7;
- cfg.rx_fifo_threshold = 7;
- bflb_uart_init(uartx, &cfg);
- bflb_uart_feature_control(uartx, UART_CMD_SET_TX_RS485_EN, 1);
- bflb_uart_feature_control(uartx, UART_CMD_SET_TX_RS485_POLARITY, 1);
- // bflb_uart_feature_control(uartx, UART_CMD_SET_RTO_VALUE, 28);
- bflb_uart_txint_mask(uartx, true);
- bflb_uart_rxint_mask(uartx, false);
- bflb_irq_attach(uartx->irq_num, uart_isr, NULL);
- bflb_irq_enable(uartx->irq_num);
- // USART_InitTypeDef USART_InitStructure;
- // GPIO_InitTypeDef GPIO_InitStructure;
- // NVIC_InitTypeDef NVIC_InitStruct;
- // /* Enable GPIO clock */
- // RCC_APB2PeriphClockCmd(
- // RCC_APB2Periph_GPIOB| RCC_APB2Periph_AFIO, ENABLE);
- // RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
-
- // GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
- // GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
- // GPIO_Init(GPIOB, &GPIO_InitStructure);
-
- // GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
- // GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- // GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
- // GPIO_Init(GPIOB, &GPIO_InitStructure);
-
- // USART_InitStructure.USART_BaudRate = baudRate;
- // USART_InitStructure.USART_WordLength = USART_WordLength_8b;
- // USART_InitStructure.USART_StopBits = USART_StopBits_1;
- // USART_InitStructure.USART_Parity = USART_Parity_No;
- // USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
- // USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
- // /* Configure USARTz */
- // USART_Init(USART3, &USART_InitStructure);
- // USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
- // USART_Cmd(USART3, ENABLE);
-
- // NVIC_InitStruct.NVIC_IRQChannel=USART3_IRQn;
- // NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
- // NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=0;
- // NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;
- // NVIC_Init(&NVIC_InitStruct);
-
- RS485RWConvInit();
- }
- void usart3_send_byte(u8 byte){
- // while(USART_GetFlagStatus(USART3,USART_FLAG_TC )==RESET);
- bflb_uart_putchar(uartx, byte);;
- // while(USART_GetFlagStatus(USART3,USART_FLAG_TC )==RESET);
- }
- void usart3_send_bytes(u8 *bytes,int len){
- int i;
- for(i=0;i<len;i++){
- usart3_send_byte(bytes[i]);
- }
- }
- void usart3_send_string(char *string){
- while(*string){
- usart3_send_byte(*string++);
- }
- }
复制代码
特别说明,M61的串口本身支持485模式,移植起来方便很多。介绍如下:UART 模块支持 RS485 模式,通过设置寄存器 UTX_RS485_CFG 中的 cr_utx_rs485_en 可以让 UART 模块工作在RS485 模式下,此时,可以通过外接一个 RS485 的收发器连接到 RS485 总线上,在该模式下,模块中的 RTS 引脚,会转为 RS485 收发器的 Dir 功能,当 UART 模块有数据需要发送时,会自动控制 RTS 引脚为高电平,让 RS485 收发器把数据发送到总线上,反之,当 UART 模块没有数据需要发送时,会自动控制 RTS 为低电平,让 RS485 收发器处在接收状态。使用该模式需要设置寄存器 UTX_RS485_CFG 中的 cr_utx_rs485_pol 和 cr_utx_rs485_en。 - bflb_uart_feature_control(uartx, UART_CMD_SET_TX_RS485_EN, 1);
- bflb_uart_feature_control(uartx, UART_CMD_SET_TX_RS485_POLARITY, 1);
复制代码反应到代码中只需要在串口初始化中添加上面两句即可实现485模式, 这样源码中的软件控制收发转换函数(void RS485RWConvInit(void))就全部注释屏蔽掉了。适用M61串口1的RTS引脚即可硬件自动控制收发转换,即M61开发板上的IO26。 串口配置好后在发送函数void MDSSerialSendBytes(uint8 *bytes,uint16 num)中/调用bsp的串口发送函数即可,对应M61的bflb_uart_putchar(uartx, byte);
3.2.2.2、定时器
定时器主要实现100us定时,依据demo中的定时简单移植过来即可实现。代码中保留了stm32的源码(注释部分),方便对照学习。在定时器中断函数中调用下面这个函数,定时器间隔100US:
/*定时器中调用该函数*/
void MDSTimeHandler100US(uint32 times){
if(_pModbus_RTU==NULL){return;}
_pModbus_RTU->mdRTUTimeHandlerFunction(_pModbus_RTU ,times);
}
- #include "tim3.h"
- #include "Sys_Config.h"
- #include "bflb_mtimer.h"
- #include "bflb_timer.h"
- #include "board.h"
- #define TEST_TIMER_COMP_ID TIMER_COMP_ID_0
- #if MD_USD_SALVE
- #include "MDS_RTU_Serial.h"
- #include "MDS_RTU_Serial_1.h"
- #else
- #include "MDM_RTU_Serial.h"
- #endif
- struct bflb_device_s *timer0;
- vu32 sys_tick_100us=0;
- void timer0_isr(int irq, void *arg)
- {
- bool status = bflb_timer_get_compint_status(timer0, TIMER_COMP_ID_0);
- if (status) {
- bflb_timer_compint_clear(timer0, TIMER_COMP_ID_0);
- // printf("timer0 comp0 trigger\r\n");
- sys_tick_100us++;
- #if !MD_RTU_USED_OS
- #if MD_USD_SALVE
- MDSTimeHandler100US(sys_tick_100us);
- MDSTimeHandler100US_1(sys_tick_100us);
- #else
- MDMTimeHandler100US(sys_tick_100us);
- #endif
- #endif
- }
- }
- //General purpose timer 3 interrupt initialization
- //The clock here is 2 times that of APB1, and APB1 is 36M
- //arr: automatic reload value.
- //psc: clock prescaler number
- //Timer 3 is used here!
- void TIM3_Int_Init(u16 arr,u16 psc)
- {
- printf("Timer basic test\n");
- /* timer clk = XCLK/(div + 1 )*/
- /* 100us定时===》》 psc=39,arr=0 */
- struct bflb_timer_config_s cfg0;
- cfg0.counter_mode = TIMER_COUNTER_MODE_PROLOAD; /* preload when match occur */
- cfg0.clock_source = TIMER_CLKSRC_XTAL;
- cfg0.clock_div = psc; /* for bl616/bl808/bl606p is 39, for bl702 is 31 psc=39*/
- cfg0.trigger_comp_id = TEST_TIMER_COMP_ID;
- cfg0.comp0_val = 100; /* match value 0 100us*/
- cfg0.preload_val = arr; /* preload value arr=0*/
-
- timer0 = bflb_device_get_by_name("timer0");
- /* Timer init with default configuration */
- bflb_timer_init(timer0, &cfg0);
- bflb_irq_attach(timer0->irq_num, timer0_isr, NULL);
- bflb_irq_enable(timer0->irq_num);
- /* Enable timer */
- bflb_timer_start(timer0);
-
-
- // TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
- // NVIC_InitTypeDef NVIC_InitStructure;
- // RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
- // TIM_TimeBaseStructure.TIM_Period = arr;
- // TIM_TimeBaseStructure.TIM_Prescaler =psc;
- // TIM_TimeBaseStructure.TIM_ClockDivision = 0;
- // TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
- // TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
- // TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE );
- // NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
- // NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
- // NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
- // NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
- // NVIC_Init(&NVIC_InitStructure);
- // TIM_Cmd(TIM3, ENABLE);
- }
- // void TIM3_IRQHandler(void)
- // {
- // if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
- // {
- // TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
- // sys_tick_100us++;
- // #if !MD_RTU_USED_OS
- // #if MD_USD_SALVE
- // MDSTimeHandler100US(sys_tick_100us);
- // MDSTimeHandler100US_1(sys_tick_100us);
- // #else
- // MDMTimeHandler100US(sys_tick_100us);
- // #endif
- // #endif
- // }
- // }
复制代码 3.3、引脚选择
先上485芯片原理图
M61的uart0用于默认打印调试信息,故选用uart1连链接485,采用模式引脚,即io23---TX,io24---RX,io26---RTS 。
M61为3.3V系统,而RS485为5V供电,安全起见手焊一个电平转换电路,该电路也是在安信可资料网站上无意看到的。
3.4、移植验证
因为只有一块板子,没法组网通讯,故适用上位机模拟modbus从机,M61做主机。
序号1为usb转485模块,为了本次移植花巨资购入,用于链接模拟从机的上位机。序号2为上学时51开发板上的485模块,也就是上面原理图所示硬件。
应为引入了电平转换,走线相对混乱了点,大致为M61的uart1——电平转换——RS485芯片—MODBUS—上位机从机 验证内容集中在APP文件夹中,首先初始化MODBUS
- /*******************************************************
- *
- * Function name :MDM_RTU_APPInit
- * Description :Host user APP initialization function
- * Parameter :None
- * Return : TRUE success , FALSE fail
- **********************************************************/
- BOOL MDM_RTU_APPInit(void){
- if(MDM_RTU_Init(&modbus_RTU,MDMInitSerial,9600,8,1,0)!=ERR_NONE){
- return FALSE;
- }
- MDM_RTU_QueueInit(&modbus_RTU,MDSRecvQueueData,sizeof(MDSRecvQueueData));
-
- if(MDM_RTU_AddMapItem(&modbus_RTU,&mapTableItemMaster0)==FALSE){
- return FALSE;
- }
- if(MDM_RTU_AddMapItem(&modbus_RTU,&mapTableItemMaster1)==FALSE){
- return FALSE;
- }
- if(MDM_RTU_AddMapItem(&modbus_RTU,&mapTableItemMaster2)==FALSE){
- return FALSE;
- }
- /*RW control block, the user controls the read and write time interval, retransmission timeout and retransmission timeout times*/
- MDM_RTU_CB_Init(&modbusRWRTUCB,&modbus_RTU,0,30000,3);
- MDM_RTU_CB_Init(&modbusRWRTUCB1,&modbus_RTU,0,30000,3);
- MDM_RTU_CB_Init(&modbusRWRTUCB2,&modbus_RTU,0,30000,3);
- MDM_RTU_CB_Init(&modbusRWRTUCB3,&modbus_RTU,0,30000,3);
- MDM_RTU_CB_Init(&modbusRWRTUCB4,&modbus_RTU,0,30000,3);
-
-
- MDM_RW_CtrlAddRW(MDM_RTU_NB_RW_CtrlTest0,NULL,"rw_test_0");
- MDM_RW_CtrlAddRW(MDM_RTU_NB_RW_CtrlTest1,NULL,"rw_test_1");
-
- return TRUE;
- }
复制代码 然后使用MDM_RTUUserRead()和MDM_RTUUserWrite()函数读写从机寄存器。为方便观看效果,每次写0xAA和0x55,即可观看到没一位来回0-1 变化。同时写完0x55后回来开头的读操作,上位机响应回最后的0x55结果。
- /*User read data*/
- static void MDM_RTUUserRead(void){
-
- uint16 resTemp;
- #if MD_NB_MODE_TEST
- MDError res;
- res = MDM_RTU_NB_ReadCoil(&modbusRWRTUCB,0x1,0,16);
- printf("01 01 00 00 00 0f over\n");
- if(res != ERR_IDLE){
- if(res != ERR_RW_FIN){/*An error occurred*/
- if(res == ERR_RW_OV_TIME_ERR){/*Timed out*/
- /*Enable retransmission*/
- MDM_RTU_CB_OverTimeReset(&modbusRWRTUCB);
- }
- }else {
- /*Read successfully*/
- MDM_RTU_ReadBits(modbusRWRTUCB.pModbus_RTU,0x0000,16, (uint8*)&resTemp,COILS_TYPE,0x1);
- resTemp=resTemp;
- }
- }
- #else
-
- if(MDM_RTU_ReadCoil(&modbusRWRTUCB,0x1,0x0000,16)==ERR_RW_FIN){
- MDM_RTU_ReadBits(modbusRWRTUCB.pModbus_RTU,0x0000,16, (uint8*)&resTemp,COILS_TYPE,0x1);
- resTemp=resTemp;
- }
- #endif
- }
- static void MDM_RTUUserWrite(void){
- MDError res;
- #if MD_NB_MODE_TEST
- res = MDM_RTU_NB_WriteCoils(&modbusRWRTUCB1,0x1,0,16,(uint8*)(&temp));
- printf("01 0f 00 00 00 10 02 AA AA over\n");
- if(res != ERR_IDLE){
- if(res != ERR_RW_FIN){/*An error occurred*/
- if(res == ERR_RW_OV_TIME_ERR){/*Timed out*/
- /*Enable retransmission*/
- MDM_RTU_CB_OverTimeReset(&modbusRWRTUCB1);
- }
- }
- }
- res = MDM_RTU_NB_WriteCoils(&modbusRWRTUCB4,0x1,0,16,(uint8*)(&temp2));
- if(res != ERR_IDLE){
- if(res != ERR_RW_FIN){/*An error occurred*/
- if(res == ERR_RW_OV_TIME_ERR){/*Timed out*/
- /*Enable retransmission*/
- MDM_RTU_CB_OverTimeReset(&modbusRWRTUCB4);
- }
- }
- printf("ERR_IDLE\n");
- }
- printf("01 0f 00 00 00 10 02 55 55 over\n");
- #else
- MDM_RTU_WriteCoils(&modbusRWRTUCB1,0x1,0,16,(uint8*)(&temp));
- MDM_RTU_WriteCoils(&modbusRWRTUCB4,0x1,0,16,(uint8*)(&temp2));
- #endif
- }
- /*Send control function*/
- static MDM_RW_CtrlErr MDM_RTU_NB_RW_CtrlTest0(void* arg){
- MDError res;
- #if MD_NB_MODE_TEST
- res = MDM_RTU_NB_WriteRegs(&modbusRWRTUCB2,0x1,0,6,data1);
- if(res != ERR_IDLE){
- if(res != ERR_RW_FIN){/*An error occurred*/
- if(res == ERR_RW_OV_TIME_ERR){/*Timed out*/
- /*Enable retransmission*/
- MDM_RTU_CB_OverTimeReset(&modbusRWRTUCB1);
- return RW_ERR;
- }
- }else{
- return RW_OK;
- }
- }
- #endif
- return RW_NONE;
- }
- /*Send control function*/
- static MDM_RW_CtrlErr MDM_RTU_NB_RW_CtrlTest1(void* arg){
- MDError res;
- #if MD_NB_MODE_TEST
- res = MDM_RTU_NB_WriteRegs(&modbusRWRTUCB3,0x1,0,6,data2);
- if(res != ERR_IDLE){
- if(res != ERR_RW_FIN){/*An error occurred*/
- if(res == ERR_RW_OV_TIME_ERR){/*Timed out*/
- /*Enable retransmission*/
- MDM_RTU_CB_OverTimeReset(&modbusRWRTUCB1);
- return RW_ERR;
- }
- }else{
- return RW_OK;
- }
- }
- #endif
- return RW_NONE;
- }
- /*User data reading and writing*/
- static void MDM_RTU_UserUpdate(void){
- MDM_RTUUserRead();
- printf("UserRead over\n");
- // bflb_mtimer_delay_ms(500);
- MDM_RTUUserWrite();
-
- // printf("UserWrite over\n");
- }
- /*Loop call*/
- void MDM_RTU_Loop(void){
- MDM_RTU_UserUpdate();
- }
复制代码 主函数循环实现主机的查询和写功能
- #include "bflb_mtimer.h"
- #include "bflb_uart.h"
- #include "board.h"
- #include "tim3.h"
- #include "Sys_Config.h"
- #include "MDM_RTU_RW_Man.h"
- #if MD_USD_SALVE
- #include "MDS_RTU_APP.h"
- #include "MDS_RTU_APP_1.h"
- #else
- #include "MDM_RTU_APP.h"
- #include "MDM_RTU_Fun.h"
- #include "MDM_RTU_User_Fun.h"
- #endif
- int main(void)
- {
- board_init();
- // board_uartx_gpio_init();
- #if MD_USD_SALVE
- MDS_RTU_APPInit();
- MDS_RTU_APPInit_1();
- #else
- MDM_RTU_APPInit();
-
- #endif
- TIM3_Int_Init(0,39);
- while(1){
-
- #if MD_USD_SALVE
- MDS_RTU_Loop();
- MDS_RTU_Loop_1();
- #else
- MDM_RTU_Loop();
- // printf("loop over\n");
- MDM_RW_CtrlLoop();
- printf("ctrlloop over\n");
- // MD_RTU_Delay(10000);
- #endif
-
- }
- }
复制代码 最后上效果:
蓝色标记到的为写操作,可以看到收到写操作是寄存器为即变化一次。
3.5、常见问题汇总
3.5.1、不同平台移植时里面的数据类型存在不同定义,例如不识别u32,需要添加定义:typedef unsigned int u32;类似还有vu32、u8、u16等,后面发现可能是没有关联标准C语言库,后期尝试#include "stdio.h"看是否有效果。
3.5.2、串口设置问题
开始研究不透彻,没有关注到串口的校验位设,一直出错,后面改为无效验,顺利收发。
附件中为移植好的工程源码和虚拟主从机
|