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

本文详细介绍了如何在RT-Thread实时操作系统(RTT)环境下,针对STM32H743开发板上的SPI驱动的0.96英寸LCD屏幕进行驱动开发。首先,解释了SPI接口的特殊性,然后在RTT的内核之下,通过HAL库初始化硬件SPI并将其包含进驱动框架。接着,在内核之上,通过RTT的API注册和配置SPI设备,实现与LCD屏幕的通信。最后,通过读取LCD ID验证SPI通信的正确性。整个过程涉及到了STM32CubeMX配置、RTT内核驱动集成以及应用层的SPI操作。

我使用的开发板是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的源文件:

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

初始化有:

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或其他日志打印,需要添加:

实际效果: 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值