前言

本次实验是基于正点原子开发板I.MX6ULL开发板来学习的

硬件原理分析

本次实验我们使用的是正点原子开发板上的led灯,我们查看开发板原理图可以看到led灯连接在GPIO_3引脚上的

image-20250723152040662

我们打开芯片核心原理图可以找到GPIO_3对应的是IMX6ULL芯片的GPIO1_IO03引脚

image-20250723152322983

我们本次实验使用的就是I/O功能,因此我们打开arch/arm/boot/dts/imx6ul-pinfunc.h文件找到对应的宏MX6UL_PAD_GPIO1_IO03__GPIO1_IO03

image-20250723152558110

设备树编写

dtbs文件:

  • 添加pinctrl节点

    iomuxc 节点的 imx6ul-evk 子节点下创建一个名为pinctrl_led的子节点,将 GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0

    imx6ull-alientek-emmc.dts文件中找到&iomuxc 节点,添加对应的电气属性

    格式:

    1
    2
    3
    4
    5
    6
    pinctrl_自定义名字: 自定义名字{
    fsl,pins = <
    引脚复用宏定义PAD(引脚)属性
    引脚复用宏定义PAD(引脚)属性
    >;
    };

    image-20241006194740370

    imx6ull-alientek-emmc.dts
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    pinctrl_gpioled: ledgrp{
    fsl,pins=<
    MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 /*0 0001 0000 1101 0000*/
    /* 3、配置GPIO1_IO03 的IO 属性(电气属性)
    *bit 16:0 HYS 关闭
    *bit [15:14]: 00 默认下拉
    *bit [13]: 0 kepper 功能
    *bit [12]: 1 pull/keeper 使能
    *bit [11]: 0 关闭开路输出
    *bit [7:6]: 10 速度100Mhz
    *bit [5:3]: 110 R0/6 驱动能力
    *bit [0]: 0 低转换率
    */
    >;
    };

    电气属性

    常用到的配置电气属性包括:压摆率,速度,驱动能力,开漏,上下拉等。

    img

    img

    SRE(bit0): 对应图中的 SRE,设置压摆率,当此位为 0 的时候是低压摆率,当为 1的时候是高压摆率。这里压摆率就是 IO 电平跳变所需要的时间,比如从 0 到 1 需要多少时间,时间越小波形就越陡,说明压摆率越高;时间越长波形就越缓,压摆率就越低。如果你的产品要过 EMC 的话那就可以使用小的压摆率,因为波形缓和,==如果使用 IO做高速通信的话就可以使用高压摆率==。(SPI通信)

    **DSE(bit5:3):**对应图中的 DSE,当 IO 用作输出的时候用来设置 IO 的驱动能力,总共有 8 个可选选项,如下表:

    驱动能力设置
    驱动能力设置

    SPEED(bit7:6): 对应图中的 SPEED,当 IO 用作输出的时候,此位用来设置 IO 速度,设置如下表:

    image-20241101102526320
    速度设置

    **ODE(bit11):**对应图中的 ODE,当 IO 作为输出的时候,此位用来禁止或者使能开路输出,此位为 0 的时候禁止开路输出, 当此位为 1 的时候就使能开路输出功能。
    PKE(bit12): 对应图中的 PKE,此位用来使能或者禁止上下拉/状态保持器功能,为0 时禁止上下拉/状态保持器,为 1 时使能上下拉和状态保持器。
    PUE(bit13): 图中没有没有给出,当 IO 作为输入的时候,这个位用来设置 IO 使用上下拉还是状态保持器。当为 0 的时候使用状态保持器,当为 1 的时候使用上下拉。状态保持器在IO 作为输入的时候才有用,就是当外部电路断电以后此 IO 口可以保持住以前的状态。
    PUS(bit15:14): 对应图中的 PUS,用来设置上下拉电阻的,一共有四种选项可以选择,如下表:

    image-20241101102634248

    上下拉设置

    **HYS(bit16):**对应图中 HYS,用来使能迟滞比较器,当 IO 作为输入功能的时候有效,用于设置输入接收器的施密特触发器是否使能。如果需要对输入波形进行整形的话可以使能此位。此位为 0 的时候禁止迟滞比较器,为 1 的时候使能迟滞比较器。

  • 添加led设备节点

    在根节点/下创建 LED 灯节点,节点名为gpioled,节点内容如下:

    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
    /*gpio-led*/
    gpio_leds {
    // 指定子节点的地址用1个u32数字表示(常用于reg属性的地址部分)
    #address-cells = <1>;

    // 指定子节点的大小用1个u32数字表示(常用于reg属性的大小部分)
    #size-cells = <1>;

    // 声明设备兼容性,驱动通过此字符串匹配设备
    compatible = "gpio-leds";

    // 定义pinctrl状态名称,此处仅声明"default"状态
    pinctrl-names = "default";

    // 绑定"default"状态到具体的PIN配置节点(来自pinctrl子系统)
    // &表示引用,pinctrl_gpioled是PIN控制器中定义的节点
    pinctrl-0 = <&pinctrl_gpioled>;

    // 定义LED使用的GPIO引脚:
    // - &gpio1:引用GPIO控制器1
    // - 3:使用该控制器的第3号引脚
    // - GPIO_ACTIVE_LOW:低电平有效(高电平熄灭,低电平点亮)
    led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;

    // 节点状态标记为启用(若为"disabled"则表示禁用)
    status = "okay";
    };
  • 进行检查:

    一个引脚一次只能实现一个功能,因此我们需要保证我们使用的引脚只被使用与pinctrl定义了一次。

    • 检查 pinctrl 设置
    • 检查这个 GPIO 有没有被别的外设使用
  • 编译设备树

    我们在linux文件的根目录下编译设备树

    1
    make dtbs

    我们启动成功之后可以在”/proc/device-tree“目录中查看到我们设置的节点是否存在

    image-20250723153934613

