hardware

【图文】Intel Edison C++ 开发之 I2C/IIC(Part3)–深入MRAA开发


这是一篇完整的关于在Intel Edison上进行原生程序开发的系列文章,以爱迪生通过I2C BUS连接温湿度传感器为例,详细地展示如何在 Eclipse上使用 C++ 进行 Edison 开发。文章将会涉及硬件、MRAA库、开发环境的设置、yocto linux 内核等内容,精彩内容不容错过。欢迎加入 Gekius 开发群(Q群: 329401876)深入讨论Intel Edison IoT 及嵌入式开发的方方面面 :)

3. 深入 MRAA 库

在上一篇中我们从应用程序(用户层)的角度展示了如何使用 MRAA 为Intel Edison进行 I2C 开发。对于嵌入式开发来说往往是面向硬件的,很多时候程序员需要面对特定的硬件环境,在更底层进行开发。这个部分我们将以 I2C 为例深入了解 MRAA 库到底为我们做了什么,文章中将会涉及Arduino Board上的 GPIO 映射,sysfs 文件系统等内容,希望能对大家开发起到帮助 :)

3.1 sysfs 文件系统

Linux 内核从 2.5 版本以后引入了统一设备模型并以 sysfs 作为实现。简单来说有了 sysfs,Linux 可以方便地描述总线、设备、驱动以及它们之间的关系,并且还可以通过一定的接口提供了控制功能,让应用程序可以通过简单地函数调用完成对设备的控制。

sysfs 的基本思路是通过把内核对象(如设备,属性等)映射到文件系统中,并通过对文件的 read/write操作以及 ioctl 操作直接对内核对象进行操作,这样在用户层我们看起来只是完成简单的文件操作即对设备进行了控制,非常简单明了。

sysfs 是个 ramfs,载入点位置在 /sys下,/sys/目录下的文件结构如下图所示

/sys 目录结构

/sys 目录结构

对于 Edison 开发来说,我们目前只需要关注两个目录:class 和 devices。class 目录是把设备按功能进行区分,每一类设备都会对应 /sys/class/ 目录下的一个子目录,如下图为 yocto linux /sys/class/的目录结构

Edison上/sys/class 目录结构

Edison上/sys/class 目录结构

在每一设备类型之下会注册有许多该设备类型的对象,对应于注册到这个类型的具体的设备,如下图我们以 gpio 类型展开,这也是进行 mraa 开发经常会接触到的目录。

Edison GPIO 目录结构

Edison GPIO 目录结构

可以看到目录之下有许多的 symbolic link 链向实际的设备对象,每个link 的名称与设备名称一致。对一某个具体的设备对象,在它目录下你会看到许多属性,如下图,许多属性是以ASCII文件形式存在的,可以直接使用 cat 命令查看其内容,通过 echo nano 等工具来修改他们的值,从而完成对设备地控制,当然使用 I/O 函数在程序中进行读写也可以达到相同的目的,这也是 MRAA 库所使用的方法,在稍后我们会详细地说明。有一点值得注意的是,sysfs 中的属性定义为单值的,也就是说一个属性对应一个文件,文件中只只留一个值,正确的值读出写入才可以正确地控制设备。

设备对象目录中的属性

设备对象目录中的属性

在上图中可以看到许多文件,这些文件对应着该设备的属性,属性都是自述的很容易猜到他们的功能,比如 “direction”可以用来设置该 gpio 接口是输入还是输出。

在 Linux 统一设备模型中通过 sysfs 可以以多种方式找到设备,比如上边可以通过设备的类型来找,也可以直接通过 /sys/devices 来找,总之目录结构都是非常简单的包含关系,这样系统可以把设备、总线、驱动等等关联起来。关于 sysfs 的内容很多可以写成书,这里只是个概述我们以后还会对此进行说明,接下来我们回到主题看看 MRAA 库是如何通过 sysfs 来完成 I/O 控制的。

