📝本文概览

本文将介绍PetaLinux驱动开发中的Pinctrl子系统和GPIO子系统的相关知识,并借助Zynq开发板完成一个点灯实验。

📚参考视频

正点原子嵌入式Linux驱动开发:点击观看
ALINX Zynq MPSoC Linux驱动开发:点击观看
前者更适合理论学习,后者更适合实操。

1. Pinctrl子系统

从Vivado的PS配置界面可以看出,每个MIO引脚都有多种复用选项,因此如果需要使用某个MIO引脚,需要pinctrl子系统进行相应的配置。

pinctrl子系统在设备树中配置。

构建PetaLinux工程并执行petalinux-build编译后,可以在[project_dir]/components/plnx_workspace/device-tree/device-tree目录中找到设备树的相关文件:

设备树的包含结构如下:

1
2
3
4
5
// system_top.dts 主设备树文件
#include "zynq-7000.dtsi" // Zynq-7000系列SOC基础定义
#include "pcw.dtsi" // Processing Configuration Wizard生成的配置
// ...
#include "system-user.dtsi" // 用户自定义配置文件

其中,system-user.dtsi文件位于另一个目录[project_dir]/project-spec/meta-user/recipes-bsp/device-tree/files中,这是用户添加自定义设备树节点的地方。

不同厂商的开发板有不同的pinctrl配置方法,这是因为它们的底层驱动实现存在差异。

构建PetaLinux工程并编译后,可以在[project_dir]/build/tmp/work-shared/zynq-generic-7z100/kernel-source目录中找到内核源码。

在内核源码kernel-source/Documentation/devicetree/bindings/pinctrl/xlnx,zynq-pinctrl.yaml文件中,可以找到Zynq pinctrl的标准配置示例:

UART1 Pinctrl配置示例

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
examples:
- |
#include <dt-bindings/pinctrl/pinctrl-zynq.h>
pinctrl0: pinctrl@700 {
compatible = "xlnx,zynq-pinctrl";
reg = <0x700 0x200>;
syscon = <&slcr>;

pinctrl_uart1_default: uart1-default {
mux {
groups = "uart1_10_grp";
function = "uart1";
};

conf {
groups = "uart1_10_grp";
slew-rate = <0>;
power-source = <IO_STANDARD_LVCMOS18>;
};

conf-rx {
pins = "MIO49";
bias-high-impedance;
};

conf-tx {
pins = "MIO48";
bias-disable;
};
};
};

uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1_default>;
};

关于mux和conf的配置该yaml文件中已经解释得很清楚了,比如groups、function的取值有哪些、引脚的配置有哪些、mux和conf中必须包含的内容等等,这里介绍一下这个示例的配置:

pinctrl0节点:在zynq-7000.dtsi中已定义基本信息(如compatible属性),用户只需在system-user.dtsi中通过&pinctrl0追加即可,示例这里是为了体现完整性。

配置结构

  • mux节点:配置引脚复用功能
  • conf节点:配置引脚电气特性
  • conf-rx/conf-tx:针对特定引脚的专门配置

引脚分组
每个外设都有多个可选的引脚组合(grp),以UART1为例:

  • MIO8-9:uart1_0_grp
  • MIO12-13:uart1_1_grp
  • ······
  • MIO48-49:uart1_10_grp

示例中选择MIO48-49作为UART1,因此使用uart1_10_grp

conf参数含义

  • slew-rate:引脚转换速率(0=慢速,1=快速),影响信号的上升和下降时间
  • power-source:I/O电平标准
    MIO48-49属于bank1,通常对应手册中的bank501。有效值为1,2,3,4,这里使用宏定义的方式,相关的宏定义在dt-bindings/pinctrl/pinctrl-zynq.h中,所以前面include了这个文件:
    1
    2
    3
    4
    #define IO_STANDARD_LVCMOS18    1
    #define IO_STANDARD_LVCMOS25 2
    #define IO_STANDARD_LVCMOS33 3
    #define IO_STANDARD_HSTL 4
  • bias-high-impedance:高阻抗偏置
  • bias-disable:禁用上下拉

