基于嵌入式Qt的多功能电子台历
1.嵌入式系统概念简述
嵌入式系统是指将计算机系统集成到其它产品中的技术。这种技术的目的是使用计算机系统来控制和协调其它产品的运行,并通过计算机系统来实现更复杂和更高效的功能。
嵌入式系统通常由以下几个部分组成:
- 微处理器:这是嵌入式系统的核心,用于执行指令并控制其它部件的运行。
- 存储器:包括ROM、RAM和Flash等不同类型的存储器,用于保存程序和数据。
- 接口:包括串口、并口、SPI、I2C等不同类型的接口,用于连接外部设备和传输数据。
- 外设:包括显示器、键盘、鼠标、打印机、传感器等不同类型的外设,用于实现更多的功能和交互。
嵌入式系统应用广泛,可以用于汽车、家电、医疗、军事等不同领域。
2.环境搭建
安装虚拟机 首先,需要在电脑上安装好Parallels Desktop软件。 打开Parallels Desktop软件,在软件的主界面中点击“创建新虚拟机”按钮。 在弹出的向导窗口中,选择“安装操作系统”,并点击“继续”按钮。 接下来,选择“Ubuntu”作为要安装的操作系统,并点击“继续”按钮。 在接下来的界面中,输入虚拟机的名称、用户名和密码,并设置虚拟机的内存、硬盘大小等参数。 点击“完成”按钮,开始安装Ubuntu虚拟机。 安装过程中,会提示选择安装方式,选择“安装Ubuntu”即可。 安装完成后,在Parallels Desktop的主界面中,会看到刚才创建的Ubuntu虚拟机,双击虚拟机图标即可启动虚拟机。
在虚拟机中,输入用户名和密码,登录Ubuntu系统,即可使用Ubuntu虚拟机。
安装Linux系统并进行NFS及交叉编译工具链等的配置 设置NFS共享目录: 在Ubuntu系统上安装NFS服务器软件包: sudo apt-get install nfs-kernel-server 创建共享目录/nfs-share,并设置相应的权限: sudo mkdir /nfs-share sudo chmod -R 777 /nfs-share 编辑/etc/exports文件,添加对/nfs-share目录的共享配置: sudo nano /etc/exports 在/etc/exports文件中添加如下内容: /nfs-share *(rw,sync,no_subtree_check) 重启NFS服务器,使配置生效: sudo service nfs-kernel-server restart 在开发板上挂载共享目录: sudo mount -t nfs [虚拟机IP地址]:/share 经过以上步骤后,虚拟机上的/share目录就会被共享到开发板上,开发板可以通过/share目录访问虚拟机上的文件。 交叉编译工具链配置:
由于我的物理机CPU为Apple M1 Pro,属于ARM64架构,使用命令lscpu | grep “Architecture”查看架构如下图所示,与开发板架构一样,所以我无需配置交叉编译,直接使用gcc编译即可
配置编译Kernel和Roofs
下载 Linux 的内核源代码。
针对所使用的目标平台,对源代码进行配置。
① 查看源码
② 编辑make_deb.sh文件
③执行“make ARCH=arm menuconfig” 配置内核
交叉编译 Linux 源代码得到内核映像文件 zImage。
编译配置Qt4.8.7环境 在Ubuntu ARM64系统中安装编译工具和库文件: sudo apt-get install build-essential libncurses5-dev libssl-dev bison flex 下载Qt4.8.7源代码,并解压到当前目录: wget https://download.qt.io/archive/qt/4.8/4.8.7/qt-everywhere-opensource-src-4.8.7.tar.gz tar xvfz qt-everywhere-opensource-src-4.8.7.tar.gz 进入Qt4.8.7源代码目录,并执行配置命令: cd qt-everywhere-opensource-src-4.8.7 ./configure -platform linux-arm-gnueabi-g++ 执行./configure命令后,会根据系统环境和你提供的参数,自动生成Qt4.8.7编译所需的Makefile文件。 编译Qt4.8.7: make sudo make install 经过以上步骤后,就可以在Ubuntu ARM64系统中编译并安装Qt4.8.7了。
3.Linux设计
DHT11:
A. 电路原理图
B. 驱动控制电平分析 DHT11温湿度传感器上电后,总线空闲状态为高电平,主机把总线拉低等待DHT11响应,主机把总线拉低必须大于18毫秒,保证DHT11能检测到起始信号。DHT11接收到主机的开始信号后,等待主机开始信号结束,然后发送80us低电平响应信号。主机发送开始信号结束后,延时等待20-40us后, 读取DHT11的响应信号,主机发送开始信号后,可以切换到输入模式,或者输出高电平均可, 总线由上拉电阻拉高。 DHT11传感器的DATA引脚检测到外部信号有低电平时,等待外部信号低电平结束,延迟后DHT11的 DATA引脚处于输出状态,输出80微秒的低电平作为应答信号,紧接着输出 80 微秒的高电平通知外设准备接收数据,微处理器的 I/O 此时处于输入状态,检测到 I/O 有低电平(DHT11 回应信号)后,等待80微秒的高电平后的数据接收。 当主机变为输入模式后,检测到总线为低电平,说明DHT11发送响应信号,DHT11发送响应信号后,再把总线拉高80us,准备发送数据,每一bit数据都以50us低电平时隙开始,高电平的长短定了数据位是“0”还是“1”。
蜂鸣器:
A. 电路原理图
B. 驱动控制电平分析 观察蜂鸣器接口电路图(如上图所示),其中的蜂鸣器接在 NPN 型三极管的集电极, 而三极管的基极接到了芯片的 GPIO1_19 引脚上,当该引脚输出高电平时,三极管 导通,蜂鸣器响,当输出低电平时,三极管截止,蜂鸣器不响; C. 驱动程序及其分析 编写驱动程序,在 Ubuntu 的/nfs-share 目录下新建一个名为 buzzer 的目录,然后在 该目录下通过 vim 创建一个名为 bz_drv.c 的文件,并输入如下所示的内容,完成后存盘退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
volatile unsigned long *(CCM_CCGR1) = NULL; //指向CCM模块中CCGR1寄存器的指针,用于控制模块的时钟。
volatile unsigned long *(IOMUXC_SW_MUX_CTL_PAD_UART1_RTS_B) = NULL; //指向IOMUXC模块中的寄存器,用于控制UART1的RTS引脚的复用
volatile unsigned long *(IOMUXC_SW_PAD_CTL_PAD_UART1_RTS_B) = NULL; //指向IOMUXC模块中的寄存器,用于控制UART1的RTS引脚的引脚配置
volatile unsigned long *(GPIO1_GDIR) = NULL; //指向GPIO1模块中的寄存器,用于控制GPIO1引脚的方向
volatile unsigned long *(GPIO1_DR) = NULL; //指向GPIO1模块中的寄存器,用于控制GPIO1引脚的输出值
static struct class *bz_drv_class; //一个指向设备类的指针,用于在内核中注册设备。
static int bz_dev_open(struct inode *inode, struct file *file) {
*(CCM_CCGR1) |= 0xC000000; //在 CCM_CCGR1 寄存器中设置控制字段,以启用 UART1 模块
*(IOMUXC_SW_MUX_CTL_PAD_UART1_RTS_B) = 0x5; //配置 UART1 的 RTS_B 接口
*(IOMUXC_SW_PAD_CTL_PAD_UART1_RTS_B) = 0x1F038; //配置 UART1 的 RTS_B 接口
*(GPIO1_GDIR) |= 0x80000; //配置 GPIO1 接口的方向,使其成为输出接口
return 0;
}
static ssize_t bz_dev_write(struct file *filp, const char *buf, size_t count, loff_t * f_pos)
{
int val = 1; //声明参数val,并赋值1
unsigned long n;
n = copy_from_user(&val, buf, count); //从用户空间获取val参数的值
if(val==0)//打开蜂鸣器
*(GPIO1_DR) |= (1<<19);
else//关闭蜂鸣器
*(GPIO1_DR) &= ~(1<<19);
return 0;
}
static struct file_operations bz_dev_fops = {
.owner = THIS_MODULE, //表示模块的所有权,即指向当前模块的指针
.open = bz_dev_open, //函数指针,用于打开设备文件
.write = bz_dev_write, //函数指针,用于向设备文件写入数据
};
int major;
static int bz_dev_init(void) {
major = register_chrdev(0, "buzzer", &bz_dev_fops); //注册一个字符设备,并获取主设备号
bz_drv_class = class_create(THIS_MODULE, "buzzer"); //创建一个设备类
device_create(bz_drv_class, NULL, MKDEV(major, 0), NULL, "buzzer"); //在该设备类下创建一个设备
CCM_CCGR1 = (volatile unsigned long *)ioremap(0x20C406C, 4); //将物理地址映射到内核虚拟地址,以便能够访问寄存器
IOMUXC_SW_MUX_CTL_PAD_UART1_RTS_B = (volatile unsigned long *)ioremap(0x20E0090, 4); //将物理地址映射到内核虚拟地址,以便能够访问寄存器
IOMUXC_SW_PAD_CTL_PAD_UART1_RTS_B = (volatile unsigned long *)ioremap(0x20E031C, 4); //将物理地址映射到内核虚拟地址,以便能够访问寄存器
GPIO1_GDIR = (volatile unsigned long *)ioremap(0x0209C004, 4); //将物理地址映射到内核虚拟地址,以便能够访问寄存器
GPIO1_DR = (volatile unsigned long *)ioremap(0x0209C000, 4); //将物理地址映射到内核虚拟地址,以便能够访问寄存器
return 0;
}
static void bz_dev_exit(void) {
device_destroy(bz_drv_class, MKDEV(major, 0)); //销毁设备
class_destroy(bz_drv_class); //销毁设备类
unregister_chrdev(major, "buzzer"); //注销字符设备,卸载设备驱动程序
}
module_init(bz_dev_init); //定义了模块的初始化函数,在模块加载时被调用
module_exit(bz_dev_exit); //定义了模块的清除函数,在模块卸载时被调用
MODULE_LICENSE("GPL"); //定义了模块的许可证类型,通常是GPL(GNU General Public License)
MODULE_INFO(intree,"Y"); //定义了模块的一些信息,并将值设为“Y”,表示模块包含在内核树中。网络更新程序:服务端使用Java部署在服务器上,用来处理客户端的HTTP Get请求,服务端代码及注释如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.*;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) throws IOException {
// 创建一个HttpServer对象,并设置它监听的端口号
HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
// 为服务端程序添加一个处理器,用于处理客户端发来的请求
server.createContext("/time", (HttpExchange exchange) -> {
// 获取当前时间
ZonedDateTime now = ZonedDateTime.now();
// 转化为东八区时间
ZonedDateTime chinaTime = now.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));
// 将时间格式化为yyyy-MM-dd HH:mm:ss
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 将时间格式化为字符串
String formattedDateTime = chinaTime.format(formatter);
// 调试输出
System.out.println(formattedDateTime);
// 设置响应的内容类型为文本
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
// 将时间字符串写入响应体
exchange.sendResponseHeaders(200, formattedDateTime.length());
OutputStream os = exchange.getResponseBody();
os.write(formattedDateTime.getBytes());
os.close();
});
// 启动服务端程序,开始处理客户端请求
server.start();
}
}2客户端直接调用Qt自带类库发起Get请求,并获取响应体内容(即格式化后的日期和时间),代码如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void MainWindow::on_syncButton_clicked()//网络校准按钮点击时发送HTTP GET请求,请求地址为http://localhost:8000/time(localhost为本机地址,也可以指定服务器域名),响应体为格式化后的yyyy-MM-dd
{
// 创建一个QNetworkAccessManager对象,用于发送网络请求
QNetworkAccessManager manager;
// 发送一个GET请求到http://localhost:8000/time
QNetworkReply* reply = manager.get(QNetworkRequest(QUrl("http://localhost:8000/time")));
// 等待响应完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 获取响应体的内容
QByteArray responseData = reply->readAll();
// 将响应体的内容转换为字符串
QString responseString = QString::fromUtf8(responseData);
QDateTime networkTime = QDateTime::fromString(responseString,"yyyy-MM-dd hh:mm:ss");
setTime(networkTime.time().hour(),networkTime.time().minute(),networkTime.time().second(),networkTime.date().year(),networkTime.date().month(),networkTime.date().day());
}
4.Qt设计
日历实现
从控件库中拖动一个Calendar Widget和一个Label到mainwindow
对日历控件做如下设置
1
2
3
4ui->calendarWidget->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader);//去除年和月标题
ui->calendarWidget->setGridVisible(true);//网格模式
ui->calendarWidget->setSelectionMode(QCalendarWidget::NoSelection);//设置不可修改
ui->calendarWidget->setNavigationBarVisible(false);//去除导航栏创建一个线程,getDate(int,int,int)函数获取当前时间时分秒,run()函数判断当前时间是否是23:59:59,如果是则发送信号使日历+1,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
refreshCalendarWidget::refreshCalendarWidget()
{
}
void refreshCalendarWidget::getDate(int hh,int mm,int ss)//获取当前时间
{
recordHour = hh;
recordMinute = mm;
recordSeconde = ss;
}
void refreshCalendarWidget::run()
{
while(1)//循环判断是否需要对日历进行+1操作
{
if (recordHour == 23 && recordMinute == 59 && recordSeconde == 59)
emit sendDate(true);
else
emit sendDate(false);
sleep(1);
}
}
日历设计界面
时钟实现 创建两个线程,一个线程在程序启动时就开始运行,循环获取系统时间,在LCD Number中显示,另一个线程在用户启动它时用setTime(int,int,int)函数获取设置的时间,然后使用重复获取系统时间的方法来计算时间差,然后调用QDateTime的secsTo()函数判断时间差是否为1s,达到更新时间的目的,关键代码如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
timeThread::timeThread()
{
}
void timeThread::setTime(int hh,int mm,int ss)//设置自定义的时间
{
qtime.setHMS(hh,mm,ss,0);
recordTime = QDateTime::currentDateTime();//获取当前系统时间
}
void timeThread::run()
{
while(1)//循环判断系统时间和手动设置的时间差是否为1秒
{
if (QDateTime::currentDateTime().secsTo(recordTime) != 0)
{
recordTime = QDateTime::currentDateTime();//更新记录的时间
qtime = qtime.addSecs(1);//手动设置的时间+1秒
}
emit refreshTime(qtime.toString("hh:mm:ss"));//发送时间到主线程
usleep(100);
}
}时钟设计界面
时钟显示代码
1
2
3
4
5
6
7
8
9
10
11void MainWindow::DisplayTime(QString time)//程序打开时默认显示系统时间
{
ui->lcdNumber->display(time);
}
void MainWindow::updateTime(QString time)//手动设置时间后显示时间
{
if (isSet)//如果是手动设置的时间则将时间传给myNow
myNow = time;
ui->lcdNumber->display(time);
}温湿度实现 新建一个线程,run()函数里面循环读入/dev/dht11文件的数据,并从数据中取出温湿度并发送到主线程,关键代码如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dhtThread::dhtThread()
{
}
void dhtThread::run()//打开/dev/dht11并循环读取温湿度数据
{
char dht[16];
int dhtfd = open("/dev/dht11",O_RDONLY);
while(1)
{
read(dhtfd,dht,16);
emit sendDHT(QString::number(dht[2]),QString::number(dht[0]));
sleep(1);
}
}温湿度设计界面
温湿度显示代码
1
2
3
4
5void MainWindow::displayDHT(QString temperature, QString humidity)//显示温湿度
{
ui->temperatureLabel->setText(temperature);
ui->humidityLabel->setText(humidity);
}闹铃实现 1创建一个窗体,窗体内放入三个Spin Box,分别获取时分秒 2创建一个线程,使用setAlarm(int,int,int)函数获取1中设置的值,使用getTime(int,int,int)函数获取当前系统时间,run()函数循环判断两者是否相等,相等则发送信号,蜂鸣器响起函数alarmRing(bool)被执行,从而实现闹钟功能,关键代码如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
alarmThread::alarmThread()
{
}
void alarmThread::setAlarm(int hh,int mm,int ss)//设置闹钟时间
{
alarmTime.setHMS(hh,mm,ss,0);
}
void alarmThread::getTime(int hh,int mm,int ss)//获取当前时间
{
nowTime.setHMS(hh,mm,ss,0);
}
void alarmThread::run()
{
while(1)//循环判断是否响起闹钟
{
if (alarmTime.hour() == nowTime.hour() && alarmTime.minute() == nowTime.minute() && alarmTime.second() == nowTime.second())
{
emit alarmRing(true);
}
else
emit alarmRing(false);
msleep(100);
}
}
1 | void MainWindow::alarmRing(bool ring)//闹钟 |
闹铃设计界面
闹铃控制代码 在闹钟窗体放置一个Check Box,发送信号时带上其值,主线程判断是否开启闹钟,关键代码如下
1
2
3
4
5
6
7
8void MainWindow::getAlarm(int hh, int mm,int ss,bool isOn)//设置闹钟
{
if (isOn)//闹钟是否启用
{
alarmthread->setAlarm(hh,mm,ss);
alarmthread->start();
}
}日历及时间设置实现 1新建一个窗体,放入3个Spin Box,1个Calendar Widget,分别设置时分秒和日期,然后发送信号到主线程的槽函数setTime(int,int,int,int,int,int),槽函数启动手动设置时间的线程,关键代码如下所示
1
2
3
4
5
6
7
8
9
10void MainWindow::setTime(int hh,int mm, int ss,int yyyy,int MM,int dd)//设置LCD显示的时间为手动设置的时间
{
QString str = QString("%1-%2-%3").arg(yyyy).arg(MM).arg(dd);
QDate date = QDate::fromString(str,"yyyy-M-d");
TimeshowThread->terminate();//将默认显示系统时间的线程关闭
TimeThread->setTime(hh,mm,ss);
TimeThread->start();
isSet = true;//将标志设置为真,表示当前LCD显示的时间已经修改为手动设置的时间
ui->calendarWidget->setSelectedDate(date);
}日历及时间设置设计界面

5.实际运行效果
6.小结
电子台历在嵌入式开发板上的实现是一个很有趣的项目,通过它可以学习到很多有关嵌入式开发的知识。在实现过程中,需要熟悉嵌入式开发板的使用,并对相关的硬件设备有一定的了解。需要编写驱动和代码来实现各种功能,例如网络对时、手动设置时间、设置蜂鸣器闹钟和使用 dht11 温湿度传感器实时显示温湿度,这些都是很实用的功能,也能够增加用户体验。 总的来说,这是一个非常有趣和实用的项目,对于学习嵌入式开发有很大的帮助。在实现过程中,需要不断学习和实践,才能够掌握嵌入式开发的相关技能。