3.2 MRAA I2C接口深入剖析

上一篇中我们已经给出了使用 MRAA 进行 I2C 总线控制的例程,基本上可以分为三步:MRAA库初始化;I2C配置,使其信道连接到扩展板上;进行数据收发。

3.2.1 MRAA库初始化

通过 MRAA C函数 mraa_init() 来对 MRAA 库进行初始化。mraa_init() 的原型如下

1
mraa_result_t __attribute__((constructor)) mraa_init();

因为使用了 constructor 属性,所以当使用GCC进行编辑时该函数会在 main() 之前被自动调用,不需要我们在应用程序中显式地调用,当然如果显式调用了也是无害的。在 mraa_init() 中可以看到下边的语句

1
2
3
4
5
6
7
8
9
#ifdef X86PLAT
// Use runtime x86 platform detection
platform_type = mraa_x86_platform();
#elif ARMPLAT
// Use runtime ARM platform detection
platform_type = mraa_arm_platform();
#else
#error mraa_ARCH NOTHING
#endif

Edison 本身使用 Atom 作为CPU,它是ia32架构的x86处理器,因此我们会在配置项目时加上 X86PLAT 定义( MRAA 目前开始支持 Raspberry Pi 的硬件,所以也支持 ARM ),当 mraa_init() 执行时会实际调用 mraa_x86_platform() 进行初始化。

mraa_x86_platform() 是在 x86.c 源文件中实现的,它的作用是识别主板类型,最后针对不同的主板进行初始化工作。所谓的初始化工作是指诸如某一GPIO的多路复用器控制端口是什么,输入输出由哪个端口控制以及支持的端口模式等等,MRAA把这些定义都放在映射表中,对开发者来说是透明的,开发者无需理会这些细节程序运行过程中由 MRAA 来自动完成控制。由于这部分是与硬件相关,不同主板初始化过程不同,这里只针对 Arduino Board 进行说明。

