赛后总结电磁循迹小车
MCU选用:STM32F103C8T6
编程语言:C语言
开发工具:MDK Keil,CubeMX
所用开发库:HAL库
适用读者: 初步接触STM32.嵌入式入门读者没有制作智能车的实践经验.
文章目录
- 赛后总结电磁循迹小车
-
- 一.小车架构
-
- 1.硬件部分
- 2.软件部分
- 3.控制逻辑
- 二.关键模块说明
- 三.CubeMX配置
-
- 1.引脚预览
- 2.ADC
- 3.TIM
- 4.I2C
- 5.USART
- 6.NVIC
- 四.编写汽车代码
-
- 1.构建基础框架
- 2.滤波算法
- 3.归一化算法
- 4.PID控制算法
- 5.赛道特征值响应函数
- 6.如何方便调参
- 7.其他代码
- 五.总结
-
-
- 1.开车前不要着急,先理清思路,确定计划,开始。
- 2.最复杂的算法不是最复杂的算法,只有最合适的算法才是最好的。
- 3.调参时,不要随机调参,先搞清楚每个参数的作用,逐步调参。
-
一.小车架构
1.硬件部分
在硬件部分,我只列出了简单电磁车最基本的部分,蓝色字体是我们选择的部分。
在上面列出的汽车底板、控制板和电磁杆中,我的团队成员使用控制板和电磁杆EDA软件画板,开板再焊接,其他模块都是通过购买获得的。
2.软件部分
3.控制逻辑
二.关键模块说明
1.电磁杆(电磁传感器)
以下电磁传感器摘自清华大学出版社出版社出版的《智能汽车生产-从元器件、机电系统、控制算法到完整的智能汽车设计》。如有侵权行为,请联系我删除。
Ⅰ.电感传感器的原理
根据电磁学的相关知识,我们知道导线中通人变化的电流(如止弦规律变化的电流).电线周围的磁场会发生变化,磁场和电流的变化规律是一致的。如果在这个磁场中放置一个电感,电感会产生感应电势,感应电势的大小与通过线圈电路的磁通量成正比。磁感应强度的大小和方向因导线周围不同的位置而不同。因此,可以确定电感的大致位置。
Ⅱ.磁传感器信号处理电路
确定使用电感作为检测导线的传感器,但其感应信号较弱,混有杂波,因此需要进行信号处理。为了获得理想的信号:信号过滤、信号放大和信号检测,需要进行以下三个步骤。
1)信号滤波
比赛选择20kHz的交变电磁场作为路径导航信号,在频谱上可以有效地避开周围其他磁场的干扰,因此信号放大需要进行选频放大,使得20kHz其他干扰信号的影响可以有效扰信号的影响。LC串联谐振电路实现选频电路(带通电路),具体电路如下图所示。
LC谐振电路
其中, E E E感应线圈中的感应电势, L L L是感应线圈的电感值, R 0 R_{0} R0主要是电感内阻, C C C是谐振电容。电路谐振频率为: f = 1 2 π L C f =\frac{1}{2π\sqrt{LC}} f=2πLC 1 已知感应电动势的频率 f = 20 k H z f=20kHz f=20kHz,感应线圈电感为 L = 10 m H L=10mH L=10mH,可以计算出谐振电容的容量为 C = 6.33 × 1 0 − 9 F C=6.33×10^{-9}F C=6.33×10−9F。通常在市场上可以购买到的标称电容与上述容值最为接近的电容为 6.8 n F 6.8nF 6.8nF,所以在实际电路中选用 6.8 n F 6.8nF 6.8nF的电容作为谐振电容。
2)信号的放大
第一步处理后的电压波形已经是较为规整的20kHz正弦波,但是幅值较小,随着距离衰减很快,不利于电压采样,所以要进行放大,官方给出了如下图所示的参考方案,即用三极管进行放大,但是用三极管放大有一个不可避免的缺点就是温漂较大,而且在实际应用中静电现象严重。
共射三极管放大电路
因此我们放弃三级管放大的方案,而是采用集成运放进行信号的放大处理,集成运放较三极管优势是准确、受温度影响很小、可靠性高。集成运放放大电路可构成同相比例运算电路和反相比例运算电路,在实际中使用反相比例运算电路。由于运放使用单电源供电,因此在同相端加 V c c / 2 V_{cc}/2 Vcc/2(典型值)的基准电位,基准电位由两个阻值相等的电阻分压得到。
3)信号的检波
测量放大后的感应电动势的幅值 E E E 可以有多种方法。最简单的方法是使用二极管检 波电路将交变的电压信号检波形成直流信号,然后再通过单片机的AD采集获得正比于感 应电压幅值的数值。
我们采用的是竞赛组委会给出的第一种方案,即使用两个二极管进行倍压检波。倍压检波电路可以获得正比于交流电压信号峰-峰值的直流信号。为了能够获得更大的动态范围,倍压检波电路中的二极管推荐使用肖特基二极管或者锗二极管。由于这类二极管的开启电压一般在0.1~0.3V,小于普通的硅二极管(0.5~0.7V),可以增加输出信号的动态范 围和增加整体电路的灵敏度。这里选用常见的肖特基二极管1N5817。
最终确定下来的电路方案如下图所示:
最终方案电路
Ⅲ.磁传感器的布局原理及改进
对于直导线,当装有小车的中轴线对称的两个线圈的小车沿其直线行驶,即两个线圈的位置关于导线对称时,则两个线圈中感应出来的电动势大小应相同且方向亦相同。若小车偏离直导线,即两个线圈关于导线不对称时,则通过两个线圈的磁通量是不一样的。这时,距离导线较近的线圈中感应出的电动势应大于距离导线较远的那个线圈中的。根据这两个不对称的信号的差值,即可调整小车的方向,引导其沿直线行驶。
对于弧形导线,即路径的转弯处,由于弧线两侧的磁力线密度不同,则当载有线圈的小车行驶至此处时,两边的线圈感应出的电动势是不同的。具体的情况是,弧线内侧线圈的感应电动势大于弧线外侧线圈的,据此信号可以引导小车拐弯。
另外,当小车驶离导线偏远致使两个线圈处于导线的一侧时,两个线圈中感应电动势也是不平衡的。距离导线较近的线圈中感应出的电动势大于距离导线较远的线圈。由此,可以引导小车重新回到导线上。
由于磁感线的闭合性和方向性,通过两线圈的磁通量的变化方向具有一致性,即产生的感应电动势方向相同,所以由以上分析,比较两个线圈中产生的感应电动势大小即可判断小车相对于导线的位置,进而做出调整,引导小车大致循线行驶。
采用双水平线圈检测方案,在边缘情况下,其单调性发生变化,这样就存在一个定位不清的区域(如下图箭头所指)。同一个差值,会对应多个位置,不利于定位。另外,受单个线圈感应电动势的最大距离限制,两个线圈的检测广度很有限。
双线圈差值法有定位不清区域
现提出一种优化方案,5个垂直放置的电感按一字排布,每个电感相距约为5cm(见下图),这样覆盖赛道范围约为25cm。三个一字排布的电感可以大大提高检测密度和广度,向前有两个电感,可以提高前瞻,改善小车入弯状态和路径,两个45°的电感,可以改善入弯和出弯的姿态。
电感排布检测方案
Ⅳ.谐振电路的改进
按照组委会推荐的10mH+6.8nF组成的谐振电路,其谐振的峰值频率为19.6kHz,并不是信号发生器的20kHz。当谐振频率与外界激励频率不匹配时,将不会输出最大的谐振电压。另外,一般电感和电容均有±20%的误差,谐振频率将会随机分布在16kHz~24kHz之间,这会对传感器的对称性造成极大的影响。因此需要对组成谐振电路的电感电容进行匹配,使得其谐振频率恰好为20kHz,同时挑选电感电容对,使得对称位置的输出电压一致。
电磁杆的具体制作可以参考我队友的博客:
- 智能车:这是你要找的电磁杆吗?_悟黎678的博客-CSDN博客
2.L298N电机驱动模块
L298N模块的介绍可以参考以下博主的博客
- 【STM32小案例 04 】STM32简单使用L298N电机驱动模块 控制直流电机正反转_我也不知道取什么好的博客-CSDN博客_stm32电机控制
- stm32单片机+驱动L298N控制直流电机调速_薄情书生的博客-CSDN博客_l298n控制电机转速
- L298N电机驱动模块详解_魏波-的博客-CSDN博客_l298n电机驱动模块
- 基于L298N的STM32的直流电机PWM调速控制_Hu.先森的博客-CSDN博客_l298n电机驱动模块pwm调速
3.HC-05蓝牙模块
相关资料可以参考以下博主的博客:
- STM32控制HC-05蓝牙模块进行通信_Zach_z的博客-CSDN博客_hc05 stm32
- 【常用模块】HC-05蓝牙串口通信模块使用详解(实例:手机蓝牙控制STM32单片机)_Yngz_Miao的博客-CSDN博客_stm32蓝牙串口通信
4.直流减速电机
可以参考以下博主的博客:
- 直流电机的原理及驱动_star-air的博客-CSDN博客_直流电机驱动
- stm32、直流减速电机(接线、编码器代码详解)_wzyannn的博客-CSDN博客_stm32与编码器接线
5.模拟舵机
可以参考以下博主的博客:
- 单片机——SG90舵机工作原理_掏一淘哆啦A梦的奇妙口袋的博客-CSDN博客_sg90舵机原理图
- stm32之MG995舵机+原理+程序+详解_-electronic-engineer的博客-CSDN博客_mg995舵机
- STM32控制舵机讲解,从入门到放弃。_KING_阿飞的博客-CSDN博客_stm32控制舵机
6.干簧管
相关原理可以参考如下链接:
- 什么是干簧管(一)|什么是磁簧开关|干簧管原理视频|干簧管工作原理-产品知识-资讯-深圳华壬电子 (chinahuaren.com)
7.OLED屏幕
OLED的相关使用方法可参考以下博主的博客:
- STM32复习笔记(九)OLED的介绍和使用方法_Sumjess的博客-CSDN博客_oledshowstring函数什么意思
- STM32——硬件IIC驱动OLED屏幕显示_ZCY(Yinyuer1)的博客-CSDN博客_oled屏幕
8.Wifi模块-ESP8266
相关使用方法可参考以下博主的博客:
- esp8266介绍和使用_世界著名CV工程师的博客-CSDN博客_esp8266介绍
- 烂大街的ESP8266该怎么玩! - 知乎 (zhihu.com)
三.CubeMX配置
CubeMX如何配置的总体介绍可以参考以下博主的博客:
- cubemx代码生成详解 - 知乎 (zhihu.com)
1.引脚预览
2.ADC
ADC我们配置了6路通道,ADC1五路,ADC2一路。最初方案我们配置了七个通道,ADC1五路,ADC2两路,我们最初也只是使用了五个电感,所以只用了ADC1的五路通道,ADC2多出来的两路当时是备用。但是在调车过程中我们发现原先中间的电感旋转角度无法兼顾环岛与Y叉标志值的检测(可能是我们的设计存在问题),因此我们在中间多加了一路电感和原先的中路电感垂直,用于标志值的检测,并关闭了ADC2的一路通道方便代码编写。
ADC1:
ADC2:
ADC的相关配置可参考以下博主的博客:
- STM32 ADC详细篇(基于HAL库) - 东小东 - 博客园 (cnblogs.com)
3.TIM
TIM1用于输出控制电机两轮转速的PWM波
TIM2用于输出控制舵机的PWM波
TIM3中断用于控制算法的执行
TIM4中断用于一些需要计时函数的执行
TIM的相关配置可以参考以下博主的博客:
- STM32CubeMX之定时器TIM_while(1)的博客-CSDN博客_stm32cubemx tim
- STM32对HAL库的PWM控制 - 无乐不作丶 - 博客园 (cnblogs.com)
4.I2C
5.USART
我们配置了两个USART,比赛中只用到一个USART1
6.NVIC
四.小车代码编写
1.基础框架的搭建
在写代码之前可以像我上面一样(所用软件:uTools插件-知犀思维导图|uTools官网下载:uTools官网 - 新一代效率工具平台).然后:
/*---------------------- 相关结构体定义 -------------------------- */
/** *@name MotorDriver *@type struct array *@about 电机控制 *@param * - MotorDriver[0] 电机1 * - MotorDriver[1] 电机2 * -- * - Onoff 电机启动与停止标志位 * - pwmrate 传给Tim -> CRR的值,可改变PWM占空比 * - IN1 L298n IN1逻辑控制引脚 * - IN2 L298n IN2逻辑控制引脚 */
typedef struct{
_Bool OnOff;
_Bool IN1;
_Bool IN2;
float pwmrate;
}MotorDriver;
/** *@name MotDiff *@type struct *@about 电机差速控制 *@param * -- * - Param 增幅系数 * - pwmSwitch 电机差速跟随转向控制开关 */
struct{
_Bool pwmSwitch;
uint16_t basepwmvalue;
float Param;
}MotDiff;
/** *@name SteMotDriver *@type struct *@about 舵机控制 *@param * - pwmrateTemp pwm控制过渡值 * - pwmrateFnal pwm控制最终值 * - Min 舵机中值 * - angle 舵机目前角度 */
typedef struct{
uint8_t angle;
float pwmrateTemp;
float pwmrateFnal;
float Min;
}SteMotDriver;
/** *@name ADCData *@type struct *@about ADC数据 *@param * - origanlData[] ADC采集到的原始数据 * - filterData[] 滤波处理后的数据 * - IDUC_L 左电感 * - IDUC_R 右电感 * - IDUC_M 中电感 * - IDUC_LM 左中电感 * - IDUC_RM 右中电感 * - Error 差值 */
typedef struct{
__IO uint16_t orignalData[5];
__IO float filterData[5];
__IO float IDUC_L;
__IO float IDUC_R;
__IO float IDUC_M;
__IO float IDUC_LM;
__IO float IDUC_RM;
__IO float IDUC_Ex;
__IO float Error;
}ADCData;
/** *@name PoorCmpAnd *@type struct *@about 差比和加权算法系数 *@param * -- * - paramA 控制系数A * - paramB 控制系数B * - paramC 控制系数C * - paramP 比例系数P */
typedef struct{
uint8_t flag; //控制算法转换
float paramA;
float paramB;
float paramC;
float paramP;
float paramL;
}PoorCmpAnd;
/** *@name Switch *@type struct *@about 函数开关 *@param * - * - */
struct{
_Bool ONOF1; //环岛
_Bool ONOF2; //十字
_Bool ONOF3; //Y形
_Bool ONOF4; //标志值捕获
uint8_t ONOF5; //滤波
}Switch = {
1, 0, 1, 0, 1};
/** *@name Kalmam *@type struct *@about Kalman Filter *@param * - symStateNow 系统实时状态 X(k) * - symStatePostFore 系统上次预测状态 X(k|k-1) * - symStatePostBest 系统上次最优状态 X(k-1|k-1) * - covNow 本次系统状态协方差 P(k|k) * - covPostFore 上次预测状态协方差 P(k|k-1) * - covPostBest 上次最优状态协方差 P(k-1|k-1) * - symControl 系统控制量 U(k) * - symParmA 系统参数A A * - symParmB 系统参数B B * - errorMes k时刻测量值 Z(k) * - mesParm 测量系统的参数 H * - pcesNoise 过程噪声 W(k) * - mesNoise 测量噪声 V(k) * - transposeA A的转置矩阵 A' * - transposeQ W(k)的转置矩阵 Q * - transposeR V(k)的转置矩阵 R * - transposeH H的转置矩阵 H' * - gain 卡尔曼增益 Kg */
typedef struct{
__IO float symStateNow[5];
__IO float symStatePostFore[5];
__IO float symStatePostBest[5];
__IO float covNow[5];
__IO float covPostFore[5];
__IO float covPostBest[5];
float symControl;
float symParmA;
float symParmB;
__IO float errorMes;
float mesParm;
float pcesNoise;
float mesNoise;
float transposeA;
float transposeQ;
float transposeR;
float transposeH;
__IO float gain[5];
}Kalman;
/** *@name TyPID *@type struct *@about PID控制系数 *@param * -- * - Err 自变量 * - ErrLastValue[] 前几次Err值 * - ErrLastSum Err累加值 * - Proportion 比例 * - Integral 积分 * - Differential 微分 * - Integra_Max 积分限幅值 */
typedef struct{
int Proportion;
int Integral;
int Differential;
float Err;
float ErrLastValue[3];
float ErrLastSum;
float Integral_Max;
float k;
float b;
}TyPID;
/** *@breif 全局标志位 */
struct Flag{
uint16_t A; //环岛
uint16_t B; //环形
uint16_t C; //Y形
uint16_t D; //捕获sign
uint16_t G; //干簧管
uint16_t S; //出库与入库
uint16_t T; //出库
uint16_t K; //Y形消抖
uint16_t W; //入库消抖
}Flag = {
0, 0, 0, 0, 0, 0, 0, 0};
/** *@breif 中断读秒 */
struct ITReadTimes{
int Tim1; //环岛
int Tim2; //入库
int Tim3; //Y形
int Tim4;
int Tim5;
int Tim6;
int Tim7;
int Tim8;
}ITRT = {
270, 45, 100, 35, 30, 30, 30, 30};
/** *@breif 赛道特征判断值 */
struct JudgeValue{
uint16_t jm1; //环岛
uint16_t jm2; //环岛
uint16_t jm3; //十字
uint16_t jm4; //十字
uint16_t jm5; //十字
uint16_t jm6; //Y形
uint16_t jm7; //Y形
uint16_t jm8; //捕获
}JDVL = {
3800, 500, 2000, 100, 100, 10, 100, 2000};
/** *@funcname bsp_LED_FeedBack() *@brief LED程序测试闪烁反馈函数 */
void bsp_LED_FeedBack(void)
{
HAL_GPIO_TogglePin(GPIOC, LED1_Pin);
HAL_Delay(100);
HAL_GPIO_TogglePin(GPIOC, LED1_Pin);
}
/** *@funcname fputc() *@brief 串口输出重定向 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/** *@funcname bsp_Usart_Receive() *@brief 串口接收数据函数 */
void bsp_Usart_Receive(void)
{
if(recv_end_flag ==1)
{
bsp_Usart_Operate(rx_buffer);
for(uint8_t i=0;i<rx_len;i++)
{
rx_buffer[i]=0;
}
rx_len=0;
recv_end_flag=0;
}
HAL_UART_Receive_DMA(&huart1,rx_buffer,200);
}
STM32串口的应用可以参考该博主博客:
- STM32 HAL库之串口详细篇(基于HAL库) - 东小东 - 博客园 (cnblogs.com)
/** *@funcname bsp_OLED_Display() *@brief OLED屏显函数 *@count * -- * - PID数值 * - ADC原始采集值 * - Err */ void bsp_OLED_Display(void) { bsp_PID_Control(); bsp_SteMot_PwmSet(SteMot.pwmrateTemp); OLED_ShowString(0, 0, (uint8_t *)"P:", 12); OLED_ShowNum(15, 0, PID.Proportion, 3, 12); OLED_ShowString(40, 0, (uint8_t *)"I:", 12); OLED_ShowNum(55, 0, PID.Integral, 3, 12); OLED_ShowString(80, 0, (uint8_t *)"D:", 12); OLED_ShowNum(95, 0, PID.Differential, 3, 12); OLED_ShowString(0, 2, (uint8_t *)"ADC OrValue:", 12); OLED_ShowNum(0, 3, adcData.orignalData[0], 4, 12); OLED_ShowNum(50, 3, adcData.orignalData[1], 4, 12); OLED_ShowNum(100, 3, adcData.orignalData[2], 4, 12); OLED_ShowNum(0, 4, adcData.orignalData[3], 4, 12); OLED_ShowNum(50, 4, adcData.orignalData[4], 4, 12); OLED_ShowNum(100, 4, adcData.IDUC_Ex, 4, 12); OLED_ShowString(0, 5, (uint8_t *)"PwmRate:", 12); OLED_ShowUnFloat(60, 5, SteMot.pwmrateFnal, 7, 2, 12); OLED_ShowString(0, 6, (uint8_t *)"A:", 12); OLED_ShowNum(30, 6, Flag.A, 4, 12); OLED_ShowString(60, 6, (uint8_t *)"B:", 12); OLED_ShowNum(100, 6, Flag.B, 4, 12); OLED_ShowString(0, 7, (uint8_t *)"C:", 12); OLED_ShowNum(30, 7, Flag.C, 4, 12); OLED_ShowString(60, 7, (uint8_t *)"D:", 12); OLED_ShowNum(100, 7, Flag 标签:
60磁电式转速传感器680nj400电容磁电式转速传感器的功能特点9磁电式转速传感器用于电磁传感器的电感c1008y三极管