详细介绍如何从零开始制作51单片机控制的智能小车(一)———让小车动起来

article/2025/8/4 5:36:44

   从本文开始,在之后的一段时间里,我会通过本系列文章,详细介绍如何从零开始用51单片机去实现智能小车的控制,本文作为本系列的第一篇文章,主要介绍如何让小车动起来。

本系列文章链接:

-----------------------------------------------------------------------------

   详细介绍如何从零开始制作51单片机控制的智能小车(一)———让小车动起来
   详细介绍如何从零开始制作51单片机控制的智能小车(二)———超声波模块、漫反射光电管、4路红外传感器的介绍和使用
   详细介绍如何从零开始制作51单片机控制的智能小车(三)———用超声波模块和漫反射光电传感器实现小车的自动避障
   详细介绍如何从零开始制作51单片机控制的智能小车(四)———通过蓝牙模块实现数据传输以及通过手机蓝牙实现对小车运动状态的控制
   详细介绍如何从零开始制作51单片机控制的智能小车(五)———对本系列第四篇文章介绍的手机蓝牙遥控加减速异常的错误的介绍及纠正

-----------------------------------------------------------------------------

一、硬件的选择

   1、底盘和电机

   底盘的形状呢,大家可以按照自己的需要自主选取,至于电机关注一下工作电压,转速,电机类型就差不多,对于新手,可以尝试以下样式(4WD智能小车底盘,附带4个直流减速电机,电机接线需要自己焊接),也就是本文例子采用的底盘和电机,组装简单,使用方便,特别适合新手。

在这里插入图片描述

   2、电机驱动模块

    L298N电机驱动模块,绝对是新手的首选,但是此系列也包含了很多类型,本文采用的是L298N双H桥驱动 红色版 ,除了性能外,我选择它是因为它具备了5v的输出接口,可以用来给单片机供电。大家可以用两个这种驱动,也可以用一个,另一个用个便宜点的。
    由于家中有一个如下式样的L298N驱动,所以为了不让资源浪费,另一个,我就用如下型号的L298N驱动。

   3、单片机最小系统

    关于最小系统,大家只要选用自己熟悉的就行了,没什么特别的讲究,我采用的样式如下(芯片采用的是STC89C52)

   4、电源

    这一部分大家根据需要自己选择即可,选的时候注意一下电压和容量就行,我选用的是常见的 9v 650mAh(容量不是很大,但是这种电池比较常见,充一次电跑2~3小时应该不是问题),我选的是USB充电款,充电很方便,电池盒我选用的是如下的这种拆卸方便的款式,缺点就是不附带电源开关。

   5、杜邦线

    这是必备的辅件,就不多说了,公对公,母对母,公对母(这一种一般用的多一些)都要买一些,家里常备物品。

二、硬件的连接

    本文涉及到的硬件连接为单片机、电源、 电机、 两个电机驱动L298N之间的连接,在这里我介绍一种参考的连接方式,大家可以自己设计连接方案
    如上图所示,四个电机的正负极分别接两个L298N的绿色电机接口,至于到底哪个接正极,哪个接负极,根据你电机安装的方式而定,建议先把电机的两根线焊上,然后把底盘安装起来,这样电机的安装方式就确定了,先随便把两个L298N的4个绿色电机接口跟电机相接,等到把其他信号线接好后,再判断对错并调节,调节方法如下:在程序中让小车往前跑,观察车轮的转向,往前转的车轮的线不用变,把往后转的电机对应的L298N绿色接口的两根线换一下就行了。
   如上图所示,L298N的左边数第一个蓝色端口 是5V输出,把其中一个L298N的该接口接到单片机的5v接口上,另一L298N该接口可以空着,左边数第二个蓝色端口是GND需要同时与单片机的GND与电源的负极相接,左边数第三个蓝色端口是L298N的电源输入端口,与电源的正极相接,我采用的是9v的电源。
   剩下的就是L298N的信号线与单片机的连接了,介绍如上图所示,在这里我采用的是双驱的接法,也就是左边两个点击用同一个信号控制,右边两个电机用同一个信号控制,单片机的I/O口自行选择,与程序配合起来就行,我选用的是 ENA接P16 ENB 接P17 IN1接P34 IN2接P35 IN3接P36 IN4接P37 若改为4驱所需的I/O将扩大一倍。

   收到部分读者说这部分连线还是不太明白,于是我简单画了以下的草图,帮助大家理解上面的内容(字不好看,请见谅)

   实物图片如下:

三、程序的编写

   1、工程的建立

   编译环境根据自己习惯和需要选择,本文以KEIL C51为例,由于本次设计的小车控制并不复杂,所以我把工程中用到的所有头文件、函数的定义、sbit定义的位变量都放到了一个头文件中,取名为car.h(名字大家随意取即可),C文件呢我建议大家把各个部分别写在不同的文件中,比如我把与电机驱动有关的函数放到了motor_control.c(名字任意取)文件中,控制方案和延时函数,中断函数放到了主函数main.c(名字任意取)文件中,后续随着功能增加还会增设其他的C文件,只要所有的C文件均包含以上共同的头文件car.h,也就互相建立了联系。

   2、根据L298N与单片机的接线,编写电机控制函数

   虽然说本文选用的车型四个电机可以独立控制,但是为了简单化,方便化,我们让左边的两个电机采用共同的信号控制,右边的两个电机采用共同的信号控制,大家若需要可以自主改为4路独立的信号控制,根据本文第二部分——硬件的连接部分的介绍,我们选用了单片机的P34 P35 I/O口作为左电机的方向控制信号,单片机的P36 P37 I/O口作为右电机的方向控制信号,单片机的P16 I/O口作为左电机的PWM输出控制信号,单片机的P17 I/O口作为右电机的PWM输出控制信号。
   以上6个I/O口的位定义如下(为方便各文件调用,我们把它放到统一的h文件car.h中)
sbit Left_moto_pwm=P1^6 ;
sbit Right_moto_pwm=P1^7;
sbit p34=P3^4;
sbit p35=P3^5; 
sbit p36=P3^6;
sbit p37=P3^7;
    左右电机的状态控制函数如下:
void Left_moto_go()  //左电机正转
{p34=0;p35=1;} 
void Left_moto_back() //左电机反转
{p34=1;p35=0;} 
void Left_moto_stp()  //左电机停转{p34=1;p35=1;} 
void Right_moto_go()  //右电机正转
{p36=0;p37=1;} 
void Right_moto_back() //右电机反转
{p36=1;p37=0;}  
void Right_moto_stp()  //右电机停转
{p36=1;p37=1;} 

  4、PWM调速输出函数的编写:

    对于新手来说,如果理解不了以下两个函数,那只需要知道如何使用就行了,即通过修改push_val_left的值就可以调节左电机的转速,通过修改push_val _right的值就可以调节右电机的转速,push_val_left和push_val_right的值均位于1到10之间,值越大电机转速越快
bit Left_moto_stop =1;
bit Right_moto_stop =1;
unsigned char pwm_val_left =0;
unsigned char push_val_left =0; 
unsigned char pwm_val_right =0;
unsigned char push_val_right=0;void pwm_out_left_moto(void)     //左电机调速
{ 
if(Left_moto_stop) 
{ 
if(pwm_val_left<=push_val_left) 
Left_moto_pwm=1; 
else 
Left_moto_pwm=0; 
if(pwm_val_left>=10) 
pwm_val_left=0; 
} 
else 
Left_moto_pwm=0; 
} void pwm_out_right_moto(void)   //右电机调速
{ 
if(Right_moto_stop) 
{ 
if(pwm_val_right<=push_val_right) 
Right_moto_pwm=1; 
else 
Right_moto_pwm=0; 
if(pwm_val_right>=10) 
pwm_val_right=0; 
} 
else 
Right_moto_pwm=0; 
} 

  5、小车姿态控制函数的编写:

   理解了 左右电机的状态控制函数,编写小车姿态控制函数就很简单了,大家稍微想一下小车左右轮的状态,小车会怎么运行,就理解了,比如 左右电机都正转,那小车运行状态肯定是前行。每个函数的前两行是左右电机转速的设置。

