本章重点

        本章讲述何如把stm32的串口收发HEX数据包程序移植到CH32中,程序基本完全适配,只需在串口中断程序前添加这句特殊声明即可。

void USART1_IRQHandler(void) __attribute__((interrupt()));

一、USART串口

        串口通信的详细知识我就不赘述了我们以实用性为主,只讲实际使用需要知道的一些信息。

1.常用配置

        因为USART串口一般都为异步通信,对时序要求较高,所以现在主流的都是使用USART硬件外设,而不会选择软件模拟。这也是现在主流芯片基本都会自带硬件USART串口的原因。

2.大致时序

        每一帧包括起始位 - 数据位 - 停止位,起始位为低电平,停止位为高电平。而数据位可以选择8位或9位数据,常见搭配为8位数据:8位有效数据+无校验位,或9位数据:8位有效数据+1位校验位。这样可以保证每一帧都收发一个完整的字节。数据位为低位先行,收发时要注意顺序。

3.HEX数据包

        HEX数据包也就是将多个HEX数据打包发送与接收,分为包首-数据-包尾。接收方根据包首包尾来识别数据包中的有效数据。常用包首为FF,包尾为FE。数据包格式需要收发双发提前进行约定,不可随意更改。

4.代码实现方法

        对于代码的实现方法我们也要有大致的了解。初始化流程为GPIO - USART外设 - NVIC,GPIO初始化开启对应的硬件串口引脚,TX配置复用推挽输出 - 它需要具备高低电平的驱动能力且被硬件外设串口直接接管;RX配置上拉输入 - USART时序空闲时为高电平,起始位由发送方拉低电平。USART硬件外设要查看引脚定义表与系统结构框图来开启对应外设。NVIC来配置硬件外设的中断优先级。

        而对于硬件外设我们调用对应的底层库函数即可实现收发单字节的操作,我们需要手动封装库函数来实现多字节、数据包、字符、字串串的收发操作。

        本章节先讲解直接移植完整代码,下节再讲解移植基础代码再自己封装库函数的操作。

二、完整代码移植

1.新建工程与代码兼容调整

        老样子新建工程 "4-1_USART-HEX" ,把之前 "3-1_EXTI_Key_Led" 的Hardware文件夹复制到本节课的工程中来,再把江协科技的串口收发HEX数据包工程中Hardware文件夹下的"Serial.c" 与"Serial.h"文件复制到我们的Hardware文件中。打开我们的工程,添加Hardware文件夹编译路径,并修改Serial.c文件中的头文件,解决CH32的兼容问题。

2.USART硬件外设配置验证

        此时程序并没有出现报错,但我们还是要检查一下USART硬件外设的配置是否无误,我们首先检查外设挂载的总线是否有误,打开数据手册查看系统框图。可以看到USART1确实是挂载在APB2上的,没毛病。

        我们再查看USART外设复用引脚我们是否选对,这一项至关重要,如果选错了程序也不会报错,但功能是无法实现的。我们查看CH32V307的引脚定义,我这里已经整理好了它的引脚定义表,后续我会把它放在资料整合的文章当中,暂时没有这张表的也可以在数据手册中查看引脚定义。没毛病,PA9->USART1_TX->复用推挽输出,PA10->USART1_RX->上拉输入

        平常移植代码的话到这里就可以进行下一步调整代码了,但是这款开发板有点不一样,它的三个串口都接到了板上的三个Type-C口上,意味着你不需要使用USB转串口模块了,直接找到对应的C口与之连接到电脑即可。目前市面上大部分的板子的外接接口为了不浪费资源,一般都会把串口也连接进去。

        我们找到官方例程中的 "PUB" ,打开中间这个 "CH32V30xSCH.pdf" 文件,里面是这款开发板的原理图资料。通过网络标签可以知道USART1是连接到我们平常烧录程序的这个Type-C口上的,也就是板子上标注的P9。那么我们一会儿烧录完程序就可以一直接通过这个Type-C口与电脑进行串口通信了,无需另外接线。

3.代码调整

        这部分主要是对代码功能进行一定的修改,以适配我们开发板上现有的外设,不必像核心板那样连太多的线。主要用之前外部中断的按键功能,实现按一次按键发送一次数据,收到或发送数据时LED翻转一下。

        我们还是首先修改外部中断按键的代码,我们需要声明一个外部变量Key_Flag让主函数知道我们的按键按下了。按键按下后标志位置1,后面我们在主函数检查按键状态时给它手动清零。

        然后是Serial.c文件,不要忘记硬件串口也调用了中断函数,所以我们要给中断函数特殊声明一下,否则接受一次数据后程序就跑飞了。

        最后就是主函数了,我们还是老样子把STM32的main.c的代码复制过来,修改头文件,记得加上CH32自带的头文件 #include "debug.h" 和我们要使用的 #include "LED.h" ,同时删除 #include "Delay.h" ,因为CH32的delay函数是放在debug文件里的。我们调整一下主函数中按键检查部分的代码,把获取按键键码的删除,检查按键的变量修改一下。

