EEPROM 驱动
本章将介绍 MS-RTOS EEPROM 驱动开发及测试。
EEPROM 是电可擦可编程只读存储器(Electrically Erasable Programmable read only memory)的英文缩写,是一种掉电后数据不丢失(不挥发)存储芯片,常用来存储一些配置信息,以便系统重新上电的时候加载之。EEPROM 芯片最常用的通讯方式就是 I2C 协议。
1. EEPROM 基础知识
1.1 硬件设计
EEPROM 芯片的设备地址一共有 7 位,其中高 4 位固定为:1010b,低 3 位则由 A0/A1/A2 信号线的电平决定。EEPROM 芯片中还有一个 WP 引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时,可写入数据。
此处 A0/A1/A2 均为 0,所以 EEPROM 的 7 位设备地址是:1010000b ,即 0x50。由于 I2C 通讯时常常是地址跟读写方向连在一起构成一个 8 位数,且当 R/W 位为 0 时,表示写方向,所以加上 7 位地址,其值为 0xA0,常称该值为 I2C 设备的写地址;当 R/W 位为 1 时,表示读方向,加上 7 位地址,其值为 0xA1,常称该值为读地址。
1.2 EEPROM 读写
EEPROM 不可跨页写,可跨页连续读。页写入模式和字节写入模式的操作一样,不同的是页写入模式需要在发送完第一个存储单元地址(页对齐)后,一次性发送 32 字节的写入数据,再发送停止位。写入过程中其余的地址增量由芯片内部自动完成,其余地址增量要求必须在当前页内,无法跨入下一页。无论字节写还是页写入方式,指令发送完成后(发送停止位后),芯片内部才开始写入,这时 SDA 会被芯片拉高,正在执行写入的从器件此时不会响应主器件的任何请求,即不会返回低电平的应答信号 ACK,最长写入时间是 10ms。编写程序时,在写操作完成后,可以不停发送伪指令(写入或读取命令)并查询是否有 ACK 返回,如果有 ACK 返回则表明从器件已处于空闲状态并可进行下一步操作。
读时序的第一个通讯过程中,使用 I2C 发送设备地址寻址(写方向),接着发送要读取的内存地址;第二个通讯过程中,再次使用 I2C 发送设备地址寻址,但这个时候的数据方向是读方向;在这个过程之后,EEPROM 会向主机返回从内存地址开始的数据,一个字节一个字节地传输,只要主机的响应为应答信号,它就会一直传输下去,主机想结束传输时,就发送非应答信号,并以停止信号结束通讯,作为从机的 EEPROM 也会停止传输。
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
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,这样从机才能发数据,另外使用推挽输出会造成短路。