at24c02-用户态-io读写.md

MingruiZhou

1. at24c02-用户态-io读写

本着一切皆文件的理念,在linux下可使用IO函数进行at24c02的读写操作,通常是按单字节进行读写,由于at24c02数据存储量并不多,对性能影响较弱。因此本文将率先进行单字节读写示例。当然按页读写也是要实现的,万一需要呢。

在读写前势必要引用一下头文件:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <unistd.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>

以及根据at24c02的特性定义一些宏:

#define AT24C02_PAGE_SIZE 16    /* 页大小 */
#define AT24C02_PAGE_MASK (AT24C02_PAGE_SIZE - 1)   /* 页掩码 */
#define AT24C02_PAGE_SHIFT 4    /* 页移位 */

为了便于i2c设备节点的open操作,也进行了初步的封装:

static int drv_i2c_open(int adapter_nr)
{
    int fd;
    char filename[20];

    /* open such as "/dev/i2c-%d" device node */
    sprintf(filename, "/dev/i2c-%d", adapter_nr);
    fd = open(filename, O_RDWR);
    if (fd >= 0) {
        return fd;
    }

    /* open such as "/dev/i2c/%d" device node */
    if (errno == ENOENT) {
        filename[8] = '/';
        fd = open(filename, O_RDWR);
    }

    return fd;
}

如上,drv_i2c_open函数考虑了"/dev/i2c-%d"和"/dev/i2c/%d"的设备节点结构,对于提高代码适应性有一定作用。


1.1. at24c02单字节读写

1.1.1. at24c02单字节读

由于at24c02读操作没有延迟,因此可直接进行IO读写,如下:

int drv_i2c_read8(int adapter_nr, const uint8_t i2c_addr,
          const uint8_t reg_addr, uint8_t * const data)
{
    int fd;
    int ret = 0;

    if (NULL == data)
        return -EINVAL;

    ret = fd = drv_i2c_open(adapter_nr);
    if (ret < 0)
        return ret;

    do {
        if ((ret = ioctl(fd, I2C_SLAVE, i2c_addr)) < 0)
            break;

        if ((ret = write(fd, &reg_addr, 1)) != 1)
            break;

        if ((ret = read(fd, data, 1)) != 1)
            break;
    } while (0);

    close(fd);

    return ret;
}

如上,首先使用drv_i2c_open打开i2c设备节点,其次使用ioctl设置设备地址,再次使用write函数设置需要操作的寄存器地址,最后使用read函数读取1字节数据。

通常这么底层的函数不会暴露给用户,用户也不必按字节一个一个读,因此做一层封装:

int drv_i2c_read_bytes(int adapter_nr, const uint8_t i2c_addr,
               const uint8_t reg_addr, uint8_t * const data,
               const uint8_t cnt)
{
    uint8_t i;
    int ret = 0;

    for (i = 0; i < cnt; i++) {
        ret = drv_i2c_read8(adapter_nr, i2c_addr,
                    reg_addr + i, data + i);
        if (ret < 0)
            break;
    }

    return ret;
}

如上,只需要用户设置i2c-bus编号、i2c设备地址、寄存器地址以及读回数据量即可轻松完成读操作。


1.1.2. at24c02单字节写

写操作就略显麻烦,由于at24c02单次写操作完毕后,需要给足够的时间at24c02进行数据同步。因此封装一个重试机制,在规定时间以及次数内无法写成功则放弃,参考如下:

static int i2c_try_write(int fd, uint8_t * data, uint8_t data_len,
             uint8_t retry_times)
{
    int ret = 0;

    while (retry_times--) {
        ret = write(fd, data, data_len);
        if (ret < 0) {
            /* ENXIO通常是芯片内部忙,无法响应外部操作 */
            if (ENXIO == errno) {
                usleep(1000);   /* 每次休眠1ms */
                continue;
            }

            perror("write");
            break;
        } else if (ret != data_len) {
            ret = -1;
            perror("write");
            break;
        }

        break;
    }

    return ret;
}

如上,设定retry_times参数可指定重试次数,上述代码每次重试间隔为1ms。其原因在于运行平台的任务调度的间隔也正好是1ms(1000Hz),可降低因休眠时间过短导致CPU盲等待,实际使用时可参考具体平台略加修改。

由于是写操作,那么就没有读操作那么多流程上的事情了。读操作首先要写寄存器来设定数据读取位置,然后读数据,需要分两步走。而写操作可在待写寄存器后直接追加数据,一步完成,如下:

