RT-Thread 串口操作实践

添加串口设备

我手头的开发版是 ART-Pi 电路上引出了两排排针,其中有一个 USART1 串口,电路图如下:

CubeMX 配置

接下来我们打开示例代码 sdk-bsp-stm32h750-realthread-artpi\projects\art_pi_blink_led 中的 board 目录中的 CubeMX 进行配置。

这里需要注意的是有些功能的管脚是共用的,如果我们在 Categories 中点击选择,会默认选择管脚,但是实际上和我们所使用的不一致,这种情况要先在 Pinout view 上面点击选择对应管脚设置再打开。

配置完后在 Configuration 的 Parameter Settings 中设置一些串口基础东西,例如 波特率、校验位、停止位等。

然后 GENERATE CODE 生成代码,生成后我们还需要打开两个文件进行配置。

Kconfig 配置

由于工程中的 kconfig 的路径不对,所以不能使用 menuconfig 来配置,那就先不管这个了,打开 kconfig.h 文件配置是一样的。

board.h 文件中新增:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*-------------------------- UART CONFIG BEGIN --------------------------*/

/** After configuring corresponding UART or UART DMA, you can use it.
 *
 * STEP 1, define macro define related to the serial port opening based on the serial port number
 *                 such as     #define BSP_USING_UATR1
 *
 * STEP 2, according to the corresponding pin of serial port, define the related serial port information macro
 *                 such as     #define BSP_UART1_TX_PIN       "PA9"
 *                             #define BSP_UART1_RX_PIN       "PA10"
 *
 * STEP 3, if you want using SERIAL DMA, you must open it in the RT-Thread Settings.
 *                 RT-Thread Setting -> Components -> Device Drivers -> Serial Device Drivers -> Enable Serial DMA Mode
 *
 * STEP 4, according to serial port number to define serial port tx/rx DMA function in the board.h file
 *                 such as     #define BSP_UART1_RX_USING_DMA
 *
 */

#ifdef BSP_USING_UART1
#define BSP_UART1_TX_PIN       "PA9"
#define BSP_UART1_RX_PIN       "PA10"
#endif

#ifdef BSP_USING_UART4
#define BSP_UART4_TX_PIN       "PA0"
#define BSP_UART4_RX_PIN       "PI9"
#endif

#ifdef BSP_USING_UART6
#define BSP_UART6_TX_PIN       "PC6"
#define BSP_UART6_RX_PIN       "PC7"
#endif


/*-------------------------- UART CONFIG END --------------------------*/

rtconfig.h 中新增:

1
2
3
4
5
6
7
/* On-chip Peripheral */

#define BSP_USING_GPIO
#define BSP_USING_UART
#define BSP_USING_UART4
#define BSP_USING_UART1
/* end of On-chip Peripheral */

接下来使用 ENV 命令构建 scons --target=mdk5

打开 MDK 工程编译运行。

使用 list_device 命令就可以看到刚才加入的串口设备了。

1
2
3
4
5
6
7
msh />list_device
device           type         ref count
-------- -------------------- ----------
uart4    Character Device     2       
uart1    Character Device     1       
pin      Miscellaneous Device 0       
msh />

访问串口设备

关于串口通信的基础知识这里就不累赘复述了,直接看 RT-Thread 中的 API.

应用程序通过 RT-Thread提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示:

函数描述
rt_device_find()查找设备
rt_device_open()打开设备
rt_device_read()读取数据
rt_device_write()写入数据
rt_device_control()控制设备
rt_device_set_rx_indicate()设置接收回调函数
rt_device_set_tx_complete()设置发送完成回调函数
rt_device_close()关闭设备

不管三七二十一,先上官网的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <rtthread.h>
#include <rtdevice.h>
#include "drv_common.h"

#define THREAD_STACK_SIZE 1000 //线程栈大小(字节)
#define THREAD_TIMESLICE 40   //占用的滴答时钟数
#define SAMPLE_UART_NAME  "uart1"    /* 串口设备名称 */

static rt_thread_t serial_thread = RT_NULL;
static rt_uint8_t thread_priority = 20;
static rt_device_t serial;                /* 串口设备句柄 */
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;  /* 初始化配置参数 */

void serial_thread_entry(void *params)
{
    serial = rt_device_find(SAMPLE_UART_NAME);
    if(serial != RT_NULL)
    {
        /* step2:修改串口配置参数 */
        config.baud_rate = BAUD_RATE_115200;        //修改波特率为 115200
        config.data_bits = DATA_BITS_8;           //数据位 8
        config.stop_bits = STOP_BITS_1;           //停止位 1
        config.bufsz     = 128;                   //修改缓冲区 buff size 为 128
        config.parity    = PARITY_NONE;           //无奇偶校验位

        /* step3:控制串口设备。通过控制接口传入命令控制字,与控制参数 */
        rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &config);

        /* step4:打开串口设备。以中断接收及轮询发送模式打开串口设备 */
        rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
    }
}

//创建串口线程
int create_serial_thread(void)
{
    serial_thread = rt_thread_create("serial_test", serial_thread_entry, 
                    RT_NULL, THREAD_STACK_SIZE, thread_priority, THREAD_TIMESLICE);
    if(serial_thread != RT_NULL)
    {
         rt_thread_startup(serial_thread);
    }
    return 0;
}

INIT_APP_EXPORT(create_serial_thread);

编译运行发现一切正常,如果有异常请下断点跟踪判断问题所在。走到这里说明我们的串口已经打开,接下来就是和具体串口协议相关的通信了。

向 PC 发数据

