第8章_瑞萨MCU零基础入门系列教程之SCI SPI

news2024/12/23 15:28:11

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=728461040949

配套资料获取:https://renesas-docs.100ask.net

瑞萨MCU零基础入门系列教程汇总: https://blog.csdn.net/qq_35181236/article/details/132779862


第8章 SCI SPI

本章目标

  • 使用RASC快速配置SCI的SPI模式
  • 学会使用SCI-SPI的API进行数据收发

8.1 sci spi模块的使用

8.1.1 配置sci spi模块

本章配置的SPI是RA芯片里SCI模块中的一种模式,因而其配置方法和上一章的SCI中的UART模式配置十分的类似。

本节实验没有和其它外设相连,因而可以选择任意SCI通道:把它设置为“Simple SPI”模式,并使用其默认引脚即可。在RASC中创建了工程后,在“Pins”里面的“Peripherals”中展开“Connectivity:SCI”,选择其中一个SCI通道,例如SCI5,在“Pin Configuration”配置界面里的“Operation Mode”中将操作模式选为“Simple SPI”,如图所示:

随后根据硬件设计来选择时钟和数据收发的引脚。

接着在“Stacks”中添加SCI SPI的堆栈模块,点击进入“Stacks”配置界面后,点击“New Stack”,展开里面的“Connectivity”,选择“SPI(r_sci_spi)”,如下图所示:

这里一定要注意,RA的SPI分为“Simple SPI”和“SPI”,“Simple SPI”是SCI的模式之一,而“SPI”是一个实际存在的SPI硬件控制器。本章的实验是基于SCI的SPI,因而选择的是“SPI(r_sci_spi)”。

完成这些操作后,会在“Stacks”配置界面的“HAL/Common Stacks”中新增一个“g_spi0 SPI(r_sci_spi)”模块,接下来就要根据实际情况配置这个模块的属性。比如在前面选择的是SCI的SPI5,而此处新增的模块默认名称是“g_spi0”且默认的通道是0,为了和实际通道匹配,就要去这个模块的属性中更改名称及其通道,如下图所示:

当这里选定好“Channel”后,下方的“Pins”里面的内容就会自动更新为前面操作的引脚。

对于属性中的“Callback”,它中断回调函数,函数的命名有两种方法:

  1. 不同的外设使用不同的回调函数;
  2. 同一类的外设使用相同的回调函数,根据参数内容做不同的处理;

对于初学者,建议使用第一种方法;而对于有经验的工程师,为了减小程序代码开销,建议使用第二种方法。本书使用第一种方法,将此SCI SPI的中断回调函数名称和它的通道相匹配,改为“sci_spi5_callback”,如图所示:

而对于SCI SPI的其它参数,需要根据实际通信的外设来设置,看一下在RASC中这些参数的信息:

  • Operating Mode:SPI的操作模式,SPI有两种模式分别是主机模式和从机模式,对应于此处的“Master”和“Slave”;

  • Clock Phase:时钟相位设置,决定SPI的数据是在时钟的上升沿还是下降沿采样,在上升沿还是下降沿保持,根据SPI通信的外设要求决定;

  • Clock Polarity:时钟优先状态,即在SPI通信空闲时,时钟保持高电平还是低电平;

  • Bit Order:数据传输方向,有两种选择分别是高位在前和低位在前,同样根据SPI通信的外设要求决定;

  • Bitrate:通信速率,这个速率要根据主机支持的SCI SPI最大通信速率和从机支持的SPI最大通信速率来共同决定,取两者的较小值;

将模块属性中的参数设置好之后点击“Generate Project Content”生成代码,可以在RASC的“Summary”界面的“Location”处快速打开工程所在目录来打开工程,如图所示:

8.1.2 配置信息解读

使用RASC配置SCI SPI后,生成了引脚配置信息和SCI SPI本身的配置信息。以本章的第一个实验“0801_sci_spi_loopback”回环收发实验为例。

  1. 引脚配置信息

该信息会在0801_sci_spi_loopback\ra_gen\pin_data.c文件里生成。在RASC里配置的每一个引脚,都会在pin_data.c生成一个ioport_pin_cfg_t数组项,里面的内容跟配置时选择的参数一致。代码如下:

const ioport_pin_cfg_t g_bsp_pin_cfg_data[] = {
    {.pin = BSP_IO_PORT_01_PIN_08,
     .pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN +
               | (uint32_t) IOPORT_PERIPHERAL_DEBUG)
    },
    {.pin = BSP_IO_PORT_03_PIN_00,
     .pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN 
              | (uint32_t) IOPORT_PERIPHERAL_DEBUG)
    },
    {.pin = BSP_IO_PORT_05_PIN_01,
     .pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN 
               | (uint32_t) IOPORT_PERIPHERAL_SCI1_3_5_7_9)
    },
    {.pin = BSP_IO_PORT_05_PIN_02,
     .pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN 
               | (uint32_t) IOPORT_PERIPHERAL_SCI1_3_5_7_9)
    },
    {.pin = BSP_IO_PORT_05_PIN_03,
     .pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN 
               | (uint32_t) IOPORT_PERIPHERAL_SCI1_3_5_7_9)
    },
};
  • 第10~21行就是配置SCI的SPI5的引脚P501/P502/P503,将他们配置为SCI通道1、3、5、7、9的外设引脚。
  1. SCI SPI配置信息

该信息会在0801_sci_spi_loopback\ra_gen\hal_data.c文件里生成。在RASC里指定了SPI使用哪个SCI通道、指定了它的通道、时钟特性、数据传输方向和中断回调函数注册等信息,这些配置信息都被放入一个spi_cfg_t结构体,部分代码摘录如下:

const spi_cfg_t g_spi5_cfg =
{
    .channel         = 5,
    .operating_mode  = SPI_MODE_MASTER,
    .clk_phase       = SPI_CLK_PHASE_EDGE_ODD,
    .clk_polarity    = SPI_CLK_POLARITY_LOW,
    .mode_fault      = SPI_MODE_FAULT_ERROR_DISABLE,
    .bit_order       = SPI_BIT_ORDER_MSB_FIRST,
    ......(省略内容)
    .p_callback      = sci_spi5_callback,
    ......(省略内容)
};
  • 第3行:指定使用的sci通道;
  • 第4~8行:设置SPI的通信参数;
  • 第10行:注册SPI的中断回调函数;

8.1.3 中断回调函数

中断回调函数的原型已经在hal_data.h中进行了声明,需要用户在自己的程序中实现,其原型代码如下:

