基于嵌入式Qt的多功能电子台历

1.嵌入式系统概念简述

嵌入式系统是指将计算机系统集成到其它产品中的技术。这种技术的目的是使用计算机系统来控制和协调其它产品的运行,并通过计算机系统来实现更复杂和更高效的功能。

嵌入式系统通常由以下几个部分组成:

  • 微处理器:这是嵌入式系统的核心,用于执行指令并控制其它部件的运行。
  • 存储器:包括ROM、RAM和Flash等不同类型的存储器,用于保存程序和数据。
  • 接口:包括串口、并口、SPI、I2C等不同类型的接口,用于连接外部设备和传输数据。
  • 外设:包括显示器、键盘、鼠标、打印机、传感器等不同类型的外设,用于实现更多的功能和交互。

嵌入式系统应用广泛,可以用于汽车、家电、医疗、军事等不同领域。

2.环境搭建

  1. 安装虚拟机 首先,需要在电脑上安装好Parallels Desktop软件。 打开Parallels Desktop软件,在软件的主界面中点击“创建新虚拟机”按钮。 在弹出的向导窗口中,选择“安装操作系统”,并点击“继续”按钮。 接下来,选择“Ubuntu”作为要安装的操作系统,并点击“继续”按钮。 在接下来的界面中,输入虚拟机的名称、用户名和密码,并设置虚拟机的内存、硬盘大小等参数。 点击“完成”按钮,开始安装Ubuntu虚拟机。 安装过程中,会提示选择安装方式,选择“安装Ubuntu”即可。 安装完成后,在Parallels Desktop的主界面中,会看到刚才创建的Ubuntu虚拟机,双击虚拟机图标即可启动虚拟机。

    在虚拟机中,输入用户名和密码,登录Ubuntu系统,即可使用Ubuntu虚拟机。

  2. 安装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编译即可

  3. 配置编译Kernel和Roofs

    1. 下载 Linux 的内核源代码。

    2. 针对所使用的目标平台,对源代码进行配置。

      ① 查看源码

      ② 编辑make_deb.sh文件

      ③执行“make ARCH=arm menuconfig” 配置内核

    3. 交叉编译 Linux 源代码得到内核映像文件 zImage。

    4. 编译配置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设计

  1. 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”。

  2. 蜂鸣器:

    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
    #include <linux/module.h>
    #include <linux/poll.h>
    #include <asm/io.h>
    #include <linux/device.h>
    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”,表示模块包含在内核树中。
  3. 网络更新程序:服务端使用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
    39
    import 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
    17
    void 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设计

  1. 日历实现

    1. 从控件库中拖动一个Calendar Widget和一个Label到mainwindow

    2. 对日历控件做如下设置

      1
      2
      3
      4
      ui->calendarWidget->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader);//去除年和月标题
      ui->calendarWidget->setGridVisible(true);//网格模式
      ui->calendarWidget->setSelectionMode(QCalendarWidget::NoSelection);//设置不可修改
      ui->calendarWidget->setNavigationBarVisible(false);//去除导航栏
    3. 创建一个线程,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
      #include "refreshcalendarwidget.h"
      #include <QTime>
      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);
      }
      }
  2. 日历设计界面

  3. 时钟实现 创建两个线程,一个线程在程序启动时就开始运行,循环获取系统时间,在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
    #include "timethread.h"
    #include <QTime>
    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);
    }
    }
  4. 时钟设计界面

  5. 时钟显示代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void MainWindow::DisplayTime(QString time)//程序打开时默认显示系统时间
    {
    ui->lcdNumber->display(time);
    }

    void MainWindow::updateTime(QString time)//手动设置时间后显示时间
    {
    if (isSet)//如果是手动设置的时间则将时间传给myNow
    myNow = time;
    ui->lcdNumber->display(time);
    }
  6. 温湿度实现 新建一个线程,run()函数里面循环读入/dev/dht11文件的数据,并从数据中取出温湿度并发送到主线程,关键代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include "dhtthread.h"
    #include <fcntl.h>
    #include <unistd.h>

    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);
    }
    }
  7. 温湿度设计界面

  8. 温湿度显示代码

    1
    2
    3
    4
    5
    void MainWindow::displayDHT(QString temperature, QString humidity)//显示温湿度
    {
    ui->temperatureLabel->setText(temperature);
    ui->humidityLabel->setText(humidity);
    }
  9. 闹铃实现 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
    #include "alarmthread.h"
    #include <QTime>
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void MainWindow::alarmRing(bool ring)//闹钟
{
if (ring)//闹钟响起
{
ringDialog->setModal(true);
ringDialog->show();
//打开蜂鸣器
Buzzerfd = open("/dev/buzzer", O_RDWR);
val = 0;
write(Buzzerfd, &val, 1);

}
if (isSet == true)
*time = QTime::fromString(myNow,"hh:mm:ss");
else
time->setHMS(QDateTime::currentDateTime().time().hour(),QDateTime::currentDateTime().time().minute(),QDateTime::currentDateTime().time().second(),0);
alarmthread->getTime(time->hour(),time->minute(),time->second());
}
  1. 闹铃设计界面

  2. 闹铃控制代码 在闹钟窗体放置一个Check Box,发送信号时带上其值,主线程判断是否开启闹钟,关键代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    void MainWindow::getAlarm(int hh, int mm,int ss,bool isOn)//设置闹钟
    {
    if (isOn)//闹钟是否启用
    {
    alarmthread->setAlarm(hh,mm,ss);
    alarmthread->start();
    }
    }
  3. 日历及时间设置实现 1新建一个窗体,放入3个Spin Box,1个Calendar Widget,分别设置时分秒和日期,然后发送信号到主线程的槽函数setTime(int,int,int,int,int,int),槽函数启动手动设置时间的线程,关键代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void 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);
    }
  4. 日历及时间设置设计界面

5.实际运行效果

6.小结

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

7.源码

GitHub