三、如何在zephyr上使用串口进行输出与输入(含dma与中断方式)

前提条件:使用 STM32H723ZG 开发板或同为 H7 系列的开发板。

1. 中断方式

使用中断方式进行收发测试,效果如下:

左边是调试打印输出口,右边是串口2,接收发送中断对数据的接收与发送回环测试。

源代码如下:

c 复制代码
#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys_clock.h>
#include <zephyr/drivers/uart.h>
#include <stm32h7xx_hal.h> 
#include <string.h>
#define MY_UART DT_NODELABEL(usart2) 

const struct device *const uart_dev = DEVICE_DT_GET(MY_UART);
#define TX_RING_BUF_SIZE 256

/* 2. 定义发送环形缓冲区 (Ring Buffer) */
static uint8_t __aligned(4) tx_ring_buffer[TX_RING_BUF_SIZE];
static struct ring_buf tx_ringbuf;
/* --- 接收缓冲区相关 --- */
#define RX_BUF_SIZE 64       // 单次从FIFO读出的数据块大小,可根据需要调整
static uint8_t rx_buf[RX_BUF_SIZE]; // 用于临时存储从FIFO读出的数据
K_MSGQ_DEFINE(uart_msgq, sizeof(uint8_t), 512, 4); // 消息队列,用于将每个字节传递给主线程

/* 4. UART 中断服务函数 (在中断上下文中执行) */
static void uart_irq_callback(const struct device *dev, void *user_data)
{

   // 更新中断状态,这一步是必须的
    uart_irq_update(dev);
   if (dev == DEVICE_DT_GET(DT_NODELABEL(usart2))) {
    /********** 接收 (RX) 处理 **********/
    // 检查接收FIFO中是否有数据
  if (uart_irq_rx_ready(dev)) {
        uint8_t rx_buf[RX_BUF_SIZE];
        int bytes_read;
        // 循环读取FIFO中的所有数据
        while ((bytes_read = uart_fifo_read(dev, rx_buf, sizeof(rx_buf))) > 0) {
            // 将接收到的每个字节放入消息队列,供主线程处理
            for (int i = 0; i < bytes_read; i++) {
                if (k_msgq_put(&uart_msgq, &rx_buf[i], K_NO_WAIT) != 0) {
                    // 队列满,可以在这里添加错误处理
                    printk("UART RX message queue full!\n");
                }
            }
        }
    }

    /* 检查发送缓冲区是否为空,并且硬件已准备好发送下一个数据 */
    if (uart_irq_tx_ready(dev)) {
        uint8_t tx_char;
        /* 从环形缓冲区中取出一个字节的数据 */
  uint32_t ret = ring_buf_get(&tx_ringbuf, &tx_char, 1);
        if (ret == 1) {
            /* 有数据,通过中断驱动的API发送出去 */
        uart_fifo_fill(dev, &tx_char, 1);
        } else {
            /* 无数据可发,关闭发送中断以节省CPU资源 */
     uart_irq_tx_disable(dev);
        }
    }
   }
}
/* 5. 外部函数:将数据放入发送环形缓冲区 */
int uart_send_data(const uint8_t *data, size_t len)
{
    if (!device_is_ready(uart_dev)) {
        return -ENODEV;
    }

    /* 将数据放入环形缓冲区 */
    uint32_t ret = ring_buf_put(&tx_ringbuf, data, len);
    if (ret < len) {
        /* 缓冲区空间不足 */
        return -ENOMEM;
    }

    /* 重要:启用发送中断,触发 uart_irq_callback 函数 */
    uart_irq_tx_enable(uart_dev);
    return 0;
}

/* 6. UART 初始化 */
void uart_interrupt_init(void)
{
    if (!device_is_ready(uart_dev)) {
        printk("UART device '%s' not ready.\n", uart_dev->name);
        //return;
    }

    /* 初始化环形缓冲区 */
    ring_buf_init(&tx_ringbuf, sizeof(tx_ring_buffer), tx_ring_buffer);

    /* 注册中断回调函数 */
  uart_irq_callback_user_data_set(uart_dev, uart_irq_callback, NULL);

    /* 启用接收中断 (可选) */
   uart_irq_rx_enable(uart_dev);
    /* 注意: 发送中断 (TX) 在初始化时先不开启,在有数据发送请求时再开启 */
}
int main(void)
{
	int ret;
    uart_interrupt_init();
    printk("Interrupt-driven UART TX demo started.\n");
    /* 示例:定时发送数据 */
    const char *test_msg = "Hello from UART Interrupt Mode!\n";
  // 接收消息队列的处理线程
    uint8_t received_char;
   
 	while (1) {
		
  // 非阻塞地处理消息队列中的字符
        if (k_msgq_get(&uart_msgq, &received_char, K_MSEC(100)) == 0) 
		{
            // 将接收到的字符回显出去,并打印到控制台
           uart_send_data(&received_char, 1);
            printk("%c", received_char);

        }
		 // 每隔2秒发送一次测试消息
   /*   static uint32_t last_send_time = 0;
        uint32_t now = k_uptime_get_32();
        if (now - last_send_time >= 2000) 
		{
			uart_send_data((const uint8_t *)test_msg, strlen(test_msg));
            last_send_time = now;
        } */
 	}
	return 0;
}

2、使用dma方式:

效果如下

源代码:

c 复制代码
/*
 * Copyright (c) 2016 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys_clock.h>
#include <zephyr/drivers/uart.h>
#include <stm32h7xx_hal.h> 
#include <string.h>
#include <stdbool.h>
#include <zephyr/drivers/dma.h>
#include <zephyr/cache.h>

/* 1000 msec = 1 sec */
#define SLEEP_TIME_MS   1000

