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

CAN总线通信协议(基础)

1.CAN简介        

        CAN总线(Controller Area Network Bus)控制器局域网总线 CAN总线是由BOSCH公司开发的一种简洁易用、传输速度快、易扩展、可靠性高的串行通信总线,广泛应用于汽车、嵌入式、工业控制等领域 CAN总线特征: 两根通信线(CAN_H、CAN_L),线路少,无需共地 差分信号通信,抗干扰能力强,用电压差值传递数据 高速CAN(ISO11898):125k~1Mbps, <40m,用处多,着重学 低速CAN(ISO11519):10k~125kbps, <1km 异步,无需时钟线,通信速率由设备各自约定 半双工,可挂载多设备,多设备同时发送数据时通过仲裁判断先后顺序 11位(标准格式)/29位(扩展格式)报文ID,用于区分消息功能,同时决定优先级 可配置1~8字节的有效载荷

广播式(发送者向接收者写入数据,这个lan网络下的其他设备被动接收,可根据ID确定要不要接收)和

请求式(发一个数据就需要先请求再接收)两种传输方式 应答、CRC校验、位填充、位同步、错误处理等特性主流通信协议对比 

对比其他串行通信协议:

名称

引脚

双工

时钟

电平

设备

应用场景

UART

TX、RX

全双工

异步

单端

点对点

两个设备互相通信

I2C

SCL、SDA

半双工

同步

单端

多设备

一个主控外挂多个模块

SPI

SCK、MOSI、MISO、

SS

全双工

同步

单端

多设备

一个主控外挂多个模块(高速)

CAN

CAN_H、CAN_L

半双工

异步

差分

多设备

多个主控互相通信

UART

IIC

SPI

2.物理层

        CAN 通讯并不是以时钟信号来进行同步的,它是一种半双工异步通讯,只具有CAN_High 和CAN_Low 两条信号线,共同构成一组差分信号线,以差分信号的形式进行通讯。

发送数据时:控制器把要发送的二进制编码通过CAN_Tx 线发送到收发器,然后由收发器把这个普通的逻辑电平信号转化成差分信号,通过差分线CAN_High 和CAN_Low 线输出到CAN 总线网络。

接收数据时:收发器把总线上收到的CAN_High 及CAN_Low 信号转化成普通的逻辑电平信号,通过CAN_Rx 输出到控制器中

怎么确定和哪个从机通信,读写关系确定?

can类似于广播,发送者向信道写入数据,这个lan网络下的其他设备被动接收(可以根据ID确定要不要接收)

两种CAN的连接结构

1.开环总线网络:低速、远距离“开环网络”,它的最大传输距离为1km,最高通讯速率为125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个“2.2 千欧”的电阻。

低速CAN使用开环总线网络

2.闭环总线网络:高速、短距离“闭环网络”,它的总线最大长度为40m,通信速度最高为1Mbps,总线的两端各要求有一个“120 欧”的电阻。

电阻的作用

1.防止回波反射 尤其是 高频信号、远距离传输的场景 如果不加终端电阻 信号波形会在线路终端反射,进而干扰原始信号 。加了终端电阻 且 阻抗匹配 则电平跳变会比较平稳 2.在没有设备操作时 将两根差分线的电压 “收紧” (收紧为 电平1 呈隐性) 使其电压一致。 收紧:在没有设备操作总线的情况下 电阻就和弹簧一样 将两根线(CAN_H CAN_L) 的电压拉到同一个电平 和I2C的上拉电阻很像。

差分协议

表示逻辑1 时(隐性电平),CAN_High 和CAN_Low 线上的电压均为2.5v,即它们的电压差VH-VL=0V;

表示逻辑0 时(显性电平),CAN_High 的电平为3.5V,CAN_Low 线的电平为1.5V,即它们的电压差为VH-VL=2V。

例如,当CAN 收发器从CAN_Tx 线接收到来自CAN 控制器的低电平信号时(逻辑0),它会使CAN_High 输出3.5V,同时CAN_Low 输出1.5V,从而输出显性电平表示逻辑0。

3.协议层

        在原始数据段的前面加上传输起始标签、片选标签和控制标签,在数据的尾段加上CRC校验标签、应答标签和传输结束标签,把这些内容按特定的格式打包好,就可以用一个通道表达各种信号了,当整个数据包被传输到其它设备时,只要这些设备按格式去解读,就能还原出原始数据,这样的报文就被称为CAN 的“数据帧”。

为了更有效地控制通讯,CAN 一共规定了5 种类型的帧:

数据帧的结构

        数据帧是在CAN 通讯中最主要、最复杂的报文。

帧起始

        SOF 段(Start Of Frame),译为帧起始,帧起始信号只有一个数据位,是一个显性电平(0),它用于通知各个节点将有数据传输。

        

仲裁段(ID)

        当同时有两个报文被发送时,总线会根据仲裁段的内容决定哪个数据包能被传输,这也是它名称的由来。

        仲裁段的内容主要为本数据帧的ID 信息(标识符),数据帧具有标准格式和扩展格式两种,区别就在于ID 信息的长度,标准格式的ID 为11 位,扩展格式的ID 为29 位,它在标准ID 的基础上多出18 位。

        在CAN 协议中,ID 起着重要的作用,它决定着数据帧发送的优先级,也决定着其它节点是否会接收这个数据帧。CAN 协议不对挂载在它之上的节点分配优先级和地址,对总线的占有权是由信息的重要性决定的,即对于重要的信息,我们会给它打包上一个优先级高的ID,使它能够及时地发送出去。也正因为它这样的优先级分配原则,使得CAN 的扩展性大大加强,在总线上增加或减少节点并不影响其它设备。

        如果总线上同时出现显性电平(0)和隐性电平(1),总线的状态会被置为显性电平,CAN 正是利用这个特性进行仲裁。若两个节点同时竞争CAN 总线的占有权,当它们发送报文时,若首先出现隐性电平,则会失去对总线的占有权。

        具体方式是当多个单元同时开始发送时,各发送单元从仲裁段的第一位开始进行仲裁。连续输出显性 电平最多的单元可继续发送。

        仲裁段ID 的优先级也影响着接收设备对报文的反应。因为在CAN 总线上数据是以广播的形式发送的,所有连接在CAN 总线的节点都会收到所有其它节点发出的有效数据,因而我们的CAN控制器大多具有根据ID 过滤报文的功能,它可以控制自己只接收某些ID 的报文

回看上图数据帧的结构中的数据帧格式,可看到仲裁段除了报文ID 外,还有RTR、IDE 和SRR 位。

(1) RTR 位(Remote Transmission Request Bit),译作远程传输请求位,它是用于区分数据帧和遥控帧的,当它为显性电平(0)时表示数据帧,隐性电平(1)时表示遥控帧。

(2) IDE 位(Identifier Extension Bit),译作标识符扩展位,它是用于区分标准格式与扩展格式,当它为显性电平时表示标准格式,隐性电平时表示扩展格式。

(3) SRR 位(Substitute Remote Request Bit),只存在于扩展格式,它用于替代标准格式中的RTR 位。由于扩展帧中的SRR 位为隐性位,RTR 在数据帧为显性位,所以在两个ID 相同的标准格式报文与扩展格式报文中,标准格式的优先级较高。

控制段

        在控制段中的r1 和r0 为保留位,默认设置为显性位。它最主要的是DLC段(Data Length Code),译为数据长度码,它由4 个数据位组成,用于表示本报文中的数据段含有多少个字节,DLC 段表示的数字为0~8。

数据段

数据段为数据帧的核心内容,它是节点要发送的原始信息,由0~8 个字节组成,MSB 先行。

CRC段(校验段)

        为了保证报文的正确传输,CAN 的报文包含了一段15 位的CRC 校验码,一旦接收节点算出的CRC 码跟接收到的CRC 码不同,则它会向发送节点反馈出错信息,利用错误帧请求它重新发送。CRC 部分的计算一般由CAN 控制器硬件完成,出错时的处理则由软件控制最大重发数。

(错误帧的用途:某个设备检测出错误时向其他设备通知错误。当某个设备发现总线上的波形不对,产生错误了,他就会发出错误帧,错误帧可以叠加在数据帧上,并且可以破坏数据帧的数据,意思就是我发现这一帧数据有错,现在我破坏了这个数据,大家都不要用了,这是一种错误处理机制。)

        在CRC 校验码之后,有一个CRC 界定符,它为隐性位(1),主要作用是把CRC 校验码与后面的ACK段间隔起来。

ACK段(确认信号)

        ACK 段包括一个ACK 槽位和一个ACK 界定符位。类似I2C 总线,在ACK 槽位中,发送节点发送的是隐性位(1),而接收节点则在这一位中发送显性位(0)以示应答。在ACK 槽和帧结束之间由ACK 界定符间隔开。

帧结束

EOF 段(End Of Frame),译为帧结束,帧结束段由发送节点发送的7 个隐性位表示结束

4.CAN的波特率及位序

        由于CAN 属于异步通讯,没有时钟信号线,连接在同一个总线网络中的各个节点会像串口异步通讯那样,节点间使用约定好的波特率进行通讯,特别地,CAN 还会使用“位同步”的方式来抗干扰、吸收误差,实现对总线电平信号进行正确的采样,确保通讯正常。

位时序

        为了实现位同步,CAN 协议把每一个数据位的时序分解成 位时序分解图所示的SS 段(同步段)、PTS 段(传播时间段)、PBS1 段(相位缓冲段1)、PBS2 段(相位缓冲段2),这四段的长度加起来即为一个CAN 数据位的长度。分解后最小的时间单位是Tq。

        1 位分为4 个段,每个段又由若干个Tq 构成,这称为位时序。

        1 位由多少个Tq 构成、每个段又由多少个Tq 构成等,可以任意设定位时序。通过设定位时序,多个单元可同时采样,也可任意设定采样点。

        上图第一张图设定CAN 通讯信号每一个数据位的长度为10Tq,其中SS 段占1Tq,PTS 段占3Tq,PBS1段占3Tq,PBS2 段占3Tq。

        信号的采样点位于PBS1 段与PBS2 段之间,通过控制各段的长度,可以对采样点的位置进行偏移,以便准确地采样,例如上图第二第三张图。

CAN通信频率:CAN总线时钟分频 = 42MHz /PSC(不需要+1) = 42 / 6 =7MHz

CAN通信单位时间Tq: 1/ 频率 = 1/ 7MHz = 1/7us

can把每个电平划分为了四个段:SS(起始段,固定一个Tq)、PTS(传播时间段)、PBS1、PBS2(至少两个Tq)

采样在PBS1和PBS2之间完成,PBS1可以延长、PBS2可以缩短(延长缩短的极限是SJW,TIPS:以上所有的值都是多少个单位时间Tq)。利用划分的四段,保证电平变换在SS段完成,采样点在PBS1和PBS2之间。

各段的作用如介绍下:

• SS 段

SS 译为同步段,若通讯节点检测到总线上信号的跳变沿被包含在SS 段的范围之内,则表示节点与总线的时序是同步的,当节点与总线同步时,采样点采集到的总线电平即可被确定为该位的电平。SS 段的大小固定为1Tq。

• PTS 段

PTS 译为传播时间段,这个时间段是用于补偿网络的物理延时时间。是总线上延时总和的两倍。PTS 段的大小可以为1~8Tq。

• PBS1 段

PBS1 译为相位缓冲段,主要用来补偿边沿阶段的误差,它的时间长度在重新同步的时候可以加长。PBS1 段的初始大小可以为1~8Tq。

• PBS2 段

PBS2 这是另一个相位缓冲段,也是用来补偿边沿阶段误差的,它的时间长度在重新同步时可以缩短。PBS2 段的初始大小可以为2~8Tq。

通讯的波特率

总线上的各个通讯节点只要约定好1 个Tq 的时间长度以及每一个数据位占据多少个Tq,就可以确定CAN 通讯的波特率。

例如,假设上图中的1Tq=1us,而每个数据位由10 个Tq 组成,则传输一位数据需要时间T=10us,从而每秒可以传输的数据位个数为:

1x10^6/10 = 100000 (bps)

这个每秒可传输的数据位的个数即为通讯中的波特率。

取得同步的方法

1.硬同步

节点将自己的位时序种的SS段平移至总线出现下降沿的部分,获得同步

硬同步只是当存在“帧起始信号”时起作用,无法确保后续一连串的位时序都是同步的,而重新同步方式可解决该问题。

2.再同步

前面的硬同步只是当存在帧起始信号时才起作用,如果在一帧很长的数据内,节点信号与总线信号相位有偏移时,这种同步方式就无能为力了。因而需要引入再同步方式,它利用普通数据位的隐性至显性电平的跳变沿来同步。重新同步与硬同步方式相似的地方是它们都使用SS 段来进行检测,同步的目的都是使节点内的SS 段把跳变沿包含起来。

同步的方式分为超前和滞后两种情况,以总线跳变沿与SS 段的相对位置进行区分。

  • 相位超前:

假设节点从总线的边沿跳变中,检测到它内部的时序比总线的时序相对超前2Tq,这时控制器在下一个位时序中的PBS1 段增加2Tq 的时间长度,使得节点与总线时序重新同步。

  • 相位滞后:

假设节点从总线的边沿跳变中,检测到它的时序比总线的时序相对滞后2Tq,这时控制器在前一个位时序中的PBS2 段减少2Tq 的时间长度,获得同步。

        在重新同步的时候,PBS1 和PBS2 中增加或减少的这段时间长度被定义为“重新同步补偿宽度SJW (reSynchronization Jump Width)”。一般来说CAN 控制器会限定SJW 的最大值,如限定了最大SJW=3Tq 时,单次同步调整的时候不能增加或减少超过3Tq 的时间长度,若有需要,控制器会通过多次小幅度调整来实现同步。当控制器设置的SJW 极限值较大时,可以吸收的误差加大,但通讯的速度会下降。

5.CAN 初始化结构体

/**
  * CAN filter init structure definition
  * CAN 筛选器结构体
 */
 typedef struct {
     //配置两个16位ID和两个16位掩码 - 32位是由两个16位拼接来的,在标志列表模式下也可以把掩码当成ID
     uint16_t CAN_FilterIdHigh; /*CAN_FxR1 寄存器的高16 位*/
     uint16_t CAN_FilterIdLow; /*CAN_FxR1 寄存器的低16 位*/
     uint16_t CAN_FilterMaskIdHigh; /*CAN_FxR2 寄存器的高16 位*/
     uint16_t CAN_FilterMaskIdLow; /*CAN_FxR2 寄存器的低16 位*/
     uint16_t CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; /* 设置经过筛选后数据存储到哪个接收FIFO

     uint8_t CAN_FilterNumber; /* 筛选器编号,范围0-27*/
     uint8_t CAN_FilterMode; /* 筛选器模式*/
     uint8_t CAN_FilterScale; /* 设置筛选器的尺度 - 16位ID或32位ID*/
     FunctionalState CAN_FilterActivation; /* 是否使能本筛选器*/
 } CAN_FilterInitTypeDef;

(1) CAN_FilterIdHigh

用于存储要筛选的ID,若筛选器工作在32 位模式,它存储的是所筛选ID 的高16 位;若筛选器工作在16 位模式,它存储的就是一个完整的要筛选的ID。

(2) CAN_FilterIdLow

也是用于存储要筛选的ID,若筛选器工作在32 位模式,它存储的是所筛选ID 的低16 位;若筛选器工作在16 位模式,它存储的就是一个完整的要筛选的ID。

(3) CAN_FilterMaskIdHigh

CAN_FilterMaskIdHigh 分两种情况,当筛选器工作在标识符列表模式时,它的功能与CAN_FilterIdHigh 相同,都是存储要筛选的ID;而当筛选器工作在掩码模式时,它存储的是CAN_FilterIdHigh 成员对应的掩码,与CAN_FilterMaskIdLow 组成一组筛选器。

4) CAN_FilterMaskIdLow

CAN_FilterMaskIdLow 存储的内容也分两种情况,当筛选器工作在标识符列表模式时,它的功能与CAN_FilterIdLow 相同,都是存储要筛选的ID;而当筛选器工作在掩码模式时,它存储的是CAN_FilterIdLow 成员对应的掩码,与CAN_FilterMaskIdHigh组成一组筛选器。

不同模式下各结构体成员的内容:

用库函数CAN_FilterInit 即可把这些参数写入到筛选控制寄存器中。

数据收发流程

封装或者解析相应的数据报,收(判断FIFO缓冲区有没有数据,没数据就收不了),发(返回发送所用的邮箱号,读取和存储报文数据段的内容

can.c

#include "public.h"

//canrx PA11    cantx   PA12

static void CAN_GPIO_init(void)
{
        
    GPIO_InitTypeDef GPIO_InitStructure;
	
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

		//CAN TX/RX
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11|GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
		//引脚复用
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource11,GPIO_AF_CAN1);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource12,GPIO_AF_CAN1);
    
}

static void CAN_NVIC_Config(void)
{
   	NVIC_InitTypeDef NVIC_InitStructure;

		NVIC_InitStructure.NVIC_IRQChannel = CAN1_RX0_IRQn;	   //CAN RX0中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;		   //抢占优先级0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;			   //子优先级为0
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

static void CAN_Mode_Config(void)
{
    CAN_InitTypeDef CAN_InitStructure;
	
		/************************CAN通信参数设置**********************************/
	  /* Enable CAN clock */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
    
	/* CAN Baudrate = 1 MBps (1MBps已为stm32的CAN最高速率) (CAN 时钟频率为 APB 1 = 42 MHz) */
		CAN_InitStructure.CAN_Prescaler=6;//时钟频率=42MHz/6=7MHz 周期 = 1/7
    CAN_InitStructure.CAN_Mode=CAN_Mode_LoopBack;//回环模式
    
		CAN_InitStructure.CAN_SJW=CAN_SJW_2tq;//重新同步跳跃宽度 2个时间单元
	  /* ss=1 bs1=4 bs2=2 位时间宽度为(1+4+2) 波特率即为时钟周期tq*(1+4+2) *///7MHz / ss+bs1+bs2 = 1MHz
    CAN_InitStructure.CAN_BS1=CAN_BS1_4tq;//时间段1 占用了4个时间单元
    CAN_InitStructure.CAN_BS2=CAN_BS2_2tq;//时间段2 占用了2个时间单元	
    
		CAN_InitStructure.CAN_TTCM = DISABLE;//关闭时钟港出发
    CAN_InitStructure.CAN_ABOM = ENABLE;//离线自动管理
    CAN_InitStructure.CAN_AWUM = ENABLE;//自动唤醒功能
 
    CAN_InitStructure.CAN_NART = ENABLE;//自动重传功能 - 直到成功
    CAN_InitStructure.CAN_RFLM = ENABLE;//锁定FIFO,数据缓冲区满了新数据自动丢弃
    CAN_InitStructure.CAN_TXFP = DISABLE;//是否使能优先级判定,不使能则按照报文ID的优先级
    CAN_Init(CAN1, &CAN_InitStructure);

}

/*CAN的过滤器 配置
把筛选器第0 组配置成了32 位的掩码模式,并且把它的输出连接到接收FIFO0,若通
过了筛选器的匹配,报文会被存储到接收FIFO0
*/
static void CAN_Filter_init(void)
{
		CAN_FilterInitTypeDef CAN_FilterInitStructure;
		
		/*CAN筛选器初始化*/
		CAN_FilterInitStructure.CAN_FilterNumber = 1; /* 筛选器编号,范围0-27*/
		CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask ; /* 筛选器模式 - 掩码模式*/ 
		CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; /* 设置筛选器的尺度 - 位宽为单个32位*/
		
	
		CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; /*要筛选的寄存器的高16 位 - CAN_FxR1 */
		CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; /*要筛选的寄存器的低16 位 - CAN_FxR1 */
		CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; /*筛选器的高16 位每位必须匹配 - CAN_FxR2 */
		CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; /*筛选器的低16 位每位必须匹配 - CAN_FxR2 */
		CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; // 设置经过筛选后数据存储到哪个接收FIFO
		FunctionalState CAN_FilterActivation = ENABLE; /* 是否使能本筛选器*/
		//CAN_FilterInit(&CAN_FilterInitStructure);
	
		/*CAN通信中断使能*/
		//CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
}

//完整配置CAN的功能
void CAN_Config(void)
{
  CAN_GPIO_init();
  //CAN_NVIC_Config();
  CAN_Mode_Config();
  CAN_Filter_init();   
}

u8 CAN_Send_data(u8 *data)
{
	CanTxMsg datatosend;
	int i= 0;
	uint8_t Mailbox = 0;
	datatosend.StdId = 0x01; /* 存储报文的标准标识符11 位,0-0x7FF. */
	datatosend.ExtId = 0x01; /* 存储报文的扩展标识符29 位,0-0x1FFFFFFF. */
	datatosend.IDE = CAN_Id_Extended; /* 存储IDE 扩展标志*/
	datatosend.RTR = CAN_RTR_Data; /* 存储RTR 远程帧标志*/
	datatosend.DLC = 8; /* 存储报文数据段的长度,0-8 */
			
	for(i=0;i<8;i++)
	{
		datatosend.Data[i] = data[i]; /* 存储报文数据段的内容*/
	}
	
	Mailbox = CAN_Transmit(CAN1,&datatosend);
	while((CAN_TransmitStatus(CAN1,Mailbox)==CAN_TxStatus_Failed) && (i<0xFFFF))
		i++;//等待发送结束
			
	if(i>=0xFFFF)
		return 0;//超时跳出

	
	return 1;
}

u8 CAN_Receive_data(u8 *data)
{
	CanRxMsg datatorev;
	int i = 0;
	
	if(CAN_MessagePending(CAN1,CAN_FIFO0) == 0)
	{
		return 0;//没有接收到数据,直接退出
	}
	CAN_Receive(CAN1,CAN_FIFO0,&datatorev);//读取数据
	
	for(i=0;i<datatorev.DLC;i++)
	{
		 data[i] = datatorev.Data[i]; /* 存储报文数据段的内容*/
	}
	
	return datatorev.DLC;
}

main.c

#include "public.h"
int main(void)
{
	u8 msg[8] = {45,46,47,48,49,50};
	u8 canbuf[8];
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	SysTick_Init();
	USART_Init_Config();
	KEY_Init();
	LED_Init();
	
	CAN_Config();
	
	u8 k =0;
	u8 i =0;
	while(1)
	{
		if(GetStaKey() == true)
		{
			CAN_Send_data(msg);
			GPIO_ToggleBits(GPIOF,GPIO_Pin_9);
		}
		Delay_ms(1);
		
		k = CAN_Receive_data(canbuf);
		
		if(k)
		{
			for(i=0;i<k;i++)
			{
				printf("%d ",canbuf[i]);
			}
		}
	}
}

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

相关文章:

  • 天才的懈怠 : 平衡二叉树
  • Java项目实战II基于微信小程序的个人行政复议在线预约系统微信小程序(开发文档+数据库+源码)
  • fastapi 查询参数支持 Pydantic Model:参数校验与配置技巧
  • 【JAVA】Java基础—面向对象编程:封装—保护类的内部数据
  • 测试实项中的偶必现难测bug--验证码问题
  • Chromium 中MemoryMappedFile使用例子c++
  • day-58 最佳观光组合
  • JS中的for...in和for...of有什么区别?
  • MySQL篇(存储过程 触发器 存储函数)(持续更新迭代)
  • 算法:斐波那契数列
  • 什么是动态数据脱敏?
  • 基于单片机的粮仓环境检测系统设计
  • 鸿蒙应用生态构建的核心目标
  • 一些线上常用排查问题的命令
  • IT行业中的技术趋势与未来展望
  • Nginx-HTTP和反向代理web服务器
  • Linux实用命令 lsof命令
  • 昇思量子计算系列教程-Grover搜索算法
  • C++学习笔记(37)
  • AMQP-CPP二次封装
  • Llama 3.1 Omni:颠覆性的文本与语音双输出模型
  • Linux下文件下载中文乱码问题
  • C++单例模式代码实现与分析
  • Spring Boot实用小技巧5 - 第527篇
  • Leetcode面试经典150题-198.打家劫舍
  • 【Git使用】删除Github仓库中的指定文件/文件夹