如何在Milk-V duo的小核FreeRTOS中跑i2c

发布时间:2024年01月03日

前言

(1)PLCT实验室实习生长期招聘:招聘信息链接
(2)如果有嵌入式企业需要招聘湖南区域日常实习生,任何区域的暑假嵌入式软件实习岗位,可C站直接私聊,或者邮件:zhangyixu02@gmail.com,此消息至2025年1月1日前均有效
(3)本来要尝试在RT-Thread中跑i2c,打算先Linux中测试i2c,再FreeRTOS,最后RT-Thread,最终发现卡在RT-Thread这一步了,因为移植一个文件就马上出现另外一个文件缺失缝缝补补搞了很久最终真的无能为力了。

在duo的小核FreeRTOS跑i2c的方法

修改部分

(1)因为duo的版本更新比较快,我就使用比较熟悉的Duo-V1.0.5版本进行讲解。

git clone -b Duo-V1.0.5 https://github.com/milkv-duo/duo-buildroot-sdk.git
cd  duo-buildroot-sdk
cp freertos/cvitek/hal/cv180x/i2c/src/hal_dw_i2c.c freertos/cvitek/task/comm/src/riscv64/
cp freertos/cvitek/hal/cv180x/i2c/include/hal_dw_i2c.h freertos/cvitek/task/comm/include/
cp freertos/cvitek/driver/i2c/include/i2c.h freertos/cvitek/task/comm/include/

在这里插入图片描述

(2)进入FreeRTOSConfig.h文件,关闭configUSE_TICK_HOOK这个宏。

vim freertos/cvitek/kernel/include/riscv64/FreeRTOSConfig.h
#define configUSE_TICK_HOOK             0

(3)进入的796行,将IC3_INTR修改为I2C3_INTR,这里有可能是官方写错了。

vim freertos/cvitek/task/comm/src/riscv64/hal_dw_i2c.c
//修改前
request_irq(IC3_INTR, i2c_dw_isr, 0, "IC2_INTR int", &dw_i2c[i2c_id]);
//修改后
request_irq(I2C3_INTR, i2c_dw_isr, 0, "IC2_INTR int", &dw_i2c[i2c_id]);

(4)在comm_main.c中利用freertosxTaskCreate()函数创建一个my_task_test()的任务函数。

vim freertos/cvitek/task/comm/src/riscv64/comm_main.c
/****************************************************************************
 * Function definitions
 ****************************************************************************/




DEFINE_CVI_SPINLOCK(mailbox_lock, SPIN_MBOX);

#include "hal_dw_i2c.h"

void my_task_test()
{
  uint8_t data= 0,data_read;
  hal_i2c_init(I2C0);
	printf("hal_i2c_init after\n");
  uint8_t *data_write =&data;
	
  for (;;) 
  {
		//i2c端口,从机地址,寄存器地址,7位从机地址,写入的数据,写1个数据
		hal_i2c_write(I2C0, 0x01, 0x28, 1, data_write, 1);
		hal_i2c_read(I2C0, 0x28, 0x17, 1, &data_read, 1);
    printf("test the RTOS: %d\r\n", data);
    vTaskDelay(100);
    data++;
  }

}

void main_cvirtos(void)
{
	printf("create cvi task\n");


	/* Start the tasks and timer running. */
  xTaskCreate(my_task_test, "my_task", 1024 * 8, NULL, 1, NULL);

  vTaskStartScheduler();


    /* If all is well, the scheduler will now be running, and the following
    line will never be reached.  If the following line does execute, then
    there was either insufficient FreeRTOS heap memory available for the idle
    and/or timer tasks to be created, or vTaskStartScheduler() was called from
    User mode.  See the memory management section on the FreeRTOS web site for
    more details on the FreeRTOS heap http://www.freertos.org/a00111.html.  The
    mode from which main() is called is set in the C start up code and must be
    a privileged mode (not user mode). */
    printf("cvi task end\n");
	
	for (;;)
        ;
}

接线说明

(1)这里需要准备的材料有,一个Typec数据线,一个USB转TTL模块,一个逻辑分析仪。

在这里插入图片描述
在这里插入图片描述

上机测试结果

