我们假定定时器工作在向上计数 PWM模式,且当 CNT小于CCRx 时,输出 0,当 CNT大于等于CCRx 时输出 1。那么就可以得到如上的 PWM示意图:当 CNT 值小于 CCRx 的时候, IO 输出低电平(0),当 CNT 值大于等于 CCRx 的时候,IO 输出高电平(1),当 CNT 达到 ARR 值的时候,重新归零,然后重新向上计数,依次循环。改变 CCRx 的值,就可以改变 PWM 输出的占空比,改变 ARR 的值,就可以改变 PWM 输出的频率,这就是 PWM 输出的原理。,
通过控制脉冲占空比来改变电机的电枢电压.改变占空比的方法有3种:(1)定宽调频法,这种方法是保持t1不变,只改变t2,这样周期(T或频率)也随之改变;(2)调宽调频法,保持t1不变,而改变t2,这样也使周期T(或频率)改变;(3)定频调宽法,这种方法是使周期(T或频率)不变,而同时改变t1和t2.由于前两利,方法都改变了周期(或频率),当控制频率与系统的固有频率接近时,将会引起振荡,用的比较少,因此本系统用的是定频调宽法.在脉冲作用下,当电机通电时,速度增加.电机断电时,速度逐渐减小.只要按一定规律,改变通断电时间,即可实现对电机的转速控制。
系统硬件电路
系统硬件电路设计框图如下:
通过对STM32F4单片机编写程序实现对步进电机的控制,并且可以利用计算机和单片机的串口通信,接收到单片机所反馈回来的控制数据,包括:**步进电机的正向转动、反向转动、步进电机的定位功能以及调速功能。**要实现上述功能需要对STM32F4的以下模块进行设置,主要包括:串口通信模块、按键输入模块、电机驱动模块三大部分。下面就以重要模块的实现过程来进行详细的论述。
串口通信模块
串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段, 其重要性不言而喻。现在基本上所有的 MCU 都会带有串口, STM32 自然也不例外。STM32F4 的串口资源相当丰富的,功能也相当强劲。 ALIENTEK 探索者 STM32F4 开发板所使用的 STM32F407ZGT6 最多可提供 6 路串口,有分数波特率发生器、支持同步单线通信和半双工单线通讯、支持 LIN、 支持调制解调器操作、 智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA 等。
处理器与外部设备通信的两种方式:
并行通信:
-传输原理:数据各个位同时传输。
-优点:速度快
-缺点:占用引脚资源多
串行通信:
-传输原理:数据按位顺序传输。
-优点:占用引脚资源少
-缺点:速度相对较慢
这里我们选用串行通信。串行通信按照数据传送方向,分为:
单工:
数据传输只支持数据在一个方向上传输
半双工:
允许数据在两个方向上传输,但是,在某一时刻,只允许数
据在一个方向上传输,它实际上是一种切换方向的单工通信;
全双工:
允许数据同时在两个方向上传输,因此,全双工通信是两个
单工通信方式的结合,它要求发送设备和接收设备都有独立
的接收和发送能力。
这里我们给出串口配置的一般步骤:
①串口时钟使能:RCC_APBxPeriphClockCmd();
GPIO时钟使能:RCC_AHB1PeriphClockCmd();
② 引脚复用映射:
GPIO_PinAFConfig();
③GPIO端口模式设置:GPIO_Init(); 模式设置为GPIO_Mode_AF
④串口参数初始化:USART_Init();
⑤开启中断并且初始化NVIC(如果需要开启中断才需要这个步骤)
NVIC_Init();
USART_ITConfig();
⑥使能串口:USART_Cmd();
⑦编写中断处理函数:USARTx_IRQHandler();
⑧串口数据收发:
void USART_SendData();//发送数据到串口,DR
uint16_t USART_ReceiveData();//接受数据,从DR读取接受到的数据
⑨串口传输状态获取:
FlagStatus USART_GetFlagStatus();
void USART_ClearITPendingBit();
相关代码如下:
void uart_init(u32 bound) //GPIO端口设置 { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1 GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //使能GPIOA9时钟 GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); //使能GPIOA10时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //GPIOA9与GPIOA10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHZ GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用功能 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA9,PA10 USART_InitStructure.USART_BaudRate = bound;//波特率设置 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式 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; //收发模式 USART_Init(USART1, &USART_InitStructure); //初始化串口1 USART_Cmd(USART1, ENABLE); //使能串口1 #if EN_USART1_RX USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启相关中断 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口1中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;//抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级3 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能 NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器 }
void USART1_IRQHandler(void) //串口1中断服务程序 { u8 Res; #if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS OSIntEnter(); #endif if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接受到的数据必须是0x0d 0x0a结尾) { Res =USART_ReceiveData(USART1);//(USART1->DR); //(USART1->DR);//读取接收到的数据 if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了0x0d { if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到0X0D { if(Res==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收 } } } } #if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS. OSIntExit(); #endif } #endif
按键输入模块
在此次实验中,我们设置按下 KEY-UP, 电机以 所设定频率回到绝对原点; 按下 KEY0, 电机以所设定频率顺时针转动; 按下 KEY1, 电机以所设定频率逆时针转动。
下面给出实现按键输入的一般步骤:
①使能按键对应IO口时钟。调用函数:
RCC_AHB1PeriphClockCmd ();
②初始化IO模式:上拉/下拉输入。调用函数:
GPIO_Init();
③扫描IO口电平(库函数/寄存器/位操作)。
相关代码如下:
void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOE, ENABLE);//使能GPIOA,GPIOE时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4; //KEY0 KEY1 KEY2对应引脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;//普通输入模式 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100M GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIO2,3,4 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//WK_UP对应引脚 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN ;//下拉 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA0 }
u8 KEY_Scan(u8 mode) { static u8 key_up=1;//按键松开标志 if(mode)key_up=1; //支持连接 if(key_up&&(KEY0==0||KEY1==0||KEY2==0||WK_UP==1)) { delay_ms(10);//去抖动 key_up=0; if(KEY0==0)return 1; else if(KEY1==0)return 2; else if(KEY2==0)return 3; else if(WK_UP==1)return 4; }else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)key_up=1; return 0;// 无按键按下 }
电机驱动模块
驱动器与STM32F4连接如下图:
代码需要用到的 4 个主要函数如下:
void Driver_Init(void);//驱动器初始化
void TIM8_OPM_RCR_Init(u16 arr, u16 psc); //TIM8_CH2 初始化 单脉冲+重复计数模式
void Locate_Rle(long num, u32 frequency, DIR_Type dir) //相对定位函数
void Locate_Abs(long num, u32 frequency);/绝对定位函数
1)驱动器初始化函数,主要就是初始化与驱动器 ENA+,DIR+相连的 2 个 IO为推挽输出。
2) TIM8_CH2 初始化, 此例程产生脉冲所使用的定时器均是 TIM8_CH2(PC7) ,定时器工作在单脉冲+重复计数模式,需要注意的是定时器必须初始化为 1MHz 计数频率。
3) 相对定位函数: 在步进电机当前位置基础上顺时针(CW)或者逆时针(CCW)走 num 个脉冲, 此函数带方向控制, DIR_Type 是 driver.h 下声明的一个枚举类型,用于设置电机旋转方向,参数 dir=CW,电机顺时针旋转; dir=CCW,电机逆时针旋转。
绝对定位函数:步进电机按设定频率转动到设置的绝对位置, 开发板上电和复位时,当前位置为 0,电机的当前位置用一个 long 型变量 current_pos 指示。 在current_pos=0 的基础上顺时针转动后 current_pos 为正, 否则为负。 5) 此例程配置了 usmart 函数和按键函数,可以通过按键或者串口调用相对定位函数和绝对定位函数控制驱动器,从而控制步进电机。
驱动模块代码如下:
u8 rcr_remainder; //重复计数余数部分 u8 is_rcr_finish=1; //重复计数器是否设置完成 long rcr_integer; //重复计数整数部分 long target_pos=0; //有符号方向 long current_pos=0; //有符号方向 DIR_Type motor_dir=CW;//顺时针
驱动控制信号线初始化:
void Driver_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);//使能GPIOE时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_6; //DRIVER_DIR DRIVER_OE对应引脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100M GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE5,6 GPIO_SetBits(GPIOE,GPIO_Pin_5);//PE5输出高 顺时针方向 DRIVER_DIR GPIO_ResetBits(GPIOE,GPIO_Pin_6);//PE6输出低 使能输出 DRIVER_OE }
单脉冲+重复计数模式:
void TIM8_OPM_RCR_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8,ENABLE); //TIM8时钟使能 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE); //使能PORTC时钟 GPIO_PinAFConfig(GPIOC,GPIO_PinSource7,GPIO_AF_TIM8); //GPIOC7复用为定时器8 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //GPIOC7 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用功能 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //速度100MHz GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉 GPIO_Init(GPIOC,&GPIO_InitStructure); //初始化PF9 TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动自动重装载寄存器周期的值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMX时钟频率除数的预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式 TIM_TimeBaseInit(TIM8, &TIM_TimeBaseStructure); //根据TIM_TimeBaseStructure中指定的参数化TIMX的时间基数单位 TIM_ClearITPendingBit(TIM8,TIM_IT_Update); TIM_UpdateRequestConfig(TIM8,TIM_UpdateSource_Regular); /********* 设计只有计数溢出作为更新中断 ********/ TIM_SelectOnePulseMode(TIM8,TIM_OPMode_Single);/******* 单脉冲模式**********/ TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出2使能 TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Disable; /****** 比较输出2N使能 *******/ TIM_OCInitStructure.TIM_Pulse = arr>>1; //设置待装入捕获比较寄存器的脉冲值 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高 TIM_OC2Init(TIM8, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMX TIM_OC2PreloadConfig(TIM8, TIM_OCPreload_Enable); //CH2预装载值 TIM_ARRPreloadConfig(TIM8, ENABLE); //使能TIMX在ARR上的预装载值 TIM_ITConfig(TIM8, TIM_IT_Update ,ENABLE); //TIM8 使能或者失能指定的TIM中断 NVIC_InitStructure.NVIC_IRQChannel = TIM8_UP_TIM13_IRQn; //TIM8中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级1级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //从优先级1级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能 NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器 TIM_ClearITPendingBit(TIM8, TIM_IT_Update); //清除TIMx的中断待处理位:TIM中断源 TIM_Cmd(TIM8, ENABLE); //使能TIM8 }
TIM8更新中断服务程序:
void TIM8_UP_TIM13_IRQHandler(void) { if(TIM_GetITStatus(TIM8,TIM_FLAG_Update)!=RESET)//更新中断 { TIM_ClearITPendingBit(TIM8,TIM_FLAG_Update);//清除更新中断标志位 if(is_rcr_finish==0)//重复计数器未设置完成 { if(rcr_integer!=0) //整数部分脉冲还未发送 { TIM8->RCR=RCR_VAL;//设置重复计数值 rcr_integer--;//减少RCR_VAL+1个脉冲 }else if(rcr_remainder!=0)//余数部分脉冲 不为0 { TIM8->RCR=rcr_remainder-1;//设置余数部分 rcr_remainder=0;//清零 is_rcr_finish=1;//重复计数器设置完成 }else goto out; //rcr_remainder=0直接退出 TIM_GenerateEvent(TIM8,TIM_EventSource_Update);//产生一个更新事件 重新初始化计数器 TIM_CtrlPWMOutputs(TIM8,ENABLE); //MOE主输出使能 TIM_Cmd(TIM8, ENABLE); //使能TIM8 if(motor_dir==CW) //如果方向为顺时针 current_pos+=(TIM8->RCR+1);//加上重复计数值 else //否则方向为逆时针 current_pos-=(TIM8->RCR+1);//减去重复计数 }else { out: is_rcr_finish=1;//重复计数器设置完成 TIM_CtrlPWMOutputs(TIM8,DISABLE); //MOE 主输出关闭 TIM_Cmd(TIM8, DISABLE); //关闭TIM8 printf("µ±Ç°Î»ÖÃ=%ld\r\n",current_pos);//打印输出 } } }
启动TIM8:
void TIM8_Startup(u32 frequency) //启动定时器8 { u16 temp_arr=1000000/frequency-1; TIM_SetAutoreload(TIM8,temp_arr);//设定自动重装值 TIM_SetCompare2(TIM8,temp_arr>>1); //匹配值2等于重装值一半,是以占空比为50% TIM_SetCounter(TIM8,0);//计数器清零 TIM_Cmd(TIM8, ENABLE); //使能TIM8 }
相对定位函数:
void Locate_Rle(long num,u32 frequency,DIR_Type dir) //相对定位函数 { if(num<=0) //数值小于等于0 则直接返回 { printf("\r\nThe num should be greater than zero!!\r\n"); return; } if(TIM8->CR1&0x01)//上一次脉冲还未发送完成 直接返回 { printf("\r\nThe last time pulses is not send finished,wait please!\r\n"); return; } if((frequency<20)||(frequency>100000))//脉冲频率不在范围之内 直接返回 { printf("\r\nThe frequency is out of range! please reset it!!(range:20Hz~100KHz)\r\n"); return; } motor_dir=dir;//得到方向 DRIVER_DIR=motor_dir;//设置方向 if(motor_dir==CW)//顺时针 target_pos=current_pos+num;//目标位置 else if(motor_dir==CCW)//逆时针 target_pos=current_pos-num;//目标位置 rcr_integer=num/(RCR_VAL+1);//重复计数整数部分 rcr_remainder=num%(RCR_VAL+1);//重复计数余数部分 is_rcr_finish=0;//重复计数器未设置完成 TIM8_Startup(frequency);//开启TIM8 }
绝对定位函数:
void Locate_Abs(long num,u32 frequency)//绝对定位函数 { if(TIM8->CR1&0x01)//上一次脉冲还未发送完成 直接返回 { printf("\r\nThe last time pulses is not send finished,wait please!\r\n"); return; } if((frequency<20)||(frequency>100000))//脉冲频率不在范围内 直接返回 { printf("\r\nThe frequency is out of range! please reset it!!(range:20Hz~100KHz)\r\n"); return; } target_pos=num;//设置目标位置 if(target_pos!=current_pos)//目标和当前位置不同 { if(target_pos>current_pos) motor_dir=CW;//顺时针 else motor_dir=CCW;//逆时针 DRIVER_DIR=motor_dir;//设置方向 rcr_integer=abs(target_pos-current_pos)/(RCR_VAL+1);//重复计数整数部分 rcr_remainder=abs(target_pos-current_pos)%(RCR_VAL+1);//重复计数余数部分 is_rcr_finish=0;//重复计数器未设置完成 TIM8_Startup(frequency);//开启TIM8 } }
最后是主函数:
int main(void) { u8 i; u8 keyval; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2 delay_init(168); //初始化延时函数 uart_init(115200); //初始化串口波特率115200 usmart_dev.init(84); //初始化USMART LED_Init(); //初始化LED KEY_Init(); //初始化按键 Driver_Init(); //驱动器初始化 TIM8_OPM_RCR_Init(999,168-1); //1MHz计数频率 单脉冲+重复计数模式 while(1) { keyval=KEY_Scan(0); if(keyval==WKUP_PRES) { Locate_Abs(0,500);//按下WKUP,回到零点 }else if(keyval==KEY0_PRES) { Locate_Rle(1200,500,CW);//按下KEY0,以500Hz的频率 顺时针发200脉冲 }else if(keyval==KEY1_PRES) { Locate_Rle(1200,500,CCW);//按下KEY1,以500Hz频率 逆时针发400脉冲 } delay_ms(10); i++; if(i==50) { i=0; LED1=!LED1; } } }
可以根据实际要求修改参数值以达到相应目的控制步进电机转动的转速和步距角。
实验验证
设置驱动器细分和电流, 完成硬件接线, 然后给开发板上电(USB232 接口), 打开 XCOM 调试助手,找到对应COM,设置波特率 115200,1 位停止位,8位数据位,无校验,勾上发送新行,然后打开串口,最后给驱动器上电。按下 KEY-UP, 电机以 500Hz 频率回到绝对原点; 按下 KEY0, 电机以 500Hz 频率顺时针走 200 个脉冲; 按下KEY1,电机以 500Hz 频率逆时针走 400 个脉冲;依次按下KEY_UP, KEY0, KEY1,串口打印的情况如下。
点击发送条目后边的数字就可调用该函数, 这儿的相对的定位函数 Locate_Rle 最后
一个参数用于设置电机旋转方向的, 0(CCW) 表示逆时针方向, 1(CW) 表示顺时针方向,实验时, 如果电机旋转方向和设置方向相反, 只需更改 driver.h 下的CW=0,CCW=1;每调用一次函数,串口打印当前的位置。 然后我们依次调用这 4 个函数, 根据串口的打印, 可以看到当前位置的变化情况,如图所示。
结语
本文介绍了利用单片机控制基于STM32F4芯片的步进电机定位控制系统的整体设计思路以及用pwm实现对步进电机控制的脉冲时序的分配进行了详细的仿真。利用STM32F4芯片严谨的基于硬件的编程语言和精确的时间控制特点,准确地实现了精确定位功能,对步进电机的运行速度的精确控制。实践证明,这种方法定位准确,控制速度精确,是一种行之有效的方案。
附
1.此次实验所选用的的驱动器为TB6600步进电机专用驱动器。
TB6600步进电机驱动器是一款专业的两相步进电机驱动,可实现正反转控制。通 过S1 S2 S3 3位拨码开关选择8 档细分控制(1、2、4、8、16 ),通过S4 S5 S6 3位拨码开关选择6档电流控制(0.5A,1A,1.5A,2.0A,2.5A,3.0A, 3.5A, 4.0A)。适合驱动86,57,42,39 型两相、四相混合式步进电机。驱动器具有 噪音小,震动小,运行平稳的特点。
TB6600步进电机专用驱动器电气参数如下表:
2.此次实验所选用的步进电机为57步进电机。其相关参数及尺寸如下。