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

STM32F103_LL库+寄存器学习笔记12 - 提高串口通讯程序的健壮性:异常监控 + 超时保护机制

导言


首先,进行USART和DMA状态监测、记录异常状态并主动处理,是高健壮性嵌入式系统开发的核心思想之一。
这种机制看似复杂,实则能有效保障系统长期、稳定地运行:

  • 提升通讯可靠性。
  • 降低维护成本。
  • 增强系统自恢复能力。

因此,建议你在STM32项目中实现USART和DMA的健康监测机制。

另外,STM32串口+DMA通信中引入超时保护机制的主要目的是:

  • 防止通讯过程中数据不完整或缺失(数据量小于半传输阈值,导致DMA半满、满传输中断均未触发)。可能有人会问?不是有接收空闲中断吗?确实有。但是,有可能接收空闲中断因某些原因导致失效。
  • 利用定时器,定期检测DMA接收缓冲区数据变化情况。比如若在一定时间内未收到数据,触发处理或重启DMA。

因此,引入超时机制(Timeout),可有效提升通信实时性与健壮性。

项目地址:https://github.com/q164129345/MCU_Develop/tree/main/stm32f103_ll_library11_usart_dma_error_check

一、监控串口异常


1.1、寄存器USART_SR

在这里插入图片描述
在这里插入图片描述
如上所示,串口的状态寄存器ISR一共有4个错误标志。其中位0的校验错误是在奇偶校验功能开启后才会触发,然而,这个功能一般不会开启。
以上这4个错误的清除方式都是先读USART_SR再读USART_DR,跟清除DILE空闲中断标志一样。所以,有了清除IDLE标志的代码后,不用单独去清除这些错误标志了。

NE、FE、ORE中断都依赖RXNE中断。 如上所示,参考手册说到NE、FE、ORE中断都依赖RXNE中断。就是说,想及时获取NE、FE、ORE中断就需要让RXNEIE置1,开启RXNE中断。但是,如果数据流量很大,开启RXNE中断会使每个字节到达时都进入中断,这可能导致中断频繁且影响系统性能。利用DMA接收数据,开启空闲中断来判断数据帧的结束。在空闲中断处理函数中,除了处理数据,还可以检查错误标志。不过,前提是必须有数据到达,才会触发空闲中断。所以,更好的方案是USART1全局中断与主轮询都要监控串口异常,记录异常并及时恢复。

1.2、代码实现(LL库)

/* LL库 */
uint16_t usart1Error = 0; // 记录串口发生的错误次数

static __inline uint8_t USART1_Error_Handler(void) {
	// 错误处理:检查USART错误标志(ORE、NE、FE、PE)
    if (LL_USART_IsActiveFlag_ORE(USART1) ||
        LL_USART_IsActiveFlag_NE(USART1)  ||
        LL_USART_IsActiveFlag_FE(USART1)  ||
        LL_USART_IsActiveFlag_PE(USART1))
    {
	    // 通过读SR和DR来清除错误标志
        volatile uint32_t tmp = USART1->SR;
        tmp = USART1->DR;
        (void)tmp;
	    return 1;
	} else {
		return 0;
	} 
}

void USART1_IRQHandler(void)
{
    // 错误处理:检查USART错误标志(ORE、NE、FE、PE)
    if (USART1_Error_Handler()) {
	    usart1Error++; // 记录事件
	} else if (LL_USART_IsActiveFlag_IDLE(USART1)) { // 空闲中断
		// 处理空闲中断
	} else {  // 其他中断
	    // 处理其他中断
	}
}

1.3、代码实现(寄存器方式)

uint16_t usart1Error = 0; // 记录串口发生的错误次数

static __inline uint8_t USART1_Error_Handler(void) {
    // 寄存器方式:检查错误标志(PE、FE、NE、ORE分别位0~3)
    if (USART1->SR & ((1UL << 0) | (1UL << 1) | (1UL << 2) | (1UL << 3))) {
	    // 清除错误标志
        volatile uint32_t tmp = USART1->SR;
        tmp = USART1->DR;
        (void)tmp;
        return 1;
    } else {
	    return 0;
    }
}

