RTOS(ENV)串口DMA接收GPS数据并解析

RTOS(ENV)配置STM32串口DMA接收模式


环境:

  1. RTOS 4.0.3
  2. Keil5
  3. ENV
  4. STm32l475

ENV配置

  1. 使能串口:

alt text
2. 使能DMA,并设置接收缓冲区大小:

alt text

  1. 创建工程
scons --target=mdk

工程配置

1. 创建串口设备句柄

#define SAMPLE_UART_NAME "uart2" /* 串口设备名称 */
rt_device_t serial; /*句柄*/

2.使用消息队列方式

/* 串口接收消息结构*/
struct rx_msg
{
    rt_device_t dev;
    rt_size_t size;
};
/* 串口设备句柄 */
static rt_device_t serial;
/* 消息队列控制块 */
static struct rt_messagequeue rx_mq;

3.打开设备方式


       /* 以 DMA 接收及轮询发送方式打开串口设备 */
       rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX);
       /* 设置接收回调函数 */
       rt_device_set_rx_indicate(serial, uart_input);
       /* 发送字符串 */
       rt_device_write(serial, 0, str, (sizeof(str) - 1));

注意:

struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; // 初始化配置
/* 控制串口设备。通过控制接口传入命令控制字,与控制参数 */
rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &config);
这些步骤是非DMA方式(中断)接收的,使用 rt_device_control函数 会影响DMA共能得使用

4. 串口接收数据

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    struct rx_msg msg;
    rt_err_t result;
    msg.dev = dev;
    msg.size = size;

    result = rt_mq_send(&rx_mq, &msg, sizeof(msg));
    if ( result == -RT_EFULL)
    {
        /* 消息队列满 */
        rt_kprintf("message queue full!\n");
    }
    return result;
}

static void serial_thread_entry(void *parameter)
{
    struct rx_msg msg;
        rt_err_t result;
        rt_uint32_t rx_length;
        static char rx_buffer[RT_SERIAL_RB_BUFSZ + 1];

        while (1)
        {
            rt_memset(&msg, 0, sizeof(msg));
            /* 从消息队列中读取消息*/
            result = rt_mq_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
            if (result == RT_EOK)
            {
                /* 从串口读取数据*/
                rx_length = rt_device_read(msg.dev, 0, rx_buffer, msg.size);
                rx_buffer[rx_length] = '\0';
                /* 通过串口设备 serial 输出读取到的消息 */
                rt_device_write(serial, 0, rx_buffer, rx_length);
                /* 打印数据 */
                rt_kprintf("%s\n",rx_buffer);
            }
        }
}

源code:

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#include <drv_lcd.h>
#include <rttlogo.h>

#define SAMPLE_UART_NAME       "uart2"

/* 串口接收消息结构*/
struct rx_msg
{
    rt_device_t dev;
    rt_size_t size;
};
/* 串口设备句柄 */
static rt_device_t serial;
/* 消息队列控制块 */
static struct rt_messagequeue rx_mq;

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    struct rx_msg msg;
    rt_err_t result;
    msg.dev = dev;
    msg.size = size;

    result = rt_mq_send(&rx_mq, &msg, sizeof(msg));
    if ( result == -RT_EFULL)
    {
        /* 消息队列满 */
        rt_kprintf("message queue full!\n");
    }
    return result;
}


static void serial_thread_entry(void *parameter)
{
    struct rx_msg msg;
        rt_err_t result;
        rt_uint32_t rx_length;
        static char rx_buffer[RT_SERIAL_RB_BUFSZ + 1];

        while (1)
        {
            rt_memset(&msg, 0, sizeof(msg));
            /* 从消息队列中读取消息*/
            result = rt_mq_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
            if (result == RT_EOK)
            {
                /* 从串口读取数据*/
                rx_length = rt_device_read(msg.dev, 0, rx_buffer, msg.size);
                rx_buffer[rx_length] = '\0';
                /* 通过串口设备 serial 输出读取到的消息 */
                rt_device_write(serial, 0, rx_buffer, rx_length);
                /* 打印数据 */
                rt_kprintf("%s\n",rx_buffer);
            }
        }
}

