RC 汽车控制编程
RC 汽车的高层设计,

引言
这辆车是一个玩具,上面载有一个摄像头,通过 PC 上的软件控制,可以被驾驶到用户感兴趣的地方。它的运动由两个直流电机(每侧一个)控制,这两个电机的速度通过齿轮箱减速。我在这里将不讨论这辆车的机械和电子部分的细节,当然也包括 PC 上软件的视频显示部分。因此,车子的输入和输出模块仅表示为方框。这是我用 Google SketchUp 绘制的草图。

在下图中,整个项目被分解为模块。

背景
正如我在上图所示,该项目主要分为两部分:PC 和车。它们之间有一个 RF 链路用于传输命令,以及一个单独的无线视频链路(此处未显示)用于视频传输。我将 RF 链路设置为单向通信,因为我找到的 RF 组件支持这种操作模式。
在图的左上角,您可以看到一个红外 LED 和一个红外传感器。这些是用于避免与障碍物碰撞的电路。电脉冲会定期发送以打开和关闭红外 LED,并且在单个脉冲周期内会读取两次读数,这样控制器就能“知道”红外传感器的实际读数。这是通过下表中描述的简单逻辑实现的。
输出记录在 |
推断 |
|
开启脉冲时 |
关闭时间 |
|
1 |
0 |
有障碍物 |
1 |
1 |
传感器被环境光饱和 |
0 |
0 |
没有障碍物 |
0 |
1 |
此读数不合逻辑(传感器错误) |
您可以参考 这里 关于构建简单红外传感器电路。
接下来是 RF 接收器。命令不断地从连接到 PC 串行端口的 RF 发射器以一系列位的形式传输,并由位于车上的 RF 接收器接收。我创建了一个简单的 C# 应用程序用于命令位生成以及视频显示和回放功能。SerialPort
类负责根据用户的输入发送这些字节序列。RF 发射器从串行端口接收这些数据,并通过其天线进行适当的调制进行传输。我使用了 Linx 的 IC 来创建我的车和 PC 之间的 RF 链路。可以在 这里 和 这里 找到发射器和接收器的数据手册及其应用。
为了控制我的 RC 车的直流电机,我使用了两个 H 桥,如前所述。这两个 H 桥由微控制器通过并行端口的某个引脚的输出驱动。可以在 这里 找到关于 H 桥的非常好介绍和应用。
Using the Code
PC 程序
PC 程序有两个主要部分:视频显示和回放,以及一个车控制模块。
视频显示和回放模块使用 DirectShow
库来访问来自无线视频接收器的视频数据,但由于这部分软件不是我的关注点,我暂不讨论。作为参考,您可以查看 Andrew Krillov 在 CodeProject 上一篇非常好的文章(这里)。
车控制模块是使用 SerialPort
对象实现的,七个用于方向控制的按钮,以及一个用于列出可用串行端口的组合框。请记住包含以下命名空间:
using System.IO.Ports;
在窗体加载事件处理程序中,我将所有可用端口提取到组合框中
// create an array for getting available port on my PC
string[] availablePorts;
// fetch them all
availablePorts = SerialPort.GetPortNames();
// copy them to a comboBox
for (int i = 0; i < availablePorts.Length; i++)
{
cboPort.Items.Add(availablePorts[i]);
}
每次单击按钮时,都会调用 SendToSerialPort(string data)
方法,并将其各自的 string
参数传递进去,如下所述。
private void SendToSerialPort(string data)
{
// create an instance of serial port
SerialPort ser = new SerialPort();
byte[] command = new byte[2];
command[0] = (byte)Convert.ToByte(data, 16);
// set its properties
// i preferred the ff values
ser.BaudRate = 9600;
ser.DataBits = 8;
ser.Parity = Parity.None;
ser.StopBits = StopBits.One;
ser.PortName = cboPort.Text;
// if our command array is not empty then...
if (command != null)
{
// open it if it is closed
if (!ser.IsOpen)
ser.Open();
// write the byte
ser.Write(command, 0, 1); // this sends only a byte to the port
// then close it
ser.Close();
}
}
电机驱动器(H 桥)连接到微控制器的 Port1
,排列如下:
- Port1 引脚 0 和 1 – 用于右手电机驱动器(H 桥),以及
- Port1 引脚 4 和 5 – 用于左手电机驱动器(H 桥)
方向控制是通过控制两个直流电机的方向来实现的。例如,要向右转,我们向前驱动左侧电机,并停止右侧电机。单个电机的方向是通过切换四个晶体管的开启和关闭来控制的,这由花括号中的字节值表示。这使我们能够决定要从应用程序发送到串行端口,然后再发送到 RC 车的位序列。因此,前进(“00100010”)、后退(“00010001”)、右转(“00100000”)、左转(“00000010”)、停止(“00000000”)、顺时针旋转(“00100001”)和逆时针旋转(“00010010”)。
微控制器程序
在深入研究代码之前,让我们先看看整个程序的指令流程。

