什么!I2C能全程无阻塞 CPU 0负载实现通信?

一、前面话

A - “I2C那点速度,需要用到DMA么?”

B - “I2C那点速度,IO模拟不就完了?”

C - “I2C那点速度,较真那么多干嘛?”

D - “来来来,要不来个活?I2C速率100kHZ,需要定期收发127字节,发送过程需要11.7ms,在这个11.7ms中里面不能影响CPU干其他的事情。该怎么做?”

image.png


嗯,这时候如果还是觉得A,B,C好,或者觉得这10多ms没什么,那恭喜,本文写得可能不太适合你发挥。
二、唠嗑唠嗑

如果能看到这里,说明我们是有点默契的。

我们对于先楫这种高性能MCU,这种高性能"牛马"能多干几件事那得肯定让他多干几件事,否则怎么对得起自己的钱包呢?

遇到了一两个开发者,有这么两个需求:

1、项目中使用了好几路数码管驱动芯片,而与其先楫通信的是I2C,显示数据管上,可能通信长达256字节,在100k速率下需要传输20多ms,使用的是poll点亮方式,20多ms无法干其他的活,比如相关计算以及采集等工作,对于项目的设计是效率是比较低的。

2、开发者比较懒,不想I2C需要自己配置开始信号,设备地址,结束信号,数据段等步骤,只想着告诉需要发的数据量和数据长度即可。

总结来说,即使I2C这么常用的外设,由于这么低速的外设,在收发比较长的数据量,如果使用阻塞式收发,那每个时刻都会有一段时间CPU只专心干这活,如上面的10多ms。而对于中断,但也需要在数据段CPU去收发FIFO操作。对于懒人的我们,肯定想着越懒越好,少点操作能少一点是一点。

于是乎,除了CPU这个牛马,当然还有DMA,DMA这个牛马,有一个特技就是支持链式,相比于CPU,画了个大饼(DMA描述符),好家伙,最后还真给你搞出了个大饼。

所以,CPU在这个活儿倒是轻松,看看下面的操作,CPU只需要操作下DMA的描述符地址,然后开启下DMA,传输这差事就交给DMA了。

就这么几个语句,就能实现前面的I2C传输,操作如此懒人。

image.png



本文代码链接:https://github.com/RCSN/hpm_sdk_extra/tree/feature/i2c_rtx_dma_chain
三、说个流程

在进行本序阐述之前,需要借助下官方的相关资料。
1、可以看看先楫UM手册的I2C章节以及官方公众号之前的文章 - 《 I2C总线的一种灵活控制方法

对于I2C通信时序,这里也不做太多阐述,I2C的时序大概以下,I2C的全部时序纯由DMA传输完成。

开始信号START + 设备地址device_addr + ack + (数据段 + ack)* n + 结束信号STOP
2、对于例子和驱动参考,本文借鉴hpm_sdk以下:

1)drivers: i2c和dma,分别对应hpm_i2c_drv和hpm_dma_drv/hpm_dmav2_drv

2)samples: drivers/i2c/dma, drivers/dma/dma_uart_rx_circle_transfer, drivers/dmav2/dmav2_uart_rx_circle_transfer,
3、在hpm_sdk中,对于i2c dma的收发,I2C的时序除了数据段由DMA来完成,其余都是cpu操作。

比如设置开始信号,数据长度,结束信号,使能I2C DMA,开启传输这些寄存器操作,都是CPU处理。

比如I2C写DMA之前操作的api- i2c_master_start_dma_write

image.png


这些操作可以分配给DMA完成,这时候就不得不提到先楫的DMA链式。对于DMA可以参考以往的文章。

DMA链式简单来说,就是允许将多个DMA操作链接在一起,形成一个连续的过程。当第一个DMA传输完成后,硬件自动启动下一个DMA传输,而不需要CPU的额外干预。这种机制可以极大地减少CPU的负担,并加快数据传输的速度。

对于I2C而言,除了数据段以外,还得操作设备地址,开始和结束信号的处理,那么多余这些处理,是可以构成链式进行多个连续顺序的DMA传输来完成一个数据移动任务。在这个链式描述符中,生成可以这么做。

