目录
4.将2个uint8_t的数据转换为一个uint16_t的数据
5.rt_ringbuffer_put() 和 rt_ringbuffer_put_force()
前言
操作过程中出现了以下2个小问题
一、操作思路
1.发送串口数据
int uart_send_char(rt_uint8_t *data_buffer, rt_uint8_t len)
{
rt_device_write(serial, 0, data_buffer, len);
}
获取串口数据,使用rt_sem_take来获取信号量进行保护、在数据收发时对数据进行临界保护,如一个线程负责收数据并解析到结构体,另一个线程负责对解析到结构体的数据进行二次操作,这两个地方都需要进行临界数据保护,防止二次操作结构体时该结构体在另一个线程被不停的改变。
2.收到的数据大于缓冲区需置于0
if(receive_count >= BUFFER_SIZE)
receive_count = 0;
3.收到数据并将其放到receive_buffer
uint8_t receive_state = FRAME_HEAD;
case FRAME_HEAD: //帧头 adressif (data == AHEAD_ADDRESS || data == REAR_ADDRESS ||data == WIRELESS_ADDRESS || set_address_flag == 1 || data == TEMP_ADDRESS)
{
receive_buffer[receive_count++] = data;
receive_state = FRAME_CMD;}
4.将2个uint8_t的数据转换为一个uint16_t的数据
这在modbus里比较常见
static uint16_t uint16_big_decode(const uint8_t *p_encoded_data)
{
return ((((uint16_t)((uint8_t *)p_encoded_data)[0]) << 8) |
(((uint16_t)((uint8_t *)p_encoded_data)[1])));
}
5.根据帧头做判断
receive_buffer[FRAME_HEAD] == WIRELESS_ADDRESS来进行正确数据的处理与接收
case FRAME_CRC16: //CRC-16/MODBUS
receive_buffer[receive_count++] = data;
temp_data_length++;
if (temp_data_length == 2)
{
if(receive_buffer[FRAME_HEAD] == WIRELESS_ADDRESS)
{
crc16_data_sum = CRC16_MODBUS(receive_buffer, receive_count - 2);
crc16_data_rec = uint16_big_decode(&receive_buffer[receive_count - 2]);
if (uint16_big_decode((uint8_t *)&crc16_data_sum) == crc16_data_rec)
{
wireless_charge_recv_process(receive_buffer, receive_count);
}
}else if(receive_buffer[FRAME_HEAD] == TEMP_ADDRESS){
crc16_data_sum = CRC16_MODBUS(receive_buffer, receive_count - 2);
crc16_data_rec = uint16_big_decode(&receive_buffer[receive_count - 2]);
if (uint16_big_decode((uint8_t *)&crc16_data_sum) == crc16_data_rec)
{
parse_and_process_data(receive_buffer, receive_count);
}
}
else
6.代码分析
其中,void wireless_charge_recv_process(rt_uint8_t *recv_buffer,rt_uint8_t receive_count)因为另一个文件的对外接口,将收到的字符复制到一个结构体中rt_memcpy(wc_rb_recv.buffer,recv_buffer,receive_count);该对外接口使用了一个循环缓冲区
if(rt_ringbuffer_put_force(wc_rb,(rt_uint8_t *)&wc_rb_recv,sizeof(struct wc_rb_t)) != sizeof(struct wc_rb_t))
{
rt_kprintf("wc_rb_recv send error\n");
}
7.在入口函数中对环形缓冲区进行数据读取
void wireless_charge_thread_entry(void *parameter)
{
rt_uint8_t recv_buffer[64] = {0};
struct wc_rb_t wc_rb_recv = {0};
struct wc_recv_t wc_recv = {0};
static int send_cnt = 0;
wc.charge_opt_mode = NOTHING_OPT;
while(1)
{
rt_memset(&wc_rb_recv,0,sizeof(struct wc_rb_t));
if(send_cnt++ > 25)
{
send_cnt = 0;
rt_uint16_t data = 0;
wc_send(WIRELESS_ADDRESS,0x03,VOLTAGE,&data,6);
}
if(rt_ringbuffer_get(wc_rb,(rt_uint8_t *)&wc_rb_recv,sizeof(struct wc_rb_t)) == sizeof(struct wc_rb_t))
{
rt_memset(wc.wc_buffer,0,sizeof((wc.wc_buffer)));
rt_memcpy(&wc_recv.buffer,wc_rb_recv.buffer,wc_rb_recv.len);
wc_recv_process(wc_recv,wc_rb_recv.cmd);
wc.tick.link_tick = rt_tick_get();
wc.tick.fault_tick = rt_tick_get();
}
wc_exception_handling();
if(simulate_flag)
{
rt_enter_critical();
rt_memcpy(&wc,&simulate_wc,sizeof(struct wc_t));
rt_exit_critical();
}
rt_thread_delay(10);
}
}
从环形缓冲区取出来的数据正确的话就wc_recv_process(wc_recv,wc_rb_recv.cmd);
直接进行数据处理。
二、消息队列rt_mq
的应用案例
在基于 RT-Thread 实时操作系统中,消息队列(Message Queue)是实现任务间通信的常用机制。rt_mq_send
是用于将消息发送到消息队列中的函数。为了创建一个消息队列应用案例,我们需要涉及到消息队列的初始化、发送和接收操作。下面是一个基于 RT-Thread 的简单消息队列应用案例。
1. 引入必要的头文件
#include <rtthread.h>
#include <rthw.h>
2. 定义消息结构体
在这个例子中,我们定义一个消息结构体 msg
,该结构体将用于存储要传递的消息内容。
struct msg {
int id;
char content[64];
};
3. 初始化消息队列
在 RT-Thread 中,消息队列需要进行初始化,使用 rt_mq_init
函数来创建消息队列。rt_mq_init
函数的参数包括消息队列的名称、消息池、每条消息的大小、消息池的总大小以及消息队列的标志位。
#define MSG_QUEUE_SIZE 10 // 消息队列的最大消息数
#define MSG_SIZE sizeof(struct msg) // 每条消息的大小
static struct rt_messagequeue rx_mq; // 消息队列对象
static char msg_pool[MSG_QUEUE_SIZE * MSG_SIZE]; // 消息池
// 初始化消息队列
rt_mq_init(&rx_mq, "rx_mq", msg_pool, MSG_SIZE, sizeof(msg_pool), RT_IPC_FLAG_FIFO);
4. 发送消息
消息发送使用 rt_mq_send
函数,该函数将消息发送到消息队列中。发送消息时,我们需要指定消息队列、消息内容及消息的大小。
struct msg send_msg; send_msg.id = 1;
strncpy(send_msg.content, "Hello, RT-Thread!", sizeof(send_msg.content));
// 发送消息到队列
rt_mq_send(&rx_mq, &send_msg, sizeof(send_msg));
5. 接收消息
接收消息使用 rt_mq_recv
函数,该函数从消息队列中获取一条消息。接收操作会阻塞,直到有消息可用为止。
struct msg recv_msg; // 从队列接收消息
rt_mq_recv(&rx_mq, &recv_msg, sizeof(recv_msg), RT_WAITING_FOREVER);
// 打印接收到的消息
rt_kprintf("Received message: ID = %d, Content = %s\n", recv_msg.id, recv_msg.content);
6. 例子总结
完整的应用程序代码如下:
#include <rtthread.h>
#include <rthw.h>
struct msg {
int id;
char content[64];
};
#define MSG_QUEUE_SIZE 10
#define MSG_SIZE sizeof(struct msg)
static struct rt_messagequeue rx_mq;
static char msg_pool[MSG_QUEUE_SIZE * MSG_SIZE];
static void message_send_thread(void *parameter)
{
struct msg send_msg;
send_msg.id = 1;
strncpy(send_msg.content, "Hello, RT-Thread!", sizeof(send_msg.content));
rt_mq_send(&rx_mq, &send_msg, sizeof(send_msg));
rt_kprintf("Message sent: ID = %d, Content = %s\n", send_msg.id, send_msg.content);
}
static void message_receive_thread(void *parameter)
{
struct msg recv_msg;
rt_mq_recv(&rx_mq, &recv_msg, sizeof(recv_msg), RT_WAITING_FOREVER);
rt_kprintf("Message received: ID = %d, Content = %s\n", recv_msg.id, recv_msg.content);
}
int main(void)
{
// 初始化消息队列
rt_mq_init(&rx_mq, "rx_mq", msg_pool, MSG_SIZE, sizeof(msg_pool), RT_IPC_FLAG_FIFO);
// 创建发送和接收线程
rt_thread_t send_thread = rt_thread_create("send", message_send_thread, RT_NULL, 1024, 10, 10);
rt_thread_t recv_thread = rt_thread_create("recv", message_receive_thread, RT_NULL, 1024, 10, 10);
if (send_thread != RT_NULL) rt_thread_startup(send_thread);
if (recv_thread != RT_NULL) rt_thread_startup(recv_thread);
return 0;
}
7. 代码说明
- 消息队列初始化:通过
rt_mq_init
创建一个消息队列,其中rx_mq
是消息队列对象,msg_pool
是消息池,MSG_SIZE
是每条消息的大小,sizeof(msg_pool)
是整个消息池的大小。 - 发送消息:在
message_send_thread
线程中,我们创建一个消息,并通过rt_mq_send
发送到队列中。 - 接收消息:在
message_receive_thread
线程中,我们使用rt_mq_recv
从消息队列中接收消息,并打印接收到的消息。
8. 消息队列的配置参数
rt_mq_init
函数的参数可以进行配置,如下表所示:
参数 | 说明 |
---|---|
rx_mq | 消息队列对象 |
"rx_mq" | 消息队列名称 |
msg_pool | 存储消息的缓冲区 |
MSG_SIZE | 每条消息的大小 |
sizeof(msg_pool) | 消息池的总大小 |
RT_IPC_FLAG_FIFO | 消息队列的调度策略(先进先出) |
通过这种方式,RT-Thread 提供了一个高效、可靠的消息队列机制,使得不同线程或任务间的通信变得简单而高效。
ps:
在RT-Thread中,
rt_ringbuffer_get()
和消息队列(rt_mq_send()
和rt_mq_recv()
)都可以用于任务间的通信和数据传递,但它们的实现方式和适用场景有所不同。下面我将给出一个基于 循环缓冲区(ring buffer) 发送数据的示例,并解释它和消息队列的区别,以及如何选择使用哪一种机制。
三、基于rt_ringbuffer
循环缓冲区发送数据
首先,基于循环缓冲区的方式会使用 rt_ringbuffer_get()
来读取缓冲区中的数据。循环缓冲区适用于高效的生产者消费者模型,其中一个任务(生产者)往缓冲区写数据,而另一个任务(消费者)从缓冲区读取数据。
假设我们有一个接收数据并处理的任务,通过 rt_ringbuffer_get()
来读取数据,然后对数据进行处理。
1.示例代码
基于循环缓冲区的发送和接收
#include <rtthread.h>
#include <rtdevice.h>
#define BUFFER_SIZE 64
// 假设有一个环形缓冲区(环形缓冲区可以存储多个结构体)
static rt_ringbuffer_t wc_rb;
static rt_uint8_t buffer[BUFFER_SIZE];
// 数据结构
struct wc_rb_t {
rt_uint8_t buffer[64];
rt_uint8_t len;
rt_uint8_t cmd;
};
// 初始化环形缓冲区
void ringbuffer_init(void)
{
rt_ringbuffer_init(&wc_rb, buffer, sizeof(buffer));
}
// 发送数据到环形缓冲区
void send_to_ringbuffer(rt_uint8_t *data, rt_uint8_t len)
{
rt_ringbuffer_put(&wc_rb, data, len);
}
// 接收环形缓冲区的数据
void receive_from_ringbuffer(void)
{
struct wc_rb_t wc_rb_recv = {0};
if (rt_ringbuffer_get(&wc_rb, (rt_uint8_t *)&wc_rb_recv, sizeof(struct wc_rb_t)) == sizeof(struct wc_rb_t))
{
// 处理接收到的数据
rt_kprintf("Received data: %s\n", wc_rb_recv.buffer);
// 进一步处理...
}
}
void ringbuffer_test_thread_entry(void *parameter)
{
static int send_cnt = 0;
while (1)
{
// 模拟发送数据
if (send_cnt++ > 25) {
send_cnt = 0;
rt_uint8_t data[] = "Hello, ring buffer!";
send_to_ringbuffer(data, sizeof(data));
}
// 接收数据并处理
receive_from_ringbuffer();
// 延时
rt_thread_delay(10);
}
}
int main(void)
{
// 初始化环形缓冲区
ringbuffer_init();
// 创建线程
rt_thread_t tid = rt_thread_create("ringbuf", ringbuffer_test_thread_entry, NULL, 1024, 20, 10);
if (tid != RT_NULL)
{
rt_thread_startup(tid);
}
return 0;
}
2.代码说明
rt_ringbuffer_t
:这是RT-Thread中的环形缓冲区数据结构,用于存储数据。环形缓冲区允许数据的连续写入和读取,特别适用于生产者消费者模型。rt_ringbuffer_get()
:用于从环形缓冲区读取数据。如果缓冲区中有足够的数据,它会将数据拷贝到指定的内存区域并返回实际读取的数据大小。send_to_ringbuffer()
:将数据写入环形缓冲区。receive_from_ringbuffer()
:从环形缓冲区读取数据,并进行处理。
四、环形缓冲区 vs 消息队列
1.环形缓冲区的优点
- 高效性:环形缓冲区具有连续的内存空间,数据的读取和写入操作不需要复杂的内存管理,因此可以在需要高频次通信或实时性要求较高的场合使用。
- 无需锁:由于环形缓冲区是一个循环结构,读取和写入数据的操作相对简单,无需频繁的上下文切换。
- 适用于大批量数据:如果需要频繁传输大块数据而非简单的消息,环形缓冲区通常是一个较好的选择。
2.消息队列的优点
- 传递简单结构的数据:消息队列适合用来传递简短的消息或标志数据(比如一个小结构体或者状态标志),并且它提供了完整的同步机制,适用于任务之间需要传递简单消息的场合。
- 同步机制:消息队列内置了同步机制,消息的发送和接收都能保证任务间的协调,这对于某些需要严格同步的场合非常适用。
- 方便的任务间通信:消息队列能够跨任务传递数据,并且在没有足够空间的情况下可以阻塞发送任务,直到队列有空间为止。
3.选择环形缓冲区还是消息队列?
使用环形缓冲区的场景:
-
高频数据传输:例如流数据、传感器数据、串口数据接收等,数据量大且连续,环形缓冲区可以高效地处理。
-
生产者-消费者模型:例如,一个任务不断将数据写入缓冲区,另一个任务从缓冲区读取数据并处理。
-
对实时性要求高的场合:环形缓冲区通常有较低的延迟,适合需要快速处理的场合。
使用消息队列的场景:
-
传输简单的消息或状态:例如发送一个控制命令、任务之间传递小块数据、通知或标志等。
-
任务间有依赖关系:如果一个任务需要等待另一个任务的通知,消息队列可以很方便地实现同步。
-
较少的数据交互:如果传输的数据量较小,消息队列通常比环形缓冲区更简洁易用。
4.总结
- 环形缓冲区适用于需要频繁、高效传输大量数据的场景,特别是生产者消费者模式。
- 消息队列适用于任务间传递简单消息的场景,且能够提供良好的同步机制。
选择哪种方式,取决于你的应用场景是偏向高效的数据流处理,还是任务间的控制消息传递。
5.rt_ringbuffer_put()
和 rt_ringbuffer_put_force()
rt_ringbuffer_put()
和rt_ringbuffer_put_force()
都是用于将数据写入到 RT-Thread 的环形缓冲区中的函数,它们的使用场景和行为有所不同。理解它们的区别有助于你在不同的场景中选择合适的 API。
5.1rt_ringbuffer_put()
rt_ringbuffer_put()
是 RT-Thread 环形缓冲区中的常规数据写入函数。它用于将数据放入环形缓冲区,但它会在缓冲区已满时阻塞调用者,直到缓冲区有足够的空间来接收新的数据。这是一个 非强制性 的写入操作。
rt_err_t rt_ringbuffer_put(rt_ringbuffer_t *rb, const rt_uint8_t *data, rt_size_t size);
-
rb
:环形缓冲区指针。 -
data
:待写入的数据缓冲区。 -
size
:数据的大小。
行为:
-
如果环形缓冲区中有足够的空间,它会将数据成功写入。
-
如果环形缓冲区已经满了,它会阻塞,直到有足够的空间写入数据。
使用场景:
-
当你希望等待空余空间时,使用
rt_ringbuffer_put()
是合适的。例如,如果生产者任务写入数据时可以等待消费者任务的处理,直到缓冲区有空间为止。
示例:
rt_ringbuffer_put(&wc_rb, data, len); // 写入数据到环形缓冲区,可能会阻塞
5.2 rt_ringbuffer_put_force()
rt_ringbuffer_put_force()
是一个强制写入环形缓冲区的操作。它与 rt_ringbuffer_put()
的主要区别在于,即使环形缓冲区满了,它也会强制将数据写入并覆盖掉原有的数据,不会阻塞调用者。
语法:
rt_err_t rt_ringbuffer_put_force(rt_ringbuffer_t *rb, const rt_uint8_t *data, rt_size_t size);
-
rb
:环形缓冲区指针。 -
data
:待写入的数据缓冲区。 -
size
:数据的大小。
行为:
-
如果环形缓冲区没有足够的空间,
rt_ringbuffer_put_force()
会覆盖掉缓冲区中的数据,且不阻塞调用者。 -
如果写入成功,返回值是
size
,如果失败则返回错误码。
使用场景:
-
当你希望强制写入数据而不被阻塞时,比如环形缓冲区满了也不能停下来,需要丢弃旧数据以便写入新数据。
-
数据丢失容忍:在某些情况下,你可能愿意丢弃缓冲区中旧的数据来为新数据腾出空间,特别是在不关心丢失数据的场合。
示例
if (rt_ringbuffer_put_force(&wc_rb, (rt_uint8_t *)&wc_rb_recv, sizeof(struct wc_rb_t)) != sizeof(struct wc_rb_t)) { rt_kprintf("wc_rb_recv send error\n"); }
这段代码的作用是:如果强制写入数据到环形缓冲区失败,打印错误信息。这里使用了 rt_ringbuffer_put_force()
,意味着即使缓冲区已经满,它也会强制写入数据,并覆盖旧的数据。
5.3 区别总结
特性 | rt_ringbuffer_put() | rt_ringbuffer_put_force() |
---|---|---|
阻塞行为 | 阻塞,直到有足够空间 | 不阻塞,覆盖掉旧数据 |
环形缓冲区满时的处理 | 等待直到有空间 | 强制写入,丢弃旧数据 |
适用场景 | 当你希望数据生产者可以等待空余空间时 | 当你希望数据生产者不被阻塞,且允许丢弃旧数据时 |
返回值 | 成功返回 size ,失败返回错误码 | 成功返回 size ,失败返回错误码 |
选择哪个?
-
选择
rt_ringbuffer_put()
:如果你希望生产者在环形缓冲区已满时等待消费者处理数据,适用于不希望丢失数据的场合。这种方式适合一般的数据传输场景,尤其是在生产者和消费者的速度不一致时,允许生产者等待缓冲区有空闲空间。例如:传感器采集的数据,可能需要按顺序处理,丢失数据不可接受,生产者会等待消费者处理。
-
选择
rt_ringbuffer_put_force()
:如果你允许丢失旧的数据并且不希望阻塞生产者任务,适用于实时性要求较高的场景,生产者任务不能被阻塞,哪怕需要丢弃旧数据。常用于数据到达频率非常高,但又不一定需要保存所有数据的场合。例如:实时视频流或高频率传感器数据,可能只关心最新的数据,而不关心丢失某些旧数据。
总结
rt_ringbuffer_put()
是默认的缓冲区写入方式,适用于生产者任务可以等待消费者任务处理数据的场合。
rt_ringbuffer_put_force()
是强制写入的方式,适用于实时性要求较高,不希望阻塞且能容忍丢弃旧数据的场合。