当前位置:   article > 正文

【电赛】STM32-PID直流减速电机小车【寻迹+避障+跟随】【更新ing】_电磁车pid避障

电磁车pid避障

一.需求分析

1.主控:STM32C8T6(没什么好说的哈哈)
2.电机:JAG25-370电机

【问】为什么要用直流减速电机??

PID控制器需要依靠精确的反馈信号来调整其输出,确保电机按照预定的速度和位置运行。

直流减速电机具有编码器,所以具有很高的可靠性。

3.电机驱动模块:A4950

A4950通过简单的PWM控制电机速度和转矩,采用了高效的H桥驱动方式,可以有效地控制电机的正反转和制动。H桥驱动可以减少能量损失,提高整体效率。

4.超声波测距模块:HC-SR04
5.寻迹:TCRT5000红外反射传感器 * 4

6.蓝牙:HC-05蓝牙串口透传模块
7.姿态控制:MPU6050(经典)
8.电源检测:ADC
9.OLED

二.硬件原理图

主控:

电源输入:

 电机驱动和电机接口:

超声波模块接口:

红外对管接口:
蓝牙:

MPU6050:

 ADC电源安全检测:

OLED:

三.实现功能的逐个实验

1.通过外部中断控制按键点灯

PB4 - KEY1

PA12 - KEY2

首先GPIO初始化一下,然后设置外部中断:

PB4 - KEY1 - 设置为下拉(3.3V,把他拉下来) - 上升沿(没按下之前是0,按下才变1,所以是检测上升沿)

PA12 - KEY2 - 设置为上拉 - 下降沿(按下之后是0,没按下之前是1,所以检测下降沿)

STM32CubeIDE中配置外部中断。

 生成项目后:找到GPIO中断的回调函数

在我们自己的gpio.c中重新写一下:

  1. void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
  2. {
  3. if(GPIO_Pin == KEY1_Pin)
  4. {
  5. HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
  6. }
  7. if(GPIO_Pin == KEY2_Pin)
  8. {
  9. HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
  10. }
  11. }

这是测试代码。

2.OLED初始化

引脚首先要配置好:

PA15    OLED_SCL

PB12    OLED_SDA

都是上拉输出模式(IIC)

