先说一下搭建以下环境的原因,开发流程如图。
实验设备为 FriendlyARM 的 CortexA9 Smart4418 实验箱。配套资源网址 为:http://wiki.friendlyarm.com/wiki/index.php/Smart4418/zh
OS:FriendlyCore,是一个没有 X-windows 环境,基于 Ubuntucore 构建的系统,使用 Qt-Embedded 作为图形界面的轻量级系统,兼容 Ubuntu 系统软件源,非常适合于企业用户用作产品的基础 OS。
开发在虚拟机下的Ubuntu16系统,安装了Qt5、Qt Creator以及Qt5交叉编译链工具。
sudo apt-get install qt5
Windows上安装Qt Creator,编写运行成功后,传至Linux上进行交叉编译,将目标程序放ARM上直接与运行。
FriendlyARM交叉编译链arm-linux-gcc下载地址:https://github.com/friendlyarm/prebuilts
如何交叉编译(以编译QtE-Demo项目为例):
mkdir build && cd build
/usr/local/Trolltech/Qt-5.10.0-nexell32-sdk/bin/qmake ../QtE-Demo.pro
make
在ARM机上运行,如提示:EGL library doesn't support Emulator extensions
输入如下命令解决:
export QT_QPA_EGLFS_INTEGRATION=none
查看Wifi设备
nmcli dev
开启Wifi设备
nmcli r wifi on
扫描附近Wifi热点
nmcli dev wifi
连接到指定Wifi热点
nmcli dev wifi connect "SSID" password "PASSWORD" ifname wlan0
查看IP地址
ifconfig
第一次连接后,以后每次开机,ARM都会自动连接之前的Wifi。
通过Win10网络设置也可以看到已连接设备的IP。
拿到设备IP后,使用SSH协议远程访问ARM,使用SFTP协议上传下载程序。
工具:MobaXterm
linux下对/sys/class/gpio中的gpio的控制 (转)
配置GPIO43与GPIO44,其分别控制两个LED灯的亮灭。
cd /sys/class/gpio
echo 43 > export
echo 44 > export
cd gpio43
echo high > direction
echo low > direction
echo 1 > value
echo 0 > value
cd gpio44
...
测试Demo:
//初始化引脚
int fd;
char buf[5];
char gpioExportPath[30] = "/sys/class/gpio/export";
//配置GPIO
fd = open(gpioExportPath, O_WRONLY);
if (fd < 0) {
printf("gpioExportPath:%s\topen failed", gpioExportPath);
exit(1);
}
sprintf(buf, "%2d",this->ledNo);
write(fd, buf, 2);
close(fd);
//设置GPIO方向
char gpioDirPath[40];
sprintf(gpioDirPath, "/sys/class/gpio/gpio%d/direction", this->ledNo);
fd = open(gpioDirPath, O_WRONLY);
if (fd < 0) {
printf("gpioDirPath:%s\topen failed", gpioDirPath);
exit(1);
}
sprintf(buf, "high");
write(fd, buf, 3);
close(fd);
//控制亮灭
void updateLedStatus(int ledNo)
{
int fd;
char buf[2];
char path[30];
sprintf(path, "/sys/class/gpio/gpio%d/value" , ledNo);
fd = open(path, O_WRONLY);
if (fd < 0) {
printf("updateLedStatus failed");
exit(1);
}
if (ledStatus)
{
buf[0] = '1';
}
else
{
buf[0] = '0';
}
write(fd, buf, 1);
close(fd);
}
测试Demo
#include
#include
#include
#include
void delay()
{
int i,j;
for (i = 0; i < 200; i++)
for (j = 0; j < 100; j++);
}
int main()
{
int fd, len, i;
char buf[20];
for (i = 0; i < 5; i++)
{
fd = open("/sys/devices/platform/c0000000.soc/c0053000.adc/iio:device0/in_voltage7_raw", 0);
if (fd < 0) exit(1);
len = read(fd, buf, sizeof buf - 1);
if (len > 0)
{
buf[len] = '\0';
printf("%s\n", buf);
}
delay();
}
close(fd);
return 0;
}
http://wiki.friendlyarm.com/wiki/index.php/Matrix_-_Buzzer/zh
https://www.eefocus.com/toradex/blog/17-05/420816_04520.html
cd /sys/class/pwm/pwmchip0
echo 0 > export
cd pwm0
echo 400000000 > period #周期,单位ns
echo 100000000 > duty_cycle #设置占空比25%
echo 1 > enable #开启
echo 0 > enable #关闭
用例图
活动图
服务端打算用Qt编写,开发流程是:1. 在Win10上使用IDE开发 2. 在虚拟机Linux上交叉编译 3. 在嵌入式Linux上运行程序查看效果。
根据详细设计的功能,规划界面内容。
界面上展示的包括:LED控制、色谱图模块、LCD数值显示模块、网络连接展示模块。
后端隐含的功能模块包括:网络连接模块、网络传输模块、网络数据解析模块、GPIO读写模块、AD读取模块、蜂鸣器控制模块。
先来实现LED控制功能。
设计一个LED控制类。
ledcontrol.h
#ifndef LEDCONTROL_H
#define LEDCONTROL_H
/**
* @brief LED控制类
*/
class LEDControl
{
public:
LEDControl(int no);
void initGPIO(); //初始化GPIO引脚
void setLedStatus(bool status); //设置LED状态
private:
void updateLedStatus(); //更新GPIO引脚状态,控制LED亮灭
public:
int ledNo; //LED编号
bool ledStatus; //LED状态
};
#endif // LEDCONTROL_H
ledcontrol.cpp
#include "ledcontrol.h"
#include
#include
#include
#include
#include
#include
#include
/**
* @brief LED控制类
*/
LEDControl::LEDControl(int no)
{
this->ledNo = no;
initGPIO(); //初始化LED对应的GPIO引脚
this->setLedStatus(true); //默认高电平,灭
}
/**
* @brief 初始化LED对应的GPIO引脚
*/
void LEDControl::initGPIO()
{
int fd;
char buf[5];
char gpioExportPath[30] = "/sys/class/gpio/export";
//配置GPIO
fd = open(gpioExportPath, O_WRONLY);
if (fd < 0) {
printf("gpioExportPath:%s\topen failed", gpioExportPath);
exit(1);
}
sprintf(buf, "%2d",this->ledNo);
write(fd, buf, 2);
close(fd);
//设置GPIO方向
char gpioDirPath[40];
sprintf(gpioDirPath, "/sys/class/gpio/gpio%d/direction", this->ledNo);
fd = open(gpioDirPath, O_WRONLY);
if (fd < 0) {
printf("gpioDirPath:%s\topen failed", gpioDirPath);
exit(1);
}
sprintf(buf, "high");
write(fd, buf, 3);
close(fd);
}
/**
* @brief 设置LED状态 false亮, true灭
*/
void LEDControl::setLedStatus(bool status)
{
this->ledStatus = status;
updateLedStatus();
qDebug() << "led" << this->ledNo << "\t status:" << this->ledStatus;
}
/**
* @brief 更新LED状态
*/
void LEDControl::updateLedStatus()
{
int fd;
char buf[2];
char path[30];
sprintf(path, "/sys/class/gpio/gpio%d/value" , this->ledNo);
fd = open(path, O_WRONLY);
if (fd < 0) {
printf("updateLedStatus failed");
exit(1);
}
if (this->ledStatus)
{
buf[0] = '1';
}
else
{
buf[0] = '0';
}
write(fd, buf, 1);
close(fd);
}
adreader.h
#ifndef ADREADER_H
#define ADREADER_H
/**
* @brief AD读取类
* 读取电压模拟量
*/
class ADReader
{
public:
ADReader();
int readVol(); //电压模拟量读取
private:
char buf[20]; //AD读取缓冲区
int readLen; //当次读取数据长度
int adFileDesc; //AD设备文件描述符
};
#endif // ADREADER_H
adreader.cpp
#include "adreader.h"
#include
#include
#include
#include
/**
* @brief AD读取类
* 读取电压模拟量
*/
ADReader::ADReader()
{
}
/**
* @brief 读取电压模拟量
* @return
*/
int ADReader::readVol()
{
//Win环境下测试用
//if (true)
//{
// return 800;
//}
//打开AD设备文件
this->adFileDesc = open("/sys/devices/platform/c0000000.soc/c0053000.adc/iio:device0/in_voltage7_raw", 0);
if (this->adFileDesc < 0)
{
printf("adFile open failed");
exit(1);
}
//读取电压模拟量到buf
this->readLen = read(this->adFileDesc, this->buf, sizeof this->buf - 1);
if (this->readLen > 0)
{
this->buf[this->readLen] = '\0';
}
close(this->adFileDesc);
return atoi(buf); //转化为int返回
}
对于服务端,如果读取的AD值超出安全范围,则控制蜂鸣器发出警报,同时告诉客户端警报开启。
buzzer.h
#ifndef BUZZER_H
#define BUZZER_H
/**
* @brief 蜂鸣器
*/
class Buzzer
{
public:
Buzzer();
void setEnable(int enable_);
private:
void initPWM();
void updateEnable();
int enable; //1开启,0关闭
};
#endif // BUZZER_H
buzzer.cpp
#include "buzzer.h"
#include
#include
#include
#include
#include
#include
#include
/**
* @brief 蜂鸣器
*/
Buzzer::Buzzer()
{
initPWM();
}
/**
* @brief 初始化PWM
*/
void Buzzer::initPWM()
{
int fd;
char buf[15];
char exportPath[35] = "/sys/class/pwm/pwmchip0/export";
//配置PWM
fd = open(exportPath, O_WRONLY);
if (fd < 0) {
printf("pwmExportPath:%s\topen failed", exportPath);
exit(1);
}
buf[0] = '0';
write(fd, buf, 1);
close(fd);
//设置PWM周期
char periodPath[45] = "/sys/class/pwm/pwmchip0/pwm0/period";
fd = open(periodPath, O_WRONLY);
if (fd < 0) {
printf("pwmPeriodPath:%s\topen failed", periodPath);
exit(1);
}
sprintf(buf, "400000000");
write(fd, buf, 9);
close(fd);
//设置PWM占空比
char dutyCyclePath[55] = "/sys/class/pwm/pwmchip0/pwm0/duty_cycle";
fd = open(dutyCyclePath, O_WRONLY);
if (fd < 0) {
printf("pwmDutyCyclePath:%s\topen failed", dutyCyclePath);
exit(1);
}
sprintf(buf, "100000000");
write(fd, buf, 9);
close(fd);
}
/**
* @brief 更新蜂鸣器状态
*/
void Buzzer::updateEnable()
{
int fd;
char buf[2];
char path[45] = "/sys/class/pwm/pwmchip0/pwm0/enable";
fd = open(path, O_WRONLY);
if (fd < 0) {
printf("updatePWMEnable failed");
exit(1);
}
buf[0] = '0' + this->enable;
write(fd, buf, 1);
close(fd);
}
/**
* @brief 设置蜂鸣器开关状态
* @param enable
*/
void Buzzer::setEnable(int enable_)
{
if(this->enable == enable_) return; //防止重复写
this->enable = enable_;
updateEnable();
qDebug() << "PWM: " << this->enable;
}
基于TCPSocket进行网络通讯。
设计流程:
使用QT封装的TCP网络通讯类:QTcpServer与QTcpSocket。
使用Qt建立一个TCP服务端分为以下几步:
Sample:
//设置监听端口
tcpserver->listen(QHostAddress::Any, this->serverPort.toInt());
//新客户端连接
connect(tcpserver, &QTcpServer::newConnection, this, &MainWidget::newConnect);
在newConnect槽函数处理TCP客户端的连接请求。
处理流程:
/**
* @brief 新客户端连接
*/
void MainWidget::newConnect()
{
clientSocket = tcpserver->nextPendingConnection();
qDebug() << "新客户端连接:"
<< QHostAddress(clientSocket->peerAddress().toIPv4Address()).toString();
//有可读消息
connect(clientSocket,SIGNAL(readyRead()),
this,SLOT(readMessage()));
//断开连接
connect(clientSocket,SIGNAL(disconnected()),
this,SLOT(lostConnect()));
//地址
QString currentIP = QHostAddress(clientSocket->peerAddress().toIPv4Address()).toString();
quint16 currentPort = clientSocket->peerPort();
//添加到显示面板
client_list->push_back(currentIP + ":" + QString::number(currentPort));
ui->tb_clients->clear();
for (int i = 0; i < client_list->length(); i++)
{
ui->tb_clients->append(client_list->at(i));
}
//添加到列表
socket_list->push_back(clientSocket);
}
/**
* @brief 接收消息
*/
void MainWidget::readMessage()
{
//遍历客户端列表,所有客户端
for (int i = 0; i < socket_list->length(); i++)
{
read_message = socket_list->at(i)->readAll();
if(!(read_message.isEmpty()))
{
qDebug() << "读取消息 [ClientIP:" << QHostAddress(socket_list->at(i)->peerAddress().toIPv4Address()).toString() << "]";
qDebug() << "[ClientMsg]:" << read_message;
//回传同样的消息
sendMessage(read_message);
//解析消息
analyzeMessage(read_message, socket_list->at(i));
break;
}
}
}
/**
* @brief 发送消息
* @param infomation
*/
void MainWidget::sendMessage(QString infomation)
{
QByteArray b = infomation.toLatin1();
char* msg = b.data();
for(int i = 0; i < socket_list->length(); i++){
socket_list->at(i)->write(msg);
qDebug() << "Send:"<< msg;
}
}
确定接收与发送功能无误。
制定TCP服务端与客户端之间的通讯协议。
数据首部:#
客户端创建一个TCP连接的流程:
Sample:
//init TCP Server
this->tcpClient = new QTcpSocket(this); //实例化tcpClient
this->tcpClient->abort(); //取消原有连接
connect(tcpClient, SIGNAL(readyRead()), this, SLOT(ReadData()));
/**
* @brief 连接按钮点击事件
*/
void MainWidget::on_btn_conn_clicked()
{
if(ui->btn_conn->text()=="连接")
{
tcpClient->connectToHost(ui->edit_ip->text(), ui->edit_port->text().toInt());
if (tcpClient->waitForConnected(1000)) // 连接成功则进入if{}
{
ui->btn_conn->setText("断开");
ui->btn_send->setEnabled(true);
ui->btn_led1->setEnabled(true);
ui->btn_led2->setEnabled(true);
}
}
else
{
tcpClient->disconnectFromHost();
if (tcpClient->state() == QAbstractSocket::UnconnectedState \
|| tcpClient->waitForDisconnected(1000)) //已断开连接则进入if{}
{
ui->btn_conn->setText("连接");
ui->btn_send->setEnabled(false);
}
}
}
消息解析流程:
QCustomPlot:https://www.qcustomplot.com/
使用的QCustomPlot绘图窗口部件。
优点是没有过多的依赖库,使用方便,将头文件与源文件添加至项目工程即可。
//init Plot
SendTimeInterval = 500; //发送时间间隔
//开始绘制
setupRealtimeDataDemo(ui->plot);
ui->plot->replot();
初始化Plot
流程:
/**
* @brief QCustomPlot初始化
* @param customPlot
*/
void MainWidget::setupRealtimeDataDemo(QCustomPlot *customPlot)
{
customPlot->addGraph();
customPlot->graph(0)->setPen(QPen(Qt::red));
customPlot->graph(0)->setName("Vol");
//横坐标
customPlot->xAxis->setTickLabelType(QCPAxis::ltDateTime);
customPlot->xAxis->setDateTimeFormat("hh:mm:ss");
customPlot->xAxis->setAutoTickStep(false);
customPlot->xAxis->setTickStep(2);
customPlot->axisRect()->setupFullAxesBox();
customPlot->legend->setVisible(true);//右上角小图标
//通过QTimer定时获取数据并刷新Plot
connect(&dataTimer, SIGNAL(timeout()), this, SLOT(realtimeDataSlot()));
dataTimer.start(SendTimeInterval);
}
/**
* @brief 绘制折线
*/
void MainWidget::realtimeDataSlot()
{
//横轴:key 时间 单位s
double key = QDateTime::currentDateTime().toMSecsSinceEpoch()/1000.0;
//纵轴:value 电压模拟量
int value = this->adReader->readVol();
ui->lcd_vol->setDigitCount(4);
ui->lcd_vol->setMode(QLCDNumber::Dec);
ui->lcd_vol->display(QString::number(value));
//发送给客户端
if (adSendSwitch)
{
QString msg = QString("#B%1").arg(value,5,10,QLatin1Char('0'));
sendMessage(msg);
}
//添加数据到曲线0
ui->plot->graph(0)->addData(key, value);
//删除8秒之前的数据
ui->plot->graph(0)->removeDataBefore(key-8);
//设定graph曲线y轴的范围
ui->plot->yAxis->setRange(500, 7000);
ui->plot->xAxis->setRange(key+0.25, 8, Qt::AlignRight);//设定x轴的范围
ui->plot->replot();
}
对于服务端,如果读取的AD值超出安全范围,则控制蜂鸣器发出警报,同时告诉客户端警报开启。
客户端实现报警就是播放一段音频。
在QT项目中添加一个资源文件,存放warning.wav报警音频文件。
在项目pro文件,添加multimedia多媒体配置
QT += core gui network multimedia
通过QSound类播放媒体资源
QSound::play(":/media/warning.wav");
将编译后的服务端项目Server放在/usr/local目录下
通过SFTP上传后,设置文件权限:chmod 777 Server
运行:./Server
若运行Qt程序报错:
解决方法:export QT_QPA_EGLFS_INTEGRATION=none
服务端:
客户端: