Linux 接入 ADIS16505 IMU

本文主要记录如何在 Rockchip RK3506 上接入并驱动 ADIS16505 IMU 模块。

Linux 有 IIO 子系统(工业输入输出),用于接入一些工业传感器,其中就包括 IMU。Analog ADIS16505 是一款使用较多的 MEMS IMU 模组,其驱动早已进入内核。

下面直接介绍相关适配工作,适配的 SOC 是 Rockchip RK3506,内核6.1,使用的模块是 ADIS16505-2。

参考资料:

1. 驱动适配

内核 CONFIG 修改:

1
2
3
+CONFIG_ADIS16475=m
+CONFIG_IIO_ADIS_LIB_BUFFER=y
+CONFIG_IIO_ADIS_LIB=y

然后编译出来 adis_lib.ko 和 adis16475.ko 拷贝到开发板,添加到 /lib/modules/$(uname -r)/kernel/drivers/iio/imu 下。

DTS 修改,在对应 spi 节点下添加 imu@0 节点(ADIS16505 最大只支持 2M 时钟,所以一般无法和其他外设共享 SPI 总线,建议单独一个 SPI 与之连接):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
+ imu@0 {
+            compatible = "adi,adis16505-2";
+            reset-gpios = <&gpio1 RK_PC7 GPIO_ACTIVE_LOW>;
+            reg = <0>;
+            spi-cpha;
+            spi-cpol;
+            spi-max-frequency = <2000000>;
+            interrupts = <RK_PB5 IRQ_TYPE_EDGE_RISING>;
+            interrupt-parent = <&gpio0>;
+            status = "okay";
+ };

pinctrl 配置复位和终端引脚:

1
2
3
4
5
6
7
8
9
+ &pinctrl {
+        adis16505 {
+                imu_pins: imu-pins {
+			             rockchip,pins =
+			             <1 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>,
+			             <0 RK_PB3 RK_FUNC_GPIO &pcfg_pull_none>;
+		         };
+        };
+}

编译,烧写 boot 镜像。

启动后,会自动加载 adis 内核模块驱动,检查 lsmod | grep adis 会输出。

2. 读取验证

数据检查,安装 apt install libiio-utils ,然后用 sudo iio_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
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
sudo iio_info
Library version: 0.23 (git tag: v0.23)
Compiled with backends: local xml ip usb
IIO context created with local backend.
Backend version: 0.23 (git tag: v0.23)
Backend description string: Linux rk3506-iot 6.1.99 #18 SMP PREEMPT Sun Sep 28 16:51:31 CST 2025 armv7l
IIO context has 2 attributes:
        local,kernel: 6.1.99
        uri: local:
IIO context has 2 devices:
        iio:device0: adis16505-2 (buffer capable)
                8 channels found:
                        anglvel_x:  (input, index: 0, format: be:S32/32>>0)
                        3 channel-specific attributes found:
                                attr  0: calibbias value: 0
                                attr  1: raw value: 822982
                                attr  2: scale value: 0.000000006
                        anglvel_y:  (input, index: 1, format: be:S32/32>>0)
                        3 channel-specific attributes found:
                                attr  0: calibbias value: 0
                                attr  1: raw value: 632205
                                attr  2: scale value: 0.000000006
                        anglvel_z:  (input, index: 2, format: be:S32/32>>0)
                        3 channel-specific attributes found:
                                attr  0: calibbias value: 0
                                attr  1: raw value: -574057
                                attr  2: scale value: 0.000000006
                        accel_x:  (input, index: 3, format: be:S32/32>>0)
                        3 channel-specific attributes found:
                                attr  0: calibbias value: 0
                                attr  1: raw value: -182676799
                                attr  2: scale value: 0.000000037
                        accel_y:  (input, index: 4, format: be:S32/32>>0)
                        3 channel-specific attributes found:
                                attr  0: calibbias value: 0
                                attr  1: raw value: 73639656
                                attr  2: scale value: 0.000000037
                        accel_z:  (input, index: 5, format: be:S32/32>>0)
                        3 channel-specific attributes found:
                                attr  0: calibbias value: 0
                                attr  1: raw value: -172736170
                                attr  2: scale value: 0.000000037
                        temp0:  (input, index: 6, format: be:S16/16>>0)
                        2 channel-specific attributes found:
                                attr  0: raw value: 307
                                attr  1: scale value: 100
                        timestamp:  (input, index: 7, format: le:S64/64>>0)
                4 device-specific attributes found:
                                attr  0: current_timestamp_clock value: monotonic

                                attr  1: filter_low_pass_3db_frequency value: 720
                                attr  2: sampling_frequency value: 400.000000
                                attr  3: waiting_for_supplier value: 0
                3 buffer-specific attributes found:
                                attr  0: data_available value: 0
                                attr  1: direction value: in
                                attr  2: watermark value: 1
                6 debug attributes found:
                                debug attr  0: firmware_date value: 08-19-2021
                                debug attr  1: firmware_revision value: 1.7
                                debug attr  2: flash_count value: 55
                                debug attr  3: product_id value: 16505
                                debug attr  4: serial_number value: 0x2aa7
                                debug attr  5: direct_reg_access value: 0x0
                Current trigger: trigger0(adis16505-2-dev0)
        trigger0: adis16505-2-dev0
                0 channels found:
                No trigger on this device