OLED的代码上优信电子官网就可以下载了,我们只要会移植,会用即可。

  1. #include "oled.h"
  2. #include "stdlib.h"
  3. #include "oledfont.h"
  4. //OLED的显存
  5. //存放格式如下.
  6. //[0]0 1 2 3 ... 127
  7. //[1]0 1 2 3 ... 127
  8. //[2]0 1 2 3 ... 127
  9. //[3]0 1 2 3 ... 127
  10. //[4]0 1 2 3 ... 127
  11. //[5]0 1 2 3 ... 127
  12. //[6]0 1 2 3 ... 127
  13. //[7]0 1 2 3 ... 127
  14. /**********************************************
  15. //IIC Start
  16. **********************************************/
  17. /**********************************************
  18. //IIC Start
  19. **********************************************/
  20. void IIC_Start()
  21. {
  22. OLED_SCLK_Set() ;
  23. OLED_SDIN_Set();
  24. OLED_SDIN_Clr();
  25. OLED_SCLK_Clr();
  26. }
  27. /**********************************************
  28. //IIC Stop
  29. **********************************************/
  30. void IIC_Stop()
  31. {
  32. OLED_SCLK_Set() ;
  33. // OLED_SCLK_Clr();
  34. OLED_SDIN_Clr();
  35. OLED_SDIN_Set();
  36. }
  37. void IIC_Wait_Ack()
  38. {
  39. //GPIOB->CRH &= 0XFFF0FFFF; //设置PB12为上拉输入模式
  40. //GPIOB->CRH |= 0x00080000;
  41. // OLED_SDA = 1;
  42. // delay_us(1);
  43. //OLED_SCL = 1;
  44. //delay_us(50000);
  45. /* while(1)
  46. {
  47. if(!OLED_SDA) //判断是否接收到OLED 应答信号
  48. {
  49. //GPIOB->CRH &= 0XFFF0FFFF; //设置PB12为通用推免输出模式
  50. //GPIOB->CRH |= 0x00030000;
  51. return;
  52. }
  53. }
  54. */
  55. OLED_SCLK_Set() ;
  56. OLED_SCLK_Clr();
  57. }
  58. /**********************************************
  59. // IIC Write byte
  60. **********************************************/
  61. void Write_IIC_Byte(unsigned char IIC_Byte)
  62. {
  63. unsigned char i;
  64. unsigned char m,da;
  65. da=IIC_Byte;
  66. OLED_SCLK_Clr();
  67. for(i=0;i<8;i++)
  68. {
  69. m=da;
  70. // OLED_SCLK_Clr();
  71. m=m&0x80;
  72. if(m==0x80)
  73. {OLED_SDIN_Set();}
  74. else OLED_SDIN_Clr();
  75. da=da<<1;
  76. OLED_SCLK_Set();
  77. OLED_SCLK_Clr();
  78. }
  79. }
  80. /**********************************************
  81. // IIC Write Command
  82. **********************************************/
  83. void Write_IIC_Command(unsigned char IIC_Command)
  84. {
  85. IIC_Start();
  86. Write_IIC_Byte(0x78); //Slave address,SA0=0
  87. IIC_Wait_Ack();
  88. Write_IIC_Byte(0x00); //write command
  89. IIC_Wait_Ack();
  90. Write_IIC_Byte(IIC_Command);
  91. IIC_Wait_Ack();
  92. IIC_Stop();
  93. }
  94. /**********************************************
  95. // IIC Write Data
  96. **********************************************/
  97. void Write_IIC_Data(unsigned char IIC_Data)
  98. {
  99. IIC_Start();
  100. Write_IIC_Byte(0x78); //D/C#=0; R/W#=0
  101. IIC_Wait_Ack();
  102. Write_IIC_Byte(0x40); //write data
  103. IIC_Wait_Ack();
  104. Write_IIC_Byte(IIC_Data);
  105. IIC_Wait_Ack();
  106. IIC_Stop();
  107. }
  108. void OLED_WR_Byte(unsigned dat,unsigned cmd)
  109. {
  110. if(cmd)
  111. {
  112. Write_IIC_Data(dat);
  113. }
  114. else {
  115. Write_IIC_Command(dat);
  116. }
  117. }
  118. /********************************************
  119. // fill_Picture
  120. ********************************************/
  121. void fill_picture(unsigned char fill_Data)
  122. {
  123. unsigned char m,n;
  124. for(m=0;m<8;m++)
  125. {
  126. OLED_WR_Byte(0xb0+m,0); //page0-page1
  127. OLED_WR_Byte(0x00,0); //low column start address
  128. OLED_WR_Byte(0x10,0); //high column start address
  129. for(n=0;n<128;n++)
  130. {
  131. OLED_WR_Byte(fill_Data,1);
  132. }
  133. }
  134. }
  135. /***********************Delay****************************************/
  136. void Delay_50ms(unsigned int Del_50ms)
  137. {
  138. unsigned int m;
  139. for(;Del_50ms>0;Del_50ms--)
  140. for(m=6245;m>0;m--);
  141. }
  142. void Delay_1ms(unsigned int Del_1ms)
  143. {
  144. unsigned char j;
  145. while(Del_1ms--)
  146. {
  147. for(j=0;j<123;j++);
  148. }
  149. }
  150. //坐标设置
  151. void OLED_Set_Pos(unsigned char x, unsigned char y)
  152. { OLED_WR_Byte(0xb0+y,OLED_CMD);
  153. OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);
  154. OLED_WR_Byte((x&0x0f),OLED_CMD);
  155. }
  156. //开启OLED显示
  157. void OLED_Display_On(void)
  158. {
  159. OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令
  160. OLED_WR_Byte(0X14,OLED_CMD); //DCDC ON
  161. OLED_WR_Byte(0XAF,OLED_CMD); //DISPLAY ON
  162. }
  163. //关闭OLED显示
  164. void OLED_Display_Off(void)
  165. {
  166. OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令
  167. OLED_WR_Byte(0X10,OLED_CMD); //DCDC OFF
  168. OLED_WR_Byte(0XAE,OLED_CMD); //DISPLAY OFF
  169. }
  170. //清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
  171. void OLED_Clear(void)
  172. {
  173. uint8_t i,n;
  174. for(i=0;i<8;i++)
  175. {
  176. OLED_WR_Byte (0xb0+i,OLED_CMD); //设置页地址(0~7)
  177. OLED_WR_Byte (0x00,OLED_CMD); //设置显示位置—列低地址
  178. OLED_WR_Byte (0x10,OLED_CMD); //设置显示位置—列高地址
  179. for(n=0;n<128;n++)OLED_WR_Byte(0,OLED_DATA);
  180. } //更新显示
  181. }
  182. void OLED_On(void)
  183. {
  184. uint8_t i,n;
  185. for(i=0;i<8;i++)
  186. {
  187. OLED_WR_Byte (0xb0+i,OLED_CMD); //设置页地址(0~7)
  188. OLED_WR_Byte (0x00,OLED_CMD); //设置显示位置—列低地址
  189. OLED_WR_Byte (0x10,OLED_CMD); //设置显示位置—列高地址
  190. for(n=0;n<128;n++)OLED_WR_Byte(1,OLED_DATA);
  191. } //更新显示
  192. }
  193. //在指定位置显示一个字符,包括部分字符
  194. //x:0~127
  195. //y:0~63
  196. //mode:0,反白显示;1,正常显示
  197. //size:选择字体 16/12
  198. void OLED_ShowChar(uint8_t x,uint8_t y,uint8_t chr,uint8_t Char_Size)
  199. {
  200. unsigned char c=0,i=0;
  201. c=chr-' ';//得到偏移后的值
  202. if(x>Max_Column-1){x=0;y=y+2;}
  203. if(Char_Size ==16)
  204. {
  205. OLED_Set_Pos(x,y);
  206. for(i=0;i<8;i++)
  207. OLED_WR_Byte(F8X16[c*16+i],OLED_DATA);
  208. OLED_Set_Pos(x,y+1);
  209. for(i=0;i<8;i++)
  210. OLED_WR_Byte(F8X16[c*16+i+8],OLED_DATA);
  211. }
  212. else {
  213. OLED_Set_Pos(x,y);
  214. for(i=0;i<6;i++)
  215. OLED_WR_Byte(F6x8[c][i],OLED_DATA);
  216. }
  217. }
  218. //m^n函数
  219. uint32_t oled_pow(uint8_t m,uint8_t n)
  220. {
  221. uint32_t result=1;
  222. while(n--)result*=m;
  223. return result;
  224. }
  225. //显示2个数字
  226. //x,y :起点坐标
  227. //len :数字的位数
  228. //size:字体大小
  229. //mode:模式 0,填充模式;1,叠加模式
  230. //num:数值(0~4294967295);
  231. void OLED_ShowNum(uint8_t x,uint8_t y,uint32_t num,uint8_t len,uint8_t size2)
  232. {
  233. uint8_t t,temp;
  234. uint8_t enshow=0;
  235. for(t=0;t<len;t++)
  236. {
  237. temp=(num/oled_pow(10,len-t-1))%10;
  238. if(enshow==0&&t<(len-1))
  239. {
  240. if(temp==0)
  241. {
  242. OLED_ShowChar(x+(size2/2)*t,y,' ',size2);
  243. continue;
  244. }else enshow=1;
  245. }
  246. OLED_ShowChar(x+(size2/2)*t,y,temp+'0',size2);
  247. }
  248. }
  249. //显示一个字符号串
  250. void OLED_ShowString(uint8_t x,uint8_t y,uint8_t *chr,uint8_t Char_Size)
  251. {
  252. unsigned char j=0;
  253. while (chr[j]!='\0')
  254. { OLED_ShowChar(x,y,chr[j],Char_Size);
  255. x+=8;
  256. if(x>120){x=0;y+=2;}
  257. j++;
  258. }
  259. }
  260. //显示汉字
  261. void OLED_ShowCHinese(uint8_t x,uint8_t y,uint8_t no)
  262. {
  263. uint8_t t,adder=0;
  264. OLED_Set_Pos(x,y);
  265. for(t=0;t<16;t++)
  266. {
  267. OLED_WR_Byte(Hzk[2*no][t],OLED_DATA);
  268. adder+=1;
  269. }
  270. OLED_Set_Pos(x,y+1);
  271. for(t=0;t<16;t++)
  272. {
  273. OLED_WR_Byte(Hzk[2*no+1][t],OLED_DATA);
  274. adder+=1;
  275. }
  276. }
  277. /***********功能描述:显示显示BMP图片128×64起始点坐标(x,y),x的范围0~127,y为页的范围0~7*****************/
  278. void OLED_DrawBMP(unsigned char x0, unsigned char y0,unsigned char x1, unsigned char y1,unsigned char BMP[])
  279. {
  280. unsigned int j=0;
  281. unsigned char x,y;
  282. if(y1%8==0) y=y1/8;
  283. else y=y1/8+1;
  284. for(y=y0;y<y1;y++)
  285. {
  286. OLED_Set_Pos(x0,y);
  287. for(x=x0;x<x1;x++)
  288. {
  289. OLED_WR_Byte(BMP[j++],OLED_DATA);
  290. }
  291. }
  292. }
  293. //初始化SSD1306
  294. void OLED_Init(void)
  295. {
  296. // GPIO_InitTypeDef GPIO_InitStructure;
  297. //
  298. // RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能A端口时钟
  299. // GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_7;
  300. // GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
  301. // GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz
  302. // GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化GPIOD3,6
  303. // GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_7);
  304. HAL_Delay(800);
  305. OLED_WR_Byte(0xAE,OLED_CMD);//--display off
  306. OLED_WR_Byte(0x00,OLED_CMD);//---set low column address
  307. OLED_WR_Byte(0x10,OLED_CMD);//---set high column address
  308. OLED_WR_Byte(0x40,OLED_CMD);//--set start line address
  309. OLED_WR_Byte(0xB0,OLED_CMD);//--set page address
  310. OLED_WR_Byte(0x81,OLED_CMD); // contract control
  311. OLED_WR_Byte(0xFF,OLED_CMD);//--128
  312. OLED_WR_Byte(0xA1,OLED_CMD);//set segment remap
  313. OLED_WR_Byte(0xA6,OLED_CMD);//--normal / reverse
  314. OLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)
  315. OLED_WR_Byte(0x3F,OLED_CMD);//--1/32 duty
  316. OLED_WR_Byte(0xC8,OLED_CMD);//Com scan direction
  317. OLED_WR_Byte(0xD3,OLED_CMD);//-set display offset
  318. OLED_WR_Byte(0x00,OLED_CMD);//
  319. OLED_WR_Byte(0xD5,OLED_CMD);//set osc division
  320. OLED_WR_Byte(0x80,OLED_CMD);//
  321. OLED_WR_Byte(0xD8,OLED_CMD);//set area color mode off
  322. OLED_WR_Byte(0x05,OLED_CMD);//
  323. OLED_WR_Byte(0xD9,OLED_CMD);//Set Pre-Charge Period
  324. OLED_WR_Byte(0xF1,OLED_CMD);//
  325. OLED_WR_Byte(0xDA,OLED_CMD);//set com pin configuartion
  326. OLED_WR_Byte(0x12,OLED_CMD);//
  327. OLED_WR_Byte(0xDB,OLED_CMD);//set Vcomh
  328. OLED_WR_Byte(0x30,OLED_CMD);//
  329. OLED_WR_Byte(0x8D,OLED_CMD);//set charge pump enable
  330. OLED_WR_Byte(0x14,OLED_CMD);//
  331. OLED_WR_Byte(0xAF,OLED_CMD);//--turn on oled panel
  332. }

