STM32 -- USB CDC 虚拟串口通信

本篇操作:

  • 通过CubeMX + Keil,配置STM32作为USB设备端,与电脑上位机进行通信(CDC);
  • 通用带USB功能的 STM32 芯片 (如F1、F4等,系统时钟配置不同,代码通用)。

目录

一、 STM32内置USB、虚拟串口简述

二、CubeMX 新建工程

三、Keil 工程配置

四、实现USB模拟插拔

五、发送

六、发送优化(连续发送)

七、接收

八、接收优化(在外部处理数据)


一、 STM32内置USB、虚拟串口简述

STM32 芯片,绝大部分型号都带内置USB,如常用的 F1、F4、H7、G4 等系列,能够通过USB接口与计算机或其他USB设备进行通信。

STM32内置的USB,均可支持USB 2.0标准,可以支持三种传输速率:

  1. 高速模式:最高可达480 Mbps  (部分型号支持,且需搭配外部芯片,不常用 )
  2. 全速模式:最高可达12 Mbps      (最常用)
  3. 低速模式:最高可达1.5 Mbps     

高速模式,需要搭配外围USB PHY芯片,如USB3300,硬件成本偏高 。
全速模式,电路很简单。从机在PCB布线时,仅需把STM32的引脚PA11、PA12,  连接至USB座的DP、DM,然后,PA12(DP线)用1.5K电阻上拉至3.3V。具体如下图:

上拉说明

插拔检测:设备未插入时,主机端DP、DM为低电平,当发现被置高,即为有设备插入;

区分速率:DM线上拉是低速模式,DP线上拉是全速\高速模式;

上拉电压:3.3V。USB通信电平是3.3V,而不是总线供电的5V。

USB虚拟串口,简称VPC,Virtual Port Com 的简写。但更习惯于把虚拟串口叫作: CDC,因为它是利用 USB 的 CDC类 实现的一种通信接口。

我们可以利用STM32自带的USB功能,通过CubeMX的配置,很方便地实现一个USB虚拟串口,从而通过USB线,实现电脑与STM32的数据互传。

哪些win系统支持虚拟串口?

Win10、Win11 已带虚拟串口驱动;无需安装任何驱动; 

Win7 要提前手动安装驱动,否则无法识别 :虚拟串口驱动 下载


二、CubeMX 新建工程

本篇为了工程的清晰,将从0开始, 新建一个虚拟串口通信的工程。

日常做项目,不建议新建,而是复制已有的旧工程,通过CubeMX增删需要的功能。

这样能减少一些常用功能的再次配置,如按键、UART等;

复用旧工程里已验证过的功能,能有效地减少常用功能的调试时间。

1、以芯片型号新建

2、搜索芯片型号

3、设置调试模式

进入配置页面后,养成习惯,优先设置调试模式:Serial Wire。

4、选择晶振源

外部高速晶振源(HSE):Crystal/Ceramic Resonator

5、USB工作模式

USB_OTG_FS:选择 Device_Only;  设备模式(从机模式);   其它参数,默认。

有些芯片型号,如F103系列,CubeMX上的显示是:USB_FS,配置步骤是一样的。

6、中间件组件

USB_DEVICE:选择 CDC (VPC);  其它参数,默认;

6、配置系统时钟

① 当启用USB功能后,进入时钟配置页面时,弹窗: 是否自动配置系统时钟?  选择:No !

② 先确认板上的晶振值

  • 配置时钟前,很重要的一个事:先核对开发板上的晶振频率(在晶振上的数字)!
  • 晶振频率配置错误时,编译不会报错,但系统可能不运行、通信错乱等,后期排查很费时间!
  • 目前STM32的板子,常用的外部高速晶振有三种:8M、12M、25M。

③ STM32F103 时钟配置

晶振值输入分频输出倍频USB分频APB1分频APB2分频系统 时钟
8191.52172MHz

④ STM32F4xx 时钟配置

