Linux UART编程 驱动蓝牙芯片

原创 专注于无线通信的蓬勃 2022-12-04 14:19

在熟悉了UART概念后,我们要学以致用,在Linux用起来来驱动起来蓝牙芯片!

我们直接借用man来看下,命令如下: man termios

1.头文件引用

#include <termios.h>
#include <unistd.h>

2.串口打开关闭

open(“/dev/ttyUSB0”, O_RDWR|O_NOCTTY);
close(fd);

Linux秉行一切皆文件的,所以打开关是用open,关闭串口使用close

3.串口配置的重要结构体

struct termios 
{
    tcflag_t c_iflag; /* input flags */
    tcflag_t c_oflag; /* output flags */
    tcflag_t c_cflag; /* control flags */
    tcflag_t c_lflag; /* local flags */
    cc_t c_cc[NCCS]; /* control characters */
};

下面我们来分别介绍下各个flag!

3.1 c_iflag输入模式标志,控制终端输入方式

在输入值传给程序之前控制其处理的方式

  • IGNBRK 忽略BREAK键输入
  • BRKINT 如果设置了IGNBRK,BREAK键的输入将被忽略,如果设置BRKINT ,将产生SIGINT中断
  • IGNPAR 忽略奇偶校验错误
  • PARMRK 标识奇偶校验错误
  • INPCK 允许输入奇偶校验
  • ISTRIP 去除字符的第8个比特
  • INLCR 将输入的NL(换行)转换成CR(回车)
  • IGNCR 忽略输入的回车
  • ICRNL 将输入的回车转化成换行(如果IGNCR未设置的情况下)
  • IUCLC 将输入的大写字符转换成小写字符(非POSIX)
  • IXON 允许输入时对XON/XOFF流进行控制
  • IXANY 输入任何字符将重启停止的输出
  • IXOFF 允许输入时对XON/XOFF流进行控制
  • IMAXBEL 当输入队列满的时候开始响铃,Linux在使用该参数而是认为该参数总是已经设置

3.2 c_oflag输出模式标志,控制终端输出方式

负责控制输出字元的处理方式

  • OPOST 处理后输出
  • OLCUC 将输入的小写字符转换成大写字符(非POSIX)
  • ONLCR 将输入的NL(换行)转换成CR(回车)及NL(换行)
  • OCRNL 将输入的CR(回车)转换成NL(换行)
  • ONOCR 第一行不输出回车符
  • ONLRET 不输出回车符
  • OFILL 发送填充字符以延迟终端输出
  • OFDEL 以ASCII码的DEL作为填充字符,如果未设置该参数,填充字符将是NUL(‘/0’)(非POSIX)
  • NLDLY 换行输出延时,可以取NL0(不延迟)或NL1(延迟0.1s)
  • CRDLY 回车延迟,取值范围为:CR0、CR1、CR2和 CR3
  • TABDLY 水平制表符输出延迟,取值范围为:TAB0、TAB1、TAB2和TAB3
  • BSDLY 空格输出延迟,可以取BS0或BS1
  • VTDLY 垂直制表符输出延迟,可以取VT0或VT1
  • FFDLY 换页延迟,可以取FF0或FF1

3.3 c_cflag控制模式标志,指定终端硬件控制信息

用于控制终端设备的硬件设置

  • CBAUD 波特率(4 1位)(非POSIX)
  • CBAUDEX 附加波特率(1位)(非POSIX)
  • CSIZE 字符长度,取值范围为CS5、CS6、CS7或CS8
  • CSTOPB 设置两个停止位
  • CREAD 使用接收器
  • PARENB 使用奇偶校验
  • PARODD 对输入使用奇偶校验,对输出使用偶校验
  • HUPCL 关闭设备时挂起
  • CLOCAL 忽略调制解调器线路状态
  • CRTSCTS 使用RTS/CTS流控制

3.4 c_lflag本地模式标志,控制终端编辑功能