void USART1_IRQHandler(void)
{
    // 寄存器方式:检查错误标志(PE、FE、NE、ORE分别位0~3)
    if (USART1_Error_Handler()) {
	    usart1Error++; // 记录事件
    } else if (USART1->SR & (1UL << 4)) { // 空闲中断
		// 处理空闲中断
    } else { // 其他中断
        // 处理其他中断
    }
}

为了更好地排查问题,可以在检测到错误时记录错误日志或计数,便于后续进行调试和系统优化。 全局变量usart1Error的目的就是统计错误的次数,在适当的实际将它上报给上位机。上位机有一个GUI程序,为了了解单片机系统的串口通讯情况,可以将usart1Error的计数显示在GUI上。当计数经常递增,表明串口有问题,需要去排查硬件或者线束问题等等。

根据实际需求,可以设计一些自动恢复机制。 当检测到错误时,除了清除错误标志、统计错误的次数。可以考虑重启串口、重新配置DMA通道或者进行数据重传,从而提高系统的鲁棒性。

1.4、串口重启的方法(重新初始化)

当错误次数累积太多时,可以考虑重启串口。进一步提升系统的鲁棒性。

1.4.1、代码实现(LL库)

/**
  * @brief  当检测到USART1异常时,重新初始化USART1及其相关DMA通道。
  * @note   此函数首先停止DMA1通道4(USART1_TX)和通道5(USART1_RX),确保DMA传输终止,
  *         然后禁用USART1及其DMA接收请求,并通过读SR和DR清除挂起的错误标志,
  *         经过短暂延时后调用MX_USART1_UART_Init()重新配置USART1、GPIO和DMA,
  *         MX_USART1_UART_Init()内部已完成USART1中断的配置,无需额外使能。
  * @retval None
  */
void USART1_Reinit(void)
{
    /* 禁用DMA,如果USART1启用了DMA的话 */
    LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_4); // 禁用DMA1通道4
    while(LL_DMA_IsEnabledChannel(DMA1, LL_DMA_CHANNEL_4)) { // 等待通道完全关闭(可以添加超时机制以避免死循环)
        // 空循环等待
    }
    LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_5); // 禁用DMA1通道5
    while(LL_DMA_IsEnabledChannel(DMA1, LL_DMA_CHANNEL_5)) {  // 等待通道完全关闭(可以添加超时机制以避免死循环)
        // 空循环等待
    }
    
    /* USART1 */
    NVIC_DisableIRQ(USART1_IRQn); // 1. 禁用USART1全局中断,避免重启过程中产生新的中断干扰
    LL_USART_DisableDMAReq_RX(USART1); // 2. 禁用USART1的DMA接收请求(如果使用DMA接收)
    LL_USART_Disable(USART1); // 3. 禁用USART1
    
    // 4. 读SR和DR以清除挂起的错误标志(例如IDLE、ORE、NE、FE、PE)
    volatile uint32_t tmp = USART1->SR;
    tmp = USART1->DR;
    (void)tmp;
    
    for (volatile uint32_t i = 0; i < 1000; i++); // 5. 可选:短暂延时,确保USART完全关闭
    tx_dma_busy = 0; // 复位发送标志!!!!!!!!!!!!!!!!!!!
    MX_USART1_UART_Init(); // 重新初始化USART1
    DMA1_Channel5_Configure(); // 重新初始化DMA1通道5
}

1.4.2、代码实现(寄存器方式)

// 配置DMA1的通道4:普通模式,内存到外设(flash->USART1_TX),优先级高,存储器地址递增、数据大小都是8bit
__STATIC_INLINE void DMA1_Channel4_Configure(void) {
    // 开启时钟
    RCC->AHBENR |= (1UL << 0UL); // 开启DMA1时钟
    // 设置并开启全局中断
    NVIC_SetPriority(DMA1_Channel4_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),2, 0));
    NVIC_EnableIRQ(DMA1_Channel4_IRQn);
    // 数据传输方向
    DMA1_Channel4->CCR &= ~(1UL << 14UL); // 外设到存储器模式
    DMA1_Channel4->CCR |= (1UL << 4UL); // DIR位设置1(内存到外设)
    // 通道优先级
    DMA1_Channel4->CCR &= ~(3UL << 12UL); // 清除PL位
    DMA1_Channel4->CCR |= (2UL << 12UL);  // PL位设置10,优先级高
    // 循环模式
    DMA1_Channel4->CCR &= ~(1UL << 5UL);  // 清除CIRC位,关闭循环模式
    // 增量模式
    DMA1_Channel4->CCR &= ~(1UL << 6UL);  // 外设存储地址不递增
    DMA1_Channel4->CCR |= (1UL << 7UL);   // 存储器地址递增
    // 数据大小
    DMA1_Channel4->CCR &= ~(3UL << 8UL);  // 外设数据宽度8位,清除PSIZE位,相当于00
    DMA1_Channel4->CCR &= ~(3UL << 10UL); // 存储器数据宽度8位,清除MSIZE位,相当于00
    // 中断
    DMA1_Channel4->CCR |= (1UL << 1UL);   // 开启发送完成中断
}