/** Called by the driver when a transfer has completed or an error has occurred (Must be implemented by the user). */
#ifndef sci_spi5_callback
void sci_spi5_callback(spi_callback_args_t *p_args);
#endif

它的参数类型是一个spi_callback_args_t结构体指针,这个结构体的原型如下:

/** Common callback parameter definition */
typedef struct st_spi_callback_args
{
    uint32_t     channel;              ///< Device channel number
    spi_event_t  event;                ///< Event code
    void const * p_context;            ///< Context provided to user during callback
} spi_callback_args_t;

此结构体会表明触发此中断的是哪一个sci通道,是什么事件触发的,而触发的内容在sci中是没有使用到的。

触发中断的事件由枚举类型spi_event_t指明,这个枚举的内容如下:

/** SPI events */
typedef enum e_spi_event
{
    SPI_EVENT_TRANSFER_COMPLETE = 1,   ///< The data transfer was completed
    SPI_EVENT_TRANSFER_ABORTED,        ///< The data transfer was aborted
    SPI_EVENT_ERR_MODE_FAULT,          ///< Mode fault error
    SPI_EVENT_ERR_READ_OVERFLOW,       ///< Read overflow error
    SPI_EVENT_ERR_PARITY,              ///< Parity error
    SPI_EVENT_ERR_OVERRUN,             ///< Overrun error
    SPI_EVENT_ERR_FRAMING,             ///< Framing error
    SPI_EVENT_ERR_MODE_UNDERRUN        ///< Underrun error
} spi_event_t;

包括了SPI发送完成中断事件、发送暂停事件和其它错误事件。用户就可以根据传参进来的事件进行对应的处理。例如在spi5的中断回调函数中,可以如下设置“发送完成标志”:

void sci_spi5_callback(spi_callback_args_t *arg)
{
    if(SPI_EVENT_TRANSFER_COMPLETE == arg->event)
        sci_spi5_tx_cplt = 1;
}
  • 第3行:判断事件是否为SPI发送完成事件;

  • 第4行:如果是发送完成触发的中断,则将发送完成标志位置1;

8.1.4 API接口及其用法

在0801_sci_spi_loopback\ra\fsp\inc\api\r_spi_api.h中定义了spi模块的接口,它定义了一个结构体类型spi_api_t,内容如下:

/** Shared Interface definition for SPI */
typedef struct st_spi_api
{
    fsp_err_t (* open)(spi_ctrl_t * p_ctrl, spi_cfg_t const * const p_cfg);
    fsp_err_t (* read)(spi_ctrl_t * const p_ctrl, 
                       void * p_dest, 
                       uint32_t const length,
                       spi_bit_width_t const bit_width);
    fsp_err_t (* write)(spi_ctrl_t * const p_ctrl, 
                        void const * p_src, 
                        uint32_t const length,
                        spi_bit_width_t const bit_width);
    fsp_err_t (* writeRead)(spi_ctrl_t * const p_ctrl, 
                            void const * p_src, 
                            void * p_dest, 
                            uint32_t const length, 
                            spi_bit_width_t const bit_width);
    fsp_err_t (* callbackSet)(spi_ctrl_t * const p_api_ctrl, 
                              void (* p_callback)(spi_callback_args_t *), 
                              void const * const p_context,
                               spi_callback_args_t * const p_callback_memory);
    fsp_err_t (* close)(spi_ctrl_t * const p_ctrl);
} spi_api_t;

在具体的C文件中,需要实现一个sci_api_t结构体变量,比如在r_sci_spi.c里实现了如下结构体:

const spi_api_t g_spi_on_sci =
{
  .open     = R_SCI_SPI_Open,
  .read     = R_SCI_SPI_Read,
  .write    = R_SCI_SPI_Write,
  .writeRead  = R_SCI_SPI_WriteRead,
  .close    = R_SCI_SPI_Close,
  .callbackSet = R_SCI_SPI_CallbackSet
};

要使用SCI SPI收发数据时,可以调用结构体g_spi_on_sci里的各个函数指针,也可以直接调用r_sci_spi.c里实现的各个函数(比如

R_SCI_SPI_Open、R_SCI_SPI_Read)。
  1. 打开SCI SPI设备

此函数用于配置SCI的SPI,并且标记转态为“已经打开”。函数原型:

fsp_err_t (* open)(spi_ctrl_t * p_ctrl, spi_cfg_t const * const p_cfg);

来看一下这个函数的参数:

a) p_ctrl:此参数是一个spi_ctrl_t结构体指针类型,该结构体本质上void类型,原型如下:

typedef void spi_ctrl_t;

它可以指向任意类型的参数,在r_sci_spi.h里,这个参数实际的类型是sci_spi_instance_ctrl_t结构体,定义如下:

typedef struct st_sci_spi_instance_ctrl
{
    uint32_t          open;
    spi_cfg_t const * p_cfg;
    R_SCI0_Type     * p_reg;
    uint8_t         * p_src;
    uint8_t         * p_dest;
    uint32_t          tx_count;
    uint32_t          rx_count;
    uint32_t          count;

    /* Pointer to callback and optional working memory */
    void (* p_callback)(spi_callback_args_t *);
    spi_callback_args_t * p_callback_memory;

    /* Pointer to context to be passed into callback function */
    void const * p_context;
} sci_spi_instance_ctrl_t;
  • 第3行:open表示SPI设备的状态,是关闭还是打开状态
  • 第4行:SPI的配置信息,指定在SPI传输过程中类如发送中断、接受中断等的中断号、中断优先级和时钟特性等;
  • 第5行:R_SCI0_Type结构体对应SCI的各个寄存器;
  • 第6、7行:指向发送数据的首地址和接收数据保存到内存中的首地址;
  • 都8、9行:当发送完一个字节或接收到一个字节的数据后,发送计数将会在中断中减1,而接收计数将会加1,直到发送计数为0或接收计数达到指定接收长度值,就会调用中断回调函数;
  • 第13行:指向中断回调函数;
  • 第14行:保存中断回调函数信息;
  • 第17行:传递给中断回调函数的其他信息;

b) p_cfg:此参数是spi_cfg_t结构体类型,该结构体就是上述第4行的内容,原型如下:

