当前位置:   article > 正文

stm32f103基于pid的蓝牙循迹小车_stm32f103蓝牙控制电机

stm32f103蓝牙控制电机

前言

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


 

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

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

a451f97b477246d19984cbffe0945f6d.png

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

5be242da8a0649f0a3b2595b64c3fa36.png

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

8dc9ffc7f4ab4c5aabb7de417ad80aa8.png

 8bfebf7d305647fa88b231da999ac806.png

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

  1. void gpio_clk_init(void)
  2. {
  3. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//定时器3使能
  4. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);//定时器4使能
  5. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//gpioA使能
  6. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//gpioB使能
  7. //定时器3gpio初始化
  8. GPIO_InitTypeDef gpioa_init;
  9. gpioa_init.GPIO_Pin=GPIO_Pin_6 |GPIO_Pin_7;
  10. gpioa_init.GPIO_Pin=GPIO_Mode_IN_FLOATING;//必须配置成浮空输入
  11. gpioa_init.GPIO_Speed=GPIO_Speed_50MHz;
  12. GPIO_Init(GPIOA,&gpioa_init);
  13. //定时器4gpio初始化
  14. GPIO_InitTypeDef gpiob_init;
  15. gpiob_init.GPIO_Pin=GPIO_Pin_6 |GPIO_Pin_7;
  16. gpiob_init.GPIO_Pin=GPIO_Mode_IN_FLOATING;//必须配置成浮空输入
  17. gpiob_init.GPIO_Speed=GPIO_Speed_50MHz;
  18. GPIO_Init(GPIOB,&gpiob_init);
  19. }
  20. void time3_init(void)
  21. {
  22. NVIC_InitTypeDef nvic_init={0};
  23. gpio_clk_init();//初始化引脚
  24. //定时器初始化
  25. tim3_timbase.TIM_Period=65535;//重装载值
  26. tim3_timbase.TIM_Prescaler=0;//分频值
  27. tim3_timbase.TIM_CounterMode=TIM_CounterMode_Up;//递增计数
  28. tim3_timbase.TIM_ClockDivision=TIM_CKD_DIV1;//不滤波
  29. tim3_timbase.TIM_RepetitionCounter=DISABLE;//失能缓冲区
  30. TIM_TimeBaseInit(TIM3,&tim3_timbase);
  31. //配置编码器捕获
  32. tim3_icinit.TIM_Channel=TIM_Channel_1;//通道1
  33. tim3_icinit.TIM_ICFilter=0;//滤波
  34. tim3_icinit.TIM_ICPolarity=TIM_ICPolarity_Rising;//上升沿捕获
  35. tim3_icinit.TIM_ICPrescaler=TIM_ICPSC_DIV1;//不分频
  36. tim3_icinit.TIM_ICSelection=TIM_ICSelection_DirectTI;//通道选择,TIM输入1、2、3或4被选择为分别连接到IC1、IC2、IC3或IC4
  37. TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//配置编码器计数。BothEdge(底部边缘)
  38. TIM_ICInit(TIM3,&tim3_icinit);
  39. tim3_icinit.TIM_Channel=TIM_Channel_2;//通道2
  40. TIM_ICInit(TIM3,&tim3_icinit);
  41. //初始化标志位,计数器
  42. TIM_ClearFlag(TIM3,TIM_FLAG_Update);//清除标志位
  43. TIM_SetCounter(TIM3,0);//TIM3->CNT=0;
  44. //配置中断
  45. nvic_init.NVIC_IRQChannel=TIM3_IRQn;//中断通道
  46. nvic_init.NVIC_IRQChannelCmd=ENABLE;//中断使能
  47. nvic_init.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级;
  48. nvic_init.NVIC_IRQChannelSubPriority=1;//响应优先级
  49. NVIC_Init(&nvic_init);
  50. TIM_ITConfig(TIM3,TIM_IT_Update | TIM_IT_CC1 |TIM_IT_CC2,ENABLE);//配置定时器,允许更新中断,CC1,CC2捕获中断
  51. TIM_Cmd(TIM3,ENABLE);//开启定时器
  52. }
  53. void time4_init(void)
  54. {
  55. NVIC_InitTypeDef nvic_init={0};
  56. gpio_clk_init();//初始化引脚
  57. //定时器初始化
  58. tim4_timbase.TIM_Period=65535;//重装载值
  59. tim4_timbase.TIM_Prescaler=0;//分频值
  60. tim4_timbase.TIM_CounterMode=TIM_CounterMode_Up;//递增计数
  61. tim4_timbase.TIM_ClockDivision=TIM_CKD_DIV1;//不滤波
  62. tim4_timbase.TIM_RepetitionCounter=DISABLE;//失能缓冲区
  63. TIM_TimeBaseInit(TIM4,&tim4_timbase);
  64. //配置编码器捕获
  65. tim4_icinit.TIM_Channel=TIM_Channel_1;//通道1
  66. tim4_icinit.TIM_ICFilter=0;//滤波
  67. tim4_icinit.TIM_ICPolarity=TIM_ICPolarity_Rising;//上升沿捕获
  68. tim4_icinit.TIM_ICPrescaler=TIM_ICPSC_DIV1;//不分频
  69. tim4_icinit.TIM_ICSelection=TIM_ICSelection_DirectTI;//通道选择,TIM输入1、2、3或4被选择为分别连接到IC1、IC2、IC3或IC4
  70. TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//配置编码器计数。BothEdge(底部边缘)
  71. TIM_ICInit(TIM4,&tim4_icinit);
  72. tim4_icinit.TIM_Channel=TIM_Channel_2;//通道2
  73. TIM_ICInit(TIM4,&tim4_icinit);
  74. //初始化标志位,计数器
  75. TIM_ClearFlag(TIM4,TIM_FLAG_Update);//清除标志位
  76. TIM_SetCounter(TIM4,0);//TIM4->CNT=0;
  77. //配置中断
  78. nvic_init.NVIC_IRQChannel=TIM4_IRQn;//中断通道
  79. nvic_init.NVIC_IRQChannelCmd=ENABLE;//中断使能
  80. nvic_init.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级;
  81. nvic_init.NVIC_IRQChannelSubPriority=1;//响应优先级
  82. NVIC_Init(&nvic_init);
  83. TIM_ITConfig(TIM4,TIM_IT_Update | TIM_IT_CC1 |TIM_IT_CC2,ENABLE);//配置定时器,允许更新中断,CC1,CC2捕获中断
  84. TIM_Cmd(TIM4,ENABLE);//开启定时器
  85. }

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

  1. void TIM3_IRQHandler(void)
  2. {
  3. //uint8_t i;
  4. if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)//判断是否为更新中断
  5. {
  6. TIM_ClearFlag(TIM3,TIM_IT_Update);
  7. }
  8. if((TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)||(TIM_GetITStatus(TIM3, TIM_IT_CC2) != RESET))//判断是否为捕获中断
  9. {
  10. left_count=(short)TIM3->CNT;
  11. TIM_ClearFlag(TIM3, TIM_IT_CC1|TIM_IT_CC2);//清除标志位
  12. }
  13. }
  14. void TIM4_IRQHandler(void)
  15. {
  16. if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)//判断是否为更新中断
  17. {
  18. TIM_ClearFlag(TIM4,TIM_IT_Update);
  19. }
  20. if((TIM_GetITStatus(TIM4, TIM_IT_CC1) != RESET)||(TIM_GetITStatus(TIM4, TIM_IT_CC2) != RESET))//判断是否为捕获中断
  21. {
  22. right_count=(short)TIM4->CNT;
  23. TIM_ClearFlag(TIM4, TIM_IT_CC1|TIM_IT_CC2);//清除标志位
  24. }
  25. }

