# MS-RTOS EEPROM 驱动开发

本章将介绍 MS-RTOS EEPROM 驱动开发及测试。

EEPROM 是电可擦可编程只读存储器(Electrically Erasable Programmable read only memory)的英文缩写,是一种掉电后数据不丢失(不挥发)存储芯片,常用来存储一些配置信息,以便系统重新上电的时候加载之。EEPOM芯片最常用的通讯方式就是I2C协议。

# 1. EEPROM 基础知识

# 1.1 硬件设计

EEPROM芯片的设备地址一共有7位,其中高4位固定为:1010 b,低3位则由A0/A1/A2信号线的电平决定。EEPROM芯片中还有一个WP引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时,可写入数据。

eeprom_interface

此处 A0/A1/A2 均为 0,所以 EEPROM 的7位设备地址是:101 0000b ,即0x50。由于I2C通讯时常常是地址跟读写方向连在一起构成一个8位数,且当R/W位为0时,表示写方向,所以加上7位地址,其值为"0xA0",常称该值为I2C设备的"写地址";当R/W位为1时,表示读方向,加上7位地址,其值为"0xA1",常称该值为"读地址"。

# 1.2 EEPROM 读写

EEPROM 不可跨页写,可跨页连续读。页写入模式和字节写入模式的操作一样,不同的是页写入模式需要在发送完第一个存储单元地址(页对齐)后,一次性发送 32 字节的写入数据,在发送停止位,写入过程中其余的地址增量由芯片内部自动完成,其余地址增量要求必须在当前页内,无法跨入下一页。无论字节写还是页写入方式,指令发送完成后(发送停止位后),芯片内部才开始写入,这时 SDA 会被芯片拉高,正在执行写入的从器件此时不会响应主器件的任何请求,即不会返回低电平的应答信号 ACK,最长写入时间是 10ms。编写程序时,在写操作完成后,可以不停发送伪指令(写入或读取命令)并查询是否有 ACK 返回,如果有 ACK 返回则表明从器件已处于空闲状态并可进行下一步操作。

eeprom_write

读时序的第一个通讯过程中,使用I2C发送设备地址寻址(写方向),接着发送要读取的"内存地址";第二个通讯过程中,再次使用I2C发送设备地址寻址,但这个时候的数据方向是读方向;在这个过程之后,EEPROM会向主机返回从"内存地址"开始的数据,一个字节一个字节地传输,只要主机的响应为"应答信号",它就会一直传输下去,主机想结束传输时,就发送"非应答信号",并以"停止信号"结束通讯,作为从机的EEPROM也会停止传输。

eeprom_read

# 2. EEPROM 驱动框架

# 2.1 驱动相关数据结构

(1)ms_io_device_t

任何 IO 设备对象都应该采用包含的方式来继承 ms_io_device 对象。在实现 UART 驱动时,可以自定义一个设备结构体 uart_dev_t ,该结构体中包含一个 ms_io_device 类型的成员,还包含一个 privinfo_t 类型的成员;privinfo_t 由驱动开发人员定义,一般用于记录一些设备私有的状态信息和控制信息。

/*
 * Private info
 */
typedef struct {
    ms_i2c_device_t i2c_dev;
    ms_handle_t     lock;

    /*
     * Expanded from geometry
     */
    ms_uint32_t     size;       /* total bytes in device */
    ms_uint16_t     pgsize;     /* write block size, in bytes */
    ms_uint16_t     addrlen;    /* number of bytes in data addresses */
    ms_uint16_t     haddrbits;  /* Number of bits in high address part */
    ms_uint16_t     haddrshift; /* bit-shift of high address part */
} privinfo_t;

/*
 * xx24xx device
 */
typedef struct {
    privinfo_t      priv;
    ms_io_device_t  dev;
} ms_xx24xx_dev_t;

(2)ms_io_driver_t

任何 IO 设备驱动对象都应该采用包含的方式来继承 ms_io_driver 对象。ms_io_driver 结构体包含一个名字节点 ms_io_name_node_t 和一个设备文件操作接口集 ms_io_driver_ops_t 指针;名字节点用于匹配设备,文件操作接口用于支持 MS-RTOS IO 系统对设备的访问和控制。

/*
 * Device operating function set
 */
static ms_io_driver_ops_t ms_xx24xx_drv_ops = {
        .type   = MS_IO_DRV_TYPE_CHR,
        .open   = __xx24xx_open,
        .close  = __xx24xx_close,
        .read   = __xx24xx_read,
        .write  = __xx24xx_write,
        .fstat  = __xx24xx_fstat,
        .ioctl  = __xx24xx_ioctl,
};

