I2C
第一次使用I2C总线,虽然之前已经了解过,大概知道这个总线是如何工作的,但没真正去实现过,一般器件都支持spi,调试和连接都简单明了,需要修改的代码也不多,多数能够从以前的代码拷过来,再对照一下官方的示例修改一下就能用。这回实在是spi总线不够用,如果每个器件都占用一个spi,需要四组spi才行,分cs的话,就要重新梳理两个器件之间的占用,纠结了一阵子,还是开始了I2C总线的调试之旅。
调试的准备
如果新手入坑,在购买示波器的时候,一定要买一个好用且精度高一些的。不然就会像我一样,根本捕捉不到I2C的信号,500ms下能轻松捕捉到,但是分辨率太差,没啥用,使用软件模拟I2C的时候,又要不停调试的调软件延时,而且达不到预想的精度。
做了一块demo板,但是没仔细审核,把I2C上拉电阻接地了,于是用跳线和夹子做的总线,结果扑街了,各种错误,后面用洞洞板焊了电路,跳线上去,它居然工作了。
I2C的精髓是阻塞
每个操作都需要等待硬件对相应的状态位置位,才能进行下一步的操作。
使用start信号后,要检查I2C_FLAG_SBSEND是否置位,要等多久才能检查到置位呢,不知道,所以需要死循环,但如果没有ACK,就卡在这里了。所以需要在死循环中检查是否发生N-ACK的错误,或者用定时回调查看是否发生了N-ACK错误,从而将相应的线程stop掉。回调检查可以检查多种错误,如果在阻塞的死循环里把所有的可能发生的错误检查都检查一遍,不仅浪费时间,代码也会臃肿。
检查状态寄存器
在没有示波器和逻辑分析仪的情况下,唯有祭出寄存器大法了,虽然可以用mega2560或者uno写一个现成但没直接查寄存器来得简单明了。
固件库里有寄存器地址转换的宏定义:
#define REG32(addr) (*(volatile uint32_t *)(uint32_t)(addr))
#define REG16(addr) (*(volatile uint16_t *)(uint32_t)(addr))
#define REG8(addr) (*(volatile uint8_t *)(uint32_t)(addr))
使用这些宏就能访问相应的寄存器,I2C的固件库里还进一步把状态寄存器进行封装
/* registers definitions */
#define I2C_CTL0(i2cx) REG32((i2cx) + 0x00U) /*!< I2C control register 0 */
#define I2C_CTL1(i2cx) REG32((i2cx) + 0x04U) /*!< I2C control register 1 */
#define I2C_SADDR0(i2cx) REG32((i2cx) + 0x08U) /*!< I2C slave address register 0*/
#define I2C_SADDR1(i2cx) REG32((i2cx) + 0x0CU) /*!< I2C slave address register */
#define I2C_DATA(i2cx) REG32((i2cx) + 0x10U) /*!< I2C transfer buffer register */
#define I2C_STAT0(i2cx) REG32((i2cx) + 0x14U) /*!< I2C transfer status register 0 */
#define I2C_STAT1(i2cx) REG32((i2cx) + 0x18U) /*!< I2C transfer status register */
#define I2C_CKCFG(i2cx) REG32((i2cx) + 0x1CU) /*!< I2C clock configure register */
#define I2C_RT(i2cx) REG32((i2cx) + 0x20U) /*!< I2C rise time register */
所以可以直接访问相关的寄存器,比如查看地址寄存器是否存放了正确的从机地址,格式是否正确。查看收发缓存是否存放正确的数据等等。
获取状态位
i2c_flag_get()是调试中使用最多、且最有用的。它能获取已经定义的状态指示、错误指示,虽然可以直接读整个状态寄存器,但直接查询到位是真的香。