RT-Thread Env开发探索——以HC-SR04超声波传感器为例

RT-Thread Env开发探索——以HC-SR04超声波传感器为例

  • 0.前言
  • 一、BSP优化
    • 1.修改芯片功能配置
    • 2.修改RTT配置菜单
  • 二、软件包加载
    • 1.外设配置
    • 2.驱动框架配置
    • 3.软件包配置
  • 三、编译及运行
  • 四、源码分析
  • 五、总结


参考文章:RT Thread Env + CLion环境搭建

0.前言

  对比使用RT Thread Stduio开发程序,使用Env工具的开发流程还是比较繁琐的,但是为了使用最新内核和现代化的IDE工具,不得不做出一些妥协。笔者这里就以开发HC-SR04超声波传感器为例,来介绍一下相关的开发流程。此外笔者在实际的开发过程中,觉得这种开发模式还是有一定的提升空间,所以后续还是暂时使用FreeRTOS吧,RTT的使用会先告一段落。

一、BSP优化

  使用Env工具进行开发时,最重要的就是做好板级支持包BSP文件,在上一篇文章中提到,rt-thread源码目录中的bsp目录下,已经适配好了一些板级支持包,如果没有自己所使用的芯片信号,可以参考RT Thread官方的文档手册进行制作。笔者这篇就主要介绍如何在已有的bsp上制作最小支持包,以及一些简单功能的适配。

1.修改芯片功能配置

  在上一篇文章中,通过scons --dist命令已经生成了现有芯片的板级支持包,生成了project工程模板后,复制一份作为自己的初始工程,在此项目目录中找到CubeMX_Config目录,打开CubeMX工程,即可修改一些现有的芯片配置:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里笔者只开启了基础的SW调试口、UART1串口,以及TIM7定时器供后续超声波传感器使用,然后配置好芯片时钟即可一键生成初始化代码,生成的代码只需要保留以下四个文件即可,其他文件是否保留无关紧要:
注:此处生成的代码,和系统时钟有关的初始化代码会被自动加载到工程中,但如果想自己添加一些其他的外设,则需要自行手动移植对应的配置代码。
在这里插入图片描述

2.修改RTT配置菜单

  修改了芯片配置后,原先的板级配置文件可能就无法生效了,甚至可能造成一些问题,所以需要针对自己已使能的外设,重新修改相关的配置文件。用文本方式打开board目录下的KConfig文件,将其修改成如下内容:

menu "Hardware Drivers Config"

menu "Onboard Peripheral Drivers"

endmenu

menu "On-chip Peripheral Drivers"

    config BSP_USING_GPIO
        bool "Enable GPIO"
        select RT_USING_PIN
        default y

    menuconfig BSP_USING_UART
        bool "Enable UART"
        default y
        select RT_USING_SERIAL
        if BSP_USING_UART
            config BSP_USING_UART1
                bool "Enable UART1"
                default y

            config BSP_UART1_RX_USING_DMA
                bool "Enable UART1 RX DMA"
                depends on BSP_USING_UART1 && RT_SERIAL_USING_DMA
                default n
        endif
	
	menuconfig BSP_USING_TIM
        bool "Enable Hardware TIM"
        default n
        select RT_USING_HWTIMER
        if BSP_USING_TIM
            config BSP_USING_TIM7
                bool "Enable TIM7"
                default n
		endif

    menuconfig BSP_USING_SPI
        bool "Enable SPI BUS"
        default n
        select RT_USING_SPI
        if BSP_USING_SPI
            config BSP_USING_SPI1
                bool "Enable SPI1 BUS"
                default n

            config BSP_SPI1_TX_USING_DMA
                bool "Enable SPI1 TX DMA"
                depends on BSP_USING_SPI1
                default n

            config BSP_SPI1_RX_USING_DMA
                bool "Enable SPI1 RX DMA"
                depends on BSP_USING_SPI1
                select BSP_SPI1_TX_USING_DMA
                default n
        endif

    menuconfig BSP_USING_I2C1
        bool "Enable I2C1 BUS (software simulation)"
        default n
        select RT_USING_I2C
        select RT_USING_I2C_BITOPS
        select RT_USING_PIN
        if BSP_USING_I2C1
            config BSP_I2C1_SCL_PIN
                int "i2c1 scl pin number"
                range 1 216
                default 15
            config BSP_I2C1_SDA_PIN
                int "I2C1 sda pin number"
                range 1 216
                default 16
        endif

    menuconfig BSP_USING_PWM
        bool "Enable PWM"
        default n
        select RT_USING_PWM
        if BSP_USING_PWM
        menuconfig BSP_USING_PWM2
            bool "Enable timer2 output PWM"
            default n
            if BSP_USING_PWM2
                config BSP_USING_PWM2_CH1
                    bool "Enable PWM2 channel 1"
                    default n

                config BSP_USING_PWM2_CH2
                    bool "Enable PWM2 channel 2"
                    default n

                config BSP_USING_PWM2_CH3
                    bool "Enable PWM2 channel 3"
                    default n
            endif
        endif

    source "$BSP_DIR/libraries/HAL_Drivers/drivers/Kconfig"

