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


    我使用的开发板是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怎么配置的,内核实际上提供了一个中间层,这样做的好处是,无论底层是串口、SPI、I2C还是其他通信接口,我们在上层都调用write就可以了,假如之前的代码里底层是串口,现在改成I2C,但是上层代码却不需要改动。

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

    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的源文件:

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

    初始化有:

    1. void mspi_rw_gpio_init(void)
    2. {
    3. rt_pin_mode(SPI_RD_PIN_NUM, PIN_MODE_OUTPUT);
    4. rt_pin_write(SPI_RD_PIN_NUM, PIN_HIGH);
    5. }
    6. void mspi_init(void)
    7. {
    8. struct rt_spi_configuration cfg;
    9. mspi_rw_gpio_init();
    10. rt_hw_spi_device_attach("spi4", "spi40", GPIOE, GPIO_PIN_11);
    11. spi_lcd = (struct rt_spi_device *)rt_device_find("spi40");
    12. if(!spi_lcd)
    13. {
    14. rt_kprintf("spi40 can't find\n");
    15. }
    16. else
    17. {
    18. spi_lcd->bus->owner = spi_lcd;
    19. cfg.data_width = 8;
    20. cfg.mode = RT_SPI_MASTER | RT_SPI_3WIRE | RT_SPI_MODE_0 | RT_SPI_MSB;
    21. cfg.max_hz = 12.5 * 1000 * 1000;
    22. rt_spi_configure(spi_lcd, &cfg);
    23. }
    24. }

    这里需要说明几点:

    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:

    1. int mspi_read_reg(uint8_t reg,uint8_t *data)
    2. {
    3. struct rt_spi_message msg;
    4. uint32_t remsg = RT_NULL;
    5. uint8_t reg1 = reg;
    6. msg.send_buf = ®1;
    7. msg.recv_buf = RT_NULL;
    8. msg.length = 1;
    9. msg.cs_take = 1;
    10. msg.cs_release = 0;
    11. msg.next = RT_NULL;
    12. LCD_RD_REG;
    13. remsg = (uint32_t)rt_spi_transfer_message(spi_lcd,&msg);
    14. LCD_RD_DATA;
    15. if(remsg == 0)
    16. {
    17. msg.send_buf = RT_NULL;
    18. msg.recv_buf = data;
    19. msg.length = 1;
    20. msg.cs_take = 0;
    21. msg.cs_release = 1;
    22. msg.next = RT_NULL;
    23. remsg += (uint32_t)rt_spi_transfer_message(spi_lcd,&msg);
    24. }
    25. if(remsg!=RT_NULL)
    26. return -1;
    27. else
    28. return 0;
    29. }

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

    1. int mlcd_readid(uint8_t *id)
    2. {
    3. if(mspi_read_reg(ST7735_READ_ID1,&id[0]))
    4. LOG_E("ID1\n");
    5. else if(mspi_read_reg(ST7735_READ_ID2,&id[1]))
    6. LOG_E("ID2\n");
    7. else if(mspi_read_reg(ST7735_READ_ID3,&id[2]))
    8. LOG_E("ID3\n");
    9. else
    10. {
    11. LOG_I("ID:%02x%02x%02x",id[0],id[1],id[2]);
    12. return 0;
    13. }
    14. return -1;
    15. }
    16. void mlcd_init(void)
    17. {
    18. mspi_init();
    19. }

    在main中调用:

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

    实际效果: 

  • 相关阅读:
    【李沐深度学习笔记】线性回归
    IPv6与VoIP——配置Cisco CME实现VoIP实验
    Kubernetes入门 十七、Helm 包管理器
    HC-05 蓝牙 2.0 串口模块
    ssm中小企业仓库管理信息系统的开发与实现毕业设计源码150916
    LinkedList源码分享
    MySQL数据库入门到精通6--进阶篇(锁)
    元器件贸易企业如何借助ERP系统,解决订单管理难题?
    多线程的线程同步(即上锁)
    Django视图与路由
  • 原文地址:https://blog.csdn.net/qwe5959798/article/details/127846330