void main(void)
{
    static char msg_pool[256];
    char str[] = "hello RT-Thread!666\r\n";
    /* 查找串口设备 */
       serial = rt_device_find(SAMPLE_UART_NAME);
       if (!serial)
       {
           rt_kprintf("find %s failed!\n", SAMPLE_UART_NAME);
       }

       /* 初始化消息队列 */
       rt_mq_init(&rx_mq, "rx_mq",
                  msg_pool,                 /* 存放消息的缓冲区 */
                  sizeof(struct rx_msg),    /* 一条消息的最大长度 */
                  sizeof(msg_pool),         /* 存放消息的缓冲区大小 */
                  RT_IPC_FLAG_FIFO);        /* 如果有多个线程等待,按照先来先得到的方法分配消息 */

       /* 以 DMA 接收及轮询发送方式打开串口设备 */
       rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX);
       /* 设置接收回调函数 */
       rt_device_set_rx_indicate(serial, uart_input);
       /* 发送字符串 */
       rt_device_write(serial, 0, str, (sizeof(str) - 1));

       /* 创建 serial 线程 */
       rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
       /* 创建成功则启动线程 */
       if (thread != RT_NULL)
       {
           rt_thread_startup(thread);
       }
       else
       {
           rt_kprintf("Create %s Entry failed!\n", SAMPLE_UART_NAME);
       }

}

附加例题:RT-Thread使用消息邮箱解析GPS数据

使用消息邮箱完成以下任务:
创建2个线程:串口接收和解析线程、LCD显示线程;

  1. 串口线程接收和解析GPS设备通过串口发来的数据(以GPRMC为主),发送给邮箱;

  2. LCD显示线程获取邮箱里的邮件消息,提取相关信息(如 经纬度位置、速度、方向、当前时间等),显示到LCD屏幕上。

  3. 使用PC上位机的串口调试助手发送GPS接收数据,模拟GPS接收机。