Arduino Board 的初始化实际由  mraa_board_t* mraa_intel_edison_fab_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
mraa_board_t*
mraa_intel_edison_fab_c()
{
mraa_board_t* b = (mraa_board_t*) malloc(sizeof(mraa_board_t));
if (b == NULL) {
return NULL;
}
 
b->platform_name_length = strlen(PLATFORM_NAME) + 1;
b->platform_name = (char*) malloc(sizeof(char) * b->platform_name_length);
if (b->platform_name == NULL) {
goto error;
}
strncpy(b->platform_name, PLATFORM_NAME, b->platform_name_length);
 
// This seciton will also check if the arduino board is there
tristate = mraa_gpio_init_raw(214);
if (tristate == NULL) {
syslog(LOG_INFO, "edison: Failed to initialise Arduino board TriState,\
assuming Intel Edison Miniboard\n");
if (mraa_intel_edison_miniboard(b) != MRAA_SUCCESS) {
goto error;
}
return b;
}
// Now Assuming the edison is attached to the Arduino board.
b->phy_pin_count = 20;
b->gpio_count = 14;
b->aio_count = 6;
 
advance_func->gpio_dir_pre = &mraa_intel_edison_gpio_dir_pre;
advance_func->gpio_init_post = &mraa_intel_edison_gpio_init_post;
advance_func->gpio_dir_post = &mraa_intel_edison_gpio_dir_post;
advance_func->i2c_init_pre = &mraa_intel_edison_i2c_init_pre;
advance_func->aio_get_valid_fp = &mraa_intel_edison_aio_get_fp;
advance_func->aio_init_pre = &mraa_intel_edison_aio_init_pre;
advance_func->aio_init_post = &mraa_intel_edison_aio_init_post;
advance_func->pwm_init_pre = &mraa_intel_edison_pwm_init_pre;
advance_func->pwm_init_post = &mraa_intel_edison_pwm_init_post;
advance_func->spi_init_pre = &mraa_intel_edison_spi_init_pre;
advance_func->spi_init_post = &mraa_intel_edison_spi_init_post;
advance_func->gpio_mode_replace = &mraa_intel_edison_gpio_mode_replace;
advance_func->uart_init_pre = &mraa_intel_edison_uart_init_pre;
advance_func->uart_init_post = &mraa_intel_edison_uart_init_post;
advance_func->gpio_mmap_setup = &mraa_intel_edison_mmap_setup;
 
b->pins = (mraa_pininfo_t*) malloc(sizeof(mraa_pininfo_t)*MRAA_INTEL_EDISON_PINCOUNT);
if (b->pins == NULL) {
goto error;
}
 
mraa_gpio_dir(tristate, MRAA_GPIO_OUT);
mraa_intel_edison_misc_spi();
 
b->adc_raw = 12;
b->adc_supported = 10;
b->pwm_default_period = 5000;
b->pwm_max_period = 218453;
b->pwm_min_period = 1;
 
strncpy(b->pins[0].name, "IO0", 8);
b->pins[0].capabilites = (mraa_pincapabilities_t) {1,1,0,0,0,0,0,1};
b->pins[0].gpio.pinmap = 130;
b->pins[0].gpio.parent_id = 0;
b->pins[0].gpio.mux_total = 0;
b->pins[0].uart.pinmap = 0;
b->pins[0].uart.parent_id = 0;
b->pins[0].uart.mux_total = 0;
 
strncpy(b->pins[1].name, "IO1", 8);
b->pins[1].capabilites = (mraa_pincapabilities_t) {1,1,0,0,0,0,0,1};
b->pins[1].gpio.pinmap = 131;
b->pins[1].gpio.parent_id = 0;
b->pins[1].gpio.mux_total = 0;
b->pins[1].uart.pinmap = 0;
b->pins[1].uart.parent_id = 0;
b->pins[1].uart.mux_total = 0;
 
...
 
//Everything else but A4 A5 LEAVE
pinmodes[18].gpio.sysfs = 14;
pinmodes[18].gpio.mode = 0;
pinmodes[18].i2c.sysfs = 28;
pinmodes[18].i2c.mode = 1;
 
pinmodes[19].gpio.sysfs = 165;
pinmodes[19].gpio.mode = 0;
pinmodes[19].i2c.sysfs = 27;
pinmodes[19].i2c.mode = 1;
 
return b;
error:
syslog(LOG_CRIT, "edison: Arduino board failed to initialise");
free(b);
return NULL;
}

可以看到第17行到25行,MRAA去到GPIO214,如果有说明用的是 Arduino board,否则会假定目前初始化的是 mini breakout board。

第31行45行,进行一些函数指针的绑定,从函数名字可以猜到这些函数是用于设置某类端口时前期做些准备工作,设置结束后再做些善后。比如配置 I2C 端口前需要将三态门(gpio214)置为高阻抗,断开所有外部的输入,在配置完成后会恢复。

52行到最后,都是针对 Arduino board 进行端口的配置,某一具体的端口可能会有多个功能,如A5(板子上的模拟信号输入端)可以作为GPIO使用,同时也是I2C接口,具体当前是用作 GPIO 还是 I2C 得由其它的 GPIO 控制电路中“开关”的导通与截止,Intel 给出了 Arduino board 的 schematic,具体的端口的控制方式可以参考该文档。我们以 A4 端口为例进行说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
strncpy(b->pins[18].name, "A4", 8);
b->pins[18].capabilites = (mraa_pincapabilities_t) {1,1,0,0,0,1,1,0};
b->pins[18].i2c.pinmap = 1;
b->pins[18].i2c.mux_total = 1;
b->pins[18].i2c.mux[0].pin = 204;
b->pins[18].i2c.mux[0].value = 0;
b->pins[18].aio.pinmap = 4;
b->pins[18].aio.mux_total = 1;
b->pins[18].aio.mux[0].pin = 204;
b->pins[18].aio.mux[0].value = 1;
b->pins[18].gpio.pinmap = 14;
b->pins[18].gpio.mux_total = 1;
b->pins[18].gpio.mux[0].pin = 204;
b->pins[18].gpio.mux[0].value = 0;