主要用来控制终端设备不同的特色

  • ISIG 当输入INTR、QUIT、SUSP或DSUSP时,产生相应的信号
  • ICANON 使用标准输入模式,允许使用特殊字符 EOF, EOL, EOL2, ERASE, KILL, LNEXT, REPRINT, STATUS, 和 WERASE,以及按行的缓冲
  • XCASE 在ICANON和XCASE同时设置的情况下,终端只使用大写。如果只设置了XCASE,则输入字符将被转换为小写字符,除非字符使用了转义字符(非POSIX,且Linux不支持该参数)
  • ECHO 显示输入字符
  • ECHOE 如果ICANON同时设置,ERASE将删除输入的字符,WERASE将删除输入的单词
  • ECHOK 如果ICANON同时设置,KILL将删除当前行
  • ECHONL 如果ICANON同时设置,即使ECHO没有设置依然显示换行符
  • ECHOPRT 如果ECHO和ICANON同时设置,将删除打印出的字符(非POSIX)
  • TOSTOP 向后台输出发送SIGTTOU信号

3.5 C_LINE行控制 c_cc[NCCS]控制字符

  • 提供使用者设定一些特殊的功能, 如Ctrl C的字元组合。
  • 特殊控制字元主要是利用termios结构里c_cc的阵列成员来做设定。
  • c_cc阵列主要用于正规与非正规两种环境,但要注意的是正规与非正规不可混为一谈。

符号下标 (初始值) 和意义(即c_cc[]数组对应下标的数值对应含义,如c_cc[VMIN] = 3):

  • VINTR:(003, ETX, Ctrl-C, or also 0177, DEL, rubout) 中断字符。发出 SIGINT 信号。当设置 ISIG 时可被识别,不再作为输入传递。
  • VQUIT :(034, FS, Ctrl-) 退出字符。发出 SIGQUIT 信号。当设置 ISIG 时可被识别,不再作为输入传递。
  • VERASE :(0177, DEL, rubout, or 010, BS, Ctrl-H, or also #) 删除字符。删除上一个还没有删掉的字符,但不删除上一个 EOF 或行首。当设置 ICANON 时可被识别,不再作为输入传递。
  • VKILL :(025, NAK, Ctrl-U, or Ctrl-X, or also @) 终止字符。删除自上一个 EOF 或行首以来的输入。当设置 ICANON 时可被识别,不再作为输入传递。
  • VEOF :(004, EOT, Ctrl-D) 文件尾字符。更精确地说,这个字符使得 tty 缓冲中的内容被送到等待输入的用户程序中,而不必等到 EOL。如果它是一行的第一个字符,那么用户程序的 read() 将返回 0,指示读到了 EOF。当设置 ICANON 时可被识别,不再作为输入传递。
  • VMIN :非 canonical 模式读的最小字符数(MIN主要是表示能满足read的最小字元数)。
  • VEOL :(0, NUL) 附加的行尾字符。当设置 ICANON 时可被识别。
  • VTIME :非 canonical 模式读时的延时,以十分之一秒为单位。
  • VEOL2 :(not in POSIX; 0, NUL) 另一个行尾字符。当设置 ICANON 时可被识别。
  • VSWTCH :(not in POSIX; not supported under Linux; 0, NUL) 开关字符。(只为 shl 所用。)
  • VSTART :(021, DC1, Ctrl-Q) 开始字符。重新开始被 Stop 字符中止的输出。当设置 IXON 时可被识别,不再作为输入传递。
  • VSTOP :(023, DC3, Ctrl-S) 停止字符。停止输出,直到键入 Start 字符。当设置 IXON 时可被识别,不再作为输入传递。
  • VSUSP :(032, SUB, Ctrl-Z) 挂起字符。发送 SIGTSTP 信号。当设置 ISIG 时可被识别,不再作为输入传递。
  • VDSUSP :(not in POSIX; not supported under Linux; 031, EM, Ctrl-Y) 延时挂起信号。当用户程序读到这个字符时,发送 SIGTSTP 信号。当设置 IEXTEN 和 ISIG,并且系统支持作业管理时可被识别,不再作为输入传递。
  • VLNEXT :(not in POSIX; 026, SYN, Ctrl-V) 字面上的下一个。引用下一个输入字符,取消它的任何特殊含义。当设置 IEXTEN 时可被识别,不再作为输入传递。
  • VWERASE :(not in POSIX; 027, ETB, Ctrl-W) 删除词。当设置 ICANON 和 IEXTEN 时可被识别,不再作为输入传递。
  • VREPRINT :(not in POSIX; 022, DC2, Ctrl-R) 重新输出未读的字符。当设置 ICANON 和 IEXTEN 时可被识别,不再作为输入传递。
  • VDISCARD :(not in POSIX; not supported under Linux; 017, SI, Ctrl-O) 开关:开始/结束丢弃未完成的输出。当设置 IEXTEN 时可被识别,不再作为输入传递。
  • VSTATUS :(not in POSIX; not supported under Linux; status request: 024, DC4, Ctrl-T).

这些符号下标值是互不相同的,除了 VTIME,VMIN 的值可能分别与 VEOL,VEOF 相同。 (在 non-canonical 模式下,特殊字符的含义更改为延时含义MIN 表示应当被读入的最小字符数。TIME 是以十分之一秒为单位的计时器。如果同时设置了它们,read 将等待直到至少读入一个字符,一旦读入 MIN 个字符或者从上次读入字符开始经过了 TIME 时间就立即返回。如果只设置了 MIN,read 在读入 MIN 个字符之前不会返回。如果只设置了 TIME,read 将在至少读入一个字符,或者计时器超时的时候立即返回。如果都没有设置,read 将立即返回,只给出当前准备好的字符。)

MIN与TIME组合有以下四种:

  • MIN = 0 , TIME =0
    有READ立即回传,否则传回 0 ,不读取任何字元
  • MIN = 0 , TIME >0
    READ 传回读到的字元,或在十分之一秒后传回TIME,若来不及读到任何字元,则传回0
  • MIN > 0 , TIME =0
    READ 会等待,直到MIN字元可读
  • MIN > 0 , TIME > 0
    每一格字元之间计时器即会被启动,READ 会在读到MIN字元,传回值或TIME的字元计时(1/10秒)超过时将值传回

4.常用的函数

/* 属性相关 */
int tcgetattr(int fd, struct termios *termios_p);

int tcsetattr(int fd, int optional_actions,
              const struct termios *termios_p);

void cfmakeraw(struct termios *termios_p);

/* 控制相关 */
int tcsendbreak(int fd, int duration);

int tcdrain(int fd);

int tcflush(int fd, int queue_selector);

int tcflow(int fd, int action);


/* 速度相关 */
speed_t cfgetispeed(const struct termios *termios_p);

speed_t cfgetospeed(const struct termios *termios_p);

int cfsetispeed(struct termios *termios_p, speed_t speed);

int cfsetospeed(struct termios *termios_p, speed_t speed);

函数名称

描述

tcgetattr

获取串口属性,填充到termios_p入参中

tcsetattr

根据termios_p的值改变串口属性

optional_actions (tcsetattr函数的第二个参数)指定了什么时候改变会起作用:
TCSANOW:改变立即发生
TCSADRAIN:改变在所有写入 fd 的输出都被传输后生效。这个函数应当用于修改影响输出的参数时使用。(当前输出完成时将值改变)
TCSAFLUSH :改变在所有写入 fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃(同TCSADRAIN,但会舍弃当前所有值)。

cfmakeraw

把串口属性设置位初始状态,比如以下值:

termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);

