当前位置: 首页 > article >正文

【江协STM32】9-4/5 USART串口数据包、串口收发HEX数据包串口收发文本数据包

1. 数据包 

把一个个单独的数据打包,方便进行多字节的数据通信。

例如陀螺仪传感器,需要用串口发送数据到STM32。对于陀螺仪的数据,假设X、Y、Z轴各为一个字节,共计3个数据需要连续不断地发送。如果像XYZXYZ...连续发送时,接收方无法区分X、Y和Z轴的数据,因为接收方可能会从任意位置开始接收,所以会出现数据错位的现象。因此,需要将数据进行分割,把XYZ一批数据分开,分成一个个数据包。

数据包分割方法有多种,串口数据包通常使用的是额外添加包头包尾的方式。

1.1 HEX数据包 

包头包尾和数据载荷重复——这里定义FF为包头,FE为包尾,如果传输的数据本身就是FF和FE,有如下几种解决方法:

  1. 限制载荷数据的范围,避免和包头包尾重复;
  2. 如果无法避免载荷数据和包头包尾重复,尽量使用固定长度的数据包;
  3. 增加包头包尾的数量,并且使之尽量呈现出载荷数据出现不了的状态,比如使用FF、FE作为包头,FD、FC作为包尾

包头包尾并不是全部都需要的,比如可以只要一个包头,把包尾删掉。这样数据包的格式就是,一个包头FF加4个数据。当检测到FF开始接收,收够4个字节后,置标志位,一个数据包接收完成。不过这种情况下载荷和包头重复的问题会更加严重。

固定包长和可变包长的选择——对应HEX数据包来说,如果载荷会出现和包头包尾重复的情况,最好选择固定包长,这样可以避免接收错误。 

1.2 文本数据包

在HEX数据包中,数据均以原始字节数据呈现。在文本数据包中,每个字节经过了一层编码和译码,最终表现出来的就是文本格式。 

1.3 固定包长HEX数据包的接收

根据之前的代码,我们知道,每收到一个字节,程序都会进一遍中断,在中断函数中获取到这一字节,但获取之后就需要退出中断,所以,每获取到一个数据都是一个独立的过程。而对于数据包来说,很明显它具有前后关联性,包头之后是数据,数据之后是包尾。对于包头、数据和包尾这3种状态,需要有不同的处理逻辑,所以需要在程序中设计一个能记住不同状态的机制,在不同状态执行不同的操作,同时还要进行状态的合理转移,这种程序设计思维就叫做“状态机”。 

这里使用状态机的方法来接收一个数据包。对于上面这样一个固定包长HEX数据包来说,可以定义三个状态“等待包头、接收数据和等待包尾”,每个状态需要用一个变量标志,可以标志三个状态依次为"S=0、S=1和S=2"。执行的流程是

  1. 最开始S=0,收到一个数据后进入中断,根据S=0进入第一个状态的程序,判断数据是否为包头FF。如果是FF,则代表收到包头,之后置S=1,退出中断,结束。 这样下次再进入中断,根据S=1,就可以进行接收数据的程序。如果在第一个状态收到的不是FF,就证明数据包没有对齐,需要等待数据包包头的出现,这时状态仍然为0,下次进入中断还是判断包头的逻辑,直到出现FF才能转到下一个状态。
  2. 收到FF进入接收数据状态S=1。此时再收到数据就直接保存在数组中,另外再使用一个变量,用来记录接收数据的个数。如果未收够4个数据,就一直是接收状态,如果收够了,就置S=2,下次中断时即可进入下一个状态。
  3. 最后一个状态为等待包尾,判断数据是否是FE。正常情况下应该为FE,这样就可以置S=0,回到最初的状态,开始下一个轮回。也有可能这个数据不是FE,比如数据和包头重复,导致包头位置判断错误,此时就可以进入重复等待包尾的状态,直到接收到真正的包尾。

