江科大无限透传模块程序编写HC-40,无线调节PID,各种参数。
本文介绍了基于STM32的蓝牙串口通信程序实现过程。主要内容包括:1.硬件连接采用蓝牙模块替代USB转串口;2.程序实现了串口数据收发、OLED显示、按键控制、滑杆和摇杆数据处理等功能;3.详细讲解了数据包格式修改、字符串分割(strtok)、比较(strcmp)和数值转换(atoi/atof)等关键函数的使用;4.提供了完整的main.c和serial.c程序代码,实现了蓝牙指令解析、数据显示和
上次我们学习完了基本的知识,这次我们来实现程序的编写。文章最后我会放上所有的程序,
一 接线图

这是我们的接线图。只需要把USB转串口换为蓝牙模块就可以了。
二 编写准备
1.编程准备
![]()
我们复制一下串口收发数据包的文件。

原来的程序是收到文本@LED_ON等就会打开。
我们先测试一下

在手机端输入@LED_ON灯就会打开,说明我们的蓝牙模块没有问题。

我们可以设置按键的自锁模式,按下发送@LED_ON开灯,松开发送@LED_OFF关灯。

我们首先需要一个对printf的重定向,让他把数据打印在串口。

我们先测试一下OLED等功能是否正常。

下载程序之后我们看见显示屏正常。

然后我们可以用这种办法显示一下变量。表示变量占据3个字符宽度,如果变量不满足3个字符宽度就高位补0。

可以看到屏幕上也是可以显示出来。
2.测试绘图功能
![]()
引用一个math.h的头文件。
![]()
定义一个全局变量。

之后再主循环中,不断刷新显示变量。

就可以看到正弦曲线已经显示出来了。

我们再来测试一下cos的波形。

可以看到同时显示了sinx和cosx两个波形。
![]()

3.修改包头包尾
我们直接让包头@变为我们的 [ 。包尾改用 ] 代表我们数据包结束。

之后把if里面的逻辑改写为结束的逻辑。

之后后面的else if直接删除就可以了。这就可以接收 [ 起始 ] 结束的数据包了。我们测试一下。
4.测试一下

if里面的逻辑先删除。现在的逻辑是将数据包的内容,显示再oled的最后一行。然后编译下载

我们可以看到单片机的OLED同步收到了数据。
三.程序编写
1.函数了解
我们需要用到三个函数,第一个是strtok,可以实现用指定字符分割字符串。
第二个是strcmp可以比对两个字符串是否相等。
第三个是atoi或者atof可以把字符串形式的数值转化为整型或者浮点型。
首先收到字符串后先调用strtok函数,这是string.h库里面的,我们还需要添加库。
![]()
strtok函数第一个参数给待分割的字符串,这里是我们接收字符的函数。
![]()
第二个参数指定用什么字符来分割字符串,我们要用逗号分割,这里注意需要加双引号。
![]()
分割后的返回值用char*接收。

这就是分割得到的第一个子串,如果我们再次调用就会得到第二个字串,第三个子串,但是我们只需要再第一次分割时候,传入待分割的字符串,后续分割时这个参数要个NULL空的,表示这是前面的后续分割,而不是开启一个新的分割。如果分割到结束,没有新的子串了,那么函数就会返回空指针。
2.测试一下

我们在发送区发送一个字符串,可以看到接受区收到了3个子串。
3.按键程序编写

我们可以判断一下,按键1的松开事件

之后我们加入打印测试一下,大家也可以自己加入自己的操作。
4.测试按键

我们先在小程序配置一下1这个按键。

按键2也是同理。

可以看到,按下按键2的瞬间,单片机显示没有问题。可以收到数据,在松开按键1的瞬间单片机也可以收到数据。
5.解析滑杆数据包

现在就可以测试了

滑动滑杆2就可以看到数值得变化。
但是现在都还是字符串数值,我们要变为真正的数值。
6.数值转换

因为atoi是strlib.h库里面的,我们还需要调用一下库。
![]()
后面我们就可以用%d和intvalue来显示了。这就是我们得滑杆得值。

转换值是小数得话就用这个。
7.测试一下

滑杆2测试,发现是小数,而且显示没有问题。

如果在程序中想要实时修改哪两个参数,就可以把这两个参数赋值给对应的参数就可以了。如果修改了滑杆得名称,就修改name比对的字符串就可以了。
8.摇杆程序编写

解析之后就得到了摇杆值,后续就可以利用这个值去操作遥控小车。
9.摇杆测试

