GD32F407_串口USART功能

目录

初始化配置

1.GPIO初始化

2.USART初始化

3.使能开关

4.中断配置

5.开启USART

发送数据

1.发送一个字节数据

2.发送多个字节数据

3.发送字符串

4.配置printf函数

三、接收数据

1.中断处理函数与数据接收

2.接受数据回调函数定义

3.接收数据验证

3.验证时序

(1)数据位

(2)校验位

(3)大小端


初始化配置

1.GPIO初始化

先对RX和TX两个引脚进行GPIO配置(时钟、模式、输出方式),并设置复用模式。

为了方便后期串口配置的维护,将 配置中涉及到变化的参数进行宏定义,以TX_PA9和RX_PA10为例,代码如下。

// PA9     USART0_TX    AF7
#define USART0_TX_RCU     RCU_GPIOA
#define USART0_TX_PUPD     GPIO_PUPD_PULLUP
#define USART0_TX_PORT     GPIOA
#define USART0_TX_PIN     GPIO_PIN_9
#define USART0_TX_AF     GPIO_AF_7

// PA10     USART0_RX    AF7
#define USART0_RX_RCU     RCU_GPIOA
#define USART0_RX_PUPD     GPIO_PUPD_PULLUP
#define USART0_RX_PORT     GPIOA
#define USART0_RX_PIN     GPIO_PIN_10
#define USART0_RX_AF     GPIO_AF_7

#define USART0_BAUDRATE 115200UL

配置中所用到的方法都可通过F12跳转到函数定义处查询参数类型和名称。

   
 // =====================TX  GPIO 初始化=====================
    // 启用GPIO时钟 rcu_periph_clock_enable
    rcu_periph_clock_enable(USART0_TX_RCU);
    // 配置TX PA9和RX PA10引脚,复用模式,推挽输出 gpio_mode_set gpio_output_options_set
    gpio_mode_set(USART0_TX_PORT,GPIO_MODE_AF,USART0_TX_PUPD,USART0_TX_PIN);
    gpio_output_options_set(USART0_TX_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_MAX,USART0_TX_PIN);
    // 设置GPIO端口的备用功能 AF7  gpio_af_set
    gpio_af_set(USART0_TX_PORT,USART0_TX_AF,USART0_TX_PIN);

      // =====================RX  GPIO 初始化=====================
    // 启用GPIO时钟 rcu_periph_clock_enable
    rcu_periph_clock_enable(USART0_RX_RCU);
    // 配置TX PA9和RX PA10引脚,复用模式,推挽输出 gpio_mode_set gpio_output_options_set
    gpio_mode_set(USART0_RX_PORT,GPIO_MODE_AF,USART0_RX_PUPD,USART0_RX_PIN);
    gpio_output_options_set(USART0_RX_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_MAX,USART0_RX_PIN);
    // 设置GPIO端口的备用功能 AF7  gpio_af_set
    gpio_af_set(USART0_RX_PORT,USART0_RX_AF,USART0_RX_PIN);

2.USART初始化

时钟、重置(可选)、配置串口参数(波特率、数据位、校验位、停止位、大小端)

USART初始化所用到的方法在gd32f4xx_usart.c驱动文件中,所以要添加改驱动文件。配置信息中波特率是必填项,其他不配置有默认值,具体见代码注释部分,本文代码中参数都进行了配置。

    // =====================USART 初始化=====================
    // 启用USART0时钟 rcu_periph_clock_enable
        rcu_periph_clock_enable(RCU_USART0);
    // 重置(可选) usart_deinit
        usart_deinit(USART0);
    // 配置串口参数:波特率(必填), 校验位(默认无校验),数据位(默认8位),停止位(默认1位), 大小端模式(默认小端)
        usart_baudrate_set(USART0,USART0_BAUDRATE);
        //数据位
        usart_word_length_set(USART0,USART_WL_9BIT);
        //校验位
        usart_parity_config(USART0,USART_PM_ODD);
        //停止位
        usart_stop_bit_set(USART0,USART_STB_1BIT);
        //大小端
        usart_data_first_config(USART0,USART_MSBF_LSB);

3.使能开关

开启USART的接受和发送功能。

//=====================使能开关===============
// 启用发送功能 usart_transmit_config
usart_transmit_config(USART0,USART_TRANSMIT_ENABLE)
// 启用接收功能 usart_receive_config
usart_receive_config(USART0,USART_RECEIVE_ENABLE);

4.中断配置

开启串口中断,然后开启接收缓冲区非空中断和线路空闲中端,详细作用在后文中介绍。

