You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
14 KiB
543 lines
14 KiB
2 years ago
|
## Modbus
|
||
|
https://blog.csdn.net/ASWaterbenben/article/details/105549750
|
||
|
https://blog.51cto.com/u_15830484/5767899
|
||
|
https://www.amobbs.com/thread-5491615-1-1.html
|
||
|
https://blog.csdn.net/qq153471503/article/details/104840279
|
||
|
https://blog.csdn.net/childbor/article/details/123803534 野火
|
||
|
30001为输入寄存器,需要使用04指令访问,而40001为保持寄存器,可以使用03、06和16指令访问
|
||
|
|
||
|
在modbus中,需要判断一帧什么时候发送结束,我们采用定时器进行判断。
|
||
|
当定时器时间大于我们设定的时间时,发生定时器中断,告诉系统,一帧数据发送完成。
|
||
|
|
||
|
首先选择TIM7下的Parameter Settings,将PSC设置为7199,Counter Mode设置为UP,Counter Period 设置为39,Auto-reload preload设置为Enable。
|
||
|
拷贝mobdus文件加
|
||
|
拷贝demo/bare 到 port文件夹, demo.c 改modbus_port.c
|
||
|
mbconfig.h 配置启用哪些模块
|
||
|
对于元文件需要做修改的,加 .port 后缀,放到code/port文件夹
|
||
|
#### 移植思路
|
||
|
##### 串口定时器初始化放到 Modbus里
|
||
|
暂无
|
||
|
##### 串口定时器单独初始化
|
||
|
|
||
|
***portserial.c***
|
||
|
extern UART_HandleTypeDef huart2; // 引入串口
|
||
|
|
||
|
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
|
||
|
依据参考开启相关中断
|
||
|
UART_IT_RXNE 接收非空
|
||
|
UART_IT_TXE 发送完成
|
||
|
|
||
|
xMBPortSerialPutByte( CHAR ucByte ) // 处理发送字节
|
||
|
```C
|
||
|
|
||
|
```
|
||
|
xMBPortSerialGetByte( CHAR * pucByte ) //处理接收字节
|
||
|
|
||
|
void USART2_IRQHandler(void)
|
||
|
prvvUARTRxISR();//接收中断
|
||
|
prvvUARTTxReadyISR();//发送中断
|
||
|
|
||
|
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//发送完成回调函数
|
||
|
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收完成回调函数
|
||
|
|
||
|
***porttimer.c***
|
||
|
定时器启用,停用。 请中断等操作
|
||
|
定时器是中断回调
|
||
|
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
|
||
|
prvvTIMERExpiredISR( ); //定时器中断
|
||
|
|
||
|
***modbus_port.c***
|
||
|
定义Input Holding 寄存器操作
|
||
|
|
||
|
中断建立联系 -- 如何无缝,不改中断函数代码,回调函数
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
***前端初始化***
|
||
|
eMBInit( MB_RTU, 0x01, 2, 115200, MB_PAR_NONE);//初始化modbus,走modbusRTU,从站地址为0x01,端口为1。
|
||
|
eMBEnable( );//使能modbus
|
||
|
|
||
|
while ( void )eMBPoll( );//启动modbus侦听
|
||
|
### 源码结构
|
||
|
|
||
|
定义结构体接收数据 usart.h
|
||
|
```C
|
||
|
|
||
|
typedef struct
|
||
|
{
|
||
|
uint8_t *rx_buf; //接收缓冲数组
|
||
|
uint16_t rx_buf_cnt; //接收缓冲计数值
|
||
|
uint16_t rx_size; //接收数据大小
|
||
|
uint8_t rx_flag; //接收完成标志位
|
||
|
|
||
|
uint8_t *tx_buf; //发送缓冲数组
|
||
|
uint16_t tx_buf_cnt; //发送缓冲计数值
|
||
|
uint16_t tx_size; //实际发送数据大小
|
||
|
}UART_BUF; //串口结构体
|
||
|
|
||
|
extern UART_BUF uart_buf_2; //预留一个定义的buf
|
||
|
|
||
|
```
|
||
|
usart.c
|
||
|
```C
|
||
|
/* USER CODE BEGIN 0 */
|
||
|
#include "string.h"
|
||
|
#include "tim.h"
|
||
|
#define UART4_RXSIZE 1024 //一帧接收数据的最大值
|
||
|
#define UART4_TXSIZE 1024 //一帧发送数据的最大值
|
||
|
|
||
|
uint8_t uart4_rx_buf[UART4_RXSIZE]; //发送数据缓冲数组
|
||
|
uint8_t uart4_tx_buf[UART4_TXSIZE]; //接收数据缓冲数据
|
||
|
|
||
|
UART_BUF uart4; //串口结构体实体
|
||
|
uint8_t RxBuffer; //接收数据中间变量
|
||
|
/* USER CODE END 0 */
|
||
|
```
|
||
|
|
||
|
重写回调
|
||
|
|
||
|
```C
|
||
|
/*****************************重写回调函数,实现串口数据接收**********************/
|
||
|
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
|
||
|
{
|
||
|
if(huart->Instance == UART4)
|
||
|
{
|
||
|
if(uart4.rx_buf_cnt >= UART4_RXSIZE-1) //接收数据量超限,错误
|
||
|
{
|
||
|
uart4.rx_buf_cnt = 0;
|
||
|
memset(uart4.rx_buf, 0x00, sizeof(uart4.rx_buf));
|
||
|
HAL_UART_Transmit(huart, (uint8_t *)"数据溢出", 10, 0xFFFF);
|
||
|
}
|
||
|
else //接收正常
|
||
|
{
|
||
|
uart4.rx_buf[uart4.rx_buf_cnt++] = RxBuffer; //接收数据存储到rx_buf
|
||
|
HAL_TIM_Base_Stop_IT(&htim7);
|
||
|
__HAL_TIM_SET_COUNTER(&htim7, 0);
|
||
|
HAL_TIM_Base_Start_IT(&htim7); //将定时器7的计数值清零后重新计数
|
||
|
}
|
||
|
HAL_UART_Receive_IT(huart, (uint8_t *)&RxBuffer, 1);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
|
||
|
port.h
|
||
|
#define ENTER_CRITICAL_SECTION( ) __set_PRIMASK(1);//关总中断
|
||
|
#define EXIT_CRITICAL_SECTION( ) __set_PRIMASK(0);//开总中断
|
||
|
如何改DMA ?
|
||
|
|
||
|
|
||
|
|
||
|
tim 初始化及回调
|
||
|
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
|
||
|
|
||
|
### main
|
||
|
buf 重置 ---写函数实现供调用
|
||
|
HAL_UART_Receive_IT(huart, uart4.rx_buf, 1); //开启接收中断
|
||
|
|
||
|
tim清理标记 -- 写函数实现供调用
|
||
|
开启中断
|
||
|
|
||
|
### modbus slave
|
||
|
将已经编写好的modbus.c和modbus.h分别复制到Src和Inc文件夹内
|
||
|
|
||
|
#### freemodbus
|
||
|
|
||
|
解压freemodbus文件后打开,
|
||
|
modbus 目录组合,重新分Src Inc
|
||
|
我们需要demo目录下的BARE,该目录下的代码是空的,STM32移植工作基本就是修改:portserial.c、porttimer.c、port.h这三个文件。
|
||
|
|
||
|
参考:
|
||
|
https://github.com/eziya/STM32_HAL_FREEMODBUS_RTU/tree/master/Middlewares/Third_Party/modbus
|
||
|
bare/port 目录下的Makefile 可以直接编译
|
||
|
modbus 目录拷贝到 Middlewares/Third_Party/modbus/
|
||
|
bare/port 目录下的Makefile 可以直接编译, demo 包含main()
|
||
|
|
||
|
functions include 是基础目录
|
||
|
rtu 需要引用 rtu目录
|
||
|
|
||
|
|
||
|
#### porttimer.c
|
||
|
将定时器设置为每50us的时长记一个数,传入的usTim1Timerout50us变量给自动装载即可
|
||
|
prvvTIMERExpiredISR函数需要在定时器中断服务函数中调用,它的作用是用于通知modbus协议栈3.5个字符的等待时间已经到达
|
||
|
|
||
|
注意一些标志清除
|
||
|
|
||
|
定时器中断 TimX_IRQHandler 通知通知modbus3.5个字符等待时间到
|
||
|
prvvTIMERExpiredISR(); // 通知modbus3.5个字符等待时间到
|
||
|
|
||
|
如果使用了485芯片的话,那么同一时刻只能接收或者发送,可以将函数vMBPortSerialEnable修改成这样
|
||
|
拉高拉低,并增加延时。
|
||
|
|
||
|
|
||
|
#### 定义port.c供前端调用
|
||
|
|
||
|
|
||
|
|
||
|
***数据定义***
|
||
|
```C
|
||
|
// 十路输入寄存器
|
||
|
#define REG_INPUT_SIZE 10
|
||
|
uint16_t REG_INPUT_BUF[REG_INPUT_SIZE];
|
||
|
|
||
|
|
||
|
// 十路保持寄存器
|
||
|
#define REG_HOLD_SIZE 10
|
||
|
uint16_t REG_HOLD_BUF[REG_HOLD_SIZE];
|
||
|
|
||
|
|
||
|
// 十路线圈
|
||
|
#define REG_COILS_SIZE 10
|
||
|
uint8_t REG_COILS_BUF[REG_COILS_SIZE] = {1, 1, 1, 1, 0, 0, 0, 0, 1, 1};
|
||
|
|
||
|
|
||
|
// 十路离散量
|
||
|
// 十路离散量
|
||
|
#define REG_DISC_SIZE 10
|
||
|
uint8_t REG_DISC_BUF[REG_DISC_SIZE] = {1,1,1,1,0,0,0,0,1,1};
|
||
|
```
|
||
|
|
||
|
对应功能函数
|
||
|
```C
|
||
|
/* 功能码 4*/
|
||
|
eMBErrorCode
|
||
|
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
|
||
|
{
|
||
|
|
||
|
return eStatus;
|
||
|
}
|
||
|
/* 功能码 6 3 16*/
|
||
|
eMBErrorCode
|
||
|
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
|
||
|
eMBRegisterMode eMode )
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
/* 功能码 1 5 15*/
|
||
|
eMBErrorCode
|
||
|
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils,
|
||
|
eMBRegisterMode eMode )
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
/* 功能码 2*/
|
||
|
eMBErrorCode
|
||
|
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
```
|
||
|
|
||
|
完整port.c
|
||
|
|
||
|
```C
|
||
|
#include "mb.h"
|
||
|
#include "mbport.h"
|
||
|
|
||
|
|
||
|
// 十路输入寄存器
|
||
|
#define REG_INPUT_SIZE 10
|
||
|
uint16_t REG_INPUT_BUF[REG_INPUT_SIZE];
|
||
|
|
||
|
|
||
|
// 十路保持寄存器
|
||
|
#define REG_HOLD_SIZE 10
|
||
|
uint16_t REG_HOLD_BUF[REG_HOLD_SIZE];
|
||
|
|
||
|
|
||
|
// 十路线圈
|
||
|
#define REG_COILS_SIZE 10
|
||
|
uint8_t REG_COILS_BUF[REG_COILS_SIZE] = {1, 1, 1, 1, 0, 0, 0, 0, 1, 1};
|
||
|
|
||
|
|
||
|
// 十路离散量
|
||
|
#define REG_DISC_SIZE 10
|
||
|
uint8_t REG_DISC_BUF[REG_DISC_SIZE] = {1,1,1,1,0,0,0,0,1,1};
|
||
|
|
||
|
|
||
|
/// CMD4命令处理回调函数
|
||
|
eMBErrorCode eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
|
||
|
{
|
||
|
USHORT usRegIndex = usAddress - 1;
|
||
|
|
||
|
// 非法检测
|
||
|
if((usRegIndex + usNRegs) > REG_INPUT_SIZE)
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
// 循环读取
|
||
|
while( usNRegs > 0 )
|
||
|
{
|
||
|
*pucRegBuffer++ = ( unsigned char )( REG_INPUT_BUF[usRegIndex] >> 8 );
|
||
|
*pucRegBuffer++ = ( unsigned char )( REG_INPUT_BUF[usRegIndex] & 0xFF );
|
||
|
usRegIndex++;
|
||
|
usNRegs--;
|
||
|
}
|
||
|
|
||
|
// 模拟输入寄存器被改变
|
||
|
for(usRegIndex = 0; usRegIndex < REG_INPUT_SIZE; usRegIndex++)
|
||
|
{
|
||
|
REG_INPUT_BUF[usRegIndex]++;
|
||
|
}
|
||
|
|
||
|
return MB_ENOERR;
|
||
|
}
|
||
|
|
||
|
/// CMD6、3、16命令处理回调函数
|
||
|
eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
|
||
|
{
|
||
|
USHORT usRegIndex = usAddress - 1;
|
||
|
|
||
|
// 非法检测
|
||
|
if((usRegIndex + usNRegs) > REG_HOLD_SIZE)
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
// 写寄存器
|
||
|
if(eMode == MB_REG_WRITE)
|
||
|
{
|
||
|
while( usNRegs > 0 )
|
||
|
{
|
||
|
REG_HOLD_BUF[usRegIndex] = (pucRegBuffer[0] << 8) | pucRegBuffer[1];
|
||
|
pucRegBuffer += 2;
|
||
|
usRegIndex++;
|
||
|
usNRegs--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 读寄存器
|
||
|
else
|
||
|
{
|
||
|
while( usNRegs > 0 )
|
||
|
{
|
||
|
*pucRegBuffer++ = ( unsigned char )( REG_HOLD_BUF[usRegIndex] >> 8 );
|
||
|
*pucRegBuffer++ = ( unsigned char )( REG_HOLD_BUF[usRegIndex] & 0xFF );
|
||
|
usRegIndex++;
|
||
|
usNRegs--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return MB_ENOERR;
|
||
|
}
|
||
|
|
||
|
/// CMD1、5、15命令处理回调函数
|
||
|
eMBErrorCode eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode )
|
||
|
{
|
||
|
USHORT usRegIndex = usAddress - 1;
|
||
|
UCHAR ucBits = 0;
|
||
|
UCHAR ucState = 0;
|
||
|
UCHAR ucLoops = 0;
|
||
|
|
||
|
// 非法检测
|
||
|
if((usRegIndex + usNCoils) > REG_COILS_SIZE)
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
if(eMode == MB_REG_WRITE)
|
||
|
{
|
||
|
ucLoops = (usNCoils - 1) / 8 + 1;
|
||
|
while(ucLoops != 0)
|
||
|
{
|
||
|
ucState = *pucRegBuffer++;
|
||
|
ucBits = 0;
|
||
|
while(usNCoils != 0 && ucBits < 8)
|
||
|
{
|
||
|
REG_COILS_BUF[usRegIndex++] = (ucState >> ucBits) & 0X01;
|
||
|
usNCoils--;
|
||
|
ucBits++;
|
||
|
}
|
||
|
ucLoops--;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ucLoops = (usNCoils - 1) / 8 + 1;
|
||
|
while(ucLoops != 0)
|
||
|
{
|
||
|
ucState = 0;
|
||
|
ucBits = 0;
|
||
|
while(usNCoils != 0 && ucBits < 8)
|
||
|
{
|
||
|
if(REG_COILS_BUF[usRegIndex])
|
||
|
{
|
||
|
ucState |= (1 << ucBits);
|
||
|
}
|
||
|
usNCoils--;
|
||
|
usRegIndex++;
|
||
|
ucBits++;
|
||
|
}
|
||
|
*pucRegBuffer++ = ucState;
|
||
|
ucLoops--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return MB_ENOERR;
|
||
|
}
|
||
|
|
||
|
/// CMD2命令处理回调函数
|
||
|
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
|
||
|
{
|
||
|
USHORT usRegIndex = usAddress - 1;
|
||
|
UCHAR ucBits = 0;
|
||
|
UCHAR ucState = 0;
|
||
|
UCHAR ucLoops = 0;
|
||
|
|
||
|
// 非法检测
|
||
|
if((usRegIndex + usNDiscrete) > REG_DISC_SIZE)
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
ucLoops = (usNDiscrete - 1) / 8 + 1;
|
||
|
while(ucLoops != 0)
|
||
|
{
|
||
|
ucState = 0;
|
||
|
ucBits = 0;
|
||
|
while(usNDiscrete != 0 && ucBits < 8)
|
||
|
{
|
||
|
if(REG_DISC_BUF[usRegIndex])
|
||
|
{
|
||
|
ucState |= (1 << ucBits);
|
||
|
}
|
||
|
usNDiscrete--;
|
||
|
usRegIndex++;
|
||
|
ucBits++;
|
||
|
}
|
||
|
*pucRegBuffer++ = ucState;
|
||
|
ucLoops--;
|
||
|
}
|
||
|
|
||
|
// 模拟离散量输入被改变
|
||
|
for(usRegIndex = 0; usRegIndex < REG_DISC_SIZE; usRegIndex++)
|
||
|
{
|
||
|
REG_DISC_BUF[usRegIndex] = !REG_DISC_BUF[usRegIndex];
|
||
|
}
|
||
|
|
||
|
return MB_ENOERR;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
```
|
||
|
|
||
|
|
||
|
#### main.c
|
||
|
将串口定时器的初始化都整到 Modbus 部分,这里只需要调用modbus初始化就可以
|
||
|
```C
|
||
|
int main(void)
|
||
|
{
|
||
|
HAL_Init();
|
||
|
SystemClock_Config();
|
||
|
MX_GPIO_Init();
|
||
|
eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_ODD); // 初始化modbus为RTU方式,波特率9600,奇校验
|
||
|
eMBEnable(); // 使能modbus协议栈
|
||
|
|
||
|
for( ;; )
|
||
|
{
|
||
|
eMBPoll(); // 轮训查询
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
```
|
||
|
|
||
|
|
||
|
我们需要demo目录下的BARE,该目录下的代码是空的,STM32移植工作基本就是修改:portserial.c
|
||
|
串口的中断服务程序,
|
||
|
prvvUARTTxReadyISR和prvvUARTRxISR函数需要填写进中断服务程序,
|
||
|
前者得到作用为通知modbus协议栈串口已经空闲可以发送数据了
|
||
|
后者的作用为通知modbus串口1有数据到达
|
||
|
#### 调用
|
||
|
|
||
|
```C
|
||
|
|
||
|
#include "stm32f4xx_hal.h"
|
||
|
#include "cmsis_os.h"
|
||
|
|
||
|
#include "mb.h"
|
||
|
#include "mbport.h"
|
||
|
|
||
|
#define REG_INPUT_START 1000
|
||
|
#define REG_INPUT_NREGS 8
|
||
|
|
||
|
static USHORT usRegInputStart = REG_INPUT_START;
|
||
|
static USHORT usRegInputBuf[REG_INPUT_NREGS];
|
||
|
|
||
|
void ModbusRTUTask(void const * argument)
|
||
|
{
|
||
|
/* ABCDEF */
|
||
|
usRegInputBuf[0] = 11;
|
||
|
usRegInputBuf[1] = 22;
|
||
|
usRegInputBuf[2] = 33;
|
||
|
usRegInputBuf[3] = 44;
|
||
|
usRegInputBuf[4] = 55;
|
||
|
usRegInputBuf[5] = 66;
|
||
|
usRegInputBuf[6] = 77;
|
||
|
usRegInputBuf[7] = 88;
|
||
|
|
||
|
eMBErrorCode eStatus = eMBInit( MB_RTU, 1, 3, 19200, MB_PAR_NONE );
|
||
|
eStatus = eMBEnable();
|
||
|
|
||
|
while(1) {
|
||
|
eMBPoll();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
eMBErrorCode
|
||
|
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
|
||
|
{
|
||
|
eMBErrorCode eStatus = MB_ENOERR;
|
||
|
int iRegIndex;
|
||
|
|
||
|
if( ( usAddress >= REG_INPUT_START )
|
||
|
&& ( usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS ) )
|
||
|
{
|
||
|
iRegIndex = ( int )( usAddress - usRegInputStart );
|
||
|
while( usNRegs > 0 )
|
||
|
{
|
||
|
*pucRegBuffer++ =
|
||
|
( unsigned char )( usRegInputBuf[iRegIndex] >> 8 );
|
||
|
*pucRegBuffer++ =
|
||
|
( unsigned char )( usRegInputBuf[iRegIndex] & 0xFF );
|
||
|
iRegIndex++;
|
||
|
usNRegs--;
|
||
|
}
|
||
|
|
||
|
HAL_GPIO_TogglePin(LD4_GPIO_Port, LD4_Pin);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
HAL_GPIO_TogglePin(LD5_GPIO_Port, LD5_Pin);
|
||
|
eStatus = MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
return eStatus;
|
||
|
}
|
||
|
|
||
|
eMBErrorCode
|
||
|
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
|
||
|
eMBRegisterMode eMode )
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
|
||
|
eMBErrorCode
|
||
|
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils,
|
||
|
eMBRegisterMode eMode )
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
|
||
|
eMBErrorCode
|
||
|
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
|
||
|
{
|
||
|
return MB_ENOREG;
|
||
|
}
|
||
|
```
|