termios_p->c_oflag &= ~OPOST;

termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);

termios_p->c_cflag &= ~(CSIZE | PARENB);

termios_p->c_cflag |= CS8;

cfgetispeed

获取input的串口波特率

cfgetospeed

获取output的串口波特率

cfsetispeed

设置input的串口波特率

cfsetospeed

获取output的串口波特率

tcsendbreak

tcdrain

等待所有写入fd中的数据输出

tcflush

清空串口BUFFER中的数据函数

常用的有三个值,

TCIFLUSH清除正收到的数据,且不会读取出来;

TCOFLUSH清除正写入的数据,且不会发送至终端;

TCIOFLUSH清除所有正在发生的I/O数据;

tcflow

挂起 fd 引用的对象上的数据传输或接收,取决于 action 的值

TCOOFF 挂起输出
TCOON 重新开始被挂起的输出
TCIOFF 发送一个 STOP 字符,停止终端设备向系统传送数据
TCION 发送一个 START 字符,使终端设备向系统传输数据

框图如下:

我们写一个sample,用H4 transport来发送一个HCI RESET然后读回来值:

#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>

#include <string.h>
#include <stdint.h>
#include <stdio.h>

#define UART_MAX_SIZE 256
int uart_fd;
uint8_t uart_buffer[UART_MAX_SIZE] = {0};


