衡山派学习之串口

相关文档链接

【立创·衡山派D133EBS开发板】UART框架使用 | 立创开发板技术文档中心

然后官方文档还是让我们先编写Sconscript

上面我们写led驱动时也写过Sconscript,然后我们现在就可以来对比一下两者有什么区别吗,看看那些东西是相同的哪些东西是不同的

要用串口我们就要看看引脚分配图,看看哪个引脚对应的是什么功能

UART设备

AI人话版

• 这篇文档本质上是在讲一件事:

在 RT-Thread 里,怎么把串口当成一个"设备"来用。

你不用先去碰底层寄存器。对应用层来说,串口就是一个 rt_device,你按固定流程去找它、打开它、配置它、读它、写它、收回调、 最后关闭它。 我主要参考了 RT-Thread 官方编程指南第 12 章、UART API 和官方例程: https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf https://www.rt-thread.org/document/api/group___u_a_r_t.html https://www.rt-thread.org/document/api/uart_sample_8c-example.html

你看完最该记住的 1 句话

RT-Thread 的 UART 使用流程就是:find -> control(可选) -> open -> set_rx_callback -> read/write -> close。


这篇文档主要讲了什么

  1. UART 是什么 它先简单复习了串口基础:波特率、数据位、停止位、奇偶校验位这些参数必须两端一致,不然通信不正常。 这部分不是重点,重点是后面的"怎么在 RT-Thread 里访问 UART"。

  2. RT-Thread 怎么管理 UART RT-Thread 把 UART 纳入统一的 I/O 设备模型。 对应用层来说,串口不是"某个芯片寄存器",而是一个设备对象。你主要通过这些通用接口访问它:

  • rt_device_find():找设备

  • rt_device_open():打开设备

  • rt_device_control():改配置

  • rt_device_read():读数据

  • rt_device_write():写数据

  • rt_device_set_rx_indicate():设置接收回调

  • rt_device_set_tx_complete():设置发送完成回调

  • rt_device_close():关闭设备

也就是说,这篇文档真正教你的不是"串口原理",而是 RT-Thread 风格的 UART 编程方法。


最核心的使用流程

你实际开发时基本就按这个顺序来:

查找串口设备
cpp 复制代码
 serial = rt_device_find("uart2");

作用:先根据名字拿到串口句柄。 找不到就没法继续。

如果参数要改,先配置
cpp 复制代码
   struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
   config.baud_rate = BAUD_RATE_9600;
   config.bufsz = 128;
   rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &config);

这一步用来改:

  • 波特率

  • 数据位

  • 停止位

  • 奇偶校验

  • 接收缓冲区大小

文档里有个很重要的点:

接收缓冲区大小 bufsz 要在 open 之前改。 因为串口一旦打开,接收缓冲区就已经建好了,之后不能动态改大小。

打开串口设备
cpp 复制代码
  rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);

打开时最关键的是选"工作模式"。


串口的几种工作模式

文档把 UART 的收发模式讲得比较清楚。你要先分清这三个概念:

轮询模式

你主动不停去查有没有数据。 简单,但效率低。

中断模式

有数据到达时,硬件触发中断,驱动通知你。 这是最常见模式。

DMA 模式

让 DMA 直接搬运数据,CPU 更省事。 适合数据量大、通信频繁的场景。

对应打开标志常见有:

  • RT_DEVICE_FLAG_INT_RX:中断接收

  • RT_DEVICE_FLAG_DMA_RX:DMA 接收

  • RT_DEVICE_FLAG_INT_TX:中断发送

  • RT_DEVICE_FLAG_DMA_TX:DMA 发送

  • RT_DEVICE_FLAG_STREAM:流模式

文档还特别提醒:

如果没指定中断或 DMA,默认就是轮询。

还有一个很实用的小点:

RT_DEVICE_FLAG_STREAM 用于终端输出时会自动把 \n 处理成 \r\n。


发送数据怎么做

很简单:

cpp 复制代码
 rt_device_write(serial, 0, str, len);

对 UART 来说,pos 参数基本没意义,可以忽略。 你主要关心:

  • buffer:发什么

  • size:发多少字节

