【解决方案】关于 UART 接收数据时丢失数据的解决办法——环形缓冲存储区

news2024/11/28 16:38:48

文章目录

  • UART 通信丢失数据的常见原因总结
  • 串口(UART)数据丢失 Bug 的复现
  • 引入环形队列解决数据丢失问题
  • 总结

在嵌入式系统和物联网(IoT)设备中,串行通信是一种非常普遍且重要的数据传输方式。无论是通过 UART、RS-232 还是其他串行接口,串口通信因其简单可靠而被广泛应用于各种场景,从简单的传感器读取到复杂的控制系统。然而,尽管串口通信具有诸多优点,但在实际应用中,开发者们常常会遇到一个令人头疼的问题——数据丢失。

数据丢失不仅可能导致信息不完整,还可能引发系统错误或导致关键操作失败。想象一下,在工业自动化控制中,如果某个指令未能正确传递,可能会造成生产流程的中断甚至安全问题;或者在一个医疗设备中,如果监测数据出现丢失,可能会对病人的健康状况判断产生严重的影响。因此,理解并解决串口接收时的数据丢失问题对于确保系统的稳定性和可靠性至关重要。

本文是我以往工作中遇到串口数据丢失的问题时,解决问题过程的总结归纳。文章将基于 STM32 HAL 库和环形队列,来解决其中一种最常见的 UART 数据丢失的问题,以帮助开发者减少或避免此类问题的发生。

在这里插入图片描述


UART 通信丢失数据的常见原因总结

在使用 UART 进行通信时,数据丢失是一个常见的问题。以下是一些导致 UART 接收数据时丢失数据的常见原因:

  1. 波特率不匹配

    这是 UART 丢失数据的众多原因中,最好解决的一种情况,只要重新配置设备的波特率即可。

  2. 缓冲区溢出

    接收方的缓冲区太小,来不及处理的数据被覆盖,或是缓冲区溢出,超出缓冲区存储空间部分的数据没被保存。

  3. 硬件中断或干扰

    外部电磁干扰、电源波动等可能导致信号失真。这属于硬件问题,通常使用屏蔽电缆,确保良好的接地,远离强电磁场源,使用滤波器或稳压器来减少电源波动的影响。

  4. 软件错误

    接收代码中的 bug,如未正确读取缓冲区数据、错误处理不当等。这属于工作失误,好好反省一下自己,然后仔细检查和测试接收代码,确保数据读取和处理逻辑正确无误。

  5. 流控机制失效

    硬件流控(RTS/CTS)或软件流控(XON/XOFF)没有被正确配置或实现。同第一条和第四条一样,工作失误,重新确认流控机制是否启用,确保流控信号线连接正确。

  6. 信号完整性问题

    信号在线缆上传输过程中衰减或受到噪声影响。硬件问题,找硬件工程师解决,通常的解决办法是使用高质量的电缆,缩短线缆长度,使用终端电阻或信号放大器来改善信号质量。

  7. 超时设置不合理

    接收超时时间设置得太短,可能会误认为没有数据到达而关闭接收操作。可以通过多次 debug,根据实际应用情况合理设置超时时间,确保有足够的时间来接收数据。实在不会,直接设置成最大值。

以上众多原因中,除了比较基础的配置问题导致,还有就是硬件问题。那么,排除硬件问题和一些非常低级的错误,个人觉得,对于初学者或者刚刚入行的菜鸟程序员来说,需要花一些时间解决的就是第二条,接下来将复现 UART 接收数据时,丢失数据的现象。以及用环形缓冲存储区解决问题的过程。

串口(UART)数据丢失 Bug 的复现

