stm32f103基于pid的蓝牙循迹小车

前言

经过一个月对stm32的学习,终于完成了一个小车的项目,本项目用到了pid对小车进行控速,两个电机,一个万向轮,一个3路灰度循迹模块进行循迹,0.96寸oled进行一些参数的显示,通信方式使用qt写的app传到手机,用手机与hc06蓝牙模块进行简单的通信。


 

一、霍尔编码器以及定时器计数原理

对于霍尔编码器,工作原理如下

a451f97b477246d19984cbffe0945f6d.png

 可以得到两种输出方式,通过定时器的编码器计数模式进行计数,计数原理如下

5be242da8a0649f0a3b2595b64c3fa36.png

 从stm32的开发手册里我们可以看到,配置编码器计数模式的方法

8dc9ffc7f4ab4c5aabb7de417ad80aa8.png

 8bfebf7d305647fa88b231da999ac806.png

 我使用的是定时器3,4对两个电机进行计数,代码如下

void gpio_clk_init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//定时器3使能
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);//定时器4使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//gpioA使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//gpioB使能
    
    //定时器3gpio初始化
    GPIO_InitTypeDef gpioa_init;
    gpioa_init.GPIO_Pin=GPIO_Pin_6 |GPIO_Pin_7;
    gpioa_init.GPIO_Pin=GPIO_Mode_IN_FLOATING;//必须配置成浮空输入
    gpioa_init.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&gpioa_init);
    
    //定时器4gpio初始化
    GPIO_InitTypeDef gpiob_init;
    gpiob_init.GPIO_Pin=GPIO_Pin_6 |GPIO_Pin_7;
    gpiob_init.GPIO_Pin=GPIO_Mode_IN_FLOATING;//必须配置成浮空输入
    gpiob_init.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&gpiob_init);
}
void time3_init(void)
{
    NVIC_InitTypeDef nvic_init={0};
    
    gpio_clk_init();//初始化引脚
    //定时器初始化
    tim3_timbase.TIM_Period=65535;//重装载值
    tim3_timbase.TIM_Prescaler=0;//分频值
    tim3_timbase.TIM_CounterMode=TIM_CounterMode_Up;//递增计数
    tim3_timbase.TIM_ClockDivision=TIM_CKD_DIV1;//不滤波
    tim3_timbase.TIM_RepetitionCounter=DISABLE;//失能缓冲区
    TIM_TimeBaseInit(TIM3,&tim3_timbase);
    
    //配置编码器捕获
    tim3_icinit.TIM_Channel=TIM_Channel_1;//通道1
    tim3_icinit.TIM_ICFilter=0;//滤波
    tim3_icinit.TIM_ICPolarity=TIM_ICPolarity_Rising;//上升沿捕获
    tim3_icinit.TIM_ICPrescaler=TIM_ICPSC_DIV1;//不分频
    tim3_icinit.TIM_ICSelection=TIM_ICSelection_DirectTI;//通道选择,TIM输入1、2、3或4被选择为分别连接到IC1、IC2、IC3或IC4
    TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//配置编码器计数。BothEdge(底部边缘)
    TIM_ICInit(TIM3,&tim3_icinit);
    
    tim3_icinit.TIM_Channel=TIM_Channel_2;//通道2
    TIM_ICInit(TIM3,&tim3_icinit);
    
    //初始化标志位,计数器
    TIM_ClearFlag(TIM3,TIM_FLAG_Update);//清除标志位
    TIM_SetCounter(TIM3,0);//TIM3->CNT=0;
    
    //配置中断
    nvic_init.NVIC_IRQChannel=TIM3_IRQn;//中断通道
    nvic_init.NVIC_IRQChannelCmd=ENABLE;//中断使能
    nvic_init.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级;
    nvic_init.NVIC_IRQChannelSubPriority=1;//响应优先级
    NVIC_Init(&nvic_init);
    
    TIM_ITConfig(TIM3,TIM_IT_Update | TIM_IT_CC1 |TIM_IT_CC2,ENABLE);//配置定时器,允许更新中断,CC1,CC2捕获中断
    TIM_Cmd(TIM3,ENABLE);//开启定时器
}

void time4_init(void)
{
    NVIC_InitTypeDef nvic_init={0};
    
    gpio_clk_init();//初始化引脚
    //定时器初始化
    tim4_timbase.TIM_Period=65535;//重装载值
    tim4_timbase.TIM_Prescaler=0;//分频值
    tim4_timbase.TIM_CounterMode=TIM_CounterMode_Up;//递增计数
    tim4_timbase.TIM_ClockDivision=TIM_CKD_DIV1;//不滤波
    tim4_timbase.TIM_RepetitionCounter=DISABLE;//失能缓冲区
    TIM_TimeBaseInit(TIM4,&tim4_timbase);
    
    //配置编码器捕获
    tim4_icinit.TIM_Channel=TIM_Channel_1;//通道1
    tim4_icinit.TIM_ICFilter=0;//滤波
    tim4_icinit.TIM_ICPolarity=TIM_ICPolarity_Rising;//上升沿捕获
    tim4_icinit.TIM_ICPrescaler=TIM_ICPSC_DIV1;//不分频
    tim4_icinit.TIM_ICSelection=TIM_ICSelection_DirectTI;//通道选择,TIM输入1、2、3或4被选择为分别连接到IC1、IC2、IC3或IC4
    TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//配置编码器计数。BothEdge(底部边缘)
    TIM_ICInit(TIM4,&tim4_icinit);
    
    tim4_icinit.TIM_Channel=TIM_Channel_2;//通道2
    TIM_ICInit(TIM4,&tim4_icinit);
    
    //初始化标志位,计数器
    TIM_ClearFlag(TIM4,TIM_FLAG_Update);//清除标志位
    TIM_SetCounter(TIM4,0);//TIM4->CNT=0;
    
    //配置中断
    nvic_init.NVIC_IRQChannel=TIM4_IRQn;//中断通道
    nvic_init.NVIC_IRQChannelCmd=ENABLE;//中断使能
    nvic_init.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级;
    nvic_init.NVIC_IRQChannelSubPriority=1;//响应优先级
    NVIC_Init(&nvic_init);
    
    TIM_ITConfig(TIM4,TIM_IT_Update | TIM_IT_CC1 |TIM_IT_CC2,ENABLE);//配置定时器,允许更新中断,CC1,CC2捕获中断
    TIM_Cmd(TIM4,ENABLE);//开启定时器
}

还有编写中断函数,对计数值进行处理

void TIM3_IRQHandler(void)
{
    //uint8_t i;
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)//判断是否为更新中断
    {
        TIM_ClearFlag(TIM3,TIM_IT_Update);
    }
    if((TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)||(TIM_GetITStatus(TIM3, TIM_IT_CC2) != RESET))//判断是否为捕获中断
    {
        left_count=(short)TIM3->CNT;
        TIM_ClearFlag(TIM3, TIM_IT_CC1|TIM_IT_CC2);//清除标志位
    }
    
}

void TIM4_IRQHandler(void)
{
    
    if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)//判断是否为更新中断
    {
        TIM_ClearFlag(TIM4,TIM_IT_Update);
    }
    if((TIM_GetITStatus(TIM4, TIM_IT_CC1) != RESET)||(TIM_GetITStatus(TIM4, TIM_IT_CC2) != RESET))//判断是否为捕获中断
    {
        right_count=(short)TIM4->CNT;
        TIM_ClearFlag(TIM4, TIM_IT_CC1|TIM_IT_CC2);//清除标志位
    }
    
}

在将电机的AB和编码器供电两极连接到对应引脚就能计数了,可以手动转动轮子,使用串口对计数进行打印,能观察到计数值。

64ca523024b1451888928d25834422dc.png

二、使用pwm占空比对电机速度进行控制

我使用的是定时器2进行4路pwm输出,由于定时器2使用了串口2,串口2我用于蓝牙通信,所以需要重定向

faa5d18e0c824cae982dffa907e6ad83.png

 我这里使用定时器2的完全重定向到PB10,PB11,PB3,PA15f39814c5719d4b9abe5d8351e48349a6.png

c93b40e6cacf4d7eaa3745690f853008.png

ce83c2e81cf747b29b9949c4fea03bcc.png

 重定向方法:

定时器2的引脚使用组合:

20170513100904881

1.当不重映射时,IO口是PA0、PA1、PA2、PA3

2.要使用PA15、PB3、PA2、PA3的端口组合,要调用下面的语句进行部分重映射:

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);   //重映射必须要开AFIO时钟

  GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);

3.要使用PA0、PA1、PB10、PB11的端口组合,要调用下面的语句进行部分重映射:

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);   //重映射必须要开AFIO时钟

  GPIO_PinRemapConfig(GPIO_PartialRemap2_TIM2, ENABLE);

4.要使用PA15、PB3、PB10、PB11的端口组合,要调用下面的语句进行完全重映射:

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);   //重映射必须要开AFIO时钟

  GPIO_PinRemapConfig(GPIO_FullRemap_TIM2, ENABLE);

同时还要禁用JTAG功能,PA15、PB3、PB10、PB11才会正常输出。

GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE);//必须禁用swj

 代码如下:

void time2_init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//定时器2使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//gpioA使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//gpioB使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//AFIO时钟使能
    GPIO_PinRemapConfig(GPIO_FullRemap_TIM2,ENABLE);//定时器2重映射,PA15,PB3,PB10,PB11
    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE);//必须禁用swj
    
    //定时器2gpio初始化
    gpioc_init.GPIO_Pin=GPIO_Pin_3 |GPIO_Pin_10 | GPIO_Pin_11;
    gpioc_init.GPIO_Mode=GPIO_Mode_AF_PP;
    gpioc_init.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&gpioc_init);
    gpioc_init.GPIO_Pin=GPIO_Pin_15 ;
    GPIO_Init(GPIOA,&gpioc_init);
    
    //初始化定时器1
    tim2_timbase.TIM_Period=999;
    tim2_timbase.TIM_Prescaler=359;
    tim2_timbase.TIM_ClockDivision=TIM_CKD_DIV1;
    tim2_timbase.TIM_CounterMode=TIM_CounterMode_Up;
    tim2_timbase.TIM_RepetitionCounter=DISABLE;
    TIM_TimeBaseInit(TIM2,&tim2_timbase);
    
    //初始化PWM
    tim2_oc.TIM_OCMode=TIM_OCMode_PWM1;//模式PWM
    tim2_oc.TIM_Pulse=0;//比较值
    tim2_oc.TIM_OutputState=TIM_OutputState_Enable;//输出比较使能
    tim2_oc.TIM_OCPolarity=TIM_OCPolarity_High;//输出极性 高电平
    TIM_OC1Init(TIM2,&tim2_oc);//通道1
    TIM_OC2Init(TIM2,&tim2_oc);//通道2
    TIM_OC3Init(TIM2,&tim2_oc);//通道3
    TIM_OC4Init(TIM2,&tim2_oc);//通道4
    
    TIM_SetCounter(TIM2,0);//TIM3->CNT=0;计数清0
    
    TIM_CtrlPWMOutputs(TIM2,ENABLE);    //MOE 主输出使能(高级定时器必须设置)
    TIM_Cmd(TIM2, ENABLE);  //使能tim2
    
}

这样定时器2,3,4我们就配置好了,通过修改定时器2每个通道pwm的占空比就能控制电机的转速了