一个外设可以具有多种引脚配置状态,这里示例中uart1只有一种配置,即default,它指向的就是pinctrl中的pinctrl_uart1_default。假如有多种配置,比如:

1
2
3
4
5
6
&i2c0 {
pinctrl-names = "default", "gpio", "sleep";
pinctrl-0 = <&i2c0_pins_default>;
pinctrl-1 = <&i2c0_pins_gpio>;
pinctrl-2 = <&i2c0_pins_sleep>;
};

对应的pinctrl节点中则需要定义三种状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
&pinctrl0 {
i2c0_pins_default: i2c0-default {
mux {
// 默认模式配置
};
conf {
// 默认模式配置
};
}

i2c0_pins_gpio: i2c0-gpio {
// GPIO模式配置
}

i2c0_pins_sleep: i2c0-sleep {
// 低功耗模式配置
}
}

驱动程序可以根据实际需要在不同状态间切换:
default:正常工作模式,通常在驱动初始化时请求
sleep:低功耗模式,设备进入睡眠状态时使用
gpio:通用GPIO模式,需要将引脚作为GPIO使用时切换

现在假设使用MIO0-MIO3控制LED,则需要在system-user.dtsi中添加如下配置:

LED Pinctrl配置

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
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/pinctrl-zynq.h>

/include/ "system-conf.dtsi"
/ {
led_gpio {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led_default>;

led-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>, /* led0 */
<&gpio0 1 GPIO_ACTIVE_HIGH>, /* led1 */
<&gpio0 2 GPIO_ACTIVE_HIGH>, /* led2 */
<&gpio0 3 GPIO_ACTIVE_HIGH>; /* led3 */
};
};

&pinctrl0 {
pinctrl_led_default: led-default {
mux {
function = "gpio0";
groups = "gpio0_0_grp", "gpio0_1_grp",
"gpio0_2_grp", "gpio0_3_grp";
};

conf {
groups = "gpio0_0_grp", "gpio0_1_grp",
"gpio0_2_grp", "gpio0_3_grp";
pins = "MIO0", "MIO1", "MIO2", "MIO3";
slew-rate = <0>;
power-source = <IO_STANDARD_LVCMOS33>;
bias-disable;
};
};
};

这里相比于之前说的多了led-gpios这一个属性,这就属于接下来要讲的gpio子系统的内容了。

2. GPIO子系统

正点原子Linux开发手册提到:

2.1 GPIO设备树配置格式

GPIO在设备树中的配置格式相当固定,详细说明可以在内核文档中找到:

kernel-source/Documentation/devicetree/bindings/gpio/gpio.txt

基本语法:

1
[name]-gpios = <&gpiox offset flags>;

参数解释:

  • [name]:自定义名称
  • &gpiox:GPIO控制器节点引用(Zynq-7000中所有MIO都在gpio0中)
  • offset:表示GPIO编号,即gpio0的第几个I/O
  • flags:配置标志,可以在kernel-source/include/dt-bindings/gpio/gpio.h中查看,可以使用或逻辑组合,比如配置高电平有效且上拉则flags为GPIO_ACTIVE_HIGH | GPIO_PULL_UP

2.2 GPIO子系统API

GPIO相关的API文档在kernel-source/Documentation/driver-api/gpio/consumer.rstkernel-source/Documentation/driver-api/gpio/board.rst中。

使用API需包含linux/gpio/consumer.h头文件,常用的API如下。

2.2.1 获取GPIO描述符

1
struct gpio_desc *gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags)

dev参数可以从device_create中得到(后面编写内核驱动代码时会提到)。

con_id就是前面设备树定义中[name]-gpios的[name],比如aaa-gpios,con_id就是”aaa”。

