Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
2024-RSOC
新手学习
星火1号_spark_星火一号_开发板
【2024-RSOC】夏令营成果基于星火一号的电机控制驱动和巡线小车
发布于 2024-08-10 00:20:26 浏览:258
订阅该版
[tocm] # 【2024-RSOC】从零开始制作基于星火一号的电机控制驱动 # **【2024-RSOC】** 基于RT-Thread官方开发板星火一号,参考RT-Thread入门pdf资料,和夏令营老师的讲解,编写而成。本内容面向小白,因为写这个的作者也是小白,跟着夏令营刚开始学rtt,欸嘿(笑)。 ![0.png](https://oss-club.rt-thread.org/uploads/20240810/f54250dee4f4760af66a43dbc51e0813.png.webp) ![1.png](https://oss-club.rt-thread.org/uploads/20240810/92a06a6b42ebe42c36b759d7a08b1017.png.webp) [在线文档♥点♥我♥看♥](hhttps://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/filesystem/filesystem) 如果初次使用spark的bsp软件包,未构建vscode文件,则参考我前日发布[【2024-RSOC】夏令营Day2:初识rt-thread及多线程简单试用](https://club.rt-thread.org/ask/article/9da7f2983ce3264f.html)中的`初次搞机(干通官方bsp源码)`小节,确保工程文件能正常打开烧录例程,再往下进行。 ## 项目简介 ## 写一个多电机驱动,想挂几个挂几个的那种。只要不在意处理延时,只要你通道数量够,你想挂几个挂几个,然后整个smartcar文件夹,挪到别的工程只需改头文件预设值,照样能用。 Q:为啥不玩rtt的高端组件和应用嘞? A:俺是村里干硬件画板子焊板子写驱动出身滴,比较高端的组合功能有俺们编程部顶着哩! 然后用某宝最垃圾的车架和驱动器,做一个能够巡线的小车(悲),主打一个该省省该花花。 ## 硬件选择 ## ![2.png](https://oss-club.rt-thread.org/uploads/20240810/3ad17daa27f197adaf15f7322469cf29.png.webp) 学RTT,我选星火一号(笑) 做一辆能实现基本功能的巡线小车,车架和光感,怎么便宜怎么来,看我截图,就是这个价格,他们还包邮我哭死。**如果你没有连接用的铜柱和螺丝还有杜邦线,请一定自行找链接购买** ![3.png](https://oss-club.rt-thread.org/uploads/20240810/e7d3af40c687095af81d29987226bb11.png.webp) ![4.png](https://oss-club.rt-thread.org/uploads/20240810/031659fa4d9c98a9e3e418701c1501e8.png.webp) ![5.png](https://oss-club.rt-thread.org/uploads/20240810/7dcf90b8ee98bce7ec01e03c068a6543.png.webp) 到手后组装车架,效果如图 ![6.jpg](https://oss-club.rt-thread.org/uploads/20240810/6242a7d190a14309355490276ac98d71.jpg.webp) 车架搞好了,那就可以开始设计驱动了 ## 驱动设计阶段 ## LN298N是一个双电机驱动器。关于这个模块,我们有几种用法。我们首先看下这个模块的设计 ![298N.png](https://oss-club.rt-thread.org/uploads/20240810/432bef90ca91b1da7d351c8165ef4582.png.webp) **!!!这个图片的12V输入和5V输入标反了!!!具体一定看你买的的298N的标注** 12V是电源输入,咱们选购的车架是带有四节干电池串联座的。但是干电池,可以用但不好用,所以我做了外接12V的DC接口,直接用稳压电源输入电源,就是试车的时候会变成天线宝宝,拖根大电线。使用干电池的电压也就6V多一点,由于本身内阻大出现的症状包括但不限于:PWM给低时电机萎靡不振,或者跑五分钟就肾虚,电机堵转时开发板掉电重启(因为撑不住)。两节18650串联就不会有这些问题。当然网上是卖有18650串联电池座的,但两节18650带上充电器就得六十块钱了,我肯定是不会花这个钱的。还有就是万用表里的9v电池,大方疙瘩,那个都比四节干电池劲大,毕竟9V力大飞砖。 5V是给单片机供电用的,不多解释,把5V和GND接到SPARK开发板的5V拓展电源PIN输入上为板子供电。 IN1-IN4四个通道,以及旁边的两个enable跳帽,控制两个OUT口的输出。跳帽将A enable和B enable跳到5V,使之一直为开启状态。 第一种用法,保留两个通道的跳帽,使两个通道一直为开启状态。设置连接IN1-IN4四个通道的单片机引脚为GPIO输出模式,通过控制IN1-IN4四个通道的引脚电平,来控制小车方向。IN1-IN4四个通道控制表如下图所示 ![controltab.png](https://oss-club.rt-thread.org/uploads/20240810/20207b79debb66d5d3fe1595413186ee.png) 由于我们保留跳帽,使两个通道一直开启,电机就会慢速转动。如果我们要在这种模式下改变速度,就需要将连接IN1-IN4四个通道的单片机引脚全部改为TIMx_CHx的模式来输出PWM,使用输出比较模式改变PWM占空比来控制通道的速度。这种方法占用四个GPIO或者四个PWM通道,适合初学者裸机开发使用。 第二种方法,则是将单片机上两个引脚输出PWM,分别连接至A B enable这两个接口,起到油门的作用。而IN1-IN4四个通道依旧占用4个GPIO。这样总共占用6个接口。 我们选用第二种方法来控制电机。 为什么占用多了我们反而要使用第二种呢?因为第二种更合适做电机驱动的框架,讲人话就是更合理。我们给电机建立一个模型,对以一个电机硬件系统而言,无非就是方向和速度(油门)这俩。控制一个电机,我们只要给出这俩信息给电机控制器就行了,占用IO多是298N的问题。我们给IN1-IN4挂载一个二选一数据选择器,就只需要给两根GPIO就能解决方向问题,所以说对于控制框架而言,第二种更合适。 我们先考虑控制一路电机需要做哪些工作。我们首先要对设备初始化,初始化包括将电机模型数据初始化,控制用的引脚初始化。然后我们给电机一个方向要让电机往某个方向走,然后我们要让他转起来,就需要给他一个速度数据。如果我们需要闭环控制固定的速度,就需要额外引用一个PID控制包,将编码器的反馈值交给PID包计算,将油门量交给PID数据包计算。这个数据包实际就是PID函数,我们直接可以用网上别人写的现成的,毕竟和自己写的也一样,没啥必要自己写。 这样的话,流程就是:`初始化->接收数据改方向->接收数据改速度`,此后便是不断的:`接收数据改方向->接收数据改速度` 如果有两套电机,我们如果还按照上面这个思路,我们就得写两套`初始化->接收数据改方向->接收数据改速度`的代码来分别控制。也就是每一个函数都要写个一号二号,这无疑是增大了工作量。如果加一套电机就要加一套函数就太恶心人了。 ``` 初始化函数 方向控制函数 速度控制函数 ——————————————— —————————————————— —————————————————— |电机1初始化函数| |电机1接收数据改方向| |电机1接收数据改速度| |电机2初始化函数| -> |电机2接收数据改方向| -> |电机2接收数据改速度| | ··· | | ··· | | ··· | ——————————————— —————————————————— —————————————————— 加一套电机就要多一个函数,麻烦 ``` 那么电机控制的流程都是一致的,可不可以我们只用一套控制函数,去控制多个电机呢?我们该怎样让这一套控制流程在某个时刻来控制某个电机呢?上边我们写的框架中,电机1系列的函数和电机2系列的函数,我们肯定会对指定电机的参数进行操作,其余部分完全一致。那么我们把电机参数和控制流程分离开,控制流程接受某个电机的参数,进而只控制该电机,需要切换电机就切换到对应电机参数进行操作,这样,一套流程,我们可以快速修改代码挂载多个电机,同时在需要的情况下,也能对某些电机做单独的参数调整或者校准。肥肠的方便。 ``` 参数定义部分 ——————————————— ————————————— ————————————— |电机1参数配置 | |电机2参数配置| |电机3参数配置| ··· ——————————————— ————————————— ————————————— 执行部分: //挂载电机参数 //初始化函数 //方向控制函数 //速度控制函数 ——————————— ——————————— —————————————— —————————————— | 挂载电机x | -> |初始化函数| -> |接收数据改方向| -> |接收数据改速度| ——————————— ——————————— —————————————— —————————————— 加一套电机就加一组电机预设配置硬件通道和单个的电机数据 ``` ## 在rtt内使用片上外设功能 ## 控制pwm输出我们需要走单片机的TIM通道,我们需要选好合适的管脚。 星火一号给了正面40PIN的GPIO排座,欸我就是不用,我就用侧面这俩PMOD PMOD管脚默认分配如下: | MODE | GPIOS | GPIOS | MODE | | :---------: | :---------: | :---------: | :---------: | | ADC3_IN14 | PF4 | PD12 | TIM4_CH1 | | ADC3_IN15 | PF5 | PB11 | TIM2_CH4 | | ADC3_IN5 | PF6 | PB10 | TIM2_CH3 | | ADC3_IN5 | PF7 | PD11 | | | MODE | GPIOS | GPIOS | MODE | | :---------: | :---------: | :---------: | :---------: | | | PE4 | PA5 | | | | PE3 | PA6 | | | | PE5 | PA7 | TIM3_CH2 | | | PE2 | PA4 | AC_OUT1 | 我们选取TIM2_CH4和TIM2_CH3作为输出,但是LN298N的通道还要占用4个GPIO,光感也需要占用俩GPIO,在官方的BSP包内是没有配置该GPIO的。需要我们手动开启。 我们选择PE2,PE5,PE3,PE4依次分别作为ln298n的四个通道的输入,PA5,PA6设置为光感的接口(配图显示的引脚编号有误),这六个口均设置成输出的GPIO_output,并且点击图中位置,四个通道配置为下拉模式即可。 ![MXGPIOMODE.png](https://oss-club.rt-thread.org/uploads/20240810/c526f618039217d0f7a901aaf3a6ad26.png.webp) 然后在这个页面,我们取消勾选这个选项,让初始化代码只在msp文件内生成。 ![MXCODE.png](https://oss-club.rt-thread.org/uploads/20240810/42847b354d8dbbf12e86e849d5bf3b9e.png.webp) 点击GENERATE CODE,生成代码。 ![generate.png](https://oss-club.rt-thread.org/uploads/20240810/006f16a40ff63e7314b42d786d4df439.png.webp) 生成代码后,MX会在CubeMX_Config目录内生成Core和Drivers文件夹,我们进入Core/Src内找到下图两个文件。 ![msp.png](https://oss-club.rt-thread.org/uploads/20240810/58f1840cf5a9d866c380704c80c7dc97.png) 剪切然后到CubeMX_Config/Src目录下替换。然后将Core和Drivers文件夹直接删掉就行,因为RTThread的引脚初始化只用到了这俩文件。 在msp文件内,仅仅是对对应功能的管脚开启了总线时钟和预设了基本模式。如果我们不去cubemx里分配管脚模式,调用rtt的管脚驱动时,会没有输出。因为rtt的drv驱动只会开启相应的ip控制器,并没有打通gpio的输入输出。额外补充一点,Kconfig内的选项是开启drv驱动里面预先写好的功能,也无法控制管脚模式。所以如果不想自己写HAL代码,就用CubeMX去做管脚的初始分配。 | MODE | GPIOS | GPIOS | MODE | | :---------: | :---------: | :---------: | :---------: | | ADC3_IN14 | PF4 | PD12 | TIM4_CH1 | | ADC3_IN15 | PF5 | PB11 | TIM2_CH4 | | ADC3_IN5 | PF6 | PB10 | TIM2_CH3 | | ADC3_IN5 | PF7 | PD11 | | | MODE | GPIOS | GPIOS | MODE | | :---------: | :---------: | :---------: | :---------: | | channel_4 | PE4 | PA5 | 光感右侧 | | channel_3 | PE3 | PA6 | 光感左侧 | | channel_2 | PE5 | PA7 | TIM3_CH2 | | channel_1 | PE2 | PA4 | | 管脚分配结束,接下来要配置kconfig,kconfig的用法看我前几篇文章,这里不再赘述。在kconfig内找到PWM功能,开启PWM2的通道3和4,对应我们选择的TIM2_CH3和TIM2_CH4。 ![kconfig.png](https://oss-club.rt-thread.org/uploads/20240810/29ab40052c6e32cee78e84d690e1c739.png) ![kconfig2.png](https://oss-club.rt-thread.org/uploads/20240810/6e873a7ca1cd80d1697e04ca38868769.png) 接下来我们可以写代码了。 ## 代码部分 ## 我们使用的rtthread,已经提供出操作外设的接口。我们使用的pwm不需要我们对通道在写初始化引脚的代码了。所以电机初始化阶段,我们做的就是状态初始化。 使用pwm,可以参考官方文档[PWM操作](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/pwm/pwm)内介绍的接口。 所以我们写一段初始化代码 ``` #define PWM_Period 500000 //PWM周期 //通道电控初始化 void MOTORINIT(const char* dev,uint8_t channel,signed int pulse) { //查找pwm设备 pwm_dev = (struct rt_device_pwm *)rt_device_find(dev); if (pwm_dev == RT_NULL) { rt_kprintf("pwm sample run failed! can't find %s device!\n", dev); } // 设置PWM周期和脉冲宽度默认值 rt_pwm_set(pwm_dev, channel, PWM_Period, pulse); // 使能pwm设备 rt_pwm_enable(pwm_dev, channel); } ``` 这时还仅有一个电机,输入的量就有三个,那怎么办? 按照我们构建的框架,一套电机对应一套参数,这三个,毫无疑问就是一个电机的信息。显而易见,我们使用结构体会很方便使用和管理。 那我们写一段结构体 ``` //设备控制结构体 typedef struct { const char* dev; //pwm设备 uint8_t channel; //通道 signed int pulse; //脉宽数据 }OUTSTRUCT; extern OUTSTRUCT outstruct; ``` 这个结构体目前包含了我们初始化点击要用到的参数,那么输入的参数,我们直接使用结构体即可。初始化函数我们就可以改为 ``` void MOTORINIT(OUTSTRUCT *p) { //查找pwm设备 pwm_dev = (struct rt_device_pwm *)rt_device_find(p->dev); if (pwm_dev == RT_NULL) { rt_kprintf("pwm sample run failed! can't find %s device!\n", p->dev); } // 设置PWM周期和脉冲宽度默认值 rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); // 使能pwm设备 rt_pwm_enable(pwm_dev, p->channel); } ``` 可是,这一套结构体只能保存一个电机的数据,如果我们要控制多个电机,就得需要这结构体的变量要变成别的电机的量。别的电机的量怎样挂载到我们的初始化代码呢? 给电机们专门建立各自的结构体存放信息,让初始化代码使用的结构体变为暂存器,使用时暂存器挂载对应结构体的值不就可以了吗? 所以我们这样写结构体: ``` //设备2数据保存结构体 typedef struct { const char* out2_dev; //pwm设备 uint8_t out2_channel; //通道 signed int out2_pulse; //脉宽数据 }OUT2STRUCT; extern OUT2STRUCT out2struct; //设备3数据保存结构体 typedef struct { const char* out3_dev; //pwm设备 uint8_t out3_channel; //通道 signed int out3_pulse; //脉宽数据 }OUT3STRUCT; extern OUT3STRUCT out3struct; //设备控制结构体 typedef struct { const char* dev; //pwm设备 uint8_t channel; //通道 signed int pulse; //脉宽数据 }OUTSTRUCT; extern OUTSTRUCT outstruct; ``` 我们这样写初始化 ``` #define OUT2_SIGNAL 1 /* 电机2识别号 */ #define OUT3_SIGNAL 2 /* 电机3识别号 */ #define PWM_Period 500000 //PWM周期 //通道参数初始化 void STRUCT(uint8_t s) { if (s==OUT2_SIGNAL) { //电机2设备数据结构体初始化 out2struct.out2_dev=OUT2_DEV; out2struct.out2_channel=OUT2_CHANNEL; out2struct.out2_pulse=0; } if (s==OUT3_SIGNAL) { //电机3设备数据结构体初始化 out3struct.out3_dev=OUT3_DEV; out3struct.out3_channel=OUT3_CHANNEL; out3struct.out3_pulse=0; } } //通道电控初始化 void MOTORINIT(uint8_t s,OUTSTRUCT *p) { DATASW(s,p);//识别电机通道切换对应数据 //查找pwm设备 pwm_dev = (struct rt_device_pwm *)rt_device_find(p->dev); if (pwm_dev == RT_NULL) { rt_kprintf("pwm sample run failed! can't find %s device!\n", p->dev); } // 设置PWM周期和脉冲宽度默认值 rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); // 使能pwm设备 rt_pwm_enable(pwm_dev, p->channel); } ``` 我们将电机设备定义一个编号,初始化参数和电机都只看设备,初始化电机时,需要识别设备编号,然后出现了一个`DATASW(s,p);`,这个就是将暂存结构体数据切换到对应通道数据的函数。我们怎么编写他呢? ``` void DATASW(uint8_t s,OUTSTRUCT *p) { if (s==OUT2_SIGNAL) { p->dev=out2struct.out2_dev; p->channel=out2struct.out2_channel; p->pulse=out2struct.out2_pulse; } if (s==OUT3_SIGNAL) { p->dev=out3struct.out3_dev; p->channel=out3struct.out3_channel; p->pulse=out3struct.out3_pulse; } } ``` 我们用了笨办法,一个一个赋值,毕竟我C语言也不是那么好,memcpy也应该可以用,并且添加新设备时会很快,我就不用了。 接下来我们写速度设置代码。 ``` #define MOTOR_PWM_MAX 400000 //禁止满占空比输出,造成MOS损坏 #define MOTOR_PWM_MIN -400000 // //速度设定 void SETSPEED(uint8_t s,OUTSTRUCT *p) { DATASW(s,&outstruct);//识别电机通道切换对应数据 /* 查找设备 */ pwm_dev = (struct rt_device_pwm *)rt_device_find(p->dev); p->pulse = -(p->pulse);//参数反转 if (p->pulse>=0) { p->dir=FORWARD; DIRSW(&outstruct); if (p->pulse>MOTOR_PWM_MAX) { p->pulse = MOTOR_PWM_MAX; } rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); } else if (p->pulse<0) { p->dir=BACKWARD; DIRSW(&outstruct); if (p->pulse
pulse = MOTOR_PWM_MIN; } p->pulse = -(p->pulse); rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); } } ``` 拓展一下,如果在设置速度前,我们闭环控制得到某个通道电机的pulse值,这个pulse是我们希望更新到电机上的,我们写一个闭环函数来看一下 ``` //闭环速度控制 void CONTROLLOOP(uint8_t s,OUTSTRUCT *p,float speed) { rt_int32_t encounter; float SpeedFeedback; DATASW(s,p);//识别电机通道切换对应数据 //闭环编码器采样段(这里只是示例,本此实验车架并不支持闭环控制,这段开了会报错,因为没开对应硬件设置) pulse_encoder_dev = rt_device_find(p->enc); rt_device_read(pulse_encoder_dev, 0, &encounter, 1);//脉冲计数器采样 rt_device_control(pulse_encoder_dev, PULSE_ENCODER_CMD_CLEAR_COUNT, RT_NULL); pidStr.vi_FeedBack=encounter; SpeedFeedback=(float)(encounter * PI * DiameterWheel)/ MOTOR_CONTROL_CYCLE / EncoderLine / 2.0f / ReductionRatio; // m/s rt_kprintf("speed is %2f\n", SpeedFeedback); if(speed > MOTOR_SPEED_MAX) speed = MOTOR_SPEED_MAX; else if(speed < -MOTOR_SPEED_MAX) speed = -MOTOR_SPEED_MAX; pidStr.vi_Ref = (float)(speed*MOTOR_CONTROL_CYCLE / DiameterWheel / PI * EncoderLine * 2.0f * ReductionRatio); p->pulse=PID_MoveCalculate(&pidStr); //不对劲 SETSPEED(s,p); } ``` 看到不对劲了吗?我们思考下流程,闭环控制给出对应电机的pulse的值,我们的设置速度的函数紧跟上去,看似没有问题。实际上在速度设置函数,我们为了让这个函数单独拿出来能用,加了一个数据选择器`DATASW(s,&outstruct);`,闭环出来的pulse,他还在缓存结构体里,立马就被速度设置函数重新选择通道的操作给覆盖了。 也就是说我们要让闭环出来的pulse,及时更新到对应通道里,我们来写一个保存数据的函数吧: ``` //结构体通道保存器 void DATASAVE(uint8_t s,OUTSTRUCT *p) { if (s==OUT2_SIGNAL) { out2struct.out2_pulse=p->pulse; } if (s==OUT3_SIGNAL) { out3struct.out3_pulse=p->pulse; } } ``` 再把这个函数添加到刚刚不对劲的地方,就可以保证数据不丢失了。 这样一个基本的框架就出来了 ``` //设备2数据保存结构体 typedef struct { const char* out2_dev; //pwm设备 uint8_t out2_channel; //通道 signed int out2_pulse; //脉宽数据 }OUT2STRUCT; extern OUT2STRUCT out2struct; //设备3数据保存结构体 typedef struct { const char* out3_dev; //pwm设备 uint8_t out3_channel; //通道 signed int out3_pulse; //脉宽数据 }OUT3STRUCT; extern OUT3STRUCT out3struct; //设备控制结构体 typedef struct { const char* dev; //pwm设备 uint8_t channel; //通道 signed int pulse; //脉宽数据 }OUTSTRUCT; extern OUTSTRUCT outstruct; #define MOTOR_PWM_MAX 400000 //禁止满占空比输出,造成MOS损坏 #define MOTOR_PWM_MIN -400000 // #define OUT2_SIGNAL 1 /* 电机2识别号 */ #define OUT3_SIGNAL 2 /* 电机3识别号 */ #define PWM_Period 500000 //PWM周期 //结构体通道选择器 void DATASW(uint8_t s,OUTSTRUCT *p) { if (s==OUT2_SIGNAL) { p->dev=out2struct.out2_dev; p->channel=out2struct.out2_channel; p->pulse=out2struct.out2_pulse; } if (s==OUT3_SIGNAL) { p->dev=out3struct.out3_dev; p->channel=out3struct.out3_channel; p->pulse=out3struct.out3_pulse; } } //结构体通道保存器 void DATASAVE(uint8_t s,OUTSTRUCT *p) { if (s==OUT2_SIGNAL) { out2struct.out2_pulse=p->pulse; } if (s==OUT3_SIGNAL) { out3struct.out3_pulse=p->pulse; } } //通道参数初始化 void STRUCT(uint8_t s) { if (s==OUT2_SIGNAL) { //电机2设备数据结构体初始化 out2struct.out2_dev=OUT2_DEV; out2struct.out2_channel=OUT2_CHANNEL; out2struct.out2_pulse=0; } if (s==OUT3_SIGNAL) { //电机3设备数据结构体初始化 out3struct.out3_dev=OUT3_DEV; out3struct.out3_channel=OUT3_CHANNEL; out3struct.out3_pulse=0; } } //通道电控初始化 void MOTORINIT(uint8_t s,OUTSTRUCT *p) { DATASW(s,p);//识别电机通道切换对应数据 //查找pwm设备 pwm_dev = (struct rt_device_pwm *)rt_device_find(p->dev); if (pwm_dev == RT_NULL) { rt_kprintf("pwm sample run failed! can't find %s device!\n", p->dev); } // 设置PWM周期和脉冲宽度默认值 rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); // 使能pwm设备 rt_pwm_enable(pwm_dev, p->channel); } //速度设定 void SETSPEED(uint8_t s,OUTSTRUCT *p) { DATASW(s,&outstruct);//识别电机通道切换对应数据 /* 查找设备 */ pwm_dev = (struct rt_device_pwm *)rt_device_find(p->dev); p->pulse = -(p->pulse);//参数反转 if (p->pulse>=0) { p->dir=FORWARD; DIRSW(&outstruct); if (p->pulse>MOTOR_PWM_MAX) { p->pulse = MOTOR_PWM_MAX; } rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); } else if (p->pulse<0) { p->dir=BACKWARD; DIRSW(&outstruct); if (p->pulse
pulse = MOTOR_PWM_MIN; } p->pulse = -(p->pulse); rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); } } //闭环速度控制 void CONTROLLOOP(uint8_t s,OUTSTRUCT *p,float speed) { rt_int32_t encounter; float SpeedFeedback; DATASW(s,p);//识别电机通道切换对应数据 //闭环编码器采样段(这里只是示例,本此实验车架并不支持闭环控制,这段开了会报错,因为没开对应硬件设置) pulse_encoder_dev = rt_device_find(p->enc); rt_device_read(pulse_encoder_dev, 0, &encounter, 1);//脉冲计数器采样 rt_device_control(pulse_encoder_dev, PULSE_ENCODER_CMD_CLEAR_COUNT, RT_NULL); pidStr.vi_FeedBack=encounter; SpeedFeedback=(float)(encounter * PI * DiameterWheel)/ MOTOR_CONTROL_CYCLE / EncoderLine / 2.0f / ReductionRatio; // m/s rt_kprintf("speed is %2f\n", SpeedFeedback); if(speed > MOTOR_SPEED_MAX) speed = MOTOR_SPEED_MAX; else if(speed < -MOTOR_SPEED_MAX) speed = -MOTOR_SPEED_MAX; pidStr.vi_Ref = (float)(speed*MOTOR_CONTROL_CYCLE / DiameterWheel / PI * EncoderLine * 2.0f * ReductionRatio); p->pulse=PID_MoveCalculate(&pidStr); DATASAVE(s,&outstruct); SETSPEED(s,p); } ``` 至此,一个粗糙的框架初见端倪,此时我们的298n的方向驱动还没有加入呢,就有些凌乱了,我们把创建一个头文件,将结构体配置和宏定义扔进去,会显得干净很多。 接下来的代码过程就不再赘述,我们可以直接看成品: 头文件`smartconfig.h`如下: ``` #ifndef __SMARTCONFIG_H #define __SMARTCONFIG_H #include
#include
#include
#ifndef RT_USING_NANO #include
#endif /* RT_USING_NANO */ #include "drv_config.h" #include "drv_tim.h" #include
#include
#include
#include
#include
#include
/*--------------------------------------- POMD引脚配置情况 -----------------------------------------------*/ // // | MODE | GPIOS | GPIOS | MODE | Config | // | :---------: | :---------: | :---------: | :---------: | :---------: | // | | PF4 | PD12 | TIM4_CH1 | | // | | PF5 | PB11 | TIM2_CH4 | TIM2_CH4 | // | | PF6 | PB10 | TIM2_CH3 | TIM2_CH3 | // | | PF7 | PD11 | | | // // | MODE | GPIOS | GPIOS | MODE | Config | // | :---------: | :---------: | :---------: | :---------: | :---------: | // | channel_4 | PE4 | PA5 | | 光感右侧 | // | channel_3 | PE3 | PA6 | | 光感左侧 | // | channel_2 | PE5 | PA7 | TIM3_CH2 | | // | channel_1 | PE2 | PA4 | | | // /*-------------------------------------------------------------------------------------------------------*/ //驱动器通道设置 #define CHANNEL1 GET_PIN(E, 2) #define CHANNEL2 GET_PIN(E, 5) #define CHANNEL3 GET_PIN(E, 3) #define CHANNEL4 GET_PIN(E, 4) #define FORWARD 2 //OUT2前进 #define BACKWARD 1 //OUT2后退 #define STOP 0 //OUT2停止 //PWM通道设置 #define OUT2_DEV "pwm2" /* 电机2 PWM设备名称 */ #define OUT2_CHANNEL 3 /* PWM通道 */ #define OUT2_SIGNAL 1 /* 电机2识别号 */ #define OUT3_DEV "pwm2" /* 电机3 PWM设备名称 */ #define OUT3_CHANNEL 4 /* PWM通道 */ #define OUT3_SIGNAL 2 /* 电机3识别号 */ //正交编码器设置(未使用) #define OUT2_ENC "pulse2" //电机2 计数器名称 #define OUT3_ENC "pulse4" //电机3 计数器名称 //车辆模型数据 #define ReductionRatio 1.0f //电机减速比 #define EncoderLine 20 //编码器线数 #define DiameterWheel 0.068f //轮子直径:mm //软件速控预设 #define PWM_Period 500000 //PWM周期 #define MOTOR_PWM_MAX 400000 //禁止满占空比输出,造成MOS损坏 #define MOTOR_PWM_MIN -400000 // #define MOTOR_SPEED_MAX 10.0f //电机最大转速(m/s) (0.017,8.04) #define PI 3.141593f //π #define MOTOR_CONTROL_CYCLE 0.01f //电机控制周期T:10ms //PID通用计算包请去pid.h修改预设值 //设备2数据保存结构体 typedef struct { uint8_t out2_signal; //结构体识别码 const char* out2_dev; //pwm设备 const char* out2_enc; //enc设备 uint8_t out2_channel; //通道 uint8_t out2_dir; //方向 signed int out2_pulse; //脉宽数据 }OUT2STRUCT; //OUT2STRUCT out2struct; extern OUT2STRUCT out2struct; //设备3数据保存结构体 typedef struct { uint8_t out3_signal; //结构体识别码 const char* out3_dev; //pwm设备 const char* out3_enc; //enc设备 uint8_t out3_channel; //通道 uint8_t out3_dir; //方向 signed int out3_pulse; //脉宽数据 }OUT3STRUCT; //OUT3STRUCT out3struct; extern OUT3STRUCT out3struct; //设备控制结构体 typedef struct { uint8_t signal; //结构体识别码 const char* dev; //pwm设备 const char* enc; //enc设备 uint8_t channel; //通道 uint8_t dir; //方向 signed int pulse; //脉宽数据 }OUTSTRUCT; extern OUTSTRUCT outstruct; void DATASW(uint8_t s,OUTSTRUCT *p); //结构体通道选择器 void DATASAVE(uint8_t s,OUTSTRUCT *p); //结构体通道保存器 void DIRINIT(uint8_t s); //LN298N驱动器通道初始化 void DIRSW(OUTSTRUCT *p); //LN298N驱动器控制 void MOTORINIT(uint8_t s,OUTSTRUCT *p); //电机初始化(一次只对一侧电机初始化方便使用单电机模式。) void SETSPEED(uint8_t s,OUTSTRUCT *p); //速度设定 void CONTROLLOOP(uint8_t s,OUTSTRUCT *p,float speed); //闭环速度控制 void CARTIM(uint8_t s,OUTSTRUCT *p,float speed); //线程控制函数 //void ENCODERINIT(const char* dev); //脉冲计数器初始化 //void ENCODER_RevSample(void); //脉冲计数器采样 #endif ``` 函数`motor.c`如下: ``` #include
struct rt_device_pwm *pwm_dev; /* PWM设备句柄 */ rt_device_t pulse_encoder_dev; /* 脉冲编码器设备句柄 */ static uint16_t counter = 0; OUT2STRUCT out2struct; OUT3STRUCT out3struct; OUTSTRUCT outstruct; //结构体通道选择器 void DATASW(uint8_t s,OUTSTRUCT *p) { if (s==OUT2_SIGNAL) { p->dev=out2struct.out2_dev; p->enc=out2struct.out2_enc; p->channel=out2struct.out2_channel; p->signal=out2struct.out2_signal; p->dir=out2struct.out2_dir; p->pulse=out2struct.out2_pulse; } if (s==OUT3_SIGNAL) { p->dev=out3struct.out3_dev; p->enc=out3struct.out3_enc; p->channel=out3struct.out3_channel; p->signal=out3struct.out3_signal; p->dir=out3struct.out3_dir; p->pulse=out3struct.out3_pulse; } } //结构体通道保存器 void DATASAVE(uint8_t s,OUTSTRUCT *p) { if (s==OUT2_SIGNAL) { // out2struct.out2_dev=p->dev; // out2struct.out2_enc=p->enc; // out2struct.out2_channel=p->channel; // out2struct.out2_signal=p->signal; out2struct.out2_dir=p->dir; out2struct.out2_pulse=p->pulse; } if (s==OUT3_SIGNAL) { // out3struct.out3_dev=p->dev; // out3struct.out3_enc=p->enc; // out3struct.out3_channel=p->channel; // out3struct.out3_signal=p->signal; out3struct.out3_dir=p->dir; out3struct.out3_pulse=p->pulse; } } //LN298N通道初始化 void DIRINIT(uint8_t s) { if (s==OUT2_SIGNAL)//OUT2通道初始化 { rt_pin_mode(CHANNEL1, PIN_MODE_OUTPUT); rt_pin_mode(CHANNEL2, PIN_MODE_OUTPUT); rt_pin_write(CHANNEL1,PIN_LOW); rt_pin_write(CHANNEL2,PIN_LOW); } if (s==OUT3_SIGNAL)//OUT3通道初始化 { rt_pin_mode(CHANNEL3, PIN_MODE_OUTPUT); rt_pin_mode(CHANNEL4, PIN_MODE_OUTPUT); rt_pin_write(CHANNEL3,PIN_LOW); rt_pin_write(CHANNEL4,PIN_LOW); } } //LN298N方向控制 void DIRSW(OUTSTRUCT *p) { if (p->signal==OUT2_SIGNAL) { if (p->dir == FORWARD)//前进 { rt_pin_write(CHANNEL1,PIN_HIGH); rt_pin_write(CHANNEL2,PIN_LOW); } else if (p->dir == BACKWARD)//后退 { rt_pin_write(CHANNEL2,PIN_HIGH); rt_pin_write(CHANNEL1,PIN_LOW); } else if (p->dir == STOP)//停止 { rt_pin_write(CHANNEL2,PIN_LOW); rt_pin_write(CHANNEL1,PIN_LOW); } } else if (p->signal==OUT3_SIGNAL) { if (p->dir == FORWARD)//前进 { rt_pin_write(CHANNEL3,PIN_HIGH); rt_pin_write(CHANNEL4,PIN_LOW); } else if (p->dir == BACKWARD)//后退 { rt_pin_write(CHANNEL4,PIN_HIGH); rt_pin_write(CHANNEL3,PIN_LOW); } else if (p->dir == STOP)//停止 { rt_pin_write(CHANNEL4,PIN_LOW); rt_pin_write(CHANNEL3,PIN_LOW); } } } //通道电控初始化 void MOTORINIT(uint8_t s,OUTSTRUCT *p) { if (s==OUT2_SIGNAL) { //电机2设备数据结构体初始化 out2struct.out2_dev=OUT2_DEV; out2struct.out2_enc=OUT2_ENC; out2struct.out2_channel=OUT2_CHANNEL; out2struct.out2_dir=STOP; out2struct.out2_signal=OUT2_SIGNAL; out2struct.out2_pulse=0; } if (s==OUT3_SIGNAL) { //电机3设备数据结构体初始化 out3struct.out3_dev=OUT3_DEV; out3struct.out3_enc=OUT3_ENC; out3struct.out3_channel=OUT3_CHANNEL; out3struct.out3_dir=STOP; out3struct.out3_signal=OUT3_SIGNAL; out3struct.out3_pulse=0; } DIRINIT(s); //LN298N对应通道初始化 DATASW(s,p);//识别电机通道切换对应数据 DIRSW(p); //读取通道信息设置通道方向 //查找pwm设备 pwm_dev = (struct rt_device_pwm *)rt_device_find(p->dev); if (pwm_dev == RT_NULL) { rt_kprintf("pwm sample run failed! can't find %s device!\n", p->dev); } /* //查找enc设备,闭环控制时可以启用,该设备也是rtt的驱动,适用于增量式编码器 pulse_encoder_dev = rt_device_find(p->enc); if (pulse_encoder_dev == RT_NULL) { rt_kprintf("pulse encoder dev run failed! can't find %s device!\n", p->enc); } // 使能enc设备 rt_device_open(pulse_encoder_dev, RT_DEVICE_OFLAG_RDONLY); */ // 设置PWM周期和脉冲宽度默认值 rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); // 使能pwm设备 rt_pwm_enable(pwm_dev, p->channel); } //速度设定 void SETSPEED(uint8_t s,OUTSTRUCT *p) { DATASW(s,&outstruct);//识别电机通道切换对应数据 /* 查找设备 */ pwm_dev = (struct rt_device_pwm *)rt_device_find(p->dev); p->pulse = -(p->pulse);//参数反转 if (p->pulse>=0) { p->dir=FORWARD; DIRSW(&outstruct); if (p->pulse>MOTOR_PWM_MAX) { p->pulse = MOTOR_PWM_MAX; } rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); //DATASAVE(s,&outstruct); } else if (p->pulse<0) { p->dir=BACKWARD; DIRSW(&outstruct); if (p->pulse
pulse = MOTOR_PWM_MIN; } p->pulse = -(p->pulse); rt_pwm_set(pwm_dev, p->channel, PWM_Period, p->pulse); // DATASAVE(s,&outstruct); } } //闭环速度控制 void CONTROLLOOP(uint8_t s,OUTSTRUCT *p,float speed) { rt_int32_t encounter; float SpeedFeedback; DATASW(s,p);//识别电机通道切换对应数据 /* //闭环编码器采样段 pulse_encoder_dev = rt_device_find(p->enc); rt_device_read(pulse_encoder_dev, 0, &encounter, 1);//脉冲计数器采样 rt_device_control(pulse_encoder_dev, PULSE_ENCODER_CMD_CLEAR_COUNT, RT_NULL); pidStr.vi_FeedBack=encounter; SpeedFeedback=(float)(encounter * PI * DiameterWheel)/ MOTOR_CONTROL_CYCLE / EncoderLine / 2.0f / ReductionRatio; // m/s */ rt_kprintf("speed is %2f\n", SpeedFeedback); if(speed > MOTOR_SPEED_MAX) speed = MOTOR_SPEED_MAX; else if(speed < -MOTOR_SPEED_MAX) speed = -MOTOR_SPEED_MAX; pidStr.vi_Ref = (float)(speed*MOTOR_CONTROL_CYCLE / DiameterWheel / PI * EncoderLine * 2.0f * ReductionRatio); p->pulse=PID_MoveCalculate(&pidStr); DATASAVE(s,&outstruct); SETSPEED(s,p); } //线程控制函数 void CARTIM(uint8_t s,OUTSTRUCT *p,float speed) { DATASW(s,p);//识别电机通道切换对应数据 counter++; if (counter>=10) { CONTROLLOOP(s,p,speed); counter=0; } } ``` 至此,电机驱动已经完成。这个驱动,能保证你只要是rtt的板子,你开了对应通道的kconfig就能正常用。 我们简简单单写一个寻线函数 就在main里写 ``` /* * Copyright (c) 2023, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * Date Author Notes * 2023-07-06 Supperthomas first version * 2023-12-03 Meco Man support nano version */ #include
#include
#include
#ifndef RT_USING_NANO #include
#endif /* RT_USING_NANO */ #include
//本巡线代码纯属无奈之举,流水线电机闭环控制硬件上真得加外挂拓展通道,真没法写闭环 //所以smartcar线程腰斩了,遥控线程也跟着腰斩了,巡线直接在主函数里写了 //巡线本身不是设计为往流水线上使用的,所以只能强行套用多电机驱动 //本来压根没想做巡线的 //哭了 #define RIGHT GET_PIN(A, 5) #define LEFT GET_PIN(A, 6) #define TURNSTOP 0 #define TURNRIGHT 1 #define TURNLEFT 2 #define TURNON 3 #define SLOW 150000 #define FAST 250000 #define QUIC 400000 #define WOCAO 400000 signed int speedinit; void sinit(signed int v,OUTSTRUCT *p) { out3struct.out3_pulse=v; DATASW(OUT3_SIGNAL,p); SETSPEED(p->signal,p); out2struct.out2_pulse=v; DATASW(OUT2_SIGNAL,p); SETSPEED(p->signal,p); } void speedsw(signed int v,OUTSTRUCT *p) { if (p->signal==OUT3_SIGNAL) { out3struct.out3_pulse=v; DATASW(OUT3_SIGNAL,p); SETSPEED(p->signal,p); } else if (p->signal==OUT2_SIGNAL) { out2struct.out2_pulse=v; DATASW(OUT2_SIGNAL,p); SETSPEED(p->signal,p); } } void turn(uint8_t dir,OUTSTRUCT *p) { if (dir==TURNRIGHT) { out3struct.out3_dir=STOP; out3struct.out3_pulse=-WOCAO; DATASW(OUT3_SIGNAL,p); SETSPEED(p->signal,p); DIRSW(p); out2struct.out2_dir=FORWARD; out2struct.out2_pulse=WOCAO; DATASW(OUT2_SIGNAL,p); SETSPEED(p->signal,p); DIRSW(p); } else if (dir==TURNLEFT) { out2struct.out2_dir=STOP; out2struct.out2_pulse=-WOCAO; DATASW(OUT2_SIGNAL,p); SETSPEED(p->signal,p); DIRSW(p); out3struct.out3_dir=FORWARD; out3struct.out3_pulse=WOCAO; DATASW(OUT3_SIGNAL,p); SETSPEED(p->signal,p); DIRSW(p); } else if (dir==TURNSTOP) { out2struct.out2_dir=STOP; out2struct.out2_pulse=0; DATASW(OUT2_SIGNAL,p); SETSPEED(p->signal,p); DIRSW(p); out3struct.out3_dir=STOP; out3struct.out3_pulse=0; DATASW(OUT3_SIGNAL,p); SETSPEED(p->signal,p); DIRSW(p); } else if (dir==TURNON) { out2struct.out2_dir=FORWARD; out2struct.out2_pulse=speedinit; DATASW(OUT2_SIGNAL,p); SETSPEED(p->signal,p); DIRSW(p); out3struct.out3_dir=FORWARD; out3struct.out3_pulse=speedinit; DATASW(OUT3_SIGNAL,p); SETSPEED(p->signal,p); DIRSW(p); } } void xunji(OUTSTRUCT *p) { if (rt_pin_read(RIGHT)==0 && rt_pin_read(LEFT)==1) { turn(TURNLEFT,p); } else if (rt_pin_read(RIGHT)==1 && rt_pin_read(LEFT)==0) { turn(TURNRIGHT,p); } else if (rt_pin_read(RIGHT)==1 && rt_pin_read(LEFT)==1) { turn(TURNON,p); } else if (rt_pin_read(RIGHT)==0 && rt_pin_read(LEFT)==0) { turn(TURNON,p); } } int main(void) { //电机2初始化 DIRINIT(OUT2_SIGNAL); MOTORINIT(OUT2_SIGNAL,&outstruct); //电机3初始化 DIRINIT(OUT3_SIGNAL); MOTORINIT(OUT3_SIGNAL,&outstruct); //sinit(WOCAO,&outstruct); speedinit=FAST; while (1) { xunji(&outstruct); } } ``` 编译烧鸡!启动吧! 测试小车就不用我说了吧,在地上贴黑色电工胶布,小车就能跟着跑了 ![7.jpg](https://oss-club.rt-thread.org/uploads/20240810/9c6a73d464a6e327f55df0bffb248a4d.jpg.webp) 开源地址我放评论区
1
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
河南理工大学恁带劲儿
俺是计算机协会硬件部嘞
文章
5
回答
0
被采纳
0
关注TA
发私信
相关文章
1
大神们,rt-thread启用WDT了,但是还是没启动,怎么办?
2
求一个师傅带带队,有偿交学费 肯吃苦
3
自己按照官方手册 在drv_gpio.c里面找不到PIN脚信息
4
rtt studio f4默认生成的代码无法使用
5
官方例程中的 USB设置配置不成功
6
STM32F4的虚拟串口 的USB时钟如何配置
7
AT24CXX 软件包函数 at24cxx的问题
8
rtthread studio和bsp文件之间生成的区别和联系?
9
pwm根据手册修改为对应的引脚后无效
10
文件系统挂实验 ls命令异常
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
rt_mq_消息队列_msg_queue
keil_MDK
ulog
MicroPython
C++_cpp
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
19
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
6
个答案
2
次被采纳
用户名由3_15位
13
个答案
1
次被采纳
本月文章贡献
程序员阿伟
9
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
RTT_逍遥
1
篇文章
6
次点赞
大龄码农
1
篇文章
5
次点赞
ThinkCode
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部