typedef struct st_spi_cfg
{
    uint8_t channel;                                   ///< Channel number to be used

    IRQn_Type                   rxi_irq;         ///< Receive Buffer Full IRQ number
    IRQn_Type                   txi_irq;       ///< Transmit Buffer Empty IRQ number
    IRQn_Type                   tei_irq;          ///< Transfer Complete IRQ number
    IRQn_Type                   eri_irq;               ///< Error IRQ number
    uint8_t                     rxi_ipl;             ///< Receive Interrupt priority
    uint8_t                     txi_ipl;            ///< Transmit Interrupt priority
    uint8_t                     tei_ipl;   ///< Transfer Complete Interrupt priority
    uint8_t                     eri_ipl;       ///< Error Interrupt priority
    spi_mode_t           operating_mode;   ///< Select master or slave operating mode
    spi_clk_phase_t      clk_phase;      ///< Data sampling on odd or even clock edge
    spi_clk_polarity_t          clk_polarity;          ///< Clock level when idle
    spi_mode_fault_t     mode_fault;///< Mode fault error (master/slave conflict) flag
    spi_bit_order_t         bit_order;         ///< Select to transmit MSB/LSB first
    transfer_instance_t const * p_transfer_tx;         ///< To use SPI DTC/DMA write transfer, link a DTC/DMA instance here.  Set to NULL if unused.
    transfer_instance_t const * p_transfer_rx;         ///< To use SPI DTC/DMA read transfer, link a DTC/DMA instance here.  Set to NULL if unused.
    void (* p_callback)(spi_callback_args_t * p_args); ///< Pointer to user callback function
    void const * p_context;      ///< User defined context passed to callback function
    void const * p_extend;   ///< Extended SPI hardware dependent configuration
} spi_cfg_t;

这些参数在使用RASC配置后,自动生成对应的配置信息代码。

开发者可以在自己的代码中调用open函数来实现对sci spi设备的初始化,例如:

void drv_sci_spi_init(void)
{
    fsp_err_t err = g_spi5.p_api->open(g_spi5.p_ctrl, g_spi5.p_cfg);
    if(FSP_SUCCESS == err)
        printf("Success to open device: spi5\r\n");
    else
        printf("Failed to open device: spi5\r\n");
}
  1. 关闭SCI SPI设备

SCI SPI设备的close函数,会将SPI设备的状态标志open设置为0,原型如下:

fsp_err_t (* close)(spi_ctrl_t * const p_ctrl);

此函数的参数就是一个spi_crl_t结构体类型的参数。开发者可以参考以下代码来关闭指定的spi设备:

void drv_sci_spi_close(void)
{
    fsp_err_t err = g_spi5.p_api->close(g_spi5.p_ctrl);
    if(FSP_SUCCESS == err)
        printf("Success to close device: spi5\r\n");
    else
        printf("Failed to close device: spi5\r\n");
}
  1. 使用SCI SPI发送指定长度的数据

SCI SPI的发送函数write适用于半双工或单工通信场景,其原型如下:

fsp_err_t (* write)(spi_ctrl_t * const p_ctrl, 
                    void const * p_src, 
                    uint32_t const length,
                    spi_bit_width_t const bit_width);

来看一下它的参数:

  • p_ctrl:spi_crl_t结构体类型参数,传入使用RASC生成的spi设备的全局结构体变量;
  • p_src:源数据(要发送的数据)地址;
  • length:要发送的数据个数;
  • bit_width:数据宽度,在SCI中一律设置为8Bit即,此位传入SPI_BIT_WIDTH_8_BITS;

开发者可以参考如下代码来发送指定长度的数据:

void drv_sci_spi_write(uint8_t *pbuf, uint16_t size)
{
    sci_spi5_tx_cplt = 0;
g_spi5.p_api->write(g_spi5.p_ctrl, 
(uint8_t*)pbuf, 
(uint32_t)size, SPI_BIT_WIDTH_8_BITS);
    while(!sci_spi5_tx_cplt);
}

第3行和第5行是发送标志:发送前清零,如果发送完成会在中断回调函数中被置1。

  1. 使用SCI SPI接收指定长度的数据

SCI SPI的接收函数read适用于半双工或单工通信场景,其原型如下:

fsp_err_t (* read)(spi_ctrl_t * const p_ctrl, 
                   void * p_dest, 
                   uint32_t const length,
                   spi_bit_width_t const bit_width);

和发送及其相似,只不过p_dest是接收数据的缓冲区首地址,其数据宽度依然是8Bit。

开发者可以参考如下代码来接收指定长度的数据:

g_spi5.p_api->read(g_spi5.p_ctrl, (uint8_t*)pbuf, (uint32_t)size, SPI_BIT_WIDTH_8_BITS);
  1. 使用SCI SPI同时收发指定长度的数据

在全双工模式下建议使用SCI SPI的同时收发函数writeRead,其原型如下:

fsp_err_t (* writeRead)(spi_ctrl_t * const p_ctrl, 
                        void const * p_src, 
                        void * p_dest, 
                        uint32_t const length, 
                        spi_bit_width_t const bit_width);

使用此函数可以同时发送和接收相同长度的数据,数据宽度是8bit。开发者可以参考以下代码来实现同时收发:

void drv_sci_spi_writeRead(uint8_t *wbuf, uint8_t *rbuf, uint16_t size)
{
    sci_spi5_tx_cplt = 0;
    g_spi5.p_api->writeRead(g_spi5.p_ctrl, 
                           (uint8_t*)wbuf, 
                           (uint8_t*)rbuf, 
                           (uint32_t)size, 
                           SPI_BIT_WIDTH_8_BITS);
    while(!sci_spi5_tx_cplt);
}

8.2 sci spi回环收发实验

本节实验会用到UART的printf功能,请参考前文《7.3 stdio实验》配置实现printf功能。

8.2.1 硬件连接

所谓回环收发就是自发自收,数据从MCU的MOSI直接发送给自身的MISO,因而在硬件上只需要将MOSI和MISO短接即可。例如本节实验0801_sci_spi_loopback使用到的SCI5,其发送引脚是P501,接收引脚是P502,做本实验将这两个脚短接即可,不用管SCK和CS引脚。

8.2.2 应用程序

本实验工程将spi的驱动代码模块化,分为初始化函数、收发函数、中断回调函数和测试函数,spi的驱动函数都在drv_sci_spi.c中实现,在drv_sci_spi.h中声明,测试函数在app_spi.c中实现,在app.h中声明,以便后续移植使用。

  1. 设备初始化

函数:void SPIDrvInit(void),实现如下:

void SPIDrvInit(void)
{
    /* 打开设备 */
    fsp_err_t err = g_spi5.p_api->open(g_spi5.p_ctrl, g_spi5.p_cfg);
    /* 发送调试信息 */
    if(FSP_SUCCESS == err)
        printf("Success to open device: spi5\r\n");
    else
        printf("Failed to open device: spi5\r\n");
}
  1. 中断回调函数

中断回调函数已经在前文讲过许多次,这里直接展示其实现:

static volatile bool gSPITxCplt = false;
void sci_spi5_callback(spi_callback_args_t *arg)
{
    /* 判断是否是发送完成触发的中断 */
    /* 如果是的话就将发送完成标志位置1 */
    if(SPI_EVENT_TRANSFER_COMPLETE == arg->event)
        gSPITxCplt = true;
}

发送完成标志是一个静态的全局变量:

static volatile bool gSPITxCplt = false;

它会在中断回调函数中被修改,使用了关键字volatile修饰。

  1. 收发完成等待函数

每次收发通信的时候,需要根据收发状态标志来判断是否通信完成,因而将这一个判断动作封装成一个超时等待函数:void SPIDrvWaitTxCplt(void),代码如下:

static void SPIDrvWaitTxCplt(void)
{
    uint16_t wTimeout = 50;
    while(!gSPITxCplt && wTimeout)
    {
        R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS);
        wTimeout--;
    }
    gSPITxCplt = false;
}
  1. 收发数据驱动函数

对于此函数的调用者而言,实际上只需要关心存有发送数据的缓冲区、用于接收数据的缓冲区、要收发的数据个数,并不关心数据传输的宽度,因而将收发函数封装为以下这样的函数:

void SPIDrvWriteReadBuf(uint8_t *wbuf, uint8_t *rbuf, uint16_t wSize)
{
    /* 调用writeRead函数收发数据 */
    g_spi5.p_api->writeRead(g_spi5.p_ctrl,
                            (uint8_t*)wbuf,
                            (uint8_t*)rbuf,
                            (uint32_t)wSize,
                            SPI_BIT_WIDTH_8_BITS);
    /* 等待size长度的数据发送和接收完成 */
    SPIDrvWaitTxCplt();
}
  1. 测试代码

测试代码比较简单,就是每次收发相同长度的数据然后进行注意比较,出错则将错误数据打印出来,代码实现如下:

void SPIAppTest(void)
{
    SPIDrvInit();

    /* 测试计数,测试count次后退出测试 */
    uint32_t dwCount = 5;
    /* 收发数据保存的数组,长度为256字节 */
    uint8_t wBuf[256] = {0};
    uint8_t rBuf[256] = {0};
    while(dwCount)
    {
        /* 每次发送数据前给发送数组赋值随机数,增加测试可靠性 */
        for(uint16_t i=0; i<256; i++)
        {
            wBuf[i] = (uint8_t)rand();
        }
        /* 同时收发256字节数据 */
        SPIDrvWriteReadBuf(wBuf, rBuf, 256);
        uint16_t err = 0;
        /* 逐一比较收发数组中的数据是否一致,如果出现不一致就将其打印出来观察并且计数错误个数 */
        for(uint16_t i=0; i<256; i++)
        {
            if(wBuf[i] != rBuf[i])
            {
                err++;
                printf("Error:\r\n\twBuf:0x%.2x\trBuf:0x%.2x\r\n", wBuf[i], rBuf[i]);
            }
        }
        if(0 == err)
        printf("Success to write and read data by sci spi:\t%d!\r\n", (int)dwCount);
dwCount--
        /* 每隔1秒测试一次 */
        R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);
    }
}

8.2.3 上机实验

在hal_entry.c中调用void drv_sci_spi_init(void)初始化函数,将编译成功后生成的可执行二进制文件烧写到芯片中,可以观察到如下现象:

8.3 sci spi驱动显示屏实验

本节实验会用到UART的printf功能,请参考前文《7.3 stdio实验》配置实现printf功能,并将0801_sci_spi_loopback/drivers中的drv_uart.c和drv_uart.h移植到本节实验的工程中。

8.3.1 硬件连接

本节实验是使用RA的SCI SPI模式驱动一块SPI接口的显示屏,这块显示屏的驱动芯片是ST7796s,实验板和显示屏的连接原理图如图所示:

本节实验只涉及SPI通信,因而只需要关心原理图中13~19号引脚,各引脚的意思如下表:

显示屏接口号接口含义MCU引脚号MCU引脚模式
13-MISOSPI从机的输出引脚,主机的输入引脚P100SCI0 SPI的RXD0
14-CSSPI片选脚,低电平有效P103GPIO Out
15-RSST7796s的数据/命令切换引脚,高电平表示接收数据,低电平表示接收命令P104GPIO Out
16-SCKSPI的时钟输出引脚P102SCI0 SPI的SCK0
17-MOSISPI主机的输出引脚,从机的输入引脚P101SCI0 SPI的TXD0
18-RESETST7796s的硬件复位引脚P105GPIO Out
19-PWM显示屏的背光控制引脚,高电平点亮P608GPIO Out/GPT5的GTIOC5A

对于背光控制引脚,既可以直接使用IO输出高电平全功率点亮屏幕,也可以使用GPT功能输出PWM调节,由于还没有讲到GPT外设,本节将P608配置为GPIO的输出模式。

8.3.2 配置SCI SPI和Ports

配置SCI的SPI和GPIO在前面的章节已经讲过,本节就不再赘述。与本章第一个实验不同的是,本节使用的引脚是SCI0的SPI引脚,因而在选择SCI通道的时候要选择为SCI0,且配置其Stack的时候要将控制块名称和通道都和通道0相匹配,中断回调函数那里也取名为sci_spi0_callback。

8.3.3 显示屏驱动解析

要驱动一块显示屏,必须要先了解这块显示屏内部的驱动芯片,弄清楚这块芯片支持什么接口,用什么协议通信,支持那种颜色格式,有哪些配置命令等等。这些都需要去仔细阅读显示屏驱动芯片的手册。

一块显示屏的驱动是比较复杂的,要想将其显示性能尽可能的发挥是需要花很长时间去钻研手册的,这不是本书的重点,本书将演示如何点亮ST7796s驱动的屏幕,并把屏幕设置为指定颜色。

  1. 屏幕分辨率

ST7796s能够驱动分辨率最大为320480的屏幕,每个像素使用16tbit表示其颜色。全屏显示需要显存320480*2字节,约300K字节。

  1. 支持的接口协议

ST7796s支持的接口协议有许多种,例如:

  • 8bit/9bit/16bit/18bit的8080并行接口;
  • 16/18 RGB接口;
  • 3线制/4线制串行接口;
  • MIPI接口;

本书使用的是4线制串行接口,也就是SPI接口,包括CS、SCK、MOSI和MISO。

  1. 命令/数据切换控制

在串行接口驱动下,通信数据从数据模式到命令模式的相互切换依靠的是ST7796s的DCX引脚,也就是前文原理图中的RS引脚,手册中对此引脚的描述如下图:

此引脚输入的电平为‘1’时,ST7796s认为收到的数据为显示数据;为‘0’时认为收到的数据是命令。也就是书SPI主机,即单片机这边通过RS引脚输出高低电平来切换数据的模式。

  1. 休眠与唤醒

当ST7796s发生硬件复位或软件复位时,复位结束后芯片是处于休眠模式的,即手册中说的Sleep in模式,在此模式下芯片的内部晶振不会工作,DC转换也会停止,因而显示也会失效。

要想正常的显示就需要将其唤醒,休眠和唤醒都是通过命令来控制的:

  • 0x10:Sleep in
  • 0x11:Sleep out

这两个命令在休眠模式下都是可以通过SPI传输给ST7796s,因而在低功耗场景下,可以用这两个命令实现屏幕的休眠和唤醒。

  1. 显示模式设置

ST7796s支持的显示模式有局部显示和普通显示。局部显示,顾名思义,就是在320 RGB*480分辨率下不显示完整,仅在某个区域显示。在局部显示模式下,需要通过命令来设置显示区域。普通显示就是整屏显示。

这两个模式的设置命令如下:

  • 0x12:局部显示;
  • 0x13:普通显示;
  1. 显示方向设置

图像在屏幕上的显示方向由两个要素控制:像素显示方向和颜色显示方向。像素显示方向由4个方位组合,即上下左右,可以分为:

  • 左->右,上->下;
  • 左->右,下->上;
  • 右->左,上->下;
  • 右->左,下->上;

而颜色方向则只有两种:RGB或BGR。

一般人们习惯的阅读方式是左->右,上->下,而颜色方向常用的是RGB。控制显示方向的命令是0x36,其描述如下:

MY/X/V是像素数据在现存中行列地址读写的方向,细节如下图:

具体的示例在手册中有详细描写,本节仅摘取部分,如图所示:

要让数据对应的像素点如何显示取决于具体的应用场景。

  1. 颜色格式设置

设置颜色格式使用的命令是0x3A,其描述如下图所示:

通常情况下将RGB接口的颜色格式和控制接口的颜色格式都设置为16bit,即写为0x55。

  1. 显示的开启和关闭

开启和关闭显示使用到的命令分别是0x29和0x28:

  • 0x28:Display Off,关闭显示;
  • 0x29:Display On,开启显示;
  1. 显示地址设置

显示地址分为行地址和列地址,设置行列地址有不同的命令,分别是:

  • 0x2A:Column Address Set,列地址设置;
  • 0x2B:Row Address Set,行地址设置;

观察下手册中对这两个命令的描述:

可以看到,他们均需要设置各自的起始地址和结束地址,且均是先发送始地址的高8位,再发送始地址的低8位,接着发送结束地址的高8位,最后发送结束地址的低8位。

清楚了这个规则后,在之后的设置显示区域的时候就知道该怎么写代码了。

  1. 将数据写入显存

将数据写入显存使用的命令是0x2C,看下它的描述:

不难理解,当发送了0x2C给芯片后,接着可以发N个字节的像素数据到显存。

8.3.4 显示屏驱动程序

通过前文已经可以理清楚驱动ST7796s的基本流程:

本次实验将显示器的驱动代码在drv_sci_spi_disp.c中实现,显示器设备结构体在drv_config.h中声明。

  1. 中断回调函数和写等待函数

中断回调函数将发送完成标志写1,而写等待函数则是等待发送完成标志被设置为1之后再将其清零,代码如下:

static volatile bool gFlagWaitTX = false;
void sci_spi0_callback(spi_callback_args_t *arg)
{
    /* 判断是否是发送完成触发的中断 */
/* 如果是的话就将发送完成标志位置1 */
    if(SPI_EVENT_TRANSFER_COMPLETE == arg->event)
        gFlagWaitTX = true;
}
static void LCDDrvWaitTX(void)
{
    volatile uint16_t wTimeout = 500;
    while(!gFlagWaitTX && wTimeout)
    {
        R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS);
        wTimeout--;
    }
    gFlagWaitTX = false;
}
  1. 定义引脚状态

根据ST7796s的引脚描述和原理图连接关系,来定义各引脚的状态枚举类型,代码如下:

typedef enum{
    notLight,
    isLight
}Black;   /* 背光引脚控制状态 */

typedef enum{
    isReset,
    notReset
}Reset;   /* 复位引脚控制状态 */

typedef enum{
    isSelect,
    notSelect
}CS;      /* 片选信号控制状态 */

typedef enum{
    isCommand,
    isData
}DC;     /* 数据/命令切换控制状态 */
  1. 控制引脚操作函数

驱动ST7796s需要控制4个引脚,用来选中设备、切换指令数据、硬件复位、控制背光等,他们的代码如下:

static void LCDDrvWriteCS(CS eState)
{
    g_ioport.p_api->pinWrite(g_ioport.p_ctrl,
                             BSP_IO_PORT_01_PIN_03,
                             (bsp_io_level_t)eState);
}
static void LCDDrvWriteDCX(DCX eState)
{
    g_ioport.p_api->pinWrite(g_ioport.p_ctrl,
                             BSP_IO_PORT_01_PIN_04,
                             (bsp_io_level_t)eState);
}
static void LCDDrvWriteReset(Reset eState)
{
    g_ioport.p_api->pinWrite(g_ioport.p_ctrl,
                             BSP_IO_PORT_01_PIN_05,
                             (bsp_io_level_t)eState);
}
static void LCDDrvWriteBlack(Black eState)
{
    g_ioport.p_api->pinWrite(g_ioport.p_ctrl,
                             BSP_IO_PORT_06_PIN_08,
                             (bsp_io_level_t)eState);
}

因为这些控制函数仅会在ST7796s的内部驱动函数中用到,因而都是静态函数,在后续的使用中可以这样使用:

LCDDrvWriteReset(isReset);
LCDDrvWriteDCX(isCommand);
LCDDrvWriteDCX(isData);
LCDDrvWriteCS(isSelect);
  1. 硬件复位

通过RESET引脚硬件复位驱动芯片,低电平复位,复位结束后要将其拉回高电平,以免使芯片一直处于复位状态,代码如下:

static void LCDDrvHWReset(void)
{
    LCDDrvWriteReset(isReset);
    R_BSP_SoftwareDelay(100, BSP_DELAY_UNITS_MILLISECONDS);
    LCDDrvWriteReset(notReset);
    R_BSP_SoftwareDelay(50, BSP_DELAY_UNITS_MILLISECONDS);
}
  1. 写寄存器命令函数

