RT-THREAD学习笔记

  • Post author:
  • Post category:其他




2020年9月29日23:36:01

系统中总共存在两类线程,分别是系统线程和用户线程

RT-Thread 的线程调度器是抢占式的

静态创建的线程需要用到线程控制块

线程栈

RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。



线程的优先级

RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,


0 为最高优先级


。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

线程不加循环的时候 在执行完毕后,线程将被系统自动删除。

#define RT_EOK           0 /* 无错误     */
#define RT_ERROR         1 /* 普通错误     */
#define RT_ETIMEOUT      2 /* 超时错误     */
#define RT_EFULL         3 /* 资源已满     */
#define RT_EEMPTY        4 /* 无资源     */
#define RT_ENOMEM        5 /* 无内存     */
#define RT_ENOSYS        6 /* 系统不支持     */
#define RT_EBUSY         7 /* 系统忙     */
#define RT_EIO           8 /* IO 错误       */
#define RT_EINTR         9 /* 中断系统调用   */
#define RT_EINVAL       10 /* 非法参数      */



线程的状态

  1. 线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT);
  2. 初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY);
  3. 就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);
  4. 当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);
  5. 处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,
  6. 如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE);
  7. 而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。

空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪态

最后空闲线程会回收被删除线程的资源



动态线程 静态线程的创建

可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。



创建动态线程

rt_thread_t rt_thread_create(const char* name,
                            void (*entry)(void* parameter),
                            void* parameter,
                            rt_uint32_t stack_size,
                            rt_uint8_t priority,
                            rt_uint32_t tick);
参数 描述
name 线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉
entry 线程入口函数
parameter 线程入口函数参数
stack_size 线程栈大小,单位是字节
priority 线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级
tick 线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回
thread 线程创建成功,返回线程句柄
RT_NULL 线程创建失败



删除动态线程线程

rt_err_t rt_thread_delete(rt_thread_t thread);

在这里插入图片描述



创建静态线程

rt_err_t rt_thread_init(struct rt_thread* thread,
                        const char* name,
                        void (*entry)(void* parameter), void* parameter,
                        void* stack_start, rt_uint32_t stack_size,
                        rt_uint8_t priority, rt_uint32_t tick);

在这里插入图片描述



静态线程脱离

rt_err_t rt_thread_detach (rt_thread_t thread);

在这里插入图片描述



启动线程

rt_err_t rt_thread_startup(rt_thread_t thread);

在这里插入图片描述



2020年9月30号

UART 设备

这里的外设的驱动都给我们写好了,

应用程序通过 RT-Thread提供的 I/O 设备管理接口来访问串口硬件

在这里插入图片描述

一般来说,使用外设,不需要自己去初始化了。用之前你需要先找到你所要用的外设,就用


rt_device_t rt_device_find(const char* name);

返回值赋值给设备结构体

在这里插入图片描述

struct rt_device
{
    struct rt_object          parent;        /* 内核对象基类 */
    enum rt_device_class_type type;          /* 设备类型 */
    rt_uint16_t               flag;          /* 设备参数 */
    rt_uint16_t               open_flag;     /* 设备打开标志 */
    rt_uint8_t                ref_count;     /* 设备被引用次数 */
    rt_uint8_t                device_id;     /* 设备 ID,0 - 255 */

    /* 数据收发回调函数 */
    rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
    rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);

    const struct rt_device_ops *ops;    /* 设备操作方法 */

    /* 设备的私有数据 */
    void *user_data;
};
typedef struct rt_device *rt_device_t;

查找到之后呢,就可以选择打开或者关闭


设备打开


rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags)


在这里插入图片描述


流模式 RT_DEVICE_FLAG_STREAM 可以和接收发送模式参数使用或 “|” 运算符一起使用。 是可以用与符号共用

在这里插入图片描述


控制串口设备



rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);


在这里插入图片描述

CMD 的取值还不知到那个是干嘛的,

serial_configure 就是定义一个结构体,初始化好之后 在使用(有宏定义使用)

在这里插入图片描述

配置好了,打开了,就可以使用了

写入数据,发送数据 这个函数应该是通用的


rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);

在这里插入图片描述

还可以设置发送完成回调函数


rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));

在这里插入图片描述

设置接收回调函数


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));


在这里插入图片描述

中断方式,和DMA方式,都会用到接收回调函数,所以你要用到的话,要设置回调函数


接收数据



rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);


在这里插入图片描述