参考资料:GPS数据包格式及数据包解析(https://blog.csdn.net/qq_17308321/article/details/80714560)

GPS接收机接收到的数据样例:

$GPRMC,092927.000,A,2235.9058,N,11400.0518,E,0.000,74.11,151216,,D*49 
$GPVTG,74.11,T,,M,0.000,N,0.000,K,D*0B 
$GPGGA,092927.000,2235.9058,N,11400.0518,E,2,9,1.03,53.1,M,-2.4,M,0.0,0*6B
$GPGSA,A,3,29,18,12,25,10,193,32,14,31,,,,1.34,1.03,0.85*31 
$GPGSV,3,1,12,10,77,192,17,25,59,077,42,32,51,359,39,193,49,157,36*48 
$GPGSV,3,2,12,31,47,274,25,50,46,122,37,18,45,158,37,14,36,326,18*70 
$GPGSV,3,3,12,12,24,045,45,26,17,200,18,29,07,128,38,21,02,174,*79

1. 添加GPS数据解析模块

  • 定义数据结构
/*邮箱控股句柄*/
rt_mailbox_t uart_gps_mail;

typedef struct GpsData
{
    char Time[30];
    char Status[30];
    char Latitude[30];
    char N_S[30];
    char Longitude[30];
    char E_W[30];
    char Ground_Speed[30];
    char Course[30];
    char Date[30];
} GpsData;

  • 添加GPS数据解析函数
 //解析 NMEA 句子并提取信息的函数
void parseNMEA(const char *sentence)
{

    char utcTime[12];
    char status;
    double latitude, longitude, groundSpeed, course;
    char utcDate[7];

    // 使用 sscanf 函数从 NMEA 句子中提取信息
    sscanf(sentence, "$GPRMC,%[^,],%c,%lf,N,%lf,E,%lf,%lf,%[^,],",
           utcTime, &status, &latitude, &longitude, &groundSpeed, &course, utcDate);

    // 提取小时、分钟和秒
    int hour, minute;
    double second;
    sscanf(utcTime, "%2d%2d%lf", &hour, &minute, &second);

    // 提取日期
    int year, month, day;
    sscanf(utcDate, "%2d%2d%2d", &day, &month, &year);

    // 打印提取的信息

    sprintf(gpsData.Time, "Time: %02d:%02d:%06.3lf", hour, minute, second);
    sprintf(gpsData.Status, "Status: %s", (status == 'A') ? "Valid" : "Invalid");
    sprintf(gpsData.Latitude, "Latitude: %.4f  N", latitude);
    sprintf(gpsData.Longitude, "Longitude: %.4f  E", longitude);
    sprintf(gpsData.Ground_Speed, "Speed: %.3f knots", groundSpeed);
    sprintf(gpsData.Course, "Course: %.2f ", course);
    sprintf(gpsData.Date, "Date: %02d-%02d-%02d", year, month, day);

    rt_mb_send(uart_gps_mail, (rt_uint32_t)&gpsData);
}

2. 添加lcd显示线程:

void lcd_show(void *parameter)
{
    lcd_clear(WHITE);
    lcd_set_color(WHITE, BLACK);

    GpsData *gpsData;

    while (1)
    {
        rt_mb_recv(uart_gps_mail, (rt_ubase_t *)&gpsData, RT_WAITING_FOREVER);
        lcd_show_string(10, 10, 16, gpsData->Time);
        lcd_show_string(10, 40, 16, gpsData->Status);
        lcd_show_string(10, 70, 16, gpsData->Latitude);
        lcd_show_string(10, 100, 16, gpsData->Longitude);
        lcd_show_string(10, 130, 16, gpsData->Ground_Speed);
        lcd_show_string(10, 160, 16, gpsData->Course);
        lcd_show_string(10, 190, 16, gpsData->Date);
    }
}

3. 源code

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#include <drv_lcd.h>
#include <rttlogo.h>
#include <stdio.h>
#include <string.h>

#define SAMPLE_UART_NAME "uart2"

/* 串口接收消息结构*/
struct rx_msg
{
    rt_device_t dev;
    rt_size_t size;
};
/* 串口设备句柄 */
static rt_device_t serial;
/* 消息队列控制块 */
static struct rt_messagequeue rx_mq;
/*邮箱控股句柄*/
rt_mailbox_t uart_gps_mail;

typedef struct GpsData
{
    char Time[30];
    char Status[30];
    char Latitude[30];
    char N_S[30];
    char Longitude[30];
    char E_W[30];
    char Ground_Speed[30];
    char Course[30];
    char Date[30];
} GpsData;

GpsData gpsData;

void lcd_show(void *parameter)
{
    lcd_clear(WHITE);
    lcd_set_color(WHITE, BLACK);

    GpsData *gpsData;

    while (1)
    {
        rt_mb_recv(uart_gps_mail, (rt_ubase_t *)&gpsData, RT_WAITING_FOREVER);
        lcd_show_string(10, 10, 16, gpsData->Time);
        lcd_show_string(10, 40, 16, gpsData->Status);
        lcd_show_string(10, 70, 16, gpsData->Latitude);
        lcd_show_string(10, 100, 16, gpsData->Longitude);
        lcd_show_string(10, 130, 16, gpsData->Ground_Speed);
        lcd_show_string(10, 160, 16, gpsData->Course);
        lcd_show_string(10, 190, 16, gpsData->Date);
    }
}

// 解析 NMEA 句子并提取信息的函数
void parseNMEA(const char *sentence)
{

    char utcTime[12];
    char status;
    double latitude, longitude, groundSpeed, course;
    char utcDate[7];

    // 使用 sscanf 函数从 NMEA 句子中提取信息
    sscanf(sentence, "$GPRMC,%[^,],%c,%lf,N,%lf,E,%lf,%lf,%[^,],",
           utcTime, &status, &latitude, &longitude, &groundSpeed, &course, utcDate);

    // 提取小时、分钟和秒
    int hour, minute;
    double second;
    sscanf(utcTime, "%2d%2d%lf", &hour, &minute, &second);

    // 提取日期
    int year, month, day;
    sscanf(utcDate, "%2d%2d%2d", &day, &month, &year);

    // 打印提取的信息

    sprintf(gpsData.Time, "Time: %02d:%02d:%06.3lf", hour, minute, second);
    sprintf(gpsData.Status, "Status: %s", (status == 'A') ? "Valid" : "Invalid");
    sprintf(gpsData.Latitude, "Latitude: %.4f  N", latitude);
    sprintf(gpsData.Longitude, "Longitude: %.4f  E", longitude);
    sprintf(gpsData.Ground_Speed, "Speed: %.3f knots", groundSpeed);
    sprintf(gpsData.Course, "Course: %.2f ", course);
    sprintf(gpsData.Date, "Date: %02d-%02d-%02d", year, month, day);

    rt_mb_send(uart_gps_mail, (rt_uint32_t)&gpsData);
}

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    struct rx_msg msg;
    rt_err_t result;
    msg.dev = dev;
    msg.size = size;

    result = rt_mq_send(&rx_mq, &msg, sizeof(msg));
    if (result == -RT_EFULL)
    {
        /* 消息队列满 */
        rt_kprintf("message queue full!\n");
    }
    return result;
}