/*
 * Device driver
 */
static ms_io_driver_t ms_xx24xx_drv = {
        .nnode = {
            .name = MS_XX24XX_DRV_NAME,
        },
        .ops = &ms_xx24xx_drv_ops,
};

# 2.2 驱动的注册和卸载

(1)EEPROM 驱动开发流程:

  • 获取必要的软硬件开发资源,了解设备的基本特性;
  • 配置通讯使用的目标引脚为开漏模式;
  • 使能I2C外设的时钟;
  • 配置I2C外设的模式、地址、速率等参数并使能I2C外设;
  • 编写基本I2C按字节收发的函数;
  • 编写读写EEPROM存储内容的函数;
  • 编写测试程序,对读写数据进行校验;

(2)驱动的注册和卸载接口:

ms_err_t ms_io_driver_register(ms_io_driver_t *drv);
ms_err_t ms_io_driver_unregister(ms_io_driver_t *drv); //此接口暂不开放

(3)设备节点的注册和卸载接口:

ms_err_t ms_io_device_register(ms_io_device_t *dev, const char *dev_path, 
                               const char *drv_name, ms_ptr_t ctx);
ms_err_t ms_io_device_unregister(ms_io_device_t *dev);

# 2.3 EEPROM ioctl 命令

以下仅列出几个最基本的命令,可以在 sdk/src/driver/ms_drv_eeprom.h 文件中找到所有命令的定义。

命令 描述 参数
MS_EEPROM_CMD_GET_GEOMETRY 获得 EEPROM 设备的几何信息 ms_eeprom_geometry_t 指针

(1)ms_eeprom_geometry_t

typedef struct {
    ms_size_t       size;           // EEPROM 的大小
    ms_size_t       page_size;      // EEPROM 的页大小
} ms_eeprom_geometry_t;

(2)ms_eeprom_msg_t

typedef struct {
    ms_uint32_t     memaddr;       // EEPROM 设备的内部内存地址
    ms_ptr_t       *buf;           // 应用程序缓冲,用于存放要读取的数据,或者指向要写入数据的基址
    ms_size_t       len;           // 应用程序缓冲大小,同时指示要读取或写入数据的总长度
} ms_eeprom_msg_t;

# 2.4 EEPROM 驱动示例

EEPROM 驱动示例,仅作为参考。

#define __MS_IO
#include "ms_config.h"
#include "ms_rtos.h"
#include "ms_io_core.h"
#include <string.h>
#include "includes.h"

/*
 * Private info
 */
typedef struct {
    ms_handle_t     lockid;
    ms_uint32_t     eeprom_size;
} privinfo_t;

/*
 * Open device
 */
static int __eeprom_open(ms_ptr_t ctx, ms_io_file_t *file, int oflag, ms_mode_t mode)
{
    ms_atomic_inc(MS_IO_DEV_REF(file));

    return 0;
}

/*
 * Close device
 */
static int __eeprom_close(ms_ptr_t ctx, ms_io_file_t *file)
{
    ms_atomic_dec(MS_IO_DEV_REF(file));

    return 0;
}

/*
 * Read data from eeprom
 */
static int __eeprom_read_data(privinfo_t *priv, ms_eeprom_msg_t *msg)
{
    ms_uint16_t len = msg->len;
    int ret;

    if ((msg->memaddr >= priv->eeprom_size) ||
        (msg->memaddr + msg->len) > priv->eeprom_size) {
        ms_thread_set_errno(EINVAL);
        ret = -1;

    } else {
        if (BSP_EEPROM_ReadBuffer(msg->buf, msg->memaddr, &len) == EEPROM_OK) {
            ret = 0;

        } else {
            ms_thread_set_errno(EIO);
            ret = -1;
        }
    }

    return ret;
}

/*
 * Write data from eeprom
 */
static int __eeprom_write_data(privinfo_t *priv, ms_eeprom_msg_t *msg)
{
    int ret;

    if ((msg->memaddr >= priv->eeprom_size) ||
        (msg->memaddr + msg->len) > priv->eeprom_size) {
        ms_thread_set_errno(EINVAL);
        ret = -1;

    } else {
        if (BSP_EEPROM_WriteBuffer(msg->buf, msg->memaddr, msg->len) == EEPROM_OK) {
            ret = 0;

        } else {
            ms_thread_set_errno(EIO);
            ret = -1;
        }
    }

    return ret;
}

