RT-Thread’s pitfall record in STM32 hardware I2C


Reference articles:
1. Cleverly "graft" hardware I2C to RTT's native analog I2C driver framework
2. Hardware I2C driver implementation notes based on STM32F4 platform
3. "Analysis of rt-thread driver framework" - i2c driver

0.Preface

  I recently planned to use RT-Thread to make a small demo, which requires I2C communication to drive an OLED screen. However, after searching around, I couldn’t find any support methods and use cases for hardware I2C in RTT. It seems that everyone understands this tacitly. Use this easy-to-use but not easy-to-use software I2C. I still can’t help but complain here. Even the hardware SPI is already supported, and it even supports SPI DMA mode. The hardware I2C has not been adapted for so many years. I also hope that someone can contribute and make a third-party hardware I2C driver that DIY players can use.

1. The difference between software and hardware I2C

  I won’t introduce much about the principles of the I2C communication protocol. This is a very common communication protocol. You can search a lot of it on the CSDN forum, and the RT Thread Document Center also has a more detailed introduction.
  Software I2C uses GPIO level flipping to simulate I2C signals. Its advantage is that it is easy to transplant. It can be applied to 51 microcontrollers and Linux platforms as long as there is GPIO (of course, no one will use this under Linux). The disadvantage is that the speed is very low, and there are inevitable delays and glitches when the software operates the GPIO level flip. In order to eliminate the impact of this phenomenon, a slightly larger time interval is needed between the simulated I2C signals. The signal frequency of software I2C is generally 30KHz ~ 50KHz. Even if the optimization is quite good, it is almost at this level. Used to operate a 128x64 OLED screen, the frame rate is basically around 2 frames.
  Hardware I2C communicates by operating the chip's own register. The disadvantage is that the driver is not universal between different chips. The advantage is that it is faster and can adapt to DMA mode to reduce CPU load. The STM32RCT6 used by the author has a hardware I2C standard mode signal frequency of 100KHz and a fast mode of 400KHz. Some chips with better performance also have a 1MHz extreme speed mode. The frame rate of operating 128x64 OLED at 400kHz is around 25 frames, which can be said to be a huge improvement.

2. I2C driver in RT Thread

Regarding the implementation of the I2C driver framework in RT Thread, you can refer to the third reference article   mentioned above . I personally think it is very detailed and easy to understand. RT Thread is a Linux-like real-time operating system, so the implementation of the I2C framework is similar to that in Linux: the I2C driver provides some operation-related ops functions and is registered in the kernel, and the I2C device can be mounted through the probe function. On the bus, I2C communication is performed through the ops operation function.
Insert image description here
And in this article, the author skipped the original bit_ops, redesigned a hardware I2C implementation, mounted the driver directly into the kernel core, and also implemented the hardware I2C driver as the master device. However, the author believes that this method is not very compatible with general structures, so I found some other methods.
Insert image description here

3. Try to adapt hardware I2C

Refer to articles 1 and 2 to "graft" a hardware I2C driver implementation by modifying the implementation function of the I2C bus. Let’s put the code here first. First, create drv_hard_i2c.c and drv_hard_i2c.h in the same directory of the original drv_soft_i2c.c and drv_soft_i2c.h:
drv_hard_i2c.h:

/*
 * Copyright (c) 2006-2018, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2018-11-08     balanceTWK   first version
 */

#ifndef __DRV_I2C__
#define __DRV_I2C__

#include <rtthread.h>
#include <rthw.h>
#include <rtdevice.h>

#ifdef BSP_USING_HARD_I2C

/* stm32 config class */
typedef void (*pI2cConfig)(void);
struct stm32_hard_i2c_config
{
    
    
    rt_uint8_t scl;                     /* scl pin */
    rt_uint8_t sda;                     /* sda pin */
    const pI2cConfig pFunc;             /* i2c init function */
    const char* pName;                  /* i2c bus name */
    I2C_HandleTypeDef* pHi2c;           /* i2c handle */
    struct rt_i2c_bus_device i2c_bus;   /* i2c bus device */
};
/* stm32 i2c dirver class */
struct stm32_i2c
{
    
    
    struct rt_i2c_bit_ops ops;
    struct rt_i2c_bus_device i2c2_bus;
};

