查看: 3827|回复: 6

[LPC双核挑战赛] 【imetro】LPC54114的I2C使用心得(2018/5/15更新I2C DMA使用方法)

[复制链接]

该用户从未签到

6

主题

32

帖子

11

中级会员

Rank: 3Rank: 3

积分
463
最后登录
2023-2-23
发表于 2018-5-8 21:07:20 | 显示全部楼层 |阅读模式
本帖最后由 imetro 于 2018-5-15 22:53 编辑

2018/5/15更新I2C DMA使用方法,见第3楼。

----------原帖内容----------

刚刚调好了一个LPC54114的I2C的bug,因为这个问题困扰了一天时间,现将遇到的问题和解决方法做个记录,希望对各位有所帮助。

先说说问题吧。在我的项目中需要用到一个心率测量模块MAX30102,该模块使用I2C总线与主机通信,在此之前需要对MAX30102初始化。但是,在初始化的过程中却遇到了以下情况:
1. 有时候程序会卡在对于MSTPENDING位的检测中。在SDK的代码中大量使用了I2C_PendingStatusWait()函数,而该函数的作用就是判断MSTPENDING位是否被置位。函数代码如下:
  1. static uint32_t I2C_PendingStatusWait(I2C_Type *base)
  2. {
  3.     uint32_t status;

  4. #if I2C_WAIT_TIMEOUT
  5.     uint32_t waitTimes = I2C_WAIT_TIMEOUT;
  6. #endif

  7.     do
  8.     {
  9.     status = I2C_GetStatusFlags(base);
  10. #if I2C_WAIT_TIMEOUT
  11.     } while (((status & I2C_STAT_MSTPENDING_MASK) == 0) && (--waitTimes));

  12.     if (waitTimes == 0)
  13.     {
  14.         return kStatus_I2C_Timeout;
  15.     }
  16. #else
  17.     } while ((status & I2C_STAT_MSTPENDING_MASK) == 0);
  18. #endif

  19.     /* Clear controller state. */
  20.     I2C_MasterClearStatusFlags(base, I2C_STAT_MSTARBLOSS_MASK | I2C_STAT_MSTSTSTPERR_MASK);

  21.     return status;
  22. }
复制代码

2. 有时候程序可以继续执行,但MSTSTATE的值表明传输有误。检查可以发现,MSTSTATE的值有时候为0x3,这表示在发送从机地址时没有应答,但地址肯定是正确的。
3. 进一步分析可以发现,在LPC54114和MAX30102上电后第一次运行时,第一次写入是正确的(包括写从机地址、写内存地址等步骤),但后续的读写会出现上述错误。

遇到这种情况,第一个想到的就是用逻辑分析仪来看看波形(忘了截图了,不好意思)。第一次写入的波形看起来似乎没有问题,但后续的读写在写从机地址这一步确实会返回NAK。那么,问题应该出在第一次写入中。仔细检查可以发现,在第一次写入后的STOP信号出现了两次,难道这就是问题的原因?不过我记得我好像没有这么做呀,是不是SDK做了什么?想到这里,我赶紧回过头去看我的读写代码:
  1. static uint8_t max30102_read_reg(uint8_t addr)
  2. {
  3.     uint8_t tmp;
  4.    
  5.     I2C_MasterStart(I2C1, 0x57, kI2C_Write);
  6.     tmp = addr;
  7.     I2C_MasterWriteBlocking(I2C1, &tmp, 1, kI2C_TransferNoStopFlag);
  8.     I2C_MasterRepeatedStart(I2C1, 0x57, kI2C_Read);
  9.     I2C_MasterReadBlocking(I2C1, &tmp, 1, 0);
  10.     I2C_MasterStop(I2C1);
  11.    
  12.     return tmp;
  13. }
复制代码
  1. static void max30102_write_reg(uint8_t addr, uint8_t data)
  2. {
  3.     uint8_t tmp;
  4.    
  5.     I2C_MasterStart(I2C1, 0x57, kI2C_Write);
  6.     tmp = addr;
  7.     I2C_MasterWriteBlocking(I2C1, &tmp, 1, kI2C_TransferNoStopFlag);
  8.     tmp = data;
  9.     I2C_MasterWriteBlocking(I2C1, &tmp, 1, 0);
  10.     I2C_MasterStop(I2C1);
  11. }
复制代码