如果有 error 或问题,则需要检查 DTS 引脚与实际的接线,接线有问题会导致出现问题。

这里需要注意 anglvel 和 accel 的 scale 字段,会跟手册的对不上,这个稍后解释。

读取实际数据,可以交叉编译内核 tools/iio/iio_generic_buffer 程序,使用 sudo iio_generic_buffer -n adis16505-2 -a -c -1 来连续可读所有数据,包括 时间戳:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sudo iio_generic_buffer -n adis16505-2 -a -c -1
iio device number being used is 0
iio trigger number being used is 0
Enabling all channels
Enabling: in_accel_y_en
Enabling: in_temp0_en
Enabling: in_anglvel_z_en
Enabling: in_accel_x_en
Enabling: in_timestamp_en
Enabling: in_anglvel_y_en
Enabling: in_accel_z_en
Enabling: in_anglvel_x_en
/sys/bus/iio/devices/iio:device0 adis16505-2-dev0
0.003762 0.003326 -0.000192 -6.759874 2.724417 -6.391016 30600.000000 1759115389553132535 
0.001698 0.002608 -0.001356 -6.764592 2.726806 -6.392885 30600.000000 1759115389555627474 
0.000364 0.004624 -0.000209 -6.756444 2.718365 -6.381812 30600.000000 1759115389558121829 
-0.000038 0.003894 -0.000131 -6.760957 2.724637 -6.373475 30600.000000 1759115389560619101 
-0.000031 0.004665 -0.001059 -6.754562 2.714244 -6.378930 30800.000000 1759115389563114623 
0.001390 0.003611 -0.001513 -6.760931 2.723385 -6.377503 31000.000000 1759115389565610144 
0.002020 0.003198 -0.000661 -6.759657 2.719233 -6.383900 31000.000000 1759115389568120833 
0.002179 0.002808 -0.004133 -6.762529 2.723939 -6.387537 31000.000000 1759115389570598855 
0.001080 0.004518 -0.003293 -6.761709 2.727158 -6.387032 30700.000000 1759115389573096710 
0.000033 0.002241 -0.002442 -6.756791 2.720568 -6.381608 30700.000000 1759115389575591065 
0.000519 0.003776 -0.003139 -6.759673 2.720824 -6.381290 30700.000000 1759115389578086587 
0.000121 0.003285 -0.004375 -6.761266 2.715935 -6.379437 30600.000000 1759115389580582108 
-0.000275 0.003437 -0.003296 -6.761114 2.716521 -6.379723 30600.000000 1759115389583077047 
0.002979 0.004323 -0.002205 -6.760320 2.717569 -6.381045 30600.000000 1759115389585569360 
0.003052 0.003355 -0.003360 -6.764820 2.720145 -6.388940 30500.000000 1759115389588067799 
0.001705 0.003426 -0.002130 -6.761583 2.715266 -6.392511 30500.000000 1759115389590562446 
0.002971 0.003035 -0.003745 -6.760653 2.715627 -6.385410 30500.000000 1759115389593058259 
0.001787 0.003784 -0.002825 -6.763578 2.716290 -6.383574 30600.000000 1759115389595552614 
0.000679 0.003716 -0.004219 -6.761706 2.714494 -6.381908 30600.000000 1759115389598048428 
0.001852 0.001673 -0.002511 -6.764761 2.713016 -6.385989 30600.000000 1759115389600543949 
0.001230 0.003286 -0.003984 -6.759422 2.706651 -6.386874 30600.000000 1759115389603039471 
0.001944 0.003745 -0.001667 -6.764474 2.714547 -6.382912 30500.000000 1759115389605535285 