在将电机的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

 代码如下:

  1. void time2_init(void)
  2. {
  3. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//定时器2使能
  4. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//gpioA使能
  5. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//gpioB使能
  6. RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//AFIO时钟使能
  7. GPIO_PinRemapConfig(GPIO_FullRemap_TIM2,ENABLE);//定时器2重映射,PA15,PB3,PB10,PB11
  8. GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE);//必须禁用swj
  9. //定时器2gpio初始化
  10. gpioc_init.GPIO_Pin=GPIO_Pin_3 |GPIO_Pin_10 | GPIO_Pin_11;
  11. gpioc_init.GPIO_Mode=GPIO_Mode_AF_PP;
  12. gpioc_init.GPIO_Speed=GPIO_Speed_50MHz;
  13. GPIO_Init(GPIOB,&gpioc_init);
  14. gpioc_init.GPIO_Pin=GPIO_Pin_15 ;
  15. GPIO_Init(GPIOA,&gpioc_init);
  16. //初始化定时器1
  17. tim2_timbase.TIM_Period=999;
  18. tim2_timbase.TIM_Prescaler=359;
  19. tim2_timbase.TIM_ClockDivision=TIM_CKD_DIV1;
  20. tim2_timbase.TIM_CounterMode=TIM_CounterMode_Up;
  21. tim2_timbase.TIM_RepetitionCounter=DISABLE;
  22. TIM_TimeBaseInit(TIM2,&tim2_timbase);
  23. //初始化PWM
  24. tim2_oc.TIM_OCMode=TIM_OCMode_PWM1;//模式PWM
  25. tim2_oc.TIM_Pulse=0;//比较值
  26. tim2_oc.TIM_OutputState=TIM_OutputState_Enable;//输出比较使能
  27. tim2_oc.TIM_OCPolarity=TIM_OCPolarity_High;//输出极性 高电平
  28. TIM_OC1Init(TIM2,&tim2_oc);//通道1
  29. TIM_OC2Init(TIM2,&tim2_oc);//通道2
  30. TIM_OC3Init(TIM2,&tim2_oc);//通道3
  31. TIM_OC4Init(TIM2,&tim2_oc);//通道4
  32. TIM_SetCounter(TIM2,0);//TIM3->CNT=0;计数清0
  33. TIM_CtrlPWMOutputs(TIM2,ENABLE); //MOE 主输出使能(高级定时器必须设置)
  34. TIM_Cmd(TIM2, ENABLE); //使能tim2
  35. }

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

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