endmenu

menu "Board extended module Drivers"

endmenu

endmenu

  这里是一些常见外设的添加模版,需要开发人员有一些KConfig的编辑基础,或者其实找一些其他的模版文件照着修改也可以。这里笔者使能了PIN设备驱动、UART驱动和TIM驱动,其他的外设驱动设置default n默认关闭,后续有需要再开启。这个配置文件本质上就是通过在menuconfig中选择是否开启相关功能,来设置是否开启代码中的一些相关的宏定义,进而就可以控制是否添加相关的功能模块源码。
  至此,一个最小BSP支持包就制作完毕,不过有一点还需要注意:在这个模板中,笔者使用了TIM7作为超声波传感器的硬件定时器依赖,但是默认的RT Thread内核中,为STM32F1系列声明的定时器名称并没有这么多,所以需要在tim_config.h中手动添加
在CLion中全局搜索一个现有定时器,找到对应的板级配置头文件,然后仿照现有的新建一个即可。除此之外,如果使用一些其他硬件外设,编译时提示找不到对应的设备,也可以看看对应的声明有没有添加。
在这里插入图片描述
在这里插入图片描述

二、软件包加载

  修改完BSP后,就可以在menuconfig中开启对应的功能,加载一些需要的软件包以便后续开发。在项目根目录下打开Env窗口,然后打开menuconfig菜单选项:

1.外设配置

Hardware Drivers Config ---> On-chip Peripheral Drivers栏中,即可看到之前在KConfig中设置的一些外设选项。
在这里插入图片描述

2.驱动框架配置

  一些外设或者软件包都需要与内核的驱动框架进行对接,所以需要在RT-Thread Components ---> Device Drivers目录下,使能对应的驱动框架,这里笔者只使能了超声波传感器需要使用的Sensor框架,需要根据实际的需求自行选择:
在这里插入图片描述

3.软件包配置

  RT-Thread已经集成的软件包较多,手动在RT-Thread online packages目录下搜索会比较麻烦,可以按下键盘上的/键,然后输入关键字进行检索,找到想要的条目后,输入对应的序号就可以直接跳入:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
选择完毕后,保存并退出,在Env窗口中输入pkgs --update即可自动将软件包加载到项目中:
在这里插入图片描述

然后使用scons --targer=cmake重新更新一下工程即可。
在这里插入图片描述

三、编译及运行

在CLion中打开项目,直接编译时会报一些错误:
1.头文件未找到
在这里插入图片描述
将这些地方修改为<drivers/sensor.h>
在这里插入图片描述
2.RT_WEAK识别错误:
在这里插入图片描述
将RT_WEAK修改为rt_weak
在这里插入图片描述
3.找不到pin脚
在这里插入图片描述
添加<drv_gpio.h>头文件
在这里插入图片描述
最后修改sr04_sample.c中的设备相关引脚定义和使用的硬件定时器声明,即可编译下载。
在这里插入图片描述
在这里插入图片描述

四、源码分析

sr04的软件包中,主要内容是这个sensor_hc_sr04.c文件,其中实现了向内核注册sensor设备的流程,以及从设备读取数据的接口:
sensor_hc_sr04.c:

