前言
一般进行远程监控时,2.4G无线通信是充当远程数据传输的一种方法。这时就需要在现场部分具备无线数据发送装置,而在上位机部分由于一般只有串口,所以将采集到的数据送到电脑里又要在上位机端设计一个数据接收的适配器。这里基于stm32分别设计了现场部分和适配器部分,这里只是基本通信功能实现的讲解,一些复杂的技术比如加密、可靠等要根据具体的应用来设计~
总体说明
这里采用stm32作为MCU,采用nRF24L01作为2.4G通信模块。其中适配器中仅仅采用了USART和NRF24L01两个主要部分,负责将下位机通过2.4G发送过来的数据通过串口发送给上位机,或者将上位机的通过串口传来的数据通过2.4G发送给下位机来实现远程监控(没有采用uc-os操作系统,也没有界面,要用串口和上位机相连);其中下位机比较复杂,因为一般下位机是一个集成的系统,包括从各种传感器收集数据、向各种类型的驱动电路发送控制命令、将数据输给打印机或显示器、和无线通信或有线通信设备进行互相通信来实现数据传输等,这里的下位机比较简单:采用uc-os实时操作系统+uc-gui负责界面显示,外接7寸TFT液晶显示屏,和适配器类似也包括USART和NRF24L01通信部分,但是因为有了操作系统和可视化交互界面,所以也有点不同,接下来开始介绍。
适配器部分
这里介绍的流程是以main函数为基准,广度拓宽知识点,最后main函数说完,整个工程的细节也就大致能了解了~
1 int main(void){ 2 uint8_t a=0;//LED高低电压控制 3 /* System Clocks Configuration */ 4 RCC_Configuration(); //系统时钟设置 5 /*嵌套向量中断控制器 6 说明了USART1抢占优先级级别0(最多1位) ,和子优先级级别0(最多7位) */ 7 NVIC_Configuration(); //中断源配置 8 /*对控制LED指示灯的IO口进行了初始化,将端口配置为推挽上拉输出,口线速度为50Mhz。PA9,PA10端口复用为串口1的TX,RX。 9 在配置某个口线时,首先应对它所在的端口的时钟进行使能。否则无法配置成功,由于用到了端口B, 因此要对这个端口的时钟 10 进行使能,同时由于用到复用IO口功能用于配置串口。因此还要使能AFIO(复用功能IO)时钟。*/ 11 GPIO_Configuration(); //端口初始化 12 SPI2_NRF24L01_Init(); //SPI2及NRF24L01接口初始化 13 USART_Config(USART1); //串口1初始化 14 /*NRF24L01设置为接收模式*/ 15 RX_Mode(); 16 17 while (1) 18 { 19 if(usart_rec_flag==1) //判断是否收到一帧有效数据 20 { 21 usart_rec_flag=0; 22 NRF_Send_Data(TxBufferRF,sizeof(TxBufferRF)); 23 if(a==0){GPIO_SetBits(GPIOB, GPIO_Pin_5);a=1;} //LED1 明暗闪烁 24 else{GPIO_ResetBits(GPIOB, GPIO_Pin_5);a=0;} 25 } 26 if(rf_rec_flag==1) 27 { 28 rf_rec_flag=0; 29 for(i=0;i<32;i++)//发送字符串 30 { 31 USART_SendChar(USART1,TxBufferUSART[i]); 32 // Delay(0x0000ff00); 33 } 34 } 35 } 36 }
第4行RCC初始化主要是系统时钟和外设时钟配置,这里注意要使能RCC_APB2Periph_USART1,当时忘了使能这个结果串口出现异常,我还以为是初始化和中断向量什么的弄错了呢,浪费了很长时间。
1 /*-------------------------------------------------------------------------------------- 2 系统时钟配置为72MHZ+外设时钟配置*/ 3 void RCC_Configuration(void){ 4 SystemInit(); 5 RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 |RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB |RCC_APB2Periph_AFIO , ENABLE); 6 }
第7行中断向量初始化设置,主要是设置串口接收中断和NRF24L01中断的,这样设置好了之后当串口中断被触发时其对应的中断子程序将被执行(这个科班的大概都知道这里就不多说了),所以我们就要在stm32f10x_it.c里实现他们各自的中断子程序了(这个一会再详细介绍,咱们先把整个框架了解下)。另外说一句,这里的的优先级组将影响主优先级和子优先级数量具体请参考stm32f10X_的固件库的NVIC.
1 void NVIC_Configuration(void){ 2 /* 结构声明*/ 3 NVIC_InitTypeDef NVIC_InitStructure; 4 EXTI_InitTypeDef EXTI_InitStructure; 5 6 /* 优先级组 1 */ 7 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); 8 9 /* Enable the USART1 Interrupt */ 10 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //设置串口1中断 11 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占优先级 0 12 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级为0 13 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能 14 NVIC_Init(&NVIC_InitStructure); 15 16 17 NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //NRF24L01 中断响应 18 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占优先级 0 19 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级为1 20 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能 21 NVIC_Init(&NVIC_InitStructure); 22 23 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); //NRF24L01 IRQ PA0 24 25 EXTI_InitStructure.EXTI_Line = EXTI_Line0; //NRF24L01 IRQ PA0 26 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //EXTI中断 27 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿触发 28 EXTI_InitStructure.EXTI_LineCmd = ENABLE; //使能 29 EXTI_Init(&EXTI_InitStructure); 30 }
第11行的GPIO初始化,主要是对通用IO口的属性设置和初始化,这里一定要对串口所需的A9和A10配置好!
1 void GPIO_Configuration(void){ 2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED1控制--PB5 3 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 4 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 5 GPIO_Init(GPIOB, &GPIO_InitStructure); 6 7 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //USART1 TX 8 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 9 GPIO_Init(GPIOA, &GPIO_InitStructure); //A端口 10 11 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //USART1 RX 12 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //复用开漏输入 13 GPIO_Init(GPIOA, &GPIO_InitStructure); //A端口 14 }
第12行的SPI2_NRF24L01_Init();主要是驱动NRF24L01的接口初始化,因为NRF24L01采用的是SPI通信,所以这里免不了SPI的设置和相关操作了,不过幸好都封装好了~像以前在51上做SPI就得自己模拟SPI,没有示波器调试起来甚是坑~此外这里我已经把NRF24L01的整个驱动都封装在NRF24L01.c这个文件里了,当想用的时候只要在中断向量里设置其中断接收函数,并在it.c里实现其接收函数;一般主函数里用到的是其初始化函数SPI2_NRF24L01_Init();和 RX_Mode();,当在过程中想利用NRF24L01向外发数据时只要调用函数void NRF_Send_Data(uint8_t* data_buffer, uint8_t Nb_bytes):
1 /**************************************************************************** 2 * 名 称:NRF_Send_Data(uint8_t* data_buffer, uint8_t Nb_bytes) 3 * 功 能:将保存在接收缓存区的32字节的数据通过NRF24L01+发送出去 4 * 入口参数:data_buffer 待发送数据 5 Nb_bytes 待发送数据长度 6 * 出口参数:无 7 * 说 明:数据小于32,把有效数据外的空间用0填满 8 * 调用方法:RX_Mode(); 9 ****************************************************************************/ 10 void NRF_Send_Data(uint8_t* data_buffer, uint8_t Nb_bytes) 11 { 12 uchar i=0; 13 MODE_CE(0); //NRF 模式控制 14 15 SPI_RW_Reg(WRITE_REG1+STATUS,0xff); //设置状态寄存器初始化 16 SPI_RW_Reg(0xe1,0); //清除TX FIFO寄存器 17 SPI_RW_Reg(0xe2,0); //清除RX FIFO寄存器 18 TX_Mode(); //设置为发送模式 19 delay_ms(1); 20 if(Nb_bytes<32){ //当接收到的USB虚拟串口数据小于32,把有效数据外的空间用0填满 21 for(i=Nb_bytes;i<32;i++) data_buffer[i]=0; 22 } 23 MODE_CE(0); 24 SPI_Write_Buf(WR_TX_PLOAD, data_buffer, TX_PLOAD_WIDTH); //发送32字节的缓存区数据到NRF24L01 25 MODE_CE(1); //保持10us以上,将数据发送出去 26 }
第13行是USART初始化,包括波特率、数据位、停止位等~
1 void USART_Config(USART_TypeDef* USARTx){ 2 USART_InitStructure.USART_BaudRate = 9600; //速率9600bps 3 USART_InitStructure.USART_WordLength = USART_WordLength_8b; //数据位8位 4 USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位1位 5 USART_InitStructure.USART_Parity = USART_Parity_No; //无校验位 6 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件流控 7 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式 8 9 /* Configure USART1 */ 10 USART_Init(USARTx, &USART_InitStructure); //配置串口参数函数 11 12 13 /* Enable USART1 Receive and Transmit interrupts */ 14 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能接收中断 15 USART_ITConfig(USART1, USART_IT_TXE, ENABLE); //使能发送缓冲空中断 16 17 /* Enable the USART1 */ 18 USART_Cmd(USART1, ENABLE); 19 }
同样的类似于NRF24L01一旦初始化之后,其数据接收一般采用中断方式、数据发送一般采用直接发送的方式。所以在中断向量里也要设置,也要在it.c中实现其接收中断子函数。其发送直接调用stm32f10的固件库函数(这里我稍加封装了下):其实就是发送一个data之后要监听是否发送完成才能进行下次发送~
1 void USART_SendChar(USART_TypeDef* USARTx,uint8_t data){ 2 USART_SendData(USARTx,data); 3 while(USART_GetFlagStatus(USARTx,USART_FLAG_TC)==RESET); 4 }
接下来进入while循环,不断进行监听看是否有串口接收标志位置1或者无线模块接收标志位置1,如果有表明相应的有数据从该通道传送过来。当是从串口传来的数据表明数据是从上位机发送来的数据,并且想把该数据通过2.4G发送出去,所以这里调用:NRF_Send_Data(TxBufferRF,sizeof(TxBufferRF));将数据发送出去;当数据是从2.4G通道中传过来的,表明数据是从下位机传送过来的想给上位机,于是调用串口发送函数将数据发送给上位机:USART_SendChar(USART1,TxBufferUSART[i]);
看了上图适配器端的数据交换过程就明白了串口中断和无线中断大致要干的事了,这里我就不多介绍,看看下面的代码就明白了(在stm32f10x_it.c中),要再次提醒的是无论是串口还是无线其接收都是采用中断,而发送采用循环直接发送,他们的中断和中断向量有关并要在stm32f10x_it.c里实现相应的中断子程序~
下位机部分
上面说过一般具有远程通信能力的嵌入式系统其下位机部分往往要干很多事,这里我们采用stm32作为MCU并搭载uc-OS实时操作系统负责任务调度,同时采用7寸TFT彩屏和uc-GUI设计可视化人机交互界面。其中任务包括主任务、界面任务和触摸任务,主任务负责建立其他任务,界面任务中将包含整个人机交互界面的界面刷新逻辑,触摸任务负责获取触摸位置数据获取~
这里我们还是得从main函数先说起:首先在main函数中进行相关初始化,然后建立主任务并启动uc-OS内核;接着在主任务中调用App_TaskCreate(); 分别建立界面任务和触摸任务(如下每个任务的建立类似,要给出指向任务代码的指针、任务执行时传递给任务的参数的指针,分配给这个任务的栈信息,任务优先级等)。这样当任务建立好之后,其执行权就由操作系统调度了~
1 static void App_TaskCreate(void) 2 { 3 /* 建立用户界面任务 */ 4 OSTaskCreateExt(AppTaskUserIF, //指向任务代码的指针 5 (void *)0, //任务开始执行时,传递给任务的参数的指针 6 (OS_STK *)&AppTaskUserIFStk[APP_TASK_USER_IF_STK_SIZE-1], //分配给任务的堆栈的栈顶指针 从顶向下递减 7 APP_TASK_USER_IF_PRIO, //分配给任务的优先级 8 APP_TASK_USER_IF_PRIO, //预备给以后版本的特殊标识符,在现行版本同任务优先级 9 (OS_STK *)&AppTaskUserIFStk[0], //指向任务堆栈栈底的指针,用于堆栈的检验 10 APP_TASK_USER_IF_STK_SIZE, //指定堆栈的容量,用于堆栈的检验 11 (void *)0, //指向用户附加的数据域的指针,用来扩展任务的任务控制块 12 OS_TASK_OPT_STK_CHK|OS_TASK_OPT_STK_CLR); //选项,指定是否允许堆栈检验,是否将堆栈清0,任务是否要 13 //进行浮点运算等等。 14 15 /* 建立触摸驱动任务 */ 16 OSTaskCreateExt(AppTaskKbd, 17 (void *)0, 18 (OS_STK *)&AppTaskKbdStk[APP_TASK_KBD_STK_SIZE-1], 19 APP_TASK_KBD_PRIO, 20 APP_TASK_KBD_PRIO, 21 (OS_STK *)&AppTaskKbdStk[0], 22 APP_TASK_KBD_STK_SIZE, 23 (void *)0, 24 OS_TASK_OPT_STK_CHK|OS_TASK_OPT_STK_CLR); 25 26 }
这里以界面任务为例:因为我们在建立界面任务时已经指定其任务代码指针AppTaskUserIF,所以这里来写其对应的函数(也就是说这里是界面任务的入口)。从下面的代码可以看出进入界面任务时首先对uc-GUI进行初始化,然后进入死循环不断执行Fun()函数(有人会疑惑:这里while死循环不就只能死在这里吗?怎么执行其他任务呢?哈哈,这就是具有操作系统和不具有操作系统的不同啦~虽然这里是while死循环,但是当OS要把CPU占有权分给其他任务时就会把当前执行的任务的信息压入其对应的栈空间,当再次要把CPU分配给该任务时,则把栈里保存的上次执行的情况拿出来继续执行,从而实现抢占与多任务的效果!)
1 static void AppTaskUserIF (void *p_arg) 2 { 3 (void)p_arg; 4 GUI_Init(); //ucgui初始化 5 while(1) 6 { 7 Fun(); //界面主程序 8 } 9 }
所以接下来我们主要看Fun.c里的Fun函数:虽然代码有点长,但是很好理解,其核心思路就是建立整个界面并对界面中的每个控件进行相关设置同时获得其句柄,在最后又进入了while死循环,在循环中不断检测2.4G是否接受到数据(和适配器端类似也是中断子程序中收数据然后置接收标志为1的),然后根据从2.4G收到的数据来刷新文本显示区;下面一个if判断speed_change_flag是否有效来向串口发送相应的数据。那么我们的问题又来了:这个speed_change_flag是在哪里被改变的呢?这个我们就要参看窗口回调函数了!这里的窗口回调函数是窗口动作响应函数(就像安卓开发里的按钮监听或MFC里的按钮点击事件等),一旦窗口里的控件有相应的触发动作就会调用该函数,并把事件类型封装在WM_MESSAGE里传过来,在该函数里对该消息进行解析并作出相应的动作即可(非常像Win32!!!我怀疑做这个uc-GUI的人有copy微软的嫌疑,(^∇^*)随便猜测,如有雷同,纯属巧合)。这样我们就很容易找到send按钮的监听用于将数据通过NRF24L01发送出去的相关操作,也就明白了滑动条监听用来改变speed1~5.上面说了这么多,少了介绍整个界面是怎么建立的了~其实整个窗体的布局都要放在一个结构体里,然后在fun()函数里调用hWin = GUI_CreateDialogBox(aDialogCreate, GUI_COUNTOF(aDialogCreate), _cbCallback, 0, 0, 0);根据定义的窗口资源和回调函数进行窗体的建立~这样我们就圆满地理解了stm32基于uc-OS并搭载uc-GUI的运行逻辑啦!
1 /* 定义了对话框资源列表 */ 2 static const GUI_WIDGET_CREATE_INFO aDialogCreate[] = { 3 //建立窗体, 大小是800X480 原点在0,0 4 { FRAMEWIN_CreateIndirect, "http://beautifulzzzz", 0,0,0, 800, 480, FRAMEWIN_CF_ACTIVE }, 5 { BUTTON_CreateIndirect, "SEND", GUI_ID_BUTTON0, 0, 395, 200, 55 }, 6 7 { BUTTON_CreateIndirect, "CLEAR", GUI_ID_BUTTON2, 200, 395, 200, 55 }, 8 { EDIT_CreateIndirect, "", GUI_ID_EDIT1, 0, 190, 400, 65, EDIT_CF_LEFT, 50 }, 9 { EDIT_CreateIndirect, "", GUI_ID_EDIT2, 0, 290, 400, 65, EDIT_CF_LEFT, 50 }, 10 11 //建立TEXT控件,起点是窗体的X,X,大小XXY 文字左对齐 12 { TEXT_CreateIndirect, "Send Text Area", GUI_ID_TEXT0, 1, 160, 400, 25, TEXT_CF_LEFT }, 13 { TEXT_CreateIndirect, "Receive Text Area ", GUI_ID_TEXT1, 1, 263, 400, 25, TEXT_CF_LEFT }, 14 15 { TEXT_CreateIndirect, "2M bps", GUI_ID_TEXT2, 23, 22, 140, 25, TEXT_CF_LEFT }, 16 { TEXT_CreateIndirect, "1M bps", GUI_ID_TEXT3, 23, 42, 140, 25, TEXT_CF_LEFT }, 17 { TEXT_CreateIndirect, "250K bps", GUI_ID_TEXT5, 23, 62, 140, 25, TEXT_CF_LEFT }, 18 19 { TEXT_CreateIndirect, "", GUI_ID_TEXT4, 0, 120, 400, 25, TEXT_CF_LEFT }, 20 21 { RADIO_CreateIndirect, "Receive Mode", GUI_ID_RADIO0, 3, 33, 40, 52, RADIO_TEXTPOS_LEFT,3}, 22 23 { LISTBOX_CreateIndirect, "", GUI_ID_LISTBOX0, 134, 13, 130, 90, 0, 0 }, 24 25 //建立滑块 26 { SLIDER_CreateIndirect, NULL, GUI_ID_SLIDER1, 440, 60, 320, 25, 0, 0 }, 27 { SLIDER_CreateIndirect, NULL, GUI_ID_SLIDER2, 440, 120, 320, 25, 0, 0 }, 28 { SLIDER_CreateIndirect, NULL, GUI_ID_SLIDER3, 440, 180, 320, 25, 0, 0 }, 29 { SLIDER_CreateIndirect, NULL, GUI_ID_SLIDER4, 440, 240, 320, 25, 0, 0 }, 30 { SLIDER_CreateIndirect, NULL, GUI_ID_SLIDER5, 440, 300, 320, 25, 0, 0 }, 31 //建立滑块对应的text 32 { TEXT_CreateIndirect, "0", GUI_ID_TEXT_SPEED1, 770, 60, 25, 25, TEXT_CF_LEFT }, 33 { TEXT_CreateIndirect, "0", GUI_ID_TEXT_SPEED2, 770, 120, 25, 25, TEXT_CF_LEFT }, 34 { TEXT_CreateIndirect, "0", GUI_ID_TEXT_SPEED3, 770, 180, 25, 25, TEXT_CF_LEFT }, 35 { TEXT_CreateIndirect, "0", GUI_ID_TEXT_SPEED4, 770, 240, 25, 25, TEXT_CF_LEFT }, 36 { TEXT_CreateIndirect, "0", GUI_ID_TEXT_SPEED5, 770, 300, 25, 25, TEXT_CF_LEFT }, 37 };
还要回过头说说我们的USART和NRF24L01,他们的初始化要看main函数中的BSP_Init();函数,该函数负相关硬件的初始化设置(中文意思是板级支持包初始化函数,因为uc-OS可以并不只限于stm32单片机,所以这里要根据不同平台进行相应的设置)。该函数位于bsp.c函数中,其作用相当于将以前我们在main函数中进行的相关硬件初始化单独拿出来封装成一个函数而已~但是,串口和无线对应的中断接收程序却有点不一样,因为这里是操作系统,所以在每个中断子程序前要调用OS_ENTER_CRITICAL();保存当前的全局中断标志,然后OSIntNesting++;中断嵌套深度加1,最后调用OS_EXIT_CRITICAL();恢复全局中断标志进入正常的中断处理,此外在中断响应函数最后要调用OSIntExit(); 检测如果有更高优先级的任务就绪了,则执行一次任务切换。
最后说明
对于纯玩软件的小伙伴,这里涉及的东西有点多,不必细究,看看了解即可。但是对于初学stm32,尤其是还在为stm32控制NRF24L01不通的同学,这个还是挺有用滴~下面有工程的链接,里面有些注释不规范,一切以我博客里说的为准哦~
链接
上述工程keil代码:http://pan.baidu.com/s/1mgqowQ0
本文链接:http://www.cnblogs.com/zjutlitao/p/4242734.html
上述工程GitHub链接:https://github.com/beautifulzzzz/stm32/tree/master/stm32_USART%2BNRF24L01
可能有用1:[stm32][ucos] 1、基于ucos操作系统的LED闪烁、串口通信简单例程
可能有用2:[stm32][ucos][ucgui] 2、LED闪烁、串口、滑块、文本编辑框简单例程
本文转自beautifulzzzz博客园博客,原文链接:http://www.cnblogs.com/zjutlitao/p/4242734.html,如需转载请自行联系原作者