(STM32)从零开始的RT-Thread之旅--SPI驱动ST7735(1)

news2025/1/15 23:48:40

上一篇:

(STM32)从零开始的RT-Thread之旅--GPIO

我使用的开发板是WeAct的H743板子,板子带一个0.96的SPI驱动的LCD,给的有现成的测试用例,看源码应该是ST的工程师写的ST7735的驱动,打算把这个驱动直接拿到RTT工程里面使用。这里按正常流程来,先打通SPI,再进行上层功能实现。一般当我们用SPI读取到LCD的ID时,即认为SPI没问题了。

这里这块由ST7735驱动的LCD屏幕的SPI接口和一般的不太一样,接线如下:

首先SPI是3线制的,MOSI可以读也可以写,然后通过一根线控制读写的是寄存器还是缓存。它和CS一样,低电平有效(低电平对应寄存器)。这里在配置SPI的时候要注意。 

我们知道,如果没有使用RTT的时候,我们需要使用SPI,只需调用HAL库相关初始化函数,把相关外设初始化完成,就可以直接调用HAL库的发送函数使用SPI了,而现在当我们想要在RTT的驱动框架内使用SPI时,则需要两步:

1.初始化硬件SPI,然后告诉RTT内核我们初始化了哪个SPI,内核可以使用它了

2.在框架内调用注册函数,其实就是从内核可用SPI列表找一个,然后使用它的时候就可以用一种规范的方式发送、接收

这就是驱动应用分离,Linux下通常都是这么开发的,简单来说就是,驱动开发负责底层初始化等,然后把它们加到内核设备列表里,应用开发需要使用SPI传输时,向内核请求使用SPI,然后直接调用内核定义的发送函数就行,而不需要考虑底层SPI怎么配置的。

这里,我把驱动干的事叫做内核之下--驱动:即底层配置,应用干的事叫做内核之上--应用:即实际使用。

1.内核之下

我们打开drivers/board.h里面有使用SPI的提示:

双击左侧RT-Thread Settings可以看到软件配置里有SPI:

直接打开就可以,然后我这里驱动屏幕用的SPI4,所以在board.h里面定义:

然后根据提示让我们去找cubemx生成的配置代码,这个代码在stm32h7xx_hal_msp.c中,由于我在用CubeMX生成代码的时候没有选择每个外设单独生成一个.c和.h,所以有关SPI配置的函数一共有两个,一个是stm32h7xx_hal_msp.c中的 HAL_SPI_MspInit ,另一个是main中的和修改HAL库配置头文件。但是这里我们并不需要做这两步,为什么?

首先由上一章我们已经把cubemx所有生成的文件都包含进来了(除了特别说明的那两个),所以我们可以直接调用HAL_SPI_MspInit函数了,其次第一章已经说过了,在用CubeMX生成文件的时候,有一个文件被重命名了(stm32h7xx_hal_conf_bak.h),那个文件就是HAL的配置文件,所以如果在CubeMX里面配置过就不需要再设置了。

根据CubeMX生成的SPI配置,我们可以知道SPI主要分为两部分配置,第一部分是SPI功能配置,在函数 MX_SPI4_Init 中,请记住这个函数:

另一部分是SPI的物理引脚配置,在函数 MX_SPI4_Init 中,请也记住这个函数:

假如我们单单看CubeMX生成的代码,发现并没有直接调用HAL_SPI_MspInit这个函数,那是在哪调用的呢?

答案在 MX_SPI4_Init  的:

HAL_SPI_Init这个函数中有:

关键这个函数在 stm32h7xx_hal_spi.c 这个文件中,这一下RTT如何把我们生成的代码包含进驱动框架的思路就清晰了,很简单,搜索这个文件:

我们可以看到这个文件CubeMX生成一个,而RTT使用的是自己生成的,其实第一章我们就讲过,我们没有用CubeMX生成的,而是用的RTT创建工程后自带的,包含文件的时候忽略了Drivers下的,但是HAL_SPI_MspInit这个函数是在stm32h7xx_hal_msp.c中的,它被我们包含了进去。如果没有CubeMX生成的 HAL_SPI_MspInit 这个函数,RTT内核本身使用的是 stm32h7xx_hal_spi.c 这个文件里面的:

同样熟悉的weak修饰,在我们把cubemx下的包含进去后,使用的就是我们生成的 HAL_SPI_MspInit 函数了。现在 HAL_SPI_MspInit 如何被包含进去我们知道了,那 MX_SPI4_Init 里面的配置呢?又如何包含进内核里?这就在下一节,内核之上里了。

2.内核之上

关于驱动所有使用均可参考官方文档:

官方文档

里面有API的相关介绍及使用案例,我觉得这点做的很好。

在上一章的BSP文件夹下新建spi的源文件:

