单片机串口异步打印
文章目录
前言
🌊在单片机开发中串口的异步打印异步打印允许单片机在执行其他任务的同时进行打印操作,无需等待打印完成后再继续执行后续代码,避免了在多处调用的时候数据覆盖的问题。
设计思路
- 👍通过创建环形缓冲区队列,设计任务调度或缓冲区管理机制,在
FreeRTOS
中创建队列来进行数据的接收,新建一个打印任务优先级设置为最低优先级,用来检测FIFO
中是否有数据,如果有数据就将FIFO
中的数据打印出来。 - 通过定时器定时检测实现非阻塞数据打印。
准备
- 移植好
FreeRTOS
操作系统 - 串口输出重定向到
printf
队列创建
🚗队列的创建涉及到了数据结构,详解可以参考数据结构与算法
书籍。
c
/* 定义环形缓冲区结构体 */
typedef struct {
uint8_t buffer[BUFFER_SIZE]; /* 缓冲区 */
volatile uint32_t head; /* 写入指针 */
volatile uint32_t tail; /* 读取指针 */
SemaphoreHandle_t mutex; /* 互斥锁,保证线程安全 */
} ringbuffer_t;
这里新建了ringbuffer_t
里面包含了buffer数据包,它的大小由BUFFER_SIZE这个宏来决定,可以通过这个宏来修改预期的buffer大小。👌
😁实例化结构体,给结构体的成员进行赋值操作。
c
ringbuffer_t ringBuffer = {
.head = 0,
.tail = 0,
.mutex = NULL
};
🎈初始化环形缓冲区队列。通过创建RingBuffer_Init
函数来实现,传入的参数是ringbuffer_trb
的结构体指针。这时队列的head
和tail
为0表示这个队列是空的,接着创建了一个互斥量,用来保护线程安全。
c
/* 初始化环形缓冲区 */
void RingBuffer_Init(ringbuffer_t *rb) {
rb->head = 0;
rb->tail = 0;
rb->mutex = xSemaphoreCreateMutex();
if (rb->mutex == NULL) {
printf("Mutex creation failed.\n");
while (1);
}
}
🥣判断队列是否为空,传入环形缓冲区的结构体,如果队列的head 和队列的tail相等那代表这个队列是空的。
c
/* 判断环形缓冲区是否为空 */
int32_t RingBuffer_IsEmpty(ringbuffer_t *rb) {
/* head == tail 为空 */
return (rb->head == rb->tail);
}
📡判断队列是否是满,传入缓冲区结构体,如果满足head + 1 %对buffer 的大小求余数,如果余数等于tail那么就代表这个队列已经填满数据了。
c
/* 判断环形缓冲区是否已满 */
int32_t RingBuffer_IsFull(ringbuffer_t *rb) {
return ((rb->head + 1) % BUFFER_SIZE == rb->tail);
}
😍向队列写入数据,是按照字节进行写入,传入的参数的ringbuffer_t 结构体指针,和写入的数据data。
c
/* 向环形缓冲区写入数据 */
int32_t RingBuffer_Write(ringbuffer_t *rb, char data) {
int32_t result = pdFALSE;
if (xSemaphoreTake(rb->mutex, portMAX_DELAY) == pdTRUE) {
if (!RingBuffer_IsFull(rb)) {
rb->buffer[rb->head] = data;
rb->head = (rb->head + 1) % BUFFER_SIZE;
result = pdTRUE;
}
xSemaphoreGive(rb->mutex); /* 释放互斥锁 */
}
return result;
}
😘从环形缓冲区读取数据,思路是传入ringbuffer_t 的结构体指针,和需要读取的指针类型字符,在读取函数中先判断是否获取到了互斥锁,如果是就判断队列是否非空,如果队列非空,就将buffer 数据指针就指向尾部每读取队列的一个数就指针向前偏移一位,直到读取完成,满足if语句每次读取完成之后返回true
,读取而结束就释放互斥锁🔒。
c
/* 从环形缓冲区读取数据 */
int32_t RingBuffer_Read(ringbuffer_t *rb, uint8_t *data) {
int32_t result = pdFALSE;
if (xSemaphoreTake(rb->mutex, portMAX_DELAY) == pdTRUE) {
if (!RingBuffer_IsEmpty(rb)) {
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % BUFFER_SIZE;
result = pdTRUE;
}
xSemaphoreGive(rb->mutex); /* 释放互斥锁 */
}
return result;
}