flags用于指定方向和初始值,可选值如下:

flags可选值

· GPIOD_ASIS 或 0 表示不初始化 GPIO,方向必须稍后使用专用函数设置。
· GPIOD_IN 初始化 GPIO 为输入.
· GPIOD_OUT_LOW 初始化 GPIO 为输出,且输出值为 0.
· GPIOD_OUT_HIGH 初始化 GPIO 为输出,且输出值为 1.
· GPIOD_OUT_LOW_OPEN_DRAIN 与 GPIOD_OUT_LOW 相同,但使用开漏输出方式。
· GPIOD_OUT_HIGH_OPEN_DRAIN 与 GPIOD_OUT_HIGH 相同,但使用开漏输出方式。

如果有多个GPIO,可以分开定义:

1
2
led0-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>;
led1-gpios = <&gpio0 1 GPIO_ACTIVE_HIGH>;

然后分别获取描述符:

1
2
led0 = gpiod_get(device, "led0", GPIOD_OUT_HIGH);
led1 = gpiod_get(device, "led1", GPIOD_OUT_HIGH);

也可以像前面led-gpios一样一次性定义多个gpio,此时则可以使用以下函数:

1
struct gpio_desc *gpiod_get_index(struct device *dev, const char *con_id, unsigned int idx,enum gpiod_flags flags)

可以看到,多了一个参数idx,代表获取第几个gpio,比如:

1
2
led0 = gpiod_get_index(device, "led", 0, GPIOD_OUT_HIGH);
led1 = gpiod_get_index(device, "led", 1, GPIOD_OUT_HIGH);

但这样还是每次只控制一个gpio,如果想同时控制多个gpio可以使用数组的方式:

1
struct gpio_descs *gpiod_get_array(struct device *dev,const char *con_id,enum gpiod_flags flags)

参数和gpiod_get一样,只是返回的结构体不一样:

1
2
3
4
5
struct gpio_descs {
struct gpio_array *info;
unsigned int ndescs;
struct gpio_desc *desc[];
}

返回的结构体中包含了一个gpio_desc类型的结构体数组。

2.2.2 GPIO方向配置

如果前面gpiod_get没有设置方向和初值,则可以使用以下函数设置:

1
2
3
4
5
// 设置为输入模式
int gpiod_direction_input(struct gpio_desc *desc)

// 设置为输出模式,并指定初值
int gpiod_direction_output(struct gpio_desc *desc, int value)

参数说明:

  • desc:之前获取的GPIO描述符
  • value:输出初值(0或1)

2.2.3 GPIO读写操作

1
2
int gpiod_get_value(const struct gpio_desc *desc);
void gpiod_set_value(struct gpio_desc *desc, int value);
1
2
3
4
5
6
7
8
int gpiod_get_array_value(unsigned int array_size, 
struct gpio_desc **desc_array,
struct gpio_array *array_info,
unsigned long *value_bitmap);
int gpiod_set_array_value(unsigned int array_size,
struct gpio_desc **desc_array,
struct gpio_array *array_info,
unsigned long *value_bitmap);

仔细观察可以发现,前面三个参数刚好来自gpiod_get_array返回的结构体。

value_bitmap是位图,位图的第N位为0/1,对应的gpio_descs-> desc[N]为0/1。

使用示例:

1
2
3
4
5
6
7
8
9
// 获取GPIO数组
struct gpio_descs *my_gpio_descs = gpiod_get_array(device, "led", GPIOD_OUT_LOW);

// 设置LED0和LED2亮起(位图为0101 = 5)
unsigned long my_gpio_value_bitmap = 5;
gpiod_set_array_value(my_gpio_descs->ndescs,
my_gpio_descs->desc,
my_gpio_descs->info,
&my_gpio_value_bitmap);

2.2.4 释放GPIO描述符

单个GPIO

1
void gpiod_put(struct gpio_desc *desc);

GPIO数组

1
void gpiod_put_array(struct gpio_descs *descs);

