字符设备驱动-IIO子系统

1 引入IIO 子系统#

随着手机、物联网、工业物联网和可穿戴设备的爆发,传感器的需求越来越多。比如手机或者手环里面的加速度计、光传感器、陀螺仪、气压计、磁力计等,这些传感器本质上都是 ADC。这些传感器对外通过 IIC 或者 SPI 接口来发送ADC转换后的原始数据。

Linux 内核为了管理这些日益增多的 ADC 类传感器,特地推出了 IIO 子系统。

IIO 全称是 Industrial I/O,翻译过来就是工业 I/O,常用的陀螺仪、加速度计、电压/电流测量芯片、光照传感器、压力传感器等内部都是有个 ADC,内部 ADC 将原始的 模拟数据转换为数字量,然后通过其他的通信接口,比如 IIC、SPI 等传输给 SOC。

因此,当使用的传感器本质是 ADC器件的时候,可以优先考虑使用 IIO 驱动框架。

2 IIO 子系统驱动框架#

image

  • IIO sysfs对用户空间提供IIO设备访问和配置。
  • IIO Core提供IIO设备、IIO Trigger、IIO Buffer分配、初始化、注册等工作。
    • industrialio-core.c
    • industrialio-buffer.c
    • industrialio-event.c
  • IIO Driver不同IIO设备的驱动程序。

3 数据结构#

3.1 iio_dev-iio设备#

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
struct iio_dev {
int id;
struct module *driver_module;
int modes;//设备支持的模式
int currentmode;//表示设备当前模式
struct device dev;
struct iio_event_interface *event_interface;
struct iio_buffer *buffer;//缓冲区
struct list_head buffer_list;//当前匹配的缓冲区列表
int scan_bytes; //捕获到,并且提供给缓冲区的字节数
struct mutex mlock;
const unsigned long *available_scan_masks;//扫描位掩码,使用触发缓冲区的时候可以通过
//设置掩码来确定使能哪些通道,使能以后的通道会将捕获到的数据发送到 IIO 缓冲区
unsigned masklength;
const unsigned long *active_scan_mask;//缓冲区已经开启的通道掩码
bool scan_timestamp;
unsigned scan_index_timestamp;//扫描时间戳,如果使能以后会将捕获时间戳放到缓冲区里面
struct iio_trigger *trig;// IIO 设备当前触发器
bool trig_readonly;
struct iio_poll_func *pollfunc;
struct iio_poll_func *pollfunc_event;
struct iio_chan_spec const *channels;//IIO设备通道列表
int num_channels;//IIO设备通道数
struct list_head channel_attr_list;
struct attribute_group chan_attr_group;
const char *name;
const struct iio_info *info;
clockid_t clock_id;
struct mutex info_exist_lock;
const struct iio_buffer_setup_ops *setup_ops;
struct cdev chrdev;
};//iio_dev用于描述一个具体IIO设备, include/linux/iio/iio.h
1
2
3
4
5
6
7
8
/* Device operating modes */
#define INDIO_DIRECT_MODE 0x01
#define INDIO_BUFFER_TRIGGERED 0x02
#define INDIO_BUFFER_SOFTWARE 0x04
#define INDIO_BUFFER_HARDWARE 0x08

#define INDIO_ALL_BUFFER_MODES \
(INDIO_BUFFER_TRIGGERED | INDIO_BUFFER_HARDWARE | INDIO_BUFFER_SOFTWARE)
模式 描述
INDIO_DIRECT_MODE 提供 sysfs 接口
INDIO_BUFFER_TRIGGERED 支持硬件缓冲触发
INDIO_BUFFER_SOFTWARE 支持软件缓冲触发
INDIO_BUFFER_HARDWARE 支持硬件缓冲区

3.1.1 iio_buffer_setup_ops#

1
2
3
4
5
6
7
struct iio_buffer_setup_ops {
int (*preenable)(struct iio_dev *); /* 缓冲区使能之前调用 */
int (*postenable)(struct iio_dev *); /* 缓冲区使能之后调用 */
int (*predisable)(struct iio_dev *); /* 缓冲区禁用之前调用 */
int (*postdisable)(struct iio_dev *); /* 缓冲区禁用之后调用 */
bool (*validate_scan_mask)(struct iio_dev *indio_dev, const unsigned long *scan_mask); /* 检查扫描掩码是否有效 */
};

在使能或禁用缓冲区的时候会调用这些函数。

3.2 iio_info-属性和函数实现#

iio_info包含每个iio设备的属性和具体实现函数。

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
struct iio_info {
struct module *driver_module;
struct attribute_group *event_attrs;
const struct attribute_group *attrs;

int (*read_raw)(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val,
int *val2,
long mask);//最终读写传感器设备内部数据的操作函数

int (*read_raw_multi)(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int max_len,
int *vals,
int *val_len,
long mask);

int (*write_raw)(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val,
int val2,
long mask);

int (*write_raw_get_fmt)(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
long mask);

int (*read_event_config)(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan,
enum iio_event_type type,
enum iio_event_direction dir);

int (*write_event_config)(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan,
enum iio_event_type type,
enum iio_event_direction dir,
int state);

int (*read_event_value)(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan,
enum iio_event_type type,
enum iio_event_direction dir,
enum iio_event_info info, int *val, int *val2);

int (*write_event_value)(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan,
enum iio_event_type type,
enum iio_event_direction dir,
enum iio_event_info info, int val, int val2);

int (*validate_trigger)(struct iio_dev *indio_dev,
struct iio_trigger *trig);
int (*update_scan_mode)(struct iio_dev *indio_dev,
const unsigned long *scan_mask);
int (*debugfs_reg_access)(struct iio_dev *indio_dev,
unsigned reg, unsigned writeval,
unsigned *readval);
int (*of_xlate)(struct iio_dev *indio_dev,
const struct of_phandle_args *iiospec);
int (*hwfifo_set_watermark)(struct iio_dev *indio_dev, unsigned val);
int (*hwfifo_flush_to_buffer)(struct iio_dev *indio_dev,
unsigned count);
};

indio_dev:需要读写的 IIO 设备。

chan:需要读取的通道。

val,val2:对于 read_raw 来说 val 和 val2 这两个就是应用程序从内核空间读取到数据,一般就是传感器指定通道值,或者传感器的量程、分辨率等。对于 write_raw 来说就是应用程序向设备写入的数据。val 和 val2 共同组成具体值,val 是整数部分,val2 是小数部分。Linux内核无法支持浮点运算,因此val2是放大后的值。扩大的倍数我们不能随便设置,而是要使用 Linux 定义的倍数,Linux 内核里面定义的数据扩大倍数:

组合宏 描述
IIO_VAL_INT 整数值,没有小数。比如 5000,那么就是 val=5000,不 需要设置 val2
IIO_VAL_INT_PLUS_MICRO 小数部分扩大 1000000 倍,比如 1.00236,此时 val=1, val2=2360
IIO_VAL_INT_PLUS_NANO 小数部分扩大 1000000000 倍,同样是 1.00236,此时 val=1,val2=2360000
IIO_VAL_INT_PLUS_MICRO_DB dB 数据,和IIO_VAL_INT_PLUS_MICRO数据形式一 样,只是在后面添加 db
IIO_VAL_INT_MULTIPLE 多个整数值,比如一次要传回 6 个整数值,那么 val 和 val2就不够用了。此宏主要用于iio_inforead_raw_multi 函数
IIO_VAL_FRACTIONAL 分数值,也就是 val/val2。比如 val=1,val2=4,那么实际 值就是 1/4
IIO_VAL_FRACTIONAL_LOG2 值为 val>>val2,也就是 val 右移 val2 位。比如 val=25600, val2=4 , 那 么 真 正 的 值 就 是 25600 右 移 4 位 , 25600>>4=1600

mask:掩码,用于指定我们读取的是什么数据,比如 ICM20608 这样的传感器,他既有原 始的测量数据,比如 X,Y,Z 轴的陀螺仪、加速度计等,也有测量范围值,或者分辨率。比如加 速度计测量范围设置为±16g,那么分辨率就是 32/65536≈0.000488,我们只有读出原始值以及 对应的分辨率(量程),才能计算出真实的重力加速度。此时就有两种数据值:传感器原始值、分辨率。Linux 内核使用 IIO_CHAN_INFO_RAW IIO_CHAN_INFO_SCALE 这两个宏来表示原 始值以及分辨率,这两个宏就是掩码。

write_raw_get_fmt 用于设置用户空间向内核空间写入的数据格式,write_raw_get_fmt函数决定了 wtite_raw 函数中 val 和 val2 的意义,也就是表中的组合 形式。比如我们需要在应用程序中设置 ICM20608 加速度计的量程为±8g,那么分辨率就是16/65536≈0.000244,我们在 write_raw_get_fmt 函数里面设置加速度计的数据格式为 IIO_VAL_INT_PLUS_MICRO。那么我们在应用程序里面向指定的文件写入 0.000244 以后,最 终传递给内核驱动的就是 0.000244*1000000=244。也就是 write_raw 函数的 val 参数为 0,val2 参数为 244。

3.3 iio_chan_spec-通道属性#

一个IIO设备可能有多个通道,每个通道由struct iio_chan_spec表示。比如一个 ADC 芯片支持 8 路采集,那 么这个 ADC 就有 8 个通道。的 ICM20608六轴传感器,可以输出三轴陀螺仪(X、Y、Z)、三轴加速度计(X、Y、Z)和一路温度,共7路数据。

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
struct iio_chan_spec {
enum iio_chan_type type;//表示channel类型,电压、电流、加速度、电磁等等。
int channel;
int channel2;
unsigned long address;//该通道对应的芯片数据寄存器地址
int scan_index;//当使用触发缓冲区的时候,scan_index 是扫描索引
struct {
char sign;
u8 realbits;
u8 storagebits;
u8 shift;
u8 repeat;
enum iio_endian endianness;
} scan_type;//扫描数据的存储格式
long info_mask_separate;
long info_mask_separate_available;
long info_mask_shared_by_type;//标记导出的信息由相同类型的通道共享,比如
long info_mask_shared_by_type_available;
long info_mask_shared_by_dir;
long info_mask_shared_by_dir_available;
long info_mask_shared_by_all;
long info_mask_shared_by_all_available;
const struct iio_event_spec *event_spec;
unsigned int num_event_specs;
const struct iio_chan_spec_ext_info *ext_info;
const char *extend_name;
const char *datasheet_name;
unsigned modified:1;//modified 为 1 的时候,channel2 为通道修饰符
unsigned indexed:1;// indexed 为 1时候,channel 为通道索引
unsigned output:1;
unsigned differential:1;
};

enum iio_chan_type {
IIO_VOLTAGE, /* 电压类型 */
IIO_CURRENT, /* 电流类型 */
IIO_POWER, /* 功率类型 */
IIO_ACCEL, /* 加速度类型 */
IIO_ANGL_VEL, /* 角度类型(陀螺仪) */
IIO_MAGN, /* 电磁类型(磁力计) */
IIO_LIGHT, /* 灯光类型 */
IIO_INTENSITY, /* 强度类型(光强传感器) */
IIO_PROXIMITY, /* 接近类型(接近传感器) */
IIO_TEMP, /* 温度类型 */
IIO_INCLI, /* 倾角类型(倾角测量传感器) */
IIO_ROT, /* 旋转角度类型 */
IIO_ANGL, /* 转动角度类型(电机旋转角度测量传感器) */
IIO_TIMESTAMP, /* 时间戳类型 */
IIO_CAPACITANCE, /* 电容类型 */
IIO_ALTVOLTAGE, /* 频率类型 */
IIO_CCT, /* 笔者暂时未知的类型 */
IIO_PRESSURE, /* 压力类型 */
IIO_HUMIDITYRELATIVE, /* 湿度类型 */
IIO_ACTIVITY, /* 活动类型(计步传感器) */
IIO_STEPS, /* 步数类型 */
IIO_ENERGY, /* 能量类型(卡路里) */
IIO_DISTANCE, /* 距离类型 */
IIO_VELOCITY, /* 速度类型 */
};

如果是 ICM20608 这样的多轴传感器,那么就是复合类型了,陀螺仪部分是 IIO_ANGL_VEL 类型,加速度计部分是IIO_ACCEL,温度部分就是 IIO_TEMP

当成员变量 modified 为 1 的时候,channel2 为通道修饰符。Linux 内核给出了 可用的通道修饰符:

1
2
3
4
5
6
7
enum iio_modifier {
IIO_NO_MOD,
IIO_MOD_X, /* X 轴 */
IIO_MOD_Y, /* Y 轴 */
IIO_MOD_Z, /* Z 轴 */
......
};

比如 ICM20608 的加速度计部分,类型设置为 IIO_ACCELX、Y、Z 这三个轴就用 channel2通道修饰符来区分.

scan_type 各个成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scan_type.sign:如果为‘u’表示数据为无符号类型,为‘s’的话为有符号类型。

scan_type.realbits:数据真实的有效位数,比如很多传感器说的 10 位 ADC,其真实有效数
据就是 10 位。

scan_type.storagebits:存储位数,有效位数+填充位。比如有些传感器 ADC 是 12 位的,
那么我们存储的话肯定要用到 2 个字节,也就是 16 位,这 16 位就是存储位数。

scan_type.shift:右移位数,也就是存储位数和有效位数不一致的时候,需要右移的位数,这
个参数不总是需要,一切以实际芯片的数据手册位数。

scan_type.repeat:实际或存储位的重复数量。
scan_type.endianness:数据的大小端模式,可设置为 IIO_CPU、IIO_BE(大端)或 IIO_LE(小
端)。

info_mask_separate 将属性标记为特定于此通

info_mask_shared_by_type 标记导出的信息由相同类型的通道共享。X、Y、Z 轴他们的 type 都 是 IIO_ACCEL,也就是类型相同。而这三个轴的分辨率(量程)是一样的,那么这3个通道info_mask_shared_by_type 中使能IO_CHAN_INFO_SCALE 这个属性,表示 这三个通道的分辨率是共用的,这样在sysfs下就会只生成一个描述分辨率的文件,这三个通道 都可以使用这一个分辨率文件。

info_mask_shared_by_dir 标记某些导出的信息由相同方向的通道共享。

info_mask_shared_by_all 表设计某些信息所有的通道共享,无论这些通道的类 型、方向如何,全部共享。

output 表示为输出通道。

differential 表示为差分通道。

3.4 iio_trigger-触发数据采集#

触发器是基于某种信号来触发数据采集,比如:数据就绪中断;周期性中断;用户空间sysfs读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct iio_trigger {
const struct iio_trigger_ops *ops;
struct module *owner;
int id;
const char *name;
struct device dev;
struct list_head list;
struct list_head alloc_list;
atomic_t use_count;
struct irq_chip subirq_chip;
int subirq_base;
struct iio_subirq subirqs[CONFIG_IIO_CONSUMERS_PER_TRIGGER];
unsigned long pool[BITS_TO_LONGS(CONFIG_IIO_CONSUMERS_PER_TRIGGER)];
struct mutex pool_lock;
bool attached_own_device;
};

3.4.1 iio_trigger_ops#

1
2
3
4
5
6
struct iio_trigger_ops {
int (*set_trigger_state)(struct iio_trigger *trig, bool state);--设置触发器状态,打开或关闭。
int (*try_reenable)(struct iio_trigger *trig);--当用户计数为0时,重新使能接口。
int (*validate_device)(struct iio_trigger *trig,
struct iio_dev *indio_dev);
};

3.5 iio_buffer-保存采集到的数据#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct iio_buffer {
int length;
int bytes_per_datum;
struct attribute_group *scan_el_attrs;
long *scan_mask;
bool scan_timestamp;
const struct iio_buffer_access_funcs *access;
struct list_head scan_el_dev_attr_list;
struct attribute_group buffer_group;
struct attribute_group scan_el_group;
wait_queue_head_t pollq;
bool stufftoread;
const struct attribute **attrs;
struct list_head demux_list;
void *demux_bounce;
struct list_head buffer_list;
struct kref ref;
unsigned int watermark;
};

4 API#

4.1 iio_dev 注册与注销#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct iio_dev *iio_device_alloc(int sizeof_priv)//申请 iio_dev
struct iio_dev *devm_iio_device_alloc(struct device *dev, int sizeof_priv)//申请 iio_dev
void iio_device_free(struct iio_dev *indio_dev);
void devm_iio_device_free(struct device *dev, struct iio_dev *indio_dev);

int iio_device_register(struct iio_dev *indio_dev)//注册iio设备
void iio_device_unregister(struct iio_dev *indio_dev)//注销iio设备

#define iio_device_register(indio_dev) \
__iio_device_register((indio_dev), THIS_MODULE)
int __iio_device_register(struct iio_dev *indio_dev, struct module *this_mod);
void iio_device_unregister(struct iio_dev *indio_dev);
#define devm_iio_device_register(dev, indio_dev) \
__devm_iio_device_register((dev), (indio_dev), THIS_MODULE);
int __devm_iio_device_register(struct device *dev, struct iio_dev *indio_dev,
struct module *this_mod);
void devm_iio_device_unregister(struct device *dev, struct iio_dev *indio_dev);

image

1
2
3
4
5
6
devm_iio_device_register/iio_device_register
  ->__iio_device_register
    ->iio_check_unique_scan_index
    ->iio_device_register_debugfs
      ->debugfs_create_file//创建direct_reg_access调试节点,通过此节点可以直接读写寄存器。
//iio_debugfs_reg_fops调用IIO设备struct iio_dev->info->debugfs_reg_access()函数。

4.1.1 iio_device_register_sysfs过程#

devm_iio_device_register过程中会调用iio_device_register_sysfs:

1
2
3
4
 ->iio_buffer_alloc_sysfs_and_mask
    ->iio_device_register_sysfs//创建adc属性和每个channel属性。
    ->cdev_init//初始化cdev设备,操作函数集为iio_buffer_fileops。
    ->cdev_device_add//创建cdev设备/dev/iio:deviceX。

展开iio_device_register_sysfs如下:

1
2
3
4
5
iio_device_register_sysfs
---> iio_device_add_channel_sysfs
---> iio_device_add_info_mask_type
---> __iio_add_chan_devattr
---> __iio_device_attr_init

其中在iio_device_add_info_mask_type中设置了读写回调函数,比如cat in_voltage0_input就会调用回调函数iio_read_channel_info.

1
2
3
4
5
6
7
8
9
10
11
12
13
int __iio_device_attr_init()
{...
case IIO_SEPARATE:
if (chan->indexed)
name = kasprintf(GFP_KERNEL, "%s_%s%d_%s",
iio_direction[chan->output],
iio_chan_type_name_spec[chan->type],
chan->channel,
full_postfix);
//in_voltage0_input
//
..........
}

格式为%s_%s%d_%s", 对应了in_voltage0_input,其中iio_direction、iio_chan_type_name_spec、iio_chan_info_postfix对应的值为 "in" 、"voltage" 、"input"

如下代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static const char * const iio_direction[] = {
[0] = "in",
[1] = "out",
};

static const char * const iio_chan_type_name_spec[] = {
[IIO_VOLTAGE] = "voltage",
[IIO_CURRENT] = "current",
[IIO_POWER] = "power",
[IIO_ACCEL] = "accel",
// 以下省略
};


/* relies on pairs of these shared then separate */
static const char * const iio_chan_info_postfix[] = {
[IIO_CHAN_INFO_RAW] = "raw",
[IIO_CHAN_INFO_PROCESSED] = "input",
[IIO_CHAN_INFO_SCALE] = "scale",
[IIO_CHAN_INFO_OFFSET] = "offset",
// 以下省略
};

4.2 iio_init-子系统的初始化#

IIO子系统初始化包括:

  • iio总线注册
  • 分配IIO字符设备号
  • 创建IIO设备debugfs
1
2
3
4
iio_init
  ->bus_register//注册iio bus。
  ->alloc_chrdev_region//创建iio cdev设备编号iio_devt。
  ->debugfs_create_dir//创建/sys/kernel/debug/iio调试目录iio_debugfs_dentry。

5 iio实验-操作ICM20608#

5.1 使能内核 IIO#

1
2
3
4
-> Device Drivers
-> Industrial I/O support (IIO [=y])
-> [*]Enable buffer support within IIO //选中
-> <*>Industrial I/O buffering based on kfifo //选中

image

5.2 驱动代码分析#

5.2.0 源码#

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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
#include <linux/spi/spi.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/platform_device.h>
#include "icm20608reg.h"
#include <linux/gpio.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/regmap.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/buffer.h>
#include <linux/iio/trigger.h>
#include <linux/iio/triggered_buffer.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/unaligned/be_byteshift.h>

#define ICM20608_NAME "icm20608"
#define ICM20608_TEMP_OFFSET 0
#define ICM20608_TEMP_SCALE 326800000

#define ICM20608_CHAN(_type, _channel2, _index) \
{ \
.type = _type, \
.modified = 1, \
.channel2 = _channel2, \
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
BIT(IIO_CHAN_INFO_CALIBBIAS), \
.scan_index = _index, \
.scan_type = { \
.sign = 's', \
.realbits = 16, \
.storagebits = 16, \
.shift = 0, \
.endianness = IIO_BE, \
}, \
}

/*
* ICM20608的扫描元素,3轴加速度计、
* 3轴陀螺仪、1路温度传感器,1路时间戳
*/
enum inv_icm20608_scan {
INV_ICM20608_SCAN_ACCL_X,
INV_ICM20608_SCAN_ACCL_Y,
INV_ICM20608_SCAN_ACCL_Z,
INV_ICM20608_SCAN_TEMP,
INV_ICM20608_SCAN_GYRO_X,
INV_ICM20608_SCAN_GYRO_Y,
INV_ICM20608_SCAN_GYRO_Z,
INV_ICM20608_SCAN_TIMESTAMP,
};

struct icm20608_dev {
struct spi_device *spi; /* spi设备 */
struct regmap *regmap; /* regmap */
struct regmap_config regmap_config;
struct mutex lock;
};

/*
* icm20608陀螺仪分辨率,对应250、500、1000、2000,计算方法:
* 以正负250度量程为例,500/2^16=0.007629,扩大1000000倍,就是7629
*/
static const int gyro_scale_icm20608[] = {7629, 15258, 30517, 61035};

/*
* icm20608加速度计分辨率,对应2、4、8、16 计算方法:
* 以正负2g量程为例,4/2^16=0.000061035,扩大1000000000倍,就是61035
*/
static const int accel_scale_icm20608[] = {61035, 122070, 244140, 488281};

/*
* icm20608通道,1路温度通道,3路陀螺仪,3路加速度计
*/
static const struct iio_chan_spec icm20608_channels[] = {
/* 温度通道 */
{
.type = IIO_TEMP,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)
| BIT(IIO_CHAN_INFO_OFFSET)
| BIT(IIO_CHAN_INFO_SCALE),
.scan_index = INV_ICM20608_SCAN_TEMP,
.scan_type = {
.sign = 's',
.realbits = 16,
.storagebits = 16,
.shift = 0,
.endianness = IIO_BE,
},
},

ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_X, INV_ICM20608_SCAN_GYRO_X), /* 陀螺仪X轴 */
ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_Y, INV_ICM20608_SCAN_GYRO_Y), /* 陀螺仪Y轴 */
ICM20608_CHAN(IIO_ANGL_VEL, IIO_MOD_Z, INV_ICM20608_SCAN_GYRO_Z), /* 陀螺仪Z轴 */

ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Y, INV_ICM20608_SCAN_ACCL_Y), /* 加速度X轴 */
ICM20608_CHAN(IIO_ACCEL, IIO_MOD_X, INV_ICM20608_SCAN_ACCL_X), /* 加速度Y轴 */
ICM20608_CHAN(IIO_ACCEL, IIO_MOD_Z, INV_ICM20608_SCAN_ACCL_Z), /* 加速度Z轴 */
};

static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
u8 ret;
unsigned int data;

ret = regmap_read(dev->regmap, reg, &data);
return (u8)data;
}

static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
regmap_write(dev->regmap, reg, value);
}

void icm20608_reginit(struct icm20608_dev *dev)
{
u8 value = 0;

icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x80);
mdelay(50);
icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x01);
mdelay(50);

value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);
printk("ICM20608 ID = %#X\r\n", value);

icm20608_write_onereg(dev, ICM20_SMPLRT_DIV, 0x00); /* 输出速率是内部采样率*/
icm20608_write_onereg(dev, ICM20_GYRO_CONFIG, 0x18); /* 陀螺仪±2000dps量程 */
icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG, 0x18); /* 加速度计±16G量程 */
icm20608_write_onereg(dev, ICM20_CONFIG, 0x04); /* 陀螺仪低通滤波BW=20Hz */
icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz */
icm20608_write_onereg(dev, ICM20_PWR_MGMT_2, 0x00); /* 打开加速度计和陀螺仪所有轴 */
icm20608_write_onereg(dev, ICM20_LP_MODE_CFG, 0x00); /* 关闭低功耗 */
icm20608_write_onereg(dev, ICM20_INT_ENABLE, 0x01); /* 使能FIFO溢出以及数据就绪中断 */
}

/*
* @description : 设置ICM20608传感器,可以用于陀螺仪、加速度计设置
* @param - dev : icm20608设备
* @param - reg : 要设置的通道寄存器首地址。
* @param - anix : 要设置的通道,比如X,Y,Z。
* @param - val : 要设置的值。
* @return : 0,成功;其他值,错误
*/
static int icm20608_sensor_set(struct icm20608_dev *dev, int reg,
int axis, int val)
{
int ind, result;
__be16 d = cpu_to_be16(val);

ind = (axis - IIO_MOD_X) * 2;
result = regmap_bulk_write(dev->regmap, reg + ind, (u8 *)&d, 2);
if (result)
return -EINVAL;

return 0;
}

/*
* @description : 读取ICM20608传感器数据,可以用于陀螺仪、加速度计、温度的读取
* @param - dev : icm20608设备
* @param - reg : 要读取的通道寄存器首地址。
* @param - anix : 需要读取的通道,比如X,Y,Z。
* @param - val : 保存读取到的值。
* @return : 0,成功;其他值,错误
*/
static int icm20608_sensor_show(struct icm20608_dev *dev, int reg,
int axis, int *val)
{
int ind, result;
__be16 d;

ind = (axis - IIO_MOD_X) * 2;
result = regmap_bulk_read(dev->regmap, reg + ind, (u8 *)&d, 2);
if (result)
return -EINVAL;
*val = (short)be16_to_cpup(&d);

return IIO_VAL_INT;
}

/*
* @description : 读取ICM20608陀螺仪、加速度计、温度通道值
* @param - indio_dev : iio设备
* @param - chan : 通道。
* @param - val : 保存读取到的通道值。
* @return : 0,成功;其他值,错误
*/
static int icm20608_read_channel_data(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val)
{
struct icm20608_dev *dev = iio_priv(indio_dev);
int ret = 0;

switch (chan->type) {
case IIO_ANGL_VEL: /* 读取陀螺仪数据, channel2为X、Y、Z轴*/
ret = icm20608_sensor_show(dev, ICM20_GYRO_XOUT_H, chan->channel2, val);
break;
case IIO_ACCEL: /* 读取加速度计数据,channel2为X、Y、Z轴 */
ret = icm20608_sensor_show(dev, ICM20_ACCEL_XOUT_H, chan->channel2, val);
break;
case IIO_TEMP: /* 读取温度 */
ret = icm20608_sensor_show(dev, ICM20_TEMP_OUT_H, IIO_MOD_X, val);
break;
default:
ret = -EINVAL;
break;
}
return ret;
}

/*
* @description : 设置ICM20608的陀螺仪计量程(分辨率)
* @param - dev : icm20608设备
* @param - val : 量程(分辨率值)。
* @return : 0,成功;其他值,错误
*/
static int icm20608_write_gyro_scale(struct icm20608_dev *dev, int val)
{
int result, i;
u8 d;

for (i = 0; i < ARRAY_SIZE(gyro_scale_icm20608); ++i) {
if (gyro_scale_icm20608[i] == val) {
d = (i << 3);
result = regmap_write(dev->regmap, ICM20_GYRO_CONFIG, d);
if (result)
return result;
return 0;
}
}
return -EINVAL;
}

/*
* @description : 设置ICM20608的加速度计量程(分辨率)
* @param - dev : icm20608设备
* @param - val : 量程(分辨率值)。
* @return : 0,成功;其他值,错误
*/
static int icm20608_write_accel_scale(struct icm20608_dev *dev, int val)
{
int result, i;
u8 d;

for (i = 0; i < ARRAY_SIZE(accel_scale_icm20608); ++i) {
if (accel_scale_icm20608[i] == val) {
d = (i << 3);
result = regmap_write(dev->regmap, ICM20_ACCEL_CONFIG, d);
if (result)
return result;
return 0;
}
}
return -EINVAL;
}

/*
* @description : 读函数,当读取sysfs中的文件的时候最终此函数会执行,此函数
* :里面会从传感器里面读取各种数据,然后上传给应用。
* @param - indio_dev : iio_dev
* @param - chan : 通道
* @param - val : 读取的值,如果是小数值的话,val是整数部分。
* @param - val2 : 读取的值,如果是小数值的话,val2是小数部分。
* @param - mask : 掩码。
* @return : 0,成功;其他值,错误
*/
static int icm20608_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct icm20608_dev *dev = iio_priv(indio_dev);
int ret = 0;
unsigned char regdata = 0;

switch (mask) {
case IIO_CHAN_INFO_RAW: /* 读取ICM20608加速度计、陀螺仪、温度传感器原始值 */
mutex_lock(&dev->lock); /* 上锁 */
ret = icm20608_read_channel_data(indio_dev, chan, val); /* 读取通道值 */
mutex_unlock(&dev->lock);/* 释放锁 */
return ret;
case IIO_CHAN_INFO_SCALE:
switch (chan->type) {
case IIO_ANGL_VEL:
mutex_lock(&dev->lock);
regdata = (icm20608_read_onereg(dev, ICM20_GYRO_CONFIG) & 0X18) >> 3;
*val = 0;
*val2 = gyro_scale_icm20608[regdata];
mutex_unlock(&dev->lock);
return IIO_VAL_INT_PLUS_MICRO; /* 值为val+val2/1000000 */
case IIO_ACCEL:
mutex_lock(&dev->lock);
regdata = (icm20608_read_onereg(dev, ICM20_ACCEL_CONFIG) & 0X18) >> 3;
*val = 0;
*val2 = accel_scale_icm20608[regdata];;
mutex_unlock(&dev->lock);
return IIO_VAL_INT_PLUS_NANO;/* 值为val+val2/1000000000 */
case IIO_TEMP:
*val = ICM20608_TEMP_SCALE/ 1000000;
*val2 = ICM20608_TEMP_SCALE % 1000000;
return IIO_VAL_INT_PLUS_MICRO; /* 值为val+val2/1000000 */
default:
return -EINVAL;
}
return ret;
case IIO_CHAN_INFO_OFFSET: /* ICM20608温度传感器offset值 */
switch (chan->type) {
case IIO_TEMP:
*val = ICM20608_TEMP_OFFSET;
return IIO_VAL_INT;
default:
return -EINVAL;
}
return ret;
case IIO_CHAN_INFO_CALIBBIAS: /* ICM20608加速度计和陀螺仪校准值 */
switch (chan->type) {
case IIO_ANGL_VEL: /* 陀螺仪的校准值 */
mutex_lock(&dev->lock);
ret = icm20608_sensor_show(dev, ICM20_XG_OFFS_USRH, chan->channel2, val);
mutex_unlock(&dev->lock);
return ret;
case IIO_ACCEL: /* 加速度计的校准值 */
mutex_lock(&dev->lock);
ret = icm20608_sensor_show(dev, ICM20_XA_OFFSET_H, chan->channel2, val);
mutex_unlock(&dev->lock);
return ret;
default:
return -EINVAL;
}

default:
return ret -EINVAL;
}
}

/*
* @description : 写函数,当向sysfs中的文件写数据的时候最终此函数会执行,一般在此函数
* :里面设置传感器,比如量程等。
* @param - indio_dev : iio_dev
* @param - chan : 通道
* @param - val : 应用程序写入的值,如果是小数值的话,val是整数部分。
* @param - val2 : 应用程序写入的值,如果是小数值的话,val2是小数部分。
* @return : 0,成功;其他值,错误
*/
static int icm20608_write_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask)
{
struct icm20608_dev *dev = iio_priv(indio_dev);
int ret = 0;

switch (mask) {
case IIO_CHAN_INFO_SCALE: /* 设置陀螺仪和加速度计的分辨率 */
switch (chan->type) {
case IIO_ANGL_VEL: /* 设置陀螺仪 */
mutex_lock(&dev->lock);
ret = icm20608_write_gyro_scale(dev, val2);
mutex_unlock(&dev->lock);
break;
case IIO_ACCEL: /* 设置加速度计 */
mutex_lock(&dev->lock);
ret = icm20608_write_accel_scale(dev, val2);
mutex_unlock(&dev->lock);
break;
default:
ret = -EINVAL;
break;
}
break;
case IIO_CHAN_INFO_CALIBBIAS: /* 设置陀螺仪和加速度计的校准值*/
switch (chan->type) {
case IIO_ANGL_VEL: /* 设置陀螺仪校准值 */
mutex_lock(&dev->lock);
ret = icm20608_sensor_set(dev, ICM20_XG_OFFS_USRH,
chan->channel2, val);
mutex_unlock(&dev->lock);
break;
case IIO_ACCEL: /* 加速度计校准值 */
mutex_lock(&dev->lock);
ret = icm20608_sensor_set(dev, ICM20_XA_OFFSET_H,
chan->channel2, val);
mutex_unlock(&dev->lock);
break;
default:
ret = -EINVAL;
break;
}
break;
default:
ret = -EINVAL;
break;
}
return ret;
}

/*
* @description : 用户空间写数据格式,比如我们在用户空间操作sysfs来设置传感器的分辨率,
* :如果分辨率带小数,那么这个小数传递到内核空间应该扩大多少倍,此函数就是
* : 用来设置这个的。
* @param - indio_dev : iio_dev
* @param - chan : 通道
* @param - mask : 掩码
* @return : 0,成功;其他值,错误
*/
static int icm20608_write_raw_get_fmt(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan, long mask)
{
switch (mask) {
case IIO_CHAN_INFO_SCALE:
switch (chan->type) {
case IIO_ANGL_VEL: /* 用户空间写的陀螺仪分辨率数据要乘以1000000 */
return IIO_VAL_INT_PLUS_MICRO;
default: /* 用户空间写的加速度计分辨率数据要乘以1000000000 */
return IIO_VAL_INT_PLUS_NANO;
}
default:
return IIO_VAL_INT_PLUS_MICRO;
}
return -EINVAL;
}

static const struct iio_info icm20608_info = {
.read_raw = icm20608_read_raw,
.write_raw = icm20608_write_raw,
.write_raw_get_fmt = &icm20608_write_raw_get_fmt, /* 用户空间写数据格式 */
};

static int icm20608_probe(struct spi_device *spi)
{
int ret;
struct icm20608_dev *dev;
struct iio_dev *indio_dev;

/* 1、申请iio_dev内存 */
indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*dev));
if (!indio_dev)
return -ENOMEM;

/* 2、获取icm20608_dev结构体地址 */
dev = iio_priv(indio_dev);
dev->spi = spi;
spi_set_drvdata(spi, indio_dev); /* 将indio_de设置为spi->dev的driver_data */
mutex_init(&dev->lock);

/* 3、iio_dev的其他成员变量 */
indio_dev->dev.parent = &spi->dev;
indio_dev->info = &icm20608_info;
indio_dev->name = ICM20608_NAME;
indio_dev->modes = INDIO_DIRECT_MODE; /* 直接模式,提供sysfs接口 */
indio_dev->channels = icm20608_channels;
indio_dev->num_channels = ARRAY_SIZE(icm20608_channels);

/* 4、注册iio_dev */
ret = iio_device_register(indio_dev);
if (ret < 0) {
dev_err(&spi->dev, "iio_device_register failed\n");
goto err_iio_register;
}

/* 5、初始化regmap_config设置 */
dev->regmap_config.reg_bits = 8; /* 寄存器长度8bit */
dev->regmap_config.val_bits = 8; /* 值长度8bit */
dev->regmap_config.read_flag_mask = 0x80;
/* 读掩码设置为0X80,ICM20608使用SPI接口读的时候寄存器最高位应该为1 */

/* 6、初始化SPI接口的regmap */
dev->regmap = regmap_init_spi(spi, &dev->regmap_config);
if (IS_ERR(dev->regmap)) {
ret = PTR_ERR(dev->regmap);
goto err_regmap_init;
}

/* 7、初始化spi_device */
spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
spi_setup(spi);

/* 初始化ICM20608内部寄存器 */
icm20608_reginit(dev);
return 0;

err_regmap_init:
iio_device_unregister(indio_dev);
err_iio_register:
return ret;
}

static int icm20608_remove(struct spi_device *spi)
{
struct iio_dev *indio_dev = spi_get_drvdata(spi);
struct icm20608_dev *dev;

dev = iio_priv(indio_dev);

/* 1、删除regmap */
regmap_exit(dev->regmap);

/* 2、注销IIO */
iio_device_unregister(indio_dev);
return 0;
}

static const struct spi_device_id icm20608_id[] = {
{"alientek,icm20608", 0},
{}
};

static const struct of_device_id icm20608_of_match[] = {
{ .compatible = "alientek,icm20608" },
{ /* Sentinel */ }
};

static struct spi_driver icm20608_driver = {
.probe = icm20608_probe,
.remove = icm20608_remove,
.driver = {
.owner = THIS_MODULE,
.name = "icm20608",
.of_match_table = icm20608_of_match,
},
.id_table = icm20608_id,
};

static int __init icm20608_init(void)
{
return spi_register_driver(&icm20608_driver);
}

static void __exit icm20608_exit(void)
{
spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

5.2.1 probe#

image

还是按照spi子系统框架probe时利用iio子系统,初始化iio_dev

iio_info属性赋值:

image

iio_channels属性赋值:

image

image

温度通道

  1. info_mask_separate设置为IIO_CHAN_INFO_RAW,IIO_CHAN_INFO_SCALE, IIO_CHAN_INFO_OFFSET此通。

    IIO_CHAN_INFO_RAW 为温度通道的原始值,IIO_CHAN_INFO_OFFSETICM20608 温度 offset 值,这个要查阅数 据手册。IIO_CHAN_INFO_SCALE ICM20608 的比例,也就是一个单位的原始值为多少℃, 这个也要查阅 ICM20608 的数据手册。

  2. 扫描元素设置成SCAN_TEMP

  3. 扫描类型:有符号数据,数据位数16位,存储位数16位,右移位数0, 大端传输(一般MSB传输)

陀螺仪通道

  1. modified 成员变量为 1,所以 channel2 就是通道修饰符,用来指定 X、Y、Z 轴。

  2. 扫描元素设置SCAN_GYRO_X, Y,Z

  3. info_mask_separate设置为IIO_CHAN_INFO_RAW, IIO_CHAN_INFO_CALIBBIAS此通。“scale”是比例的意思,在这里就是量程(分辨率),因为 ICM20608 的陀螺仪和加速度计的量程是可以调整的,量程不同分辨率也就不同。设置每个通道的IIO_CHAN_INFO_RAWIIO_CHAN_INFO_CALIBBIAS这两个属性都是独立的,IIO_CHAN_INFO_RAW 表示 ICM20608 每个通道的原始值,这个肯定 是每个通道独立的。IIO_CHAN_INFO_CALIBBIAS 是 ICM20608 每个通道的校准值,这个是 ICM20608 的特性,不是所有的传感器都有校准值,一切都要以实际所使用的传感器为准。

  4. info_mask_shared_by_type设置为IIO_CHAN_INFO_SCALE此通。表示量程分辨率共享对陀螺仪通道。

  5. 扫描类型:有符号数据,数据位数16位,存储位数16位,右移位数0, 大端传输(一般MSB传输)

加速度通道:同理与陀螺仪通道.

INDIO_DIRECT_MODE 直接模式,提供sysfs接口。

5.2.2 icm20608_read_raw#

image

5.2.2.1 读raw数据#

image

icm20608_read_raw会传入具体通道和mask, 比如温度通道的raw,传入ICM20_TEMP_OUT_H寄存器,IIO_MOD_X那么ind就等于0,那么regmap_bulk_read出来就是一个16位的温度值。

同理,读加速度通道,传入ICM20_ACCEL_XOUT_H寄存器,ind = (IIO_MOD_X - IIO_MOD_X )*2 = 0;,因此读出16bit的x数据。再次传入ind = (IIO_MOD_Y - IIO_MOD_X)*2 = 2,得到ICM20_ACCEL_YOUT_H寄存器,因此读取出16bit的y数据。再次传入ind = (IIO_MOD_Z - IIO_MOD_X)*2 = 4,得到ICM20_ACCEL_ZOUT_H寄存器,因此读取出16bit的z数据。

同理,读陀螺仪通道。

5.2.2.2 读量程#

image

温度通道: 直接用预设量程即可或者val1,val2。分别表示整数,小数。

加速度通道: 获取量程参考数据手册:icm20608控制寄存器

icm20608控制寄存器

根据读到的寄存器值:regdata配置量程如下:

1
2
3
4
5
6
7
8
9
10
11
/*
* icm20608陀螺仪分辨率,对应250、500、1000、2000,计算方法:
* 以正负250度量程为例,500/2^16=0.007629,扩大1000000倍,就是7629
*/
static const int gyro_scale_icm20608[] = {7629, 15258, 30517, 61035};

/*
* icm20608加速度计分辨率,对应2、4、8、16 计算方法:
* 以正负2g量程为例,4/2^16=0.000061035,扩大1000000000倍,就是61035
*/
static const int accel_scale_icm20608[] = {61035, 122070, 244140, 488281};

陀螺仪通道:同理

陀螺仪量程计算:

可选的量程有±250、±500、±1000 和±2000°/s。以± 250 这个量程为例,每个数值对应的度数就是 500/2^10≈0.007629°/s。同理,±500 量程对应 的度数为 0.015258±1000 量程对应的度数为 0.030517±2000 量程为0.061035。假设现在设 置量程为±2000,读取到的原始值为 12540,那么对应的度数就是 12540*0.061035≈765.37°/s。 注意,这里扩大了 1000000 倍。

加速度量程计算:

计算方法和陀螺仪一样。这里扩大了 1000000000 倍。

5.2.2.3 读offset#

image

只有温度有offset

5.2.3 icm20608_write_raw#

image

通过写寄存器来配置量程,校准值。

5.2.3.1 配置量程#

image

根据设置的val量程值,匹配到i量程等级,d<<3然后设置量程等级。

5.2.3.2 配置校准值#

image

image

image

根据静态偏移寄存器,进行校准,写入校准值。

5.2.4 icm20608_write_raw_get_fmt#

image

用户空间设置数据格式。

陀螺仪通道:规定用户空间写的陀螺仪分辨率数据要乘以1000000。

加速度通道:规定用户空间写的陀螺仪分辨率数据要乘以1000000000。

比如我们在用户空间要设置加速度计量程为±4g,只需要向 in_accel_scale 写 入 0.000122070 , 那 么 最 终 传 入 到 驱 动 里 面 的 就 是 0.000122070*1000000000=122070

5.3 测试#

进入“/sys/bus/iio/devices/”目录:可以看到,此时有两个 IIO 设备“iio:device0”,iio:device0 是 I.MX6ULL 内 部 ADC,iio:device1 才是 ICM20608。

image

image

可以看出,iio:device1 对应spi2.0 上的设备,也就是 ICM20608

此目录下有 很多文件,比如in_accel_scale、in_accel_x_calibias、in_accel_x_raw等,这些就是我们设置的通道。in_accel_scale 就是加速度计的比例,也就是分辨率(量程),in_accel_x_calibias 就是加速度 计 X 轴的校准值,in_accel_x_raw 就是加速度计的 X 轴原始值。我们在配置通道的时候,设置 了类型相同的所有通道共用 SCALE,所以这里只有一个 in_accel_scale,而 X、Y、Z 轴的原始值和校准值每个轴都有一个文件,陀螺仪和温度计同理。

5.3.1 通道文件命名方式#

源码见4.1.1 iio_device_register_sysfs过程。

IIO_CHAN_INFO_RAW IIO_CHAN_INFO_CALIBBIAS这两个专属属性,对应in_accel_x_raw in_accel_x_calibias 这两个文件。

通道的命名:[direction]_[type]_[index]_[modifier]_[info_mask]

direction:为属性对应的方向

1
2
3
4
static const char * const iio_direction[] = {
[0] = "in",
[1] = "out",
};

type:也就是配置通道的时候 type 值,type 对应的字符可以参考 iio_chan_type_name_spec:

1
2
3
4
5
6
7
8
static const char * const iio_chan_type_name_spec[] = {
[IIO_VOLTAGE] = "voltage",
[IIO_CURRENT] = "current",
[IIO_POWER] = "power",
[IIO_ACCEL] = "accel",
[IIO_ANGL_VEL] = "anglvel",
...
}

index:索引,如果配置通道的时候设置了 indexed=1,那么就会使用通道的 channel 成员变 量来替代此部分命名。比如,有个 ADC 芯片支持 8 个通道,那么就可以使用 channel 来表示对 应的通道,最终在用户空间呈现的每个通道文件名的 index 部分就是通道号。

modifier:当通道的 modified 成员变量为 1 的时候,channel2 就是修饰符, iio_modifier_names:

1
2
3
4
5
6
7
8
static const char * const iio_modifier_names[] = {
[IIO_MOD_X] = "x",
[IIO_MOD_Y] = "y",
[IIO_MOD_Z] = "z",
......
[IIO_MOD_PM4] = "pm4",
[IIO_MOD_PM10] = "pm10",
};

info_mask:属性掩码,也就是属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
static const char * const iio_chan_info_postfix[] = {
[IIO_CHAN_INFO_RAW] = "raw",
[IIO_CHAN_INFO_PROCESSED] = "input",
[IIO_CHAN_INFO_SCALE] = "scale",
[IIO_CHAN_INFO_OFFSET] = "offset",
[IIO_CHAN_INFO_CALIBSCALE] = "calibscale",
[IIO_CHAN_INFO_CALIBBIAS] = "calibbias",
......
[IIO_CHAN_INFO_DEBOUNCE_TIME] = "debounce_time",
[IIO_CHAN_INFO_CALIBEMISSIVITY] = "calibemissivity",
[IIO_CHAN_INFO_OVERSAMPLING_RATIO] = "oversampling_ratio",
};

综上所述,in_accel_x_raw 组成形式如图:

image

5.3.2 读取raw数据#

进入“/sys/bus/iio/devices/”目录,cat iio:device1/in_ccel_x,这时驱动中的icm20608_read_raw执行。

读取一下 in_accel_scale 这个文件,这是加速度计的分辨率,我们默认设置了加速度计 量程为±16g,因此分辨率为 0.000488281

image

这时候有朋友可能会疑问,我们设置加速度计±16g 的分辨率为 488281,也就是扩大了1000000000 倍,为啥这里读出来的是 0.000488281 这个原始值?

驱动返回的是IIO_VAL_INT_PLUS_NANO, 因 此 用 户 空 间 得 到 分 辨 率 以 后 会 除 以 1000000000 , 得 到 真 实 的 分 辨 率 , 488281/1000000000=0.000488281

再读取一下 in_accel_z_raw,加速度计的 Z 轴原始值。静态情况 下 Z 轴应该是 1g 的重力加速度计。

image

2074×0.000488281≈1.01g,此时 Z 轴重力为 1g,结果正确。

5.3.3 编写应用程序测试#

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
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>

/* 字符串转数字,将浮点小数字符串转换为浮点数数值 */
#define SENSOR_FLOAT_DATA_GET(ret, index, str, member)\
ret = file_data_read(file_path[index], str);\
dev->member = atof(str);\

/* 字符串转数字,将整数字符串转换为整数数值 */
#define SENSOR_INT_DATA_GET(ret, index, str, member)\
ret = file_data_read(file_path[index], str);\
dev->member = atoi(str);\

/* icm20608 iio框架对应的文件路径 */
static char *file_path[] = {
"/sys/bus/iio/devices/iio:device1/in_accel_scale",
"/sys/bus/iio/devices/iio:device1/in_accel_x_calibbias",
"/sys/bus/iio/devices/iio:device1/in_accel_x_raw",
"/sys/bus/iio/devices/iio:device1/in_accel_y_calibbias",
"/sys/bus/iio/devices/iio:device1/in_accel_y_raw",
"/sys/bus/iio/devices/iio:device1/in_accel_z_calibbias",
"/sys/bus/iio/devices/iio:device1/in_accel_z_raw",
"/sys/bus/iio/devices/iio:device1/in_anglvel_scale",
"/sys/bus/iio/devices/iio:device1/in_anglvel_x_calibbias",
"/sys/bus/iio/devices/iio:device1/in_anglvel_x_raw",
"/sys/bus/iio/devices/iio:device1/in_anglvel_y_calibbias",
"/sys/bus/iio/devices/iio:device1/in_anglvel_y_raw",
"/sys/bus/iio/devices/iio:device1/in_anglvel_z_calibbias",
"/sys/bus/iio/devices/iio:device1/in_anglvel_z_raw",
"/sys/bus/iio/devices/iio:device1/in_temp_offset",
"/sys/bus/iio/devices/iio:device1/in_temp_raw",
"/sys/bus/iio/devices/iio:device1/in_temp_scale",
};

/* 文件路径索引,要和file_path里面的文件顺序对应 */
enum path_index {
IN_ACCEL_SCALE = 0,
IN_ACCEL_X_CALIBBIAS,
IN_ACCEL_X_RAW,
IN_ACCEL_Y_CALIBBIAS,
IN_ACCEL_Y_RAW,
IN_ACCEL_Z_CALIBBIAS,
IN_ACCEL_Z_RAW,
IN_ANGLVEL_SCALE,
IN_ANGLVEL_X_CALIBBIAS,
IN_ANGLVEL_X_RAW,
IN_ANGLVEL_Y_CALIBBIAS,
IN_ANGLVEL_Y_RAW,
IN_ANGLVEL_Z_CALIBBIAS,
IN_ANGLVEL_Z_RAW,
IN_TEMP_OFFSET,
IN_TEMP_RAW,
IN_TEMP_SCALE,
};

/*
* icm20608数据设备结构体
*/
struct icm20608_dev{
int accel_x_calibbias, accel_y_calibbias, accel_z_calibbias;
int accel_x_raw, accel_y_raw, accel_z_raw;

int gyro_x_calibbias, gyro_y_calibbias, gyro_z_calibbias;
int gyro_x_raw, gyro_y_raw, gyro_z_raw;

int temp_offset, temp_raw;

float accel_scale, gyro_scale, temp_scale;

float gyro_x_act, gyro_y_act, gyro_z_act;
float accel_x_act, accel_y_act, accel_z_act;
float temp_act;
};

struct icm20608_dev icm20608;

/*
* @description : 读取指定文件内容
* @param - filename : 要读取的文件路径
* @param - str : 读取到的文件字符串
* @return : 0 成功;其他 失败
*/
static int file_data_read(char *filename, char *str)
{
int ret = 0;
FILE *data_stream;

data_stream = fopen(filename, "r"); /* 只读打开 */
if(data_stream == NULL) {
printf("can't open file %s\r\n", filename);
return -1;
}

ret = fscanf(data_stream, "%s", str);
if(!ret) {
printf("file read error!\r\n");
} else if(ret == EOF) {
/* 读到文件末尾的话将文件指针重新调整到文件头 */
fseek(data_stream, 0, SEEK_SET);
}
fclose(data_stream); /* 关闭文件 */
return 0;
}

/*
* @description : 获取ICM20608数据
* @param - dev : 设备结构体
* @return : 0 成功;其他 失败
*/
static int sensor_read(struct icm20608_dev *dev)
{
int ret = 0;
char str[50];

/* 1、获取陀螺仪原始数据 */
SENSOR_FLOAT_DATA_GET(ret, IN_ANGLVEL_SCALE, str, gyro_scale);
SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_X_RAW, str, gyro_x_raw);
SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_Y_RAW, str, gyro_y_raw);
SENSOR_INT_DATA_GET(ret, IN_ANGLVEL_Z_RAW, str, gyro_z_raw);

/* 2、获取加速度计原始数据 */
SENSOR_FLOAT_DATA_GET(ret, IN_ACCEL_SCALE, str, accel_scale);
SENSOR_INT_DATA_GET(ret, IN_ACCEL_X_RAW, str, accel_x_raw);
SENSOR_INT_DATA_GET(ret, IN_ACCEL_Y_RAW, str, accel_y_raw);
SENSOR_INT_DATA_GET(ret, IN_ACCEL_Z_RAW, str, accel_z_raw);

/* 3、获取温度值 */
SENSOR_FLOAT_DATA_GET(ret, IN_TEMP_SCALE, str, temp_scale);
SENSOR_INT_DATA_GET(ret, IN_TEMP_OFFSET, str, temp_offset);
SENSOR_INT_DATA_GET(ret, IN_TEMP_RAW, str, temp_raw);

/* 3、转换为实际数值 */
dev->accel_x_act = dev->accel_x_raw * dev->accel_scale;
dev->accel_y_act = dev->accel_y_raw * dev->accel_scale;
dev->accel_z_act = dev->accel_z_raw * dev->accel_scale;

dev->gyro_x_act = dev->gyro_x_raw * dev->gyro_scale;
dev->gyro_y_act = dev->gyro_y_raw * dev->gyro_scale;
dev->gyro_z_act = dev->gyro_z_raw * dev->gyro_scale;

dev->temp_act = ((dev->temp_raw - dev->temp_offset) / dev->temp_scale) + 25;
return ret;
}

/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int ret = 0;

if (argc != 1) {
printf("Error Usage!\r\n");
return -1;
}

while (1) {
ret = sensor_read(&icm20608);
if(ret == 0) { /* 数据读取成功 */
printf("\r\n原始值:\r\n");
printf("gx = %d, gy = %d, gz = %d\r\n"
, icm20608.gyro_x_raw, icm20608.gyro_y_raw, icm20608.gyro_z_raw);
printf("ax = %d, ay = %d, az = %d\r\n"
, icm20608.accel_x_raw, icm20608.accel_y_raw, icm20608.accel_z_raw);
printf("temp = %d\r\n", icm20608.temp_raw);
printf("实际值:");
printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n"
, icm20608.gyro_x_act, icm20608.gyro_y_act, icm20608.gyro_z_act);
printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n"
, icm20608.accel_x_act, icm20608.accel_y_act, icm20608.accel_z_act);
printf("act temp = %.2f°C\r\n", icm20608.temp_act);
}
usleep(100000); /*100ms */
}
return 0;
}

image

6 IIO实验-操作vf610_adc.c#

以imx6ull芯片为例子,drivers/iio/adc/vf610_adc.c

6.1 dts描述#

dtsi描述如下:默认是关闭的

1
2
3
4
5
6
7
8
9
adc1: adc@02198000 {
compatible = "fsl,imx6ul-adc", "fsl,vf610-adc";
reg = <0x02198000 0x4000>;
interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_ADC1>;
num-channels = <2>;
clock-names = "adc";
status = "disabled";
};

我们最后在 imx6ull-alientek-emmc.dts添加节点内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pinctrl_adc1: adc1grp {
fsl,pins = <MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0xb0>;
};
reg_vref_adc: regulator@2 {
compatible = "regulator-fixed";
regulator-name = "VREF_3V3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
};
&adc1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_adc1>;
num-channels = <2>;
vref-supply = <&reg_vref_adc>;
status = "okay";
};
  1. 添加 ADC 使用的GPIO1_IO01引脚配置信息
  2. regulators 节点下添加参考电源子节点,最后status设置成okay

6.2 使能 ADC 驱动#

1
2
3
4
-> Device Drivers
-> Industrial I/O support
-> Analog to digital converters
-> <*> Freescale vf610 ADC driver //选中

image

drivers/iio/adc/下的makefile和Kconfig:

image

6.3 驱动代码分析#

image

6.3.1 probe#

image

申请初始化iio设备,alloc iio设备有多分一块内存给info结构体。

从dts获取寄存器地址信息,进行ioremap。获取中断号,注册中断服务程序。

获取时钟资源,电源资源,使能电源输出。并且获取电源参考电压vref_uv.

image

获取adc采样周期,num-channels =2.

中间vf610_adc_cfg_init和vf610_adc_hw_init是ADC controller的寄存器配置不展开介绍。可以参考裸机实验IMX6ULL ADC控制器

然后设置iio_dev结构体,最后注册iio设备。

6.3.2 vf610_read_raw#

image

读取 ADC 原始数据值,type 值为 IIO_VOLTAGE,也就是读取电压值。这里info->value是怎么来的呢?

当然是中断服务程序进行ADC采样啊,如下:

image

6.4 用户态测试#

进入/sys/bus/iio/devices/iio:device0 目录下,该iio就是对应vf610这个ADC:

image

in_voltage1_raw:ADC1 通道 1 原始值文件。

in_voltage_scale:ADC1 比例文件(分辨率),单位为 mV。实际电压值(mV)=in_voltage1_raw* in_voltage_scale

我的开发板此时 in_voltage1_rawin_voltage_scale 这两个文件内容如下:

image

经过计算,实际电压:991*0.805664062≈798.4mV,也就是 0.7984V

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
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>

/* 字符串转数字,将浮点小数字符串转换为浮点数数值 */
#define SENSOR_FLOAT_DATA_GET(ret, index, str, member)\
ret = file_data_read(file_path[index], str);\
dev->member = atof(str);\

/* 字符串转数字,将整数字符串转换为整数数值 */
#define SENSOR_INT_DATA_GET(ret, index, str, member)\
ret = file_data_read(file_path[index], str);\
dev->member = atoi(str);\

static char *file_path[] = {
"/sys/bus/iio/devices/iio:device0/in_voltage_scale",
"/sys/bus/iio/devices/iio:device0/in_voltage1_raw",
};
enum path_index {
IN_VOLTAGE_SCALE = 0,
IN_VOLTAGE_RAW,
};

struct adc_dev{
int raw;
float scale;
float act;
};
struct adc_dev imx6ulladc;

static int file_data_read(char *filename, char *str) {
int ret = 0;
FILE *data_stream;

data_stream = fopen(filename, "r"); /* 只读打开 */
if(data_stream == NULL) {
printf("can't open file %s\r\n", filename);
return -1;
}

ret = fscanf(data_stream, "%s", str);
if(!ret) {
printf("file read error!\r\n");
} else if(ret == EOF) {
/* 读到文件末尾的话将文件指针重新调整到文件头 */
fseek(data_stream, 0, SEEK_SET);
}
fclose(data_stream); /* 关闭文件 */
return 0;
}

static int adc_read(struct adc_dev *dev){
int ret = 0;
char str[50];

SENSOR_FLOAT_DATA_GET(ret, IN_VOLTAGE_SCALE, str, scale);
SENSOR_INT_DATA_GET(ret, IN_VOLTAGE_RAW, str, raw);
/* 转换得到实际电压值mV */
dev->act = (dev->scale * dev->raw)/1000.f;
return ret;
}
int main(int argc, char *argv[]) {
int ret = 0;

if (argc != 1) {
printf("Error Usage!\r\n");
return -1;
}
while (1) {
ret = adc_read(&imx6ulladc);
if(ret == 0) { /* 数据读取成功 */
printf("ADC原始值:%d,电压值:%.3fV\r\n", imx6ulladc.raw, imx6ulladc.act);
}
usleep(100000); /*100ms */
}
return 0;
}

注意应用程序用到了浮点运算,因此:

arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard adcApp.c -o adcApp

image

字符设备驱动-regmap子系统

1 regmap的架构#

regmap是为了方便操作寄存器而设计的,它将所有模块的寄存器(包括soc上模块的寄存器和外围设备的寄存器等)
抽象出来,用一套统一接口来操作寄存器,统一操作 i2c、i3c、spi、mmio、sccb、sdw、slimbus、irq等。

image

image

regmap 框架分为三层:

①、底层物理总线: regmap 支持的物理总线有 i2c、i3c、spi、mmio、sccb、sdw、slimbus、irq、spmi 和 w1

②、regmap 核心层:用于实现 regmap核心。

③、regmapAPI 抽象层,regmap 向驱动编写人员提供的 API 接口。

2 数据结构#

2.1 regmap 结构体#

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
struct regmap {
union {
struct mutex mutex;
struct {
spinlock_t spinlock;
unsigned long spinlock_flags;
};
};
regmap_lock lock;
regmap_unlock unlock;
void *lock_arg; /* This is passed to lock/unlock functions */
struct device *dev; /* Device we do I/O on */
void *work_buf; /* Scratch buffer used to format I/O */
struct regmap_format format; /* Buffer format */
const struct regmap_bus *bus;
void *bus_context;
const char *name;

bool async;
spinlock_t async_lock;
wait_queue_head_t async_waitq;
struct list_head async_list;
struct list_head async_free;
int async_ret;
...
unsigned int max_register;
bool (*writeable_reg)(struct device *dev, unsigned int reg);
bool (*readable_reg)(struct device *dev, unsigned int reg);
bool (*volatile_reg)(struct device *dev, unsigned int reg);
bool (*precious_reg)(struct device *dev, unsigned int reg);
const struct regmap_access_table *wr_table;
const struct regmap_access_table *rd_table;
const struct regmap_access_table *volatile_table;
const struct regmap_access_table *precious_table;

int (*reg_read)(void *context, unsigned int reg,
unsigned int *val);
int (*reg_write)(void *context, unsigned int reg,
unsigned int val);
......
struct rb_root range_tree;
void *selector_work_buf; /* Scratch buffer used for selector */
}; // drivers/base/regmap/internal.h

regmap 的初始化通过结构体 regmap_config来完成。

2.2 regmap_config#

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
struct regmap_config {
const char *name;//名字
int reg_bits;//寄存器地址位数,必填字段。
int reg_stride;//寄存器地址步长。
int pad_bits; //寄存器和值之间的填充位数
int val_bits; //寄存器值位数,必填字段

//可写回调函数
bool (*writeable_reg)(struct device *dev, unsigned int reg);
//可读回调函数
bool (*readable_reg)(struct device *dev, unsigned int reg);
//可缓存回调函数
bool (*volatile_reg)(struct device *dev, unsigned int reg);
//是否不能读回调函数,当寄存器不能读,就返回tree
bool (*precious_reg)(struct device *dev, unsigned int reg);
regmap_lock lock;
regmap_unlock unlock;
void *lock_arg;
//读操作
int (*reg_read)(void *context, unsigned int reg, unsigned int *val);
//写操作
int (*reg_write)(void *context, unsigned int reg, unsigned int val);
bool fast_io;//快速 I/O,使用 spinlock 替代 mutex 来提升锁性能。
unsigned int max_register;//有效的最大寄存器地址
const struct regmap_access_table *wr_table;//可写的地址范围
const struct regmap_access_table *rd_table;//可读的地址范围
const struct regmap_access_table *volatile_table;//可缓存的地址范围
const struct regmap_access_table *precious_table;//不可读的地址范围
const struct reg_default *reg_defaults;//寄存器模式值,有两个成员变量:reg是寄存器地址,def是默认值
unsigned int num_reg_defaults;//默认寄存器表中的元素个数
enum regcache_type cache_type;
const void *reg_defaults_raw;
unsigned int num_reg_defaults_raw;

u8 read_flag_mask; //读标志掩码。
u8 write_flag_mask; //写标志掩码

bool use_single_rw;
bool can_multi_write;

enum regmap_endian reg_format_endian;
enum regmap_endian val_format_endian;

const struct regmap_range_cfg *ranges;
unsigned int num_ranges;
};

3 API#

3.1 Regmap 申请与初始化#

regmap 支持多种物理总线,比如 I2C 和 SPI,我们需要根据所使用的接口来选 择合适的 regmap 初始化函数。

1
2
3
struct regmap * regmap_init_spi(struct spi_device *spi, const struct regmap_config *config);
struct regmap * regmap_init_i2c(struct i2c_client *i2c, const struct regmap_config *config);
void regmap_exit(struct regmap *map);//不管是什么物理接口,退出都用regmap_exit

3.2 regmap 设备访问#

1
2
int regmap_read(struct regmap *map, unsigned int reg, unsigned int *val);
int regmap_write(struct regmap *map, unsigned int reg, unsigned int val);

regmap_readregmap_write 的基础上还衍生出了其他一些 regmap 的 API 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* regmap_update_bits函数,看名字就知道,此函数用来修改寄存器指定的 bit。
* mask:掩码,需要更新的位必须在掩码中设置为 1。
* val:需要更新的位值。
* 比如要将寄存器的 bit1 和 bit2 置 1,那么 mask 应该设置为 0X00000011,此时 val 的 bit1
和 bit2 应该设置为 1,也就是 0Xxxxxxx11。如果要清除寄存器的 bit4 和 bit7,那么 mask 应该
设置为 0X10010000,val 的 bit4 和 bit7 设置为 0,也就是 0X0xx0xxxx。
*/

int regmap_update_bits (struct regmap *map, unsigned int reg, unsigned int mask, unsigned int val);

//读取写入多个寄存器的值
//map:要操作的 regmap。
//reg:要读写的第一个寄存器。
//val:要读写的寄存器数据缓冲区。
//val_count:要读写的寄存器数量
int regmap_bulk_read(struct regmap *map, unsigned int reg, void *val, size_t val_count);
int regmap_bulk_write(struct regmap *map, unsigned int reg, const void *val, size_t val_count)

3.3 regmap_config 掩码设置#

结构体regmap_config里面有三个关于掩码的成员变量:read_flag_mask write_flag_mask.

Linux下SPI子系统驱动 字符设备驱动-SPI子系统 | Hexo (fuzidage.github.io)

image

其中读操作要将寄存器的地址 bit7 置 1,表示这是 一个读操作。当我们使用regmap的时候就不需要手动将寄存器地址的 bit7 置 1,在初始化 regmap_config 的时候直接将read_flag_mask设置为 0X80 即可,这样通过regmap读取 SPI 内部寄存器的时候就会将寄存器地址与read_flag_mask进行或运算,结果就是将 bit7 置 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static struct regmap_bus regmap_spi = {
.write = regmap_spi_write,
.gather_write = regmap_spi_gather_write,
.async_write = regmap_spi_async_write,
.async_alloc = regmap_spi_async_alloc,
.read = regmap_spi_read,
.read_flag_mask = 0x80,
.reg_format_endian_default = REGMAP_ENDIAN_BIG,
.val_format_endian_default = REGMAP_ENDIAN_BIG,
};
...
struct regmap *regmap_init_spi(struct spi_device *spi, const struct regmap_config *config)
{
return regmap_init(&spi->dev, &regmap_spi, &spi->dev, config);
}// regmap-spi.c

regmap_init 函数中找到如下所示内容:

1
2
3
4
5
6
if (config->read_flag_mask || config->write_flag_mask) {
map->read_flag_mask = config->read_flag_mask;
map->write_flag_mask = config->write_flag_mask;
} else if (bus) {
map->read_flag_mask = bus->read_flag_mask;
}

可以看到初始化时用regmap_config 中的读写掩码来初始化regmap_bus中的掩码。由于 regmap_spi 默认将 read_flag_mask 设置为 0X80,假如你所使用的 SPI 设备不需要读掩码,在初始 化regmap_config的时候一定要将 read_flag_mask 设置为 0X00。

4 Regmap操作SPI-ICM20608实验#

Linux下SPI子系统驱动 字符设备驱动-SPI子系统 | Hexo (fuzidage.github.io)

image

image

之前构造了icm20608_dev利用spi子系统框架通信。框架还是保持不变,添加regmap和regmap_config参数。

4.1 初始化 regmap#

image

image

4.2 regmap 访问icm20608#

以前的读写寄存器操作要构造spi_message,spi_transfer:

image

image

现在直接套用reg_map函数,无需要调用spi数据传输相关api:

image

image

读多组寄存器也是一样,调用regmap_bulk_read

image

5 Regmap操作I2C-ap3216c实验#

image

Linux I2C子系统驱动 字符设备驱动-I2C子系统 | Hexo (fuzidage.github.io)

5.1 初始化 regmap#

image

5.2 regmap 访问ap3216c#

image

image

这里初始化open fd时,要用到regmap_write进行i2c从设备的寄存器写入初始化。然后调用read,调用regmap_read进行读操作,这里每次都只读取一个byte,不需要用到regmap_bulk_read.

字符设备驱动-UART子系统

1 引入UART工作原理#

uart硬件传输原理s3c2440裸机编程-UART体系

2 Linux下TTY驱动框架#

image

可以看到tty框架下不止包含uart,还有display设备,键盘设备。

详细展开如下,tty_driver位于tty_io.c, 调用底下的uart_diver位于serial_core.cuart_driver子系统会被最底层的soc厂商拿去适配,调用uart_register_driver注册自己的uart控制器,去实现控制器要实现的uart_fops操作函数。
image

2.1 设备节点差别#

image

2.1.1 串口终端(/dev/ttyS*)#

串口终端是使用计算机串口连接的终端设备。Linux把每个串行端口都看做是一个字符设备。这些串行端口所对应的设备名称是/dev/ttySAC*;

2.1.2 控制台终端(/dev/console)#

在Linux系统中,计算机的输出设备通常被称为控制台终端,这里特指printk信息输出到设备。/dev/console是一个虚拟的设备,它需要映射到真正的tty上,比如通过内核启动参数“console=ttySCA0”就把console映射到了串口0

2.1.3 虚拟终端(/dev/tty*)#

当用户登录时,使用的是虚拟终端。使用Ctcl+Alt[F1 - F6]组合键时,我们就可以切换到tty1、tty2、tty3等上面去。tty*就称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名。

2.2 架构层次#

image

TTY核心层->线路规划层->tty驱动层。

2.3 UART驱动子系统#

2.3.1 数据结构和API#

2.3.1.1 uart_driver#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct uart_driver {
struct module *owner; /* 模块所属者 */
const char *driver_name; /* 驱动名字 */
const char *dev_name; /* 设备名字 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
int nr; /* 设备数 */
struct console *cons; /* 控制台 */
/*
* these are private; the low level driver should not
* touch these; they should be initialised to NULL
*/
struct uart_state *state;
struct tty_driver *tty_driver;
};
//include/linux/serial_core.h
2.3.1.1.1 uart_driver 注册与注销#

int uart_register_driver(struct uart_driver *drv);

返回值:0,成功;负值,失败。

image

1
2
3
4
5
6
7
retval = tty_register_driver(normal);
error = register_chrdev_region(dev, driver->num, driver->name);//也是通过字符设备驱动框架注册
d = tty_register_device(driver, i, NULL);
tty_register_device_attr(driver, index, device, NULL, NULL);
retval = tty_cdev_add(driver, devt, index, 1);
driver->cdevs[index]->ops = &tty_fops;
err = cdev_add(driver->cdevs[index], dev, count);

image

设置uart_ops为tty的tty_operations。然后调用tty_register_driver注册到tty子系统。

void uart_unregister_driver(struct uart_driver *drv);

2.3.1.2 uart_port#

描述串口端口的I/O端口或I/O内存地址、FIFO大小、端口类型、串口时钟等信息。实际上,一个uart_port实现对应一个串口设备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct uart_port {
spinlock_t lock; /* port lock */
unsigned long iobase; /* in/out[bwl] */
unsigned char __iomem *membase; /* read/write[bwl] */
..
const struct uart_ops *ops;
unsigned int custom_divisor;
unsigned int line; /* port index */
unsigned int minor;
resource_size_t mapbase; /* for ioremap */
resource_size_t mapsize;
struct device *dev; /* parent device */
..
};
//include/linux/serial_core.h
2.3.1.2.1 uart_port 的添加与移除#

int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport);

返回值:0,成功;负值,失败。

uart_port uart_driver 结合起来。

int uart_remove_one_port(struct uart_driver *drv, struct uart_port *uport);

2.3.1.2.2 uart_port 的休眠与恢复#
1
2
3
4
int uart_suspend_port(struct uart_driver *drv, struct uart_port *port);//挂起特定的串口端口
int uart_resume_port(struct uart_driver *drv, struct uart_port *port);
//唤醒上层因串口端口写数据而堵塞的进程,通常在串口发送中断处理函数中调用该函数
void uart_write_wakeup(struct uart_port *port);

2.3.1.3 uart_ops#

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
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
unsigned int (*get_mctrl)(struct uart_port *);
void (*stop_tx)(struct uart_port *);
void (*start_tx)(struct uart_port *);
void (*throttle)(struct uart_port *);
void (*unthrottle)(struct uart_port *);
void (*send_xchar)(struct uart_port *, char ch);
void (*stop_rx)(struct uart_port *);
void (*enable_ms)(struct uart_port *);
void (*break_ctl)(struct uart_port *, int ctl);
int (*startup)(struct uart_port *);
void (*shutdown)(struct uart_port *);
void (*flush_buffer)(struct uart_port *);
void (*set_termios)(struct uart_port *, struct ktermios *new,struct ktermios *old);
void (*set_ldisc)(struct uart_port *, struct ktermios *);
void (*pm)(struct uart_port *, unsigned int state,unsigned int oldstate);
/*
* Return a string describing the type of the port
*/
const char *(*type)(struct uart_port *);

/*
* Release IO and memory resources used by the port.
* This includes iounmap if necessary.
*/
void (*release_port)(struct uart_port *);
/*
* Request IO and memory resources used by the port.
* This includes iomapping the port if necessary.
*/
int (*request_port)(struct uart_port *);
void (*config_port)(struct uart_port *, int);
int (*verify_port)(struct uart_port *, struct serial_struct *);
int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct uart_port *);
void (*poll_put_char)(struct uart_port *, unsigned char);
int (*poll_get_char)(struct uart_port *);
#endif
};

2.3.1.4 console#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct console {
char name[16];
void(*write)(struct console *,const char *, unsigined);
int (*read)(struct console *, char *, unsigned);
struct tty_driver *(struct console *,int*);
void (*unblank)(void);
int (*setup)(struct console *, char *);
int (*early_setup)(void);
short flags;
short index; /*用来指定该console使用哪一个uart port (对应的uart_port中的line),
如果为-1,kernel会自动选择第一个uart port*/
int cflag;
void *data;
struct console *next;
};

2.3.1.5 波特率相关#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*功能:uart_get_baud_rate通过解码termios结构体来获取指定串口的波特率
*参数:
* port:要获取波特率的串口端口
* termios:当前期望的termios配置(包括串口波特率)
* old:以前的termios配置,可以为NULL
* min:可以接受的最小波特率
* max:可以接受的最大波特率
* 返回值:串口波特率
*/
unsigned int uart_get_baund_rate(struct uart_port *port, struct ktermios *termios
, struct ktermios *old,unsigned int min, unsigned int max);
/*功能:uart_get_divisor 用于计算某一波特率的串口时钟分频数(串口波特率除数)
*参数:
* port:要计算分频数的串口端口
* baud:期望的波特率
*返回值:串口时钟分频数
*/
unsigned int uart_get_divisor(struct uart_port *port, unsigned int baund);

2.3.1.6 向串口写控制台信息#

1
2
3
4
5
6
7
8
9
/*功能:uart_console_write用于向串口端口写一控制台信息
*参数:
* port:要写信息的串口端口
* s:要写的信息
* count:信息的大小
* putchar:用于向串口端口写字符的函数,该函数有两个参数:串口端口和要写的字符
*/
Void uart_console_write(struct uart_port *port,const char *s,
unsigned int count,viod(*putchar)(struct uart_port*, int));

2.4 UART控制器示例-imx为例#

image

位于drivers\tty\serial\imx.c,使用platform_driver框架,调用uart_register_driver注册到uart子系统。

2.4.1 dts描述#

板子使用的是uart3,打开imx6ul.dtsi,可以看到默认status是disabled。

1
2
3
4
5
6
7
8
9
10
11
12
uart3: serial@021ec000 {
compatible = "fsl,imx6ul-uart",
"fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x021ec000 0x4000>;
interrupts = <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART3_IPG>,
<&clks IMX6UL_CLK_UART3_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 29 4 0>, <&sdma 30 4 0>;
dma-names = "rx", "tx";
status = "disabled";
};

我们再外面引用它,打开imx6ull-alientek-emmc.dts

1
2
3
4
5
6
7
8
9
10
11
pinctrl_uart3: uart3grp {
fsl,pins = <
MX6UL_PAD_UART3_TX_DATA__UART3_DCE_TX 0X1b0b1
MX6UL_PAD_UART3_RX_DATA__UART3_DCE_RX 0X1b0b1
>;
};
&uart3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart3>;
status = "okay";
};

2.4.2 probe初始化#

当dts和驱动的compatible匹配,那么probe执行如下:

初始化 uart_port,然后将其添加到对应的 uart_driver 中。

  1. 解析dts, 设置中断号,io基地址后ioremap, 设置port属性。

image

  1. 设置port属性的ops

image

  1. 更具dts获取和设置时钟频率。
  2. 注册中断

image

  1. uart_add_one_port(&imx_reg, &sport->port);添加端口到uart_driver。

2.4.2 收据收发流程#

2.4.2.1 open过程#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//tty_io.c
static const struct file_operations tty_fops = {
.read = tty_read,
.write = tty_write,
.open = tty_open,
.release = tty_release,
······
};//open("/dev/tty");
//进入tty_open函数:
static int tty_open(struct inode *inode, struct file *filp){
struct tty_struct *tty;
struct tty_driver *driver = NULL;
......
if (tty->ops->open) /*即uart_open*/
retval = tty->ops->open(tty, filp);//
return 0;
}
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
static int uart_open(struct tty_struct *tty, struct file *filp) {
struct uart_driver *drv = (struct uart_driver *)tty->driver->driver_state;
int retval;
struct uart_state *state = drv->state + line;
retval = uart_startup(tty, state, 0);
}
static int uart_startup(struct tty_struct *tty, struct uart_state *state, int init_hw) {
retval = uart_port_startup(tty, state, init_hw);
}
static int uart_port_startup(struct tty_struct *tty, struct uart_state *state, int init_hw) {
struct uart_port *uport = state->uart_port;
if (!state->xmit.buf) {
page = get_zeroed_page(GFP_KERNEL); /*分配了一页内存*/
state->xmit.buf = (unsigned char *) page; /*串口底层发送缓冲区*/
uart_circ_clear(&state->xmit);
}
retval = uport->ops->startup(uport); /*即imx_startup*/
return retval;
}

static int imx_startup(struct uart_port *port)
{
struct imx_port *sport = (struct imx_port *)port;
int retval, i;
unsigned long flags, temp;
.....
/* Can we enable the DMA support? */
if (is_imx6q_uart(sport) && !uart_console(port)
&& !sport->dma_is_inited)
imx_uart_dma_init(sport); //配置发送消息时DMA搬运的目标地址,接收消息时DMA搬运的源地址

if (sport->dma_is_inited)
INIT_DELAYED_WORK(&sport->tsk_dma_tx, dma_tx_work);//定义了一个延后工作任务
//DMA发送搬运,在串口发送数据会唤醒调度
return 0;
}

2.4.2.2 发送接收总流程#

image

2.4.2.3 发送-write过程#

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 应用层调用write系统调用来写入数据
2. write系统调用会调用到tty_write函数,这个函数定义在driver/tty/tty_io.c文件中。
而在tty_write函数里面调用的是ld->ops->write函数,这个就是line discipline层的write函数。
3. line discipline层的write函数就是n_tty_write函数,这个函数定义在driver/tty/n_tty.c文件中。
n_tty_write函数会进一步调用到uart_write函数,这个函数是通过tty_operations结构体的指针来访问的。
4. uart_write函数会进一步调用到start_tx函数,这个函数也是通过tty_operations结构体的指针来访问的。
在i.MX6ULL平台上,这个函数对应的就是imx_start_tx函数。
5. 在imx_start_tx函数中,会通过设置UCR1寄存器的TXMPTYEN位来使能发送缓冲区空中断。
这个操作是通过调用writel(temp | UCR1_TXMPTYEN, sport->port.membase + UCR1)来完成的。
6. 当UART控制器的发送缓冲区空了之后,就会产生一个中断。这个中断会被内核的中断处理机制捕获,
并调用相应的中断处理程序来处理。
7. 在中断处理程序中,会从环形缓冲区中取出数据,并写入到UART控制器的发送缓冲区中。
然后UART控制器会自动将这些数据发送出去。
2.4.2.3.1 用户写数据流程#

image

image

tty_write中通过ld->ops->write调用了线路规程的write函数,也就是调用了tty_ldisc_N_TTYntty_write函数。

2.4.2.3.2 硬件写数据流程#

drivers\tty\serial\imx.c的发送函数imx_start_tx和发送中断函数imx_txint

image

一开始时,发送buffer肯定为空,会立刻产生中断:

image

2.4.2.4 接收-read过程#

1
2
3
4
5
6
7
8
9
10
11
1. UART硬件控制器的接收端接收到数据后,数据会被自动写入接收缓冲区。
如果接收缓冲区中的数据达到一定数量(例如,半满或者满),
或者在一定时间内没有新的数据到来,那么就会产生一个接收中断。
2. 执行中断服务程序。在i.MX6ULL平台上,对应imx_rxint函数,在drivers/tty/serial/imx.c文件中。
2.1 imx_rxint函数中,首先会得到数据,然后通知line_disciplie层来处理,
调用line_disciplie层的n_tty_receive_buf函数。
2.2 n_tty_receive_buf调用n_tty_receive_buf_common__receive_buf。
这个函数会根据TTY设备的配置来处理新的数据,例如进行字符映射、回显等操作。
处理完之后,数据已经被存储在了环形缓冲区,并且也已经进行了必要的处理,接下来等待应用程序的read来读取。
3. 应用层的read来读取数据,调用tty层的tty_read函数,进一步调用line discipline层的read函数,
这个line discipline层的read函数就可以读取前面从底层传过来的数据了。
2.4.2.4.1 中断产生数据流程#

imx_rxint
// 读取硬件状态
// 得到数据
// 在对应的uart_port中更新统计信息, 比如sport->port.icount.rx++;
// 把数据存入tty_port里的tty_buffer
tty_insert_flip_char(port, rx, flg)
// 通知行规程来处理
tty_flip_buffer_push(port);
tty_schedule_flip(port);
queue_work(system_unbound_wq, &buf->work); // 使用工作队列来处理
// 对应flush_to_ldisc函数

2.4.2.4.2 用户读取数据流程#

image

tty_read中通过ld->ops->read调用了线路规程的read函数,也就是调用了tty_ldisc_N_TTYntty_read函数。

3 UART用户态控制函数#

3.1 API说明#

属性 说明
tcgetatrr 取属性(termios结构)
tcsetarr 设置属性(termios结构)
cfgetispeed 得到输入速度
cfsetispeed 得到输出速度
cfstospeed 设置输出速度
tcdrain 等待所有输出都被传输
tcflow 挂起传输或接收
tcflush 刷请未决输出和/或输入
tcsendbreak 送BREAK字符
tcgetpgrp 得到前台进程组ID
Tcsetpgrp 设置前台进程组ID

3.1.1 设置参数#

  1. 获取属性, tegetatrr(fd, &oldtio);
1
2
struct termious newtio, oldtio;
tegetattr(fd, &oldtio);
  1. 激活选项有CLOCALCREAD,用于本地连接和接收使用
1
newtio.cflag |= CLOCAL|CREAD;
  1. 设置波特率
1
newtio.c_cflag = B115200;
  1. 设置数据位,需使用掩码设置
1
2
newtio.c_cflag &= ~CSIZE;
Newtio.c_cflag |= CS8;
  1. 设置停止位,通过激活c_cflag中的CSTOP实现。若停止位为1,则清除CSTOPB,若停止位为2,则激活CSTOP
1
2
newtio.c_cflag &= ~CSTOPB; /*停止位设置为1*/
Newtio.c_cflag |= CSTOPB; /*停止位设置为2 */
  1. 设置流控
1
2
newtio.c_cfag |= CRTSCTS /*开启硬件流控 */
newtio.c_cfag |= (IXON | IXOFF | IXANY); /*开启软件流控*/
  1. 奇偶检验位设置,使用c_cflagc_ifag.
    7.1 设置奇校验
1
2
3
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);

7.2 设置偶校验

1
2
3
newtio.c_iflag |= (INPCK | ISTRIP);
newtio.c_cflag |= PARENB;
newtio.c_cflag |= ~PARODD;
  1. 设置最少字符和等待时间,对于接收字符和等待时间没有什么特别的要求,可设置为0:
1
2
newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN] = 0;
  1. 处理要写入的引用对象
    tcflush函数刷清(抛弃)输入缓冲(终端程序已经接收到,但用户程序尚未读)或输出缓冲(用户程序已经写,但未发送)。
1
2
3
4
5
6
7
int tcflash(int filedes, int quene)
quene数应当是下列三个常数之一:
*TCIFLUSH 刷清输入队列
*TCOFLUSH 刷清输出队列
*TCIOFLUSH 刷清输入、输出队列
例如:
tcflush(fd, TCIFLUSH);
  1. 激活配置,在完成配置后,需要激活配置使其生效。使用tcsetattr()函数:
1
2
3
4
5
6
7
int tcsetarr(int filedes, const struct termios *termptr);
opt 指定在什么时候新的终端属性才起作用,
*TCSANOW:更改立即发生
*TCSADRAIN:发送了所有输出后更改才发生。若更改输出参数则应使用此选项
*TCSAFLUSH:发送了所有输出后更改才发生。更进一步,在更改发生时未读的
所有输入数据都被删除(刷清)
例如:tcsetatrr(fd, TCSANOW, &newtio);

3.1.2 操作流程#

  1. 打开串口,例如"/dev/ttySLB0"
1
2
3
4
5
6
fd = open("/dev/ttySLB0",O_RDWR | O_NOCTTY | O_NDELAY);
/*O_NOCTTY:是为了告诉Linux这个程序不会成为这个端口上的“控制终端”。
如果不这样做的话,所有的输入,比如键盘上过来的Ctrl+C中止信号等等,会影响到你的进程。
O_NDELAY:这个标志则是告诉Linux这个程序并不关心DCD信号线的状态,
也就是不管串口是否有数据到来,都是非阻塞的,程序继续执行。
*/
  1. 恢复串口状态为阻塞状态,用于等待串口数据的读入,用fcntl函数:
1
fcntl(fd,F_SETFL,0);  //F_SETFL:设置文件flag为0,即默认,即阻塞状态
  1. 接着测试打开的文件描述符是否应用一个终端设备,以进一步确认串口是否正确打开。
1
isatty(STDIN_FILENO);
  1. 读写串口
1
2
3
串口的读写与普通文件一样,使用read,write函数
read(fd, buf ,8);
write(fd,buff,8);

3.2 demo举例#

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <log/log.h>
#include <stdlib.h>
#define UART_DEVICE "/dev/ttySLB1"
struct temp {
float temp_max1;
float temp_max2;
float temp_max3;
float temp_min;
float temp_mean;
float temp_enviromem;
char temp_col[1536];
};

int main(void) {
int count, i, fd;
struct termios oldtio, newtio;
struct temp *temp;
temp = (struct temp *)malloc(sizeof(struct temp));
if (!temp) {
printf("malloc failed\n");
return -1;
}

char cmd_buf1[] = { 0xAA, 0x01, 0x04, 0x00, 0x06, 0x10, 0x05, 0x00, 0xBB};
char cmd_buf2[] = { 0xAA, 0x01, 0x04, 0x00, 0x00, 0xA0, 0x00, 0x03, 0xBB};
char cmd_buf3[] = { 0xAA, 0x01, 0x04, 0x00, 0x03, 0x10, 0x01, 0x00, 0xBB};
char read_buf[2000];

fd = open(UART_DEVICE, O_RDWR | O_NOCTTY);
if (fd < 0) {
printf("Open %s failed\n", UART_DEVICE);
return -1;
}
tcgetattr(fd, &oldtio);//获取当前操作模式参数
memset(&newtio, 0, sizeof(newtio));
//波特率=230400 数据位=8 使能数据接收
newtio.c_cflag = B230400 | CS8 | CLOCAL | CREAD | CSTOPB;
newtio.c_iflag = IGNPAR;
tcflush(fd, TCIFLUSH);//清空输入缓冲区和输出缓冲区
tcsetattr(fd, TCSANOW, &newtio);//设置新的操作参数

//printf("input: %s, len = %d\n", cmd_buf, strlen(cmd_buf));
//------------向uart发送数据-------------------
for (i = 0; i < 9; i++)
printf("%#X ", cmd_buf1[i]);

count = write(fd, cmd_buf1, 9);
if (count != 9) {
printf("send failed\n");
return -1;
}
usleep(500000);
memset(read_buf, 0, sizeof(read_buf));
count = read(fd, read_buf, sizeof(read_buf));
if (count > 0) {
for (i = 0; i < count; i++);
temp->temp_max1 = read_buf[7] << 8 | read_buf[6];
temp->temp_max2 = read_buf[9] << 8 | read_buf[8];
temp->temp_max3 = read_buf[11] << 8 | read_buf[10];
temp->temp_min = read_buf[13] << 8 | read_buf[12];
temp->temp_mean = read_buf[15] << 8 | read_buf[14];

printf("temp->temp_max1 = %f\n", temp->temp_max1 * 0.01);
printf("temp->temp_max2 = %f\n", temp->temp_max2 * 0.01);
printf("temp->temp_max3 = %f\n", temp->temp_max3 * 0.01);
printf("temp->temp_min = %f\n", temp->temp_min * 0.01);
printf("temp->temp_mean = %f\n", temp->temp_mean * 0.01);
} else {
printf("read temp failed\n");
return -1;
}

count = write(fd, cmd_buf3, 9);
if (count != 9) {
printf("send failed\n");
return -1;
}

usleep(365);
memset(read_buf, 0, sizeof(read_buf));
count = read(fd, read_buf, sizeof(read_buf));
if (count > 0) {
for (i = 0; i < count; i++);
temp->temp_enviromem = read_buf[7] << 8 | read_buf[6];
printf("temp->temp_enviromem = %f\n", temp->temp_enviromem * 0.01);
} else {
printf("read enviromem failed\n");
return -1;
}

count = write(fd, cmd_buf2, 9);
if (count != 9) {
printf("send failed\n");
return -1;
}

usleep(70000);
memset(read_buf, 0, sizeof(read_buf));
memset(temp->temp_col, 0, sizeof(temp->temp_col));
count = read(fd, read_buf, sizeof(read_buf));
printf("count = %d\n", count);
if (count > 0) {
for (i = 0; i < count - 7; i++)
temp->temp_col[i] = read_buf[i+6];
for (i = 0; i < 1536; i++)
{
if (!(i%10))
printf("\n");
printf("%#X ", temp->temp_col[i]);
}
} else {
printf("read temp colour failed\n");
return -1;
}
free(temp);
close(fd);
tcsetattr(fd, TCSANOW, &oldtio); //恢复原先的设置
return 0;
}

image

image

字符设备驱动-RTC子系统

Linux下RTC子系统驱动#

1 引入RTC#

CPU内部有很多定时器,像看门狗WDT,PWM定时器,高精度定时器Timer等等, 只在“启动”即“通电时”运行,断电时停止。

当然,如果时钟不能连续跟踪时间,则必须手动设置。那么当关机后就没办法自动计数统计时间了。RTC 就很好的解决了这个问题,RTC是实时时钟,用于记录当前系统时间。

2 Linux 内核 RTC 驱动框架#

RTC在linux内核态也是用一个字符设备驱动去实现的。Linux 内核将 RTC 设备抽象为rtc_device结构体,定义在 include/linux/rtc.h, 进入drivers/rtc子系统目录:

1
2
3
4
5
6
7
class.c:为底层驱动提供 register 与 unregister 接口用于 RTC 设备的注册/注销。初始化 RTC 设备结构、sysfs、proc
interface.c:提供用户程序与 RTC 的接口函数
dev.c:将 RTC 设备抽象为通用的字符设备,提供文件操作函数集
sysfs.c:管理 RTC 设备的 sysfs 属性,获取 RTC 设备名、日期、时间等
proc.c:管理 RTC 设备的 procfs 属性,提供中断状态和标志查询
lib.c:提供 RTC、Data 和 Time 之间的转换函数
rtc-xxx.c:各平台 RTC 设备的实际驱动

image

2.1 rtc子系统Makefile#

rtc子系统Makefile如下, 可以根据配置宏去裁剪rtc子系统。

1
2
3
4
5
6
7
8
9
10
obj-$(CONFIG_RTC_LIB)           += lib.o
obj-$(CONFIG_RTC_SYSTOHC) += systohc.o
obj-$(CONFIG_RTC_CLASS) += rtc-core.o
obj-$(CONFIG_RTC_MC146818_LIB) += rtc-mc146818-lib.o
rtc-core-y := class.o interface.o

rtc-core-$(CONFIG_RTC_NVMEM) += nvmem.o
rtc-core-$(CONFIG_RTC_INTF_DEV) += dev.o
rtc-core-$(CONFIG_RTC_INTF_PROC) += proc.o
rtc-core-$(CONFIG_RTC_INTF_SYSFS) += sysfs.o

image

Linux默认rtc是开启的。

1
2
3
4
5
6
7
8
9
10
11
Device Drivers
  ->Real Time Clock
    []Set system time from RTC on startup and resume
    [](rtc0) RTC used to set the system time
    []Set the RTC time based on NTP synchronization
    [](rtc0) RTC used to synchronize NTP adjustment
    []RTC debug support
    []RTC non volatile storage support
    []/sys/class/rtc/rtcN (sysfs)
    []/proc/driver/rtc (procfs for rtcN)
    []/dev/rtcN (character devices)

2.2 rtc数据结构#

2.2.1 rtc_device#

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
struct rtc_device {
struct device dev;
struct module *owner;

int id; /* ID, 当前rtc设备在rtc子系统的子序号*/
char name[RTC_DEVICE_NAME_SIZE]; /* 名字 */

const struct rtc_class_ops *ops; /* RTC 设备底层操作函数 */
struct mutex ops_lock;

struct cdev char_dev; /* 字符设备 */
unsigned long flags;

unsigned long irq_data;
spinlock_t irq_lock;
wait_queue_head_t irq_queue;
struct fasync_struct *async_queue;

struct rtc_task *irq_task;
spinlock_t irq_task_lock;
int irq_freq;
int max_user_freq;

struct timerqueue_head timerqueue;
struct rtc_timer aie_timer;
struct rtc_timer uie_rtctimer;
struct hrtimer pie_timer; /* sub second exp, so needs hrtimer */
int pie_enabled;
struct work_struct irqwork;
/* Some hardware can't support UIE mode */
int uie_unsupported;
};

2.2.2 rtc_class_ops#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct rtc_class_ops {
int (*open)(struct device *);
void (*release)(struct device *);
int (*ioctl)(struct device *, unsigned int, unsigned long);
int (*read_time)(struct device *, struct rtc_time *);
int (*set_time)(struct device *, struct rtc_time *);
int (*read_alarm)(struct device *, struct rtc_wkalrm *);
int (*set_alarm)(struct device *, struct rtc_wkalrm *);
int (*proc)(struct device *, struct seq_file *);
int (*set_mmss64)(struct device *, time64_t secs);
int (*set_mmss)(struct device *, unsigned long secs);
int (*read_callback)(struct device *, int data);
int (*alarm_irq_enable)(struct device *, unsigned int enabled);
};

rtc_class_ops 为 RTC 设备的最底层操作函数集合,包括从 RTC 设备中读取时间、向 RTC 设备写入新的时间值等,对接RTC硬件控制器,不直接对接应用。

2.2.3 rtc_dev_fops#

Linux 内核提供了一个 RTC 通用字符设备驱动文件,文件名为 drivers/rtc/rtc-dev.c,r该文件提供了所有 RTC 设备共用的 file_operations 函数操作集,对接应用ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static const struct file_operations rtc_dev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = rtc_dev_read,
.poll = rtc_dev_poll,
.unlocked_ioctl = rtc_dev_ioctl,
.open = rtc_dev_open,
.release = rtc_dev_release,
.fasync = rtc_dev_fasync,
};

//调用关系:以RTC_RD_TIME为例
rtc_dev_ioctl
->rtc_read_time
->rtc->ops->read_time(rtc->dev.parent, tm);
//可以看出,rtc_read_time 函数最终会调用 rtc_class_ops 中的.read_time 来从 RTC 设备中获取当前时间

rtc_dev_ioctl 函数对其他的命令处理都是类似的,比 如 RTC_ALM_READ 命令会通过rtc_read_alarm函数获取到闹钟值,而 rtc_read_alarm 函数经过层层调用,最终会调用rtc_class_ops中的 read_alarm 函数来获取闹钟值。上下调用关系如下:

image

2.3 rtc子系统初始化#

rtc子系统初始化,主要分配rtc_class类,以及rtc设备的rtc_devt为设备号:

1
2
3
4
rtc_init
  ->class_create--创建rtc_class类。  
->rtc_dev_init   
->alloc_chrdev_region--为rtc设备分配子设备号范围0~15。主设备号随机分配。最终结果放入rtc_devt。

image

系统启动时会将RTC时间设置到系统时间:

1
2
3
4
rtc_hctosys
  ->rtc_read_time
  ->rtc_tm_to_time64
  ->do_settimeofday64

2.4 rtc设备操作API#

对rtc设备的操作主要有:alarm读取和设置、rtc time读取和设置、中断配置, 对应drivers\rtc\interface.c,头文件对应include/linux/rtc.h

1
2
3
4
5
6
7
8
9
10
11
12
13
extern int rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm);
extern int rtc_set_time(struct rtc_device *rtc, struct rtc_time *tm);
extern int rtc_set_ntp_time(struct timespec64 now, unsigned long *target_nsec);
int __rtc_read_alarm(struct rtc_device *rtc, struct rtc_wkalrm *alarm);
extern int rtc_read_alarm(struct rtc_device *rtc,struct rtc_wkalrm *alrm);
extern int rtc_set_alarm(struct rtc_device *rtc,struct rtc_wkalrm *alrm);
extern int rtc_initialize_alarm(struct rtc_device *rtc,struct rtc_wkalrm *alrm);
extern void rtc_update_irq(struct rtc_device *rtc,
unsigned long num, unsigned long events);
extern int rtc_irq_set_state(struct rtc_device *rtc, int enabled);
extern int rtc_irq_set_freq(struct rtc_device *rtc, int freq);
extern int rtc_update_irq_enable(struct rtc_device *rtc, unsigned int enabled);
extern int rtc_alarm_irq_enable(struct rtc_device *rtc, unsigned int enabled);

2.5 注册RTC设备#

devm_rtc_device_register或者rtc_register_device注册rtc设备到rtc子系统。

1
2
3
4
5
6
7
8
struct rtc_device *devm_rtc_device_register(struct device *dev,
const char *name,
const struct rtc_class_ops *ops,
struct module *owner);

int __rtc_register_device(struct module *owner, struct rtc_device *rtc);

void rtc_device_unregister(struct rtc_device *rtc);

image

3 RTC驱动实例#

以nxp的imx6ull芯片为例,打开imx6ull.dtsi,找到snvs_rtc设备节点。

3.1 设备树节点#

1
2
3
4
5
6
snvs_rtc: snvs-rtc-lp {
compatible = "fsl,sec-v4.0-mon-rtc-lp";
regmap = <&snvs>;
offset = <0x34>;
interrupts = <GIC_SPI 19 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>;
};

对应驱动文件 drivers/rtc/rtc-snvs.c

image

3.2 驱动probe#

3.2.1 snvs_rtc_probe#

image

  1. Linux3.1 引入了一个全新的 regmap 机制(Linux下regmap模型驱动 - fuzidage - 博客园 (cnblogs.com)),devm_regmap_init_mmio。regmap 用于提供一套方便的 API 函 数去操作底层硬件寄存器,以提高代码的可重用性。snvs-rtc.c 文件会采用 regmap 机制来读写 RTC 底层硬件寄存器。这里使用 devm_regmap_init_mmio 函数将 RTC 的硬件寄存器转化为 regmap 形式,这样 regmap 机制的 regmap_writeregmap_read 等 API 函数才能操作寄存器。

  2. 获取中断号,时钟,使能时钟。

  3. 设置 RTC_ LPPGDR 寄存器值为 SNVS_LPPGDR_INIT= 0x41736166,这里就是用的 regmap 机制的 regmap_write 函数完成对寄存器进行写操作。

  4. RTC_LPSR 寄存器,写入 0xffffffffLPSRRTC 状态寄存器,写 1 清零, 因此这一步就是清除 LPSR 寄存器。

  5. 调用snvs_rtc_enable函数使能 RTC,此函数会设置 RTC_LPCR 寄存器。

  6. devm_request_irq函数请求RTC中断,中断服务函数为snvs_rtc_irq_handler, 用于 RTC 闹钟中断。

  7. 设置rtc_class_ops,并且调用rtc_register_device注册rtc子系统。

3.2.2 rtc_class_ops实例#

1
2
3
4
5
6
7
static const struct rtc_class_ops snvs_rtc_ops = {
.read_time = snvs_rtc_read_time,
.set_time = snvs_rtc_set_time,
.read_alarm = snvs_rtc_read_alarm,
.set_alarm = snvs_rtc_set_alarm,
.alarm_irq_enable = snvs_rtc_alarm_irq_enable,
};

3.2.2.1 snvs_rtc_read_time#

获取rtc时间的细节详见 IMX6ULL裸机-RTC定时器

SNVS_SRTCMR[14:0]代表SRTC计数器的高15位
SNVS_SRTCLR[31:15]代表SRTC计数器的低17位
注意:是以 1970 年 1 月 1 日0点0分0秒为起点,加上经过的总秒数即可得到现在的时间点。
SNVS_HPCOMR[31], NPSWA_EN位,非特权软件访问控制位,如果非特权软件要访问 SNVS 的话此位必须为 1。
SNVS_LPCR[0], SRTC_ENV位,使能 RTC 计数器。

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
#define SNVS_LPREGISTER_OFFSET	0x34
/* These register offsets are relative to LP (Low Power) range */
#define SNVS_LPCR 0x04
#define SNVS_LPSR 0x18
#define SNVS_LPSRTCMR 0x1c
#define SNVS_LPSRTCLR 0x20
#define SNVS_LPTAR 0x24
#define SNVS_LPPGDR 0x30
#define SNVS_LPCR_SRTC_ENV (1 << 0)
#define SNVS_LPCR_LPTA_EN (1 << 1)
#define SNVS_LPCR_LPWUI_EN (1 << 3)
#define SNVS_LPSR_LPTA (1 << 0)
#define SNVS_LPPGDR_INIT 0x41736166
#define CNTR_TO_SECS_SH 15
static int snvs_rtc_read_time(struct device *dev, struct rtc_time *tm){
struct snvs_rtc_data *data = dev_get_drvdata(dev);
unsigned long time;
int ret;
if (data->clk) {
ret = clk_enable(data->clk);
if (ret)
return ret;
}

time = rtc_read_lp_counter(data);
rtc_time64_to_tm(time, tm);

if (data->clk)
clk_disable(data->clk);
return 0;
}

image

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
/* Read 64 bit timer register, which could be in inconsistent state */
static u64 rtc_read_lpsrt(struct snvs_rtc_data *data){
u32 msb, lsb;
regmap_read(data->regmap, data->offset + SNVS_LPSRTCMR, &msb);
regmap_read(data->regmap, data->offset + SNVS_LPSRTCLR, &lsb);
return (u64)msb << 32 | lsb;
}

/* Read the secure real time counter, taking care to deal with the cases of the
* counter updating while being read.
*/
static u32 rtc_read_lp_counter(struct snvs_rtc_data *data){
u64 read1, read2;
unsigned int timeout = 100;

/* As expected, the registers might update between the read of the LSB
* reg and the MSB reg. It's also possible that one register might be
* in partially modified state as well.
*/
read1 = rtc_read_lpsrt(data);
do {
read2 = read1;
read1 = rtc_read_lpsrt(data);
} while (read1 != read2 && --timeout);
if (!timeout)
dev_err(&data->rtc->dev, "Timeout trying to get valid LPSRT Counter read\n");
/* Convert 47-bit counter to 32-bit raw second count */
return (u32) (read1 >> CNTR_TO_SECS_SH);
}

image

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
void rtc_time64_to_tm(time64_t time, struct rtc_time *tm) {
unsigned int month, year, secs;
int days;
/* time must be positive */
days = div_s64_rem(time, 86400, &secs);
/* day of the week, 1970-01-01 was a Thursday */
tm->tm_wday = (days + 4) % 7;
year = 1970 + days / 365;
days -= (year - 1970) * 365
+ LEAPS_THRU_END_OF(year - 1)
- LEAPS_THRU_END_OF(1970 - 1);
while (days < 0) {
year -= 1;
days += 365 + is_leap_year(year);
}
tm->tm_year = year - 1900;
tm->tm_yday = days + 1;
for (month = 0; month < 11; month++) {
int newdays;
newdays = days - rtc_month_days(month, year);
if (newdays < 0)
break;
days = newdays;
}
tm->tm_mon = month;
tm->tm_mday = days + 1;
tm->tm_hour = secs / 3600;
secs -= tm->tm_hour * 3600;
tm->tm_min = secs / 60;
tm->tm_sec = secs - tm->tm_min * 60;
tm->tm_isdst = 0;
}

image

  1. rtc_read_lpsrt函数RTC定时器寄存器,得到64位原始数据。让后将其转换成按秒计算单位。可以看到就是设置读取RTC寄存器信息。
  2. 调用drivers\rtc\lib.crtc_time64_to_tm将秒数转换成rtc_time, 也就是年月日单位。

3.2.2.2 snvs_rtc_set_time#

设置时钟,和snvs_rtc_read_time同理,也是寄存器操作,就不展开细节分析。

3.3 应用测试#

3.3.1 读取rtc时间#

1
2
[root@imx6ull]~# date
Thu Jan 1 08:00:13 CST 1970

3.3.2 设置rtc时间#

image

现在我要设置当前时间为 2023 年 8 月 31 日 18:13:00。
date -s "2023-08-31 18:13:00"

1
2
[root@imx6ull]~# date -s "2023-08-31 18:13:00"
Thu Aug 31 18:13:00 CST 2023

注意我们使用“date -s”命令仅仅是将当前系统时间设置了,此时间还没有写入到RTC 芯片里面,因此系统重启以后时间又会丢失。我们需要将 当前的时间写入到 RTC 里面,这里要用到 hwclock 命令,输入如下命令将系统时间写入到 RTC 里面:
hwclock -w

字符设备驱动-Framebuffer子系统

1 引入Framebuffer#

s3c2440裸机-LCD编程一、LCD硬件原理

s3c2440裸机编程-LDC | Hexo (fuzidage.github.io)

介绍了LDC的基本原理。裸机 LCD 驱动编写流程如下:

  1. 初始化 I.MX6U 的 eLCDIF 控制器,屏幕宽(width)、高(height)、hspw、 hbp、hfp、vspw、vbp 和 vfp 等信息。
  2. 初始化 LCD 像素时钟。
  3. 设置 RGBLCD 显存属性。
  4. 应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。

同理linux系统下也是希望应用程序来直接操作一块内存来实现实现在 LCD 上显示字符、图片等信息,Framebuffer就是用来干这件事的。Framebuffer 翻译过来就是帧缓冲,简称 fb

作用:把显示设备描述成一个缓冲区,允许应用程序通过帧缓冲定义好的接口访问这些图形设备,从而不用关心具体的硬件细节。

因此需要在底层framebuffer框架去对接具体的显示设备,显示设备控制器。

2 Framebuffer驱动介绍#

2.1 Framebuffer设备节点#

当我们编写好 LCD 驱动以后会生成一个名为/dev/fbX(X=0~n)的设备,应用程序通 过访问/dev/fbX 这个设备就可以访问 LCD。

image

2.2 Framebuffer框架#

image

  • drivers/video/fbmem.c:主要任务是创建graphics类、注册FB的字符设备驱动(主设备号是29)、提供register_framebuffer接口给具体framebuffer驱动编写着来注册fb设备的。
1
#define FB_MAJOR		29   /* /dev/fb* framebuffers */
  • drivers/video/fbsys.c:是fbmem.c引出来的,处理fb在/sys/class/graphics/fb0目录下的一些属性文件的。

  • xxx_fb.c: 具体的显示设备控制器驱动代码,dts描述对应的显示设备,控制器去驱动显示设备。

2.3 Framebuffer数据结构#

2.3.1 fb_info#

fb_info结构体记录了帧缓冲设备的全部信息,包括设备的设置参数、状态以及操作函数指针,对于每一个帧缓冲设备都必须对应一个fb_info结构体实例。

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
struct fb_info {
atomic_t count;
int node;
int flags;
struct mutex lock; /* 互斥锁 */
struct mutex mm_lock; /* 互斥锁,用于 fb_mmap 和 smem_*域*/
struct fb_var_screeninfo var; /* 当前可变参数 */
struct fb_fix_screeninfo fix; /* 当前固定参数 */
struct fb_monspecs monspecs; /* 当前显示器特性 */
struct work_struct queue; /* 帧缓冲事件队列 */
struct fb_pixmap pixmap; /* 图像硬件映射 */
struct fb_pixmap sprite; /* 光标硬件映射 */
struct fb_cmap cmap; /* 当前调色板 */
struct list_head modelist; /* 当前模式列表 */
struct fb_videomode *mode; /* 当前视频模式 */
#ifdef CONFIG_FB_BACKLIGHT /* 如果 LCD 支持背光的话 */
/* assigned backlight device */
/* set before framebuffer registration,
remove after unregister */
struct backlight_device *bl_dev; /* 背光设备 */
/* Backlight level curve */
struct mutex bl_curve_mutex;
u8 bl_curve[FB_BACKLIGHT_LEVELS];
#endif
...
struct fb_ops *fbops; /* 帧缓冲操作函数集 */
struct device *device; /* 父设备 */
struct device *dev; /* 当前 fb 设备 */
int class_flag; /* 私有 sysfs 标志 */
...
char __iomem *screen_base; /* 虚拟内存基地址(屏幕显存) */
unsigned long screen_size; /* 虚拟内存大小(屏幕显存大小) */
void *pseudo_palette; /* 伪 16 位调色板 */
};

2.3.1.1 fb_ops#

帧缓冲操作函数集,包含open,release,read,write等操作函数。

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
/*
* Frame buffer operations
*
* LOCKING NOTE: those functions must _ALL_ be called with the console
* semaphore held, this is the only suitable locking mechanism we have
* in 2.6. Some may be called at interrupt time at this point though.
*
* The exception to this is the debug related hooks. Putting the fb
* into a debug state (e.g. flipping to the kernel console) and restoring
* it must be done in a lock-free manner, so low level drivers should
* keep track of the initial console (if applicable) and may need to
* perform direct, unlocked hardware writes in these hooks.
*/
struct fb_ops {
/* open/release and usage marking */
struct module *owner;
int (*fb_open)(struct fb_info *info, int user);
int (*fb_release)(struct fb_info *info, int user);
/* For framebuffers with strange non linear layouts or that do not
* work with normal memory mapped access
*/
ssize_t (*fb_read)(struct fb_info *info, char __user *buf,
size_t count, loff_t *ppos);
ssize_t (*fb_write)(struct fb_info *info, const char __user *buf,
size_t count, loff_t *ppos);
/* checks var and eventually tweaks it to something supported,
* DO NOT MODIFY PAR */
int (*fb_check_var)(struct fb_var_screeninfo *var, struct fb_info *info);
/* set the video mode according to info->var */
int (*fb_set_par)(struct fb_info *info);
/* set color register */
int (*fb_setcolreg)(unsigned regno, unsigned red, unsigned green,
unsigned blue, unsigned transp, struct fb_info *info);
/* set color registers in batch */
int (*fb_setcmap)(struct fb_cmap *cmap, struct fb_info *info);
/* blank display */
int (*fb_blank)(int blank, struct fb_info *info);
/* pan display */
int (*fb_pan_display)(struct fb_var_screeninfo *var, struct fb_info *info);
/* Draws a rectangle */
void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect);
/* Copy data from area to another */
void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region);
/* Draws a image to the display */
void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image);
/* Draws cursor */
int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor);
/* wait for blit idle, optional */
int (*fb_sync)(struct fb_info *info);
/* perform fb specific ioctl (optional) */
int (*fb_ioctl)(struct fb_info *info, unsigned int cmd,
unsigned long arg);
/* Handle 32bit compat ioctl (optional) */
int (*fb_compat_ioctl)(struct fb_info *info, unsigned cmd,
unsigned long arg);
/* perform fb specific mmap */
int (*fb_mmap)(struct fb_info *info, struct vm_area_struct *vma);
/* get capability given var */
void (*fb_get_caps)(struct fb_info *info, struct fb_blit_caps *caps,
struct fb_var_screeninfo *var);
/* teardown any resources to do with this framebuffer */
void (*fb_destroy)(struct fb_info *info);
/* called at KDB enter and leave time to prepare the console */
int (*fb_debug_enter)(struct fb_info *info);
int (*fb_debug_leave)(struct fb_info *info);
};

2.3.1.2 fb_var_screeninfo#

记录用户可修改的显示控制器参数,包括了屏幕的分辨率和每个像素点的比特数bpp,pixclock等。

image

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
struct fb_var_screeninfo {
__u32 xres; /* visible resolution */
__u32 yres;
__u32 xres_virtual; /* virtual resolution */
__u32 yres_virtual;
__u32 xoffset; /* offset from virtual to visible */
__u32 yoffset; /* resolution */

__u32 bits_per_pixel; /* guess what */
__u32 grayscale; /* 0 = color, 1 = grayscale, */
/* >1 = FOURCC */
struct fb_bitfield red; /* bitfield in fb mem if true color, */
struct fb_bitfield green; /* else only length is significant */
struct fb_bitfield blue;
struct fb_bitfield transp; /* transparency */

__u32 nonstd; /* != 0 Non standard pixel format */

__u32 activate; /* see FB_ACTIVATE_* */

__u32 height; /* height of picture in mm */
__u32 width; /* width of picture in mm */

__u32 accel_flags; /* (OBSOLETE) see fb_info.flags */

/* Timing: All values in pixclocks, except pixclock (of course) */
__u32 pixclock; /* pixel clock in ps (pico seconds) */
__u32 left_margin; /* time from sync to picture */
__u32 right_margin; /* time from picture to sync */
__u32 upper_margin; /* time from sync to picture */
__u32 lower_margin;
__u32 hsync_len; /* length of horizontal sync */
__u32 vsync_len; /* length of vertical sync */
__u32 sync; /* see FB_SYNC_* */
__u32 vmode; /* see FB_VMODE_* */
__u32 rotate; /* angle we rotate counter clockwise */
__u32 colorspace; /* colorspace for FOURCC-based modes */
__u32 reserved[4]; /* Reserved for future compatibility */
};
2.3.1.2.1 引入可视屏幕和虚拟屏幕#

image

1
2
3
4
5
(1)可视屏幕:LCD分辨率,这是硬件相关的。比如:LCD屏幕的分辨率是800x480,那可视屏幕的最大分辨率就是800x480;
(2)虚拟屏幕:我们在内核中开辟的帧缓冲区的大小。比如:屏幕分辨率是800x480,但是我们可以将帧缓冲区开辟成1920x1080,
在刷新屏幕时可以直接将1080p的图像一次性刷新到帧缓冲区中;
(3)虚拟屏到可视屏的偏移量:虚拟屏大小是超过可视屏幕的大小,偏移量决定了可视屏显示虚拟屏的哪一个部分;
(4)总结:通过改变虚拟屏到可视屏的偏移量,可以将虚拟屏的不同部分图像显示到可视屏中,而不需要每次都刷新帧缓冲区;

2.3.1.3 fb_fix_screeninfo#

记录了用户不能修改的显示控制器的参数,比如说屏幕缓冲区的物理地址、长度。

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct fb_fix_screeninfo {
char id[16]; /* identification string eg "TT Builtin" */
unsigned long smem_start; /* Start of frame buffer mem */
/* (physical address) */
__u32 smem_len; /* Length of frame buffer mem */
__u32 type; /* see FB_TYPE_* */
__u32 type_aux; /* Interleave for interleaved Planes */
__u32 visual; /* see FB_VISUAL_* */
__u16 xpanstep; /* zero if no hardware panning */
__u16 ypanstep; /* zero if no hardware panning */
__u16 ywrapstep; /* zero if no hardware ywrap */
__u32 line_length; /* length of a line in bytes */
unsigned long mmio_start; /* Start of Memory Mapped I/O */
/* (physical address) */
__u32 mmio_len; /* Length of Memory Mapped I/O */
__u32 accel; /* Indicate to driver which */
/* specific chip/card we have */
__u16 capabilities; /* see FB_CAP_* */
__u16 reserved[2]; /* Reserved for future compatibility */
};

2.3.1.4 fb_bitfield#

描述每一像素缓冲区的组织方式,包括域偏移、位域长度和MSB指示。

1
2
3
4
5
6
struct fb_bitfield {
__u32 offset; /* beginning of bitfield */
__u32 length; /* length of bitfield */
__u32 msb_right; /* != 0 : Most significant bit is */
/* right */
};

2.4 Framebuffer源码分析#

2.4.1 编写fb驱动大致流程#

  1. 构建fb_info结构体
  2. register_framebuffer注册fb_infofb框架中,驱动框架会自动创建/dev/fbx设备节点
  3. app通过open、ioctl等函数接口去操作设备节点/dev/fb0,驱动框架就会调用fb_info实例化中对应的open、ioctl接口去完成具体的硬件操作。

2.4.2 fb子系统注册卸载#

如果定义了MODULE宏就表示要fb子系统单独编译成ko文件,否则用subsys_initcall编译进内核。入口在drivers\video\fbdev\core\fbmem.c

image

  1. 创建proc条目/proc/fb

  2. 注册成字符设备,fb主设备号定义在include\uapi\linux\major.h

    #define FB_MAJOR 29 /* /dev/fb* framebuffers */

  3. 建立graphics类。

2.4.2.1 fb注册卸载相关函数#

2.4.2.1.1 framebuffer_alloc#
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
struct fb_info *framebuffer_alloc(size_t size, struct device *dev)
{
//计算私有数据起始地址需要补齐的字节数
#define BYTES_PER_LONG (BITS_PER_LONG/8)
#define PADDING (BYTES_PER_LONG - (sizeof(struct fb_info) % BYTES_PER_LONG))

int fb_info_size = sizeof(struct fb_info);
struct fb_info *info;
char *p;
if (size)
fb_info_size += PADDING;

//申请内存
p = kzalloc(fb_info_size + size, GFP_KERNEL);
if (!p)
return NULL;
info = (struct fb_info *) p;

//将申请的私有数据的地址赋值给info->par
if (size)
info->par = p + fb_info_size;

//设备的父节点
info->device = dev;
#ifdef CONFIG_FB_BACKLIGHT
mutex_init(&info->bl_curve_mutex);
#endif
return info;
#undef PADDING
#undef BYTES_PER_LONG
}
1
2
3
4
5
6
(1)famebuffer_alloc函数是用来申请一个struct fb_info结构体的,传参的size是设备私有数据的大小;
(2)申请sizeof(struct fb_info) + PADDING + size大小的空间分配给fb_info结构体类型的指针info,
加上PADDING 字节是为了后面的设备私有数据保持BYTES_PER_LONG字节对齐;
(3)将fb_info结构体后面size大小且BYTES_PER_LONG 字节的设备私有数据地址赋值info->par;
(4)将传入的参数dev赋值给info->device,作为父设备;
(5)返回创建好的struct fb_into结构体指针info;
2.4.2.1.2 register_framebuffer#
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
int register_framebuffer(struct fb_info *fb_info) {
int i;
struct fb_event event;
struct fb_videomode mode;

//检查已经注册的帧缓冲设备是否已经达到上限
if (num_registered_fb == FB_MAX)
return -ENXIO;

//判断 fb_ info->flags 标志中关于控制器大小端的设置是否正确
if (fb_check_foreignness(fb_info))
return -ENOSYS;

//在registered_fb数组中找一个空闲的变量
num_registered_fb++;
for (i = 0 ; i < FB_MAX; i++)
if (!registered_fb[i])
break;

//将申请到的变量在数组中的下标赋值给fb_info->node
fb_info->node = i;

mutex_init(&fb_info->lock);
mutex_init(&fb_info->mm_lock);

//创建帧缓冲设备
fb_info->dev = device_create(fb_class, fb_info->device,
MKDEV(FB_MAJOR, i), NULL, "fb%d", i);

if (IS_ERR(fb_info->dev)) {
printk(KERN_WARNING "Unable to create device for framebuffer %d; errno = %ld\n",
i, PTR_ERR(fb_info->dev));
fb_info->dev = NULL;
} else
//初始化帧缓冲设备,创建更多设备属性文件
fb_init_device(fb_info);

//初始化fb_info->pixmap,该变量的作用是将用于显示的硬件无关数据转换为设备需要的格式
if (fb_info->pixmap.addr == NULL) {
fb_info->pixmap.addr = kmalloc(FBPIXMAPSIZE, GFP_KERNEL);
if (fb_info->pixmap.addr) {
fb_info->pixmap.size = FBPIXMAPSIZE;
fb_info->pixmap.buf_align = 1;
fb_info->pixmap.scan_align = 1;
fb_info->pixmap.access_align = 32;
fb_info->pixmap.flags = FB_PIXMAP_DEFAULT;
}
}
fb_info->pixmap.offset = 0;

if (!fb_info->pixmap.blit_x)
fb_info->pixmap.blit_x = ~(u32)0;

if (!fb_info->pixmap.blit_y)
fb_info->pixmap.blit_y = ~(u32)0;

//初始化显示模式链表 fb_ info->modelist
if (!fb_info->modelist.prev || !fb_info->modelist.next)
INIT_LIST_HEAD(&fb_info->modelist);

//根据fb_info->var设置一个 mode
fb_var_to_videomode(&mode, &fb_info->var);

//将该mode添加到fb_info->modelist中
fb_add_videomode(&mode, &fb_info->modelist);

//将fb_info注册到registered_fb结构体中
registered_fb[i] = fb_info;

event.info = fb_info;
if (!lock_fb_info(fb_info))
return -ENODEV;

//〕通知发生了FB_EVENT_FB_REGISTERED事件(帧缓冲设备注册事件)
fb_notifier_call_chain(FB_EVENT_FB_REGISTERED, &event);
unlock_fb_info(fb_info);
return 0;
}
2.4.2.1.3 unregister_framebuffer#
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
int unregister_framebuffer(struct fb_info *fb_info) {
struct fb_event event;
int i, ret = 0;
//检查传入的fb_info是否已经注册过
i = fb_info->node;
if (!registered_fb[i]) {
ret = -EINVAL;
goto done;
}

if (!lock_fb_info(fb_info))
return -ENODEV;
event.info = fb_info;

//通知发生FB_EVENT_FB_UNBIND事件,绑定了该帧缓冲设备的都解绑
ret = fb_notifier_call_chain(FB_EVENT_FB_UNBIND, &event);
unlock_fb_info(fb_info);

if (ret) {
ret = -EINVAL;
goto done;
}

//释放掉申请的fb_info->pixmap.addr
if (fb_info->pixmap.addr &&
(fb_info->pixmap.flags & FB_PIXMAP_DEFAULT))
kfree(fb_info->pixmap.addr);

//销毁fb_info->modelist模式链表
fb_destroy_modelist(&fb_info->modelist);

//将占用的registered_fb数组中的变量置为NULL,表示空闲
registered_fb[i]=NULL;

//内核中注册的帧缓冲设备数量减一
num_registered_fb--;

//销毁点帧缓冲设备的属性文件
fb_cleanup_device(fb_info);

//销毁掉帧缓冲设备
device_destroy(fb_class, MKDEV(FB_MAJOR, i));
event.info = fb_info;

//通知发生了FB_EVENT_FB_UNREGISTERED事件,表示该帧缓冲设备已经被注销掉
fb_notifier_call_chain(FB_EVENT_FB_UNREGISTERED, &event);

//如果fb_info结构体中有销毁函数就调用销毁函数
/* this may free fb info */
if (fb_info->fbops->fb_destroy)
fb_info->fbops->fb_destroy(fb_info);
done:
return ret;
}

2.4.3 fb_ops分析#

fb_ops中的操作函数属于框架部分,并不和具体的硬件相关,在进行一些处理后最后都是调用struct fb_info结构体中fb_ops定义的操作方法;

!image

2.4.3.1 fb_open#

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
static int fb_open(struct inode *inode, struct file *file)
__acquires(&info->lock)
__releases(&info->lock)
{
//获取次设备号
int fbidx = iminor(inode);

struct fb_info *info;
int res = 0;

//判断次设备号是否在合法范围
if (fbidx >= FB_MAX)
return -ENODEV;

//根据次设备号找到对应的struct fb_info结构体指针
info = registered_fb[fbidx];
if (!info)
//如果数组下标fbidx的变量是NULL,手动加载帧缓冲设备
request_module("fb%d", fbidx);

//再次从registered_fb数组中获取对应的struct fb_info结构体指针
info = registered_fb[fbidx];
if (!info)
return -ENODEV;
mutex_lock(&info->lock);
if (!try_module_get(info->fbops->owner)) {
res = -ENODEV;
goto out;
}

//将struct fb_info结构体指针保存到struct file结构体的私有数据指针中,后续的接口会用到
file->private_data = info;

//调用帧缓冲设备驱动的fb_open函数
if (info->fbops->fb_open) {
res = info->fbops->fb_open(info,1);
if (res)
module_put(info->fbops->owner);
}
out:
mutex_unlock(&info->lock);
return res;
}

2.4.3.2 fb_write#

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
static ssize_t fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos){
//显示的偏移量
unsigned long p = *ppos;

//获取到设备节点的struct inode结构体
struct inode *inode = file->f_path.dentry->d_inode;

//从inode节点中获取次设备号
int fbidx = iminor(inode);

//以次设备号为下标在registered_fb数组中获取到对应的struct fb_info结构体指针
struct fb_info *info = registered_fb[fbidx];

u32 *buffer, *src;
u32 __iomem *dst;
int c, i, cnt = 0, err = 0;
unsigned long total_size;

if (!info || !info->screen_base)
return -ENODEV;

if (info->state != FBINFO_STATE_RUNNING)
return -EPERM;

//如果帧缓冲设备驱动中的struct fb_ops中有定义写帧缓冲的方法就执行
if (info->fbops->fb_write)
return info->fbops->fb_write(info, buf, count, ppos);

/*************执行通用的写帧缓冲的方法*************/
//虚拟内存的大小
total_size = info->screen_size;

if (total_size == 0)
total_size = info->fix.smem_len;

if (p > total_size)
return -EFBIG;

if (count > total_size) {
err = -EFBIG;
count = total_size;
}

//检查偏移量加上写入数据的大小是否超过虚拟内存的大小
if (count + p > total_size) {
if (!err)
err = -ENOSPC;

count = total_size - p;
}

buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,
GFP_KERNEL);
if (!buffer)
return -ENOMEM;

//得到要写入帧缓冲区的起始地址:帧缓冲虚拟起始地址加上偏移量
dst = (u32 __iomem *) (info->screen_base + p);

//对于某些帧缓冲设备来说,必须等待它完成之前的显示处理操作,
//才能继续向帧缓冲中送入显示数据,该方t法用于该过程的同步
if (info->fbops->fb_sync)
info->fbops->fb_sync(info);

//向帧缓冲写入count个字节数据,如果写入的数据超过一个页的大小,则分多次写入
while (count) {
//将预写数据依次读到buffer中,每次写数据不超过PAGE_SIZE大小
c = (count > PAGE_SIZE) ? PAGE_SIZE : count;
src = buffer;

//把数据从用户空间拷贝到内核空间
if (copy_from_user(src, buf, c)) {
err = -EFAULT;
break;
}
//将数据写入到目标帧缓冲区地址
for (i = c >> 2; i--; )
fb_writel(*src++, dst++);
if (c & 3) {
u8 *src8 = (u8 *) src;
u8 __iomem *dst8 = (u8 __iomem *) dst;
for (i = c & 3; i--; )
fb_writeb(*src8++, dst8++);
dst = (u32 __iomem *) dst8;
}
*ppos += c;
buf += c;
cnt += c;
count -= c;
}
kfree(buffer);
return (cnt) ? cnt : err;
}

2.4.3.3 fb_mmap#

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
static int fb_mmap(struct file *file, struct vm_area_struct * vma){
//获取次设备号
int fbidx = iminor(file->f_path.dentry->d_inode);
//根据次设备号获取到struct fb_info 结构体
struct fb_info *info = registered_fb[fbidx];
//得到驱动的fbops操作方法
struct fb_ops *fb = info->fbops;
unsigned long off;
unsigned long start;
u32 len;
if (vma->vm_pgoff > (~0UL >> PAGE_SHIFT))
return -EINVAL;
off = vma->vm_pgoff << PAGE_SHIFT;
if (!fb)
return -ENODEV;
mutex_lock(&info->mm_lock);
//如果fb_ops中实现了mmap方法,则调用之
if (fb->fb_mmap) {
int res;
res = fb->fb_mmap(info, vma);
mutex_unlock(&info->mm_lock);
return res;
}

/*******下面是通用的mmap方法********/
/* 获取映射帧缓冲的物理起始地址和长度 */
start = info->fix.smem_start;
len = PAGE_ALIGN((start & ~PAGE_MASK) + info->fix.smem_len);
if (off >= len) {
/* 如果off大于帧缓冲长度.则认为映射的是内存映射IO */
off -= len;
if (info->var.accel_flags) {
mutex_unlock(&info->mm_lock);
return -EINVAL;
}
//获取内存映射IO的物理起始地址和长度
start = info->fix.mmio_start;
len = PAGE_ALIGN((start & ~PAGE_MASK) + info->fix.mmio_len);
}
mutex_unlock(&info->mm_lock);
//保证页对齐
start &= PAGE_MASK;
if ((vma->vm_end - vma->vm_start + off) > len)
return -EINVAL;
//现在off表示映射设备内存实际的物理地址
off += start;
vma->vm_pgoff = off >> PAGE_SHIFT;
/* This is an IO map - tell maydump to skip this VMA */
vma->vm_flags |= VM_IO | VM_RESERVED;
//置页保护标识
fb_pgprotect(file, vma, off);
//建立从物理页帧号为 off》PAGE SH 工FT的物理内存,到虚拟地址为 vma->vm start 、
//大小为 vma->vm_end - vma->vm_start 、页保护标志为 vma->vm_page_prot的映射
if (io_remap_pfn_range(vma, vma->vm_start, off >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot))
return -EAGAIN;
return 0;
}

2.4.3.4 fb_ioctl#

image

宏定义 功能说明
FBIOGET_VSCREENINFO 获取屏幕可变参数
FBIOPUT_VSCREENINFO 设置屏幕可变参数
FBIOGET_FSCREENINFO 获取屏幕固定参数
FBIOPUTCMAP 设置颜色表
FBIOGETCMAP 获取颜色表
FBIOPAN_DISPLAY 动视窗显示
FBIO_CURSOR 光标设置,目前不支持
FBIOGET_CON2FBMAP 获取指定帧缓冲控制台对应的帧缓冲设备
FBIOPUT_CON2FBMAP 置指定的帧缓冲控制台对应的帧缓冲设备
FBIOBLANK 显示空白

3 Framebuffer驱动实例#

image

3.1定义fb_info实例#

image

以飞思卡尔nxp的LCD控制器来说,叫做lcdif。位于drivers/video/fbdev/mxsfb.c,以像素时钟模式为例:

image

3.1.0 控制器dts配置#

打开imx6ull.dtsi

1
2
3
4
5
6
7
8
9
10
lcdif: lcdif@021c8000 {
compatible = "fsl,imx6u l-lcdif", "fsl,imx28-lcdif";
reg = <0x021c8000 0x4000>;
interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
<&clks IMX6UL_CLK_LCDIF_APB>,
<&clks IMX6UL_CLK_DUMMY>;
clock-names = "pix", "axi", "disp_axi";
status = "disabled";
};

compatible匹配,probe执行。

3.1.1 mxsfb_probe过程分析#

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
static int mxsfb_probe(struct platform_device *pdev){
const struct of_device_id *of_id =
of_match_device(mxsfb_dt_ids, &pdev->dev);
struct resource *res;
struct mxsfb_info *host;
struct fb_info *fb_info;
struct pinctrl *pinctrl;
int irq = platform_get_irq(pdev, 0);
int gpio, ret;
if (of_id)
pdev->id_entry = of_id->data;
gpio = of_get_named_gpio(pdev->dev.of_node, "enable-gpio", 0);
if (gpio == -EPROBE_DEFER)
return -EPROBE_DEFER;
if (gpio_is_valid(gpio)) {
ret = devm_gpio_request_one(&pdev->dev, gpio, GPIOF_OUT_INIT_LOW, "lcd_pwr_en");
if (ret) {
dev_err(&pdev->dev, "faild to request gpio %d, ret = %d\n", gpio, ret);
return ret;
}
}
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "Cannot get memory IO resource\n");
return -ENODEV;
}
host = devm_kzalloc(&pdev->dev, sizeof(struct mxsfb_info), GFP_KERNEL);
if (!host) {
dev_err(&pdev->dev, "Failed to allocate IO resource\n");
return -ENOMEM;
}
fb_info = framebuffer_alloc(sizeof(struct fb_info), &pdev->dev);
if (!fb_info) {
dev_err(&pdev->dev, "Failed to allocate fbdev\n");
devm_kfree(&pdev->dev, host);
return -ENOMEM;
}
host->fb_info = fb_info;
fb_info->par = host;
ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0,
dev_name(&pdev->dev), host);
if (ret) {
dev_err(&pdev->dev, "request_irq (%d) failed with error %d\n",
irq, ret);
ret = -ENODEV;
goto fb_release;
}

host->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(host->base)) {
dev_err(&pdev->dev, "ioremap failed\n");
ret = PTR_ERR(host->base);
goto fb_release;
}
host->pdev = pdev;
platform_set_drvdata(pdev, host);

host->devdata = &mxsfb_devdata[pdev->id_entry->driver_data];
host->clk_pix = devm_clk_get(&host->pdev->dev, "pix");
if (IS_ERR(host->clk_pix)) {
host->clk_pix = NULL;
ret = PTR_ERR(host->clk_pix);
goto fb_release;
}

host->clk_axi = devm_clk_get(&host->pdev->dev, "axi");
if (IS_ERR(host->clk_axi)) {
host->clk_axi = NULL;
ret = PTR_ERR(host->clk_axi);
goto fb_release;
}

host->clk_disp_axi = devm_clk_get(&host->pdev->dev, "disp_axi");
if (IS_ERR(host->clk_disp_axi)) {
host->clk_disp_axi = NULL;
ret = PTR_ERR(host->clk_disp_axi);
goto fb_release;
}

host->reg_lcd = devm_regulator_get(&pdev->dev, "lcd");//电流整流:和电源管理有关,实现低功耗
if (IS_ERR(host->reg_lcd))
host->reg_lcd = NULL;

fb_info->pseudo_palette = devm_kzalloc(&pdev->dev, sizeof(u32) * 16,
GFP_KERNEL);
if (!fb_info->pseudo_palette) {
ret = -ENOMEM;
goto fb_release;
}

INIT_LIST_HEAD(&fb_info->modelist);

pm_runtime_enable(&host->pdev->dev);

ret = mxsfb_init_fbinfo(host);
if (ret != 0)
goto fb_pm_runtime_disable;

mxsfb_dispdrv_init(pdev, fb_info);
if (!host->dispdrv) {
pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
if (IS_ERR(pinctrl)) {
ret = PTR_ERR(pinctrl);
goto fb_pm_runtime_disable;
}
}

if (!host->enabled) {
writel(0, host->base + LCDC_CTRL);
mxsfb_set_par(fb_info);
mxsfb_enable_controller(fb_info);
pm_runtime_get_sync(&host->pdev->dev);
}

ret = register_framebuffer(fb_info);
if (ret != 0) {
dev_err(&pdev->dev, "Failed to register framebuffer\n");
goto fb_destroy;
}
console_lock();
ret = fb_blank(fb_info, FB_BLANK_UNBLANK);
console_unlock();
if (ret < 0) {
dev_err(&pdev->dev, "Failed to unblank framebuffer\n");
goto fb_unregister;
}
dev_info(&pdev->dev, "initialized\n");
return 0;
fb_unregister:
unregister_framebuffer(fb_info);
fb_destroy:
if (host->enabled)
clk_disable_unprepare(host->clk_pix);
fb_destroy_modelist(&fb_info->modelist);
fb_pm_runtime_disable:
pm_runtime_disable(&host->pdev->dev);
devm_kfree(&pdev->dev, fb_info->pseudo_palette);
fb_release:
framebuffer_release(fb_info);
devm_kfree(&pdev->dev, host);
return ret;
}
  1. host 结构体指针变量,表示LDCIF控制器,包含Framebuffer设备详细信息,比如时钟eLCDIF 控制器寄存器基地址fb_info 等。

  2. 从dts中提取gpio, irq, res,时钟等信息。初始化host, fb_info等结构体。

  3. host->base = devm_ioremap_resource(&pdev->dev, res);对io内存进行ioremap, 把eLCDIF 控制器地址映射成虚拟地址。

  4. mxsfb_init_fbinfo:

    image

    1. 设置eLCDIF控制器具体的fb_ops

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      static struct fb_ops mxsfb_ops = {
      .owner = THIS_MODULE,
      .fb_check_var = mxsfb_check_var,
      .fb_set_par = mxsfb_set_par,
      .fb_setcolreg = mxsfb_setcolreg,
      .fb_ioctl = mxsfb_ioctl,
      .fb_blank = mxsfb_blank,
      .fb_pan_display = mxsfb_pan_display,
      .fb_mmap = mxsfb_mmap,
      .fb_fillrect = cfb_fillrect,
      .fb_copyarea = cfb_copyarea,
      .fb_imageblit = cfb_imageblit,
      };
    2. 从dts获取LCD 的各个参数信息,然后调用mxsfb_map_videomem申请framebuffer空间,也就是显存。

  5. fb_videomode_to_var设置ldc的可变属性

  6. 设置控制器寄存器信息

    1
    2
    3
    writel(0, host->base + LCDC_CTRL);
    mxsfb_set_par(fb_info);
    mxsfb_enable_controller(fb_info);
  7. 调用 register_framebuffer 函数向 Linux 内核注册 fb_info

mxsfb.c中已经定义了 eLCDIF 控制器各个寄存器相比于基地址的偏移值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define LCDC_CTRL			0x00
#define LCDC_CTRL1 0x10
#define LCDC_V4_CTRL2 0x20
#define LCDC_V3_TRANSFER_COUNT 0x20
#define LCDC_V4_TRANSFER_COUNT 0x30
#define LCDC_V4_CUR_BUF 0x40
#define LCDC_V4_NEXT_BUF 0x50
#define LCDC_V3_CUR_BUF 0x30
#define LCDC_V3_NEXT_BUF 0x40
#define LCDC_TIMING 0x60
#define LCDC_VDCTRL0 0x70
#define LCDC_VDCTRL1 0x80
#define LCDC_VDCTRL2 0x90
#define LCDC_VDCTRL3 0xa0
#define LCDC_VDCTRL4 0xb0
#define LCDC_DVICTRL0 0xc0
#define LCDC_DVICTRL1 0xd0
#define LCDC_DVICTRL2 0xe0
#define LCDC_DVICTRL3 0xf0
#define LCDC_DVICTRL4 0x100
#define LCDC_V4_DATA 0x180
#define LCDC_V3_DATA 0x1b0
#define LCDC_V4_DEBUG0 0x1d0
#define LCDC_V3_DEBUG0 0x1f0

3.2 LCD屏幕dts描述#

3.2.1 屏幕 IO 配置#

除了eLCDIF 控制器,对LCD设备也需要进行描述,主要是引脚pinmux。比如imx6ull-alientek-emmc.dts这块板子对应的LCD屏幕使用引脚如下,iomuxc 节点下有如下节点:

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
pinctrl_lcdif_dat: lcdifdatgrp {//数据引脚,24根,rgb888
fsl,pins = <
MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x79
MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x79
MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x79
MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x79
MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x79
MX6UL_PAD_LCD_DATA05__LCDIF_DATA05 0x79
MX6UL_PAD_LCD_DATA06__LCDIF_DATA06 0x79
MX6UL_PAD_LCD_DATA07__LCDIF_DATA07 0x79
MX6UL_PAD_LCD_DATA08__LCDIF_DATA08 0x79
MX6UL_PAD_LCD_DATA09__LCDIF_DATA09 0x79
MX6UL_PAD_LCD_DATA10__LCDIF_DATA10 0x79
MX6UL_PAD_LCD_DATA11__LCDIF_DATA11 0x79
MX6UL_PAD_LCD_DATA12__LCDIF_DATA12 0x79
MX6UL_PAD_LCD_DATA13__LCDIF_DATA13 0x79
MX6UL_PAD_LCD_DATA14__LCDIF_DATA14 0x79
MX6UL_PAD_LCD_DATA15__LCDIF_DATA15 0x79
MX6UL_PAD_LCD_DATA16__LCDIF_DATA16 0x79
MX6UL_PAD_LCD_DATA17__LCDIF_DATA17 0x79
MX6UL_PAD_LCD_DATA18__LCDIF_DATA18 0x79
MX6UL_PAD_LCD_DATA19__LCDIF_DATA19 0x79
MX6UL_PAD_LCD_DATA20__LCDIF_DATA20 0x79
MX6UL_PAD_LCD_DATA21__LCDIF_DATA21 0x79
MX6UL_PAD_LCD_DATA22__LCDIF_DATA22 0x79
MX6UL_PAD_LCD_DATA23__LCDIF_DATA23 0x79
>;
};
pinctrl_lcdif_ctrl: lcdifctrlgrp {//控制引脚,hsync vsync pixclk en等...
fsl,pins = <
MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x79
MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x79
MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x79
MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x79
>;
};
pinctrl_pwm1: pwm1grp {//背光亮度
fsl,pins = <
MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
>;
};

以及iomixc_snvs下有一个reset节点:

image

1
2
3
pinctrl_lcdif_dat,为 RGB LCD 的 24 根数据线配置项
pinctrl_lcdif_ctrl,RGB LCD 的 4 根控制线配置项,包括 CLK、 ENABLE、VSYNC 和 HSYNC
pinctrl_pwm1,LCD 背光 PWM 引脚配置项

可以看到控制和数据引脚的电器属性默认nxp都帮我们设置成了0x79。

3.2.2 屏幕节点#

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
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat /* 使用到的 IO */
&pinctrl_lcdif_ctrl
&pinctrl_lcdif_reset>;
display = <&display0>;
status = "okay";
display0: display { /* LCD 属性信息 */
bits-per-pixel = <16>; /* 一个像素占用几个 bit */
bus-width = <24>; /* 总线宽度 */
display-timings {
native-mode = <&timing0>; /* 时序信息 */
timing0: timing0 {
clock-frequency = <9200000>; /* LCD 像素时钟,单位 Hz */
hactive = <480>; /* LCD X 轴像素个数 */
vactive = <272>; /* LCD Y 轴像素个数 */
hfront-porch = <8>; /* LCD hfp 参数 */
hback-porch = <4>; /* LCD hbp 参数 */
hsync-len = <41>; /* LCD hspw 参数 */
vback-porch = <2>; /* LCD vbp 参数 */
vfront-porch = <4>; /* LCD vfp 参数 */
vsync-len = <10>; /* LCD vspw 参数 */
hsync-active = <0>; /* hsync 数据线极性 */
vsync-active = <0>; /* vsync 数据线极性 */
de-active = <1>; /* de 数据线极性 */
pixelclk-active = <0>; /* clk 数据线先极性 */
};
};
};
};

display0 子节点,描述 LCD 的参数信息,包括bpp, bus-width,时序特性,这些参数跟随具体的屏厂屏幕规格走。例如另一款屏幕ATK7016(7 寸 1024*600)屏幕:可以看到这款屏幕是RGB888的,bpp是3byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
display0: display {
bits-per-pixel = <24>; /* 一个像素占用 24bit */
bus-width = <24>; /* 总线宽度 */
display-timings {
native-mode = <&timing0>; /* 时序信息 */
timing0: timing0 {
clock-frequency = <51200000>;/* LCD 像素时钟,单位 Hz */
hactive = <1024>; /* LCD X 轴像素个数 */
vactive = <600>; /* LCD Y 轴像素个数 */
hfront-porch = <160>; /* LCD hfp 参数 */
hback-porch = <140>; /* LCD hbp 参数 */
hsync-len = <20>; /* LCD hspw 参数 */
vback-porch = <20>; /* LCD vbp 参数 */
vfront-porch = <12>; /* LCD vfp 参数 */
vsync-len = <3>; /* LCD vspw 参数 */
hsync-active = <0>; /* hsync 数据线极性 */
vsync-active = <0>; /* vsync 数据线极性 */
de-active = <1>; /* de 数据线极性 */
pixelclk-active = <0>; /* clk 数据线先极性 */
};
};
};

3.2.3 背光节点#

LCD 背光要用到 PWM1,因此也要设置 PWM1 节点,如果背光只用简单的gpio,那么只能控制亮灭。无法控制亮度, 在imx6ull.dtsi文件中找到pwm1描述:

1
2
3
4
5
6
7
8
9
pwm1: pwm@02080000 {
compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
reg = <0x02080000 0x4000>;
interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_PWM1>,
<&clks IMX6UL_CLK_PWM1>;
clock-names = "ipg", "per";
#pwm-cells = <2>;
};

imx6ull 的 PWM 驱动文件为 drivers/pwm/pwm-imx.c,具体见Linux下PWM子系统 - fuzidage - 博客园 (cnblogs.com)

字符设备驱动-PWM子系统 | Hexo (fuzidage.github.io)。只要会使用pwm1即可,打开imx6ull-alientek-emmc.dts这块板子:

往pwm1添加如下内容,设置好pwm1对应的引脚:

1
2
3
4
5
&pwm1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm1>;
status = "okay";
};

也就是MX6UL_PAD_GPIO1_IO08__PWM1_OUT,将gpio1_8设成pwm输出。

3.2.3.1 backlight设置#

我们还需要一个节点来将 LCD 背光 PWM1_OUT 连接起来。这个节点就是 backlightbacklight 节点描述可以参考 Documentation/devicetree/indings/video/backlight/pwm-backlight.txt

1
2
3
4
5
6
7
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <6>;
status = "okay";
};
  1. 设置背 8 级背光(0~7),分别为 0、4、8、16、32、64、128、255,对应占空比为 0%、1.57%、3.13%、6.27%、12.55%、25.1%、50.19%、100%,如果嫌少的话可以自行添加一 些其他的背光等级值。

  2. 设置默认背光等级为 6,也就是 50.19%的亮度

backlight 节点说明:

1
2
3
4
5
6
7
1. 节点名称要为“backlight”
2. compatible 属性值要为“pwm-backlight”,因此可以通过在 Linux 内核中搜索 “ pwm-backlight ”
来查找PWM背光控制驱动程序 , 文件为 drivers/video/backlight/pwm_bl.c
3. pwms属性用于描述背光所使用的PWM以及PWM频率,比如本章我们要使用的pwm1, pwm 频率设置为 200Hz
4. brightness-levels 属性描述亮度级别,范围为 0~255,0 表示 PWM 占空比为 0%,也就 是亮度最低,
255 表示 100%占空比,也就是亮度最高
5. default-brightness-level 属性为默认亮度级别

4 LCD测试#

#

Linux 内核启动的时候可以选择显示小企鹅 logo,一般默认关闭。

1
2
3
4
5
6
-> Device Drivers
-> Graphics support
-> Bootup logo (LOGO [=y])
-> Standard black and white Linux logo
-> Standard 16-color Linux logo
-> Standard 224-color Linux logo

三个选项分别对应黑白、16 位、24 位色彩格式的 logo,我们把这三个都选 中,都编译进 Linux 内核里面。

4.2 LCD 作为终端控制台console#

  1. u-boot中bootargs设置

    setenv bootargs 'console=tty1 console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.250: /home/zuozhongkai/linux/nfs/rootfs ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0: off'

    第一次设置 console=tty1, 也就是设置 LCD 屏幕为控制台,第二遍又设置 console=ttymxc0,115200,也就是设置串口也作为控制台。大家重启开发板就会发 现 LCD 和串口都会显示 Linux 启动 log 信息。

  2. 修改/etc/inittab

    添加下面这行,

    tty1::askfirst:-/bin/sh

image

修改完成以后保存/etc/inittab 并退出,然后重启开发板,重启以后开发板 LCD 屏幕最后一 行会显示下面一行语句:

Please press Enter to activate this console

为什么请参考: linux内核-4.rootfs构建移植

Linux内核-rootfs构建移植 | Hexo (fuzidage.github.io)

大家也可以接上一个 USB 键盘,Linux 内核默认已经使能了 USB 键盘驱动 了,因此可以直接使用 USB 键盘Enter键。

当然也可以利用input子系统来用一个gpio按键做成Enter键。见: linux驱动-17-input子系统

字符设备驱动-input子系统 | Hexo (fuzidage.github.io)

4.3 LCD 背光调节命令#

前面背光设备树节点设置了 8 个等级的背光调节,可以设置为 0~7,我 们可以通过设置背光等级来实现 LCD 背光亮度的调节:

/sys/devices/platform/backlight/backlight/backlight

image

image

echo 7 > brightness设置亮度为100% echo 0 >brightness设置成熄灭背光。

4.4 LCD屏幕自动熄灭#

4.1.1 按键盘唤醒#

默认情况下 10 分钟以后 LCD 就会熄屏,这个并不是代码有问题,而是 Linux 内核设置的。按下回车键就会唤醒屏幕。

4.1.2 关闭10分钟自动熄屏#

  1. drivers/tty/vt/vt.c中, blankinterval 变量控制着 LCD 关闭时间,默认是 10*60,也就是 10 分钟。将 blankinterval 的值改为 0 即可关闭 10 分钟熄屏的功能。
1
2
181 static int blankinterval = 10*60;
182 core_param(consoleblank, blankinterval, int, 0444);
  1. echo -e '\033[9;0]' > /dev/tty0

4.5 测试代码#

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
#include <unistd.h>  
#include <stdio.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/ioctl.h>

#define PAUSE() \
do { \
printf("---------------press Enter key to continue!---------------\n"); \
getchar(); \
} while (0)

#if 1 // 32bits
#define RED 0xFFFF0000
#define GREEN 0xFF00FF00
#define BLUE 0xFF0000FF
#define YELLOW 0xFFFFFF00
#define WHITE 0xFFFFFFFF
#define BLACK 0xFF000000
void fill_color(uint32_t *fb_addr, uint32_t bit_map, int psize)
{
int i;
for(i=0; i<psize; i++) {
*fb_addr = bit_map;
fb_addr++;
}
}
#else // 16bits
#define RED 0xFC00
#define GREEN 0x83E0
#define BLUE 0x801F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
#define BLACK 0x8000
void fill_color(short *fb_addr, short bit_map, int psize)
{
int i;
for(i=0; i<psize; i++) {
*fb_addr = bit_map;
fb_addr++;
}
}
#endif

void _fb_get_info(int fp, struct fb_fix_screeninfo *finfo, struct fb_var_screeninfo *vinfo)
{
long screensize=0;

if(ioctl(fp, FBIOGET_FSCREENINFO, finfo)){
printf("Error reading fixed information/n");
exit(2);
}

if(ioctl(fp, FBIOGET_VSCREENINFO, vinfo)){
printf("Error reading variable information/n");
exit(3);
}

screensize = finfo->line_length * vinfo->yres;

printf("The ID=%s\n", finfo->id);
printf("The phy mem = 0x%x, total size = %d(byte)\n", finfo->smem_start, finfo->smem_len);
printf("line length = %d(byte)\n", finfo->line_length);
printf("xres = %d, yres = %d, bits_per_pixel = %d\n", vinfo->xres, vinfo->yres, vinfo->bits_per_pixel);
printf("xresv = %d, yresv = %d\n", vinfo->xres_virtual, vinfo->yres_virtual);
printf("vinfo.xoffset = %d, vinfo.yoffset = %d\n", vinfo->xoffset, vinfo->yoffset);
printf("vinfo.vmode is :%d\n", vinfo->vmode);
printf("finfo.ypanstep is :%d\n", finfo->ypanstep);
printf("vinfo.red.offset=0x%x\n", vinfo->red.offset);
printf("vinfo.red.length=0x%x\n", vinfo->red.length);
printf("vinfo.green.offset=0x%x\n", vinfo->green.offset);
printf("vinfo.green.length=0x%x\n", vinfo->green.length);
printf("vinfo.blue.offset=0x%x\n", vinfo->blue.offset);
printf("vinfo.blue.length=0x%x\n", vinfo->blue.length);
printf("vinfo.transp.offset=0x%x\n", vinfo->transp.offset);
printf("vinfo.transp.length=0x%x\n", vinfo->transp.length);
printf("Expected screensize = %d(byte), using %d frame\n", screensize, finfo->smem_len/screensize);
}

int main () {
int fp=0;
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
void *fbp = NULL;
char *test_fbp = NULL;
int x = 0, y = 0;
long location = 0;
int i;
int num = 2;
int pix_size=0;

fp = open("/dev/fb0", O_RDWR);

if(fp < 0) {
printf("Error : Can not open framebuffer device/n");
exit(1);
}

printf("-- Default fb info --\n");
_fb_get_info(fp, &finfo, &vinfo);

#if 1
vinfo.xres = 720;
vinfo.yres = 1280;
if(ioctl(fp, FBIOPUT_VSCREENINFO, &vinfo)){
printf("Error putting variable information/n");
exit(3);
}

printf("-- Updated fb info --\n");
_fb_get_info(fp, &finfo, &vinfo);
#endif

fbp = mmap(0, finfo.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fp, 0);
if (fbp == MAP_FAILED){
printf ("Error: failed to map framebuffer device to memory.\n");
exit (4);
}
printf("Get virt mem = %p\n", fbp);

pix_size = vinfo.xres * vinfo.yres;
/* using first frame, for FBIOPAN_DISPLAY
* 当刷新需要调用FBIOPAN_DISPLAY, 要告知驱动刷哪块帧, 用到下面两个参数
* 如果使用第二帧buffer -> vinfo.xoffset = 0; vinfo.yoffset = vinfo.yres;
*/
vinfo.xoffset = 0;
vinfo.yoffset = 0;

/* show color loop */
while(num--) {
printf("\ndrawing YELLOW......\n");
fill_color(fbp, YELLOW, pix_size);
//ioctl(fp, FBIOPAN_DISPLAY, &vinfo);
sleep(3);

printf("\ndrawing BLUE......\n");
fill_color(fbp, BLUE, pix_size);
//ioctl(fp, FBIOPAN_DISPLAY, &vinfo);
sleep(3);

printf("\ndrawing RED......\n");
fill_color(fbp, RED, pix_size);
//ioctl(fp, FBIOPAN_DISPLAY, &vinfo);
sleep(3);
PAUSE();
}
#if 1
/*这是你想画的点的位置坐标,(0,0)点在屏幕左上角*/
x = 10;
y = 10;
location = x * (vinfo.bits_per_pixel / 8) + y * finfo.line_length;
test_fbp = fbp + location;
printf("draw line.......\n");
for(i = 0; i < (vinfo.xres - x); i++)
*test_fbp++ = i+30;

//ioctl(fp, FBIOPAN_DISPLAY, &vinfo);
PAUSE();
#endif

munmap(fbp, finfo.smem_len); /*解除映射*/

close (fp);
return 0;
}

字符设备驱动-PWM子系统

1 pwm子系统框架#

image

用户态:基于sysfs操作pwm

内核态分为:

1
2
3
4
5
pwm core:pwm_chip的添加删除,pwm_class类pwm_chip/pwm_device的sysfs创建。

pwm driver:pwm_chip对象实例,注册添加到pwm core。
pwm_chip可以包含一个或多个pwm_device,每个pwm_device通过设置不同pwm_state来达到目的。

1.1 源码结构#

1
2
3
4
drivers/pwm/
core.c //pwm子系统核心。
sysfs.c//pwm子系统的pwm_class注册,pwm_chip属性,pwm_device属性等定义。
pwm-imx.c//imx的pwm_chip驱动。

image

我已经编译进vmlinux了,可以看到built-in.oMakefile如下:

image

Kconfig如下,我的内核.config配置选中了PWM和PWM_IMX,因此编译进了内核镜像。

image

image

1.2 数据结构#

1.2.1 pwm_chip#

是对一个pwm控制器的抽象。

1
2
3
4
5
6
7
8
9
10
11
12
struct pwm_chip {
struct device *dev;
const struct pwm_ops *ops;
int base;
unsigned int npwm;//pwm控制器的pwm数量。
struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
const struct of_phandle_args *args);
unsigned int of_pwm_n_cells;
/* only used internally by the PWM framework */
struct list_head list;
struct pwm_device *pwms;
}; //include/linux/pwm.h

1.2.2 pwm_ops#

pwm控制器的操作接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct pwm_ops {
int (*request)(struct pwm_chip *chip, //请求 PWM
struct pwm_device *pwm);
void (*free)(struct pwm_chip *chip, //释放 PWM
struct pwm_device *pwm);
int (*config)(struct pwm_chip *chip, //配置 PWM 周期和占空比
struct pwm_device *pwm,
int duty_ns, int period_ns);
int (*set_polarity)(struct pwm_chip *chip, //设置 PWM 极性
struct pwm_device *pwm,
enum pwm_polarity polarity);
int (*enable)(struct pwm_chip *chip, //使能 PWM
struct pwm_device *pwm);
void (*disable)(struct pwm_chip *chip, //关闭 PWM
struct pwm_device *pwm);
struct module *owner;
};

1.2.3 pwm_state#

pwm_state就是控制占空比控制转速,亮度参数。

1
2
3
4
5
6
7
struct pwm_state {
unsigned int period; //pwm的周期,单位ns。
unsigned int duty_cycle; //占空比duty_cycle,单位ns。
enum pwm_polarity polarity;//PWM_POLARITY_NORMAL表示高电平持续duty_cycle,
//然后是低电平持续剩余时间。PWM_POLARITY_INVERSED表示低电平持续duty_cycle,然后是高电平持续剩余时间。
bool enabled; //是否使能
};

1.3 API#

api声明见linux\include\linux\pwm.h,实现linux\drivers\pwm\core.c

1.3.1 pwmchip_add#

pwm子系统注册一个pwm_chip

int pwmchip_add(struct pwm_chip *chip);

1
2
3
4
5
6
7
pwmchip_add
pwmchip_add_with_polarity
->pwm_ops_check //检查pwm_ops是否支持apply等。
->alloc_pwms //为pwm_chip的pwm_device分配allocated_pwms。
->//初始化每个pwm_device,并加入pwm_tree。
->pwmchip_sysfs_export
->pwmchip_sysfs_export //创建pwm_class类设备pwmchpX,位于/sys/class/pwm/pwmchipX。

image

1.3.2 pwmchip_remove#

int pwmchip_remove(struct pwm_chip *chip);

删除一个pwm_chip

image

1.3.3 pwm_request#

请求 PWM。

struct pwm_device *pwm_request(int pwm, const char *label)

image

image

可以看到就是调用具体pwm示例的request函数。

1.3.4 pwm_free#

释放 PWM。

void pwm_free(struct pwm_device *pwm)

image

1.3.5 pwm_config#

配置 PWM 周期和占空比,操作具体pwm实例。

int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns)

image

1.3.6 pwm_set_polarity#

设置 PWM 极性。

int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity)

image

1.3.6 pwm_enable#

image

1.3.7 pwm_disable#

image

1.3.31.3.7本质都是调用pwm_ops

2 pwm驱动实例#

I.MX6ULL 有 8 路 PWM 控制器。这 8 路 PWM 都属于I.MX6ULL 的 AIPS-1域,但是在设备树imx6ull.dtsi中 分为了两部分,PWM1~PWM4 在一起,PWM5~PWM8 在一起。以pwm3为例:

2.1 dts描述#

打开imx6ull.dtsi:可以看到pwm3的描述:

1
2
3
4
5
6
7
8
9
pwm3: pwm@02088000 {
compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
reg = <0x02088000 0x4000>;
interrupts = <GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_PWM3>,
<&clks IMX6UL_CLK_PWM3>;
clock-names = "ipg", "per";
#pwm-cells = <2>;
};

关 于I.MX6ULLPWM dts节点描述参考对应的绑定文档 : Documentation/devicetree/bindings/pwm/ imx-pwm.txt

GPIO1_IO04 这里作为PWM3的输出引脚,所以我们需要在设备树里面添加 GPIO1_IO04 的引脚信息以及PWM3控制器对应的节点信息:

打开 imx6ull-alientek-emmc.dts, 添加iomux配置信息:

1
2
3
pinctrl_pwm3: pwm3grp {
fsl,pins = <MX6UL_PAD_GPIO1_IO04__PWM3_OUT 0x110b0>;
};
1
2
3
4
5
&pwm3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm3>;
status = "okay";
};

2.2 使能PWM驱动#

image

.config中已经使能了,但是为了学习, 我们还是需要知道怎么使能。

image

1
2
3
-> Device Drivers
-> Pulse-Width Modulation (PWM) Support
-> <*> i.MX PWM support

2.3 PWM 背光设置#

linux 内核里面关于 backlight(背光)的绑定文档,路径为 Documentation/devicetree/bindings/video/backlight/pwm-backlight.txt,此文档描述了如何创建 backlight 节点来使用linux内核自带的pwm背光驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
compatible:内容必须为“pwm-backlight”,通过这个可以匹配到内核自带的 PWM 背光驱
动,驱动文件为 drivers/video/backlight/pwm_bl.c,这里就不去分析驱动源码了。

pwms:此属性指定背光使用哪一路 PWM,以及 PWM 相关的属性。

brightness-levels:背光等级数组,范围 0~255,对应占空比为 0%~100%。数组内的值必须
0 开始,也就是 0%占空比,最后一个值必须是 255,也就是 100%占空比。数组中间值的个
数以及值大小可以自行定义。

default-brightness-level:默认的背光等级,也就是 brightness-levels 属性中第几个值,注意
这里是数索引编号,不是具体的数值!

power-supply:支持的电压,此属性可以不需要
1
2
3
4
5
6
7
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>;// PWM 周期为 5000000ns,频率为 200Hz
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <7>;
status = "okay";
};

2.4 驱动源码分析#

2.4.1 probe过程#

2.4.1.1 imx_chip#

image

定义了imx_chip,包装了pwm_chip结构。

image

probe时,先分配内存,从dts获取per, ipg等时钟信息,设置pwm的ops为imx_pwm_ops。最后pwmchip_add注册进pwm子系统。注意这里有一个of_id->data,对应如下:可以看到有v1,v2两2版本,到时候会被ops中的函数调用。

image

同理,驱动卸载最后调用pwmchip_remove注销pwm。

image

2.4.2 imx_pwm_ops#

image

2.4.2.1 imx_pwm_config#

配置 PWM 周期和占空比。根据dts描述(”imx27-pwm“)我们使用的是v2。

image

image

image

PWMv2会有4 word的采样fifo, 为了避免采样FIFO溢出,当pwm关闭时,对所有采样FIFO进行软件复位。当pwm使能后处于工作中,要等待完整的 PWM 周期以保证pwm空闲。

然后设置PWM 周期period_cycles,和占空比duty_cycles

最后调用writel写入寄存器。

image

2.4.2.2 imx_pwm_enable#

控制MX3_PWMCR寄存器,使能关闭开关。

image

image

2.4.2.3 imx_pwm_disable#

image

3 基于pwm sysfs测试#

alpha开发板 JP2 排针上的 GPIO_4(GPIO1_IO04)引脚连接到 示波器上。等下看pwm信号效果。

可以看到一共8 路 PWM 控制器:

image

我们使用的pwm3,对应出 pwmchip2, 导出chip2通道的0设备文件:

1
echo 0 > /sys/class/pwm/pwmchip2/export

执行完成会在pwmchip2 目录下生成一个名为“pwm0”的子目录:

image

1
2
3
echo 1 > /sys/class/pwm/pwmchip2/pwm0/enable #使能pwm3
echo 50000 > /sys/class/pwm/pwmchip2/pwm0/period #设置周期值,单位为 ns,比如 20KHz 频率的周期就是 50000ns
echo 10000 > /sys/class/pwm/pwmchip2/pwm0/duty_cycle #20%占空比

image

总结:

1
2
3
4
5
6
导出chip1通道的0设备文件:echo 0 > /sys/class/pwm/pwmchip1/export
配置chip1通道0的周期: echo 10000000 > /sys/class/pwm/pwmchip1 /pwm0/period
配置chip1通道0的占空比:echo 4000000 >/sys/class/pwm/pwmchip1/pwm0/duty_cycle
配置片chip通道0使能: echo 1 > /sys/class/pwm/pwmchip1/pwm0/enable
配置片chip通道0禁能: echo 0 > /sys/class/pwm/pwmchip1/pwm0/enable
取消导出片chip通道0设备文件: echo 0 >/sys/class/pwm/pwmchip1/unexport

字符设备驱动-input子系统

1 input 子系统介绍#

按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux 内核为此专门做了一个叫做 input子系统的框架来处理输入事件。
image
input 子系统分为 input 驱动层、input 核心层、input 事件处理层,最终给用户空间提供可访问的设备节点。
驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理。
事件层:主要和用户空间进行交互。

1.0 数据结构#

1.0.1 input_dev#

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
1:  struct input_dev {
2: const char *name; //设备名
3: const char *phys; // 设备在系统中路径
4: const char *uniq;
5: struct input_id id; //与input_handler匹配用的id;

9: unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; //设备所支持事件类型主要有EV_SYNC,EV_KEY,EV_REL,EV_ABS
10: unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];// 按键所对应的位图
11: unsigned long relbit[BITS_TO_LONGS(REL_CNT)];// 相对坐标对应位图
12: unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];// 绝对坐标对应位图
13: unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];//支持其它事件
14: unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];//支持led事件
15: unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];//支持声音事件
16: unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];//支持受力事件
17: unsigned long swbit[BITS_TO_LONGS(SW_CNT)];//支持开关机事件
18:
19: unsigned int hint_events_per_packet;
21: unsigned int keycodemax;
22: unsigned int keycodesize;
23: void *keycode;
24:
25: int (*setkeycode)(struct input_dev *dev,
26: const struct input_keymap_entry *ke,
27: unsigned int *old_keycode);
28: int (*getkeycode)(struct input_dev *dev,
29: struct input_keymap_entry *ke);
30:
31: struct ff_device *ff;
33: unsigned int repeat_key;//最近一次的按键值

1.1 input 驱动编写流程#

1.1.0 input类的建立和proc建立#

drivers/input/input.c就是input子系统的核心层,此文件里面有如下代码:

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
struct class input_class = {
.name = "input",
.devnode = input_devnode,
};
.....
static int __init input_init(void)
{
int err;
err = class_register(&input_class);
if (err) {
pr_err("unable to register input_dev class\n");
return err;
}

err = input_proc_init();
if (err)
goto fail1;

err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),
INPUT_MAX_CHAR_DEVICES, "input");
if (err) {
pr_err("unable to register char major %d", INPUT_MAJOR);
goto fail2;
}

return 0;

fail2: input_proc_exit();
fail1: class_unregister(&input_class);
return err;
}

注册一个 input 类,这样系统启动以后就会在/sys/class 目录下有一个 input 子目录:
image
创建proc/input信息,申请主设备号为 INPUT_MAJOR, INPUT_MAJOR 定义在 include/uapi/linux/major.h:
#define INPUT_MAJOR 13
因此,input 子系统的所有设备主设备号都为 13,我们在使用 input 子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个 input_device 即可。

1.1.1 注册 input_dev#

使用 input 子系统的时候我们只需要注册一个 input 设备即可,input_dev 结构体表示 input设备,此结构体定义在 include/linux/input.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct input_dev {
const char *name;
const char *phys;
const char *uniq;
struct input_id id;
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /* 事件类型的位图 */
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /* 按键值的位图 */
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /* 相对坐标的位图 */
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /* 绝对坐标的位图 */
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; /* 杂项事件的位图 */
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /*LED 相关的位图 */
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];/* sound 有关的位图 */
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; /* 压力反馈的位图 */
unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; /*开关状态的位图 */
.....
bool devres_managed;
};

evbit 表示输入事件类型:

1
2
3
4
5
6
7
8
9
10
11
12
#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /* 按键事件 */
#define EV_REL 0x02 /* 相对坐标事件 */
#define EV_ABS 0x03 /* 绝对坐标事件 */
#define EV_MSC 0x04 /* 杂项(其他)事件 */
#define EV_SW 0x05 /* 开关事件 */
#define EV_LED 0x11 /* LED */
#define EV_SND 0x12 /* sound(声音) */
#define EV_REP 0x14 /* 重复事件 */
#define EV_FF 0x15 /* 压力事件 */
#define EV_PWR 0x16 /* 电源事件 */
#define EV_FF_STATUS 0x17 /* 压力状态事件 */

evbit、keybit、relbit 等等都是存放不同事件对应的值。比如我们本章要使用按键事件,因此要用到 keybit,keybit 就是按键事件使用的位图,Linux 内核定义了很多按键值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
#define KEY_3 4
#define KEY_4 5
#define KEY_5 6
#define KEY_6 7
#define KEY_7 8
#define KEY_8 9
#define KEY_9 10
#define KEY_0 11
......
#define BTN_TRIGGER_HAPPY39 0x2e6
#define BTN_TRIGGER_HAPPY40 0x2e7

我们可以将开发板上的按键值设置为任意一个,比如就叫KEY_0

申请input内存:
struct input_dev *input_allocate_device(void);

释放input内存:
void input_free_device(struct input_dev *dev);

注册input设备:
int input_register_device(struct input_dev *dev);

注销input设备:
void input_unregister_device(struct input_dev *dev);

1.1.1.1 input_dev 注册过程#

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
struct input_dev *inputdev; /* input 结构体变量 */

static int __init xxx_init(void)
{
inputdev = input_allocate_device(); /* 申请 input_dev */
inputdev->name = "test_inputdev"; /* 设置 input_dev 名字 */

/*********第一种设置事件和事件值的方法***********/
__set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
__set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
__set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
/************************************************/

/*********第二种设置事件和事件值的方法***********/
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
T_MASK(EV_REP);
keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |=
T_MASK(KEY_0);
/************************************************/

/*********第三种设置事件和事件值的方法***********/
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
T_MASK(EV_REP);
input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
/************************************************/
input_register_device(inputdev);
return 0;
}
static void __exit xxx_exit(void)
{
input_unregister_device(inputdev); /* 注销 input_dev */
input_free_device(inputdev); /* 删除 input_dev */
}

1.1.2 中断上报输入事件#

当输入设备中断到来,我们需要上报input输入事件给linux内核的input核心层,比如按键:我们需要在按键中断处理函数,或者消抖定时器中断函数中将按键值上报给 Linux 内核,这样 Linux 内核才能获取到正确的输入值。
不同的事件,上报事件的 API 函数不一样:
input_event:

1
2
3
4
5
6
7
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)
/*
dev:需要上报的 input_dev。
type: 上报的事件类型,比如 EV_KEY。
code:事件码,也就是我们注册的按键值,比如 KEY_0、KEY_1 等等。
value:事件值,比如 1 表示按键按下,0 表示按键松开。
*/

当然linux系统帮我们也封装了一层api去使用:
input_report_key:

1
2
3
static inline void input_report_key(struct input_dev *dev,unsigned int code, int value){
input_event(dev, EV_KEY, code, !!value);
}

同样的还有一些其他的事件上报函数:

1
2
3
4
5
void input_report_rel(struct input_dev *dev, unsigned int code, int value)
void input_report_abs(struct input_dev *dev, unsigned int code, int value)
void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
void input_report_switch(struct input_dev *dev, unsigned int code, int value)
void input_mt_sync(struct input_dev *dev)

我们上报事件以后还需要使用input_sync函数来告诉 Linux 内核 input 子系统上报结束,input_sync 函数本质是上报一个同步事件:
void input_sync(struct input_dev *dev);
举个例子,按键中断服务函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 用于按键消抖的定时器服务函数 */
void timer_function(unsigned long arg) {
unsigned char value;
value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */
if(value == 0){ /* 按下按键 */
/* 上报按键值 */
input_report_key(inputdev, KEY_0, 1); /* 最后一个参数 1,按下 */
input_sync(inputdev); /* 同步事件 */
} else { /* 按键松开 */
input_report_key(inputdev, KEY_0, 0); /* 最后一个参数 0,松开 */
input_sync(inputdev); /* 同步事件 */
}
}

1.1.3 input_event 结构体#

include/uapi/linux/input.h 文件中:
image

type:事件类型,比如 EV_KEY,表示此次事件为按键事件,此成员变量为 16 位。
code:事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,如:KEY_0、KEY_1等等这些按键。此成员变量为 16 位。
value:值,比如 EV_KEY 事件中 value 就是按键值,表示按键有没有被按下,如果为 1 的话说明按键按下,如果为 0 的话说明按键没有被按下或者按键松开了

input_envent 这个结构体非常重要,用户态的应用程序也是通过 input_event 来获取到具体的输入事件或相关的值,比如按键值等。

2 input子系统示例#

还是以之前的按键来举例,利用input子系统来做一个按键驱动程序:

点击查看代码
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
#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 <linux/input.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define KEYINPUT_CNT 1 /* 设备号个数 */
#define KEYINPUT_NAME "keyinput" /* 名字 */
#define KEY0VALUE 0X01 /* KEY0按键值 */
#define INVAKEY 0XFF /* 无效的按键值 */
#define KEY_NUM 1 /* 按键数量 */

struct irq_keydesc {
int gpio; /* gpio */
int irqnum; /* 中断号 */
unsigned char value; /* 按键对应的键值 */
char name[10]; /* 名字 */
irqreturn_t (*handler)(int, void *); /* 中断服务函数 */
};

struct keyinput_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
struct timer_list timer;/* 定义一个定时器*/
struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述数组 */
unsigned char curkeynum; /* 当前的按键号 */
struct input_dev *inputdev; /* input结构体 */
};

struct keyinput_dev keyinputdev;

static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct keyinput_dev *dev = (struct keyinput_dev *)dev_id;

dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定时 */
return IRQ_RETVAL(IRQ_HANDLED);
}
void timer_function(unsigned long arg)
{
unsigned char value;
unsigned char num;
struct irq_keydesc *keydesc;
struct keyinput_dev *dev = (struct keyinput_dev *)arg;

num = dev->curkeynum;
keydesc = &dev->irqkeydesc[num];
value = gpio_get_value(keydesc->gpio); /* 读取IO值 */
if(value == 0){/* 按下按键 */
/* 上报按键值 */
//input_event(dev->inputdev, EV_KEY, keydesc->value, 1);
/* 最后一个参数表示按下还是松开,1为按下,0为松开 */
input_report_key(dev->inputdev, keydesc->value, 1);
input_sync(dev->inputdev);
} else {/* 按键松开 */
//input_event(dev->inputdev, EV_KEY, keydesc->value, 0);
input_report_key(dev->inputdev, keydesc->value, 0);
input_sync(dev->inputdev);
}
}
int keyio_init(void)
{
unsigned char i = 0;
char name[10];
int ret = 0;

keyinputdev.nd = of_find_node_by_path("/key");
if (keyinputdev.nd== NULL){
printk("key node not find!\r\n");
return -EINVAL;
}

/* 提取GPIO */
for (i = 0; i < KEY_NUM; i++) {
keyinputdev.irqkeydesc[i].gpio = of_get_named_gpio(keyinputdev.nd ,"key-gpio", i);
if (keyinputdev.irqkeydesc[i].gpio < 0) {
printk("can't get key%d\r\n", i);
}
}

/* 初始化key所使用的IO,并且设置成中断模式 */
for (i = 0; i < KEY_NUM; i++) {
memset(keyinputdev.irqkeydesc[i].name, 0, sizeof(name)); /* 缓冲区清零 */
sprintf(keyinputdev.irqkeydesc[i].name, "KEY%d", i); /* 组合名字 */
gpio_request(keyinputdev.irqkeydesc[i].gpio, name);
gpio_direction_input(keyinputdev.irqkeydesc[i].gpio);
keyinputdev.irqkeydesc[i].irqnum = irq_of_parse_and_map(keyinputdev.nd, i);
}
/* 申请中断 */
keyinputdev.irqkeydesc[0].handler = key0_handler;
keyinputdev.irqkeydesc[0].value = KEY_0;

for (i = 0; i < KEY_NUM; i++) {
ret = request_irq(keyinputdev.irqkeydesc[i].irqnum, keyinputdev.irqkeydesc[i].handler,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
keyinputdev.irqkeydesc[i].name, &keyinputdev);
if(ret < 0){
printk("irq %d request failed!\r\n", keyinputdev.irqkeydesc[i].irqnum);
return -EFAULT;
}
}

/* 创建定时器 */
init_timer(&keyinputdev.timer);
keyinputdev.timer.function = timer_function;

/* 申请input_dev */
keyinputdev.inputdev = input_allocate_device();
keyinputdev.inputdev->name = KEYINPUT_NAME;
#if 0
/* 初始化input_dev,设置产生哪些事件 */
__set_bit(EV_KEY, keyinputdev.inputdev->evbit); /* 设置产生按键事件 */
__set_bit(EV_REP, keyinputdev.inputdev->evbit); /* 重复事件,比如按下去不放开,就会一直输出信息*/

/* 初始化input_dev,设置产生哪些按键 */
__set_bit(KEY_0, keyinputdev.inputdev->keybit);
#endif

#if 0
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
#endif

keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);

/* 注册输入设备 */
ret = input_register_device(keyinputdev.inputdev);
if (ret) {
printk("register input device failed!\r\n");
return ret;
}
return 0;
}
static int __init keyinput_init(void)
{
keyio_init();
return 0;
}
static void __exit keyinput_exit(void)
{
unsigned int i = 0;
del_timer_sync(&keyinputdev.timer); /* 删除定时器 */

for (i = 0; i < KEY_NUM; i++) {
free_irq(keyinputdev.irqkeydesc[i].irqnum, &keyinputdev);
}
input_unregister_device(keyinputdev.inputdev);
input_free_device(keyinputdev.inputdev);
}

module_init(keyinput_init);
module_exit(keyinput_exit);

2.1 定义input_dev#

image

2.2 初始话input_dev#

image

  1. input_allocate_device分配内存
  2. 设置事件和事件值,input_event类型是EV_KEY,键值KEY_0, evbitEV_KEY | EV_REP
  3. 注册input设备

2.3 中断上报输入事件#

image

2.4 释放input_dev#

image

3 APP测试#

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 "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
#include <linux/input.h>
static struct input_event inputevent;
int main(int argc, char *argv[]){
int fd;
int err = 0;
char *filename;
filename = argv[1];
if(argc != 2) {
printf("Error Usage!\r\n");
return -1;
}

fd = open(filename, O_RDWR);
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
while (1) {
err = read(fd, &inputevent, sizeof(inputevent));
if (err > 0) { /* 读取数据成功 */
switch (inputevent.type) {
case EV_KEY:
if (inputevent.code < BTN_MISC) { /* 键盘键值 */
printf("key %d %s\r\n", inputevent.code,
inputevent.value ? "press" : "release");
} else {
printf("button %d %s\r\n", inputevent.code,
inputevent.value ? "press" : "release");
}
break;
/* 其他类型的事件,自行处理 */
case EV_REL:
break;
case EV_ABS:
break;
}
} else
printf("读取数据失败\r\n");
}
return 0;
}

当我们向 Linux 内核成功注册 input_dev 设备以后,会在/dev/input 目录下生成一个名为“eventX(X=0….n)”的文件,这个/dev/input/eventX 就是对应的 input 设备文件。

测试:
在加载keyinput.ko驱动模块之前,先看一下/dev/input 目录下都有哪些文件:
image
modprobe keyinput.ko 可以看到多一个input1,就是我们刚创建的设备节点:
image
运行app:
./keyinputApp /dev/input/event1
image
可以看出,当我们按下或者释放开发板上的按键以后都会在终端上输出相应的内容,提示我们哪个按键按下或释放了,在 Linux 内核中 KEY_011
另外,我们也可以不用keyinputApp来测试驱动,可以直接使用hexdump命令来查看/dev/input/event1 文件内容,输入如下命令:
hexdump /dev/input/event1
这就是input_event 类型的原始事件数据值:
image
含义如下:
image
type 为事件类型,EV_KEY 事件值为 1,EV_SYN 事件值为0。因此第 1 行表示 EV_KEY 事件,第 2 行表示 EV_SYN 事件。code 为事件编码,也就是按键号,KEY_0 这个按键编号为 11,对应的十六进制为 0xb,因此第1 行表示 KEY_0 这个按键事件,最后的 value 就是按键值,为 1 表示按下,为 0 的话表示松开。
综上所述,上述原始事件值含义如下:
第 1 行,按键(KEY_0)按下事件。
第 2 行,EV_SYN 同步事件,因为每次上报按键事件以后都要同步的上报一个 EV_SYN 事件。
第 3 行,按键(KEY_0)松开事件。
第 4 行,EV_SYN 同步事件,和第 2 行一样。

4 Linux input子系统补充-Linux 自带按键驱动#

Linux 内核也自带了 KEY 驱动,如果要使用内核自带的 KEY 驱动的话需要配置 Linux 内核,不过 Linux 内核一般默认已经使能了 KEY 驱动。

1
2
3
4
5
-> Device Drivers
-> Input device support
-> Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y])
-> Keyboards (INPUT_KEYBOARD [=y])
->GPIO Buttons

image

选中以后就会在.config 文件中出现“CONFIG_KEYBOARD_GPIO=y”这一行,Linux 内核
就会根据这一行来将 KEY 驱动文件编译进 Linux 内核。Linux 内核自带的 KEY 驱动文件为
drivers/input/keyboard/gpio_keys.cgpio_keys.c 采用了 platform 驱动框架,在 KEY 驱动上使用
了 input 子系统实现。

4.1 gpio_keys源码分析#

image
image

要使用 Linux 内 核 自 带 的 按 键 驱 动 程 序 很 简 单 , 只 需 要 根 据Documentation/devicetree/bindings/input/gpio-keys.txt 这个文件在设备树中添加指定的设备节点即可,节点要求如下:

1
2
3
4
5
6
7
8
①、节点名字为“gpio-keys”。
②、gpio-keys 节点的 compatible 属性值一定要设置为“gpio-keys”。
③、所有的 KEY 都是 gpio-keys 的子节点,每个子节点可以用如下属性描述自己:
gpios:KEY 所连接的 GPIO 信息。
interrupts:KEY 所使用 GPIO 中断信息,不是必须的,可以不写。
label:KEY 名字
linux,code:KEY 要模拟的按键,也就是示例代码
④、如果按键要支持连按的话要加入 autorepeat。

打开 imx6ull-alientek-emmc.dts定义如下:

1
2
3
4
5
6
7
8
9
10
11
gpio-keys {
compatible = "gpio-keys";
#address-cells = <1>;
#size-cells = <0>;
autorepeat;
key0 {
label = "GPIO Key Enter";
linux,code = <KEY_ENTER>;
gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
};
};

ALPHA 开发板 KEY 按键信息,名字设置为“GPIO Key Enter”,这里我们将开发板上的 KEY 按键设置为“EKY_ENTER”这个按键,也就是回车键,效果和键盘上的回车键一样。

4.1.1 gpio_keys_probe分析#

image
可以看到就是对input子系统的调用封装,实例化了一个利用input子系统写的驱动程序。
调用 gpio_keys_get_devtree_pdata 函数从设备树中获取到 KEY 相关的设备节点信息。
使用 devm_input_allocate_device 函数申请 input_dev。
初始化 input_dev
设置 input_dev 事件,这里设置了 EV_REP 事件.
调用 gpio_keys_setup_key 函数继续设置 KEY,此函数会设置 input_dev 的EV_KEY 事件已经事件码(也就是 KEY 模拟为哪个按键)
调用 input_register_device 函数向 Linux 系统注册 input_dev

4.1.1.1 gpio_keys_setup_key#

image

调用 input_set_capability 函数设置 EV_KEY 事件以及 KEY 的按键类型,也就是 KEY 作为哪个按键?我们会在设备树里面设置指定的 KEY 作为哪个按键.

4.1.1.2 gpio_keys_irq_isr#

当dts对应的按键按下后,中断进行响应,函数如下:
image
可以看到同理还是调用input_event, input_sync进行上报事件。

image-20240825144156414

4.2 测试验证#

烧录新的dtb和kernel进去,可以看出存在 event1 这个文件,这个文件就是 KEY 对应的设备文件,使用
hexdump 命令来查看/dev/input/event1 文件:
hexdump /dev/input/event1
image
大家如果发现按下 KEY 按键以后没有反应,那么请检查一下三方面:
①、是否使能 Linux 内核 KEY 驱动。
②、设备树中 gpio-keys 节点是否创建成功。
③、在设备树中是否有其他外设也使用了 KEY 按键对应的 GPIO,但是我们并没有删除掉这些外设信息。检查 Linux 启动 log 信息,看看是否有类似下面这条信息:
gpio-keys gpio_keys:Failed to request GPIO 18, error -16

5 Linux input子系统-电容触摸屏应用#

5.1 FT5426电容触摸屏简介#

5.1.1 特性#

image

特性如下:

1
2
3
4
5
1. 自动模式切换。
2. 100hz采样率
3. 自动校准
4. i2c接口,速率高达400k
5. 12位ADC精度转换

image

触摸 IC 提供了中断信号引脚(INT),可以通过中断来获取触摸信息。电容触摸屏得到的是触摸位置绝对信息以及触摸屏是否有按下。

5.1.2 i2c传输格式#

image

image

image

可以看到slave addr是7位,第8位表示方向。数据是每次传输1byte。

5.1.3 上电及复位时序#

image

image

image

5.1.4 寄存器描述#

image

5.2 多点触摸(MT)协议#

多点电容触摸屏协议,文档路径为:Documentation/input/multitouch-protocol.txt。

老版本的 2.x 版本 linux 内核是不支持多点电容触摸的(Multi-touch,简称 MT),MT 协议是后面加入 的。

MT 协议被分为两种类型,Type ATypeB,这两种类型的区别如下:

1
2
Type A:适用于触摸点不能被区分或者追踪,此类型的设备上报原始数据(此类型在实际使用中非常少)。
Type B:适用于有硬件追踪并能区分触摸点的触摸设备,此类型设备通过 slot 更新某一个 触摸点的信息,FT5426 就属于此类型,一般的多点电容触摸屏 IC 都有此能力。

5.2.1 ABS_MT 事件#

ABS_MT 事件是用于多点触摸的,ABS_MT 事件定义在文件 include/uapi/linux/input.h 中, 上报给 linux 内核。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define ABS_MT_SLOT 0x2f /* MT slot being modified */
#define ABS_MT_TOUCH_MAJOR 0x30 /* Major axis of touching ellipse */
#define ABS_MT_TOUCH_MINOR 0x31 /* Minor axis (omit if circular) */
#define ABS_MT_WIDTH_MAJOR 0x32 /* Major axis of approaching ellipse */
#define ABS_MT_WIDTH_MINOR 0x33 /* Minor axis (omit if circular) */
#define ABS_MT_ORIENTATION 0x34 /* Ellipse orientation */
#define ABS_MT_POSITION_X 0x35 /* Center X touch position */
#define ABS_MT_POSITION_Y 0x36 /* Center Y touch position */
#define ABS_MT_TOOL_TYPE 0x37 /* Type of touching device */
#define ABS_MT_BLOB_ID 0x38 /* Group a set of packets as a blob */
#define ABS_MT_TRACKING_ID 0x39 /* Unique ID of initiated contact */
#define ABS_MT_PRESSURE 0x3a /* Pressure on contact area */
#define ABS_MT_DISTANCE 0x3b /* Contact hover distance */
#define ABS_MT_TOOL_X 0x3c /* Center X tool position */
#define ABS_MT_TOOL_Y 0x3d /* Center Y tool position */

ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 用来上 报触摸 点的 (X,Y) 坐标 信息。ABS_MT_SLOT 用来上 报触摸点 ID , 对 于 Type B 类型的设备,需要用到 ABS_MT_TRACKING_ID 事件来区分触摸点。

5.2.1.1 input_mt_sync-隔离typeA类的不同触摸点#

对于 Type A 类型的设备,通过 input_mt_sync()函数来隔离不同的触摸点数据信息。nput_mt_sync() 函数会触发 SYN_MT_REPORT 事件,此事件会通知接收者获取当前触摸数据,并且准备接收 下一个触摸点数据。

void input_mt_sync(struct input_dev *dev);

5.2.1.2 input_mt_slot-区分typeB类的不同触摸点#

对于Type B类型的设备,上报触摸点信息的时候需要通过 input_mt_slot()函数区分是哪一 个触摸点。

void input_mt_slot(struct input_dev *dev, int slot);

第一个参数是 input_dev 设备,第二个参数 slot 用于指定当前上报的是 哪个触摸点信息。input_mt_slot()函数会触发 ABS_MT_SLOT 事件,此事件会告诉接收者当前 正在更新的是哪个触摸点(slot)的数据。

5.2.1.3 input_sync-结束上报#

不管是哪个类型的设备,最终都要调用 input_sync()函数来标识多点触摸信息传输完成,告 诉接收者处理之前累计的所有消息,并且准备好下一次接收。

可以通过 slot 的 ABS_MT_TRACKING_ID 来新增、替换或删除触摸点。一个非负数 的 ID 表示一个有效的触摸点,-1 这个 ID 表示未使用 slot。一个以前不存在的 ID 表示这是一个 新加的触摸点,一个 ID 如果再也不存在了就表示删除了。上报 SYN_REPORT 事件。

5.2.1.4 input_report_abs-上报坐标#

上报触摸屏原始数据。

void input_report_abs(struct input_dev *dev, unsigned int code, int value);

5.2.2 Type A 触摸点信息上报流程#

1
2
3
4
5
6
7
8
ABS_MT_POSITION_X x[0]//通过 ABS_MT_POSITION_X 事件上报第一个触摸点的 X 坐标数据,通过input_report_abs 函数实现,下面同理
ABS_MT_POSITION_Y y[0]//上报第一个触摸点的 Y 坐标数据
SYN_MT_REPORT //上报 SYN_MT_REPORT 事件,通过调用 input_mt_sync 函数来实现。

ABS_MT_POSITION_X x[1]//通过 ABS_MT_POSITION_X 事件上报第二个触摸点的 X 坐标数据
ABS_MT_POSITION_Y y[1]//上报第二个触摸点的 Y 坐标数据
SYN_MT_REPORT //上报 SYN_MT_REPORT 事件,通知接收者获取坐标
SYN_REPORT //最后,上报 SYN_REPORT 事件,通过调用 input_sync 函数实现。

Linux 内核里面也有 Type A 类型的多点触摸驱动,如 st2332.c

image-20240825151728439

5.2.3 Type B 触摸点信息上报流程#

对于Type B类型的设备,发送触摸点信息的时序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ABS_MT_SLOT 0 /* 上报 ABS_MT_SLOT 事件。
每次上报一个触摸点坐标之前要先使用input_mt_slot函数上报当前触摸点SLOT,触摸点的SLOT其实就是触摸点ID,需要由触摸 IC 提供
*/
ABS_MT_TRACKING_ID 45 /*根据 Type B 的要求,每个 SLOT 必须关联一个 ABS_MT_TRACKING_ID,通过
修改 SLOT 关联的 ABS_MT_TRACKING_ID 来完成对触摸点的添加、替换或删除。具体用到
的函数就是 input_mt_report_slot_state,如果是添加一个新的触摸点,那么此函数的第三个参数
active 要设置为 true,linux 内核会自动分配一个 ABS_MT_TRACKING_ID 值,不需要用户去指
定具体的 ABS_MT_TRACKING_ID 值。
*/
ABS_MT_POSITION_X x[0] //上报触摸点 0 的 X 轴坐标,使用函数 input_report_abs 来完成
ABS_MT_POSITION_Y y[0] //上报触摸点 0 的 y 轴坐标

ABS_MT_SLOT 1 //同理, 上报 ABS_MT_SLOT 事件。
ABS_MT_TRACKING_ID 46
ABS_MT_POSITION_X x[1] //坐标1的x
ABS_MT_POSITION_Y y[1] //坐标1的y
SYN_REPORT //最后,上报 SYN_REPORT 事件,通过调用 input_sync 函数实现。

当一个触摸点移除以后,同样需要通过 SLOT 关联的ABS_MT_TRACKING_ID来处理:

1
2
3
4
5
ABS_MT_TRACKING_ID -1 /*当一个触摸点(SLOT)移除以后,需要通过 ABS_MT_TRACKING_ID 事件发送一
个-1 给内核。方法很简单,同样使用 input_mt_report_slot_state 函数来完成,只需要将此函数的
第三个参数 active 设置为 false 即可,不需要用户手动去设置-1。
*/
SYN_REPORT

Linux 内核里面有大量的 Type B 类型的多点触摸驱动程序,drivers/input/touchscreen/ili210x.c 上报示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void ili210x_report_events(struct input_dev *input, const struct touchdata *touchdata) {
int i;
bool touch;
unsigned int x, y;
const struct finger *finger;

for (i = 0; i < MAX_TOUCHES; i++) {
input_mt_slot(input, i);//上报ABS_MT_SLOT事件
finger = &touchdata->finger[i];
touch = touchdata->status & (1 << i);
//上报ABS_MT_TRACKING_ID和ABS_MT_TOOL_TYPE事件
input_mt_report_slot_state(input, MT_TOOL_FINGER, touch);
if (touch) {
x = finger->x_low | (finger->x_high << 8);
y = finger->y_low | (finger->y_high << 8);
input_report_abs(input, ABS_MT_POSITION_X, x);
input_report_abs(input, ABS_MT_POSITION_Y, y);
}
}
input_mt_report_pointer_emulation(input, false);
input_sync(input);
}

5.2.4 ABS_MT其他事件#

如果设备支持的话,还可以使用 ABS_MT_TOUCH_MAJORABS_MT_WIDTH_MAJOR 这两个消息上报触摸面积信息,关于 其他 ABS_MT 事件的具体含义大家可以查看 Linux 内核中的 multi-touch-protocol.txt 文档。

5.2.4.1 ABS_MT_TOOL_TYPE#

上报触摸工具类型。

很多内核驱动都不能区分出触摸设备类型 ,是手指还是触摸 笔? 这种情况下, 这个事件可以忽略掉 。目前的协议支持

1
2
3
MT_TOOL_FINGER(手指)
MT_TOOL_PEN(笔)
MT_TOOL_PALM(手掌)这三种触摸设备类型

要 上 报 ABS_MT_TOOL_TYPE 事件,那么可以使用 input_mt_report_slot_state 函数来完成此工作。

5.3 多点触摸API#

5.3.1 input_mt_init_slots#

初始化 MT 的输入 slots槽,drivers/input/input-mt.c

int input_mt_init_slots( struct input_dev *dev, unsigned int num_slots, unsigned int flags);

num_slots: 要使用的 SLOT 数量,也就是触摸点的数量。

flags:

1
2
3
4
5
#define INPUT_MT_POINTER 0x0001 /* pointer device, e.g. trackpad */
#define INPUT_MT_DIRECT 0x0002 /* direct device, e.g. touchscreen */
#define INPUT_MT_DROP_UNUSED0x0004 /* drop contacts not seen in frame */
#define INPUT_MT_TRACK 0x0008 /* use in-kernel tracking */
#define INPUT_MT_SEMI_MT 0x0010 /* semi-mt device, finger count handled manually */

5.3.2 input_mt_slot#

Type B类型,此函数用于产生 ABS_MT_SLOT 事件,告诉内核当前上报的是哪个触摸点的坐标数据,定义在文件 include/linux/input/mt.h

void input_mt_slot(struct input_dev *dev, int slot);

slot:当前发送的是哪个 slot 的坐标信息,也就是哪个触摸点。

5.3.3 input_mt_report_slot_state#

Type B类型,用于产生ABS_MT_TRACKING_ID和ABS_MT_TOOL_TYPE 事件 ,ABS_MT_TRACKING_ID事件给slot关联一个ABS_MT_TRACKING_ID ABS_MT_TOOL_TYPE事件指定触摸类型(是笔还是手指等 )。此函数定义在文件drivers/input/input-mt.c

void input_mt_report_slot_state( struct input_dev *dev, unsigned int tool_type, bool active);

tool_type:触摸类型,可以选择 MT_TOOL_FINGER(手指)、MT_TOOL_PEN(笔)或 MT_TOOL_PALM(手掌),对于多点电容触摸屏来说一般都是手指。

active:true,连续触摸,input 子系统内核会自动分配一个 ABS_MT_TRACKING_ID 给 slot。 false,触摸点抬起,表示某个触摸点无效了,input 子系统内核会分配一个-1 给 slot,表示触摸 点溢出。

5.3.4 input_report_abs#

上报ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 事件,上报坐标。include/linux/input.h。

void input_report_abs( struct input_dev *dev, unsigned int code, int value);

code:要上报的是什么数据,设置为 ABS_MT_POSITION_XABS_MT_POSITION_Y

value:具体的 X 轴或 Y 轴坐标数据值。

5.3.5 input_mt_report_pointer_emulation#

如果追踪到的触摸点数量多于当前上报的数量,驱动程序使用 BTN_TOOL_TAP 事件来通 知用户空间当前追踪到的触摸点总数量,然后调用input_mt_report_pointer_emulation函数将 use_count 参数设置为 false。否则的话将 use_count 参数设置为 true,表示当前的触摸点数量(此函数会获取到具体的触摸点数量,不需要用户给出),drivers/input/input-mt.c

void input_mt_report_pointer_emulation(struct input_dev *dev, bool use_count);

use_count:true,有效的触摸点数量;false,追踪到的触摸点数量多于当前上报的数量.

5.4 Linux触摸屏驱动示例-FT5426#

5.4.1 设备树添加#

5.4.1.1 iomux引脚配置#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pinctrl_tsc: tscgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0xF080 /* TSC_INT */
>;
};
pinctrl_tsc_reset: tsc_reset {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x10B0 /* TSC_RST */
>;
}
pinctrl_i2c2: i2c2grp {
fsl,pins = <
MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0
MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0
>;
};

触摸屏要用到4个IO, i2c 2个引脚和1个RST, 1个INT引脚。

5.4.1.2 ft5426节点#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
&i2c2 {
clock_frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c2>;
status = "okay";
......
ft5426: ft5426@38 {
compatible = "edt,edt-ft5426";
reg = <0x38>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_tsc
&pinctrl_tsc_reset>;
interrupt-parent = <&gpio1>;
interrupts = <9 0>;
reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
};
};

找到i2c2节点,引用pinctrl_i2c2,设置好I2c的iomux。设置时钟100k, status开启okay。

添加子节点ft5426,器件地址为 0X38,引用pinctrl_tscpinctrl_tsc_reset设置rstint引脚的iomux。

interrupt-parent 属性描述中断 IO 对应的 GPIO 组为 GPIO1

interrupts 属性描述中断 IO 对应的是 GPIO1 组的 IOI09

reset-gpios 属性描述复位 IO 对应的 GPIO 为 GPIO5_IO09

interrupt-gpios 属性描述中断 IO 对应的 GPIO 为 GPIO1_IO09

5.4.2 FT5426驱动源码解析#

点击查看代码
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//ft5x06.c
#include <linux/module.h>
#include <linux/ratelimit.h>
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/i2c.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/debugfs.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/input/mt.h>
#include <linux/input/touchscreen.h>
#include <linux/input/edt-ft5x06.h>
#include <linux/i2c.h>

#define MAX_SUPPORT_POINTS 5 /* 5点触摸 */
#define TOUCH_EVENT_DOWN 0x00 /* 按下 */
#define TOUCH_EVENT_UP 0x01 /* 抬起 */
#define TOUCH_EVENT_ON 0x02 /* 接触 */
#define TOUCH_EVENT_RESERVED 0x03 /* 保留 */

/* FT5X06寄存器相关宏定义 */
#define FT5X06_TD_STATUS_REG 0X02 /* 状态寄存器地址 */
#define FT5x06_DEVICE_MODE_REG 0X00 /* 模式寄存器 */
#define FT5426_IDG_MODE_REG 0XA4 /* 中断模式 */
#define FT5X06_READLEN 29 /* 要读取的寄存器个数 */

struct ft5x06_dev {
struct device_node *nd; /* 设备节点 */
int irq_pin,reset_pin; /* 中断和复位IO */
int irqnum; /* 中断号 */
void *private_data; /* 私有数据 */
struct input_dev *input; /* input结构体 */
struct i2c_client *client; /* I2C客户端 */
};

static struct ft5x06_dev ft5x06;

static int ft5x06_ts_reset(struct i2c_client *client, struct ft5x06_dev *dev){
int ret = 0;
if (gpio_is_valid(dev->reset_pin)) {
/* 申请复位IO,并且默认输出低电平 */
ret = devm_gpio_request_one(&client->dev,
dev->reset_pin, GPIOF_OUT_INIT_LOW,
"edt-ft5x06 reset");
if (ret) {
return ret;
}
msleep(5);
gpio_set_value(dev->reset_pin, 1);/* 输出高电平,停止复位 */
msleep(300);
}
return 0;
}

static int ft5x06_read_regs(struct ft5x06_dev *dev, u8 reg, void *val, int len){
int ret;
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->client;
/* msg[0]为发送要读取的首地址 */
msg[0].addr = client->addr; /* ft5x06地址 */
msg[0].flags = 0; /* 标记为发送数据 */
msg[0].buf = &reg; /* 读取的首地址 */
msg[0].len = 1; /* reg长度*/
/* msg[1]读取数据 */
msg[1].addr = client->addr; /* ft5x06地址 */
msg[1].flags = I2C_M_RD; /* 标记为读取数据*/
msg[1].buf = val; /* 读取数据缓冲区 */
msg[1].len = len; /* 要读取的数据长度*/
ret = i2c_transfer(client->adapter, msg, 2);
if(ret == 2) {
ret = 0;
} else {
ret = -EREMOTEIO;
}
return ret;
}

static s32 ft5x06_write_regs(struct ft5x06_dev *dev, u8 reg, u8 *buf, u8 len) {
u8 b[256];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client *)dev->client;
b[0] = reg; /* 寄存器首地址 */
memcpy(&b[1],buf,len); /* 将要写入的数据拷贝到数组b里面 */
msg.addr = client->addr; /* ft5x06地址 */
msg.flags = 0; /* 标记为写数据 */

msg.buf = b; /* 要写入的数据缓冲区 */
msg.len = len + 1; /* 要写入的数据长度 */
return i2c_transfer(client->adapter, &msg, 1);
}

static void ft5x06_write_reg(struct ft5x06_dev *dev, u8 reg, u8 data) {
u8 buf = 0;
buf = data;
ft5x06_write_regs(dev, reg, &buf, 1);
}

static irqreturn_t ft5x06_handler(int irq, void *dev_id) {
struct ft5x06_dev *multidata = dev_id;

u8 rdbuf[29];
int i, type, x, y, id;
int offset, tplen;
int ret;
bool down;

offset = 1; /* 偏移1,也就是0X02+1=0x03,从0X03开始是触摸值 */
tplen = 6; /* 一个触摸点有6个寄存器来保存触摸值 */

memset(rdbuf, 0, sizeof(rdbuf)); /* 清除 */

/* 读取FT5X06触摸点坐标从0X02寄存器开始,连续读取29个寄存器 */
ret = ft5x06_read_regs(multidata, FT5X06_TD_STATUS_REG, rdbuf, FT5X06_READLEN);
if (ret) {
goto fail;
}

/* 上报每一个触摸点坐标 */
for (i = 0; i < MAX_SUPPORT_POINTS; i++) {
u8 *buf = &rdbuf[i * tplen + offset];

/* 以第一个触摸点为例,寄存器TOUCH1_XH(地址0X03),各位描述如下:
* bit7:6 Event flag 0:按下 1:释放 2:接触 3:没有事件
* bit5:4 保留
* bit3:0 X轴触摸点的11~8位。
*/
type = buf[0] >> 6; /* 获取触摸类型 */
if (type == TOUCH_EVENT_RESERVED)
continue;

/* 我们所使用的触摸屏和FT5X06是反过来的 */
x = ((buf[2] << 8) | buf[3]) & 0x0fff;
y = ((buf[0] << 8) | buf[1]) & 0x0fff;

/* 以第一个触摸点为例,寄存器TOUCH1_YH(地址0X05),各位描述如下:
* bit7:4 Touch ID 触摸ID,表示是哪个触摸点
* bit3:0 Y轴触摸点的11~8位。
*/
id = (buf[2] >> 4) & 0x0f;
down = type != TOUCH_EVENT_UP;

input_mt_slot(multidata->input, id);
input_mt_report_slot_state(multidata->input, MT_TOOL_FINGER, down);

if (!down)
continue;

input_report_abs(multidata->input, ABS_MT_POSITION_X, x);
input_report_abs(multidata->input, ABS_MT_POSITION_Y, y);
}

input_mt_report_pointer_emulation(multidata->input, true);
input_sync(multidata->input);
fail:
return IRQ_HANDLED;

}

static int ft5x06_ts_irq(struct i2c_client *client, struct ft5x06_dev *dev) {
int ret = 0;
if (gpio_is_valid(dev->irq_pin)) {
ret = devm_gpio_request_one(&client->dev, dev->irq_pin,
GPIOF_IN, "edt-ft5x06 irq");
if (ret) {
dev_err(&client->dev,
"Failed to request GPIO %d, error %d\n",
dev->irq_pin, ret);
return ret;
}
}
ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
ft5x06_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
client->name, &ft5x06);
if (ret) {
dev_err(&client->dev, "Unable to request touchscreen IRQ.\n");
return ret;
}
return 0;
}

static int ft5x06_ts_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret = 0;

ft5x06.client = client;

ft5x06.irq_pin = of_get_named_gpio(client->dev.of_node, "interrupt-gpios", 0);
ft5x06.reset_pin = of_get_named_gpio(client->dev.of_node, "reset-gpios", 0);

ret = ft5x06_ts_reset(client, &ft5x06);
if(ret < 0) {
goto fail;
}

ret = ft5x06_ts_irq(client, &ft5x06);
if(ret < 0) {
goto fail;
}

/* 4,初始化FT5X06 */
ft5x06_write_reg(&ft5x06, FT5x06_DEVICE_MODE_REG, 0); /* 进入正常模式 */
ft5x06_write_reg(&ft5x06, FT5426_IDG_MODE_REG, 1); /* FT5426中断模式 */

/* 5,input设备注册 */
ft5x06.input = devm_input_allocate_device(&client->dev);
if (!ft5x06.input) {
ret = -ENOMEM;
goto fail;
}
ft5x06.input->name = client->name;
ft5x06.input->id.bustype = BUS_I2C;
ft5x06.input->dev.parent = &client->dev;
__set_bit(EV_KEY, ft5x06.input->evbit);
__set_bit(EV_ABS, ft5x06.input->evbit);
__set_bit(BTN_TOUCH, ft5x06.input->keybit);

input_set_abs_params(ft5x06.input, ABS_X, 0, 1024, 0, 0);
input_set_abs_params(ft5x06.input, ABS_Y, 0, 600, 0, 0);
input_set_abs_params(ft5x06.input, ABS_MT_POSITION_X,0, 1024, 0, 0);
input_set_abs_params(ft5x06.input, ABS_MT_POSITION_Y,0, 600, 0, 0);
ret = input_mt_init_slots(ft5x06.input, MAX_SUPPORT_POINTS, 0);
if (ret) {
goto fail;
}
ret = input_register_device(ft5x06.input);
if (ret)
goto fail;
return 0;
fail:
return ret;
}

static int ft5x06_ts_remove(struct i2c_client *client){
input_unregister_device(ft5x06.input);
return 0;
}

static const struct i2c_device_id ft5x06_ts_id[] = {
{ "edt-ft5206", 0, },
{ "edt-ft5426", 0, },
{ /* sentinel */ }
};
static const struct of_device_id ft5x06_of_match[] = {
{ .compatible = "edt,edt-ft5206", },
{ .compatible = "edt,edt-ft5426", },
{ /* sentinel */ }
};

static struct i2c_driver ft5x06_ts_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "edt_ft5x06",
.of_match_table = of_match_ptr(ft5x06_of_match),
},
.id_table = ft5x06_ts_id,
.probe = ft5x06_ts_probe,
.remove = ft5x06_ts_remove,
};

static int __init ft5x06_init(void){
int ret = 0;

ret = i2c_add_driver(&ft5x06_ts_driver);

return ret;
}
static void __exit ft5x06_exit(void){
i2c_del_driver(&ft5x06_ts_driver);
}
module_init(ft5x06_init);
module_exit(ft5x06_exit);
MODULE_LICENSE("GPL");

5.4.2.1 probe过程#

image

dts中的compatible和驱动匹配,of_match_table匹配,因此触发probe函数。可以看到FT5426使用的标准I2C从设备驱动框架Linux I2C子系统驱动

字符设备驱动-I2C子系统 | Hexo (fuzidage.github.io)。因为使用的I2c2。

image

由于i2c_client描述i2c从设备的i2c相关硬件信息。 一个i2c_driver可以支持多个同类型的i2c_clienti2c_client一般描述在设备树中, 这里对应i2c2的ft5426子节点

  1. 当驱动和设备匹配,ft5x06_ts_probe执行。首先获取dts中的属性reset-gpios,interrupt-gpios

    image

  2. 对复位引脚进行复位。(参考5.1.3上电复位时序

image

  1. 注册中断服务

image

  1. 初始化ft5426内部寄存器

    image

  2. 利用input子系统设置MT协议参数,并且注册input设备。

    image

需要上报的事件为 EV_KEY 和 EV_ABS,需要上报的按键码为 BTN_TOUCH(BTN_TOUCH见include\uapi\linux\input-event-codes.h)。EV_KEY 是按键事件,用于上报触摸屏是否被按下,相当于把触摸屏当做一个按键。EV_ABS 是触摸点坐标数据,BTN_TOUCH 表示将触摸屏的按下和抬起用作 BTN_TOUCH 按键。

input_set_abs_params 函数设置 EV_ABS 事件需要上报 ABS_X、ABS_Y、ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y。单点触摸需要上报ABS_X ABS_Y,对于多点触摸需要上报 ABS_MT_POSITION_X ABS_MT_POSITION_Y

input_mt_init_slots 函数初始 化 slots,也就是最大触摸点数量,FT5426 是个 5 点电容触摸芯片,因此一共 5 个 slot。

5.4.2.2 I2C数据传输#

其实就是i2c数据传输的应用。参考Linux I2C子系统驱动

字符设备驱动-I2C子系统 | Hexo (fuzidage.github.io)

image

读过程:

构造i2c_msg[0],flag=0表示写,先发送i2c从设备器件地址0x38。然后发送要读哪个寄存器地址。

构造i2c_msg[1],flag=1表示读,先发送i2c从设备器件地址0x38。然后传入要读的buf。

写过程:

构造i2c_msg,flag=0表示写,先发送i2c从设备器件地址0x38。然后发送要写哪个寄存器地址和写入的内容。

5.4.2.3 中断触摸数据上报#

image

  1. 通过I2c_read获取寄存器值。一共29个寄存器,读出29 byte。从0X02寄存器开始读。
  2. for循环内部用来拆解坐标信息,上报每一个点的坐标。
  3. 最后调用input_sync上传上报SYN_REPORT事件。

5.4.3 用户态应用测试#

image

驱动加载成功以后就会生成/dev/input/eventX(X=1,2,3…)

image

5.4.3.1 触摸屏原始数据解析#

hexdump /dev/input/event2可以查看原始数据。

image

第1行,type为0x3,说明是一个EV_ABS事件,code为0x2f,为ABS_MT_SLOT,因此这一行就是input_mt_slot函数上报的ABS_MT_SLOT事件。value=0,说明接下来上报的是第一个触摸点坐标。

第2行,type为0x3,说明是一个EV_ABS事件,code为0x39,也就是ABS_MT_TRACKING_ID,这一行就是input_mt_report_slot_state函数上报ABS_MT_TRACKING_ID事件。value=5说明给SLOT0分配的ID为5。

​ 第3行,type为0x3,是一个EV_ABS事件,code为0x35,为ABS_MT_POSITION_X,这一行就是input_report_abs函数上报的ABS_MT_POSITION_X事件,也就是触摸点的X轴坐标。value=0x03ec=1004,说明触摸点X轴坐标为1004,属于屏幕右上角区域。

​ 第4行,type为0x3,是一个EV_ABS事件,code为0x36,为ABS_MT_POSITION_Y,这一行就是input_report_abs函数上报的ABS_MT_POSITION_Y事件,也就是触摸点的Y轴坐标。value=0x17=23,说明Y轴坐标为23,由此可以看出本次触摸的坐标为(1004,23),处于屏幕右上角区域。

​ 第5行,type为0x1,是一个EV_KEY事件,code=0x14a,为BTN_TOUCHvalue=0x1表示触摸屏被按下。

​ 第6行,type为0x3,是一个EV_ABS事件,code为0x0,为ABS_X,用于单点触摸的时候上报X轴坐标。在这里和ABS_MT_POSITION_X相同,value也为0x3f0=1008。ABS_X是由input_mt_report_pointer_emulation函数上报的。

​ 第7行,type为0x3,是一个EV_ABS事件,code为0x1,为ABS_Y,用于单点触摸的时候上报Y轴坐标。在这里和ABS_MT_POSITION_Y相同,value也为0x17=23。ABS_Y是由input_mt_report_pointer_emulation函数上报的。

第8行,type为0x0,是一个EV_SYN事件,由input_sync函数上报。

第9行,type为0x3,是一个EV_ABS事件,code为0x39,也就是ABS_MT_TRACKING_IDvalue=0xffffffff=-1,说明触摸点离开了屏幕。

第10行,type为0x1,是一个EV_KEY事件,code=0x14a,为BTN_TOUCHvalue=0x0表示手指离开触摸屏,也就是触摸屏没有被按下了。

第11行,type为0x0,是一个EV_SYN事件,由input_sync函数上报。

以上就是一个触摸点的坐标上报过程。

5.5 Linux内核自带的触摸驱动#

打开driver/input/touchscreen/Makefile

image

linux内核默认已经帮我们实现了这款edt-ft5x06.c触摸屏驱动。此驱动文件不仅仅 能够驱动 FT5426,FT5206、FT5406 这些都可以驱动。

make menuconfig,选中这款触摸屏即可。

1
2
3
4
5
6
Location:
-> Device Drivers
-> Input device support
-> Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y])
-> Touchscreens (INPUT_TOUCHSCREEN [=y])
-> <*> EDT FocalTech FT5x06 I2C Touchscreen support

image

编译后开机如下打印:

image

直接运行 ts_test_mt 来测试触摸屏是否可以使用。

6 IS_ENABLED-在驱动中判断某CONFIG是否定义#

1
linux/kconfig.h:73:#define IS_ENABLED(option) __or(IS_BUILTIN(option), IS_MODULE(option))

字符设备驱动-SPI子系统

1 Linux SPI驱动框架#

image

linux SPI驱动框架层次如上图:

除开硬件和用户态应用程序,由上到下分成3层:

1
2
3
设备驱动层: spi框架使用者
核心层:spi框架搭建者
控制器驱动层: spi框架适配者

1.1 spi核心层#

SPI核心层代码位于linux_5.10\drivers\spi目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for kernel SPI drivers.
#
ccflags-$(CONFIG_SPI_DEBUG) := -DDEBUG
# small core, mostly translating board-specific
# config declarations into driver model code
obj-$(CONFIG_SPI_MASTER) += spi.o
obj-$(CONFIG_SPI_MEM) += spi-mem.o
obj-$(CONFIG_SPI_MUX) += spi-mux.o
obj-$(CONFIG_SPI_SPIDEV) += spidev.o
obj-$(CONFIG_SPI_LOOPBACK_TEST) += spi-loopback-test.o
# SPI master controller drivers (bus)
obj-$(CONFIG_SPI_ALTERA) += spi-altera.o
obj-$(CONFIG_SPI_AR934X) += spi-ar934x.o
obj-$(CONFIG_SPI_ARMADA_3700) += spi-armada-3700.o
obj-$(CONFIG_SPI_ATMEL) += spi-atmel.o
obj-$(CONFIG_SPI_ATMEL_QUADSPI) += atmel-quadspi.o
obj-$(CONFIG_SPI_AT91_USART) += spi-at91-usart.o
obj-$(CONFIG_SPI_ATH79) += spi-ath79.o
obj-$(CONFIG_SPI_AU1550) += spi-au1550.o
obj-$(CONFIG_SPI_AXI_SPI_ENGINE) += spi-axi-spi-engine.o
obj-$(CONFIG_SPI_BCM2835) += spi-bcm2835.o
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
drivers/spi/spi.c spi-mem.c spi-mux.c
include/linux/spi/spi.h
spi.c:
一方面对SPI子系统进行初始化工作,注册spi bus,注册spi_master class
同时提供spi设备驱动对spi总线进行操作的API
另一方面SPI子系统对spi控制器层,提供注册控制器的api和回调操作函数。
spi.h包含了spi核心层的一些重要数据结构,struct spi_master;
struct spi_transfer; struct spi_message,以及一些实现比较简单的函数等。

spi-gpio.cSPI GPIO框架:SPI子系统提供了一个名为spi-gpio的框架,
可使用GPIO引脚模拟SPI总线,gpio模拟spi代码在drivers/spi/spi-gpio.c中。
这个框架允许将GPIO引脚配置为SPI总线的时钟、片选、输入和输出信号,
并提供了对应的接口函数供驱动程序使用。
spi-bitbangspi-bitbangLinux内核中提供的一个通用框架,用于在没有硬件SPI控制器
或需要灵活控制SPI时序和配置的系统中模拟SPI总线的通信。代码在spi-bitbang.c

核心层的作用:

对上层的使用者,也就是SPI设备驱动:提供标准的spi收发API,以及设备注册函数。
对底下的适配者,也就是控制器驱动层:提供注册控制器接口,并提供一些需要控制器驱动实现的回调函数。

1.1.1 核心层初始化#

1.1.1.1 spi_init#

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
static int __init spi_init(void) {
int status;
buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL);
if (!buf) {
status = -ENOMEM;
goto err0;
}
status = bus_register(&spi_bus_type);
if (status < 0)
goto err1;
status = class_register(&spi_master_class);
if (status < 0)
goto err2;
if (IS_ENABLED(CONFIG_SPI_SLAVE)) {
status = class_register(&spi_slave_class);
if (status < 0)
goto err3;
}

if (IS_ENABLED(CONFIG_OF_DYNAMIC))
WARN_ON(of_reconfig_notifier_register(&spi_of_notifier));
if (IS_ENABLED(CONFIG_ACPI))
WARN_ON(acpi_reconfig_notifier_register(&spi_acpi_notifier));

return 0;
err3:
class_unregister(&spi_master_class);
err2:
bus_unregister(&spi_bus_type);
err1:
kfree(buf);
buf = NULL;
err0:
return status;
}

spi子系统初始化函数,仅仅是注册了spi bus,以及spi_master class

成功注册后,在/sys/bus 下即可找到spi 文件目录,在/sys/class下可以看到spi_master目录:

image

image

1.1.1.2 spi从设备和驱动的匹配#

当spi总线和类注册后,当有驱动和设备匹配上就会调用spi_match_device,也就是spi_bus_type.match函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int spi_match_device(struct device *dev, struct device_driver *drv) {
const struct spi_device *spi = to_spi_device(dev);
const struct spi_driver *sdrv = to_spi_driver(drv);

/* Check override first, and if set, only use the named driver */
if (spi->driver_override)
return strcmp(spi->driver_override, drv->name) == 0;

/* Attempt an OF style match */
if (of_driver_match_device(dev, drv))
return 1;

/* Then try ACPI */
if (acpi_driver_match_device(dev, drv))
return 1;

if (sdrv->id_table)
return !!spi_match_id(sdrv->id_table, spi);
return strcmp(spi->modalias, drv->name) == 0;
}
  1. 通过of_match_tablecompatible和设备树匹配;
  2. 通过acpi_match_tablecompatibledeviceof_nodecompatible匹配;
  3. 通过驱动和设备的id_table去匹配。
  4. 最后通过驱动和设备的名字去匹配。

匹配过程参考[字符设备驱动-2.总线/平台设备/平台驱动模型

字符设备驱动-2-总线模型和平台设备驱动 | Hexo (fuzidage.github.io)

1.1.2 核心层API#

1.1.2.1 spi_register_master-(供适配层调用)#

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
int spi_register_master(struct spi_master *master) {
...
status = of_spi_register_master(master);
if (status)
return status;
...
dev_set_name(&master->dev, "spi%u", master->bus_num);
status = device_add(&master->dev);
if (status < 0)
goto done;
dev_dbg(dev, "registered master %s%s\n", dev_name(&master->dev),
dynamic ? " (dynamic)" : "");

if (master->transfer)
dev_info(dev, "master is unqueued, this is deprecated\n");
else {
status = spi_master_initialize_queue(master);
if (status) {
device_del(&master->dev);
goto done;
}
}
...
mutex_lock(&board_lock);
list_add_tail(&master->list, &spi_master_list);
list_for_each_entry(bi, &board_list, list)
spi_match_master_to_boardinfo(master, &bi->board_info);
mutex_unlock(&board_lock);
...
of_register_spi_devices(master);
...
done:
return status;
}
  1. of_spi_register_master,根据设备树节点中的"cs-gpios",向struct spi_master添加gpio cs引脚。

  2. device_add将device注册到设备模型中。

  3. 如果控制器驱动没有自己实现transfer函数,则初始化发送队列spi_master_initialize_queue。(核心层填充默认transfer函数)

  4. spi_match_master_to_boardinfo老的方式,遍历所有spi_board_info数据结构,并注册spi_device

  5. of_register_spi_devices新的设备树方式,遍历spi控制器节点下所有子节点,并注册成对应的spi_device设备

1.1.2.1.1 spi_controller_initialize_queue/spi_master_initialize_queue#

image

可以看到是对控制器spi_master(或者叫做spi_controller)成员函数赋值。transfer_one_message或者transfer赋值成默认的函数。然后调用spi_init_queuespi_start_queue函数初始化队列并启动工作线程。spi_init_queue函数最主要的作用就是建立一个内核工作线程。

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
static int spi_init_queue(struct spi_master *master)
{
......
kthread_init_worker(&master->kworker);
master->kworker_task = kthread_run(kthread_worker_fn,
&master->kworker, "%s", dev_name(&master->dev));
......
kthread_init_work(&master->pump_messages, spi_pump_messages);
......
return 0;
}
static int spi_start_queue(struct spi_master *master)
{
......
master->running = true;
master->cur_msg = NULL;
......
kthread_queue_work(&master->kworker, &master->pump_messages);
......
return 0;
}
/* spi_init_queue函数先初始化kthread_worker,为kthread_worker创建一个内核线程来处理work,
随后初始化kthread_work,设置work执行函数,work执行函数为spi_pump_messages
spi_start_queue就相对简单了,只是唤醒该工作线程而已;自此,队列化的相关工作已经完成,
系统等待message请求被发起,然后在工作线程中处理message的传送工作。
*/

1.1.2.2 spi_message_init-(供从设备调用)#

spi_massage进行初始化.

1
2
3
4
5
6
7
8
9
10
static inline void spi_message_init_no_memset(struct spi_message *m)
{
INIT_LIST_HEAD(&m->transfers);
INIT_LIST_HEAD(&m->resources);
}
static inline void spi_message_init(struct spi_message *m)
{
memset(m, 0, sizeof *m);
spi_message_init_no_memset(m);
}

1.1.2.3 spi_message_add_tail-(供从设备调用)#

1
2
3
4
static inline void
spi_message_add_tail(struct spi_transfer *t, struct spi_message *m) {
list_add_tail(&t->transfer_list, &m->transfers);
}

可以看到就是把spi_transfer这个buffer添加到spi_message传输链表中。

1.1.2.4 spi_async-(供从设备调用)#

发起数据传输。可以看到就是调用控制器内部的master->transfer。既然是async那就是异步执行的,不会等待传输是否完成,就直接返回。

1
2
3
spi_async->
__spi_async->
master->transfer

那假如master->transfer控制器这边没有去实现。内核会自己填充默认的transfer函数spi_queued_transfer.

1.1.2.4.1 spi_queued_transfer#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spi_queued_transfer(struct spi_device *spi, struct spi_message *msg) {
__spi_queued_transfer(spi, msg, true);
}
static int __spi_queued_transfer(struct spi_device *spi,
struct spi_message *msg,
bool need_pump) {
struct spi_master *master = spi->master;
unsigned long flags;

spin_lock_irqsave(&master->queue_lock, flags);
...
list_add_tail(&msg->queue, &master->queue);
if (!master->busy && need_pump)
kthread_queue_work(&master->kworker, &master->pump_messages);
spin_unlock_irqrestore(&master->queue_lock, flags);
return 0;
}

可以看到默认的spi_queued_transfer就是去把message添加到master的发送链表中。接下来就交给工作服务线程__spi_pump_messages去处理message

1.1.2.4 spi_sync-(供从设备调用)#

image

同理,spi_sync就是用来同步传输 spi_message, 完成传输后会调用spi_comlete唤醒等待的线程。

那假如master->transfer控制器这边没有去实现。内核也会自己填充默认的transfer函数spi_queued_transfer.可以看到need_pump=false,因此还是一样就是去把message添加到master的发送链表中。

然后调用__spi_pump_messages,也就是工作线程服务,交给工作服务线程去处理message。

最后调用wait_for_completion去等待传输完成。

1.1.2.5 spi_bitbang_start-(供适配层调用)#

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
int spi_bitbang_start(struct spi_bitbang *bitbang)
{
struct spi_master *master = bitbang->master;
int ret;

if (!master || !bitbang->chipselect)
return -EINVAL;

mutex_init(&bitbang->lock);

if (!master->mode_bits)
master->mode_bits = SPI_CPOL | SPI_CPHA | bitbang->flags;

if (master->transfer || master->transfer_one_message)
return -EINVAL;

master->prepare_transfer_hardware = spi_bitbang_prepare_hardware;
master->unprepare_transfer_hardware = spi_bitbang_unprepare_hardware;
master->transfer_one = spi_bitbang_transfer_one;
master->set_cs = spi_bitbang_set_cs;

if (!bitbang->txrx_bufs) {
bitbang->use_dma = 0;
bitbang->txrx_bufs = spi_bitbang_bufs;
if (!master->setup) {
if (!bitbang->setup_transfer)
bitbang->setup_transfer =
spi_bitbang_setup_transfer;
master->setup = spi_bitbang_setup;
master->cleanup = spi_bitbang_cleanup;
}
}

/* driver may get busy before register() returns, especially
* if someone registered boardinfo for devices
*/
ret = spi_register_master(spi_master_get(master));
if (ret)
spi_master_put(master);

return ret;
}
  1. 如果主设备结构体中的传输模式位字段 mode_bits 未设置,则设置为默认的模式位,包括 SPI_CPOLSPI_CPHA 位移传输控制结构体中的标志位。

  2. 检查主设备结构体中的传输函数是否已经定义,如果已定义则返回错误码 -EINVAL

  3. 设置主设备结构体中的准备硬件传输函数、释放硬件传输函数、单次传输函数和片选信号控制函数,分别对应位移传输控制结构体中的对应函数。

  4. 如果位移传输控制结构体中的数据缓冲区传输函数未定义,则设置使用 DMA 标志为 0,并将数据缓冲区传输函数设置为默认的位移传输函数 spi_bitbang_bufs。如果主设备结构体中的设置函数未定义,则设置使用默认的设置函数 spi_bitbang_setup 和清理函数 spi_bitbang_cleanup

  5. 注册 SPI 主设备,将其添加到系统中。

1.1.2.5.1 spi_register_master#

1.2 SPI控制器驱动层#

控制器驱动就是用来实现spi_master中的成员函数。如transfer, transfer_one_message。基本流程是:申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册 spi_master

1.2.1 SPI控制器API#

1.2.1.1 spi_alloc_master#

申请spi控制器内存。

struct spi_master *spi_alloc_master(struct device *dev, unsigned size);

dev:设备,一般是 platform_device 中的 dev 成员变量。

size:私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。

返回值:申请到的 spi_master。

1.2.1.2 spi_master_put#

spi_master 的释放通过 spi_master_put函数来完成,当我们删除一个 SPI 主机驱动的时候就 需要释放掉前面申请的 spi_master,spi_master_put 函数原型如下:

void spi_master_put(struct spi_master *master);

释放spi控制器内存。

1.2.1.3 spi_register_master/spi_register_controller#

当 spi_master 初始化完成以后就需要将其注册到 Linux 内核,spi_register_master注册spi控制器。

int spi_register_master(struct spi_master *master);

1.2.1.4 spi_unregister_master/spi_unregister_controller#

spi控制器卸载。

void spi_unregister_master(struct spi_master *master);

1.2.2 SPI控制器示例#

以飞思卡尔nxp官方spi驱动为例,文件位于linux\drivers\spi\spi-imx.c

1.2.2.1 spi控制器设备树描述#

打开设备树文件imx6ul.dtsi

image

1
2
3
4
5
6
7
8
9
10
11
12
13
ecspi3: spi@2010000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
reg = <0x02010000 0x4000>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_ECSPI3>,
<&clks IMX6UL_CLK_ECSPI3>;
clock-names = "ipg", "per";
dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
dma-names = "rx", "tx";
status = "disabled";
};

1.2.2.2 imx6ul spi控制器驱动#

1.2.2.2.1 probe-(初始化spi_master)#

image

  1. 解析设备树,初始化控制器

    image

    配置master和spi_imx。包括传输函数,片选函数,片选引脚。master作为平台设备的dev的driver_data, spi_imx作为master的dev的driver_data。

    获取irq, res等信息,进行ioremap和注册irq。

    设置时钟,开启时钟。

    初始话dma寄存器。进行控制器初始化和复位。

    image

    image

  2. 申请并初始化 spi_master, 调用 spi_bitbang_start 函数(spi_bitbang_start 会调用 spi_register_master 函数)向 Linux 内核注册 spi_master。

1.2.2.2.2 spi_imx_setupxfer-(设置位宽和配置控制器)#

image

设置 spi_imx 的 tx 和 rx 传输函数。根据要发送的数据数据位宽的不 同,分别有 8 位、16 位和 32 位的发送函数:

1
2
3
4
5
6
spi_imx_buf_tx_u8
spi_imx_buf_tx_u16
spi_imx_buf_tx_u32
spi_imx_buf_rx_u8
spi_imx_buf_rx_u16
spi_imx_buf_rx_u32
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
#define MXC_SPI_BUF_RX(type)						\
static void spi_imx_buf_rx_##type(struct spi_imx_data *spi_imx) \
{ //将要接收的数据值读到 ECSPI 的 MXC_CSPIRXDATA 寄存器里面去 \
unsigned int val = readl(spi_imx->base + MXC_CSPIRXDATA); \
\
if (spi_imx->rx_buf) { \
*(type *)spi_imx->rx_buf = val; \
spi_imx->rx_buf += sizeof(type); \
} \
}
#define MXC_SPI_BUF_TX(type) \
static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx) \
{ \
type val = 0; \
\
if (spi_imx->tx_buf) { \
val = *(type *)spi_imx->tx_buf; \
spi_imx->tx_buf += sizeof(type); \
} \
\
spi_imx->count -= sizeof(type); \
//将要发送的数据值写入到 ECSPI 的 TXDATA 寄存器里面去 \
writel(val, spi_imx->base + MXC_CSPITXDATA); \
}

MXC_SPI_BUF_RX(u8)
MXC_SPI_BUF_TX(u8)
MXC_SPI_BUF_RX(u16)
MXC_SPI_BUF_TX(u16)
MXC_SPI_BUF_RX(u32)
MXC_SPI_BUF_TX(u32)

image

image

调用关系如下:mx51_ecspi_config就是最底层SPI controller的寄存器配置,这里不做分析。

1
2
spi_imx->bitbang.setup_transfer = spi_imx_setupxfer
spi_imx->devtype_data->config = mx51_ecspi_config
1.2.2.2.3 spi_imx_transfer-(数据收发)#

数据收发函数为 spi_imx_transfer:

1
2
3
4
spi_imx_transfer
-> spi_imx_pio_transfer
-> spi_imx_push
-> spi_imx->tx

spi_imx 是个 spi_imx_data 类型的机构指针变量,其中 tx 和 rx 这两个成员变量分别为 SPI 数据发送和接收函数。

1
2
3
4
5
spi_bitbang_start中
->master->transfer_one = spi_bitbang_transfer_one;
当spi_bitbang_transfer_on时
bitbang->txrx_bufs(spi,t) = spi_imx_transfer
也就是spi_imx->tx就可以spi_imx_buf_tx_u8
1.2.2.2.4 spi_imx_isr#

image

中断服务程序,只要rx_available,启用spi_imx->rx。从MXC_CSPIRXDATA 寄存器读出数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int __maybe_unused mx51_ecspi_rx_available(struct spi_imx_data *spi_imx)
{
return readl(spi_imx->base + MX51_ECSPI_STAT) & MX51_ECSPI_STAT_RR;
}
#define MXC_SPI_BUF_RX(type) \
static void spi_imx_buf_rx_##type(struct spi_imx_data *spi_imx) \
{ \
unsigned int val = readl(spi_imx->base + MXC_CSPIRXDATA); \
\
if (spi_imx->rx_buf) { \
*(type *)spi_imx->rx_buf = val; \
spi_imx->rx_buf += sizeof(type); \
} \
}
1.2.2.2.5 spi_imx_chipselect-(片选)#

image

设置片选gpio电平。

1.3 SPI从设备驱动层#

1.3.1 SPI从设备API#

1.3.1.1 spi_message_init#

void spi_message_init(struct spi_message *m);

在使用spi_message之前需要对其进行初始化。

1.3.1.2 spi_message_add_tail#

void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);

spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中。

1.3.1.3 spi_async#

参考核心层api有介绍。

1.3.1.4 spi_sync#

参考核心层api有介绍。

1.3.1.5 spi_register_driver#

1.3.1.6 spi_unregister_driver#

1.3.1.7 spi_read/spi_write#

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
static inline void spi_message_init_with_transfers(struct spi_message *m,
struct spi_transfer *xfers, unsigned int num_xfers) {
unsigned int i;
spi_message_init(m);
for (i = 0; i < num_xfers; ++i)
spi_message_add_tail(&xfers[i], m);
}
static inline int spi_sync_transfer(struct spi_device *spi, struct spi_transfer *xfers,
unsigned int num_xfers) {
struct spi_message msg;
spi_message_init_with_transfers(&msg, xfers, num_xfers);
return spi_sync(spi, &msg);
}
static inline int spi_read(struct spi_device *spi, void *buf, size_t len) {
struct spi_transfer t = {
.rx_buf = buf,
.len = len,
};
return spi_sync_transfer(spi, &t, 1);
}
static inline int spi_write(struct spi_device *spi, const void *buf, size_t len)
{
struct spi_transfer t = {
.tx_buf = buf,
.len = len,
};

return spi_sync_transfer(spi, &t, 1);
}

spi_read, spi_write本质还是调用spi_sync传输函数,做了一层封装把spi_transfrerspi_message包装好了。

1.3.2 SPI从设备示例-ICM-20608-G#

该传感器详细介绍:6轴陀螺仪加速度传感器ICM-20608-G

IMX6ULL SPI应用-6轴陀螺仪加速度传感器ICM-20608-G - fuzidage - 博客园 (cnblogs.com)

1.3.2.1 dts描述#

打开自己board对应的dts,我这里是imx6ull-alientek-emmc.dts。我们修改ecspi3 spi控制器。我的spi3接了一个ICM-20608。资源定义如下:

image

根据原理图接线先配置iomux:

1
2
3
4
5
6
7
8
pinctrl_ecspi3: icm20608 {
fsl,pins = <
MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK */
MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO */
MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI */
>;
};

UART2_TX_DATA 这个 IO 是 ICM20608 的片选信号,这里我们并没有将其复用为 ECSPI3 的SS0信号,而是将其复用为了普通的 GPIO。因为我们需要自己控制片选信号,所以将其复 用为普通的 GPIO。

imx6ull-alientek-emmc.dts重新修改ecspi3节点,追加从设备描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
&ecspi3 {
fsl,spi-num-chipselects = <1>;
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi3>;
status = "okay";

spidev: icm20608@0 {
compatible = "alientek,icm20608";
spi-max-frequency = <8000000>;
reg = <0>;
};
};
  1. 设置当前片选数量为 1,因为就只接了一个 ICM20608
  2. 一定要使用 “cs-gpios”属性来描述片选引脚,SPI 主机驱动就会控制片选引脚。
  3. imx6ull.dtsi 文件中默认将 ecspi3 节点状态(status)设置为“disable”,这里我们要将 其改为“okay”
  4. icm20608 设备子节点,因为 icm20608 连接在ECSPI3的第 0 个通道上,因此 @后面为 0。
    1. 设置节点属性兼容值为“alientek,icm20608”
    2. 设置 SPI 最大时钟频 率为 8MHz,这是 ICM20608 的 SPI 接口所能支持的最大的时钟频率。
    3. icm20608 连接 在通道 0 上,因此 reg 为 0。
1.3.2.1.1 spi从设备dts描述规则#

打开linux\Documentation\devicetree\bindings\spi\spi-bus.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SPI slave nodes must be children of the SPI master node and can
contain the following properties.
- reg - (required) chip select address of device.
- compatible - (required) name of SPI device following generic names
recommended practice
- spi-max-frequency - (required) Maximum SPI clocking speed of device in Hz
- spi-cpol - (optional) Empty property indicating device requires
inverse clock polarity (CPOL) mode
- spi-cpha - (optional) Empty property indicating device requires
shifted clock phase (CPHA) mode
- spi-cs-high - (optional) Empty property indicating device requires
chip select active high
- spi-3wire - (optional) Empty property indicating device requires
3-wire mode.
- spi-lsb-first - (optional) Empty property indicating device requires
LSB first mode.
- spi-tx-bus-width - (optional) The bus width(number of data wires) that
used for MOSI. Defaults to 1 if not present.
- spi-rx-bus-width - (optional) The bus width(number of data wires) that
used for MISO. Defaults to 1 if not present.

1.3.2.2 ICM20608驱动#

1.3.2.2.1 icm20608reg.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
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
#ifndef ICM20608_H
#define ICM20608_H
/***************************************************************
文件名 : icm20608reg.h
***************************************************************/
#define ICM20608G_ID 0XAF /* ID值 */
#define ICM20608D_ID 0XAE /* ID值 */
/* ICM20608寄存器
*复位后所有寄存器地址都为0,除了
*Register 107(0X6B) Power Management 1 = 0x40
*Register 117(0X75) WHO_AM_I = 0xAF或0xAE
*/
/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
#define ICM20_SELF_TEST_X_GYRO 0x00
#define ICM20_SELF_TEST_Y_GYRO 0x01
#define ICM20_SELF_TEST_Z_GYRO 0x02
#define ICM20_SELF_TEST_X_ACCEL 0x0D
#define ICM20_SELF_TEST_Y_ACCEL 0x0E
#define ICM20_SELF_TEST_Z_ACCEL 0x0F
/* 陀螺仪静态偏移 */
#define ICM20_XG_OFFS_USRH 0x13
#define ICM20_XG_OFFS_USRL 0x14
#define ICM20_YG_OFFS_USRH 0x15
#define ICM20_YG_OFFS_USRL 0x16
#define ICM20_ZG_OFFS_USRH 0x17
#define ICM20_ZG_OFFS_USRL 0x18
#define ICM20_SMPLRT_DIV 0x19
#define ICM20_CONFIG 0x1A
#define ICM20_GYRO_CONFIG 0x1B
#define ICM20_ACCEL_CONFIG 0x1C
#define ICM20_ACCEL_CONFIG2 0x1D
#define ICM20_LP_MODE_CFG 0x1E
#define ICM20_ACCEL_WOM_THR 0x1F
#define ICM20_FIFO_EN 0x23
#define ICM20_FSYNC_INT 0x36
#define ICM20_INT_PIN_CFG 0x37
#define ICM20_INT_ENABLE 0x38
#define ICM20_INT_STATUS 0x3A
/* 加速度输出 */
#define ICM20_ACCEL_XOUT_H 0x3B
#define ICM20_ACCEL_XOUT_L 0x3C
#define ICM20_ACCEL_YOUT_H 0x3D
#define ICM20_ACCEL_YOUT_L 0x3E
#define ICM20_ACCEL_ZOUT_H 0x3F
#define ICM20_ACCEL_ZOUT_L 0x40
/* 温度输出 */
#define ICM20_TEMP_OUT_H 0x41
#define ICM20_TEMP_OUT_L 0x42
/* 陀螺仪输出 */
#define ICM20_GYRO_XOUT_H 0x43
#define ICM20_GYRO_XOUT_L 0x44
#define ICM20_GYRO_YOUT_H 0x45
#define ICM20_GYRO_YOUT_L 0x46
#define ICM20_GYRO_ZOUT_H 0x47
#define ICM20_GYRO_ZOUT_L 0x48
#define ICM20_SIGNAL_PATH_RESET 0x68
#define ICM20_ACCEL_INTEL_CTRL 0x69
#define ICM20_USER_CTRL 0x6A
#define ICM20_PWR_MGMT_1 0x6B
#define ICM20_PWR_MGMT_2 0x6C
#define ICM20_FIFO_COUNTH 0x72
#define ICM20_FIFO_COUNTL 0x73
#define ICM20_FIFO_R_W 0x74
#define ICM20_WHO_AM_I 0x75
/* 加速度静态偏移 */
#define ICM20_XA_OFFSET_H 0x77
#define ICM20_XA_OFFSET_L 0x78
#define ICM20_YA_OFFSET_H 0x7A
#define ICM20_YA_OFFSET_L 0x7B
#define ICM20_ZA_OFFSET_H 0x7D
#define ICM20_ZA_OFFSET_L 0x7E
#endif
1.3.2.2.2 icm20608.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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
#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_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "icm20608reg.h"
#define ICM20608_CNT 1
#define ICM20608_NAME "icm20608"
struct icm20608_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
int major; /* 主设备号 */
void *private_data; /* 私有数据 */
signed int gyro_x_adc; /* 陀螺仪X轴原始值 */
signed int gyro_y_adc; /* 陀螺仪Y轴原始值 */
signed int gyro_z_adc; /* 陀螺仪Z轴原始值 */
signed int accel_x_adc; /* 加速度计X轴原始值 */
signed int accel_y_adc; /* 加速度计Y轴原始值 */
signed int accel_z_adc; /* 加速度计Z轴原始值 */
signed int temp_adc; /* 温度原始值 */
};
static struct icm20608_dev icm20608dev;

/*
* @description : 从icm20608读取多个寄存器数据
* @param - dev: icm20608设备
* @param - reg: 要读取的寄存器首地址
* @param - val: 读取到的数据
* @param - len: 要读取的数据长度
* @return : 操作结果
*/
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
int ret = -1;
unsigned char txdata[1];
unsigned char * rxdata;
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)dev->private_data;

t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
if(!t) {
return -ENOMEM;
}

rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL); /* 申请内存 */
if(!rxdata) {
goto out1;
}
#if 1
/* 一共发送len+1个字节的数据,第一个字节为
寄存器首地址,一共要读取len个字节长度的数据,*/
txdata[0] = reg | 0x80; /* 写数据的时候首寄存器地址bit8要置1 */
t->tx_buf = txdata; /* 要发送的数据 */
t->rx_buf = rxdata; /* 要读取的数据 */
t->len = len+1; /* t->len=发送的长度+读取的长度 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
#else
//第一步,发送寄存器地址
txdata[0] = reg | 0x80;
t->tx_buf = txdata;
t->len = 1;
spi_message_init(&m);
spi_message_add_tail(t, &m);
ret = spi_sync(spi, &m);

//第二步,读取数据
txdata[0] = 0xff;
t->rx_buf = buf;
t->len = len;
spi_message_init(&m);
spi_message_add_tail(t, &m);
ret = spi_sync(spi, &m);
#endif
if(ret) {
goto out2;
}
memcpy(buf , rxdata+1, len); /* 只需要读取的数据 */
out2:
kfree(rxdata); /* 释放内存 */
out1:
kfree(t); /* 释放内存 */
return ret;
}
/*
* @description : 向icm20608多个寄存器写入数据
* @param - dev: icm20608设备
* @param - reg: 要写入的寄存器首地址
* @param - val: 要写入的数据缓冲区
* @param - len: 要写入的数据长度
* @return : 操作结果
*/
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
int ret = -1;
unsigned char *txdata;
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)dev->private_data;

t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
if(!t) {
return -ENOMEM;
}

txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
if(!txdata) {
goto out1;
}
/* 一共发送len+1个字节的数据,第一个字节为
寄存器首地址,len为要写入的寄存器的集合,*/
#if 1
*txdata = reg & ~0x80; /* 写数据的时候首寄存器地址bit8要清零 */
memcpy(txdata+1, buf, len); /* 把len个寄存器拷贝到txdata里,等待发送 */
t->tx_buf = txdata; /* 要发送的数据 */
t->len = len+1; /* t->len=发送的长度+读取的长度 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
#else
//第一步,发送寄存器地址
txdata[0] = reg & ~0x80;
t->tx_buf = txdata;
t->len = 1;
spi_message_init(&m);
spi_message_add_tail(t, &m);
ret = spi_sync(spi, &m);

//第二步,写入数据
t->tx_buf = buf;
t->len = len;
spi_message_init(&m);
spi_message_add_tail(t, &m);
ret = spi_sync(spi, &m);
#endif
if(ret) {
goto out2;
}
out2:
kfree(txdata); /* 释放内存 */
out1:
kfree(t); /* 释放内存 */
return ret;
}
/*
* @description : 读取icm20608指定寄存器值,读取一个寄存器
* @param - dev: icm20608设备
* @param - reg: 要读取的寄存器
* @return : 读取到的寄存器值
*/
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg) {
u8 data = 0;
icm20608_read_regs(dev, reg, &data, 1);
return data;
}
/*
* @description : 向icm20608指定寄存器写入指定的值,写一个寄存器
* @param - dev: icm20608设备
* @param - reg: 要写的寄存器
* @param - data: 要写入的值
* @return : 无
*/
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value) {
u8 buf = value;
icm20608_write_regs(dev, reg, &buf, 1);
}
void icm20608_readdata(struct icm20608_dev *dev) {
unsigned char data[14] = { 0 };
icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);

dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]);
dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]);
dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]);
dev->temp_adc = (signed short)((data[6] << 8) | data[7]);
dev->gyro_x_adc = (signed short)((data[8] << 8) | data[9]);
dev->gyro_y_adc = (signed short)((data[10] << 8) | data[11]);
dev->gyro_z_adc = (signed short)((data[12] << 8) | data[13]);
}


static int icm20608_open(struct inode *inode, struct file *filp){
filp->private_data = &icm20608dev; /* 设置私有数据 */
return 0;
}
static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off){
signed int data[7];
long err = 0;
struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;

icm20608_readdata(dev);
data[0] = dev->gyro_x_adc;
data[1] = dev->gyro_y_adc;
data[2] = dev->gyro_z_adc;
data[3] = dev->accel_x_adc;
data[4] = dev->accel_y_adc;
data[5] = dev->accel_z_adc;
data[6] = dev->temp_adc;
err = copy_to_user(buf, data, sizeof(data));
return 0;
}
static int icm20608_release(struct inode *inode, struct file *filp){
return 0;
}
static const struct file_operations icm20608_ops = {
.owner = THIS_MODULE,
.open = icm20608_open,
.read = icm20608_read,
.release = icm20608_release,
};

void icm20608_reginit(void){
u8 value = 0;
icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);
mdelay(50);
icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);
mdelay(50);
value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);
printk("ICM20608 ID = %#X\r\n", value);
icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00); /* 输出速率是内部采样率 */
icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18); /* 陀螺仪±2000dps量程 */
icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18); /* 加速度计±16G量程 */
icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04); /* 陀螺仪低通滤波BW=20Hz */
icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz */
icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00); /* 打开加速度计和陀螺仪所有轴 */
icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00); /* 关闭低功耗 */
icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00); /* 关闭FIFO */
}
static int icm20608_probe(struct spi_device *spi){
if (icm20608dev.major) {
icm20608dev.devid = MKDEV(icm20608dev.major, 0);
register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
} else {
alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
icm20608dev.major = MAJOR(icm20608dev.devid);
}

cdev_init(&icm20608dev.cdev, &icm20608_ops);
cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);

icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
if (IS_ERR(icm20608dev.class)) {
return PTR_ERR(icm20608dev.class);
}
icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);
if (IS_ERR(icm20608dev.device)) {
return PTR_ERR(icm20608dev.device);
}

/*初始化spi_device */
spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
spi_setup(spi);
icm20608dev.private_data = spi; /* 设置私有数据 */
/* 初始化ICM20608内部寄存器 */
icm20608_reginit();
return 0;
}
static int icm20608_remove(struct spi_device *spi) {
cdev_del(&icm20608dev.cdev);
unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
device_destroy(icm20608dev.class, icm20608dev.devid);
class_destroy(icm20608dev.class);
return 0;
}

/* 传统匹配方式ID列表 */
static const struct spi_device_id icm20608_id[] = {
{"alientek,icm20608", 0},
{}
};

/* 设备树匹配列表 */
static const struct of_device_id icm20608_of_match[] = {
{ .compatible = "alientek,icm20608" },
{ /* Sentinel */ }
};

/* SPI驱动结构体 */
static struct spi_driver icm20608_driver = {
.probe = icm20608_probe,
.remove = icm20608_remove,
.driver = {
.owner = THIS_MODULE,
.name = "icm20608",
.of_match_table = icm20608_of_match,
},
.id_table = icm20608_id,
};

static int __init icm20608_init(void)
{
return spi_register_driver(&icm20608_driver);
}

static void __exit icm20608_exit(void)
{
spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
1.3.2.2.2.1 驱动过程分析#
  1. 作为一个spi从设备驱动,需要定义一个spi_driver icm20608_driver。通过spi_register_driverspi_unregister_driver注册卸载。

  2. compatible匹配执行probe。

    1. 定一个icm20608_dev,按照字符设备框架,注册字符设备。
    2. spi_setup设置spi_device从设备, 设置spi设备的模式和速率。
      image-20240831182608432
    3. 初始化ICM20608内部寄存器(发起spi传输)。
      1. 设置启动时序,使能读写。
      2. 读id。
      3. 设置量程,速率。
  3. ICM20608的读写

  4. icm20608_read调用icm20608_readdata,调用icm20608_read_regs

  5. icm20608_write_regsicm20608_read_regs用来spi协议让主控去读写spi从设备,都是通过spi_sync进行数据传输。

image

image

  将函数精简话一下:

image

image

注意精简后有个bug, 就是调用spi_write后,又继续调用spi_read。这时imx6ul spi控制器驱动内部会帮忙控制cs片选信号。又会重新拉高,再拉低cs片选。因此导致数据传输异常。nxp官方也是用的gpio作为cs片选,软件手动去控制的。因此优化如下:

image

image

probe读出icm20608 id为:

image

1.3.2.2.3 icm20608App.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
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd;
char *filename;
signed int databuf[7];
unsigned char data[14];
signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
signed int accel_x_adc, accel_y_adc, accel_z_adc;
signed int temp_adc;

float gyro_x_act, gyro_y_act, gyro_z_act;
float accel_x_act, accel_y_act, accel_z_act;
float temp_act;
int ret = 0;

if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0) {
printf("can't open file %s\r\n", filename);
return -1;
}
while (1) {
ret = read(fd, databuf, sizeof(databuf));
if(ret == 0) { /* 数据读取成功 */
gyro_x_adc = databuf[0];
gyro_y_adc = databuf[1];
gyro_z_adc = databuf[2];
accel_x_adc = databuf[3];
accel_y_adc = databuf[4];
accel_z_adc = databuf[5];
temp_adc = databuf[6];

/* 计算实际值 */
gyro_x_act = (float)(gyro_x_adc) / 16.4;
gyro_y_act = (float)(gyro_y_adc) / 16.4;
gyro_z_act = (float)(gyro_z_adc) / 16.4;
accel_x_act = (float)(accel_x_adc) / 2048;
accel_y_act = (float)(accel_y_adc) / 2048;
accel_z_act = (float)(accel_z_adc) / 2048;
temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;
printf("\r\n原始值:\r\n");
printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
printf("temp = %d\r\n", temp_adc);
printf("实际值:");
printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
printf("act temp = %.2f°C\r\n", temp_act);
}
usleep(100000); /*100ms */
}
close(fd);
return 0;
}

测试结果:

image

1.3.3 SPI万能从设备驱动-spidev.c#

万能SPI从设备驱动对应spidev, 驱动代码位于/drivers/spi/spidev.c。不用为每个spi从设备去写一个驱动,linux内核有一个万能通用的从设备驱动。

image

为什么说spidev.c是一个通用的从设备驱动。

spidev不是专门针对某一SPI硬件设备做的驱动,只是简单的注册字符设备,用户空间通过read、write、ioctl,直接对spi进行操作,相当于是用户空间和spi设备的桥梁。用户空间操作时,打开设备节点,然后通过IOCTL设置模式、速度等参数,然后就可以调用read write进行操作了。

1.3.3.1 spidev_init#

image

注册了字符设备,主设备号SPIDEV_MAJOR= 153,绑定spidev_fops

创建了spidev的class,创建完成后在用户空间/sys/class/下可以看到spidev目录结构。

spi_register_driver按照标准流程注册spidev从设备驱动。

1.3.3.2 spidev的probe#

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
static const struct of_device_id spidev_dt_ids[] = {
{ .compatible = "rohm,dh2228fv" },
{ .compatible = "lineartechnology,ltc2488" },
{ .compatible = "ge,achc" },
{ .compatible = "nanopi,spidev" },
{ .compatible = "semtech,sx1301" },
{},
};
static struct spi_driver spidev_spi_driver = {
.driver = {
.name = "spidev",
.of_match_table = of_match_ptr(spidev_dt_ids),
.acpi_match_table = ACPI_PTR(spidev_acpi_ids),
},
.probe = spidev_probe,
.remove = spidev_remove,
};
static int spidev_probe(struct spi_device *spi){
struct spidev_data *spidev;
int status;
unsigned long minor;

if (spi->dev.of_node && !of_match_device(spidev_dt_ids, &spi->dev)) {
dev_err(&spi->dev, "buggy DT: spidev listed directly in DT\n");
WARN_ON(spi->dev.of_node &&
!of_match_device(spidev_dt_ids, &spi->dev));
}

spidev_probe_acpi(spi);
/* Allocate driver data */
spidev = kzalloc(sizeof(*spidev), GFP_KERNEL);
if (!spidev)
return -ENOMEM;
/* Initialize the driver data */
spidev->spi = spi;
spin_lock_init(&spidev->spi_lock);
mutex_init(&spidev->buf_lock);
INIT_LIST_HEAD(&spidev->device_entry);
/* If we can allocate a minor number, hook up this device.
* Reusing minors is fine so long as udev or mdev is working.
*/
mutex_lock(&device_list_lock);
minor = find_first_zero_bit(minors, N_SPI_MINORS);
if (minor < N_SPI_MINORS) {
struct device *dev;
spidev->devt = MKDEV(SPIDEV_MAJOR, minor);
dev = device_create(spidev_class, &spi->dev, spidev->devt,
spidev, "spidev%d.%d",
spi->master->bus_num, spi->chip_select);
status = PTR_ERR_OR_ZERO(dev);
} else {
dev_dbg(&spi->dev, "no minor number available!\n");
status = -ENODEV;
}
if (status == 0) {
set_bit(minor, minors);
list_add(&spidev->device_entry, &device_list);
}
mutex_unlock(&device_list_lock);
spidev->speed_hz = spi->max_speed_hz;
if (status == 0)
spi_set_drvdata(spi, spidev);
else
kfree(spidev);

return status;
}

调用device_create创建了/dev/下的spidev节点,主设备号SPIDEV_MAJOR= 153,如spi总线0cs1设备,则设备名为/dev/spidev0.1,其他以此类推。

1.3.3.3 spidev_fops#

1.3.3.3.1 spidev_read#
1
2
3
4
spidev_read
->spidev_sync_read
->spidev_sync
->spi_sync
1.3.3.3.2 spidev_write#
1
2
3
4
spidev_write
->spidev_sync_write
->spidev_sync
->spi_sync

image

构造spi_message,spi_transfer调用spi_sync进行数据传输。

1.3.3.3.3 spidev_ioctl#
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
static long spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
int retval = 0;
struct spidev_data *spidev;
struct spi_device *spi;
u32 tmp;
unsigned n_ioc;
struct spi_ioc_transfer *ioc;
/* Check type and command number */
if (_IOC_TYPE(cmd) != SPI_IOC_MAGIC)
return -ENOTTY;
/* guard against device removal before, or while,
* we issue this ioctl.
*/
spidev = filp->private_data;
spin_lock_irq(&spidev->spi_lock);
spi = spi_dev_get(spidev->spi);
spin_unlock_irq(&spidev->spi_lock);

if (spi == NULL)
return -ESHUTDOWN;

/* use the buffer lock here for triple duty:
* - prevent I/O (from us) so calling spi_setup() is safe;
* - prevent concurrent SPI_IOC_WR_* from morphing
* data fields while SPI_IOC_RD_* reads them;
* - SPI_IOC_MESSAGE needs the buffer locked "normally".
*/
mutex_lock(&spidev->buf_lock);
switch (cmd) {
/* read requests */
case SPI_IOC_RD_MODE:
retval = put_user(spi->mode & SPI_MODE_MASK,
(__u8 __user *)arg);
break;
case SPI_IOC_RD_MODE32:
retval = put_user(spi->mode & SPI_MODE_MASK,
(__u32 __user *)arg);
break;
case SPI_IOC_RD_LSB_FIRST:
retval = put_user((spi->mode & SPI_LSB_FIRST) ? 1 : 0,
(__u8 __user *)arg);
break;
case SPI_IOC_RD_BITS_PER_WORD:
retval = put_user(spi->bits_per_word, (__u8 __user *)arg);
break;
case SPI_IOC_RD_MAX_SPEED_HZ:
retval = put_user(spidev->speed_hz, (__u32 __user *)arg);
break;

/* write requests */
case SPI_IOC_WR_MODE:
case SPI_IOC_WR_MODE32:
if (cmd == SPI_IOC_WR_MODE)
retval = get_user(tmp, (u8 __user *)arg);
else
retval = get_user(tmp, (u32 __user *)arg);
if (retval == 0) {
struct spi_controller *ctlr = spi->controller;
u32 save = spi->mode;

if (tmp & ~SPI_MODE_MASK) {
retval = -EINVAL;
break;
}

if (ctlr->use_gpio_descriptors && ctlr->cs_gpiods &&
ctlr->cs_gpiods[spi->chip_select])
tmp |= SPI_CS_HIGH;

tmp |= spi->mode & ~SPI_MODE_MASK;
spi->mode = (u16)tmp;
retval = spi_setup(spi);
if (retval < 0)
spi->mode = save;
else
dev_dbg(&spi->dev, "spi mode %x\n", tmp);
}
break;
case SPI_IOC_WR_LSB_FIRST:
retval = get_user(tmp, (__u8 __user *)arg);
if (retval == 0) {
u32 save = spi->mode;

if (tmp)
spi->mode |= SPI_LSB_FIRST;
else
spi->mode &= ~SPI_LSB_FIRST;
retval = spi_setup(spi);
if (retval < 0)
spi->mode = save;
else
dev_dbg(&spi->dev, "%csb first\n",
tmp ? 'l' : 'm');
}
break;
case SPI_IOC_WR_BITS_PER_WORD:
retval = get_user(tmp, (__u8 __user *)arg);
if (retval == 0) {
u8 save = spi->bits_per_word;

spi->bits_per_word = tmp;
retval = spi_setup(spi);
if (retval < 0)
spi->bits_per_word = save;
else
dev_dbg(&spi->dev, "%d bits per word\n", tmp);
}
break;
case SPI_IOC_WR_MAX_SPEED_HZ:
retval = get_user(tmp, (__u32 __user *)arg);
if (retval == 0) {
u32 save = spi->max_speed_hz;

spi->max_speed_hz = tmp;
retval = spi_setup(spi);
if (retval == 0) {
spidev->speed_hz = tmp;
dev_dbg(&spi->dev, "%d Hz (max)\n",
spidev->speed_hz);
}
spi->max_speed_hz = save;
}
break;
default:
/* segmented and/or full-duplex I/O request */
/* Check message and copy into scratch area */
ioc = spidev_get_ioc_message(cmd,
(struct spi_ioc_transfer __user *)arg, &n_ioc);
if (IS_ERR(ioc)) {
retval = PTR_ERR(ioc);
break;
}
if (!ioc)
break; /* n_ioc is also 0 */
/* translate to spi_message, execute */
retval = spidev_message(spidev, ioc, n_ioc);
kfree(ioc);
break;
}
mutex_unlock(&spidev->buf_lock);
spi_dev_put(spi);
return retval;
}

image

image

这些SPI_IOC命令就是一些设置速率参数,spi模式啊,然后就可以通过read,write操作/dev/spidev%d.%d设备了。

1.3.4 使用SPI万能驱动oled举例#

1.3.4.1 spi oled原理#

SPI-OLED显示面板介绍

S3c2440裸机-spi编程-2.OLED显示面板 - fuzidage - 博客园 (cnblogs.com)

S3c2440裸机-spi编程-3.gpio模拟spi驱动OLED - fuzidage - 博客园 (cnblogs.com)

1.3.4.2 spi_oled.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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include "font.h"
static int fd_spidev;
static int dc_pin_num;
void OLED_DIsp_Set_Pos(int x, int y);
void dc_pin_init(int number)
{
// echo 509 > /sys/class/gpio/export
// echo out > /sys/class/gpio/gpio509/direction
char cmd[100];
dc_pin_num = number;

sprintf(cmd, "echo %d > /sys/class/gpio/export", number);
system(cmd);
sprintf(cmd, "echo out > /sys/class/gpio/gpio%d/direction", number);
system(cmd);
}

void oled_set_dc_pin(int val)
{
/* echo 1 > /sys/class/gpio/gpio509/value
* echo 0 > /sys/class/gpio/gpio509/value
*/
char cmd[100];
sprintf(cmd, "echo %d > /sys/class/gpio/gpio%d/value", val, dc_pin_num);
system(cmd);
}

void spi_write_datas(const unsigned char *buf, int len)
{
write(fd_spidev, buf, len);
}

void oled_write_datas(const unsigned char *buf, int len)
{
oled_set_dc_pin(1);//拉高,表示写入数据
spi_write_datas(buf, len);
}

/**********************************************************************
* 函数名称: oled_write_cmd
* 功能描述: oled向特定地址写入数据或者命令
* 输入参数:@uc_data :要写入的数据
@uc_cmd:为1则表示写入数据,为0表示写入命令
* 输出参数:无
* 返 回 值: 无
***********************************************************************/
void oled_write_cmd_data(unsigned char uc_data,unsigned char uc_cmd)
{
unsigned char uc_read=0;
if(uc_cmd==0)
oled_set_dc_pin(0);
else
oled_set_dc_pin(1);//拉高,表示写入数据

spi_write_datas(&uc_data, 1);//写入
}
int oled_init(void)
{
unsigned char uc_dev_id = 0;

oled_write_cmd_data(0xae,OLED_CMD);//关闭显示
oled_write_cmd_data(0x00,OLED_CMD);//设置 lower column address
oled_write_cmd_data(0x10,OLED_CMD);//设置 higher column address
oled_write_cmd_data(0x40,OLED_CMD);//设置 display start line
oled_write_cmd_data(0xB0,OLED_CMD);//设置page address
oled_write_cmd_data(0x81,OLED_CMD);// contract control
oled_write_cmd_data(0x66,OLED_CMD);//128
oled_write_cmd_data(0xa1,OLED_CMD);//设置 segment remap
oled_write_cmd_data(0xa6,OLED_CMD);//normal /reverse
oled_write_cmd_data(0xa8,OLED_CMD);//multiple ratio
oled_write_cmd_data(0x3f,OLED_CMD);//duty = 1/64
oled_write_cmd_data(0xc8,OLED_CMD);//com scan direction
oled_write_cmd_data(0xd3,OLED_CMD);//set displat offset
oled_write_cmd_data(0x00,OLED_CMD);//
oled_write_cmd_data(0xd5,OLED_CMD);//set osc division
oled_write_cmd_data(0x80,OLED_CMD);//
oled_write_cmd_data(0xd9,OLED_CMD);//ser pre-charge period
oled_write_cmd_data(0x1f,OLED_CMD);//
oled_write_cmd_data(0xda,OLED_CMD);//set com pins
oled_write_cmd_data(0x12,OLED_CMD);//
oled_write_cmd_data(0xdb,OLED_CMD);//set vcomh
oled_write_cmd_data(0x30,OLED_CMD);//
oled_write_cmd_data(0x8d,OLED_CMD);//set charge pump disable
oled_write_cmd_data(0x14,OLED_CMD);//
oled_write_cmd_data(0xaf,OLED_CMD);//set dispkay on
return 0;
}
/**********************************************************************
* 函数名称: oled_fill_data
* 功能描述: 整个屏幕显示填充某个固定数据
* 输入参数:@fill_Data:要填充的数据
***********************************************************************/
int oled_fill_data(unsigned char fill_Data)
{
unsigned char x,y;
for(x=0;x<8;x++) {
oled_write_cmd_data(0xb0+x,OLED_CMD); //page0-page1
oled_write_cmd_data(0x00,OLED_CMD); //low column start address
oled_write_cmd_data(0x10,OLED_CMD); //high column start address
for(y=0;y<128;y++)
oled_write_cmd_data(fill_Data,OLED_DATA);//填充数据
}
return 0;
}
void OLED_DIsp_Clear(void)
{
unsigned char x, y;
char buf[128];
memset(buf, 0, 128);
for (y = 0; y < 8; y++) {
OLED_DIsp_Set_Pos(0, y);
//for (x = 0; x < 128; x++)
// oled_write_cmd_data(0, OLED_DATA); /* 清零 */
oled_write_datas(buf, 128);
}
}

/**********************************************************************
* 函数名称: OLED_DIsp_All
* 功能描述: 整个屏幕显示全部点亮,可以用于检查坏点
***********************************************************************/
void OLED_DIsp_All(void)
{
unsigned char x, y;
for (y = 0; y < 8; y++)
{
OLED_DIsp_Set_Pos(0, y);
for (x = 0; x < 128; x++)
oled_write_cmd_data(0xff, OLED_DATA); /* 全点亮 */
}
}

//坐标设置
/**********************************************************************
* 函数名称: OLED_DIsp_Set_Pos
* 功能描述:设置要显示的位置
* 输入参数:@ x :要显示的column address
@y :要显示的page address
***********************************************************************/
void OLED_DIsp_Set_Pos(int x, int y)
{ oled_write_cmd_data(0xb0+y,OLED_CMD);
oled_write_cmd_data((x&0x0f),OLED_CMD);
oled_write_cmd_data(((x&0xf0)>>4)|0x10,OLED_CMD);
}
void OLED_DIsp_Char(int x, int y, unsigned char c)
{
int i = 0;
/* 得到字模 */
const unsigned char *dots = oled_asc2_8x16[c - ' '];

/* 发给OLED */
OLED_DIsp_Set_Pos(x, y);
/* 发出8字节数据 */
//for (i = 0; i < 8; i++)
// oled_write_cmd_data(dots[i], OLED_DATA);
oled_write_datas(&dots[0], 8);

OLED_DIsp_Set_Pos(x, y+1);
/* 发出8字节数据 */
//for (i = 0; i < 8; i++)
//oled_write_cmd_data(dots[i+8], OLED_DATA);
oled_write_datas(&dots[8], 8);
}

void OLED_DIsp_String(int x, int y, char *str)
{
unsigned char j=0;
while (str[j]){
OLED_DIsp_Char(x, y, str[j]);//显示单个字符
x += 8;
if(x > 127){
x = 0;
y += 2;
}//移动显示位置
j++;
}
}

void OLED_DIsp_Test(void)
{
int i;
OLED_DIsp_String(0, 0, "wiki.100ask.net");
OLED_DIsp_String(0, 2, "book.100ask.net");
OLED_DIsp_String(0, 4, "bbs.100ask.net");
}

/* spi_oled /dev/spidevB.D <DC_pin_number> */
int main(int argc, char **argv)
{
int dc_pin;

if (argc != 3){
printf("Usage: %s <dev/spidevB.D> <DC_pin_number>\n", argv[0]);//B表示spi bus, D表示cs片选
return -1;
}

fd_spidev = open(argv[1], O_RDWR);
if (fd_spidev < 0) {
printf("open %s err\n", argv[1]);
return -1;
}

dc_pin = strtoul(argv[2], NULL, 0);
dc_pin_init(dc_pin);

oled_init();
OLED_DIsp_Clear();
OLED_DIsp_Test();
return 0;
}
1.3.4.2.1 用户态驱动分析#

有了spidev.c万能SPI从设备驱动,就不需要去写spi从设备驱动了,直接用户态去读写spidev即可。

  1. open("/dev/spi%d.%d", O_RDWR);//根据自己外设使用的spi bus和cs片选去设置
  2. 对外设oled进行初始化D/C(data or cmd )引脚, 利用gpio子系统的命令去设置gpio模式为输出。
  3. 利用spi协议发送初始化序列:
    1. spi_write_datas就是操作spidev,write数据到底层spidev,再调用对应的fops中的spidev_write,最终调用spi_sync传输数据。这里是每次传输1byte,把初始化序列利用spi协议写完。
  4. 清屏并且测试oled显示字符。

1.3.5 不使用SPI万能驱动oled举例#

1.3.5.1 dts描述#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
fsl,spi-num-chipselects = <2>;
cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>, <&gpio4 24 GPIO_ACTIVE_LOW>;
status = "okay";

oled: oled {
compatible = "100ask,oled";
reg = <0>;
spi-max-frequency = <10000000>;
dc-gpios = <&gpio4 20 GPIO_ACTIVE_HIGH>;
};
};

spi1下接了一个spi oled,修改对应dts文件,修改ecspi1节点,添加oled子节点。oled有一个dc引脚,叫做data/cmd引脚,选择是发送数据还是命令。参考spi oled原理。

1.3.5.2 oled_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
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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/list.h>
#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/compat.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/acpi.h>
#include <linux/spi/spi.h>
#include <linux/spi/spidev.h>
#include <linux/uaccess.h>
#include <linux/gpio/consumer.h>

#define OLED_IOC_INIT 123
#define OLED_IOC_SET_POS 124

//为0 表示命令,为1表示数据
#define OLED_CMD 0
#define OLED_DATA 1

static struct spi_device *oled;
static int major;
static struct gpio_desc *dc_gpio;

static void dc_pin_init(void) {
gpiod_direction_output(dc_gpio, 1);
}

static void oled_set_dc_pin(int val) {
gpiod_set_value(dc_gpio, val);
}

static void spi_write_datas(const unsigned char *buf, int len) {
spi_write(oled, buf, len);
}

static void oled_write_cmd_data(unsigned char uc_data,unsigned char uc_cmd) {
if(uc_cmd==0)
oled_set_dc_pin(0);
else
oled_set_dc_pin(1);//拉高,表示写入数据

spi_write_datas(&uc_data, 1);//写入
}

static int oled_init(void) {
oled_write_cmd_data(0xae,OLED_CMD);//关闭显示
oled_write_cmd_data(0x00,OLED_CMD);//设置 lower column address
oled_write_cmd_data(0x10,OLED_CMD);//设置 higher column address
oled_write_cmd_data(0x40,OLED_CMD);//设置 display start line
oled_write_cmd_data(0xB0,OLED_CMD);//设置page address
oled_write_cmd_data(0x81,OLED_CMD);// contract control
oled_write_cmd_data(0x66,OLED_CMD);//128
oled_write_cmd_data(0xa1,OLED_CMD);//设置 segment remap
oled_write_cmd_data(0xa6,OLED_CMD);//normal /reverse
oled_write_cmd_data(0xa8,OLED_CMD);//multiple ratio
oled_write_cmd_data(0x3f,OLED_CMD);//duty = 1/64
oled_write_cmd_data(0xc8,OLED_CMD);//com scan direction
oled_write_cmd_data(0xd3,OLED_CMD);//set displat offset
oled_write_cmd_data(0x00,OLED_CMD);//
oled_write_cmd_data(0xd5,OLED_CMD);//set osc division
oled_write_cmd_data(0x80,OLED_CMD);//
oled_write_cmd_data(0xd9,OLED_CMD);//ser pre-charge period
oled_write_cmd_data(0x1f,OLED_CMD);//
oled_write_cmd_data(0xda,OLED_CMD);//set com pins
oled_write_cmd_data(0x12,OLED_CMD);//
oled_write_cmd_data(0xdb,OLED_CMD);//set vcomh
oled_write_cmd_data(0x30,OLED_CMD);//
oled_write_cmd_data(0x8d,OLED_CMD);//set charge pump disable
oled_write_cmd_data(0x14,OLED_CMD);//
oled_write_cmd_data(0xaf,OLED_CMD);//set dispkay on

return 0;
}

static void OLED_DIsp_Set_Pos(int x, int y)
{ oled_write_cmd_data(0xb0+y,OLED_CMD);
oled_write_cmd_data((x&0x0f),OLED_CMD);
oled_write_cmd_data(((x&0xf0)>>4)|0x10,OLED_CMD);
}

static long spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
int x, y;
/* 根据cmd操作硬件 */
switch (cmd) {
case OLED_IOC_INIT: /* init */
{
dc_pin_init();
oled_init();
break;
}
case OLED_IOC_SET_POS: /* set pos */
{
x = arg & 0xff;
y = (arg >> 8) & 0xff;
OLED_DIsp_Set_Pos(x, y);
break;
}
}
return 0;
}

static ssize_t spidev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
char *ker_buf;
int err;
ker_buf = kmalloc(count, GFP_KERNEL);
err = copy_from_user(ker_buf, buf, count);
oled_set_dc_pin(1);//拉高,表示写入数据
spi_write_datas(ker_buf, count);
kfree(ker_buf);
return count;
}

static const struct file_operations spidev_fops = {
.owner = THIS_MODULE,
.write = spidev_write,
.unlocked_ioctl = spidev_ioctl,
};

static struct class *spidev_class;
static const struct of_device_id spidev_dt_ids[] = {
{ .compatible = "100ask,oled" },
{},
};

static int spidev_probe(struct spi_device *spi)
{
oled = spi;

major = register_chrdev(0, "100ask_oled", &spidev_fops);
spidev_class = class_create(THIS_MODULE, "100ask_oled");
device_create(spidev_class, NULL, MKDEV(major, 0), NULL, "100ask_oled");

/* 3. 获得GPIO引脚 */
dc_gpio = gpiod_get(&spi->dev, "dc", 0);
return 0;
}

static int spidev_remove(struct spi_device *spi) {
gpiod_put(dc_gpio);
device_destroy(spidev_class, MKDEV(major, 0));
class_destroy(spidev_class);
unregister_chrdev(major, "cumtchw_oled");
return 0;
}
static struct spi_driver spidev_spi_driver = {
.driver = {
.name = "cumtchw_spi_oled_drv",
.of_match_table = of_match_ptr(spidev_dt_ids),
},
.probe = spidev_probe,
.remove = spidev_remove,
};
static int __init spidev_init(void) {
int status;
status = spi_register_driver(&spidev_spi_driver);
if (status < 0) {
}
return status;
}
static void __exit spidev_exit(void){
spi_unregister_driver(&spidev_spi_driver);
}
module_init(spidev_init);
module_exit(spidev_exit);
MODULE_LICENSE("GPL");

1.3.5.2.1 驱动分析#
  1. 调用spi_register_driver注册SPI从设备驱动,probe函数中利用标准字符设备驱动框架编写的驱动,注册字符设备,添加类。
  2. 当用户调用open("/dev/100ask_oled")后,就可以调用read,write函数读写oled。
    1. spidev_ioctl提供了2个ioctl命令,用来初始化oled和设置坐标位置。用spi_write_datas写入初始化序列,每次写1byte。
    2. spi_write_datas调用标准的SPI从设备驱动API(spi_write)传输数据。
  3. spi oled初始化完后就可以使用另一个OLED_IOC_SET_POS ioctl命令设置坐标位置。
  4. 调用spidev_write写入数据。

1.3.5.3 spi_oled.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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include "font.h"
#define OLED_IOC_INIT 123
#define OLED_IOC_SET_POS 124

//为0 表示命令,为1表示数据
#define OLED_CMD 0
#define OLED_DATA 1
static int fd_spidev;
static int dc_pin_num;
void OLED_DIsp_Set_Pos(int x, int y);

void oled_write_datas(const unsigned char *buf, int len)
{
write(fd_spidev, buf, len);
}
void OLED_DIsp_Clear(void) {
unsigned char x, y;
char buf[128];
memset(buf, 0, 128);
for (y = 0; y < 8; y++) {
OLED_DIsp_Set_Pos(0, y);
oled_write_datas(buf, 128);
}
}

void OLED_DIsp_All(void) {
unsigned char x, y;
char buf[128];
memset(buf, 0xff, 128);
for (y = 0; y < 8; y++) {
OLED_DIsp_Set_Pos(0, y);
oled_write_datas(buf, 128);
}
}

void OLED_DIsp_Set_Pos(int x, int y)
{
ioctl(fd_spidev, OLED_IOC_SET_POS, x | (y << 8));
}

void OLED_DIsp_Char(int x, int y, unsigned char c)
{
int i = 0;
/* 得到字模 */
const unsigned char *dots = oled_asc2_8x16[c - ' '];

/* 发给OLED */
OLED_DIsp_Set_Pos(x, y);
/* 发出8字节数据 */
//for (i = 0; i < 8; i++)
// oled_write_cmd_data(dots[i], OLED_DATA);
oled_write_datas(&dots[0], 8);

OLED_DIsp_Set_Pos(x, y+1);
/* 发出8字节数据 */
//for (i = 0; i < 8; i++)
//oled_write_cmd_data(dots[i+8], OLED_DATA);
oled_write_datas(&dots[8], 8);
}
void OLED_DIsp_String(int x, int y, char *str)
{
unsigned char j=0;
while (str[j]) {
OLED_DIsp_Char(x, y, str[j]);//显示单个字符
x += 8;
if(x > 127) {
x = 0;
y += 2;
}//移动显示位置
j++;
}
}
void OLED_DIsp_Test(void) {
int i;
OLED_DIsp_String(0, 0, "100ask test");
}

int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s /dev/cumtchw_oled\n", argv[0]);
return -1;
}

fd_spidev = open(argv[1], O_RDWR);
if (fd_spidev < 0) {
printf("open %s err\n", argv[1]);
return -1;
}
ioctl(fd_spidev, OLED_IOC_INIT);
OLED_DIsp_Clear();
OLED_DIsp_Test();
return 0;
}

2 数据结构#

2.1 spi_master/spi_controller#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct spi_master {
struct device dev; /* SPI设备的device数据结构 */
s16 bus_num; /* SPI总线序号 */
u16 num_chipselect; /* 片选信号数量 */
u16 dma_alignment; /* SPI控制器DMA缓冲区对齐定义 */
u16 mode_bits; /* 工作模式位,由驱动定义 */
u32 min_speed_hz; /* 最小速度 */
u32 max_speed_hz; /* 最小速度 */
u16 flags; /* 限制条件标志 */
int (*setup)(struct spi_device *spi); /* 设置SPI设备的工作参数 */
int (*transfer)(struct spi_device *spi, /* SPI发送函数1 */
struct spi_message *mesg);
void (*cleanup)(struct spi_device *spi); /* SPI清除函数,当spi_master被释放时调用 */
int (*transfer_one_message)(struct spi_master *master, /* SPI发送函数2 */
int (*transfer_one)(struct spi_master *master, struct spi_device *spi,/* SPI发送函数3 */
struct spi_transfer *transfer);
...
};

struct spi_master描述一个spi控制器,包扩spi控制器的属性描述信息,spi的一些操作回调函数,如transfersetup

2.2 spi_driver#

1
2
3
4
5
6
7
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);
struct device_driver driver;
};

用来描述一个spi从设备驱动。用来和spi从设备匹配。

2.3 spi_device#

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
struct spi_device {
struct device dev;
struct spi_controller *controller;
struct spi_controller *master; /* compatibility layer */
u32 max_speed_hz;
u8 chip_select;
#define SPI_CPHA 0x01 /* clock phase */
#define SPI_CPOL 0x02 /* clock polarity */
#define SPI_MODE_0 (0|0) /* (original MicroWire) */
#define SPI_MODE_1 (0|SPI_CPHA)
#define SPI_MODE_2 (SPI_CPOL|0)
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)
#define SPI_CS_HIGH 0x04 /* chipselect active high? */
#define SPI_LSB_FIRST 0x08 /* per-word bits-on-wire */
#define SPI_3WIRE 0x10 /* SI/SO signals shared */
#define SPI_LOOP 0x20 /* loopback mode */
#define SPI_NO_CS 0x40 /* 1 dev/bus, no chipselect */
#define SPI_READY 0x80 /* slave pulls low to pause */
#define SPI_TX_DUAL 0x100 /* transmit with 2 wires */
#define SPI_TX_QUAD 0x200 /* transmit with 4 wires */
#define SPI_RX_DUAL 0x400 /* receive with 2 wires */
#define SPI_RX_QUAD 0x800 /* receive with 4 wires */
#define SPI_CS_WORD 0x1000 /* toggle cs after each word */
#define SPI_TX_OCTAL 0x2000 /* transmit with 8 wires */
#define SPI_RX_OCTAL 0x4000 /* receive with 8 wires */
#define SPI_3WIRE_HIZ 0x8000 /* high impedance turnaround */
int irq;
void *controller_state;
void *controller_data;
char modalias[SPI_NAME_SIZE];
const char *driver_override;
int cs_gpio; /* LEGACY: chip select gpio */
struct gpio_desc *cs_gpiod; /* chip select gpio desc */
struct spi_delay word_delay; /* inter-word delay */
};

用来描述一个spi从设备。用来和spi从设备驱动匹配。

2.4 spi_message/spi_transfer#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct spi_transfer {
const void *tx_buf;
void *rx_buf;
unsigned len;
...
u8 bits_per_word;
u16 delay_usecs;
u32 speed_hz;
...
struct list_head transfer_list;
};
struct spi_message {
struct list_head transfers;
...
struct spi_device *spi;
...
void (*complete)(void *context);
void *context;
...
struct list_head queue;
};

spi_message是发起一次数据传输,里面的transfers构成基本输入输出数据,通过spi_sync函数或spi_async函数发送。

字符设备驱动-I2C子系统

1 Linux I2C 驱动框架#

image

由上到下分为3层结构:

1
2
3
4
5
6
i2c设备驱动层: 作为client使用者使用i2c子系统。提供操作接口给应用层,与应用层交互数据。

I2C核心层:提供transfer send recv函数。把client设备挂载到I2C总线上;
维护i2c driver和i2c client 链表 ,实现i2c_client和i2c_driver匹配。

I2C适配器层:底层SOC I2C控制器驱动,实现i2c时序,实现i2c总线发送和接收数据的方法。

目录结构位于drivers/i2c:

1
2
3
4
robin.lee@WORKSTATION5:/media/robin.lee/zip/A2/linux_5.10/drivers/i2c$ ls
algos i2c-core-acpi.c i2c-core-of.c i2c-dev.c i2c-slave-testunit.c Kconfig
busses i2c-core-base.c i2c-core-slave.c i2c-mux.c i2c-smbus.c Makefile
i2c-boardinfo.c i2c-core.h i2c-core-smbus.c i2c-slave-eeprom.c i2c-stub.c muxes

2 数据结构#

2.1 控制器相关#

2.1.1 i2c_adapter-控制器#

I2C 适配器,也就是 SOC 的 I2C 控制器。i2c_adapter 结构体定义在 include/linux/i2c.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* i2c_adapter is the structure used to identify a physical i2c bus along
* with the access algorithms necessary to access it.
*/
struct i2c_adapter {
struct module *owner;
unsigned int class; /* classes to allow probing for */
const struct i2c_algorithm *algo; /* the algorithm to access the bus */
void *algo_data;

/* data fields that are valid for all devices */
struct rt_mutex bus_lock;
int timeout; /* in jiffies */
int retries;
struct device dev; /* the adapter device */
int nr; //总线的编号
char name[48];
struct completion dev_released;
struct mutex userspace_clients_lock;
struct list_head userspace_clients;
struct i2c_bus_recovery_info *bus_recovery_info;
const struct i2c_adapter_quirks *quirks;
};
变量名 解释
class 适配器的类类型,在一些口I2C设备驱动中会检查该成员,以判断设备能否被该适配器操作
algo 指向该造配器通信方法描述结构的指针,就是该适配器具体操作I2C控制器的函数
algo_data 指向通信方法数据的指针,该成员不会被I2C核心层修改,仅供具体的 i2c_algorithm使用
timeout 传输超时时间
retries 传输超时的重试次数
name 适配器名称,该名称可以通过sys/bus/i2c/devices/i2c-x/name (x=0,1,2 … )来访问
nr 总线编号(也是适配器编号),同时对应设备节点/dev/i2c-x (x=0,1,2 …)中的 x

2.1.2 i2c_algorithm-通信方法#

对于一个 I2C 适配器,肯定要对外提供读 写 API 函数,设备驱动程序可以使用这些 API 函数来完成读写操作。i2c_algorithm 就是 I2C 适 配器与 IIC 设备进行通信的方法。包括transfer send recv等函数。i2c_algorithm 结构体定义在 include/linux/i2c.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
struct i2c_algorithm {
/*
* If an adapter algorithm can't do I2C-level access, set master_xfer
* to NULL. If an adapter algorithm can do SMBus access, set
* smbus_xfer. If set to NULL, the SMBus protocol is simulated
* using common I2C messages.
*
* master_xfer should return the number of messages successfully
* processed, or a negative value on error
*/
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
int num);
int (*master_xfer_atomic)(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);
int (*smbus_xfer_atomic)(struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);
/* To determine what the adapter supports */
u32 (*functionality)(struct i2c_adapter *adap);
#if IS_ENABLED(CONFIG_I2C_SLAVE)
int (*reg_slave)(struct i2c_client *client);
int (*unreg_slave)(struct i2c_client *client);
#endif
};

master_xfer 就是 I2C 适配器的传输函数,可以通过此函数来完成与 IIC 设备之 间的通信。 用于产生I2C访问周期需要的信号, 以i2c_msg为单位(i2c_msg中的成员表明了I2C的传输地址、 方向、 缓冲区、 缓冲区长度等信息) 。

smbus_xfer 是 SMBUS 总线协议的传输函数。

functionality:查看适配的能力。这些功能都是以宏定义的方式表示,定义在include/linux/i2c.h中,以I2C_FUNC开头:

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
/* To determine what functionality is present */
#define I2C_FUNC_I2C 0x00000001
#define I2C_FUNC_10BIT_ADDR 0x00000002
#define I2C_FUNC_PROTOCOL_MANGLING 0x00000004 /* I2C_M_IGNORE_NAK etc. */
#define I2C_FUNC_SMBUS_PEC 0x00000008
#define I2C_FUNC_NOSTART 0x00000010 /* I2C_M_NOSTART */
#define I2C_FUNC_SLAVE 0x00000020
#define I2C_FUNC_SMBUS_BLOCK_PROC_CALL 0x00008000 /* SMBus 2.0 */
#define I2C_FUNC_SMBUS_QUICK 0x00010000
#define I2C_FUNC_SMBUS_READ_BYTE 0x00020000
#define I2C_FUNC_SMBUS_WRITE_BYTE 0x00040000
#define I2C_FUNC_SMBUS_READ_BYTE_DATA 0x00080000
#define I2C_FUNC_SMBUS_WRITE_BYTE_DATA 0x00100000
#define I2C_FUNC_SMBUS_READ_WORD_DATA 0x00200000
#define I2C_FUNC_SMBUS_WRITE_WORD_DATA 0x00400000
#define I2C_FUNC_SMBUS_PROC_CALL 0x00800000
#define I2C_FUNC_SMBUS_READ_BLOCK_DATA 0x01000000
#define I2C_FUNC_SMBUS_WRITE_BLOCK_DATA 0x02000000
#define I2C_FUNC_SMBUS_READ_I2C_BLOCK 0x04000000 /* I2C-like block xfer */
#define I2C_FUNC_SMBUS_WRITE_I2C_BLOCK 0x08000000 /* w/ 1-byte reg. addr. */
#define I2C_FUNC_SMBUS_HOST_NOTIFY 0x10000000
#define I2C_FUNC_SMBUS_BYTE (I2C_FUNC_SMBUS_READ_BYTE | \
I2C_FUNC_SMBUS_WRITE_BYTE)
#define I2C_FUNC_SMBUS_BYTE_DATA (I2C_FUNC_SMBUS_READ_BYTE_DATA | \
I2C_FUNC_SMBUS_WRITE_BYTE_DATA)
#define I2C_FUNC_SMBUS_WORD_DATA (I2C_FUNC_SMBUS_READ_WORD_DATA | \
I2C_FUNC_SMBUS_WRITE_WORD_DATA)
#define I2C_FUNC_SMBUS_BLOCK_DATA (I2C_FUNC_SMBUS_READ_BLOCK_DATA | \
I2C_FUNC_SMBUS_WRITE_BLOCK_DATA)
#define I2C_FUNC_SMBUS_I2C_BLOCK (I2C_FUNC_SMBUS_READ_I2C_BLOCK | \
I2C_FUNC_SMBUS_WRITE_I2C_BLOCK)

2.2 客户端相关#

i2c 设备(client使用者)驱动要使用i2c_driveri2c_client数据结构并填充i2c_driver中的成员函数。

2.2.1 i2c_driver-I从设备驱动#

代表一个i2c使用者设备驱动。结构体定义在 include/linux/i2c.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct i2c_driver {
unsigned int class;
/* Notifies the driver that a new bus has appeared. You should avoid
* using this, it will be removed in a near future.
*/
int (*attach_adapter)(struct i2c_adapter *) __deprecated;

/* Standard driver model interfaces */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);

/* New driver model interface to aid the seamless removal of the
* current probe()'s, more commonly unused than used second parameter.
*/
int (*probe_new)(struct i2c_client *);

/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *);

/* Alert callback, for example for the SMBus alert protocol.
* The format and meaning of the data value depends on the protocol.
* For the SMBus alert protocol, there is a single bit of data passed
* as the alert response's low bit ("event flag").
* For the SMBus Host Notify protocol, the data corresponds to the
* 16-bit payload data reported by the slave device acting as master.
*/
void (*alert)(struct i2c_client *, enum i2c_alert_protocol protocol,
unsigned int data); // 警告回调函数(例如SMBus警报协议)

/* a ioctl like command that can be used to perform specific functions
* with the device.
*/
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);//类似于ioctl 的命令控制函数
struct device_driver driver;
const struct i2c_device_id *id_table; // 这个i2c驱动支持的设备链表

/* Device detection callback for automatic device creation */
int (*detect)(struct i2c_client *, struct i2c_board_info *); // 检测设备的回调函数;
const unsigned short *address_list; // 要探测的I2C地址(用于检测)
struct list_head clients;
bool disable_i2c_core_irq_mapping;
};
变量名 含义
attach_adapter 依附i2c_adapter的函数指针
detach_adapter 脱离i2c_adapter的函数指针
probe 设备和驱动匹配时调用
driver 在注册i2c_driver对象时,i2c_driver->driver的总线类型被指定为i2c_bus_type
id_table 匹配列表,驱动和设备匹配时会用到
detect 基于设备探测机制实现的 12C 设备驱动:设备探测的回调函数
address_list 设备探测的地址范围
clients 探测到的设备列表

i2c_driver对应于一套驱动方法, 其主要成员函数是probe()remove()suspend()resume()等。例如:

1
2
3
4
5
6
7
8
9
10
/* drivers/rtc/rtc-ds1307.c */
static struct i2c_driver ds1307_driver = {
.driver = {
.name = "rtc-ds1307",
.of_match_table = of_match_ptr(ds1307_of_match),
.acpi_match_table = ACPI_PTR(ds1307_acpi_ids),
},
.probe = ds1307_probe,
.id_table = ds1307_id,
};

2.2.2 i2c_client-从设备#

代表一个连接到i2c_bus总线上的从设备,结构体定义在 include/linux/i2c.h。描述i2c从设备的i2c相关硬件信息。 一个i2c_driver可以支持多个同类型的i2c_client。i2c_client一般描述再设备树中。

1
2
3
4
5
6
7
8
9
10
struct i2c_client{
unsigned short flags; //描述从设备的一些特性,如I2C_CLIENT_TEN---使用的10位地址
unsigned short addr; //设备的i2c地址,7位地址用低7位,10位地址用低10位。
char name[I2C_NAME_SIZE]; // 设备的名字;

struct i2c_adapter *adapter; //所属的适配器i2c_adapter,挂载在哪条i2c物理总线上
struct i2c_driver *driver; //匹配成功的i2c_driver
int irq;
};

2.2.2.1 i2c_board_info#

也是描述从设备i2c硬件属性。通常情况下先填充i2c_board_info对象的成员,然后去初始化i2c_client对象。

1
2
3
4
5
6
7
8
9
struct i2c_board_info {
char type[I2C_NAME_SIZE];//名字,驱动层和设备层匹配参数
unsigned short flags;//设备地址位数,一般不填或填0表示7位地址
unsigned short addr;//IIC设备地址
void *platform_data;//私有数据
struct dev_archdata *archdata;
struct device_node *of_node;
int irq;//中断号
};

2.3 i2c_msg-消息#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct i2c_msg {
__u16 addr; /* 从机在I2C总线上的地址*/
__u16 flags; /* 消息特征的标志 */

//下面的宏定义就是消息特征的标志
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RD 0x0001 /* read data, from slave to master */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */

__u16 len; /* 消息数据长度,单位是字节 */
__u8 *buf; /* 指向存放消息数据的缓冲区 */
};

I2C的传输地址、 方向、 缓冲区、 缓冲区长度等信息。

3 I2C子系统API#

3.1 控制器相关#

3.1.1 注册控制器#

通过i2c_add_numbered_adapteri2c_add_adapter 这两个函数向系统注册设置好的 i2c_adapter。

1
2
int i2c_add_adapter(struct i2c_adapter *adapter);//include/linux/i2c.h
int i2c_add_numbered_adapter(struct i2c_adapter *adap);

这两个函数的区别在于 i2c_add_adapter 使用动态的总线号,而 i2c_add_numbered_adapter 使用静态总线号。

3.1.2 卸载控制器#

1
void i2c_del_adapter(struct i2c_adapter * adap);

3.2 客户端设备相关#

3.2.1 添加i2c设备驱动#

1
2
3
4
int i2c_register_driver(struct module *owner, struct i2c_driver *driver);//include/linux/i2c.h
/* use a define to avoid include chaining to get THIS_MODULE */
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver);

3.2.2 删除i2c设备驱动#

1
void i2c_del_driver(struct i2c_driver *driver);

3.2.3 module_driver和builtin_driver使用#

在编写从设备驱动时为了方便也可以直接调用module_i2c_driver完成i2c驱动的module_init。或者使用builtin_i2c_driver完成i2c驱动的device_initcall。两者区别一个是编译成内核模块,一个是编译进内核镜像。

image

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
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
__unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);

/**
* builtin_driver() - Helper macro for drivers that don't do anything
* special in init and have no exit. This eliminates some boilerplate.
* Each driver may only use this macro once, and calling it replaces
* device_initcall (or in some cases, the legacy __initcall). This is
* meant to be a direct parallel of module_driver() above but without
* the __exit stuff that is not used for builtin cases.
*
* @__driver: driver name
* @__register: register function for this driver type
* @...: Additional arguments to be passed to __register
*
* Use this macro to construct bus specific macros for registering
* drivers, and do not use it on its own.
*/
#define builtin_driver(__driver, __register, ...) \
static int __init __driver##_init(void) \
{ \
return __register(&(__driver) , ##__VA_ARGS__); \
} \
device_initcall(__driver##_init);

3.2.4 数据传输#

3.2.4.1 i2c_transfer#

发送或接收指定字节数的数据。

1
Int i2c_transfer(struct i2c_adapter *adap,struct i2c_msg *msgs, int num);

3.2.4.2 i2c_master_recv#

接收指定字节的数据。

1
Int i2c_master_recv(const struct i2c_client *client,const char *buf,int count);

3.2.4.3 i2c_master_send#

发送指定字节的数据。

1
int i2c_master_send(const struct i2c_client *client, const char *buf, int count);

4 i2c子系统驱动流程举例#

4.1 核心core层注册#

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
//i2c-core.c
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match, //总线上驱动和设备的匹配函数
.probe = i2c_device_probe, //总线上设备和驱动匹配时调用
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
.pm = &i2c_device_pm_ops,
};
EXPORT_SYMBOL_GPL(i2c_bus_type);
static const struct i2c_device_id dummy_id[] = {
{ "dummy", 0 },
{ },
};
static struct i2c_driver dummy_driver = {
.driver.name = "dummy",
.probe = dummy_probe,
.remove = dummy_remove,
.id_table = dummy_id,
};
static int __init i2c_init(void) {
int retval;
//注册I2C总线
retval = bus_register(&i2c_bus_type);
if (retval)
return retval;
//向I2C总线注册一个名字为dummy的驱动,这个驱动没什么实际的功能,空实现的驱动
retval = i2c_add_driver(&dummy_driver);
if (retval)
goto class_err;
return 0;
class_err:
bus_unregister(&i2c_bus_type);
return retval;
}
static void __exit i2c_exit(void) {
//删除dummy驱动
i2c_del_driver(&dummy_driver);
//卸载I2C总线
bus_unregister(&i2c_bus_type);
}

/* We must initialize early, because some subsystems register i2c drivers
* in subsys_initcall() code, but are linked (and initialized) before i2c.
*/
postcore_initcall(i2c_init);
module_exit(i2c_exit);

可以看到内核启动阶段就调用i2c_init 注册了I2C总线和注册了I2C设备驱动dummy_driver

4.1.1 I2C总线的匹配函数#

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
static const struct i2c_device_id *i2c_match_id(const struct i2c_device_id *id,
const struct i2c_client *client) {
while (id->name[0]) {
if (strcmp(client->name, id->name) == 0)
return id;
id++;
}
return NULL;
}

static int i2c_device_match(struct device *dev, struct device_driver *drv) {
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
if (!client)
return 0;
/* Attempt an OF style match */
if (of_driver_match_device(dev, drv))
return 1;
/* Then ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
driver = to_i2c_driver(drv);
/* match on an id table if there is one */
if (driver->id_table)
return i2c_match_id(driver->id_table, client) != NULL;
return 0;
}

向I2C总线注册I2C驱动或者I2C设备时,会逐一将I2C驱动的名字和I2C设备的名字进行匹配,如果匹配上则调用I2C总线的probe方法;

I2C总线的probe方法就是进一步调用i2c_driver的probe方法。

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
static int i2c_device_probe(struct device *dev) {
//利用container_of宏获取到I2C设备结构体
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
int status;

if (!client)
return 0;

//利用container_of宏获取到I2C驱动结构体
driver = to_i2c_driver(dev->driver);
if (!driver->probe || !driver->id_table)
return -ENODEV;

// 把I2C设备驱动和I2C设备绑定,将来可以互相查找到对方
client->driver = driver;
if (!device_can_wakeup(&client->dev))
device_init_wakeup(&client->dev,
client->flags & I2C_CLIENT_WAKE);
dev_dbg(dev, "probe\n");

//调用I2C驱动的probe函数
status = driver->probe(client, i2c_match_id(driver->id_table, client));
if (status) {
client->driver = NULL;
i2c_set_clientdata(client, NULL);
}
return status;
}

4.2 适配器驱动示例流程#

4.2.1 适配器注册#

4.2.1.1 i2c控制器描述#

以nxp的imx6ull芯片为例,在imx6ull.dtsi文件中找到 I2C1 控制器节点:

1
2
3
4
5
6
7
8
9
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
};

i2c1节点的compatible属性值有两个:fsl,imx6ul-i2cfsl,imx21-i2c,在 Linux 源码中搜索这两个字符串即可找到对应的驱动文件为drivers/i2c/busses/i2c-imx.c

4.2.1.2 i2c控制器驱动probe示例#

image

I2C 适配器驱动也是使用标准的 platform 驱动框架。compatible 属性匹配成功后就会调用probe函数如下:

image

点击展开代码
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
static int i2c_imx_probe(struct platform_device *pdev) {
struct imx_i2c_struct *i2c_imx;
struct resource *res;
struct imxi2c_platform_data *pdata = dev_get_platdata(&pdev->dev);
void __iomem *base;
int irq, ret;
dma_addr_t phy_addr;
const struct imx_i2c_hwdata *match;

dev_dbg(&pdev->dev, "<%s>\n", __func__);

irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;

res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);

phy_addr = (dma_addr_t)res->start;
i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
if (!i2c_imx)
return -ENOMEM;

match = device_get_match_data(&pdev->dev);
if (match)
i2c_imx->hwdata = match;
else
i2c_imx->hwdata = (struct imx_i2c_hwdata *)
platform_get_device_id(pdev)->driver_data;

/* Setup i2c_imx driver structure */
strlcpy(i2c_imx->adapter.name, pdev->name, sizeof(i2c_imx->adapter.name));
i2c_imx->adapter.owner = THIS_MODULE;
i2c_imx->adapter.algo = &i2c_imx_algo;
i2c_imx->adapter.dev.parent = &pdev->dev;
i2c_imx->adapter.nr = pdev->id;
i2c_imx->adapter.dev.of_node = pdev->dev.of_node;
i2c_imx->base = base;
ACPI_COMPANION_SET(&i2c_imx->adapter.dev, ACPI_COMPANION(&pdev->dev));

/* Get I2C clock */
i2c_imx->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(i2c_imx->clk))
return dev_err_probe(&pdev->dev, PTR_ERR(i2c_imx->clk),
"can't get I2C clock\n");

ret = clk_prepare_enable(i2c_imx->clk);
if (ret) {
dev_err(&pdev->dev, "can't enable I2C clock, ret=%d\n", ret);
return ret;
}

/* Init queue */
init_waitqueue_head(&i2c_imx->queue);

/* Set up adapter data */
i2c_set_adapdata(&i2c_imx->adapter, i2c_imx);

/* Set up platform driver data */
platform_set_drvdata(pdev, i2c_imx);

pm_runtime_set_autosuspend_delay(&pdev->dev, I2C_PM_TIMEOUT);
pm_runtime_use_autosuspend(&pdev->dev);
pm_runtime_set_active(&pdev->dev);
pm_runtime_enable(&pdev->dev);

ret = pm_runtime_get_sync(&pdev->dev);
if (ret < 0)
goto rpm_disable;

/* Request IRQ */
ret = request_threaded_irq(irq, i2c_imx_isr, NULL, IRQF_SHARED,
pdev->name, i2c_imx);
if (ret) {
dev_err(&pdev->dev, "can't claim irq %d\n", irq);
goto rpm_disable;
}

/* Set up clock divider */
i2c_imx->bitrate = I2C_MAX_STANDARD_MODE_FREQ;
ret = of_property_read_u32(pdev->dev.of_node,
"clock-frequency", &i2c_imx->bitrate);
if (ret < 0 && pdata && pdata->bitrate)
i2c_imx->bitrate = pdata->bitrate;
i2c_imx->clk_change_nb.notifier_call = i2c_imx_clk_notifier_call;
clk_notifier_register(i2c_imx->clk, &i2c_imx->clk_change_nb);
i2c_imx_set_clk(i2c_imx, clk_get_rate(i2c_imx->clk));

/* Set up chip registers to defaults */
imx_i2c_write_reg(i2c_imx->hwdata->i2cr_ien_opcode ^ I2CR_IEN,
i2c_imx, IMX_I2C_I2CR);
imx_i2c_write_reg(i2c_imx->hwdata->i2sr_clr_opcode, i2c_imx, IMX_I2C_I2SR);

/* Init optional bus recovery function */
ret = i2c_imx_init_recovery_info(i2c_imx, pdev);
/* Give it another chance if pinctrl used is not ready yet */
if (ret == -EPROBE_DEFER)
goto clk_notifier_unregister;

/* Add I2C adapter */
ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
if (ret < 0)
goto clk_notifier_unregister;

pm_runtime_mark_last_busy(&pdev->dev);
pm_runtime_put_autosuspend(&pdev->dev);

dev_dbg(&i2c_imx->adapter.dev, "claimed irq %d\n", irq);
dev_dbg(&i2c_imx->adapter.dev, "device resources: %pR\n", res);
dev_dbg(&i2c_imx->adapter.dev, "adapter name: \"%s\"\n",
i2c_imx->adapter.name);
dev_info(&i2c_imx->adapter.dev, "IMX I2C adapter registered\n");

/* Init DMA config if supported */
i2c_imx_dma_request(i2c_imx, phy_addr);

return 0; /* Return OK */

clk_notifier_unregister:
clk_notifier_unregister(i2c_imx->clk, &i2c_imx->clk_change_nb);
free_irq(irq, i2c_imx);
rpm_disable:
pm_runtime_put_noidle(&pdev->dev);
pm_runtime_disable(&pdev->dev);
pm_runtime_set_suspended(&pdev->dev);
pm_runtime_dont_use_autosuspend(&pdev->dev);
clk_disable_unprepare(i2c_imx->clk);
return ret;
}
  1. platform_get_irq 函数获取中断号。

  2. platform_get_resource 函数从设备树中获取 I2C1 控制器寄存器物理基 地址,也就是 0X021A0000。使用 devm_ioremap_resource 函数对其进 行内存映射,得到可以在 Linux 内核中使用的虚拟地址。

  3. 使用 imx_i2c_struct 结构体来表示 I.MX 系列 SOC 的 I2C 控制器,这里使 用 devm_kzalloc 函数来申请内存。

  4. 初始化i2c_adapter。设置i2c_adapter 的algo成员变量为i2c_imx_algo, 也就是设置 i2c_algorithm

  5. 开启i2c时钟。

  6. 注册 I2C 控制器中断,中断服务函数为 i2c_imx_isr

  7. 设置 I2C 频率默认为 IMX_I2C_BIT_RATE=100KHz,如果设备树节点设 置了“clock-frequency”属性的话 I2C 频率就使用 clock-frequency 属性值。

  8. 设置 I2C1 控制的 I2CR 和 I2SR 寄存器。

    image-20240825172735043

  9. 调用 i2c_add_numbered_adapter 函数向 Linux 内核注册 i2c_adapter。

4.2.2 适配器操作-i2c_algorithm#

1
2
3
4
static struct i2c_algorithm i2c_imx_algo = {
.master_xfer = i2c_imx_xfer,
.functionality = i2c_imx_func,
};

4.2.2.1 i2c_imx_func#

functionality用于返回此I2C适配器支持什么样的通信协议, 在这里 functionality如下:

1
2
3
static u32 i2c_imx_func(struct i2c_adapter *adapter) {
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL | I2C_FUNC_SMBUS_READ_BLOCK_DATA;
}

4.2.2.2 i2c_imx_xfer#

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
static int i2c_imx_xfer(struct i2c_adapter *adapter,
struct i2c_msg *msgs, int num) {
struct imx_i2c_struct *i2c_imx = i2c_get_adapdata(adapter);
int result;
result = pm_runtime_get_sync(i2c_imx->adapter.dev.parent);
if (result < 0)
return result;

result = i2c_imx_xfer_common(adapter, msgs, num, false);
pm_runtime_mark_last_busy(i2c_imx->adapter.dev.parent);
pm_runtime_put_autosuspend(i2c_imx->adapter.dev.parent);
return result;
}

static int i2c_imx_xfer_atomic(struct i2c_adapter *adapter,
struct i2c_msg *msgs, int num) {
struct imx_i2c_struct *i2c_imx = i2c_get_adapdata(adapter);
int result;
result = clk_enable(i2c_imx->clk);
if (result)
return result;

result = i2c_imx_xfer_common(adapter, msgs, num, true);
clk_disable(i2c_imx->clk);
return result;
}
4.2.2.2.1 i2c_imx_xfer_common#
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
static int i2c_imx_xfer_common(struct i2c_adapter *adapter, struct i2c_msg *msgs,
int num, bool atomic) {
unsigned int i, temp;
int result;
bool is_lastmsg = false;
struct imx_i2c_struct *i2c_imx = i2c_get_adapdata(adapter);

dev_dbg(&i2c_imx->adapter.dev, "<%s>\n", __func__);

/* Start I2C transfer */
result = i2c_imx_start(i2c_imx, atomic);
if (result) {
/*
* Bus recovery uses gpiod_get_value_cansleep() which is not
* allowed within atomic context.
*/
if (!atomic && i2c_imx->adapter.bus_recovery_info) {
i2c_recover_bus(&i2c_imx->adapter);
result = i2c_imx_start(i2c_imx, atomic);
}
}

if (result)
goto fail0;
/* read/write data */
for (i = 0; i < num; i++) {
if (i == num - 1)
is_lastmsg = true;

if (i) {
dev_dbg(&i2c_imx->adapter.dev,
"<%s> repeated start\n", __func__);
temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);
temp |= I2CR_RSTA;
imx_i2c_write_reg(temp, i2c_imx, IMX_I2C_I2CR);
result = i2c_imx_bus_busy(i2c_imx, 1, atomic);
if (result)
goto fail0;
}
dev_dbg(&i2c_imx->adapter.dev,
"<%s> transfer message: %d\n", __func__, i);
/* write/read data */
#ifdef CONFIG_I2C_DEBUG_BUS
temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);
dev_dbg(&i2c_imx->adapter.dev,
"<%s> CONTROL: IEN=%d, IIEN=%d, MSTA=%d, MTX=%d, TXAK=%d, RSTA=%d\n",
__func__,
(temp & I2CR_IEN ? 1 : 0), (temp & I2CR_IIEN ? 1 : 0),
(temp & I2CR_MSTA ? 1 : 0), (temp & I2CR_MTX ? 1 : 0),
(temp & I2CR_TXAK ? 1 : 0), (temp & I2CR_RSTA ? 1 : 0));
temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2SR);
dev_dbg(&i2c_imx->adapter.dev,
"<%s> STATUS: ICF=%d, IAAS=%d, IBB=%d, IAL=%d, SRW=%d, IIF=%d, RXAK=%d\n",
__func__,
(temp & I2SR_ICF ? 1 : 0), (temp & I2SR_IAAS ? 1 : 0),
(temp & I2SR_IBB ? 1 : 0), (temp & I2SR_IAL ? 1 : 0),
(temp & I2SR_SRW ? 1 : 0), (temp & I2SR_IIF ? 1 : 0),
(temp & I2SR_RXAK ? 1 : 0));
#endif
if (msgs[i].flags & I2C_M_RD) {
result = i2c_imx_read(i2c_imx, &msgs[i], is_lastmsg, atomic);
} else {
if (!atomic &&
i2c_imx->dma && msgs[i].len >= DMA_THRESHOLD)
result = i2c_imx_dma_write(i2c_imx, &msgs[i]);
else
result = i2c_imx_write(i2c_imx, &msgs[i], atomic);
}
if (result)
goto fail0;
}
fail0:
/* Stop I2C transfer */
i2c_imx_stop(i2c_imx, atomic);

dev_dbg(&i2c_imx->adapter.dev, "<%s> exit with: %s: %d\n", __func__,
(result < 0) ? "error" : "success msg",
(result < 0) ? result : num);
return (result < 0) ? result : num;
}
  1. 调用i2c_imx_start函数开启 I2C 通信。
  2. 读数据的话就调用 i2c_imx_read 函数。
  3. 向 I2C 设备写数据,如果要用 DMA 的话就使用 i2c_imx_dma_write 函数来 完成写数据。如果不使用 DMA 的话就使用 i2c_imx_write 函数完成写数据。
  4. I2C 通信完成以后调用i2c_imx_stop函数停止 I2C 通信。

i2c_imx_starti2c_imx_readi2c_imx_writei2c_imx_stop 这些函数就是 I2C 寄存器的具体操作函数,按照i2c协议

4.3 从设备驱动示例流程#

4.3.1 i2c从设备描述#

4.3.1.1 不使用dts时描述#

在未使用设备树的时候需要在 BSP 里面使用i2c_board_info结构体来描 述一个具体的 I2C 设备。

举个例子,arch/arm/mach-imx/mach-mx27_3ds.cOV2640 摄像头使用的 I2C 设备信息描述如下:

1
2
3
static struct i2c_board_info mx27_3ds_i2c_camera = {
I2C_BOARD_INFO("ov2640", 0x30),
};//从设备OV2640 的名字和i2c地址

4.3.1.2 使用dts描述#

比如NXP 官方的 EVK 开发 板在 I2C1 上接了 mag3110 这个磁力计芯片,因此必须在 i2c1 节点下创建 mag3110 子节点,然 后在这个子节点内描述 mag3110 这个i2c外设的相关信息。打开 imx6ull-14x14-evk.dts

1
2
3
4
5
6
7
8
9
10
11
12
13
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";

mag3110@0e {
compatible = "fsl,mag3110";
reg = <0x0e>;
position = <2>;
};
....
};

重点 是 compatible 属性和 reg 属性的设置,一个用于匹配驱动,一个用于设置器件地址。

4.3.2 从设备驱动代码示例#

4.3.2.1 AP3216C环境传感器#

AP3216C是一个三合一环境传感器,包含环境光强度(ALS)、接近距离(PS)和红外线强度(IR)这 三个环境参数检测。接开发板子的i2c1。因此用这个外设作为i2c从设备来举例。

AP3216C 的特点 如下:

1
2
3
4
5
6
7
1. I2C 接口,快速模式下波特率可以到 400Kbit/S
2. 多种工作模式选择:ALS、PS+IR、ALS+PS+IR、PD 等等。
3. 内建温度补偿电路。
4. 宽工作温度范围(-30°C ~ +80°C)。
5. 超小封装,4.1mm x 2.4mm x 1.35mm
6. 环境光传感器具有 16 位分辨率。
7. 接近传感器和红外传感器具有 10 位分辨率

AP3216C 常被用于手机、平板、导航设备等,其内置的接近传感器可以用于检测是否有物体接近,比如手机上用来检测耳朵是否接触听筒,如果检测到的话就表示正在打电话,手机就 会关闭手机屏幕以省电。也可以使用环境光传感器检测光照强度,可以实现自动背光亮度调节。

image

image

4.3.2.2 dts设置#

打开 imx6ull-alientek-emmc.dts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";

mag3110@0e {
compatible = "fsl,mag3110";
reg = <0x0e>;
position = <2>;
};
};

i2c1使用pinctrl子系统配置了iomux属性。pinctrl_i2c1 就是 I2C1 的 IO 节点,这里将 UART4_TXDUART4_RXD 这两个 IO 分别 复用为 I2C1_SCL I2C1_SDA,电气属性都设置为 0x4001b8b0

默认i2c1 节点下并不是对应 ap3216c 从设备,而是mag3110。修改dts如下:

1
2
3
4
5
6
7
8
9
10
11
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";

ap3216c@1e {
compatible = "alientek,ap3216c";
reg = <0x1e>;
};
};

ap3216c 子节点,@后面的“1e”是 ap3216c 的器件地址

reg 属性也是设置 ap3216c 器件地址的,因此 reg 设置为 0x1e

修改编译启动linux,可以看到/sys/bus/i2c/devices 目录下存放着所有 I2C 设备,如果设备树修改正确的话,会在 /sys/bus/i2c/devices 目录下看到一个名为“0-001e”的子目录:

image

“0-001e”就是 ap3216c 的设备目录,“1e”就是 ap3216c 器件地址。进入 0-001e 目录,可以看到“name”文件,name 问价就保存着此设备名字,在这里就是“ap3216c”

image

4.3.2.3 AP3216C 驱动示例#

ap3216creg.h定义AP3216C 的寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef AP3216C_H
#define AP3216C_H
#define AP3216C_ADDR 0X1E /* AP3216C器件地址 */
#define AP3216C_SYSTEMCONG 0x00 /* 配置寄存器 */
#define AP3216C_INTSTATUS 0X01 /* 中断状态寄存器 */
#define AP3216C_INTCLEAR 0X02 /* 中断清除寄存器 */
#define AP3216C_IRDATALOW 0x0A /* IR数据低字节 */
#define AP3216C_IRDATAHIGH 0x0B /* IR数据高字节 */
#define AP3216C_ALSDATALOW 0x0C /* ALS数据低字节 */
#define AP3216C_ALSDATAHIGH 0X0D /* ALS数据高字节 */
#define AP3216C_PSDATALOW 0X0E /* PS数据低字节 */
#define AP3216C_PSDATAHIGH 0X0F /* PS数据高字节 */
#endif

ap3216c.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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#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_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"

#define AP3216C_CNT 1
#define AP3216C_NAME "ap3216c"

struct ap3216c_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev*/
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
int major; /* 主设备号 */
void *private_data; /* 私有数据 */
unsigned short ir, als, ps; /* 三个光传感器数据 */
};

static struct ap3216c_dev ap3216cdev;

static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len) {
int ret;
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->private_data;

/* msg[0]为发送要读取的首地址 */
msg[0].addr = client->addr; /* ap3216c地址 */
msg[0].flags = 0; /* 标记为发送数据 */
msg[0].buf = &reg; /* 读取的首地址 */
msg[0].len = 1; /* reg长度*/

/* msg[1]读取数据 */
msg[1].addr = client->addr; /* ap3216c地址 */
msg[1].flags = I2C_M_RD; /* 标记为读取数据*/
msg[1].buf = val; /* 读取数据缓冲区 */
msg[1].len = len; /* 要读取的数据长度*/

ret = i2c_transfer(client->adapter, msg, 2);
if(ret == 2) {
ret = 0;
} else {
printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
ret = -EREMOTEIO;
}
return ret;
}

static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len) {
u8 b[256];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client *)dev->private_data;

b[0] = reg; /* 寄存器首地址 */
memcpy(&b[1],buf,len); /* 将要写入的数据拷贝到数组b里面 */

msg.addr = client->addr; /* ap3216c地址 */
msg.flags = 0; /* 标记为写数据 */

msg.buf = b; /* 要写入的数据缓冲区 */
msg.len = len + 1; /* 要写入的数据长度 */

return i2c_transfer(client->adapter, &msg, 1);
}

static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg) {
u8 data = 0;

ap3216c_read_regs(dev, reg, &data, 1);
return data;

#if 0
struct i2c_client *client = (struct i2c_client *)dev->private_data;
return i2c_smbus_read_byte_data(client, reg);
#endif
}


static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data) {
u8 buf = 0;
buf = data;
ap3216c_write_regs(dev, reg, &buf, 1);
}

void ap3216c_readdata(struct ap3216c_dev *dev) {
unsigned char i =0;
unsigned char buf[6];

/* 循环读取所有传感器数据 */
for(i = 0; i < 6; i++)
buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);

if(buf[0] & 0X80) /* IR_OF位为1,则数据无效 */
dev->ir = 0;
else /* 读取IR传感器的数据*/
dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);

dev->als = ((unsigned short)buf[3] << 8) | buf[2];/* 读取ALS传感器的数据*/

if(buf[4] & 0x40) /* IR_OF位为1,则数据无效 */
dev->ps = 0;
else /* 读取PS传感器的数据 */
dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}

static int ap3216c_open(struct inode *inode, struct file *filp) {
filp->private_data = &ap3216cdev;
/* 初始化AP3216C */
ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x04);/* 复位AP3216C */
mdelay(50); /* AP3216C复位最少10ms */
ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X03);/* 开启ALS、PS+IR */
return 0;
}

static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off) {
short data[3];
long err = 0;

struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;
ap3216c_readdata(dev);

data[0] = dev->ir;
data[1] = dev->als;
data[2] = dev->ps;
err = copy_to_user(buf, data, sizeof(data));
return 0;
}

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

static const struct file_operations ap3216c_ops = {
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
.release = ap3216c_release,
};

static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id) {
if (ap3216cdev.major) {
ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
} else {
alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);
ap3216cdev.major = MAJOR(ap3216cdev.devid);
}

cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
if (IS_ERR(ap3216cdev.class)) {
return PTR_ERR(ap3216cdev.class);
}

ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid,
NULL, AP3216C_NAME);
if (IS_ERR(ap3216cdev.device)) {
return PTR_ERR(ap3216cdev.device);
}

ap3216cdev.private_data = client;

return 0;
}

static int ap3216c_remove(struct i2c_client *client) {
cdev_del(&ap3216cdev.cdev);
unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);
device_destroy(ap3216cdev.class, ap3216cdev.devid);
class_destroy(ap3216cdev.class);
return 0;
}

static const struct i2c_device_id ap3216c_id[] = {
{"alientek,ap3216c", 0},
{}
};

static const struct of_device_id ap3216c_of_match[] = {
{ .compatible = "alientek,ap3216c" },
{ /* Sentinel */ }
};

static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.owner = THIS_MODULE,
.name = "ap3216c",
.of_match_table = ap3216c_of_match,
},
.id_table = ap3216c_id,
};

#if 0
static int __init ap3216c_init(void) {
int ret = 0;

ret = i2c_add_driver(&ap3216c_driver);
return ret;
}
static void __exit ap3216c_exit(void) {
i2c_del_driver(&ap3216c_driver);
}
module_init(ap3216c_init);
module_exit(ap3216c_exit);
#else
module_i2c_driver(ap3216c_driver);
#endif
  1. 典型的i2c驱动框架编写的从设备驱动示例,i2c_add_driver/i2c_del_driver添加和删除从设备驱动。
  2. ap3216c_of_matchcompatible匹配上,执行ap3216c_probe,把从设备ap3216c按照字符设备框架构造驱动。
  3. ap3216cdev.private_data = clientprivate_data 成员变量用于存放 ap3216c 对 应的 i2c_client
  4. ap3216c_open初始化ap3216c
  5. 传感器数据获取
1
2
3
4
ap3216c_read
ap3216c_readdata//读取数据6字节
ap3216c_read_reg//读取一个字节
ap3216c_read_regs//构造i2c_msg[2]调用i2c_transfer获取1个字节传感器数据
  1. ap3216c_write_reg调用ap3216c_write_regs再调用i2c_transfer写入数据到指定地址寄存器。

4.3.2.4 AP3216C应用测试#

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
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
int main(int argc, char *argv[]) {
int fd;
char *filename;
unsigned short databuf[3];
unsigned short ir, als, ps;
int ret = 0;

if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}

filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0) {
printf("can't open file %s\r\n", filename);
return -1;
}

while (1) {
ret = read(fd, databuf, sizeof(databuf));
if(ret == 0) { /* 数据读取成功 */
ir = databuf[0]; /* ir传感器数据 */
als = databuf[1]; /* als传感器数据 */
ps = databuf[2]; /* ps传感器数据 */
printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
}
usleep(200000); /*100ms */
}
close(fd); /* 关闭文件 */
return 0;
}
1
2
执行程序如下:
./a.out /dev/ap3216c

测试 APP 会不断的从 AP3216C 中读取数据,然后输出到终端上:

image

字符设备驱动-内核led子系统

1 LED子系统介绍#

用来管理控制板子的led灯,比如系统心跳灯,普通的硬盘指示灯光,颜色灯,休眠唤醒灯等等。

image
led 子系统相关描述可在内核源码 Documentation/leds/leds-class.txt 了解。

led 子系统是一个简单的 Linux 子系统 ,在目录 /sys/class/leds 下展示该子系统设备,每个设备都有自己的属性:
image

1
2
3
4
brightness:设置 LED 亮度,范围 0 ~ max_brightness
max_brightness:最大亮度(255 或其他数字)
trigger:触发方式,如 heartbeat、mmc0、backlight、gpio
delay_off、delay_on:trigger为timer时,LED亮灭的时间,单位ms

kernel/include/linux/leds.h

1
2
3
4
5
enum led_brightness {
LED_OFF = 0, //全暗
LED_HALF = 127, //一半亮度
LED_FULL = 255, //最大亮度
};

1.1 代码框架分析#

led-class.c (led 子系统框架的入口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
维护 LED 子系统的所有 LED 设备,为 LED 设备提供注册操作函数: 

led_classdev_register()
devm_led_classdev_register()

注销操作函数:
led_classdev_unregister()
devm_led_classdev_unregister();

电源管理的休眠和恢复操作函数:
led_classdev_suspend()
led_classdev_resume();

用户态操作接口:brightness 、max_brightness

led-core.c

1
2
3
4
5
6
7
8
9
10
11
12
抽象出 LED 操作逻辑,封装成函数导出,供其它文件使用:

led_init_core(): 核心初始化;
led_blink_set(): 设置led闪烁时间:
led_blink_set_oneshot() : 闪烁一次
led_stop_software_blink() : led停止闪烁
led_set_brightness() : 设置led的亮度
led_update_brightness : 更新亮度
led_sysfs_disable : 用户态关闭
led_sysfs enable : 用户态打开
leds_list : leds链表;
leds_list_lock : leds链表锁

led-triggers.c

1
2
3
4
5
6
7
8
9
10
11
维护 LED 子系统的所有触发器,为触发器提供注册操作函数: 

led_trigger_register()
devm_led_trigger_register()
led_trigger_register_simple()

注销操作函数:
led_trigger_unregister()
led_trigger_unregister_simple()

以及其它触发器相关的操作函数

ledtrig-timer.c、ledtrig-xxx.c

1
2
3
4
5
6
7
8
以 ledtrig-timer.c 为例

入口函数调用 led_trigger_register() 注册触发器,
注册时候传入 led_trigger 结构体,里面有 activate 和 deactivate 成员函数指针,
作用是生成 delay_on 、 delay_off 文件

同时还提供 delay_on 和 delay_off 的用户态操作接口
卸载时,使用 led_trigger_unregister() 注销触发器

leds-gpio.c、leds-xxx.c

1
2
3
4
5
以 leds-gpio.c 为例

在通过设备树或者其它途径匹配到设备信息后,将调用 probe() 函数,
然后再根据设备信息设置 led_classdev,
最后调用 devm_led_classdev_register() 注册 LED 设备。

对于驱动开发人员,LED框架已经有了,并不需要我们去熟悉,只要知道如何使用,它是如何与我们的硬件相关联的,只要熟悉leds-gpio.c

1.2 结构体描述#

1.2.1 led_classdev#

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
struct led_classdev {
const char *name;//名字
enum led_brightness brightness;//亮度
enum led_brightness max_brightness;//最大亮度
int flags;

/* Lower 16 bits reflect status */
#define LED_SUSPENDED (1 << 0)
/* Upper 16 bits reflect control information */
#define LED_CORE_SUSPENDRESUME (1 << 16)
#define LED_BLINK_ONESHOT (1 << 17)
#define LED_BLINK_ONESHOT_STOP (1 << 18)
#define LED_BLINK_INVERT (1 << 19)
#define LED_SYSFS_DISABLE (1 << 20)
#define SET_BRIGHTNESS_ASYNC (1 << 21)
#define SET_BRIGHTNESS_SYNC (1 << 22)
#define LED_DEV_CAP_FLASH (1 << 23)

//设置亮度API
void (*brightness_set)(struct led_classdev *led_cdev,enum led_brightness brightness);
int (*brightness_set_sync)(struct led_classdev *led_cdev,enum led_brightness brightness);

//获取亮度API
enum led_brightness (*brightness_get)(struct led_classdev *led_cdev);

//闪烁时点亮和熄灭的时间设置
int (*blink_set)(struct led_classdev *led_cdev,unsigned long *delay_on,unsigned long *delay_off);

struct device *dev;
const struct attribute_group **groups;

//leds-list的node
struct list_head node;
//默认trigger的名字
const char *default_trigger;
//闪烁的开关时间
unsigned long blink_delay_on, blink_delay_off;
//闪烁的定时器链表
struct timer_list blink_timer;
//闪烁的亮度
int blink_brightness;
void (*flash_resume)(struct led_classdev *led_cdev);

struct work_struct set_brightness_work;
int delayed_set_value;

#ifdef CONFIG_LEDS_TRIGGERS
//trigger的锁
struct rw_semaphore trigger_lock;
//led的trigger
struct led_trigger *trigger;
//trigger的链表
struct list_head trig_list;
//trigger的数据
void *trigger_data;
bool activated;
#endif
struct mutex led_access;
};

1.2.2 gpio_led#

1
2
3
4
5
6
7
8
9
10
11
struct gpio_led {
const char *name;
const char *default_trigger;
unsigned gpio;
unsigned active_low : 1;
unsigned retain_state_suspended : 1;
unsigned panic_indicator : 1;
unsigned default_state : 2;
/* default_state should be one of LEDS_GPIO_DEFSTATE_(ON|OFF|KEEP) */
struct gpio_desc *gpiod;
};

name: led名字
default_trigger: LED 灯在Linux 系统中的默认功能,比如作为系统心跳指示灯等等。
default_state: 默认状态,如:

1
2
3
#define LEDS_GPIO_DEFSTATE_OFF		0
#define LEDS_GPIO_DEFSTATE_ON 1
#define LEDS_GPIO_DEFSTATE_KEEP 2

gpiod:是gpio描述,详见linux内核驱动-gpio子系统 - fuzidage - 博客园 (cnblogs.com)

字符设备驱动-gpio子系统 | Hexo (fuzidage.github.io)

2 LED 驱动使能#

输入make menuconfig

1
2
3
-> Device Drivers
-> LED Support (NEW_LEDS [=y])
->LED Support for GPIO connected LEDs

image
可 以 看 出 , 把 Linux 内 部 自 带 的 LED 灯 驱 动 编 译 进 内 核 以 后,CONFIG_LEDS_GPIO 就会等于‘y’:
image

3 Linux 内核自带 LED 驱动分析#

LED 灯驱动文件为/drivers/leds/leds-gpio.c,大家可以打开/drivers/leds/Makefile 这个文件:
image
来看一下leds-gpio.c这个驱动文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = "gpio-leds", },
{},
};
......
static struct platform_driver gpio_led_driver = {
.probe = gpio_led_probe,
.remove = gpio_led_remove,
.driver = {
.name = "leds-gpio",
.of_match_table = of_gpio_leds_match,
},
};

module_platform_driver(gpio_led_driver);

LED 驱动的匹配表,此表只有一个匹配项,compatible 内容为“gpio-leds”
因此设备树中的 LED 灯设备节点的 compatible 属性值也要为“gpio-leds”,否则设备和驱动匹
配不成功,驱动就没法工作。

利用内核自带LED子系统驱动,可以帮我们很好的控制板子产品的指示灯,不需要单独编写驱动程序。

3.1 gpio_led_probe 函数简析#

image
进入probe函数,pdata此时为空,进入gpio_leds_create
image

  1. 调用 device_get_child_node_count 函数统计子节点数量,一般在在设备树中创建
    一个节点表示 LED 灯,然后在这个节点下面为每个 LED 灯创建一个子节点。因此子节点数量也是 LED 灯的数量。
  2. 遍历每个子节点,获取每个子节点的信息:
    2.1 devm_get_gpiod_from_child获取每个gpio灯的gpio_desc信息。
    2.2 获取label属性,label作为led的名字
    2.3 获取“linux,default-trigger”属性,可以通过此属性设置某个 LED 灯在Linux 系统中的默认功能,比如作为系统心跳指示灯等等。
    2.4 获取“default-state”属性值,也就是 LED 灯的默认状态属性
    2.5 create_gpio_led 函数创建 LED 相关的 io,常用gpio操作,下面详细介绍

3.1.1 create_gpio_led#

image

  1. 先获取gpiod信息
  2. 配置led_dat属性,包括default_state, brightness等信息
  3. 配置gpio方向
  4. 将led注册给LED子系统

3.1.2 开启关闭led#

image

4 led子系统应用举例#

4.1 dts编写#

1
2
3
4
5
6
7
8
dtsleds {
compatible = "gpio-leds";
led0 {
label = "red";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
default-state = "off";
};
};

创建一个节点表示 LED 灯设备,比如 dtsleds,如果板子上有多个 LED 灯的话每个 LED灯都作为 dtsleds 的子节点。

  1. dtsleds 节点的compatible属性值一定要为“gpio-leds”
  2. 设置label属性,此属性为可选,每个子节点都有一个 label 属性,label 属性一般表示LED 灯的名字,比如以颜色区分的话就是 red、green 等等。
  3. 每个子节点必须要设置gpios属性值,表示此 LED 所使用的 GPIO 引脚!
  4. 可以设置“linux,default-trigger”属性值,也就是设置 LED 灯的默认功能,可以查阅Documentation/devicetree/bindings/leds/common.txt 这个文档来查看可选功能,比如:
    1
    2
    3
    4
    5
    backlight:LED 灯作为背光。
    default-on:LED 灯打开
    heartbeat:LED 灯作为心跳指示灯,可以作为系统运行提示灯。
    ide-disk:LED 灯作为硬盘活动指示灯。
    timer:LED 灯周期性闪烁,由定时器驱动,闪烁频率可以修改
  5. 可以设置“default-state”属性值,可以设置为 on、off 或 keep,为 on 的时候 LED 灯默认打开,为 off 的话 LED 灯默认关闭,为 keep 的话 LED 灯保持当前模式

启动开发板:
image
进入到 leds 目录中:
image
测试:

1
2
echo 1 > /sys/class/leds/red/brightness //打开 LED0
echo 0 > /sys/class/leds/red/brightness //关闭 LED0

如果能正常的打开和关闭 LED 灯话就说明使用led子系统ok。

4.2 修改该led成系统心跳灯#

1
2
3
4
5
6
7
8
9
dtsleds {
compatible = "gpio-leds";
led0 {
label = "red";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
linux,default-trigger = "heartbeat";
default-state = "on";
};
};

设置 LED0 为 heartbeat
第 8 行,默认打开 LED0。

5 基于sysfs操作led子系统#

5.1 点亮 LED#

1
2
3
echo 255 > /sys/class/leds/led1/brightness
cat /sys/class/leds/led1/brightness
cat /sys/class/leds/led1/max_brightness

5.2 闪烁#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat /sys/class/leds/led1/trigger

会看到 trigger_list
[none] mmc0 mmc1 mmc2 timer
其中的 timer 这个 triggerledtrig-timer.c 中模块初始化的时候注册进去的

echo timer > /sys/class/leds/led1/trigger
这一句会调用
led_trigger_store()->
led_trigger_set()->
trigger->activate(led_cdev);
从而调用 ledtrig-timer.c 文件里 的timer_trig_activate(),
在 /sys/class/leds/led1/ 下创建 delay_ondelay_off 两个文件

echo 100 > /sys/class/leds/led1/delay_on
echo 200 > /sys/class/leds/led1/delay_off
这样会闪烁,亮 100ms 灭 200ms

5.3 关闭 LED#

1
2
3
echo 0 > /sys/class/leds/led1/delay_on

echo 0 > /sys/class/leds/led1/brightness