前言

目前有一个项目中用到了TFT-LCD,其驱动芯片为ILI9341。为更好的达到显示效果,在最终的代码中我们会使用单片机自带的硬件SPI+DMA模块(由于调试过程中SPI+DMA输出的波形没能驱屏成功,最终只使用GD32的硬件SPI,这里也请在阅读完本文后对这方面有调试经验的大哥给个建议,我会详细的说明其中的现象),以尽可能的减轻CPU的负荷。不过在前期调试过程中,我们会使用到软件模拟SPI以及单独的硬件SPI。由于我们只是使用SPI与屏显的驱动芯片通信,所以只使用SPI的发送,在接下来的代码演示中,不包含SPI接收。以后如果有机会用到SPI通信的FLASH存储芯片或其他,会再出一个调试文章。(关于驱屏显示和LVGL图形界面库的移植我会另出一篇文章详细介绍,有兴趣的朋友可以耐心等待。)

SPI

一种串行通信总线,包含一根时钟线一根片选线以及一根两根数据线

  • 若使用一根数据线代表SPI的TX与RX共用一根线,也就是3线SPI,发送数据与接收数据不能同步进行。
  • 若使用两根数据线代表SPI的TX与RX各自单独用一根线,也就是4线SPI,发送数据与接收数据能同步进行。
  • 片选线为从设备的SPI通信使能,这样在主机(如单片机)控制的SPI通信总线上可挂载多个可以SPI通信的从设备(这也意味着在多个从设备下片选线也是多个的)。
  • 时钟线为SPI通信的速率,就如单片机的晶振,人的心跳一样。数据线上的发送与接收肯定是随着时钟线上电平的跳变而进行。

这里不例举出SPI通信的典型时序图,因为凡是支持的SPI通信的从设备的芯片手册上都会有对应的通信时序,而且在下面的章节中会有实际示波器抓到的波形。

DMA

一个能帮助CPU大大减轻处理压力的助手,这里不多作介绍。

各模块程序编写

在配置前,请确保你已经有一个GD32F303包含其对应标准库的keil工程,工程可使用官方的例程或可按照GD32F303调试小记(零)之工程创建与编译创建。此外,强烈建议身边有个示波器逻辑分析仪,用于查看我们端口输出的通信波形。

一、时钟配置

  • 开启GPIO端口时钟、GPIO引脚复用时钟、DMA时钟和SPI2模块的时钟。
void SystemClock_Reconfig(void)
{/* Enable all peripherals clocks you need*/rcu_periph_clock_enable(RCU_GPIOA);rcu_periph_clock_enable(RCU_GPIOB);rcu_periph_clock_enable(RCU_GPIOC);rcu_periph_clock_enable(RCU_GPIOD);rcu_periph_clock_enable(RCU_DMA0);rcu_periph_clock_enable(RCU_DMA1);
//      rcu_periph_clock_enable(RCU_I2C1);
//      rcu_periph_clock_enable(RCU_ADC0);
//      rcu_periph_clock_enable(RCU_ADC2);
//      rcu_periph_clock_enable(RCU_USART1);rcu_periph_clock_enable(RCU_USART2);rcu_periph_clock_enable(RCU_SPI2);/* Timer1,2,3,4,5,6,11,12,13 are hanged on APB1,* Timer0,7,8,9,10             are hanged on APB2*/rcu_periph_clock_enable(RCU_TIMER1);   rcu_periph_clock_enable(RCU_AF);
}

二、GPIO配置


  • 根据上图中手册中对SPI2引脚的描述以及实际电路原理图中的接线,相关IO配置如下:
/* SPIx By Soft Or Hardware */
#define SPI2_SOFT           0// 1:使用软件SPI             0:使用硬件SPI 这里使用软件SPI初始化屏,硬件SPI刷屏
#define SPI2_DMA            0//1:使用硬件SPI+DMA      0:仅使用硬件SPI(使用软件SPI时该宏无意义)// SPI port and pins definition
#define SPI2_PORT                       GPIOB
#define SPI2_SCK_PIN                    GPIO_PIN_3
#define SPI2_MISO_PIN                   GPIO_PIN_4
#define SPI2_MOSI_PIN                   GPIO_PIN_5
#define SPI2_CS_PIN                     GPIO_PIN_6//NSS// TFT port and pins definition
#define TFT_PORT                        GPIOB
#define TFT_RS_PIN                      GPIO_PIN_7//1:发数据 0:发命令
#define TFT_RST_PIN                     GPIO_PIN_8//1:不复位 0:复位
#define TFT_BG_PIN                      GPIO_PIN_9//1:亮         0:不亮void GPIO_Init(void)
{/* 使用SW下载,不使用JTAG下载,管脚用作其它功能 */gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE);#if SPI2_SOFTgpio_init(SPI2_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, SPI2_CS_PIN | SPI2_MOSI_PIN | SPI2_SCK_PIN); gpio_init(SPI2_PORT, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, SPI2_MISO_PIN);
#elsegpio_pin_remap_config(GPIO_SPI2_REMAP,DISABLE);gpio_init(SPI2_PORT, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, SPI2_MOSI_PIN | SPI2_SCK_PIN); // | SPI2_MISO_PINgpio_init(SPI2_PORT, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, SPI2_MISO_PIN);gpio_init(SPI2_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, SPI2_CS_PIN); #endif/* demo board TFT_LCD I/O */gpio_init(TFT_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, TFT_RST_PIN | TFT_BG_PIN);   gpio_init(TFT_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, TFT_RS_PIN);}

三、DMA配置

  • 由上图可知,SPI2的TX对应DMA1的CH1,SPI2的RX对应DMA1的CH0。
  • 由于是驱动屏的,代码里只需要写发送就可以了。
#define SPI2_TX_SIZE    8
uint8_t SPI2TX_Buffer[SPI2_TX_SIZE] = {0};void DMA_Init(void)
{dma_parameter_struct dma_init_SPI2_TX={0};dma_deinit(DMA1, DMA_CH1);                                 //SPI2_TX/* initialize DMA1 channel1(SPI2_TX) */                      dma_init_SPI2_TX.direction = DMA_MEMORY_TO_PERIPHERAL;dma_init_SPI2_TX.memory_addr = (uint32_t)SPI2TX_Buffer;dma_init_SPI2_TX.memory_inc = DMA_MEMORY_INCREASE_ENABLE;dma_init_SPI2_TX.memory_width = DMA_MEMORY_WIDTH_32BIT;dma_init_SPI2_TX.number = SPI2_TX_SIZE;dma_init_SPI2_TX.periph_addr = (uint32_t)(&SPI_DATA(SPI2));dma_init_SPI2_TX.periph_inc = DMA_PERIPH_INCREASE_DISABLE;dma_init_SPI2_TX.periph_width = DMA_PERIPHERAL_WIDTH_32BIT;dma_init_SPI2_TX.priority = DMA_PRIORITY_LOW;dma_init(DMA1, DMA_CH1, &dma_init_SPI2_TX);   dma_circulation_disable(DMA1, DMA_CH1);dma_memory_to_memory_disable(DMA1,DMA_CH1);                  //SPI2_TX/* enable all DMA channels you need */dma_channel_disable(DMA1,DMA_CH1);                         //SPI2_TX
}

四、SPI配置

  • 主机全双工模式、数据位8位、时钟线空闲高电平第2个边沿检测、不适用硬件NSS、SPI波特率为27MHz、数据先发送高位。
  • 其中spi2_init.clock_polarity_phase可以换成其它、spi2_init.prescale在最初调试时设置的慢一点如几百kHz。
void SPIx_Init(void)
{spi_parameter_struct spi2_init = {0};/* deinitilize SPI and the parameters */spi_i2s_deinit(SPI2);/* SPI2 parameter config */spi2_init.device_mode         = SPI_MASTER;spi2_init.trans_mode         = SPI_TRANSMODE_FULLDUPLEX;spi2_init.frame_size           = SPI_FRAMESIZE_8BIT;spi2_init.clock_polarity_phase   = SPI_CK_PL_HIGH_PH_2EDGE;       //SPI_CK_PL_HIGH_PH_1EDGE       SPI_CK_PL_LOW_PH_1EDGEspi2_init.nss                   = SPI_NSS_SOFT;spi2_init.prescale             = SPI_PSC_2;                 // Fspi1=54MHz/32=0.84375MHzspi2_init.endian              = SPI_ENDIAN_MSB;spi_init(SPI2, &spi2_init);/* enable SPI2 */spi_enable(SPI2);/* enable SPI2 DMA channel */spi_dma_enable(SPI2,SPI_DMA_TRANSMIT);
//  spi_dma_enable(SPI2,SPI_DMA_RECEIVE);
}