/* The devicetree node identifier for the "led0" alias. */
#define LED0_NODE DT_ALIAS(led0)
#define MY_GPIO_NODE DT_ALIAS(mygpiog7)
#define UART2_NODE  DT_NODELABEL(usart2) 
#define SW0_NODE    DT_ALIAS(mysingpiod9)


// 辅助函数:获取当前 VOS 等级
static uint32_t get_vos_scale(void) {
    // 读取 PWR 控制寄存器 3 的 VOS 位
    return (PWR->D3CR & PWR_D3CR_VOS) >> PWR_D3CR_VOS_Pos;
}
/* 定义接收和发送缓冲区,必须使用 __nocache 解决 D-Cache 一致性问题 */
#define RX_BUF_SIZE 256
static uint8_t __nocache rx_buf0[RX_BUF_SIZE];
static uint8_t __nocache rx_buf1[RX_BUF_SIZE];
static uint8_t __nocache *current_rx_buf = rx_buf0;

/* 发送缓冲区,用于回环发送 */
static uint8_t __nocache tx_buf[RX_BUF_SIZE];

static const struct device *uart2_dev;
static bool tx_pending = false; // 标记是否正在发送
/* UART 异步事件回调 */
static void uart_callback(const struct device *dev, struct uart_event *evt, void *user_data)
{
    switch (evt->type) {
    case UART_RX_RDY:
        {
            /* 接收到的数据位置和长度 */
            uint8_t *data = evt->data.rx.buf + evt->data.rx.offset;
            size_t len = evt->data.rx.len;

            /* 拷贝数据到发送缓冲区 */
            memcpy(tx_buf, data, len);
  if (!tx_pending) {
       // 用 K_NO_WAIT 或 极短超时,不等待,立刻返回
            if (uart_tx(dev, tx_buf, len, 0) == 0) {
                tx_pending = true; // 标记发送开始
            } else {
                printk("TX start failed\n");
            }
        } else {
            // 如果上一次发送还没完,可以选择丢弃新数据或覆盖缓冲区
            // 注意:这里简单丢弃了
            printk("TX busy, data dropped\n");
        }
		}
        break;
 case UART_TX_DONE:
        // DMA 发送真正完成,清除"忙"标志
        tx_pending = false;
        break;
    case UART_RX_BUF_REQUEST:
        /* 提供新的接收缓冲区,实现链式接收 */
        if (current_rx_buf == rx_buf0) {
            uart_rx_buf_rsp(dev, rx_buf1, RX_BUF_SIZE);
            current_rx_buf = rx_buf1;
        } else {
            uart_rx_buf_rsp(dev, rx_buf0, RX_BUF_SIZE);
            current_rx_buf = rx_buf0;
        }
        break;

    case UART_RX_STOPPED:
        printk("UART RX stopped, reason: %d\n", evt->data.rx_stop.reason);
        break;

    default:
        break;
    }
}
int main(void)
{
    int ret;
  // 读取并打印当前 VOS 等级
    uint32_t current_vos = get_vos_scale();
    printk("Current VOS scale: %lu\n", current_vos);
    if (current_vos == 0) {
        printk("VOS0 is active, system is running at high performance.\n");
    } else {
        printk("VOS0 is NOT active. Check PM configuration.\n");
    }
     printk("HW cycles per sec: %u\n", sys_clock_hw_cycles_per_sec());

    /* 1. 获取UART2设备句柄 */
    uart2_dev = DEVICE_DT_GET(UART2_NODE);
    if (!device_is_ready(uart2_dev)) {
        printk("UART2 device not ready!\n");
        return 0;
    }

    /* 2. 注册异步回调函数 */
    ret = uart_callback_set(uart2_dev, uart_callback, NULL);
    if (ret < 0) {
        printk("Failed to set callback: %d\n", ret);
        return 0;
    }

 /* 启动接收,使用第一个缓冲区 */
    ret = uart_rx_enable(uart2_dev, rx_buf0, RX_BUF_SIZE, 50);
    if (ret < 0) {
        printk("RX enable failed: %d\n", ret);
        return 0;
    }
   printk("UART2 echo (DMA RX+TX) ready. Send data to USART2, 'ok' will appear on USART1.\n");

    while (1) {
        k_sleep(K_FOREVER);
    }
	return 0;
}

关于app.overlay

c 复制代码
&usart2 {
  status = "okay";
    current-speed = <115200>;
    pinctrl-0 = <&usart2_tx_pd5 &usart2_rx_pd6>;
    pinctrl-names = "default";
    // DMA配置部分
    dmas = <&dmamux1 2 44 0x440>,  // TX通道的DMAMUX配置
           <&dmamux1 3 43 0x480>;  // RX通道的DMAMUX配置
    dma-names = "tx", "rx";
};
&dma1 {
    status = "okay";
};
&dmamux1 {
    status = "okay";
};

prj.conf

c 复制代码
# 启用 UART 和 Console
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y

# 启用 Shell
CONFIG_SHELL=y
# [关键] 解决可能的 cache 一致性问题的关键选项
CONFIG_UART_ASYNC_API=y
CONFIG_DMA=y                  # 显式启用 DMA 支持
CONFIG_DMA_STM32=y
CONFIG_NOCACHE_MEMORY=y       # 保持 DMA 缓冲区一致性

# 启用接收中断
CONFIG_UART_INTERRUPT_DRIVEN=y