如果底层支持异步发送,你还可以设置发送完成回调:

cpp 复制代码
  rt_device_set_tx_complete(serial, tx_done);

这个回调的作用是: 底层真把数据发完了,再通知你。


接收数据怎么做

这是这篇文档的重点。

初学者容易以为"接收数据"就是直接 read()。 但在 ++RT-Thread++ 里,推荐思路通常是:

回调负责"通知有数据来了",线程负责"真正去读数据"。

典型写法:

  1. 设置接收回调
cpp 复制代码
 rt_device_set_rx_indicate(serial, uart_input);
  1. 回调里别做重活,只做通知
cpp 复制代码
 static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
 {
   rt_sem_release(&rx_sem);
   return RT_EOK;
 }
  1. 在线程里读数据
cpp 复制代码
 while (rt_device_read(serial, -1, &ch, 1) != 1)
 {
   rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
 }

这套设计的意思是:

  • 中断/回调尽量短

  • 真正处理数据放在线程里

  • 这样更稳定,也更符合 RTOS 风格

这是整篇文档最值得学的思想。


文档给了两个典型例子

中断接收 + 轮询发送

这是最常见、最适合入门的模式。

套路是:

  • 找串口

  • 初始化一个信号量

  • 以 INT_RX 方式打开串口

  • 设置接收回调

  • 回调里释放信号量

  • 单独建一个线程等信号量

  • 线程被唤醒后去 read()

你可以把它理解成:

"中断负责叫醒我,线程负责处理数据。"

DMA 接收 + 轮询发送

这个模式适合一批一批收数据。 回调里不再只是发信号量,而是把"收到多少字节"通过消息队列发给处理线程。 线程收到消息后,再一次性读出这批数据。

你可以把它理解成:

中断模式适合来一个字节处理一下,DMA 模式更适合来一段数据再处理。


这篇文档的重点,不是 API 名字,而是下面这几个思想

复制代码
 1. UART 在 RT-Thread 里是设备,不是寄存器。
    应用层主要通过 rt_device_* 接口操作。
 ​
 2. 接收和发送模式要分开理解。
    例如你完全可以:
  • 中断接收 + 轮询发送

  • DMA 接收 + 轮询发送

文档里的例子就是这么组合的。

  1. 回调不是让你在里面处理全部业务的。 回调更适合做"通知"。 真正读数据、解析协议、打印内容,尽量放到线程里。

  2. 接收缓冲区大小很重要。 默认只有 RT_SERIAL_RB_BUFSZ,文档里默认值是 64。 如果你收得快、处理得慢,缓冲区可能被覆盖,导致丢数据。

  3. bufsz 要在 open 之前配置。 这是一个很容易踩坑的点。


如果我帮你把整篇文档翻译成人话

它其实就在回答这几个问题:

  • RT-Thread 里串口怎么找到?

  • 串口怎么设置波特率和缓冲区?

  • 串口怎么选择中断模式还是 DMA 模式?

  • 串口收到数据后,程序应该怎么组织?

  • 为什么回调里最好不要直接做很多事?


你现在最实用的最小心智模型

以后你写 RT-Thread 串口程序,脑子里先套这个模板:

cpp 复制代码
   serial = rt_device_find("uart2");
 ​
   config = RT_SERIAL_CONFIG_DEFAULT;
   config.bufsz = 128;
   rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &config);
 ​
   rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
 ​
   rt_device_write(serial, 0, "hello\r\n", 7);
 ​
   /* 在线程里等通知,再 read() */

你最该背下来的 6 个点

  1. rt_device_find() 先找串口。

  2. 参数不合适就先 rt_device_control() 配置。

  3. bufsz 要在 open 前改。

  4. 打开时要选接收模式:轮询 / 中断 / DMA。

  5. 接收回调主要负责通知,不要塞太多业务。

  6. 真正的数据处理尽量放在线程里做。

代码:

