本篇文章讲述的是一个基于STM32的温控门闸装置的毕业设计,实现基本测温和自动开关门功能。
主要功能器件:
STM32F103VET6(主控芯片);
MXL90614(红外温度计);
HR4988(步进电机驱动IC);
HC_SR04(超声波测距模块);
OLED(0.96寸显示屏);
光电开关;
蜂鸣器。
注:以上测温、电机、测距3个器件的驱动在以下文件中有介绍,本片文章就不另作介绍。
1. 设计框架
温控门闸,测温和门闸控制是放在首位的。测温传感器在该设计上主打一个无接触式测温,所以选用了红外温度计。MXL90614测温模块的作用在于测温,MCU可根据测温情况控制门闸打开。门闸的开关控制则使用步进电机实现,通过控制电机的正反转一定角度实现门闸开关效果。
在设计时使用HC_SR04模块作为人体检测传感器,通过检测到与障碍物的距离小于一定值时,判断为有人经过(本次设计的距离范围为25cm以内)。在实际使用中,使用了两个该模块,主要用于出门人体检测以及是否完全通过。当行人出门时,检测到行人则控制门闸打开,当两个传感器都没有检测到行人时则表示完全通过,MCU控制门闸关闭。工作流程如下图。
2. 驱动设计
2.1 门闸控制
HR4988引脚连接:
ENABLE | PB5 |
STEP | PB6 |
DIR | PB7 |
GATE | PE3 |
驱动程序1:初始化。
控制步进电机的转动方向,转动速度以及角位移实际是控制HR4988的方向引脚以及步进引脚。给DIR引脚高低电平可控制电机正反转;STEP引脚则使用脉冲控制,在1/16步进模式下,16个脉冲电机转动1.8度。控制脉冲的频率可实现电机转动速度的控制,控制脉冲数,可实现角位移量的控制。在编写程序时使用了TIM4输出PWM波控制STEP引脚。
GATE_Init()为光电开关初始化函数,空闲时,光电开关输出引脚输出高电平,遮挡光源后为低电平。光电开关用于门闸定位。
/*
引脚连接:
DIR PB7 方向
ENABLE PB5 使能引脚(低有效)
STEP PB6 步进引脚
*/
//步进电机驱动初始化
void GATE_Init(void)
{
GPIO_InitTypeDef GATE_init;
GATE_init.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GATE_init.GPIO_Pin = GPIO_Pin_3;
GATE_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE,&GATE_init);
}
void HR4988_Init(void)
{
//1.引脚初始化配置
GPIO_InitTypeDef hr4988_gpio_init;
hr4988_gpio_init.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_5;
hr4988_gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
hr4988_gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&hr4988_gpio_init); //PB4、PB5通用推挽输出模式
hr4988_gpio_init.GPIO_Pin = GPIO_Pin_6;
hr4988_gpio_init.GPIO_Mode = GPIO_Mode_AF_PP; //PB6复用推挽输出
hr4988_gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&hr4988_gpio_init);
MOTOR_ENABLE();//使能输出
//2.配置TIM4基本计数模式
TIM_DeInit(TIM4); //复位定时器
TIM_TimeBaseInitTypeDef hr4988_TimeBase_Init;
hr4988_TimeBase_Init.TIM_Prescaler = 71; //预分频值 72分频
hr4988_TimeBase_Init.TIM_CounterMode = TIM_CounterMode_Up;//向上计数模式
hr4988_TimeBase_Init.TIM_Period = 3999; //重装载值4000
hr4988_TimeBase_Init.TIM_ClockDivision = TIM_CKD_DIV1;//时钟分频:不分频
hr4988_TimeBase_Init.TIM_RepetitionCounter = 0; //重复计数值
TIM_TimeBaseInit(TIM4,&hr4988_TimeBase_Init);
//3.配置TIM输出比较模式
TIM_OCInitTypeDef hr4988_OCInit;
TIM_OCStructInit(&hr4988_OCInit);
hr4988_OCInit.TIM_OCMode = TIM_OCMode_PWM1; //PWM1模式
hr4988_OCInit.TIM_Pulse = 1999; //输出比较值
hr4988_OCInit.TIM_OutputState = TIM_OutputState_Enable;
TIM_OC1Init(TIM4,&hr4988_OCInit);
//4.配置中断
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE);//使能更新中断
TIM_ClearFlag(TIM4,TIM_IT_Update);//清除挂起标志位
}
电机结构体参数:
typedef struct
{
uint32_t currentLocation;//当前位置
uint32_t sargetLocation;//目标位置
uint16_t state;//运行状态
uint16_t speedCount;
uint8_t direction;//方向
}Motor_parameter;
电机在工作时可分为复位状态、工作状态以及停止状态,复位状态则是上电后,电机控制门闸运行到关闭位置的这一阶段;工作模式则是正常开门和关门动作;停止状态时电机处于静止。在控制电机运行指定角位移时,可用到当前位置和目标位置两个参数。(例如,门闸关闭时与装置处于90度角,电机带动门闸转动90度至开门状态,1.8度需要16个脉冲,90度则需要800个脉冲。将当前距离设为0,目标距离为800,每进一次定时器中断当前位置加1,待当前位置等于目标位置时,门闸以实现90度开门动作。)
驱动程序2:电机工作函数
1.复位工作:复位就是把门闸运行至关闭状态。
在此处目标位置可不用管,因为目标位置是未知的(上电前门闸的位置是未知的)。只需要把门闸按顺时针方向运动到光电开关处,将此处位置设为0,随后逆转反向,再转动90度便到达关门复位位置。
在连接电机和门闸时使用同步轮,电机与门闸的同步齿轮大小为1:2,门闸转动90度电机需要转动180度,即门闸转动90度需要1600个脉冲。
2.开关门工作:开门分为顺时针开门和逆时针开门。
在进门时,门闸需要顺着行人方向打开,即逆时针开门。出门时,门闸顺时针方向开门。
复位后,电机的距离设定为 0,1600,3200。光电开关处位置为0,关门时当前位置为1600。控制门闸进行开门动作,进门时,则需将方向设为逆时针,目标位置为3200,电机便会控制门闸转动90度至开门;出门开门时,将方向设为顺时针,目标位置为0;而关门则与开门方向相反,目标位置1600。
逆时针转动当前位置++,顺时针转动当前位置-- 详细看中断服务函数。由于电机控制门闸转动速度不需要很快,就没有可屏蔽S曲线。
//电机复位工作
void Motor_Reset(void)
{
motor_parameter.direction = CW;
motor_parameter.currentLocation = 0;
motor_parameter.sargetLocation = 0;
motor_parameter.state = MOTOR_STATE_RESET; //进入复位模式
MOTOR_DIR(motor_parameter.direction); //顺时针
MOTOR_SET_SPEED(MOTOR_GO_HOME_SPEED);
MOTOR_START();
while(motor_parameter.state != MOTOR_STATE_STOP);
MOTOR_STOP();
//calculate_S_PulseFreq(2 ,MOTOR_90_LOCATION);//S曲线运动可不用
}
/*
\brief: 配置motor工作模式
\param: dir :运行方向
location:目标距离
\retval: none
*/
void MOTOR_Work_Config(uint8_t dir,uint32_t location)
{
if(dir==CW)//顺时针 目标距离<当前距离
{
if(location > motor_parameter.currentLocation)
{
motor_parameter.sargetLocation = motor_parameter.currentLocation;
}
}
else
{
if(location < motor_parameter.currentLocation)
{
motor_parameter.sargetLocation = motor_parameter.currentLocation;
}
else if(location > MOTOR_180_LOCATION)
{
motor_parameter.sargetLocation = MOTOR_180_LOCATION;
}
}
motor_parameter.speedCount = 0;
motor_parameter.state = MOTOR_STATE_WORKING;
motor_parameter.direction = dir;
MOTOR_DIR(motor_parameter.direction);
//MOTOR_SET_SPEED(motor_S_Array[motor_parameter.speedCount]);//设置速度
MOTOR_SET_SPEED(MOTOR_GO_HOME_SPEED);//设置速度,可不用S曲线速度
MOTOR_START();
}
驱动程序3:中断服务函数
void TIM4_IRQHandler(void)
{
if(RESET != TIM_GetITStatus(TIM4,TIM_IT_Update))
{
TIM_ClearITPendingBit(TIM4,TIM_IT_Update); //清除中断标志位
if(motor_parameter.state == MOTOR_STATE_STOP) //停止模式
{
MOTOR_STOP();
}
else if(motor_parameter.state == MOTOR_STATE_RESET) //复位模式
{
motor_parameter.currentLocation++;
static uint8_t motor_flag=0;
if(MOTOR_HOME() && motor_flag == 0)
{
motor_parameter.direction = CCW;
motor_parameter.currentLocation = 0; //归0当前位置
motor_parameter.sargetLocation = MOTOR_90_LOCATION;//目标位置 90°
MOTOR_DIR(motor_parameter.direction);
motor_flag = 1;
}
if(motor_flag==1 && motor_parameter.currentLocation == motor_parameter.sargetLocation)
{
motor_parameter.state &= ~MOTOR_STATE_RESET;
motor_flag = 0;
MOTOR_STOP();
}
}
else if(motor_parameter.state & MOTOR_STATE_WORKING)//运行
{
if(motor_parameter.direction == CCW) //逆时针
motor_parameter.currentLocation++;
else if(motor_parameter.direction == CW) //顺时针
motor_parameter.currentLocation--;
if(fabs((float )motor_parameter.currentLocation - (float )motor_parameter.sargetLocation) < (MOTOR_S_NUM -1))//减速阶段
{
motor_parameter.speedCount--;
}
else if(motor_parameter.speedCount < (MOTOR_S_NUM-1))//加速阶段
{
motor_parameter.speedCount++;
}
if(motor_parameter.currentLocation==motor_parameter.sargetLocation)
{
if(motor_parameter.currentLocation==MOTOR_90_LOCATION)//门关闭状态
{
DOOR_state = DOOR_STATIC;
}
motor_parameter.state &= ~MOTOR_STATE_WORKING;
MOTOR_STOP();
}
//MOTOR_SET_SPEED(motor_S_Array[motor_parameter.speedCount]);
}
}
}
2.2 温度采集
MXL90614引脚连接:
SCL | PB10 |
SDA | PB11 |
红外温度计采集温度驱动程序可直接使用【STM32篇】驱动MXL90614红外测温模块
读取温度函数:
//读内存
uint16_t MLX906_ReadMemory(void)
{
uint8_t Pec,PecReg,ErrorCounter;
uint8_t TempL=0;
uint8_t TempH=0;
uint8_t arr[6];
ErrorCounter=0;
//slaveAddr <<=1;//2-7位表示从机地址
do
{
ErrorCounter++;
if(ErrorCounter==10)
{
return 0;
}
MLX906_START_Sign();//起始信号
if(MLX906_SendByte(0x00))//发送MLX90614地址
continue;
if(MLX906_SendByte(0x07))//发送读MLX90614 RAM地址
continue;
MLX906_START_Sign();//重新启动
if(MLX906_SendByte(0x01))//发送数据采集命令
continue;
TempL=MLX906_ReadByte();//读取地位数据
TempH=MLX906_ReadByte();//读取高位数据
Pec=MLX906_ReadByte();//读取校验位
MLX906_STOP_Sign();//停止信号
arr[5]= 0x00;
arr[4]= 0x07;
arr[3]= 0x01;
arr[2]= TempL;
arr[1]= TempH;
arr[0]= 0;
PecReg=CRC_Calculation(arr);//计算CRC校验
}while(PecReg!=Pec);
return (uint16_t)((TempH<<8)|TempL);
}
//读温度
float MLX906_Read_Temp(void)
{
return (float )MLX906_ReadMemory()*0.02-273.15;
}
2.3 人体检测
HC_SR04引脚连接:
TRIG_1 | PD11 |
ECHO_1 | PD0 |
TRIG_2 | PD13 |
ECHO_2 | PD1 |
HC_SR04的驱动使用外部中断和TIM两个MCU外设,当距离小于25cm时,标志位置位,检测到行人,大于25cm时标志位复位。
#include "hc_sr04.h"
/*
引脚说明:
TRIG :触发控制信号输入(推挽输出)
ECHO :回响信号输出(配置为外部中断输入)
*/
uint32_t HCSR04_1_usTime=0;//HCSR04_1 回响信号时间间隔
uint32_t HCSR04_2_usTime=0;//HCSR04_2 回响信号时间间隔
uint8_t HC_TIM6_IT_count=0;//进中断次数
uint8_t HC_TIM7_IT_count=0;
uint8_t HCSR04_1_READ_STATE=0;//HCSR04_1 读状态标志位
uint8_t HCSR04_2_READ_STATE=0;
//TIM定时器配置
void HC_SR04_TIM_config(void)
{
//1.外设复位
TIM_DeInit(TIM6);
TIM_DeInit(TIM7);
//2.TIM外设相关功能配置
TIM_TimeBaseInitTypeDef hcsr04_tim_init;
hcsr04_tim_init.TIM_Prescaler = 71; //72分频
hcsr04_tim_init.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
hcsr04_tim_init.TIM_Period = 0xFFFF;//重装载值65535
hcsr04_tim_init.TIM_ClockDivision = TIM_CKD_DIV1;//时钟分频 1
hcsr04_tim_init.TIM_RepetitionCounter = 0;//从0开始计数
TIM_TimeBaseInit(TIM6,&hcsr04_tim_init);
TIM_TimeBaseInit(TIM7,&hcsr04_tim_init);
//3.配置TIM中断
TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE);//更新中断
TIM_ITConfig(TIM7,TIM_IT_Update,ENABLE);
TIM_ClearFlag(TIM6,TIM_FLAG_Update);//清除更新标志位
TIM_ClearFlag(TIM7,TIM_FLAG_Update);
//失能TIM
TIM_Cmd(TIM6,DISABLE);//关闭定时器
TIM_Cmd(TIM7,DISABLE);//关闭定时器
}
//HC_SR04初始化配置
void HC_SR04_Init(void)
{
//初始化GPIO
GPIO_InitTypeDef hcsr04_gpio_init;
hcsr04_gpio_init.GPIO_Pin = GPIO_Pin_11|GPIO_Pin_3;
hcsr04_gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;//通用推挽
hcsr04_gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD,&hcsr04_gpio_init);//初始化PD11 PD13
hcsr04_gpio_init.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;
hcsr04_gpio_init.GPIO_Mode = GPIO_Mode_IPD;//下拉输入
GPIO_Init(GPIOD,&hcsr04_gpio_init);//初始化PD12 PD14
//配置外部中断
GPIO_EXTILineConfig(GPIO_PortSourceGPIOD,GPIO_PinSource0);//选择中断引脚
GPIO_EXTILineConfig(GPIO_PortSourceGPIOD,GPIO_PinSource1);
EXTI_InitTypeDef hcsr04_exti_init;
hcsr04_exti_init.EXTI_Line = EXTI_Line0|EXTI_Line1;
hcsr04_exti_init.EXTI_Mode = EXTI_Mode_Interrupt;//中断模式
hcsr04_exti_init.EXTI_Trigger = EXTI_Trigger_Rising_Falling;//双边沿触发
hcsr04_exti_init.EXTI_LineCmd= ENABLE;//使能中断线
EXTI_Init(&hcsr04_exti_init);
// EXTI_GenerateSWInterrupt(EXTI_Line0);
// EXTI_GenerateSWInterrupt(EXTI_Line1);
EXTI_ClearFlag(EXTI_Line0|EXTI_Line1);//清除挂起标志位
//配置定时器
HC_SR04_TIM_config();
HCSR04_1_TRIG_0();//拉低输入引脚
HCSR04_2_TRIG_0();
}
/*
外部中断服务函数0
函数功能:捕获HCSR04_1_ECHO引脚电平
*/
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0))
{
EXTI_ClearITPendingBit(EXTI_Line0);
if(Bit_RESET!=GPIO_ReadInputDataBit(GPIOD,GPIO_Pin_0)) //上升沿
{
//启动定时器
TIM_Cmd(TIM6,ENABLE);
}
else //下降沿
{
HCSR04_1_usTime=TIM_GetCounter(TIM6);//获取计数值
//失能定时器
TIM_Cmd(TIM6,DISABLE);
HCSR04_1_usTime=HCSR04_1_usTime+HC_TIM6_IT_count*65536;
HC_TIM6_IT_count=0;
TIM_SetCounter(TIM6,0x00);//清空计数器
HCSR04_1_READ_STATE=HCSR04_1_READABLE;//可读
if((HCSR04_1_usTime/58)<25)//小于25cm
{
HCSR04_1_READ_STATE = HCSR04_1_READABLE;//有人经过
}
else
{
HCSR04_1_READ_STATE = HCSR04_1_UNREADABLE;
}
}
}
}
/*
外部中断服务函数1
函数功能:捕获HCSR04_2_ECHO引脚电平
*/
void EXTI1_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0))
{
EXTI_ClearITPendingBit(EXTI_Line0);
if(Bit_RESET!=GPIO_ReadInputDataBit(GPIOD,GPIO_Pin_1)) //上升沿
{
//启动定时器
TIM_Cmd(TIM7,ENABLE);
}
else //下降沿
{
HCSR04_2_usTime=TIM_GetCounter(TIM7);//获取计数值
//失能定时器
TIM_Cmd(TIM7,DISABLE);
HCSR04_2_usTime=HCSR04_2_usTime+HC_TIM7_IT_count*65536;
HC_TIM7_IT_count=0;
TIM_SetCounter(TIM7,0x00);//清空计数器
if((HCSR04_2_usTime/58)<25)//小于25cm
{
HCSR04_2_READ_STATE = HCSR04_2_READABLE;//有人经过
}
else
{
HCSR04_2_READ_STATE = HCSR04_2_UNREADABLE;
}
}
}
}
void TIM6_IRQHandler(void)
{
if(RESET!=TIM_GetFlagStatus(TIM6,TIM_FLAG_Update))
{
TIM_ClearFlag(TIM6,TIM_FLAG_Update);//清除更新标志位
HC_TIM6_IT_count++;
}
}
void TIM7_IRQHandler(void)
{
if(RESET!=TIM_GetFlagStatus(TIM7,TIM_FLAG_Update))
{
TIM_ClearFlag(TIM7,TIM_FLAG_Update);//清除更新标志位
HC_TIM7_IT_count++;
}
}
//触发信号
void HCSR04_TriggerGignal(uint8_t hcsr04_x)
{
if(hcsr04_x==HCSR04_1)
{
HCSR04_1_TRIG_1();
Delay_us(30);
HCSR04_1_TRIG_0();
}
else if(hcsr04_x==HCSR04_2)
{
HCSR04_2_TRIG_1();
Delay_us(30);
HCSR04_2_TRIG_0();
}
}
触发信号则使用TIM中断,定时发送触发信号,间隔为200ms。
/*
\brief: TIM3工作服务函数
函数功能: 负责给超声波模块发送触发信号
*/
void TIM3_IRQHandler(void)
{
if(RESET!=TIM_GetITStatus(TIM3,TIM_IT_Update))
{
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);//清除外部中断
HCSR04_TriggerGignal(HCSR04_1);
HCSR04_TriggerGignal(HCSR04_2);
}
}
2.4 显示
OLED屏引脚连接:
D0 CLK | PC0 |
D1 MOSI | PC1 |
RES# | PC2 |
DC | PC3 |
CS | PC4 |
在编写OLED显示函数时使用显存进行屏幕刷新,建立一个与屏幕等大的数组保存显示数据,随后需要实现画点函数,向数组里写入需要的数据,并将数据刷到屏幕上。
/*
\brief: 画点函数
\param: x 横坐标 0~127
y 纵坐标 0~63
stat 1 点亮 0 不点亮
\retval: none
*/
void OLED_Draw_Dot(uint8_t x,uint8_t y,uint8_t stat)
{
if(x<128 && y<64)
{
uint8_t bit=y%8;
uint8_t page=y/8;
if(stat)
OLED_GRAM[page][x]|=1<<bit;
else
OLED_GRAM[page][x]&=~(1<<bit);
}
}
//刷新函数(将缓存数据刷新到屏幕上)
void OLED_Refresh(uint8_t x1,uint8_t x2,uint8_t y1,uint8_t y2)
{
uint8_t i,j;
for(i = y1;i <= y2;i++)
{
OLED_Set_Pos(i,x1);//设置起始坐标
for(j=x1;j<=x2;j++)
{
OLED_Write_Byte(OLED_GRAM[i][j],OLED_DAT);
}
}
}
详细请看附件。