GD32F4学习之路第二章----串口以及DMA
这里笔者再补充一点小bug,当输出中文内容时,串口助手往往容易乱码,在串口助手里面选择utf8字体,同时代码文件也选择utf8,如果还是不行,用记事本打开代码文件,修改格式为同样的即可。主要原理就是通过判断环形缓冲区是否有需要发送的数据,如果有数据,则开启DMA,届时DMA会自动将数据发出去。上一个章节初步介绍了GPIO口的使用,这一章你可以看成是GPIO的进阶使用,毕竟本质上串口通信也是GPIO
一、初始化串口
上一个章节初步介绍了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,如果还是不行,用记事本打开代码文件,修改格式为同样的即可。
更多推荐



所有评论(0)