unsigned char Left_Speed_Ratio;  //左电机转速的设定值
unsigned char Right_Speed_Ratio; //右电机转速的设定值void run(void)     //小车前行
{ 
push_val_left =Left_Speed_Ratio;    
push_val_right =Right_Speed_Ratio; 
Left_moto_go(); 
Right_moto_go(); } void back(void)   //小车后退
{ 
push_val_left =Left_Speed_Ratio; 
push_val_right =Right_Speed_Ratio; 
Left_moto_back();
Right_moto_back();} void left(void)   //小车左转
{ 
push_val_left =Left_Speed_Ratio; 
push_val_right =Right_Speed_Ratio;
Right_moto_go(); 
Left_moto_stp();
} void right(void) //小车右转
{ 
push_val_left =Left_Speed_Ratio;
push_val_right =Right_Speed_Ratio;
Right_moto_stp();
Left_moto_go();
} void stop(void)  //小车停止
{ 
push_val_left =Left_Speed_Ratio; 
push_val_right =Right_Speed_Ratio; 
Left_moto_stp();
Right_moto_stp();} void rotate(void) //小车原地转圈
{ 
push_val_left =Left_Speed_Ratio; 
push_val_right =Right_Speed_Ratio; 
Left_moto_back();
Right_moto_go();} 

  6、与定时器中断有关函数的编写

void Timer0Init()    //定时器初始化函数
{TMOD|=0X01;//选择为定时器0模式,工作方式1,仅用TR0打开启动。TH0=0XFC;	//给定时器赋初值,定时1msTL0=0X18;	ET0=1;//打开定时器0中断允许EA=1;//打开总中断TR0=1;//打开定时器			
}void timer0()interrupt 1 using 2  //定时器中断函数,此处配置为1ms产生一次中断,对PWM的输出进行控制
{ 
TH0=0XFC;	//给定时器赋初值,定时1ms
TL0=0X18;
time++; 
pwm_val_left++; 
pwm_val_right++; 
pwm_out_left_moto(); 
pwm_out_right_moto(); 
} 

  7、延时函数的编写

   关于延时函数,大家只要会用就行,可以用单片机小精灵等辅助软件生成,以下为延时1秒的函数
void delay1s(void)   
{unsigned char a,b,c;for(c=167;c>0;c--)for(b=171;b>0;b--)for(a=16;a>0;a--);_nop_();  
}

  8、主函数内容的编写

   关于主函数的内容,首先要调用定时器中断初始化函数,其次要设置左右电机的速度参数,本文的主要内容是让车动起来,所以主函数内要调用本部分第5步中编写的小车姿态控制函数,对其进行检验,为了便于观察两个状态之间加了5秒的延时,代码如下:
void main(){Timer0Init();  Left_Speed_Ratio=5;   //设置左电机车速为最大车速的50%Right_Speed_Ratio=5;	设置右电机车速为最大车速的50%while(1){run();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();back();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();left();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();right();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();stop();		delay1s(); delay1s();  delay1s();  delay1s();  delay1s();rotate();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();		}
}

四、本文例子完整的C文件和H文件代码

   1、motor_control.c文件完整代码如下:

#include <car.h>unsigned char pwm_val_left =0;
unsigned char push_val_left =0; 
unsigned char pwm_val_right =0;
unsigned char push_val_right=0;
unsigned char Left_Speed_Ratio;
unsigned char Right_Speed_Ratio;bit Left_moto_stop =1;
bit Right_moto_stop =1;void Left_moto_go()  //左电机正转
{p34=0;p35=1;} 
void Left_moto_back() //左电机反转
{p34=1;p35=0;} 
void Left_moto_stp()  //左电机停转{p34=1;p35=1;} 
void Right_moto_go()  //右电机正转
{p36=0;p37=1;} 
void Right_moto_back() //右电机反转
{p36=1;p37=0;}  
void Right_moto_stp()  //右电机停转
{p36=1;p37=1;} void pwm_out_left_moto(void)    //左电机PWM
{ 
if(Left_moto_stop) 
{ 
if(pwm_val_left<=push_val_left) 
Left_moto_pwm=1; 
else 
Left_moto_pwm=0; 
if(pwm_val_left>=10) 
pwm_val_left=0; 
} 
else 
Left_moto_pwm=0; 
} void pwm_out_right_moto(void)    //右电机PWM
{ 
if(Right_moto_stop) 
{ 
if(pwm_val_right<=push_val_right) 
Right_moto_pwm=1; 
else 
Right_moto_pwm=0; 
if(pwm_val_right>=10) 
pwm_val_right=0; 
} 
else 
Right_moto_pwm=0; 
} void run(void)     //小车前行
{ 
push_val_left =Left_Speed_Ratio;    
push_val_right =Right_Speed_Ratio; 
Left_moto_go(); 
Right_moto_go(); } void back(void)   //小车后退
{ 
push_val_left =Left_Speed_Ratio; 
push_val_right =Right_Speed_Ratio; 
Left_moto_back();
Right_moto_back();} void left(void)   //小车左转
{ 
push_val_left =Left_Speed_Ratio; 
push_val_right =Right_Speed_Ratio;
Right_moto_go(); 
Left_moto_stp();
} void right(void) //小车右转
{ 
push_val_left =Left_Speed_Ratio;
push_val_right =Right_Speed_Ratio;
Right_moto_stp();
Left_moto_go();
} void stop(void)  //小车停止
{ 
push_val_left =Left_Speed_Ratio; 
push_val_right =Right_Speed_Ratio; 
Left_moto_stp();
Right_moto_stp();} void rotate(void) //小车原地转圈
{ 
push_val_left =Left_Speed_Ratio; 
push_val_right =Right_Speed_Ratio; 
Left_moto_back();
Right_moto_go();} 

   2、main.c文件完整代码如下:

#include <car.h>extern unsigned char Left_Speed_Ratio;
extern unsigned char Right_Speed_Ratio;
unsigned int time=0; 
extern unsigned char pwm_val_left;
extern unsigned char pwm_val_right;void delay1s(void)   
{unsigned char a,b,c;for(c=167;c>0;c--)for(b=171;b>0;b--)for(a=16;a>0;a--);_nop_();  
}void Timer0Init()
{TMOD|=0X01;//选择为定时器0模式,工作方式1,仅用TR0打开启动。TH0=0XFC;	//给定时器赋初值,定时1msTL0=0X18;	ET0=1;//打开定时器0中断允许EA=1;//打开总中断TR0=1;//打开定时器			
}void timer0()interrupt 1 using 2 
{ 
TH0=0XFC;	//给定时器赋初值,定时1ms
TL0=0X18;
time++; 
pwm_val_left++; 
pwm_val_right++; 
pwm_out_left_moto(); 
pwm_out_right_moto(); 
} void main(){Timer0Init();  Left_Speed_Ratio=5;   //设置左电机车速为最大车速的50%Right_Speed_Ratio=5;	设置右电机车速为最大车速的50%while(1){run();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();back();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();left();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();right();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();stop();		delay1s(); delay1s();  delay1s();  delay1s();  delay1s();rotate();delay1s(); delay1s();  delay1s();  delay1s();  delay1s();		}
}

   3、car.h文件完整代码如下:

#ifndef __car_H
#define __car_H#include <reg52.h>
#include <intrins.h>sbit Left_moto_pwm=P1^6 ;
sbit Right_moto_pwm=P1^7;
sbit p34=P3^4;
sbit p35=P3^5; 
sbit p36=P3^6;
sbit p37=P3^7;void Left_moto_go() ;
void Left_moto_back() ;
void Left_moto_stp() ;
void Right_moto_go();
void Right_moto_back(); 
void Right_moto_stp(); 
void delay(unsigned int k) ;
void delay1s(void) ;
void pwm_out_left_moto(void) ;
void pwm_out_right_moto(void);
void run(void);
void back(void);
void left(void);
void right(void);
void stop(void);
void rotate(void);