初始化端口后,会读取红外传感器作为红外传感器输出调试的参考,并将其保存为一个变量。然后,红外 LED 会被点亮,红外传感器的点亮状态读数被保存为第二个变量。接下来是一系列对这两个读数的比较,然后采取适当的行动。
如果关闭状态读数是高电平,则要么是传感器被高环境光饱和的问题,要么是传感器本身的问题,这需要通过查看点亮状态的读数来确定(参见上表)。对于每种情况,都会打开相应的调试 LED(高环境光和传感器错误 LED),程序会跳转到下一步。
如果关闭状态读数是低电平,则不存在上述问题。因此,如果第二个(点亮状态)读数是高电平,则传感器前方肯定有障碍物,两个电机都会向后驱动一秒钟。如果点亮状态读数是低电平,则传感器前方没有任何东西,程序会转为接收来自 RF 接收器的命令,并将它们发送到直流电机。
这个过程会无限重复,所以我把代码放在了一个 while(1)
循环中。
在完成每次端口读写操作后,我希望等待一段时间。因此,我在程序中使用了 50ms 的延迟函数 delay_50ms(void)
,该函数使用微控制器自身的定时器实现。此处假设微控制器频率为 12MHz,振荡周期为 12。要等待的确定时间(50ms 的整数倍)作为参数传递给 wait(int sec)
函数。
void delay_50ms(void)
{
// Configure Timer 0 as a 16-bit timer
TMOD &= 0xF0; // Clear all T0 bits (T1 left unchanged)
TMOD |= 0x01; // Set required T0 bits (T1 left unchanged
ET0 = 0; // No interrupts
// Values for 50 ms delay
TH0 = 0x3C; // Timer 0 initial value (High Byte)
TL0 = 0xB0; // Timer 0 initial value (Low Byte)
TF0 = 0; // Clear overflow flag
TR0 = 1; // Start timer 0
while (TF0 == 0); // Loop until Timer 0 overflows (TF0 == 1)
TR0 = 0; // Stop Timer 0
}
以下是(假定为 9600 波特率、无校验位和 1 个停止位)串行端口(微控制器)的初始化函数:
// serial port initializing function
void serial_init(void)
{
TMOD = 0x20; // T1 in mode 2, 8-bit auto reload
SCON = 0x50; // 8-bit data, none parity bit, 1 stop bit
TH1 = 0xFD; //12MHz freq. 12 osc. cycle and 9600 baud rate
TL1 = 0xFD;
TR1 = 1; // Run the timer
}
从串行端口读取命令的任务由以下方法管理,该方法返回读取到的 char
值。
// serial port reading function
unsigned char serial_read(void)
{
bit over = 0;
while(!RI || !over)
{
wait(500);
over = 1;
RI = 0;
return SBUF;
}//wait some time till received flag is set and read the buffer
}
所有其他内容都在程序的 main 函数中处理,这里提供了 main( void )
函数和 wait(int sec)
函数(负责根据输入参数 sec 设置的延迟时间来延迟程序执行)。
// some 'sec' milliseconds wait function
void wait (int sec)
{
unsigned int i;
for ( i = 0; i < (sec / 50); i++ )
{
delay_50ms();
}
}
//here goes the main function
void main( void )
{
P0 = 0; // initialize P0
P1 = 0; // initialize P1
P2 = 0; // initialize P2
while(1)
{
unsigned char val = 0x00;
unsigned char var1 = 0x00;
unsigned char var2 = 0x00;
var1 = P2; //read IR sensor
wait(50); // delay
P2 = num[1]; //turn IR LED ON
wait(200); // delay
var2 = P2; //read IR sensor again
wait(50); // delay
P2 = num[0]; //turn IR LED OFF
if(var1 == num[2])
{
if(var2 == num[1])
P0 = num[2]; //Set sensor error flag
if(var2 == num[3])
P0 = num[1]; //Set high ambiet light flag
serial_init();
val = serial_read(); //Read the serial port
P1 = val; //Command motors
}
if(var1 == num[0])
{
if(var2 == num[3])
{
P1 = num[4]; //drive motors backward
wait(1000); //delay for a second
P1 = num[0];
}
if(var2 == num[1])
{
serial_init();
val = serial_read(); //Read the serial port
P1 = val; //Command the motors
}
P0 = num[0]; //Set the flags to zero
}
}
}
摘要
我使用以下图表编译了上述 C 代码并在 Proteus 仿真程序中进行了模拟:

为了模拟目的,我使用了虚拟串行端口驱动程序(它有 14 天的评估期,可以在 这里 找到免费试用下载)来创建一个虚拟端口对,并将我的 PC 软件连接到其中一个 COM 对的端点,以及 Proteus 仿真程序的 COMPIM 串行端口连接到另一个端点。底部的两个 H 桥由 NPN 和 PNP BJT 晶体管制成。
就是这样。如果您需要更多细节,请告诉我!
历史
- 文章提交 - 2010 年 11 月 12 日
- 仅进行了一些语法编辑 - 2012 年 1 月 10 日