USART
串口通信作为一种异步串行通信方式,被我们广泛地应用在设备调试及数据交互上。关于串口,我们已经很熟悉了。下文就根据笔者学习的GD32F3来说一下单片机中串口配置及其注意事项。
上文我们是进行串口重定向后使用printf进行数据的发送,关于GD32串口,当收发数据量大的时候,我们一般使用DMA+空闲中断来进行数据搬运。
GD32的串口配置,DMA配置,中断配置如下。需要注意的是,使用DMA没有直接使用串口重定向那么方便,因为串口重定向后的printf和scanf已经帮我们完成很多工作了。而使用DMA传输的是字节的形式,多个字节可以组成字符串,即收发可以是字符串,这和我们需要的数据可能不符,要求我们进行对应的编码和解码。
根据上文对串口的简单配置,我们在其基础上配置DMA和中断即可,下面一步步进行配置
1、首先进行GPIO的配置,这里使用到串口2,对应是PB10,PB11引脚。
#include "gd32f30x.h"
#include <stdio.h>
#include <string.h>
void USART_GPIO_Config(void)
{
/* 初始化GPIO外设 */
rcu_periph_clock_enable(RCU_GPIOB);
/* TX管脚,PA10,复用推挽输出,速度50MHz */
gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
/* RX管脚,PA11,下拉输入,速度50MHz */
gpio_init(GPIOB, GPIO_MODE_IPD, GPIO_OSPEED_50MHZ, GPIO_PIN_11);
}
2、进行串口的配置,需要注意的是,在配置串口的基础上,我们需要开启串口的DMA发送和接收使能,即可完成串口配置
void USART_Config(void)
{
/* 初始化USART外设 */
rcu_periph_clock_enable(RCU_USART2); // 使能串口2时钟
usart_baudrate_set(USART2, 115200); // 波特率115200
usart_parity_config(USART2, USART_PM_NONE); // 无校检
usart_word_length_set(USART2, USART_WL_8BIT); // 8位数据位
usart_stop_bit_set(USART2, USART_STB_1BIT); // 1位停止位
usart_transmit_config(USART2, USART_TRANSMIT_ENABLE); // 使能串口发送
usart_receive_config(USART2, USART_RECEIVE_ENABLE); // 使能串口接收
/* config USARTx_TXRX transmit by DMA */
usart_dma_transmit_config(USART2,USART_TRANSMIT_DMA_ENABLE ); //配置DMA发送使能
usart_dma_receive_config(USART2,USART_RECEIVE_DMA_ENABLE); //配置DMA接收使能
usart_enable(USART2);//使能串口
}
3、接下来配置DMA,也是简单的进行配置。其中我们要指定一个发送缓冲区和接收缓冲区,需要注意的是,如果数据传输速率很快,最好设置两个接收缓冲区。即用乒乓操作的方式,防止旧数据被覆盖。以下是详细的配置过程,注释很详细。
char USART2RX_Buffer[20]={0};//指定接收缓冲区的地址,存储接收的数据
char USART2TX_Buffer[2];//用不上,初始化中我们还是要指定一个发送缓冲区地址
char buff_string[20] = {0};//存储复制的数据
uint8_t rxFlg=0;//默认串口未接收到数据
void DMA_Init(void)
{
rcu_periph_clock_enable(RCU_DMA0);
dma_parameter_struct dma_init_Usart2_TX = {0};
dma_parameter_struct dma_init_Usart2_RX = {0};
/* deinitialize DMA channel */
dma_deinit(DMA0, DMA_CH1);//USART2_TX
dma_deinit(DMA0, DMA_CH2);//USART2_RX
/* initialize DMA0 channel2(Usart2_TX) */
dma_init_Usart2_TX.direction = DMA_MEMORY_TO_PERIPHERAL; //DAM到基地址
dma_init_Usart2_TX.memory_addr = (uint32_t)USART2TX_Buffer; //基地址
dma_init_Usart2_TX.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //地址自增
dma_init_Usart2_TX.memory_width = DMA_MEMORY_WIDTH_8BIT; //8bit
dma_init_Usart2_TX.number = (uint32_t)0; //将基地址中0个位发出
dma_init_Usart2_TX.periph_addr = (uint32_t)(&USART_DATA(USART2)); //串口地址
dma_init_Usart2_TX.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //外设地址不变
dma_init_Usart2_TX.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; //外设数据位宽为8位
dma_init_Usart2_TX.priority = DMA_PRIORITY_ULTRA_HIGH; //优先级
dma_init(DMA0, DMA_CH1, &dma_init_Usart2_TX);
dma_circulation_disable(DMA0, DMA_CH1); //禁止DMA循环接收
/* initialize DMA0 channel2(Usart2_RX) */
dma_init_Usart2_RX.direction = DMA_PERIPHERAL_TO_MEMORY; //基地址到DAM
dma_init_Usart2_RX.memory_addr = (uint32_t)USART2RX_Buffer; //基地址
dma_init_Usart2_RX.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //地址自增
dma_init_Usart2_RX.memory_width = DMA_MEMORY_WIDTH_8BIT; //8bit
dma_init_Usart2_RX.number = (uint32_t)100; //将基地址中0个位接收
dma_init_Usart2_RX.periph_addr = (uint32_t)(&USART_DATA(USART2)); //串口地址
dma_init_Usart2_RX.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //外设地址不变
dma_init_Usart2_RX.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; //外设数据位宽为8位
dma_init_Usart2_RX.priority = DMA_PRIORITY_ULTRA_HIGH; //优先级
dma_init(DMA0, DMA_CH2, &dma_init_Usart2_RX);
dma_circulation_disable(DMA0, DMA_CH2); //禁止DMA循环接收
dma_memory_to_memory_disable(DMA0, DMA_CH1); //关闭内存到内存方式
dma_memory_to_memory_disable(DMA0, DMA_CH2); //关闭内存到内存方式
/* 使能 DMA0 通道2 */
dma_channel_enable(DMA0,DMA_CH1);
dma_channel_enable(DMA0, DMA_CH2);
}
4、初始化的最后我们进行中断的配置,我们要采用空闲中断的方式获取存放在DMA里面的数据。所谓空闲中断,就是在串口一帧数据发完空闲时,会产生一个中断标志,根据这个中断标志位我们就可以在中断函数里面进行相应操作而获取接收到的数据。
void NVIC_Init(void)
{
nvic_irq_enable(USART2_IRQn, 2, 0); //开中断,中断优先级
usart_interrupt_enable(USART2, USART_INT_IDLE); //开启空闲中断
usart_flag_clear(USART2, USART_FLAG_IDLE);//清除IDLE空闲标志,防止上电即误触发空闲
}
5、接下来便是中断函数,中断函数对应着串口的接收功能,基本流程是:检测到是空闲中断,先关闭DMA,随后清除空闲中断标志位,根据手册,usart_data_receive(USART2)是读出数据时会清除空闲中断标志位,然后再将缓冲区地址指回USART2RX_Buffer。最后开启DMA传输,完成数据的接收。
void USART2_IRQHandler(void)
{
if(RESET != usart_interrupt_flag_get(USART2, USART_INT_FLAG_IDLE)) //空闲中断
{
dma_channel_disable(DMA0, DMA_CH2); //失能DAM,保护数据
usart_data_receive(USART2); /* 数据被读出,清除空闲中断标志位 */
rxFlg=1;
/* 重新设置DMA传输 */
dma_memory_address_config(DMA0,DMA_CH2,(uint32_t)USART2RX_Buffer);
dma_transfer_number_config(DMA0,DMA_CH2,sizeof(USART2RX_Buffer));
dma_channel_enable(DMA0, DMA_CH2); //开启DMA传输
}
}
不过,这样子配置中断函数会有个大问题。那就是DAM的缓冲区在接收完毕后并没有清空。
举个例子,如果串口DMA接收的是123456\n,而后再接收1234\n,则DMA缓冲区里面的数据会变成1234\n6\n。这里有两个解决办法,
第一个是每次接收完毕就清空DMA缓存区,第二个是采用数据处理的方式解决。
直接清空就好了,为啥还需要数据处理呢,这是因为我们在读取模块数据并与上位机通信时,大概率是需要进行编码处理,也就是说我们需要的不是字符串类型的数据,比如上位机输入100,串口收到是字符串类型的,可能需要转成整形才符合我们要求,而我们在上位机发送100时,是可以选择带\n的形式的,故实际发送的是100\n。由于\n的ASCII码为10,而数字的ASCII码为48~57。所以当我们在解码时,判断数据是否在48~57内,自然而然就能把多余的数字滤除掉啦。
不过,为了保险起见,我们还是在得到数据后复制一份,然后将缓冲区清零,需要调用string.h里面的memset函数,修改后的中断函数代码如下:
void USART2_IRQHandler(void)
{
if(RESET != usart_interrupt_flag_get(USART2, USART_INT_FLAG_IDLE)) //空闲中断
{
dma_channel_disable(DMA0, DMA_CH2); //失能DAM,保护数据
usart_data_receive(USART2); /* 数据被读出,清除空闲中断标志位 */
strcpy(buff_string,USART2RX_Buffer); //将数据复制到buff_string
memset(USART2RX_Buffer,0,sizeof (USART2RX_Buffer)); //清空缓冲区
rxFlg=1;
/* 重新设置DMA传输 */
dma_memory_address_config(DMA0,DMA_CH2,(uint32_t)USART2RX_Buffer); //重新指回首地址
dma_channel_enable(DMA0, DMA_CH2); //开启DMA传输
}
}
6、串口发送函数,将要发送的数据地址及数据长度传入,DMA自动发送此数据。
void Usartx_Transmit_DMA(uint32_t usart_periph,uint8_t * data_buffer, uint32_t length)
{
if(usart_periph==USART2)
{
dma_channel_disable(DMA0, DMA_CH1); //失能DAM,保护数据
/*将要发送的数据地址写入DAM通道1,发送*/
dma_memory_address_config(DMA0, DMA_CH1,(uint32_t)data_buffer);
/*设置DAM通道1数据长度,发送*/
dma_transfer_number_config(DMA0,DMA_CH1,length);
/* enable DMA channel to start send */
dma_channel_enable(DMA0, DMA_CH1); //数据传输完毕停止
}
}
到这里,串口的所有函数就已经编写完毕了,编写主函数,我们来进行一个数据回环的实验:
extern char buff_string[20] ;//存储串口接收的数据
extern uint8_t rxFlg;//默认串口未接收到数据
int main(void)
{
systick_config();
//串口相关
USART_Config();
NVIC_Init();
DMA_Init();
USART_GPIO_Config();
//串口相关
Delay_init();
while(1)
{
if(rxFlg==1)
{
rxFlg=0 ;
Usartx_Transmit_DMA(USART2,(uint8_t*)buff_string,strlen(buff_string));
}
Delay_ms(100);
}
}
实验结果如下:
数据正确接收且不存在数据覆盖的情况。
下面附带简单的整形与字符串互相转换的代码,字节转二进制代码,以便接收到的数据的使用。
void Int_String(int num,char *str)//10进制 //数字转字符串
{
int i = 0;//指示填充str
if(num<0)//如果num为负数,将num变正
{
num = -num;
str[i++] = '-';
}
//转换
do
{
str[i++] = num%10+48;//取num最低位转 ASCII码
num /= 10;//去掉最低位
}while(num); //num不为0继续循环
str[i] = '\n';
str[i+1] = '\0'; //****添加换行符号方便串口观察*****
//确定开始调整的位置
int j = 0;
if(str[0]=='-')//如果有负号,负号不用调整
{
j = 1;//从第二位开始调整
++i;//由于有负号,所以交换的对称轴也要后移1位
}
//对称交换
for(;j<i/2;j++)
{
str[j] = str[j] + str[i-1-j];
str[i-1-j] = str[j] - str[i-1-j];
str[j] = str[j] - str[i-1-j];
}
}
int String_Int(char *str) //字符串转数字
{
char flag = '+'; //指示结果是否带符号
long int res = 0;
if(*str=='-') //字符串带负号
{
++str; //指向下一个字符
flag = '-'; //将标志设为负号
}
//逐个字符转换,并累加到结果res
while(*str>=48 && *str<=57) //如果是数字才进行转换,数字0~9的ASCII码:48~57
{
res = 10*res + *str++ -48; //字符'0'的ASCII码为48,48-48=0刚好转化为数字0
}
if(flag == '-') //处理是负数的情况
{
res = -res;
}
return (int)res;
}
void ob_change(uint8_t VCC_GND ,uint8_t *a) //字节转八位二进制
{
uint8_t i=0;
for(i=0;i<8;i++)
{
if(VCC_GND%2 == 0)
{
a[8-i-1]=0;
VCC_GND=VCC_GND/2;
}
else
{
VCC_GND=(VCC_GND-1)/2;
a[8-i-1]=1;
}
}
}
到此,串口部分使用完毕,今天是星期五!