文章目录[隐藏]
前言
这个卡了好久,网上资料实在是太少了。其中比较迷惑的点是RTThread的SPI驱动框架底层实现了一部分hal库的功能,那么它把底层实现到了哪里?我的HAL_SPI_Transmit( )函数还应不应该使用?CubeMX帮我做到了哪里?
还有一个最关键的问题,我该如何将我的SPI设备融入RTT的框架里?
硬件
参考使用DMA+SPI驱动Aliyun Things 上的ST7789H2 LCD屏幕文章的硬件章节
CubeMX
SPI配置
这里只需要配置这两项就行,
本页面中传输字长,模式,波特率啥的已经被RTT接管。
DMA配置,一定要配置。
引脚复用,一定要配置。
打开中断
工程配置
一定要生成单独的.c.h ,否则没有HAL_SPI_MspInit()
结束,生成工程就行了。
RTT studio
开启SPI1总线设备
按照board.h中的提示
step1选择完后记得ctrl+S保存
step2
step3,只要复制过来就行
HAL_SPI_MspInit主要干两件事情,设置SPI1_SCK,SPI1_MOSI输出的引脚,设置SPI1_TX DMA。
step3的另一种做法:修改SConscript脚本,使hal库生成的spi.c加入编译,推荐使用
加入之后记得更新
step4:不需要做,CubeMX已经帮你做好了
到此为止,SPI1总线已经可以使用了。
大家可能比较迷惑,
第一个问题:
还没有初始化SPI,配置极性,配置数据宽度之类的,为什么就说SPI可以用了呢?
原因在于,RTT认为SPI是总线设备,不同的外设使用一个SPI总线设备的时候,需要不同的配置参数。所以,是一个具体的外设一套参数。这个具体在代码里可以提现出来。
第二个问题:
我并没有调用HAL_SPI_MspInit,它是怎么起作用的?
这个问题在挂载SPI设备的时候会细说。
到此为止,我们通过CubeMX配置好了SPI1的硬件引脚,通过RTT studio 配置好了SPI1设备,接下来我们要将我们的lcd挂载到SPI1总线设备上。大家先将代码下载进去,应该可以看到SPI1总线设备的。
挂载SPI设备
首先,在driver目录下新建ST7789.c, ST7789.h,
C文件里,包含#include <drv_spi.h>,定义引脚,定义LCD设备名称,定义LCD设备句柄。
#include <drv_spi.h>
#include "st7789.h"
#include <rtdbg.h>
#define LCD_PWR GET_PIN(E,7)
#define LCD_RST GET_PIN(B,2)
#define LCD_DC GET_PIN(A,6)
#define LCD_SPI_NAME "spi10"
static struct rt_spi_device *lcd_dev;
H文件里,加入以下代码
#include <rtdevice.h>
#include <drv_common.h>
//LCD屏幕分辨率定义
#define LCD_Width 240
#define LCD_Height 240
#define LCD_RAM_SIZE LCD_Width*LCD_Height*2 //长240 宽240 色深2bit
#define Pixel_NUM (LCD_RAM_SIZE/2)
之后,我们要讲LCD设备挂载到SPI1总线设备上。
rt_hw_spi_device_attach函数形参如下:
const char *bus_name:总线名称,一般为"spix"
const char *device_name:设备名称, 挂在SPI1上的设备一般起名为"spi10",“spi11”…
GPIO_TypeDef *cs_gpiox:片选引脚所在的组,比如GPIOA
uint16_t cs_gpio_pin:片选引脚,比如GPIO_PIN_4
rt_err_t ret;
/**********************将LCD挂载到SPI1上*********************/
ret = rt_hw_spi_device_attach("spi1", LCD_SPI_NAME, GPIOA, GPIO_PIN_4);
if(ret != RT_EOK)
{
LOG_E("rt_hw_spi_device_attach %s err ...\n",LCD_SPI_NAME);
return -EBUSY;
}
到此,我们定义一个函数将这些包含进去,并导出到shell
static int lcd_hw_init(void)
{
...
}
MSH_CMD_EXPORT(lcd_hw_init,lcd_hw_init);
串口窗口中,可以看到执行lcd_hw_init函数后,已经挂载成功。
挂载成功之后,我们配置LCD设备的SPI属性。在上述代码之后,添加如下代码
/**********************配置LCD设备需要的spi属性*********************/
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = RT_SPI_MASTER|RT_SPI_MODE_0|RT_SPI_MSB;
cfg.max_hz =40 * 1000 * 1000;
lcd_dev = (struct rt_spi_device *)rt_device_find(LCD_SPI_NAME);
if(lcd_dev == NULL){
LOG_E("rt_device_find %s err ...\n",LCD_SPI_NAME);
return -ENODEV;
}
else
{
ret = rt_spi_configure(lcd_dev, &cfg);
}
这里我们就能看到, rt_spi_configure(lcd_dev, &cfg)该函数是将一套spi配置绑定到了一个设备上的。这个验证了之前我们所说的,一个设备一套参数的说法。在我们使用RTT SPI相关的API的时候,会自动判断之前占用总线的SPI设备和当前使用SPI的设备是否相同,不同的话会重新配置一次再执行收发操作。
插播一条:HAL_SPI_MspInit就是再rt_spi_configure中被调用的,调用顺序如下
rt_spi_configure()->函数指针configure,指向spi_configure()->stm32_spi_init()->HAL_SPI_Init()->HAL_SPI_MspInit();
到此为止,我们就完成了SPI的配置,之后就可以使用过RTT SPI相关的API了。
举一个例子来挖一下RTT的SPI发送函数干了啥,rt_spi_send是我们常用的SPI发送函数。
rt_inline rt_size_t rt_spi_send(struct rt_spi_device *device,
const void *send_buf,
rt_size_t length)
{
return rt_spi_transfer(device, send_buf, RT_NULL, length);
}
struct rt_spi_device *device: 指明操作SPI总线的设备
const void *send_buf:发送缓冲
rt_size_t length:发送字长
该函数内联到rt_spi_transfer,向下挖:
插播一条:这里就可以看到,RTT对SPI的互斥操作是通过内建互斥量实现的,当前操作SPI的不是之前的设备的话是会重新配置的。继续往下翻
找到了,就是这个xfer()是核心的发送语句,但这是一个函数指针,直接go to definition就到这里了
有一个意外发现的方法:ctrl+H全局搜索 “xfer =”
然后就找到了,xfer函数指针指向的是spixfer函数,继续往下挖
这里我们终于看到了HAL的引用,到此为止。
我当时就是到此为止哈哈哈哈,然后被大佬提醒这个函数末尾有需要注意的地方。
翻译翻译,就是说为了简化使用,你使不使用DMA,我都死等SPI传输结束。大家记住我们如果使用DMA的话,这里需要修改,怎么修改后面说。
编写驱动程序
完善C文件
/*
* Copyright (c) 2006-2021, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2022-01-15 12526 the first version
*/
#include <drv_spi.h>
#include "st7789.h"
#include <rtdbg.h>
#define LCD_PWR GET_PIN(E,7)
#define LCD_RST GET_PIN(B,2)
#define LCD_DC GET_PIN(A,6)
#define LCD_SPI_NAME "spi10"
static struct rt_spi_device *lcd_dev;
static void LCD_Write_Cmd(uint8_t cmd)
{
rt_pin_write(LCD_DC, PIN_LOW);
rt_spi_send(lcd_dev, &cmd, 1);
}
static void LCD_Write_Data(uint8_t dat)
{
rt_pin_write(LCD_DC, PIN_HIGH);
rt_spi_send(lcd_dev, &dat, 1);
}
static void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
/* 指定X方向操作区域 */
LCD_Write_Cmd(0x2a);
LCD_Write_Data(x1 >> 8);
LCD_Write_Data(x1);
LCD_Write_Data(x2 >> 8);
LCD_Write_Data(x2);
/* 指定Y方向操作区域 */
LCD_Write_Cmd(0x2b);
LCD_Write_Data(y1 >> 8);
LCD_Write_Data(y1);
LCD_Write_Data(y2 >> 8);
LCD_Write_Data(y2);
/* 发送该命令,LCD开始等待接收显存数据 */
LCD_Write_Cmd(0x2C);
}
static int lcd_hw_init(void)
{
rt_err_t ret;
/**********************将LCD挂载到SPI1上*********************/
ret = rt_hw_spi_device_attach("spi1", LCD_SPI_NAME, GPIOA, GPIO_PIN_4);
if(ret != RT_EOK)
{
LOG_E("rt_hw_spi_device_attach %s err ...\n",LCD_SPI_NAME);
return -EBUSY;
}
/**********************配置LCD设备需要的spi属性*********************/
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = RT_SPI_MASTER|RT_SPI_MODE_0|RT_SPI_MSB;
cfg.max_hz =40 * 1000 * 1000;
lcd_dev = (struct rt_spi_device *)rt_device_find(LCD_SPI_NAME);
if(lcd_dev == NULL){
LOG_E("rt_device_find %s err ...\n",LCD_SPI_NAME);
return -ENODEV;
}
else
{
ret = rt_spi_configure(lcd_dev, &cfg);
}
rt_pin_mode(LCD_PWR, PIN_MODE_OUTPUT);
rt_pin_mode(LCD_RST, PIN_MODE_OUTPUT);
rt_pin_mode(LCD_DC, PIN_MODE_OUTPUT);
return ret;
}
MSH_CMD_EXPORT(lcd_hw_init,lcd_hw_init);
static int lcd_init(void)
{
/*初始化SPI*/
lcd_hw_init();
/* 复位 */
rt_pin_write(LCD_RST, PIN_LOW);
rt_thread_delay(100);
rt_pin_write(LCD_RST, PIN_HIGH);
/* 关闭睡眠模式 */
LCD_Write_Cmd(0x11);
rt_thread_delay(120);
/* 开始设置显存扫描模式,数据格式等 */
LCD_Write_Cmd(0x36);
LCD_Write_Data(0x00);
/* RGB 5-6-5-bit格式 */
LCD_Write_Cmd(0x3A);
LCD_Write_Data(0x65);
/* porch 设置 */
LCD_Write_Cmd(0xB2);
LCD_Write_Data(0x0C);
LCD_Write_Data(0x0C);
LCD_Write_Data(0x00);
LCD_Write_Data(0x33);
LCD_Write_Data(0x33);
/* VGH设置 */
LCD_Write_Cmd(0xB7);
LCD_Write_Data(0x72);
/* VCOM 设置 */
LCD_Write_Cmd(0xBB);
LCD_Write_Data(0x3D);
/* LCM 设置 */
LCD_Write_Cmd(0xC0);
LCD_Write_Data(0x2C);
/* VDV and VRH 设置 */
LCD_Write_Cmd(0xC2);
LCD_Write_Data(0x01);
/* VRH 设置 */
LCD_Write_Cmd(0xC3);
LCD_Write_Data(0x19);
/* VDV 设置 */
LCD_Write_Cmd(0xC4);
LCD_Write_Data(0x20);
/* 普通模式下显存速率设置 60Mhz */
LCD_Write_Cmd(0xC6);
LCD_Write_Data(0x0F);
/* 电源控制 */
LCD_Write_Cmd(0xD0);
LCD_Write_Data(0xA4);
LCD_Write_Data(0xA1);
/* 电压设置 */
LCD_Write_Cmd(0xE0);
LCD_Write_Data(0xD0);
LCD_Write_Data(0x04);
LCD_Write_Data(0x0D);
LCD_Write_Data(0x11);
LCD_Write_Data(0x13);
LCD_Write_Data(0x2B);
LCD_Write_Data(0x3F);
LCD_Write_Data(0x54);
LCD_Write_Data(0x4C);
LCD_Write_Data(0x18);
LCD_Write_Data(0x0D);
LCD_Write_Data(0x0B);
LCD_Write_Data(0x1F);
LCD_Write_Data(0x23);
/* 电压设置 */
LCD_Write_Cmd(0xE1);
LCD_Write_Data(0xD0);
LCD_Write_Data(0x04);
LCD_Write_Data(0x0C);
LCD_Write_Data(0x11);
LCD_Write_Data(0x13);
LCD_Write_Data(0x2C);
LCD_Write_Data(0x3F);
LCD_Write_Data(0x44);
LCD_Write_Data(0x51);
LCD_Write_Data(0x2F);
LCD_Write_Data(0x1F);
LCD_Write_Data(0x1F);
LCD_Write_Data(0x20);
LCD_Write_Data(0x23);
/* 显示开 */
LCD_Write_Cmd(0x21);
LCD_Write_Cmd(0x29);
rt_pin_write(LCD_PWR, PIN_HIGH);
return RT_EOK;
}
INIT_DEVICE_EXPORT(lcd_init);
int lcd_hw_write(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1,
void *buffer, uint32_t size)
{
LCD_Address_Set(x0, y0, x1, y1);
rt_pin_write(LCD_DC, PIN_HIGH);
rt_spi_send(lcd_dev, (void *)buffer, size);
return RT_EOK;
}
完善H文件
/*
* Copyright (c) 2006-2021, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2022-01-19 12526 the first version
*/
#ifndef DRIVERS_ST7789_H_
#define DRIVERS_ST7789_H_
#include <rtdevice.h>
#include <drv_common.h>
//LCD屏幕分辨率定义
#define LCD_Width 240
#define LCD_Height 240
#define LCD_RAM_SIZE LCD_Width*LCD_Height*2 //长240 宽240 色深2bit
#define Pixel_NUM (LCD_RAM_SIZE/2)
int lcd_hw_write(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1,
void *buffer, uint32_t size);
#endif /* DRIVERS_ST7789_H_ */
main.c里加入测试
#include <rtthread.h>
#include <drv_common.h>
#include <drivers/pin.h>
#include "st7789.h"
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>
uint8_t data[LCD_RAM_SIZE];
int main(void)
{
static uint16_t color;
rt_tick_t t1,t2;
while(1)
{
color +=0x3333;
for(uint16_t j = 0; j < Pixel_NUM; j++)
{
data[j * 2] = color >> 8;
data[j * 2 + 1] = color;
}
t1 = rt_tick_get();
lcd_hw_write(0, 0, LCD_Width-1, LCD_Height-1, data, LCD_RAM_SIZE);
t2 = rt_tick_get();
rt_kprintf("tick = %d\r\n", t2-t1);
rt_thread_delay(500);
}
return RT_EOK;
}
此时,屏幕应该可以正常点亮了,可以看到执行一次全屏写入需要40 ticks,这明显是CPU在写,我们要修改为DMA传输方式
修改驱动程序为DMA方式
board.h里添加宏BSP_SPI1_TX_USING_DMA。
加入同步信号量,DMA传输完成之前,发送线程都得等待。
之前挖的死等的语句,也用宏屏蔽掉。
到此为止,SPI+DMA传输,使用RTT SPI框架就全部完成了。此时下载,打开串口能看到,CPU已经被解放到其他任务里去了。写入函数只耽误CPU 2个tick。
注意:这并不意味着DMA会加速一帧的传输,提高帧率。只是意味着CPU从以前的自己搬砖,变成了指挥别人搬砖了。所以write函数只是配置一些DMA参数就退出了,消耗时间会比之前少很多
吐槽一下,RTT资料好少。2013年的RTT参考手册都被我扒出来用了。但同时,RTT做的确实很漂亮。
版权声明:本文为CSDN博主「_Winston_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_42039294/article/details/122574704
暂无评论