GPIO子系统

简介

GPIO名为”General Purpose Input/Output”,通用目的输入/输出,就是常用的引脚。

GPIO可能是芯片自带的,也可能通过I2C、SPI接口扩展:

02_gpio_block

通用功能

  • 可以设为输出:让它输出高低电平;
  • 可以设为输入,读取引脚当前电平;
  • 可以用来触发中断

通用属性

  • Active-High and Active-Low

    以LED为例,需要设置GPIO电平。但是有些电路可能是高电平点亮LED,有些是低电平点亮LED。

    可以使用如下代码:

    1
    2
    gpiod_set_value(gpio, 1);  // 输出高电平点亮LED
    gpiod_set_value(gpio, 0); // 输出低电平点亮LED

    对应同一个目标:点亮LED,对于不同的LED,就需要不同的代码,原因在于上面的代码中1、0表示的是”物理值”。

    如果能使用”逻辑值”,同样的逻辑值在不同的配置下输出对应的物理值,就可以保持代码一致,比如:

    1
    2
    3
    gpiod_set_value(gpio, 1);  // 输出逻辑1
    // 在Active-High的情况下它会输出高电平
    // 在Active-Low的情况下它会输出低电平
  • dtsi文件,设备树中的gpio控制节点:“GPIO组”就是一个GPIO Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    gpio1: gpio@0209c000 {
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x0209c000 0x4000>;
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
    <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    };

    针对gpio我们只关注两个属性:

    1
    2
    gpio-controller;
    #gpio-cells = <2>;
    • “gpio-controller”表示这个节点是一个GPIO Controller,它下面有很多引脚。
    • “#gpio-cells = <2>”表示这个控制器下每一个引脚要用2个32位的数(cell)来描述。比如可以用其中一个cell来表示那是哪一个引脚,用另一个cell来表示它是高电平有效还是低电平有效,甚至还可以用更多的cell来示其他特性。

    当我们在设备树中使用的时候的格式为:

    1
    (diyname)-gpios = <&gpiox x xxxxx>;

    例如我们使用GPIO1_IO03的时候

    1
    2
    3
    4
    5
    // 定义LED使用的GPIO引脚:
    // - &gpio1:引用GPIO控制器1
    // - 3:使用该控制器的第3号引脚
    // - GPIO_ACTIVE_LOW:低电平有效(高电平熄灭,低电平点亮)
    led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;