细心的小伙伴会发现我把上一章的文件名称改了,因为发现有重名文件!所以起名要谨慎。

初始化有:

void mspi_rw_gpio_init(void)
{
    rt_pin_mode(SPI_RD_PIN_NUM, PIN_MODE_OUTPUT);
    rt_pin_write(SPI_RD_PIN_NUM, PIN_HIGH);
}
void mspi_init(void)
{
    struct rt_spi_configuration cfg;
    mspi_rw_gpio_init();
    rt_hw_spi_device_attach("spi4", "spi40", GPIOE, GPIO_PIN_11);
    spi_lcd = (struct rt_spi_device *)rt_device_find("spi40");
    if(!spi_lcd)
    {
        rt_kprintf("spi40 can't find\n");
    }
    else
    {
        spi_lcd->bus->owner = spi_lcd;
        cfg.data_width = 8;
        cfg.mode = RT_SPI_MASTER | RT_SPI_3WIRE | RT_SPI_MODE_0 | RT_SPI_MSB;
        cfg.max_hz = 12.5 * 1000 * 1000;
        rt_spi_configure(spi_lcd, &cfg);
    }
}

这里需要说明几点:

SPI_RD_PIN_NUM 这个和上一章的LED一样的配置方法,这个引脚就是最开始说的控制寄存器和缓存切换的。

然后 rt_hw_spi_device_attach("spi4", "spi40", GPIOE, GPIO_PIN_11) 里面传入的引脚就是CS引脚,我们在调用内核提供的API时,它会自动设置这个引脚。而"spi4"和"spi40",第一个是我们最开始在board.h中使用了SPI4的宏定义,内核会自动关联,它表示spi4总线。"spi40"是指的spi4总线的第0个设备。

创建完设备后用rt_device_find函数关联到本地一个变量,这里我申请的变量是 spi_lcd

而 spi_lcd->bus->owner = spi_lcd; 则是把 spi_lcd 的总线的使用者设置为 spi_lcd。这是啥意思?

因为SPI4总线其实可以注册很多设备,但是同一时间,只能被其中一个设备使用,所以每当一个设备成功申请到总线的使用权时,会把总线的使用者指向自己。设备在使用完总线后,并不会把使用者指向NULL,而是留给下个使用者判断,如果上个使用者不是自己,则会重新初始化总线的配置。从这里也可以看出来,总线能不能申请到,并不是看有没有使用者,而是用的另一个总线初始化的互斥量判断。

最后,我们第一节提到的剩下的另一个SPI配置就在这里被使用:

这个cfg的变量里保存所有SPI的配置,定义如下:

可以看到主要集中在mode里:

 

但是对比CubeMX生成的配置,总感觉还是少了很多,可能RTT对H7的支持还不是很完善,所以我们需要自己修改。还有需要特别注意的就是最大频率max_hz我们这里如果自己选的SPI时钟源,则需要手动修改源码,这个值我们不使用。 

打开 rt_spi_configure 函数,可以看到,设置SPI参数的函数是:

可以看到这里判断了总线当前的拥有者是不是传入的设备,如果不是,就不会初始化!那是不是

spi_lcd->bus->owner = spi_lcd 这行代码就一定要加? 其实上文说的很明白了,每次使用总线的时候都会判断,这里没有初始化也没有关系,当使用的时候总线拥有者不是自己自然会初始化,这里重要的是把配置保存在设备信息里:

查看configure函数发现跳转的是一个函数指针的定义处:

在很多开源项目中,很喜欢使用函数指针,究其原因是为了兼容二字,用函数指针可以只改实现不用改接口。

这里教一个搜索技巧,遇到这种函数我们搜索名字是很难搜索出来的,这个时候可以搜索参数!因为接口名字再改,参数是不会改的(别抬变参函数)。这里我搜索完整的参数:

发现没有匹配的函数,为什么?

因为有些人写代码的时候,函数过长会折到下一行,所以匹配不到,这时候就要搜索参数的一部分了,这里看到函数是配置函数,第一个参数是 struct rt_spi_device *device 不用说肯定一搜一大把,而第二个参数 struct rt_spi_configuration *configuration 一看只和配置有关,肯定就少得多了。这里我们选择第二个参数搜索:
 

可以看到一下就搜索出来了,而且我们也可以看到这个接口格式还有可能被用作QSPI设备。在这个函数中我们可以找到SPI初始化函数:

这个函数完全可以对比CubeMX生成的SPI配置代码一点点对比,具体不再赘述,只讲最重要的:

首先我们看到我们设置的最大频率参数影响的其实是SPI的分频参数。参考芯片手册可以知道:

H7的SPI4的主机模式最高频率为100MHz,这里分频最小二分频,也就是实际通信速率50Mbps,为什么我们通过设置最大频率直接设置?