可以看到 A4 口有三个用处,可以作为 GPIO使用,也可以作为模拟信号输入端,同时也是 I2C 端口。代码中三个端口都是复用的(mux_total=1),当需要把A4定为某类用途时得设置多路复用器,这样子可以中止两路输入/输出,只让我们想要的那一路导通。对于多路复用器(这里是两路复用)通过 gpio204作为地址进行选择,所以 mux[0].pin=204,而对应用途复用器地址由 mux[0].value 决定。代码中的 .pinmap 是 sysfs 中的端口号,这里作映射可以方便 MRAA 在后边对具体的端口设置时快速地找到它在 sysfs 的具体端口,我们在后边详细说明。

之后就是写 pinmodes 映射,因为之前提到过许多端口是复用的,意思是指 arduino board或者mini breakout board上使用了外部的复用器,在 SoC内部一些 GPIO 是有内部复用的,通过 pinmodes MRAA 可以让某个 GPIO 管脚作为特定的用途使用。这些我们也会在稍后进行详述。

3.2.2 I2C 初始化

完成对整个 MRAA 库初始化后,我们要使用 I2C 接口还得事先对其初始化,初始化的过程本质上是设置对应的端口,好让 I2C 接口的信道能接通到 Arduino board 上的跳帽上。

下图为 Arduino board 上 I2C GPIO 映射逻辑电路图

I2C接口GPIO映射逻辑电路图

I2C接口GPIO映射逻辑电路图

结合 mraa_i2c_init() 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int pos = plat->i2c_bus[bus].sda;
if (plat->pins[pos].i2c.mux_total > 0)
if (mraa_setup_mux_mapped(plat->pins[pos].i2c) != MRAA_SUCCESS) {
syslog(LOG_ERR, "i2c: Failed to set-up i2c sda multiplexer");
return NULL;
}
 
pos = plat->i2c_bus[bus].scl;
if (plat->pins[pos].i2c.mux_total > 0)
if (mraa_setup_mux_mapped(plat->pins[pos].i2c) != MRAA_SUCCESS) {
syslog(LOG_ERR, "i2c: Failed to set-up i2c scl multiplexer");
return NULL;
}
 
return mraa_i2c_init_raw((unsigned int) plat->i2c_bus[bus].bus_id);

前两段是设置多路复用器使得 I2C 被连接到 A4/A5 接线柱上,关键的初始化部分在 mraa_i2c_init_raw()里实现,而 mraa_i2c_init_raw() 会调用 mraa_intel_edison_i2c_init_pre() 来完成,该函数实现如下

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
mraa_result_t mraa_intel_edison_i2c_init_pre(unsigned int bus)
{
if (miniboard == 0) {
if (bus != 6) {
syslog(LOG_ERR, "edison: You can't use that bus, switching to bus 6");
bus = 6;
}
mraa_gpio_write(tristate, 0);
mraa_gpio_context io18_gpio = mraa_gpio_init_raw(14);
mraa_gpio_context io19_gpio = mraa_gpio_init_raw(165);
mraa_gpio_dir(io18_gpio, MRAA_GPIO_IN);
mraa_gpio_dir(io19_gpio, MRAA_GPIO_IN);
mraa_gpio_close(io18_gpio);
mraa_gpio_close(io19_gpio);
 
mraa_gpio_context io18_enable = mraa_gpio_init_raw(236);
mraa_gpio_context io19_enable = mraa_gpio_init_raw(237);
mraa_gpio_dir(io18_enable, MRAA_GPIO_OUT);
mraa_gpio_dir(io19_enable, MRAA_GPIO_OUT);
mraa_gpio_write(io18_enable, 0);
mraa_gpio_write(io19_enable, 0);
mraa_gpio_close(io18_enable);
mraa_gpio_close(io19_enable);
 
mraa_gpio_context io18_pullup = mraa_gpio_init_raw(212);
mraa_gpio_context io19_pullup = mraa_gpio_init_raw(213);
mraa_gpio_dir(io18_pullup, MRAA_GPIO_IN);
mraa_gpio_dir(io19_pullup, MRAA_GPIO_IN);
mraa_gpio_close(io18_pullup);
mraa_gpio_close(io19_pullup);
 
mraa_intel_edison_pinmode_change(28, 1);
mraa_intel_edison_pinmode_change(27, 1);
 
mraa_gpio_write(tristate, 1);
} else {
if(bus != 6 && bus != 1) {
syslog(LOG_ERR, "edison: You can't use that bus, switching to bus 6");
bus = 6;
}
int scl = plat->pins[plat->i2c_bus[bus].scl].gpio.pinmap;
int sda = plat->pins[plat->i2c_bus[bus].sda].gpio.pinmap;
mraa_intel_edison_pinmode_change(sda, 1);
mraa_intel_edison_pinmode_change(scl, 1);
}
 
return MRAA_SUCCESS;
}

