【开源】多任务裸机之DWIN屏
在DWIN屏嵌入式开发中,特别是工业控制中,外设比较多,比如串口通讯这块,有GPS模块,wifi模块,RS485通讯,232通讯(DGUS屏通讯)。还有十几路的输入采样,8路输出控制,DGUS屏按键事件,DGUS屏数据实时显示事件等多种事件。大部分都会采用实时操作系统,但是实时操作系统对RAM和ROM都有要求的,所以我们看看在单片机裸奔时如何高效的实现多任务处理。其实作为嵌入式工程师来说,大家大部分时间都一直忙碌于公司的项目,没有时间做自己的东西,其实有空时多回头看看自己多年来做的项目,可以把以前的积累沉淀一下。怎么做才能使整个单片机系统的框架更加简洁方便可靠,总结出单片机大致应用程序的架构有三种:
第一种,简单的前后台顺序执行程序,这类写法是大多数人使用的方法,不需用思考程序的具体架构,直接通过执行顺序编写应用程序即可。 第二种,时间片轮询法,此方法是介于顺序执行与操作系统之间的一种方法。 第三种,操作系统,此法应该是应用程序编写的最高境界。
如果你能转换一下思路,不再把业务逻辑中各个模块的关系看成基于因果(顺序),而是基于时间,模块间如果需要确定次序可以采用标志位进行同步。那么恭喜你,你已经有了采用实时系统的思想,可以尝试使用RT-thread等操作系统来完成你的项目了。但是,使用操作系统有几个问题:第一是当单片机资源有限的时候,使用操作系统恐怕不太合适;第二是学习操作系统本身有一定的难度,至少你需要花费一定的时间;第三如果你的项目复杂度没有那么高,使用操作系统有点大材小用。
一、单片机如何实现多任务处理 1.看看时间轮询如何实现单片机逻辑程序的高效执行
基于定时器触发,调度效率高,最大化减少无效的代码运行时间,将定时器与状态机和伪线程语法融合到一个框架,任务函数可以有两种写法。具体看下怎么写: - <font face="新宋体" size="5">#ifndef huihui
- #define huihui
- #define MAXTASKS 6 //事件任务的数量,根据需要而定
- extern volatile unsigned char timers[MAXTASKS];
- #define _SS static unsigned char _lc=0; switch(_lc){default:
- #define _EE ;}; _lc=0; return 255;
- #define WaitX(tickets) do {_lc=(__LINE__+((__LINE__%256)==0))%256; return tickets ;} while(0); case (__LINE__+((__LINE__%256)==0))%256:
- #define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0) timers[TaskID]=TaskName(); } while(0);
- #define RunTaskA(TaskName,TaskID) { if (timers[TaskID]==0) {timers[TaskID]=TaskName(); continue;} } //前面的任务优先保证执行
- #define CallSub(SubTaskName) do {unsigned char currdt; _lc=(__LINE__+((__LINE__%256)==0))%256; return 0; case (__LINE__+((__LINE__%256)==0))%256: currdt=SubTaskName(); if(currdt!=255) return currdt;} while(0);
- #define InitTasks() {unsigned char i; for(i=MAXTASKS;i>0 ;i--) timers[i-1]=0; }
- #define UpdateTimers() {unsigned char i; for(i=MAXTASKS;i>0 ;i--){if((timers[i-1]!=0)&&(timers[i-1]!=255)) timers[i-1]--;}}
- #define SEM unsigned int
- //初始化信号量
- #define InitSem(sem) sem=0;
- //等待信号量
- #define WaitSem(sem) do{ sem=1; WaitX(0); if (sem>0) return 1;} while(0);
- //等待信号量或定时器溢出, 定时器tickets 最大为0xFFFE
- #define WaitSemX(sem,tickets) do { sem=tickets+1; WaitX(0); if(sem>1){ sem--; return 1;} } while(0);
- //发送信号量
- #define SendSem(sem) do {sem=0;} while(0);
- #endif//终止</font>
复制代码
2.我们这次是5个事件
- 电压采集
- 处理dgus屏的数据1
- 处理dgus屏的数据2
- 向上位机发送数据
- AD采集数据上传DGUS屏
- <font face="新宋体" size="5">unsigned char task0(){
- _SS
- while(1)
- {
-
- WaitX(100);
- FXJ_DYCJ(); /*电压采集*/
- }
- _EE
- }
- unsigned char task1(){
- _SS
- while(1){
-
- WaitX(10);
- Usart2_PeiFangDeal();
- }
- _EE
- }
- unsigned char task2(){
- _SS
- while(1){
- WaitX(100);
- Usart1_ReceiveDeal();
- }
- _EE
- }
- unsigned char task3(){
- _SS
- while(1){
- WaitX(100);
-
- USART2_Send();/*向上位机发送数据*/
- }
- _EE
- }
- unsigned char task4(){
- _SS
- while(1){
- WaitX(200);
- USART1_SendADC();/*向屏上发送采集的电压信号*/
- }
- _EE
- }</font>
复制代码
3.定时设置
定时这块都是采用滴答定时器中断,1ms为时标void SysTick_Handler(void)
每1ms对定时时间执行减一操作 - {unsigned char i; for(i=MAXTASKS;i>0 ;i--){if((timers[i-1]!=0)&&(timers[i-1]!=255)) timers[i-1]--;}}
复制代码
4.主函数主函数只需要调用事件就可以,可以设置任务函数的优先级执行 - <font face="新宋体" size="5">int main(void)
- {
- BSP_Init();
- InitTasks(); //初始化任务,给TIME清零
-
- while(1)
- {
- RunTask(task0,0);
- RunTaskA(task1,1);
- RunTaskA(task2,2);
- RunTaskA(task3,3);
- RunTaskA(task4,4);
- }
-
- }</font>
复制代码
二、如何在裸机中对串口数据进行有效处理 在DGUS屏使用中,串口通讯应用几乎是必须的,非常常用,上面的工程中,好多事件都是和串口有关,做好串口的数据处理是非常关键的一步,这里再分享一下如何在裸机中对串口数据的有效处理,比如一包数据0xaa,0x55,0xXX,0xXX,0x0a,0x0d,简单介绍串口处理的方法。
1.直接在接受中断中判断数据 先定义一个uint8_t Buff和一个uint8_t Table[10]。 在接收中断函数里用HAL_UART_Receive_IT(&UART1_Handler,&Buff,1)这个函数,每次在中断里面都判断Buff是不是0xaa,如果是则将数据存入到Table[0]中且继续接收下面的数据,都存入到Table的数组中,如果不是则继续进行判断。然后对Table进行判断,首先判断Table[0]和Table[1]分别为0xaa,0x55后,在进行判断Table[4]和Table[5]分别为0x0a,0x0d。在进行判断校验是否正确,正确后取出Table[2]的数据进行处理。这种方法在数据传输慢的情况下,比较简单方便,还可以,但是在数据快的时候,非常容易造成数据的丢失,还有就是要是第一次数据接收错误,回不到初始化状态,必须复位操作。
2.FIFO方式 超时接受
接收中断函数里用HAL_UART_Receive_IT(&UART1_Handler, &Buff, 1)这个函数接收数据的时候不要做数据处理,而是忠实地接收原始字节流,只管接受入列,就是接受完数据0xaa,0x55,0xXX,0xXX,0x0a,0x0d后,超时时间RxTimeOut3到以后再对数据处理:
/* 数据入队 */
chfifo_in(&RxFifo, &Buff);
/* 清零超时 */
RxTimeOut3 = 0;
3.DMA+空闲中断
(1)开启串口DMA接收;
(2)串口收到数据,DMA不断传输数据到存储buf;
(3)一帧数据发送完毕,串口暂时空闲,触发串口空闲中断;
(4)在中断服务函数中,可以计算刚才收到了多少个字节的数据;
(5)解码存储buf,清除标志位,开始下一帧接收。
就是接收到一帧数据0xaa,0x55,0xXX,0xXX,0x0a,0x0d后才触发中断接受,处理数据非常高效。
三、对于串口FIFO方式,就是环形缓冲区,简单介绍一下 环形缓冲区就是一个带“头指针”和“尾指针”的数组。“头指针”指向环形缓冲区中可读的数据,“尾指针”指向环形缓冲区中可写的缓冲空间。通过移动“头指针”和“尾指针”就可以实现缓冲区的数据读取和写入。在通常情况下,应用程序读取环形缓冲区的数据仅仅会影响“头指针”,而串口接收数据仅仅会影响“尾指针”。当串口接收到新的数组,则将数组保存到环形缓冲区中,同时将“尾指针”加1,以保存下一个数据;应用程序在读取数据时,“头指针”加1,以读取下一个数据。当“尾指针”超过数组大小,则“尾指针”重新指向数组的首元素,从而形成“环形缓冲区”!,有效数据区域在“头指针”和“尾指针”之间。
当然,环形缓冲区的“头指针”和“尾指针”可以用“头变量”和“尾变量”来代替,因为切换数组的元素空间,除了可以用“指针偏移法”之外,还可以用“素组下标偏移法”。当串口接收到新的数组,则将数组保存到环形缓冲区中,同时将“尾变量”加一,以保存下一个数据;应用程序在读取数据时,“头变量”加一,以读取下一个数据。
“环形缓冲区”数据接收处理机制的好处在于:利用了队列的特点,一头进,一头出,互不影响,在数据进去(往里存)的时候,另一边也可以把数据读出来,而读出来的数据,留下的空位,又可以增加多的存储空间,从而避免一边接收数据且一边处理数据会在数据量密集的时候而导致的丢掉数据或者数据产生冲突的问题。
如果仅有一个线程读取环形缓冲区的数据,只有一个串口往环形缓冲区写入数据,则不需要添加互斥保护机制就可以保证数据的正确性。
需要注意的是,如果串口每接收x个字节的数据才处理一次,则环形缓冲区的缓冲数组的大小必须是x的N倍,具体N为多少,需要结合具体的数据接收速率以及处理速率,适当调节。这就好比喻,水壶永远大于水杯,这样子水壶才能存放很多杯水。
如果觉得前文隐晦难懂,那么下面我们来一起讨论一下环形队列的具体状态以及实现。下文构建的环形队列采用的是“头变量”“尾变量”来控制队列的存储和读取。 首先,我们会构造一个结构体,并定义一个结构变量。
- <font face="宋体" size="5">#define MAX_SIZE 12 //缓冲区大小
-
- typedef struct
- {
- unsigned char head; //缓冲区头部位置
- unsigned char tail; //缓冲区尾部位置
- unsigned char ringBuf[MAX_SIZE]; //缓冲区数组
- } ringBuffer_t;
-
- ringBuffer_t buffer; //定义一个结构体
- </font>
复制代码
定义一个结构头体则表示新的消息队列已经创建完成。新建创建的队列,头指针head和尾指针tail都是指向数组的元素0。 当如果l加入队列,则缓冲队列处于满载状态,如果此时,接收到新的数据并需要保存,则tail需要归零,将接收到的数据存到数组的第一个元素空间,如果尚未读取缓冲数组的一个元素空间的数据,则此数据会被新接收的数据覆盖。同时head需要增加1,修改头节点偏移位置丢弃早期数据。 当消息队列中的所有数据都读取出来后,此时环形队列是空的,如果tail和head相等,则表示缓冲队列是空的。
- <font face="宋体" size="5">/**********************************************************************************************
- ***********************************************************************************************/
- #include "ringbuffer.h"
- #define BUFFER_MAX 36 //缓冲区大小
- typedef struct
- {
- unsigned char headPosition; //缓冲区头部位置
- unsigned char tailPositon; //缓冲区尾部位置
- unsigned char ringBuf[BUFFER_MAX]; //缓冲区数组
- } ringBuffer_t;
- ringBuffer_t buffer; //定义一个结构体</font>
复制代码
首先,需要构建一个结构体ringBuffer_t,如果定义一个结构体变量buffer,则意味着创建一个环形缓冲区。这是缓冲区里写数据:
- <font face="宋体" size="5">/**
- * @brief 写一个字节到环形缓冲区
- * @param data:待写入的数据
- * @return none
- */
- void RingBuf_Write(unsigned char data)
- {
- buffer.ringBuf[buffer.tailPositon]=data; //从尾部追加
- if(++buffer.tailPositon>=BUFFER_MAX) //尾节点偏移
- buffer.tailPositon=0; //大于数组最大长度 归零 形成环形队列
- if(buffer.tailPositon == buffer.headPosition)//如果尾部节点追到头部节点,则修改头节点偏移位置丢弃早期数据
- if(++buffer.headPosition>=BUFFER_MAX)
- buffer.headPosition=0;
- }</font>
复制代码
这是读取缓冲区的数据:
- <font size="3" face="宋体"><font face="宋体" size="5">/**
- * @brief 读取环形缓冲区的一个字节的数据
- * @param *pData:指针,用于保存读取到的数据
- * @return 1表示缓冲区是空的,0表示读取数据成功
- */
- u8 RingBuf_Read(unsigned char* pData)
- {
- if(buffer.headPosition == buffer.tailPositon) //如果头尾接触表示缓冲区为空
- {
- return 1; //返回1,环形缓冲区是空的
- }
- else
- {
- *pData=buffer.ringBuf[buffer.headPosition]; //如果缓冲区非空则取头节点值并偏移头节点
- if(++buffer.headPosition>=BUFFER_MAX)
- buffer.headPosition=0;
- return 0; //返回0,表示读取数据成功
- }
- }</font></font>
复制代码
串口中断函数:
- <font size="3" face="宋体"><font face="宋体" size="5">/**
- * @brief 串口1中断函数
- * @param none
- * @return none
- */
- void USART1_IRQHandler(void)
- {
- if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //判断接收标志位是否为1
- {
- USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清楚标志位
- RingBuf_Write(USART_ReceiveData(USART1));
- //阻塞等待直到传输完成
- while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
-
- }
- }</font></font>
复制代码
主程序读取数据处理就可以了:
- <font size="3" face="宋体"><font face="宋体" size="5"> while(1)
- {
- if(0 == RingBuf_Read(&data)) //从环形缓冲区中读取数据
- { }
- </font></font>
复制代码
|