本次复现是基于 STM32F103 单片机,复现 Bug 之前,先来了解一下 STM32 有多少种 UART 的编程方式。根据 STM32F103 的 UART 硬件框架,STM32F103 有三种不同编程方法:

  1. 查询方式
    • 发送数据:先把准备发送的数据写入 TDR(Transmit Data Register),然后判断 TDR 是否空,为空则返回。
    • 接收数据:先判断 RDR(Receive Data Register)是否为非空,如果为非空状态,则读取 RDR 数据。
  2. 中断方式
    • 发送数据:先启用 TXE(Transmit buffer empty)中断,然后在 TXE 中断处理函数中,从程序的发送 buffer 里取出一个字节的数据,写入 TDR。等再次触发 TXE 中断时,再从发送 buffer 里取出下一个数据写入 TDR,如此循环,直到发送 buffer 中的数据全部发送完毕。最后禁用 TXE 中断。
    • 接收数据:先启用 RXNE(Receive buffer not empty)中断,当 UART 接收器接收到一个数据时,会立刻触发中断。在中断程序中,会读取 RDR 的数据并存入接收 buffer 中。最后禁用 RXNE 中断。如果需要处理串口接收数据的话,直接读取接收 buffer 的数据即可。
  3. DMA 方式
    • 发送数据:DMA 从 SRAM 得到数据,写入 UART 的 TDR。
    • 接收数据:DMA 从 UART 的 RDR 读取数据,并写入 SRAM。
    • 以上两个动作,任意一个完成都会触发 DMA 中断,可以作为完成标志。

对于以上三种编程方式,使用的函数如下表:

查询方式中断方式DMA 方式
发送HAL_UART_TransmitHAL_UART_Transmit_IT
HAL_UART_TxCpltCallback
HAL_UART_Transmit_DMA
HAL_UART_TxHalfCpltCallback
HAL_UART_TxCpltCallback
接收HAL_UART_ReceiveHAL_UART_Receive_IT
HAL_UART_RxCpltCallback
HAL_UART_Receive_DMA
HAL_UART_RxHalfCpltCallback
HAL_UART_RxCpltCallback
错误HAL_UART_ErrorCallbackHAL_UART_ErrorCallback

工作中最常用的还是中断方式和 DMA 方式,不过,使用中断方式编程的过程中,软件处理不得当,一样会出现接收数据丢失的风险,本文也为这种编程方式重点阐述如何解决这种风险。当然,使用 DMA 方式编程,基本不当心数据会丢失(只要空间足够大),再配合空闲中断(Idle Interrupt)是一种在数据传输过程中优化资源利用和提高系统响应性的技术。这种组合可以确保数据能够高效且可靠地传输。所以 DMA 方式不是本文的讨论重点。

[!NOTE]

其实有些应用场合并不适用 DMA 方式编程,DMA 方式是一次性将单次的接收到的数据写入 SRAM, 其实这样做的效率并不高,而且可能接收到无效的数据。例如,在一些私有协议中,对数据帧都会规定帧头和帧尾的格式,通常做法是一边接收数据一边解析数据,如果发现帧头是错的,就抛弃本次接收的数据。

本次复现 Bug 所用的程序是将接收到的数据再发送出去,项目初始化由 STM32CubeMX 生成,组态过程不做展示,串口基本设置如下:

void MX_USART1_UART_Init(void)
{
    huart1.Init.BaudRate = 115200;                    // 设置波特率为 115200bps
    huart1.Init.WordLength = UART_WORDLENGTH_8B;      // 设置数据位为 8 位
    huart1.Init.StopBits = UART_STOPBITS_1;           // 设置停止位为 1 位
    huart1.Init.Parity = UART_PARITY_NONE;            // 设置无校验位
    huart1.Init.Mode = UART_MODE_TX_RX;               // 设置模式为发送和接收(全双工)
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;      // 禁用硬件流控
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;  // 设置过采样率为16倍

    if (HAL_UART_Init(&huart1) != HAL_OK) {
        Error_Handler();
    }
}

先使用一个字节作为串口接收数据的缓冲存储区,程序不展示全部代码,只展示人为输入的代码,如下:

uint8_t c;
volatile uint8_t RxCplt = 0;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        RxCplt = 1;
        HAL_UART_Receive_IT(&huart1, &c, 1);
    }
}

int main(void)
{
    const uint8_t *str = "Grayson Zheng\r\n";

    ...
        
    HAL_UART_Transmit(&huart1, str, strlen(str), HAL_MAX_DELAY);	// 先测试一下串口功能
    HAL_UART_Receive_IT(&huart1, &c, 1);	//启动接收中断

    while (1) {
        /* 如果接收缓冲区接受到了数据,就将缓冲区内容发送出来 */
        if (RxCplt) {
           RxCplt = 0;
           HAL_UART_Transmit(&huart1, (const uint8_t *)&c, 1, HAL_MAX_DELAY);
        }
    }
}

RxCplt 作为数据接收的标志,当 RxCplt 为 1 时,说明触发了串口接收中断,就可以将接收到的数据回传,同时将 RxCplt 再次置为 0,等待下一个数据。

执行结果如下图,对于单个数据和单次少量的数据,可以做到完整的数据回传,但是单次数据量较大时,就会有数据丢失的风险。

在这里插入图片描述

此时,绝大多数人能想到的解决办法就是把接收缓冲存储区变大,使当次可接收到的数据更多,于是就有如下的代码修改方案:

#define BUFFER_SIZE 16

uint8_t c = 0;
uint8_t RxBuffer[BUFFER_SIZE] = {0};
uint8_t RxIndex = 0;
volatile uint8_t RxCplt = 0;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        if (RxIndex < BUFFER_SIZE)
            RxBuffer[RxIndex++] = c;
        else
            RxIndex = 0;

        RxCplt = 1;
        HAL_UART_Receive_IT(&huart1, &c, 1);
    }
}

void ProcessReceivedData(UART_HandleTypeDef *huart)
{
    for (uint8_t i = 0; i < RxIndex; i++)
        HAL_UART_Transmit(huart, (const uint8_t *)&RxBuffer[i], 1, HAL_MAX_DELAY);

    memset(RxBuffer, 0, BUFFER_SIZE);
    RxIndex = 0;
}

int main(void)
{
    /* USER CODE BEGIN 1 */
    const uint8_t *str = "Grayson Zheng\r\n";

	...

    HAL_UART_Transmit(&huart1, str, strlen((const char *)str), HAL_MAX_DELAY);
    HAL_UART_Receive_IT(&huart1, &c, 1);

    while (1) {
        if (RxCplt) {
           RxCplt = 0;
           ProcessReceivedData(&huart1);
        }
}

该方案添加了一个 16 字节的接收缓冲存储区,将接收到的每个字节都依次写入接收缓冲存储区,再通过 ProcessReceivedData 函数回传数据。效果如下图:

在这里插入图片描述

为接收更多字节,我们把接收缓冲存储区扩大到 128 字节。

不过,这种做法显然治标不治本。虽然更大的缓冲存储区确实可以接收单次以内较大数量的数据,但是如果单次接收到的数据依然大于缓冲存储区的最大长度,还是会丢失数据。

在这里插入图片描述

[!NOTE]

通常情况下,128KB 的缓冲存储区是可以解决大部分项目的需求,但是不排除有些特殊的项目需求,接收的数据量不确定。例如,需要实现 Modbus-RTU 协议的项目,当需要读取外设连续的存储器的数值时,就可能有大量数据要接收。而且这个过程,是边接收边解析数据,并不是一次性接收完后,再进行解析。

因此更适用于这种情况的解决方法就是把缓冲存储区设计成循环缓冲区(环形队列)。

引入环形队列解决数据丢失问题

关于环形队列的介绍,在我之前的博客《【数据结构】环形队列(循环队列)学习笔记总结》做过详细的总结。没接触过或不熟悉环形队列的小伙伴,建议先移步阅读一下此博客。该博客中提供的环形队列的库由我自己编写,会直接引用到本次的解决方案中,该库可能不适用于所有应用场景,也欢迎各位高手指正。

首相把整个库复制到项目中,为了区分 STM32CubeMX 生成的库文件,我在项目根目录下新建了 Lib 文件(个人做项目的习惯)。

在这里插入图片描述

[!NOTE]

我个人习惯用 VSCode 做项目,对于使用 Keil 做项目的小伙伴,这里简单的教一下怎么在 Keil 中添加文件夹。已经会的小伙伴请跳过这段。

先点击 ”魔术棒“,在弹窗中选择 ”C/C++“ 选项卡,再点击 ”Include Paths“ 输入框最右边的三个点的按钮。 在这里插入图片描述
点击插入,再点击三个点的按钮。
在这里插入图片描述
找到要添加的文件夹的路径,选择文件夹后点击 ”选择文件夹“。
在这里插入图片描述
添加进来后,按顺序点击 ”OK“ 关掉弹窗。
在这里插入图片描述
点击 ”品“ 字形的图标,弹窗中点击 ”Groups“ 的插入按钮,在下面的空白框输入刚刚添加进来的文件夹的名字(要保证一致,不能写错,字母大小写有区别),然后再点击右下角的 ”Add files……“。
在这里插入图片描述
再次出现弹窗,在文件类型中选择 ”All files“,把要添加进来的文件,挨个双击添加,这里不会提示是否已经添加,所以添加完毕后直接关掉弹窗。
在这里插入图片描述
这里可以看到已经添加进来,直接点 ”OK“。
在这里插入图片描述
在项目树上也可以看到。
在这里插入图片描述

先在主代码文件上加载头文件:

#include "circular_queue.h"

再定义一个临时接收变量 c,一个接收缓冲存储区 RxBuffer,长度为 16 个字节,用宏定义 BUFFER_SIZE 指代,方便后期修改长度。此时的接收缓冲存储区在逻辑上还不是环形缓冲存储区,需要用到 circular_queue_t 结构体进行管理,所以定义一个 RxQueue 的结构体变量。标志位 RxCplt,用于标记接收中断是否完成。

#define BUFFER_SIZE 16

uint8_t c = 0;
uint8_t RxBuffer[BUFFER_SIZE] = {0};
circular_queue_t RxQueue = {0};
volatile uint8_t RxCplt = 0;

main 函数中调用环形缓冲存储区初始化函数:

circular_queue_init(&RxQueue, RxBuffer, BUFFER_SIZE);

如果接收到数据,把数据写入环形缓冲存储区就是入队操作,需要调用入队函数,这个动作在串口接收中断的回调函数中完成:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        if (!circular_queue_is_full(&RxQueue))	// 判断队列是否满了
            circular_queue_enqueue(&RxQueue, c);  // 将接收到的数据入队

        RxCplt = 1;
        HAL_UART_Receive_IT(&huart1, &c, 1);
    }
}

把环形缓冲存储区的数据取出是出队操作,需要调用出队函数:

void ProcessReceivedData(UART_HandleTypeDef *huart)
{
    while (!circular_queue_is_empty(&RxQueue)) {	// 判断队列是否为空
        uint8_t data;
        if (circular_queue_dequeue(&RxQueue, &data))	// 将数据取出
            HAL_UART_Transmit(huart, (const uint8_t *)&data, 1, HAL_MAX_DELAY);
    }
}

主体代码如下(已删去不必要的注释和与主题关联性不大的代码片段):

#include "circular_queue.h"

#define BUFFER_SIZE 16

uint8_t c = 0;
uint8_t RxBuffer[BUFFER_SIZE] = {0};
circular_queue_t RxQueue = {0};
volatile uint8_t RxCplt = 0;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        if (!circular_queue_is_full(&RxQueue))
            circular_queue_enqueue(&RxQueue, c);  // 将接收到的数据入队

        RxCplt = 1;
        HAL_UART_Receive_IT(&huart1, &c, 1);
    }
}

void ProcessReceivedData(UART_HandleTypeDef *huart)
{
    while (!circular_queue_is_empty(&RxQueue)) {
        uint8_t data;
        if (circular_queue_dequeue(&RxQueue, &data))
            HAL_UART_Transmit(huart, (const uint8_t *)&data, 1, HAL_MAX_DELAY);
    }
}

int main(void)
{

    const uint8_t *str = "Grayson Zheng\r\n";

	/* 其他初始化代码在此处省略 */
    
    circular_queue_init(&RxQueue, RxBuffer, BUFFER_SIZE);		// 初始化环形缓冲存储区
    HAL_UART_Transmit(&huart1, str, strlen((const char *)str), HAL_MAX_DELAY);	// 测试串口发送
    HAL_UART_Receive_IT(&huart1, &c, 1);	// 打开串口接收中断

    while (1) {
        if (RxCplt) {
           RxCplt = 0;
           ProcessReceivedData(&huart1);
        }
    }
}

测试结果如下图所示,对单个数据、单次少量数据、单次最大数据、和单次数倍于最大量做了测试,均没有出现数据丢失的现象。

在这里插入图片描述

但是连续多出大数量后,数据出现混乱的现象:

在这里插入图片描述

原因在于 RxCplt 这个标志响应不及时导致,解决方案就是不再通过 RxCplt 判断,而是通过环形队列剩余的数量判断,代码如下:

#include "circular_queue.h"

#define BUFFER_SIZE 16

uint8_t c = 0;
uint8_t RxBuffer[BUFFER_SIZE] = {0};
circular_queue_t RxQueue = {0};

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        if (!circular_queue_is_full(&RxQueue))
            circular_queue_enqueue(&RxQueue, c);  // 将接收到的数据入队

        HAL_UART_Receive_IT(&huart1, &c, 1);
    }
}

void ProcessReceivedData(UART_HandleTypeDef *huart)
{
    while (!circular_queue_is_empty(&RxQueue)) {
        uint8_t data;
        if (circular_queue_dequeue(&RxQueue, &data))
            HAL_UART_Transmit(huart, (const uint8_t *)&data, 1, HAL_MAX_DELAY);
    }
}

int main(void)
{
    const uint8_t *str = "Grayson Zheng\r\n";

    /* 其他初始化代码在此处省略 */

    circular_queue_init(&RxQueue, RxBuffer, BUFFER_SIZE);
    HAL_UART_Transmit(&huart1, str, strlen((const char *)str), HAL_MAX_DELAY);
    HAL_UART_Receive_IT(&huart1, &c, 1);

    while (1) {
        if (RxQueue.count)		// 判断环形队列中的元素个数
           ProcessReceivedData(&huart1);
    }
}

测试结果如下图,数据不再丢失,回传也准确无误。

在这里插入图片描述

总结

对于本次解决串口接收丢失数据的改进措施就是使用了环形队列库来管理接收数据,确保数据不会丢失。在中断回调函数中直接操作全局变量 c 和标志位 RxCplt 是一种常见做法,随后也放弃了 RxCplt ,用环形队列管理结构体的成员变量 count 取代,这样可以提高代码的可维护性和减少潜在的竞态条件,也是更安全的方法。通过这些改进,代码变得更加健壮和高效,能够更好地处理连续的数据流。各位读者也可以根据实际需求进一步调整缓冲区大小和其他参数。

当然,这里还有一个改进建议,就是在中断回调函数中,对于队列已满的情况没有做特殊处理,建议读者根据自己的需求,增加了对队列溢出的处理,并可以在需要时添加更多的错误处理机制。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        if (!circular_queue_is_full(&rxQueue)) {
            circular_queue_enqueue(&rxQueue, c);  // 将接收到的数据入队
        } else {
            // 队列已满,丢弃新数据
            // 这里可以根据需要添加日志或警告
        }

        RxCplt = 1;
        HAL_UART_Receive_IT(&huart1, &c, 1);
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Godot4.3】基于中心点连线的矩形重叠检测

概述 这个方法是我自己想到的&#xff0c;经典的矩形重叠&#xff08;碰撞&#xff09;检测&#xff0c;是一段很复杂的逻辑判断&#xff0c;而根据两个矩形中点连线&#xff0c;与两个矩形宽度和高度之和一半的比较&#xff0c;就可以判断两个矩形是否重叠&#xff0c;并且能…

SQL进阶技巧:统计各时段观看直播的人数

目录 0 需求描述 1 数据准备 2 问题分析 3 小结 如果觉得本文对你有帮助&#xff0c;那么不妨也可以选择去看看我的博客专栏 &#xff0c;部分内容如下&#xff1a; 数字化建设通关指南 专栏 原价99&#xff0c;现在活动价39.9&#xff0c;十一国庆后将上升至59.9&#…

TransFormer 视频笔记

TransFormer BasicsAttention单头注意力 single head attentionQ&#xff1a; query 查寻矩阵 128*12288K key matrix 128*12288SoftMax 归一 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/19e3cf1ea28442eca60d5fc1303921f4.png)Value matrix 12288*12288 MLP Bas…

边缘概率 | 条件概率

关于什么是边缘概率分布和条件概率分布&#xff0c;在理论上&#xff0c;我自己也还没有理解&#xff0c;那么现在就根据我学习到的理解方式来记录一下&#xff0c;有错误指出&#xff0c;请大家指正&#xff01;&#xff01;&#xff01; 例如&#xff0c;一个箱子里有十个乒乓…

YOLO11改进|上采样篇|引入CARAFE上采样模块

目录 一、CARAFE上采样模块1.1CARAFE上采样模块介绍1.2CARAFE核心代码 五、添加MLCA注意力机制5.1STEP15.2STEP25.3STEP35.4STEP4 六、yaml文件与运行6.1yaml文件6.2运行成功截图 一、CARAFE上采样模块 1.1CARAFE上采样模块介绍 CARAFE 的主要思想&#xff1a; 将特征图的上采…

C# (.net6)实现Redis发布和订阅简单案例

概念&#xff1a; 在 .NET 6 中使用 Redis 的/订发布阅模式。发布/订阅&#xff08;Pub/Sub&#xff09;是 Redis 支持的一种消息传递模式&#xff0c;其中一个或多个发布者向一个或多个订阅者发送消息,Redis 客户端可以订阅任意数量的频道。 多个客户端可以订阅一个相同的频道…

【Java】—— 集合框架:Collection接口中的方法与迭代器(Iterator)

目录 1. 集合框架概述 1.1 生活中的容器 1.2 数组的特点与弊端 1.3 Java集合框架体系 1.4 集合的使用场景 2. Collection接口及方法 2.1 添加 2.2 判断 2.3 删除 2.4 其它 3. Iterator(迭代器)接口 3.1 Iterator接口 3.2 迭代器的执行原理 3.3 foreach循环 1. 集…

【rust/egui/android】在android中使用egui库

文章目录 说在前面AndroidStudio安装编译安装运行问题 说在前面 操作系统&#xff1a;windows11java版本&#xff1a;23android sdk版本&#xff1a;35android ndk版本&#xff1a;22rust版本&#xff1a; AndroidStudio安装 安装AndroidStudio是为了安装sdk、ndk&#xff0c;…

【Matlab绘图】从Excel导入表格并进行三维绘图

前言 今天手头上拿到一份论文的xlsx数据&#xff0c;要求使用MATLAB绘制进行三维图标坐标绘制。那么我们来看看如何使用如下数据进行绘图。 如上数据所示&#xff0c;数据是一个30行25列的数据&#xff0c;数据的内容是论文某项模型模拟的结果&#xff0c;我们希望把横坐标x取…

【ADC】ADC 系统失调和增益误差的校准方法

