在监控和控制系统中嵌入 Linux

作者:Rick Brown

这个项目的目标车辆是一辆老式城际交通巴士(想想灰狗巴士),它的仪表盘很简陋,而且大部分都不能用。速度计电缆在 40 英尺长的电缆中的某个地方扭断了,燃油传感器也早就沉没了。我想要的是一个更符合现代 практики 的仪表盘。

为了弥合差距,我使用了一台运行 Fedora 20-KDE Linux 发行版的笔记本电脑作为主机,三个数字信号处理器板作为硬件接口处理器 (HIP),一个 USB/RS422 转换器连接到连接 HIP 的 RS422 环路,以及一些我称之为车辆监控和控制应用程序的软件。

系统架构

HIP 基于信号处理器芯片,用 C 语言编程,除了一个心跳 LED 指示处理器在一定程度上工作之外,没有用户界面。HIP 为模拟输入缩放提供信号调理电路,为控制信号提供光隔离,以及一些特殊功能,如热电偶转换器和压力传感器芯片。还有两对 RS422 接收器/发射器。一对连接上行网络(朝向主机),另一对连接下行网络(朝向其他 HIP)。

此应用程序的工作方式是,消息由主机处理器发起,并向下环路传输到第一个 HIP。在那里,消息可能会在 HIP 程序控制下被修改,并向下环路中继到下一个 HIP。“环路”中的最后一个 HIP 在物理跳线控制下向上环路传输其消息。更靠近主机的处理器只是将来自“环路”下方的消息向上传递。主机是其发起的消息的最终接收者。

消息由一个 SOM 字节、一个带有确认位的地址字节、一个命令字节、四个数据字节和两个 CRC 字节组成。向下环路传输时,HIP 以字符为单位中继消息,每个 HIP 延迟一个字符。消息的接收者设置确认位,并动态插入或提取数据。因此,在像这里这样的短环路中,主机在完成发送原始消息之前就开始接收来自网络的响应。对于这个环路,通信速率被任意选择为 57,600 波特,因此环路消息时间为 (9 + 3)/57600 或 208 微秒。图 1 的左侧部分描绘了环路拓扑。

图 1. 系统架构

车辆监控和控制 (VMC) 应用程序将发起诸如“HIP1 设置或获取寄存器 whatever”之类的消息。首先,我将展示如何在 Linux 机器上设置开发环境,然后我将讨论如何使用那里提供的工具来编织一个执行 VMC 应用程序的 Linux 实时应用程序。

设置开发环境

我选择的开发环境是 KDE 的 KDevelop,工具包是 Qt Project 的 Qt。第一步是获取开发环境,然后构建一个“Hello World”应用程序。所以,这是在 Fedora 世界中


yum install kdevelop 
yum install qt 
yum install gcc 

...以及很多其他东西。如果您还没有启动并运行 Qt,预计要花一些时间。

当您加载 KDevelop 时,单击“会话”→“开始新会话”→“新建项目”。这将是“Qt”和“图形化”。起一个名字(例如 VMC),接受默认值,很快您将有机会“构建”然后“执行”。在“执行”时,“启动配置”对话框将使您能够选择您的项目名称,“添加新”和您的项目。单击一两次后,您应该会在屏幕上看到一个基本的“Hello World”窗口。您可以将其扩展到您的实时应用程序中。

您看到的“Hello World”是一个 Qt 应用程序。Qt 是一个优秀的工具包,它健壮且文档齐全——除了一些怪癖之外。新的项目构建过程创建一个目录结构,其中包括 ~/projects/VMC/build。为了最大限度地减少难以诊断的 Qt 构建错误,请将所有源文件和头文件保存在 ~/projects/VMC 中,直到您知道如何执行其他操作为止。~/projects/VMC/build 目录是 KDevelop 的执行目录。运行目录文件应驻留在此处。当您添加源文件和库时,您必须保持 ~/projects/VMC/CmakeLists.txt 为最新。

构建应用程序

以下是如何使用 Linux 环境中可用的工具来创建 VMC 应用程序。首先是通信。对于您的应用程序,通信环路看起来像一个文件流,如下所示创建


int hNet = open("/dev/ttyUSB0", O_RDWR);

或 /dev/ttyUSBwhatever,具体取决于您的系统中正在发生的其他情况。

现在您可以 read()write() hNet,USB<->RS422 转换器将您连接到环路。写入没有问题,直到环路速度(在本例中为 57600/9 = 6400 消息/秒),因此这是您的写入 (hNet,...) 速度限制。读取是另一回事,因为 read(hNet,...) 是一个阻塞操作。进行该调用的进程会一直卡在那里,直到有数据到达。因此,您希望从一个进程(线程)中进行 read(hNet,...) 调用,该进程的唯一任务是捕获传入的字符,然后将它们放入缓冲区中,供其他进程在需要时使用——最简洁地说,在缩写代码中


