2016年,曾经尝试用51单片机制作一台循迹避障小车,已经准备好了大部分零件,也组装好了车身,试机时发现两个轮子的转速不一样,而且转速太快导致小车在瓷砖地面上打滑,当时一筹莫展,只好就此搁置. 这反映了我在硬件选型上的失误(当然自身水平也很有限哦). 前几天看了一篇安卓控制Arduino小车的文章 <<手机控制蓝牙小车>> ,这篇文章简要介绍了使用Ardiuno制作小车并用APP控制小车的基本流程,可以看出使用Arduino制作控制小车要容易很多,因为它的很多功能硬件和软件都已经模块化, 每个模块对应一块PCB和一个标准化的软件库/接口,这样编写程序就清晰很多,有问题也比较容易调试。
下面就介绍一下嵌入式系统的硬件选型, 嵌入式系统是指以应用为中心、以计算机技术为基础,软件硬件可裁剪,适应应用系统对功能、可靠性、成本、体积和功耗严格要求的专用计算机系统,我们就从功能、可靠性、成本、体积和功耗来分析智能小车的硬件选型:
功能:
能够实现循迹、避障,手机通过蓝牙可以操控小车,而Arduino UNO的基本指标如下
Arduino Uno
- 处理器: ATmega328 (8位 CPU, 16MHz 时钟频率, 2KB SRAM, 32KB 闪存)
- 特性: 14 个数字 I/O 口, 6 个模拟输入口, 6个PWM口,可更换处理器设计
- 尺寸: 75 x 55mm
- 价格: $30
这是我目前程序使用的Flash和内存空间的情况
项目使用了 7168 字节,占用了 (22%) 程序存储空间。最大为 32256 字节。
全局变量使用了388字节,(18%)的动态内存,余留1660字节局部变量。最大为2048字节。
显然它能够配合相关模块和程序实现功能
可靠性:Arduino Uno采用标准化的设计和制造,可靠性比较高
成本:Arduino Uno成本显然是比较低的,一般人都可以负担得起
体积:75 x 55mm的面积对智能小车来说显然是能够承受的
功耗:智能小车的功耗主要是电机驱动和运转产生的功耗,Arduino Uno功耗所占比重是很低的,而且现在可以使用18650高容量电池
这里转载一篇有关的文章
嵌入式系统硬件设计流程
1)详细理解设计需求,从需求中整理出电路功能模块和性能指标要求。
2)根据功能和性能需求制定总体设计方案,对CPU进行选型,CPU选型有以下几点要求:
a)性价比高;
b)容易开发:体现在硬件调试工具种类多,参考设计多,软件资源丰富,成功案例多;
c)可扩展性好。
3)针对已经选定的CPU芯片,选择一个与我们需求比较接近的成功参考设计,一般CPU生产商或他们的合作方都会对每款CPU芯片做若干款开发板进行验证,比如AMCC的PPC440EP就有yosemite和bamboo两款开发版。厂家最后公开给用户的参考设计图虽说不是产品级的东西,也应该是经过严格验证的,否则将会影响到他们的芯片推广应用。纵然参考设计的外围电路有可推敲的地方,CPU本身的管脚连接使用方法也绝对是值得我们信赖的,当然如果万一出现多个参考设计某些管脚连接方式不同的情况,我们可以细读CPU芯片手册和勘误表,或者找厂商确认。另外在设计之前,最好能外借或者购买一块选定的参考板进行软件验证,如果软件验证没有问题,那么硬件参考设计也是可以信赖的。但要注意一点,现在很多CPU都有若干种启动模式,我们要自己选一种最适合的启动模式,或者做成兼容设计。
4)根据需求对外设功能模块进行元器件选型,元器件选型应该遵守以下原则:
a)普遍性原则:所选的元器件要是被广泛使用验证过的,尽量少使用冷门、偏门芯片,减少开发风险。
b)高性价比原则:在功能、性能、使用率都相近的情况下,尽量选择价格比较好的元器件,降低成本。
c)采购方便原则:尽量选择容易买到、供货周期短的元器件。
d)持续发展原则:尽量选择在可预见的时间内不会停产的元器件。
e)可替代原则:尽量选择pin to pin兼容芯片品牌比较多的元器件。
f)向上兼容原则:尽量选择以前老产品用过的元器件。
g)资源节约原则:尽量用上元器件的全部功能和管脚。
5)对选定的CPU参考设计原理图外围电路进行修改。修改时对于每个功能模块都要找至少3个相同外围芯片的成功参考设计,如果找到的参考设计连接方法都是完全一样的,那么基本可以放心参照设计,但即使只有一个参考设计与其他的不一样,也不能简单地按少数服从多数的原则,而是要细读芯片数据手册,深入理解那些管脚含义,多方讨论,联系芯片厂技术支持,最终确定科学、正确的连接方式,如果仍有疑义,可以做兼容设计。这是整个原理图设计过程中最关键的部分,我们必须做到以下几点:
a)对于每个功能模块要尽量找到更多的成功参考设计,越难的应该越多,成功的参考设计是“前人”的经验和财富,我们理当借鉴吸收,站在“前人”的肩膀上,也就提高了自己的起点。
b)要多向权威请教、学习,但不能迷信权威,因为人人都有认知误差,很难保证对哪怕是自己最了解的事物总能做出最科学的理解和判断,开发人员一定要在广泛调查、学习和讨论的基础上做出最科学正确的决定。
c)如果是参考已有的老产品设计,设计中要留意老产品有哪些遗留问题,这些遗留问题与硬件哪些功能模块相关,在设计这些相关模块时要更加注意推敲,不能机械照抄原来设计,比如我们老产品中的IDE经常出问题,经过仔细斟酌,广泛讨论和参考其他成功设计,发现我们的IDE接口有两个管脚连线方式确实不规范。还有,针对FGPI通道丢失视频同步信号的问题,可以在硬件设计中引出硬件同步信号管脚,以便进一步验证,更好发现问题的本质。
6)硬件原理图设计还应该遵守一些基本原则,这些基本原则要贯彻到整个设计过程,虽然成功的参考设计中也体现了这些原则,但因为是“拼”出来的原理图,所以我们还要随时根据这些原则来设计审查原理图,这些原则包括:
a)数字电源和模拟电源分割。
b)数字地和模拟地分割,单点接地,数字地可以直接接机壳地(大地),机壳必须接地,以保护用护人身安全。
c)保证系统各模块资源不能冲突,例如:同一I2C总线上的设备地址不能相同等。
d)阅读系统中所有芯片的手册(一般是设计参考手册),看它们未用的输入管脚是否需要做外部处理,是要上拉、下拉,还是悬空,如果需要上拉或下拉,则一定要做相应处理,否则可能引起芯片内部振荡,导致芯片不能正常工作。
e)在不增加硬件设计难度的情况下尽量保证软件开发方便,或者以较小的硬件设计难度来换取更多方便、可靠、高效的软件设计,这点需要硬件设计人员懂得底层软件开发调试,要求较高。
f)功耗问题,设计时尽量降低功耗。
g)产品散热问题,可以在功耗和发热较大的芯片增加散热片或风扇,产品机箱也要考虑这个问题,不能把机箱做成保温盒,电路板对“温室”是感冒的。还要考虑产品的安放位置,最好是放在空间比较大,空气流动畅通的位置,有利于热量散发出去。
7)硬件原理图设计完成之后,设计人员应该按照以上步骤和要求首先进行自审,自审后要有95%以上的把握和信心,然后再提交给他人审核,其他审核人员同样按照以上要求对原理图进行严格审查,如发现问题要及时进行讨论分析,分析解决过程同样遵循以上原则和步骤。
8)只要开发和审核人员都能够严格按照以上要求进行电路设计和审查,我们就有理由相信,所有硬件开发人员设计出的电路板第一版成功率都会很高的,所以我提出以下几点:
a)设计人员自身应该保证原理图的正确性和可靠性,要做到设计即是审核,严格自审,不要把希望寄托在审核人员身上,设计出现的任何问题应由设计人员自己承担,其他审核人员不负连带责任。
b)其他审核人员虽然不承担连带责任,也应该按照以上要求进行严格审查,一旦设计出现问题,同样反映了审核人员的水平、作风和态度。
c)普通原理图设计,包括老产品升级修改,原则上要求原理图一版成功,最多两版封板,超过两版将进行绩效处罚。
d)对于功能复杂,疑点较多的全新设计,原则上要求原理图两版内成功,最多三版封板,超过三版要进行绩效处罚。
e)原理图封板标准为:电路板没有任何原理性飞线和其他处理点。
9)制定上述规范的目的和出发点是为了培养硬件开发人员严谨、务实的工作作风和严肃、认真的工作态度,增强他们的责任感和使命感,提高工作效率和开发成功率,保证产品质量。希望年轻的硬件开发人员能在磨练中迅速成长起来。
对于复杂的PCB板设计,如高频多层板一般都请专人布线,因为复杂的PCB板涉及电磁兼容和电磁干扰方面的问题,这是一门高深的学问,除了一些基本的注意事项外还有一些特殊的防护措施,比如设计时使用一些专门的电磁兼容芯片等,感兴趣的读者可以看一些电磁理论方面的书籍。
这是我初步完成的小车外观(暂时使用DC12V电源):
附录:小车源代码, 源文件、安卓蓝牙遥控程序和相关资料参见https://pan.baidu.com/s/1WyoaKsiQ_LMk6e_UdRCs6w
1 #include <SoftwareSerial.h> 2 #include <Servo.h> 3 4 #define IN1 10 5 #define IN2 11 6 #define IN3 12 7 #define IN4 13 //电机驱动脚 8 9 #define ENA 5 10 #define ENB 6 //PWM调速脚 11 12 #define TrigPin 2 13 #define EchoPin 4 //超声波测距脚 14 15 #define sensorPin1 0 16 #define sensorPin2 1 17 #define sensorPin3 2 18 #define sensorPin4 3 // 红外循迹探头,使用A0~A3模拟输入引脚 19 20 Servo myservo; // 创建舵机对象来控制舵机 21 SoftwareSerial BT(8, 9); //软串口,蓝牙接口 22 char Direction; // 用来存储蓝牙发送过来的指令 23 int pos = 90; // 用来存储舵机位置的变量 24 float dist,dist1,dist2; 25 bool mode = 0; // mode等于零时为手动模式,等于一时为循迹或避障模式 26 27 void Forward() // 前进 28 { 29 MotorA(001, 150); 30 MotorB(001, 156); 31 } 32 void Backward() // 后退 33 { 34 MotorA(002, 150); 35 MotorB(002, 156); 36 } 37 void TurnLeft() // 向左转 38 { 39 MotorA(004, 150); 40 MotorB(004, 150); 41 } 42 void TurnRight() // 向右转 43 { 44 MotorA(005, 150); 45 MotorB(005, 150); 46 } 47 void Stop() // 停止 48 { 49 MotorA(003,0); 50 MotorB(003,0); 51 delay(100); 52 } 53 float Distance() //超声波测距 54 { 55 float distance; // 用来存储小车距周边障碍物距离 56 digitalWrite(TrigPin, LOW); 57 delayMicroseconds(2); 58 digitalWrite(TrigPin, HIGH); //高低电平发一个短时间脉冲去TrigPin 59 delayMicroseconds(10); 60 digitalWrite(TrigPin, LOW); 61 distance = pulseIn(EchoPin, HIGH) / 58.0; //采样高电平宽度并折算成距离 62 return distance; 63 } 64 void MotorA(char Action, int speed1) { // 电机A在不同运动模式下的设定 65 if (Action == 001) { 66 digitalWrite(IN1, HIGH); 67 digitalWrite(IN2, LOW); 68 analogWrite(ENA, speed1); 69 } 70 if (Action == 002) { 71 digitalWrite(IN1, LOW); 72 digitalWrite(IN2, HIGH); 73 analogWrite(ENA, speed1); 74 } 75 if (Action == 003) { 76 digitalWrite(IN1, LOW); 77 digitalWrite(IN2, LOW); 78 } //001正转,002反转,003停止,下同 79 if (Action == 004) { 80 digitalWrite(IN1, HIGH); 81 digitalWrite(IN2, LOW); 82 analogWrite(ENA, speed1); 83 } 84 if (Action == 005) { 85 digitalWrite(IN1, LOW); 86 digitalWrite(IN2, HIGH); 87 analogWrite(ENA, speed1); 88 } //004前进左转,005前进右转,下同 89 if (Action == 006) { 90 digitalWrite(IN1, LOW); 91 digitalWrite(IN2, LOW); 92 } 93 if (Action == 007) { 94 digitalWrite(IN1, LOW); 95 digitalWrite(IN2, HIGH); 96 analogWrite(ENA, speed1); 97 } //006后退左转,007后退右转,下同 98 } 99 100 void MotorB(char Action, int speed2) { // 电机B在不同运动模式下的设定 101 if (Action == 001) { 102 digitalWrite(IN3, HIGH); 103 digitalWrite(IN4, LOW); 104 analogWrite(ENB, speed2); 105 } 106 if (Action == 002) { 107 digitalWrite(IN3, LOW); 108 digitalWrite(IN4, HIGH); 109 analogWrite(ENB, speed2); 110 } 111 if (Action == 003) { 112 digitalWrite(IN3, LOW); 113 digitalWrite(IN4, LOW); 114 } //001正转,002反转,003停止,下同 115 if (Action == 004) { 116 digitalWrite(IN3, LOW); 117 digitalWrite(IN4, HIGH); 118 analogWrite(ENB, speed2); 119 } 120 if (Action == 005) { 121 digitalWrite(IN3, HIGH); 122 digitalWrite(IN4, LOW); 123 analogWrite(ENB, speed2); 124 } //004前进左转,005前进右转,下同 125 if (Action == 006) { 126 digitalWrite(IN3, LOW); 127 digitalWrite(IN4, HIGH); 128 analogWrite(ENB, speed2); 129 } 130 if (Action == 007) { 131 digitalWrite(IN3, LOW); 132 digitalWrite(IN4, LOW); 133 } //006后退左转,007后退右转,下同 134 } 135 void setup() { 136 Serial.begin(9600); 137 BT.begin(9600); 138 pinMode(TrigPin, OUTPUT); 139 pinMode(EchoPin, INPUT); 140 for (int i = 10; i <= 13; i++){ 141 pinMode(i, OUTPUT); 142 } 143 myservo.attach(3); // 把连接在引脚3上的舵机赋予舵机对象 144 145 myservo.write(85); //设置舵机初始位置 146 delay(1000); 147 } 148 149 void loop() { 150 if (mode == 0) // 小车默认为手动模式并读取蓝牙输入,在循迹或避障模式设定后不再读取蓝牙输入 151 Direction = BT.read(); 152 153 switch (Direction) { 154 case 'A': // 高速前进 155 MotorA(001, 180); 156 MotorB(001, 188); 157 break; 158 159 case 'C': // 前进 160 MotorA(001, 150); 161 MotorB(001, 156); 162 break; 163 164 case 'E': // 高速后退 165 MotorA(002, 180); 166 MotorB(002, 188); 167 break; 168 169 case 'G': // 后退 170 MotorA(002, 150); 171 MotorB(002, 156); 172 break; 173 174 case 'Z': // 停止 175 MotorA(003,0); 176 MotorB(003,0); 177 break; 178 179 case 'L': // 向前左转 180 MotorA(004, 150); 181 MotorB(004, 150); 182 break; 183 184 case 'R': // 向前右转 185 MotorA(005, 150); 186 MotorB(005, 150); 187 break; 188 189 case 'M': // 向后左转 190 MotorA(006, 0); 191 MotorB(006, 150); 192 break; 193 194 case 'N': // 向后右转 195 MotorA(007, 150); 196 MotorB(007, 0); 197 break; 198 199 case 'P': // 避障模式 200 mode = 1; 201 Direction = 'P'; 202 dist = Distance(); // 读取前方距离
205 if (dist < 15 ) 206 { 207 Backward(); 208 delay(100); 209 Stop(); //距离障碍物小于15cm时立即后退并刹车,然后测量左右距离
210 myservo.write(0); 211 delay(300); 212 dist1 = Distance();
215 myservo.write(180); 216 delay(600); 217 dist2 = Distance();
220 myservo.write(85); // 测量完后舵机回归正前方位置 221 delay(300); 222 if (dist1 > dist2) { // 向远离障碍物的方向转弯后继续前进 223 TurnRight(); 224 delay(150); 225 Forward(); 226 } 227 else { 228 TurnLeft(); 229 delay(150); 230 Forward(); 231 } 232 } 233 else if (dist > 300) // 若测量数据过大为异常情况,此时尝试调整车身方位,此处程序为实验性质,可能不是最好的方案 234 { 235 TurnLeft(); 236 delay(200); 237 Forward(); 238 } 239 else 240 { 241 Forward(); // 正常情况向前 242 } 243 break; 244 245 case 'Q': // 循迹模式 246 mode = 1; 247 Direction = 'Q'; 248 int num1 = analogRead(sensorPin1); // 收集四个红外感应头的输出电压
251 int num2 = analogRead(sensorPin2);
254 int num3 = analogRead(sensorPin3);
257 int num4 = analogRead(sensorPin4);
260 if (num1 > 500) { // 最左侧的感应头在跑道上方,车身应向左转 261 TurnLeft(); 262 delay(100); 263 Stop(); 264 } 265 else if (num4 > 500) { // 最右侧的感应头在跑道上方,车身应向右转 266 TurnRight(); 267 delay(100); 268 Stop(); 269 } 270 else if (num2 > 500 || num3 > 500) // 中间两个感应头至少有一个在跑道上方则继续前进 271 Forward(); 272 else { 273 Stop(); //注意发生此种情况往往是因为小车冲出跑道, 274 Backward(); //因此往后退100毫秒,这样一般都能重新 275 delay(100); //找回跑道. 276 Stop(); 277 } 278 } 279 }