static void serial_thread_entry(void *parameter)
{
    struct rx_msg msg;
    rt_err_t result;
    rt_uint32_t rx_length;
    static char rx_buffer[RT_SERIAL_RB_BUFSZ + 1];

    while (1)
    {
        rt_memset(&msg, 0, sizeof(msg));
        /* 从消息队列中读取消息*/
        result = rt_mq_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
        if (result == RT_EOK)
        {
            /* 从串口读取数据*/
            rx_length = rt_device_read(msg.dev, 0, rx_buffer, msg.size);
            rx_buffer[rx_length] = '\0';
            /* 通过串口设备 serial 输出读取到的消息 */
            rt_device_write(serial, 0, rx_buffer, rx_length);
            /* 打印数据 */
            // rt_kprintf("%s\n",rx_buffer);
            parseNMEA(rx_buffer);
        }
    }
}

int main(void)
{

    static char msg_pool[256];
    char str[] = "hello RT-Thread!666\r\n";
    /*动态创建邮箱*/
    uart_gps_mail = rt_mb_create("uart_gps", 1024, RT_IPC_FLAG_FIFO);

    /* 查找串口设备 */
    serial = rt_device_find(SAMPLE_UART_NAME);
    if (!serial)
    {
        rt_kprintf("find %s failed!\n", SAMPLE_UART_NAME);
    }

    /* 初始化消息队列 */
    rt_mq_init(&rx_mq, "rx_mq",
               msg_pool,              /* 存放消息的缓冲区 */
               sizeof(struct rx_msg), /* 一条消息的最大长度 */
               sizeof(msg_pool),      /* 存放消息的缓冲区大小 */
               RT_IPC_FLAG_FIFO);     /* 如果有多个线程等待,按照先来先得到的方法分配消息 */

    /* 以 DMA 接收及轮询发送方式打开串口设备 */
    rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX);
    /* 设置接收回调函数 */
    rt_device_set_rx_indicate(serial, uart_input);
    /* 发送字符串 */
    rt_device_write(serial, 0, str, (sizeof(str) - 1));

    /* 创建 serial 线程 */
    rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
    rt_thread_t lcd_th = rt_thread_create("lcd", lcd_show, RT_NULL, 1024, 25, 10);
    /* 创建成功则启动线程 */
    if (thread != RT_NULL && lcd_th != RT_NULL)
    {
        rt_thread_startup(thread);
        rt_thread_startup(lcd_th);
    }
    else
    {
        rt_kprintf("Create %s Entry failed!\n", SAMPLE_UART_NAME);
    }
    return RT_EOK;
}

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

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

