MilkV Duo I2C 适配 RT-Thread 设备驱动框架

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 传感器从机,因此没有应到信号,行为正常。