#define HARD_I2C_CONFIG(x)  \
{
      
      
    .scl        = BSP_I2C##x##_SCL_PIN,    \
    .sda        = BSP_I2C##x##_SDA_PIN,    \
    .pFunc      = MX_I2C##x##_Init,         \
    .pHi2c      = &hi2c##x,                 \
    .pName      = "i2c"#x,                  \
    .i2c_bus    = {
    
    
            .ops = &i2c_bus_ops,
    },
}
    
int rt_hw_i2c_init(void);

#endif

#endif /* RT_USING_I2C */

Among them, stm32_hard_i2c_config can be understood as an i2c instance object, and its attributes include scl and sda pins, bus name and initialization function, etc. (Note: The bus speed, semaphore and mutex lock in reference article 2 are not required, because the initialization function generated using CubeMx already has the bus speed, and the I2C operation function in the HAL library already has the bus lock inside)
stm32_i2c It encapsulates device operation functions and buses for interfacing with the kernel.
The function macro HARD_I2C_CONFIG(x) is used to subsequently create an I2C device object.

drv_hard_i2c.c:

/*
 * Copyright (c) 2006-2018, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2018-11-08     balanceTWK   first version
 */

#include <board.h>
#include "drv_hard_i2c.h"
#include "drv_config.h"
#include<rtthread.h>
#include<rtdevice.h>

#ifdef BSP_USING_HARD_I2C

//#define DRV_DEBUG
#define LOG_TAG              "drv.i2c"
#include <drv_log.h>

static const struct stm32_hard_i2c_config hard_i2c_config[] =
{
    
    
#ifdef BSP_USING_HARD_I2C1
    HARD_I2C_CONFIG(1),
#endif
#ifdef BSP_USING_HARD_I2C2
    HARD_I2C_CONFIG(2),
#endif
#ifdef BSP_USING_HARD_I2C3
    HARD_I2C_CONFIG(3),
#endif
#ifdef BSP_USING_HARD_I2C4
    HARD_I2C_CONFIG(4),
#endif
};

static struct stm32_i2c i2c_obj[sizeof(hard_i2c_config) / sizeof(hard_i2c_config[0])];

/**
 * This function initializes the i2c pin.
 *
 * @param Stm32 i2c dirver class.
 */
static void stm32_i2c_gpio_init(struct stm32_i2c *i2c)
{
    
    
    struct stm32_soft_i2c_config* cfg = (struct stm32_soft_i2c_config*)i2c->ops.data;

    rt_pin_mode(cfg->scl, PIN_MODE_OUTPUT_OD);
    rt_pin_mode(cfg->sda, PIN_MODE_OUTPUT_OD);

    rt_pin_write(cfg->scl, PIN_HIGH);
    rt_pin_write(cfg->sda, PIN_HIGH);
}

/**
 * The time delay function.
 *
 * @param microseconds.
 */
static void stm32_udelay(rt_uint32_t us)
{
    
    
    rt_uint32_t ticks;
    rt_uint32_t told, tnow, tcnt = 0;
    rt_uint32_t reload = SysTick->LOAD;

    ticks = us * reload / (1000000 / RT_TICK_PER_SECOND);
    told = SysTick->VAL;
    while (1)
    {
    
    
        tnow = SysTick->VAL;
        if (tnow != told)
        {
    
    
            if (tnow < told)
            {
    
    
                tcnt += told - tnow;
            }
            else
            {
    
    
                tcnt += reload - tnow + told;
            }
            told = tnow;
            if (tcnt >= ticks)
            {
    
    
                break;
            }
        }
    }
}

/**
 * if i2c is locked, this function will unlock it
 *
 * @param stm32 config class
 *
 * @return RT_EOK indicates successful unlock.
 */
static rt_err_t stm32_i2c_bus_unlock(const struct stm32_soft_i2c_config *cfg)
{
    
    
    rt_int32_t i = 0;

    if (PIN_LOW == rt_pin_read(cfg->sda))
    {
    
    
        while (i++ < 9)
        {
    
    
            rt_pin_write(cfg->scl, PIN_HIGH);
            stm32_udelay(100);
            rt_pin_write(cfg->scl, PIN_LOW);
            stm32_udelay(100);
        }
    }
    if (PIN_LOW == rt_pin_read(cfg->sda))
    {
    
    
        return -RT_ERROR;
    }

    return RT_EOK;
}