1.4 可变包长文本数据包的接收

同样也是利用状态机,定义3个状态。

  1. 等待包头S=0。判断收到的是否为规定的@符号。
  2. 接收数据等待包尾S=1。因为是可变包长,所以此状态需要兼具等待包尾的功能。收到一个数据,判断是否为\r。如果不是,则正常接收,如果是,则不接收,同时跳到下一个状态。
  3. 等待包尾S=2。因为数据包有两个包尾\r、\n,所以需要第三个状态等待包尾\n。如果只有一个包尾,那么出现包尾之后就可以直接回到初始状态了,只需两个状态即可,因为接收数据和等待包尾需要在一个状态内同时进行。

2. 串口收发HEX数据包 

2.1 接线图

按键按一下,发送一次数据包。同时在OLED上显示发送的数据包和接收的数据包。

2.2 代码

在“9-2 串口发送+接收”的程序上修改。

HEX数据包格式:固定包长,含包头包尾,其中包头为FF,包尾为FE,载荷数据固定4字节。

Serial.c

#include "stm32f10x.h"                  // Device header
#include <stdio.h>

//  为了收发数据包,先定义两个缓存区的数组
uint8_t Serial_TxPacket[4];//   4个数据只存储发送或接收的载荷数据
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;//    收到一个数据包,置RxFlag

void Serial_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//	复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//	上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;//    波特率
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//   不使用流控
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//   同时开启发送和接收
    USART_InitStructure.USART_Parity = USART_Parity_No;//   校验位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;//    停止位
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;//   字长。不需要校验,所以选8位即可
    USART_Init(USART1, &USART_InitStructure);
    
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//    开启RXNE标志位到NVIC的输出
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitSturcture;
    NVIC_InitSturcture.NVIC_IRQChannel = USART1_IRQn;//    中断通道
    NVIC_InitSturcture.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitSturcture.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitSturcture);
    
    USART_Cmd(USART1, ENABLE);
}

//  发送字节
void Serial_SendByte(uint8_t Byte)
{
    USART_SendData(USART1, Byte);// 调用此库函数,Byte变量就写入TDR了,写完后需要等待TDR数据转移至移位寄存器。如果数据在TDR内再写入数据,会产生数据覆盖
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//  发送数据寄存器(TDR)空标志位
    //  这里标志位置1后不需要手动清零,当下一次再SendData时,此标志位会自动清零
}

//  发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)// 指向待发送数组的首地址。由于数组无法判断是否结束,所以需要传递一个Length进来
{
    uint16_t i;
    for(i = 0; i < Length; i++)
    {
        Serial_SendByte(Array[i]);
    }
}

//  发送字符串
void Serial_SendString(char *String)//  由于字符串自带一个结束标志位,所以不需要传递长度参数
{
    uint8_t i;
    for(i = 0; String[i] != 0; i++)//   循环条件用结束标志位来判断。这里数字0对应空字符,是字符串的结束标志位。如果不等于0,就是还没结束,进行循环
                                   //   这里数据0也可以写成字符形式,就是'\0',这就是空字符的转义字符表示形式for(i = 0; String[i] != '\0'; i++),和直接写0最终效果是一样的
    {
        Serial_SendByte(String[i]);
    }
}

//  计算次方函数
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1;
    while(Y--)
    {
        Result *= X;
    }
    return Result;
}

//  发送数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
    //  需要把Number的个位、十位、百位等以十进制拆分开,然后转换成字符数字对应的数据,依次发送
    uint8_t i;
    for(i = 0; i < Length; i++)
    {
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');//    最终要以字符的形式发送,所以最后要加上字符的偏移,根据ASCII码表,字符0对应的数据是0x30,也可以以字符的形式写'0'
    }
}

int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);
    return ch;
}

