前言
通过本次代码的学习掌握,USART的发送和接收,以及程序架构上实现驱动层和应用层的分离,
学会串口驱动配置,中断函数以及printf打印输出重定向,回调函数在程序中的实际使用方式
1.0 串口接收数据的几种场景
MCU 只发送命令接收响应
串口接收数据比较的复杂,我们可以给数据包定义一定的格式,数据按照固定的格式进行数据的发送和接收。
1:数据的接收端不知道什么时候会接收到数据
2:数据包的长度是不确定长度的
3:接收到的数据频率很快,有时候来不及处理,引入问题
收发双方需要协商出一些数据格式
帧头:表示一包数据开始了
数据域的长度:也就是包含的数据,后面有几个数据
功能字:0x06 使用8421包含的含义就是 0x0000 0101
LED编号:00/01/02
开关的状态: 0x00 表示关闭,0x01表示开启
校验:异或校验,和校验,奇数偶数校验
一包数据的格式协商出来
数据帧头:表示数据开始了,有时可以使用两个数据来表示数据帧头
数据域的长度:0x03 表示后面的数据是三个数据帧
功能字: 控制外设发出的动作
LED编号:第几个led灯
开关状态:0 表示的是关,1 表示的是开
校验:有多种的校验方式
数据帧头:一般使用特殊的数值【0x55|0xaa】,最好不要出现在有效的数据域内,后面的是数据的长度03表示后面的几个字节,功能字,编号,开关状态,与校验位。
2.0 串口驱动编写
1:串口初始化
初始化GPIO时钟,初始化串口的发送接收引脚,目前使用到MCU的串口时钟端口为9号引脚和10号引脚,如以下的原理图所示
// rcc 开启时钟
rcu_periph_clock_enable(RCU_GPIOA);
// 初始化发送引脚
gpio_init(GPIOA,GPIO_MODE_AF_PP,GPIO_OSPEED_10MHZ,GPIO_PIN_9);
// 初始化接收引脚
gpio_init(GPIOA,GPIO_MODE_IPU,GPIO_OSPEED_10MHZ,GPIO_PIN_10);
使能串口时钟初始化,使用串口与结构体的方式
// rcc 使能串口时钟
rcu_periph_clock_enable(RCU_USART0);
// 复位usart时钟
usart_deinit(USART0);
// 设置串口的波特率
usart_baudrate_set(USART0,115200);
// 使能串口发送
usart_transmit_config(USART0,USART_TRANSMIT_ENABLE);
// 使能串口接收
usart_receive_config(USART0,USART_RECEIVE_ENABLE);
// 使能串口中断,接收中断标志位
usart_interrupt_enable(USART0,USART_INT_RBNE);
// 配置nvic嵌套中断向量控制器,优先级设置为最高
nvic_irq_enable(USART0_IRQn,0,0);
// 使能串口
usart_enable(USART0);
2: 创建串口中断服务函数
/*
串口中断服务函数
*/
void USART0_IRQHandler(void)
{
// 获取串口中断标志位,
if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_RBNE) != RESET)
{
// 获取标志位后清除标志位,否则会一直进入中断
usart_interrupt_flag_clear(USART0,USART_INT_FLAG_RBNE);
// 获取接收数据寄存器中的数据
uint8_t uData = (uint8_t)usart_data_receive(USART0);
// 回调函数处理
pProcUartDataFunc(uData);
}
}
usart_interrupt_flag_get(USART0,USART_INT_FLAG_RBNE) != RESET 作用是判断是否进入中断如果等于1进入中断,该函数的作用是获取中断标志位,注:获取中断标志位后需要手动清除中断标志位,如果不手动清除中断标志位,程序会在这个位置一直进入中断,会卡在这里。
3: 创建函数指针与回调函数
使用函数指针与回调函数的目的是驱动层和应用层进行分离,做到程序的架构合理化
// 创建函数指针变量
static void (*pProcUartDataFunc)(uint8_t data);
这是一个函数指针变量简单解释就是(*pProcUartDataFunc)这是一个指针指向的是一个函数这个函数的参数是uint8_t 类型返回值是void (*)(uint8_t data)类型
4:注册回调函数
// 注册回调函数
void regUsb2ComCb(void (*pFunc)(uint8_t data))
{
// 参数是一个函数指针变量指向函数的地址
pProcUartDataFunc = pFunc;
}
目的是将返回来函数的地址赋值给全局静态的函数指针变量,注意在C语言中Static关键字定义函数表示这个函数只在当前的.c文件中有效,无法被extern等关键字声明和调用。
5: printf 输出重定向
// fputc重定向打印输出
int fputc(int ch,FILE *f)
{
usart_data_transmit(USART0, (uint8_t)ch);
// 发送数据寄存器空表示数据已经发送出去
while (RESET == usart_flag_get(USART0, USART_FLAG_TBE));
return ch;
}
这是一个固定的写法,printf在keil这个编译器中默认是向屏幕打印输出,如果需要在串口或者是命令行中显示需要进行重定向,这里就是将串口发送的数据重新向,返回输出在命令行中。
6:完整代码
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include "gd32f30x.h"
#include "led_drv.h"
// 创建函数指针变量
static void (*pProcUartDataFunc)(uint8_t data);
// 注册回调函数
void regUsb2ComCb(void (*pFunc)(uint8_t data))
{
// 参数是一个函数指针变量指向函数的地址
pProcUartDataFunc = pFunc;
}
// 串口初始化
void USART_Init(void)
{
// rcc 开启时钟
rcu_periph_clock_enable(RCU_GPIOA);
// 初始化发送引脚
gpio_init(GPIOA,GPIO_MODE_AF_PP,GPIO_OSPEED_10MHZ,GPIO_PIN_9);
// 初始化接收引脚
gpio_init(GPIOA,GPIO_MODE_IPU,GPIO_OSPEED_10MHZ,GPIO_PIN_10);
// rcc 使能串口时钟
rcu_periph_clock_enable(RCU_USART0);
// 复位usart时钟
usart_deinit(USART0);
// 设置串口的波特率
usart_baudrate_set(USART0,115200);
// 使能串口发送
usart_transmit_config(USART0,USART_TRANSMIT_ENABLE);
// 使能串口接收
usart_receive_config(USART0,USART_RECEIVE_ENABLE);
// 使能串口中断,接收中断标志位
usart_interrupt_enable(USART0,USART_INT_RBNE);
// 配置nvic嵌套中断向量控制器,优先级设置为最高
nvic_irq_enable(USART0_IRQn,0,0);
// 使能串口
usart_enable(USART0);
}
/*
串口中断服务函数
*/
void USART0_IRQHandler(void)
{
// 获取串口中断标志位,
if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_RBNE) != RESET)
{
// 获取标志位后清除标志位,否则会一直进入中断
usart_interrupt_flag_clear(USART0,USART_INT_FLAG_RBNE);
// 获取接收数据寄存器中的数据
uint8_t uData = (uint8_t)usart_data_receive(USART0);
// 回调函数处理
pProcUartDataFunc(uData);
}
}
// fputc重定向打印输出
int fputc(int ch,FILE *f)
{
usart_data_transmit(USART0, (uint8_t)ch);
// 发送数据寄存器空表示数据已经发送出去
while (RESET == usart_flag_get(USART0, USART_FLAG_TBE));
return ch;
}
头文件部分代码
#ifndef _USB2COM_DRV_H_
#define _USB2COM_DRV_H_
#include <stdint.h>
void USART_Init(void);
void regUsb2ComCb(void (*pFunc)(uint8_t data));
#endif
3.0 应用层代码编写
设定串口发送数据帧的格式,使用宏定义的方式给常量重新赋值避免在程序中出现和使用魔鬼数字
提高代码的可移植性。
/**
***********************************************************************
包格式:帧头0 帧头1 数据长度 功能字 LED编号 亮/灭 异或校验数据
0x55 0xAA 0x03 0x06 0x00 0x01 0xFB
***********************************************************************
*/
#define FRAME_HEAD_0 0x55 // 一包数据的开头
#define FRAME_HEAD_1 0xAA // 一包数据的开头
#define CTRL_DATA_LEN 3 //数据域长度
#define PACKET_DATA_LEN (CTRL_DATA_LEN + 4) //包长度
#define FUNC_DATA_IDX 3 //功能字数组下标
#define LED_CTRL_CODE 0x06 //功能字
#define MAX_BUF_SIZE 20 // 返回数组的大小
static uint8_t g_rcvDataBuf[MAX_BUF_SIZE]; // 存储数据的数组
static bool g_pktRcvd = false; // 全局变量标志位
串口应用处理函数,这个函数的作用是判断接收的数据包是不是完整的数据包,如果是完成的数据包就进行后续的处理
static void ProcUartData(uint8_t data)
{
// 创建一个数组下标索引
static uint8_t index = 0;
// 数组每次存储一个数据,索引值 + 1
g_rcvDataBuf[index++] = data;
// 判断接收的数据是不是复合定义的数据包格式
switch (index)
{
case 1:
// 第一个数据是不是包头是下一步,不是index = 0 继续判断
if (g_rcvDataBuf[0] != FRAME_HEAD_0)
{
index = 0;
}
break;
case 2:
if (g_rcvDataBuf[1] != FRAME_HEAD_1)
{
index = 0;
}
break;
case PACKET_DATA_LEN:
g_pktRcvd = true;
index = 0;
break;
default:
break;
}
}
串口异或校验函数
/**
***********************************************************
* @brief 对数据进行异或运算
* @param data, 存储数组的首地址
* @param len, 要计算的元素的个数
* @return 异或运算结果
***********************************************************
*/
static uint8_t CalXorSum(const uint8_t *data, uint32_t len)
{
uint8_t xorSum = 0;
for (uint32_t i = 0; i < len; i++)
{
xorSum ^= data[i];
}
return xorSum;
}
串口数据处理函数
/**
***********************************************************
* @brief USB转串口任务处理函数
* @param
* @return
***********************************************************
*/
void Usb2ComTask(void)
{
if (!g_pktRcvd)
{
return;
}
g_pktRcvd = false;
if (CalXorSum(g_rcvDataBuf, PACKET_DATA_LEN - 1) != g_rcvDataBuf[PACKET_DATA_LEN - 1])
{
return;
}
if (g_rcvDataBuf[FUNC_DATA_IDX] == LED_CTRL_CODE)
{
CtrlLed((LedCtrlInfo_t *)(&g_rcvDataBuf[FUNC_DATA_IDX + 1]));
}
}
LED灯控制函数
/**
***********************************************************
* @brief LED控制处理函数
* @param ctrlData,结构体指针,传入LED的编号和状态
* @return
***********************************************************
*/
static void CtrlLed(LedCtrlInfo_t *ctrlData)
{
ctrlData->ledState != 0 ? TurnOnLed(ctrlData->ledNo) : TurnOffLed(ctrlData->ledNo);
}
这段代码里面使用到三元运算符
USB 转串口初始化函数,底层调用应用层的代码
/**
***********************************************************
* @brief USB转串口应用初始化函数
* @param
* @return
***********************************************************
*/
void Usb2ComAppInit(void)
{
regUsb2ComCb(ProcUartData);
}
完整代码
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include "usb2com_drv.h"
#include "led_drv.h"
/**
***********************************************************************
包格式:帧头0 帧头1 数据长度 功能字 LED编号 亮/灭 异或校验数据
0x55 0xAA 0x03 0x06 0x00 0x01 0xFB
***********************************************************************
*/
#define FRAME_HEAD_0 0x55
#define FRAME_HEAD_1 0xAA
#define CTRL_DATA_LEN 3 //数据域长度
#define PACKET_DATA_LEN (CTRL_DATA_LEN + 4) //包长度
#define FUNC_DATA_IDX 3 //功能字数组下标
#define LED_CTRL_CODE 0x06 //功能字
#define MAX_BUF_SIZE 20
static uint8_t g_rcvDataBuf[MAX_BUF_SIZE];
static bool g_pktRcvd = false;
typedef struct
{
uint8_t ledNo;
uint8_t ledState;
} LedCtrlInfo_t;
static void ProcUartData(uint8_t data)
{
// 创建一个数组下标索引
static uint8_t index = 0;
// 数组每次存储一个数据,索引值 + 1
g_rcvDataBuf[index++] = data;
// 判断接收的数据是不是复合定义的数据包格式
switch (index)
{
case 1:
// 第一个数据是不是包头是下一步,不是index = 0 继续判断
if (g_rcvDataBuf[0] != FRAME_HEAD_0)
{
index = 0;
}
break;
case 2:
if (g_rcvDataBuf[1] != FRAME_HEAD_1)
{
index = 0;
}
break;
case PACKET_DATA_LEN:
g_pktRcvd = true;
index = 0;
break;
default:
break;
}
}
/**
***********************************************************
* @brief 对数据进行异或运算
* @param data, 存储数组的首地址
* @param len, 要计算的元素的个数
* @return 异或运算结果
***********************************************************
*/
static uint8_t CalXorSum(const uint8_t *data, uint32_t len)
{
uint8_t xorSum = 0;
for (uint32_t i = 0; i < len; i++)
{
xorSum ^= data[i];
}
return xorSum;
}
/**
***********************************************************
* @brief LED控制处理函数
* @param ctrlData,结构体指针,传入LED的编号和状态
* @return
***********************************************************
*/
static void CtrlLed(LedCtrlInfo_t *ctrlData)
{
ctrlData->ledState != 0 ? TurnOnLed(ctrlData->ledNo) : TurnOffLed(ctrlData->ledNo);
}
/**
***********************************************************
* @brief USB转串口任务处理函数
* @param
* @return
***********************************************************
*/
void Usb2ComTask(void)
{
if (!g_pktRcvd)
{
return;
}
g_pktRcvd = false;
if (CalXorSum(g_rcvDataBuf, PACKET_DATA_LEN - 1) != g_rcvDataBuf[PACKET_DATA_LEN - 1])
{
return;
}
if (g_rcvDataBuf[FUNC_DATA_IDX] == LED_CTRL_CODE)
{
CtrlLed((LedCtrlInfo_t *)(&g_rcvDataBuf[FUNC_DATA_IDX + 1]));
}
}
/**
***********************************************************
* @brief USB转串口应用初始化函数
* @param
* @return
***********************************************************
*/
void Usb2ComAppInit(void)
{
regUsb2ComCb(ProcUartData);
}
头文件代码
#include <stdint.h>
/**
***********************************************************
* @brief USB转串口硬件初始化
* @param
* @return
***********************************************************
*/
void Usb2ComDrvInit(void);
/**
***********************************************************
* @brief 注册回调函数
* @param pFunc,函数指针变量,接收传入的回调函数地址
* @return
***********************************************************
*/
void regUsb2ComCb(void (*pFunc)(uint8_t data));
#endif
main 函数调用代码
#include <stdint.h>
#include <stdio.h>
#include "gd32f30x.h"
#include "led_drv.h"
#include "key_drv.h"
#include "systick.h"
#include "usb2com_drv.h"
#include "usb2com_app.h"
static void DrvInit(void)
{
SystickInit();
LedDrvInit();
KeyDrvInit();
USART_Init();
}
static void AppInit(void)
{
Usb2ComAppInit();
}
int main(void)
{
DrvInit();
AppInit();
while (1)
{
Usb2ComTask();
//DelayNms(1000);
}
}
串口接收数据测试
后记
以上内容基于本人学习总结和思考,学习自郭天祥ARM32教程,仅供学习参考