STM32F407 freemodbus移植
一、ModBus介绍
Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。 [1] Modbus比其他通信协议使用的更广泛的主要原因有:
(1) 公开发表并且无版权要求
(2) 易于部署和维护
(3) 对供应商来说,修改移动本地的比特或字节没有很多限制
Modbus允许多个 (大约240个) 设备连接在同一个网络上进行通信,举个例子,一个由测量温度和湿度的装置,并且将结果发送给计算机。在数据采集与监视控制系统(SCADA)中,Modbus通常用来连接监控计算机和远程终端控制系统(RTU)。
支持的功能码
目前版本的FreeModbus支持如下的功能码:
读输入寄存器 (0x04)
读保持寄存器 (0x03)
写单个寄存器 (0x06)
写多个寄存器 (0x10)
读/写多个寄存器 (0x17)
读取线圈状态 (0x01)
写单个线圈 (0x05)
写多个线圈 (0x0F)
读输入状态 (0x02)
报告从机标识 (0x11)
二、freemodbus准备
1、freemodbus可以从官网进行下载,从官网下载可以获取到最新的freemodbus源码,freemodbus官网下载地址:
官网:
https://www.embedded-solutions.at/en/freemodbus/
下载地址:
https://github.com/cwalter-at/freemodbus
2、从我个人百度网盘直接下载,现在我网盘是v1.5版本,网盘链接地址:
链接:https://pan.baidu.com/s/1Jc1YhdinmmKvNt107wsMeA 提取码:88mc
三、freemodbus移植到STM32F407
1、准备STM32工程与freemodbus源码
准备STM32F407工程,工程时钟必须配置正确,如果开发板串口能正确收数据,表示工程是正确的,同时要准备好freemodbus源码。
2、freemodbus源码添加到STM32工程
(1)解压freemodbus源码,解压得到如理内容。
我们需要的是modbus这个文件夹,和demo->BARE下的port文件夹。
核心文件结构
FreeModbus\modbus\mb.c
给应用层提供Modbus从机设置及轮询相关接口
FreeModbus\modbus\mb_m.c
给应用层提供Modbus主机设置及轮询相关接口
FreeModbus\modbus\ascii\mbascii.c
ASCII模式设置及其状态机
FreeModbus\modbus\functions\mbfunccoils.c
从机线圈相关功能
FreeModbus\modbus\functions\mbfunccoils_m.c
主机线圈相关功能
FreeModbus\modbus\functions\mbfuncdisc.c
从机离散输入相关功能
FreeModbus\modbus\functions\mbfuncdisc_m.c
主机离散输入相关功能
FreeModbus\modbus\functions\mbfuncholding.c
从机保持寄存器相关功能
FreeModbus\modbus\functions\mbfuncholding_m.c
主机保持寄存器相关功能
FreeModbus\modbus\functions\mbfuncinput.c
从机输入寄存器相关功能
FreeModbus\modbus\functions\mbfuncinput_m.c
主机输入寄存器相关功能
FreeModbus\modbus\functions\mbfuncother.c
其余Modbus功能
FreeModbus\modbus\functions\mbutils.c
一些协议栈中需要用到的小工具
FreeModbus\modbus\rtu\mbcrc.c
CRC校验功能
FreeModbus\modbus\rtu\mbrtu.c
从机RTU模式设置及其状态机
FreeModbus\modbus\rtu\mbrtu_m.c
主机RTU模式设置及其状态机
FreeModbus\modbus\tcp\mbtcp.c
TCP模式设置及其状态机
FreeModbus\port\port.c
实现硬件移植部分接口
FreeModbus\port\portevent.c
实现从机事件移植接口
FreeModbus\port\portevent_m.c
实现主机事件及错误处理移植接口
FreeModbus\port\portserial.c
从机串口移植
FreeModbus\port\portserial_m.c
主机串口移植
FreeModbus\port\porttimer.c
从机定时器移植
FreeModbus\port\porttimer_m.c
主机定时器移植
FreeModbus\port\user_mb_app.c
定义从机数据缓冲区,实现从机Modbus功能的回调接口
FreeModbus\port\user_mb_app_m.c
定义主机数据缓冲区,实现主机Modbus功能的回调接口
(2)将上面两个文件夹内容拷贝到STM32工程文件夹中,并在STM32工程中添加管理及下图中的文件
工程中包含modbus头文件
在主函数中最下面添加代码,这部分非常重要
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t* file, uint32_t line)
{
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* Infinite loop */
while (1)
{
}
}
#else
void __aeabi_assert(const char * x1, const char * x2, int x3)
{
}
#endif
3、修改工程代码,完成Modbus移植
(1)完善portserial.c文件。该文件就是modbus通信中用到的串口的初始化配置文件。我这里选择USART2,波特率9600,由于我使用的开发板RS485连接在USART2.
USART2初始化代码如下:
/*********************************
引脚说明:
PA2 ---- USART2_TX(发送端)
PA3 ---- USART2_RX(接收端)
**********************************/
void Usart2_Init(int MyBaudRate)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
//GPIO 时钟使能。
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
//串口时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
//设置引脚复用器映射:调用 GPIO_PinAFConfig 函数。
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2);
//初始化设置:要设置模式为复用功能。
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3; //引脚2 3
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF; //复用功能模式
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; //推挽输出
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //速度
//初始化IO口为复用功能输出
GPIO_Init(GPIOA, &GPIO_InitStruct);
USART_InitStruct.USART_BaudRate = MyBaudRate; //波特率
USART_InitStruct.USART_Mode = USART_Mode_Tx|USART_Mode_Rx; //全双工模式
USART_InitStruct.USART_StopBits = USART_StopBits_1; //停止位为1
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //8位
USART_InitStruct.USART_Parity = USART_Parity_No; //无奇偶校验位
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件控制流
//串口参数初始化:设置波特率,字长,奇偶校验等参数。
USART_Init(USART2, &USART_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel = USART2_IRQn; //中断通道,可在stm32f4xx.h文件当中查找
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //响应优先级
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //通道使能
//开启中断并且初始化 NVIC,使能中断
NVIC_Init(&NVIC_InitStruct);
//配置为接收中断(表示有数据过来,CPU要中断进行接收)
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
//使能串口。
USART_Cmd(USART2, ENABLE);
}
打开portserial.c,内容如下
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
}
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
return FALSE;
}
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
/* Put a byte in the UARTs transmit buffer. This function is called
* by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
* called. */
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
/* Return the byte in the UARTs receive buffer. This function is called
* by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
*/
return TRUE;
}
认真看一下函数名字,你会发现这些函数分别是:串口使能、串口初始化、发送一个字节、接收一个字节。
将上面代码修改如下
#include "port.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
#include "stm32f4xx.h" //添加STM32头文件
#include "usart.h" //添加串口头文件
/* ----------------------- static functions ---------------------------------*/
static void prvvUARTTxReadyISR( void );
static void prvvUARTRxISR( void );
/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
//STM32串口 接收中断使能
if(xRxEnable==TRUE)
{
//使能接收和接收中断
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
}
else if(xRxEnable == FALSE)
{
//禁止接收和接收中断
USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
}
//STM32串口 发送中断使能
if(xTxEnable==TRUE)
{
//使能发送完成中断
USART_ITConfig(USART2, USART_IT_TXE, ENABLE);
}
else if(xTxEnable == FALSE)
{
//禁止发送完成中断
USART_ITConfig(USART2, USART_IT_TXE, DISABLE);
}
else if(xTxEnable == FALSE)
{
//MODBUS_RECIEVE();
USART_ITConfig(USART2, USART_IT_TC, DISABLE);
}
}
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
//串口初始化,只需要传输串口波特率
Usart2_Init((uint16_t)ulBaudRate);
return TRUE;
}
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
/* Put a byte in the UARTs transmit buffer. This function is called
* by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
* called. */
//发送数据
USART_SendData(USART2, ucByte);
while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET){};
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
/* Return the byte in the UARTs receive buffer. This function is called
* by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
*/
//接收数据
*pucByte = USART_ReceiveData(USART2);
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.
*/
static void prvvUARTTxReadyISR( void )
{
pxMBFrameCBTransmitterEmpty( );
}
/* 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.
*/
static void prvvUARTRxISR( void )
{
pxMBFrameCBByteReceived( );
}
//串口中断处理函数
void USART2_IRQHandler(void)
{
if(USART_GetITStatus(USART2, USART_IT_TXE) == SET)
{
prvvUARTTxReadyISR();
USART_ClearITPendingBit(USART2, USART_IT_TXE);
}
if(USART_GetITStatus(USART2, USART_IT_RXNE) == SET)
{
prvvUARTRxISR();
USART_ClearITPendingBit(USART2, USART_IT_RXNE);
}
}
(2)完善porttimer.c文件。并配置一个大于3.5字节的定时器中断
/*********************************
定时器说明
TIM3 -- APB1(定时器频率:84MHZ)
TIM3是16位定时器
**********************************/
void Tim3_Init(uint16_t tim)
{
//4、设置 TIM3_DIER 允许更新中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
//5、使能定时器。
TIM_Cmd(TIM3, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
//1、能定时器时钟。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseInitStruct.TIM_Prescaler = (4200-1); //4200分频,定时器频率84MHZ/4200 = 20000HZ
TIM_TimeBaseInitStruct.TIM_Period = tim; //定时器周期 20000
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStruct.TIM_ClockDivision= TIM_CKD_DIV1; //分频因子 1脉冲计一个数
//2、初始化定时器,配置ARR,PSC。
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn; //中断通道,可在stm32f4xx.h文件当中查找
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //响应优先级
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //通道使能
//3、启定时器中断,配置NVIC。
NVIC_Init(&NVIC_InitStruct);
//清除溢出中断标志位
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
//定时器溢出中断关闭
TIM_ITConfig(TIM3,TIM_IT_Update,DISABLE);
//失能定时器
TIM_Cmd(TIM3, DISABLE);
}
porttimer.c修改如下
#include "port.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
#include "tim.h"
/* ----------------------- static functions ---------------------------------*/
static void prvvTIMERExpiredISR( void );
/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
/********************************************
usTim1Timerout50us实际值
usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );
ulBaudRate为9600,计算得出usTimerT35_50us为80
********************************************/
Tim3_Init(usTim1Timerout50us);
return TRUE;
}
inline void
vMBPortTimersEnable( )
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
//设置定时器的初值
TIM_SetCounter(TIM3,0x0000);
TIM_Cmd(TIM3, ENABLE);
}
inline void
vMBPortTimersDisable( )
{
/* Disable any pending timers. */
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
TIM_ITConfig(TIM3, TIM_IT_Update, DISABLE);
TIM_SetCounter(TIM3,0x0000);
//关闭定时器
TIM_Cmd(TIM3, DISABLE);
}
/* Create an ISR which is called whenever the timer has expired. This function
* must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
* the timer has expired.
*/
static void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( );
}
//Modbus 定时器中断函数
void TIM3_IRQHandler( void )
{
if(TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
prvvTIMERExpiredISR();
}
}
(3)在main.c文件中,定义各个模拟寄存器的地址和大小。
#include "stm32f4xx.h"
#include "led.h"
#include "delay.h"
#include "tim.h"
#include "usart.h"
#include "string.h"
#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];
// 十路离散量
#define REG_DISC_SIZE 10
uint8_t REG_DISC_BUF[10];
int main(void)
{
//NVIC分组一个工程只能设置一次
//设置NVIC分组为第二分组; 抢占优先级取值范围:0~3, 响应优先级取值范围:0~3
int i = 0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Delay_Init();
Led_Init();
Usart1_Init(115200);
//printf("helloworld\r\n");
eMBInit(MB_RTU, 0x01, 3, 9600, MB_PAR_NONE);
/* Enable the Modbus Protocol Stack. */
eMBEnable();
while(1)
{
(void)eMBPoll();
}
return 0;
}
// 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)
{
//判断寄存器,用于控制灯
if(pucRegBuffer[1] ==0)
{
GPIO_SetBits(GPIOF, GPIO_Pin_9);
GPIO_SetBits(GPIOF, GPIO_Pin_10);
GPIO_SetBits(GPIOE, GPIO_Pin_13);
}
if(pucRegBuffer[1] ==1)
{
GPIO_ResetBits(GPIOF, GPIO_Pin_9);
}
if(pucRegBuffer[1] ==2)
{
GPIO_ResetBits(GPIOF, GPIO_Pin_10);
}
if(pucRegBuffer[1] ==3)
{
GPIO_ResetBits(GPIOE, GPIO_Pin_13);
}
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;
USHORT usCoilGroups = ((usNCoils - 1) / 8 + 1);
UCHAR ucStatus = 0;
UCHAR ucBits = 0;
UCHAR ucDisp = 0;
// 非法检测
if((usRegIndex + usNCoils) > REG_COILS_SIZE)
{
return MB_ENOREG;
}
// 写线圈
if(eMode == MB_REG_WRITE)
{
while(usCoilGroups--)
{
ucStatus = *pucRegBuffer++;
ucBits = 8;
while((usNCoils--) != 0 && (ucBits--) != 0)
{
REG_COILS_BUF[usRegIndex++] = ucStatus & 0X01;
ucStatus >>= 1;
}
}
}
// 读线圈
else
{
while(usCoilGroups--)
{
ucDisp = 0;
ucBits = 8;
while((usNCoils--) != 0 && (ucBits--) != 0)
{
ucStatus |= (REG_COILS_BUF[usRegIndex++] << (ucDisp++));
}
*pucRegBuffer++ = ucStatus;
}
}
return MB_ENOERR;
}
// CMD4
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
USHORT usRegIndex = usAddress - 1;
USHORT usCoilGroups = ((usNDiscrete - 1) / 8 + 1);
UCHAR ucStatus = 0;
UCHAR ucBits = 0;
UCHAR ucDisp = 0;
// 非法检测
if((usRegIndex + usNDiscrete) > REG_DISC_SIZE)
{
return MB_ENOREG;
}
// 读离散输入
while(usCoilGroups--)
{
ucDisp = 0;
ucBits = 8;
while((usNDiscrete--) != 0 && (ucBits--) != 0)
{
if(REG_DISC_BUF[usRegIndex])
{
ucStatus |= (1 << ucDisp);
}
ucDisp++;
}
*pucRegBuffer++ = ucStatus;
}
// 模拟改变
for(usRegIndex = 0; usRegIndex < REG_DISC_SIZE; usRegIndex++)
{
REG_DISC_BUF[usRegIndex] = !REG_DISC_BUF[usRegIndex];
}
return MB_ENOERR;
}
//这里的代码就是上面所说的主函数最下面的代码,如果上面已经添加,这里可以忽略
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t* file, uint32_t line)
{
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* Infinite loop */
while (1)
{
}
}
#else
void __aeabi_assert(const char * x1, const char * x2, int x3)
{
}
#endif
编译通过,nice
三、下载及验证
1、功能码,04,读线圈
2、功能码,06,写保持寄存器
“01 06 00 01 00 01 19 CA”,//点亮一盏LED
“01 06 00 01 00 02 59 CB”,//点亮二盏LED
“01 06 00 01 00 03 98 0B”,//点亮三盏LED
“01 06 00 01 00 00 D8 0A”,//熄灭所有LED
“01 06 00 01 00 01 19 CA”,//点亮一盏LED
“01 06 00 01 00 02 59 CB”,//点亮二盏LED
这里是看着亮两个灯,原因上面演示里灯1没有关闭,须知“01 06 00 01 00 03 98 0B”,//点亮三盏LED
这里是看着亮两个灯,原因上面演示里灯1, 灯2没有关闭,须知“01 06 00 01 00 00 D8 0A”,//熄灭所有LED