相关文章

从零开始实现一个可靠、健壮的内存池

文章目录 概要 这个项目是干什么的项目所需储备知识什么是内存池 池化技术内存池内存池主要解决的问题框架设计开发计划系统测试情况遇到的主要问题和解决方法分工和协作提交仓库目录和文件描述比赛收获 概要 这个项目是干什么的 当前项目是实现一个高并发的内存池&#xff0c…

养生与健康|一起跟随林曦老师养个元气满满

暄桐是一间传统美学教育教室&#xff0c;创办于2011年&#xff0c;林曦是创办人和授课老师&#xff0c;教授以书法为主的传统文化和技艺&#xff0c;皆在以书法为起点&#xff0c;亲近中国传统之美&#xff0c;以实践和所得&#xff0c;滋养当下生活。    在暄桐教室的六阶…

QT 使用信号和槽,让QLabel的内容实时与QLineEdit同步,类似vue框架的双向绑定

在窗口里放置一个单行文本编辑器&#xff08;QLineEdit&#xff09;和一个标签控件&#xff08;QLabel&#xff09;&#xff0c;实现的效果就是当编辑器的内容被编辑时&#xff0c;标 签控件同步显 示编辑控件里的内容 1&#xff09;当 lineEdit 控件被用户编辑时&#xff0c;它…

边缘密度分布图 | ggExtra包/aplot拼图/ggpubr包 等的实现方法

概述&#xff1a;aplot 拼图效果好 根据网友探索[1]&#xff0c;总结如下&#xff1a; ggExtra 包的拼图间隙有点大&#xff0c;图例在主图和边缘图之间&#xff0c;除非去掉图例&#xff0c;否则没法看。aplot包的默认拼图间隙很小&#xff0c;比较美观&#xff0c;图例在外…

Java——二进制原码、反码和补码

一、简要介绍 原码、反码和补码只是三种二进制不同的表示形式&#xff0c;每个二进制数都有这三个形式。 1、原码 原码是将一个数的符号位和数值位分别表示的方法。 最高位为符号位&#xff0c;0表示正&#xff0c;1表示负&#xff0c;其余位表示数值的绝对值。 例如&…

生成式AI,在云端的绽放与盛开

编辑&#xff1a;阿冒 设计&#xff1a;沐由 毫无疑问&#xff0c;生成式AI已然成为当今技术发展和应用创新的重要引擎之一。 过去的一年多时间里&#xff0c;我们每个人都在目睹和见证着生成式AI是如何以移山倒海的力量&#xff0c;为诸多行业带来革命性乃至颠覆性的变革&…

FS118M 单A口QC协议芯片

FS118M是一个QC快充协议芯片&#xff0c;FS118M可以识别插入的手机类型&#xff0c;选择最为合适的协议应对手机需要。USB Type-A 口的 D连接到FS118M芯片&#xff0c;当手机插入到 USB Type-A 口后&#xff0c;根据各个协议的约定&#xff0c;手机和FS118M之间将开始互相识别&…

计网ppt标黄知识点整理第(2)章节——谢希仁版本、期末复习自用

物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是指具体的传输媒体。4 个特性&#xff1a; 机械特性&#xff1a;指明接口所用接线器的形状和尺寸、引线数目和排列、固定和锁定装置等。 电气特性&#xff1a;指明在接口电缆的各条线上出现…

C/C++开发,2024.x CLion安装,亲测有效

CLion 是一款专为 C 和 C 开发者设计的跨平台集成开发环境&#xff08;IDE&#xff09;&#xff0c;提供了智能代码补全、代码分析、调试和 Git 集成等功能&#xff0c;以提高开发效率和代码质量。 1.下载安装c/c开发工具 CLion 先去官网下载&#xff0c;我这里下载的是最新版…

【docker】docker的安装