概述 本文学习于TI 高精度实验室课程&#xff0c;讨论如何计算增益和偏移误差并通过校准消除。首先对数据转换器系统进行偏移和增益误差计算&#xff0c;然后讨论几种校准此误差的方法。最后介绍一些难以校准的误差源。 文章目录 概述一、误差校准原理与方法二、实际应用问题举…

Linux进程概念二

我们利用fork函数来辅助我们讲解进程 首先认识两个调用进程地址的函数&#xff1a;getpid(),和getppid()&#xff0c;他们分别可以调用自己的pid和父进程的pid fork()可以在代码层面来创建子进程&#xff0c;一般而言&#xff0c;父进程fork出来的子进程与父进程代码和数据相…

【Docker】配置文件

问题 学习Docker期间会涉及到docker的很多配置文件&#xff0c;可能会涉及到的会有&#xff1a; /usr/lib/systemd/system/docker.service 【docker用于被systemd管理的配置文件】 /etc/systemd/system/docker.service.d【覆盖配置文件的存放处】 /etc/systemd/system/mul…

[Cocoa]_[初级]_[绘制文本如何设置断行效果]

场景 在开发Cocoa程序时&#xff0c;表格NSTableView是经常使用的控件。其基于View Base的视图单元格模式就是使用NSCell或其子类来控制每个单元格的呈现。当一个单元格里的文字过多时&#xff0c;需要截断超出宽度的文字&#xff0c;怎么实现&#xff1f; 说明 Cocoa下的文本…

【java数据结构】顺序表

【java数据结构】顺序表 一、了解List接口二、顺序表2.1 线性表2.2 顺序表2.2.1 顺序表接口的实现给数组增加新元素判断数组数据是否为满在 pos 位置新增元素判定是否包含某个元素查找某个元素对应的位置获取 pos 位置的元素给 pos 位置的元素设为 value删除第一次出现的关键字…

Manim:使用Python绘制数学动画

Manim是一个由3Blue1Brown的Grant Sanderson开发的开源框架&#xff0c;用户可以通过编写Python代码来创建数学动画&#xff0c;适用于教学、科研和科普宣传等多个领域。 Manim的核心功能之一是动画效果的创建和控制。它提供了多种动画效果&#xff0c;如创建、变换、淡入淡出…

分布式数据库知识详解

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

QT学习笔记3.1(建立项目、执行_建立第一个工程)

QT学习笔记3.1&#xff08;建立项目、执行_建立第一个工程) 建立第一个工程&#xff0c;使用widget模板 使用的版本是 Qt Creator 4.11.0 Based on Qt 5.14.0 (MSVC 2017, 32 bit) 1.选择Application-》QT Widget Application&#xff08;最常使用&#xff09; 2.项目保存位…

使用Pytorch构建自定义层并在模型中使用

使用Pytorch构建自定义层并在模型中使用 继承自nn.Module类&#xff0c;自定义名称为NoisyLinear的线性层&#xff0c;并在新模型定义过程中使用该自定义层。完整代码可以在jupyter nbviewer中在线访问。 import torch import torch.nn as nn from torch.utils.data import T…

LSM6DSV16X基于MLC智能笔动作识别(2)----MLC数据采集

LSM6DSV16X基于MLC智能笔动作识别.2--MLC数据采集 概述视频教学样品申请源码下载输出速率执行流程速率设置量程设置检测状态数据单位采集数据静止(Steady)闲置(Idle)书写(Writing)其他(other) 概述 MLC 是“机器学习核心”&#xff08;Machine Learning Core&#xff09;的缩写…

全球购的智能引擎:AI与RPA如何重塑跨境电商帝国?

在全球化的大潮中&#xff0c;跨境电商已成为连接世界的桥梁。随着人工智能&#xff08;AI&#xff09;和机器人流程自动化&#xff08;RPA&#xff09;技术的飞速发展&#xff0c;跨境电商领域的运作模式正在经历一场革命性的变革。 一、跨境电商的挑战 随着互联网技术的普及…