在我的代码中,确实在最后发送了STOP信号,但是看起来并没有调用两次。难道是调用的I2C_MasterReadBlocking()和I2C_MasterWriteBlocking()函数有问题?我可是照着例程打的呀。看来这两个函数的最后一个参数一定有什么玄机。赶紧又跑去看SDK的相关代码,这一看终于发现了问题:
  1. status_t I2C_MasterWriteBlocking(I2C_Type *base, const void *txBuff, size_t txSize, uint32_t flags)
  2. {
  3. ...
  4.     if ((status & (I2C_STAT_MSTARBLOSS_MASK | I2C_STAT_MSTSTSTPERR_MASK)) == 0)
  5.     {
  6.         if (!(flags & kI2C_TransferNoStopFlag))
  7.         {
  8.             /* Initiate stop */
  9.             base->MSTCTL = I2C_MSTCTL_MSTSTOP_MASK;
  10.             status = I2C_PendingStatusWait(base);
  11.             if (status == kStatus_I2C_Timeout)
  12.             {
  13.                 return kStatus_I2C_Timeout;
  14.             }
  15.         }
  16.     }
  17. ...
  18. }
复制代码
  1. status_t I2C_MasterReadBlocking(I2C_Type *base, void *rxBuff, size_t rxSize, uint32_t flags)
  2. {
  3. ...
  4.     if ((flags & kI2C_TransferNoStopFlag) == 0)
  5.     {
  6.         /* initiate NAK and stop */
  7.         base->MSTCTL = I2C_MSTCTL_MSTSTOP_MASK;
  8.         status = I2C_PendingStatusWait(base);
  9.         if (status == kStatus_I2C_Timeout)
  10.         {
  11.             return kStatus_I2C_Timeout;
  12.         }
  13.     }
  14. ...
  15. }
复制代码

可以看到,这两处代码是整个函数唯一用到flags这个参数的地方,且参数均只用到了kI2C_TransferNoStopFlag,且它们的作用相同:在flags为0时终止传输并发送STOP信号(当然I2C_MasterReadBlocking()函数还包括对最后一次读操作返回NAK)。按照我的代码,在max30102_read_reg()和max30102_write_reg()的结尾处确实会发送两次STOP信号,而这导致了MCU和MAX30102无法正确通信。至此问题得到解决,将两个多余的I2C_MasterStop()函数去掉就正常了。

这个问题解决得比较艰难,一是没有认识到多次发送STOP信号可能会造成问题,二是该问题可能会导致MCU和MAX30102都无法再正常工作,难以分开考察(现在看来情况1像是MCU的I2C状态机出错,而情况2像是MAX30102进入了某种锁定状态),好在最后通过逻辑分析和SDK代码阅读发现了问题。最后,建议修改SDK中demo的相关代码,以免造成不必要的困扰。最后贴一段SDK的代码来说明问题,其中最后一个I2C_MasterStop是应该去掉的:

  1. ...
  2.     if (kStatus_Success == I2C_MasterStart(EXAMPLE_I2C_MASTER, I2C_MASTER_SLAVE_ADDR_7BIT, kI2C_Write))
  3.     {
  4.         /* subAddress = 0x01, data = g_master_txBuff - write to slave.
  5.           start + slaveaddress(w) + subAddress + length of data buffer + data buffer + stop*/
  6.         reVal = I2C_MasterWriteBlocking(EXAMPLE_I2C_MASTER, &deviceAddress, 1, kI2C_TransferNoStopFlag);
  7.         if (reVal != kStatus_Success)
  8.         {
  9.             return -1;
  10.         }

  11.         reVal = I2C_MasterRepeatedStart(EXAMPLE_I2C_MASTER, I2C_MASTER_SLAVE_ADDR_7BIT, kI2C_Read);
  12.         if (reVal != kStatus_Success)
  13.         {
  14.             return -1;
  15.         }

  16.         reVal = I2C_MasterReadBlocking(EXAMPLE_I2C_MASTER, g_master_rxBuff, I2C_DATA_LENGTH - 1, 0);
  17.         if (reVal != kStatus_Success)
  18.         {
  19.             return -1;
  20.         }

  21.         reVal = I2C_MasterStop(EXAMPLE_I2C_MASTER);
  22.         if (reVal != kStatus_Success)
  23.         {
  24.             return -1;
  25.         }
  26.     }
  27. ...
复制代码


评分

参与人数 1 +3 收起 理由
doatello + 3

查看全部评分

回复

使用道具 举报

该用户从未签到

6

主题

32

帖子

11

中级会员

Rank: 3Rank: 3

积分
463
最后登录
2023-2-23
 楼主| 发表于 2018-5-8 22:15:33 | 显示全部楼层
顺便晒一下MAX30102的效果,这里用了GitHub上共享的源代码,据称效果比Maxim提供的标准代码要好。地址:MAX30102_by_RF
MAX30102.png
回复 支持 反对