驱动默认使用 realtime,可以通过修改 /sys/bus/iio/devices/iio:deviceX/current_timestamp_clock 来修改时间戳,可选 monotonic 使用 MONO 时间,来避免系统时间调整导致的跳跃。

3. 注意事项

由于内核中不支持浮点数,所以内核驱动在 sysfs 中展示的浮点字段,都存在精度损失(内核打印浮点只保留1e-9的精度,无法满足这款IMU的比例系数),也就意味着,不能直接使用驱动提供的 sysfs 中的 scale 字段来求解物理值,而是直接使用芯片手册中的比例换算,在用户空间解算物理值,否则会有严重的精度丢失。针对 IIO 驱动,只使用 buffer 提供的原始值,和手册的比例系数进行换算,是最为稳妥的方式。

同时驱动在 /sys/kernel/debug/iio/iio\:deviceX/ 目录下提供一些额外信息:

1
2
ls  /sys/kernel/debug/iio/iio\:device1/
direct_reg_access  firmware_date      firmware_revision  flash_count        product_id         serial_number

这些字段可以通过参考资料2了解具体含义,比较有用的是 direct_reg_access 这个文件,可以通过它来实现直接的底层寄存器读写:

1
2
3
4
5
6
7
8
# 读取 0x60
echo 0x60 > direct_reg_access
cat 0x60
=> 0x2c1
# 写入 0x60
echo 0x60 0x2c1 > direct_reg_access
cat 0x60
=> 0x2c1

另外如频率设置、滤波设置等,可以参考资料说明,直接写入 sysfs 下对应文件即可。

C 语言中的 X-Macros

在 C 语言中,有一种较为高级的宏用法,称为 X-Macros,本文就是讲述 X-Macros 的应用。

X-Macros 用在一些需要“表驱动法”的地方,针对编译期间已知的数据集合,生成对应的接口方法,当修改数据时, 只需要考虑修改“表”本身,而已有的接口方法会自动适应。

一般来说 X-Macros 核心就是一个列表宏,形如:

1
2
3
#define LIST_XMACROS_LIST(x) \
    X(a, b, c ,d, e) \
    X(f, g, h)

LIST_XMACROS_LIST 是一个列表宏,一行定义一个元素,其宏参数 X 是一个操作宏,当需要使用列表的时候,传入不同的操作宏,来实现针对列表数据的不同处理。

最常见的,就是枚举类型的字面字符串和值之间的匹配,参见如下代码:

 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
#define MAIN_STATE_LIST(X) \
    X(ERROR, "something error") \
    X(INIT, "initialize device") \
    X(BUSY, "busy moving") \
    X(READY, "ready for scan") \
    X(SCAN, "scanning")

#define MAIN_STATE(NAME)    MST_##NAME
#define ENUM_MAIN_STATE(NAME, ...) MAIN_STATE(NAME),
enum MainStateEnum {
    MAIN_STATE_LIST(ENUM_MAIN_STATE)
    MAIN_STATE__ENUM_CNT
};

static const struct MainStateBundles {
    int state;
    const char *str;
    const char *desc;
} M_STATE_BUNDLES[] = {
#define BUNDLE_MAIN_STATE(NAME, DESC, ...) \
{ \
    .state = MAIN_STATE(NAME), \
    .str = #NAME, \
    .desc = DESC, \
},
    MAIN_STATE_LIST(BUNDLE_MAIN_STATE)
};
#undef BUNDLE_MAIN_STATE

代码最终基于列表生成一个常量结构体数组,并按照枚举值、枚举名字面量和描述来自动填充,后续函数就可以根据这个常量结构体数组,来实现枚举值、字符串和描述之间的匹配和转换。

本质上,宏是递归地字符串替换,那么,就可以利用 X-Macros 实现更复杂的功能,比如说,参数的序列化和反序列化。可以将用到的参数,定义一个列表宏,将其类型、名称、限制、是否必选等属性填入其中,然后利用该列表来完成序列化/反序列化的接口函数。

一个样例代码如下,这其中,还将参数按照 SECTION 来区分分组,目前的限制就是不支持超过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
 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