//A thread to perform the read(hNet,...) function
class COMthread : public Qthread
{
Q_OBJECT    //Notice use of the Qt tools
protected:
     //Start point when myThread->start(); is called
void run() 
{
while (1)
{
pthread_mutex_t mutex1\
         = PTHREAD_MUTEX_INITIALIZER;
//Lock out other processes while working
pthread_mutex_lock( &mutex1 );
          -manipulate shared variables here-
//unlock for other processes during read(hNet,...
pthread_mutex_unlock( &mutex1 );

//This is where this thread spends
read(hNet, Buf, BUF_SIZE);/////////
//99.99 (+/-) percent of its time

//Now lock while updating for new data
pthread_mutex_lock( &mutex1 );
         -buffer data and update pointers-
pthread_mutex_unlock( &mutex1 );
}
}
};

要激活该代码,您在 VMC 构造函数中的语句是


COMthread   *gCOMgo = new COMthread;
gCOMgo->start();

该环路数据获取的补充是一个字符获取例程,它在其他一些进程下运行。该例程使用自己的互斥锁,从上面线程源缓冲的缓冲区中提取数据。

现在您可以发送和接收通过环路的数据,让我们看看应用程序如何与硬件交互。

图 2 显示了在驾驶员视野中安装的视频显示器上看到的仪表盘显示。

图 2. 仪表盘显示

转速表和速度显示数据来自 HIP 中的定时器寄存器,该 HIP 正在定时轴旋转之间的周期。下面的五个指示器来自各种 HIP 中的 A/D 寄存器。这七个数据项是通过向环路发送七个九字符数据请求消息并解码返回的 63 个字符 (7X9) 来收集的。下面是一个 4X4 键盘的部分填充地图的表示,该键盘由其中一个 HIP 提供服务。该地图上表示的每个键都会查询负责物理键盘的 HIP,以查看其键是否是最后按下的键。它会得到是或否的响应。

当您使用 KDevelop 创建 VMC 项目时,现在创建了一些您感兴趣的文件。查看目录 ~/projects/VMC,您将在其中找到 main.cpp 和 VMC.cpp。文件 main.cpp 很好。它只是声明并运行 VMC.cpp 中的代码描述的应用程序。VMC.cpp 中花括号内的任何示例代码对您都没有用,所以让我们用 VMC 应用程序的构造函数替换它。正如我之前提到的,此应用程序依赖于 Qt,因此对您来说一个重要的资源是 http://qt-project.org/doc/qt-4.8/classes.html

您的 VMC 类将继承自 QmainWindow,因此您的构造函数将在 VMC.cpp 中定义,如下所示


 VMC::VMC() : QMainWindow()
 {
 //declare a central widget to host our screen:
 QWidget* gCentralWidget = new QWidget(this);
 setCentralWidget(gCentralWidget);
//Set fonts, colors, geometry, etc
       - - - -
//Declare an object to hold the screen features:
ScreenC cScreenLayout = new ScreenC(); 
//Lastly, breathe life into the application
cHeartBeat = new QTimer (this);
connect(cHeartBeat, SIGNAL(timeout()), this,\
                          SLOT(slotPaintScreen()));
cHeartBeat->setSingleShot(false);
cHeartBeat->start(50); //milliseconds/20Hz
}

这是构造函数的缩略视图,但实际代码并没有长多少。连接的例程 slotPaintScreen() 将在定时器溢出时以 50 毫秒的间隔激活。它也很简短


//Fetch loop characters gathered by COMthread
SensorLoopService(); 
//Update the display
cScreenLayout->Update(); 
//Redraw the screen
update();

(再次缩写,因为这是一个关于如何做而不是如何编码的故事。)

图 1 的中心部分显示了将体现 VMC 应用程序的对象创建级联。请注意 VMC 构造函数对 ScreenC 对象的声明以及该对象以 20Hz 速率的更新。

ScreenC 类构造函数只是为屏幕上出现的每个实体声明一个 ScreenItemC 对象。一个典型的声明是


pF[i] = new ScreenItemC(xOff,yOff,xSiz,ySiz,\
                  MEAS_TACHOMETER, 0, "Tach");

在这里,您定义位置和大小,并命名每个屏幕对象的对象类型。在更新时,“update”只是像这样中继到其子对象


//Update screen features
for (i=0; i<cNumberFeatures; i++)
{
 pF[i]->Update();
}

ScreenItemC 类构造函数负责屏幕上项目的外观。在此应用程序中,ScreenItemC 项目由两个 QLabel 对象组成,它们上下放置,以便看起来像一个单独的仪表。QLabel 声明的形式是


QLabel cReading = new QLabel(gCentralWidget, Qt::FramelessWindowHint);

图 2 的仪表显示非常“朴素”。在 ScreenItemC 类代码中,您可以对其进行美化。ScreenItemC 构造函数还声明了一个 MeasureC 对象。该对象的更新例程返回 ScreenItemC 对象放置在屏幕上的数据