/*
 * Copyright (c) 2006-2020, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2020-05-07     shany       the first version
 */

#include "sensor_hc_sr04.h"
#include "drivers/sensor.h"
#include "board.h"
#include <rtdbg.h>

#define DBG_TAG "sensor.hc.sr04"
#define DBG_LVL DBG_INFO

#define SENSOR_DISTANCE_RANGE_MAX (400)
#define SENSOR_DISTANCE_RANGE_MIN (2)

static struct sr04_device *sr04_dev;

rt_weak void rt_hw_us_delay(rt_uint32_t us)
{
    rt_uint32_t delta;

    us = us * (SysTick->LOAD / (1000000 / RT_TICK_PER_SECOND));
    delta = SysTick->VAL;

    while (delta - SysTick->VAL < us) continue;
}

static rt_err_t _sr04_hwtimer_cb(rt_device_t dev, rt_size_t size)
{
    sr04_dev->err = 0x88;

    return 0;
}

static rt_device_t _sr04_hwtimer_init(void)
{
    rt_device_t dev;
    rt_err_t ret = RT_EOK;

    dev = rt_device_find(sr04_dev->hwtimer);
    if (dev == RT_NULL) {
        rt_kprintf("can't find %s device!\n", sr04_dev->hwtimer);
        return RT_NULL;
    }

    ret = rt_device_open(dev, RT_DEVICE_OFLAG_RDWR);
    if (ret != RT_EOK) {
        rt_kprintf("open hwtimer device failed!\n");
        return RT_NULL;
    }

    rt_device_set_rx_indicate(dev, _sr04_hwtimer_cb);

    static rt_hwtimer_mode_t hw_mode = HWTIMER_MODE_PERIOD;
    ret = rt_device_control(dev, HWTIMER_CTRL_MODE_SET, &hw_mode);
    if (ret != RT_EOK) {
        rt_kprintf("set hwtimer mode failed!\n");
        return RT_NULL;
    }

    return dev;
}

static rt_err_t _sr04_hwtimer_start(rt_device_t dev)
{
    rt_hwtimerval_t hw_val;

    hw_val.sec = 1;
    hw_val.usec = 0;
    if (rt_device_write(dev, 0, &hw_val, sizeof(hw_val)) != sizeof(hw_val)) {
        rt_kprintf("set value failed\n");
        return RT_ERROR;
    }

    return RT_EOK;
}

static int32_t _sr04_hwtimer_stop(rt_device_t dev)
{
    rt_hwtimerval_t hw_val;

    rt_device_read(dev, 0, &hw_val, sizeof(hw_val));
    // rt_kprintf("read: sec = %d, usec = %d\n", hw_val.sec, hw_val.usec);

    rt_device_close(dev);

    return (int32_t)(hw_val.sec * 1000000 + hw_val.usec);
}

int32_t sr04_get_distance(void)
{
    int32_t duration = 0, distance = 0;
    rt_device_t dev;

    dev = _sr04_hwtimer_init();
    rt_pin_write(sr04_dev->trig_pin, PIN_LOW);
    rt_hw_us_delay(2);
    rt_pin_write(sr04_dev->trig_pin, PIN_HIGH);
    rt_hw_us_delay(10);
    rt_pin_write(sr04_dev->trig_pin, PIN_LOW);
    while ((rt_pin_read(sr04_dev->echo_pin) == PIN_LOW));
    _sr04_hwtimer_start(dev);
    while ((rt_pin_read(sr04_dev->echo_pin) == PIN_HIGH) && (sr04_dev->err != 0x88));
    duration = _sr04_hwtimer_stop(dev);

    distance = (int32_t)(duration * 340.0 / 2000.0 + 0.5);

    return distance;
}

static rt_size_t _sr04_polling_get_data(rt_sensor_t sensor, struct rt_sensor_data *data)
{
    rt_int32_t distance_x10;

    if (sensor->info.type == RT_SENSOR_CLASS_PROXIMITY) {
        distance_x10 = sr04_get_distance();
        data->data.proximity = distance_x10;
        data->timestamp = rt_sensor_get_ts();
    }

    return 1;
}