SCL和SDA宏定义要改一下;

库函数要改成hal库的函数;

最后进行测试:

  1. OLED_Init(); //初始化OLED
  2. OLED_Clear();
  3. OLED_ShowCHinese(0,0,0);//中
  4. OLED_ShowCHinese(18,0,1);//景
  5. OLED_ShowCHinese(36,0,2);//园
  6. OLED_ShowCHinese(54,0,3);//电
  7. OLED_ShowCHinese(72,0,4);//子
  8. OLED_ShowCHinese(90,0,5);//科
  9. OLED_ShowCHinese(108,0,6);//技

3.串口初始化

硬件接线:

因为用到了CH340,USB转串口模块:

PA9 TX     --------     CH340的TX

PA10 RX   ---------     CH340的RX

然后我们要实现printf函数:

打开魔术棒 -  Target  -  Use MicroLIB打勾

然后重定向fputc:

  1. /**
  2. * @brief 重定向printf (重定向fputc),
  3. 使用时候记得勾选上魔法棒->Target->UseMicro LIB
  4. 可能需要在C文件加typedef struct __FILE FILE;
  5. 包含这个文件#include "stdio.h"
  6. * @param
  7. * @return
  8. */
  9. int fputc(int ch,FILE *stream)
  10. {
  11. HAL_UART_Transmit(&huart1,( uint8_t *)&ch,1,0xFFFF);
  12. return ch;
  13. }
在usart.c添加 typedef struct __FILE FILE;

测试一下printf即可。

4.PWM控制电机

硬件接线 :

AIN2 - PWMA - PA11

BIN2 - PWMB - PA8

【问】两个PWM的通道对应的引脚是如何选择的?

查阅STM32数据手册可以发现:

PA8和PA11刚好对应TIM1的通道1和通道4。

所以我们在IDE中进行设置一下复用功能重映射:

设置PWM输出 ,设置占空比 

生成代码后打开定时器1的通道1和4 

  1. HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);//开启定时器1 通道1 PWM输出
  2. HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_4);//开启定时器1 通道4 PWM输出

【仿真测试PWM】

然后设置时钟频率,板子外部晶振8Mhz

开启仿真

仿真成功。

5.电机驱动初始化

硬件连接:

AIN1和BIN1我们通过引脚电平控制高低:

AIN1低电平 - 转速为正方向 - 此时AIN2的PWM占空比数值对应着转速

