以前通过接触公司里的产品,以及自己实际编写过的代码,总结出一点分层编程的思维,在这里分享一下。首先分层思维,就是把整个工程按功能或控制分成几层(一般小的项目3到4层)以我这次做的蓝奥声智能硬件项目为例来讲解一下。
我负责的这个功能块是一个可以上升、下降的机械部件,这个机械部件叫“X管”。用直流电机驱动“X管”上升、下降,同时带动一个旋转编码器用于获取实时位置。当然还有一个原点检测开关。功能要求就是这个机械部件可以手动上下移动,以及通过两个“一键到位”按钮,让它移动到指定的两个位置。
首先介绍一下硬件。MCU是STM32F100,晶振 8.00M。直流电机驱动是一块蓝奥声科技PWM驱动板,驱动板有3个控制端口,两个逻辑端口控制电机正反转,一个PWM输入控制电机转速。编码器就是常见的欧姆龙增量式编码器,编码器的计数使用了 TIM3的编码器计数模式。
这个小项目我把他们分成了3层:
第1层:硬件的配置(电机的控制I/O和编码器定时器模式及相关I/O的设定)和电器设备的基本控制(比如:电机的正反转,获取编码器的值,清空编码器值等)可以把这层理解为驱动层。
第2层:具体某个功能的实现,“X管”的上升、下降,“X管”移动到指定的位置,以及“X管”的复位。
第3层:状态层,怎么理解这层呢?比如说你按了一下“复位按钮”,那么“X管”的复位状态就置1,然后“X管”就执行“复位函数”自动去寻找原点。直到“X管”复位完成,程序自动清除复位状态,并且把已复位的状态置1(这个“已复位”状态也很关键,后面再讲)。
***层,硬件的配置和电器设备的基本控制:
第1步:初始化电机的控制引脚、编码器输出引脚、PWM输出配置和定时器编码
器模式配置(其他的都很常见,只介绍定时器的编码模式)
u16 tim3_encoder_val = 0; //TIM3 编码器模式计数值 encoder_val_ms
/*
普通定时器 TIM3 初始化为编码器模式
说 明: 用作旋转编码器计数(欧姆龙 E6B2_CWZ6C)
参 数: psc 分频系数, arr 自动重载计数周期
返回值: 无
*/
void TIM3_EncoderInit(u16 psc, u16 arr)
{
GPIO_InitTypeDef GPIO_InitStructure; //GPIO参数
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; //定时器参数
TIM_ICInitTypeDef TIM_ICInitStruct; //编码器参数
NVIC_InitTypeDef NVIC_InitStruct; //中断 NVIC 参数
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启 PA 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //开启 TIM3 时钟
// A相 / B相
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
//GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
//定时器参数设置
TIM_DeInit(TIM3); //把定时器寄存器的值设为默认值
TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct); //用默认值填充指定的结构体
TIM_TimeBaseInitStruct.TIM_Prescaler = (psc - 1); //(168-1)设置分频系数
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; //计数方式(TIM9-TIM14只支持向上计数)
TIM_TimeBaseInitStruct.TIM_Period = (arr - 1); //自动重载计数周期
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频因子
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
//编码器模式设置
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Falling, TIM_ICPolarity_Falling); //下降沿
//TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI2, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); //上升沿
TIM_ICStructInit(&TIM_ICInitStruct); //用默认值填充指定的结构体
TIM_ICInitStruct.TIM_ICFilter = 6; //指定输入捕获过滤器
TIM_ICInit(TIM3, &TIM_ICInitStruct);
//配置中断 NVIC 优先级分组
NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01; //抢占有限级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x01; //响应优先级
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断
NVIC_Init(&NVIC_InitStruct);
TIM_ClearFlag(TIM3, TIM_FLAG_Update); //清除TIM3(中断)的挂起标志
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); //开启TIM3的更新中断
TIM3 -> CNT = 0; //清除定时器计数值
TIM_Cmd(TIM3, ENABLE); //开启定时器
tim3_encoder_val = TIM_GetCounter(TIM3); //获取 TIM3 编码器计数值
//printf("编码器值:%u\r\n", tim3_encoder_val);
}
第2步:电机驱动和编码器数值读取、写入(编码器省略)
/*
直流电机驱动
说 明: 注意!!! 在进行大能量正反转切换时, PWM 占空比缓降为零(≥100ms)后再切换,否则可能造成驱动器损坏
参 数: status 电机状态。 0停止 1正转 2反转
speed 电机速度(0-1000)
返回值: 无
*/
void DC_MotorDrive(u8 status, u16 speed)
{
static u8 stop_sign = 0; //停止标记(电机启动后必须停止 100ms 以上才可以换方向)
static u8 dir_sign = 0; //电机转动方向标志(1正转 2反转)
u16 pwm_val = 0; //PWM 输出占空比
if(status==1 & dir_sign!=2) //正转
{
dir_sign = 1; //标记电机转动方向
stop_sign = 1;
DC_MOTOR_FWD = 0; //正转(有效)
DC_MOTOR_REV = 1; //反转(无效)
TIM_SetCompare1(TIM1, speed); //设置 PWM 占空比
TIM_Cmd(TIM1, ENABLE); //开启PWM定时器
}
else if(status==2 & dir_sign!=1) //反转
{
dir_sign = 2;
stop_sign = 1;
DC_MOTOR_FWD = 1; //正转(无效)
DC_MOTOR_REV = 0; //反转(有效)
TIM_SetCompare1(TIM1, speed); //设置 PWM 占空比
TIM_Cmd(TIM1, ENABLE);
}
else if(status==0 | speed == 0)
{
dir_sign = 0;
DC_MOTOR_FWD = 0; //正转
DC_MOTOR_REV = 0; //反转(正反转都位低电平,电机制动)
TIM_SetCompare1(TIM1, speed);
if(stop_sign)
{
//printf("电机开始停止\r\n");
delay_ms(200);
stop_sign = 0;
//printf("电机停止完成\r\n");
}
TIM_Cmd(TIM1, DISABLE); //关闭 TIM1_PWM 定时器
}
}
第二层,功能层:“X管”的上升、下降,以及移动到指定位置
第1步:电机复位,复位虽然并不复杂但却是电机控制中非常关键的一步,只有通过电机复位,才能确定“X管”***的零点位置,以后“X管”的限位,以及“X管”移动到指定的位置都是相对“X管”的零点位来确定的。
/*
球馆状态
pipe_status 球馆状态值
位0: 球馆上升
位1: 球馆下降
位2: 球馆复位中...
位3: 球馆自动移动中...
位4-6: 保留
位7: 球馆复位标志(0未复位 1已复位)
*/
vu8 pipe_status = 0;
u16 pipe_target = 0; //球馆目标位置
/*
球馆复位
说 明: 球馆移动到原点位置
参 数: 无
返回值: 无
*/
void X_Pipe_Reset(void)
{
static u8 status = 0;
u16 site = 0; //球馆上下位置
if(!status)
{
if(DI_KEY & 0x10) //不在零点
{
DC_MotorDrive(2, UP_SPEED_FAST); //球馆上升
}
else //回到零点
{
DC_MotorDrive(0, 0); //球馆电机停止
EncoderReset(); //编码器复位
status = 1;
}
}
if(status) //已回零点
{
site = GetEncoder_val(); //获取电机当前位置
if(site < 20) //不在原点
{
DC_MotorDrive(1, DOWN_SPEED_FAST); //球馆下降
}
else //到达原点
{
DC_MotorDrive(0, 0); //球馆拉杆电机停止
status = 0; //复位完成
pipe_status &= ~0x04; //复位状态清0
pipe_status |= 0x80; //复位标志置1
}
}
}
第2步“X管”手动移动,这里只用上升来举例,下降同理
#define UP_LIMIT 20 //球馆上限位
#define DOWN_LIMIT 2500 //球馆下限位
/*
球馆上升
说 明: 只有复位后,上限位才有效。
参 数: speed 速度
返回值: 无
*/void X_Pipe_up(u16 speed)
{
u16 site;
if(pipe_status & 0x80) //球馆已复位
{
site = GetEncoder_val(); //获取电机当前位置
if(site > UP_LIMIT)
{
DC_MotorDrive(2, speed); //直流电机反转
}
else
{
DC_MotorDrive(0, 0); //球馆电机停止
}
}
else //没有复位,上限位无效
{
DC_MotorDrive(2, speed); //直流电机反转
}
}
第3步“X管”移动到指定位置
/*
球馆移动到指定位置
说 明: 指定位置是相对于零点位的***位置
参 数: site ***位置,
site 为0或超出限位,终止自动移动状态
返回值: 无
*/
void X_Pipe_Move(u16 site)
{
static u8 status = 0; //状态
u16 site2 = 0; //移动中位置
if(!(pipe_status & 0x80)) //球馆未复位
{
status = 0;
pipe_status &= ~0x08; //球馆移动状态清0
return; //退出(这里有多个判断代码段,每个判断段里执行的程序差不多,可以用goto语句优化一下)
}
if(site>=DOWN_LIMIT || site<=UP_LIMIT) //目标位置超出限位范围
{
DC_MotorDrive(0, 0); //球馆电机停止
status = 0; //状态清0
pipe_status &= ~0x08; //球馆移动状态清0
return; //退出
}
site2 = GetEncoder_val(); //获取电机位置
if(site2>=DOWN_LIMIT || site2<=UP_LIMIT) //当前位置超出限位范围
{
DC_MotorDrive(0, 0); //球馆电机停止
status = 0;
pipe_status &= ~0x08;
return;
}
if(!status)
{
if(site > site2)
status = 1; //球馆向下移动
else if(site < site2)
status = 2; //球馆向上移动
else
{
DC_MotorDrive(0, 0); //球馆电机停止
status = 0;
pipe_status &= ~0x08;
return;
}
}
if(status == 1) //球馆向下移动(反转)
{
if((site-site2) > 50) //快速移动
{
X_Pipe_down(DOWN_SPEED_FAST); //球馆下降
}
else //慢速移动
{
if(!(site2>=site))
{
X_Pipe_down(DOWN_SPEED_FAST); //球馆慢速下降
}
else //到达目标位
{
DC_MotorDrive(0, 0); //球馆电机停止
status = 0;
pipe_status &= ~0x08; //球馆移动状态清0
}
}
}
else if(status == 2) //向上移动(正转)
{
if((site2-site) > 50) //快速移动
{
X_Pipe_up(DOWN_SPEED_FAST); //球馆上升
}
else //慢速移动
{
if(!(site>=site2))
{
X_Pipe_up(DOWN_SPEED_SLOW); //球馆慢速上升
}
else //到达目标位
{
DC_MotorDrive(0, 0); //球馆电机停止
status = 0;
pipe_status &= ~0x08; //球馆移动状态清0
}
}
}
}
第三层,状态层:根据状态自动运行(相应的按钮操作部分很常见,也很简单所以就省略这部分代码)
说明:下面的这个“状态函数”是工程中的一个任务,还有“按钮扫描”等其他任务,下面举了两个列子来说明。
1.假如有人按了一下“复位”按钮,“按钮扫描”函数仅仅把复位状态置1,状态函数根据状态值自动执行相应的函数。
2.按了一下“一键到位”按钮,“按钮扫描”同样把自动移动状态置1,并把一键到位的位置值赋给 pipe_target 变量。状态函数根据状态值自动执行相应的函数。
/*
设备状态
说 明: 根据设备的状态执行相应的程序
参 数: 无
返回值: 无
*/
void XH_status(void)
{
if(pipe_status & 0x04) //球馆复位状态置1
{
X_Pipe_Reset(); //球馆复位
}
else if(pipe_status & 0x08) //球馆自动移动状态置1
{
X_Pipe_Move(pipe_target); //球馆移动到指定位置
}
if(!(pipe_status & 0x7F)) //球馆状态为0
{
DC_MotorDrive(0, 0); //球馆电机停止
}
}
好了,以上就是我要分享的分层思维,以及具体的实现过程,欢迎大家指点。
您好,请点击在线客服进行在线沟通!