static rt_size_t sr04_fetch_data(struct rt_sensor_device *sensor, void *buf, rt_size_t len)
{
    RT_ASSERT(buf);

    if (sensor->config.mode == RT_SENSOR_MODE_POLLING) {
        return _sr04_polling_get_data(sensor, buf);
    }
    else {
        return 0;
    }
}

static rt_err_t sr04_control(struct rt_sensor_device *sensor, int cmd, void *args)
{
    rt_err_t ret = RT_EOK;

    return ret;
}

static struct rt_sensor_ops sensor_ops =
{
    sr04_fetch_data,
    sr04_control
};

static sr04_device_t _sr04_init(struct rt_sensor_config *cfg)
{
    rt_base_t *pins;
    sr04_device_t dev;

    dev = rt_calloc(1, sizeof(struct sr04_device));
    if (dev == RT_NULL) {
        LOG_E("Can't allocate memory for sr04 device on '%s'.\n", cfg->intf.dev_name);
        return RT_NULL;
    }

    dev->hwtimer = cfg->intf.dev_name;
    rt_kprintf("hwtimer: %s\n", dev->hwtimer);

    pins = (rt_base_t *)cfg->intf.user_data;
    // rt_kprintf("trig: %d, echo: %d\n", pins[0], pins[1]);
    dev->trig_pin = pins[0];
    dev->echo_pin = pins[1];
    // rt_kprintf("trig: %d, echo: %d\n", dev->trig_pin, dev->echo_pin);
    rt_pin_mode(dev->trig_pin, PIN_MODE_OUTPUT);
    rt_pin_mode(dev->echo_pin, PIN_MODE_INPUT);

    return dev;
}

int rt_hw_sr04_init(const char *name, struct rt_sensor_config *cfg)
{
    rt_int8_t result;
    rt_sensor_t sensor_sr04 = RT_NULL;

    sr04_dev = _sr04_init(cfg);
    /* sr04 sensor register */
    sensor_sr04 = rt_calloc(1, sizeof(struct rt_sensor_device));
    if (sensor_sr04 == RT_NULL) {
        return -1;
    }

    sensor_sr04->info.type       = RT_SENSOR_CLASS_PROXIMITY;
    sensor_sr04->info.vendor     = RT_SENSOR_VENDOR_UNKNOWN;
    sensor_sr04->info.model      = "sr04";
    sensor_sr04->info.unit       = RT_SENSOR_UNIT_CM;
    sensor_sr04->info.intf_type  = RT_SENSOR_INTF_ONEWIRE;
    sensor_sr04->info.range_max  = SENSOR_DISTANCE_RANGE_MAX;
    sensor_sr04->info.range_min  = SENSOR_DISTANCE_RANGE_MIN;
    sensor_sr04->info.period_min = 5;

    rt_memcpy(&sensor_sr04->config, cfg, sizeof(struct rt_sensor_config));
    sensor_sr04->ops = &sensor_ops;

    result = rt_hw_sensor_register(sensor_sr04, name, RT_DEVICE_FLAG_RDONLY, RT_NULL);
    if (result != RT_EOK) {
        LOG_E("device register err code: %d", result);
        goto __exit;
    }

    return RT_EOK;

__exit:
    if (sensor_sr04)
        rt_free(sensor_sr04);
    return -RT_ERROR;
}

测距的主要流程:
首先TRIG引脚产生一个10us的高电平信号,即代表开启一次测距,读取到ECHO引脚高电平后,在_sr04_hwtimer_start函数中将定时器的超时时间设置成了1s,如果超时就会触发_sr04_hwtimer_cb回调函数,将标志位置成0x88,在读取ECHO引脚的死循环中,如果高电平结束或者超时标志位被置0x88,则结束本次测距。
问题1:SR04的最大测距量程为4米,所以超时时间可以调小一些,按照一个来回最大量程,预留30ms左右的超时时间即可。(影响不大)
问题2:没有对测距超时的结果做出舍弃,无论本次测距成功或者超时,都会将结果输出。(影响较大)