关闭设备


rt_err_t rt_device_close(rt_device_t dev);


在这里插入图片描述


关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。


线程之间的通信



信号量


rt_semaphore

在这里插入图片描述

创建信号量

rt_sem_t rt_sem_create(const char *name,
                        rt_uint32_t value,
                        rt_uint8_t flag);


flag RT_IPC_FLAG_FIFO(先进先出)RT_IPC_FLAG_PRIO(优先级等待)


在这里插入图片描述

信号量还有静态信号量

不需要创建,直接使用?应该也要用一个结构体来获取这个静态结构体

rt_err_t rt_sem_init(rt_sem_t       sem,
                    const char     *name,
                    rt_uint32_t    value,
                    rt_uint8_t     flag)

在这里插入图片描述




2020年10月11日21:50:33

AT组件



AT Client 初始化

配置开启 AT Client 配置之后,需要在启动时对它进行初始化 (如果使用组件自动初始化,就不用单独初始化了)

AT Client 初始化


int at_client_init(const char *dev_name, rt_size_t recv_bufsz);

创建响应结构体


at_response_t at_create_resp(rt_size_t buf_size, rt_size_t line_num, rt_int32_t timeout);



//响应结构体初始化吧

struct at_response
{
    /* response buffer */
    char *buf;
    /* the maximum response buffer size */
    rt_size_t buf_size;
    /* the number of setting response lines
     * == 0: the response data will auto return when received 'OK' or 'ERROR'
     * != 0: the response data will return when received setting lines number data */
    rt_size_t line_num;
    /* the count of received response lines */
    rt_size_t line_counts;
    /* the maximum response time */
    rt_int32_t timeout;
};
typedef struct at_response *at_response_t;

删除响应结构体


void at_delete_resp(at_response_t resp);


该函数用于删除创建的响应结构体对象,一般与at_create_resp创建函数成对出现。

设置响应结构体参数


at_response_t at_resp_set_info(at_response_t resp, rt_size_t buf_size, rt_size_t line_num, rt_int32_t timeout);


在这里插入图片描述


发送命令并接收响应

这里只发送 ,响应有其他函数


rt_err_t at_exec_cmd(at_response_t resp, const char *cmd_expr, ...);


在这里插入图片描述


该函数用于 AT Client 发送命令到 AT Server,并等待接收响应,其中 resp 是已经创建好的响应结构体的指针,AT 命令的使用匹配表达式的可变参输入,输入命令的结尾不需要添加命令结束符。



AT Client 数据解析方式


前提是开发者需要提前查看相关手册了解 AT Client 连接的 AT Server 设备响应数据的基本格式。

(还要你说?)

获取指定行号的响应数据


const char *at_resp_get_line(at_response_t resp, rt_size_t resp_line);

该函数用于在 AT Server 响应数据中获取指定行号的一行数据。行号是以标准数据结束符来判断的,上述发送和接收函数 at_exec_cmd 已经对响应数据的数据和行号进行记录处理存放于 resp 响应结构体中,这里可以直接获取对应行号的数据信息。

在这里插入图片描述

解析指定行号的响应数据


int at_resp_parse_line_args(at_response_t resp, rt_size_t resp_line, const char *resp_expr, ...);

该函数用于在 AT Server 响应数据中获取指定行号的一行数据, 并解析该行数据中的参数。

在这里插入图片描述

解析指定关键字行的响应数据


int at_resp_parse_line_args_by_kw(at_response_t resp, const char *keyword, const char *resp_expr, ...);

该函数用于在 AT Server 响应数据中获取包含关键字的一行数据, 并解析该行数据中的参数。

在这里插入图片描述

2020年10月12日22:21:28

AT 组件使用到的***

sscanf

***函数用法详解


使用

%s

赋值,遇到空格就会结束




使用

%4s

表示取最大长度为4字节的字符串

%ns






使用

%[^a-z]

表示取遇到任意小写字母为止的字符串 ^是异或的意思吧,然后遇到这些字符就不要




%[1-9a-z]

表示 只获取括号里面的字符



但是一旦取不到里面的任何字符就会停止 不会取到最后一个


sscanf(“192.168.1.1”, “%d.%d.%d.%d”,&a,&b,&c,&d); 取到了里面的数字







sscanf("hello/you are good!@world", "%*[^/]/%[^@]", str);




you are good!”




表示跳过数据%