如果之前安装了旧版本的docker我们需要进行卸载&#xff1a; 卸载之前的旧版本 卸载 # 卸载旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc # 卸载历史版本 apt-get purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker…

问答机器人

怎样做自己的问答机器人&#xff1f; 根据我们提供的数据分析出问题的答案&#xff0c;我们并不需要训练自己的模型 微调模型 finetune&#xff0c;将语言模型调成另外的语言模型&#xff0c;更适合不同类型数据&#xff0c;运用finetune方法将模型变化 知识库模型 embedd…

word避免画质画质模糊方法

问题描述&#xff1a;   近期写文档时会高频率贴图&#xff0c;粘图过程中发现Word会自动压缩图片画质&#xff0c;而且压缩得很严重&#xff0c;下面是一幅图被压缩前后的画质对比 &#xff08;图片压缩前&#xff09; &#xff08;图片压缩后&#xff09; 解决方案&#x…

修改文档日期神器 - Python打造日期修改器

这篇文章将介绍一款使用 Python 开发的实用工具 - 日期修改器。它可以帮助您轻松修改 Word (.docx) 和 PDF 文档的日期信息&#xff0c;满足日常办公和文档整理的需求。 C:\pythoncode\new\modifyfiledate.py 软件功能 支持修改 Word (.docx) 日期信息。允许选择要修改的日期…

leetcode146.LRU缓存,从算法题引入,全面学习LRU和链表哈希表知识

leetcode146. LRU 缓存 题目链接 请你设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中&#xff0c;则返回关…

三种字符串的管理方式

NSString的三种实现方式 OC这个语言在不停的升级自己的内存管理&#xff0c;尽量的让自己的 OC的字符串 问题引入 在学习字符串的过程中间会遇到一个因为OC语言更新造成的问题 例如&#xff1a; int main(int argc, const char * argv[]) {autoreleasepool {NSString* str1 …

ZCU102启动镜像(详细版)

ZCU102启动镜像--详细版本 详细步骤1、安装好Vitis&#xff08;GUI界面&#xff09;、 Vivado、 Petalinux软件然后vivado这边的操作就先结束了 创建Petalinux工程编译镜像打包 详细步骤 B站参考视频链接: link 1、安装好Vitis&#xff08;GUI界面&#xff09;、 Vivado、 Pe…

SpringBoot:手动创建应用

Spring提供了在线的Spring Initialzr在线创建Spring Boot项目&#xff0c;为了更好的理解Spring Boot项目&#xff0c;这里我们选择手动创建。 1.新建Web应用 1.1 生成工程 首先要做是创建一个Java项目&#xff0c;这里我们选择使用Maven来支持&#xff0c;使用archetype:ge…

C++进阶之AVL树+模拟实现

目录 目录 一、AVL树的基本概念 1.1 基本概念 二、AVL树的模拟实现 2.1 AVL树节点的定义 2.2 插入操作 2.3 旋转操作 2.4 具体实现 一、AVL树的基本概念 1.1 基本概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&…

【论文速读】Self-Rag框架,《Self-Rag: Self-reflective Retrieval augmented Generation》

关于前面的文章阅读《When to Retrieve: Teaching LLMs to Utilize Information Retrieval Effectively》&#xff0c;有网友问与Self-Rag有什么区别。 所以&#xff0c;大概看了一下Self-Rag这篇论文。 两篇文章的方法确实非常像&#xff0c;Self-Rag相对更加复杂一些。 When …

图数据库Neo4j——Neo4j简介、数据结构 Docker版本的部署安装 Cypher语句的入门

前言 MySQL是一种开源的关系型数据库管理系统&#xff0c;使用SQL作为其查询语言&#xff0c;常见的关系型数据库有MySQL、Oracle、SQL Server、PostgreSQL等。相关博客文章如下&#xff1a; 【合集】MySQL的入门进阶强化——从 普通人 到 超级赛亚人 的 华丽转身PostgreSQL数…