五、总结

  RT Thread的亮点就是实现了不少驱动框架封装,使得应用层代码可以与底层分离开来,这样如果更换了底层硬件平台,上层的应用代码改动就不是很大。但在笔者的实际使用中发现,目前实现的框架大多只有一些基本功能,比如想要实现STM32的多个ADC外设交替采样,那么还是只能通过手动移植HAL库来实现。
  软件层面的软件包以及驱动框架,目前看起来确实有一些独到之处,毕竟参考了linux内核的实现方式来做的,对于有一定的linux基础的开发人员来说还是比较容易上手。不过软件包的质量良莠不齐,此外在资源有限的单片机上,这种框架所带来的开销也确实需要仔细考量。所以笔者目前对RT Thread的探索就先到这,再观望一阵RTT的发展。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

文件自动同步备份-FreeFileSync工具解决硬盘损坏、误操作覆盖导致数据丢失

文件自动同步备份-FreeFileSync工具解决硬盘损坏、误操作覆盖导致数据丢失 文章目录 文件自动同步备份-FreeFileSync工具解决硬盘损坏、误操作覆盖导致数据丢失前言一、FreeFileSync二、使用方法1.用外部存储卡或盘作为异地备份目标盘2.设置同步策略3.设置为windows的自动计划 …

在数据中心网络中隔离大象流

1000 条短突发中混入几条大象流将严重影响短突发 p99 latency&#xff0c;造成抖动。这个我在 隔离网络流以优化网络 论证过了&#xff0c;还有另一种更直观的理解方式&#xff1a; 规模差异越大&#xff0c;算术均值越偏离中位数&#xff0c;即算术均值的分位数越高。 可以…

Redis 源码学习记录:散列 (dict)

散列 Redis 源码版本&#xff1a;Redis-6.0.9&#xff0c;本篇文章的代码均在 dict.h / dict.c 文件中。 散列类型可以存储一组无需的键值对&#xff0c;他特别适用于存储一个对象数据。 字典 Redis 通常使用字典结构体存储用户散列数据。字典是 Redis 的重要数据结构。除了散…

DNS服务的部署与配置(1)

一、DNS的定义 1、域名系统&#xff08;英文&#xff1a;Domain Name System&#xff0c;缩写&#xff1a;DNS&#xff09;是互联网的一项服务。 它作为将域名和IP地址相互映射的一个分布式数据库&#xff0c;能够使人更方便地访问互联网。 DNS使用UDP端口53。 当前&#xff0…

AUTOMATIC1111/stable-diffusion-webui/stable-diffusion-webui-v1.9.3

配置环境介绍 目前平台集成了 Stable Diffusion WebUI 的官方镜像&#xff0c;该镜像中整合如下资源&#xff1a; GpuMall智算云 | 省钱、好用、弹性。租GPU就上GpuMall,面向AI开发者的GPU云平台 Stable Diffusion WebUI版本&#xff1a;v1.9.3 Python版本&#xff1a;3.10.…

一维前缀和[模版]

题目链接 题目: 分析: 因为要求数组中连续区间的和, 可以使用前缀和算法注意:下标是从1开始算起的, 真正下标0的位置是0第一步: 预处理出来一个前缀和数组dp dp[i] 表示: 表示[1,i] 区间所有元素的和dp[i] dp[i-1] arr[i]例如示例一中: dp数组为{1,3,7}第二步: 使用前缀数…

02_前端三大件HTML

文章目录 HTML用于网页结构搭建1. 标签2. 客户端服务器交互流程3. 专业词汇4. html语法细节5. 安装VSCODE安装插件6. Live Server插件使用7. 标题&段落&换行&列表8. 超链接标签使用9. 图片10. 表格的写法11. 表单标签*(重点)12. 下拉框13. 页面布局标签14. 块元素和…

数理逻辑:1、预备知识

17.1 命题和联结词 ​ 命题&#xff1a;可以判定真假的陈述句。&#xff08;则悖论&#xff0c;祈使句&#xff0c;疑问句都不是命题&#xff09; ​ 原子命题&#xff1a;不能被分割为更小的命题的命题 例如&#xff1a; 2既是素数又是偶数 可以由$p: 2 是素数&#xff0c;…

基于移动多媒体信源与信道编码调研

前言 移动多媒体是指在移动通信环境下&#xff0c;通过无线网络传输的音频、视频、图像等多种媒体信息。移动多媒体的特点是数据量大、传输速率高、服务质量要求高&#xff0c;因此对信源编码和信道编码的性能提出了更高的要求。 本文对进3年的移动多媒体信源与信道编码的研究…