注意:F4系列,各板商略有不同,大部分是25M, 少部分是8M,使用效果一样。

晶振值输入分频输出倍频输出分频USB分频APB1分频APB2分频系统时钟
25253362742168MHz

7、工程配置

  • 工程名称、路径,这两项,必须英文。否则,生成的工程将会缺少启动文件
  • 开发工具:MDK-ARM,  即生成Keil工程 
  • 堆大小,建议:0x400
  • 栈大小,建议:0x1000

8、文件和代码的配置

9、生成

稍等 片刻:

生成的工程文件夹:

Keil工程的入口文件:


三、Keil 工程配置

按上述,双击打开Keil工程。

1、新建的工程,需要设置一次仿真器参数 。(点击 OK 保存,否则无效

2、配置常用的调试选项

下面这两项是非必要的,建议打勾使用;编译后生效; 打勾会令编译速度变慢;

  • Debug Infomation: 生成调试信息。debug模式中无法设置断点,就是这个选项没打勾。
  • Bowse Infomation: 生成追踪信息。如,右击函数、变量,点击弹出菜单:Go To Definition...

 3、编译 验证

  • 0 Error,正程正常。
  • 有 Error,失败;应该是 (2-7) 那一步工程名称、路径有中文。修改后重新生成即可。
  • 先别烧录,别烧录,别烧录。


四、实现USB模拟插拔

通过 CubeMX 配置后生成的工程,它已带需要的初始化代码、配置代码、基础函数等。

我们只需在工程里,按需进行简单的配置、修改代码,即可使用。

 1、包含 USB接口 的头文件

  • 打开 main.c文件,大约第26行,配对的 /* USER CODE ...... Includes */ 注释之间,
  • 添加:#include  "usbd_cdc_if.h"

完成后,是这个样子的:

2、增加 USB模拟插拔

我们在调试STM32程序期间,需要反复地 修改程序、编译、烧录;  这是常规操作,用于调试其它通信模块,如DHT11、ESP8266等,是没有问题的,但用于调试USB的通信,就会翻车。

当虚拟串口所用的USB线一直插在USB口上,程序重新烧录后,程序的重新运行,将导致通信错误、USB端口"假死"等现象; 

上文中已介绍,电脑端USB口没有插入设备时,DP和DM线,是低电平状态,而设备端的DP线,有1.5K电阻上拉到3.3V,当设备插入到电脑USB口,USB口的DP线就会被置高电平,主机是依靠这个机制判断设备是否插入、拔出,继而触发不同的动作,如枚举、释放端口等。

当虚拟串口所用的USB线一直插在USB口上,在STM32烧录程序重新运行后,程序里的USB代码等待着主机方发起枚举过程;而这个期间虚拟串口的USB线没有断开,主机方认为设备方一直在线,早已枚举成功,一直对其轮询数据收发。双方“南辕北辙,胡言乱语”,注定翻车。

正常操作是:每次烧录前,先把虚拟串口的USB线拔下来,烧录好了,再插上,......。

为了避免调试期间频繁地手动操作,我们可以在程序开跑后、USB初始化前,用代码把PA12置低,使D+线为低电平,持续一段时间,模拟USB拔出动作,令主机认为设备已断开连接,释放端口;
然后,当程序运行到后面的USB初始化函数时,PA12会被正常配置(DP线电平被置高),USB主机就会"发现"有设备插入,开始尝试枚举、配置;

具体操作:

  • 打开 usbd_conf.c 文件,大约第70行附近,找到HAL_PCD_MspInit ( )函数;
  • 在两对 /* USER ... 0 */ 注释之间,约75行,添加PA12引脚置低电平操作,  如下(可复制):
    __HAL_RCC_GPIOA_CLK_ENABLE();                   // 使能GPIOA端口GPIO_InitStruct.Pin = GPIO_PIN_12;              // 引脚PA12, 即D+GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;     // 引脚工作模式GPIO_InitStruct.Pull = GPIO_PULLDOWN;           // 下拉GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;    // 引脚反转速度HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);         // 初始化HAL_Delay(5);                                   // 持续片刻

注意,最后一行的延时,是必须的,建议在5ms左右;

完成后,是这个样子的:

增加这段代码后,再无需手动插拔 虚拟串口的USB 线了,程序将模拟 “ 断开、插入”;

再次编译,确保上述操作正常 (先别烧录)。


五、发送

发送数据的函数;

uint8_t  CDC_Transmit_FS ( uint8_t* Buf,  uint16_t Len );

        函数接受两个参数:数据缓冲区的地址、字节数。

        如果USB设备正忙,它会返回USBD_BUSY状态。

        这个函数的作用是设置传输数据的缓冲区,并标记数据包为待发送。数据并非立刻发出,而是被存储在USB外设的缓冲区中,等待主机轮询请求传输。

发送操作:

  • 在main.c 的 while 循环中,添加三行测试代码:1行延时、两行发送数据;

注意:是共三行,1行间隔延时,2行发送,下面会解释具体原因!

新手,要省时间,就请按步骤操作 :

  • 先别插虚拟串口所用的USB线
  • 编译、烧录代码
  • 打开串口助手, (这时是没有插虚拟串口USB线的),查看目前有哪些端口号
  • 插入虚拟串口的USB线,到开发板的 USB-Slave接口 
  • ( 前提:win10、11系统已带虚拟串口驱动; win7要手动安装驱动:虚拟串口下载)
  • ( 电脑会自动识别到设备;如果是第一次使用虚拟串口,电脑将自动安装驱动程序)
  • 检查串口助手,发现多了一个端口号, 选择它,波特率等参数不用修改,打开端口;

接线,如下图(示例所用的开发板):

  • 左侧为用户USB接口,已连接PA11、PA12,我们就是用这个U口实现虚拟串口通信。
  • 右侧是板载仿真器CMSIS DAP的接口,它自带了USB转TTL功能。

注意端口的选择:

        当使用的开发板,已带USB转TTL功能,如上面这个。在烧录虚拟串口的程序后,串口助手会有至少两个端口号:板子自带的USB转TTL(右侧)、程序实现的虚拟串口(左侧),注意不要选择错了。

        如果不知道哪根线对应哪个端口:在烧录后,拔一下USB线,看看哪个端口消失了。

        另一方法,有些串口助手的端口列表,能显示设备信息,找到带“STM...”描述的那个。

现在能看到,串口助手能接收到程序持续发出的数据了!

效果如下图:


六、发送优化(连续发送)

上节的发送,实现时,只能收到第一行"Hello",而第二行发出的数据:"借点钱 "却没收到!!

不是没收到。其实,从STM32程序的角度,是没有发出数据!

先说说,USB虚拟串口通信的几个重点 (特指:USB2.0、全速模式、中断传输):

  • USB是轮询机制,主机对设备不断轮询,间隔最小1ms;不是固定的1ms,  是最小间隔时间;
  • USB的数据,是按包传输的; 
  • 每个设备,每1ms,最多传输1包数据;
  • 每包最多64字节(有效负载);

再说说,CDC_Transmit_FS ( ) 函数:

  • 它的第2个参数,"字节数",范围:0~2048; 这个2048可以在CubeMX里进行设置大小; 
  • 字节数 <= 64,算1包。如:发3个字节,也算1包。
  • 字节数 == 0,也算1包。俗称:空包; 如果上一帧刚好发送64字节,再发一个空包作为结束包;
  • 字节数 > 64, CDC_Transmit_FS ( ) 背后有缓存,它自动分包,1ms左右发1包,直至发完;
  • 如果上一包还没发完,再次调用CDC_Transmit_FS ( ) ,将放弃本次调用。

上面while循环中,连续、两次调用CDC_Transmit_FS ( ) .

  • 第1次调用,将正常发出一包数据;
  • 第2次调用,再发送一包。但是,没有间隔1ms以上,导致了第2次的发送,被舍弃了。

我们先打开 CDC_Transmit_FS ( ) 函数,看看函数原型。

在代码中,右击CDC_Transmit_FS ( ) ,弹出菜单,选择Go To Definition...,将跳转到函数位置;

或者,在左侧文件树中,双击打开usbd_cdc_if.c 文件 ,CDC_Transmit_FS ( ) 位于大约280行 ;

特别注意:

usbd_cdc_if.c 是应用层文件,我们对收、发有啥特殊需求,通过修改文件里的发送函数、接收回调函数、类请求函数,基本都能实现。

下图,是 CDC_Transmit_FS ( ) 函数截图。

红框的内容:当设备忙时,直接放弃发送,:

修改:

  • 注释掉 if 体3行代码;
  • 增加等待发送空闲、判断超时,如下6行;
  • 整个CDC_Transmit_FS ( ) 函数,如下:(可复制)
uint8_t CDC_Transmit_FS(uint8_t *Buf, uint16_t Len)
{uint8_t result = USBD_OK;/* USER CODE BEGIN 7 */USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)hUsbDeviceFS.pClassData; // 获得设备的状态信息结构体// if (hcdc->TxState != 0){// return USBD_BUSY;// }uint32_t timeStart = HAL_GetTick();while (hcdc->TxState){if (HAL_GetTick() - timeStart > 20)return USBD_BUSY;}USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);/* USER CODE END 7 */return result;
}