/*
 * Read device
 */
static ms_ssize_t __eeprom_read(ms_ptr_t ctx, ms_io_file_t *file, ms_ptr_t buf, ms_size_t len)
{
    ms_ssize_t ret;

    if (len % sizeof(ms_eeprom_msg_t) == 0) {
        privinfo_t      *priv = ctx;
        ms_eeprom_msg_t *msg = (ms_eeprom_msg_t *)buf;
        ms_uint32_t      n_msg = len / sizeof(ms_eeprom_msg_t);
        ms_uint32_t      i;

        ret = 0;

        ms_mutex_lock(priv->lockid, MS_TIMEOUT_FOREVER);

        for (i = 0; i < n_msg; i++, msg++) {
            if (!ms_access_ok((ms_const_ptr_t)msg->buf, msg->len, MS_ACCESS_W)) {
                ms_thread_set_errno(EFAULT);
                break;
            }

            if (__eeprom_read_data(priv, msg) != 0) {
                ms_thread_set_errno(EIO);
                break;
            }
        }

        ms_mutex_unlock(priv->lockid);

        if (i == n_msg) {
            ret = len;
        } else {
            ret = -1;
        }
    } else {
        ms_thread_set_errno(EINVAL);
        ret = -1;
    }

    return ret;
}

/*
 * Write device
 */
static ms_ssize_t __eeprom_write(ms_ptr_t ctx, ms_io_file_t *file, ms_const_ptr_t buf, ms_size_t len)
{
    ms_ssize_t ret;

    if (len % sizeof(ms_eeprom_msg_t) == 0) {
        privinfo_t      *priv = ctx;
        ms_eeprom_msg_t *msg = (ms_eeprom_msg_t *)buf;
        ms_uint32_t      n_msg = len / sizeof(ms_eeprom_msg_t);
        ms_uint32_t      i;

        ret = 0;

        ms_mutex_lock(priv->lockid, MS_TIMEOUT_FOREVER);

        for (i = 0; i < n_msg; i++, msg++) {
            if (!ms_access_ok((ms_const_ptr_t)msg->buf, msg->len, MS_ACCESS_R)) {
                ms_thread_set_errno(EFAULT);
                break;
            }

            if (!ms_eeprom_write_ok(msg->memaddr, msg->len)) {
                ms_thread_set_errno(EACCES);
                break;
            }

            if (__eeprom_write_data(priv, msg) != MS_ERR_NONE) {
                break;
            }
        }

        ms_mutex_unlock(priv->lockid);

        if (i == n_msg) {
            ret = len;
        } else {
            ret = -1;
        }
    } else {
        ms_thread_set_errno(EINVAL);
        ret = -1;
    }

    return ret;
}

/*
 * Control device
 */
static int __eeprom_ioctl(ms_ptr_t ctx, ms_io_file_t *file, int cmd, ms_ptr_t arg)
{
    privinfo_t *priv = ctx;
    int ret;

    switch (cmd) {
    case MS_EEPROM_CMD_GET_GEOMETRY:
        if (ms_access_ok(arg, sizeof(ms_eeprom_geometry_t), MS_ACCESS_W)) {
            ms_eeprom_geometry_t *geometry = (ms_eeprom_geometry_t *)arg;
            geometry->size = priv->eeprom_size;
            ret = 0;
        } else {
            ms_thread_set_errno(EFAULT);
            ret = -1;
        }
        break;

    default:
        ms_thread_set_errno(EINVAL);
        ret = -1;
        break;
    }

    return ret;
}

/*
 * Get device status
 */
static int __eeprom_fstat(ms_ptr_t ctx, ms_io_file_t *file, ms_stat_t *buf)
{
    privinfo_t *priv = ctx;

    buf->st_size = priv->eeprom_size;

    return 0;
}

/*
 * Device operating function set
 */
static const ms_io_driver_ops_t eeprom_drv_ops = {
        .type   = MS_IO_DRV_TYPE_CHR,
        .open   = __eeprom_open,
        .close  = __eeprom_close,
        .write  = __eeprom_write,
        .read   = __eeprom_read,
        .ioctl  = __eeprom_ioctl,
		.fstat  = __eeprom_fstat,
};

/*
 * Device driver
 */
static ms_io_driver_t eeprom_drv = {
        .nnode = {
            .name = "eeprom",
        },
        .ops = &eeprom_drv_ops,
};

/*
 * Register eeprom device driver
 */