main.c

#include "ch32v30x.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
#include "LED.h"
#include "debug.h"

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	Key_Init();			//按键初始化
	Serial_Init();		//串口初始化
	LED_Init();			//LED初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "TxPacket");
	OLED_ShowString(3, 1, "RxPacket");
	
	/*设置发送数据包数组的初始值,用于测试*/
	Serial_TxPacket[0] = 0x01;
	Serial_TxPacket[1] = 0x02;
	Serial_TxPacket[2] = 0x03;
	Serial_TxPacket[3] = 0x04;
	
	while (1)
	{
		if (Key_Flag == 1)				//按键按下
		{
			Key_Flag = 0;				//清除按键标志位
			LED1_Turn();				//即将发送数据,LED翻转
			Serial_TxPacket[0] ++;		//测试数据自增
			Serial_TxPacket[1] ++;
			Serial_TxPacket[2] ++;
			Serial_TxPacket[3] ++;
			
			Serial_SendPacket();		//串口发送数据包Serial_TxPacket
			
			OLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2);	//显示发送的数据包
			OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);
			OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);
			OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2);
		}
		
		if (Serial_GetRxFlag() == 1)	//如果接收到数据包
		{
			LED1_Turn();									//收到数据,LED翻转
			OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2);	//显示接收的数据包
			OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);
			OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);
			OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2);
		}
	}
}

三、完整代码

Serial.c

#include "ch32v30x.h"
#include <stdio.h>
#include <stdarg.h>

uint8_t Serial_TxPacket[4];				//定义发送数据包数组,数据包格式:FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4];				//定义接收数据包数组
uint8_t Serial_RxFlag;					//定义接收数据包标志位

/**
  * 函    数:串口初始化
  * 参    数:无
  * 返 回 值:无
  */
void Serial_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA9引脚初始化为复用推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA10引脚初始化为上拉输入
	
	/*USART初始化*/
	USART_InitTypeDef USART_InitStructure;					//定义结构体变量
	USART_InitStructure.USART_BaudRate = 9600;				//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//硬件流控制,不需要
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;	//模式,发送模式和接收模式均选择
	USART_InitStructure.USART_Parity = USART_Parity_No;		//奇偶校验,不需要
	USART_InitStructure.USART_StopBits = USART_StopBits_1;	//停止位,选择1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//字长,选择8位
	USART_Init(USART1, &USART_InitStructure);				//将结构体变量交给USART_Init,配置USART1
	
	/*中断输出配置*/
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);			//开启串口接收数据的中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);			//配置NVIC为分组2
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;					//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;		//选择配置NVIC的USART1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;		//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);							//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*USART使能*/
	USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行
}

/**
  * 函    数:串口发送一个字节
  * 参    数:Byte 要发送的一个字节
  * 返 回 值:无
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

/**
  * 函    数:串口发送一个数组
  * 参    数:Array 要发送数组的首地址
  * 参    数:Length 要发送数组的长度
  * 返 回 值:无
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:串口发送一个字符串
  * 参    数:String 要发送字符串的首地址
  * 返 回 值:无
  */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
	{
		Serial_SendByte(String[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:次方函数(内部使用)
  * 返 回 值:返回值等于X的Y次方
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;	//设置结果初值为1
	while (Y --)			//执行Y次
	{
		Result *= X;		//将X累乘到结果
	}
	return Result;
}

/**
  * 函    数:串口发送数字
  * 参    数:Number 要发送的数字,范围:0~4294967295
  * 参    数:Length 要发送数字的长度,范围:0~10
  * 返 回 值:无
  */
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i ++)		//根据数字长度遍历数字的每一位
	{
		Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');	//依次调用Serial_SendByte发送每位数字
	}
}

/**
  * 函    数:使用printf需要重定向的底层函数
  * 参    数:保持原始格式即可,无需变动
  * 返 回 值:保持原始格式即可,无需变动
  */
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
	return ch;
}

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串
  * 参    数:... 可变的参数列表
  * 返 回 值:无
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				//定义字符数组
	va_list arg;					//定义可变参数列表数据类型的变量arg
	va_start(arg, format);			//从format开始,接收参数列表到arg变量
	vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中
	va_end(arg);					//结束变量arg
	Serial_SendString(String);		//串口发送字符数组(字符串)
}

/**
  * 函    数:串口发送数据包
  * 参    数:无
  * 返 回 值:无
  * 说    明:调用此函数后,Serial_TxPacket数组的内容将加上包头(FF)包尾(FE)后,作为数据包发送出去
  */
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}