完成后,是这个样子:

再次编译、烧录程序。

串口助手,现在是这个样子的:

连续发送已实现了。

上述方法能够解决连续发送失败的问题。

但它存在一个显著缺点:由于其阻塞性质,频密连续发送时,将导致运行“死等”,影响程序效率。

(对于大部分场景,上述方法已足够。本节内容,只是为你预埋一种备用思路,无需死磕。)

如果项目对实时性有较高的要求,可以通过结合使用发送数据函数 CDC_Transmit_FS() 和发送完成回调函数 CDC_TransmitCplt_FS() 来提高传输效率。CDC_TransmitCplt_FS() 会在 CDC_Transmit_FS() 函数发送数据完毕后自动被调用。

根据这两个函数的特点,可以设计一套高效的发送缓存机制。例如,可以维护一个发送队列,当 CDC_Transmit_FS() 完成发送后,CDC_TransmitCplt_FS() 被调用时,从队列中取出下一个数据项进行发送,这样可以确保数据传输的连续性。

这个发送完成回调函数 CDC_TransmitCplt_FS() ,位于发送函数 CDC_Transmit_FS() 的正下方。

至于具体的代码实现,不同项目需求各异,无法提供一个通用的解决方案。因此,需要根据具体的项目需求,进行针对性的设计和优化,不能一药治百病。