ms_err_t eeprom_drv_register(void)
{
    return ms_io_driver_register(&eeprom_drv);
}

/*
 * Create eeprom device file
 */
ms_err_t eeprom_dev_register(const char *path)
{
    static privinfo_t priv_info;
    static ms_io_device_t dev;
    ms_err_t err;

    err = ms_mutex_create("eeprom_lock", MS_WAIT_TYPE_PRIO, &priv_info.lockid);
    if (err == MS_ERR_NONE) {

        if (BSP_EEPROM_Init() == EEPROM_OK) {

            priv_info.eeprom_size = BSP_EEPROM_GetCapacity();

            err = ms_io_device_register(&dev, path, "eeprom", &priv_info);

        } else {
            err = MS_ERR;
        }
    }

    return err;
}

# 3. EEPROM 应用程序

# 3.1 读写 EEPROM

#include <ms_rtos.h>
#include <string.h>
#include <driver/ms_drv_eeprom.h>
#include "test/include/greatest.h"

/*
 * Marco definition area
 */
#define EEPROM_DEVICE_PATH    "/dev/eeprom"
#define TEST_TEXT_BUF_SIZE    (64)

/*
 * Global data area
 */
static ms_uint8_t test_data_buf[TEST_TEXT_BUF_SIZE] = {"Hello, MSRTOS!"};

/*
 * test_eeprom_read_write
 */
int main(int argc, char *argv[])
{
    int             fd;
    int             ret;
    ms_uint8_t      data_buf[TEST_TEXT_BUF_SIZE + 1];
    ms_eeprom_msg_t msg;

    fd = ms_io_open(EEPROM_DEVICE_PATH, O_RDWR, 0);
    if (fd < 0) {
        ms_printf("[error]: open device file %s failed!\n", EEPROM_DEVICE_PATH);
        return  (-1);
    }

    msg.mem_addr    = 0;
    msg.byte_buffer = (ms_uint8_t *)test_data_buf;
    msg.bytes_size  = TEST_TEXT_BUF_SIZE;
    msg.next_msg    = NULL;
    ret = ms_io_ioctl(fd, MS_EEPROM_CMD_WRITE_DATA, &msg);
    if (ret != 0) {
        ms_printf("[error]: write data failed! errno = %d\n", errno);
        ms_io_close(fd);
        return  (-1);
    }

    msg.mem_addr    = 0;
    msg.byte_buffer = data_buf;
    msg.bytes_size  = TEST_TEXT_BUF_SIZE;
    msg.next_msg    = NULL;
    ret = ms_io_ioctl(fd, MS_EEPROM_CMD_READ_DATA, &msg);
    if (ret != 0) {
        ms_printf("[error]: read data failed! errno = %d\n", errno);
        ms_io_close(fd);
        return  (-1);
    }

    if (memcmp(test_data_buf, data_buf, TEST_TEXT_BUF_SIZE) != 0) {
        ms_printf("[error]: data compare failed! errno = %d\n", errno);
        ms_io_close(fd);
        return  (-1);
    }

    ms_io_close(fd);

    return  (0);
}

# 3.2 已支持的 EEPROM 驱动

最新的支持情况,请自行查看 libmsdriver/src/eeprom 目录。

厂商 型号
Microchip EEPROM_24XX00, EEPROM_24XX01, EEPROM_24XX02, 等
Atmel (暂无)
STM EEPROM_M24C01, EEPROM_M24C02, EEPROM_M24M02, 等

# 附录(Appendix)

1. Reference

I2C 读写 EEPROM:https://blog.csdn.net/flyleaf91/article/details/52325512

开漏和推挽:https://blog.csdn.net/qq_27016651/article/details/83096126

2. FAQ

(1)为什么要加上拉电阻? IIC的从器件,不具备拉高总线的能力,从机控制SDA上的电平,只能通过令SDA接地或不接地来实现输出0或1,而无法通过接地或接VCC来实现输出0或1。这就要求IIC总线上必须是默认上拉的,否则从机无法令SDA为高。

(2)为什么要用开漏? 主机如果使用了推挽,当主机释放SDA时(令SDA=1时),相当于SDA被接通到VCC,这时从机无法控制SDA为0; 主机如果使用了开漏,当主机释放SDA时(令SDA=1时),相当于SDA被悬空上拉到VCC,这时从机可以拉低SDA,使SDA=0,也可以释放SDA使SDA=1,这样从机才能发数据。另外使用推挽输出会造成短路。