void usage()
{
	printf("---------------------------------\n");
	printf("      ./uart_test port_name      \n");
	printf("example: ./uart_test /dev/ttyUSB0\n");
	printf("---------------------------------\n");
}

#define MAX_COL 16
#define SHOW_LINE_SIZE 16
void bt_hex_dump(uint8_t *data,uint32_t len)
{
    uint32_t line;
    uint32_t curline = 0;
    uint32_t curcol = 0;
    char showline[SHOW_LINE_SIZE];
    uint32_t data_pos = 0;

    if(len % MAX_COL)
    {
        line = len/MAX_COL 1;
    }
    else
    {
        line = len/MAX_COL;
    }

    for(curline = 0; curline < line; curline  )
    {
        sprintf(showline,"xh:",curline*MAX_COL);
        printf("%s",showline);
        for(curcol = 0; curcol < MAX_COL; curcol  )
        {
            if(data_pos < len)
            {
                printf("x ",data[data_pos]);
                data_pos  ;
                continue;
            }
            else
            {
                break;
            }
        }
        printf("\n");
    }
}

void alarm_send_command()
{
	uint8_t hci_reset[] = {0x01,0x03,0x0c,0x00};
	printf("send HCI command\n");
	write(uart_fd,hci_reset,sizeof(hci_reset));
	
	alarm(1);
}

int main(int argc, char *argv[])
{
	struct termios toptions;
	
	usage();
	if(argc != 2)
	{
	  printf("Usage error\n"); 
	  return 0;
	}

	uart_fd = open(argv[1], O_RDWR | O_NOCTTY);
   
	printf("uart_fd %d\n",uart_fd);
    if (tcgetattr(uart_fd, &toptions) < 0)
    {
        printf("ERROR:Couldn't get term attributes\n");
        return -1;
    }

    cfmakeraw(&toptions);

    // 8N1
    toptions.c_cflag &= ~CSTOPB;
    toptions.c_cflag |= CS8;

    toptions.c_cflag |= CREAD | CLOCAL | CRTSCTS;
    toptions.c_iflag &= ~(IXON | IXOFF | IXANY);
    toptions.c_cflag &= ~PARENB;

    toptions.c_cc[VMIN]  = 1;
    toptions.c_cc[VTIME] = 0;

    if(tcsetattr(uart_fd, TCSANOW, &toptions) < 0)
    {
        printf("ERROR:Couldn't set term attributes\n");
        return -1;
    }

    if (tcgetattr(uart_fd, &toptions) < 0)
    {
        printf("ERROR:Couldn't get term attributes\n");
        return -1;
    }
    cfsetospeed(&toptions, B115200);
    cfsetispeed(&toptions, B115200);


    if( tcsetattr(uart_fd, TCSANOW, &toptions) < 0)
    {
        printf("ERROR:Couldn't set term attributes\n");
        return -1;
    }
	
	signal(SIGALRM, alarm_send_command);
	alarm(1);
	
	while(1)
    {
		int read_result = read(uart_fd,uart_buffer,UART_MAX_SIZE);
		bt_hex_dump(uart_buffer,read_result);
		memset(uart_buffer,0,UART_MAX_SIZE);
    }
}

整个程序实现的效果很简单,就是打开串口(8N1 流控),设置波特率,然后1s发送一次hci reset,然后读取数据uart数据

 

专注于无线通信的蓬勃 朝气蓬勃——不积跬步 无以至千里, 不积小流 无以成江海
评论
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 98浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 100浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 71浏览
  • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
    youyeye 2024-11-30 14:30 78浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 119浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 106浏览
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 108浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 120浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 68浏览
  • TOF多区传感器: ND06   ND06是一款微型多区高集成度ToF测距传感器,其支持24个区域(6 x 4)同步测距,测距范围远达5m,具有测距范围广、精度高、测距稳定等特点。适用于投影仪的无感自动对焦和梯形校正、AIoT、手势识别、智能面板和智能灯具等多种场景。                 如果用ND06进行手势识别,只需要经过三个步骤: 第一步&
    esad0 2024-12-04 11:20 50浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 101浏览
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 84浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 122浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