七、接收

1、接收方式的简述

当USB CDC接收到来自USB主机的数据时,触发中断进入中断函数,继而自动调用接收回调函数:

int8_t  CDC_Receive_FS (uint8_t* Buf, uint32_t *Len);  
  • uint8_t* Buf:   指向接收缓冲区的指针,即数据缓存的地址。
  • uint16_t* Len: 当前数据包的字节数。

我们就在这个回调函数里,处理接收到的数据!

它在 usbd_cdc_if.c 文件,位于发送函数的正上方;

函数内部,生成的代码里,只有2行执行代码,指定下次接收的存放位置;  如下图所示:

而本次所接收到的数据,该如何处理,需要我们自行添加代码。

注意事项:

  • 每当接收到一包数据,硬件自动触发中断函数, 继而调用此接收回调函数,无需人工调用。
  • 与发送机制相似,每间隔1ms,最多接收1包数据,每包最大64字节。。
  • 如果需要接收超过64字节的数据帧,注意,指上位机发送的1个完整数据帧,而非USB的单包数据,如,上位机发来一张图片数据,8350个字节,则需要在此回调函数中添加额外的代码来判断帧数据传输完整结束 、手动将多个数据包拼接成完整的数据帧。
  • 接收到数据时,缓存不会提前自动清零,新数据从Buf的起始位置开始,覆盖存放。
  • 由于该回调函数是被中断函数调用的,因此建议函数内部的处理尽可能地简短,以避免影响系统的实时性(中断函数运行期间,会令程序持续挂起)。