//  调用此函数后,TxPacket数组的4个数据会自动加上包头包尾发送出去
void Serial_SendPacket(void)
{
    Serial_SendByte(0xFF);//    发送包头
    Serial_SendArray(Serial_TxPacket, 4);// 发送载荷数据
    Serial_SendByte(0xFE);//    发送包尾
}

//  实现Serial_RxFlag标志位读后自动清除
uint8_t Serial_GetRxFlag(void)
{
    if(Serial_RxFlag == 1)
    {
        Serial_RxFlag = 0;
        return 1;
    }
    return 0;
}

//  中断函数。用状态机执行接收逻辑
void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0;//   标志当前状态的变量S
                               //   这个静态变量类似于全局变量,函数进入只会初始化一次0,在函数退出后数据仍然有效
    static uint8_t pRxPacket = 0;// 指示接收到第几个数据
    if(USART_GetFlagStatus(USART1, USART_IT_RXNE) == SET)
    {
        uint8_t RxData = USART_ReceiveData(USART1);
        
        if(RxState == 0)//  等待包头
        {
            if(RxData == 0xFF)//    收到包头
            {
                RxState = 1;//  转移状态RxState=1
                pRxPacket = 0;//    清零
            }
        }
        else if(RxState == 1)// 接收数据
        {
            Serial_RxPacket[pRxPacket] = RxData;
            pRxPacket++;
            if(pRxPacket >= 4)
            {
                RxState = 2;//  转移状态RxState=2
            }
        }
        else if(RxState == 2)// 等待包尾
        {
            if(RxData == 0xFE)
            {
                RxState = 0;//  回到最初的状态,同时,代表一个数据包收到了
                Serial_RxFlag = 1;//    置接收标志位1
            }
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);//   清除标志位
    }
}

Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>

extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
uint32_t Serial_Pow(uint32_t X, uint32_t Y);
void Serial_SendNumber(uint32_t Number, uint8_t Length);

void Serial_SendPacket(void);
uint8_t Serial_GetRxFlag(void);
    
#endif

main.c

#include "stm32f10x.h"                  // Device 
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"

uint8_t KeyNum;

int main(void)
{
	OLED_Init();
    Key_Init();
    Serial_Init();
    
    OLED_ShowString(1, 1, "TxPacket");
    OLED_ShowString(3, 1, "RxPacket");
    
    Serial_TxPacket[0] = 0x01;
    Serial_TxPacket[1] = 0x02;
    Serial_TxPacket[2] = 0x03;
    Serial_TxPacket[3] = 0x04;
    
	while(1)
	{
        //  按键按下后,数组数据加1发送
        KeyNum = Key_GetNum();
        if(KeyNum == 1)
        {
            Serial_TxPacket[0] ++;
            Serial_TxPacket[1] ++;
            Serial_TxPacket[2] ++;
            Serial_TxPacket[3] ++;
            Serial_SendPacket();
            
            OLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2);
            OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);
            OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);
            OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2);
        }
		if(Serial_GetRxFlag() == 1)
        {
            OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2);
            OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);
            OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);
            OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2);
        }
	}
}

其他引用的头文件和c代码可在此处查阅:OLED.h(【江协STM32】4 OLED调试工具)、 Delay.h(【江协STM32】3-2 LED闪烁&LED流水灯&蜂鸣器,第1.3节)、 Key.h(【江协STM32】3-4 按键控制LED&光敏传感器控制蜂鸣器,第1.3节)

3. 串口收发文本数据包

3.1 接线图

3.2 代码

在上节程序的基础上进行修改。

文本数据包格式:可变包长,含包头包尾,以@符号为包头,以\r\n两个符号为包尾,载荷字符数量不固定。

Serial.c

#include "stm32f10x.h"                  // Device header
#include <stdio.h>

//  为了接收数据包,先定义缓存区的数组
char Serial_RxPacket[100];//    设置单条指令最长不能超过100个字符
uint8_t Serial_RxFlag;//    收到一个数据包,置RxFlag

