一、初始化串口

上一个章节初步介绍了GPIO口的使用,这一章你可以看成是GPIO的进阶使用,毕竟本质上串口通信也是GPIO口的快速的电平变换。
首先,进行第一步,先开启时钟,然后初始化GPIO。

	rcu_periph_clock_enable(RCU_GPIOA);
	rcu_periph_clock_enable(RCU_USART0);
    /* 配置GPIO */
    gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_9);  /* TX */
    gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_10); /* RX */
    
    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_9);
    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_10);
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_10);

第二步,初始化串口。初始化串口是为了规定串口通信协议层面的各项参数,方便后续统一规范。

    /* 配置USART */
    usart_deinit(USART0);
    usart_baudrate_set(USART0, g_uart_config.baudrate);
    usart_receive_config(USART0, USART_RECEIVE_ENABLE);
    usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
    
    /* 使能中断 */
    nvic_irq_enable(USART0_IRQn, 0, 0);
    usart_interrupt_enable(USART0, USART_INT_RBNE);
    
    /* 使能USART */
    usart_enable(USART0);

这里在初始化串口之前先反初始化串口,避免之前配置过的串口参数影响。这里把串口中断打开,配置优先级为最高。
完成之后,继续开始配置DMA,对于Cortex-M4内核的MCU来说,各种外设的套路基本差不多。

    dma_single_data_parameter_struct dma_init_struct;
    
    /* 使能DMA时钟 */
    rcu_periph_clock_enable(RCU_DMA1);
    
    /* 复位DMA配置 */
    dma_deinit(UART_DMA_PERIPH, UART_DMA_TX_CH);
    dma_single_data_para_struct_init(&dma_init_struct);
    
    /* 配置DMA参数 */
    dma_init_struct.periph_addr = (uint32_t)&USART_DATA(USART0);
    dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
    dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
    dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;
    dma_init_struct.circular_mode = DMA_CIRCULAR_MODE_DISABLE;
    dma_init_struct.direction = DMA_MEMORY_TO_PERIPH;
    dma_init_struct.number = 0;
    dma_init_struct.priority = DMA_PRIORITY_HIGH;
    
    /* 初始化DMA */
    dma_single_data_mode_init(UART_DMA_PERIPH, UART_DMA_TX_CH, &dma_init_struct);
    dma_channel_subperipheral_select(UART_DMA_PERIPH, UART_DMA_TX_CH, UART_DMA_TX_SUBPERI);
    
    /* 配置DMA中断 */
    nvic_irq_enable(DMA1_Channel7_IRQn, 1, 0);
    dma_interrupt_enable(UART_DMA_PERIPH, UART_DMA_TX_CH, DMA_CHXCTL_FTFIE);

DMA原理这一块笔者就不再赘述了,现在各种各样的AI工具十分发达,丢给AI询问一下即可了解其原理。
完成DMA配置之后,可以开始着手编写串口DMA传输方面的代码。
首先,笔者喜欢加入环形buffer来充当数据缓冲区。代码如下,主要就是一个ringbuffer的方式,各位嫌麻烦也可以直接用AI完成这些基础功能。切记不能将时间浪费在无用的东西上。

/* 环形缓冲区结构体 */
typedef struct {
    uint8_t *buffer;           /* 缓冲区指针 */
    uint32_t size;             /* 缓冲区大小 */
    uint32_t read_index;       /* 读指针 */
    uint32_t write_index;      /* 写指针 */
    uint32_t data_count;       /* 当前数据量 */
} ring_buffer_t;
/* 初始化环形缓冲区 */
void ring_buffer_init(ring_buffer_t *rb, uint8_t *buffer, uint32_t size)
{
   rb->buffer = buffer;
   rb->size = size;
   ring_buffer_reset(rb);
}

/* 重置环形缓冲区 */
void ring_buffer_reset(ring_buffer_t *rb)
{
   rb->read_index = 0;
   rb->write_index = 0;
   rb->data_count = 0;
}

/* 写入数据到环形缓冲区 */
uint32_t ring_buffer_write(ring_buffer_t *rb, uint8_t *data, uint32_t len)
{
   uint32_t free_size = ring_buffer_get_free_size(rb);
   uint32_t write_len = (len > free_size) ? free_size : len;
   uint32_t i;
   
   for (i = 0; i < write_len; i++)
   {
       rb->buffer[rb->write_index] = data[i];
       rb->write_index = (rb->write_index + 1) % rb->size;
       rb->data_count++;
   }
   
   return write_len;
}