接口函数

gpio子系统有两套接口函数,新版的是使用struct gpio_desc结构体来表示一个引脚,驱动中需要包含头文件

1
#include <linux/gpio/consumer.h>  

GPIO获取函数

函数原型 参数 作用 使用场景
struct gpio_desc *gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags) dev: 设备指针 con_id: GPIO标识符 flags: 配置标志 获取单个GPIO描述符 基础GPIO控制
struct gpio_desc *gpiod_get_index(struct device *dev, const char *con_id, unsigned int idx, enum gpiod_flags flags) idx: GPIO索引号 获取指定索引的GPIO 设备树中多个同名GPIO
struct gpio_descs *gpiod_get_array(struct device *dev, const char *con_id, enum gpiod_flags flags) 参数同gpiod_get 获取GPIO数组 LED矩阵/按键阵列
struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags) 参数同gpiod_get 自动管理资源的GPIO获取 设备驱动中推荐使用
devm_gpiod_get_index(struct device *dev,const char *con_id,unsigned int idx,enum gpiod_flags flags) - 自动管理的索引GPIO 多GPIO设备
devm_gpiod_get_array(struct device *dev, const char *con_id, enum gpiod_flags flags) - 自动管理的GPIO数组 资源敏感的复杂设备

参数的意义:

  1. dev

    • 类型:struct device *
    • 含义:关联的设备指针,通常由平台驱动或设备树提供。
    • 作用:用于查找设备树中与该设备关联的 GPIO 资源。
    • 示例:&pdev->dev(在平台驱动 probe 函数中)。
  2. con_id

    • 类型:const char *
    • 含义:GPIO 的连接标识符(名称),对应设备树中 gpios 属性的名称。
    • 作用:匹配设备树中 gpios 属性的 name 字段。
    • 示例:
      • 设备树中定义:gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>;
      • con_id 应为 "led"(若设备树中使用 led-gpiosgpios)。
  3. flags

    • 类型:enum gpiod_flags

    • 含义:GPIO 的初始化标志,用于配置方向、上下拉、驱动能力等。

    • 可选值:

      • 方向标志

        • GPIOD_IN:配置为输入。
        • GPIOD_OUT_LOW:配置为输出,默认低电平。
        • GPIOD_OUT_HIGH:配置为输出,默认高电平。
      • 其他标志

        • GPIOD_ACTIVE_LOW:反转 GPIO 极性(低电平有效)。
        • GPIOD_OPEN_DRAIN:开漏输出模式。
        • GPIOD_PULL_UP / GPIOD_PULL_DOWN:启用上拉/下拉电阻(部分平台支持)。
  4. 返回值

    • 成功 :返回指向 struct gpio_desc 的指针(GPIO 描述符)。
    • 失败 :返回错误指针(需用IS_ERR()检查)。

GPIO方向设置

函数原型 参数 作用 返回
int gpiod_direction_input(struct gpio_desc *desc) desc: GPIO描述符 设为输入模式 0成功,负错误码
int gpiod_direction_output(struct gpio_desc *desc, int value) value: 初始输出值
0:低电平(对应物理上的 GND 电压)。
1:高电平(对应物理上的 VDD 电压,如 3.3V)。
设为输出模式 同上

GPIO值读写