int drv_i2c_write8(const int adapter_nr, const uint8_t i2c_addr,
           const uint8_t reg_addr, const uint8_t data)
{
    int fd;
    int ret = 0;

    ret = fd = drv_i2c_open(adapter_nr);
    if (ret < 0)
        return ret;

    do {
        uint8_t cnt = 5;    /* 重试次数 */
        uint8_t msg_payload[] = { reg_addr, data };

        if ((ret = ioctl(fd, I2C_SLAVE, i2c_addr)) < 0)
            break;

        ret = i2c_try_write(fd, msg_payload, sizeof(msg_payload), cnt);
        if (ret < 0)
            break;
    } while (0);

    close(fd);

    return ret;
}

同样,便于操作,也封装了这个函数如下:

int drv_i2c_write_bytes(const int adapter_nr, const uint8_t i2c_addr,
            const uint8_t reg_addr, uint8_t * const data,
            const uint8_t cnt)
{
    uint8_t i;
    int ret = 0;

    for (i = 0; i < cnt; i++) {
        ret = drv_i2c_write8(adapter_nr, i2c_addr,
                     reg_addr + i, *(data + i));
        if (ret < 0)
            break;
    }

    return ret;
}

使用方法与drv_i2c_read_bytes类似。


1.1.3. at24c02单字节综合测试

测试方法很简单,从at24c02的0x11寄存开始,使用写函数写入一句不要脸的情话:"I Love You, Baby !",然后使用读函数读出来,如成功读出则测试合格,示例代码如下:

    int adapter_nr = 1; /* probably dynamically determined */
    int ret;
    uint8_t reg = 0x11; /* Device register to access */
    uint8_t i2c_addr = 0x50;    /* Device address to access */
    
    uint8_t buf[] = "I Love You, Baby !";
    
    /* 恢复待写区域 */
    system("i2ctransfer -y 1 w255@0x50 0x10 0xff=");
    system("i2ctransfer -y 1 w255@0x50 0x20 0xff=");
    
    ret = drv_i2c_write_bytes(adapter_nr, i2c_addr,
                  reg, buf, sizeof(buf));
    if (ret < 0) {
        printf("ERROR HANDLING: i2c transaction failed\n");
    }
    usleep(10000);
    
    system("i2cdump -y 1 0x50 b");
    memset(buf, 0, sizeof(buf));
    ret = drv_i2c_read_bytes(adapter_nr, i2c_addr, reg,
                 buf, sizeof(buf));
    if (ret < 0) {
        printf("ERROR HANDLING: i2c transaction failed\n");
    }
    buf[sizeof(buf) - 1] = '\0';
    
    printf("reg %d -> %s\n", reg, buf);

虽然,情话不要脸,但实验还是成功的,如下:

# ./at24c02a_app_io
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f    0123456789abcdef
00: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
10: ff 49 20 4c 6f 76 65 20 59 6f 75 2c 20 42 61 62    .I Love You, Bab
20: 79 20 21 00 ff ff ff ff ff ff ff ff ff ff ff ff    y !.............
30: 00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
40: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
50: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
60: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
70: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
80: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
90: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
a0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
b0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
c0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
d0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
e0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
f0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
reg 17 -> I Love You, Baby !

1.2. at24c02按页读写

上述使用单字节读写at24c02效率明显不高,同时反复的进行文件的打开和关闭,专治强迫症患者。因此可考虑按页读写at24c02。


1.2.1. at24c02按页读

同样,由于at24c02读没有延迟,因此可放心大胆的随意读。

int drv_i2c_read(int adapter_nr, const uint8_t i2c_addr,
         const uint8_t reg_addr, uint8_t * const data,
         const uint8_t data_len)
{
    int fd;
    uint8_t addr = reg_addr;
    int ret = 0;

    if (NULL == data)
        return -EINVAL;

    ret = fd = drv_i2c_open(adapter_nr);
    if (ret < 0)
        return ret;

    do {
        if ((ret = ioctl(fd, I2C_SLAVE, i2c_addr)) < 0)
            break;

        if ((ret = write(fd, &addr, 1)) != 1)
            break;

        if ((ret = read(fd, data, data_len)) != data_len)
            ret = -EINVAL;
    } while (0);

    close(fd);

    return ret;
}

如上,读操作与drv_i2c_read8如出一辙,不再赘述。


1.2.2. at24c02按页写

写操作就比较麻烦了,同时要考虑写延迟和换页问题,但聪明如我怎么可能搞不定呢。