写命令需要将引脚DCX拉低,然后发送一个字节的命令数据,代码如下:

static void LCDDrvWriteReg(uint8_t reg)
{
    LCDDrvWriteDCX(isCommand);
    g_spi0.p_api->write(&g_spi0_ctrl, (uint8_t*)&reg, 1, SPI_BIT_WIDTH_8_BITS);
    LCDDrvWaitTX();
}
  1. 写数据函数

写数据则需要将DCX拉高,然后开始发送数据,此处封装的是给寄存器发送数据的函数,每次只发送一个字节,代码如下:

static void LCDDrvWriteDat(uint8_t dat)
{
    LCDDrvWriteDCX(isData);
    g_spi0.p_api->write(&g_spi0_ctrl, (uint8_t*)&dat, 1, SPI_BIT_WIDTH_8_BITS);
    LCDDrvWaitTX();
}
  1. 写多字节数据函数

在显示器驱动中,通常是把很多数据连续地发送给显示器,本工程将其封装成了如下函数:

static void LCDDrvWriteBuf(uint8_t* buf, uint32_t size)
{
    LCDDrvWriteReg(0x3C);
    LCDDrvWriteDCX(isData);
    
    unsigned char *pbuf = (unsigned int*)buf;
    while(size)
    {
        uint32_t length = 0;
        if(size<65536)
            length = (uint16_t)size;
        else
        {
            length = 65535;
        }
        fsp_err_t err = g_spi0.p_api->write(g_spi0.p_ctrl, pbuf, length, SPI_BIT_WIDTH_8_BITS);
        assert(FSP_SUCCESS==err);
        LCDDrvWaitTX();
        size = size - length;
        pbuf = pbuf + length;
    }
}
static void LCDDrvWriteBuf(uint8_t* buf, uint32_t size)
{
    LCDDrvWriteReg(0x3C);
    LCDDrvWriteDCX(isData);
    g_spi0.p_api->write(&g_spi0_ctrl, (uint8_t*)buf, size, SPI_BIT_WIDTH_8_BITS);
    LCDDrvWaitTX();
}

先发送一个0x3C指令,告诉显示器即将来很多个数据,然后再开始发送数据。

0x3C连续写模式仅在ST7796s的’MX=1’下才可使用。

  1. 注册显示器控制函数

同样使用面向对象编程思想,将显示器的操作封装到一个结构体中,用函数指针成员来指向具体的操作函数,此结构体在drv_disp.h中定义如下:

typedef struct DisplayDevice {
    char *name;
    void *FBBase; /* CPU能直接读写的显存 */
    unsigned short wXres;    /* X方向分辨率 */
    unsigned short wYres;    /* Y方向分辨率 */
    unsigned short wBpp;     /* 每个像素使用多少个像素 */
    unsigned int   dwSize;
    void           (*Init)(struct DisplayDevice *ptDev);   /* 硬件初始化 */
    void           (*DisplayON)(struct DisplayDevice *ptDev);   /* 开启显示 */
    void           (*DisplayOFF)(struct DisplayDevice *ptDev);   /* 关闭显示 */
    void           (*SetDisplayWindow)(struct DisplayDevice* ptDev, \
                                     unsigned short wXs, unsigned short wYs, \
                                     unsigned short wXe, unsigned short wYe);
    void           (*Flush)(struct DisplayDevice *ptDev); /* 把FBBase的数据刷到LCD的显存里 */
    /* 设置FBBase中的数据, 把(iX,iY)的像素设置为颜色dwColor
     * dwColor的格式:0x00RRGGBB
     */
    int          (*SetPixel)(struct DisplayDevice *ptDev, \
                             unsigned short wX, unsigned short wY, \
                             unsigned short wColor);
    struct DisplayDevice *pNext;
}DisplayDevice, *PDisplayDevice;

开发者需要在驱动程序中实现此结构体:设置里面的参数(比如分辨率)、函数指针。本工程实现的结构体如下:

static DisplayDevice gLcdDevice = {
        .name = "LCD",
        .FBBase = gLcdFbuf,
        .wXres = 320,
        .wYres = 480,
        .wBpp = 16,
        .dwSize = 320*480*16/8,
        .Init = LCDDrvInit,
        .DisplayON = LCDDrvSetDisplayOn,
        .DisplayOFF = LCDDrvSetDisplayOff,
        .SetDisplayWindow = LCDDrvSetDisplayWindow,
        .Flush = LCDDrvFlush,
        .SetPixel = LCDDrvSetPixel
};

第二个成员FBBase指向的数组,需要根据显示器的特性来确定,比如本实验的ST7796s,它的一个像素由16bit数据组成,全屏有320480个像素,因而就需要320480*16/8个字节的数组来表示显存在处理器内存中的映射,如下所示:

static unsigned short gLcdFbuf[320*480];

本实验已经实现的操作函数将会在后文进行介绍。

  1. 获取显示设备函数

出于良好的编程习惯,将显示设备结构体定义为了一个静态的全局变量,上层代码需要一个接口来获取这个结构体:

struct DisplayDevice *LCDGetDevice(void)
{
    return &gLcdDevice;
}

此函数返回一个DisplayDevice指针,上层应用通过它来操作显示设备。

  1. 显示器的开启和关闭函数

这两个操作函数比较简单,就是向ST7796s发送0x28/0x29指令即可,代码如下:

void LCDDrvSetDisplayOn(struct DisplayDevice* ptDev)
{
    if(NULL == ptDev->name)    return;
    LCDDrvWriteReg(0x29);
}
void LCDDrvSetDisplayOff(struct DisplayDevice* ptDev)
{
    if(NULL == ptDev->name)    return;
    LCDDrvWriteReg(0x28);
}
  1. 设置显示区域函数

通过前文已知,设置显示区域就是设置显示行、列的起始地址、结束地址,先发地址高8位再发地址低8位的规则,封装得到了如下代码的函数:

void LCDDrvSetDisplayWindow(struct DisplayDevice* ptDev, \
                           unsigned short hwXs, unsigned short hwYs, \
                           unsigned short hwXe, unsigned short hwYe)
{
    if(NULL == ptDev->name)    return;
    /* 设置列地址 */
    LCDDrvWriteReg(0x2A);
    LCDDrvWriteDat((uint8_t)(hwXs>>8));       // 起始地址先高后低
    LCDDrvWriteDat((uint8_t)(0x00FF&hwXs));
    LCDDrvWriteDat((uint8_t)(hwXe>>8));        // 结束地址先高后低
    LCDDrvWriteDat((uint8_t)(0x00FF&hwXe));
    /* 设置行地址 */
    LCDDrvWriteReg(0x2B);
    LCDDrvWriteDat((uint8_t)(hwYs>>8));
    LCDDrvWriteDat((uint8_t)(0x00FF&hwYs));
    LCDDrvWriteDat((uint8_t)(hwYe>>8));
    LCDDrvWriteDat((uint8_t)(0x00FF&hwYe));
}
  1. 全屏刷新函数