cpp 复制代码
/*
 * 立创开发板软硬件资料与相关扩展板软硬件资料官网全部开源
 * 开发板官网:www.lckfb.com
 * 文档网站:wiki.lckfb.com
 * 技术支持常驻论坛,任何技术问题欢迎随时交流学习
 * 嘉立创社区问答:https://www.jlc-bbs.com/lckfb
 * 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
 * 不靠卖板赚钱,以培养中国工程师为己任
 */

#include <getopt.h>
#include <string.h>
#include <rtthread.h>
#include <aic_core.h>
#include <stdlib.h>
#include <sys/time.h>
#include "hal_adcim.h"
#include "rtdevice.h"
#include "aic_log.h"
#include "hal_gpai.h"
#include <stdio.h>
#include "aic_hal_gpio.h"


#define SAMPLE_UART_NAME    "uart3"                 // 串口设备名称
#define RCV_BUFF_SIZE_MAX   1024                    // 接收最大字节长度

static struct rt_semaphore rx_sem;                  // 用于接收消息的信号量
static rt_device_t serial;                          // 串口设备句柄
static rt_thread_t serial_recv_thread;              // 串口接收线程句柄

static char serial_recv_buff[RCV_BUFF_SIZE_MAX];    // 串口接收缓存区
static char serial_recv_flag;                       // 串口接收标志
static int  serial_recv_length;                     // 接收字节长度


/* ====================串口发送和打印线程=================== */

#define THREAD_PRIORITY         25      // 线程优先级
#define THREAD_STACK_SIZE       4096    // 线程大小
#define THREAD_TIMESLICE        20      // 时间片

static rt_thread_t serial_thread = RT_NULL;   // 线程控制块


// 中断接收回调函数
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    /* 串口有数据传入后产生中断,调用此回调函数,释放信号量 */
    if (size > 0)
        rt_sem_release(&rx_sem);

    return RT_EOK;
}

// 串口接收线程入口函数
static void serial_recv_thread_entry(void *param)
{
    rt_kprintf("\nserial_recv_thread_entry run ......\n");

    while(1)
    {
        char temp_recv_buff = 0; // 接收临时缓存区

        int ret = rt_device_read(serial, 0, &temp_recv_buff, 1);

        if(ret < 0) // 出现了错误
        {
            pr_debug("read() return [%ld] %s\n", rt_get_errno(), rt_strerror(rt_get_errno()));
        }

        if(ret == 0) // 未接到数据
        {
            // 重置信号量
            rt_sem_control(&rx_sem, RT_IPC_CMD_RESET, RT_NULL);

            // 获取信号量,如果没有获取得到则阻塞在这里永远等待。
            rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
        }

        if(ret == 1) // 接收到1字节的数据
        {
            // 防止数据超出缓存区的大小
            if(serial_recv_length < RCV_BUFF_SIZE_MAX - 1)
            {
                // 存入接收缓存区并递增长度
                serial_recv_buff[serial_recv_length++] = temp_recv_buff;
                // rt_kprintf("%x\n", temp_recv_buff); // 打印接收到的字节,用于调试
            }
            else
            {
                // 如果缓冲区已满,则从0开始覆盖旧数据
                serial_recv_length = 0;
                serial_recv_buff[serial_recv_length++] = temp_recv_buff;
            }

            // 为接收缓存区最后添加 '\0'
            serial_recv_buff[serial_recv_length] = '\0';

            // 设置串口接收完成标志
            serial_recv_flag = 1;
        }
    }
}

/************************************************
函数名称 : Clear_recv_buff
功    能 : 清空串口接收缓存区
参    数 : 无
返 回 值 :
作    者 : LC
*************************************************/
static void Clear_recv_buff(void)
{
    // 清空接收缓存区
    rt_memset(serial_recv_buff, 0, sizeof(serial_recv_buff));

    // 清空标志位
    serial_recv_flag = 0;

    // 清空缓存区长度计量
    serial_recv_length = 0;
}

/************************************************
函数名称 : serial_send_byte
功    能 : 串口发送一个字节
参    数 : 发送的数据
返 回 值 : RT_EOK成功   -RT_ERROR失败
作    者 : LC
*************************************************/
static int Serial_Send_Byte(uint8_t dat)
{
    int ret = rt_device_write(serial, 0, &dat, 1);
    if(ret != 1)
    {
        LOG_E("Failed to [Serial_Send_Byte] code[%d] !!!", ret);
        return -RT_ERROR;
    }

    return RT_EOK;
}