代码如下:

  1. void SysTick_Handler(void)
  2. {
  3. float left_speed=0;//左轮转轴速度
  4. float right_speed=0;//右轮转轴速度
  5. float left_pwm=0;
  6. int left_temp=0;//pid调试用,表示当前速度
  7. float right_pwm=0;
  8. int right_temp=0;//pid调试用,表示当前速度
  9. int integer=0;//整数
  10. int decimal=0;//小数
  11. i++;
  12. if(i>=100)//0.1s,适当要大,因为pwm控速时间越长越稳定
  13. {
  14. //数据处理
  15. left_speed=((float)left_count/1320)*10;//速度=计数值/转一圈的计数值*时间系数,单位:圈/s
  16. right_speed=((float)right_count/1320)*10;//速度=计数值/转一圈的计数值*时间系数,单位:圈/s
  17. if(left_count<0)
  18. left_speed=-left_speed;
  19. if(right_count<0)
  20. right_speed=-right_speed;
  21. //oled显示
  22. j++;
  23. if(j>20)//2s刷新一次,因为耗时较大
  24. {
  25. if(motion_mode==1)//前进
  26. {
  27. integer=left_speed;//整数
  28. decimal=(left_speed-integer)*100;//小数
  29. oled_show_string(65,5,"+",12);
  30. }
  31. else if(motion_mode==-1)//后退
  32. {
  33. integer=right_speed;//整数
  34. decimal=(right_speed-integer)*100;//小数
  35. oled_show_string(65,5,"-",12);
  36. }
  37. if(left_count!=0 || right_count!=0)//电机转动
  38. {
  39. //printf("count=%d\r\n",count);//脉冲个数
  40. //printf("integer=%d,decimal=%d\r\n",integer,decimal);//速度
  41. cap_flag=1;//电机转动
  42. oled_show_num(65,17,integer,2,12);//显示整数
  43. oled_show_char(81,20,'.',12,1);
  44. oled_show_num(97,20,decimal,2,12);//显示小数
  45. oled_show_num(65,33,integer,2,12);//显示整数
  46. oled_show_char(81,35,'.',12,1);
  47. oled_show_num(97,33,decimal,2,12);//显示小数
  48. oled_area_refresh_gram(65,0,108,48);//oled更新
  49. }
  50. else//电机未转动
  51. {
  52. if(cap_flag==1)//判断是否之前转动过,防止车轮未转而重复刷新
  53. {
  54. oled_area_clear(65,0,108,48);
  55. oled_area_refresh_gram(65,0,108,48);//oled更新
  56. cap_flag=0;
  57. }
  58. }
  59. j=0;
  60. SysTick_Config(90000);//使用systick延时后必须重新配置,i2c中使用了,所以必须重新配置
  61. }
  62. //printf("cap_flag=%d,left_count=%d,right_count=%d\r\n",cap_flag,left_count,right_count);
  63. //printf("left_count=%d,left_speed=%f,compara_add=%d\r\n",left_count,left_speed,left_compara);
  64. //printf("right_count=%d,right_speed=%f,right_add=%d\r\n",right_count,right_speed,right_compara);
  65. //pid算法实现
  66. if(open)
  67. {
  68. //(位置离散pid)
  69. left_pwm = PID_realize(&left_pid,left_speed*SPEED_AMPLIF);//逐步调整pwm值到set_point(目标值)(位置离散pid) 速度放大100倍,便于调试
  70. left_compara+=(int)left_pwm;
  71. right_pwm = PID_realize(&right_pid,right_speed*SPEED_AMPLIF);//逐步调整pwm值到set_point(目标值)(位置离散pid) 速度放大100倍,便于调试
  72. right_compara+=(int)right_pwm;
  73. //(增量式pid)
  74. //pwm=PID_add_realize(shaft_speed*SPEED_AMPLIF);//逐步调整pwm值到set_point(目标值)(增量式pid) 速度放大100倍,便于调试
  75. //compara_add=(int)pwm;
  76. left_temp=left_speed*SPEED_AMPLIF;
  77. right_temp=right_speed*SPEED_AMPLIF;
  78. }
  79. set_computer_value(SEND_FACT_CMD, CURVES_CH1, &left_temp, 1);//发送实际值,通道选择ch1以让软件显示图像
  80. set_computer_value(SEND_FACT_CMD, CURVES_CH2, &right_temp, 1);//发送实际值,通道选择ch1以让软件显示图像
  81. i=0;
  82. TIM_SetCounter(TIM3,0);//计数清零
  83. TIM_SetCounter(TIM4,0);//计数清零
  84. left_count=0;//计数清零
  85. right_count=0;//计数清零
  86. }

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

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

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

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

  1. //用于pid调试
  2. void USART1_IRQHandler(void) //串口1中断服务程序
  3. {
  4. uint8_t ucTemp;
  5. if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE) != RESET)//判断是哪个进入了中断函数
  6. {
  7. ucTemp = USART_ReceiveData(DEBUG_USARTx);//有清除RXNE功能,下面不需要写clearitpending
  8. protocol_data_recv(&ucTemp, 1);
  9. //Usart_SendByte(DEBUG_USARTx,ucTemp);
  10. }
  11. USART_ClearITPendingBit(DEBUG_USARTx,USART_IT_RXNE);
  12. }
  13. /* 发送8位数据的数组 */
  14. void Usart_SendArray(USART_TypeDef* USARTx, uint8_t *array,uint8_t num)
  15. {
  16. uint8_t i;
  17. for( i=0; i<num; i++ )
  18. {
  19. Usart_SendByte(USARTx, array[i]);
  20. }
  21. while( USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET );
  22. }

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

