嵌入式软件架构--多窗口显示1(后台软件实现)
开始
嵌入式系统常见显示器
分类1:
1,动态数码管
8个动态数码管分辨率有164
2,点阵屏
3,LCD
单色屏:1个像素点占1bit 分辨率12864
彩色屏:分辨率有480*320
8位:332格式 RGB 分别占3,3,2bit
26位:565格式 RGB 分别占5,6,5bit
32位:888格式RGBA 分别占8,8,8,8bit
针对这些不同类型的显示器,要显示图像本质都是将字模写入到控制器,软件架构是一样的。
分类2:
是否带驱动器:
是:OLED,LCD 驱动器自动不断刷新 有显存 有时要防止闪烁还要双缓冲。
否:动态数码管,点阵屏 需要软件不断刷新 没有显存需要自己开辟内存空间当显存
显示架构
整个的显示架构分为三层:
1,前台软件:用户设计的界面(多窗口多界面的方式显示),人机交互信息(消息与事件机制)
2,后台软件:进行编码格式的转换,功能性的转换 (命令式)
3,底层硬件驱动:gpio配置 IO状态变化,updateLed是驱动的入口函数。
一屏内容先分成多个窗口,窗口再分解成控件
用户界面是能看到的属于前台软件,其他用户看不到的属于后台软件,这里要分成两个任务。驱动每3ms调用一次,后台任务每100ms调用一次刷新显示。
底层驱动
一共8位共阴数码管,段选接在PB8-PB15,位选通过38译码器控制接PB5,PB6,PB7
//一些端口宏定义
#define LED_PORT GPIOB
#define LED_PIN (GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15)
#define LED_PORT_RCC RCC_APB2Periph_GPIOB#define LED_CHIP_PORT GPIOB
#define LED_CHIP_PIN (GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7)
#define LED_CHIP_PORT_RCC RCC_APB2Periph_GPIOBextern void ledDrvInit(void);
extern void ledDrv(UINT8 dat,UINT8 chip);
extern void led_write(UINT8 dat);
驱动的实现
void ledDrvInit(void)
{GPIO_InitTypeDef led; //声明一个结构体变量,用来初始化GPIO/* 开启GPIO时钟 */RCC_APB2PeriphClockCmd(LED_PORT_RCC,ENABLE);//GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable,ENABLE);/* 配置GPIO的模式和IO口 */led.GPIO_Pin=LED_PIN; //选择你要设置的IO口led.GPIO_Mode=GPIO_Mode_Out_OD;led.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(LED_PORT,&led); /* 初始化GPIO */LED_PORT->ODR = 0x00; RCC_APB2PeriphClockCmd(LED_CHIP_PORT_RCC,ENABLE);/* 配置GPIO的模式和IO口 */led.GPIO_Pin=LED_CHIP_PIN; //选择你要设置的IO口led.GPIO_Mode=GPIO_Mode_Out_OD;led.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(LED_CHIP_PORT,&led); /* 初始化GPIO */
}
void ledDrv(UINT8 dat,UINT8 chip)
{UINT16 temp;GPIOB->BRR = 0xff00;//所有段选拉低,因为是共阴数码管,消影temp = GPIOB->ODR;//读取输出寄存器值,防止修改低八位temp &= 0x001f;//PB5-PB15清零temp |= chip << 5;//写入位选值temp |= (~dat) << 8;//字模是共阳的所以需要取反GPIOB->ODR = temp; //写入输出寄存器。
}
//这个函数给1位的共阳静态数码管,所以不用管位选
void led_write(UINT8 dat)
{LED_PORT->ODR = (dat<<8);
}
显示的抽象
typedef struct{UINT16 cnt; //显示时间计数器UINT16 reload;//显示时间重载值
}LED_TIMER_S;//窗口结构体
typedef struct{UINT8 dat[LED_NUM];//缓冲区UINT8 start;//此窗口显示内容在显存中的起始位置UINT8 len;//窗口长度LED_TIMER_S timer;UINT8 cmd;//转换命令UINT8 attr;//参数,不同场景意义不同void * p;//兼容多种类型参数
}LED_WIN_S;//多窗口
typedef struct {UINT8 Buf[LED_NUM];//显存LED_WIN_S pWin[2];UINT8 cnt;//窗口个数
}LED_S;
一些宏定义
#define LED_NUM 8 // 数码管8个//================转换命令===========================
#define LED_TEXT 0X00 //普通文本显示
#define LED_FLASH 0x01 //闪烁显示
#define LED_FLASH_N 0x02 //多次闪烁
#define LED_DEC 0x03 //倒计时
#define LED_MOVE 0x04 //滚动
#define LED_PIC 0x05
#define LED_PASSWORD 0x06 //显示**#define LED_FLASH_STATUS 0x80
#define FLASH_N 0x3f //attr低6位记录显示次数
#define LED_FLUSH 0x80 //是否闪烁
显示过程梳理
时间片轮询中每100ms调用一次ledMain函数,对每个窗口的显示命令进行解析显示
ledMain–》ledCmd–》
disLedFlash(pLed,winNum);
disLedFlashN(pLed,winNum);
ledDec(pLed,winNum);
ledMove(pLed,winNum);
disLedGif(pLed,winNum);
disLedPassword(pLed,winNum);
disLedText(pLed,winNum);
void ledMain(void *pMsg)
{UINT8 i;LED_S *pLed = (LED_S *)pMsg;for(i=0;i<pLed->cnt;i++){ledCmd(pLed,i);}
}
后台软件具体代码
#include "led.h"
UINT8 LedChars[]={
// 0 1 2 3 4 5 6 7 8 90x03,0x9f,0x25,0x0d,0x99,0x49,0x41,0x1f,0x01,0x09,
// a b c d e f g h i j0x11,0xc1,0x63,0x85,0x61,0x71,0x09,0xd1,0xdf,0x8f,
// k L m n o p q r s t0xe1,0xe3,0x55,0x13,0x03,0x31,0x19,0x73,0x49,0xf1,
// u v w x y z ' ' . - #0x83,0xc7,0x46,0xd9,0x89,0x25,0xff,0xfe,0xfd,0x31
};
const UINT8 MyGif[] =
{9, // 0 :总共有9帧8, // 1 :一帧占8个字节10,10,10,10,10,5,5,5,5, // 2: 图片显示时长0x9f,0x9f,0x9f,0x9f,0x9f,0x9f,0x9f,0x9f, //11 9帧图像数据。0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x99,0x99,0x99,0x99,0x99,0x99,0x99,0x99,0x49,0x49,0x49,0x49,0x49,0x49,0x49,0x49,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x09,0x09,0x09,0x09,0x09,0x09,0x09,0x09
};/*
函数功能:多窗口初始化
参数说明:参数1:返回值:void*/
void Ledinit(LED_S *p){UINT8 i;p->cnt=0x00;for(i=0;i<LED_NUM;i++){p->Buf[i]=0xff;}
}/*
函数功能:添加窗口个数
参数说明:参数1:窗口数组指针参数2:数量返回值:void*/
void setWinNum(LED_S *pLed,UINT8 num){pLed->cnt=num;
}/*
函数功能:显存地址映射
参数说明:参数1:窗口数组指针餐数2:第几个窗口参数3:此窗口在显存的起始地址参数4:窗口长度
返回值:void*/
void setWinSize(LED_S *pLed,UINT8 winNum,UINT8 start,UINT8 len){pLed->pWin[winNum].start=start;pLed->pWin[winNum].len=len;
}/*
函数功能:设置缓冲区到显存的转换命令
参数说明:参数1:窗口数组指针餐数2:第几个窗口参数3:命令参数4:窗口显示时间计数重载值参数5:命令参数1 attr分位表示参数6:命令参数2
返回值:void*/
void setWinCmd(LED_S *pLed,UINT8 winNum,UINT8 cmd,UINT16 cnt,UINT16 reload,UINT8 attr,void *p){pLed->pWin[winNum].cmd=cmd;pLed->pWin[winNum].attr=attr;pLed->pWin[winNum].timer.cnt=cnt;pLed->pWin[winNum].timer.reload=reload;pLed->pWin[winNum].p=p;
}/*
函数功能:窗口显示倒计时
参数说明:参数1:窗口数组指针餐数2:第几个窗口返回值:TRUE:计时到FALSE:计时未到*/
BOOL ledDelay(LED_S *pLed,UINT8 winNum){if(!pLed->pWin[winNum].timer.cnt){//即时到pLed->pWin[winNum].timer.cnt=pLed->pWin[winNum].timer.reload;return TRUE;}pLed->pWin[winNum].timer.cnt--;return FALSE;
}/*
函数功能:窗口命令解析
参数说明:参数1:窗口数组指针餐数2:第几个窗口返回值:void*/
void ledCmd(LED_S *pLed,UINT8 winNum)
{switch(pLed->pWin[winNum].cmd){case LED_FLASH:{disLedFlash(pLed,winNum);break;}case LED_FLASH_N:{disLedFlashN(pLed,winNum);break;}case LED_DEC:{ledDec(pLed,winNum);break;}case LED_MOVE:{ledMove(pLed,winNum);break;}case LED_PIC:{disLedGif(pLed,winNum);break;}case LED_PASSWORD:{disLedPassword(pLed,winNum);break;}default:{disLedText(pLed,winNum);break;}}
}/*
函数功能:解析所有窗口的命令
参数说明:参数1:窗口数组的指针返回值:void
*/
void ledMain(void *pMsg){UINT8 i;LED_S *pLed=(LED_S *)pMsg;for(i=0;i<pLed->cnt;i++){ledCmd(pLed,i);}
}/*
函数功能:查找小数点的位置,并将小数点后的显示内容前移一位
参数说明:参数1:显示内容参数2:显示内容长度返回值:小数点位置
*/
UINT8 findDot(UINT8 *pDat,UINT8 len){UINT8 i,dot=0x00;for(i=0;i<len;i++){if(pDat[i]=='.'){dot=i;break;}}if(dot){for(;i<len;i++){pDat[i]=pDat[i+1];}}return dot;
}/*
函数功能:将整个的显示内容加工成数码管能显示的字模
参数说明:参数1:窗口数组指针餐数2:第几个窗口参数3:格式化字符串参数4:参数列表
返回值:void*/
void writeLed(LED_S *pLed,UINT8 winNum,const char *fmt,...)
{int len,dot;UINT8 *pDat = pLed->pWin[winNum].dat;//拿到缓冲区的指针__va_list arg;//定义vi_list 变量va_start(arg,fmt);//初始化vi_list 变量arg ,使其指向可变参数列表(即...)的的第一参数len = vsprintf((INT8 *)pDat,fmt,arg);//将格式化字符串写入pDat中并拿到写入个数dot = findDot(pDat,len);//找小数点位置toChar(pDat,len);//转字模if(dot){printDot(pDat,dot-1);//字模层面添加小数点}
}/*
函数功能:计算字符在字模LedChars中的位置
参数说明:参数1:字符
返回值:字模位置
*/
UINT8 toIdx(UINT8 dat){if(dat<=0x0f){return dat;//数字0-15对应LedChar的前15个}else if(dat>='0'&& dat<='9'){//字符0-9 也是要显示0-9 也就是相对于字符0的偏移位置return dat-'0';}else if(dat>='a'&&dat<='z'){return dat-'a'+10;//LedChar从低11个开始位a}else if(dat >='A'&& dat<= 'Z'){return dat-'A'+10;}else if(dat=='.'){return 37;}else if(dat == '-'){return 38;}else if(dat == '#'){return 39;}else{return 36;//空格}
}
/*
函数功能:显示内容转字模
参数说明:参数1:显示缓冲指针参数2:长度返回值:void
*/
void toChar(UINT8 *pDat,UINT8 len){UINT8 i,index;for(i=0;i<len;i++){index=toIdx(pDat[i]);pDat[i]=LedChars[index];}
}/*
函数功能:字模层面合并小数点
参数说明:参数1:显示缓冲指针参数2:小数点位置返回值:void
*/
void printDot(UINT8 *pDat,UINT8 dot){if(dot){pDat[dot]^=0x01;//pDat[dot-1]|=0x01;为什么要使用异或,直接或不就行了?}
}/*
函数功能:普通显示,缓冲区数据写入显存。
参数说明:参数1:窗口数组指针参数2:第几个窗口返回值:void
*/
void disLedText(LED_S *pLed,UINT8 winNum){UINT8 i;for(i=0;i<pLed->pWin[winNum].len;i++){//因为有多个窗口只有一个显存,这里显存的下标使用start+i,通过每个窗口的start来分配显存而不是使用i//所以start是次此窗口在显存中的起始位置 pLed->Buf[pLed->pWin[winNum].start+i]=pLed->pWin[winNum].dat[i];//缓冲区数据写到显存}
}/*
函数功能:闪烁多次
参数说明:参数1:窗口数组指针参数2:第几个窗口返回值:void
*/
void disLedFlashN(LED_S *pLed,UINT8 winNum){UINT8 i;if(pLed->pWin[winNum].attr & FLASH_N){if(ledDelay(pLed,winNum)){pLed->pWin[winNum].attr^=LED_FLASH_STATUS;//每次进来翻转一下显示状态if(pLed->pWin[winNum].attr&LED_FLASH_STATUS){//正常显示disLedText(pLed,winNum);}else{//显示为空pLed->pWin[winNum].attr--;//闪烁次数递减for(i=0;i<pLed->pWin[winNum].len;i++){pLed->Buf[pLed->pWin[winNum].start+i]=LedChars[toIdx(' ')];} }} }
}/*
函数功能:闪烁显示
参数说明:参数1:窗口数组指针参数2:第几个窗口
假如一秒闪烁,ledDelay一秒时间到,attr原来是0x80亮 异或0x80就是0x00灭,
下一次计数时间再到,0x00异或0x80为0x80又亮。
返回值:void
*/
void disLedFlash(LED_S *pLed,UINT8 winNum){UINT8 i;if(ledDelay(pLed,winNum)==TRUE){pLed->pWin[winNum].attr^=LED_FLASH_STATUS;//attr第8位记录亮灭状态if(pLed->pWin[winNum].attr&LED_FLASH_STATUS){disLedText(pLed,winNum);}else{for(i=0;i<pLed->pWin[winNum].len;i++){pLed->Buf[pLed->pWin[winNum].start+i]=LedChars[toIdx(' ')];}}}
}/*
函数功能:显示倒计时
参数说明:参数1:窗口数组指针参数2:第几个窗口attr代表的是倒计时时间
解释1:这里为什么不是检测attr!=0?初始化时cnt=0,ledDelay返回TRUE,会立即调用disLedText显示用户给定值初始化是cnt!=0,ledDelay返回FALSE,将延迟一会再显示用户给定值要求用户设定完立即显示所以cnt=0,实际显示 : 3 2 1 0 0->3 2 1 0 ...
attr-- : 2 1 0 ff fe
假如从3开始倒计时,3显示attr--=2,2显示attr--=1,1显示attr--=0,
0开始显示,attr--=0xff,要求0要显示1秒所以下一次还是显示0,attr--=0xfe
所以显示0的判断条件是attr==0xff,attr==0xfe,当attr=0xfe显示一轮结束。如果需要循环显示则
在attr=0xfe时修改attr=3,进行下一轮显示。attr是UINT8类型,表示0-255,显示长度可能为1,2,3,这里使用len填充格式化字符串,自动调整
显示的长度。返回值:void
*/
void ledDec(LED_S *pLed,UINT8 winNum){if(pLed->pWin[winNum].attr !=0xfe){//解释1if(ledDelay(pLed,winNum)==TRUE){//延迟时间到UINT8 fmt[4]="% d";fmt[1]=pLed->pWin[winNum].len +'0';if(pLed->pWin[winNum].attr ==0xff || pLed->pWin[winNum].attr ==0xfe){writeLed(pLed,winNum,(const char *)fmt,0);} else{writeLed(pLed,winNum,(const char *)fmt,pLed->pWin[winNum].attr);}disLedText(pLed,winNum);pLed->pWin[winNum].attr--;} }
}/*
函数功能:滚动显示
参数说明:参数1:窗口数组指针参数2:第几个窗口
注释1:
p[0]:用来记录移动了多少个字符
p[1]:用来记录移动完成返回值:void
*/
void ledMove(LED_S *pLed,UINT8 winNum){UINT8 i;UINT8 temp;UINT8 * p=(UINT8 *)pLed->pWin[winNum].p;//注释1UINT8 winLen=pLed->pWin[winNum].len;//窗口长度if(ledDelay(pLed,winNum)==TRUE){temp=pLed->Buf[pLed->pWin[winNum].start];//拿到显存指针for(i=0;i<winLen;i++){//显存里的内容向前移动一位(实现滚动效果)pLed->Buf[pLed->pWin[winNum].start+i]=pLed->Buf[pLed->pWin[winNum].start+i+1];}//窗口缓冲区中的第一个字符拷贝到本窗口显存的最后一个位置//等于说将缓冲区的头与显存的尾进行连接//类似于spi通信,接收缓冲出去一位,发送缓冲移动一位到接收缓冲的最后一位pLed->Buf[pLed->pWin[winNum].len-1]=pLed->pWin[winNum].dat[0];for(i=0;i<pLed->pWin[winNum].attr-1;i++){//这里attr代表的是滚动个数?pLed->pWin[winNum].dat[i]=pLed->pWin[winNum].dat[i+1];//窗口缓冲区中的内容也全部向前移动一位}//窗口缓冲区最后一位用显存中的第一位递补,这样就将显存的头和缓冲区的尾相连接了pLed->pWin[winNum].dat[pLed->pWin[winNum].attr-1]=temp;p[0]++;//移动次数加一if(p[0]==pLed->pWin[winNum].attr+winLen){p[0]=0x00;p[1]=0x01;//移动一屏结束} }
}/*
函数功能:判断倒计时显示是否结束
参数说明:参数1:窗口数组指针参数2:第几个窗口返回值:TRUE:倒计时显示结束FASLE:倒计时显示未结束
*/
BOOL ledDecOver(LED_S *pLed,UINT8 winNum){return (pLed->pWin[winNum].attr ==0xfe)?TRUE :FALSE;
}
/*
函数功能:显示图片
参数说明:参数1:窗口数组指针参数2:第几个窗口
图片数据结构:用一个数组将图片的信息头和图片的内容数据放在一起。图片信息头包含:1,每张图片的大小,1个字节存储2,图片的帧数,1个字节存储3,图片播放时长,单位是msattr存放当前显示的帧数,void * p存放图像数组的内容
返回值:void
*/
void disLedGif(LED_S *pLed,UINT8 winNum){UINT8 i;UINT8 * p=(UINT8 *)pLed->pWin[winNum].p;//记录一些参数UINT8 frame=p[0];//图像总共几帧UINT8 size =p[1];//一帧占几个字节UINT8 *pDelay=&p[2];//每帧的显示时长,UINT8 *pGif=&p[2+frame];//图片实际内容UINT8 *pBuf=&pLed->Buf[pLed->pWin[winNum].start];//拿到本窗口显存位置UINT8 current = pLed->pWin[winNum].attr;//当前显示帧if(ledDelay(pLed,winNum)==TRUE){pLed->pWin[winNum].timer.cnt=pDelay[current];//显示时长计数器保存当前显示帧的显示时长for(i=0;i<size;i++){//修改显存,实际上就是循环给8个数码管赋值pBuf[i]=pGif[i+current*size];}pLed->pWin[winNum].attr=(current+1)%frame;//跳到下一帧 0-8}
}
/*
函数功能:数码管驱动入口
参数说明:参数1:显示对象数组指针
3ms调用一次
返回值:void
*/
void updataLed(void *pMsg){static UINT8 idx=0x00;LED_S *p =(LED_S *)pMsg;ledDrv(p->Buf[idx],idx);//数码管驱动idx=(idx+1)%8;
}
/*
函数功能:闪烁结束判断
参数说明:参数1:窗口数组指针参数2:第几个窗口返回值:void
*/
BOOL FlashNOver(LED_S *pLed,UINT8 winNum){if(pLed->pWin[winNum].attr){return FALSE;}else{return TRUE;}
}void flushLed(LED_S *pLed,UINT8 winNum){pLed->pWin[winNum].attr|=LED_FLASH;//开启闪烁
}//void disLedPassword(LED_S *pLed,UINT8 winNum)
//{
// UINT8 len,i;
// ABUF_S *pBuf;
// if(pLed->pWin[winNum].attr & LED_FLUSH)
// {
// pBuf = (ABUF_S *)pLed->pWin[winNum].p;
// len = (pBuf->idx <= pLed->pWin[winNum].len)? pBuf->idx:pLed->pWin[winNum].len;
// if(!len)
// {
// for(i=0;i<pLed->pWin[winNum].len;i++)
// {
// pLed->Buf[pLed->pWin[winNum].start+i] = LedChars[toIdx(' ')];
// }
// }
// else
// {
// for(i=0;i<len-1;i++)
// {
// pLed->Buf[pLed->pWin[winNum].start+i] = LedChars[toIdx('-')];
// }
// pLed->Buf[pLed->pWin[winNum].start+i] = LedChars[toIdx(pBuf->dat[len-1])];
// i++;
// for(;i<pLed->pWin[winNum].len;i++)
// {
// pLed->Buf[pLed->pWin[winNum].start+i] = LedChars[toIdx(' ')];
// }
// }
// if(ledDelay(pLed,winNum)== TRUE)
// {
// pLed->Buf[pLed->pWin[winNum].start+len-1] = LedChars[toIdx('-')];
// pLed->pWin[winNum].attr &= ~LED_FLUSH;
// }
// }
//}//=========================以下为单个共阳数码管测试函数==============================
void test(void *pMsg);
void combinationKey(void *pMsg);
使用vsprintf生成显示字模
先看包装过程:
void writeLed(LED_S *pLed,UINT8 winNum,const char *fmt,...)
{int len,dot;UINT8 *pDat = pLed->pWin[winNum].dat;//拿到缓冲区的指针__va_list arg;//定义vi_list 变量va_start(arg,fmt);//初始化vi_list 变量arg ,使其指向可变参数列表(即...)的的第一参数len = vsprintf((INT8 *)pDat,fmt,arg);//将格式化字符串写入pDat中并拿到写入个数dot = findDot(pDat,len);//找小数点位置toChar(pDat,len);//转字模if(dot){printDot(pDat,dot-1);//字模层面添加小数点}
}
vsprintf函数简介
用于将格式化的数据写入一个字符串缓冲区。它的功能与 printf 类似,但是输出不是到标准输出,而是到一个字符数组中。
//函数原型:
int vsprintf(char *str, const char *format, va_list ap);
参数说明:
str:指向一个字符数组的指针,该数组将存储生成的格式化字符串。必须确保该缓冲区足够大以容纳生成的字符串。
format:格式化字符串,指定如何格式化后续参数。格式字符串与 printf 函数的格式字符串相同。ap:一个 va_list 类型的变量,该变量已经由 va_start 初始化,用于访问可变参数列表。
返回值:
如果成功,返回写入的字符数(不包括字符串结尾的空字符);如果发生错误,则返回一个负数。
/*
使用步骤:
定义一个足够大的缓冲区来存储结果字符串。使用 va_list 类型的变量来管理可变参数列表。使用 va_start 初始化 va_list 变量。调用 vsprintf 进行格式化。使用 va_end 清理 va_list 变量。
*/
#include <stdio.h>
#include <stdarg.h>void my_printf(char *format, ...) {char buffer[100];va_list args;va_start(args, format);vsprintf(buffer, format, args);va_end(args);printf("%s", buffer);
}int main() {my_printf("This is a number: %d, and a string: %s\n", 123, "hello");return 0;
}
va_list 是什么
//可以理解为一个指针
typedef struct __va_list {void *__ap;
} va_list;
va_start 是一个宏
#define va_start(ap, parmN) __va_start(ap, parmN)
参数:
ap:va_list 类型的变量
parmN:最后一个已知的固定参数(…之前的参数)
功能:初始化 va_list 对象,使其指向可变参数列表的第一个参数。