先看看硬件电路图,在外部排针上面完成接线,如下图使用的是 P1 那一排排针 TX 和 RX 使用的是 8 和 10 两个排针针脚, 14 脚接地线。

接线电路图 接线电路图

接线实物图 接线实物图

先发送一个测试字符串:

1
2
3
4
char str[] = "hello RT-Thread!\r\n";

//发送字符串
rt_device_write(serial, 0, str, (sizeof(str) - 1));

在 PC 上用串口助手连接并接收数据如下:

中断接收及轮询发送

我们打开串口的模式为 RT_DEVICE_FLAG_INT_RX 表示中断接收数据模式,轮训发送数据模式,该参数还支持以下几种:

1
2
3
4
5
6
7
#define RT_DEVICE_FLAG_STREAM       0x040     /* 流模式      */
/* 接收模式参数 */
#define RT_DEVICE_FLAG_INT_RX       0x100     /* 中断接收模式 */
#define RT_DEVICE_FLAG_DMA_RX       0x200     /* DMA 接收模式 */
/* 发送模式参数 */
#define RT_DEVICE_FLAG_INT_TX       0x400     /* 中断发送模式 */
#define RT_DEVICE_FLAG_DMA_TX       0x800     /* DMA 发送模式 */

可以通过如下函数来设置数据接收指示,当串口收到数据时,通知上层应用线程有数据到达 :

1
rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev,rt_size_t size));
参数描述
dev设备句柄
rx_ind回调函数指针
dev设备句柄(回调函数参数)
size缓冲区数据大小(回调函数参数)
返回——
RT_EOK设置成功

可调用如下函数读取串口接收到的数据:

1
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);
参数描述
dev设备句柄
pos读取数据偏移量,此参数串口设备未使用
buffer缓冲区指针,读取的数据将会被保存在缓冲区中
size读取数据的大小
返回——
读到数据的实际大小如果是字符设备,返回大小以字节为单位
0需要读取当前线程的 errno 来判断错误状态

整个处理过程如下图:

实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <rtthread.h>
#include <rtdevice.h>
#include "drv_common.h"

#define THREAD_STACK_SIZE 1000 //线程栈大小(字节)
#define THREAD_TIMESLICE 40   //占用的滴答时钟数
#define SAMPLE_UART_NAME  "uart1"    /* 串口设备名称 */

static rt_thread_t serial_thread = RT_NULL;
static rt_uint8_t thread_priority = 20;
static rt_device_t serial;                /* 串口设备句柄 */
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;  /* 初始化配置参数 */
static struct rt_semaphore rx_sem; /* 用于接收消息的信号量 */

char str[] = "hello RT-Thread!\r\n";

static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    // 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量
    rt_sem_release(&rx_sem);

    return RT_EOK;
}

static void serial_read_entry(void *parameter)
{
    char ch;
    while (1)
    {
        //从串口读取一个字节的数据,没有读取到则等待接收信号量
        while (rt_device_read(serial, -1, &ch, 1) != 1)
        {
            // 阻塞等待接收信号量,等到信号量后再次读取数据
            rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
        }
        //读取到的数据通过串口错位输出
        ch = ch + 1;
        rt_device_write(serial, 0, &ch, 1);
    }
}

void serial_thread_entry(void *params)
{
    serial = rt_device_find(SAMPLE_UART_NAME);
    if(serial != RT_NULL)
    {
        //修改串口配置参数
        config.baud_rate = BAUD_RATE_115200;       //修改波特率为 115200
        config.data_bits = DATA_BITS_8;           //数据位 8
        config.stop_bits = STOP_BITS_1;           //停止位 1
        config.bufsz     = 128;                   //修改缓冲区 buff size 为 128
        config.parity    = PARITY_NONE;           //无奇偶校验位

        //控制串口设备。通过控制接口传入命令控制字,与控制参数 */
        rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &config);

        //打开串口设备。以中断接收及轮询发送模式打开串口设备 */
        rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
			
        //发送字符串
        rt_device_write(serial, 0, str, (sizeof(str) - 1));

        //初始化信号量
        rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);

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

        //创建读线程
        rt_thread_t thread = rt_thread_create("serial_read", serial_read_entry, RT_NULL, 1024, 25, 10);
        //启动读线程
        if (thread != RT_NULL)
        {
            rt_thread_startup(thread);
        }
    }
}

//创建串口线程
int create_serial_thread(void)
{
    serial_thread = rt_thread_create("serial_init", serial_thread_entry, 
                    RT_NULL, THREAD_STACK_SIZE, thread_priority, THREAD_TIMESLICE);
    if(serial_thread != RT_NULL)
    {
         rt_thread_startup(serial_thread);
    }
    return 0;
}

INIT_APP_EXPORT(create_serial_thread);

关于上面的信号量实质上是为了解决线程间同步问题的,上面例子中只使用了一个线程,实际上我们可以创建多个线程来处理读取的数据。

信号量控制块结构的详细定义如下:

1
2
3
4
5
6
7
struct rt_semaphore
{
   struct rt_ipc_object parent;  /* 继承自 ipc_object 类 */
   rt_uint16_t value;              /* 信号量的值 */
};
/* rt_sem_t 是指向 semaphore 结构体的指针类型 */
typedef struct rt_semaphore* rt_sem_t;

线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减 1(相当于停了一辆车):

1
rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);

释放信号量可以唤醒挂起在该信号量上的线程(相当于车位管理员说有空余车位了):

1
rt_err_t rt_sem_release(rt_sem_t sem);

当信号量的值等于零时,并且有线程等待这个信号量时,释放信号量将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量;否则将把信号量的值加 1。