int drv_i2c_write(const int adapter_nr, const uint8_t i2c_addr,
          const uint8_t reg_addr, uint8_t * const data,
          const uint8_t data_len)
{
    int fd;
    int ret = 0;
    int loop;

    if (NULL == data)
        return -EINVAL;

    ret = fd = drv_i2c_open(adapter_nr);
    if (ret < 0)
        return ret;

    /* 计算跨页次数 */
    loop = (reg_addr & AT24C02_PAGE_MASK) + data_len;
    loop = (loop + AT24C02_PAGE_MASK) / AT24C02_PAGE_SIZE;

    do {
        int i;

        if ((ret = ioctl(fd, I2C_SLAVE, i2c_addr)) < 0)
            break;

        for (i = 0; i < loop; i++) {
            int j;
            uint8_t cnt = 5;    /* 重试次数 */
            uint8_t start_addr;
            uint8_t xfer_len;
            uint8_t xfer_cnt;
            uint8_t *xfer_offset;
            uint8_t transfer_data[AT24C02_PAGE_SIZE + 1];

            /* 首次以reg_addr为准,后续则以页起止地址为准 */
            if (0 == i) {
                start_addr = reg_addr;
            } else {
                start_addr = (reg_addr + i * AT24C02_PAGE_SIZE);
                start_addr &= ~AT24C02_PAGE_MASK;
            }

            /* 已传输数据量 */
            xfer_cnt = start_addr - reg_addr;

            /* 计算本次需要传输的数据量 */
            xfer_len = AT24C02_PAGE_SIZE -
                (start_addr & AT24C02_PAGE_MASK);

            /* 最后一次少于一页则按实际情况传输 */
            if ((data_len - xfer_cnt) < AT24C02_PAGE_SIZE)
                xfer_len = data_len - xfer_cnt;

            xfer_offset = data + xfer_cnt;

            /* 填充寄存器地址以及待写数据 */
            transfer_data[0] = start_addr;
            for (j = 1; j <= xfer_len; j++)
                transfer_data[j] = *(xfer_offset + j - 1);

            /* 写入 */
            ret = i2c_try_write(fd, transfer_data,
                        xfer_len + 1, cnt);
            if (ret < 0)
                break;
        }
    } while (0);

    close(fd);

    return ret;
}

本函数的关键点在于第一页和最后一页的处理,第一页可能并不在页头部,因此首次写的位置和长度需要额外处理;而最后一页可能数据量不足一页,处理不慎可能会覆盖后续数据。


1.2.3. at24c02按页读写综合测试

测试方法同样简单,也是从at24c02的0x11寄存开始,不过本次写入一句更不要脸的情话:"I Love You more than I can say!",由于这句情话正好略过页头,完整写一页并且最后一页只写一个数据。一句情话同时满足三个场景的测试要求,妙啊!

    int adapter_nr = 1; /* probably dynamically determined */
    int ret;
    uint8_t reg = 0x11; /* Device register to access */
    uint8_t i2c_addr = 0x50;    /* Device address to access */
    
    uint8_t buf[] = "I Love You more than I can say!";

    /* 恢复待写区域 */
    system("i2ctransfer -y 1 w255@0x50 0x10 0xff=");
    system("i2ctransfer -y 1 w255@0x50 0x20 0xff=");

    ret = drv_i2c_write(adapter_nr, i2c_addr, reg,
                buf, sizeof(buf));
    if (ret < 0) {
        printf("ERROR HANDLING: i2c transaction failed\n");
    }

    usleep(10000);
    system("i2cdump -y 1 0x50 b");

    memset(buf, 0, sizeof(buf));
    ret = drv_i2c_read(adapter_nr, i2c_addr, reg, buf, sizeof(buf));
    if (ret < 0) {
        printf("ERROR HANDLING: i2c transaction failed\n");
    }
    buf[sizeof(buf) - 1] = '\0';
    printf("reg %d -> %s\n", reg, buf);

如上,虽然,“我爱你在心口难开”,但实验的成功是必须的,如下:

# ./at24c02a_app_io
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f    0123456789abcdef
00: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
10: ff 49 20 4c 6f 76 65 20 59 6f 75 20 6d 6f 72 65    .I Love You more
20: 20 74 68 61 6e 20 49 20 63 61 6e 20 73 61 79 21     than I can say!
30: 00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
40: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
50: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
60: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
70: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
80: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
90: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
a0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
b0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
c0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
d0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
e0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
f0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff    ................
reg 17 -> I Love You more than I can say!

1.3. 小结

使用IO函数进行at24c02的读写操作显然是最容易理解的也比较容易,但没有了设备驱动操作的灵魂,略显不伦不类。完整代码如下:

/* aarch64-linux-gnu-gcc at24c02a_app_io.c -o at24c02a_app_io -Wall */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <unistd.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>

#define AT24C02_PAGE_SIZE 16    /* 页大小 */
#define AT24C02_PAGE_MASK (AT24C02_PAGE_SIZE - 1)   /* 页掩码 */
#define AT24C02_PAGE_SHIFT 4    /* 页移位 */

static int drv_i2c_open(int adapter_nr)
{
    int fd;
    char filename[20];

    /* open such as "/dev/i2c-%d" device node */
    sprintf(filename, "/dev/i2c-%d", adapter_nr);
    fd = open(filename, O_RDWR);
    if (fd >= 0) {
        return fd;
    }

    /* open such as "/dev/i2c/%d" device node */
    if (errno == ENOENT) {
        filename[8] = '/';
        fd = open(filename, O_RDWR);
    }

    return fd;
}

static int i2c_try_write(int fd, uint8_t * data, uint8_t data_len,
             uint8_t retry_times)
{
    int ret = 0;

    while (retry_times--) {
        ret = write(fd, data, data_len);
        if (ret < 0) {
            /* ENXIO通常是芯片内部忙,无法响应外部操作 */
            if (ENXIO == errno) {
                usleep(1000);   /* 每次休眠1ms */
                continue;
            }

            perror("write");
            break;
        } else if (ret != data_len) {
            ret = -1;
            perror("write");
            break;
        }

        break;
    }

    return ret;
}

int drv_i2c_write8(const int adapter_nr, const uint8_t i2c_addr,
           const uint8_t reg_addr, const uint8_t data)
{
    int fd;
    int ret = 0;

    ret = fd = drv_i2c_open(adapter_nr);
    if (ret < 0)
        return ret;

    do {
        uint8_t cnt = 5;    /* 重试次数 */
        uint8_t msg_payload[] = { reg_addr, data };

        if ((ret = ioctl(fd, I2C_SLAVE, i2c_addr)) < 0)
            break;

        ret = i2c_try_write(fd, msg_payload, sizeof(msg_payload), cnt);
        if (ret < 0)
            break;
    } while (0);

    close(fd);

    return ret;
}

int drv_i2c_read8(int adapter_nr, const uint8_t i2c_addr,
          const uint8_t reg_addr, uint8_t * const data)
{
    int fd;
    int ret = 0;

    if (NULL == data)
        return -EINVAL;

    ret = fd = drv_i2c_open(adapter_nr);
    if (ret < 0)
        return ret;

    do {
        if ((ret = ioctl(fd, I2C_SLAVE, i2c_addr)) < 0)
            break;

        if ((ret = write(fd, &reg_addr, 1)) != 1)
            break;

        if ((ret = read(fd, data, 1)) != 1)
            break;
    } while (0);

    close(fd);

    return ret;
}

int drv_i2c_write_bytes(const int adapter_nr, const uint8_t i2c_addr,
            const uint8_t reg_addr, uint8_t * const data,
            const uint8_t cnt)
{
    uint8_t i;
    int ret = 0;

    for (i = 0; i < cnt; i++) {
        ret = drv_i2c_write8(adapter_nr, i2c_addr,
                     reg_addr + i, *(data + i));
        if (ret < 0)
            break;
    }

    return ret;
}

int drv_i2c_read_bytes(int adapter_nr, const uint8_t i2c_addr,
               const uint8_t reg_addr, uint8_t * const data,
               const uint8_t cnt)
{
    uint8_t i;
    int ret = 0;

    for (i = 0; i < cnt; i++) {
        ret = drv_i2c_read8(adapter_nr, i2c_addr,
                    reg_addr + i, data + i);
        if (ret < 0)
            break;
    }

    return ret;
}