/**
  * 函    数:获取串口接收数据包标志位
  * 参    数:无
  * 返 回 值:串口接收数据包标志位,范围:0~1,接收到数据包后,标志位置1,读取后标志位自动清零
  */
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)			//如果标志位为1
	{
		Serial_RxFlag = 0;
		return 1;					//则返回1,并自动清零标志位
	}
	return 0;						//如果标志位为0,则返回0
}

/**
  * 函    数:USART1中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void USART1_IRQHandler(void) __attribute__((interrupt()));
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;		//定义表示当前状态机状态的静态变量
	static uint8_t pRxPacket = 0;	//定义表示当前接收数据位置的静态变量
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)		//判断是否是USART1的接收事件触发的中断
	{
		uint8_t RxData = USART_ReceiveData(USART1);				//读取数据寄存器,存放在接收的数据变量
		
		/*使用状态机的思路,依次处理数据包的不同部分*/
		
		/*当前状态为0,接收数据包包头*/
		if (RxState == 0)
		{
			if (RxData == 0xFF)			//如果数据确实是包头
			{
				RxState = 1;			//置下一个状态
				pRxPacket = 0;			//数据包的位置归零
			}
		}
		/*当前状态为1,接收数据包数据*/
		else if (RxState == 1)
		{
			Serial_RxPacket[pRxPacket] = RxData;	//将数据存入数据包数组的指定位置
			pRxPacket ++;				//数据包的位置自增
			if (pRxPacket >= 4)			//如果收够4个数据
			{
				RxState = 2;			//置下一个状态
			}
		}
		/*当前状态为2,接收数据包包尾*/
		else if (RxState == 2)
		{
			if (RxData == 0xFE)			//如果数据确实是包尾部
			{
				RxState = 0;			//状态归0
				Serial_RxFlag = 1;		//接收数据包标志位置1,成功接收一个数据包
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);		//清除标志位
	}
}

Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H

#include <stdio.h>

extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);

void Serial_SendPacket(void);
uint8_t Serial_GetRxFlag(void);

#endif

Key.c

#include "ch32v30x.h"
#include "debug.h"
#include "LED.h"

/**
  * 函    数:按键初始化
  * 参    数:无
  * 返 回 值:无
  */
void Key_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		//开启GPIOA的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		//开启AFIO的时钟,外部中断必须开启AFIO的时钟

	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);						//将PA2引脚初始化为上拉输入

	/*AFIO选择中断引脚*/
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource2);//将外部中断的14号线映射到GPIOB,即选择PA2为外部中断引脚

	/*EXTI初始化*/
	EXTI_InitTypeDef EXTI_InitStructure;						//定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line2;					//选择配置外部中断的14号线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;					//指定外部中断线使能
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;			//指定外部中断线为中断模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;		//指定外部中断线为下降沿触发
	EXTI_Init(&EXTI_InitStructure);								//将结构体变量交给EXTI_Init,配置EXTI外设
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;			//选择配置NVIC的EXTI0线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设

}

/**
  * 函    数:EXTI2外部中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
uint8_t Key_Flag;
void EXTI2_IRQHandler(void) __attribute__((interrupt()));
void EXTI2_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line2) == SET)		//判断是否是外部中断2号线触发的中断
	{
		Key_Flag = 1;
		EXTI_ClearITPendingBit(EXTI_Line2);			//清除外部中断2号线的中断标志位
													//中断标志位必须清除
													//否则中断将连续不断地触发,导致主程序卡死
	}
}

Key.h

#ifndef __KEY_H
#define __KEY_H

extern uint8_t Key_Flag;
void Key_Init(void);

#endif

LED.c / LED.h

        LED模块代码与 第三章 "CH32C307-通用模块" 的模块代码一致。

OLED.c / OLED.h / OLED_Font.h

        OLED模块代码与 第二章 "CH32V307-OLED驱动" 的模块代码一致。

四、实验现象

        依旧老样子 先编译-后连线-再上电-才烧录 的顺序来操作,要把板子当宝贝。

        然后电脑上打开串口助手软件,波特率设置9600,收发都为HEX模式,选择串口号后打开串口。我这里用的是江协科技的软件,其他串口助手也都是一样的效果。

        此时按下USER按键,LED灯(蓝灯)亮起,电脑串口助手收到 "FF 02 03 04 05 FE" ,OLED屏幕上"TxPacket"下方显示"02 03 04 05"。把接收区的数据复制发送,能看到LED熄灭,且OLED屏幕上"RxPacket"下方显示对应数据,FF与FE为包首包尾,不可修改,中间的数据可以随意修改。


下节预告

        下一节我们讲解手动封装HEX数据包发送与接收的函数,过一遍手打代码的过程可以加深对通信协议的理解。加油!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