2、接收示范

本节将示范:

  • 通过串口助手,发送字符串 
  • STM32(设备方)收到数据后,把收到的字节数、字符串,发回串口助手显示(主机方)

在函数内的注释行  /* USER CODE BEGIN */  下方,添加4行自定义代码(可复制)。

    char myStr[64] = {0};                                                     // 定义一个数组,用于存放要输出的字符串sprintf(myStr, "\r\r收到 %d 个字节;\r内容是:%s\r\r", *Len, (char *)Buf); // 格式化字符串CDC_Transmit_FS((uint8_t *)myStr, strlen(myStr));                         // 发送memset(Buf, 0, 64);                                                       // 处理完数据,清0接收缓存;

注意:

  • 用char 声明myStr[ ], 是因为此处想把它作为一段字符串空间;
  • sprintf是C语言标准输入输出库的函数,如果报错没有这个函数,就:#include <stdio.h>
  • 获取字节数,是*Len,而不是Len;  因为它在函数参数里的声明,是一个指针;
  • 为了格式化成字符串,Buf用了(char*)进行强制转换成字符类型; 
  • CDC_Transmit_FS( )里,用了strlen获取字符串的字节数,它只对字符串有效,对其它数据类型无效;  如果报错没有这个函数,就:#include  <string.h>
  • 如果传输的是16进制数,用uint8_t 声明上面数组,然后修改sprintf的格式化方式。

添加完成后,文件是这样子的:

再次编译、烧录程序。

串口助手,打开对应的端口号(波特率等参数不用修改),

在发送区,以ASCII方式,发送字符串(因为添加的代码里用%s格式化,处理的是字符串),

然后,串口助手的接收区,马上能接收到刚才发出的数据!

至此,已实现接收的处理。


八、接收优化(在外部处理数据)

上面,我们已实现:获取、使用接收到的数据。

在接收回调函数中,直接操作数据的发送,通常是安全的,因为这种操作耗时非常短,最多等待1次主机轮询周期(1ms)。这种情况下,不会对系统的稳定性和数据接收造成显著影响。

但是,如果在接收回调函数中执行耗时较长的操作,如显示到LCD或存储到Flash等,这些操作可能需要数毫秒到数十毫秒才能完成。耗时较长的操作,可能会导致接收过程中出现漏包现象。

因为接收回调函数是由USB中断服务程序调用的,属于中断处理的一部分,在回调函数执行期间,主程序还处于中断挂起状态,其他代码和中断也会被暂停执行,形象地描述:“卡死”。

如果中断服务程序的执行操作较耗时,会导致下一包数据无法及时进入中断,从而造成数据丢失。

举例说明:

  • A (主机)每隔1ms扔出1枚鸡蛋,B(中断服务函数)负责接鸡蛋。
  • 当B处置鸡蛋的时间占时极短(只要比A扔出的间隔更短,如0.5ms),那,没问题。
  • 当B处置鸡蛋的时间较长,接了鸡蛋还要写上价格,再放置到货架,共20ms, 那肯定就接不住A持续扔过来的鸡蛋了,每接1个,就会丢失后面的19个,再接1个,再丢失19个.......