使用道具 举报

  • TA的每日心情
    无聊
    2018-7-31 08:40
  • 签到天数: 43 天

    [LV.5]常住居民I

    299

    主题

    876

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    14704
    最后登录
    2020-3-1
    发表于 2018-5-9 08:57:33 | 显示全部楼层
    感谢分享~
    该会员没有填写今日想说内容.
    回复

    使用道具 举报

    该用户从未签到

    6

    主题

    32

    帖子

    11

    中级会员

    Rank: 3Rank: 3

    积分
    463
    最后登录
    2023-2-23
     楼主| 发表于 2018-5-15 21:11:09 | 显示全部楼层
    本帖最后由 imetro 于 2018-5-15 22:50 编辑

    今天补充一下关于I2C DMA的内容。

    之前使用的MAX30102的算法设定的采样率只有25 sps,没有办法满足心率1%精度的要求,因此需要提高采样率到100 sps。但是,这样一来I2C的数据传输就达到了每秒600+字节,如果使用400 kHz的I2C快速模式,则需要占用超过1%的CPU运行时间,在此期间CPU只能等待。一个比较好的解决方案是使用RTOS和DMA。不过呢,SDK中I2C的驱动中对RTOS的支持仅有FreeRTOS,而且只支持基于中断的传输,显然不符合我的需要。我的目标是支持CMSIS-RTOS V2,且支持DMA传输,因此需要自己写相关的驱动。通过阅读参考手册和SDK源代码,且经过多次实验后,我找到了I2C DMA的工作原理。

    I2C在Flexcomm支持的总线协议中是比较特殊的,因为I2C本身的协议比较复杂(包括启停信号、传输地址、写入数据等)且工作速度相对较慢,因此本身没有FIFO,DMA也只能支持连续的数据传输,更为复杂的协议控制仍需软件完成。

    首先,只使用寄存器轮询完成I2C的操作,一次完整的读或写流程如下(不包括需要重新发出START等多次传输的情况,也不考虑出现错误的情况):
    读:
    1. 写入7位地址和1位读指示位到MSTDAT,写入MSTSTART到MSTCTL,传输开始。
    2. 等待STAT中MSTPENDING置1。在MSTPENDING为0期间,MSTDAT完成发送地址、从机响应、从机返回数据(若从机返回ACK),但主机尚未应答当前数据
    3. 从MSTDAT读数据,并在MSTCTL写入MSTCONTINUE。
    4. 等待STAT中MSTPENDING置1。在MSTPENDING为0期间,主机发送上个数据的ACK、主机继续发起下一轮传输、从机返回数据,但主机尚未应答当前数据。5. 重复3和4,直到倒数第二个数据被读取且MSTPENDING从0返回至1。
    6. 从MSTDAT读取最后一个字节的数据,并在MSTCTL写入MSTSTOP。
    7. 等待STAT中MSTPENDING置1。在MSTPENDING为0期间,主机发送上个数据的NAK、主机发出STOP信号。
    写:
    1. 写入7位地址和1位写指示位到MSTDAT,写入MSTSTART到MSTCTL,传输开始。
    2. 等待STAT中MSTPENDING置1。在MSTPENDING为0期间,MSTDAT完成发送地址、从机响应,若从机响应NAK则结束传输。
    3. 向MSTDAT写数据,并在MSTCTL写入MSTCONTINUE。
    4. 等待STAT中MSTPENDING置1。在MSTPENDING为0期间,主机发送上个数据、从机响应,若从机响应NAK则结束传输。
    5. 重复3和4,直到最后一个数据被写入且MSTPENDING从0返回至1。
    6. 写入MSTSTOP到MSTCTL,传输结束。

    可以看到,读和写还是有些不一样的,这和I2C的协议有关。我们总结一下,就是:
    读:
    向MSTCTL写入MSTSTART先后完成“发送START信号、写入地址+读指示位、从机响应、从机返回第一个字节数据”这四件事;
    每一次向MSTCTL写入MSTCONTINUE都先后完成了“ACK上个数据、发起下一轮传输、读取1个字节”这三件事;
    向MSTCTL写入MSTSTOP完成“NAK上个数据、发送STOP信号”这两件事。
    写:
    向MSTCTL写入MSTSTART先后完成“写入地址+写指示位、从机响应”这两件事;
    每一次向MSTCTL写入MSTCONTINUE则是先后完成“发起下一轮传输、写入1个字节、收到从机回复”这三件事;
    向MSTCTL写入MSTSTOP完成“NAK上个数据”这一件事。这表明在读和写的时候开始的时机是不一致的。


    ----------I2C DMA 分割线----------

    有了轮询模式下的例子,I2C DMA做的工作就好理解多了。简单来说,I2C DMA可以完成我们在轮询模式下使用MSTCONTINUE完成的功能,即:
    对于读而言,从第一个字节到倒数第二个字节的“MSTDAT读取、ACK、发起下一轮传输”(注意顺序和轮询模式略有不同);
    对于写而言,从第一个字节到最后一个字节的“发起下一轮传输、MSTDAT写入、等待ACK”。

    特别需要注意的是DMA判断一次传输完成(产生中断)的时间节点。DMA总是会将“内存到MSTDAT的读取/写入完成”作为一次传输完成的标志,并在XFERCOUNT减少为0x3FF的时候产生中断;但是,非常重要的一点是,由DMA接管的I2C仍然会继续完成后续的一系列操作,直到MSTCTL中的MSTDMA被撤销为止。对于读模式来说,产生DMA中断时ACK已被回复,这显然不是我们希望的,因此只能通过手动传输最后一个字节的方法来解决。

    Talk is cheap,接下来就到了喜闻乐见的伪代码时间。由于DMA完成操作时通常已经远离调用DMA的地方(总不能在这儿干等着吧),因此接下来的代码仅表示时间上的顺序执行,实际还需根据代码分成两个区域执行,或是利用信号量等把等待时的CPU使用权交给其它程序(论操作系统的好处)。

    读:
    1. SetDMACfg(xferCfg, xferSize - 1, ...); // Only (n-1) bytes read via DMA
    2. StartDMATransfer(i2c, xferCfg); // First we start DMA

    3. i2c->MSTDAT = (addr << 1) | 0x1; // Set address and READ signal
    4. i2c->MSTCTL = I2C_MSTCTL_MSTSTART(1) | I2C_MSTCTL_MSTDMA(1); // Send START signal, address and READ signal
    5. // Now DMA takes control of I2C transfer

    6. while (GetDMATransferStatus(i2c) == DMA_RUNNING);

    7. i2c->MSTCTL = 0; // DMA releases control, now the last byte is being read
    8. while ((i2c->STAT & I2C_STAT_MSTPENDING) == 0); // Wait until last byte read
    9. *lastBytePointer = i2c->MSTDAT;
    10. i2c->MSTCTL = I2C_MSTCTL_MSTSTOP; // Send STOP signal
    复制代码


    写:
    1. SetDMACfg(xferCfg, xferSize, ...); // n bytes written via DMA
    2. StartDMATransfer(i2c, xferCfg); // First we start DMA

    3. i2c->MSTDAT = (addr << 1) | 0x0; // Set address and WRITE signal
    4. i2c->MSTCTL = I2C_MSTCTL_MSTSTART(1) | I2C_MSTCTL_MSTDMA(1); // Send START signal, address and WRITE signal
    5. // Now DMA takes control of I2C transfer

    6. while (GetDMATransferStatus(i2c) == DMA_RUNNING);

    7. i2c->MSTCTL = 0; // DMA releases control, now the last byte is being written
    8. while ((i2c->STAT & I2C_STAT_MSTPENDING) == 0); // Wait until last byte written
    9. i2c->MSTCTL = I2C_MSTCTL_MSTSTOP; // Send STOP signal
    复制代码
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    6 天前
  • 签到天数: 822 天

    [LV.10]以坛为家III

    71

    主题

    2452

    帖子

    24

    金牌会员

    Rank: 6Rank: 6

    积分
    5531
    最后登录
    2024-5-1
    发表于 2018-5-16 09:42:26 | 显示全部楼层
    谢谢分享,看看是不是采样时间间隔太短了,如果只有第一次采样正确,不妨可以试试增加两次采样的时间间隔测试一下。
    该会员没有填写今日想说内容.
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    6

    主题

    32

    帖子

    11

    中级会员

    Rank: 3Rank: 3

    积分
    463
    最后登录
    2023-2-23
     楼主| 发表于 2018-5-16 19:08:29 | 显示全部楼层
    leo121_3006061 发表于 2018-5-16 09:42
    谢谢分享,看看是不是采样时间间隔太短了,如果只有第一次采样正确,不妨可以试试增加两次采样的时间间隔测 ...

    每次操作后都等到MSTPENDING置位再读取的,应该不是这个问题。感觉还是因为第二个STOP信号导致I2C的硬件状态机出错导致的,不过也不排除是MAX30102拉低了SCL导致没有检测到合适数量的时钟边沿。主要还是MAX30102只支持1.8 V,而手头没有好的逻辑分析仪,没法验证是谁的问题。
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    6 天前
  • 签到天数: 822 天

    [LV.10]以坛为家III

    71

    主题

    2452

    帖子

    24

    金牌会员

    Rank: 6Rank: 6

    积分
    5531
    最后登录
    2024-5-1
    发表于 2018-5-17 09:29:01 | 显示全部楼层
    imetro 发表于 2018-5-16 19:08
    每次操作后都等到MSTPENDING置位再读取的,应该不是这个问题。感觉还是因为第二个STOP信号导致I2C的硬件 ...

    嗯,没有合适仪器确实比较难确定原因,不过最终还是解决了,厉害
    该会员没有填写今日想说内容.
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2024-5-5 21:04 , Processed in 0.126870 second(s), 26 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.

    快速回复 返回顶部 返回列表