// 编译 gcc test.c -o test $(pkg-config --cflags --libs libcjson)
//
#include <stdio.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include <cjson/cJSON.h>

#define CFG_STR_MAXCHARS    32

#define LTS_CFG_CONTROL_LIST(X) \
    X(STR, serial, CFG_STR_MAXCHARS, "/dev/ttyS4", "control serial device", true) \
    X(INT, baudrate, 460800, "control serial baudrate", false) \
    X(FLOAT, scan_speed, 5.0, "scan speed deg/s", false)

#define LIDAR_ROT_DEFAULT ((double[]){ 0, 0, -1, -1, 0, 0, 0, 1, 0 })

#define LTS_CFG_LIDAR_LIST(X) \
    X(STR, ip, CFG_STR_MAXCHARS, "192.168.0.240", "lidar ip addr", false) \
    X(INT, port, 2111, "lidar port", false) \
    X(FLOAT_N, p_rot, 9, LIDAR_ROT_DEFAULT, "rot matrix", true)

#define LTS_CFG_CAMERA_LIST(X) \
    X(STR, device, CFG_STR_MAXCHARS, "/dev/video0", "camera device", false) \
    X(INT, width, 1920, "camera frame width", false) \
    X(INT, height, 1080, "camera frame height", false)

#define LTS_CFG_SECTIONS_LIST(SECTION, X) \
    SECTION(control,  LtsConfigControl,   LTS_CFG_CONTROL_LIST(X)) \
    SECTION(lidar,    LtsConfigLidar,     LTS_CFG_LIDAR_LIST(X)) \
    SECTION(camera,   LtsConfigCamera,    LTS_CFG_CAMERA_LIST(X))

#define X_INT__STDEF(N, DEF, DESC, REQD)               int     N;
#define X_FLOAT__STDEF(N, DEF, DESC, REQD)             double  N;
#define X_FLOAT_N__STDEF(N, L, DEF, DESC, REQD)        double  N[L];
#define X_STR__STDEF(N, L, DEF, DESC, REQD)            char    N[L + 1];

#define X_STDEF(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__STDEF(N, __VA_ARGS__)

#define X_SECTION__STRUCT(name, stname, list) \
    struct stname { \
        list \
    } name;

struct LtsConfig {
    LTS_CFG_SECTIONS_LIST(X_SECTION__STRUCT, X_STDEF)
};

#define X_INT__INIT(N, DEF, DESC, REQD)                 cfg->N = DEF;
#define X_FLOAT__INIT(N, DEF, DESC, REQD)               cfg->N = DEF;
#define X_FLOAT_N__INIT(N, L, DEF, DESC, REQD)          memcpy(cfg->N, DEF, sizeof(cfg->N));
#define X_STR__INIT(N, L, DEF, DESC, REQD)              snprintf(cfg->N, sizeof(cfg->N), "%s", DEF);

#define X_INIT(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__INIT(N, __VA_ARGS__)

#define X_SECTION__INIT(name, stname, list) \
    { \
        struct stname *cfg = &config->name; \
        list \
    }

static inline void lts_config_init(struct LtsConfig *config)
{
    LTS_CFG_SECTIONS_LIST(X_SECTION__INIT, X_INIT)
}