我们需要采取一种策略,使得程序的中断响应、“卡死”占时,尽可能地短:

  • 接收数据:在中断回调函数中,我们仅执行必要的数据复制操作,即把接收到的数据迅速复制到外部缓存中。这一操作的耗时通常在us级别;
  • 处理数据:在主程序的while循环中,我们再对数据进行进一步的处理。由于这一处理过程不占用中断资源,因此不会影响程序对新数据的接收;

这种策略通过分离数据接收和数据处理两个步骤,确保了程序能够快速响应连续的数据流,同时避免了因处理时间过长而导致的数据丢失。

操作共4个步骤,具体如下:

1、增加全局变量

在usbd_cdc_if.c文件大约97行,配对的注释内,定义两个变量:

/* USER CODE BEGIN PRIVATE_VARIABLES */
uint8_t  myUsbRxData[64] = { 0 };   // 接收到的数据
uint16_t myUsbRxNum = 0;            // 接收到的字节数/* USER CODE END PRIVATE_VARIABLES */

现在,它俩只是本地变量,等会要在main中用extern再声明一次,才能被外部调用。

完成后,是这个样子的:

2、修改接收回调函数

在CDC_Receive_FS() 里,删除我们上节增加的测试代码;

把Buf和*Len的数据,复制到我们刚才的两个变量里。函数修改成:

static int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)
{/* USER CODE BEGIN 6 */// 把Buf里面的数据,复制到外部缓存memset(myUsbRxData, 0, 64);                     // 清0缓存区 memcpy(myUsbRxData, Buf, *Len);                 // 把接收到的数据,复制到自己的缓存区中myUsbRxNum = *Len;                              // 复制字节数    memset(Buf, 0, 64);                             // 处理完数据,清0接收缓存;                       // CubeMX生成的代码,保留    USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);   // 设置下-个接收缓冲区USBD_CDC_ReceivePacket(&hUsbDeviceFS);          // 启动下一个数据包的接收return (USBD_OK);                               /* USER CODE END 6 */
}

完成后,是这个样子的:

现在,数据接收部分,已处理好了。

以后回调函数运行时,只复制数据至外部缓存(备用),中断时间占用极短,不会影响下包接收。

3、 在外部用extern声明变量,令外部可调用数据

外部,哪个文件里要使用CDC接收的数据,就在这个文件里,用extern声明那俩变量。

如,可以在LCD文件,也可以在SD卡的文件中,都行。

建议在main.h文件中声明,其它文件再#include "main.h",这样,可以令变量全局可用。

  • 打开 main.c,右击空白,点击"Toggle Header/Code File",可以跳转到头文件:main.h  

在main.h中,大约38行,找到 配对的注释行 /* USER CODE BEGIN ET */

用 extern 再次声明刚才两个变量。如下(可复制):

注意,是只声明,不要赋值,否则编译错误。

/* USER CODE BEGIN ET */
extern uint8_t myUsbRxData[ ] ;
extern uint16_t myUsbRxNum ;/* USER CODE END ET */

完成后,是这个样子的:

代码规范:
        这里的示范,使用全局变量,只是为了更清晰地演示操作思路。

        项目中,尽量避免使用全局变量;不同文件间的数据获取,可以封装成函数,如 CDC_GetRxData()、CDC_GetRxNum(),返回数据地址、接收的字节数。    

4、使用接收到数据

在main.c的while循环中,通过判断myUsbRxNum的值,只要大于0,就表示收到数据了

记得每次处理完数据,把myUsbRxNum置0,以便于下一轮的判断。

再次烧录,烧录程序。

打开串口助手,发送测试文本,可以发现,能成功收到STM32发过来的回传数据。

        如果,外部处理数据的速度跟不上,如,在while里每次收到数据都要显示到LCD,LCD的速度远慢于USB的传输,那,还不是变相丢了数据?!是的,会有这种情况!

        但,那已经是程序逻辑和时间片机制的问题了,3天3夜也嗑不完!

        本节只讨论:确保每一包数据,都能被正常接收到。外部能否及时处理,不述。

至此,本篇完结。

