CH32V307 - USART串口收发文本数据包(第七章)
CH32的串口总是有问题?文本格式发送不了?我来教你
目录
本章重点
本章主要是将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数据包发送与接收的函数,过一遍手打代码的过程可以加深对通信协议的理解。加油!
更多推荐



所有评论(0)