Linux设备驱动之IIO子系统——Triggered buffer support触发缓冲支持
triggered buffer support触发缓冲支持
在许多数据分析应用中,能够基于某些外部信号(触发器)捕获数据是比较有用的。 这些触发器可能是:
-
- 数据就绪信号
- 连接到某个外部系统的irq线路(gpio或其他)
- 处理器周期性中断
- 用户空间在sysfs中读/写特定文件
iio设备驱动程序与触发器完全无关。 触发器可以初始化一个或多个设备上的数据捕获。 这些触发器用于填充缓冲区,然后作为字符设备暴露给用户空间。
可以开发一个自己的触发驱动程序,但这超出了本书的范围。 我们将尝试仅关注现有的。 这些是:
- iio-trig-interrupt:这为使用任何irq作为iio触发器提供了支持。 在旧的内核版本中,它曾经是iio-trig-gpio。 启用此触发模式的内核选项是config_iio_interrupt_trigger。 如果构建为模块,则该模块将被称为iio-trig-interrupt。
- iio-trig-hrtimer:这提供了一个基于频率的iio触发器,使用hrt作为中断源(因为内核v4.5)。 在较旧的内核版本中,它曾经是iio-trig-rtc。 负责此触发模式的内核选项是iio_hrtimer_trigger。 如果构建为模块,则该模块将被称为iio-trig-hrtimer。
- iio-trig-sysfs:这允许我们使用sysfs条目来触发数据捕获。 config_iio_sysfs_trigger是添加此触发模式支持的内核选项。
- iio-trig-bfin-timer:这允许我们使用blackfin定时器作为iio触发器(仍然在staging文件夹中)。
利用iio公开的api,我们可以:
- 声明任何给定数量的触发器
- 选择将其数据推入缓冲区的通道
当您的iio设备支持触发缓冲区时,您必须设置iio_dev.pollfunc,它在触发器触发时执行。 此处理程序负责通过indio_dev-> active_scan_mask查找已启用的通道,检索其数据,并使用iio_push_to_buffers_with_timestamp函数将它们提供给indio_dev-> buffer。 因此,缓冲区和触发器需要在iio子系统中连接。
iio核心提供了一组辅助函数来设置触发缓冲区,可以在drivers / iio / industrialio-triggered-buffer.c中找到。
以下是从驱动程序中支持触发缓冲区的步骤:
1.如果需要,填写iio_buffer_setup_ops结构:
1 const struct iio_buffer_setup_ops sensor_buffer_setup_ops = { 2 .preenable = my_sensor_buffer_preenable, 3 .postenable = my_sensor_buffer_postenable, 4 .postdisable = my_sensor_buffer_postdisable, 5 .predisable = my_sensor_buffer_predisable, 6 };
2. 写下与触发器关联的上半部分。 在99%的情况下,只需提供与捕获相关的时间戳:
1 irqreturn_t sensor_iio_pollfunc(int irq, void *p) 2 { 3 pf->timestamp = iio_get_time_ns((struct indio_dev *)p); 4 return irq_wake_thread; 5 }
3. 写入触发器下半部分,它将从每个启用的通道获取数据,并将它们提供给缓冲区:
1 irqreturn_t sensor_trigger_handler(int irq, void *p) 2 { 3 u16 buf[8]; 4 int bit, i = 0; 5 struct iio_poll_func *pf = p; 6 struct iio_dev *indio_dev = pf->indio_dev; 7 8 /* one can use lock here to protect the buffer */ 9 /* mutex_lock(&my_mutex); */ 10 /* read data for each active channel */ 11 for_each_set_bit(bit, indio_dev->active_scan_mask, 12 indio_dev->masklength) 13 buf[i++] = sensor_get_data(bit) 14 15 /* 16 * if iio_dev.scan_timestamp = true, the capture timestamp 17 * will be pushed and stored too, as the last element in the 18 * sample data buffer before pushing it to the device buffers. 19 */ 20 iio_push_to_buffers_with_timestamp(indio_dev, buf, timestamp); 21 22 /* please unlock any lock */ 23 24 /* mutex_unlock(&my_mutex); */ 25 26 /* notify trigger */ 27 28 iio_trigger_notify_done(indio_dev->trig); 29 return irq_handled; 30 }
4. 最后,在probe函数中,必须在使用iio_device_register()注册设备之前连接触发器和缓冲区:
iio_triggered_buffer_setup(indio_dev, sensor_iio_polfunc, sensor_trigger_handler, sensor_buffer_setup_ops);
这里的神奇函数是iio_triggered_buffer_setup。 这也将为您的设备提供indio_direct_mode功能。 当触发器(从用户空间)连接到您的设备时,您无法知道何时触发捕获。
当连续缓冲捕获处于活动状态时,应该阻止(通过返回错误)驱动程序执行sysfs每通道数据捕获(由read_raw()挂钩执行)以避免未确定的行为,因为触发器处理程序和read_raw( )hook会尝试同时访问设备。 用于检查是否实际使用缓冲模式的函数是iio_buffer_enabled()。 钩子看起来像这样:
static int my_read_raw(struct iio_dev *indio_dev, const struct iio_chan_spec *chan, int *val, int *val2, long mask) { [...] switch (mask) { case iio_chan_info_raw: if (iio_buffer_enabled(indio_dev)) return -ebusy; [...] }
iio_buffer_enabled()函数只是确定是否为给定的iio设备启用了缓冲区。
使用中一些重要事项:
iio_buffer_setup_ops提供缓冲区设置函数,以便在缓冲区配置序列的固定步骤(在启用/禁用之前/之后)调用。 如果未指定,则iio内核将为您的设备提供默认的iio_triggered_buffer_setup_ops。
sensor_iio_pollfunc是触发器的上半部分。 与每个上半部分一样,它在中断上下文中运行,并且必须尽可能少地处理。 在99%的情况下,您只需提供与捕获相关的时间戳。 再次,可以使用默认的iio iio_pollfunc_store_time函数。
sensor_trigger_handler是下半部分,它在内核线程中运行,允许我们进行任何处理,包括甚至获取互斥或睡眠。 重处理应该在这里进行。 它通常从设备读取数据并将其与上半部分中记录的时间戳一起存储在内部缓冲区中,并将其推送到iio设备缓冲区。
注意:触发缓冲必须使用触发器。 它告诉驱动程序何时从设备读取样本并将其放入缓冲区。 触发缓冲对于编写iio设备驱动程序不是必需的。 通过读取通道的原始属性,也可以通过sysf使用单次捕获,这只会执行单次转换(对于正在读取的通道属性)。 缓冲模式允许连续转换,从而在单次捕获多个通道。
iio trigger and sysfs (user space)用户空间触发器和sysfs
sysfs中有两个与触发器相关的位置:
-
- /sys/bus/iio/devices/triggery/:一旦iio触发器注册到iio核心并且对应于索引为y的触发器,就会创建该目录。目录中至少有一个属性:
- name:这是可以在以后用于与设备关联的触发器名称
- 另一个可能是采样频率或其他,和触发器类型相关
- /sys/bus/iio/devices/iio:devicex/trigger/*如果您的设备支持触发缓冲区,将自动创建目录。 通过在current_trigger文件中写入触发器的名称,可以将触发器与我们的设备相关联。
- /sys/bus/iio/devices/triggery/:一旦iio触发器注册到iio核心并且对应于索引为y的触发器,就会创建该目录。目录中至少有一个属性:
sysfs trigger interface sysfs触发器接口
通过config_iio_sysfs_trigger = y config选项在内核中启用sysfs触发器,将自动创建/ sys / bus / iio / devices / iio_sysfs_trigger /文件夹,并可用于sysfs触发器管理。 目录中将有两个文件add_trigger和remove_trigger。 它的驱动程序在drivers / iio / trigger / iio-trig-sysfs.c中。
add_trigger file
这用于创建新的sysfs触发器。 您可以通过将正值(将用作触发器id)写入该文件来创建新触发器。 它将创建新的sysfs触发器,可在/ sys / bus / iio / devices / triggerx中访问,其中x是触发器编号。
例如:# echo 2 > add_trigger
这将创建一个新的sysfs触发器,可在/ sys / bus / iio / devices / trigger2中访问。 如果系统中已存在具有指定id的触发器,则将返回无效的参数消息。 sysfs触发器名称模式为sysfstrig {id}。 命令echo 2> add_trigger将创建名为sysfstrig2的trigger / sys / bus / iio / devices / trigger2:
$ cat /sys/bus/iio/devices/trigger2/name sysfstrig2
每个sysfs触发器至少包含一个文件:trigger_now。 将1写入该文件将指示其current_trigger中具有相应触发器名称的所有设备开始捕获,并将数据推送到其各自的缓冲区中。 每个设备缓冲区必须设置其大小,并且必须启用(echo 1> / sys / bus / iio / devices / iio:devicex / buffer / enable)。
remove_trigger file
要删除触发器,请使用以下命令:
# echo 2 > remove_trigger
使用触发器绑定设备
将设备与给定触发器相关联包括将触发器的名称写入设备触发器目录下可用的current_trigger文件。 例如,假设我们需要将设备与具有索引2的触发器绑定:
# set trigger2 as current trigger for device0 # echo sysfstrig2 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
要从设备分离触发器,应该将空字符串写入设备触发器目录的current_trigger文件,如下所示:
# echo "" > iio:device0/trigger/current_trigger
我们将在下一节进一步看到一个处理数据捕获的sysfs触发器的实际示例。
the interrupt trigger interface中断触发器接口
请考虑以下代码示例
static struct resource iio_irq_trigger_resources[] = { [0] = { .start = irq_nr_for_your_irq, .flags = ioresource_irq | ioresource_irq_lowedge, }, }; static struct platform_device iio_irq_trigger = { .name = "iio_interrupt_trigger", .num_resources = array_size(iio_irq_trigger_resources), .resource = iio_irq_trigger_resources, }; platform_device_register(&iio_irq_trigger);
声明我们的irq触发器,它将加载irq触发器独立模块。 如果其探测功能成功,则会有一个与触发器对应的目录。 irq触发器名称的格式为irqtrigx,其中x对应于刚刚传递的虚拟irq,您将在/ proc / interrupt中看到:
$ cd /sys/bus/iio/devices/trigger0/ $ cat name
正如我们对其他触发器所做的那样,您只需将该触发器分配给设备current_trigger文件即可将该触发器分配给您的设备。
# echo "irqtrig85" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
现在,每次触发中断时,都会捕获设备数据。
注意:irq触发器驱动程序还不支持dt,这就是我们使用board init文件的原因。 但是这没关系; 由于驱动程序需要资源,我们可以使用dt而无需更改任何代码。
以下是声明irq触发器接口的设备树节点的示例:
mylabel: my_trigger@0{ compatible = "iio_interrupt_trigger"; interrupt-parent = <&gpio4>; interrupts = <30 0x0>; };
该示例假设irq线是属于gpio控制器节点gpio4的gpio#30。 这包括使用gpio作为中断源,这样无论何时gpio变为给定状态,都会引发中断,从而触发捕获。
hrtimer触发器接口(4.5内核以下可能不支持)
hrtimer触发器依赖于configfs文件系统(请参阅内核源代码中的documentation / iio / iio_configfs.txt),可以通过config_iio_configfs配置选项启用它,并挂载在我们的系统上(通常位于/ config目录下):
# mkdir /config # mount -t configfs none /config
现在,加载模块iio-trig-hrtimer将创建在/ config / iio下可访问的iio组,允许用户在/ config / iio / triggers / hrtimer下创建hrtimer触发器。如:
# create a hrtimer trigger $ mkdir /config/iio/triggers/hrtimer/my_trigger_name # remove the trigger $ rmdir /config/iio/triggers/hrtimer/my_trigger_name
每个hrtimer触发器在触发器目录中包含单个sampling_frequency属性(/sys/bus/iio/devices/triggery/文件夹下)。 在使用hrtimer触发器的数据捕获一节中的章节中进一步提供了完整且有效的示例。
iio buffers iio缓冲区
iio缓冲区提供连续数据捕获,可同时读取多个数据通道。 可以通过/ dev / iio:device字符设备节点从用户空间访问缓冲区。 在触发器处理程序中,用于填充缓冲区的函数是iio_push_to_buffers_with_timestamp。 负责为您的设备分配触发缓冲区的函数是iio_triggered_buffer_setup()。
iio缓冲sysfs接口
iio缓冲区在/ sys / bus / iio / iio:devicex / buffer / *下有一个关联的属性目录。 以下是一些现有属性:
- length: 缓冲区可以存储的数据样本总数(容量)。 这是缓冲区包含的扫描数。
- enable: 这将激活缓冲区捕获,启动缓冲区捕获。
- watermark: 自内核版本v4.2起,此属性已可用。 它是一个正数,指定阻塞读取应等待的扫描元素数。 例如,如果使用轮询,它将阻塞,直到达到水印。 只有当水印大于请求的读取量时才有意义。 它不会影响非阻塞读取。 可以在超时时阻止轮询并在超时到期后读取可用样本,因此具有最大延迟保证。
iio缓冲区设置
将要读取数据并将其推入缓冲区的通道称为扫描元素(scan element )。 可以从用户空间通过/ sys / bus / iio / iio:devicex / scan_elements / *目录访问它们的配置,其中包含以下属性:
- en (实际上是属性名称的后缀)用于启用通道。 当且仅当其属性为非零时,触发捕获将包含此通道的数据样本。 例如,in_voltage0_en,in_voltage1_en等。
- type 描述了缓冲区内的扫描元素数据存储,因此描述了从用户空间读取它的形式。 例如,in_voltage0_type。 格式为[be | le]:
[s|u]bits/storagebitsxrepeat[>>shift].
-
- be或le指定字节序(大或小)
- s或u指定符号(带符号(2的补码)或无符号)。
- bits 是有效数据位的数量。
- storagebits :是此通道在缓冲区中占用的位数。 也就是说,一个值可以用12位(位)真正编码,但在缓冲区中占用16位(存储位)。 因此,必须将数据向右移动四次以获得实际值。 此参数取决于设备,应参考其数据表。
- shift:表示在屏蔽掉未使用的位之前应该移位数据值的次数。 并不总是需要此参数。 如果有效位(位)的数量等于存储位的数量,则移位将为0.还可以在器件数据手册中找到该参数。
- repeat 指定位/存储位重复的数量。 当repeat元素为0或1时,则省略重复值。
解释这一部分的最好方法是通过内核文档的摘录,可以在这里找到:https://www.kernel.org/doc/html/latest/driver-api/iio/buffers.html。 例如,用于3轴加速度计的驱动程序,具有12位分辨率,其中数据存储在两个8位寄存器中,如下所示:
7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
|d3 |d2 |d1 |d0 | x | x | x | x | (low byte, address 0x06)
+---+---+---+---+---+---+---+---+
7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
|d11|d10|d9 |d8 |d7 |d6 |d5 |d4 | (high byte, address 0x07)
+---+---+---+---+---+---+---+---+
每个轴将具有以下扫描元素类型:
$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_type le:s12/16>>4
人们应该将其解释为16位大小的小端符号数据,需要在屏蔽12个有效数据位之前将其右移4位。
struct iio_chan_spec中负责确定如何将通道的值存储到缓冲区中的元素是scant_type。
struct iio_chan_spec { [...] struct { char sign; /* should be 'u' or 's' as explained above */ u8 realbits; u8 storagebits; u8 shift; u8 repeat; enum iio_endian endianness; } scan_type; [...] };
这个结构绝对匹配[be | le]:[s|u]bits/storagebitsxrepeat[>>shift], ,这是上一节中描述的模式。 让我们看看结构的每个成员:
-
- sign表示数据的符号,并匹配模式中的[s | u]
- realbits对应于模式中的位
- storagebits与模式中的相同名称匹配
- shift对应于模式的移位,重复相同
- iio_indian表示字节序,并匹配模式中的[be | le]
此时,可以编写与前面解释的类型相对应的iio通道结构:
struct struct iio_chan_spec accel_channels[] = { { .type = iio_accel, .modified = 1, .channel2 = iio_mod_x, /* other stuff here */ .scan_index = 0, .scan_type = { .sign = 's', .realbits = 12, .storagebits = 16, .shift = 4, .endianness = iio_le, }, } /* similar for y (with channel2 = iio_mod_y, scan_index = 1) * and z (with channel2 = iio_mod_z, scan_index = 2) axis */ }
putting it all together
让我们仔细看看bosh的数字三轴加速度传感器bma220。 这是一个spi / i2c兼容器件,具有8位大小的寄存器,以及片上运动触发中断控制器,实际上可以感应倾斜,运动和冲击振动。 其数据表可从以下网址获得:http://www.mouser.fr/pdfdocs/bstbma220ds00308.pdf,其驱动程序自内核v4.8(config_bma200)开始引入。 让我们一起来看看:
首先,我们使用struct iio_chan_spec声明我们的iio通道。 一旦使用了触发缓冲区,我们就需要填充.scan_index和.scan_type字段:
#define bma220_data_shift 2 #define bma220_device_name "bma220" #define bma220_scale_available "0.623 1.248 2.491 4.983" #define bma220_accel_channel(index, reg, axis) { \ .type = iio_accel, \ .address = reg, \ .modified = 1, \ .channel2 = iio_mod_##axis, \ .info_mask_separate = bit(iio_chan_info_raw), \ .info_mask_shared_by_type = bit(iio_chan_info_scale), \ .scan_index = index, \ .scan_type = { \ .sign = 's', \ .realbits = 6, \ .storagebits = 8, \ .shift = bma220_data_shift, \ .endianness = iio_cpu, \ }, \ } static const struct iio_chan_spec bma220_channels[] = { bma220_accel_channel(0, bma220_reg_accel_x, x), bma220_accel_channel(1, bma220_reg_accel_y, y), bma220_accel_channel(2, bma220_reg_accel_z, z), };
.info_mask_separate = bit(iio_chan_info_raw)表示每个通道都有一个* _raw sysfs条目(属性),而.info_mask_shared_by_type = bit(iio_chan_info_scale)表示所有相同类型的通道只有一个* _scale sysfs条目:
jma@jma:~$ ls -l /sys/bus/iio/devices/iio:device0/ (...) # without modifier, a channel name would have in_accel_raw (bad) -rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_scale -rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_x_raw -rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_y_raw -rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_z_raw (...)
读取in_accel_scale会调用read_raw()挂钩,并将掩码设置为iio_chan_info_scale。 读取in_accel_x_raw会调用read_raw()挂钩,并将掩码设置为iio_chan_info_raw。 因此,实际值是raw_value * scale。
.scan_type所说的是每个通道返回的值是8位大小(将占用缓冲区中的8位),但有用的有效负载仅占用6位,并且数据必须在屏蔽之前右移2次 出未使用的位。 任何扫描元素类型将如下所示:
$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_x_type le:s6/8>>2
以下是我们的pollfunc(实际上是下半部分),它从设备读取样本并将读取值推送到缓冲区(iio_push_to_buffers_with_timestamp())。 完成后,我们通知核心(iio_trigger_notify_done()):
static irqreturn_t bma220_trigger_handler(int irq, void *p) { int ret; struct iio_poll_func *pf = p; struct iio_dev *indio_dev = pf->indio_dev; struct bma220_data *data = iio_priv(indio_dev); struct spi_device *spi = data->spi_device; mutex_lock(&data->lock); data->tx_buf[0] = bma220_reg_accel_x | bma220_read_mask; ret = spi_write_then_read(spi, data->tx_buf, 1, data->buffer, array_size(bma220_channels) - 1); if (ret < 0) goto err; iio_push_to_buffers_with_timestamp(indio_dev, data->buffer, pf->timestamp); err: mutex_unlock(&data->lock); iio_trigger_notify_done(indio_dev->trig); return irq_handled; }
以下是读取功能。 它是一个钩子,每次读取设备的sysfs条目时都会调用它:
static int bma220_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask) { int ret; u8 range_idx struct bma220_data *data = iio_priv(indio_dev); switch (mask) { case iio_chan_info_raw: /* if buffer mode enabled, do not process single-channel read */ if (iio_buffer_enabled(indio_dev)) return -ebusy; /* else we read the channel */ ret = bma220_read_reg(data->spi_device, chan->address); if (ret < 0) return -einval; *val = sign_extend32(ret >> bma220_data_shift, 5); return iio_val_int; case iio_chan_info_scale: ret = bma220_read_reg(data->spi_device, bma220_reg_range); if (ret < 0) return ret; range_idx = ret & bma220_range_mask; *val = bma220_scale_table[range_idx][0]; *val2 = bma220_scale_table[range_idx][1]; return iio_val_int_plus_micro; } return -einval; }
当读取*raw sysfs文件时,调用挂钩程序,在mask参数中给定iio_chan_info_raw,并在* chan参数中调用相应的通道。 * val和val2实际上是输出参数。 必须使用raw值设置它们(从设备读取)。 在* scale sysfs文件上执行的任何读取都将使用掩码参数中的iio_chan_info_scale调用挂钩,依此类推每个属性掩码。
写入功能也是如此,用于将值写入设备。 您的驱动程序有80%的可能性不需要写入功能。 此写挂钩允许用户更改设备的比例:
static int bma220_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int val, int val2, long mask) { int i; int ret; int index = -1; struct bma220_data *data = iio_priv(indio_dev); switch (mask) { case iio_chan_info_scale: for (i = 0; i < array_size(bma220_scale_table); i++) if (val == bma220_scale_table[i][0] && val2 == bma220_scale_table[i][1]) { index = i; break; } if (index < 0) return -einval; mutex_lock(&data->lock); data->tx_buf[0] = bma220_reg_range; data->tx_buf[1] = index; ret = spi_write(data->spi_device, data->tx_buf, sizeof(data->tx_buf)); if (ret < 0) dev_err(&data->spi_device->dev, "failed to set measurement range\n"); mutex_unlock(&data->lock); return 0; } return -einval; }
只要将值写入设备,就会调用此函数。 经常更改的参数是比例。 一个例子可能是:
echo <desired-scale> > /sys/bus/iio/devices/iio;devices0/in_accel_scale.
现在,它来填充一个结构iio_info结构,给我们的iio_device:
static const struct iio_info bma220_info = { .driver_module = this_module, .read_raw = bma220_read_raw, .write_raw = bma220_write_raw, /* only if your driver need it */ };
在probe函数中,我们分配并设置了一个struct iio_dev iio设备。 私人数据的内存也被保留:
/* * we provide only two mask possibility, allowing to select none or every * channels. */ static const unsigned long bma220_accel_scan_masks[] = { bit(axis_x) | bit(axis_y) | bit(axis_z), 0 }; static int bma220_probe(struct spi_device *spi) { int ret; struct iio_dev *indio_dev; struct bma220_data *data; indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*data)); if (!indio_dev) { dev_err(&spi->dev, "iio allocation failed!\n"); return -enomem; } data = iio_priv(indio_dev); data->spi_device = spi; spi_set_drvdata(spi, indio_dev); mutex_init(&data->lock); indio_dev->dev.parent = &spi->dev; indio_dev->info = &bma220_info; indio_dev->name = bma220_device_name; indio_dev->modes = indio_direct_mode; indio_dev->channels = bma220_channels; indio_dev->num_channels = array_size(bma220_channels); indio_dev->available_scan_masks = bma220_accel_scan_masks; ret = bma220_init(data->spi_device); if (ret < 0) return ret; /* this call will enable trigger buffer support for the device */ ret = iio_triggered_buffer_setup(indio_dev, iio_pollfunc_store_time, bma220_trigger_handler, null); if (ret < 0) { dev_err(&spi->dev, "iio triggered buffer setup failed\n"); goto err_suspend; } ret = iio_device_register(indio_dev); if (ret < 0) { dev_err(&spi->dev, "iio_device_register failed\n"); iio_triggered_buffer_cleanup(indio_dev); goto err_suspend; } return 0; err_suspend: return bma220_deinit(spi); }
可以通过config_bma220内核选项启用此驱动程序。 也就是说,这只能从内核中的v4.8开始提供。 可以在较旧的内核版本上使用的最接近的设备是bma180,可以使用config_bma180选项启用它。
上一篇: 腾讯云centos7.2安装mysql
下一篇: 跟我干,有钱赚