AIN1高电平 - 转速为负方向 - 此时AIN2的 (1 - PWM占空比数值)对应着转速

【一个朴实无华的电机转速控制函数】

  1. #define AIN1_SET HAL_GPIO_WritePin(AIN1_GPIO_Port,AIN1_Pin,GPIO_PIN_SET)
  2. #define AIN1_RESET HAL_GPIO_WritePin(AIN1_GPIO_Port,AIN1_Pin,GPIO_PIN_RESET)
  3. #define BIN1_SET HAL_GPIO_WritePin(BIN1_GPIO_Port,BIN1_Pin,GPIO_PIN_SET)
  4. #define BIN1_RESET HAL_GPIO_WritePin(BIN1_GPIO_Port,BIN1_Pin,GPIO_PIN_RESET)
  5. void Motor_Set(int Motor1,int Motor2)
  6. {
  7. //1.先根据正负设置方向GPIO 高低电平
  8. if(Motor1 <0) BIN1_SET;
  9. else BIN1_RESET;
  10. if(Motor2 <0) AIN1_SET;
  11. else AIN1_RESET;
  12. //2.然后设置占空比
  13. if(Motor1 <0)
  14. {
  15. if(Motor1 <-99) Motor1 =-99;
  16. __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (100+Motor1));
  17. }
  18. else
  19. {
  20. if(Motor1 >99) Motor1 = 99;
  21. __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1,Motor1);
  22. }
  23. if(Motor2<0)
  24. {
  25. if(Motor2 <-99) Motor2=-99;
  26. __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, (100+Motor2));
  27. }
  28. else
  29. {
  30. if(Motor2 >99) Motor2 =99;
  31. __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, Motor2);
  32. }
  33. }

这个函数就是根据我们传进去的Motor数值设置占空比 

__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (100+Motor1));

6.编码器测速

参考这一篇:

STM32定时器(4.JGA25-370霍尔编码器测速)icon-default.png?t=N7T8https://blog.csdn.net/Xiaoxuexxxxx/article/details/137937083

【使用中断定时测速】

设置定时器1,时钟为内部时钟,开启自动更新中断

 NVIC开启中断