如有错漏,望留言指正,及时更新!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python中双引号和单引号的区别是什么

python3中的单引号‘’和双引号“”的作用一样。 3个单引号的作用&#xff1a; 1、表示注释 #3个单引号表示注释多行gf_of_archerzon "Wang" print("archerzon的女盆友是",gf_of_archerzon) print("archerzon的女盆友是%s"%gf_of_archerzon)2…

Vue基础(三)

生命周期 又名生命周期回调函数&#xff0c;生命周期函数&#xff0c;生命周期钩子 是什么&#xff1a;Vue在关键时刻帮我们调用的一些特殊名称的函数 生命周期函数的名字不可更改&#xff0c;但函数的具体内容是程序员根据需求编写的 生命周期函数中的this指向是vm或者组件实…

Leetcode - 周赛418

目录 一&#xff0c;3309. 连接二进制表示可形成的最大数值 二&#xff0c;3310. 移除可疑的方法 三&#xff0c;3311. 构造符合图结构的二维矩阵 四&#xff0c;3312. 查询排序后的最大公约数 一&#xff0c;3309. 连接二进制表示可形成的最大数值 本题数据范围较小&#…

鼓组编写:SsdSample鼓映射 GM Map 自动保存 互换midi位置 风格模板 逻辑编辑器

SsdSample音源的键位映射 方便编写鼓的技巧 可以这样去设置键位关系的面板和钢琴卷帘窗的面板&#xff0c;方便去写鼓。 可以先按GM的midi标准去写鼓&#xff0c;然后比对下鼓的键位映射的关系&#xff0c;去调整鼓。 可以边看自己发b站等处的图文笔记&#xff0c;然后边用电…

网络初识基本概念总结

网络发展背景 经历了 单机阶段 -> 局域网阶段 -> 广域网阶段 -> 移动互联网阶段 (简单介绍一下) 其他一些小概念 局域网LAN: 是把一些设备通过交换机 / 路由器连接, 形成的私有网络广域网WAN: 是把更多的局域网相互连接起来,当规模足够大时形成广域网交换机和路由器…

STM32F103ZET6 FREERTOS 双UART 多任务多串口输出(配置教程)

基本的stm32cubemx使用就不细说了&#xff0c;要想配置freertos&#xff0c;用这个工具配置那是相当方便和简单 1、系统晶振配置 使用外部时钟晶振&#xff0c;配置如图 2、系统定时器设置 serial wire 保证下次可以程序下载 SysTick 是 Cortex-M 内核中的一个系统定时器&a…

用C++编写信息管理系统(歌单信息管理)

C语言是面向过程的编程语言&#xff0c;而C是面向对象的编程语言&#xff0c;在书写代码时风格有所不同&#xff08;也存在很多共性&#xff09;。 程序说明 本次系统程序使用的是C语言进行编写&#xff0c;主要考虑怎么实现面向对象的问题。 因为本次程序属于小型系统程序&…

C语言 | 第十六章 | 共用体 家庭收支软件-1

P 151 结构体定义三种形式 2023/3/15 一、创建结构体和结构体变量 方式1-先定义结构体&#xff0c;然后再创建结构体变量。 struct Stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 }; struct Stu stu1, stu2; //…

从二维到三维,电商行业有哪些变化?

从二维到三维&#xff0c;电商行业经历了一系列显著的变化&#xff0c;这些变化不仅体现在商品展示的方式上&#xff0c;还深刻影响了消费者的购物体验、电商平台的运营策略以及整个电商行业的竞争格局。 一、商品展示方式的变革 二维展示阶段&#xff1a; 在电商行业的早期&…

【黑苹果】记录MacOS升级Sonoma的过程

【黑苹果】记录MacOS升级Sonoma的过程 一、硬件二、提前说明三、准备OC四、选择驱动五、选择ACPI六、下载内核扩展七、其他问题 一、硬件 设备是神舟zx6-ct5da 具体参照下图 二、提前说明 本机器已经安装过 macOS Monterey 12.6&#xff0c;这次是升级到 macOS Sonoma 14。 …