关于 Intel Edison 的 GPIO mapping 除了可以参考逻辑电路图以外,emutex lab 专门为此出了一篇详尽的文章《Intel Edison GPIO Pin Multiplexing Guide》,对照两个文档我们来一步步说明 MRAA I2C 初始化过程。

1
mraa_gpio_write(tristate, 0);

在作出任何设置之前,先把三态门至成高阻抗

1
2
3
4
5
6
mraa_gpio_context io18_gpio = mraa_gpio_init_raw(14);
mraa_gpio_context io19_gpio = mraa_gpio_init_raw(165);
mraa_gpio_dir(io18_gpio, MRAA_GPIO_IN);
mraa_gpio_dir(io19_gpio, MRAA_GPIO_IN);
mraa_gpio_close(io18_gpio);
mraa_gpio_close(io19_gpio);

因为 I2C_6 的SDA/SCL是跟 GPIO14和165复用的(这也就是说为什么 A4/A5 跳线冒可以作为 GPIO或者I2C使用的原因),所以我们得先保证 GPIO14 和 GPIO165不输出信号,mraa_gpio_init_raw()函数会检查是否系统中已经export了指定的gpio端口,如果没有在系统启动时注册过或者export过,那么这个函数会直接export对应端口,这时可以看到在 /sys/class/gpio/看到对应的目录,如 gpio14 和 gpio165。

接下来是调用 mraa_gpio_dir() 来设置端口的方向,即指定端口是输出信号还是接收信号。通过把这两个端口都设成输入口避免 gpio14/gpio165信号对 I2C 信号干扰。如果深入到 mraa_gpio_dir() 函数实现,可以看到如下的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
snprintf(filepath, MAX_SIZE, SYSFS_CLASS_GPIO "/gpio%d/direction", dev->pin);
 
int direction = open(filepath, O_RDWR);
 
...
 
switch(dir) {
case MRAA_GPIO_OUT:
length = snprintf(bu, sizeof(bu), "out");
break;
case MRAA_GPIO_IN:
length = snprintf(bu, sizeof(bu), "in");
break;
default:
close(direction);
return MRAA_ERROR_FEATURE_NOT_IMPLEMENTED;
}
 
if (write(direction, bu, length*sizeof(char)) == -1) {
close(direction);
return MRAA_ERROR_INVALID_RESOURCE;
}

可以看到实际上 MRAA 是使用 sysfs 文件系统,打开对应的内核对象设置其属性完成对端口的设置。

回到 mraa_intel_edison_i2c_init_pre(),接下来是