😊在填充数据的时候参考了C语言中标准格式化输出,需要进行标准的格式化输出需要先引入其头文件#include"stdarg.h"
之后就可以进行标准的格式化输出了。在函数内部,其中可以使用 va_start() 和 va_end() 宏来访问变长参数列表中的值,创建临时的缓冲区来存放格式化的数据,然后再通过vsnprintf 函数将可变的数据存到locabuffer中,返回写入的长度。
c
/* 格式化并写入数据的函数 */
void RingBufferWriteFormatted(ringbuffer_t* rb, const uint8_t* format, ...) {
va_list args;
va_start(args, format);
/* 使用 vsnprintf 来格式化字符串到局部缓冲区 */
uint8_t localBuffer[BUFFER_SIZE]; // 根据需要调整大小
uint32_t written = vsnprintf(localBuffer, sizeof(localBuffer), format, args);
/* 检查格式化是否成功 */
if (written > 0) {
/* 将格式化后的字符串逐字符写入环形缓冲区 */
for (int i = 0; i < written; ++i) {
if (!RingBuffer_Write(rb, localBuffer[i])) {
break;
}
}
}
va_end(args);
}
😄创建打印任务不断的取读取环形缓冲区的数据如果有数据就打印出来。
c
/* 数据读取和打印任务 */
void PrintTask(void *pvParameters) {
char data;
while (1) {
/* 读取缓冲区,有数据就打印 */
if (RingBuffer_Read(&ringBuffer, &data)) {
printf("%c",data);
}
}
}
完整代码
c
/* FreeRTOS kernel includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "stdarg.h"
#define BUFFER_SIZE 1024 /* 环形缓冲区大小 根据实际的数据大小进行调整 */
/* 定义环形缓冲区结构体 */
typedef struct {
uint8_t buffer[BUFFER_SIZE]; /* 缓冲区 */
volatile uint32_t head; /* 写入指针 */
volatile uint32_t tail; /* 读取指针 */
SemaphoreHandle_t mutex; /* 互斥锁,保证线程安全 */
} ringbuffer_t;
/* 创建一个全局环形缓冲区实例 */
ringbuffer_t ringBuffer = {
.head = 0,
.tail = 0,
.mutex = NULL
};
/* 初始化环形缓冲区 */
void RingBuffer_Init(ringbuffer_t *rb) {
rb->head = 0;
rb->tail = 0;
rb->mutex = xSemaphoreCreateMutex();
if (rb->mutex == NULL) {
printf("Mutex creation failed.\n");
while (1);
}
}
/* 判断环形缓冲区是否为空 */
int32_t RingBuffer_IsEmpty(ringbuffer_t *rb) {
/* head == tail 为空 */
return (rb->head == rb->tail);
}
/* 判断环形缓冲区是否已满 */
int32_t RingBuffer_IsFull(ringbuffer_t *rb) {
return ((rb->head + 1) % BUFFER_SIZE == rb->tail);
}
/* 向环形缓冲区写入数据 */
int32_t RingBuffer_Write(ringbuffer_t *rb, char data) {
int32_t result = pdFALSE;
if (xSemaphoreTake(rb->mutex, portMAX_DELAY) == pdTRUE) {
if (!RingBuffer_IsFull(rb)) {
rb->buffer[rb->head] = data;
rb->head = (rb->head + 1) % BUFFER_SIZE;
result = pdTRUE;
}
xSemaphoreGive(rb->mutex); /* 释放互斥锁 */
}
return result;
}
/* 从环形缓冲区读取数据 */
int32_t RingBuffer_Read(ringbuffer_t *rb, char *data) {
int32_t result = pdFALSE;
if (xSemaphoreTake(rb->mutex, portMAX_DELAY) == pdTRUE) {
if (!RingBuffer_IsEmpty(rb)) {
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % BUFFER_SIZE;
result = pdTRUE;
}
xSemaphoreGive(rb->mutex); /* 释放互斥锁 */
}
return result;
}
/* 格式化并写入数据的函数 */
void RingBufferWriteFormatted(ringbuffer_t* rb, const char* format, ...) {
va_list args;
va_start(args, format);
/* 使用 vsnprintf 来格式化字符串到局部缓冲区 */
char localBuffer[BUFFER_SIZE]; // 根据需要调整大小
int written = vsnprintf(localBuffer, sizeof(localBuffer), format, args);
// 检查格式化是否成功
if (written > 0) {
// 将格式化后的字符串逐字符写入环形缓冲区
for (int i = 0; i < written; ++i) {
if (!RingBuffer_Write(rb, localBuffer[i])) {
break;
}
}
}
va_end(args);
}
/* 数据填充任务 测试任务向队列填充数据依次进行添加 */
void DataFillTask(void *p) {
RingBufferWriteFormatted(&ringBuffer," 1 test test test %d\r\n",0x88);
RingBufferWriteFormatted(&ringBuffer, "2 test size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "3 xx size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "4 00 size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "5 11 size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "6 22 size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "7 33 size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "8 44 size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "9 55 size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "10 66 size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "11 ww size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "12 xiao size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer, "13 bai size value %d\r\n", 0x88);
RingBufferWriteFormatted(&ringBuffer,"*************DataFillTask******************\n");
while (1) {
vTaskDelay(200);
}
}
/* 数据读取和打印任务 */
void PrintTask(void *pvParameters) {
char data;
while (1) {
/* 读取缓冲区,有数据就打印 */
if (RingBuffer_Read(&ringBuffer, &data)) {
printf("%c",data);
// printf("Data '%c' read from buffer.\n", data); /* 从缓冲区去取数据并打印 */
}
}
}
/* 创建任务 */
void app_CreateTasks(void) {
if(pdPASS != xTaskCreate(DataFillTask, "DataFillTask", 512, NULL, 1, NULL)) {
printf("Task DataFillTask creation failed!\r\n");
}
/* 设置打印任务为最低优先级 */
if (pdPASS != xTaskCreate(PrintTask, "PrintTask", 256, NULL, 0, NULL)) {
printf("Task PrintTask creation failed!\r\n");
}
}
/* 函数入口 */
int main(void) {
/* 在这里进行硬件的初始化操作 */
printf("rtos_log print\r\n");
/* 初始化环形缓冲区*/
RingBuffer_Init(&ringBuffer);
/* 创建任务 */
app_CreateTasks();
/* 启动调度器*/
vTaskStartScheduler();
/* 如果调度器启动失败 */
while (1) {
};
return 0;
}
总结
本文主要介绍了在单片机中实现串口的异步打印,避免了数据覆盖的问题。