3. 点灯实验

3.1 硬件设计配置

Vivado Block Design配置

在Vivado Block Design中完成基础系统配置:

  1. 外设接口启用:打开SD卡、以太网、串口并根据开发板手册选择对应的MIO引脚
  2. 电平设置:配置Bank 0和Bank 1的电平(对应板子手册的Bank500/501电平)

由于我所使用的开发板只有4个PL LED,没有PS LED,因此通过EMIO来控制LED,位宽为4。

GPIO引脚导出

将GPIO_O进行Make External操作:
注意:如果使用MIO控制LED,则不需要Make External和引脚绑定操作

引脚约束配置

在I/O Planning中将导出的GPIO信号绑定到PL LED引脚。

生成和编译

完成以上配置后:

  1. 生成XSA文件
  2. 构建PetaLinux工程
  3. 执行编译操作

3.2 设备树配置

由于使用的是EMIO方式,不涉及引脚复用,所以不需要配置pinctrl子系统,只需要配置GPIO子系统即可。

修改system-user.dtsi文件:

1
2
3
4
5
6
7
8
9
10
11
#include <dt-bindings/gpio/gpio.h>
/include/ "system-conf.dtsi"

/ {
led_gpio{
led-gpios = <&gpio0 54 GPIO_ACTIVE_HIGH>, /* led0 */
<&gpio0 55 GPIO_ACTIVE_HIGH>, /* led1 */
<&gpio0 56 GPIO_ACTIVE_HIGH>, /* led2 */
<&gpio0 57 GPIO_ACTIVE_HIGH>; /* led3 */
};
};

Zynq-7000系列中,MIO占据了gpio0的前54个编号(0-53),EMIO从编号54开始。

3.3 Linux驱动程序开发

3.3.1 字符设备驱动模板

以下是一个简单的字符设备驱动模板。

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define xxx_COUNT 1
#define xxx_NAME "xxx"

static struct xxx_device{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
} xxx;

// 文件操作函数
static int xxx_open(struct inode *inode, struct file *filp) {
return 0;
}

static int xxx_release(struct inode *inode, struct file *filp) {
return 0;
}

static ssize_t xxx_write(struct file *filp, const char *buf,
size_t count, loff_t *ppos) {
int retval;
unsigned char databuf[x];

retval = copy_from_user(databuf, buf, count);
if(retval < 0) {
printk("copy_from_user error\r\n");
return -EFAULT;
}

// 执行操作
// ...
return 0;
}

static ssize_t xxx_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos) {
int retval = 0;
unsigned char databuf[x];

// 执行操作
// ...

/* 向用户空间发送数据 */
retval = copy_to_user(buf, databuf, count);
if(retval < 0) {
printk("copy_to_user error\r\n");
return -EFAULT;
}

return 0;
}

// 文件操作结构体
static const struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.release = xxx_release,
.write = xxx_write,
.read = xxx_read
// 其余文件操作...
};

// 模块初始化函数
static int __init xxx_init(void) {
int retval;

// 1. 申请设备号
retval = alloc_chrdev_region(&xxx.devid, 0, xxx_COUNT, xxx_NAME);
if (retval < 0) {
printk("fail to register devid\r\n");
goto fail_devid;
}

// 2. 添加字符设备
xxx.cdev.owner = THIS_MODULE;
cdev_init(&xxx.cdev, &xxx_fops);
retval = cdev_add(&xxx.cdev, xxx.devid, xxx_COUNT);
if (retval < 0) {
printk("fail to add cdev\r\n");
goto fail_cdev;
}

// 3. 创建设备类
xxx.class = class_create(THIS_MODULE, xxx_NAME);
if(IS_ERR(xxx.class)){
printk("fail to create device class\r\n");
retval = PTR_ERR(xxx.class);
goto fail_class;
}

// 4. 自动创建设备文件:/dev/xxx
xxx.device = device_create(xxx.class, NULL, xxx.devid, NULL, xxx_NAME);
if(IS_ERR(xxx.device)){
printk("fail to create device\r\n");
retval = PTR_ERR(xxx.device);
goto fail_device;
}

// 5. 其余特定需求的初始化
// ...

return 0;

// 错误处理
fail_device:
class_destroy(xxx.class);
fail_class:
cdev_del(&xxx.cdev);
fail_cdev:
unregister_chrdev_region(xxx.devid, xxx_COUNT);
fail_devid:
return retval;
}