void Serial_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//	复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//	上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;//    波特率
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//   不使用流控
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//   同时开启发送和接收
    USART_InitStructure.USART_Parity = USART_Parity_No;//   校验位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;//    停止位
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;//   字长。不需要校验,所以选8位即可
    USART_Init(USART1, &USART_InitStructure);
    
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//    开启RXNE标志位到NVIC的输出
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitSturcture;
    NVIC_InitSturcture.NVIC_IRQChannel = USART1_IRQn;//    中断通道
    NVIC_InitSturcture.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitSturcture.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitSturcture);
    
    USART_Cmd(USART1, ENABLE);
}

//  发送字节
void Serial_SendByte(uint8_t Byte)
{
    USART_SendData(USART1, Byte);// 调用此库函数,Byte变量就写入TDR了,写完后需要等待TDR数据转移至移位寄存器。如果数据在TDR内再写入数据,会产生数据覆盖
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//  发送数据寄存器(TDR)空标志位
    //  这里标志位置1后不需要手动清零,当下一次再SendData时,此标志位会自动清零
}

//  发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)// 指向待发送数组的首地址。由于数组无法判断是否结束,所以需要传递一个Length进来
{
    uint16_t i;
    for(i = 0; i < Length; i++)
    {
        Serial_SendByte(Array[i]);
    }
}

//  发送字符串
void Serial_SendString(char *String)//  由于字符串自带一个结束标志位,所以不需要传递长度参数
{
    uint8_t i;
    for(i = 0; String[i] != 0; i++)//   循环条件用结束标志位来判断。这里数字0对应空字符,是字符串的结束标志位。如果不等于0,就是还没结束,进行循环
                                   //   这里数据0也可以写成字符形式,就是'\0',这就是空字符的转义字符表示形式for(i = 0; String[i] != '\0'; i++),和直接写0最终效果是一样的
    {
        Serial_SendByte(String[i]);
    }
}

//  计算次方函数
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1;
    while(Y--)
    {
        Result *= X;
    }
    return Result;
}

//  发送数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
    //  需要把Number的个位、十位、百位等以十进制拆分开,然后转换成字符数字对应的数据,依次发送
    uint8_t i;
    for(i = 0; i < Length; i++)
    {
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');//    最终要以字符的形式发送,所以最后要加上字符的偏移,根据ASCII码表,字符0对应的数据是0x30,也可以以字符的形式写'0'
    }
}

int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);
    return ch;
}

//  中断函数。用状态机执行接收逻辑
void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0;//   标志当前状态的变量S
                               //   这个静态变量类似于全局变量,函数进入只会初始化一次0,在函数退出后数据仍然有效
    static uint8_t pRxPacket = 0;// 指示接收到第几个数据
    if(USART_GetFlagStatus(USART1, USART_IT_RXNE) == SET)// RXNE:当RDR移位寄存器中的数据被转移到USART_DR寄存器中,该位被硬件置位。
    {
        uint8_t RxData = USART_ReceiveData(USART1);
        
        if(RxState == 0)//  等待包头
        {
            if(RxData == '@' && Serial_RxFlag == 0)//    收到包头。等待每次处理完成之后再开始接收下一个数据包,Serial_RxFlag==0才执行接收,否则就是发送太快,还没处理完成
            {
                RxState = 1;//  转移状态RxState=1
                pRxPacket = 0;//    清零
            }
        }
        else if(RxState == 1)// 接收数据
        {
            if(RxData == '\r')
            {
                RxState = 2;
            }
            else
            {
                Serial_RxPacket[pRxPacket] = RxData;
                pRxPacket++;
            }
        }
        else if(RxState == 2)// 等待包尾
        {
            if(RxData == '\n')
            {
                RxState = 0;//  回到最初的状态,同时,代表一个数据包收到了
                Serial_RxPacket[pRxPacket] = '\0';//    在字符数组最后加字符串结束标志位\0,方便对字符串进行处理。否则使用ShowString时由于没有结束标志位,无法判断字符串长度
                Serial_RxFlag = 1;//    置接收标志位1
            }
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);//   清除标志位
    }
}

Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>

extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
uint32_t Serial_Pow(uint32_t X, uint32_t Y);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
    
#endif

main.c

#include "stm32f10x.h"                  // Device 
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include <string.h> //  判断字符串时使用

int main(void)
{
	OLED_Init();
    LED_Init();
    Serial_Init();
    
    OLED_ShowString(1, 1, "TxPacket");
    OLED_ShowString(3, 1, "RxPacket");
    
	while(1)
	{
        if(Serial_RxFlag == 1)//    代表接收到数据包
        {
            OLED_ShowString(4, 1, "                ");//    擦除第4行
            OLED_ShowString(4, 1, Serial_RxPacket);
            
            if(strcmp(Serial_RxPacket, "LED_ON") == 0)// 判断两个字符串是否相等。相等则返回0
            {
                LED1_ON();
                Serial_SendString("LED_ON_OK\r\n");
                OLED_ShowString(2, 1, "                ");//    擦除第4行
                OLED_ShowString(2, 1, "LED_ON_OK");
            }
            else if(strcmp(Serial_RxPacket, "LED_OFF") == 0)// 判断两个字符串是否相等。相等则返回0
            {
                LED1_OFF();
                Serial_SendString("LED_ON_OFF\r\n");
                OLED_ShowString(2, 1, "                ");//    擦除第4行
                OLED_ShowString(2, 1, "LED_OFF_OK");
            }
            else
            {
                Serial_SendString("ERROR_COMMAND\r\n");
                OLED_ShowString(2, 1, "                ");//    擦除第4行
                OLED_ShowString(2, 1, "ERROR_COMMAND");
            }
            Serial_RxFlag = 0;//    清零
        }
    }
}

其他引用的头文件和c代码可在此处查阅:OLED.h(【江协STM32】4 OLED调试工具,第5节)、Delay.h(【江协STM32】3-2 LED闪烁&LED流水灯&蜂鸣器,第1.3节)、LED.h(【江协STM32】3-4 按键控制LED&光敏传感器控制蜂鸣器,第1.3节)


http://www.kler.cn/a/473547.html

相关文章:

  • JS进阶--JS听到了不灭的回响
  • MySQL insert or update方式性能比较
  • 【gRPC】Keepalive连接保活配置,go案例
  • 【Linux 之 二十 】使用 ln 命令创建符号链接
  • Meilisearch ASP.Net Core API 功能demo
  • [离线数仓] 总结二、Hive数仓分层开发
  • micro-app vite4接入vite6 遇到的问题
  • 数据库管理系统
  • 利用Java爬虫获取1688商品详情:API返回值说明及代码示例
  • VsCode对Arduino的开发配置
  • 【深度学习】布匹寻边:抓边误差小于3px【附完整链接】
  • 从零用java实现 小红书 springboot vue uniapp (9)消息推送功能
  • 【Unity3D】导出Android项目以及Java混淆
  • 初学vue3心得
  • VSCode 远程开发环境中的 Python 虚拟环境切换详解
  • Python 植物大战僵尸
  • HTML 迷宫游戏
  • Python编程实例-特征向量与特征值编程实现
  • Idea-离线安装SonarLint插件地址
  • json相关内容(python)
  • 力扣-数据结构-13【算法学习day.84】
  • 基于 Apache Commons Pool 实现的 gRPC 连接池管理类 GrpcChannelPool 性能分析与优化
  • HTMLHTML5革命:构建现代网页的终极指南 - 0. 课程目录设计
  • AI华佗?港中大、深圳大数据研究院提出医疗推理大模型HuatuoGPT-o1
  • 深度学习的加速器:Horovod,让分布式训练更简单高效!
  • Element plus中el-input框回车触发页面刷新问题以及解决办法