三、使用systick的中断函数进行pid和速度的计算,还有oled的显示

代码如下:

void SysTick_Handler(void)
{
    float left_speed=0;//左轮转轴速度
    float right_speed=0;//右轮转轴速度
    
    float left_pwm=0;
    int left_temp=0;//pid调试用,表示当前速度
    float right_pwm=0;
    int right_temp=0;//pid调试用,表示当前速度
    
    int integer=0;//整数
    int decimal=0;//小数
    
    i++;
    if(i>=100)//0.1s,适当要大,因为pwm控速时间越长越稳定
    {
        //数据处理
        left_speed=((float)left_count/1320)*10;//速度=计数值/转一圈的计数值*时间系数,单位:圈/s
        right_speed=((float)right_count/1320)*10;//速度=计数值/转一圈的计数值*时间系数,单位:圈/s
        if(left_count<0)
            left_speed=-left_speed;
        if(right_count<0)
            right_speed=-right_speed;
        
        
        
        //oled显示
        j++;
        if(j>20)//2s刷新一次,因为耗时较大
        {
            if(motion_mode==1)//前进
            {
                integer=left_speed;//整数
                decimal=(left_speed-integer)*100;//小数
                oled_show_string(65,5,"+",12);
            }
            else if(motion_mode==-1)//后退
            {
                integer=right_speed;//整数
                decimal=(right_speed-integer)*100;//小数
                oled_show_string(65,5,"-",12);
            }
            if(left_count!=0 || right_count!=0)//电机转动
            {
                //printf("count=%d\r\n",count);//脉冲个数
                //printf("integer=%d,decimal=%d\r\n",integer,decimal);//速度
                cap_flag=1;//电机转动
                oled_show_num(65,17,integer,2,12);//显示整数
                oled_show_char(81,20,'.',12,1);
                oled_show_num(97,20,decimal,2,12);//显示小数
                oled_show_num(65,33,integer,2,12);//显示整数
                oled_show_char(81,35,'.',12,1);
                oled_show_num(97,33,decimal,2,12);//显示小数
                oled_area_refresh_gram(65,0,108,48);//oled更新
            }
            else//电机未转动
            {
                if(cap_flag==1)//判断是否之前转动过,防止车轮未转而重复刷新
                {
                    oled_area_clear(65,0,108,48);
                    oled_area_refresh_gram(65,0,108,48);//oled更新
                    cap_flag=0;
                }
                
            }
            j=0;
            SysTick_Config(90000);//使用systick延时后必须重新配置,i2c中使用了,所以必须重新配置
        }
        
        //printf("cap_flag=%d,left_count=%d,right_count=%d\r\n",cap_flag,left_count,right_count);
        //printf("left_count=%d,left_speed=%f,compara_add=%d\r\n",left_count,left_speed,left_compara);
        //printf("right_count=%d,right_speed=%f,right_add=%d\r\n",right_count,right_speed,right_compara);
        
        //pid算法实现
        if(open)
        {
            //(位置离散pid)
            left_pwm = PID_realize(&left_pid,left_speed*SPEED_AMPLIF);//逐步调整pwm值到set_point(目标值)(位置离散pid)      速度放大100倍,便于调试
            left_compara+=(int)left_pwm;
            right_pwm = PID_realize(&right_pid,right_speed*SPEED_AMPLIF);//逐步调整pwm值到set_point(目标值)(位置离散pid)      速度放大100倍,便于调试
            right_compara+=(int)right_pwm;
            //(增量式pid)
            //pwm=PID_add_realize(shaft_speed*SPEED_AMPLIF);//逐步调整pwm值到set_point(目标值)(增量式pid)      速度放大100倍,便于调试
            //compara_add=(int)pwm;
            
            left_temp=left_speed*SPEED_AMPLIF;
            right_temp=right_speed*SPEED_AMPLIF;
        }
        set_computer_value(SEND_FACT_CMD, CURVES_CH1, &left_temp, 1);//发送实际值,通道选择ch1以让软件显示图像
        set_computer_value(SEND_FACT_CMD, CURVES_CH2, &right_temp, 1);//发送实际值,通道选择ch1以让软件显示图像

        i=0;
        TIM_SetCounter(TIM3,0);//计数清零
        TIM_SetCounter(TIM4,0);//计数清零
        left_count=0;//计数清零
        right_count=0;//计数清零
    }

set_computer_value函数是通过串口发送数据到野火的调试助手,可以用于调整合适的pid;

野火的调试助手需要移植到自己的项目中才能使用串口进行通信,移植方法如下

1、野火的protocol.h和protocol.c两个文件添加到项目中,这两个文件包含了通信协议,并进行适当修改

2、修改自己的串口1中断函数,并添加一个Usart_SendArray函数

//用于pid调试
void USART1_IRQHandler(void)                	//串口1中断服务程序
{
      uint8_t ucTemp;
    if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE) != RESET)//判断是哪个进入了中断函数
    {
        ucTemp = USART_ReceiveData(DEBUG_USARTx);//有清除RXNE功能,下面不需要写clearitpending
        protocol_data_recv(&ucTemp, 1);
        //Usart_SendByte(DEBUG_USARTx,ucTemp); 
    }
    USART_ClearITPendingBit(DEBUG_USARTx,USART_IT_RXNE);
}

/* 发送8位数据的数组 */
void Usart_SendArray(USART_TypeDef* USARTx, uint8_t *array,uint8_t num)
{
    uint8_t i;
    for( i=0; i<num; i++ )
    {
        Usart_SendByte(USARTx, array[i]);
    }
    while( USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET );
}

3、main函数要protocol_init(); //初始化pid串口调试协议

结果如图:

33fd10cbd6224d3f8d409fdd09a143c7.png

 

四、常用的测速方法:

3d7873e75e08466a8e839eef5dfb411f.png

我使用的是M法

left_speed=((float)left_count/1320)*10;//速度=计数值/转一圈的计数值*时间系数,单位:圈/s
right_speed=((float)right_count/1320)*10;//速度=计数值/转一圈的计数值*时间系数,单位:圈/s

 五、pid原理

1f99936364334c91937347cc35dfd4cc.png

 积分控制法也就是 位置离散pid;本项目使用的就是积分控制法

微分控制法也就是 增量式pid;

d8284e32144346a08b78533e60bc481d.png

 通过调整Kp,Ki,Kd能达到自己预期的效果,我使用的是试凑法,通过野火串口调试助手进行调参

2a2218ebe21b46999be53518c906de20.png

 代码如下

//pid结构体存储数据(位置离散pid)
extern struct _pid{
    float target_val;           //定义设定值
    float actual_val;           //定义实际值
    float err;                  //定义偏差值
    float integral;             //定义积分值
    float err_last;             //定义上一个偏差值
    float Kp,Ki,Kd;             //定义比例、积分、微分系数
}left_pid,right_pid;

//pid结构体存储数据(增量式pid)
extern struct _pid_add{
    float target_val;           //定义设定值
    float actual_val;           //定义实际值
    float err;                  //定义偏差值
    float err_next;             //定义下一个
    float err_last;             //定义最后一个偏差值
    float Kp,Ki,Kd;             //定义比例、积分、微分系数
}pid_add;
//pid初始化(位置离散pid)
void PID_init(void)
{
    left_pid.target_val=0.0;
    left_pid.actual_val=0.0;
    left_pid.err=0.0;
    left_pid.err_last=0.0;
    left_pid.integral=0.0;
    left_pid.Kp=1;//第一个调,曲线快速上升
    left_pid.Ki=0.003;//第三个调,增加波动
    left_pid.Kd=1.2;//第二个调,曲线快速稳定在目标
    
    right_pid.target_val=0.0;
    right_pid.actual_val=0.0;
    right_pid.err=0.0;
    right_pid.err_last=0.0;
    right_pid.integral=0.0;
    right_pid.Kp=1;//第一个调,曲线快速上升
    right_pid.Ki=0.003;//第三个调,增加波动
    right_pid.Kd=1.2;//第二个调,曲线快速稳定在目标
}

//pid初始化(增量式pid)
void PID_add_init(void)
{
    pid_add.target_val=0.0;
    pid_add.actual_val=0.0;
    pid_add.err=0.0;
    pid_add.err_last=0.0;
    pid_add.err_next=0.0;
    pid_add.Kp=1;//第二个调,曲线快速稳定在目标
    pid_add.Ki=0.75;//第一个调,曲线快速上升
    pid_add.Kd=0.5;//第三个调,曲线波动小
}

//设置pid
void set_p_i_d(float P,float I,float D)
{
    left_pid.Kp=P;
    left_pid.Ki=I;
    left_pid.Kd=D;
    right_pid.Kp=P;
    right_pid.Ki=I;
    right_pid.Kd=D;
    pid_add.Kp=P;
    pid_add.Ki=I;
    pid_add.Kd=D;
}

//pid算法(位置离散pid)
float PID_realize(struct _pid *pid,float speed)
{
    pid->err=pid->target_val-speed;//计算目标值与实际值的误差
    pid->integral+=pid->err;//误差累计
    pid->actual_val=pid->Kp*pid->err+pid->Ki*pid->integral+pid->Kd*(pid->err-pid->err_last);//pid算法(位置离散pid)
    //printf("pid.err=%f,pid.integral=%f,pid.err_last=%f,pid.actual_val=%f\r\n",pid->err,pid->integral,pid->err_last,pid->actual_val);
    pid->err_last=pid->err;//误差传递
    return pid->actual_val;//返回实际值
}

//pid算法(增量式pid)
float PID_add_realize(float speed)
{
    pid_add.err=pid_add.target_val-speed;//计算目标值与实际值的误差
    float increment_val=pid_add.Kp*(pid_add.err-pid_add.err_next)+pid_add.Ki*pid_add.err+pid_add.Kd*(pid_add.err-2*pid_add.err_next+pid_add.err_last);
    pid_add.actual_val+=increment_val;//累计
    pid_add.err_last=pid_add.err_next;//误差传递
    pid_add.err_next=pid_add.err;//误差传递
    return pid_add.actual_val;//返回实际值
}

六、oled的实现

我的oled使用的是iic通信协议,这里要注意,我使用了systick来进行延时,所以使用完后要重新设置systick,你也可以自己设置一个基本定时器来进行延时或者代替systick。

SysTick_Config(90000); //1ms,必须放在delay_us和delay_ms后,

oled相关代码:

//OLED的显存, 每个字节表示8个像素, 128,表示有128列, 8表示有64行, 高位表示第行数.
static uint8_t g_oled_gram[128][8];

//写命令
void oled_write_cmd(uint8_t cmd)
{
    i2c_star();
    i2c_write_byte(0x78);//oled地址
    i2c_wait_ack();
    i2c_write_byte(0x00);//写命令命令
    i2c_wait_ack();
    i2c_write_byte(cmd);//写命令
    i2c_wait_ack();
    i2c_stop();
}

//写数据
void oled_write_data(uint8_t dat)
{
    i2c_star();
    i2c_write_byte(0x78);//oled地址
    i2c_wait_ack();
    i2c_write_byte(0x40);//写数据命令
    i2c_wait_ack();
    i2c_write_byte(dat);//写数据
    i2c_wait_ack();
    i2c_stop();
}