五、SPI写函数

  • 使用的芯片不一样或者实现的方法不一样,意味着编写底层代码时也会略有区别。在这一章节中,就是SPI写这个函数的区别。
  • 如我文章标题所示,我会列出软件SPI、硬件SPI、硬件SPI+DMA三种方式的底层写函数。

1. 软件SPI写函数

  • 软件SPI,说白了就是让CPU去操作IO来模拟SPI波形。
  • 我们写一个字节,SCK时钟线翻转8次。数据从高位开始发送,那么就是与上0x80,数据线根据与上0x80后的结果判断当前位是高还是低输出高低电平。这么一帧的操作,我们认为是写一个字节的操作。
void SPI_WriteByte(uint8_t ndata)
{uint8_t i;for(i=0;i<8;i++){gpio_bit_write(SPI2_PORT, SPI2_SCK_PIN, RESET);if(ndata&0x80)gpio_bit_write(SPI2_PORT, SPI2_MOSI_PIN, SET);elsegpio_bit_write(SPI2_PORT, SPI2_MOSI_PIN, RESET);gpio_bit_write(SPI2_PORT, SPI2_SCK_PIN, SET);ndata <<= 1;}
}

2.硬件SPI写函数

  • 硬件SPI,则是让CPU去控制单片机里能输出SPI波形的硬件模块。
  • 还记得上面我们初始化配置的SPI2吗,在那里我们已经设置好了SPI2的工作模式,这里我们只需发送读写命令即可。
  • 为了保证严格遵循通信的时序,读/写前需查看相关缓存寄存器是否置位。
  • 为防止实际情况下意外干扰导致MCU卡死或跑飞,除了必要的看门狗外,任何非必要的while循环里都得有超时跳出机制。
  • 这里我虽然进行了读操作,但并没有去处理读的内容,若想有此功能,将读到的值作为函数的返回值即可。这里这么写还有别的用处。
void SPIx_Master_Write_Byte_Hard(uint8_t ndata,uint32_t TimeOut)
{uint32_t timeout_t=0;timeout_t = TimeOut;/* loop while spi tx data register in not emplty */while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) ){if(TimeOut > 0) TimeOut--; else               break;            }/* send byte through the SPI2 peripheral */spi_i2s_data_transmit(SPI2,ndata);while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_RBNE)){if(timeout_t > 0) timeout_t--; else                  break;        }        spi_i2s_data_receive(SPI2);
}

3.硬件SPI+DMA写函数

  • 硬件SPI+DMA,道理与单纯操作硬件SPI一样,不过是变成了DMA去操作硬件SPI模块,而CPU去控制DMA罢了。
  • 根据上面我们对DMA1_CH1的配置,当我们把该通道使能就意味着一次数据传输的开始。
void SPIx_Transmit_DMA(uint32_t spi_periph,uint16_t* ndata,uint16_t Size)
{if(SPI2 == spi_periph){/* TX */dma_channel_disable(DMA1,DMA_CH1);    dma_memory_address_config(DMA1,DMA_CH1,(uint32_t)ndata);dma_transfer_number_config(DMA1,DMA_CH1,Size);dma_channel_enable(DMA1,DMA_CH1);     }
}

六、主函数部分

1. 显示部分

  • 这里我们再封装一层,用于切软件SPI和硬件SPI底层函数。
void lcd_wr_byte(uint8_t ndata)
{        gpio_bit_write(TFT_PORT,TFT_RS_PIN,SET); gpio_bit_reset(SPI2_PORT,SPI2_CS_PIN);  //CS=0
#if SPI2_SOFTSPIx_Master_Write_Byte(ndata,0xFFFFFFFFU);
#elseSPIx_Master_Write_Byte_Hard(ndata,0xFFFFFFFFU);
#endifgpio_bit_set(SPI2_PORT,SPI2_CS_PIN);   //CS=1
}
  • 这里是TFT-LCD刷彩屏部分,大概理解即可。
