i2c-slave-queue
有些跑在i2c的协议(例如MCTP Over I2C)都是需要双向传输消息的。

当请求方发送MCTP请求消息之后,会等待响应方处理请求之后,读取响应方的响应消息。对于i2c器件来说,一般都是主设备给从设备发消息。当请求方发送MCTP请求消息时,请求方作为主设备。响应方处理完请求发送给请求方时,响应方是作为主设备。所以对于支持MCTP的设备来说需要是能主从功能。在实际的开发中,请求方是不清楚响应方是何时处理完消息给请求方发响应的。所以都会在发送完消息后,进行轮训,等待响应。伪代码如下:
void handle_request()
{
send_request();
while(read(i2c_fd, data_buf, rsp_len) < 0) {
sleep(1);
}
parse_rsp();
}
前段时间在查找MCTP相关资料的时候,偶然间看到linux的commit-i2c: slave-mqueue: add a slave backend to receive and queue messages。给从设备提供了message queue,用户可以直接使用poll监听message queue是否可读。使用示例如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <poll.h>
#include <time.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int i, r;
struct pollfd pfd;
struct timespec ts;
unsigned char data[256];
pfd.fd = open(argv[1], O_RDONLY | O_NONBLOCK);
if (pfd.fd < 0)
return -1;
pfd.events = POLLPRI;
while (1) {
r = poll(&pfd, 1, 5000);
if (r < 0)
break;
if (r == 0 || !(pfd.revents & POLLPRI))
continue;
lseek(pfd.fd, 0, SEEK_SET);
r = read(pfd.fd, data, sizeof(data));
if (r <= 0)
continue;
clock_gettime(CLOCK_MONOTONIC, &ts);
printf("[%ld.%.9ld] :", ts.tv_sec, ts.tv_nsec);
for (i = 0; i < r; i++)
printf(" %02x", data[i]);
printf("\n");
}
close(pfd.fd);
return 0;
}
主设备获取到i2c bus的fd下发消息后,如果是之前,则需要再判断fd是否有响应返回。而这个patch的目的是提供一个message queue,当底层有可读的消息,i2c的驱动会把消息放到这个message queue中,上层的用户只需要监听这个message queue即可。
2 i2c-slave-read.c源码分析
i2c-slave-read.c是实现slave-mqueue的源文件,提供i2c-slave-mqueue的驱动。struct mq_queue是slave queue的关键数据结构,理解slave queue的关键是弄清楚这个数据结构的功能。
struct mq_queue {
struct bin_attribute bin;
struct kernfs_node *kn;
spinlock_t lock; /* spinlock for queue index handling */
int in;
int out;
struct mq_msg *curr;
int truncated; /* drop current if truncated */
struct mq_msg queue[MQ_QUEUE_SIZE];
};
看到这个数据结构的时候会对这几个数据成员抱有疑问:
int in;
int out;
struct mq_msg *curr;
int truncated;
但是看到这个宏定义后就明白这几个数据成员的含义了:
#define MQ_QUEUE_NEXT(x) (((x) + 1) & (MQ_QUEUE_SIZE - 1))
所以struct mq_queue数据结构定义了一个环形数组用来存放作为slave device接收到的数据。in和out可以看做是环形缓冲区的两个下标,in表示接收到数据存放到环形缓冲区的下标,out表示将读取数据缓冲区的下标。
i2c_slave_mqueue_probe函数会分配一块大小为sizeof(struct mq_queue)的内存,并初始化各个数据成员,初始化后内存的示意图如下:

2.1 i2c_slave_mqueue_callback
i2c_slave_mqueue_callback和i2c_slave_mqueue_bin_read这两个函数会移动in和out下标。i2c_slave_mqueue_callback会接收到数据存放到mq_queue,在接收到一个完整的消息后,in的值加一,移动到下个缓冲区。
switch (event) {
case I2C_SLAVE_WRITE_REQUESTED:/* 接收到地址数据 */
mq->truncated = 0;
msg->len = 1;
msg->buf[0] = client->addr << 1;
break;
case I2C_SLAVE_WRITE_RECEIVED:
if (msg->len < MQ_MSGBUF_SIZE) {
msg->buf[msg->len++] = *val; /* 把接收到的数据一个一个存放到buf中 */
} else {
dev_err(&client->dev, "message is truncated!\n");
mq->truncated = 1;
ret = -EINVAL;
}
break;
case I2C_SLAVE_STOP:
if (unlikely(mq->truncated || msg->len < 2))
break;
spin_lock(&mq->lock);
mq->in = MQ_QUEUE_NEXT(mq->in);/* 接收到STOP信号,表示当前完整的i2c消息传输完成,移动in下标 */
mq->curr = &mq->queue[mq->in];/* 移动curr指针,指向下一个的msg buf */
mq->curr->len = 0;
/* Flush the oldest message */
if (mq->out == mq->in)/* in和out相等表示mq_queue为空,没有可读消息。 */
mq->out = MQ_QUEUE_NEXT(mq->out);/* 所以这个时候需要把最老的消息刷掉,移动out下标 */
spin_unlock(&mq->lock);
kernfs_notify(mq->kn);
break;
default:
*val = 0xFF;
break;
}
读取完完整i2c消息,填充的缓冲区表示已经存放了i2c数据的:

假设读取数据到某个时刻,in和out相等:

这个mq_queue还是有可读数据的,所以这个时候只能移动out,刷掉最老的未读取的数据(未被填充表示该buf可存放数据,实际上并未把buf清空,只是直接写入覆盖。这里未填充只是一个示意图):

2.2 i2c_slave_mqueue_bin_read
i2c_slave_mqueue_bin_read函数是从mq_queue读取数据,移动out下标。
if (mq->out != mq->in) {
msg = &mq->queue[mq->out];
if (msg->len <= count) {
ret = msg->len;
memcpy(buf, msg->buf, ret);/* 把数据拷贝到用户传入的buf中 */
} else {
ret = -EOVERFLOW; /* Drop this HUGE one. */
}
mq->out = MQ_QUEUE_NEXT(mq->out);/* 读取完数据移动out下标 */
if (mq->out != mq->in)/* in和out不相等,表示还有可读数据 */
more = true;
}

浙公网安备 33010602011771号