五、本文例子实物视频演示

       实物视频演示视频链接

    点击上面的链接即可查看本文介绍内容的视频演示,内容依次为(即主函数中程序的内容):前进5秒 、后退5秒、左转5秒、右转5秒、停转5秒、转圈5秒。附视频网址:

    https://www.bilibili.com/video/bv1N5411x7zL

   本文到这里就结束了,本文完整的工程文件我会放在附件里,需要者自取,我放的时候都是免费的,但是过段时间它会自己涨…,欢迎大家继续阅读本系列的后续文章“详细介绍如何从零开始制作51单片机控制的智能小车(二)———超声波模块、漫反射光电管、4路红外传感器的介绍和使用”

   本系列文章的附件已经支持自主下载,附件获取方式如下(推荐通过Gitee免费下载):
https://blog.csdn.net/qq_44339029/article/details/114887405

   欢迎大家积极交流,本文未经允许谢绝转载


http://chatgpt.dhexx.cn/article/SA10hUXS.shtml

相关文章

乐观锁-基于CAS原理

乐观锁理论基础 乐观锁的操作过程中其实没有没有任何锁的参与&#xff0c;乐观锁只是和悲观锁相对&#xff0c;严格的说乐观锁不能称之为锁。下面我们就通过乐观锁与悲观锁的对比来更好的理解乐观锁。 乐观锁与悲观锁的概念 乐观锁&#xff1a;总是假设最好的情况…

JAVA CAS原理深度分析

CAS CAS:Compare and Swap, 翻译成比较并交换。 java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。 本文先从CAS的应用说起&#xff0c;再深入原理解析。 CAS应用 CAS有3个操作数&#xff0c;内存值V&#xff0c;旧的预期值A&#xff0c;要修…

CAS原理图

主要原理 1 用户第一次访问一个CAS 服务的客户web 应用时&#xff08;访问URL &#xff1a;http://192.168.1.90:8081/web1 &#xff09;&#xff0c;部署在客户web 应用的cas AuthenticationFilter &#xff0c;会截获此请求&#xff0c;生成service 参数 2 然后redirect 到C…

java---CAS原理分析详解

目录 一、什么是CAS 二、乐观锁与悲观锁 1.乐观锁出现原因 2.乐观锁 3.乐观锁的实现机制---CAS 三、JAVA对CAS的支持 首先演示实际的操作 上述过程的内部原理(java层面) 四、CAS缺陷 1.ABA问题 解决ABA问题 2.循环时间长开销大 3.只能保证一个变量的原子操作 4.解…

AQS和CAS原理

锁机制&#xff08;AQS和CAS&#xff09; 一、AQS 1、AQS原理 AQS&#xff1a;AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。     AQS的全称为&#xff08;AbstractQueuedSynchronizer&#xff09;&#xff0c;这个类…

CAS原理解析

什么是CAS cas全称为compareAndSwap&#xff0c;可以很清楚的翻译知道意思为“比较和交换”&#xff0c;字面意思其实就已经解释了CAS的实现原理 CAS简介 从jdk5开始&#xff0c;jdk提供了java.util.concurrent.*&#xff0c;此包下面的类在高并发场景下经常使用&#xff0c;包…

搞定CAS的原理,看这一篇就够了!

一、什么是CAS&#xff1f; CAS &#xff08;compareAndSwap&#xff09;&#xff0c;中文叫比较交换&#xff0c;是一种无锁原子算法&#xff0c;映射到操作系统就是一条CPU的原子指令&#xff0c;其作用是让CPU先进行比较两个值是否相等&#xff0c;然后原子地更新某个位置的…

深入理解vue.js双向绑定的实现原理

vue.js是MVVM&#xff08;模型到视图和视图到模型&#xff09;结构的&#xff0c;同类的还有AngularJs&#xff1b;至于MVC、MVP、MVVM的比较网上已经有很多了&#xff0c;这样不再重复。这篇文章将给大家深入的介绍vue.js双向绑定的实现原理&#xff0c;有需要的朋友们可以参考…

MVVM数据双向绑定

MVVM采用双向数据绑定&#xff0c;view中数据变化将自动反映到viewmodel上&#xff0c;反之&#xff0c;model中数据变化也将会自动展示在页面上。把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来&#xff0c;还负责把View的修改同步回Mo…