__STATIC_INLINE void USART1_Configure(void) {
    /* 1. 使能外设时钟 */
    // RCC->APB2ENR 寄存器控制 APB2 外设时钟
    RCC->APB2ENR |= (1UL << 14UL); // 使能 USART1 时钟 (位 14)
    RCC->APB2ENR |= (1UL << 2UL);  // 使能 GPIOA 时钟 (位 2)
    /* 2. 配置 GPIO (PA9 - TX, PA10 - RX) */
    // GPIOA->CRH 寄存器控制 PA8-PA15 的模式和配置
    // PA9: 复用推挽输出 (模式: 10, CNF: 10)
    GPIOA->CRH &= ~(0xF << 4UL);        // 清零 PA9 的配置位 (位 4-7)
    GPIOA->CRH |= (0xA << 4UL);         // PA9: 10MHz 复用推挽输出 (MODE9 = 10, CNF9 = 10)
    // PA10: 浮空输入 (模式: 00, CNF: 01)
    GPIOA->CRH &= ~(0xF << 8UL);        // 清零 PA10 的配置位 (位 8-11)
    GPIOA->CRH |= (0x4 << 8UL);         // PA10: 输入模式,浮空输入 (MODE10 = 00, CNF10 = 01)
    // 开启USART1全局中断
    NVIC_SetPriority(USART1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0)); // 优先级1(优先级越低相当于越优先)
    NVIC_EnableIRQ(USART1_IRQn);
    /* 3. 配置 USART1 参数 */
    // (1) 设置波特率 115200 (系统时钟 72MHz, 过采样 16)
    // 波特率计算: USART_BRR = fPCLK / (16 * BaudRate)
    // 72MHz / (16 * 115200) = 39.0625
    // 整数部分: 39 (0x27), 小数部分: 0.0625 * 16 = 1 (0x1)
    USART1->BRR = (39 << 4UL) | 1;      // BRR = 0x271 (39.0625)
    // (2) 配置数据帧格式 (USART_CR1 和 USART_CR2)
    USART1->CR1 &= ~(1UL << 12UL);      // M 位 = 0, 8 位数据
    USART1->CR2 &= ~(3UL << 12UL);      // STOP 位 = 00, 1 个停止位
    USART1->CR1 &= ~(1UL << 10UL);      // 没奇偶校验
    // (3) 配置传输方向 (收发双向)
    USART1->CR1 |= (1UL << 3UL);        // TE 位 = 1, 使能发送
    USART1->CR1 |= (1UL << 2UL);        // RE 位 = 1, 使能接收
    // (4) 禁用硬件流控 (USART_CR3)
    USART1->CR3 &= ~(3UL << 8UL);       // CTSE 和 RTSE 位 = 0, 无流控
    // (5) 配置异步模式 (清除无关模式位)
    USART1->CR2 &= ~(1UL << 14UL);      // LINEN 位 = 0, 禁用 LIN 模式
    USART1->CR2 &= ~(1UL << 11UL);      // CLKEN 位 = 0, 禁用时钟输出
    USART1->CR3 &= ~(1UL << 5UL);       // SCEN 位 = 0, 禁用智能卡模式
    USART1->CR3 &= ~(1UL << 1UL);       // IREN 位 = 0, 禁用 IrDA 模式
    USART1->CR3 &= ~(1UL << 3UL);       // HDSEL 位 = 0, 禁用半双工
    // (6) 中断
    USART1->CR1 |= (1UL << 4UL);        // 使能USART1空闲中断 (IDLEIE, 位4)
    // (7) 关联DMA的接收请求
    USART1->CR3 |= (1UL << 6UL);        // 使能USART1的DMA接收请求(DMAR,位6) 
    // (7) 启用 USART
    USART1->CR1 |= (1UL << 13UL);       // UE 位 = 1, 启用 USART
}