/* I2C initialization function */
int rt_hw_i2c_init(void)
{
    
    
    rt_int8_t ret = RT_ERROR;
    rt_size_t obj_num = NR(hard_i2c_config);
    rt_err_t result;

    for (int i = 0; i < obj_num; i++)
    {
    
    
        //GPIO初始化
        stm32_i2c_gpio_init(&hard_i2c_config[i]);

        //检测SDA是否为低电平,低电平则通过管脚模拟9个CLK解锁
        stm32_i2c_bus_unlock(&hard_i2c_config[i]);

        //调用Hal库MX_I2Cx_Init(),配置硬件I2C
        hard_i2c_config[i].pFunc();

        //向内核注册I2C Bus设备
        if(rt_i2c_bus_device_register(&(hard_i2c_config[i].i2c_bus), hard_i2c_config[i].pName) != RT_EOK)
        {
    
    
            LOG_E("%s bus init failed!\r\n", hard_i2c_config[i].pName);
            ret |= RT_ERROR;
        }
        else
        {
    
    
            ret |= RT_EOK;
            LOG_I("%s bus init success!\r\n", hard_i2c_config[i].pName);
        }
    }
    return ret;
}
//INIT_BOARD_EXPORT(rt_hw_i2c_init);

#endif /* BSP_USING_HARD_I2C */

In this file, I2C instance objects are mainly created based on macro definition switches and initialized. The main function is rt_hw_i2c_init(). The gpio init, delay functions, etc. required in this function retain the initialization operations in the software i2c.

user_i2c.h:

/*
 * Copyright (c) 2006-2021, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2023-08-27     14187       the first version
 */
#ifndef DRIVERS_HARD_I2C_H_
#define DRIVERS_HARD_I2C_H_

//硬件i2c宏开关
//#define     BSP_USING_HARD_I2C
#ifdef      BSP_USING_HARD_I2C
//      #define BSP_USING_HARD_I2C1
//      #define BSP_USING_HARD_I2C2
//      #define BSP_USING_HARD_I2C3
//      #define BSP_USING_HARD_I2C4

        #if defined(BSP_USING_HARD_I2C1 || BSP_USING_HARD_I2C2 || BSP_USING_HARD_I2C3 || BSP_USING_HARD_I2C4)
            //#define BSP_USING_DMA_I2C_TX
            //#define BSP_USING_DMA_I2C_RX
        #endif
#endif

#endif /* DRIVERS_HARD_I2C_H_ */

In order not to overwrite and refresh my configuration every time I save RT Thread Settings, I define an additional header file to save the customized I2C macro switch, so that I only need to include this in board.h after each refresh. Just the header file.

At this point, the creation of the custom hardware I2C macro switch and device object has been completed. All that remains is to replace the bit_ops operation function in the kernel.

4. i2c-bit-ops operation function replacement

In the rt-thread/components/drivers/i2c/ directory under the root directory of the rt thread project, there is an i2c-bit-ops.c file, which stores the ops operation functions registered in the i2c driver framework:

static rt_size_t i2c_bit_xfer(struct rt_i2c_bus_device *bus,
                              struct rt_i2c_msg         msgs[],
                              rt_uint32_t               num)
{
    
    
    struct rt_i2c_msg *msg;
    struct rt_i2c_bit_ops *ops = (struct rt_i2c_bit_ops *)bus->priv;
    rt_int32_t i, ret;
    rt_uint16_t ignore_nack;

    if (num == 0) return 0;

    for (i = 0; i < num; i++)
    {
    
    
        msg = &msgs[i];
        ignore_nack = msg->flags & RT_I2C_IGNORE_NACK;
        if (!(msg->flags & RT_I2C_NO_START))
        {
    
    
            if (i)
            {
    
    
                i2c_restart(ops);
            }
            else
            {
    
    
                LOG_D("send start condition");
                i2c_start(ops);
            }
            ret = i2c_bit_send_address(bus, msg);
            if ((ret != RT_EOK) && !ignore_nack)
            {
    
    
                LOG_D("receive NACK from device addr 0x%02x msg %d",
                        msgs[i].addr, i);
                goto out;
            }
        }
        if (msg->flags & RT_I2C_RD)
        {
    
    
            ret = i2c_recv_bytes(bus, msg);
            if (ret >= 1)
            {
    
    
                LOG_D("read %d byte%s", ret, ret == 1 ? "" : "s");
            }
            if (ret < msg->len)
            {
    
    
                if (ret >= 0)
                    ret = -RT_EIO;
                goto out;
            }
        }
        else
        {
    
    
            ret = i2c_send_bytes(bus, msg);
            if (ret >= 1)
            {
    
    
                LOG_D("write %d byte%s", ret, ret == 1 ? "" : "s");
            }
            if (ret < msg->len)
            {
    
    
                if (ret >= 0)
                    ret = -RT_ERROR;
                goto out;
            }
        }
    }
    ret = i;

out:
    if (!(msg->flags & RT_I2C_NO_STOP))
    {
    
    
        LOG_D("send stop condition");
        i2c_stop(ops);
    }

    return ret;
}
...
static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
    
    
    i2c_bit_xfer,
    RT_NULL,
    RT_NULL
};