此函数用于将设备结构体里FBBase的所有数据,一次性发送给屏幕,用来刷新整个屏幕,代码如下:

void LCDDrvFlush(struct DisplayDevice *ptDev)
{
    if(NULL == ptDev->name)    return;
    LCDDrvWriteBuf((uint8_t*)ptDev->FBBase, (uint32_t)ptDev->dwSize);
}
  1. 设置像素点函数

在有些应用场景下会对某个或者某几个像素点进行单独操作,来显示指定图像,因而需要实现一个单独像素点设置的接口以供使用,代码如下所示:

int LCDDrvSetPixel(struct DisplayDevice *ptDev, \
                   unsigned short wX, unsigned short wY, \
                   unsigned short wColor)
{
    if(NULL == ptDev->name)    return -1;
    if (wX >= ptDev->wXres || wY >= ptDev->wYres)
        return -1;

    unsigned short *buf = (unsigned short*)ptDev->FBBase;

    buf[wY * ptDev->wXres + wX] = (unsigned short)wColor;

    return 0;
}

像素点位置的偏移根据屏幕的分辨率和显示模式决定,本章的计算公式不一定适合所有的屏幕,读者移植使用的时候需要注意。

  1. 屏幕初始化函数

本次实验初始化屏幕设置的参数比较简单:打开SPI->硬件复位->唤醒设备->设置颜色格式->开启显示->点亮背光。代码如下:

void LCDDrvInit(struct DisplayDevice* ptDev)
{
    if(NULL == ptDev->name)    return;
    /* 打开SPI设备完成初始化 */
    fsp_err_t err = g_spi0.p_api->open(&g_spi0_ctrl, &g_spi0_cfg);
    if(FSP_SUCCESS == err)
        printf("Success to open device:\tspi0\r\n");
    else
        printf("Failed to open device:\tspi0\r\n");
    
    /* 初始化屏幕设备 */
    LCDDrvHWReset(); //LCD 复位
    LCDDrvWriteCS(isSelect);
    LCDDrvWriteBlack(isLight);//点亮背光
    
    LCDDrvWriteReg(0x11);
    LCDDrvWriteReg(0x20);
    LCDDrvWriteReg(0x36);
    LCDDrvWriteDat(0x40);
    LCDDrvWriteReg(0x3A);
    LCDDrvWriteDat(0x55);
    LCDDrvWriteReg(0x13);
    LCDDrvWriteReg(0x29);
}

8.3.5 显示屏测试程序

本次实验的测试程序有两个功能:清屏和画圆。测试程序在app_disp.c文件中实现,在app.h中声明。测试程序代码如下:

#define FLOYRGB565(r, g, b) ((unsigned short)((((unsigned short)(r>>3)<<11)|(((unsigned short)(g>>2))<<5)|((unsigned short)b>>3))))

void DispAppTest(void)
{
    DisplayDevice *ptDispDev = LCDGetDevice();
    if(NULL == ptDispDev)
    {
        printf("Failed to get LCD device!\r\n");
        return;
    }
    /* 初始化显示设备 */
    ptDispDev->Init(ptDispDev);
    /* 设置屏幕显示区域 */
    ptDispDev->SetDisplayWindow(ptDispDev, 0, 0, ptDispDev->wXres - 1, ptDispDev->wYres - 1);
    /* 清除屏幕 */
    memset((uint8_t*)ptDispDev->FBBase, 0x00, ptDispDev->dwSize);
    ptDispDev->Flush(ptDispDev);
    /* 画一个实心圆 */
    uint16_t x = 0, y = 0, r = 100;
    for(x = ((ptDispDev->wXres>>1) - r); x<((ptDispDev->wXres>>1) + r); x++)
    {
        for(y = ((ptDispDev->wYres>>1) - r); y<((ptDispDev->wYres>>1) + r); y++)
        {
            if(((x-(ptDispDev->wXres>>1)) * (x-(ptDispDev->wXres>>1))+(y-(ptDispDev->wYres>>1)) * (y-(ptDispDev->wYres>>1))) <= r*r)
            {
                ptDispDev->SetPixel(ptDispDev, x, y, FLOYRGB565(0, 255, 0));
            }
        }
    }
    ptDispDev->Flush(ptDispDev);
}
  • 第01行:使用宏函数将RGB颜色转换为16bit的颜色值;
  • 第15~16行:清屏;
  • 第19~30行:在屏幕中心画一个半径为100像素点的实心圆;

随后在hal_entry.c中的hal_entry()函数调用各设备初始化函数以及这个圆圆函数,即可在屏幕上显示一个填充的黑色圆,代码如下:

#include "drv_uart.h"
#include "app.h"
#include "hal_data.h"
void hal_entry(void)
{
    /* TODO: add your own code here */
    UARTDrvInit();
    DispAppTest();
}

1.3.6 上机实验

将编译生成的可执行二进制文件烧入到芯片中后,通过串口可以得到如下打印信息:

Success to open device: uart7
Success to open device: spi0

观察屏幕可以看到一个填充圆。


本章完

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

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

相关文章

DBSCAN 揭秘:了解此算法的工作原理

一、说明 DBSCAN 代表 基于密度的带噪声应用程序空间聚类。它是一种流行的聚类算法&#xff0c;用于机器学习和数据挖掘&#xff0c;根据数据集中紧密排列在一起的点与其他点的距离对点进行分组。 二、DBSCAN的算法原理 DBSCAN 的工作原理是将数据划分为由密度较低的区域分隔的…

数据结构与算法:数据结构基础

目录 数组 定义 形式 顺序存储 基本操作 读取元素 更新元素 插入元素 删除元素 扩容 初始化 时机 步骤 优劣势 链表 定义 单向链表 特点 双向链表 随机存储 基本操作 查找节点 更新节点 插入节点 删除元素 数组VS链表 栈与队列 栈 定义 基本操作…

分类预测 | Matlab实现SO-RF蛇群算法优化随机森林多输入分类预测

分类预测 | Matlab实现SO-RF蛇群算法优化随机森林多输入分类预测 目录 分类预测 | Matlab实现SO-RF蛇群算法优化随机森林多输入分类预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现SO-RF蛇群算法优化随机森林多输入分类预测&#xff08;完整源码和数据&…