// 模块退出函数
static void __exit xxx_exit(void) {
// 销毁设备文件
device_destroy(xxx.class, xxx.devid);
// 销毁设备类
class_destroy(xxx.class);
// 删除字符设备
cdev_del(&xxx.cdev);
// 释放设备号
unregister_chrdev_region(xxx.devid, xxx_COUNT);
}

module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");

对于特定的驱动,只需要在该模板上修改增删即可。

3.3.2 LED驱动实现

对于特定的LED控制驱动,只需要在通用模板上进行以下修改:

  1. 去除read操作:LED只需要写入控制,不需要读取状态
  2. init函数:添加GPIO描述符的获取操作
  3. exit函数:添加GPIO资源的释放操作
  4. write函数实现:根据用户数据控制对应LED

LED驱动完整代码

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h> // of_find_node_by_path所需
#include <linux/gpio/consumer.h> // GPIO API所需

#define LED_COUNT 1
#define LED_NAME "led"

// LED设备结构体
static struct led_device{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
struct gpio_descs *led_gpios; // GPIO描述符数组
} led;

// 文件操作函数
static int led_open(struct inode *inode, struct file *filp) {
return 0;
}

static int led_release(struct inode *inode, struct file *filp) {
return 0;
}

static ssize_t led_write(struct file *filp, const char *buf,
size_t count, loff_t *ppos) {
int retval;
unsigned char databuf[1];
unsigned long led_bitmap;

// 从用户空间复制数据
retval = copy_from_user(databuf, buf, count);
if(retval < 0) {
printk("copy_from_user error\r\n");
return -EFAULT;
}

// 根据databuf的值决定打开哪个灯(位图方式)
led_bitmap = databuf[0] == 0 ? 1 : // LED0: 0001
databuf[0] == 1 ? 2 : // LED1: 0010
databuf[0] == 2 ? 4 : // LED2: 0100
databuf[0] == 3 ? 8 : // LED3: 1000
0; // 其他值关闭所有LED

// 设置GPIO数组的值
gpiod_set_array_value(led.led_gpios->ndescs,
led.led_gpios->desc,
led.led_gpios->info,
&led_bitmap);

return 0;
}

// 文件操作结构体
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
};

// 模块初始化函数
static int __init led_init(void) {
int retval;

// 1. 注册设备号
retval = alloc_chrdev_region(&led.devid, 0, LED_COUNT, LED_NAME);
if (retval < 0) {
printk("fail to register devid\r\n");
goto fail_devid;
}

// 2. 添加字符设备
led.cdev.owner = THIS_MODULE;
cdev_init(&led.cdev, &led_fops);
retval = cdev_add(&led.cdev, led.devid, LED_COUNT);
if (retval < 0) {
printk("fail to add cdev\r\n");
goto fail_cdev;
}

// 3. 创建设备类
led.class = class_create(THIS_MODULE, LED_NAME);
if(IS_ERR(led.class)) {
printk("fail to create device class\r\n");
retval = PTR_ERR(led.class);
goto fail_class;
}

// 4. 自动创建设备文件:/dev/led
led.device = device_create(led.class, NULL, led.devid, NULL, LED_NAME);
if(IS_ERR(led.device)) {
printk("fail to create device\r\n");
retval = PTR_ERR(led.device);
goto fail_device;
}

// 5. 指定设备树节点(防止多个led-gpios情况)
led.device->of_node = of_find_node_by_path("/led_gpio");
if(led.device->of_node == NULL) {
printk("fail to find node\r\n");
retval = -EINVAL;
goto fail_findnd;
}

// 6. 获取GPIO描述符数组
led.led_gpios = gpiod_get_array(led.device, "led", GPIOD_OUT_LOW);
if(IS_ERR(led.led_gpios)) {
printk("fail to get gpio array\r\n");
retval = PTR_ERR(led.led_gpios);
goto fail_gpio;
}

printk("LED driver loaded successfully\r\n");
return 0;

// 错误处理
fail_gpio:
fail_findnd:
device_destroy(led.class, led.devid);
fail_device:
class_destroy(led.class);
fail_class:
cdev_del(&led.cdev);
fail_cdev:
unregister_chrdev_region(led.devid, LED_COUNT);
fail_devid:
return retval;
}