[^/]里面又是异或的,说明包括那个数据和前面的数据都不要 %应该就是表示获取的意思

在这里插入图片描述

"%*[^\"]\"%[^\"]\""

意思是 忽略

\ ”

两个符号之前的数据,包括他本身 然后 \ ” 是为了匹配 %是为了匹配到对应格式数据 \ ” 之后的数据 直到遇到了 ” 就把ip 截取下来了

%*[^”] 忽略”之前的数据 “%[^”]读取 \ “之后的数据直到遇到了 ” 最后的” 应该是表示完整个字符串




2020年10月13日22:08:07

SAL 套接字抽象层 我现在还不懂这东西,,我先学习下

我怀疑在直接引用EC20组件之后,,然后就能直接使用这个SAL

好像直接使用,不用管底层,一套标准 BSD Socket API 直接调用即可

最顶层是网络应用层,提供一套标准 BSD Socket API ,如 socket、connect 等函数,用于系统中大部分网络开发应用。

往下第二部分为 SAL 套接字抽象层,通过它 RT-Thread 系统能够适配下层不同的网络协议栈,并提供给上层统一的网络编程接口,方便不同协议栈的接入。套接字抽象层为上层应用层提供接口有:accept、connect、send、recv 等。

第三部分为 netdev 网卡层,主要作用是解决多网卡情况设备网络连接和网络管理相关问题,通过 netdev 网卡层用户可以统一管理各个网卡信息和网络连接状态,并且可以使用统一的网卡调试命令接口。

第四部分为协议栈层,该层包括几种常用的 TCP/IP 协议栈,例如嵌入式开发中常用的轻型 TCP/IP 协议栈 lwIP 以及 RT-Thread 自主研发的 AT Socket 网络功能实现等。这些协议栈或网络功能实现直接和硬件接触,完成数据从网络层到传输层的转化。

RT-Thread 的网络应用层提供的接口主要以标准 BSD Socket API 为主,这样能确保程序可以在 PC 上编写、调试,然后再移植到 RT-Thread 操作系统上。(我们不用管底层驱动比如我们用qemu写网络驱动)

有好几种协议栈和网络类型 lwIP 协议栈、AT Socket 协议栈(rt-thread自己编写的)、WIZnet 硬件 TCP/IP 协议栈。


int socket(int domain, int type, int protocol);


上述为标准 BSD Socket API 中 socket 创建函数的定义,domain 表示协议域又称为协议簇(family),用于判断使用哪种协议栈或网络实现,AT Socket 协议栈使用的簇类型为

AF_AT

,lwIP 协议栈使用协议簇类型有

AF_INET

等,WIZnet 协议栈使用的协议簇类型为

AF_WIZ

SAL 组件主要作用是统一抽象底层 BSD Socket API 接口,下面以 connect 函数调用流程为例说明 SAL 组件函数调用方式:

connect:SAL 组件对外提供的抽象的 BSD Socket API,用于统一 fd 管理;(意思就是,只要关注这个就行,不用关注各自的名字了)

sal_connect:SAL 组件中 connect 实现函数,用于调用底层协议栈注册的 operation 函数。

lwip_connect:底层协议栈提供的层 connect 连接函数,在网卡初始化完成时注册到 SAL 组件中,最终调用的操作函数。


SAL TLS

加密用的

在 TCP、UDP等协议数据传输时,由于数据包是明文的,所以很可能被其他人拦截并解析出信息,这给信息的安全传输带来很大的影响。为了解决此类问题,一般需要用户在应用层和传输层之间添加 SSL/TLS 协议。

使用历程

#include <stdio.h>
#include <string.h>

#include <rtthread.h>
#include <sys/socket.h> 
#include <netdb.h>

/* RT-Thread 官网,支持 TLS 功能 */
#define SAL_TLS_HOST    "www.rt-thread.org"
#define SAL_TLS_PORT    443
#define SAL_TLS_BUFSZ   1024

static const char *send_data = "GET /download/rt-thread.txt HTTP/1.1\r\n"
    "Host: www.rt-thread.org\r\n"
    "User-Agent: rtthread/4.0.1 rtt\r\n\r\n";