因为内核默认的SPI时钟源是APB,而我在设置时钟时,选择的是:

 所以这里计算的肯定不对。这个地方我建议先设置分频大一点,然后逐渐缩小,因为你很难确认从机实际最大频率是多少。这里我选择8分频:

然后在下面可以看到M7内核专属的一些SPI配置:

这些需要我们手动根据需求更改。

完成这些后,我们可以先写一个读取寄存器的函数,来读取LCD的ID:

int mspi_read_reg(uint8_t reg,uint8_t *data)
{
    struct rt_spi_message msg;
    uint32_t remsg = RT_NULL;
    uint8_t reg1 = reg;
    msg.send_buf = &reg1;
    msg.recv_buf = RT_NULL;
    msg.length = 1;
    msg.cs_take = 1;
    msg.cs_release = 0;
    msg.next = RT_NULL;
    LCD_RD_REG;
    remsg = (uint32_t)rt_spi_transfer_message(spi_lcd,&msg);
    LCD_RD_DATA;
    if(remsg == 0)
    {
        msg.send_buf = RT_NULL;
        msg.recv_buf = data;
        msg.length = 1;
        msg.cs_take = 0;
        msg.cs_release = 1;
        msg.next = RT_NULL;
        remsg += (uint32_t)rt_spi_transfer_message(spi_lcd,&msg);
    }
    if(remsg!=RT_NULL)
        return -1;
    else
        return 0;
}

重新创建lcd.c及头文件:


int mlcd_readid(uint8_t *id)
{
    if(mspi_read_reg(ST7735_READ_ID1,&id[0]))
        LOG_E("ID1\n");
    else if(mspi_read_reg(ST7735_READ_ID2,&id[1]))
        LOG_E("ID2\n");
    else if(mspi_read_reg(ST7735_READ_ID3,&id[2]))
        LOG_E("ID3\n");
    else
    {
        LOG_I("ID:%02x%02x%02x",id[0],id[1],id[2]);
        return 0;
    }
    return -1;
}

void mlcd_init(void)
{
    mspi_init();
}

在main中调用:

注意最好在循环中尝试循环读取,因为有时候CS或RD引脚配置有问题会导致只有第一次读取正常,或者只有第一次读取不正常。还有就是mlcd.c中需要使用LOG_D或其他日志打印,需要添加:

实际效果: 

 

 

 

 

 

 

 

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/10460.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SolidWorks 入门笔记01:草图绘制

全文目录简介1. 草图的创建1.1 在基准面上新建一个二维草图多学一招:退出草图绘制模式,快捷键切换视图1.2 从已有的草图派生新的草图。1.3 在零件的平面上绘制草图多学一招:SolidWorks 中鼠标滚轮放大缩小功能反的解决办法 。2. 基本图形绘制…

【2022秋线上作业-第5次-第11-13周】判断题

1-1 一棵有124个结点的完全二叉树,其叶结点个数是确定的。T 解析: 一棵124个叶节点的完全二叉树,假设n0为叶子节点数,n1为度为1结点数,n2为度为2结点数,则有总结点数为n0n1n2;而n2n0-1123&#…

如何杜绝 spark history server ui 的未授权访问?

如何杜绝 spark history server ui 的未授权访问? 1 问题背景 默认状况下,Spark history Sever ui 是没有任何访问控制机制的,任何用户只要知道 shs 对应的 url,就可以访问链接查看 spark 作业的运行状况。 在证券基金银行等金融行业中&a…

Kotlin 开发Android app(六):Kotlin 中的空判断 问号和感叹号

如果有人对程序的崩溃原因做下统计的话,那么由于对象为空,但是又访问了对象的某个属性而导致的崩溃,也许会是程序崩溃的第一大原因了。 比如我们在使用字符串的时候,变量字符串为空的时候,我们去访问了这个字符串变量的…

2022-11-16 每日打卡:单调栈解决最大矩形问题(一维直方图,二维最大红矩形)

每日打卡:单调栈解决最大矩形问题(一维直方图,二维最大红矩形) 柱状图中最大的矩形 思路 这个题最明显的思路就是:矩形面积底高。 版本1:底的长度可以通过二重循环来完成,高通过循环来寻找最…

44、Spring AMQP 数据转换器

1、操作案例 2、发送一个对象到队列中 3、控制台查看 4、使用消息转换器 5、消费者接收消息, 传递什么类型,就接受什么类型,发送方与接收方所使用的消息转换器必须对应 6、总结分析 默认的消息推送是通过JDK序列化的方式进行的,…

【STM32+cubemx】0029 HAL库开发:HMC5883L磁力计的应用(电子指南针)