notepad++去除每一行第二个等号之后的内容解决ResolvePackageNotFound

([^])$ 正则表达式用第一行的 https://blog.csdn.net/Charlotte_Si/article/details/132333988 原文的正则表达式不知道为什么没用

XXE-Lab for PHP

环境配置 1.将靶场进行下载.... https://github.com/c0ny1/xxe-lab 2.将PHPStudy的中间件与版本信息调制为php-5.4.45Apache访问以下地址开始练习... http://127.0.0.1/xxelabs/php_xxe/ 靶场实操 1.在登录界面输入账号密码并抓取数据包.... 2.尝试读取本地文件.... <…

第6章_瑞萨MCU零基础入门系列教程之串行通信接口(SCI)

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

TCP协议报文,核心特性可靠的原因,超时重传详细介绍

目录 一、TCP协议 二、TCP核心特性的保障 三、保留的六位标志位对于应答报文的作用 四、如何处理丢包——超时重传的原理 五、超时重传的时间 一、TCP协议 每一行是四个字节&#xff0c;前面的20个字节是固定的&#xff08;TCP最短长度&#xff0c;20字节&#xff0c;选项…

进制和编码

目标&#xff1a;了解计算机中一些必备的尝试知识&#xff0c;了解常见名词背后的意义 1.python的运行方式 交互式运行脚本式运行 2.进制 2.1 进制的转换 计算机中底层的所有数据都是0101010101的形式存在的 八进制无法直接转为2进制 十进制转其他进制 bin(25) # 10进制转…

[XSCTF]easyxor

查看&#xff0c;main函数&#xff0c;首先是将输入的数据与key中的字符依次异或 在这个循环里&#xff0c;将异或得到的结果每次减去1&#xff0c;同时在数组里写上1&#xff0c;往后遍历数组 直到异或得到的结果减为0&#xff0c;在数组中写一个0进行标记 最后比较数组是否与…

百度飞桨(厦门)人工智能产业赋能中心签约,共创人工智能产业协同服务新生态...

9月8日&#xff0c;第二十三届中国国际投资贸易洽谈会在厦门正式开幕。 当日&#xff0c;厦门市思明区政府和火炬管委会&#xff0c;与百度正式签约&#xff0c;联手共建百度飞桨&#xff08;厦门&#xff09;人工智能产业赋能中心。思明区委书记林重阳&#xff0c;厦门市工信局…

【 NTR-domain ;NTR-C345C】

tissue inhibitors of metalloproteases (TIMP) TIMP NTR-C345C HPX-binding-domain [ MMP CAT(FN2)PEX (hemopexin) ] [ MMP-9-inhibitor a-2M (NTR-C345C) ] netrin Laminin-GLaminin-EGFNTR-domain

软件测试报告有什么用?

报告类型 不同的报告类型有不同的报告用途&#xff0c;以下分类别进行分析 1、登记测试报告 可以用于软件产品的增值税即征即退、软件企业的双软评估以及计算机系统集成资质的材料 2、鉴定\确认测试报告 可以用用于政府项目申报、高新认证、项目结题、创新产品认定、各类政…

c++11 override 和 final 关键字

1. final&#xff1a;修饰虚函数&#xff0c;表示该虚函数不能再被重写 如果一个类不想被继承&#xff1a;下面分别提供了c98 和 c11的不同写法 c 98 的写法&#xff1a; c 11 的写法&#xff1a;final最终类 2. override: 检查派生类虚函数是否重写了基类某个虚函数&#xff…

机器学习——K最近邻算法(KNN)

机器学习——K最近邻算法&#xff08;KNN&#xff09; 文章目录 前言一、原理二、距离度量方法2.1. 欧氏距离2.2. 曼哈顿距离2.3. 闵可夫斯基距离2.4. 余弦相似度2.5. 切比雪夫距离2.6. 马哈拉诺比斯距离2.7. 汉明距离 三、在MD编辑器中输入数学公式&#xff08;额外&#xff0…

C++数据结构X篇_11_C++栈的应用-后缀表达式求解

上篇C栈的应用-中缀转后缀中我们介绍了我们所熟知的中缀表达式转为后缀表达式&#xff0c;那么如何通过后缀表达式获得原表达式的值呢&#xff1f;本篇将会参考博文栈的应用-后缀表达式求解介绍计算机是如何基于后缀表达式计算的&#xff1f; 文章目录 1. 后缀表达式求解计算规…

【MySQL】一文带你理解索引事务及其原理

MySQL- 索引事务 文章目录 MySQL- 索引事务索引操作索引原理 事务操作并发执行 索引 索引是一种特殊的文件&#xff0c;包含着对数据表里所有记录的引用指针。可以对表中的一列或多列创建索引&#xff0c;并指定索引的类型&#xff0c;各类索引有各自的数据结构实现。 通俗来…

C++项目实战——基于多设计模式下的同步异步日志系统-④-日志系统框架设计

文章目录 专栏导读模块划分日志等级模块日志消息模块日志消息格式化模块日志消息落地模块日志器模块日志器管理模块异步线程模块 模块关系图 专栏导读 &#x1f338;作者简介&#xff1a;花想云 &#xff0c;在读本科生一枚&#xff0c;C/C领域新星创作者&#xff0c;新星计划导…

新知同享 | Web 开发性能提升,优化体验

更加强大且开放的 Web 可以简化开发工作并支持 AI 一起来看 2023 Google 开发者大会上 Web 开发值得重点关注的升级与成果 了解 Web 如何实现加速开发&#xff0c;更加便捷 精彩大会现场一览 Web 开发不断发展&#xff0c;每年都带来性能提升和功能迭代&#xff0c;开启丰富多…

【多线程】volatile 关键字

volatile 关键字 1. 保证内存可见性2. 禁止指令重排序3. 不保证原子性 1. 保证内存可见性 内存可见性问题: 一个线程针对一个变量进行读取操作&#xff0c;另一个线程针对这个变量进行修改操作&#xff0c; 此时读到的值&#xff0c;不一定是修改后的值&#xff0c;即这个读线…

用python实现基本数据结构【01/4】

说明 如果需要用到这些知识却没有掌握&#xff0c;则会让人感到沮丧&#xff0c;也可能导致面试被拒。无论是花几天时间“突击”&#xff0c;还是利用零碎的时间持续学习&#xff0c;在数据结构上下点功夫都是值得的。那么Python 中有哪些数据结构呢&#xff1f;列表、字典、集…