// 配置USART1_RX的DMA1通道5
__STATIC_INLINE void DMA1_Channel5_Configure(void) {
    // 时钟
    RCC->AHBENR |= (1UL << 0UL); // 开启DMA1时钟
    // 开启全局中断
    NVIC_SetPriority(DMA1_Channel5_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0));
    NVIC_EnableIRQ(DMA1_Channel5_IRQn);
    /* 1. 禁用DMA通道5,等待其完全关闭 */
    DMA1_Channel5->CCR &= ~(1UL << 0);  // 清除EN位
    while(DMA1_Channel5->CCR & 1UL);    // 等待DMA通道5关闭

    /* 2. 配置外设地址和存储器地址 */
    DMA1_Channel5->CPAR = (uint32_t)&USART1->DR;  // 外设地址为USART1数据寄存器
    DMA1_Channel5->CMAR = (uint32_t)rx_buffer;    // 存储器地址为rx_buffer
    DMA1_Channel5->CNDTR = RX_BUFFER_SIZE;        // 传输数据长度为缓冲区大小
    /* 3. 配置DMA1通道5 CCR寄存器
         - DIR   = 0:外设→存储器
         - CIRC  = 1:循环模式
         - PINC  = 0:外设地址不自增
         - MINC  = 1:存储器地址自增
         - PSIZE = 00:外设数据宽度8位
         - MSIZE = 00:存储器数据宽度8位
         - PL    = 10:优先级设为高
         - MEM2MEM = 0:非存储器到存储器模式
    */
    DMA1_Channel5->CCR = 0;  // 清除之前的配置
    DMA1_Channel5->CCR |= (1UL << 5);       // 使能循环模式 (CIRC,bit5)
    DMA1_Channel5->CCR |= (1UL << 7);       // 使能存储器自增 (MINC,bit7)
    DMA1_Channel5->CCR |= (3UL << 12);      // 设置优先级为非常高 (PL置为“11”,bit12-13)
    
    // 增加传输完成与传输过半中断
    DMA1_Channel5->CCR |= (1UL << 1);       // 传输完成中断 (TCIE)
    DMA1_Channel5->CCR |= (1UL << 2);       // 传输过半中断 (HTIE)
    /* 4. 使能DMA通道5 */
    DMA1_Channel5->CCR |= 1UL;  // 置EN位启动通道
}

/**
  * @brief  当检测到USART1异常时,重新初始化USART1及其相关DMA通道。
  * @note   此函数首先停止DMA1通道4(USART1_TX)和通道5(USART1_RX),确保DMA传输终止,
  *         然后禁用USART1及其DMA接收请求,并通过读SR和DR清除挂起的错误标志,
  *         经过短暂延时后调用MX_USART1_UART_Init()重新配置USART1、GPIO和DMA,
  *         MX_USART1_UART_Init()内部已完成USART1中断的配置,无需额外使能。
  * @retval None
  */
void USART1_Reinit(void)
{
    /* 禁用DMA,如果USART1启用了DMA的话 */
    DMA1_Channel4->CCR &= ~(1UL << 0); // 禁用DMA1通道4:清除CCR的EN位(位0)
    while (DMA1_Channel4->CCR & 1UL) { // 等待DMA1通道4完全关闭(建议增加超时处理)
        // 空循环等待
    }
    DMA1_Channel5->CCR &= ~(1UL << 0); // 禁用DMA1通道5:清除CCR的EN位(位0)
    while (DMA1_Channel5->CCR & 1UL) { // 等待DMA1通道5完全关闭(建议增加超时处理)
        // 空循环等待
    }
    NVIC_DisableIRQ(USART1_IRQn); // 禁用USART1全局中断,避免重启过程中产生新的中断干扰
    USART1->CR3 &= ~(1UL << 6); // 禁用USART1的DMA接收请求:清除CR3的DMAR位(位6)
    USART1->CR1 &= ~(1UL << 13); // 禁用USART1:清除CR1的UE位(位13)
    
    // 读SR和DR以清除挂起的错误标志(例如IDLE、ORE、NE、FE、PE)
    volatile uint32_t tmp = USART1->SR;
    tmp = USART1->DR;
    (void)tmp;
    
    for (volatile uint32_t i = 0; i < 1000; i++); // 可选:短暂延时,确保USART1完全关闭
    tx_dma_busy = 0; // 复位发送标志!!!!!
    
    /* 重新初始化USART1、DMA1 */
    USART1_Configure(); // USART1
    DMA1_Channel4_Configure(); // DMA1通道4
    DMA1_Channel5_Configure(); // DMA1通道5
}

1.4.3、测试

测试方法:当串口在频繁收发时,中途让串口重启,看看串口收发能不能恢复正常。
在这里插入图片描述
如上所示,一开始串口正在正常收发大量的数据。接着,设置全局变量rebootUsart1等于1后,在主循环里就会执行函数USART1_Reinit()将串口与DMA重新初始化。可以看到,重新初始化后,串口能正常恢复收发!!

二、监控DMA异常


2.1、寄存器DMA_ISR

在这里插入图片描述
DMA每一个通道都只有一个错误标志TEIFx。 如上所示,例如,当TEIF5被置1时,表示DMA1通道5的传输发生了错误。
在这里插入图片描述
清除DMA通道的传输错误,使用寄存器DMA_IFCR。 如上所示,例如,当DMA1通道5发生传输错误之后,寄存器DMA_ISR的位19-TEIF5会被单片机系统置1。此时,通过软件将寄存器DMA_IFCR的位19-CTEIF置1,单片机系统就会清除错误。

2.2、代码实现(LL库)

/* LL库 */
uint16_t dma1Channel4Error = 0;
uint16_t dam1Channel5Error = 0;

/* DMA1通道4,USART1发送 */
static __inline uint8_t DMA1_Channel4_Error_Handler(void) {
    // 检查通道4是否发生传输错误(TE)
    if (LL_DMA_IsActiveFlag_TE4(DMA1)) {
		// 清除传输错误标志
        LL_DMA_ClearFlag_TE4(DMA1);
        // 禁用DMA通道4,停止当前传输
        LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_4);
        // 清除USART1的DMA发送请求(DMAT位)
        LL_USART_DisableDMAReq_TX(USART1);
        // 清除发送标志!!
        tx_dma_busy = 0;
	    return 1;
    } else {
	    return 0;
    }
}

void DMA1_Channel4_IRQHandler(void) {
	if (DMA1_Channel4_Error_Handler()) { // 传输错误中断
		dma1Channel4Error++; // 统计错误次数
		tx_dma_busy = 0; // 复位发送标志
	} else if () {
		// 传输完成中断处理
	}
}

/* DMA1通道5,USART1接收 */
static __inline uint8_t DMA1_Channel5_Error_Hanlder(void) {
    // 检查通道5是否发生传输错误(TE)
    if (LL_DMA_IsActiveFlag_TE5(DMA1)) {
        // 清除传输错误标志
        LL_DMA_ClearFlag_TE5(DMA1);
        // 禁用DMA通道5,停止当前传输
        LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_5);
        // 重新设置传输长度,恢复到初始状态
        LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, RX_BUFFER_SIZE);
        // 重新使能DMA通道5,恢复接收
        LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_5);
        return 1;
    } else {
	    return 0;
    }
}

void DMA1_Channel5_IRQHandler(void) {
	if (DMA1_Channel5_Error_Hanlder()) {
		dam1Channel5Error++; // 统计错误次数
	} else if () {
		// 传输过半、传输完成中断处理
	}
}

2.3、代码实现(寄存器方式)

uint16_t dma1Channel4Error = 0;
uint16_t dam1Channel5Error = 0;