代码中开启中断:

  HAL_TIM_Base_Start_IT(&htim1);                //开启定时器1 中断

 定时中断函数中添加测速函数:

  1. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
  2. {
  3. if(htim == &htim1)//htim1 500HZ 2ms中断一次
  4. {
  5. TimerCount++;
  6. if(TimerCount %5 == 0)//每10ms 执行一次
  7. {
  8. Encode1Count = -(short)__HAL_TIM_GET_COUNTER(&htim4);
  9. Encode2Count = (short)__HAL_TIM_GET_COUNTER(&htim2);
  10. __HAL_TIM_SET_COUNTER(&htim4,0);
  11. __HAL_TIM_SET_COUNTER(&htim2,0);
  12. Motor1Speed = (float)Encode1Count*100/9.6/11/4;
  13. Motor2Speed = (float)Encode2Count*100/9.6/11/4;
  14. TimerCount=0;
  15. }
  16. }

7.观察PID控制曲线的准备工作:匿名上位机的移植

【大端模式】

数据的低位存储在地址的高位,数据的高位存储在地址的低位。

【小端模式】

数据的低位存储在地址的低位,数据的高位存储在地址的高位。

STM32数据是小端存储 

匿名上位机通信协议:

新建一个NiMing.c

  1. #include "niming.h"
  2. #include "main.h"
  3. #include "usart.h"
  4. uint8_t data_to_send[100];
  5. //通过F1帧发送4个uint16类型的数据
  6. void ANO_DT_Send_F1(uint16_t _a, uint16_t _b, uint16_t _c, uint16_t _d)
  7. {
  8. uint8_t _cnt = 0; //计数值
  9. uint8_t sumcheck = 0; //和校验
  10. uint8_t addcheck = 0; //附加和校验
  11. uint8_t i = 0;
  12. data_to_send[_cnt++] = 0xAA;//帧头
  13. data_to_send[_cnt++] = 0xFF;//目标地址
  14. data_to_send[_cnt++] = 0xF1;//功能码
  15. data_to_send[_cnt++] = 8; //数据长度
  16. //单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
  17. data_to_send[_cnt++] = BYTE0(_a);
  18. data_to_send[_cnt++] = BYTE1(_a);
  19. data_to_send[_cnt++] = BYTE0(_b);
  20. data_to_send[_cnt++] = BYTE1(_b);
  21. data_to_send[_cnt++] = BYTE0(_c);
  22. data_to_send[_cnt++] = BYTE1(_c);
  23. data_to_send[_cnt++] = BYTE0(_d);
  24. data_to_send[_cnt++] = BYTE1(_d);
  25. for ( i = 0; i < data_to_send[3]+4; i++)
  26. {
  27. sumcheck += data_to_send[i];//和校验
  28. addcheck += sumcheck;//附加校验
  29. }
  30. data_to_send[_cnt++] = sumcheck;
  31. data_to_send[_cnt++] = addcheck;
  32. HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
  33. }
  34. //,通过F2帧发送4个int16类型的数据
  35. void ANO_DT_Send_F2(int16_t _a, int16_t _b, int16_t _c, int16_t _d) //F2帧 4个
  36. int16 参数
  37. {
  38. uint8_t _cnt = 0;
  39. uint8_t sumcheck = 0; //和校验
  40. uint8_t addcheck = 0; //附加和校验
  41. uint8_t i=0;
  42. data_to_send[_cnt++] = 0xAA;
  43. data_to_send[_cnt++] = 0xFF;
  44. data_to_send[_cnt++] = 0xF2;
  45. data_to_send[_cnt++] = 8; //数据长度
  46. //单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
  47. data_to_send[_cnt++] = BYTE0(_a);
  48. data_to_send[_cnt++] = BYTE1(_a);
  49. data_to_send[_cnt++] = BYTE0(_b);
  50. data_to_send[_cnt++] = BYTE1(_b);
  51. data_to_send[_cnt++] = BYTE0(_c);
  52. data_to_send[_cnt++] = BYTE1(_c);
  53. data_to_send[_cnt++] = BYTE0(_d);
  54. data_to_send[_cnt++] = BYTE1(_d);
  55. for ( i = 0; i < data_to_send[3]+4; i++)
  56. {
  57. sumcheck += data_to_send[i];
  58. addcheck += sumcheck;
  59. }
  60. data_to_send[_cnt++] = sumcheck;
  61. data_to_send[_cnt++] = addcheck;
  62. HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
  63. }
  64. //通过F3帧发送2个int16类型和1个int32类型的数据
  65. void ANO_DT_Send_F3(int16_t _a, int16_t _b, int32_t _c ) //F3帧 2个 int16 参数
  66. 1个 int32 参数
  67. {
  68. uint8_t _cnt = 0;
  69. uint8_t sumcheck = 0; //和校验
  70. uint8_t addcheck = 0; //附加和校验
  71. uint8_t i=0;
  72. data_to_send[_cnt++] = 0xAA;
  73. data_to_send[_cnt++] = 0xFF;
  74. data_to_send[_cnt++] = 0xF3;
  75. data_to_send[_cnt++] = 8; //数据长度
  76. //单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
  77. data_to_send[_cnt++] = BYTE0(_a);
  78. data_to_send[_cnt++] = BYTE1(_a);
  79. data_to_send[_cnt++] = BYTE0(_b);
  80. data_to_send[_cnt++] = BYTE1(_b);
  81. data_to_send[_cnt++] = BYTE0(_c);
  82. data_to_send[_cnt++] = BYTE1(_c);
  83. data_to_send[_cnt++] = BYTE2(_c);
  84. data_to_send[_cnt++] = BYTE3(_c);
  85. for ( i = 0; i < data_to_send[3]+4; i++)
  86. {
  87. sumcheck += data_to_send[i];
  88. addcheck += sumcheck;
  89. }
  90. data_to_send[_cnt++] = sumcheck;
  91. data_to_send[_cnt++] = addcheck;
  92. HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
  93. }
  1. #define BYTE0(dwTemp) (*(char *)(&dwTemp))
  2. #define BYTE1(dwTemp) (*((char *)(&dwTemp) + 1))
  3. #define BYTE2(dwTemp) (*((char *)(&dwTemp) + 2))
  4. #define BYTE3(dwTemp) (*((char *)(&dwTemp) + 3))

8.PID

  1. #include "pid.h"
  2. //定义一个结构体类型变量
  3. tPid pidMotor1Speed;
  4. //给结构体类型变量赋初值
  5. void PID_init()
  6. {
  7. pidMotor1Speed.actual_val=0.0;
  8. pidMotor1Speed.target_val=0.00;
  9. pidMotor1Speed.err=0.0;
  10. pidMotor1Speed.err_last=0.0;
  11. pidMotor1Speed.err_sum=0.0;
  12. pidMotor1Speed.Kp=0;
  13. pidMotor1Speed.Ki=0;
  14. pidMotor1Speed.Kd=0;
  15. }
  16. //比例p调节控制函数
  17. float P_realize(tPid * pid,float actual_val)
  18. {
  19. pid->actual_val = actual_val;//传递真实值
  20. pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
  21. //比例控制调节 输出=Kp*当前误差
  22. pid->actual_val = pid->Kp*pid->err;
  23. return pid->actual_val;
  24. }
  25. //比例P 积分I 控制函数
  26. float PI_realize(tPid * pid,float actual_val)
  27. {
  28. pid->actual_val = actual_val;//传递真实值
  29. pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
  30. pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
  31. //使用PI控制 输出=Kp*当前误差+Ki*误差累计值
  32. pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum;
  33. return pid->actual_val;
  34. }
  35. // PID控制函数
  36. float PID_realize(tPid * pid,float actual_val)
  37. {
  38. pid->actual_val = actual_val;//传递真实值
  39. pid->err = pid->target_val - pid->actual_val;当前误差=目标值-真实值
  40. pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
  41. //使用PID控制 输出 = Kp*当前误差 + Ki*误差累计值 + Kd*(当前误差-上次误差)
  42. pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid-
  43. >err - pid->err_last);
  44. //保存上次误差: 这次误差赋值给上次误差
  45. pid->err_last = pid->err;
  46. return pid->actual_val;
  47. }

PID的精髓就是这句代码:

PID输出 = Kp*当前误差 + Ki*误差累计值 + Kd*(当前误差-上次误差)

  1. pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid-
  2. >err - pid->err_last);

PID本身可以理解成一个封装好的函数,输入的两个参数,一个是理想值(自己设置),一个是实际值,比如速度,我设置一个理想值,那么当前速度如何输入进去?

用到上一节的编码器测速:实时检测当前小车速度,然后输入进PID,PID输出一个速度实时调整小车的速度,使其尽可能稳定在设置的理想值速度

9.使用cjson:PID调参:确定Kp、Ki、Kd

首先软件开启USART1,主函数开启接收中断:

__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE); //开启串口1接收中断

在USART1的串口中断回调函数中:

  1. uint8_t Usart1_ReadBuf[256]; //串口1 缓冲数组
  2. uint8_t Usart1_ReadCount = 0; //串口1 接收字节计数
  3. void USART1_IRQHandler(void)
  4. {
  5. if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE))//判断huart1 是否读到字节
  6. {
  7. if(Usart1_ReadCount >= 255) Usart1_ReadCount = 0;
  8. HAL_UART_Receive(&huart1,&Usart1_ReadBuf[Usart1_ReadCount++],1,1000);
  9. }
  10. HAL_UART_IRQHandler(&huart1);
  11. }

再写一个函数判断是否接收完一帧数据:

  1. extern uint8_t Usart1_ReadBuf[255]; //串口1 缓冲数组
  2. extern uint8_t Usart1_ReadCount; //串口1 接收字节计数
  3. //判断否接收完一帧数据
  4. uint8_t Usart_WaitReasFinish(void)
  5. {
  6. static uint16_t Usart_LastReadCount = 0;//记录上次的计数值
  7. if(Usart1_ReadCount == 0)
  8. {
  9. Usart_LastReadCount = 0;
  10. return 1;//表示没有在接收数据
  11. }
  12. if(Usart1_ReadCount == Usart_LastReadCount)//如果这次计数值等于上次计数值
  13. {
  14. Usart1_ReadCount = 0;
  15. Usart_LastReadCount = 0;
  16. return 0;//已经接收完成了
  17. }
  18. Usart_LastReadCount = Usart1_ReadCount;
  19. return 2;//表示正在接受中
  20. }