//全屏更新
void oled_all_refresh_gram(void)
{
    uint8_t i,j;
    for(i=0;i<8;i++)//设置为8页
    {
        oled_write_cmd(0xb0 |i);//设置页
        oled_write_cmd(0x00);//设置列(低4bit)因为128列,至少要7bit
        oled_write_cmd(0x10);//设置列(高4bit)
        for(j=0;j<128;j++)
        {
            oled_write_data(g_oled_gram[j][i]);//写入数据
        }
    }
}

//区域更新
void oled_area_refresh_gram(uint8_t x1,uint8_t y1,uint8_t x2,uint8_t y2)
{
    uint8_t pos1,pos2,i,j;
    if (x1 > 127 || y1 > 63) return;  /* 超出范围了. */
    if (x2 > 127 || y2 > 63) return;  /* 超出范围了. */
    pos1=y1/8;
    pos2=y2/8;
    for(i=pos1;i<=pos2;i++)//设置为8页
    {
        oled_write_cmd(0xb0 |i);//设置页
        for(j=x1;j<=x2;j++)
        {
            oled_write_cmd(j&0xf);//设置列(低4bit)因为128列,至少要7bit
            oled_write_cmd((j>>4)|0x10);//设置列(高4bit)
            oled_write_data(g_oled_gram[j][i]);//写入数据
        }
    }
}

//全屏清屏
void oled_all_clear(void)
{
    uint8_t i, j;

    for (i = 0; i < 8; i++)
        for (j = 0; j < 128; j++)
            g_oled_gram[j][i] = 0X00;

    oled_all_refresh_gram();    /* 更新显示 */
}

//区域清屏
void oled_area_clear(uint8_t x1,uint8_t y1,uint8_t x2,uint8_t y2)
{
    uint8_t pos1,pos2,i,j;
    if (x1 > 127 || y1 > 63) return;  /* 超出范围了. */
    if (x2 > 127 || y2 > 63) return;  /* 超出范围了. */
    pos1=y1/8;
    pos2=y2/8;
    //printf("x1=%d,y1=%d,x2=%d,y2=%d,pos1=%d,pos2=%d\r\n",x1,y1,x2,y2,pos1,pos2);
    for(i=pos1;i<=pos2;i++)//设置为8页
        for(j=x1;j<=x2;j++)
        {
            //printf("i=%d,j=%d\r\n",i,j);
            g_oled_gram[j][i] = 0X00;
        }
}


//全屏点亮
void oled_fill(void)
{
    uint8_t i, j;

    for (i = 0; i < 8; i++)
        for (j = 0; j < 128; j++)
            g_oled_gram[j][i] = 0XFF;

    oled_all_refresh_gram();    /* 更新显示 */
}

//打开
void oled_display_on(void)
{
    oled_write_cmd(0X8D);   /* SET DCDC命令 */
    oled_write_cmd(0X14);   /* DCDC ON */
    oled_write_cmd(0XAF);   /* DISPLAY ON */
}

//关闭
void oled_display_off(void)
{
    oled_write_cmd(0X8D);   /* SET DCDC命令 */
    oled_write_cmd(0X10);   /* DCDC OFF */
    oled_write_cmd(0XAE);   /* DISPLAY OFF */
}

//oled初始化
void oled_init(void)
{
    i2c_init();//i2c初始化
    delay_ms(100);//延时,重要
    
    oled_write_cmd(0xAE);   /* 关闭显示 */
    oled_write_cmd(0xD5);   /* 设置时钟分频因子,震荡频率 */
    oled_write_cmd(0xf0);     /* [3:0],分频因子;[7:4],震荡频率 */
    oled_write_cmd(0xA8);   /* 设置驱动路数 */
    oled_write_cmd(0X3F);   /* 默认0X3F(1/64) */
    oled_write_cmd(0xD3);   /* 设置显示偏移 */
    oled_write_cmd(0X00);   /* 设置列(低4bit)*/
    oled_write_cmd(0x10);//设置列(高4bit)

    oled_write_cmd(0x40);   /* 设置显示开始行 [5:0],行数. */

    oled_write_cmd(0x8D);   /* 电荷泵设置 */
    oled_write_cmd(0x14);   /* bit2,开启/关闭 */
    oled_write_cmd(0x20);   /* 设置内存地址模式 */
    oled_write_cmd(0x02);   /* [1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10; */
    oled_write_cmd(0xb0);   //开启地址0-7
    oled_write_cmd(0xA1);   /* 段重定义设置,bit0:0,0->0;1,0->127; */
    oled_write_cmd(0xC8);   /* 设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数 */
    oled_write_cmd(0xDA);   /* 设置COM硬件引脚配置 */
    oled_write_cmd(0x12);   /* 128*64 */

    oled_write_cmd(0x81);   /* 对比度设置 */
    oled_write_cmd(0xEF);   /* 1~255;默认0X7F (亮度设置,越大越亮) */
    oled_write_cmd(0xD9);   /* 设置预充电周期 */
    oled_write_cmd(0x22);   //充电时间
    oled_write_cmd(0xf1);   /* [3:0],PHASE 1;[7:4],PHASE 2; */
    oled_write_cmd(0xDB);   /* 设置VCOMH 电压倍率 */
    oled_write_cmd(0x20);   //0x20,0.77xVcc

    oled_write_cmd(0xA4);   /* 全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) */
    oled_write_cmd(0xA6);   /* 设置显示方式;bit0:1,反相显示;0,正常显示 */
    oled_write_cmd(0xAF);   /* 开启显示 */
    
    oled_all_clear();
}

