上一篇:
(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 = ®1;
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或其他日志打印,需要添加:
实际效果: