仪表盘Can数据接收(2)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

229.4.3 仪表盘Can数据接收(2)

/* 本节目的:把每项仪表的数据与刷新标志放在同一个结构体内,实现收到新报文后才更新对应对象。 */
/* 以下为 can_handle.h 的本节末态:先定义公共类型,再声明五个全局对象和处理函数。 */
/* 日期、时间指令本节明确暂不处理,所以不提前收录后续 ui_date、ui_time 及其分支。 */
#ifndef __CAN_HANDLE_H__                      /* 第一次包含时允许展开,防止同一翻译单元重复处理这个头文件。 */
#define __CAN_HANDLE_H__                      /* 标记本头文件已展开,宏名沿用课堂源码。 */
#include "main.h"                             /* 提供 uint32_t、uint8_t 等工程基础类型。 */
typedef struct                               /* 声明一个结构体类型,并用 typedef 为它建立简短类型名。 */
{                                            /* 结构体成员定义开始;这里只描述布局,还没有创建具体对象。 */
    uint32_t ui_value;                        /* 保存该项目最新收到的数值,如指针百分比或报警状态。 */
    uint8_t ui_freshflag;                     /* 保存是否有待更新数据的标志:0 无更新请求,1 有更新请求。 */
} ui_Typedef;                                /* 结束结构体类型定义,以后用 ui_Typedef 声明具体对象。 */
extern ui_Typedef ui_rpm;                     /* 声明指针对象的定义在 can_handle.c 中,使 main.c 使用同一份对象。 */
extern ui_Typedef ui_water_temp;              /* 声明水温图标对象,包含状态值和刷新标志。 */
extern ui_Typedef ui_safty_belt;              /* 声明安全带对象,保留课程 safty 的拼写,不擅自改接口名称。 */
extern ui_Typedef ui_light;                   /* 声明灯光对象,接收白、黄、红三种状态。 */
extern ui_Typedef ui_turn_light;              /* 声明转向灯对象,控制白色与红色状态。 */
void Can_Handle(void);                        /* 对外声明接收解析函数,主循环通过它更新上述对象。 */
#endif                                       /* 结束头文件重复包含保护。 */

can_handle.c——本节完成的五类数据接收与置位:

#include "main.h"                             /* 引入公共类型和工程声明。 */
#include "canif.h"                            /* 引入上一节复用的 Can_Recv_Msg() 接收接口。 */
#include "can.h"                              /* 引入 CAN 外设声明,与课堂文件保持一致。 */
#include "can_handle.h"                       /* 取得 ui_Typedef 类型和本模块公开接口。 */
ui_Typedef ui_rpm;                            /* 定义指针数据对象;文件作用域对象启动时成员置零。 */
ui_Typedef ui_water_temp;                     /* 定义水温对象,ui_value 在这里表示报警状态而非温度数值。 */
ui_Typedef ui_safty_belt;                     /* 定义安全带对象,字段命名沿用课程源码。 */
ui_Typedef ui_light;                          /* 定义灯光对象,独立保存它的数据与待更新状态。 */
ui_Typedef ui_turn_light;                     /* 定义转向灯对象,不与灯光对象共用刷新标志。 */
void Can_Handle(void)                         /* 每轮主循环调用,尝试从 CAN FIFO0 取出一帧。 */
{                                            /* CAN 应用处理函数体开始。 */
    uint32_t recv_len;                        /* 保存接收接口返回的数据长度。 */
    uint32_t recv_id;                         /* 保存标准 CAN 标识符,通过接口指针输出参数获得。 */
    uint8_t rx_data[8] = {0};                 /* 分配并清零最多 8 字节的数据接收数组。 */
    recv_len = Can_Recv_Msg(&recv_id, rx_data); /* 取一帧,填入 ID 和字节数据,并返回其 DLC。 */
    if(recv_len > 0)                          /* 课程先检查长度非零;严格的 8 字节长度校验仍未加入。 */
    {                                        /* 有非零长度数据,继续识别应用消息。 */
        if(recv_id == 0x0B4)                  /* 筛选发送端与接收端共同约定的标准 CAN ID。 */
        {                                    /* CAN ID 满足仪表业务约定。 */
            if(rx_data[0] == 0xAF)            /* 检查 Byte0 是否为课程自定义数据头。 */
            {                                /* 数据头正确,接着按 Byte1 选择唯一功能分支。 */
                if(rx_data[1] == 0x1)         /* 功能码 1 表示指针位置数据。 */
                {                            /* 指针分支开始。 */
                    ui_rpm.ui_value = rx_data[2]; /* 保存 Byte2 的 0~100 指针位置百分比。 */
                    ui_rpm.ui_freshflag = 1;  /* 通知 main.c:指针收到了一次新的更新请求。 */
                }                            /* 指针分支结束。 */
                else if(rx_data[1] == 0x2)    /* 功能码 2 表示水温报警状态,与上一个分支互斥。 */
                {                            /* 水温分支开始。 */
                    ui_water_temp.ui_value = rx_data[2]; /* 保存水温报警状态,协议约定 0 白色、1 红色。 */
                    ui_water_temp.ui_freshflag = 1; /* 仅请求刷新水温图标,不改变其他项目标志。 */
                }                            /* 水温分支结束。 */
                else if(rx_data[1] == 0x3)    /* 功能码 3 表示安全带状态,原笔记的 13 是识别错误。 */
                {                            /* 安全带分支开始。 */
                    ui_safty_belt.ui_value = rx_data[2]; /* 保存该图标的状态值。 */
                    ui_safty_belt.ui_freshflag = 1; /* 请求 main.c 更新安全带图标。 */
                }                            /* 安全带分支结束。 */
                else if(rx_data[1] == 0x4)    /* 功能码 4 表示灯光状态,原笔记的 14 是识别错误。 */
                {                            /* 灯光分支开始。 */
                    ui_light.ui_value = rx_data[2]; /* 保存协议约定的 0 白、1 黄、2 红状态值。 */
                    ui_light.ui_freshflag = 1; /* 置位灯光对象的刷新标志。 */
                }                            /* 灯光分支结束。 */
                else if(rx_data[1] == 0x5)    /* 功能码 5 表示转向灯状态,原笔记的 15 是识别错误。 */
                {                            /* 转向灯分支开始。 */
                    ui_turn_light.ui_value = rx_data[2]; /* 保存转向灯当前状态。 */
                    ui_turn_light.ui_freshflag = 1; /* 请求 main.c 更新转向灯图标。 */
                }                            /* 本节最后一个功能分支结束。 */
            }                                /* 应用数据头检查结束。 */
        }                                    /* CAN ID 检查结束。 */
    }                                        /* 长度非零检查结束。 */
}                                            /* 处理完一帧后返回,后续界面操作由 main.c 执行。 */

main.c——沿用的颜色宏与颜色变量:

#define COLOR_RED 0xff,0x0,0x0                /* 宏展开为三个实参 R=255、G=0、B=0,表示红色。 */
#define COLOR_WHITE 0xff,0xff,0xff            /* 宏展开为 R=255、G=255、B=255,表示白色。 */
#define COLOR_YELLOW 0xff,0xff,0x0            /* 宏展开为 R=255、G=255、B=0,表示黄色。 */
lv_color_t color_value;                      /* 沿用文件作用域的 LVGL 颜色变量,供各图标分支依次赋值使用。 */

main.c——本节在 while(1) 内写到的接收与五项显示处理:

Can_Handle();                               /* 每轮先解析一帧 CAN 数据,有有效业务数据时相应对象标志被置 1。 */
if(ui_rpm.ui_freshflag == 1)                 /* 仅当指针收到新报文时调用它的角度设置接口。 */
{                                           /* 指针更新分支开始。 */
    ui_rpm.ui_freshflag = 0;                 /* 消费本次更新请求,下一轮没有新数据就不会再次进入。 */
    lv_img_set_angle(ui_ImageNeedle, ui_rpm.ui_value*2500/100); /* 将 0~100 百分比映射到 0~250.0°,乘法先于整数除法执行。 */
}                                           /* 指针更新分支结束。 */
if(ui_water_temp.ui_freshflag == 1)          /* 判断水温对象是否有新的报警状态要显示。 */
{                                           /* 水温图标更新分支开始。 */
    ui_water_temp.ui_freshflag = 0;          /* 清除本次水温更新标志,避免下一轮重复设置图标。 */
    if(ui_water_temp.ui_value == 0)          /* 状态为零时,按课堂约定显示白色。 */
    {                                       /* 水温白色分支开始。 */
        color_value = lv_color_make(COLOR_WHITE); /* 将宏展开的三个 RGB 分量转换为 LVGL 使用的颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); /* 设置水温图片主部分默认状态的重着色颜色。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); /* 设置完全重着色;255 是最大混合强度而非报警值。 */
    }                                       /* 水温白色分支结束。 */
    else                                    /* 课程将所有非零值都当作红色,并未严格限定为 1。 */
    {                                       /* 水温红色分支开始。 */
        color_value = lv_color_make(COLOR_RED); /* 生成红色颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); /* 对水温对象应用红色重着色。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); /* 使重着色完整生效,原图片透明区域仍由其 alpha 控制。 */
    }                                       /* 水温红色分支结束。 */
}                                           /* 水温图标本次更新结束。 */
if(ui_safty_belt.ui_freshflag == 1)          /* 判断安全带对象是否收到更新请求。 */
{                                           /* 安全带图标更新分支开始。 */
    ui_safty_belt.ui_freshflag = 0;          /* 消费本次安全带状态更新。 */
    if(ui_safty_belt.ui_value == 0)          /* 状态为零时显示白色。 */
    {                                       /* 安全带白色分支开始。 */
        color_value = lv_color_make(COLOR_WHITE); /* 生成白色颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); /* 设置安全带图片颜色;保留课程对象名 Sfaty 的拼写。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); /* 启用完全重着色,使白色状态可见。 */
    }                                       /* 安全带白色分支结束。 */
    else                                    /* 课程把非零安全带状态显示为红色。 */
    {                                       /* 安全带红色分支开始。 */
        color_value = lv_color_make(COLOR_RED); /* 生成红色颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); /* 红色作用于安全带对象,避免误用复制来源的水温对象。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); /* 使安全带红色重着色完全生效。 */
    }                                       /* 安全带红色分支结束。 */
}                                           /* 安全带本次更新结束。 */
if(ui_light.ui_freshflag == 1)               /* 判断灯光对象是否收到新的颜色状态。 */
{                                           /* 灯光更新分支开始。 */
    ui_light.ui_freshflag = 0;              /* 清除灯光本次更新请求。 */
    if(ui_light.ui_value == 0)              /* 值为 0 时显示白色。 */
    {                                       /* 灯光白色分支开始。 */
        color_value = lv_color_make(COLOR_WHITE); /* 生成白色颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 设置灯光对象的白色重着色。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 完全应用重着色。 */
    }                                       /* 灯光白色分支结束。 */
    else if(ui_light.ui_value == 2)         /* 值为 2 时显示红色,区别于两状态图标。 */
    {                                       /* 灯光红色分支开始。 */
        color_value = lv_color_make(COLOR_RED); /* 生成红色颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 将灯光图标设为红色。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 完全应用红色重着色。 */
    }                                       /* 灯光红色分支结束。 */
    else                                    /* 其余值都进入此分支;协议合法值中此时仅剩 1,但代码未排除非法值。 */
    {                                       /* 灯光黄色分支开始。 */
        color_value = lv_color_make(COLOR_YELLOW); /* 生成黄色颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 设置灯光图标的黄色重着色。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 完全应用黄色重着色。 */
    }                                       /* 灯光黄色分支结束。 */
}                                           /* 灯光本次更新结束。 */
if(ui_turn_light.ui_freshflag == 1)          /* 判断转向灯对象是否有待显示状态。 */
{                                           /* 转向灯更新分支开始。 */
    ui_turn_light.ui_freshflag = 0;         /* 清除转向灯本次更新标志。 */
    if(ui_turn_light.ui_value == 0)         /* 零值按课堂显示为白色,未调用隐藏或删除对象接口。 */
    {                                       /* 转向灯白色分支开始。 */
        color_value = lv_color_make(COLOR_WHITE); /* 生成白色颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); /* 重着色对象为转向灯图片。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); /* 使白色重着色生效。 */
    }                                       /* 转向灯白色分支结束。 */
    else                                    /* 所有非零值按课堂显示为红色。 */
    {                                       /* 转向灯红色分支开始。 */
        color_value = lv_color_make(COLOR_RED); /* 生成红色颜色值。 */
        lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); /* 设置转向灯图片红色重着色。 */
        lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); /* 完全应用红色重着色。 */
    }                                       /* 转向灯红色分支结束。 */
}                                           /* 转向灯本次更新结束,主循环继续执行。 */

源码按本节视频与配套工程核对,保留五项业务代码和现有拼写。各块属于指定文件与局部位置,不是完整独立程序。原有 CAN 初始化、滤波器、LVGL 初始化、RTC 显示和节拍沿用前课;本节未改写的库实现不在这里重复抄录。

一、数据接收基本功能实现

上一节每轮 while(1) 都调用 lv_img_set_angle(),即使没有新报文也重新传入旧值。本节增加一个软件刷新标志,让“收到数据”和“更新对象”通过状态衔接。

时刻ui_valueui_freshflag发生的动作
启动后00全局对象由启动初始化置零,没有 CAN 更新请求
收到有效指针报文Byte2 的值1接收解析保存数据并提出更新请求
主循环检查到 1保留该值清为 0调用角度设置函数一次
下一轮无新报文仍保留该值0跳过指针设置,界面保持原有状态
收到下一帧新的 Byte21再触发一次对应更新

视频先用独立变量表示指针数据和标志,随后发现每项数据都需要一对变量,于是改成结构体。最终采用 ui_rpm.ui_freshflag,原笔记的 RPM_flash_flag 并非本节最终名称;这里的 fresh/refresh 表示刷新,与 Flash 存储器无关。

必须清零标志,否则它一直为 1,每轮仍会重复调用。课堂源码是进入更新分支后先清零,再调用 LVGL 接口;语义上是在消费一次请求,并没有等待 LCD 硬件刷屏完成后才清零。

同一数值重复收到两次也会两次置位,因为当前代码判断的是“有新报文”,不是“数值改变”。若接收频率本身很高,更新请求也会相应增加。本节的优化是消除无报文时的重复业务调用,没有设置固定帧率上限。

lv_img_set_angle() 本身会对相同角度作提前返回等处理,所以不应把每次函数调用都等同于一次完整屏幕重绘。刷新标志仍可减少主循环无意义的接口调用,并把接收数据与界面更新职责连接得更清楚。

1.1 定义结构体

ui_Typedef 把两个相关状态放在一起:

  • ui_value:最新收到的数据值;
  • ui_freshflag:这个数据是否还需要提交给界面。

typedef struct { ... } ui_Typedef; 定义的是类型,相当于规定“每个对象都有这两个成员”;ui_Typedef ui_rpm; 才创建指针对象。水温、安全带、灯光、转向灯分别创建各自对象,成员名相同但内存和状态独立。

类型定义放在 can_handle.h 中,其他文件包含该头文件后才能知道对象有哪些成员。若类型只写在 can_handle.c 内,main.c 和头文件中的 extern ui_Typedef ... 就无法识别这个类型。

使用对象时写英文点运算符,例如 ui_water_temp.ui_value。这里 ui_water_temp 是一个具体结构体对象,不是结构体指针,所以使用 .;本节不需要 ->。

结构体让数据组织清楚,但不会自动消除每种功能的接收分支和显示分支,也不保证节省 RAM。C 允许结构体为对齐插入填充,不能把 uint32_t 的 4 字节与 uint8_t 的 1 字节简单相加,断言 sizeof(ui_Typedef) 必定为 5。若关心空间应按当前编译器实际 sizeof 核对,而本节并未测量或打包结构体。

跨文件定义与声明:

文件位置写法/职责
can_handle.h定义公共类型,写 extern ui_Typedef ui_rpm; 等声明
can_handle.c写 ui_Typedef ui_rpm; 等定义,建立实际对象
main.c包含 can_handle.h 后读写同一对象的成员

视频强调“extern 声明不要赋值”,在本工程头文件里是正确的操作规则。准确原理是:文件作用域的 extern ... = ...; 带初始化时会成为定义,多个 .c 包含该头文件便可能形成多个同名定义,导致链接错误;并非 C 语言一律禁止 extern 与初始化一起出现。

更换普通变量为结构体后,必须删掉旧的同名定义/声明,并同步修改所有使用点。头文件保护只防止一个翻译单元内重复包含,不会防止多个 .c 分别定义同一个外部对象。

接收功能码采用 if/else if 链,因为一帧 Byte1 只表示一种功能。一旦前面分支成立,后面分支跳过。相比彼此独立的 if,它也更清楚地表达了“每帧只选择一项”的协议关系。

1.2 结构体的使用

本节完成前五个功能码,日期和时间设置明确暂不处理:

Byte1保存到的对象Byte2 协议意义对应 LVGL 对象
0x01ui_rpm0~100 指针位置百分比ui_ImageNeedle
0x02ui_water_temp0 白色,1 红色ui_ImageWatertemp
0x03ui_safty_belt0 白色,1 红色ui_ImageSfatyBelt
0x04ui_light0 白色,1 黄色,2 红色ui_ImageLight
0x05ui_turn_light0 白色,1 红色ui_ImageTurnlight

原笔记中的安全带=13、灯=14、转向灯=15 是识别错误,应分别为 0x03、0x04、0x05。源码中的 safty、Sfaty 是课程已有拼写,阅读时理解为 safety,抄录时保留,以免与生成的 UI 对象名称不一致。

ui_water_temp.ui_value 存的只是报警状态,不是摄氏度;ui_rpm.ui_value 存的只是百分比,不是已经标定好的 r/min。每个成员的业务单位由协议决定,不由成员类型或名称自动决定。

RGB 宏为什么看似一个参数,实际是三个:

COLOR_RED 宏替换文本为 0xff,0x0,0x0。预处理之后,lv_color_make(COLOR_RED) 相当于 lv_color_make(0xff,0x0,0x0),函数实际收到红、绿、蓝三个分量。两个逗号属于宏展开文本,不能把宏误认为一个打包整数或一个颜色对象。

宏RGB结果
COLOR_WHITE255255255白色
COLOR_RED25500红色
COLOR_YELLOW2552550黄色

lv_color_make() 返回符合当前 LVGL 色深配置的 lv_color_t,赋给 color_value 后用于图像重着色。不要为这个宏自行补一层括号:把三个逗号分隔的实参整体包在一个括号中,会改变 C 表达式含义,与课堂写法不再相同。

每个图标分支调用两个接口:img_recolor 指定要混入的颜色;img_recolor_opa=255 指定完全混入该颜色。若混合强度是 0,就不会出现设置的重着色效果。最后的选择器 0 对应主部分、默认状态,不是关闭接口。

重着色不等同于改变整张图片透明度,更不等同于隐藏对象;原图透明区域仍由图像透明信息决定。尤其转向灯零值在当前源码中是白色,非零为红色,并没有实现自动闪烁、隐藏或左右方向选择。

灯光是三状态,源码先判断 0 白色,再判断 2 红色,最后 else 黄色。合法协议输入下,最后分支对应 1;但非法的 3、255 等也会显示黄色。水温、安全带、转向灯则把任何非零值当作红色。学习时要把协议约定和代码实际接受范围区分开,当前课程仍缺少参数范围校验。