CJSON库加进去:

  1. #include "cJSON.h"
  2. #include <string.h>
  3. cJSON *cJsonData ,*cJsonVlaue;
  4. if(Usart_WaitReasFinish() == 0)//是否接收完毕
  5. {
  6. cJsonData = cJSON_Parse((const char *)Usart1_ReadBuf);
  7. if(cJSON_GetObjectItem(cJsonData,"p") !=NULL)
  8. {
  9. cJsonVlaue = cJSON_GetObjectItem(cJsonData,"p");
  10. p = cJsonVlaue->valuedouble;
  11. pidMotor1Speed.Kp = p;
  12. }
  13. if(cJSON_GetObjectItem(cJsonData,"i") !=NULL)
  14. {
  15. cJsonVlaue = cJSON_GetObjectItem(cJsonData,"i");
  16. i = cJsonVlaue->valuedouble;
  17. pidMotor1Speed.Ki = i;
  18. }
  19. if(cJSON_GetObjectItem(cJsonData,"d") !=NULL)
  20. {
  21. cJsonVlaue = cJSON_GetObjectItem(cJsonData,"d");
  22. d = cJsonVlaue->valuedouble;
  23. pidMotor1Speed.Kd = d;
  24. }
  25. if(cJSON_GetObjectItem(cJsonData,"a") !=NULL)
  26. {
  27. cJsonVlaue = cJSON_GetObjectItem(cJsonData,"a");
  28. a = cJsonVlaue->valuedouble;
  29. pidMotor1Speed.target_val =a;
  30. }
  31. if(cJsonData != NULL){
  32. cJSON_Delete(cJsonData);//释放空间、但是不能删除cJsonVlaue不然会 出现异常错误
  33. }
  34. memset(Usart1_ReadBuf,0,255);//清空接收buf,注意这里不能使用strlen
  35. }
  36. printf("P:%.3f I:%.3f D:%.3f A:%.3f\r\n",p,i,d,a);

然后通过串口1在匿名上位机发送json格式的数据:

{“p”: 11,“i”:1,“d”:1,“a”:-4 }

PID调参方法:

1. 调节 P:先设置 I=0 D=0, 先给正值或负值值测试 P 正负、然后根据 PID 函数输入和输出估算 P 大小,然后 I=0 D=0去测试,调节一个较大值。
2. 调节 I : P 等于前面的值, 给I较大正值和负值,测试出 I 正负,然后 I 从小值调节,直到没有偏 差存在。
3. 一般系统不使用 D 观察PID波形调参。

10.PID整定

那么多久采样一次速度呢?同时PID整定也需要放在采样周期的中断里执行才更加合理对吧?

我们选择的是 20ms 一次采样,代码放在编码器测速的中断里面。

  1. if(TimerCount %10 ==0)//每20ms一次
  2. {
  3. Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),0);
  4. TimerCount=0;
  5. }

11.双电机的PID控制

两个电机,两个速度,两个不同的PID需要单独调参,我们封装一个函数:

  1. typedef struct
  2. {
  3. float target_val;//目标值
  4. float actual_val;//实际值
  5. float err;//当前偏差
  6. float err_last;//上次偏差
  7. float err_sum;//误差累计值
  8. float Kp,Ki,Kd;//比例,积分,微分系数
  9. } tPid;
  1. //定义一个结构体类型变量
  2. tPid pidMotor1Speed;
  3. tPid pidMotor2Speed;
  4. //给结构体类型变量赋初值
  5. void PID_init()
  6. {
  7. pidMotor1Speed.actual_val=0.0;
  8. pidMotor1Speed.target_val=3.00;
  9. pidMotor1Speed.err=0.0;
  10. pidMotor1Speed.err_last=0.0;
  11. pidMotor1Speed.err_sum=0.0;
  12. pidMotor1Speed.Kp=15;
  13. pidMotor1Speed.Ki=5;
  14. pidMotor1Speed.Kd=0;
  15. pidMotor2Speed.actual_val=0.0;
  16. pidMotor2Speed.target_val=3.00;
  17. pidMotor2Speed.err=0.0;
  18. pidMotor2Speed.err_last=0.0;
  19. pidMotor2Speed.err_sum=0.0;
  20. pidMotor2Speed.Kp=15;
  21. pidMotor2Speed.Ki=5;
  22. pidMotor2Speed.Kd=0;
  23. }
  24. //PID控制函数
  25. float PID_realize(tPid * pid,float actual_val)
  26. {
  27. pid->actual_val = actual_val;//传递真实值
  28. pid->err = pid->target_val - pid->actual_val;当前误差=目标值-真实值
  29. pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
  30. //使用PID控制 输出 = Kp*当前误差 + Ki*误差累计值 + Kd*(当前误差-上次误差)
  31. pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid->err - pid->err_last);
  32. //保存上次误差: 这次误差赋值给上次误差
  33. pid->err_last = pid->err;
  34. return pid->actual_val;
  35. }

12.小车前后左右停

封装一个函数:

  1. /*******************
  2. * @brief 通过PID控制电机转速
  3. * @param Motor1Speed:电机1 目标速度、Motor2Speed:电机2 目标速度
  4. * @return 无
  5. *
  6. *******************/
  7. void motorPidSetSpeed(float Motor1SetSpeed,float Motor2SetSpeed)
  8. {
  9. //改变电机PID参数的目标速度
  10. pidMotor1Speed.target_val = Motor1SetSpeed;
  11. pidMotor2Speed.target_val = Motor2SetSpeed;
  12. //根据PID计算 输出作用于电机
  13. Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),PID_realize(&pidMotor2Speed,M
  14. otor2Speed));
  15. }