//画点
void oled_draw_point(uint8_t x, uint8_t y, uint8_t dot)
{
    uint8_t pos, bx, temp = 0;

    if (x > 127 || y > 63) return;  /* 超出范围了. */

    pos = y / 8;        /* 计算GRAM里面的y坐标所在的字节, 每个字节可以存储8个行坐标 */

    bx = y % 8;         /* 取余数,方便计算y在对应字节里面的位置,及行(y)位置 */
    temp = 1 << bx;     /* 高位表示低行号, 得到y对应的bit位置,将该bit先置1 */

    if (dot)    /* 画实心点 */
    {
        g_oled_gram[x][pos] |= temp;
    }
    else        /* 画空点,即不显示 */
    {
        g_oled_gram[x][pos] &= ~temp;
    }
}

// 显示字符
void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode)
{
    uint8_t temp, t, t1;
    uint8_t y0 = y;
    uint8_t *pfont = 0;
    uint8_t csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 得到字体一个字符对应点阵集所占的字节数 */
    chr = chr - ' ';         /* 得到偏移后的值,因为字库是从空格开始存储的,第一个字符是空格 */

    if (size == 12)          /* 调用1206字体 */
    {
        pfont = (uint8_t *)oled_asc2_1206[chr];
    }
    else if (size == 16)     /* 调用1608字体 */
    {
        pfont = (uint8_t *)oled_asc2_1608[chr];
    }
    else if (size == 24)     /* 调用2412字体 */
    {
        pfont = (uint8_t *)oled_asc2_2412[chr];
    }
    else                    /* 没有的字库 */
    {
        return;
    }

    for (t = 0; t < csize; t++)
    {
        temp = pfont[t];

        for (t1 = 0; t1 < 8; t1++)
        {
            if (temp & 0x80)oled_draw_point(x, y, mode);
            else oled_draw_point(x, y, !mode);

            temp <<= 1;
            y++;

            if ((y - y0) == size)
            {
                y = y0;
                x++;
                break;
            }
        }
    }
}


//平方函数, m^n

static uint32_t oled_pow(uint8_t m, uint8_t n)
{
    uint32_t result = 1;

    while (n--)
    {
        result *= m;
    }

    return result;
}

//显示len个数字
void oled_show_num(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
{
    uint8_t t, temp;
    uint8_t enshow = 0;

    for (t = 0; t < len; t++)   /* 按总显示位数循环 */
    {
        temp = (num / oled_pow(10, len - t - 1)) % 10;  /* 获取对应位的数字 */

        if (enshow == 0 && t < (len - 1))   /* 没有使能显示,且还有位要显示 */
        {
            if (temp == 0)
            {
                oled_show_char(x + (size / 2)*t, y, ' ', size, 1); /* 显示空格,站位 */
                continue;       /* 继续下个一位 */
            }
            else
            {
                enshow = 1;     /* 使能显示 */
            }
        }

        oled_show_char(x + (size / 2)*t, y, temp + '0', size, 1);    /* 显示字符 ,因为是通过两页写的所以size除2*/
    }
}

//显示字符串
void oled_show_string(uint8_t x, uint8_t y, const char *p, uint8_t size)
{
    while ((*p <= '~') && (*p >= ' '))  /* 判断是不是非法字符! */
    {
        if (x > (128 - (size / 2)))     /* 宽度越界 */
        {
            x = 0;
            y += size;                  /* 换行 */
        }

        if (y > (64 - size))            /* 高度越界 */
        {
            y = x = 0;
            oled_all_clear();
        }

        oled_show_char(x, y, *p, size, 1);   /* 显示一个字符 */
        x += size / 2;      /* ASCII字符宽度为汉字宽度的一半 因为是通过两页写的所以size除2*/
        p++;
    }
}

七、蓝牙通信

使用串口2

首先对串口2进行初始化

再编写中断函数,通过中断函数对手机发来的数据进行处理,代码如下

void USART2_IRQHandler(void)                	//串口x中断服务程序
{
    float val=0;
    uint8_t i=0;
    uint8_t temp2;
    float decimal=0;
    if(USART_GetITStatus(USART2,USART_IT_RXNE) !=  RESET)//判断中断位
    {
        USART_ClearITPendingBit(USART2, USART_IT_RXNE);
        temp2 = USART_ReceiveData(USART2); //接收数据
        if(temp2!='\n')
        {
            //printf("temp2=%c\r\n",temp2);
            if(temp2=='.')
            {
                integer_flag=2;
                return;
            }
            if(integer_flag==0)//整数部分
            {
                    integer_val=temp2-48;
                    integer_flag=1;
            }
            else if(integer_flag==1)
                integer_val=integer_val*10+temp2-48;
            if(integer_flag==2)//小数部分
            {
                decimal=(temp2-48);
                for(i=0;i<decimal_bit;i++)
                    decimal/=10;
                decimal_val+=decimal;
                decimal_bit++;//小数位数
            }
        }
        else
        {
            val=integer_val+decimal_val;
            printf("data=%.2f\r\n",val);
            integer_flag=0;
            decimal_bit=1;
            integer_val=0;
            decimal_val=0;
            switch((int)val)
            {
                case 490://停止
                    left_compara=0;
                    right_compara=0;
                    motion_mode=0;
                    open=0;
                    break;
                case 491://前进
                    left_compara=0;
                    right_compara=0;
                    motion_mode=1;
                    open=1;
                    break;
                case 492://后退
                    left_compara=0;
                    right_compara=0;
                    motion_mode=-1;
                    open=1;
                    break;
                case 493://左转
                    left_compara=0;
                    right_compara=0;
                    motion_mode=2;
                    open=1;
                    break;
                case 494://右转
                    left_compara=0;
                    right_compara=0;
                    motion_mode=3;
                    open=1;
                    break;
                case 501://蓝牙按键模式
                    tracking_mod=0;
                    break;
                case 502://循迹模式
                    tracking_mod=1;
                    break;
            }
            if((int)val<10)
            {
                if(val>3.5)//最大速度
                    val=3.5;
                left_targetspeed=val;//速度放大100倍,便于调试,
                right_targetspeed=val;//速度放大100倍,便于调试
            }
        }
    }
} 

本人使用的是qt自己开发的一个小app,效果如下

aca74f6b12524df19836e8d195834f9e.jpeg

 

八、3路循迹模块

我使用的是中间3路,当没有遇到黑线是对应引脚输出高电平,灯亮;遇到黑线对应引脚时输出低电平,灯灭。

1、对使用到的gpio初始化,设置为输入模式

2、通过对应引脚的电平循迹

代码如下

if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14)==0)//中
            {
                left_set_point=left_targetspeed*SPEED_AMPLIF;//速度放大100倍,便于调试,
                right_set_point=right_targetspeed*SPEED_AMPLIF;//速度放大100倍,便于调试
                TIM_SetCompare1(TIM2,0);
                TIM_SetCompare2(TIM2,left_compara);
                TIM_SetCompare3(TIM2,0);
                TIM_SetCompare4(TIM2,right_compara);
            }
            else if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==0)//左
            {
                left_set_point=left_targetspeed/2*SPEED_AMPLIF;//0.5m/s.速度放大100倍,便于调试,
                right_set_point=right_targetspeed*SPEED_AMPLIF;//1m/s.速度放大100倍,便于调试
                left_compara=0;
                TIM_SetCompare1(TIM2,0);
                TIM_SetCompare2(TIM2,0);
                TIM_SetCompare3(TIM2,0);
                TIM_SetCompare4(TIM2,right_compara);
            }
            else if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_15)==0)//右
            {
                right_set_point=right_targetspeed/2*SPEED_AMPLIF;//0.5m/s.速度放大100倍,便于调试
                left_set_point=left_targetspeed*SPEED_AMPLIF;//1m/s.速度放大100倍,便于调试,
                right_compara=0;
                TIM_SetCompare1(TIM2,0);
                TIM_SetCompare2(TIM2,left_compara);
                TIM_SetCompare3(TIM2,0);
                TIM_SetCompare4(TIM2,0);
            }