Docker 模块在宝塔中怎么使用

么是 Docker&#xff1f; Docker 是一个用于开发、发布和运行应用程序的开放平台。Docker 使您能够将应用程序与基础架构分离&#xff0c;以便您可以快速交付软件。使用 Docker&#xff0c;您可以像管理应用程序一样管理基础设施。通过利用 Docker 快速交付、测试和部署代码的方…

Django中model中的抽象类

Django中model中的抽象类 当我们在app中models.py文件中定义model表并执行python manage.py makemigrations和python manage.py migrate后&#xff0c;Django就会在数据库中创建表 但是我们也可以对其默认配置修改&#xff0c;定义model类但是不在数据库中创建 from django.…

ubuntu20.04 安装系统后-开机黑屏-nvidia显卡驱动没问题_thinkpad-intel-13700H

文章目录 硬件现象原因&解决 硬件 thinkpad p1 gen6笔记本&#xff0c; intel 13代cpu 13700H,nvidia rtx 2000 Ada laptop gpu 13700H应该是有集显的&#xff0c;但可能没装集显驱动or由于Bios设置的缘故&#xff0c;我的win任务管理器只能看到一个gpu(gpu0)&#xff1…

c++编程14——STL(3)list

欢迎来到博主的专栏&#xff1a;c编程 博主ID&#xff1a;代码小豪 文章目录 list成员类型构造、析构、与赋值iterator元素访问修改元素list的操作 list list的数据结构是一个链表&#xff0c;准确的说应该是一个双向链表。这是一个双向链表的节点结构&#xff1a; list的使用…

关于学习Go语言的并发编程

开始之前&#xff0c;介绍一下​最近很火的开源技术&#xff0c;低代码。 作为一种软件开发技术逐渐进入了人们的视角里&#xff0c;它利用自身独特的优势占领市场一角——让使用者可以通过可视化的方式&#xff0c;以更少的编码&#xff0c;更快速地构建和交付应用软件&#…

Capture One Studio for Mac:打造完美影像的利器

对于摄影师而言&#xff0c;每一次按下快门都是一次对完美影像的追求。而Capture One Studio for Mac正是这样一款能够帮助你实现这一追求的利器。 Capture One Studio for Mac v16.4.2.1中文直装版下载 首先&#xff0c;Capture One Studio for Mac拥有出色的图像处理能力。它…

java项目之人事系统源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的人事系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 基于vue的人事系统的主要使用者…

独享IP是原生IP吗?

原生IP&#xff1a; 原生IP是指由Internet服务提供商&#xff08;ISP&#xff09;直接分配给用户的IP地址&#xff0c;这些IP地址通常反映了用户的实际地理位置和网络连接。原生IP是用户在其所在地区或国家使用的真实IP地址&#xff0c;与用户的物理位置直接相关。在跨境电商中…

C++奇迹之旅:vector使用方法以及操作技巧

文章目录 &#x1f4dd;前言&#x1f320; 熟悉vector&#x1f309;使用vector &#x1f320;构造函数&#x1f309;vector遍历 &#x1f320;operator[]&#x1f309;迭代器 &#x1f320;Capacity容量操作&#x1f309; size()&#x1f309; capacity()&#x1f309;resize()…

【Android开发】Android请求出现网络请求失败,HTTP请求,安全网络通信与权限管理

额外权限 要有这个权限&#xff1a; <uses-permission android:name"android.permission.INTERNET" />HTTP安全考虑 从 Android 9&#xff08;API 级别 28&#xff09;开始&#xff0c;默认情况下不支持通过 HTTP 访问网络&#xff0c;而要求使用 HTTPS。这…

html 字体设置 (web端字体设置)

windows自带的字体是有版权的&#xff0c;包括微软雅黑&#xff08;方正&#xff09;、宋体&#xff08;中易&#xff09;、黑体&#xff08;中易&#xff09;等 版权算是个大坑&#xff0c;所谓为了避免版权问题&#xff0c;全部使用开源字体即可 我这里选择的是思源宋体&…