本章主要是将STM32的串口收发文本数据包的程序移植到CH32上来,跟收发HEX数据包是基本一样的,程序基本都适配,只需改动两个地方,第一处是串口中断函数前添加特殊声明。

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

        第二处是在主函数中添加一点点的延时,或者在while循环里随便调用OLED显示一些东西,总之让MCU随便干点小活,否则MCU频繁访问串口中断标志位程序就会跑飞卡死。

Delay_Us(1);
// OLED_ShowString(4, 1, Serial_RxPacket);

一、完整代码移植

        前面的原理部分就不再赘述,我们直接开始移植代码。

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

        老样子新建工程 "4-2_USART-TXT" ,把之前 "3-1_EXTI_Key_Led" 的Hardware文件夹复制到本节课的工程中来,再把江协科技的串口收发文本数据包工程中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.代码调整

        这部分主要是对代码进行与CH32的适配。

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

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

        因为这部分代码是纯软件通信之间的交互,所以主函数里面代码我们就不用去改动了。此时你可以试着烧录,你会发现收发功能完全无法使用。其实主程序是卡死了,可能是因为主程序频繁地访问USART硬件外设的标志位寄存器导致程序崩溃,也可能是MCU本身的问题。我们只需要在while循环里延时1us即可解决这个问题,在while循环里加上Delay_Us(1); 。当然如果你觉得这样阻塞了主程序的话也可以随便让OLED显示一些东西也行,甚至让主程序去访问我们的按键标志位也没问题。

main.c

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

uint16_t X;

int main(void)
{
	/*模块初始化*/
	Delay_Init();
	OLED_Init();		//OLED初始化
	LED_Init();			//LED初始化
	Serial_Init();		//串口初始化
	Key_Init();
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "TxPacket");
	OLED_ShowString(3, 1, "RxPacket");
	
	while (1)
	{
		//任意一种方法都可以
		Delay_Us(1);            //延时1us

		// OLED_ShowString(4, 1, Serial_RxPacket);    //OLED显示任意东西

		// if(Key_Flag == 1)    //访问按键标志位
		// {
		// 	Key_Flag = 0;
		// }
/////////////////////////////////////////////////////////////////////////////////////////
		Delay_Us(10);
		if (Serial_RxFlag == 1)		//如果接收到数据包
		{
			OLED_ShowString(4, 1, "                ");
			OLED_ShowString(4, 1, Serial_RxPacket);				//OLED清除指定位置,并显示接收到的数据包
			
			/*将收到的数据包与预设的指令对比,以此决定将要执行的操作*/
			if (strcmp(Serial_RxPacket, "LED_ON") == 0)			//如果收到LED_ON指令
			{
				LED1_ON();										//点亮LED
				Serial_SendString("LED_ON_OK\r\n");				//串口回传一个字符串LED_ON_OK
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "LED_ON_OK");				//OLED清除指定位置,并显示LED_ON_OK
			}
			else if (strcmp(Serial_RxPacket, "LED_OFF") == 0)	//如果收到LED_OFF指令
			{
				LED1_OFF();										//熄灭LED
				Serial_SendString("LED_OFF_OK\r\n");			//串口回传一个字符串LED_OFF_OK
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "LED_OFF_OK");			//OLED清除指定位置,并显示LED_OFF_OK
			}
			else						//上述所有条件均不满足,即收到了未知指令
			{
				Serial_SendString("ERROR_COMMAND\r\n");			//串口回传一个字符串ERROR_COMMAND
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "ERROR_COMMAND");			//OLED清除指定位置,并显示ERROR_COMMAND
			}
			
			Serial_RxFlag = 0;			//处理完成后,需要将接收数据包标志位清零,否则将无法接收后续数据包
		}
	}
}

二、完整代码

Serial.c

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

char Serial_RxPacket[100];				//定义接收数据包数组,数据包格式"@MSG\r\n"
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);		//串口发送字符数组(字符串)
}

/**
  * 函    数: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 == '@' && Serial_RxFlag == 0)		//如果数据确实是包头,并且上一个数据包已处理完毕
			{
				RxState = 1;			//置下一个状态
				pRxPacket = 0;			//数据包的位置归零
			}
		}
		/*当前状态为1,接收数据包数据,同时判断是否接收到了第一个包尾*/
		else if (RxState == 1)
		{
			if (RxData == '\r')			//如果收到第一个包尾
			{
				RxState = 2;			//置下一个状态
			}
			else						//接收到了正常的数据
			{
				Serial_RxPacket[pRxPacket] = RxData;		//将数据存入数据包数组的指定位置
				pRxPacket ++;			//数据包的位置自增
			}
		}
		/*当前状态为2,接收数据包第二个包尾*/
		else if (RxState == 2)
		{
			if (RxData == '\n')			//如果收到第二个包尾
			{
				RxState = 0;			//状态归0
				Serial_RxPacket[pRxPacket] = '\0';			//将收到的字符数据包添加一个字符串结束标志
				Serial_RxFlag = 1;		//接收数据包标志位置1,成功接收一个数据包
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);		//清除标志位
	}
}

Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H
#include "ch32v30x.h"

#include <stdio.h>

extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;

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, ...);

#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 = 2;			//指定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,收发都为文本模式,选择串口号后打开串口。我这里用的是江协科技的软件,其他串口助手也都是一样的效果。

        此时在发送区输入"@LED_ON"+回车换行,注意一定要按回车换行,然后点击发送。OLED屏幕上"TxPacket"下方显示"LED_ON_OK","RxPacket"下方显示"LED_ON",电脑接收区显示"LED_ON_OK",板子上LED1(蓝灯)亮起;

        在发送区输入"@LED_OFF"+回车换行,注意一定要按回车换行,然后点击发送。OLED屏幕上"TxPacket"下方显示"LED_OFF_OK","RxPacket"下方显示"LED_OFF",电脑接收区显示"LED_OFF_OK",板子上LED1(蓝灯)熄灭。


下节预告

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

Logo

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

更多推荐