本文以 立创·天空星开发板-GD32F407VET6-青春版 作为学习的板子,记录学习笔记。
立创·天空星开发板-GD32F407VE-USART
- 基础通信概念
- 同步通信 & 异步通信
- 串行通信 & 并行通信
- 双工 & 单工
- 通讯速率
- 码元
- 串口通信
- 数据帧
- 串口封装
基础通信概念
通信协议是网络中(包括互联网)设备之间交换信息时所必须遵守的规则的正式描述。
同步通信 & 异步通信
- 分类依据:通信过程中发送方和接收方是否使用相同的时钟频率。
- 同步通信(Synchronous Communication):发送方和接收方使用相同的时钟频率,数据以比特流(数据帧)的形式连续传输。数据帧包括同步字符、数据部分和校验码等。
- 异步通信(Asynchronous Communication):发送方和接收方使用各自的时钟频率,数据以字符为单位进行传输。每个字符数据前有一个起始位,后有停止位,用于标识字符的开始和结束。
故而,同步通信和异步通信的最大区别在于传输数据时是否需要时钟信号同步。有钟为同,无钟为异。
串行通信 & 并行通信
- 分类依据:数据在传输线上的传输方式。
- 串行通信(Serial Communication):数据的各个位在同一根数据线上逐位发送和接收。串行通信传输速度慢,但传输距离远,适合远距离通信。
- 并行通信(Parallel Communication):数据的各个位同时在多根数据线上发送或接收。并行通信传输速度快,但成本高,适合短距离通信。
故而,串行通信和并行通信的最大区别在于传输数据时数据线的根数。单根为串,多根为并。
双工 & 单工
- 分类依据:通信双方的信息传输方向。
- 双工(Duplex):
- 全双工(Full-Duplex):通信双方可以同时发送和接收数据。
- 半双工(Half-Duplex):通信双方可以相互发送和接收数据,但不能同时进行。
- 单工(Simplex):通信是单向的,一方只能发送数据,另一方只能接收数据。
故而,双工和单工的最大区别在于传输方向是否是双向的。双传为双工,单传为单工。
通讯速率
衡量通讯性能的一个非常重要的参数就是通讯速率
- 比特率:每秒钟传输的二进制位数,单位为(bit/s)
- 波特率:每秒钟传输了多少个码元
码元
码元是通讯信号调制的概念,通讯中常用时间间隔相同的符号来表示一个二进制数字,这样的信号称为码元。
如果在通讯传输中,用 0V 表示数字 0,5V 表示数字 1,那么一个码元可以表示两种状态 0 和 1,所以一个码元等于一个二进制比特位,此时波特率的大小与比特率一致。
如果在通讯传输中,有 0V、2V、4V 以及 6V 分别表示二进制数 00、01、10、11,那么每个码元可以表示四种状态,即两个二进制比特位,所以码元数是二进制比特位数的一半,这个时候的波特率为比特率的一半。
因为很多常见的通讯中一个码元都是表示两种状态(0和1),所以常常直接以波特率来表示比特率。
串口通信
Universal Asynchronous Receiver Transmitter 即 : 通用异步收发器
串口通信是一种通用的串行的异步通信总线,该总线有两条数据线,可以实现全双工的发送和接收,在嵌入式系统中常用于主机与辅助设备之间的通信。
数据帧
- 在串口通信中,数据帧是传输数据的单元。
- 一帧数据包含多个组成部分,以确保数据的正确传输和接收。
以传递一个字母 k 为例,若数据为采用八位,并使用低位先行的方式进行传输。过程大致如下: - 字母 k 的二进制为
01101011
, - 采用低位先行的传输方式的话,其传输顺序应该是
11010110
- 传输波形大致如下所示:
串口封装
天空星开发板的 PA9 和 PA10 对应的是 USART0 的 TX 和 RX 功能。
运用库函数,封装代码如下:
- ExtendedUSART.h
#ifndef __EXTENDED_USART_H__
#define __EXTENDED_USART_H__
#include "gd32f4xx.h"
#include <stdio.h>
// 发送配置
#define USART_TX_RCU RCU_GPIOA
#define USART_TX_PORT GPIOA
#define USART_TX_PIN GPIO_PIN_9
#define USART_TX_ALT GPIO_AF_7
// 接收配置
#define USART_RX_RCU RCU_GPIOA
#define USART_RX_PORT GPIOA
#define USART_RX_PIN GPIO_PIN_10
#define USART_RX_ALT GPIO_AF_7
// 中断配置
#define USART_RCU RCU_USART0
#define USART_NUM USART0
#define USART_IRQ USART0_IRQn
#define USART_IRQ_Handler USART0_IRQHandler
/*!
\brief 初始化串口 0
\param[in] b : [uint8_t] 一个字节的数据
\param[out] none
\retval none
*/
void USART0_config();
/*!
\brief 往串口 0 发送1个字节数据
\param[in] b : [uint8_t] 一个字节的数据
\param[out] none
\retval none
*/
void USART0_send_byte(uint8_t b);
/*!
\brief 往串口 0 发送多个字节数据
\param[in] dat : [uint8_t*] 字节数据首元素地址
\param[in] len : [uint32_t] 字节数据的长度
\param[out] none
\retval none
*/
void USART0_send_data(uint8_t* dat, uint32_t len);
/*!
\brief 往串口 0 发送发送字符串 (结尾标记\0)
\param[in] dat : [char*] 字符串首字符地址
\param[out] none
\retval none
*/
void USART0_send_string(char *dat);
#endif
- ExtendedUSART.c
#include "ExtendedUSART.h"
// 串口接收缓冲区大小
#define USART_RECEIVE_LENGTH 1024
// 串口接收数据缓冲区
static uint8_t g_recv_buff[USART_RECEIVE_LENGTH];
// 接收到字符的长度
static uint32_t g_recv_length = 0;
/*!
\brief 初始化串口 0
\param[in] b : [uint8_t] 一个字节的数据
\param[out] none
\retval none
*/
void USART0_config() {
// GPIO 配置: TXD, PA9
rcu_periph_clock_enable(USART_TX_RCU);
gpio_mode_set(USART_TX_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, USART_TX_PIN);
gpio_af_set(USART_TX_PORT, USART_TX_ALT, USART_TX_PIN);
// GPIO 配置: TXD, PA10
rcu_periph_clock_enable(USART_RX_RCU);
gpio_mode_set(USART_RX_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, USART_RX_PIN);
gpio_af_set(USART_RX_PORT, USART_RX_ALT, USART_RX_PIN);
// 启用串口时钟
rcu_periph_clock_enable(USART_RCU);
// 复位串口
usart_deinit(USART_NUM);
usart_baudrate_set(USART_NUM, 115200); // 波特率
usart_parity_config(USART_NUM, USART_PM_NONE); // 校验位, 无校验(USART_PM_NONE), 偶校验(USART_PM_EVEN), 奇校验(USART_PM_ODD)
usart_word_length_set(USART_NUM, USART_WL_8BIT); // 数据位数
usart_stop_bit_set(USART_NUM, USART_STB_1BIT); // 停止位
usart_data_first_config(USART_NUM, USART_MSBF_LSB); // 先发送高位还是低位
// 发送功能配置
usart_transmit_config(USART_NUM, USART_TRANSMIT_ENABLE);
// 接收功能配置
usart_receive_config(USART_NUM, USART_RECEIVE_ENABLE);
// 接收中断配置
nvic_irq_enable(USART_IRQ, 2, 2);
// 串口中断能
usart_interrupt_enable(USART_NUM, USART_INT_RBNE);
usart_interrupt_enable(USART_NUM, USART_INT_IDLE);
// 串口使能
usart_enable(USART_NUM);
}
/*!
\brief 往串口 0 发送1个字节数据
\param[in] b : [uint8_t] 一个字节的数据
\param[out] none
\retval none
*/
void USART0_send_byte(uint8_t b) {
//通过USART发送
usart_data_transmit(USART0, b);
//判断缓冲区是否已经空了
while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
}
/*!
\brief 往串口 0 发送多个字节数据
\param[in] dat : [uint8_t*] 字节数据首元素地址
\param[in] len : [uint32_t] 字节数据的长度
\param[out] none
\retval none
*/
void USART0_send_data(uint8_t* dat, uint32_t len) {
while(dat && len--) {
USART0_send_byte(*dat);
dat++;
}
}
/*!
\brief 往串口 0 发送发送字符串 (结尾标记\0)
\param[in] dat : [char*] 字符串首字符地址
\param[out] none
\retval none
*/
void USART0_send_string(char *dat) {
//满足: 1.data指针不为空 2.发送的数据不是\0结束标记
while(dat && *dat) {
USART0_send_byte((uint8_t)(*dat));
dat++;
}
}
int fputc(int ch, FILE *f) {
USART0_send_byte((uint8_t)ch);
return ch;
}
// 声明弱函数, 用于接收来自串口 0 的数据并回显
// 外部可以通过声明同名强函数来覆盖该函数的默认行为
__attribute__((weak)) void USART0_on_received(uint8_t* dat, uint32_t len) {
USART0_send_data(dat,len);
}
// 串口接收数据的中断处理函数
void USART_IRQ_Handler(void) {
if ((usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) == SET) {
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE);
uint16_t value = usart_data_receive(USART0);
g_recv_buff[g_recv_length] = value;
g_recv_length++;
}
if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE) == SET) {
//读取缓冲区,清空缓冲区
usart_data_receive(USART0);
g_recv_buff[g_recv_length] = '\0';
// g_recv_buff 为接收的数据, g_recv_length 为接收的长度
USART0_on_received(g_recv_buff, g_recv_length);
g_recv_length = 0;
}
}
- main.c
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "ExtendedUSART.h"
void USART0_on_received(uint8_t* dat, uint32_t len) {
printf("Received: %s\r\n", dat);
}
int main(void) {
systick_config();
USART0_config();
uint8_t cnt = 0;
while(1) {
USART0_send_byte(cnt++);
USART0_send_byte(0x0D); // \r
USART0_send_byte(0x0A); // \n
USART0_send_string("This is string!\r\n");
printf("cnt = %d\r\n", cnt);
if(cnt > 99) {
cnt = 0;
}
delay_1ms(1000);
}
}
额外说明
- 如果希望通过
printf
函数将数据打印到串口助手上显示,需要重写stdio.h
中的fputc
函数。 - 如果希望调用方无论是否定义回调函数都能使得回调函数默认可用,可以采用定义弱函数的方式来实现。也就是我这边提供的
ExtendedUSART.c
文件中的USART0_on_received
函数的定义方式,即便是在main.c
中没有定义,照样能回显数据。