复制相似显示代码后,至少核对四处:刷新标志对象、清零对象、值判断对象、两个 LVGL 调用的目标对象。只修改其中一个名称,会出现“收到安全带数据却改变水温图标”等串项目错误。视频也现场纠正过复制对象错误,因此这一点直接关系实验调试。

1.3 数据接收功能优化

当前 Can_Handle() 依次判断非零长度、CAN ID、应用数据头和功能码,符合条件时保存值并置位。接收端仍复用第 228 篇的轮询 FIFO 封装,每次调用最多取一帧,没有改成 CAN 中断接收。

主循环中的五个刷新判断使用独立的 if,与解析函数的 else if 用途不同:解析时一帧只属于一种指令;显示时检查每个对象是否有待更新请求,独立判断能逐个消费各对象的标志。

接收处理和显示处理都在同一主循环中顺序执行,当前没有两个任务同时修改这些成员的问题。若后续改用 ISR、RTOS 任务或 DMA 回调并发更新,就需要重新设计数据与标志的交接,不能只因为变量加了 volatile 就认为已经解决竞争。本节无需凭空加入这类并发改造。

本节还删除了前面自动自增指针、周期自动变色等演示代码,让显示由 CAN 数据控制。RTC 时间/日期读取显示仍保留,CAN 设置时间/日期尚未完成。

主循环保留的工作:

  1. 周期调用 lv_timer_handler(),处理 LVGL 定时器、输入、动画及刷新等相关任务;它不是只负责时间标签。
  2. 周期读取 RTC 并更新日期、时间标签。
  3. 调用 Can_Handle() 取帧并解析。
  4. 根据各对象刷新标志,设置指针角度或图标颜色。

工程以 HAL_GetTick()-lvtick>5 控制 LVGL 调用。HAL tick 为 1 ms 时,严格大于 5 意味着差值至少 6 ms,再叠加主循环其他工作所用时间,所以不能写成“精确每 5 ms 调用”。RTC 显示的 >500 同样是至少 501 个 tick 后满足条件。本节沿用这些语句,没有改变比较运算符。

软件标志清零只表示“请求已经交给界面接口”,实际 LCD 刷新由 LVGL 调度和显示驱动完成。没有新 CAN 报文时,图标仍保留最后状态;本节也没有超时恢复、断线提示或强制恢复默认状态。

课程末尾编译检查未出现错误,但这只是课堂演示结果,不代表已在硬件上验证 CAN 接收。下一篇开始写 C8T6 发送端,再联合测试五项指令。原接收代码仍保留长度检查、帧类型检查、HAL 返回状态和参数校验不足,见第 228 篇;增加结构体和标志不会自动修复这些问题。

二、学习检查与资料依据

完成本节后,应能解释:为什么一项数据需要值和标志;为什么 typedef 放在头文件里;为何头文件只用 extern 声明对象;接收端的 else if 与显示端独立 if 分别解决什么问题;颜色宏如何变成三个函数实参;为什么 255 的重着色强度不能理解为温度或报警码。

  • 课程视频与转录:约 00:00~04:38 的刷新标志、04:41~11:28 的结构体和跨文件声明、11:30~18:12 的五项显示、18:16~20:02 的主循环整理。
  • 配套 26Can_Recv/BSP/Canif/can_handle.c/.h 与 Core/Src/main.c:核对本节前五个分支,未抄录后续日期和时间设置。
  • WG14 C11 草案 N1570:6.7.2.1 核对结构体布局/填充,6.7.8 核对 typedef,6.9.2 核对带初始化的 extern 定义。
  • LVGL 8.3 Image 文档:核对图片重着色颜色、混合强度与角度单位。
  • LVGL 8.3 Timer Handler 文档:核对 lv_timer_handler() 的调度用途。
  • LVGL 8.3.11 官方 lv_img.c:核对角度设置对相同值的处理;笔记仅引用行为,没有抄录库函数实现。