/* DMA1通道4,USART1发送 */
static __inline uint8_t DMA1_Channel4_Error_Handler(void) {
    // 检查传输错误(TE)标志,假设TE对应位(1UL << 15)(请根据具体芯片参考手册确认)
    if (DMA1->ISR & (1UL << 15)) {
	    // 清除TE错误标志
        DMA1->IFCR |= (1UL << 15);
        // 禁用DMA通道4
        DMA1_Channel4->CCR &= ~(1UL << 0);
        // 清除USART1中DMAT位
        USART1->CR3 &= ~(1UL << 7);
        // 清除发送标志!!
        tx_dma_busy = 0;
	    return 1;
    } else {
	    return 0;
    }
}

void DMA1_Channel4_IRQHandler(void) {
	if (DMA1_Channel4_Error_Handler()) { // 传输错误中断
		dma1Channel4Error++; // 统计错误次数
		tx_dma_busy = 0; // 复位发送标志
	} else if () {
		// 传输完成中断处理
	}
}

/* DMA1通道5,USART1接收 */
static __inline uint8_t DMA1_Channel5_Error_Hanlder(void) {
    // 检查传输错误(TE)标志,假设TE对应位(1UL << 19)(请确认具体位)
    if (DMA1->ISR & (1UL << 19)) {
	    // 清除错误标志
        DMA1->IFCR |= (1UL << 19);
        // 禁用DMA通道5
        DMA1_Channel5->CCR &= ~(1UL << 0);
        // 重置传输计数
        DMA1_Channel5->CNDTR = RX_BUFFER_SIZE;
        // 重新使能DMA通道5
        DMA1_Channel5->CCR |= 1UL;
        return 1;
    } else {
	    return 0;
    }
}

void DMA1_Channel5_IRQHandler(void) {
	if (DMA1_Channel5_Error_Hanlder()) {
		dam1Channel5Error++; // 统计错误次数
	} else if () {
		// 传输过半、传输完成中断处理
	}
}

DMA传输错误,需立即清除,以免影响数据收发。 统计错误次数跟串口统计错误次数的目的是一样的。将信息传递给上位机的GUI程序,提供发生单片机系统问题的一些参考信息。

三、超时保护机制


超时保护机制,实际就是超时重启机制。 这里只简单聊一下实现思路,不提供代码,因为实现的方式太多了,需要根据项目的实际情况来修改。比如,某个项目的STM32控制板跟上位机有很频繁的串口通讯。正常情况下,每1S至少有10个消息包下发给STM32控制板。那么,此时可以在一个软件定时器(回调周期5S)里,监控消息包的数量(可以在中断回调里用一个变量来统计收到的串口消息包)。如果数量没有变化,可以认为有通讯超时异常。可以调用函数USART1_Reinit()去重启USART、DMA等外设。如果超过20S都还没有收到任何消息,可以调用函数NVIC_SystemReset()重启单片机系统等等。

相关文章:

  • 织梦xml网站地图国际新闻最新消息2022
  • 网创项目平台好的seo平台
  • 如何查询网站备案进度大二网页设计作业成品
  • php网站制作工具抖音推广公司
  • 邦策网站建设长沙网站推广
  • 简单网站开发准备百度不让访问危险网站怎么办
  • Muduo网络库实现 [二] - Buffer模块
  • 计算机组成原理————计算机运算方法精讲<1>原码表示法
  • P4551 最长异或路径
  • c++生成html文件helloworld
  • 智能体开发平台与大模型关系图谱
  • 经典论文解读系列:MapReduce 论文精读总结:简化大规模集群上的数据处理
  • SpringBoot条件装配注解
  • 网络原理-TCP/IP
  • rviz可视化(一、可视化点云)
  • 4. Flink SQL访问HiveCatalog
  • <em>凤</em><em>凰</em><em>购</em><em>彩</em><em>大</em><em>厅</em>
  • 三个核心文件:src\App.vue文件,index.html文件,src\main.js文件 的关系与运行流程解析(通俗形象)
  • L2-037 包装机 (分数25)(详解)
  • DeepSeek协助优化-GTX750Ti文物显卡0.65秒卷完400MB float 音频512阶时域FIR
  • OTN(Optical Transport Network)详解
  • RK3588,V4l2 读取Gmsl相机, Rga yuv422转换rgb (dma), 实现零拷贝
  • 【Deep Reinforcement Learning Hands-On Third Edition】【序】
  • Python Django基于人脸识别的票务管理系统(附源码,文档说明)
  • 运算放大器(三)运算放大器的典型应用
  • Zoomlt使用