结果如图:

33fd10cbd6224d3f8d409fdd09a143c7.png

 

四、常用的测速方法:

3d7873e75e08466a8e839eef5dfb411f.png

我使用的是M法

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

 五、pid原理

1f99936364334c91937347cc35dfd4cc.png

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

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

d8284e32144346a08b78533e60bc481d.png

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

2a2218ebe21b46999be53518c906de20.png

 代码如下

  1. //pid结构体存储数据(位置离散pid)
  2. extern struct _pid{
  3. float target_val; //定义设定值
  4. float actual_val; //定义实际值
  5. float err; //定义偏差值
  6. float integral; //定义积分值
  7. float err_last; //定义上一个偏差值
  8. float Kp,Ki,Kd; //定义比例、积分、微分系数
  9. }left_pid,right_pid;
  10. //pid结构体存储数据(增量式pid)
  11. extern struct _pid_add{
  12. float target_val; //定义设定值
  13. float actual_val; //定义实际值
  14. float err; //定义偏差值
  15. float err_next; //定义下一个
  16. float err_last; //定义最后一个偏差值
  17. float Kp,Ki,Kd; //定义比例、积分、微分系数
  18. }pid_add;
  19. //pid初始化(位置离散pid)
  20. void PID_init(void)
  21. {
  22. left_pid.target_val=0.0;
  23. left_pid.actual_val=0.0;
  24. left_pid.err=0.0;
  25. left_pid.err_last=0.0;
  26. left_pid.integral=0.0;
  27. left_pid.Kp=1;//第一个调,曲线快速上升
  28. left_pid.Ki=0.003;//第三个调,增加波动
  29. left_pid.Kd=1.2;//第二个调,曲线快速稳定在目标
  30. right_pid.target_val=0.0;
  31. right_pid.actual_val=0.0;
  32. right_pid.err=0.0;
  33. right_pid.err_last=0.0;
  34. right_pid.integral=0.0;
  35. right_pid.Kp=1;//第一个调,曲线快速上升
  36. right_pid.Ki=0.003;//第三个调,增加波动
  37. right_pid.Kd=1.2;//第二个调,曲线快速稳定在目标
  38. }
  39. //pid初始化(增量式pid)
  40. void PID_add_init(void)
  41. {
  42. pid_add.target_val=0.0;
  43. pid_add.actual_val=0.0;
  44. pid_add.err=0.0;
  45. pid_add.err_last=0.0;
  46. pid_add.err_next=0.0;
  47. pid_add.Kp=1;//第二个调,曲线快速稳定在目标
  48. pid_add.Ki=0.75;//第一个调,曲线快速上升
  49. pid_add.Kd=0.5;//第三个调,曲线波动小
  50. }
  51. //设置pid
  52. void set_p_i_d(float P,float I,float D)
  53. {
  54. left_pid.Kp=P;
  55. left_pid.Ki=I;
  56. left_pid.Kd=D;
  57. right_pid.Kp=P;
  58. right_pid.Ki=I;
  59. right_pid.Kd=D;
  60. pid_add.Kp=P;
  61. pid_add.Ki=I;
  62. pid_add.Kd=D;
  63. }
  64. //pid算法(位置离散pid)
  65. float PID_realize(struct _pid *pid,float speed)
  66. {
  67. pid->err=pid->target_val-speed;//计算目标值与实际值的误差
  68. pid->integral+=pid->err;//误差累计
  69. pid->actual_val=pid->Kp*pid->err+pid->Ki*pid->integral+pid->Kd*(pid->err-pid->err_last);//pid算法(位置离散pid)
  70. //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);
  71. pid->err_last=pid->err;//误差传递
  72. return pid->actual_val;//返回实际值
  73. }
  74. //pid算法(增量式pid)
  75. float PID_add_realize(float speed)
  76. {
  77. pid_add.err=pid_add.target_val-speed;//计算目标值与实际值的误差
  78. 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);
  79. pid_add.actual_val+=increment_val;//累计
  80. pid_add.err_last=pid_add.err_next;//误差传递
  81. pid_add.err_next=pid_add.err;//误差传递
  82. return pid_add.actual_val;//返回实际值
  83. }