函数原型 参数 作用 注意
int gpiod_get_value(const struct gpio_desc *desc) - 读取GPIO值 返回0/1或负错误
void gpiod_set_value(struct gpio_desc *desc, int value) value: 输出值
0:低电平(对应物理上的 GND 电压)。
1:高电平(对应物理上的 VDD 电压,如 3.3V)。
设置GPIO输出 需先设为输出

GPIO释放函数

函数原型 参数 作用 适用对象
void gpiod_put(struct gpio_desc *desc) desc: GPIO描述符 释放单个GPIO gpiod_get获取的
void gpio_free(unsigned gpio) gpio: 全局编号 旧版释放方式 不推荐使用
void gpiod_put_array(struct gpio_descs *descs) descs: GPIO数组 释放整个数组 gpiod_get_array
void devm_gpiod_put(struct device *dev, struct gpio_desc *desc) dev: 关联设备 自动管理释放 devm_gpiod_get
void devm_gpiod_put_array(struct device *dev, struct gpio_descs *descs) - 自动释放数组 devm_gpiod_get_array

驱动编写

我们编写驱动程序leddrv.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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>

#define LEDDRV_CNT 1
#define LEDDRV_NAME "gpioled"
#define LED_OPEN 1
#define LED_CLOSE 0

struct leddev_struct{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
struct gpio_desc *leddes;
};
struct leddev_struct *leddev;

static int ledopen(struct inode *node, struct file *file)
{
file->private_data = leddev;
gpiod_direction_output(leddev->leddes,0);
printk("led device open\n");
return 0;
}

static int ledrelease (struct inode *node, struct file *file)
{
printk("led device closed\n");
return 0;
}
static ssize_t ledwrite (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
struct leddev_struct *leddev=(struct leddev_struct *)file->private_data;
int ret=0;
char receive;
if(size!=1)
return -EINVAL;
ret = copy_from_user(&receive, buf, sizeof(receive));
if(ret!=0){
return -EFAULT;
}

if(receive==LED_CLOSE){
//printk("led close\n");
gpiod_set_value(leddev->leddes,0);
}
else if(receive==LED_OPEN){
//printk("led open\n");
gpiod_set_value(leddev->leddes,1);
}
else {
//printk(KERN_ERR "Invalid command: %c (0x%02x)\n", receive, receive);
return -EINVAL; // 返回无效参数错误
}

return size;
}

static struct file_operations led_fops={
.owner=THIS_MODULE,
.open=ledopen,
.write=ledwrite,
.release=ledrelease,
};

int leddrv_probe(struct platform_device *pdev){
int ret=0;
struct device *dev=&pdev->dev;
printk("matched success!\n");
leddev=devm_kzalloc(dev,sizeof(struct leddev_struct),GFP_KERNEL);
if(!leddev){
printk("kzalloc failed!\n");
return -ENOMEM;
}
if(leddev->major)
{
leddev->devid=MKDEV(leddev->major,0);
ret=register_chrdev_region(leddev->devid,LEDDRV_CNT,LEDDRV_NAME);
}
else{
ret=alloc_chrdev_region(&leddev->devid,0,LEDDRV_CNT,LEDDRV_NAME);
leddev->major=MAJOR(leddev->devid);
}
if(ret<0){
printk("devid register failed!\n");
goto chrdev_fail;
}
printk("major is %d\n",leddev->major);

cdev_init(&leddev->cdev,&led_fops);
ret=cdev_add(&leddev->cdev,leddev->devid,LEDDRV_CNT);
if(ret<0)
{
printk("cdev create error!\n");
goto cdev_fail;
}

leddev->class=class_create(THIS_MODULE,LEDDRV_NAME);
if(IS_ERR(leddev->class))
{
printk("class create error!\n");
ret=PTR_ERR(leddev->class);
goto class_fail;
}

leddev->device=device_create(leddev->class,NULL,leddev->devid,NULL,LEDDRV_NAME);
if(IS_ERR(leddev->device))
{
printk("device create error!\n");
ret=PTR_ERR(leddev->device);
goto device_fail;
}

leddev->leddes=devm_gpiod_get(dev,"led",GPIOD_OUT_LOW);
if(IS_ERR(leddev->leddes))
{
printk("leddes get error!\n");
ret=PTR_ERR(leddev->leddes);
goto leddes_fail;
}
platform_set_drvdata(pdev, leddev);


return 0;
leddes_fail:
device_destroy(leddev->class,leddev->devid);
device_fail:
class_destroy(leddev->class);
class_fail:
cdev_del(&leddev->cdev);
cdev_fail:
unregister_chrdev_region(leddev->devid,LEDDRV_CNT);
chrdev_fail:

return ret;
}
int leddev_resume(struct platform_device *pdev){
struct leddev_struct *leddev = platform_get_drvdata(pdev);

device_destroy(leddev->class,leddev->devid);
class_destroy(leddev->class);
cdev_del(&leddev->cdev);
unregister_chrdev_region(leddev->devid,LEDDRV_CNT);

return 0;
}


