目录
前言
这学期数字电路课程设计是用Altera的主控芯片是Cyclone IV EP4CE6F17C8的FPGA实现2021年电子设计竞赛C题——双车跟随系统。三个人的电赛小队再加上一个人工专业的外援,四个人肝了五天(因为驱动坏了耽搁了一天多一点),最后做的效果还可以,40s左右的时间跑三圈,比起很多电赛的大佬团队还是差了很多。这块FPGA板子玩得还不够多,那些模块也是现调,然后也没用什么算法(主要是PID等控制算法会涉及到浮点运算,不知道怎么处理会得当一点),加上课设之后就是好几科的期末考,很多粗糙的地方实在没时间再调了,原题里面的专家等停指示也没时间搞了(老师的课设题目里面好像也没要求做)。
总体来说,这次项目虽然有不少遗憾,但也让我学到了不少东西,特别是在FPGA开发方面。希望以后有机会能继续深入,做得更好。
提示:以下是本篇文章正文内容,下面案例可供参考
一、题目介绍
我们课设的题目是根据原题改的,最主要的不同还是主控必须用限制的FPGA开发板和两车之间的通信必须要用红外对管(这部分完全是我通信的队友搞的,效果非常好,基本就是秒传秒启动^~^)。
设计一套小车跟随行驶系统,采用 FPGA 实验板,由一辆领头小车和一辆跟随小车组成,要求小车具有循迹功能,且速度在 0.3 ~ 1m/s 可调,能在指定 路径上完成行驶操作,行驶场地的路径如图 1 所示。
【图1】小车跟随行驶场地示意图
其中,路径上的 A 点为 领头小车每次行驶的起始点和终点。当小车完成一次行驶到达终点,领头小车和跟随小车要发出声音提示。领头小车和跟随小车既可以沿着 ABFDE 圆角矩形 ( 简称为内圈 )路径行驶,也可以沿着 ABCDE 的圆角矩形( 简称为外圈 ) 路径行驶。行驶在内圈 BFD 段时,小车要发出灯光指示。
任务一:从起始位置 A 点开始到停止位置 A 点为止,单独一辆小车能独立 一次性沿着图 1 跑道跑 3 圈,其中第 1、3 圈是内圈,第 2 圈是外圈,中途不能停车调整。
任务二:将领头小车放在路径的起始位置 A 点,跟随 小车放在其后 20cm 处,沿着外圈路径行驶一圈停止:
- 领头小车的平均速度误差不大于 10%;
- 跟随小车能跟随领头小车行驶,全程不能发生小车碰撞;
- 完成一圈行驶后领头小车到达 A 点处停车,跟随小车应及时停止,停止时间差不超过 1s,且与领头小车的间距为 20cm,误差不大于 6cm。
任务三:将领头小车放在路径的起始位置 A 点, 跟随小车放在其后 20cm 处,领头小车和跟随小车连续完成三圈路径的行驶。 第一圈领头小车和跟随小车都沿着外圈路径行驶。第二圈领头小车沿着外圈路径 行驶,跟随小车沿着内圈路径行驶,实现超车领跑。第三圈跟随小车沿着外圈路 径行驶,领头小车沿着内圈路径行驶,实现反超和再次领跑,要求:
- 全程两个小车行驶平稳,顺利完成两次超车,且不能发生小车碰撞;
- 完成三圈行驶后领头小车到达 A 点停止,跟随小车应及时停止,两车停止的时间差不超过 1s,且与领头小车的间距为 20cm,误差不大于 6cm;
- 小车行驶速度可自主设定,但不得低于 0.3m/s,且完成所规定的三圈轨 迹行驶所需时间越短越好;
- 跟随小车只有一个上电开关,接收到前车传来的红外信号才能启动。
由红外信号实现停止,领头小车抓起后,跟随小车依然不动。
二、FPGA和单片机的异同
记得课设前我学长就和我说用FPGA来做小车这样的控制系统,有点大材小用,毕竟FPGA成本更高,更适合用来处理高速实时的任务。也确实是这样的,电赛没怎么见过大佬用FPGA做控制题,隔壁信号题好像才是FPGA的主战场。学长用Xilinx的FPGA做的2024的信号题,效果就蛮好的。
但是从学习的角度讲吧,这个作为一个FPGA在嵌入式系统的小应用,我感觉收获还是蛮大的。本次课设使用了FPGA开发板,我们在实际课程设计中切身体会到了它的特点所在。具体可知FPGA(现场可编程门阵列)和单片机(MCU,微控制器)在结构和应用上有着明显的异同。
2.1 FPGA开发板与单片机的不同点
在工作原理上,FPGA通过硬件电路(逻辑门)并行执行任务,而单片机通过中央处理器(CPU)串行处理指令,具体表现为FPGA的灵活性强于单片机。例如单片机固定的UART外设在需要使用UART协议时,只能选用特定的接口。而FPGA则可以选用两个普通的IO口来模拟RX和TX。
在性能上,FPGA是并行处理,适合高速实时的任务,单片机串行处理,性能较低,适合低功耗任务。一个非常显著的区别在于,FPGA的逻辑是通过硬件电路并行执行的。例如,在用VHDL编程时,代码中的两个process(比如数码管驱动和LED驱动)即使书写上是一个在前,一个在后,但最终会被综合为独立的硬件模块。硬件电路中,这些模块在同一个时钟周期内可以同时工作,因此不会出现“数码管先显示数据再点亮LED”的现象,除非代码中明确加入了延时或同步控制。
而单片机的代码执行逻辑则是顺序执行的。它依赖于CPU逐条指令的执行,因此本质上是串行的逻辑控制。即使单片机具备中断功能(可以暂时中断当前任务处理紧急事件)或有独立的外设(如定时器、PWM),这些只是部分任务的并行化,CPU主程序的大部分运行依然是按照代码从上到下依次执行的。并行处理的特性决定了FPGA更适合高性能的实时应用处理,比如现在很热门的图像处理和通信协议解析,而单片机则适合复杂度较低的控制任务,如家电控制和LoT(物联网)中的传感器数据采集,还有当下单片机应用十分广泛的汽车行业。
2.2 FPGA开发板与单片机的相同点
相同点在于二者都可以应用于嵌入式开发系统,执行特定的任务。例如本次课程设计的《双车跟随系统》,就是一个典型的FPGA应用于嵌入式系统中的小项目。而在电子设计竞赛中要求用载TI芯片的单片机来完成跟随跑、分道跑、超车跑等指定的任务。
其次,两者都可以通过编程来实现功能,FPGA使用Verilog HDL/VHDL等硬件描述语言,单片机通常用C或者汇编语言来编程。
另外,FPGA和单片机开发和设计都是基于硬件的编程,需要硬件基础知识,涉及引脚配置和相关硬件电路的设计。超声波模块支持不同的模式,如GPIO/UART等,大部分单片机拥有UART外设,我们可以通过配置相关特定的引脚来使用超声波模块,也可以选用两个普通的IO口。而FPGA使用的UART资源是USB接口,需要通过数据线上位到电脑来观察数据和现象,在设计中选的是IO口的办法。
三、系统方案
右边两辆是我们用来验收的,双四驱。
【图2】小车组装图
3.1 小车跟随系统的设计方案
设计的整个系统分为主控模块、电机驱动模块、红外通信模块、循迹模块、超声波测距模块、电源模块等组成。
【图3】系统框架图
我们用3S 12V的锂电池给FPGA供电(我懒得去给FPGA配断电不丢失,所以烧录的时候也就是直接上电烧录了),经过驱动模块的稳压和控制后驱动霍尔编码器电机。两车之间的通过红外对管来通信,主要用到通信的地方有两个:一个是前车启动后车,另外一个就是前车停后车立马制动。在中途跑圈的过程中,通过灰度传感器传回来的指示状态控制不同的电机转向,此外,两车通过超声波模块来调整速度,进而调整距离。
3.2 各硬件模块
小车框架:用的是铝板的小车框架,用铜柱隔开两层,下面一层放电池和面包板,空间大点。
电机:四驱,用的是霍尔编码器电机,实际上没调闭环控制,中间的AB相接口没用到。
【图4】电机控制逻辑
主控模块:FPGA开发板。用的时候在Quartus里面把那些复用功能都关了,变成普通的IO口。
电源模块:3S 12V锂电池。实测的时候发现12V电源会比2S 7.4V的锂电池稳定很多。
循迹模块:用的是感为的八路灰度传感器,一直感觉蛮不错的,电赛用的也是这一款。详情看链接
测距模块:用的是HC-SR04,也是电赛的常客了。
通信模块:红外对管(38KHZ载波),这个是我们专业的一次课程任务,感觉都可以单独水一篇了。
小车组装的时候还是有用到面包板和挺多杜邦线的,其实这样还是蛮不好的,下次改到PCB上。
3.3 难点分析
1.第一个就是那个B点的处理,就是判断小车走内圈还是外圈的标志。原先是想着让小车要走外圈的时候,检测到A点指示灯全黑的状态,就开始加速直行;要走内圈的时候,就来个左转。实战发现,这和电量和车头与状态还有很大的关系,电量少一点,车头歪一点,还真不一定能走对圈。某个深夜,我实在受不了了,直接跪在地上,把两辆车经过B点指示灯的大多数状态给记录下来,写到状态机里面。要走外圈的时候,一检测到这些特定状态里面的某一种就直接直行;要走内圈的时候,一检测到就让他左转。虽然方法笨了一些,但是结果就是,内圈外圈一直都是走对的!
2.第二个就是A点的处理,就是判断小车走第几圈的标志。在原题里面是有声光提示这一功能的,但是老师在课设里面给省略了。但是我们还是加上去了,因为这样只要蜂鸣器一响就能知道是第几圈。新的一个问题就是有时候小车明明经过A标志点,但是蜂鸣器不响(没进入计圈状态),我们认为最可能的原因就是8个指示灯全灭的限制太强了(A点是用胶带贴的),直接把限制的数量减少一些,好了很多。还有就是如果小车跑三圈没停的话,要加一个消抖。
3.我队友为了精益求精,直接把超声波测算的距离精确到小数点后两位,很多时候停止时两车的距离是准之又准。原理就是把信号放大一百倍,然后再去计算各个位数(因为Quartus里面好像不支持浮点的运算,我们也没找有没有别人写好的,就算有感觉资源消耗也蛮大的)。
4.红外的话是完全脱手了,是真不知道遇到了啥问题。
四、程序设计
用FPGA来开发的话,我们是写好各个部分代码,然后生成芯片,连接顶层电路,最好再下载到FPGA上,进行调试。主要是三部分:PWM生成和循迹部分、超声波测距部分、红外通信部分。以下以第三问的后车部分代码为例:
4.1 PWM生成和循迹部分
4.1.1 电机控制逻辑
设好各个状态的状态机,电机控制逻辑,基础占空比
-- 电机控制逻辑
process(clk)
begin
if rising_edge(clk) then
case direction is
when "000" =>
-- 直行,电机同向转动
ForwardLeft_INT1 <= '1';
ForwardLeft_INT2 <= '0';
ForwardLeft_PWM <= pwm_signal_left;
ForwardRight_INT1 <= '1';
ForwardRight_INT2 <= '0';
ForwardRight_PWM <= pwm_signal_right;
BackLeft_INT1 <= '1';
BackLeft_INT2 <= '0';
BackLeft_PWM <= pwm_signal_left;
BackRight_INT1 <= '1';
BackRight_INT2 <= '0';
BackRight_PWM <= pwm_signal_right;
when "001" | "100" =>
-- 左转,左侧电机退后,右侧电机前进
ForwardLeft_INT1 <= '0';
ForwardLeft_INT2 <= '1';
ForwardLeft_PWM <= pwm_signal_left;
ForwardRight_INT1 <= '1';
ForwardRight_INT2 <= '0';
ForwardRight_PWM <= pwm_signal_right;
BackLeft_INT1 <= '0';
BackLeft_INT2 <= '1';
BackLeft_PWM <= pwm_signal_left;
BackRight_INT1 <= '1';
BackRight_INT2 <= '0';
BackRight_PWM <= pwm_signal_right;
when "010" | "101" =>
-- 右转,右侧电机退后,左侧电机前进
ForwardLeft_INT1 <= '1';
ForwardLeft_INT2 <= '0';
ForwardLeft_PWM <= pwm_signal_left;
ForwardRight_INT1 <= '0';
ForwardRight_INT2 <= '1';
ForwardRight_PWM <= pwm_signal_right;
BackLeft_INT1 <= '1';
BackLeft_INT2 <= '0';
BackLeft_PWM <= pwm_signal_left;
BackRight_INT1 <= '0';
BackRight_INT2 <= '1';
BackRight_PWM <= pwm_signal_right;
when others =>
-- 停止,停止电机
ForwardLeft_INT1 <= '0';
ForwardLeft_INT2 <= '0';
ForwardLeft_PWM <= '0';
ForwardRight_INT1 <= '0';
ForwardRight_INT2 <= '0';
ForwardRight_PWM <= '0';
BackLeft_INT1 <= '0';
BackLeft_INT2 <= '0';
BackLeft_PWM <= '0';
BackRight_INT1 <= '0';
BackRight_INT2 <= '0';
BackRight_PWM <= '0';
end case;
end if;
end process;
4.1.2 循迹逻辑
循迹就根据实际情况调就可以了,我们的循迹代码蛮丑陋的,但是实际的效果还可以
if (sensors = "11100011" or sensors = "11010111" or sensors = "11101101" or sensors = "11011101" or
sensors = "10110011" or sensors = "11101110" or sensors = "10111101" or sensors = "11011110" or
sensors = "11011101" or sensors = "11010011" or sensors = "11011100" ) and (lap_counter = 3)
then
direction <= "100"; -- 左转
-- 检测直行(黑线在中间)
elsif
(sensors(3) = '0' and sensors(4) = '0') or
sensors = "11110111" or
sensors = "11101111" then
direction <= "000"; -- 直行
-- 检测左小转
elsif
(sensors(2) = '0' and sensors(3) = '0') or
(sensors(1) = '0' and sensors(2) = '0') or
sensors = "11111101" then
direction <= "001"; -- 左小转
-- 检测左大转
elsif (sensors(0) = '0' and sensors(1) = '0') or
(sensors(0) = '0' and sensors(2) = '0') or
sensors = "11111110" then
direction <= "100"; -- 左大转
-- 检测右小转
elsif (sensors(4) = '0' and sensors(5) = '0') or
(sensors(5) = '0' and sensors(6) = '0') or
sensors = "10111111" then
direction <= "010"; -- 右小转
-- 检测右大转
elsif (sensors(6) = '0' and sensors(7) = '0') or
(sensors(5) = '0' and sensors(7) = '0') or
sensors = "01111111" then
direction <= "101"; -- 右大转
-- 检测无黑线
elsif sensors = "11111111" then
direction <= "000"; -- 停止
-- 默认直行
else
direction <= "000"; -- 前进
end if;
4.1.3 圈数更新逻辑
-- 圈数更新逻辑
process(clk, reset)
begin
if reset = '1' then
lap_counter <= 0; -- 复位圈数计数器
lap_detected <= '0'; -- 清除圈检测标志位
stop_flag <= '0'; -- 清除停车标志位
debounce_counter <= 0; -- 复位去抖动计时器
debounce_active <= '0'; -- 清除去抖动标志位
Buzzer <= '1';
elsif rising_edge(clk) then
-- 去抖动逻辑
if debounce_active = '1' then
if debounce_counter < 5000000 then -- 等待指定的时钟周期数
debounce_counter <= debounce_counter + 1;
else
debounce_counter <= 0; -- 计时器复位
debounce_active <= '0'; -- 清除去抖动标志位
end if;
else
-- 如果没有检测到圈并且传感器条件满足,则增加圈数并设置标志位
if lap_detected = '0' and sensors = "00000000" then
if lap_counter < 4 then -- 只有当圈数小于4时才增加圈数
lap_counter <= lap_counter + 1;
lap_detected <= '1'; -- 设置圈检测标志位
debounce_active <= '1'; -- 激活去抖动
Buzzer <= '0';
end if;
-- 当传感器不再满足条件时,清除标志位
elsif not(sensors(1) = '0' and sensors(2) = '0' and sensors(3) = '0' and sensors(4) = '0' and sensors(5) = '0' and sensors(6) = '0') then
lap_detected <= '0';
Buzzer <= '1';
end if;
end if;
-- 达到三圈后设置停车标志
if lap_counter = 4 then
stop_flag <= '1';
Buzzer <= '0';
lap_counter <= lap_counter + 1;
end if;
if lap_counter = 5 then
lap_counter_out<='0';
else
lap_counter_out<='1';
end if;
end if;
end process;
4.1.4 动态调整逻辑
这部分就根据与20cm距离的远近来调整占空比就可以了,超声波计算得到两车距离,反馈给主控进行速度控制。距离超过20cm,后车加速,小于20cm,后车减速。
4.2 超声波测距部分
超声波部分在上一篇已经介绍了
4.3 红外通信模块
这一部分完全是队友做的,没征得同意不好发出来,有机会再更一篇原理分析+代码