注意:在做一些外设配置初始化的时候,建议参数先用default API进行初始化,比如DMA。

image.png

4、DMA链式生成

1)链式0:设置I2C的CTRL寄存器,也就是设置I2C的开始号,数据长度,停止信号,该链式的下一个链式地址指向链式1。

注意:这个配置不需要外设发起请求才需要搬运,只需要软件配置好参数,开启DMA之后即可搬运。故设备端dst和源地址src的传输模式都为normal。

image.png


2)链式1: 设置I2C的SETUP寄存器,开启I2C DMA请求使能,该链式的下一个链式地址指向链式2。

注意:这个配置不需要外设发起请求才需要搬运,只需要软件配置好参数,开启DMA之后即可搬运。故设备端dst和源地址src的传输模式都为normal。

image.png


3)链式2:设置I2C的CMD寄存器,开启一次transmission传输,也就是开启I2C的数据段传输。该链式的下一个链式地址指向链式3。

注意:这个配置不需要外设发起请求才需要搬运,只需要软件配置好参数,开启DMA之后即可搬运。故设备端dst和源地址src的传输模式都为normal。

image.png


4)链式3:设置I2C的DATA寄存器,也就是数据的收发,需要判断是读还是写。该链式的下一个链式地址指向链式4。

注意:这个配置需要外设发起请求才需要搬运,对于读,源地址src指向I2C的DATA,需要I2C外设产生DMA请求才开始搬运,故src传输模式需要设置握手handshake;对于写,设备地址dst指向I2C的DATA,需要I2C外设产生DMA请求才开始搬运,故dts传输模式需要设置握手handshake。

image.png


5)链式4:设置I2C的ADDR寄存器,设置I2C从设备地址。这也是方便下一次传输的时候提前设置好i2c地址。下一个链式地址设置为NULL,DMA传输完毕。

image.png


支持DMA链式描述符生成完毕,这个步骤可能稍微麻烦点,但是每个链式基本都差不多的套路。而且只需要初始化生成一次。
5、DMA配置

DMA链式配置好了之后,需要配置下DMA的相关配置,比如设备地址,设备地址模式,宽度,长度等等,以及把上面的DMA描述符放在DMA链式地址上。

注意:DMA开启在每次传输前开启,在初始化DMA配置不需要开启,另外DMA完成后会自动禁能DMA传输,所以也不需要在确定传输完成后软件操作关闭DMA。

对于I2C,这里先配置I2C的地址赋值,接下来的实际传输交给DMA链式。

image.png


1)对于DMA链式,存储的是相关变量地址,所以可以在传输之前,可以如果需要改变I2C的相关寄存器,在DMA开启传输之前可以通过改变相关变量。这里可以弄个结构体存储相关变量。

image.png


为了方便I2C管理,也定义了个DMA存储I2C基地址,DMA基地址和通道,以及DMA描述符。

如此一来就封装了三个接口。

a 生成DMA描述符接口

b 配置DMA接口

c 开启DMA传输

支持I2C的读取和写入。其中a和b只需要初始化一次,c可以在应用中调用。

image.png

四、来个例子

1、初始化I2C,dmamux I2C DMA,可参考drivers/i2c例子。

2、配置一些所需的变量

image.png


生成描述符文件以及配置DMA。

image.png


while循环DMA收发数据,全程无阻塞。

image.png


注意:实际测试需要一个板子做从机,从机代码可以使用samples/drivers/i2c/dma/slave。也可自行在应用中使用相关API。

每次开启传输前,需要清楚I2C的CMPL位确保本次传输。

效果:

收发在i2c 100khz频率中,传输需要23ms,但此过程不需要占用cpu。

image.png

五、总结

1、先楫MCU的链式DMA的使用,可解放CPU大量工作,专心参与其他重要任务,对于一些繁琐的外设任务可交给DMA链式自行运行。

2、配合DMA链式,I2C主机可做到cpu 0负载,自行完成低速传输工作。