硬件介绍
蓝牙模块HC-01,其实之前就用过,使用起来非常简单
继电器模块, (VCC 3.3V)当左侧IN输入低电平时,右侧的ON 和 COM会导通,左上的绿灯会亮,此处充当插座的角色
项目需求
通过蓝牙的串口发送open打开继电器,输入close关闭继电器
项目接线
将HC-01 的 TXD和RXD 分别接到单片机的RX1和TX1;
将继电器的 IN 接入单片机的PB5;(电源接3.3V)
蓝牙模块波特率的修改
由于之前的波特率和现在不同,因此需要先将蓝牙模块接上CH340接到电脑上,使用AT指令修改波特率:
先将串口助手的波特率调回9600,然后发送AT指令:
AT+BAUD=115200,0(这里我淘宝了HC01的介绍)
设置完成!然后再将蓝牙插回单片机!
使用非中断的方式实现
CubeMX
惯例配置+上节的串口配置(此时不用打开中断)+配置GPIO口
Keil
还是先打开Micro-lib,这样才能重写printf函数
#include "stdio.h"
#include "string.h"
int fputc(int a, FILE *f) //一个字符一个字符发送
{
unsigned char temp[1] = {a};
HAL_UART_Transmit(&huart1, temp, 1, 0xffff);
return a;
}
int main(void)
{
unsigned char buff[20] = {0};
while (1)
{
HAL_UART_Receive(&huart1, buff, 19, 100);//19是因为我定义了20位字节的缓冲区,但实际字符串的发送结束会有\0,所以要预留一位,也就是说最多接收19个字符
if(buff[0] == 'o' && buff[1] == 'p'){ //open
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET); //导通继电器
}
if(buff[0] == 'c' && buff[1] == 'l'){ //close
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET); //断开继电器
}
memset(buff,0,strlen(buff));
printf("mjm bibi\r\n"); //心跳包
}
}
实现效果
可见,在蓝牙端可以不断收到心跳包的信息,并且输入open可以打开继电器(close未演示)
使用中断的方式实现
CubeMX
再刚刚的工程文件的基础上,在NVIC打开中断就可以:
Keil
还是先打开Micro-lib,这样才能重写printf函数
在这次的代码中,对于接收的串口的指令的代码也可以优化一下,因为之前在做89C52的时候,判断命令都是判断前几个字符是啥,例如open就判断第一个字符是o,第二个字符是p,这种方法在指令少的时候比较管用,但是如果指令多了很容易产生歧义。
而现在,有了流程式的代码,尤其是使用中断的串口时,可以一次接收到完整的指令,这就使得可以对指令的判断使用字符串比较函数strcmp,但是定义的存放数据的是一个字符串变量,因此,只需要对其进行一个强制的转换,使其变成字符串常量,并使用字符串比较,就可以实现精准的命令判断!
#include "stdio.h"
#define UART1_REC_LEN 200 //定义最大接收字节数 200,可根据需求调整
uint8_t buf=0; //串口接收缓存(1字节)
uint8_t UART1_RX_Buffer[UART1_REC_LEN];//接收缓冲, 串口接收到的数据放在这个数组里,最大UART1_REC_LEN个字节
uint16_t UART1_RX_STA=0; //bit15,接收完成标志 //bit14,接收到0x0d //bit13~0,接收到的有效字节数目
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) // 接收完成回调函数,每收到一个字符(字节)后,就会在这里处理
{
if(huart->Instance == USART1){ // 判断中断是由哪个串口触发的
if((UART1_RX_STA & 0x8000) == 0){ // 判断接收是否完成(UART1_RX_STA bit15 位是否为1)
if(UART1_RX_STA & 0x4000){ // 如果已经收到了 0x0d (回车)
// 则接着判断是否收到 0x0a (换行)
if(buf == 0x0a){ // 如果 0x0a 和 0x0d 都收到,
UART1_RX_STA |= 0x8000; //则将 bit15 位 置为1
}else{ // 否则认为接收错误,重新开始
UART1_RX_STA = 0;
}
}else{ // 如果没有收到 0x0d (回车)
//则先判断收到的这个字符是否是 0x0d (回车)
if(buf == 0x0d){ // 是的话则将 bit14 位置为1
UART1_RX_STA |= 0x4000;
}else{ // 否则将接收到的数据保存在缓存数组里
UART1_RX_Buffer[UART1_RX_STA & 0X3FFF] = buf; //因为UART1_RX_STA只有前14位为有效数据,所以缓存数组UART1_RX_Buffer[X]中的X作为16位的二进制数,最高两位的判断应该写在前面代码的判断中,在此处不用
UART1_RX_STA++;
if(UART1_RX_STA > UART1_REC_LEN - 1){ //如果接收数据大于UART1_REC_LEN(200字节),则重新开始接收
UART1_RX_STA = 0;
}
}
}
}
HAL_UART_Receive_IT(&huart1, &buf, 1); // 重新开启中断
}
}
int fputc(int a, FILE *f) //一个字符一个字符发送
{
unsigned char temp[1] = {a};
HAL_UART_Transmit(&huart1, temp, 1, 0xffff);
return a;
}
int main(void)
{
HAL_UART_Receive_IT(&huart1, &buf, 1);//开启中断,并把数据存到buf里
while (1)
{
if(UART1_RX_STA & 0x8000){//不断的判断串口是否接收完成
if(!strcmp((const char *)UART1_RX_Buffer,"open")){ //如果strcmp返回0,则说明指令是open
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET); //导通继电器
}
if(!strcmp((const char *)UART1_RX_Buffer,"close")){ //如果strcmp返回0,则说明指令是close
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET); //断开继电器
}
printf("收到数据:");
HAL_UART_Transmit(&huart1, UART1_RX_Buffer, UART1_RX_STA & 0x3fff, 0xffff);// 将收到的数据发送到串口(此处也可以使用printf)
while(huart1.gState != HAL_UART_STATE_READY);// 等待发送完成
printf("\r\n");
memset(UART1_RX_Buffer,0,strlen(UART1_RX_Buffer)); //这句话得加,不然strcmp的判断方法就不知道为啥会不行
UART1_RX_STA = 0;// 重新开始下一次接收
}
printf("hello mjm\r\n"); //心跳包
HAL_Delay(1000);
}
}
实现效果
同时!虽然在WINDOWS中的串口中,换行符是“\r\n",但是在ios系统中的串口中,换行符似乎有所区别,这个问题之前就遇到过,但是由于之前51/52的代码中,是直接通过字符串的比对来接收串口的,修改很容易,但是对于STM32来说,尤其是串口中断的处理程序,比较固定,所以解决办法就是直接拿出我的安卓备用机,下载一下蓝牙的APP= =
在APP中选择“发送新行“,并自定义两个按钮:
可见,在不断收到间隔1秒的心跳包的同时,只要发送open,就会打开继电器,并收到单片机的串口回传消息;发送close就会立刻关闭继电器,基本实现了蓝牙遥控的插座!
同时,注意到上图中,指令的发送到继电器的响应其实有一点的延时,而我发现这个延迟是main函数中的HAL_Delay造成的,一开始我很奇怪,因为HAL_Delay调用的是滴答定时器,默认的优先级最低,那么串口发生中断的时候,照理说应该会立刻发生响应才对。
但是后来我发现了我的逻辑问题,串口中断的响应优先级的确远高于滴答定时器,但问题是我的继电器响应函数并没有写在串口的中断服务程序之中= =,而是写在了main函数里,那么加入HAL_Delay一秒,进行到半秒的时候来了串口中断,的确是会立刻跳转到串口中断,但是当执行完串口的中断处理函数之后,程序会回到滴答定时器,把剩下的半秒走完才会继续执行下面的代码。