int drv_i2c_write(const int adapter_nr, const uint8_t i2c_addr,
          const uint8_t reg_addr, uint8_t * const data,
          const uint8_t data_len)
{
    int fd;
    int ret = 0;
    int loop;

    if (NULL == data)
        return -EINVAL;

    ret = fd = drv_i2c_open(adapter_nr);
    if (ret < 0)
        return ret;

    /* 计算跨页次数 */
    loop = (reg_addr & AT24C02_PAGE_MASK) + data_len;
    loop = (loop + AT24C02_PAGE_MASK) / AT24C02_PAGE_SIZE;

    do {
        int i;

        if ((ret = ioctl(fd, I2C_SLAVE, i2c_addr)) < 0)
            break;

        for (i = 0; i < loop; i++) {
            int j;
            uint8_t cnt = 5;    /* 重试次数 */
            uint8_t start_addr;
            uint8_t xfer_len;
            uint8_t xfer_cnt;
            uint8_t *xfer_offset;
            uint8_t transfer_data[AT24C02_PAGE_SIZE + 1];

            /* 首次以reg_addr为准,后续则以页起止地址为准 */
            if (0 == i) {
                start_addr = reg_addr;
            } else {
                start_addr = (reg_addr + i * AT24C02_PAGE_SIZE);
                start_addr &= ~AT24C02_PAGE_MASK;
            }

            /* 已传输数据量 */
            xfer_cnt = start_addr - reg_addr;

            /* 计算本次需要传输的数据量 */
            xfer_len = AT24C02_PAGE_SIZE -
                (start_addr & AT24C02_PAGE_MASK);

            /* 最后一次少于一页则按实际情况传输 */
            if ((data_len - xfer_cnt) < AT24C02_PAGE_SIZE)
                xfer_len = data_len - xfer_cnt;

            xfer_offset = data + xfer_cnt;

            /* 填充寄存器地址以及待写数据 */
            transfer_data[0] = start_addr;
            for (j = 1; j <= xfer_len; j++)
                transfer_data[j] = *(xfer_offset + j - 1);

            /* 写入 */
            ret = i2c_try_write(fd, transfer_data,
                        xfer_len + 1, cnt);
            if (ret < 0)
                break;
        }
    } while (0);

    close(fd);

    return ret;
}

int drv_i2c_read(int adapter_nr, const uint8_t i2c_addr,
         const uint8_t reg_addr, uint8_t * const data,
         const uint8_t data_len)
{
    int fd;
    uint8_t addr = reg_addr;
    int ret = 0;

    if (NULL == data)
        return -EINVAL;

    ret = fd = drv_i2c_open(adapter_nr);
    if (ret < 0)
        return ret;

    do {
        if ((ret = ioctl(fd, I2C_SLAVE, i2c_addr)) < 0)
            break;

        if ((ret = write(fd, &addr, 1)) != 1)
            break;

        if ((ret = read(fd, data, data_len)) != data_len)
            ret = -EINVAL;
    } while (0);

    close(fd);

    return ret;
}

int main(void)
{
    int adapter_nr = 1; /* probably dynamically determined */
    int ret;
    uint8_t reg = 0x11; /* Device register to access */
    uint8_t i2c_addr = 0x50;    /* Device address to access */

    {
        uint8_t buf[] = "I Love You, Baby !";

        /* 恢复待写区域 */
        system("i2ctransfer -y 1 w255@0x50 0x10 0xff=");
        system("i2ctransfer -y 1 w255@0x50 0x20 0xff=");

        ret = drv_i2c_write_bytes(adapter_nr, i2c_addr,
                      reg, buf, sizeof(buf));
        if (ret < 0) {
            printf("ERROR HANDLING: i2c transaction failed\n");
        }
        usleep(10000);

        system("i2cdump -y 1 0x50 b");
        memset(buf, 0, sizeof(buf));
        ret = drv_i2c_read_bytes(adapter_nr, i2c_addr, reg,
                     buf, sizeof(buf));
        if (ret < 0) {
            printf("ERROR HANDLING: i2c transaction failed\n");
        }
        buf[sizeof(buf) - 1] = '\0';

        printf("reg %d -> %s\n", reg, buf);
    }

    {
        uint8_t buf[] = "I Love You more than I can say!";

        /* 恢复待写区域 */
        system("i2ctransfer -y 1 w255@0x50 0x10 0xff=");
        system("i2ctransfer -y 1 w255@0x50 0x20 0xff=");

        ret = drv_i2c_write(adapter_nr, i2c_addr, reg,
                    buf, sizeof(buf));
        if (ret < 0) {
            printf("ERROR HANDLING: i2c transaction failed\n");
        }

        usleep(10000);
        system("i2cdump -y 1 0x50 b");

        memset(buf, 0, sizeof(buf));
        ret = drv_i2c_read(adapter_nr, i2c_addr, reg, buf, sizeof(buf));
        if (ret < 0) {
            printf("ERROR HANDLING: i2c transaction failed\n");
        }
        buf[sizeof(buf) - 1] = '\0';
        printf("reg %d -> %s\n", reg, buf);
    }

    return 0;
}

email:MingruiZhou@outlook.com


阅读 557

linux内核从业者,略懂内存管理、进程调度以及驱动框架。

10 声望
0 粉丝
0 条评论
你知道吗?

linux内核从业者,略懂内存管理、进程调度以及驱动框架。

10 声望
0 粉丝
宣传栏