目录
初始化配置
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'二进制的正序。