1
2
3
4
5
6
7
8
mraa_gpio_context io18_enable = mraa_gpio_init_raw(236);
mraa_gpio_context io19_enable = mraa_gpio_init_raw(237);
mraa_gpio_dir(io18_enable, MRAA_GPIO_OUT);
mraa_gpio_dir(io19_enable, MRAA_GPIO_OUT);
mraa_gpio_write(io18_enable, 0);
mraa_gpio_write(io19_enable, 0);
mraa_gpio_close(io18_enable);
mraa_gpio_close(io19_enable);

GPIO14/165两个端口除了内部的方向设置以外,还通过 GPIO236/237两个口各控制一个门电路用以控制外部的输入输出设置,同样的外部的门控制仍然是把 14/165 作为输入口设置。

接下来的代码为

1
2
3
4
5
6
mraa_gpio_context io18_pullup = mraa_gpio_init_raw(212);
mraa_gpio_context io19_pullup = mraa_gpio_init_raw(213);
mraa_gpio_dir(io18_pullup, MRAA_GPIO_IN);
mraa_gpio_dir(io19_pullup, MRAA_GPIO_IN);
mraa_gpio_close(io18_pullup);
mraa_gpio_close(io19_pullup);

Arduino board 配备了完整的板载上拉电路,通过导通 GPIO212&213两个口可以把 I2C 总线连接上两个 4.7KΩ 的上拉电阻并默认接到 5.5V 电源上,上边这段代码就是把212、213接通上拉电阻。

接下来

1
2
3
4
mraa_intel_edison_pinmode_change(28, 1);
mraa_intel_edison_pinmode_change(27, 1);
 
mraa_gpio_write(tristate, 1);

这一段设置 GPIO27和28为 I2C 端口(详见《Intel Edison GPIO Pin Multiplexing Guide》中表3),并恢复三态门。这里不再详述。

3.3.3 使用 I2C 收发数据

通过上边的设置,Arduino board上的A4/A5接线帽会实际接上 I2C_6的 SDA/SCL,把从机接上这两个跳线就可以直接使用了。要收发数据可以参考前一篇的源码,如下

mraa_i2c_address( i2c_context, TRH_SENSOR_I2C_ADDR );
data_requirement_buffer[0] = TRH_SENSOR_FUN_CODE_READ_REG;
data_requirement_buffer[1] = TRH_RH_REG_ADDR_H;
data_requirement_buffer[2] = 4;
if ( mraa_i2c_write( i2c_context, data_requirement_buffer, 3 )==MRAA_SUCCESS)
printf(“requirement has been send\n”);

发送数据使用 mraa_i2c_write() 或者 mraa_i2c_write_byte() ,这两个函数会调用  mraa_i2c_smbus_access() 完成数据发送任务,本质上是使用 ioctl() 通过内核的支持来发送二进制数据。同样的道理,接收数据使用 mraa_i2c_read()、mraa_i2c_read_byte()、mraa_i2c_read_byte_data() 通过 read() 或者 mraa_i2c_smbus_access() 由内核来处理数据的接收。

关于内核和驱动开发,是一个大的话题,我们也会在开源项目中通过实例来一一讲述如何在 yocto linux 上做到这些。

这里还有一个细节,在第一篇中我们说过 I2C 可以工作在多种模式下,如果我们使用的是官方给出的 image ,那么 I2C 只能工作在 fast mode下,也就是时钟频率为 400KHz,而实际应用中会遇到很多传感器只能工作在低频率下(节能),所以我们将会在下一篇文章中讲述如何通过重新编辑 yocto 来达到这一目的 :)

希望文章能给大家进行 Edison 开发起到些提示作用,文章中有错误、遗漏可以通过加入我们的开发群 Q:329401876 来与我们联系,Gekius 愿为你解答问题 :)

Graphics/3D programming
虚拟头盔 Oculus 获增 7500万美元B轮投资,加快进入消费领域的步伐
General
Mac Pro新工作站开始发售,来感受一下它的迷人之处
hardware
【图文】Intel Edison Arduino kit 初步上手安装教程
There are currently no comments.