目录
导言
一、小车概述
二、基础部分硬件概述
2.1、小车的眼睛——TCRT5000循迹传感器
2.2、小车的方向盘——舵机模块SG90/MG90
2.3、小车的腿——直流减速电机(TT马达)
2.4、小车的大脑——STM32F103C8T6最小系统板
2.5、小车的动力保障——L298N驱动模块
2.6、小车的能源——电源模块以及升降压模块
2.7、小车的血管——导线和开关
2.8、推荐基础器件清单
2.9、推荐STM32F103C8T6最小系统板引脚资源分配
2.10、推荐接线图
三、基础部分软件概述
3.1、巡线原理概述
3.2、代码实现步骤概述
3.3、初始化系统时钟
3.2、设置中断分组
3.3、初始化延时函数
3.4、初始化板载LED灯
3.5、初始化电机PWM输出引脚
3.6、初始化红外传感器模块
3.7、初始化舵机模块
3.8、初始化中断
3.9、小车逻辑功能实现
3.9.1、实现红外扫描读取函数的封装和处理
3.9.2、 舵机转向控制
3.9.3、电机的差速控制辅助转向
四、调试部分
五、代码模板下载
导言
大家好,本期为大家带来校园比赛的一个项目分享:11路数字量红外巡线小车
本分享的元器件仅推荐使用,在调试过程中可以更换更好的元器件模块。
本文仅写到入门处理,过程中有一些知识如PWM等需要自行学习,更多功能可以自己添加完善。
本文以学习为主,积极探索。
希望大家可以给我多提提意见,感谢每个人的支持!
一、小车概述
这是我初始做的9路数字量红外巡线小车
图1 9路数字量红外巡线小车样例图
二、基础部分硬件概述
2.1、小车的眼睛——TCRT5000循迹传感器
实现巡线的必不可少的部分,通过单片机读取模块的OUT引脚,我们可以得到高低电平信号,根据检测到的信号进行分情况处理,就能实现我们的循迹处理。
这里推荐淘宝的如下图的模块
图2 TCRT5000循迹传感器样例图
2.2、小车的方向盘——舵机模块SG90/MG90
舵机能有助于小车转弯,能让小车调节更加简单,这里推荐SG90进行入门,因为其价格更加便宜,不用担心一下烧坏损失太多。
注意!:
1.舵机需要外部5V的供电,直接使用STM32F103C8T6最小系统板上的5V供电可能不够用。
2.舵机有一个死区电压,一般使用PWM驱动时需要注意这个电压。
图3 SG90舵机模块样例图
2.3、小车的腿——直流减速电机(TT马达)
没有好的腿怎么走得快呢,在装电机前请记得检测电机的转向,不然一条腿向东一条腿向西边跑了,跑不快的哦。
电机在高速转动时电流较大,这个时候我们可以更换电机自带的两根线,换更粗的上去,增大载流量。
图4 TT马达模块样例图
2.4、小车的大脑——STM32F103C8T6最小系统板
这个是我们用来实现全部思想的核心,开发时推荐使用STM32F103C8T6最小系统板,其单价便宜,可拆卸方便,开发前需要准备和了解其资源分布,引脚映射功能等,并且在使用时需要配备st-link等的烧录器进行代码的烧写。
使用时需要注意:
1、接线的正负极,
2、注意使用的IO口的电压电流,防止烧坏芯片或者烧坏IO引脚。
图5 STM32F103C8T6最小系统板样例图和资源分配图
图6 st-link烧录器模块样例
2.5、小车的动力保障——L298N驱动模块
L298N能实现两个电机的正反转控制和速度控制,自身也配备了5V降压模块,但是其需要12V的电源输入,也需要和其他模块共地。
由于我们的小车一般不用倒车功能,可以将其4个接口中的两个接口直接给低电平,另外两个给PWM输入,即可通过程序控制电机固定方向和给定速度前进。
图7 L298N电机驱动模块示意图
2.6、小车的能源——电源模块以及升降压模块
2.6.1、电源模块
小车在使用时需要消耗的电量比较大,可以考虑使用三节18650的电池盒,但是如果考虑车的整体重量,使用两节18650电池盒就足够了。电池盒的自带的线比较细,可以自行更换较大的线。
图8 18650电池模块示意图
2.6.2、升压模块
为给L298N驱动模块稳定供电,我们需要配备一个升压模块,升压模块有输入端int和输出端out,注意的是正负级不能接反,要先将电池盒的输出接入到升压模块输入端,然后将电压表调节到直流电压档(注意量程),检测输出端电压,然后旋转电位器,将输出电压调节到12V。
图9 XL6009 DC-DC升压电源模块样例图
2.6.3、降压模块
小车的芯片、舵机等模块都需要稳定的供电,电池盒输出出来的电压太大,我们需要进行降压稳压操作,同样,降压模块有输入端int和输出端out。
注意:
正负级不能接反,要先将电池盒的输出接入到降压模块输入端,然后将电压表调节到直流电压档(注意量程),检测输出端电压,然后旋转电位器,将输出电压调节到5V。
图10 LM2596S DC-DC降压电源模块示意图
2.7、小车的血管——导线和开关
2.7.1、导线
可以买一些杜邦线和一些口径相对原来的大一些的导线。
2.7.2、开关
开关能方便我们启动车辆,节约电量,保护电路等操作。
2.8、推荐基础器件清单
模块名称 | 数量 |
TCRT5000循迹传感器 | 11 |
舵机模块SG90 | 1 |
直流减速电机(TT马达+轮子) | 2 |
STM32F103C8T6最小系统板 | 1 |
st-link烧录器 | 1 |
L298N驱动模块 | 1 |
18650电池 | 2 |
18650两节电池盒 | 1 |
XL6009 DC-DC升压电源模块 | 1 |
LM2596S DC-DC降压电源模块 | 1 |
杜邦线、导线 | 若干 |
开关 | 1 |
1寸定向轮 | 1 |
2.9、推荐STM32F103C8T6最小系统板引脚资源分配
模块名称 | 引脚号 |
舵机 | PB6 |
红外传感器 | PA0,PA1,PA2,PA3,PA4,PA5,PA6,PA7,PB9,PB10,PB11 |
左电机 | PB0 |
右电机 | PB1 |
2.10、推荐接线图
画工不精,请忍住别笑!
图11 推荐接线样例图
三、基础部分软件概述
3.1、巡线原理概述
我们知道,舵机可以根据输出不同的PWM值进行不同位置的转向,在使用时我们先调试让舵机带的定向轮方向跟黑线方向平行,记录这个时候的PWM值为DUOJIZHONGZHI。
在这个中值的基础上进行加减,就能实现舵机的转向,那如何知道舵机什么时候加减呢,这个时候就要依靠我们的数字红外传感器了,我们可以像如图一样将下面红外检测到黑线时给我们的error变量进行赋值(这个赋值可以根据车模具体情况来使用)。
图12 红外传感器对应的误差赋值图
当黑线在对应传感器位置下时,对error变量进行赋值,但是这个error的值比较小,直接使用不足以让舵机转动,所以我们可以再定义一个变量为DUOJI=100,(这个100的值需要根据具体车模和舵机的情况进行调节),我们可以得到舵机转向的PWM计算公式为
DjPwm = DUOJIZHONGZHI + DUOJI * error;
注意:如果发现舵机转向不对,只需要更改 “+” 号变为 “-” 号
电机的差速也有利于转弯,可以模仿舵机的PWM计算方法运用到电机上,会使转弯更加丝滑。
3.2、代码实现步骤概述
初始化系统时钟
设置中断分组
初始化延时函数
初始化板载LED灯
初始化TTL电机PWM输出引脚
初始化红外传感器模块
初始化舵机模块
初始化中断
小车逻辑功能实现
3.3、初始化系统时钟
初始化时钟是代码必要的初始化步骤,直接调用库函数自带的SystemInit();函数即可
代码示例如下:
1
2
3
|
/* 系统时钟的初始化 */
SystemInit();
|
3.2、设置中断分组
设置中断分组的目的是为了管理优先级,把一些检测代码和控制代码放进中断服务函数里能够使数据等更加精确。
我们直接调用库函数里面的NVIC_PriorityGroupConfig();函数即可
代码示例如下:
1
|
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
|
3.3、初始化延时函数
延时函数是我们进行代码调试的一个有利工具,这个模块是我们自己添加的模块
添加模块的delay.c代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
#include "delay.h"
#if SYSTEM_SUPPORT_OS
#include "includes.h"
#endif
static u8 fac_us=0;
static u16 fac_ms=0;
#if SYSTEM_SUPPORT_OS
#ifdef OS_CRITICAL_METHOD
#define delay_osrunning OSRunning
#define delay_ostickspersec OS_TICKS_PER_SEC
#define delay_osintnesting OSIntNesting
#endif
#ifdef CPU_CFG_CRITICAL_METHOD
#define delay_osrunning OSRunning
#define delay_ostickspersec OSCfg_TickRate_Hz
#define delay_osintnesting OSIntNestingCtr
#endif
void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD
OS_ERR err;
OSSchedLock(&err);
#else
OSSchedLock();
#endif
}
void delay_osschedunlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD
OS_ERR err;
OSSchedUnlock(&err);
#else
OSSchedUnlock();
#endif
}
void delay_ostimedly(u32 ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
OS_ERR err;
OSTimeDly(ticks,OS_OPT_TIME_PERIODIC,&err);
#else
OSTimeDly(ticks);
#endif
}
void SysTick_Handler(void)
{
if(delay_osrunning==1)
{
OSIntEnter();
OSTimeTick();
OSIntExit();
}
}
#endif
void delay_init()
{
#if SYSTEM_SUPPORT_OS
u32 reload;
#endif
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
fac_us=SystemCoreClock/8000000;
#if SYSTEM_SUPPORT_OS
reload=SystemCoreClock/8000000;
reload*=1000000/delay_ostickspersec;
fac_ms=1000/delay_ostickspersec;
SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;
SysTick->LOAD=reload;
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk;
#else
fac_ms=(u16)fac_us*1000;
#endif
}
#if SYSTEM_SUPPORT_OS
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD;
ticks=nus*fac_us;
tcnt=0;
delay_osschedlock();
told=SysTick->VAL;
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow;
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break;
}
};
delay_osschedunlock();
}
void delay_ms(u16 nms)
{
if(delay_osrunning&&delay_osintnesting==0)
{
if(nms>=fac_ms)
{
delay_ostimedly(nms/fac_ms);
}
nms%=fac_ms;
}
delay_us((u32)(nms*1000));
}
#else
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us;
SysTick->VAL=0x00;
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16)));
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL =0X00;
}
void delay_ms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms;
SysTick->VAL =0x00;
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16)));
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL =0X00;
}
#endif
|
添加模块的delay.h代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#ifndef __DELAY_H
#define __DELAY_H
#include "sys.h"
void delay_init(void);
void delay_ms(u16 nms);
void delay_us(u32 nus);
#endif
|
需要调用的初始化函数是delay_init();
代码示例如下:
3.4、初始化板载LED灯
板载led灯的初始化为普通gpio的推挽输出模式初始化即可。
我们需要先封装如下函数,再调用此函数进行初始化。
封装函数如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/*
板载LED初始化
*/
void my_LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //GPIO管脚的结构体定义
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//使能GPIO的时钟C
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;//IO管脚输出模式为推挽输出,此模式既可以输出高电平或低电平
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;//13号管脚
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//IO管脚的传输速度
GPIO_Init(GPIOC,&GPIO_InitStructure);//PC13管脚配置
GPIO_SetBits(GPIOC,GPIO_Pin_13); //PC13管脚输出高电平
}
|
需要调用的函数如下:
1
2
3
|
/* STM32f103c8t6最小系统板上的LED灯的初始化 */
my_LED_Init();
|
3.5、初始化电机PWM输出引脚
对于电机,我们只需要向前的转速即可,即只需要初始化两个PWM输出模式的端口,并对他们进行一个值运算的限幅,和频率的初始化。我们可以将自动重装载寄存器的值设置为7199,预分频值为0,改变这两个值会让电机有不同的响声,大家可以自己体验一下。
电机的两个PWM的映射引脚为 PB0 PB1 对应定时器资源3的3通道和4通道。
我们需要写一个初始化函数
代码示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
/*
电机初始化
参数7199,0
初始化PWM 定时器3 PB0 PB1
*/
void PWM_Init_TIM3(u16 Per,u16 Psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);//
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE); //使能GPIO外设时钟使能
//设置该(PB0和PB1)引脚为复用输出功能,输出TIM4 CH3和CH4的PWM脉冲波形
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; //TIM_CH3 CH4
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
TIM_TimeBaseStructure.TIM_Period = Per; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 80K
TIM_TimeBaseStructure.TIM_Prescaler =Psc; //设置用来作为TIMx时钟频率除数的预分频值 不分频
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_Pulse = 0; //设置待装入捕获比较寄存器的脉冲值
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
TIM_OC3Init(TIM3, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMx
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_Pulse = 0; //设置待装入捕获比较寄存器的脉冲值
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
TIM_OC4Init(TIM3, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMx
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); //CH1预装载使能
TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); //CH1预装载使能
TIM_ARRPreloadConfig(TIM3, ENABLE); //使能TIMx在ARR上的预装载寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIM3
}
|
然后我们需要在主函数中调用这个封装函数
代码示例如下:
1
2
3
|
/* 电机引脚及PWM初始化 */
PWM_Init_TIM3(7199,0);
|
3.6、初始化红外传感器模块
对于红外传感器模块,需要使用的引脚比较多,每个引脚只需要使用普通的GPIO模式,将GPIO模式设置为浮空输入即可。
红外模块占用的端口资源为 PA0~PA7 PB9~PB11
他们对应的的传感器位置从左到右依次为 PA0 PA1 PA2 PA3 PA4 PA5 PA6 PA7 PB9 PB19 PB11
需要封装一个初始化函数
代码示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
/*
红外初始化
*/
void hongwai_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//使能GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//使能复用功能的时钟
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;//IO管脚模式配置为浮空输入,这样就能获取传感器传回来的数字信号(高低电平)
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);//PA4,PA5,PA6,PA7管脚的初始化
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;//浮空输入模式
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10 | GPIO_Pin_9 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);//PB0,PB1,PB3,PB4,PB5管脚的初始化
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);//关闭STM32f103c8t6的JTAGD功能,使PB3和PB4用作普通的IO管脚//必须开启复用功能的时钟才能关闭该功能
}
|
然后在主函数中调用它
代码示例如下:
1
2
3
|
/* 11个红外传感器连接最小系统板上的管脚IO口的初始化 */
hongwai_Init();
|
3.7、初始化舵机模块
对于舵机模块的初始化,我们需要初始化一个PWM输出的GPIO口,并且这个GPIO口的PWM频率跟前面的电机的频率不一样,在库函数的写法下,我们需要选择一个新的定时器进行初始化。
关于sg90舵机的PWM频率,有如下图:
图13 舵机的角度与脉冲对应图
因此我们可以对舵机的PWM的自动重装载寄存器的值设置为19999,预分频值为71
使用定时器4的 通道1 PB6
根据图13可以得到舵机的角度映射公式为
Angle/180*2000+500
其中Angle是我们需要调整的舵机角度,值范围为0~180
需要封装一个初始化函数
代码示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
/*
舵机PWM初始化
定时器4
通道1
PB6
arr:自动重装值
psc:时钟预分频数
*/
void TIM4_PWM_Init(u16 arr,u16 psc)//在主函数main中传入arr和psc的数值
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //定时器TIM结构体定义
TIM_OCInitTypeDef TIM_OCInitStructure; //定时器TIM通道结构体定义,每个定时器有四个通道
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);// 使能定时器四的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//使能GPIO B的时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//PB6管脚的初始化,管脚对应复用功能:TIM4_CH1(定时器四的通道一)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //因为要用到管脚的复用功能,所以这里是管脚模式是复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//PB6
TIM_TimeBaseStructure.TIM_Period = arr;//; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc;//设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = 0;//设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的相应模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_Pulse =0; //设置待装入捕获比较寄存器的脉冲值
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
TIM_OC1Init(TIM4, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化TIM4_CH1定时器四的通道一
TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);//使能TIM4在 CCR1 上的预装载寄存器
TIM_Cmd(TIM4, ENABLE); //开启TIM4
}
|
需要封装一个舵机角度的改变函数
其代码示例如下:
1
2
3
4
5
6
7
|
void Srevo_SetAngle(float Angle)//舵机设置角度
{
TIM_SetCompare1(TIM4,Angle/180*2000+500);//映射计算,
}
|
在主函数中需要调用初始化函数进行初始化
其代码示例如下:
1
2
3
|
/* 舵机的PWM初始化,这里设置的频率为50hz 这样得出的周期T=1/f=20ms 因为MG90S舵机的PWM驱动信号为20ms */
TIM4_PWM_Init(19999,71);
|
我们还需要对舵机一开始的角度进行调整,确保它连接的定向轮指向黑线,在车的中间
即先用宏定义一个变量为 DUOJIZHONGZHI
其代码示例如下:
1
|
#define DUOJIZHONGZHI 90
|
每台车的舵机安装都不一样,所以这个值需要自己进行调试。
调试的代码示例如下:
1
2
3
|
/* 舵机归中 */
Srevo_SetAngle(DUOJIZHONGZHI);
|
3.8、初始化中断
初始化中断的目的是中断能够更加准确的对小车进行调控,让小车的实时性响应更好
我们前面的使用占用了定时器3和定时器4,所以我们使用定时2作中断
初始化为10ms一次
所以其自动重装载寄存器的值设置为9999,预分频值为71
需要封装一个初始化函数
其代码示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
/*
中断函数的定义
定时器:TIM2
*/
void TIM2_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //时钟使能
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到5000为500ms
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 10Khz的计数频率
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
TIM_ITConfig( //使能或者失能指定的TIM中断
TIM2, //TIM2
TIM_IT_Update ,
ENABLE //使能
);
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级0级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级3级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
TIM_Cmd(TIM2, ENABLE); //使能TIMx外设
}
|
在主函数中调用其初始化函数
代码示例如下:
1
2
3
4
5
|
/* 中断服务函数的初始化,也就是PWM波的初始化,这里的周期为10ms
也就是每10ms会执行一次control.c文件里TIM2_IRQHandler函数里的操作 */
TIM2_Int_Init(9999,71);
|
3.9、小车逻辑功能实现
建设好前面的初始化后,我们就要进行小车逻辑功能的实现了。
3.9.1、实现红外扫描读取函数的封装和处理
我们需要先定义一个数组存储扫描回来的值方便我们后续使用
数组定义为 :
int sensor[11]={0,0,0,0,0,0,0,0,0,0,0};
对应的GPIO引脚号、小车传感器位置和数组位置映射如下
sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
PA0 PA1 PA2 PA3 PA4 PA5 PA6 PA7 PB9 PB10 PB11
-5 -4 -3 -2 -1 0 1 2 3 4 5
代码示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
void read_sensor(void)//红外传感器识别到黑线返回数字信号低电平0,未识别到黑线返回高电平1
{
/*将位置从最左按顺序到最右的传感器返回的数字信号依次存入数组sensor[0~10]里*/
sensor[0]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0);//最左的传感器
sensor[1]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1);
sensor[2]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_2);
sensor[3]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_3);
sensor[4]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_4);
sensor[5]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_5);//中央的传感器
sensor[6]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
sensor[7]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_7);
sensor[8]=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9);
sensor[9]=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10);
sensor[10]=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);//最右的传感器
/* 处理部分 */
/* 最左边的红外扫到黑线 */
if(sensor[0]==0&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
{
error = -5;
}
/* 最左边旁边的左边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==0&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
{
error = -4;
}
/* 最左边的旁边的旁边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==0&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
{
error = -3;
}
/* 最左边的旁边的旁边的旁边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==0&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
{
error = -2;
}
/* 最左边的旁边的旁边的旁边的旁边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==0&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
{
error = -1;
}
/* 中间的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==0&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
{
error = 0;
}
/* 中间的右边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==0&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
{
error = 1;
}
/* 中间的右边的右边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==0&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
{
error = 2;
}
/* 中间的右边的右边的右边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==0&&sensor[9]==1&&sensor[10]==1)
{
error = 3;
}
/* 中间的右边的右边的右边的右边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==0&&sensor[10]==1)
{
error = 4;
}
/* 最右边的红外扫到黑线 */
else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==0)
{
error = 5;
}
/* 如果都不满足,就将上一次的值传给error */
else
{
error = lastError;
}
/* 记录上一次的值 */
lastError = error;
}
|
具体条件还可以根据实际更改。
3.9.2、 舵机转向控制
关于舵机转向,舵机是从0~180度的范围的,根据前面描述的舵机公式
DjPwm = DUOJIZHONGZHI + DUOJI * error;
可以写出我们的控制代码为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* 根据反馈的error值进行舵机的角度运算 */
DjPwm = DUOJIZHONGZHI + DUOJI * error;//把这个算式中的“+”换成“-”可以改变舵机转动方向
/* 对运算的数据进行限幅,防止溢出或者过大 */
if(DjPwm > 180)
DjPwm = 180;
if(DjPwm < 0)
DjPwm = 0;
/* 将运算完后的值传输进入PWM输出 */
Srevo_SetAngle(DjPwm);
|
3.9.3、电机的差速控制辅助转向
小车可以通过轮子的差速进行转向控制。
即简单来说,要使小车向左边转,需要给左轮小于右轮的速度,小车两个轮子不同的转速会推动小车往左边偏,同理,要使小车向右边转,需要给右轮小于左轮的速度。
那在程序中如何实现呢。
我们现在使用了驱动模块驱动电机,然后我们只需要设置单片机的电机资源的两个引脚的PWM比较值,即设置两个引脚的电平输出时间,即可给电机一个驱动的电压。
这个比较值的大小不一样,电机的转速就不一样,因此,我们需要先给电机一个初始的比较值,让它有一个能跑的速度先。
我们可以 #define DIANJI_PWM 4000
int DIANJI = 100;//这个值用来差速运算,改变这个值可以改变差速大小
如果需要改变电机的速度就改变这个DIANJI_PWM 的值
那如何实现电机的差速运算呢,由前面的红外模块的扫描读取,当小车在黑线右边时,小车的左边红外传感器扫到黑线。error赋值小于0,这个时候我们小车要向左边转动,即左边轮子转速小于右边轮子,
即左边轮子的PWM比较值公式为 DIANJI_PWM + error * DIANJI;
即右边轮子的PWM比较值公式为 DIANJI_PWM - error * DIANJI;
同理,分析小车在黑线左边的情况,发现公式一致。
所以我们可以写出控制代码
代码示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/* 根据反馈的error值进行电机的差速运算 */
DianJiPwm_L = DIANJI_PWM + DIANJI * error;
DianJiPwm_R = DIANJI_PWM - DIANJI * error;
/* 对运算的数据进行限幅,防止溢出或者过大 */
DianJiPwm_L = DianJiPwm_L > 7200 ? DianJiPwm_L : 7200;
DianJiPwm_L = DianJiPwm_L < 0 ? DianJiPwm_L : 500;
DianJiPwm_R = DianJiPwm_R > 7200 ? DianJiPwm_R : 7200;
DianJiPwm_R = DianJiPwm_R < 0 ? DianJiPwm_R : 500;
/* 将运算完后的值传输进入PWM输出 */
TIM_SetCompare3(TIM3,DianJiPwm_L);
TIM_SetCompare4(TIM3,DianJiPwm_R);
|
如果发现转的太大或者太小,就可以调节DIANJI的值,时期达到平滑。
四、调试部分
完成以上的步骤之前,需要先对小车的各个模块进行检查,确保上电后能正常使用,随后,需要调整舵机的中值 DUOJIZHONGZHI ,让舵机下面的定向轮朝着黑线方向。随后,调整两个电机的接线等,让电机朝前走即可。
代码的调试,主要的调试有:中断的时间,error的赋值,舵机的DUOJI 转向值,电机的DIANJI转向值等。
调整完就能正常巡线了。
每个人的车的结构都不一样,代码参数也不一样,制作过程需要多理解,多实践,才能更加明白这样使用的原理。
五、代码模板下载
码云:
https://gitee.com/wgjwgj030430/learning
百度网盘:
链接:https://pan.baidu.com/s/1YxLXIUNtS8yhvzVPjmRH2Q?pwd=wugj
提取码:wugj
如若下载不了也可私信我
欢迎大家多多指教!