void LCD_Fillx(uint16_t sx,uint16_t sy,uint16_t ex,uint16_t ey,uint16_t* color)
{          uint16_t i;uint8_t MSB_8=0,LSB_8=0;if(sx>ex){i    = sx;sx = ex;ex = i;}if(sy > ey){i   = sy;sy = ey;ey = i;           }uint32_t total = (ex - sx + 1)*(ey - sy + 1);uint32_t j = 0;        LCD_SetCursor(sx,sy,ex,ey);lcd_wr_gram();for(j = 0;j < total;j++){                                       lcd_wr_byte((*color)>>8);lcd_wr_byte((*color)&0x00FF);}
} void LCD_Fill_DMA(uint16_t sx,uint16_t sy,uint16_t ex,uint16_t ey,uint16_t* color)
{          uint16_t i;if(sx>ex){i  = sx;sx = ex;ex = i;}if(sy > ey){i   = sy;sy = ey;ey = i;           }uint32_t total = (ex - sx + 1)*(ey - sy + 1)*2;LCD_SetCursor(sx,sy,ex,ey);lcd_wr_gram();gpio_bit_reset(SPI2_PORT,SPI2_CS_PIN);  //CS=0gpio_bit_write(TFT_PORT,TFT_RS_PIN,SET);    SPIx_Transmit_DMA(SPI2,color,total);     //(uint16_t*)0xF800     color
}

2. 任务函数

void TASK_TFT_REFRESH(void)
{static uint32_t refresh_count=0;static uint16_t color_temp=0;if(refresh_count%3==0)color_temp = 0xF800;else if(refresh_count%3==1)color_temp = 0x001F;else if(refresh_count%3==2)color_temp = 0xFFE0;
#if (SPI2_SOFT==0) && (SPI2_DMA==1)LCD_Fill_DMA(0,0,319,239,&color_temp);
#elseLCD_Fillx(0,0,319,239,&color_temp);
#endifrefresh_count++;
}

3. 主函数

  • 主函数程序逻辑如下:
  • TMT是个时间片框架,源码见GITEE,这里我们是让TASK_TFT_REFRESH();这个函数每1.5秒执行一次。主函数理解到此处即可。
int main(void)
{    SystemTick_Init();SystemClock_Reconfig();GPIO_Init();Timer1_Init();DMA_Init();USARTx_Init();
#if !SPI2_SOFTSPIx_Init();
#endifNVIC_Init();TMT_Init();LCD_Init();TMT.Create(TASK_TFT_REFRESH,1500);delay_ms(2000);while(1){TMT.Run();}
}

七、结果演示

1. 实际效果

  • 最终效果如下,软硬件SPI的区别也就在刷屏速度上,就不单独再加视频了。

2. 驱动波形

  • 调试过程中,其实这里才是重点,判断我们代码是否配置成功,逻辑是否有误,均需要查看相应IO的输入/输出波形才能明白。

- 软件SPI波形

  • 上图中,黄色的波形是SPI的时钟线(SCK),蓝色是SPI的数据输出线(MOSI)。
  • 我们可以清楚的看到SPI波形是以一个字节(8位)为一帧,时钟线翻转8次,发送的数据为0x55(0b01010101),速率在2.3MHz左右。

- 硬件SPI波形

  • 上图,在硬件SPI模拟下,我们可以清楚的看到SPI波形还是以一个字节(8位)为一帧,时钟线翻转8次,发送的数据依然为0x55(0b01010101),速率在3.5MHz左右。不过硬件的IO响应比CPU软件模拟快,通信时SCK和MOSI的翻转几乎是同时的。

  • 这个时候我们可以调整硬件SPI的配置,将spi2_init.prescale = SPI_PSC_2; 由于我的单片机是工作在108M的频率下,该配置下SPI2模块频率为27MHz。

  • 上图为示波器抓到的波形,可以看到频率为25.64MHz,与配置的差不多。然而时钟线(SCK)的波形已经发生了很明显的畸变。个人认为是由PCB的走线长短示波器抓取的参考地以及芯片本身该模块内部电路导致的,不过仍能刷屏成功,我也就不在此纠结了。

  • 多字节的SPI发送时钟线波形如上:

- 硬件SPI+DMA波形

  • 上图是使用硬件SPI+DMA的时钟线波形图。
  • 我们发现这种情况下的波形是连续的,从某种程度上说,我们的代码配置并没有问题。但是,该连续波形无法驱动我的彩屏,后经反复验证发现,无论是否使用DMA,只要输出的SPI波形为连续的,都无法与TFT彩屏通信上。
  • 还记得上面我们怎么写纯硬件SPI的写字节函数吗?
void SPIx_Master_Write_Byte_Hard(uint8_t ndata,uint32_t TimeOut)
{uint32_t timeout_t=0;timeout_t = TimeOut;/* loop while spi tx data register in not emplty */while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) ){if(TimeOut > 0) TimeOut--; else               break;            }/* send byte through the SPI2 peripheral */spi_i2s_data_transmit(SPI2,ndata);while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_RBNE)){if(timeout_t > 0) timeout_t--; else                  break;        }        spi_i2s_data_receive(SPI2);
}
  • 这里我们稍作修改,变成这样:
  • 在4线全双工模式下,如果我只用到了发送,不去管接收,实际上也没接收处理什么。
void SPIx_Master_Write_Byte_Hard(uint8_t ndata,uint32_t TimeOut)
{uint32_t timeout_t=0;timeout_t = TimeOut;/* loop while spi tx data register in not emplty */while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) ){if(TimeOut > 0) TimeOut--; else               break;            }/* send byte through the SPI2 peripheral */spi_i2s_data_transmit(SPI2,ndata);//  while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_RBNE))
//  {
//      if(timeout_t > 0) timeout_t--;
//      else                    break;
//  }
//  spi_i2s_data_receive(SPI2);
}
  • 这里我们发现纯硬件SPI发送多字节的情况下时钟线也变成了连续的波形。 而但凡只要硬件上输出的是连续的波形,就会造成与屏通信的不正常。

八、总结与疑问

1. 总结

  • 至此,我们最终用上述的这三种方式成功输出了SPI波形。其中两种方式能成功的让屏跑起来,从结果上来说也算是成功了。

2. 疑问

  • 网上搜集信息,有人的文章里说SPI波形连续是正常的,而且连续波形反而比不连续的更好,说明SPI模块响应速度更快。
  • 所以我想向各位问下:
    1、是否有SPI连续波形通信正常的案例,若有这种案例在哪类模块中比较常见?
    2、现有的SPI通信屏是在时钟翻转8次后稍有停顿就能成功驱屏,那么在GD32中如何在使用硬件SPI+DMA情况下插入短暂的停顿操作?
    3、是否有使用ILI9341使用连续波形SPI四线通信成功的案例,若有能否给予相关建议?
    4、对于该现象,是否是硬件兼容性的问题?是否有的SPI通信模块只支持的帧(8bit或16bit)传输而不支持这种多bit传输?
  • 若有相关建议的请在评论区留言即可,相互学习,共同成长。

!!!本文为欢喜6666在CSDN原创发布,复制或转载请注明出处:)!!!