// 模块退出函数
static void __exit led_exit(void) {
// 释放GPIO描述符
gpiod_put_array(led.led_gpios);
// 销毁设备文件
device_destroy(led.class, led.devid);
// 销毁设备类
class_destroy(led.class);
// 删除字符设备
cdev_del(&led.cdev);
// 释放设备号
unregister_chrdev_region(led.devid, LED_COUNT);

printk("LED driver unloaded\r\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("raiset");
MODULE_DESCRIPTION("LED GPIO Driver for PetaLinux");

3.4 用户空间应用程序

📱应用程序作用

用户空间应用程序负责与内核驱动交互,通过标准的文件操作接口(open/write/close)实现LED控制。

LED控制应用程序

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

#define FILE_NAME "/dev/led"

int main(int argc, char **argv) {
int fd;
int ret;
unsigned char databuf[1];

// 参数数量检查
if(argc != 2) {
printf("Usage: %s <led_number>\r\n", argv[0]);
printf(" led_number: 0-3 (LED0-LED3)\r\n");
return -1;
}

// 打开设备文件
fd = open(FILE_NAME, O_RDWR);
if(fd < 0) {
printf("Error: cannot open file %s\r\n", FILE_NAME);
printf("Please check if the driver is loaded: sudo insmod led.ko\r\n");
return -1;
}

// 将字符串转换为整数
databuf[0] = (unsigned char)atoi(argv[1]);

// 参数范围检查
if(databuf[0] > 3) {
printf("Warning: LED number should be 0-3, got %d\r\n", databuf[0]);
}

// 写入数据到驱动
ret = write(fd, databuf, 1);
if(ret < 0) {
printf("Error: cannot write to file %s\r\n", FILE_NAME);
close(fd);
return -1;
}

printf("LED%d controlled successfully\r\n", databuf[0]);

// 关闭设备文件
ret = close(fd);
if(ret < 0) {
printf("Error: cannot close file %s\r\n", FILE_NAME);
return -1;
}

return 0;
}

⚙️主要功能:

  1. 参数验证:检查命令行参数是否正确
  2. 设备文件操作:打开/dev/led设备文件
  3. 数据传输:将LED编号写入驱动

3.5 交叉编译

也可以参考第4节使用petalinux-create的方法,这样无需配置交叉编译器,也不需要自己写Makefile。

由于开发主机(x86/x64)和目标开发板(ARM)架构不同,驱动和应用程序需要在主机上使用交叉编译器进行编译,然后将目标文件传输到开发板上运行。

3.5.1 生成SDK

在PetaLinux工程目录下执行以下命令:

1
2
petalinux-build --sdk
petalinux-package --sysroot

执行成功后,在<项目路径>/images/linux/目录下会生成一个sdk文件夹。

3.5.2 配置交叉编译环境

按照上面的终端提示,执行以下命令加载交叉编译环境:

1
source /home/raiset/Project/peta_prj/zynq_test/images/linux/sdk/environment-setup-cortexa9t2hf-neon-xilinx-linux-gnueabi

验证环境变量是否设置成功:

1
2
echo $CROSS_COMPILE
echo $CC
每次打开新的终端都需要重新source一下环境配置文件。

3.6 Makefile编写

3.6.1 驱动模块Makefile

驱动Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 指定要编译的模块目标文件
obj-m := led.o

# Linux内核源码路径(需要根据实际情况修改)
LINUX_KERNEL_SRC := /home/raiset/Project/peta_prj/zynq_test/build/tmp/work/zynq_generic_7z100-xilinx-linux-gnueabi/linux-xlnx/6.1.5-xilinx-v2023.1+gitAUTOINC+716921b6d7-r0/linux-zynq_generic_7z100-standard-build

# 目标架构(ARM 32位)
ARCH := arm

# 当前目录
SRC := $(shell pwd)

# 编译目标
all:
$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(LINUX_KERNEL_SRC) M=$(PWD) modules

# 清理目标
clean:
rm -f *.o *.mod.o *.mod.c *.order *.symvers *.ko *.mod

.PHONY: all clean

需要修改的参数如下:

  • obj-m:指定要编译的模块

  • LINUX_KERNEL_SRC:内核源码路径,需要根据实际PetaLinux工程路径修改

  • ARCH:如果是64位的板子,则ARCH改为arm64

3.6.2 应用程序Makefile

应用程序Makefile

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
# 使用环境变量中的交叉编译器
CC := $(CC)

# 编译选项
CFLAGS += -Wall -O2
LDFLAGS +=

# 目标文件名
TARGET := led_test

# 源文件
SRCS := led_test.c

# 目标文件
OBJS := $(SRCS:.c=.o)

# 编译规则
all: $(TARGET)

$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^

%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<

# 清理规则
clean:
rm -f $(OBJS) $(TARGET)

.PHONY: all clean

这里只需要修改TARGETSRCS即可。

3.7 上板测试与验证

3.7.1 文件传输

将编译好的led.ko和应用程序传输到开发板上,可以使用网络传输,也可以直接复制到SD卡的文件系统下:

3.7.2 驱动加载与测试

加载驱动模块

在开发板上执行以下命令加载驱动:

1
sudo insmod led.ko

加载驱动时会执行led_init函数中的内容,初始化设备并创建设备文件。

验证设备注册(可选)

通过以下命令验证设备号分配是否成功:

1
cat /proc/devices
验证设备文件(可选)

检查设备文件是否创建成功:

1
ls /dev/
设备树信息查看(可选)

如果需要查看设备树信息,可以进入/proc/device-tree目录:

某些设备树属性值可能显示为乱码,这是二进制数据的正常表现。

3.7.3 LED控制测试

测试LED0(第一个灯):

1
sudo ./led_test 0

可以看到最左边的LED亮起,说明驱动工作正常。

测试LED1(第二个灯):

1
sudo ./led_test 1

依此类推,运行sudo ./led_test 1时亮第2个灯,这和驱动中的led_write函数定义的逻辑一致。

4. 使用petalinux-create创建用户模块和应用程序

运行以下命令创建用户模块和应用程序:

1
2
petalinux-create -t modules --name ledmodule --enable
petalinux-create -t apps --name ledapp --enable

project_dir/project-spec/meta-user/recipes-modules/ledmodule/files目录下可以找到ledmodule.c文件,在该文件中编写驱动代码。
project_dir/project-spec/meta-user/recipes-apps/ledapp/files目录下可以找到ledapp.c文件,在该文件中编写应用程序代码。

之后运行petalinux-build命令编译工程,该驱动会自动加入到内核中,上板后使用sudo ledapp 0即可测试。

这种方法虽然简单,但是如果频繁修改代码,需要重新build工程,效率较低,适合后期项目固定时使用。使用交叉编译器可以随时将编译好的文件传过去测试,无需更改工程。


Site Total Visits: Loading
Site Total Visitors: Loading