vue双向绑定的理解

什么是双向绑定 把Model绑定到View&#xff0c;当我们用JavaScript代码更新Model时&#xff0c;View就会自动更新。在单向绑定的基础上&#xff0c;用户更新了View&#xff0c;Model的数据也自动被更新了&#xff0c;这种情况就是双向绑定 如&#xff1a; 当用户填写表单时&a…

实现vue数据双向绑定

关注公众号&#xff0c;每天都能领外卖红包 关于vue数据双向绑定也是面试很喜欢问的题目了&#xff0c;这里讲下实现方式&#xff0c;效果图、源码、demo在文章末尾 首先看下vue的基本结构 <div id"app"> <div>{{a.b.c}}</div></div> n…

双向绑定原理

适合读者: 了解 MV* 架构模式 希望了解双向绑定原理 从MVC、MVVM说起 参考阮一峰老师的文章:http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html 流程:View根据Model展示页面,当页面发生操作时(commander),View传递指令到Controller层,Controller层根据comman…

Angular 双向绑定

Angular10教程--2.3 双向绑定 双向绑定大致可以分成两种类型&#xff1a;一、普通组件的双向绑定二、表单中的双向绑定[(ngModel)]单独使用表单元素在标签中使用 总结&#xff1a; 前面我们了解了属性绑定、事件绑定以及输入和输出的使用&#xff0c;是时候了解双向绑定了。本节…

理解双向绑定

这里是修真院前端小课堂&#xff0c;每篇分享文从 【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】 八个方面深度解析前端知识/技能&#xff0c;本篇分享的是&#xff1a; 【 理解双向绑定】 大家好&#xff0c;我是IT修…

html双向绑定,双向绑定

单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新。 有单向绑定,就有双向绑定。如果用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定。 什么情况下用户可以更新View呢?填写表单就是一个最直接的例子。当用户…

数据双向绑定

#一、什么是双向绑定 我们先从单向绑定切入单向绑定非常简单&#xff0c;就是把Model绑定到View&#xff0c;当我们用JavaScript代码更新Model时&#xff0c;View就会自动更新双向绑定就很容易联想到了&#xff0c;在单向绑定的基础上&#xff0c;用户更新了View&#xff0c;M…

双向数据绑定是什么

面试官&#xff1a;双向数据绑定是什么 一、什么是双向绑定 我们先从单向绑定切入单向绑定非常简单&#xff0c;就是把Model绑定到View&#xff0c;当我们用JavaScript代码更新Model时&#xff0c;View就会自动更新双向绑定就很容易联想到了&#xff0c;在单向绑定的基础上&am…

第三届全国大学生算法设计与编程挑战赛题解【金奖全国第九】

❥这次秋季赛查重后有效提交队伍共1000余队&#xff0c;前5%金&#xff0c;10%银&#xff0c;20%铜&#xff0c;冠军1名&#xff0c;亚军2名&#xff0c;季军3名。每次比赛之余都不得不感慨oier的可怕实力和某些竞赛强省的高端水平。 ❥赛时一直稳定在前5%&#xff08;金奖行列…

2020-2021年度第⼆届全国⼤学⽣算法设计与编程挑战赛(冬季赛)——正式赛(做题过程)

2020-2021年度第⼆届全国⼤学⽣算法设计与编程挑战赛&#xff08;冬季赛&#xff09;——正式赛&#xff08;做题记录&#xff09; A-塔 【题⽬描述】 初来到海拉尔⼤陆的你&#xff0c;有些许的局促&#xff0c;但当你看到塔&#xff0c;或许⼀切的⼀切都迎刃⽽解。 ⼀个层…

阿里移动推荐算法大赛总结

一、 赛题说明 1. 竞赛题目 在真实的业务场景下&#xff0c;我们往往需要对所有商品的一个子集构建个性化推荐模型。在完成这件任务的过程中&#xff0c;我们不仅需要利用用户在这个商品子集上的行为数据&#xff0c;往往还需要利用更丰富的用户行为数据。定义如下的符号&…