经过测试可以看到我们得值在上面打印出来了。滑动摇杆,数值也会变化。
三.所有程序
1.main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include <string.h>
//使用转换函数得头文件
#include <stdlib.h>
//测试绘图
#include <math.h>
uint8_t a;
float x,y1,y2;
int main(void)
{
OLED_Init();
Serial_Init();
LED_Init();
//显示字符
printf("[display, 0, 0, hello word]");
//显示变量,表示变量占据3个字符宽度,如果变量不满足3个字符宽度就高位补0
printf("[display, 0, 0, a:%03d]", a);
OLED_ShowString(1, 1,"TxPacket");//显示发送
OLED_ShowString(1, 1,"RxPacket");//显示接受
while(1)
{
//测试绘图
x += 0.1;
y1 = sin(x);
y2 = cos(x);
printf("[plot,%f,%f]", y1, y2);
//标志位为1代表接收到了数据
if(Serial_RxFlag == 1)
{
//先清除一下第四行,防止数据过长
OLED_ShowString(4, 1, " ");
OLED_ShowString(4, 1, Serial_RxPacket);
//三个字符串函数strtok分割字符串,strcmp字符串比角atoi和atof字符串数据转整型或浮点型
//第一个参数给要分割的字符串 第二个参数指定用什么字符来分割字符串
// 我们要用逗号分割,这里注意需要加双引号。
//分割后的返回值用char*接收
// char* substr1 = strtok(Serial_RxPacket, ",");
// char* substr2 = strtok(NULL, ",");
// char* substr3 = strtok(NULL, ",");
//第一个子串是数据包标识符,我们改革名字
char* tag = strtok(Serial_RxPacket, ",");
if(strcmp(tag,"key") == 0)//判断数据包是不是等于key,key就是按键数据包
{
//分割字符串
char* name = strtok(NULL, ",");
char* action = strtok(NULL, ",");
//执行名称为1的按键松开事件
if((strcmp(name, "1") == 0) && (strcmp(action, "up") == 0))
{
printf("key, 1, up\r\n");
}
else if((strcmp(name, "2") == 0) && (strcmp(action, "down") == 0))
{
printf("key, 1, down\r\n");
}
}
//解析滑杆数据包
else if(strcmp(tag, "slider") == 0)
{
//分割字符串
char* name = strtok(NULL, ",");
char* value = strtok(NULL, ",");
//检查是不是收到了名称为1的滑杆数据包
if(strcmp(name, "1") == 0)
{
//现在得value是字符串,我们要转化为数值形式
uint8_t intvalue = atoi(value);
printf("slider, 1, %d\r\n", intvalue);
}
//检查是不是第二个
else if(strcmp(name, "1") == 0)
{
//如果转换值是小数,用这个
float floatvalue = atof(value);
printf("slider, 2, %.f\r\n", floatvalue);
}
}
//解析摇杆数据包
else if(strcmp(tag, "joystick") == 0)
{
//左摇杆横向值
int8_t LH = atoi(strtok(NULL, ","));
//左摇杆纵向值
int8_t LV = atoi(strtok(NULL, ","));
//右摇杆横向值
int8_t RH = atoi(strtok(NULL, ","));
//右摇杆纵向值
int8_t RV = atoi(strtok(NULL, ","));
//打印测试
printf("joystick, %d, %d, %d, %d\r\n", LH, LV, RH, RV);
}
// //打印测试,看看是否分割完成
// printf("%s\r\n", substr1);
// printf("%s\r\n", substr2);
// printf("%s\r\n", substr3);
//标志位清0再程序结束之后
Serial_RxFlag = 0;
}
}
}
2.serial.c
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "stdarg.h"
//这四个数据只存放用来发送和接受的载荷数据,包头,包尾就不存了。
char Serial_RxPacket[100];//接收缓存字符数组
uint8_t Serial_RxFlag;
void Serial_Init(void)
{
//开启USART的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
//开启GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//初始化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;
//这里就是把PA9设置为复用推挽输出供给USART的TX使用
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
//这里就是把PA9设置为复用推挽输出供给USART的TX使用
GPIO_Init(GPIOA, &GPIO_InitStructure);
//初始化USART
USART_InitTypeDef USART_InitStructcture;
//波特率,直接写个参数init函数会自动计算。
USART_InitStructcture.USART_BaudRate = 9600;
//硬件流控制选择无
USART_InitStructcture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
//USART模式选择发送模式
USART_InitStructcture.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
//校验位
USART_InitStructcture.USART_Parity = USART_Parity_No;
//停止位
USART_InitStructcture.USART_StopBits = USART_StopBits_1;
//字长
USART_InitStructcture.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructcture);
//开启中断 选择谁触发中断 开启
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//NVIC配置
//先进性NVIC的分组,因为只用分一次 2位抢占,2位响应
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//初始化NVIC
NVIC_InitTypeDef NVIC_InitStructure;
//中断通道
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
//中断使能,使能
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//中断优先级设置 先都给个1
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//启动USART1串口
USART_Cmd(USART1, ENABLE);
}
//发送数据函数
void Serial_SendByte(uint8_t Byte)
{
//发送数据的函数
USART_SendData(USART1, Byte);
//等待发送完成
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
//发送数组函数 由于数组无法判断结束,需要加一个Length进来
void Serial_SendArray(uint8_t* Array, uint16_t Length)
{
uint16_t i;
for(i=0; i<Length; i++)
{
Serial_SendByte(Array[i]);
}
}
//封装字符串 由于字符串自带一个结束标志位,所以就不需要进行长度判断了。
void Serial_SendString(char* String)
{
uint8_t i;
//字符串的本质就是一个带\0的数组。
for(i=0; String[i] != 0;i++)
{
Serial_SendByte(String[i]);
}
}
//次方函数
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while(Y--)//循环Y次
{
Result *= X;//RESULT累乘y次的x,就是x的y次方
}
return Result;
}
//封装显示数字
void Serial_SendNum(uint32_t Number, uint8_t Length)
{
//我们需要把数字的个位,十位,百位拆开,然后转换成对应的字符数据依次发送出去。
uint8_t i;
for(i=0; i<Length; i++)
{
//10的0次方是个位,所以要反过来。
//假如是2位数,length为2,第一次循环length-i-1=2-0-1=1,就是发送十位,10的1次方。
//第二次循环就是2-1-1=0,也就是10的0次方,个位。
//最终要以字符的形式显示,所以我们要加一个偏移。
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + 0x30);
}
}
//重写fputc函数
//int fputc(int ch, FILE* f)
//{
// Serial_SendByte(ch);
// return ch;
//}
//对sprintf函数进行封装 第一个参数用来接收格式化字符串
void Serial_printf(char* format, ...)
// 第二个参数加三个.用于接收可变参数列表
{
//定义输出的字符串
char string[100];
//va_list是类型名 arg是变量名
va_list arg;//定义一个参数列表变量
//从format开始接收参数表,放在arg里面
va_start(arg, format);
//之后sprinf打印位置是string
//这里sprinf要改为 vsprintf因为sprintf只能接收直接写的参数
//对于封装方式,只能用vsprintf
vsprintf(string, format, arg);//格式化字符串是format,参数表是arg
//释放参数表
va_end(arg);
//把string发送出去
Serial_SendString(string);
}
//中断函数直接在启动文件复制
void USART1_IRQHandler(void)
{
//首先要定一个标志当前状态的变量S,我们可以在中断里面定义一个静态变量。
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;//指示接收到哪一个数据了
//先判断标志位
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
//获取一下RXData,读取寄存器里面的值
uint8_t RxData = USART_ReceiveData(USART1);
//根据RxState不同我们要进入不同的函数
//进入等待包头程序
if(RxState == 0)
{
//因为我们用@当包头
if((RxData == '[') && (Serial_RxFlag == 0))//如果收到了包头
{
//为什么不读寄存器第一个数值?因为寄存器一次只能存储一个字节数据
RxState = 1;//让状态变为1,可以进行下一步了,如果不等于,就不转运状态了
pRxPacket = 0;//清除一下接收计数,为下次接收做准备
}
}
//进入接受数据状态
else if(RxState == 1)
{
//因为我们是不固定的长度,所以每次都要判断是不是为包尾
if(RxData == ']')
{
RxState = 0;//是包尾的话就回到第一个状态。
//还需要对字符数据加一个字符串结束标志位\0方便我们后续对字符串进行处理
Serial_RxPacket[pRxPacket] = '\0';
Serial_RxFlag = 1;//代表接收到一组完整的数据了,置一个标志位。
}
else//如果不是包尾才需要接收数据
{
//这里需要依次接受四个数组,存在RxPack数组里,所以需要一个变量用来记录接受几个数据
//这里数组的第pRxPacket数是RxData,就是我们寄存器的值。
//这就是每进入一次接收状态,数据就转运一次缓存数组,同时存位置++
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket++;//移动到下一个位置
}
}
//清除标志位,不影响函数
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
3.serial.h
#ifndef __SERIAL_
#define __SERIAL_
#include <stdio.h>
extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;
void Serial_SendByte(uint8_t Byte);
void Serial_Init(void);
void Serial_SendArray(uint8_t* Array, uint16_t Length);
void Serial_SendString(char* String);
void Serial_SendNum(uint32_t Number, uint8_t Length);
void Serial_printf(char* format, ...);
uint8_t Serial_GetFlag(void);
void Serial_SendPacket(void);
#endif
更多推荐


所有评论(0)