屏幕使用的是淘晶池的串口屏,显示内容可以通过官方上位机来编辑,屏幕学习平台:http://wiki.tjc1688.com/doku.php?id=start
通信原理:屏幕里实际也是由单片机在驱动,在屏幕上的操作,最终会变成数据通过底层单片机的串口发送到我们的单片机串口上,就能控制自己的单片机做出相应操作,同时自己的单片机也能发送屏幕能识别的特殊指令到屏幕的单片机里,实现屏幕切换页面等操作
在官方提供的上位机软件里,新建一个工程后,会有个Program.s的文件,这是屏幕的系统文件,可以配置一些屏幕参数,比如背光亮度、通信波特率等
设置屏幕串口通信的波特率为115200,背光10,page 0表示上电显示第一个页面
这些参数可以在屏幕学习平台的系统变量指令查看到
上位机软件的使用类似安卓开发的Android Studio,也是可以通过拖控件来完成设计,可对控件的位置,属性和事件进行个性化编辑,类似于面向对象的设计方法,而且软件也是全中文,上手还是比较容易的
软件的使用方法可在官方学习平台上查看,这里记录几点比较重要的地方
要想在文本框或者按钮上显示文字,就要先创建一个字库,设置字体的编码格式,大小,是指定内容还是所有字符,因为在控件属性里没有设置文字大小的选项,也没有编码选项,所以要制作自己想要的字库
点击工具,字库制作
选择字高,编码格式,字体,范围有ASCII字符,所有字符和指定字符,所有字符因为包含的字符多,想到什么字就可以写什么字,所以最后编译出来的文件就大,烧录到屏幕上时就比较费时间,这次实验使用指定字符,选择指定字符后在输入框里写入想要的字符,后面的控件就能使用这些字符,没有写到的则不能使用,比如指定字符没有“串口”两个字,则控件文本就不能写“串口"两个字
生成字库后导入到软件中,后续的控件就可以根据字库ID号来选择合适大小的文字并显示
按钮需要编写事件才能发挥作用,比如下面的main主界面(id:0),有文本框,图片,两个按钮
点击“数码管”按钮后,在下方会显示事件,按下事件是按下瞬间触发,弹起事件是按下后按键恢复原样瞬间触发,一个按键动作就是先按下,再弹起;需要勾选发送键值,键值信息会在后面调试时显示,同时也可以通过串口发送给单片机处理,page 1表示跳转到页面1(id:1),当前页面id是0,页面1就是数码管显示页面
创建三个页面,page 0则跳转到main主页面,page 1则跳转到Display数码管页面,page 2则跳转到Motor单极性步进电机页面
这是比较重要的一步,在设计好页面后,如果不测试就烧录到屏幕中,则可能会出现按钮按下了没有执行跳转命令的情况,又需要改动重新烧录,浪费时间;所以软件有个调试功能,所见即所得,在调试页面可以实现按键跳转页面,更改控件值等功能,也可以通过编写指令控制按钮按下弹起,这些指令后续就可以通过单片机来发送;调试出来的效果什么样,烧录到屏幕中就是什么样
点击上方工具栏的调试按钮,则进入调试页面,调试前最好编译一下,保证页面控件的属性和事件代码没有写错
进入调试页面,如果点击主页面的“数码管”按钮,则会跳转到数码管页面
在模拟器返回数据框内可以看到按下“数码管”按钮后的键值信息,这是因为在按钮事件中勾选了发送键值才会有数据返回,不勾选则没有数据,填写page 1跳转指令,则在调试中按下按钮就会跳转到数码管页面,没写则只会返回键值数据,页面不会跳转
数据格式:
每一次按钮的键值信息都是7个字节数据,首字节0x65是固定的,是协议头,说明从0x65起后面的6个字节数据都是有效数据;
第二个字节0x00表示的是页面id,每个页面创建时都有id,0x00表示页面0,如果是0x01则表示页面1,指示的是哪个页面的按钮控件被触发;
第三个字节0x01表示是控件1触发的事件,在“数码管”按钮属性中可以看到id号,0x01就表示id为1的按钮触发了事件
第四个字节0x00表示按钮弹起,如果是0x01则标志按钮按下,一个按钮完成一次按键操作是先按下再弹起的
后面三个字节0xFF,0xFF,0xFF是结束符,表示一组通信数据的结束
指令输入:
在指令输入区输入点击“数码管”按钮指令,则“数码管”按钮会被按下再弹起,页面切换到页面1
click表示点击,b0是控件的名称,也可改为控件id,1是按下,0是弹起,发送这两个指令是则表示b0按钮先被按下,再弹起
这些指令就可以通过单片机来发送,用自己的单片机控制屏幕按钮,或者可以更改数字控件的值
分别配置数码管、按键、LED灯、步进电机引脚
因为需要串口与屏幕进行通信,所以需要初始化串口1,波特率要与屏幕的波特率一致
因为使用DMA来搬运屏幕发来的串口数据,所以要配置DMA,模式选择普通模式,内存选择地址增加
嵌套中断向量中,开启触摸按键的外部中断,DMA只是搬运数据,不需要通知CPU,所以不需要开启中断,串口需要开启中断,会用到空闲中断
代码有点多,记录些比较重要的
使用串口空闲中断+DMA的方法接收屏幕数据,这里使用宏定义判断IDLE标志位,清除标志位后再调用自己的空闲中断回调函数,也可以改为使用HAL库提供的HAL_UARTEx_ReceiveToIdle_DMA函数,DMA接收完数据后串口进入空闲状态,在HAL_UARTEx_RxEventCallback回调函数中处理接收到数据
因为使用了自己编写的函数,所以要引入自己的头文件MyApplication.h
...
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f1xx_it.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "MyApplication.h"
/* USER CODE END Includes */
...
/**
* @brief This function handles USART1 global interrupt.
*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
//检测到串口空闲中断标志位置位
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) != 0x00u)
{
//先清除标志位IDLE
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
//再调用自己写的串口空闲中断回调函数
HAL_UART_IdleCallback(&huart1);
}
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
单片机发送给屏幕的数据要以0xFF 0xFF 0xFF作为结束符,所以HMI_SendString函数通过串口发送数据后再调用HMI_SendEndData函数发送结束符
/*
* @name HMI_SendEndData
* @brief 往HMI屏发送结束符
* @param None
* @retval None
*/
static void HMI_SendEndData()
{
//通过串口发送0xFF 0xFF 0xFF结束符
HAL_UART_Transmit(&huart1,ucHMI_EndData,3,0x0A);
}
/*
* @name HMI_SendString
* @brief 往HMI屏发送数据
* @param pucStr:待发送的数据
* @retval None
*/
static void HMI_SendString(uint8_t* pucStr)
{
//通过串口往HMI屏幕发送数据
HAL_UART_Transmit(&huart1,pucStr,strlen((const char*)pucStr),0x0A);
//每一次发送数据,都要发送结束符
HMI_SendEndData();
}
HMI_Protocol函数会在HAL_UART_IdleCallback(&huart1)中调用,说明串口接收完一组数据,进入空闲中断后,则调用HMI_Protocol来处理数据
因为一组数据是以0x65开始的,所以要找出0x65,往后的就是正确的数据
处理数据就是根据屏幕的上位机软件调试页面的键值信息来判断哪个按钮被按下了,按下了就执行相应的操作
/*
* @name HMI_Protocol
* @brief HMI协议
* @param None
* @retval None
*/
static void HMI_Protocol()
{
uint8_t index = 0,i = 0;
uint8_t Temp_Array[7] = {0x00};
//串口1停止DMA接收
HAL_UART_DMAStop(&huart1);
//读取HMI缓存数据,共7个字节,起始值为0x65
for(i=0;i<7;i++)
{
//检测键值起始数据0x65
if(index == 0)
{
//从下标0开始找,如果不是0x65开头,则从该位往后的共7个字节的数据都不是正确数据,舍弃
if(*(HMI.pucRec_Buffer+i) != 0x65){continue;}
}
//如果找到0x65,则往后的7个字节数据是HMI传来的数据,则进行数据保存
Temp_Array[index] = *(HMI.pucRec_Buffer+i);
//如果已经读到7个字节数据,就退出循环
if(index == PROTOCOL_DATA_LEN)
{
break;
}
index++;
}
//串口1开启DMA接收
HAL_UART_Receive_DMA(&huart1,HMI.pucRec_Buffer,(uint16_t)20);
//处理数据
if(index == PROTOCOL_DATA_LEN)
{
//主页面的键值信息
if(Temp_Array[1] == 0x00)
{
//数码管按键弹起事件
//下标为3的数据表示按钮按下或弹起,按下:0x01,弹起:0x00
if(Temp_Array[2] == 0x01 && Temp_Array[3] == 0x00)
{
HMI.Page = Page_Display; //切换到数码管显示页面
Display.Disp_Clr(); //清除数码管
}
//步进电机按键弹起事件
if(Temp_Array[2] == 0x03 && Temp_Array[3] == 0x00)
{
HMI.Page = Page_Step_Motor; //切换到电机显示页面
Display.Disp_Clr(); //清除数码管
//数码管显示电机圈数和速度
//显示圈数
Display.Disp_Hex(Disp_NUM_6,Uniplar_Step_Motor.Circle,Disp_DP_OFF);
//显示速度
Display.Disp_Hex(Disp_NUM_1,0,Disp_DP_OFF);
}
}
...
不但屏幕上的按钮可以控制数码管和电机,板子上的触摸按键也能控制数码管和电机,同时,在触摸按键按下后屏幕的按钮也会显示被按下,这就需要用到一个标志位Page_Step_Motor_KEY_Flag,标志位默认为FALSE
因为触摸按键之前就实现了电机控制功能,所以屏幕按钮按下就调用触摸按键的外部中断函数即可,所以要加标志位判断是屏幕按钮按下的还是板子上的触摸按键按下的
如果是屏幕按键按下,则标志位置为TRUE,再调用外部中断回调函数控制电机,控制完后再将标志位清零,为下次按钮准备
如果是触摸按键按下的,则执行控制电机函数,再判断该标志位是否为FALSE,如果是,则说屏幕按钮没按下,需要将按下按钮的指令发送到屏幕,同时控制屏幕按钮按下,如果标志位为TRUE,则说明屏幕按下了按钮,则不需要将控制指令发送到屏幕
HMI_Protocol()
//正反按钮弹起事件
if(Temp_Array[2] == 0x09 && Temp_Array[3] == 0x00)
{
//标志位置位
HMI.Page_Step_Motor_KEY_Flag = TRUE;
//调用外部中断回调函数,传入参数按键2,控制电机正反转
HAL_GPIO_EXTI_Callback(KEY2_Pin);
//标志位清零
HMI.Page_Step_Motor_KEY_Flag = FALSE;
}
HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
//如果按键2被按下
if(GPIO_Pin == KEY2_Pin)
{
LED.LED_Fun(LED2,LED_Flip);
Uniplar_Step_Motor.Direction_Adjust(); //控制步进电机正反转
if(HMI.Page_Step_Motor_KEY_Flag == FALSE)
{
HMI.HMI_SendString("click bt1,1");
HMI.HMI_SendString("click bt1,0");
}
}
Disp_Str数据内容就是发送给屏幕的控制指令,可将下标为1和7的内容改变,再发送到屏幕,以控制不同控制显示不同的内容
/*
* @name Error_Handler
* @brief 数码管显示页面
* @param None
* @retval None
*/
static void Fun_Page_Display()
{
uint32_t i = 0;
uint8_t Disp_Str[9] = {'n','0','.','v','a','l','=','0','\0'}; //字符串以'\0'结束
uint8_t Cnt_Arr[6] = {0x00};
Cnt_Arr[0] = Cnt%10; //个位
Cnt_Arr[1] = Cnt/10%10; //十位
Cnt_Arr[2] = Cnt/100%10; //百位
Cnt_Arr[3] = Cnt/1000%10; //千位
Cnt_Arr[4] = Cnt/10000%10; //万位
Cnt_Arr[5] = Cnt/100000%10; //十万位
//数码管显示计数值
Display.Disp_Hex(Disp_NUM_1,Cnt_Arr[0],Disp_DP_OFF);
Display.Disp_Hex(Disp_NUM_2,Cnt_Arr[1],Disp_DP_OFF);
Display.Disp_Hex(Disp_NUM_3,Cnt_Arr[2],Disp_DP_OFF);
Display.Disp_Hex(Disp_NUM_4,Cnt_Arr[3],Disp_DP_OFF);
Display.Disp_Hex(Disp_NUM_5,Cnt_Arr[4],Disp_DP_OFF);
Display.Disp_Hex(Disp_NUM_6,Cnt_Arr[5],Disp_DP_OFF);
//数据传给HMI显示屏
for(i=0;i<6;i++)
{
Disp_Str[1] = i + '0';
Disp_Str[7] = Cnt_Arr[i] + '0';
HMI.HMI_SendString(Disp_Str);
}
//更新计数值
if(++Cnt > 999999)
{
Cnt = 0;
}
//延时50ms
HAL_Delay(50);
}
主页面
数码管页面
步进电机页面