const struct of_device_id led_device_id[]={
{.compatible="gpio-led"},
{},
};
struct platform_driver led_dev_driver={
.probe=leddrv_probe,
.driver={
.of_match_table=led_device_id,
.name="gpioled",
},
.remove=leddev_resume,
};

static int __init leddrv_init(void){
int ret=0;
platform_driver_register(&led_dev_driver);
return ret;
}

static void __exit leddrv_exit(void){
platform_driver_unregister(&led_dev_driver);
}


module_init(leddrv_init);
module_exit(leddrv_exit);

MODULE_AUTHOR("KEVIN");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("GPIO LED Platform Driver");

代码分析

驱动流程图为:

image-20241016140835398

应用程序

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char **argv) {
int fd;
char input_mode;

if (argc < 3) {
printf("Usage: ./%s /dev/<device> module(0/1)\n", argv[0]);
exit(1);
}

// 打开设备文件(确保有写权限)
fd = open(argv[1], O_WRONLY);
if (fd < 0) {
perror("open failed"); // 显示系统错误
exit(1);
}

// 解析并验证模块参数
input_mode = atoi(argv[2]);
if (input_mode != 0 && input_mode != 1) {
printf("Error: module must be 0 or 1\n");
exit(1);
}

// 写入数据并验证写入成功
if (write(fd, &input_mode, sizeof(input_mode)) != sizeof(input_mode)) {
perror("write failed"); // 显示系统错误
printf("Failed to write mode=%d\n", input_mode);
exit(1);
}

close(fd); // 显式关闭文件描述符(虽然 exit 会自动关闭,但显式更好)
exit(0);
}

Qt编写

main.cpp

1
2
3
4
5
6
7
8
9
10
11
#include <QApplication>
#include "mainwindow.h"

int main(int argc, char *argv[]) {
QApplication app(argc, argv);
MainWindow window;
window.setWindowTitle("GPIO LED 控制器");
window.show();
return app.exec();
}

mainwindow.cpp

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
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
: QWidget(parent),
deviceFile("/dev/gpioled") // 初始化时直接构造QFile对象
{
// 初始化界面
btnOn = new QPushButton("LED ON", this);
btnOff = new QPushButton("LED OFF", this);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(btnOn);
layout->addWidget(btnOff);

// 连接信号与槽
connect(btnOn, &QPushButton::clicked, this, &MainWindow::turnOn);
connect(btnOff, &QPushButton::clicked, this, &MainWindow::turnOff);

// 打开设备文件(仅在初始化时执行一次)
if (!deviceFile.open(QIODevice::WriteOnly)) {
QMessageBox::critical(this, "Error", "无法打开设备: " + deviceFile.errorString());
// 关闭应用程序(或禁用按钮)
qApp->exit(1);
}
}

MainWindow::~MainWindow() {
if (deviceFile.isOpen()) {
deviceFile.close(); // 程序退出前关闭文件
}
}

void MainWindow::turnOn() {
writeLedStatus(1); // 写入1打开LED
}

void MainWindow::turnOff() {
writeLedStatus(0); // 写入0关闭LED
}

void MainWindow::writeLedStatus(int status) {
char data = static_cast<char>(status); // 确保类型安全

// 直接使用已打开的设备文件
if (deviceFile.write(&data, 1) != 1) {
QMessageBox::warning(this, "警告", "写入设备失败: " + deviceFile.errorString());
}
}

mainwindow.h

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
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QPushButton>
#include <QFile>
#include <QMessageBox>

class MainWindow : public QWidget {
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();

private slots:
void turnOn();
void turnOff();

private:
void writeLedStatus(int status);
QPushButton *btnOn;
QPushButton *btnOff;
QFile deviceFile; // 将QFile作为成员变量,避免重复创建
};

#endif // MAINWINDOW_H

Qt 信号与槽

信号与槽( Signal & Slot )是 Qt 编程的基础,也是 Qt 的一大创新。

  • 信号(Signal)

    • 就是在特定情况下被发射的事件,例如 PushButton 最常见的信号就是鼠标单击时发射的 clicked() 信号,一个 ComboBox
  • 槽(Slot )

    • 就是对信号响应的函数。槽就是一个函数,与一般的 C++ 函数是一样的,可以定义在类的任何部分( public 、 private 或 protected ),可以具有任何参数,也可以被直接调用。

    • 槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数
      被自动执行。

    • 槽可以是任何成员函数、普通全局函数、静态函数

  • 关联:

    信号与槽关联是用QObject::connect() 函数实现的,其基本格式有两种:

    • 方式一:

      1
      QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));

      connect()是 QObject 类的一个静态函数,而 QObject 是所有 Qt 类的基类,在实际调用时可以忽略前面的限定符,所以可以直接写为:

      1
      2
      3
      4
      5
      6
      7
      8
      connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
      /*
      - sender是**发射信号的对象的名称**
      - signal() 是**信号名称**。信号可以看做是特殊的函数,需要带括号,有参数时还需要指明参数。
      - receiver 是**接收信号的对象名称**
      - slot() 是**槽函数的名称**,需要带括号,有参数时还需要指明参数。
      - SIGNAL和SLOT是 Qt 的宏,用于指明信号和槽,并将它们的参数转换为相应的字符
      */
    • 方式二(==推荐==):

      1
      2
      3
      4
      5
      6
      7
      connect(sender,&signal,receiver,&slot)
      /*
      - sender是**发射信号的对象的名称**
      - &signal是**信号名称**。信号可以看做是特殊的函数,需要带括号,有参数时还需要指明参数。
      - receiver 是**接收信号的对象名称**
      - &slot是**槽函数的名称**,需要带括号,有参数时还需要指明参数。
      */
  • 流程

    当一个信号被发射时,与其关联的槽函数通常被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。

    image-20250609212739315
  • 特点:

    • ==在使用信号与槽的类中,必须在类的定义中加入宏Q_OBJECT (特别重要) 。==

    • 一个信号可以连接多个槽,例如:

      1
      2
      connect(pushButton,SIGNAL(clickedO),this,SLOT(hide();
      connect(pushButton,SIGNAL(clicked(),this,SLOT(close();

      这是当一个对象pushButton 的 被单击 时,所在窗体有两个槽进行响应,一 个 hide() 用于隐藏主窗体,一个 close 用于关闭主窗体

    • 信号槽函数 带有参数时,在connect() 函数里,要写明参数的类型,但可以不写参数名称。

      1
      2
      connect(pushButton,SIGNAL(objectNameChanged(QString)),this,SIGNAL(window TitelChan
      ged(QString)));
    • 多个信号可以连接同一个槽

      1
      2
      3
      connect( pushButton ,SIGNAL(clicked()),this, close()
      connect( pushButton_2 ,SIGNAL(clicked()),this, close()
      connect( pushButton_3 ,SIGNAL(clicked()),this, close()

      当任何一个pushButton 被单击时,都会执行 close() 函 数,进而关闭或者退出程序。

    • 一个信号可以连接另外一个信号(说明了 connect 万物皆可连,非常好用!) ,例如:

      1
      2
      connect(pushButton,SIGNAL(objectNameChanged(QString)),this,SIGNAL(window TitelChan
      ged(QString)));

      这样,当一个信号发射时,也会发射另外一个信号,实现某些特殊的功能。

    • 严格的情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,会出现编译错误或运行错误。例如:

      • 当你发送一个信号时,Qt 需要将这个信号携带的数据(参数)传递给连接的槽函数。
      • 如果槽函数期望接收某些参数,而信号没有提供足够的参数,就会导致数据缺失或类型不匹配。
      • 如果使用==&符号时==,编译器会强制检查信号和槽的参数是否一致,否则无法通过编译。因此必须要参数保持一致。
      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
      //--------------------实例1--------------------------------
      #include <QObject>
      #include <QDebug>

      class MyClass : public QObject {
      Q_OBJECT

      public slots:
      void receiveData(int value, QString text) {
      qDebug() << "Received:" << value << text;
      }
      };
      QPushButton button;
      MyClass obj;

      // 错误:clicked(bool) 只有一个 bool 参数,而槽需要 int + QString
      connect(&button, &QPushButton::clicked, &obj, &MyClass::receiveData);

      //--------------------实例2--------------------------------
      class MyEmitter : public QObject {
      Q_OBJECT

      signals:
      void dataSent(int value, QString text, double extra); // 3 个参数
      };

      class MyReceiver : public QObject {
      Q_OBJECT

      public slots:
      void receiveData(int value, QString text) { // 2 个参数
      qDebug() << "Received:" << value << text;
      }
      };
      MyEmitter emitter;
      MyReceiver receiver;

      connect(&emitter, &MyEmitter::dataSent, &receiver, &MyReceiver::receiveData);
      //合法:因为信号有 3 个参数,槽有 2 个参数,信号参数多于槽参数,多余参数被忽略。
    • 信号只需声明无需定义,所以我们只需要在 mianwindow.h 里声明信号即可。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      #ifndef MAINWINDOW_H
      #define MAINWINDOW_H
      #include <QMainWindow>
      /* 引入QPushButton */
      #include <QPushButton>
      class MainWindow : public QMainWindow
      {
      Q_OBJECT
      public:
      MainWindow(QWidget *parent = nullptr);
      ~MainWindow();

      signals:
      /* 声明一个信号,只需声明,无需定义 */
      void pushButtonTextChanged();
      };
      #endif // MAINWINDOW_H

QFile

QFile 类提供了读取和写入文件的接口。在嵌入式里如果需要读写文件,最简单的方法就是用Qfile。QFile 是一个读写文本、二进制文件和资源的I/O 设备。QFile 可以自己使用,也可以更方便地与QTextStream 或QDataStream 一起使用。

  • 对一个文本文件的操作流程

    image-20250609220624715

  • 构造函数

    使用 QFile 类操作文件之前,程序中需引入头文件。创建 QFile 类的对象,常用的构造函数有:

    1
    2
    QFile::QFile() QFile::QFile(const QString &name)
    QFile file("/dev/gpioled");

    参数 name 用来指定要操作的目标文件,包含文件的存储路径和文件名,存储路径可以使用绝对路径(如”D:/Demo/test.txt”)或者相对路径(比如”./Demo/test.txt”),路径中的分隔符要用 “/“ 表示。不支持使用其他分隔符(例如’')。所以在Windows、 Linux 或者Mac 里文件的路径都是用’/‘

    • 通常情况下,我们会调用第二个构造函数,直接指明要操作的文件。对于第一个构造函数创建的 QFile 对象,需要再调用setFileName() 方法指明要操作的文件。

    • void QFile::setFileName(const QString &name)
      /*例子:*/
      QFile file;
      file.setFileName("/sys/devices/platform/leds/leds/beep/brightness");
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

      - 设置文件的名称。`name` 可以不包含路径、包含相对路径,也可以是绝对路径。

      如果文件已经被打开,则不要调用此函数。

      - **检查函数**

      可以使用exists()检查文件是否存在,并使用remove()删除文件

      if (!file.exists()) { return; }
    1
    2
    3
    4
    5
    6
    7
    8

    - **打开函数**

    ```c++
    bool QFile::open(OpenMode mode)
    /*例子:*/
    if(!file.open(QIODevice::ReadWrite))
    qDebug()<<file.errorString();

    QFile::open()函数打开文件时需要传递 QIODevice::OpenModeFlag 枚举类型的参数,决定文件以什么方式打开

    mode 参数用来指定文件的打开方式,下表罗列了此参数的可选值以及各自的含义

    打开方式 含 义
    QIODevice::ReadOnly 只能对文件进行读操作
    QIODevice::WriteOnly 只能对文件进行写操作,如果目标文件不存在,会自行创建一个新文件。
    QIODevice::ReadWrite 等价于 ReadOnly | WriteOnly,能对文件进行读和写操作。
    QIODevice::Append 以追加模式打开文件,写入的数据会追加到文件的末尾(文件原有的内容保留)。
    QIODevice::Truncate 以重写模式打开,写入的数据会将原有数据全部清除。注意,此打开方式不能单独使用,通常会和 ReadOnly 或 WriteOnly 搭配。
    QIODevice::Text 读取文件时,会将行尾结束符(Unix 系统中是 “\n”,Windows 系统中是 “\r\n”)转换成‘\n’;将数据写入文件时,会将行尾结束符转换成本地格式,例如 Win32 平台上是‘\r\n’。
  • 读函数

    1
    qint64 read(char *data, qint64 maxSize)
    • 功能:从设备读取原始字节到缓冲区
    • 返回值
      • >0:成功读取的字节数
      • 0:无更多数据(非错误)
      • -1:错误(如设备关闭或只写模式)
    • 关键点:需手动管理缓冲区,但错误处理最可靠
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    QFile file("example.txt");
    if (!file.open(QIODevice::ReadOnly)) { // 以读模式打开文件
    qDebug() << "无法打开文件读取:" << file.errorString();
    return -1;
    }

    char buffer[100];
    qint64 bytesRead = file.read(buffer, sizeof(buffer) - 1); // 保留最后一个字节给'\0'
    if (bytesRead > 0) {
    buffer[bytesRead] = '\0'; // 添加字符串结束符
    qDebug() << "读取到:" << buffer;
    } else {
    qDebug() << "读取失败:" << file.errorString();
    }
    file.close();
  • 写函数

    1
    qint64 write(const char *data, qint64 len);

    参数说明

    • data :指向要写入数据的缓冲区的指针。
    • len :要写入的字节数(最大值为 qint64 类型)。

    返回值

    • 成功时返回实际写入的字节数(可能小于 len,例如磁盘空间不足)。
    • 如果发生错误(如文件未打开或不可写),返回 -1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    QFile file("example.txt");
    if (!file.open(QIODevice::WriteOnly)) { // 以写模式打开文件
    qDebug() << "无法打开文件写入:" << file.errorString();
    return -1;
    }

    const char *data = "Hello World!";
    qint64 written = file.write(data, strlen(data)); // 写入字符串
    if (written != strlen(data)) {
    qDebug() << "写入失败:" << written << "字节(应写入" << strlen(data) << "字节)";
    } else {
    qDebug() << "成功写入" << written << "字节";
    }
    file.close();

实验效果

ae6f19608e040334195dad59abef446