motorPidSetSpeed(1,2);       // 向右转弯
motorPidSetSpeed(2,1);       // 向左转弯
motorPidSetSpeed(1,1);       // 前进
motorPidSetSpeed(-1,-1);     // 后退
motorPidSetSpeed(0,0);       // 停止
motorPidSetSpeed(-1,1);      // 右原地旋转
motorPidSetSpeed(1,-1);      // 左原地旋转

加速减速函数:

  1. //向前加速函数
  2. void motorSpeedUp(void)
  3. {
  4. static float MotorSetSpeedUp=0.5;//静态变量 函数结束 变量不会销毁
  5. if(MotorSetSpeedUp <= MAX_SPEED_UP) MotorSetSpeedUp +=0.5 ; //如果没有超过最大
  6. 值就增加0.5
  7. motorPidSetSpeed(MotorSetSpeedUp,MotorSetSpeedUp);//设置到电机
  8. }
  9. //向前减速函数
  10. void motorSpeedCut(void)
  11. {
  12. static float MotorSetSpeedCut=3;//静态变量 函数结束 变量不会销毁
  13. if(MotorSetSpeedCut >=0.5) MotorSetSpeedCut-=0.5;//判断是否速度太小
  14. motorPidSetSpeed(MotorSetSpeedCut,MotorSetSpeedCut);//设置到电机
  15. }

13.ADC采集电源电压

根据电阻分压:ADC 点的电压是VBAT_IN 的五分之一
软件中进行配置后:
  1. float adcGetBatteryVoltage(void)
  2. {
  3. HAL_ADC_Start(&hadc2);//启动ADC转化
  4. if(HAL_OK == HAL_ADC_PollForConversion(&hadc2,50))//等待转化完成、超时时间50ms
  5. return (float)HAL_ADC_GetValue(&hadc2)/4096*3.3*5;//计算电池电压
  6. return -1;
  7. }

14.寻迹

【问】如何通过4个TCRT5000红外对管传感器,使小车沿着地面的黑线运动?

红外对管的特性就是,如果检测到黑线,DO引脚高电平,小灯会灭。

首先通过软件初始化四个引脚,宏定义为读取四个引脚电平

  1. #define READ_HW_OUT_1 HAL_GPIO_ReadPin(HW_OUT_1_GPIO_Port,HW_OUT_1_Pin)
  2. #define READ_HW_OUT_2 HAL_GPIO_ReadPin(HW_OUT_2_GPIO_Port,HW_OUT_2_Pin)
  3. #define READ_HW_OUT_3 HAL_GPIO_ReadPin(HW_OUT_3_GPIO_Port,HW_OUT_3_Pin)
  4. #define READ_HW_OUT_4 HAL_GPIO_ReadPin(HW_OUT_4_GPIO_Port,HW_OUT_4_Pin)

 根据红外对管状态改变两个电机状态使其沿黑线前进的逻辑:

自己可以画张图,逻辑会非常明了。

  1. if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4
  2. == 0 )
  3. {
  4. printf("应该前进\r\n");
  5. motorPidSetSpeed(1,1);//前运动
  6. }
  7. if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 1&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4
  8. == 0 )
  9. {
  10. printf("应该右转\r\n");
  11. motorPidSetSpeed(0.5,2);//右边运动
  12. }
  13. if(READ_HW_OUT_1 == 1&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4
  14. == 0 )
  15. {
  16. printf("快速右转\r\n");
  17. motorPidSetSpeed(0.5,2.5);//快速右转
  18. }
  19. if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 1&&READ_HW_OUT_4
  20. == 0 )
  21. {
  22. printf("应该左转\r\n");
  23. motorPidSetSpeed(2,0.5);//左边运动
  24. }
  25. if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4
  26. == 1 )
  27. {
  28. printf("快速左转\r\n");
  29. motorPidSetSpeed(2.5,0.5);//快速左转
  30. }

加PID:

  1. g_ucaHW_Read[0] = READ_HW_OUT_1;//读取红外对管状态、这样相比于写在if里面更高效
  2. g_ucaHW_Read[1] = READ_HW_OUT_2;
  3. g_ucaHW_Read[2] = READ_HW_OUT_3;
  4. g_ucaHW_Read[3] = READ_HW_OUT_4;
  5. if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==
  6. 0&&g_ucaHW_Read[3] == 0 )
  7. {
  8. // printf("应该前进\r\n");//注释掉更加高效,减少无必要程序执行
  9. g_cThisState = 0;//前进
  10. }
  11. else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 1&&g_ucaHW_Read[2] ==
  12. 0&&g_ucaHW_Read[3] == 0 )//使用else if更加合理高效
  13. {
  14. // printf("应该右转\r\n");
  15. g_cThisState = -1;//应该右转
  16. }
  17. else if(g_ucaHW_Read[0] == 1&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==
  18. 0&&g_ucaHW_Read[3] == 0 )
  19. {
  20. // printf("快速右转\r\n");
  21. g_cThisState = -2;//快速右转
  22. }
  23. else if(g_ucaHW_Read[0] == 1&&g_ucaHW_Read[1] == 1&&g_ucaHW_Read[2] ==
  24. 0&&g_ucaHW_Read[3] == 0)
  25. {
  26. // printf("快速右转\r\n");
  27. g_cThisState = -3;//快速右转
  28. }
  29. else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==
  30. 1&&g_ucaHW_Read[3] == 0 )
  31. {
  32. // printf("应该左转\r\n");
  33. g_cThisState = 1;//应该左转
  34. }
  35. else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==
  36. 0&&g_ucaHW_Read[3] == 1 )
  37. {
  38. // printf("快速左转\r\n");
  39. g_cThisState = 2;//快速左转
  40. }
  41. else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==
  42. 1&&g_ucaHW_Read[3] == 1)
  43. {
  44. // printf("快速左转\r\n");
  45. g_cThisState = 3;//快速左转
  46. }
  47. g_fHW_PID_Out = PID_realize(&pidHW_Tracking,g_cThisState);//PID计算输出目标速度
  48. 这个速度,会和基础速度加减
  49. g_fHW_PID_Out1 = 3 + g_fHW_PID_Out;//电机1速度=基础速度+循迹PID输出速度
  50. g_fHW_PID_Out2 = 3 - g_fHW_PID_Out;//电机1速度=基础速度-循迹PID输出速度
  51. if(g_fHW_PID_Out1 >5) g_fHW_PID_Out1 =5;//进行限幅 限幅速度在0-5之间
  52. if(g_fHW_PID_Out1 <0) g_fHW_PID_Out1 =0;
  53. if(g_fHW_PID_Out2 >5) g_fHW_PID_Out2 =5;
  54. if(g_fHW_PID_Out2 <0) g_fHW_PID_Out2 =0;
  55. if(g_cThisState != g_cLastState)
  56. //如何这次状态不等于上次状态、就进行改变目标速度和控制电机、在定时器中依旧定时控制电机
  57. {
  58. motorPidSetSpeed(g_fHW_PID_Out1,g_fHW_PID_Out2);//通过计算的速度控制电机
  59. }
