本章的重点在于:DMA接收串口数据+串口接收空闲中断>>在空闲中断的回调函数里面就可以去处理解析数据,根据解析出来的数据,进行不同的操作。
关于串口最简单的最直接的最底层的操作就是将数据寄存器DR里面的数据的读出来(接收)或者往DR寄存器里面写入数据(发送),唯一要注意的就是时间点的问题,写入数据是在之前的数据已经被发送。而接收数据则是在上一次数据已经被读出来。so:软件的操作为:接收数据(读取DR寄存器的值,保存起来。)发送数据(往DR寄存器里面写入要发送的值)。就是这样简单。
stm32的外设的串口寄存器是带有空闲检测的,类似的,其余不同品牌的控制器假如有带有空闲检测的状态标志位,那么都可以写成串口接收空闲中断+DMA的实现。
那么问题有3个:
其一:什么是串口的空闲的概念?
串口的空闲字符被解释为:从下一个框架的起始位开始,到停止位全部为“1”表示为空闲字符。(参考stm32f10x官方手册An Idle character is interpreted as an entire frame of “1”s followed by the start bit of the next frame which contains data (The number of “1” ‘s will include the number of stop bits).)
其二:什么是空闲中断呢? 怎么样才会进入空闲中断呢?
空闲中断就是,串口外设里面的SR状态寄存器里面有一个IDLE的标志位,当这个IDLEIE=1的时候(满足允许发生空闲中断的条件),当RX上检测到了空闲帧才会使IDLE标志位为1,这时候会触发空闲中断。
重点来了:上面一行说当RX上检测到了空闲帧就会发生空闲中断是不完全正确的。直接点:最正确的说法是:先要在RX上接收到了数据以后并且又在RX上检测到了空闲帧。这样才会进入到空闲中断。(换而言之,只有当RX先工作起来再进入空闲状态,才会判定为进入到了空闲中断。)
在进入到了空闲中断以后:(其实不管进入什么中断以后),第一件要做的事情就是清除中断标志位,随后在进行中断程序的业务逻辑处理等等。
总结:怎么样才会进入空闲中断呢?答:只有RX先忙起来,在进入空闲状态,就会进入空闲中断。
其三:DMA是怎么样将DR(数据寄存器)里面RDR(接收数据寄存器)的数据搬走的呢?
简单来说DMA控制器可以直接访问内存单元,将指定位置的地址里面存入我们想要得到的在另外的一个内存单元里面保存的值即可。
显然,只有搞清楚以上的问题,才能对空闲中断有深刻的理解。
实践操作过程中遇到的问题:
1.如何使用C语言外加串口发送写出发送字符串的函数:
下列代码可供参考:
extern size_t strlen(const char *);
/*
* @name UART_HMI_Init
* @brief 串口屏初始化,使用到了串口发送字符串命令
* @param None
* @retval None
*/
static void UART_HMI_Init(void)
{
uint16_t i=1000;
HAL_UART_Transmit(&uart_hmi,(uint8_t *)HMI_Command_Init,sizeof(HMI_Command_Init),10);//发送HMI初始化的命令
Transmit_string_to_HMI((uint8_t *)"page main6");
while(i--)
{
UART_HMI_DisplayNUM(i--);
}
Transmit_string_to_HMI((uint8_t *)"page main1");
}
/*
* @name Transmit_string_to_HMI
* @brief 向HMI发送字符串
* @param string(无符号8位的指针变量等同于字符型指针变量) string 指 传入需要发送的字符串的首地址指针变量
* @retval None
* @note 这里使用了函数strlen,是用来计算指定字符串大小的函数。需要加入外部函数申明(在第一行解决警告问题)
* 在ASCII码中每个字符都是一个字节大小的,所以字符串也是由n个字符组成的n个字节。
*/
static void Transmit_string_to_HMI(uint8_t* string)
{
HAL_UART_Transmit(&uart_hmi,string,strlen((const char*)string),10);//发送HMI控件命令
HAL_UART_Transmit(&uart_hmi,(uint8_t *)Stop_HMI_Buffer,Stop_Buffer_Length,10);//发送HMI结束的命令
}
关于串口接收中断这里的部分是我重新写的:
使用的是空闲中断的方式:代码如下
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* 添加一个空闲中断,通过对应的判断标志位来识别
具体判断SR寄存器的IDLE位是否为1,如果满足IDLE=1,则进入空闲中断。
SR寄存器的地址为=0x40013800+0x00
DR寄存器的地址为=0x40013800+0x04
*/
uint32_t *pReg;
volatile uint32_t tmp=0;
pReg=(uint32_t *)(0x40013800+0x00);
if(Tools.Get_RnStatus(0x40013800+0x00,4)==1)
{
//清除空闲中断标志,通过软件序列来完成: It is cleared by a software sequence (an read to the USART_SR register followed by a read to the USART_DR register).
tmp=(*pReg); //读取SR
pReg=(uint32_t *)(0x40013800+0x04);
tmp=(*pReg); //读取DR
//已经满足空闲中断的条件,调用我们仿造HAL库定义的空闲中断回调函数。
Auqin_UARTEx_IDLECallback(&uart_hmi);
}
/* USER CODE END USART1_IRQn 0 */
// HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
在使用空闲中断的时候一定要记住打开空闲中断的使能,这样才可以在检测到接收数据之后的IDLE状态标志改变时进入中断。打开的中断的方式如下:
/*
* @name UART_HMI_Receive_Init
* @brief 做初始化处理,目的是启用MCU接收来自HMI屏幕的数据
* @brief 使用串口DMA接收方式实现
* @param None
* @retval None
*/
static void UART_HMI_Receive_Init(void)
{
HAL_UART_Receive_DMA(&uart_hmi,UART_HMI.pucReceive_Buffer,ucReceive_Buffer_Length);//串口USART1的接收方式,使用DMA的方式进行接收。
Tools.Write_RnStatus(0x40013800+0x0c,4,1);//打开USART1的IDLE中断。
}
/*
这里都使用到了一个小工具tools,我们将源码粘贴如下:
/*
* @name Get_RnStatus
* @brief 得到寄存器某位的状态 1or0。
* @param Raddr 寄存器里面的绝对地址,n 为这个寄存器的第n位。
* @retval int8_t 返回值为第N位的状态 1 or 0.
*/
static int8_t Get_RnStatus(uint32_t Raddr,uint8_t n)
{
uint32_t* RRaddr;
RRaddr=(uint32_t*)Raddr;
if((*RRaddr&(1<<n))==0)
return 0;
else
return 1;
}
/*
* @name Write_RnStatus
* @brief 写入寄存器第某位的状态 1 or 0。
* @param Raddr 寄存器里面的绝对地址, n 为这个寄存器的第n位 val 为要写入的值。
* @retval int8_t 返回值为第N位的状态 1 or 0.
*/
static void Write_RnStatus(uint32_t Raddr,uint8_t n,uint8_t val)
{
uint32_t* RRaddr;
RRaddr=(uint32_t*)Raddr;
if(1==val)
{
*RRaddr|=(1<<n);
}
else
{
*RRaddr&=~(1<<n);
}
}
这里的tools是一个结构体头文件如下:
//定义结构体类型
typedef struct
{
void(*Write_RnStatus)(uint32_t,uint8_t,uint8_t);
int8_t(*Get_RnStatus)(uint32_t,uint8_t);
} Tools_t;
/* extern variables-----------------------------------------------------------*/
extern Tools_t Tools;
以上的就是要特别注意的部分:在初始化函数里面内容如下:
static void Peripheral_Set()
{
Timer6.Timer6_Start_IT();
HAL_TIM_Base_Start_IT(&htim7);
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);
DisPlay.DisPlay_TM1620_Init();
// HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&NTC.usADC_Value,(uint32_t)1);
// DAC_Apply.DAC_Out_signal_wave();
HAL_Delay(1000);
UART_HMI.UART_HMI_Init();
UART_HMI.UART_HMI_Receive_Init();//串口屏接收初始化
}
以上就是使用串口时的技术难点与要点:
其余的有关模块HMI的头文件如下:
//定义枚举类型
typedef enum
{
Page_main1 = (uint8_t)0x01,
Page_main2 = (uint8_t)0x02,
Page_main3 = (uint8_t)0x03,
Page_main4 = (uint8_t)0x04,
Page_main5 = (uint8_t)0x05,
Page_main6 = (uint8_t)0x06,
}Page_num_t;
//定义结构体类型
typedef struct
{
uint8_t Page_num;//显示页面
uint8_t* pucReceive_Buffer;//存放接收缓存数组首地址的指针
void(*UART_HMI_Init)(void);//串口屏幕初始化
void(*Transmit_string_to_HMI)(uint8_t*);//发送字符串
void(*UART_HMI_Receive_Init)(void);//串口使用DMA接收方式实现
void(*UART_HMI_DisplayNUM)(uint32_t);//串口屏幕数码管显示
void(*Analyse_Receive_Data)(void);//接收数据解析
}UART_HMI_t;
/* extern variables-----------------------------------------------------------*/
extern UART_HMI_t UART_HMI;
/* extern function prototypes-------------------------------------------------*/
/********************************************************
End Of File
********************************************************/
c文件如下:
/* Includes ------------------------------------------------------------------*/
/* Private define-------------------------------------------------------------*/
/* Private variables----------------------------------------------------------*/
extern size_t strlen(const char *);
const uint8_t Stop_HMI_Buffer[3]={0xff ,0xff ,0xff};//HMI结束的命令
//关于串口屏TJC3224T124 来说,发送给他的命令采用字符的形式,具体参考官网http://www.tjc1688.com/
uint8_t Display_char[8]={'n','0','.','v','a','l','=','0'};//修改控件“n0.val=0”指令。
uint8_t HMI_Command_Init[4]={0x00,0xff,0xff,0xff};//上电后的初始化指令
uint8_t Receive_Buffer[20]={0x00};//定义一个接收数组,用来存放HMI发送来的数据指令。
/*每一个HMI发来的指令都是7个字节的*/
static void UART_HMI_Init(void);//串口屏幕初始化
static void Transmit_string_to_HMI(uint8_t*);//发送字符串
static void UART_HMI_Receive_Init(void);//串口DMA接收方式实现
static void UART_HMI_DisplayNUM(uint32_t);//串口屏幕数码管显示
static void Analyse_Receive_Dat(void);//接收数据解析
static void HMI_KEY1_Service(void);
static void HMI_KEY2_Service(void);
static void HMI_KEY3_Service(void);
static void HMI_KEY4_Service(void);
/* Public variables-----------------------------------------------------------*/
/* Private function prototypes------------------------------------------------*/
UART_HMI_t UART_HMI=
{
Page_main3,
Receive_Buffer,//将接收数据数组的首地址赋给指针变量
UART_HMI_Init,
Transmit_string_to_HMI,
UART_HMI_Receive_Init,
UART_HMI_DisplayNUM,
Analyse_Receive_Dat,
};
/*
* @name UART_HMI_Init
* @brief 串口屏初始化,使用到了串口发送字符串命令
* @param None
* @retval None
*/
static void UART_HMI_Init(void)
{
uint16_t i=1000;
HAL_UART_Transmit(&uart_hmi,(uint8_t *)HMI_Command_Init,sizeof(HMI_Command_Init),10);//发送HMI初始化的命令
Transmit_string_to_HMI((uint8_t *)"page main6");
while(i--)//开机倒计时界面的制作
{
UART_HMI_DisplayNUM(i--);
}
Transmit_string_to_HMI((uint8_t *)"page main1");
}
/*
* @name Transmit_string_to_HMI
* @brief 向HMI发送字符串
* @param string(无符号8位的指针变量等同于字符型指针变量) string 指 传入需要发送的字符串的首地址指针变量
* @retval None
* @note 这里使用了函数strlen,是用来计算指定字符串大小的函数。需要加入外部函数申明(在第一行解决警告问题)
* 在ASCII码中每个字符都是一个字节大小的,所以字符串也是由n个字符组成的n个字节。
*/
static void Transmit_string_to_HMI(uint8_t* string)
{
HAL_UART_Transmit(&uart_hmi,string,strlen((const char*)string),10);//发送HMI控件命令
HAL_UART_Transmit(&uart_hmi,(uint8_t *)Stop_HMI_Buffer,Stop_Buffer_Length,10);//发送HMI结束的命令
}
/*
* @name UART_HMI_Receive_Init
* @brief 做初始化处理,目的是启用MCU接收来自HMI屏幕的数据
* @brief 使用串口DMA接收方式实现
* @param None
* @retval None
*/
static void UART_HMI_Receive_Init(void)
{
HAL_UART_Receive_DMA(&uart_hmi,UART_HMI.pucReceive_Buffer,ucReceive_Buffer_Length);//串口USART1的接收方式,使用DMA的方式进行接收。
Tools.Write_RnStatus(0x40013800+0x0c,4,1);//打开USART1的IDLE中断。
}
/*
* @name UART_HMI_DisplayNUM
* @brief HMI的数码管实时显示数据(需要不断的调用来更新数据)
* @param DisplayNUM 需要显示的数字 range 【000 000 --999 999】 。
* @retval None
*/
static void UART_HMI_DisplayNUM(uint32_t DisplayNUM)
{
int8_t i=0;
for(i=0;i<6;i++)
{
Display_char[1]+=i;//选择处于不同位置的数字控件。从最右边第一个控件n0开始。
switch(i)//对每个位置要显示的数字控件的显示值进行分离。
{
case 0:Display_char[7]+=(uint8_t)(DisplayNUM%10);break;//将 要显示的数值进行分离,然后转换为ASCII码所对应的字符,EP: '5'='0'+5 ;
case 1:Display_char[7]+=(uint8_t)(DisplayNUM/10%10);break;
case 2:Display_char[7]+=(uint8_t)(DisplayNUM/100%10);break;
case 3:Display_char[7]+=(uint8_t)(DisplayNUM/1000%10);break;
case 4:Display_char[7]+=(uint8_t)(DisplayNUM/10000%10);break;
case 5:Display_char[7]+=(uint8_t)(DisplayNUM/100000);break;
default:System.Error_Handler();break;
}
HAL_UART_Transmit(&uart_hmi,(uint8_t *)Display_char,Command_Buffer_Length,10);//发送HMI改变数字控件的命令
HAL_UART_Transmit(&uart_hmi,(uint8_t *)Stop_HMI_Buffer,Stop_Buffer_Length,10);//发送HMI结束的命令
Display_char[1]='0';//将其改成起始状态,为下一次对应的位置做准备。
Display_char[7]='0';//将其改成起始状态,为下一次对应的数值做准备。
}
}
/*
* @name Analyse_Receive_Dat
* @brief 分析接收回来的数据,对用户发出的指令进行辨别。比如按键1被按下了,按键2被按下了。
* @param None
* @retval None
*/
static void Analyse_Receive_Dat(void)
{
uint8_t i;
uint8_t data_Buffer[7]={0x00};
HAL_UART_DMAStop(&uart_hmi);
if((data_Buffer[0]=*(UART_HMI.pucReceive_Buffer+0))==0x65)
{
for(i=1;i<6;i++)
{
data_Buffer[i]=*(UART_HMI.pucReceive_Buffer+i);
}
}
if(0x01==data_Buffer[2])
{
HMI_KEY1_Service();
}
else if(0x02==data_Buffer[2])
{
HMI_KEY2_Service();
}
else if(0x03==data_Buffer[2])
{
HMI_KEY3_Service();
}
else
{
HMI_KEY4_Service();
}
HAL_UART_Receive_DMA(&uart_hmi,UART_HMI.pucReceive_Buffer,ucReceive_Buffer_Length);
}
/*
* @name HMI_KEY1_Service
* @brief HMI_KEY1 服务函数
* @param None
* @retval None
*/
static void HMI_KEY1_Service(void)
{
Transmit_string_to_HMI((uint8_t *)"page main7");
}
/*
* @name HMI_KEY2_Service
* @brief HMI_KEY2 服务函数
* @param None
* @retval None
*/
static void HMI_KEY2_Service(void)
{
Transmit_string_to_HMI((uint8_t *)"page main8");
}
/*
* @name HMI_KEY3_Service
* @brief HMI_KEY3 服务函数
* @param None
* @retval None
*/
static void HMI_KEY3_Service(void)
{
Transmit_string_to_HMI((uint8_t *)"page main5");
}
/*
* @name HMI_KEY4_Service
* @brief HMI_KEY4 服务函数
* @param None
* @retval None
*/
static void HMI_KEY4_Service(void)
{
Transmit_string_to_HMI((uint8_t *)"page main3");
}
/********************************************************
End Of File
********************************************************/
所有重点代码里面都有,欢迎各位大神指点。