/************************************************
函数名称 : Serial_Send_String
功    能 : 串口发送字符串
参    数 : data_buff缓存区地址
返 回 值 : RT_EOK成功   -RT_ERROR失败
作    者 : LCKFB
*************************************************/
static int Serial_Send_String(uint8_t *data_buff)
{
    int err_count = 0;

    /* 地址为空 或者 值为空 跳出 */
    while(data_buff && *data_buff)
    {
        if(RT_EOK != Serial_Send_Byte(*data_buff++))
        {
            err_count++;
            continue;
        }
    }

    /* 如果err_count不为0,则说明发送的时候有错误!!! */
    if(err_count)
    {
        LOG_E("serial_send_string failed !!!");
        return -RT_ERROR;
    }

    return RT_EOK;
}

/******************************************************************
 * 函 数 名 称:Serial_Recv_DATA
 * 函 数 说 明:接串口的数据
 * 函 数 形 参:data_buff数据缓存区
 * 函 数 返 回: 0:  未接收到数据
 *             其他: 接收到的数据长度
 * 作       者:LCKFB
 * 备       注:无
******************************************************************/
int Serial_Recv_DATA(uint8_t *data_buff)
{
    int i;

    /* 判断是否接到了数据 */
    if((serial_recv_flag != 1) || (serial_recv_length == 0))
    {
        /* 未接到 */
        return 0;
    }

    /* 将数据转存到指针指向的地址中 */
    for(i = 0; i < serial_recv_length; i++)
    {
        data_buff[i] = serial_recv_buff[i];
    }

    /* 加入字符串结尾 */
    data_buff[i] = '\0';

    /* 清除接收的数据、标志位和数据长度。 */
    Clear_recv_buff();

    return i; // 返回接收到的数据长度
}

/************************************************
函数名称 : UART_Init
功    能 : 串口初始化
参    数 : 无
返 回 值 : RT_EOK成功   -RT_ERROR失败
作    者 : LCKFB
*************************************************/
static int UART_Init(void)
{
    int ret = 0;

    // 清空接收缓存区
    rt_memset(serial_recv_buff,0,sizeof(serial_recv_buff));

    // 清空标志位
    serial_recv_flag = 0;

    // 清空缓存区长度计量
    serial_recv_length = 0;

    rt_kprintf("Try to open(%s)\n", SAMPLE_UART_NAME);

    // 获取串口句柄
    serial = rt_device_find(SAMPLE_UART_NAME);
    if (!serial)
    {
        LOG_E("find %s failed!\n", SAMPLE_UART_NAME);

        return -RT_ERROR;
    }

    // 初始化信号量
    ret = rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);
    if (ret != RT_EOK)
    {
        LOG_E("failed to rt_sem_init !\n");
        return -RT_ERROR;
    }

    // 打开串口设备
    ret = rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
    if (ret != RT_EOK)
    {
        LOG_E("open %s failed : %d !\n", SAMPLE_UART_NAME, ret);
        return -RT_ERROR;
    }

    // 设置接收回调函数
    rt_device_set_rx_indicate(serial, uart_input);

    // 创建串口数据接收线程
    serial_recv_thread = rt_thread_create("serial", serial_recv_thread_entry, RT_NULL, 1024*2, 15, 20);
    if (serial_recv_thread != RT_NULL)
    {
        // 启动线程
        rt_thread_startup(serial_recv_thread);
    }
    else
    {
        rt_device_close(serial);
        LOG_E("Failed to [rt_thread_create] !!!");
        return -RT_ERROR;
    }

    return RT_EOK;
}


