stm32f103基于pid的蓝牙循迹小车

这篇具有很好参考价值的文章主要介绍了stm32f103基于pid的蓝牙循迹小车。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前言

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

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

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

四、常用的测速方法:

 五、pid原理

六、oled的实现

七、蓝牙通信

八、3路循迹模块

总结


 文章来源地址https://www.toymoban.com/news/detail-573242.html

前言

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


 

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

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

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

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

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

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

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

 stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

 我使用的是定时器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和编码器供电两极连接到对应引脚就能计数了,可以手动转动轮子,使用串口对计数进行打印,能观察到计数值。

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

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

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

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

 我这里使用定时器2的完全重定向到PB10,PB11,PB3,PA15stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

 重定向方法:

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

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串口调试协议

结果如图:

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

 

四、常用的测速方法:

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

我使用的是M法

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

 五、pid原理

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

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

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

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

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

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

 代码如下

//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,效果如下

stm32f103霍尔编码器,stm32,stm32,嵌入式硬件,单片机

 

八、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);
            }

就此,整个流程结束。


总结

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

 

到了这里,关于stm32f103基于pid的蓝牙循迹小车的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 基于STM32F103C8T6的HC-06蓝牙通信

    基于STM32F103C8T6的HC-06蓝牙通信

    如果朋友们 遇到了如下问题 ,可以仔细借鉴本文章和另一篇专门讲解 蓝牙通信问题 的文章,一定能够解决你在蓝牙通信时遇到的诸多困难 1.在调试蓝牙模块AT指令时无返回值 2.身边 无USB转TTL模块 可以直接调试蓝牙模块(本人就是由于无模块花了了整整一天才调试成功)

    2024年02月03日
    浏览(23)
  • 基于STM32f103c8t6的简单红外巡迹避障小车制作

    基于STM32f103c8t6的简单红外巡迹避障小车制作

    (1)电源 电源模块选用的是18650锂电池(充电器及电池底座)、3.3v稳压模块。 (2)车模 淘宝最常见的智能车底。 (3)电机 买的智能车带有四个电机,选用L298N电机驱动板对电机进行驱动。 (4)巡迹及避障 巡迹选用四路红外模块实现,避障选用超声波模块HC-SR04实现。 (

    2023年04月15日
    浏览(14)
  • 单相逆变电路实战!(基于STM32F103C8T6的单相逆变电路,PID控制输出额定电压)

    单相逆变电路实战!(基于STM32F103C8T6的单相逆变电路,PID控制输出额定电压)

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 我建了一个群,分享我个人做项目的经历和资源,纯个人爱好,一切免费,看自己空闲时间答疑,有想法的可以加QQ群280730348 本篇文章是对单相逆变电路进行实战演练的一次项目,主要实现功能为: 直

    2023年04月08日
    浏览(24)
  • 从零开始制作STM32F103RCT6小车(一)

    从零开始制作STM32F103RCT6小车(一)

            仅以此系列给实验室的学弟学妹作为小车制作教程来使用,后续的内容我会在这个暑假陆续更新出来,本篇的内容是新建一个适用于STM32F103RCT6的工程         接下来的操作几乎是基于STM32F1xx系列的固件库,这里我给大家列出链接 STM32F1xx系列固件库               

    2023年04月08日
    浏览(48)
  • 移植FreeRTOS的STM32F103双轮平衡小车(开源,代码文末)

    移植FreeRTOS的STM32F103双轮平衡小车(开源,代码文末)

    耗时大概三四天吧,主要时间还是花在硬件方面上, 目录 引言 1、系统概述 1.1、设计任务 1.2、设计要求 1.3、硬件清单 2、方案设计与论证 2.1、芯片选择方案芯片 2.2 、系统概述 2.3、设计要求 2.4、系统总体设计 2.5、重要功能模块程序实现原理分析 2.5.1、MPU6050模块的介绍 小

    2024年01月20日
    浏览(10)
  • 关于两个STM32F103系列单片机的蓝牙通信

    关于两个STM32F103系列单片机的蓝牙通信

       毕设做的是掌控小车,因此采用蓝牙通信作为小车和手部通信,前段时间做出实物,对其遇到的问题以及解决的方法做一些总结。一个主控芯片采用STM32F103ZET6,另一个主控芯片采用STM32F103C8T6,原因是本来准备了两个主控C8T6,不小心烧了一个。 1.两个蓝牙的配对  需要准

    2024年02月13日
    浏览(47)
  • JDY-31蓝牙模块远程控制STM32F103单片机

    JDY-31蓝牙模块远程控制STM32F103单片机

       手机app通过蓝牙模块发送指令实时控制单片机的外设功能,比如发送衣柜开关门指令(舵机旋转),衣架上升降落(步进电机正转反转),远程开启去污除湿功能(继电器控制打开关闭小风扇+加热片)。 本次例子:手机APP连接蓝牙模块远程控制SG90舵机正转(0-180°)和反

    2024年02月01日
    浏览(19)
  • STM32F103C8T6智能小车舵机超声波避障

    目录 一、定时器 计数和定时器中断  输出比较(PWM) 二、 舵机 三、超声波测距 四、主函数 总结 推荐的STM32学习链接:  [6-1] TIM定时中断_哔哩哔哩_bilibili [6-1] TIM定时中断是STM32入门教程-2022持续更新中的第13集视频,该合集共计29集,视频收藏或关注UP主,及时了解更多相关视

    2024年02月15日
    浏览(15)
  • 基于STM32F103C8T6与ESP8266的物联网智能温度采集与蓝牙OLED数字钟的设计与实现

    作者: 颜孙炜 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wC12xZrc-1673843611066)(C:UsersadminAppDataRoamingTyporatypora-user-imagesimage-20230110223524043.png)] 用STM32F103C8T6自有的RTC功能实现一款数字钟的设计,包括温度输入检测和显示模块、数字钟显示模块

    2024年02月02日
    浏览(13)
  • [proteus]STM32F103单片机直流电机PID算法PWM波电机调速正反转Proteus仿真

    1、实现功能: (1)、基于STM32F103单片机PID算法PWM控制直流电机正反转调速,LCD1602显示转速等。可通过“加速”、“减速”按键修改“目标转速”并实时测量“实际转速”送到LCD1602上显示。 (2)、“启动”按键控制电机启动,默认启动电机是正转(示波器上的黄色PWM波)。 (3)、“

    2024年02月06日
    浏览(10)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包