#define X_INT__PRINT(N, DEF, DESC, REQD)      fprintf(fp, "  %-10s = %d\n", #N, sec->N);
#define X_FLOAT__PRINT(N, DEF, DESC, REQD)    fprintf(fp, "  %-10s = %f\n", #N, sec->N);
#define X_FLOAT_N__PRINT(N, L, DEF, DESC, REQD) \
    { \
        fprintf(fp, "  %-10s = [", #N); \
        for (size_t i = 0; i < (L); i++) { \
            fprintf(fp, "%f%s", sec->N[i], (i + 1 < (L)) ? ", " : ""); \
        } \
        fprintf(fp, "]\n"); \
    }
#define X_STR__PRINT(N, L, DEF, DESC, REQD)   fprintf(fp, "  %-10s = \"%s\"\n", #N, sec->N);
#define X_PRINT(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__PRINT(N, __VA_ARGS__)

#define X_SECTION__PRINT(name, stname, list) \
    { \
        const struct stname *sec = &config->name; \
        fprintf(fp, "[%s]\n", #name); \
        list \
        fprintf(fp, "\n"); \
    }

static inline void lts_config_fprint(FILE *fp, const struct LtsConfig *config)
{
    LTS_CFG_SECTIONS_LIST(X_SECTION__PRINT, X_PRINT)
}


#define X_INT__JSON(N, DEF, DESC, REQD) \
    cJSON_AddNumberToObject(sec_json, #N, sec->N);
#define X_FLOAT__JSON(N, DEF, DESC, REQD) \
    cJSON_AddNumberToObject(sec_json, #N, sec->N);
#define X_FLOAT_N__JSON(N, L, DEF, DESC, REQD) \
    { \
        cJSON *arr = cJSON_CreateArray(); \
        for (size_t i = 0; i < (L); i++) { \
            cJSON_AddItemToArray(arr, cJSON_CreateNumber(sec->N[i])); \
        } \
        cJSON_AddItemToObject(sec_json, #N, arr); \
    }
#define X_STR__JSON(N, L, DEF, DESC, REQD) \
    cJSON_AddStringToObject(sec_json, #N, sec->N);

#define X_JSON(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__JSON(N, __VA_ARGS__)
#define X_SECTION__JSON(name, stname, list) \
    { \
        const struct stname *sec = &config->name; \
        cJSON *sec_json = cJSON_CreateObject(); \
        list \
        cJSON_AddItemToObject(root, #name, sec_json); \
    }

// 主函数
static inline char *lts_config_dump_json(const struct LtsConfig *config)
{
    cJSON *root = cJSON_CreateObject();
    if (!root) return NULL;

    LTS_CFG_SECTIONS_LIST(X_SECTION__JSON, X_JSON)

    char *json_str = cJSON_Print(root);
    cJSON_Delete(root);
    return json_str; // 调用者需要 free(json_str)
}

int main(int argc, char *argv)
{
    struct LtsConfig config;
    lts_config_init(&config);
    lts_config_fprint(stdout, &config);
    char *json = lts_config_dump_json(&config);
    printf("JSON len=%zu:\n %s\n", strlen(json), json);
    return 0;
}

这里需要注意的是,不同类型的参数,其参数数量是不同的,然后根据类型来实现不同的操作宏,最终利用占位宏来实现不同类型的不同处理宏方法:

1
2
// 基于二次展开的展位宏,利用 ## 拼接真正的操作宏
#define X_INIT(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__INIT(N, __VA_ARGS__)

如果想在这个参数中,实现无 SECTION 全局参数,可选的办法是,除了 SECTION 外,还提供一个 MAIN 宏,来针对处理全局参数, 这里不再赘述。

X-Macros 虽然不能让你像用库一样写很少的接口代码,但是允许只用写一次代码的代价来实现后续修改的自适应。其难点在于理解起来稍微有点绕,还有就是,编译时候出错的话,错误信息会较难理解。但是对于 C 语言而言,静态地类似方法,除了宏外,别无选择。

在 Lan 网络中自动识别 Wan 网络中的共享设备

局域网中通常有一些共享设备比如 NAS、打印机等,在同网络的主机可以自主发现这些设备,但是该网络的下属子网无法自动识别。

如果使用的是 OpenWrt 路由,那么可以尝试使用 mdns-repeater 这个工具来帮助子网获取上级网络的共享设备。

NAS、打印机等共享设备的自主发现均基于 UDP 的 mDNS 协议,如果要实现 Wan 到 Lan 的设备自主发现,则需要将 Wan 网络的 mDNS 消息播发到 Lan 网络中。

mdns-repeater 就是这样一个工具,在 OpenWrt 路由中,可以通过 系统-> 软件包 搜索 mdns 来安装该包,然后通过终端修改其配置文件,主要是指定 Wan 和 Lan 网络的接口名称:

1
2
3
4
5
# /etc/config/mdns_repeater

config mdns_repeater 'main'
        list interface 'wan'
        list interface 'br-lan'

然后在终端执行 /etc/init.d/mdns-repeater enable 和 /etc/init.d/mdns-repeater start 来使能开机启动和启动程序。

同时还需要修改路由防火墙,允许 Wan 的 mDNS 消息可以通过。在 网络->防火墙 中添加一条通信规则:名称 Allow-mDNS,协议选择 UDP,源区域选择 wan,目标区域选择 设备(输入),目标端口选择 5353,操作选择 接受,然后保存应用。

然后过一段时间后,在 Lan 网络的主机就可以识别到 Wan 网络上的而共享设备了。