uint32_t ring_buffer_put(ring_buffer_t *rb, uint8_t data)
{
   uint32_t free_size = ring_buffer_get_free_size(rb);
   if (free_size == 0) {
       return 0;  // 缓冲区已满
   }

   rb->buffer[rb->write_index] = data;
   rb->write_index = (rb->write_index + 1) % rb->size;
   rb->data_count++;
   
   return 1;  // 成功写入1字节
}

/* 从环形缓冲区读取数据 */
uint32_t ring_buffer_read(ring_buffer_t *rb, uint8_t *data, uint32_t len)
{
   uint32_t available = ring_buffer_get_data_size(rb);
   uint32_t read_len = (len > available) ? available : len;
   uint32_t i;
   
   for (i = 0; i < read_len; i++)
   {
       data[i] = rb->buffer[rb->read_index];
       rb->read_index = (rb->read_index + 1) % rb->size;
       rb->data_count--;
   }
   
   return read_len;
}

/* 获取环形缓冲区空闲空间大小 */
uint32_t ring_buffer_get_free_size(ring_buffer_t *rb)
{
   return rb->size - rb->data_count;
}

/* 获取环形缓冲区已用空间大小 */
uint32_t ring_buffer_get_data_size(ring_buffer_t *rb)
{
   return rb->data_count;
}

完成ringbuffer代码编写后,着手编写串口DMA通信方面。
首先,我们要编写启动DMA传输函数。主要原理就是通过判断环形缓冲区是否有需要发送的数据,如果有数据,则开启DMA,届时DMA会自动将数据发出去。

/* 启动DMA传输 */
static void uart_start_dma_tx(void)
{
   uint32_t tx_len;
   uint8_t *tx_buf;
   
   /* 检查是否有数据需要发送 */
   tx_len = ring_buffer_get_data_size(&g_uart_config.tx_rb);
   if(tx_len > 0 && g_dma_tx_state.complete)
   {
       if(tx_len > sizeof(g_dma_tx_state.buffer[0])) {
           tx_len = sizeof(g_dma_tx_state.buffer[0]);
       }
       
       g_dma_tx_state.complete = 0;
       tx_buf = g_dma_tx_state.buffer[g_dma_tx_state.current_buf];
       
       /* 从环形缓冲区读取数据 */
       ring_buffer_read(&g_uart_config.tx_rb, tx_buf, tx_len);
       
       /* 配置并启动DMA传输 */
       dma_channel_disable(UART_DMA_PERIPH, UART_DMA_TX_CH);
       while(DMA_CHCTL(UART_DMA_PERIPH, UART_DMA_TX_CH) & DMA_CHXCTL_CHEN);
       
       dma_memory_address_config(UART_DMA_PERIPH, UART_DMA_TX_CH, 0, (uint32_t)tx_buf);
       dma_transfer_number_config(UART_DMA_PERIPH, UART_DMA_TX_CH, tx_len);
       
       usart_dma_transmit_config(USART0, USART_TRANSMIT_DMA_ENABLE);
       dma_channel_enable(UART_DMA_PERIPH, UART_DMA_TX_CH);
   }
}

那么,问题来了,什么时候调用这个函数呢?继续编写一个发送信息函数,作用是为了将需要发送的数据填入ringbuffer,之后启动DMA传输。

/* 发送数据 */
uint32_t uart_send(uint8_t *data, uint32_t len)
{
   uint32_t sent_len;
   
   /* 将数据写入环形缓冲区 */
   sent_len = ring_buffer_write(&g_uart_config.tx_rb, data, len);
   
   /* 启动DMA传输 */
   uart_start_dma_tx();
   
   return sent_len;
}

接受数据方面直接使用中断接收。

/* 串口接收环形缓冲区 */
/**
 * @brief       串口0中断服务函数
 * @param       无
 * @retval      无
 */
void USART0_IRQHandler(void)
{
    if(RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE))
    {
        uint8_t data = (uint8_t)usart_data_receive(USART0);
        ring_buffer_write(&g_uart_config.rx_rb, &data, 1);
    }
}

/* USART1接收中断处理函数 */
void USART1_IRQHandler(void)
{
    uint8_t res;
    if(RESET != usart_interrupt_flag_get(USART1, USART_INT_FLAG_RBNE))
    {
        res = (uint8_t)usart_data_receive(USART1);
        ring_buffer_write(&g_uart1_config.rx_rb, &res, 1);
    }
}

可以继续抽象一层读取数据的函数,让代码看起来更简洁明了。