This code implements the process of sending the corresponding i2c msg to each i2c device, and modifies it to the hardware i2c sending method:

static rt_size_t i2c_xfer(struct rt_i2c_bus_device *bus,
                              struct rt_i2c_msg     msgs[],
                              rt_uint32_t           num)
{
    
    
    rt_uint32_t i;
    struct rt_i2c_msg *msg;
    struct stm32_hard_i2c_config *Pconfig = rt_container_of(bus, struct stm32_hard_i2c_config, i2c_bus);

    fot(i = 0;i < num;i++)
    {
    
    
        msg = &msgs[i];
        if(msg->flags & RT_I2C_RD)
        {
    
    
#if defined(BSP_USING_DMA_I2C_RX)
            HAL_I2C_Master_Receive_DMA(Pconfig->pHi2c, (msg->addr)<<1, msg->buf, msg->len);
            rt_hw_us_delay(100);
#else
            HAL_I2C_Master_Receive(Pconfig->pHi2c, (msg->addr)<<1, msg->buf, msg->len, 100);
#endif
        }
        else
        {
    
    
#if defined(BSP_USING_DMA_I2C_TX)
            HAL_I2C_Master_Transmit_DMA(Pconfig->pHi2c, (msg->addr)<<1, msg->buf, msg->len);
            rt_hw_us_delay(100);
#else
            HAL_I2C_Master_Transmit(Pconfig->pHi2c, (msg->addr)<<1, msg->buf, msg->len, 100);
#endif
        }
    }
    return i;
}

static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
    
    
    i2c_xfer,
    RT_NULL,
    RT_NULL
};

Refer to the sending method of software i2c, create a new sending function rt_size_t i2c_xfer(), and modify the corresponding sending method in rt_i2c_bus_device_ops to this method.

At this point, the hardware I2C driver is partially completed. The device can be mounted on the hardware I2C bus through the same declaration and mounting methods as the software i2c.

五、Attention Please!

问题1:In the above implementation, I2C messages can be sent through HAL_I2C_Master_Transmit() or HAL_I2C_Master_Transmit_DMA() according to the macro definition, but no judgment is made on whether the transmission is successful.
问题2:In ST's official HAL library, there are three ways for I2C to send messages, polling mode (polling), interrupt mode, and DMA mode. HAL_I2C_Master_Transmit() corresponds to polling mode. Although this mode has an improved speed compared to software I2C, it is The actual improvement effect is actually not particularly large. For the interrupt mode, you need to transplant and implement the corresponding interrupt processing function, which can be implemented according to reference article 2. However, the author believes that there are many things to pay attention to in this article, such as the operation of releasing the semaphore in the interrupt processing function, which may cause Some hidden dangers (the semaphore can be removed directly). For DMA mode, theoretically it is also necessary to transplant some interrupt processing functions, but the author has not used this method at present, so I have not studied it in detail. So in theory it can only stay in polling mode.
问题3:In drv_hard_i2c.c, INIT_BOARD_EXPORT(rt_hw_i2c_init); this registration step needs to be determined according to the actual situation. If you want to use DMA mode, you need to register MX_DMA_Init() before this registration step. This function is generated for CubeMX. Use to initialize the DMA function. The same is true for interrupt mode.

6. Summary

  At present, it seems that it is still difficult to transplant ST's hardware I2C driver, so the author chose to change the platform (I escaped...). I modified the OLED circuit to SPI mode and changed the chip platform. I still have an LPC54110 and A CH32 development board. The RTT BSP support packages of these two boards seem to have adapted hardware I2C drivers. Goodbye ST. I hope someone will adapt the hardware I2C next time.

Guess you like

Origin blog.csdn.net/weixin_45682654/article/details/132571753