1、了解rtthread中模拟I2C驱动框架
模拟I2C驱动框架是将I2C的START、STOP、READ、WRITE通过操作GPIO拉高拉低实现的,时钟周期控制则是通过IO翻转附加延时的方式实现,模拟的方式最简单粗暴,但是相对于硬件的方式比较浪费CPU资源。
关于模拟I2C框架中需要实现的操作函数:
static const struct rt_i2c_bit_ops stm32_bit_ops_default =
{
.data = RT_NULL,
.set_sda = stm32_set_sda, //SDA数据线操作
.set_scl = stm32_set_scl, //SCL时钟线操作
.get_sda = stm32_get_sda, //SDA数据线电平读取
.get_scl = stm32_get_scl, //SCL时钟线电平读取
.udelay = stm32_udelay, //延时函数实现
.delay_us = 1,
.timeout = 100
};
在驱动初始化中通过结构体注册的方式进行总线注册:
i2c_obj[i].ops = stm32_bit_ops_default;
i2c_obj[i].ops.data = (void*)&soft_i2c_config[i];
i2c_obj[i].i2c2_bus.priv = &i2c_obj[i].ops;
stm32_i2c_gpio_init(&i2c_obj[i]);
/* IO操作函数结构体注册到总线驱动框架 */
result = rt_i2c_bit_add_bus(&i2c_obj[i].i2c2_bus, soft_i2c_config[i].bus_name);
RT_ASSERT(result == RT_EOK);
stm32_i2c_bus_unlock(&soft_i2c_config[i]);
这里的核心还是这个函数:rt_i2c_bit_add_bus,将IO操作的函数注册到设备驱动框架里面。
这边的操作代码需要看下 \components\drivers\i2c\i2c-bit-ops.c 里面的 i2c_bit_xfer 函数。
2、开始适配硬件I2C驱动
了解清楚rtthread提供的软件模拟I2C驱动框架后,就知道后面该怎样去实现硬件I2C驱动接口了。
实现硬件I2C驱动接口本质是去替换 i2c-bit-ops.c 这个文件,将里面的 i2c_bit_xfer 通过硬件I2C驱动的 Transmit 及 Receive 去实现。
硬件I2C配置结构体:
先看配置结构体部分,这部分主要是总线管脚、通信速率及一些HAL库句柄的赋值定义。
这部分也参考了 [url=home.php?mod=space&uid=721591]@07lhluo[/url] 大佬的结构体,在此基础上加入了总线速率设置、信号量及互斥锁。
typedef void (*pI2CInit)(rt_uint32_t speed);
struct stm32_hard_i2c_config
{
const char *bus_name; //总线名称
const char *scl_pin_name; //scl管脚定义字符串
const char *sda_pin_name; //sda管脚定义字符串
rt_uint32_t speed; //总线时钟速率
pI2CInit pInitFunc; //硬件I2C外设初始化函数指针
I2C_HandleTypeDef pHi2c; //HAL库硬件I2C句柄
struct rt_i2c_bus_device i2c_bus;//总线操作函数结构体指针
/ notice and lock define */
rt_sem_t tx_notice; //总线发送完成信号量
rt_sem_t rx_notice; //总线接收完成信号量
rt_mutex_t lock; //总线操作互斥锁
};
定义好配置结构体后对于任意总线的配置就能统一使用一个宏定义去实现了:
#define HAED_I2C_CONFIG(x)
{
.bus_name = "i2c"#x,
.scl_pin_name = BSP_I2C##x##_SCL_PIN,
.sda_pin_name = BSP_I2C##x##_SDA_PIN,
.speed = BSP_I2C##x##_CLOCK,
.pInitFunc = MX_I2C##x##_Init,
.pHi2c = &hi2c##x,
.i2c_bus = {
.ops = &i2c_bus_ops,
},
}
其中的pin脚和通信速率的定义,我把它们放到board.h中:
#define BSP_USING_I2C1
#ifdef BSP_USING_I2C1
#define BSP_I2C1_CLOCK 400000
#define BSP_I2C1_SCL_PIN "PB6"
#define BSP_I2C1_SDA_PIN "PB7"
#endif
所以最后我们定义的结构体就是这样的形式:
static struct stm32_hard_i2c_config hard_i2c_config[] = {
#ifdef BSP_USING_I2C1
HAED_I2C_CONFIG(1)
#endif /* BSP_USING_I2C1 /
#ifdef BSP_USING_I2C2
HAED_I2C_CONFIG(2)
#endif / BSP_USING_I2C2 /
#ifdef BSP_USING_I2C3
HAED_I2C_CONFIG(3)
#endif / BSP_USING_I2C3 */
};
至此,关于任意总线配置部分结构体就是这样。
硬件I2C外设初始化函数:
这部分初始化来源与CubeMX生成的代码,唯一不同的就是增加了总线时钟形参的传入,这里以I2C1的初始化为例:
static void MX_I2C1_Init(rt_uint32_t speed)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = speed;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_16_9;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
/## Configure the NVIC for I2C1 ########################################/
/* NVIC for I2Cx */
HAL_NVIC_SetPriority(I2C1_EV_IRQn, 10, 0);
HAL_NVIC_EnableIRQ(I2C1_EV_IRQn);
HAL_NVIC_SetPriority(I2C1_ER_IRQn, 11, 0);
HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
}
其中对I2C的事件中断及总线错误中断进行使能。
硬件I2C总线GPIO初始化函数:
讲到这里,是不是发现GPIO初始化还没有添加进来,于是乎,我们参考一下串口部分的GPIO初始化,以字符串的形式进行解析。外设时钟的初始化也是参考串口驱动里面的形式。
static rt_err_t hard_i2c_gpio_configure(struct stm32_hard_i2c_config *config)
{
GPIO_InitTypeDef GPIO_InitStruct = { 0 };
GPIO_TypeDef *scl_port;
GPIO_TypeDef sda_port;
uint16_t scl_pin;
uint16_t sda_pin;
uint8_t i2c_num = config->bus_name[3] - '0';
/ get gpio port and pin address /
get_pin_by_name(config->scl_pin_name, &scl_port, &scl_pin);
get_pin_by_name(config->sda_pin_name, &sda_port, &sda_pin);
/ gpio ports clock enable /
hard_i2c_gpio_clk_enable(scl_port);
if (scl_port != sda_port) {
hard_i2c_gpio_clk_enable(sda_port);
}
/ scl pin initialize /
GPIO_InitStruct.Pin = scl_pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(scl_port, &GPIO_InitStruct);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
/ sda pin initialize /
GPIO_InitStruct.Pin = sda_pin;
HAL_GPIO_Init(sda_port, &GPIO_InitStruct);
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_SET);
/ i2c periphal clock enable */
hard_i2c_clk_enable(i2c_num);
return RT_EOK;
}
到这里,所有底层的初始化已经完成了,接下来可以进行数据传输接口及中断服务函数的操作了。
3、硬件I2C驱动数据传输接口:
与软件模拟不同,硬件I2C只有数据发送与接收操作,HAL库提供三种操作方式供开发者选择使用:polling方式、中断方式及DMA方式。
这三种方式在HAL库中函数名上有体现,这里我选用中断的方式,因为我个人觉得I2C通信速率不高没必要占用宝贵的DMA资源,如果使用polling的方式则和模拟I2C比起来体现不出来优势。
![1.jpg](//www.obk20.com/file/web2/M00/7F/66/poYBAGOJtz2ALJcvAAHuwj64Zd0127.jpg)
了解完HAL层的API函数后,开始适配中断接口:
oid I2C1_EV_IRQHandler(void)
{
/* enter interrupt /
rt_interrupt_enter();
i2cx_event_isr(&hard_i2c_config[I2C1_INDEX]);
/ leave interrupt /
rt_interrupt_leave();
}
void I2C1_ER_IRQHandler(void)
{
/ enter interrupt /
rt_interrupt_enter();
i2cx_error_isr(&hard_i2c_config[I2C1_INDEX]);
/ leave interrupt */
rt_interrupt_leave();
}
这里我把总线的事件中断和错误中断统一管理起来:
static inline void i2cx_event_isr(struct stm32_hard_i2c_config *config)
{
HAL_I2C_EV_IRQHandler(config->pHi2c);
}
static inline void i2cx_error_isr(struct stm32_hard_i2c_config *config)
{
HAL_I2C_ER_IRQHandler(config->pHi2c);
}
以及HAL库里面的总线发送接收完成的回调函数:
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef hi2c)
{
/ release sem to notice function */
rt_sem_release(hard_i2c_config[I2C1_INDEX].tx_notice);
}
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef hi2c)
{
/ release sem to notice function */
rt_sem_release(hard_i2c_config[I2C1_INDEX].rx_notice);
}
到这里中断及回调部分完成。
结合中断及回调函数,核心数据传输接口函数我们就比较容易实现了:
static rt_size_t i2c_xfer( struct rt_i2c_bus_device bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num)
{
HAL_StatusTypeDef status;
rt_err_t ret = 0;
rt_size_t xfer_len = 0;
struct rt_i2c_msg msg;
struct stm32_hard_i2c_config pCfg = rt_container_of(bus, struct stm32_hard_i2c_config, i2c_bus);
for (int i = 0; i < num; i++)
{
msg = &msgs[i];
if (msg->flags & RT_I2C_RD)
{
/** I2C master to receive /
rt_mutex_take(pCfg->lock, RT_WAITING_FOREVER);
/ master receive data by interrupt /
status = HAL_I2C_Master_Receive_IT(pCfg->pHi2c, msg->addr << 1, msg->buf, msg->len);
if (status != HAL_OK) {
HAL_I2C_Master_Abort_IT(pCfg->pHi2c, msg->addr << 1);
rt_mutex_release(pCfg->lock);
ret = RT_ERROR;
goto __exit;
}
/ wait receive complete /
ret = rt_sem_take(pCfg->rx_notice, (10 * msg->len));
if (ret != RT_EOK)
{
HAL_I2C_Master_Abort_IT(pCfg->pHi2c, msg->addr << 1);
rt_mutex_release(pCfg->lock);
goto __exit;
}
rt_mutex_release(pCfg->lock);
xfer_len++;
}
else
{
/ I2C master to transmit ****/
rt_mutex_take(pCfg->lock, RT_WAITING_FOREVER);
/ master transmit data by interrupt /
status = HAL_I2C_Master_Transmit_IT(pCfg->pHi2c, msg->addr << 1, msg->buf, msg->len);
if (status != HAL_OK)
{
HAL_I2C_Master_Abort_IT(pCfg->pHi2c, msg->addr << 1);
rt_mutex_release(pCfg->lock);
ret = RT_ERROR;
goto __exit;
}
/ wait transmit complete */
ret = rt_sem_take(pCfg->tx_notice, (10 * msg->len));
if (ret != RT_EOK)
{
HAL_I2C_Master_Abort_IT(pCfg->pHi2c, msg->addr << 1);
rt_mutex_release(pCfg->lock);
goto __exit;
}
rt_mutex_release(pCfg->lock);
xfer_len++;
}
}
__exit:
if (xfer_len == num) {
ret = xfer_len;
}
return ret;
}
到这里,基本的硬件I2C驱动适配完成,驱动初始化及注册部分就是通用的路子了,大差不差了。
原作者:cheung