今天我们来学习电子磁力计HMC5883L的使用。先介绍磁力计的基础知识,再给一个获取磁力计数据的例子,最后讲解HMC5883L磁力计的校准,以及一些使用中的经验。 1)HMC5883L磁力计的基础知识 磁力计是用来测量磁场强弱(也就…

Android 录音没有声音,设置AudioSource.VOICE_CALL直接MediaRecorder.start异常等系列问题

一、我的需求:来电后,我的三方应用主动开启录音,挂断后结束录音,查验音频 我遇到的问题:录制的音频没有声音。 通过各种尝试,结果如下 :设置不同的录音来源的效果 MediaRecorder API\创建MediaR…

Springboot 结合 MQTT、Redis ,对接硬件以及做消息分发,最佳实践

Springboot 结合 mqtt、redis对接硬件以及做消息分发,最佳实践 一,认识 需要了解EMQX 基本知识原理,不了解的可以查看我之间的博客,以及网上的资料,这里不在过多撰述。 二,开发思路 这里以对接雷达水位计…

【最优化理论】03-无约束优化

无约束优化无约束优化问题无约束优化问题的应用无约束优化问题的最优性条件无约束-凸函数-最优性条件(充要)无约束-一般函数-最优性条件必要条件一阶必要条件:梯度为0二阶必要条件:hessian矩阵半正定充分条件二阶充分条件&#xf…

元宇宙-漫游世界后与Cocos一起看湖南卫视直播

使用参考资源 CocosCreator v3.6.2 cocomat 腾讯开源公共组件框架 Cocos Creator 3D特制 Video MeshRender 播放器(Cocos商店购买) TcPlayer 腾讯开源 Web 播放器 视频流 hls 库 正文 场景漫游引发的思考 元宇宙,虚拟世界。OK,…

【UI编程】将Java awt/swing应用移植到JavaFX纪实

1. 背景 最近想做一个实用的小工具,能屏幕截图,录屏和录制课件,简单的图像处理,和制作gif表情包。翻出了很久以前用Java awt/swing写的一个屏幕截图小程序,能运行,但是屏幕截图到剪贴板后,发现…

深入理解JavaScript-this关键字

先说结论:谁调用它,this 就指向谁 前言 在讲 Function、作用域 时,我们都讲到了 this,因为 JavaScript 中的作用域是词法作用域,在哪里定义,就在哪里形成作用域。而与词法作用域相对应的还有一个作用域叫…

MP157-0-遇见的问题及解决办法

MP157-0-遇见的问题及解决办法1.Win11运行VMware15虚拟机崩溃死机,蓝屏。1.Win11运行VMware15虚拟机崩溃死机,蓝屏。 时间:2022.11.15 解决办法: Hyper-V方案。 打开控制面板-程序-启用或关闭Windows功能,可能你的电…

【JavaScript高级】03-JavaScript内存管理和闭包

JavaScript内存管理和闭包JavaScript内存管理垃圾回收机制算法常见的GC算法-标记清除闭包闭包的概念理解闭包的形成过程闭包的内存泄露JavaScript内存管理 JavaScript会在定义数据时为我们分配内存: JS对于原始数据类型内存的分配会在执行时,直接在栈空…

Sentinel使用教程

文章目录一、Sentinel简介1.sentinel介绍2.sentinel应用场景3.sentinel与hystrix4.sentinel组件介绍二、Sentinel使用说明1.控制台Dashboard2.Sentinel 流量控制和熔断降级3.常见报错解决一、Sentinel简介 1.sentinel介绍 Sentinel 是由阿里巴巴中间件团队开发的开源项目&…

Java三大特性篇之——继承篇(超详解的好吧!)

😍😍😍欢迎欢迎欢迎欢迎,我的朋友,答应我,看完好吗?🥴 文章目录前言:何为继承?不谈钱的继承实现!嘘:偷偷访问父类的私密成员&#xff…

OkHttp相关知识(二)

okhttp中一次网络请求的大致过程: Call对象对请求的封装 dispatcher对请求的分发 getResponseWithInterceptors()方法 一、OkHttp同步方法总结: 创建OkHttpClient和构建了携带请求信息的Request对象将Request封装成Call对象调用Call的execute()发送…

【11.16】Codeforces 刷题

DP\text{DP}DP :(今天做的这两道都没啥 DP 相关来着 D. Match & Catch 题意: 给定两个字符串 1≤∣s1∣,∣s2∣≤50001\leq |s_1|,|s_2|\leq 50001≤∣s1​∣,∣s2​∣≤5000 ,求最短的满足各只出现一次的连续公共字串。 思…

实验27:红外遥控三级控速风扇实验

今天介绍一个稍微复杂点的实验,复杂在设计和代码 ——OK,受了抖音西湖大学教授刺激,任何人都可以做研究 ——实验:红外遥控三级风速小电扇 ——每按一下CH-,风速从1-2-3-1-2-3-1循环 ——按下CH+,风扇停止 ——没有背景音乐目的是听风扇声音大小判断风速 OK实验介绍完了…