/* USART1接收数据函数 */
uint32_t uart1_receive(uint8_t *data, uint32_t len)
{
    return ring_buffer_read(&g_uart1_config.rx_rb, data, len);
}
/* 获取USART1接收缓冲区中的数据量 */
uint32_t uart1_get_rx_size(void)
{
    return ring_buffer_get_data_size(&g_uart1_config.rx_rb);
}

/* 清空USART1接收缓冲区 */
void uart1_flush_rx(void)
{
    ring_buffer_reset(&g_uart1_config.rx_rb);
}

接下来编写DMA的中断服务函数,主要内容就是当DMA传输完成的时候做的一系列处理动作。

/* USART1 DMA发送完成中断处理函数 */
void DMA0_Channel6_IRQHandler(void)
{
    if(dma_interrupt_flag_get(UART1_DMA_PERIPH, UART1_DMA_TX_CH, DMA_INT_FLAG_FTF))
    {
        dma_interrupt_flag_clear(UART1_DMA_PERIPH, UART1_DMA_TX_CH, DMA_INT_FLAG_FTF);
        
        /* 切换到另一个缓冲区 */
        g_dma1_tx_state.current_buf = !g_dma1_tx_state.current_buf;
        g_dma1_tx_state.complete = 1;
        
        /* 检查是否还有数据需要发送 */
        uart1_start_dma_tx();
    }
}

/* USART1 DMA接收完成中断处理函数 */
void DMA0_Channel5_IRQHandler(void)
{
    if(dma_interrupt_flag_get(UART1_DMA_PERIPH, UART1_DMA_RX_CH, DMA_INT_FLAG_FTF))
    {
        dma_interrupt_flag_clear(UART1_DMA_PERIPH, UART1_DMA_RX_CH, DMA_INT_FLAG_FTF);
        ring_buffer_write(&g_uart1_config.rx_rb, g_dma1_tx_state.buffer[g_dma1_tx_state.current_buf], sizeof(g_dma1_tx_state.buffer[0]));
    }
}

写了这么多函数,初始化参数变量怎么弄?可以参考笔者的。

/* DMA配置参数 */
#define UART_DMA_PERIPH     DMA1
#define UART_DMA_TX_CH      DMA_CH7
#define UART_DMA_TX_SUBPERI DMA_SUBPERI4

/* USART1 DMA配置参数 */
#define UART1_DMA_PERIPH     DMA0
#define UART1_DMA_TX_CH      DMA_CH6
#define UART1_DMA_RX_CH      DMA_CH5
#define UART1_DMA_TX_SUBPERI DMA_SUBPERI4
#define UART1_DMA_RX_SUBPERI DMA_SUBPERI4

/* DMA传输状态 */
typedef struct {
    volatile uint8_t complete;      /* 传输完成标志 */
    volatile uint8_t current_buf;   /* 当前使用的缓冲区 */
    uint8_t buffer[2][256];        /* 双缓冲区 */
} dma_tx_state_t;

uint8_t uart_rx_buffer[UART_RX_BUF_SIZE];
uint8_t uart_tx_buffer[UART_TX_BUF_SIZE];

uint8_t uart1_rx_buffer[UART_RX_BUF_SIZE];
uint8_t uart1_tx_buffer[UART_TX_BUF_SIZE];

static uart_config_t g_uart1_config = 
{
    .baudrate = baud_1,
    .rx_buffer = uart1_rx_buffer,
    .rx_size = UART_RX_BUF_SIZE,
    .tx_buffer = uart1_tx_buffer,
    .tx_size = UART_TX_BUF_SIZE
};

static dma_tx_state_t g_dma1_tx_state = {1, 0, {{0}}};


static uart_config_t g_uart_config = 
{
    .baudrate = baud_0,
    .rx_buffer = uart_rx_buffer,
    .rx_size = UART_RX_BUF_SIZE,
    .tx_buffer = uart_tx_buffer,
    .tx_size = UART_TX_BUF_SIZE
};

static dma_tx_state_t g_dma_tx_state = {1, 0, {{0}}};

最后最后,不要忘了修改一下printf重定向。

int fputc(int ch, FILE *f)
{
    // 将字符放入发送环形缓冲区
    ring_buffer_put(&g_uart_config.tx_rb, (uint8_t)ch);
    
    // 启动 DMA 传输(内部有非忙检查)
    uart_start_dma_tx();
    
    return ch;
}

想要测试的话,直接printf输出想要输出的内容就好咯。
这里笔者再补充一点小bug,当输出中文内容时,串口助手往往容易乱码,在串口助手里面选择utf8字体,同时代码文件也选择utf8,如果还是不行,用记事本打开代码文件,修改格式为同样的即可。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