Java后端面试题(day16)

目录 java常见的引用类型java中深拷贝和浅拷贝如何设计一个秒杀系统?谈一下对高并发的理解&#xff0c;平时怎么处理高并发问题?Comparable和Comparator区别&#xff1f;解决hash冲突有哪些方法&#xff1f;Synchronized锁的升级过程 java常见的引用类型 java的引用类型一般分…

图论day56|广度优先搜索理论基础 、bfs与dfs的对比(思维导图)、 99.岛屿数量(卡码网)、100.岛屿的最大面积(卡码网)

图论day56|广度优先搜索理论基础 、bfs与dfs的对比&#xff08;思维导图&#xff09;、 99.岛屿数量&#xff08;卡码网&#xff09;、100.岛屿的最大面积&#xff08;卡码网&#xff09;&#xff09; 广度优先搜索理论基础bfs与dfs的对比&#xff08;思维导图&#xff09;&…

C++调试方法(Vscode)(一) ——本地调试

初学者在调试一段代码的时候&#xff0c;经常出于不明原因&#xff0c;写出bug&#xff0c;导致程序崩溃。但是定位崩溃的地方时&#xff0c;往往采用简单而朴素的方法&#xff1a;即采用cout或者printf进行输出。这种方式既原始&#xff0c;又低效。一个合格的工程师应该是通过…

RabbitMQ简介及安装类

RabbitMQ概述-MQ介绍 RabbitMQ是一个开源的消息代理和队列服务器&#xff0c;它支持多种消息协议&#xff0c;并且可以轻松地与多种编程语言和框架集成。RabbitMQ是使用Erlang语言编写的&#xff0c;因此它具有高并发和高可用性的特点。以下是RabbitMQ的一些关键特性和概念 消息…

华为OD机试 - 区间交叠问题 - 贪心算法(Python/JS/C/C++ 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试真题&#xff08;Python/JS/C/C&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加入华为OD刷题交流群&#xff0c;…

Django的请求与响应

Django的请求与响应 1、常见的请求2、常见的响应3、案例 1、常见的请求 函数的参数request是一个对象&#xff0c;封装了用户发送过来的所有请求相关数据。 get请求一般用来请求获取数据&#xff0c;get请求也可以传参到后台&#xff0c;但是传递的参数显示在地址栏。 post请求…

【CSS3】css开篇基础(2)

1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; Hello, Hello~ 亲爱的朋友们&#x1f44b;&#x1f44b;&#xff0c;这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章&#xff0c;请别吝啬你的点赞❤️❤️和收藏&#x1f4d6;&#x1f4d6;。如果你对我的…

el-date-picker设置只有某些日期可选

示例图&#xff1a; <el-date-pickerv-model"topFormObj.upTime"type"date"value-format"timestamp"format"dd/MM/yyyy":picker-options"pickerOptions" /> 固定限制每周的周末周三不可选 data() {return {pickerOp…

[Python学习日记-46] Python 中第三方开源模块的安装、使用与上传自己写的模块

[Python学习日记-46] Python 中第三方开源模块的安装、使用与上传自己写的模块 简介 下载与安装 如何使用安装好的第三方开源模块 如何上传自己写的模块到 PyPi 简介 在前面的模块介绍和导入当中主要介绍的都是 Python 内置的一些模块&#xff0c;我们把它称为标准库&#…

【多版本并发控制(MVCC)】

并发事务问题&#xff1a; MySQL隔离级别-未提交读&#xff0c;提交读&#xff0c;可重复读&#xff0c;序列化 隔离级别对于并发事务的解决情况 隔离级别脏读不可重复读幻读未提交读不可不可不可读已提交可不可不可可重复读 &#xff08;默认&#xff09;可可不可串行化&…