相关文档链接
【立创·衡山派D133EBS开发板】UART框架使用 | 立创开发板技术文档中心
然后官方文档还是让我们先编写Sconscript
上面我们写led驱动时也写过Sconscript,然后我们现在就可以来对比一下两者有什么区别吗,看看那些东西是相同的哪些东西是不同的

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


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。
这篇文档主要讲了什么
-
UART 是什么 它先简单复习了串口基础:波特率、数据位、停止位、奇偶校验位这些参数必须两端一致,不然通信不正常。 这部分不是重点,重点是后面的"怎么在 RT-Thread 里访问 UART"。
-
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++ 里,推荐思路通常是:
回调负责"通知有数据来了",线程负责"真正去读数据"。
典型写法:
- 设置接收回调
cpp
rt_device_set_rx_indicate(serial, uart_input);
- 回调里别做重活,只做通知
cpp
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
rt_sem_release(&rx_sem);
return RT_EOK;
}
- 在线程里读数据
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 接收 + 轮询发送
文档里的例子就是这么组合的。
-
回调不是让你在里面处理全部业务的。 回调更适合做"通知"。 真正读数据、解析协议、打印内容,尽量放到线程里。
-
接收缓冲区大小很重要。 默认只有 RT_SERIAL_RB_BUFSZ,文档里默认值是 64。 如果你收得快、处理得慢,缓冲区可能被覆盖,导致丢数据。
-
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 个点
rt_device_find() 先找串口。
参数不合适就先 rt_device_control() 配置。
bufsz 要在 open 前改。
打开时要选接收模式:轮询 / 中断 / DMA。
接收回调主要负责通知,不要塞太多业务。
真正的数据处理尽量放在线程里做。

代码:
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连接的纸飞机调试助手发送数据测试程序了


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