GD32F303调试小记(二)之SPI(软件SPI、硬件SPI、硬件SPI+DMA)相关推荐

  1. GD32F303调试小记(三)之IIC(硬件IIC+PCF8563实时时钟)

    前言 前面的文章介绍了在单片机中常用的两种通信协议(USART和SPI),并给出了GD32F303对应的配置流程.这次介绍第三种常见的通信协议IIC.这此使用GD32的硬件IIC通信PCF8563实时 ...

  2. GD32F303调试小记(一)之USART(接收中断、接收空闲中断+DMA、发送DMA)

    前言 之前写了GD32F103调试小记(二)之USART(接收中断.接收空闲中断+DMA.发送DMA)一文.这次我们来看看GD32F303的USART是如何配置的,结合这两篇文章,相信大家GD32的U ...

  3. GD32F303固件库开发(16)----移植兆易创新SPI Nor Flash之GD25Q64Flash

    spi概述 SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的 ...

  4. GD32F350.SPI软件片选模式

    在开始使用GD32F350G8的硬件SPI时对于NSS功能下的主从模式有点迷惑, 需求为主机模式下对从机发送数据,片选线低有效 手册上说If the application wants to use ...

  5. STM32L475 硬件SPI+软件SPI驱动ST7789V2

    前言 最近购买了IoT Board 潘多拉开发板来研究,学习使用STM32CubeMX工具配置SPI,然后驱动了TFTLCD.潘多拉开发板的TFTLCD驱动IC是ST7789V2,结合原子哥的TFTL ...

  6. AS5047P磁编码器ESP32驱动程序、硬件电路设计、SPI通信时序、逻辑波形分析、注意事项

    1.AS5047P硬件设计 1.1 简介.性能参数 AS5047P 是一种款高分辨率旋转位置传感器,用于在整个 360 度范围内进行高速(高达 28krpm)角度测量.这种新型位置传感器配备了革命性的 ...

  7. SPI接口通信协议详解:SPI时序、2线、3线、4线SPI及4种常用工作模式

    简介 SPI通信原理比I2C要简单,它主要是主从方式通信.这种模式通常只有一个主机和一个或者多个从机,标准的SPI是4根线,分别是SSEL(片选,也写作 SCS).SCLK(时钟,也写作SCK).MO ...

  8. 软件开发模型_QT开发(二十三)——软件开发流程

    一.软件开发流程简介 软件开发流程是通过一系列步骤保证软件产品的顺利完成,是软件产品在生命周期内的管理学. 软件开发流程的本质是软件开发流程与具体技术无关,是开发团队必须遵守开的规则. 二.常见软件开 ...

  9. 【STM32】SPI的基本原理、库函数(SPI一般步骤)

    STM32F1xx官方资料: <STM32中文参考手册V10>-第23章 串行外设接口SPI SPI的基本介绍 SPI的简介 SPI,是英语Serial Peripheral interf ...

最新文章

  1. 关于for中思维卡机的小悲剧
  2. 有关软件开发中的一些想法
  3. Python网络爬虫--Scrapy使用IP代理池
  4. 9.VMware vsphere 5.0新体验-新增功能
  5. 从mysql数据库读取Blob_读取数据库Blob类型的文本数据
  6. 洛谷 2312 / bzoj 3751 解方程——取模
  7. 是否要学点GUI编程
  8. jqgrid 固定列宽度_jqGrid 设置列宽
  9. hpm1216nfh驱动程序_惠普m1216nfh打印机驱动
  10. 最常用的GitHub—— Android 开源项目整理(精品)
  11. FastReport 动态加载图片
  12. php中文手册 最新评论整合,ThinkPHP整合百度Ueditor
  13. hexo文章图片加载不出来 | hexo文章插入图片
  14. iPhone6/6S的适配
  15. 重庆“易法院”上线 民众足不出户参与诉讼全过程
  16. 视频拍摄技巧——构图
  17. SLAM大牛Cyrill 开源SuMa ++:基于语义激光雷达过滤动态物体提高定位精度
  18. c++入门学习笔记继承
  19. element 合并单元格
  20. 机器视觉未来发展的想法

热门文章

  1. 年终总结PPT 注意事项
  2. 360导致html异常,出现360安全浏览器异常崩溃的情况怎么办
  3. JAVA版本微信管家平台—JeeWx 捷微 4.1 微服务版本发布,微信砍价活动闪亮登场!...
  4. 物联网会成为移动互联网的下一个红海市场吗
  5. MATLAB自动驾驶学习(3)——以编程方式创建驾驶场景的变体
  6. EduSoho开源网络课堂网校系统 v8.3.36官方最新版
  7. 2022年深圳市促进大健康产业集群高质量发展的若干措施
  8. android即刻夜间模式,【即刻热评】夜间模式:苹果和微信单选一,你选谁?
  9. 下载免费的中文字体及生成jsPDF需要的js文件
  10. java面向对象--继承与多态