MilkV Duo I2C 适配 RT-Thread 设备驱动框架
SDK 相关
基于官方 duo-buildroot-sdk 适配 RT-Thread 硬件 i2c 设备驱动。
-
首先复制寄存器,指令等结构体及宏定义,主要位于文件
hal_dw_i2c.h
中 -
i2c_regs 为 各 i2c 外设的寄存器地址,对应以下起始地址,以便于访问,可通过 get_i2c_base 获取:
#define I2C0_BASE 0x4000000 #define I2C1_BASE 0x4010000 #define I2C2_BASE 0x4020000 #define I2C3_BASE 0x4030000 #define I2C4_BASE 0x4040000
-
i2c_write_cmd_data,i2c_enable,i2c_disable,i2c_flush_rxfifo,i2c_wait_for_bb,i2c_setaddress,i2c_xfer_init,i2c_xfer_finish,hal_i2c_read,hal_i2c_write,i2c_set_bus_speed,均为操作 i2c 外设的基础方法,通过组合以上函数,基本可以对接至 RT-Thread 的 i2c 设备驱动框架,dw_i2c_dev 那些就不实现了,直接将 i2c 底层的操作 api 对接到 rtt 的 i2c 设备驱动即可。
-
要实现以上API,需要先实现 mimo 方便对寄存器进行读写,目前 rtt 中对该需求已实现。还需要实现 uldey 等延时函数,rtt 中已经实现该芯片架构运行时间的获取:
static rt_uint64_t get_ticks(void) { __asm__ __volatile__( "rdtime %0" : "=r"(time_elapsed)); return time_elapsed; }
基于其获取的时间,再实现 us 延时即可:
void rt_hw_us_delay(rt_uint32_t us) { unsigned long start_time; unsigned long end_time; unsigned long run_time; start_time = get_ticks(); end_time = start_time + us * (TIMER_CLK_FREQ / 1000000); do{ run_time = get_ticks(); } while(run_time < end_time); }
这里特殊说明一下,芯片运行时钟的获取是移植 RTOS 前期基本,RTOS 移植适配完成后,一般 ms 级的延时就是具备了,例如
rt_thread_mdelay
并且一般情况,考虑系统资源的消耗,RTOS 的时间片大小通常设置为 ms 级,所以调用rt_thread_mdelay
rt_thread_delay
等 api 时,会进入调度。但对于 us 级即以下的延时处理,通常需要针对开发平台特殊实现,并且通常是阻塞型的,延时不会触发调度器,因此也要小心使用,尽量避免使用这类 api 延时较长时间。
RTT 设备驱动框架
RT-Thread 设备框架属于组件和服务层,是基于 RT-Thread 内核之上的上层软件。设备框架是针对某一类外设,抽象出来的一套统一的操作方法及接入标准,可以屏蔽硬件差异,为应用层提供统一的操作方法。
RT-Thread 设备框架分为三层:设备驱动层、设备驱动框架层、I/O 设备管理层。其中设备驱动层直接对接底层硬件设备,也是我们需要对接实现的部分。对于 i2c 总线设备有以下方法:
struct rt_i2c_bus_device_ops
{
rt_ssize_t (*master_xfer)(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_ssize_t (*slave_xfer)(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_err_t (*i2c_bus_control)(struct rt_i2c_bus_device *bus,
int cmd,
void *args);
};
实现其中的 master_xfer 即可,其中包含 i2c 消息的收发。
-
其中一部分原代码是对接 sdk 中的 i2c 设备,我们这里不使用其抽象的 i2c 设备,对接至 rtt 的设备驱动框架,部分函数需要改写,去除其 i2c 设备相关部分,例如重写
i2c_disable
:static void i2c_disable(struct dw_i2c_dev *dw_i2c) { int timeout = 100; if (dw_i2c->wait_irq || dw_i2c->use_interstop) return; /* return due to i2c is still active */ do { mmio_write_32((uintptr_t)&dw_i2c->i2c->ic_enable, 0x0); if ((mmio_read_32((uintptr_t)&dw_i2c->i2c->ic_enable_status) & IC_ENABLE) == 0x0) return; /* * Wait 10 times the signaling period of the highest I2C * transfer supported by the driver (for 400KHz this is * 25us) as described in the DesignWare I2C databook. */ arch_usleep(25); } while (timeout--); printf("timeout in disabling I2C adapter\n"); }
重写后:
static void i2c_disable(struct i2c_regs *i2c) { int timeout = 100; do { mmio_write_32((uintptr_t)&i2c->ic_enable, 0x0); if ((mmio_read_32((uintptr_t)&i2c->ic_enable_status) & IC_ENABLE) == 0x0) return; /* * Wait 10 times the signaling period of the highest I2C * transfer supported by the driver (for 400KHz this is * 25us) as described in the DesignWare I2C databook. */ rt_hw_us_delay(25); } while (timeout--); LOG_I("timeout in disabling I2C adapter\n"); }
-
hal_i2c_init
重写如下:static void hal_i2c_init(uint8_t i2c_id) { struct i2c_regs *i2c; uint32_t i2c_intr; LOG_I("%s, i2c-%d\n", __func__, i2c_id); /* Disable i2c */ //Need to acquire lock here i2c = get_i2c_base(i2c_id); i2c_intr = get_i2c_intr(i2c_id); // request_irq(i2c_intr, i2c_dw_isr, 0, "IC2_INTR int", &dw_i2c[i2c_id]); i2c_enable(i2c, false); mmio_write_32((uintptr_t)&i2c->ic_con, (IC_CON_SD | IC_CON_SPD_FS | IC_CON_MM | IC_CON_RE)); mmio_write_32((uintptr_t)&i2c->ic_rx_tl, IC_RX_TL); mmio_write_32((uintptr_t)&i2c->ic_tx_tl, IC_TX_TL); mmio_write_32((uintptr_t)&i2c->ic_intr_mask, 0x0); i2c_set_bus_speed(i2c, I2C_SPEED); //mmio_write_32((uintptr_t)&i2c->ic_sar, slaveaddr); /* Enable i2c */ i2c_enable(i2c, false); //Need to release lock here }
-
接下来需要对接一下 i2c 外设的对应中断号,sdk 中使用
request_irq
注册中断及其对应的回调函数,在 rtt 中使用rt_hw_interrupt_install
:rt_hw_interrupt_install(irqno, rt_hw_i2c_isr, _i2c, _i2c_obj[i].device_name);
并且为了便于应对大小核中断号不一致的情况,在 Kconfig 中添加对应处理:
config I2C_IRQ_BASE int default 32
并通过如下方式表示 i2c 外设的对应中断号:
#define I2C0_IRQ (I2C_IRQ_BASE + 0) #define I2C1_IRQ (I2C_IRQ_BASE + 1) #define I2C2_IRQ (I2C_IRQ_BASE + 2) #define I2C3_IRQ (I2C_IRQ_BASE + 3) #define I2C4_IRQ (I2C_IRQ_BASE + 4)
-
不要忘记了使能对应的引脚作为 i2c 总线的 sda 和 scl ,例如 i2c0 和 i2c1 分别使用以下引脚:
PINMUX_CONFIG(IIC0_SCL, IIC0_SCL); PINMUX_CONFIG(IIC0_SDA, IIC0_SDA); PINMUX_CONFIG(PAD_MIPIRX1P, IIC1_SDA); PINMUX_CONFIG(PAD_MIPIRX0N, IIC1_SCL);
这部分内容是在 sdk 的 u-boot 中进行实现,启动阶段对各外设和引脚进行初始化配置,如果我们在移植到 rtt 中时遗漏了这部分的处理,外设对应的引脚将不会出现我们期望的行为。
测试结果
基本对接完成以后,我们就可以编写一个调用 rtt i2c 设备的简单示例,并使用逻辑分析仪观察一下行为是否正确,这里主要基于 rtt 提供的示例编写一个测试用例 官方示例
使用逻辑分析仪查看波形如下:
因为并没有挂载对应的 i2c 传感器从机,因此没有应到信号,行为正常。