// 线程入口函数
static void serial3_thread_entry(void *param)
{
    rt_kprintf("Start serial3_thread_entry...\n");

    while(1)
    {
        int count = 0;

        /* 接收缓存区 */
        uint8_t recv_buff[128] = {0};

        /* 获取接收到的数据长度 */
        count = Serial_Recv_DATA(recv_buff);

        /* 确保 count 不超过 recv_buff 大小,避免越界访问 */
        if (count > sizeof(recv_buff))
        {
            LOG_E("Error: Received data exceeds buffer size! count = %d",count);
            count = sizeof(recv_buff);  // 限制数据长度避免溢出
        }

        if (count > 0)
        {
            rt_kprintf("\n======================================\n");
            rt_kprintf("\nRead Data = %s\n", recv_buff);
            rt_kprintf("\n======================================\n");
        }

        /* 延迟 1000 毫秒 */
        rt_thread_mdelay(1000);
    }
}

// 数据发送函数
static void send_demoData(int argc, char **argv)
{
    static int num = 1;
    uint8_t Send_Buff[128] = {"立创·衡山派D133EBS开发板 * UART框架使用测试"};
    int ret = 0;
    char buffer[128] = {0};

    // 使用 snprintf 来格式化要发送的字符串
    snprintf(buffer, sizeof(buffer), "【%d】%s", num, (argc == 2) ? *(argv+1) : (char *)Send_Buff);

    // 发送数据
    ret = Serial_Send_String((uint8_t *)buffer);

    if(ret != RT_EOK)
    {
        LOG_E("%s: The test data transmission failed.", __FUNCTION__);
    }
    else
    {
        rt_kprintf("\n[%d] Send success\n", num);
        num++; // 只有发送成功时才递增 num
    }
}

// 导出函数为命令
MSH_CMD_EXPORT(send_demoData, Send test data);

// 串口接收和发送线程开启
static void uart3_test_on(int argc, char **argv)
{
    int ret = UART_Init();  // 串口初始化
    if(ret != RT_EOK)
    {
        LOG_E("Failed to [UART_Init] !!!");
        LOG_E("file: %s line: %d", __FILE__, __LINE__);
        return;
    }
    rt_kprintf("UART_Init run END!!\n");

    /* 创建线程,名称是 serial3_thread,入口是 serial3_thread_entry */
    serial_thread = rt_thread_create("serial3_thread",
                            serial3_thread_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);

    /* 如果获得线程控制块,启动这个线程 */
    if (serial_thread != RT_NULL)
        rt_thread_startup(serial_thread);

    rt_kprintf("Test transmission and reception using UART3 serial port!!\n");
}

// 导出函数为命令
MSH_CMD_EXPORT(uart3_test_on, Test transmission and reception using UART3 serial port);

最后测试串口(坑)

注意,这里官方没有说一个软件,但是其实我们测试是需要的

但是我用他的软件感觉不好用,所以这里推荐一个软件:Tabby

Tabby - a terminal for a more modern age

然后打开这个东西,我们把DeBug串口通过串口调试器和电脑端口相连,这里我的DeBug串口是com14,然后在纸飞机中观测的串口是com8

如果在Tabby这个软件的串口终端下按【回车】键没有反应的话,可以按一下开发板上的RST按键

然后我们就可以用com8连接的纸飞机调试助手发送数据测试程序了

其他的基本上看官方文档就行

相关推荐
Ww.xh1 小时前
STM32按键去抖动软件实现详解
stm32·单片机·嵌入式硬件
ghie90901 小时前
基于STM32的CAN通信完整例程(HAL库实现)
stm32·单片机·嵌入式硬件
lzj_pxxw2 小时前
W25Q64存储芯片 软件设计刚需常识
stm32·单片机·嵌入式硬件·mcu·学习
时空自由民.5 小时前
蓝牙协议栈介绍
linux·网络·单片机
蓝天居士5 小时前
M24C64芯片资料与程序代码(2)
嵌入式硬件·芯片资料
asjodnobfy7 小时前
开关电源尖峰电压计算
嵌入式硬件·硬件工程
振南的单片机世界7 小时前
开漏输出:只能拉低,不能拉高,高电平靠“外部”帮忙
stm32·单片机·嵌入式硬件
FFF团团员9099 小时前
CCS快速使用4(tim,pwm)
单片机·嵌入式硬件
某先森不吃鱼9 小时前
工程日志——离轴编码器矫正与磁场串扰解决
嵌入式硬件