输入的是需要调整的速度,这个速度根据自己的小车和自己想要的效果可以进行调整。
实时监测红外对管的状态,实时输出需要调整的速度值,然后维持沿着黑线寻迹的状态。

15.通过串口控制小车

同样首先通过软件初始化串口3。

打开串口三的全局中断。
  1. uint8_t g_ucUsart3ReceiveData; //保存串口三接收的数据
  2. HAL_UART_Receive_IT(&huart3,&g_ucUsart3ReceiveData,1); //串口三接收数据

找到串口3的中断处理函数,根据自己喜好进行编写:

  1. extern uint8_t g_ucUsart3ReceiveData; //保存串口三接收的数据
  2. //串口接收回调函数
  3. void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
  4. {
  5. if( huart == &huart3)//判断中断源
  6. {
  7. if(g_ucUsart3ReceiveData == 'A') motorPidSetSpeed(1,1);//前运动
  8. if(g_ucUsart3ReceiveData == 'B') motorPidSetSpeed(-1,-1);//后运动
  9. if(g_ucUsart3ReceiveData == 'C') motorPidSetSpeed(0,0);//停止
  10. if(g_ucUsart3ReceiveData == 'D') motorPidSetSpeed(1,2);//右边运动
  11. if(g_ucUsart3ReceiveData == 'E') motorPidSetSpeed(2,1);//左边运动
  12. if(g_ucUsart3ReceiveData == 'F') motorPidSpeedUp();//加速
  13. if(g_ucUsart3ReceiveData == 'G') motorPidSpeedCut();//减速
  14. HAL_UART_Receive_IT( &huart3, &g_ucUsart3ReceiveData, 1);//继续进行中断接收
  15. }
  16. }

16.HC-SR04超声波避障

参考这篇:

超声波测距实验icon-default.png?t=N7T8https://blog.csdn.net/Xiaoxuexxxxx/article/details/137917873?spm=1001.2014.3001.5501避障的逻辑代码:

  1. //避障逻辑
  2. if(HC_SR04_Read() > 25)//前方无障碍物
  3. {
  4. motorPidSetSpeed(1,1);//前运动
  5. HAL_Delay(100);
  6. }
  7. else{ //前方有障碍物
  8. motorPidSetSpeed(-1,1);//右边运动 原地
  9. HAL_Delay(500);
  10. if(HC_SR04_Read() > 25)//右边无障碍物
  11. {
  12. motorPidSetSpeed(1,1);//前运动
  13. HAL_Delay(100);
  14. }
  15. else{//右边有障碍物
  16. motorPidSetSpeed(1,-1);//左边运动 原地
  17. HAL_Delay(1000);
  18. if(HC_SR04_Read() >25)//左边无障碍物
  19. {
  20. motorPidSetSpeed(1,1);//前运动
  21. HAL_Delay(100);
  22. }
  23. else{
  24. motorPidSetSpeed(-1,-1);//后运动
  25. HAL_Delay(1000);
  26. motorPidSetSpeed(-1,1);//右边运动
  27. HAL_Delay(50);
  28. }
  29. }
  30. }

17.超声波跟随

如果不加PID,那么如下代码就是跟随的朴实无华的逻辑:

  1. //超声波跟随
  2. if(HC_SR04_Read() > 25)
  3. {
  4. motorForward();//前进
  5. HAL_Delay(100);
  6. }
  7. if(HC_SR04_Read() < 20)
  8. {
  9. motorBackward();//后退
  10. HAL_Delay(100);
  11. }

显然不行,那么让我们加上PID

首先再定义一个用于跟随的PID结构体:

  1. tPid pidFollow; //定距离跟随PID
  2. pidFollow.actual_val=0.0;
  3. pidFollow.target_val=22.50;//定距离跟随 目标距离22.5cm
  4. pidFollow.err=0.0;
  5. pidFollow.err_last=0.0;
  6. pidFollow.err_sum=0.0;
  7. pidFollow.Kp=-0.5;//定距离跟随的Kp大小通过估算PID输入输出数据,确定大概大小,然后在调试
  8. pidFollow.Ki=-0.001;//Ki小一些
  9. pidFollow.Kd=0;

PID跟随逻辑:

输入检测的小车和前方物体的距离,实时输出两个电机的速度,实现稳定跟随。

  1. //**********PID跟随功能***********//
  2. g_fHC_SR04_Read=HC_SR04_Read();//读取前方障碍物距离
  3. //如果前60cm 有东西就启动跟随
  4. if(g_fHC_SR04_Read < 60){
  5. //PID计算输出目标速度 这个速度,会和基础速度加减
  6. g_fFollow_PID_Out = PID_realize(&pidFollow,g_fHC_SR04_Read);
  7. //对输出速度限幅
  8. if(g_fFollow_PID_Out > 6) g_fFollow_PID_Out = 6;
  9. if(g_fFollow_PID_Out < -6) g_fFollow_PID_Out = -6;
  10. motorPidSetSpeed(g_fFollow_PID_Out,g_fFollow_PID_Out);//速度作用与电机上
  11. }
  12. else motorPidSetSpeed(0,0);//如果前面60cm 没有东西就停止
  13. HAL_Delay(10);//读取超声波传感器不能过快

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

闽ICP备14008679号