MeasureC cMeasure = new MeasureC(MEAS_TACHOMETER);

MeasureC 类是描述硬件接口的地方。HIP 地址、寄存器号和比例因子都在这里定义。例如


case MEAS_TACHOMETER:
{
fScale = 27648000.0;  //29.75Hz -> 1788rpm
      // RPM = fScale / binary from loop + fOffset
fOffset = 0;
rule = MEAS_RULE_RECIPROCAL_TACHO;
DeviceId = NODE_E;                  //Loop device id
DevicePort = P_IC_PERIOD_2;  //Sensor on device
//Create a sensor for the measurement
pSens = new SensorC(MEAS_TACHOMETER,\
    DeviceId, DevicePort, fScale, Offset, rule);
break;
}

请注意上面 SensorC 对象的声明。在更新时,该 SensorC 对象将从环路缓冲区中获取其最新的原始读数,缩放该读数,并将结果返回给其 MeasureC 父对象,MeasureC 父对象将该结果中继回其 ScreenItemC 父对象,ScreenItemC 父对象将在屏幕上显示该结果。代表键盘按键的 MeasureC 项目将在此处声明一个 ControlC 对象。ControlC 对象将使用其自己的 SensorC 对象向环路查询其按键是否是最近按下的按键。ControlC 对象还运行特定于设备的代码(例如定时闪烁器)。ControlC 对象可能会根据需要将命令放置在环路上。ControlC 更新例程将返回 1 或 0,具体取决于其控制目标是否已更改状态。该返回值会流回其祖父 ScreenItemC 对象,然后在显示屏上反映出来。

此对象创建级联以 SensorC 对象结束,SensorC 对象返回其先前对环路请求的结果,并在每次更新时发出新的数据请求。由于 ControlC 对象可能会随意将命令放置在环路上,因此环路将混合循环的独立命令,这些命令必须解析回其发起者。当向环路发出命令时,该命令的发出者还会将指向自身的指针插入到类可见的循环缓冲区中。

如上所述,slotPaintScreen() 将在每次更新时调用 SensorLoopService()。SensorLoopService() 提取已由 gCOMgo 线程放入环路接收缓冲区中的字符。此处使用互斥锁来防止其他线程的干扰。SensorLoopService() 在从缓冲区中获取数据时解析字符,当它检测到完整的有效消息时,它会将四个数据字节放入指针指向的位置(如上所述)。此数据将在下次更新时向上级联返回。

用更少的文字来说:更新事件从 ScreenC 对象级联到多个 SensorC 对象,这些对象将参数状态弹回 ScreenItemC 对象,ScreenItemC 对象将这些状态绘制在屏幕上。图 1 的左侧面板描绘了这一点。

Linux 环境注意事项

Linux 带来的一些问题包括默认激活的屏幕保护程序,但在监控应用程序中,这是一个坏消息。要关闭它,请转到“系统设置”→“电源管理”,并禁用所有“屏幕节能”选项。另一个问题是自动软件更新。我认为,如果某些东西工作正常,就不要乱搞操作系统环境,因为软件更新会这样做。抑制更新的最安全方法是在应用程序处于活动状态时断开 Internet 连接。另一种方法是通过软件控制禁用更新。为此,请转到“应用程序启动器”(桌面左下角),并从“收藏夹”启动“系统设置”,转到“软件管理”,然后左键单击右上角的扳手图标。从其菜单中选择“设置”。在“常规设置”页面中,将“检查更新”菜单设置为“从不”,然后“应用”它。此外,转到 /etc/yum/pluginconf.d/refresh-packagekit.conf 并将 enabled 设置为 0(禁用更新)。对我来说,当我想获得稳定的环境时,关闭 Wi-Fi 太容易了,所以我无法在此处为您提供其他建议。

要声称自己是“嵌入式”应用程序,此系统应在通电后启动——即,无需登录或任何其他用户输入即可使其运行。要取消登录,请转到 /etc/kde/kdm/kdmrc 并设置 AutoLoginAgain=trueAutoLoginUser=YourUserName。要在系统启动时启动您的应用程序,请转到 ~/.kde/Autostart 并在那里放置一个可执行脚本,如下所示


#!/bin/bash
cd /home/YourUserName/projects/VMC/build
./VMC

偶然地,如果您上次关机时应用程序处于活动状态,并且您已将系统设置为在开机时恢复以前的会话,则这不会启动应用程序的多个实例。

有了人在环路中,VMC 应用程序根本不是时间关键型的,并且可以在提供 CPU 时间时占用其份额。Linux 系统中还有很多其他东西也想要 CPU 时间(查看 ps -A)。如果您的应用程序是时间关键型的,并且在事件之间具有接近公差的预定响应时间,则此方案将不适用于您。但是,如果您有几毫秒的空闲时间,Linux 将以相当小的精力和良好的可靠性托管您的监控和控制应用程序。

加载 Disqus 评论