(1)对于逻辑分析仪的采样率必须达到被测信号最高频率的 5 倍以上,推荐 10 倍以上。
(2)通过阅读代码我们可以知道,I2C的频率应该是400KHZ,5倍以上,那么就是2MHZ。因此逻辑分析仪的采样率设置为2MHZ

在这里插入图片描述

(3)然后我这里每隔100ms进行一次,所以采样深度尽量设置在200k samples以上。我这里为了保险,设置的采样深度为1M samples

在这里插入图片描述

(4)最终发现,成功进行了I2C通讯。因为我们这里I2C接口是连接的逻辑分析仪,主机给0x01地址发送写和0x28地址发送读信息的时候,发现找不到从机的应答信号,因此停止了通讯。

在这里插入图片描述

(5)细心的人肯定发现了一个问题,第二个我们明明是向0x28地址读取数据,怎么抓取到的是写指令。讲实话,这个地方我也比较懵,但是从代码中来看,似乎又应该是这样的,因为代码的i2c_xfer_init()函数其实就是调用的i2c_write_cmd_data(),所以应该是写指令没有问题。再深入的具体细节我也不太想分析了,毕竟我不是他们芯片原厂的人。走到这一步已经尽力了。

在这里插入图片描述

(6)个人认为,他的读时序应该是采用的方式3。所以一开始是主机写数据。

在这里插入图片描述

测试流程

(1)这里我将会介绍我是如何测试出在Milk-V duo的小核FreeRTOS中跑I2C,方便各位学习,如果有错也欢迎各位大佬指出更正。

前期猜测和测试

(1)刚开始的时候,我看到Milk-V duoFreeRTOS中存在i2c.c,打算直接在comm_main.c中包含i2c.h文件,然后引用相关的API函数。执行编译指令,发现他说找不到对应的函数。

export MILKV_BOARD=milkv-duo
source milkv/boardconfig-milkv-duo.sh
source build/milkvsetup.sh
defconfig cv1800b_milkv_duo_sd
build_all

(2)当时我是比较纳闷的,怎么会找不到i2c.c的源代码呢?执行find指令后发现,i2c.c是在freertos/cvitek/driver/i2c/src/路径中,按理说是有这个文件的呀。

find -name *i2c*.c

在这里插入图片描述

(3)后面就想,那就看看编译过程中产生的日志信息吧。因为编译过程一般都是先将所以c文件编译,但是不链接,最后都编译完成之后,再进行链接的。所以说,我这里只需要关注 -c 文件路径 就可以知道编译了那些.c文件了。但是,不幸的是,我发现freertos/cvitek/driver/i2c/并不在编译路径中。

build_all | tee build_all_log.txt

在这里插入图片描述

(4)之后尝试阅读i2c.c的源码,发现其实就是对hal_dw_i2c.c中的函数进行了一次封装。

在这里插入图片描述

(5)因为如上原因,我打算直接将duohal层代码复制到编译路径里面。那么就不再需要写我不那么熟悉的CMake来添加编译路径了,哈哈哈哈。

找到官方i2c代码

(1)依旧是先执行find指令,查找到官方的i2c代码。我们知道Milk-V duocv1800芯片,但是从下面查找发现,只有cv181xcv1835这两款芯片的hali2c代码,那么怎么办呢?

find -name *i2c*.c

在这里插入图片描述

(2)之后我翻阅了一下GitHub,发现是有cv180xi2c库的,只不过他这里写的是../cv181x/i2c,意思说cv180xcv181xi2c库函数是一样的。

在这里插入图片描述

官方i2c代码可能存在的问题

(1)当我尝试将hal_dw_i2c.chal_dw_i2c.h文件移植进编译路径的时候,发现hal_dw_i2c.c文件的796行IC3_INTR报错,说没有被定义。
(2)之后我看了一下,IC3_INTR其实就是一个宏定义,然后我尝试将IC3_INTR修改为71,编译进入芯片,发现会卡死。

#define IC3_INTR 71

(2)最后我发现还有一个I2C3_INTR,之后修改成I2C3_INTR就没有问题了。所以说我猜测i2c hal层代码这个地方也许写错了。

在这里插入图片描述

编译路径查找

(1)依旧是将编译过程中的打印信息导出来。 -c 文件路径就是编译录, -I 文件路径就是头文件包含路径。

build_all | tee build_all_log.txt