六、oled的实现

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

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

oled相关代码:

  1. //OLED的显存, 每个字节表示8个像素, 128,表示有128列, 8表示有64行, 高位表示第行数.
  2. static uint8_t g_oled_gram[128][8];
  3. //写命令
  4. void oled_write_cmd(uint8_t cmd)
  5. {
  6. i2c_star();
  7. i2c_write_byte(0x78);//oled地址
  8. i2c_wait_ack();
  9. i2c_write_byte(0x00);//写命令命令
  10. i2c_wait_ack();
  11. i2c_write_byte(cmd);//写命令
  12. i2c_wait_ack();
  13. i2c_stop();
  14. }
  15. //写数据
  16. void oled_write_data(uint8_t dat)
  17. {
  18. i2c_star();
  19. i2c_write_byte(0x78);//oled地址
  20. i2c_wait_ack();
  21. i2c_write_byte(0x40);//写数据命令
  22. i2c_wait_ack();
  23. i2c_write_byte(dat);//写数据
  24. i2c_wait_ack();
  25. i2c_stop();
  26. }
  27. //全屏更新
  28. void oled_all_refresh_gram(void)
  29. {
  30. uint8_t i,j;
  31. for(i=0;i<8;i++)//设置为8页
  32. {
  33. oled_write_cmd(0xb0 |i);//设置页
  34. oled_write_cmd(0x00);//设置列(低4bit)因为128列,至少要7bit
  35. oled_write_cmd(0x10);//设置列(高4bit)
  36. for(j=0;j<128;j++)
  37. {
  38. oled_write_data(g_oled_gram[j][i]);//写入数据
  39. }
  40. }
  41. }
  42. //区域更新
  43. void oled_area_refresh_gram(uint8_t x1,uint8_t y1,uint8_t x2,uint8_t y2)
  44. {
  45. uint8_t pos1,pos2,i,j;
  46. if (x1 > 127 || y1 > 63) return; /* 超出范围了. */
  47. if (x2 > 127 || y2 > 63) return; /* 超出范围了. */
  48. pos1=y1/8;
  49. pos2=y2/8;
  50. for(i=pos1;i<=pos2;i++)//设置为8页
  51. {
  52. oled_write_cmd(0xb0 |i);//设置页
  53. for(j=x1;j<=x2;j++)
  54. {
  55. oled_write_cmd(j&0xf);//设置列(低4bit)因为128列,至少要7bit
  56. oled_write_cmd((j>>4)|0x10);//设置列(高4bit)
  57. oled_write_data(g_oled_gram[j][i]);//写入数据
  58. }
  59. }
  60. }
  61. //全屏清屏
  62. void oled_all_clear(void)
  63. {
  64. uint8_t i, j;
  65. for (i = 0; i < 8; i++)
  66. for (j = 0; j < 128; j++)
  67. g_oled_gram[j][i] = 0X00;
  68. oled_all_refresh_gram(); /* 更新显示 */
  69. }
  70. //区域清屏
  71. void oled_area_clear(uint8_t x1,uint8_t y1,uint8_t x2,uint8_t y2)
  72. {
  73. uint8_t pos1,pos2,i,j;
  74. if (x1 > 127 || y1 > 63) return; /* 超出范围了. */
  75. if (x2 > 127 || y2 > 63) return; /* 超出范围了. */
  76. pos1=y1/8;
  77. pos2=y2/8;
  78. //printf("x1=%d,y1=%d,x2=%d,y2=%d,pos1=%d,pos2=%d\r\n",x1,y1,x2,y2,pos1,pos2);
  79. for(i=pos1;i<=pos2;i++)//设置为8页
  80. for(j=x1;j<=x2;j++)
  81. {
  82. //printf("i=%d,j=%d\r\n",i,j);
  83. g_oled_gram[j][i] = 0X00;
  84. }
  85. }
  86. //全屏点亮
  87. void oled_fill(void)
  88. {
  89. uint8_t i, j;
  90. for (i = 0; i < 8; i++)
  91. for (j = 0; j < 128; j++)
  92. g_oled_gram[j][i] = 0XFF;
  93. oled_all_refresh_gram(); /* 更新显示 */
  94. }
  95. //打开
  96. void oled_display_on(void)
  97. {
  98. oled_write_cmd(0X8D); /* SET DCDC命令 */
  99. oled_write_cmd(0X14); /* DCDC ON */
  100. oled_write_cmd(0XAF); /* DISPLAY ON */
  101. }
  102. //关闭
  103. void oled_display_off(void)
  104. {
  105. oled_write_cmd(0X8D); /* SET DCDC命令 */
  106. oled_write_cmd(0X10); /* DCDC OFF */
  107. oled_write_cmd(0XAE); /* DISPLAY OFF */
  108. }
  109. //oled初始化
  110. void oled_init(void)
  111. {
  112. i2c_init();//i2c初始化
  113. delay_ms(100);//延时,重要
  114. oled_write_cmd(0xAE); /* 关闭显示 */
  115. oled_write_cmd(0xD5); /* 设置时钟分频因子,震荡频率 */
  116. oled_write_cmd(0xf0); /* [3:0],分频因子;[7:4],震荡频率 */
  117. oled_write_cmd(0xA8); /* 设置驱动路数 */
  118. oled_write_cmd(0X3F); /* 默认0X3F(1/64) */
  119. oled_write_cmd(0xD3); /* 设置显示偏移 */
  120. oled_write_cmd(0X00); /* 设置列(低4bit)*/
  121. oled_write_cmd(0x10);//设置列(高4bit)
  122. oled_write_cmd(0x40); /* 设置显示开始行 [5:0],行数. */
  123. oled_write_cmd(0x8D); /* 电荷泵设置 */
  124. oled_write_cmd(0x14); /* bit2,开启/关闭 */
  125. oled_write_cmd(0x20); /* 设置内存地址模式 */
  126. oled_write_cmd(0x02); /* [1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10; */
  127. oled_write_cmd(0xb0); //开启地址0-7
  128. oled_write_cmd(0xA1); /* 段重定义设置,bit0:0,0->0;1,0->127; */
  129. oled_write_cmd(0xC8); /* 设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数 */
  130. oled_write_cmd(0xDA); /* 设置COM硬件引脚配置 */
  131. oled_write_cmd(0x12); /* 128*64 */
  132. oled_write_cmd(0x81); /* 对比度设置 */
  133. oled_write_cmd(0xEF); /* 1~255;默认0X7F (亮度设置,越大越亮) */
  134. oled_write_cmd(0xD9); /* 设置预充电周期 */
  135. oled_write_cmd(0x22); //充电时间
  136. oled_write_cmd(0xf1); /* [3:0],PHASE 1;[7:4],PHASE 2; */
  137. oled_write_cmd(0xDB); /* 设置VCOMH 电压倍率 */
  138. oled_write_cmd(0x20); //0x20,0.77xVcc
  139. oled_write_cmd(0xA4); /* 全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) */
  140. oled_write_cmd(0xA6); /* 设置显示方式;bit0:1,反相显示;0,正常显示 */
  141. oled_write_cmd(0xAF); /* 开启显示 */
  142. oled_all_clear();
  143. }
  144. //画点
  145. void oled_draw_point(uint8_t x, uint8_t y, uint8_t dot)
  146. {
  147. uint8_t pos, bx, temp = 0;
  148. if (x > 127 || y > 63) return; /* 超出范围了. */
  149. pos = y / 8; /* 计算GRAM里面的y坐标所在的字节, 每个字节可以存储8个行坐标 */
  150. bx = y % 8; /* 取余数,方便计算y在对应字节里面的位置,及行(y)位置 */
  151. temp = 1 << bx; /* 高位表示低行号, 得到y对应的bit位置,将该bit先置1 */
  152. if (dot) /* 画实心点 */
  153. {
  154. g_oled_gram[x][pos] |= temp;
  155. }
  156. else /* 画空点,即不显示 */
  157. {
  158. g_oled_gram[x][pos] &= ~temp;
  159. }
  160. }
  161. // 显示字符
  162. void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode)
  163. {
  164. uint8_t temp, t, t1;
  165. uint8_t y0 = y;
  166. uint8_t *pfont = 0;
  167. uint8_t csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 得到字体一个字符对应点阵集所占的字节数 */
  168. chr = chr - ' '; /* 得到偏移后的值,因为字库是从空格开始存储的,第一个字符是空格 */
  169. if (size == 12) /* 调用1206字体 */
  170. {
  171. pfont = (uint8_t *)oled_asc2_1206[chr];
  172. }
  173. else if (size == 16) /* 调用1608字体 */
  174. {
  175. pfont = (uint8_t *)oled_asc2_1608[chr];
  176. }
  177. else if (size == 24) /* 调用2412字体 */
  178. {
  179. pfont = (uint8_t *)oled_asc2_2412[chr];
  180. }
  181. else /* 没有的字库 */
  182. {
  183. return;
  184. }
  185. for (t = 0; t < csize; t++)
  186. {
  187. temp = pfont[t];
  188. for (t1 = 0; t1 < 8; t1++)
  189. {
  190. if (temp & 0x80)oled_draw_point(x, y, mode);
  191. else oled_draw_point(x, y, !mode);
  192. temp <<= 1;
  193. y++;
  194. if ((y - y0) == size)
  195. {
  196. y = y0;
  197. x++;
  198. break;
  199. }
  200. }
  201. }
  202. }
  203. //平方函数, m^n
  204. static uint32_t oled_pow(uint8_t m, uint8_t n)
  205. {
  206. uint32_t result = 1;
  207. while (n--)
  208. {
  209. result *= m;
  210. }
  211. return result;
  212. }
  213. //显示len个数字
  214. void oled_show_num(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
  215. {
  216. uint8_t t, temp;
  217. uint8_t enshow = 0;
  218. for (t = 0; t < len; t++) /* 按总显示位数循环 */
  219. {
  220. temp = (num / oled_pow(10, len - t - 1)) % 10; /* 获取对应位的数字 */
  221. if (enshow == 0 && t < (len - 1)) /* 没有使能显示,且还有位要显示 */
  222. {
  223. if (temp == 0)
  224. {
  225. oled_show_char(x + (size / 2)*t, y, ' ', size, 1); /* 显示空格,站位 */
  226. continue; /* 继续下个一位 */
  227. }
  228. else
  229. {
  230. enshow = 1; /* 使能显示 */
  231. }
  232. }
  233. oled_show_char(x + (size / 2)*t, y, temp + '0', size, 1); /* 显示字符 ,因为是通过两页写的所以size除2*/
  234. }
  235. }
  236. //显示字符串
  237. void oled_show_string(uint8_t x, uint8_t y, const char *p, uint8_t size)
  238. {
  239. while ((*p <= '~') && (*p >= ' ')) /* 判断是不是非法字符! */
  240. {
  241. if (x > (128 - (size / 2))) /* 宽度越界 */
  242. {
  243. x = 0;
  244. y += size; /* 换行 */
  245. }
  246. if (y > (64 - size)) /* 高度越界 */
  247. {
  248. y = x = 0;
  249. oled_all_clear();
  250. }
  251. oled_show_char(x, y, *p, size, 1); /* 显示一个字符 */
  252. x += size / 2; /* ASCII字符宽度为汉字宽度的一半 因为是通过两页写的所以size除2*/
  253. p++;
  254. }
  255. }

七、蓝牙通信

使用串口2

首先对串口2进行初始化

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

  1. void USART2_IRQHandler(void) //串口x中断服务程序
  2. {
  3. float val=0;
  4. uint8_t i=0;
  5. uint8_t temp2;
  6. float decimal=0;
  7. if(USART_GetITStatus(USART2,USART_IT_RXNE) != RESET)//判断中断位
  8. {
  9. USART_ClearITPendingBit(USART2, USART_IT_RXNE);
  10. temp2 = USART_ReceiveData(USART2); //接收数据
  11. if(temp2!='\n')
  12. {
  13. //printf("temp2=%c\r\n",temp2);
  14. if(temp2=='.')
  15. {
  16. integer_flag=2;
  17. return;
  18. }
  19. if(integer_flag==0)//整数部分
  20. {
  21. integer_val=temp2-48;
  22. integer_flag=1;
  23. }
  24. else if(integer_flag==1)
  25. integer_val=integer_val*10+temp2-48;
  26. if(integer_flag==2)//小数部分
  27. {
  28. decimal=(temp2-48);
  29. for(i=0;i<decimal_bit;i++)
  30. decimal/=10;
  31. decimal_val+=decimal;
  32. decimal_bit++;//小数位数
  33. }
  34. }
  35. else
  36. {
  37. val=integer_val+decimal_val;
  38. printf("data=%.2f\r\n",val);
  39. integer_flag=0;
  40. decimal_bit=1;
  41. integer_val=0;
  42. decimal_val=0;
  43. switch((int)val)
  44. {
  45. case 490://停止
  46. left_compara=0;
  47. right_compara=0;
  48. motion_mode=0;
  49. open=0;
  50. break;
  51. case 491://前进
  52. left_compara=0;
  53. right_compara=0;
  54. motion_mode=1;
  55. open=1;
  56. break;
  57. case 492://后退
  58. left_compara=0;
  59. right_compara=0;
  60. motion_mode=-1;
  61. open=1;
  62. break;
  63. case 493://左转
  64. left_compara=0;
  65. right_compara=0;
  66. motion_mode=2;
  67. open=1;
  68. break;
  69. case 494://右转
  70. left_compara=0;
  71. right_compara=0;
  72. motion_mode=3;
  73. open=1;
  74. break;
  75. case 501://蓝牙按键模式
  76. tracking_mod=0;
  77. break;
  78. case 502://循迹模式
  79. tracking_mod=1;
  80. break;
  81. }
  82. if((int)val<10)
  83. {
  84. if(val>3.5)//最大速度
  85. val=3.5;
  86. left_targetspeed=val;//速度放大100倍,便于调试,
  87. right_targetspeed=val;//速度放大100倍,便于调试
  88. }
  89. }
  90. }
  91. }

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

aca74f6b12524df19836e8d195834f9e.jpeg

 

八、3路循迹模块

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

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

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

代码如下

  1. if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14)==0)//中
  2. {
  3. left_set_point=left_targetspeed*SPEED_AMPLIF;//速度放大100倍,便于调试,
  4. right_set_point=right_targetspeed*SPEED_AMPLIF;//速度放大100倍,便于调试
  5. TIM_SetCompare1(TIM2,0);
  6. TIM_SetCompare2(TIM2,left_compara);
  7. TIM_SetCompare3(TIM2,0);
  8. TIM_SetCompare4(TIM2,right_compara);
  9. }
  10. else if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==0)//左
  11. {
  12. left_set_point=left_targetspeed/2*SPEED_AMPLIF;//0.5m/s.速度放大100倍,便于调试,
  13. right_set_point=right_targetspeed*SPEED_AMPLIF;//1m/s.速度放大100倍,便于调试
  14. left_compara=0;
  15. TIM_SetCompare1(TIM2,0);
  16. TIM_SetCompare2(TIM2,0);
  17. TIM_SetCompare3(TIM2,0);
  18. TIM_SetCompare4(TIM2,right_compara);
  19. }
  20. else if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_15)==0)//右
  21. {
  22. right_set_point=right_targetspeed/2*SPEED_AMPLIF;//0.5m/s.速度放大100倍,便于调试
  23. left_set_point=left_targetspeed*SPEED_AMPLIF;//1m/s.速度放大100倍,便于调试,
  24. right_compara=0;
  25. TIM_SetCompare1(TIM2,0);
  26. TIM_SetCompare2(TIM2,left_compara);
  27. TIM_SetCompare3(TIM2,0);
  28. TIM_SetCompare4(TIM2,0);
  29. }

就此,整个流程结束。


总结

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

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/694974
推荐阅读
相关标签
  

闽ICP备14008679号