就此,整个流程结束。


总结

经过这个小项目我获得了很多收获,虽然工程中遇到很多问题,有的问题会困扰我一天,但是做出来后,我觉得一切的努力都是值得的,可能这个小车还有可以改进的地方,欢迎建议。 

 

  • 14
    点赞
  • 143
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
1.题目背景及意义 1.1题目研究背景、目的及意义 近年来,智能小车作为现代的新发明,是世界车辆工程领域研究的热点和汽车工业增长的新动力。它可以按照预先设定的模式在一个环境里自动的运作,不需要人为的管理,可以应用在科学勘探、无人驾驶机动车、无人工厂、仓库、服务机器人等等。智能小车能够实时显示时间、速度、里程,具有自动寻迹、寻光、避障功能,可远程控制行驶速度、准确定位停车,远程传输图像等功能。在本次自动寻迹小车测控系统的设计中,基于单片机控制技术,通过传感器给出信号驱动两个直流电机正反运动,以实现小车在白色地面上寻着黑色线路正确行使。小车的寻迹和避障功能在生产生活中都有着广泛的用途。例如:可以用在大的生产车间的物流系统中,按照预先设定的路线来传输货物自动躲避障碍从而使工作更加安全和效率更高。 1.2 题目国内外研究现状及趋势 目前对智能车辆的研究主要致力于提高汽车的安全性、舒适性,以及提供优良的人车交互界面。智能化、IT化和新能源是未来智能汽车发展的趋势。 2017年12月2日,深圳的无人驾驶公交车正式上路,从深圳福田穿梭驶出。支撑这次无人驾驶的“阿尔法巴-智能驾驶公交系统”,是由中国企业自主研发的无人驾驶系统,目前,已实现自动驾驶下的行人、车辆检测、减速避让、紧急停车、障碍物绕行、变道、自动按站停靠等功能。 本次自动寻迹电动小车系统设计,是智能寻迹小车中最普通常见的功能。在全国乃至国际大学生智能小车比赛中,往往增加了设计难度。如不通过光电对管,红外线等视觉传感器或激光扫描检测线路,而是通过电磁模块检测中间黑线下埋设的漆包线以供赛车检测赛道;对现场光线的正确探测以达到黑夜行驶;非匀速行驶记忆算法的创新;图像采集和处理的重要性等。我们可以使它实现WIFI控制,蓝牙传输,自动报警,红外遥控等多种功能,实现了更加智能的电动小车设计。功能的逐渐强大,更是为了能应用于快速发展的智能汽车行业。如今的汽车行业在人工智能领域的发展可谓势如破竹,智能汽车遍地开花。 1,3 设计思想及技术路线 通过红外线对黑色路线进行寻迹,将收到的信号传送给单片机,使其控制小车无偏差行驶。当小车沿着路面的黑色轨道行驶遇到障碍物时,传感器检测到信号就可确认前方有障碍物,并将信号传送给单片机单片机进行一系列分析后由内部程序控制小车后退、转向,从而实现避障功能。为实现此功能,需要设置寻迹模块和避障模块发送信号给单片机STC89C52以此驱动电机进行准确的行驶。技术路线如图1.3所示: 检测信号 单片机 驱动 电机 图1.3 技术路线 2.主要设计内容 2.1 主要设计内容 该小车有五大组成部分:避障模块,寻迹模快,驱动模块,单片机控制模块,电源模块。 避障模块:采用超声波控制,能准确探测周围障碍物。寻迹模快:采用红外线精确探测,减小路线误差,以实现匀速稳定运行。单片机:对其进行编程控制电机相应运动。电源模块:使用5节1.5V干电池实现对单片机、驱动和电机供电。电机驱动模块:使用直流电机即可,一个驱动板能同时驱动2个直流电机。通过设计电路图,硬件连接,软件编程和最终调试,完成此次设计。 2.2 总体设计方案 图2.2 单片机 电机驱动 避障模块 寻迹模块 电源模块 总体设计方案 该系统采用模块化控制方案,本课题主要开发一个能自动循迹,自动避障的智能小车控制系统。本设计以两个直流电动机为主要驱动,通过寻迹模块和避障模块采集周围信息,送入主控单元STC89C52单片机,通过编程有序合理的将各模块信号整合在一起后控制电机完成相应动作,实现了智能控制。 2.3 设计的预期目标 1.按下启动键,小车能自动按照白色地面的黑色线路匀速行驶,完成一圈的寻迹,其中包括前进,左转,右转,刹车停止,且不出现路线偏移。当遇到障碍物时,小车立即后退并通过转向躲避障碍物。 2.行走路线中心点始终与黑色线路的中心位置重合。 3.超声波避障距离小于0.5m. 3.工作计划及进度安排 第1周 收集毕业设计相关资料,准备毕业翻译和开题报告 第2周 确定毕业设计总体方案,确保合理性 第3周 撰写开题报告,确认后提交 第4周 学习和掌握电动小车的结构和工作原理 第5周 根据控制要求初步确定所使用的元器件 第6周 复习单片机的相关知识,完成所需硬件相关的电路设计 第7周 确定电路原理图并仿真 第8周 硬件组装 第9周 编写程序 第10周 运用电脑软件初步对程序进行调试 第11周 配合智能小车硬件部分,并完善功能,达到设计要求 第12周 对智能小车性能进行测试并记录 第13周 撰写毕业设计论文 第14周 经指导老师审核确认后,完成毕业论文 第15周 提前准备毕业设计答辩 第16周 完成毕业设计答辩 4.可行性分析 4.1 技术可行性 单片机:使用简单,软件编程灵活,成本较低。对于本系统的设计要求,使用STC89C51单片机足以满足它的算法和控制功能。 寻迹模块:用红外发射管和接收管检测和传递信号。 红外发射管发出红外线,当发出的红外线照射到白色的平面后反射,若红外接收管能接收到反射回的光线则检测出白线继而输出低电平,若接收不到发射管发出的光线则检测出黑线继而输出高电平。这样的传感器能够满足要求。 驱动模块: L298N是一个具有高电压大电流的H桥驱动,相应频率高,一片L298N可以控制两个直流电机,而且还带有控制使能端。用该模块作为电机驱动,操作方便,稳定性好,性能优良。 电机:采用直流减速电机。直流减速电机转动力矩大,体积小,重量轻,装配简单,使用方便。由于其内部由高速电动机提供原始动力,带动变速(减速)齿轮组,可以产生较大扭力。采用PWM调速法进行调速,利用单片机输出端输出高电平的脉宽及其占空比的大小来控制电机的转速,从而控制小车的速度。 自动寻迹电动小车测控系统已经发展的相对成熟,有很多成熟的方案可以借鉴参考,方案的可行性和成功性很大。 4.2 经济可行性 小车底板用AS- 2WD铝合金移动平台车盘,采用STC89C52单片机作为主控制器,用超声波进行避障和红外线进行寻迹,65CS舵机进行驱动,L298N作为直流电机的驱动芯片,整个系统价格合理,经济适用,有一定应用价值。 4.3 现有工作条件可行性 学校实验室有相应的工具和测试仪器,可方便我们进行硬件组装和软件编程,学生具有相应知识基础,老师具有工作经验和技术指导能力来指导我们完成这个课题。 参考文献 [1] 曹建平,雷丹,郭磊.基于LDC1000电感数字传感器的自动循迹智能小车控制系统设计[J].自动化技术与应用,2017(12). [2] 刘晓萌.基于摄像头的智能循迹小车控制算法设计[J].科技创新与应用,2017(27). [3] 王瑞琦.基于STC89C51单片机的多功能智能小车设计[J]. 国外电子测量技术,2017(07). [4] 黄健,董三锋,王利平.基于LDC1000自动循迹智能小车设计[J]. 微特电机,2017(06). [5] 王慧,华成.一种比例调节转速差的Arduino小车设计[J]. 数字技术与应用,2017(05). [6] 刘环,贾鹤鸣,朱传旭,杨泽文,莫冲.智能循迹小车创新实训系统设计[J].科教文汇(上旬刊), 2017(05). [7] 周淑娟.基于单片机智能寻迹小车的设计方案[J].工业技术与职业教育2011,第9卷第2期. [8] 韩毅,杨天.基于HCS12单片机的智能寻迹模型车的设计与实现[J].学术期刊,2008,29(18). [9] 于连国,李伟,王妍玮.基于单片机的智能小车设计[J].林业机械与木工设备,2011,4期. [10] 赵振德.多功能遥控智能小车的制作[J].电子制作,2011, 4期. [11] 黎宇科,刘宇.国内智能网联汽车发展现状及建议[J]. 汽车与配件, 2016(41). [12] 郭旭.人工智能视角下的无人驾驶技术分析与展望[J].电子世界,2017(20). [13] Li Xin,Xiang Qiang.Design of Intelligent CarBased on Single Chip Processor STC89C52[A]. Proceedings of 2015 InternationalPower,Electronics and Materials Engineering Conference(IPEMEC 2015)[C]. 2015 [14] YE Xiao-dong.The Design and Implementation ofIntelligent Car Based on Scm[A].2011 International Conference on Computers,Communications, Control and Automation Proceedings(CCCA 2011 V2)[C].2011 [15] ShengMin Cui, Chao Zhang, Jian Feng Wang, Kun Zhang. Research on Path Tracking Control for Vision BasedIntelligent Vehicle[J]. Applied Mechanics and Materials, 2011(63)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值