API接口的使用

(1)一般I2C只会进行读写操作,所以我只关注了这三个函数。因为找不到官方的相关API接口介绍函数,所以这三个函数的使用完全是我看代码猜的。(其实只要是对I2C协议稍微熟悉一点,猜这个还是不算太难。)

void hal_i2c_init(uint8_t i2c_id);
int hal_i2c_write(uint8_t i2c_id, uint8_t dev, uint16_t addr, uint16_t alen, uint8_t *buffer, uint16_t len);
int hal_i2c_read(uint8_t i2c_id, uint8_t dev, uint16_t addr, uint16_t alen, uint8_t *buffer, uint16_t len);

hal_i2c_init()函数使用

(1)这个从名字上,就很清楚是干嘛的的。明显就是I2C的初始化函数,那么传入的i2c_id是什么呢?
(2)那我们就看看源码,从源码一路追溯,就很容易发现如果要初始化I2Cx,那么就传入I2Cx

在这里插入图片描述

I2C时序简单讲解

(1)后面几个参数就设计I2C时序知识了,为了防止有朋友不了解,或者说不太熟悉了。先介绍一下:
<1>I2C在通讯的时候,先发送一个起始信号(SCL高电平,SDA下降沿),然后主机广播一个从机地址。I2C的从机地址有两种,一种是7位地址,一种是10地址的。
<2>找到从机地址之后,从机发送ACK回应信号。I2C有两种策略,你可以自己选择,第一种是主机等待从机的ACK回应信号,第二种是不等待,自顾自的发数据。测试结果发现Milk-V duoI2C通讯似乎只能是第一种,必须等待ACK回应信号,否则会终止通讯过程。
(这也很好的介绍了,明明代码中写了那么多数据,最终逻辑分析仪抓到的数据那么短)
<3>然后主机发送这里需要分成读写两种情况讨论,加粗部分表示主机发送的信号,没加粗部分是从机的信号
(注:Milk-V duo的手册的I2C章节时序图感觉有问题,是经典的I2C时序没错。但是Milk-V duo的代码中读时序应该是采用的第三种,主机先发数据,然后再读数据)

  • 起始信号 —> 从机地址+写 —> 应答信号 —> 要写入从机的寄存器地址 —> 应答信号 —> 写入的数据 —> 应答信号 —> 停止信号
  • 起始信号 —> 从机地址+写 —> 应答信号 —> 要读取从机的寄存器地址 —> 应答信号 —> 起始信号 —> 从机地址+读 —> 应答信号 —> 从机发送数据 —> 回应信号 —> 停止信号

在这里插入图片描述
在这里插入图片描述

hal_i2c_write()函数使用

(1)如果明白了上述所说的I2C时序,那么这个函数的传参就很好猜了。

/**
 * @brief   主机I2C写入数据
 *
 * @param   i2c_id           i2c端口,传入I2Cx(x为1,2,3...)
 *         -dev              从机地址
 *         -addr             寄存器地址
 *         -alen             如果是7bit从机,传入1。10bit从机传入2
 *         -buffer           写入数据缓冲区
 *         -len              要写入几个字节数据
 *
 * @return  成功写入数据,返回0
 */
int hal_i2c_write(uint8_t i2c_id, uint8_t dev, uint16_t addr, uint16_t alen, uint8_t *buffer, uint16_t len);

hal_i2c_read()函数使用

(1)同理

/**
 * @brief   主机I2C写入数据
 *
 * @param   i2c_id           i2c端口,传入I2Cx(x为1,2,3...)
 *         -dev              从机地址
 *         -addr             寄存器地址
 *         -alen             如果是7bit从机,传入1。10bit从机传入2
 *         -buffer           读取数据缓冲区
 *         -len              要读取几个字节数据
 *
 * @return  成功写入数据,返回0
 */
int hal_i2c_read(uint8_t i2c_id, uint8_t dev, uint16_t addr, uint16_t alen, uint8_t *buffer, uint16_t len);

参考文章

(1)如何自己生成fip.bin在Milkv-duo上跑freertos
(2)逻辑分析仪采样率和采样深度
(3)I2C总线协议详解(特点、通信过程、典型I2C时序)

文章来源:https://blog.csdn.net/qq_63922192/article/details/135349487
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。