void sal_tls_test(void)//测试tls
{
    int ret, i;
    char *recv_data;
    struct hostent *host;
    int sock = -1, bytes_received;
    struct sockaddr_in server_addr;

    /* 通过函数入口参数url获得host地址(如果是域名,会做域名解析) */ // 解析出域名
    host = gethostbyname(SAL_TLS_HOST);//获取域名

    recv_data = rt_calloc(1, SAL_TLS_BUFSZ);//分配多个内存块  一个内存块 容量为1024
    if (recv_data == RT_NULL)
    {
        rt_kprintf("No memory\n");
        return;
    }
//簇用的是at的    流套接字  加密类型
    /* 创建一个socket,类型是SOCKET_STREAM,TCP 协议, TLS 类型 */
    if ((sock = socket(AF_INET, SOCK_STREAM, PROTOCOL_TLS)) < 0)//创建套接字
    {
        rt_kprintf("Socket error\n");
        goto __exit;
    }

    /* 初始化预连接的服务端地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SAL_TLS_PORT);
    //端口号把实际主机内存中的整数存放方式调整成网络字节顺序
    server_addr.sin_addr = *((struct in_addr *)host->h_addr);
    //上面解析出来了  我觉的他给解析好了 ip 地址直接出来了
    rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));//初始化

    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
    {//连接
        rt_kprintf("Connect fail!\n");
        goto __exit;
    }

    /* 发送数据到 socket 连接 */ //get 数据   然后就会返回数据过来  那是标准的格式
    ret = send(sock, send_data, strlen(send_data), 0);
    if (ret <= 0)
    {
        rt_kprintf("send error,close the socket.\n");
        goto __exit;
    }

    /* 接收并打印响应的数据,使用加密数据传输 */
    bytes_received = recv(sock, recv_data, SAL_TLS_BUFSZ  - 1, 0);
    if (bytes_received <= 0)
    {
        rt_kprintf("received error,close the socket.\n");
        goto __exit;
    }

    rt_kprintf("recv data:\n");
    for (i = 0; i < bytes_received; i++)
    {
        rt_kprintf("%c", recv_data[i]);
    }

__exit:
    if (recv_data)
        rt_free(recv_data);

    if (sock >= 0)
        closesocket(sock);
}

#ifdef FINSH_USING_MSH
#include <finsh.h>
MSH_CMD_EXPORT(sal_tls_test, SAL TLS function test);
#endif /* FINSH_USING_MSH */

2020年10月22日22:44:07

使用sal套接字抽象层

使用步骤


  1. 首先先创建套接字

    ;socket(协议簇类型,协议类型,运输层协议)

    domain / 协议族类型:

    • AF_INET: IPv4

    • AF_INET6: IPv6

    type / 协议类型:

    • SOCK_STREAM:流套接字

    • SOCK_DGRAM:数据报套接字

    • SOCK_RAW:原始套接字

    运输层协议;加密不加密这些东西

  2. 创建成功之后就可以连接了

    。连接首先要解决 服务器地址,服务器端口号
int connect(int s, const struct sockaddr *name, socklen_t namelen);
s 套接字描述符  传入上面创建的套接字
name 服务器地址信息  这是一个结构体。 
namelen 服务器地址结构体的长度
返回 –
0 成功,返回新创建的套接字描述符
-1 失败

不使用了就要关闭套接字

int closesocket(int s);//传入套接字描述符,

tcp粘包 拆包分析

发送的数据的时候,封包 在每次数据的结尾添加上结束标识符,每次加上 \r\n 行不行?

如果我发送的字符串的画,可以 不管是什么数据,,在发送完 数据的时候,在结尾就加上接收标识符

接收的话 ,定义两个数据,,tcp接收的数据,判断下,,有没有结束符,如果有,结束符前 的数据放另一个数组中 ,如果没有的话,就一直放,知道检测到为止

当接收到一个数据时候,用strstr判断字符的结尾标志,如果没找到,就把接受到的数据添加到字符串的尾巴上,定义一个添加到尾巴上的函数,,,然后如果下一次接受到了结束符,然后就看下结束符前有多少个数据,把这之前的数据存到字符串里面然后 就可以使用了 ,发完一次数据,如果数据挨 的很紧,,,只要一接受完数据,就把数据处理掉,然后还要判断结束符后面还有没有数据,把数据保存起来,,我觉的应该在把数据用完之后,然后接受的字符串清除,然后在在把数据放进去, 服务端劲量发完一次消息之后隔个一段时间在发出来

首先先定义添加到字符串尾巴的函数 然后判断字符串尾巴,,字符串尾巴前面的数据添加

2020年11月11日22:39:51

HTTP学习

在这里插入图片描述



版权声明:本文为qq_41531368原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。