//=====================中断===================
//开启中断 nvic_irq_enable
nvic_irq_enable(USART0_IRQn,2,2);
// USART_INT_RBNE接收到的数据可以读取,缓冲区有内容,
//触发中断 usart_interrupt_enable  USART_INT_RBNE是接受缓冲区不为空的标志
 usart_interrupt_enable(USART0,USART_INT_RBNE);
// USART_INT_IDLE检测到线路空闲,触发中断 usart_interrupt_enable
usart_interrupt_enable(USART0,USART_INT_IDLE);

上述代码中,nvic_irq_enable(USART0_IRQn,2,2;是关于串口中断优先级的配置,这里不做解释,后期有关于中断优先级的详细解释。

5.开启USART

//=====================USART开关==============
// 启用USART usart_enable
usart_enable(USART0);

最后可以将上述所谓配置整合到串口初始化函数中,方便在主入口调用。

发送数据

1.发送一个字节数据

在GD32中串口发送数据的核心方法是usart_data_transmit, 函数定义及参数见下图:

基于库函数的usart_data_transmit,自定义一个发送一个字节的方法。

// 发送1个byte数据
void USART0_send_byte(uint8_t byte){
    usart_data_transmit(USART0,byte);
    //等待发送完毕
    //USART_FLAG_TBE 发送区缓存为空标志  为SET表示发送结束,为RESET代表没有发送结束,进入循环等待。
    while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
}

USART0_send_byte方法中,while的处理很重要。以下是我的一些理解:

虽然程序是线性执行的,但是程序执行到usart_data_transmit后,软件层就会接着往下走,硬件层数据是否发送完毕程序是未知的,如果还在发送中,执行了其他业务,就会产生冲突。所以需要进入等待,在GD32中USART_FLAG_TBE这个标识符表示发送缓冲区是否为空。为1表示空,所以系统需要在标识符为0即RESET的状态下进行等待。硬件层发送完数据,标识符置自动置1,程序会退出循环执行其他业务。

2.发送多个字节数据

// 发送多个byte数据
void USART0_send_data(uint8_t* data, uint32_t len){
    //循环发送条件,指针不为空并且长度不为0,这里必须加上len的判断,因为data指针跳出数据后,会变成野指针,为不为空是未知的
    while(data!=NULL && len--){
        //这里的参数要加上* 因为发送一个字节的方法参数是数据,不是指针
        USART0_send_byte(*data);
        data++;
    }
}

//函数调用
// uint8_t temp[]={'a','b','c','\n'};
//USART0_send_data(temp,4);

循环条件是指针不为空并且数据长度不为0,初次学习时觉的不需要加上数据长度len的限制,之后明白data指针指完所要发送的数据后,指针指向的内容是未知的,data会变为一个野指针。所以有必要加上len的限制。

3.发送字符串

// 发送字符串 (结尾标记\0)
void USART0_send_string(char *data){
    //循环发送条件:data不为空并且数值不为0,字符串结束符'\0'在ASCII中的数值为0
    while(data&&*data){
        USART0_send_byte((uint8_t)*data);
        data++;
    }
}

字符串会有结束符'\0',其ASCII的十进制值就是0,所以循环发送的条件就是while(data&&*data),表示非空并且不是结束符。这里需要注意到的是USART0_send_byte函数的参数类型是数据,不是指针,所以传入时需要加上*,即*data进行解指针取值。

4.配置printf函数

在开发中,经常用到printf进行测试,根据GD32官方示例代码,配置printf函数。

查看官方示例代码,printf的本质就是MCU通过串口发送数据,根据示例代码重写fputc方法。

int fputc(int ch, FILE *f); 是C语言标准库中的一个函数,用于向指定的文件流中写入一个字符。这个函数定义在 <stdio.h> 头文件中。----来自文心一言。
int fputc(int ch,FILE * f){
    USART0_send_byte((uint8_t)ch);
    return ch;
}

三、接收数据

1.中断处理函数与数据接收

在前文USART中断配置中,配置了接收数据中断,和线路空闲中断。

也就是说,当有数据传入缓冲区或者接受数据线路空闲时触发中断,一个名为USART0_IRQHandler函数会被MCU自动调用,函数名可以在CMSIS/startup_gd32f407_427.s汇编启动文件中查找。

USART0_IRQHandler 函数是一个中断处理函数(Interrupt Handler),它专门用于处理USART0(通用同步异步收发传输器0)的中断事件。在嵌入式系统中,当USART0发生特定类型的中断(如接收数据中断、发送完成中断等)时,该函数会被自动调用。----来自文心一言。

作为使用者,需要定义USART0_IRQHandler函数,在函数中可以根据接受到的数据进行业务逻辑处理。但是在MCU中寄存器很珍贵,用于接收数据的USART的寄存器只有8位,只能存储一个字节,那么这样化的,就不能处理像数据帧一样的数据,因为在判断接受数据时,往往都是判断多个字节的命令才回去执行业务操作。

所以,可以自定义一个缓冲区buffer,逐个存储接收来的数据,最后进行集中处理。

//自定义一个buf缓冲区接受接受的数据
#define    RX_BUFFER_LEN    1024
uint8_t g_rx_buffer[RX_BUFFER_LEN];
uint32_t g_rx_cnt = 0;

// USART0中断处理函数USART0_IRQHandler,处理接收逻辑
// 中断处理函数,不能乱写名字,在CMSIS/startup_gd32f407_427.s拷贝名字
void USART0_IRQHandler(){
    //USART_INT_FLAG_RBNE是缓冲区是否为空的标志,为1SET表示有数据
    //不为空 将缓冲区的数据逐个字节存入自定义 g_rx_buffer
    if(SET==usart_interrupt_flag_get(USART0,USART_INT_FLAG_RBNE)){
        //RBNE标志位不会自动置0,只有当有数据来会自动变为1,所以要人为置0
        usart_flag_clear(USART0,USART_FLAG_RBNE);
        //接受端口数据
        uint8_t data=usart_data_receive(USART0);
        //存入 g_rx_buffer
        g_rx_buffer[g_rx_cnt++]=data;
        //越界处理
        if(g_rx_cnt>=RX_BUFFER_LEN) g_rx_cnt=0;
    }
    
    //当接受数据逻辑后,在线路空闲进行业务处理
    //线路空闲 USART_INT_IDLE 标志位是线路空闲的标志位
    if(SET==usart_interrupt_flag_get(USART0,USART_INT_FLAG_IDLE)){
        //清除标志位,但是使用再接受一次数据的方式进行清除,数据不使用
        usart_data_receive(USART0);
        //进行字符串处理,给接收到的数据末尾加'\0'结束符
        g_rx_buffer[g_rx_cnt]='\0';
        
        //调用业务处理函数,在main.c中定义
        #if USART0_RECV_CALLBACK
            USART0_on_recv(g_rx_buffer,g_rx_cnt);
        #endif
        
        //长度置0,不然下次接收到的数据会接着上次的续写
        g_rx_cnt=0;
    }
}

上述代码中涉及两个标志位USART_INT_FLAG_RBNE和USART_INT_FLAG_IDLE,分别表示接收数据缓冲区不为空和空闲线路检测中断标志。

当USART接收到数据并将其存储在接收缓冲区中时,如果接收缓冲区不为空(即已接收到数据但尚未被读取),则USART_INT_FLAG_RBNE标志位会被置位(即设置为1)。满足if条件后,先进行标志位清除,因为这个标志位不会自动置0,需要手动置0,然后调用usart_data_receive方法获取到数据,并将数据存取自定义的缓冲区中,用于后续集中处理。

USART_INT_FLAG_IDLE标志位是在USART通信过程中,当串口接收到最后一个字节后,在接下来的一段时间内没有接收到新的数据时,该标志位会被置1。这个状态通常用来指示一帧数据已经完全接收完成,且线路现在处于空闲状态。也就是说当整个数据发送完毕后,在一定时间内不会再接收数据就会执行下面的代码。这个时候数据是完整的,可以对整个数据进行操作,业务逻辑写在USART0_RECV_CALLBACK回调函数中,用户可以根据需要在合适位置进行定义。

在线路空闲中断处理中,需要清除USART_INT_FLAG_IDLE标志位,与清除USART_INT_FLAG_RBNE不一样的是,调用一次usart_data_receive清除标志位。这里参考的是GD32官方示例代码。如下:

USART0_RECV_CALLBACK函数如果不定义的话会报错,因为是在没有定义的情况下进行了调用,所以在使用前必须进行定义,为了方便使用和维护,上述代码对USART0_RECV_CALLBACK函数和fputc函数进行了条件编译,在头文件中添加宏定义开关。

// 功能开关配置
#define USART0_RECV_CALLBACK    1
#define USART0_PRINTF           1

#if USART0_PRINTF
#include "stdio.h"
int fputc(int ch,FILE *f) {
    USART0_send_byte((uint8_t)ch);
    return ch;
}
#endif

//调用
#if USART0_RECV_CALLBACK
       USART0_on_recv(g_rx_buffer,g_rx_cnt);
#endif

2.接受数据回调函数定义

上一节代码中调用了处理函数,如需使用接收数据处理函数,需要在相应头文件中添加函数声明同时加上条件编译,还需要在合适位置定义该函数。下面以定义在main.c功能是以字符串回显的方式为例:

//USART头文件中声明
#if USART0_RECV_CALLBACK
// 收到串口0数据,回调函数
extern void USART0_on_recv(uint8_t* data, uint32_t len);
#endif

//main.c中定义
//回调函数定义
void USART0_on_recv(uint8_t * data, uint32_t len) {
  printf("recv[%d] %s\n",len,data);
//    for(uint32_t i=0;i<len;i++){
//        USART0_send_byte(data[i]);
//    }
}

3.接收数据验证

在主函数中编写示例代码验证定义的函数是否正确:

int main(void) {

    // 系统滴答定时器初始化
    systick_config();
    USART0_init();
    printf("==================\n");//printf函数
    USART0_send_byte('A');//发送一个字节数据
    uint8_t temp[]={'b','c','\n'};
    USART0_send_data(temp,3);//发送多个字节
    USART0_send_string("hello\n");//发送字符串
    printf("jay\n");//printf函数
    while(1) {
    }
    return 0;
}

验证字符串回显的功能:打开串口调试工具,将参数和USART的参数保持一致。

波特率都为115200,停止位1位,数据位8位(部分ARM系列库要把发送的BIT长度word length设置为9,才能正确发送校验位),无校验位。设置成功后打开串口,多次复位发送数据。结果如下:

3.验证时序

接来下验证数据传输中数据位、停止位、校验位、大小端具体是什么。

天空星板子用于调试的串口引脚A9和A10已经占用,且没有引出引脚,为了方便调试,修改签名宏定义的RX和TX相关参数。

查询GD32F407数据手册,复用功能非常丰富,下图中显示,PB6、PB7引脚也可作为TX和RX,那么要通过逻辑分析仪查验时序,只需要连接B6引脚和GND,同时修改宏定义就可以。

修改TX宏定义位B6:

// PB6     USART0_TX    AF7
#define USART0_TX_RCU     RCU_GPIOB
#define USART0_TX_PUPD     GPIO_PUPD_PULLUP
#define USART0_TX_PORT     GPIOB
#define USART0_TX_PIN     GPIO_PIN_6
#define USART0_TX_AF     GPIO_AF_7

发送数据代码:

int main(void) {
  // 系统滴答定时器初始化
  systick_config();
    USART0_init();
    uint8_t temp[]={'a','b','c','\n'};
    USART0_send_data(temp,4);
    while(1) {
    }
}

(1)数据位

烧录程序后,打开Logic查看MCU通过USART发送来的数据,这里需要先配置分析数据的一些参数,需要和自己设置的USART参数保持一致。

按下复位键查看接受的数据:

这是一个数据帧的格式

接来下看我们接收到的第一个数据:

因为我们设置的是小端模式,数据是从低位开始接收,接收到的数据是1000 0110 ,逆序之后原数据就是 0110 0001 对应十六进制是0X61,十进制是97,对应的ASCII值就是字符'a'。这与我们发送的数据对上了。

(2)校验位

奇校验(ODD):校验位被设置为确保数据位中1的总数为奇数。例如,数据位中的“1”总数为奇数,校验位被设置为低电平(拉低为0),否则设置为高电平。故而,如果接收方统计发现“1”总数为偶数,且校验是低电平,则校验失败,否则成功。

偶校验(Even): 校验位被设置为确保数据位中1的总数为偶数。例如,数据位中的“1”总数为偶数,校验位被设置为低电平(拉低为0),否则设置为高电平。故而,如果接收方统计发现“1”总数为奇数,且校验是低电平,则校验失败,否则成功。

在接收'a'的过程中,高电平次数为3,3是奇数,因此校验位会被拉低

现在将程序的校验位设置为偶校验,接受端的数据分析也设置为偶校验,重新烧录程序。

高电平出现3次,不是偶数,校验位拉高。

(3)大小端

通俗讲就是大端是先处理高位,得到的数据和原数据是一样的。小端就是先处理地位,和原数据的位是逆序的。例如 1100 1100的原数据通过大端发送,数据还是1100 1100,用小端方式发送则是0011 0011是原数据的逆序。

修改USART为大端并重新烧录,同时接受端的分析也配置为大端:

配置完成后,复位发送数据:

可以看到接收到的数据是字符'a'二进制的正序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值