用单片机并行驱动HUB75全彩单元板#HUB75并行驱动#P2.5全彩单元板驱动#DMA+PWM+GPIO
1.前言
用单片机驱动HUB75接口的全彩单元板,需要单片机与HUB75接口连接的控制引脚一直维持HUB75时序,通俗来说就是这种 点阵屏幕 需要单片机引脚一直刷新才能正常显示。我们知道,绝大多数单片机都是没有HUB75这个外设的,那么要想让单片机的GPIO一直产生HUB75时序,就需要CPU不断的操作GPIO,这样非常占用单片机资源,并且无法驱动分辨率更大的屏幕。本文浅谈用单片机的GPIO+DMA+PWM来模拟出HUB75这个外设,用这种方法驱动HUB75全彩单元板,相当于单片机有了一个专门的HUB75外设来维持HUB75时序,这样CPU就得到了解放,可以去做更多的事情。
2.P2.5全彩单元板介绍
市面上常见的P2.5全彩单元板,分辨率都是128×64,但是单元板上的控制芯片分为普刷和高刷两种,高刷屏幕虽然也是HUB75接口,但是控制时序更复杂,且各家时序都不一样(并且不公开),普刷屏幕一般来说是通用的,本文只研究普刷P2.5全彩单元板。
单元板控制接口如下图(HUB75接口):
行地址选择引脚:A、B、C、D、E
当A、B、C、D、E 分别为 0、0、0、0、0时选中的是屏幕的第1行和第33行
当A、B、C、D、E 分别为 1、0、0、0、0时选中的是屏幕的第2行和第34行
依次类推直到
当A、B、C、D、E 分别为 1、1、1、1、1时选中的是屏幕的第32行和第64行
颜色数据引脚:R1、G1、B1、R2、G2、B2
R1、G1、B1负责上半屏的颜色(第1行~第32行),R2、G2、B2负责下半屏的颜色(第33行~第64行)
数据锁存引脚:LAT
每传输完1行的颜色数据,产生一个脉冲信号,将颜色数据锁存,然后开始下1行的颜色数据传输(实际上,同一时刻是有2行的颜色数据在传输的,因为行地址选择引脚一次性是选中两行,比如当A、B、C、D、E 分别为 0、0、0、0、0时选中的是屏幕的第1行和第33行)
显示使能引脚:OE
低电平开启显示。这种普刷单元板上的每一个像素都是一个RGB灯珠,灯珠的控制引脚只能输出高电平,或者低电平,不能向DAC一样输出一个可变的电压,这样就决定了R、G、B三个颜色的亮度是不能控制的,只能全灭或者全亮,这就意味着只能实现七彩显示RGB111。通过这个OE引脚能实现全彩显示实现RGB555或者RGB888。
时钟引脚:CLK
每传输完一个像素点的颜色数据,产生一个脉冲信号。(同上面一样,同一时刻也是有两个像素点在传输数据的)
以上就是普刷P2.5全彩单元板HUB75接口介绍。
3.P2.5全彩单元板驱动
本文重点是并行驱动,为了更好的理解,先介绍一下串行驱动,也就是用单片机操作GPIO来模拟HUB75时序。
3.1.串行驱动
串行驱动步骤如下
static uint8_t Gray,j=0;
for(j=0;j<32;){//整个屏幕64行,一次刷新2行,32次刷完整个屏幕
Display_OE(1);//关闭显示,消影
/* 行选 */
Gray=(j>>1)^j;
Display_E(Gray&0x10);//操作GPIO引脚
Display_D(Gray&0x08);
Display_C(Gray&0x04);
Display_B(Gray&0x02);
Display_A(Gray&0x01);
/* 每一行有128个像素点 */
for(uint8_t i=0;i<128;i++)
{
Display_R1(Display_ScanBuf[i][31-Gray]&0x1);//操作GPIO引脚
Display_G1(Display_ScanBuf[i][31-Gray]&0x2);
Display_B1(Display_ScanBuf[i][31-Gray]&0x4);
Display_R2(Display_ScanBuf[i][15-Gray]&0x1);
Display_G2(Display_ScanBuf[i][15-Gray]&0x2);
Display_B2(Display_ScanBuf[i][15-Gray]&0x4);
Display_CLK(1);
Display_CLK(0);//CLK脉冲
}
j++;
Display_LAT(1);
Display_LAT(0);//锁存数据
Display_OE(0);//打开显示
}
首先控制OE引脚关闭显示;
然后控制A、B、C、D、E这五个引脚选中2行;
再控制R1、G1、B1引脚将颜色数据传给上半屏选中的行,同时控制R2、G2、B2将颜色数据传给下半屏选中的行,如此循环128次,直至被选中的行的128个像素点都获得颜色数据;
要想保持显示,单片机需要如此往复一直执行,这也只是勉强实现了RGB111。
3.2.DMA+GPIO并行驱动
串行驱动是按一定顺序先后控制与HUB75接口连接的14个信号引脚(一次只能控制一个引脚),并行驱动就是一次性控制与HUB75接口连接的14个信号引脚(一次控制14个引脚),虽然一次性控制14个引脚能提高效率,但是仍然需要CPU来执行。怎么才能将CPU从刷屏中彻底释放出来呢?
我们可以用 定时器的更新事件 来触发 DMA 从内存中搬运数据到GPIO外设的ODR寄存器中,整个过程没有产生任何中断,所以不需要CPU来干预,我们只需要提前初始化好内存中的数据,就能使GPIOx端口的16个引脚产生特定时序的电平,这个时序可以是绝大多数方波信号的时序,比如II2、SPI等,也可以是HUB75时序。
先简单模拟一下上述方案是否可行,这里以HC32F4A0PITB单片机为例:
先对DMA 初始化,我们需要设置DMA1通道3的触发源为TIM2的更新事件;然后设置目标地址为GPIOD端口的ODR寄存器、设置源地址为一个16bit的数组;设置目标地址不递增、源地址递增具体程序如下:
//源地址
uint16_t u32SrcBuf[2] = {0x0,0xffff};
//目标地址
//CM_GPIO->PODRD
static void DmaInit(void){
stc_dma_init_t stcDmaInit;
stc_dma_repeat_init_t stcDmaRepeatInit;
/* DMA/AOS/TMR2 FCG enable */
FCG_Fcg0PeriphClockCmd(FCG0_PERIPH_DMA1|FCG0_PERIPH_AOS, ENABLE);//开DMA、AOS时钟
AOS_SetTriggerEventSrc(DMA_TRIGGER_CH, EVT_SRC_TMR2_4_CMP_A);//设置DMA触发源
(void)DMA_StructInit(&stcDmaInit);
stcDmaInit.u32IntEn = DMA_INT_DISABLE;
stcDmaInit.u32BlockSize = DMA_BC;//块的大小
stcDmaInit.u32TransCount = DMA_TC;//传输次数,0为无限传输
stcDmaInit.u32DataWidth = DMA_DW;//数据大小16bit
stcDmaInit.u32DestAddr = (uint32_t)(&CM_GPIO->PODRD);//目标地址
stcDmaInit.u32SrcAddr = (uint32_t)(u32SrcBuf);//源地址
stcDmaInit.u32SrcAddrInc = DMA_SRC_ADDR_INC;//源地址递增
stcDmaInit.u32DestAddrInc = DMA_DEST_ADDR_FIX;//目标地址不递增
(void)DMA_Init(DMA_UNIT, DMA_CH, &stcDmaInit);
(void)DMA_RepeatStructInit(&stcDmaRepeatInit);
stcDmaRepeatInit.u32Mode = DMA_RPT_SRC;//源地址重复,目标地址不重复
stcDmaRepeatInit.u32SrcCount = 2;
(void)DMA_RepeatInit(DMA_UNIT, DMA_CH, &stcDmaRepeatInit);
/* DMA module enable */
DMA_Cmd(DMA_UNIT, ENABLE);
/* DMA channel enable */
(void)DMA_ChCmd(DMA_UNIT, DMA_CH, ENABLE);
}
然后初始化TIM2,使其更新事件发生的频率为1MHZ
static void Tmr2Config(void){
stc_tmr2_init_t stcTmr2Init;
/* 1. Enable Timer2 peripheral clock. */
FCG_Fcg2PeriphClockCmd(TMR2_PERIPH_CLK, ENABLE);
/* 2. Set a default initialization value for stcTmr2Init. */
(void)TMR2_StructInit(&stcTmr2Init);
/* 3. Modifies the initialization values depends on the application. */
stcTmr2Init.u32ClockSrc = TMR2_CLK_SRC;
stcTmr2Init.u32ClockDiv = TMR2_CLK_DIV;
stcTmr2Init.u32Func = TMR2_FUNC_CMP;//功能选择
stcTmr2Init.u32CompareValue = TMR2_CMP_VAL;
(void)TMR2_Init(TMR2_UNIT, TMR2_CH, &stcTmr2Init);
}
最后开启TIM2
/* 开启TMR2触发DMA传输 */
TMR2_Start(TMR2_UNIT, TMR2_CH);
这样DMA就会以1MHZ的频率将u32SrcBuf这个数组中的值一个一个的搬运到GPIOD的ODR寄存器中,在DMA初始化中,我们有设置源地址重复次数为2,意思就是DMA每搬运2次数据,地址就重复为之前设置的源地址,这样只要TIM2在运行,DMA就会一直循环往复的更新GPIOD的ODR寄存器中的值。于是在没有CPU干预的情况下,GPIOD端口的16个引脚上都产生了一个频率为500KHZ的方波信号(GPIOD端口不要忘记初始化),为什么是500KHZ而不是1MHZ,这个我就不解释了,细细想一下就能想明白。
如果我们将上述程序中源地址数组中的值改为
uint16_t u32SrcBuf[4] = {0x0000,0x0001,0x8000,0x8001};
然后运行程序,现象是PD1引脚上产生一个500KHZ的方波,PD15引脚上产生一个250KHZ的方波,其他引脚都是低电平。
到这里,如果你能搞懂上述现象产生的原因,那么你便能理解 可以通过控制源地址数组的长度和数组中的值,来模拟出HUB75时序;关于源地址数组的初始化到后面再讲,因为里面涉及到全彩(RGB555)如何实现。
所以先来说一下如何在这个理论上只能实现七彩(RGB111)显示的屏幕上实现RGB555;上文有说到,每个像素灯珠 三个颜色的控制引脚都只能输出高低电平,不能控制R、G、B三个颜色的亮度,只能全灭或者全亮;但是有一个OE引脚,可以控制屏幕是否显示,所以我们可以通过控制OE引脚的占空比来控制整个屏幕显示的亮度,因为OE是低电平使能,所以低电平的占空比越大屏幕显示就越亮。我们在极短时间内连续将整个屏幕刷新五次,使第一次刷新时亮度最低 为1、第二次亮度为2、第三次亮度为4、第四次亮度为8、第五次亮度最亮 为16,这样在这极端时间内呈现出来的画面肉眼看上去就是RGB555的全彩画面。也就是完整的一帧画面,需要将屏幕刷新五遍,如下图所示
RGB555完整的一帧由RGB555_Bit0、RGB555_Bit1、RGB555_Bit2、RGB555_Bit3、RGB555_Bit4组成,每个RGB555_Bit都是将整个屏幕刷新一遍,不同的Bit亮度不一样(控制OE来实现)。
接下来开始讲解如何 初始化源地址数组
首先我们得确定源地址数组的长度,HUB75时序中最基本的就是CLK脉冲,其他信号都是伴随CLK脉冲产生的,所以需要的CLK脉冲个数就决定了这个源地址数组的长度,产生一个脉冲,DMA需要搬运两次数据,第一次搬运使CLK为低电平,第二次搬运使CLK为高电平,这样两次才能产生一个脉冲,所以需要的数组长度就等于 将整个屏幕完整的刷新一帧所需要的CLK脉冲个数 乘以2。
上面说到,要实现RGB555的话,将整个屏幕完整的刷新一帧,需要将整个屏幕刷新5次,屏幕总共64行,分32次刷完,因为一次刷新2行,每一行有128个像素点,刷新一个像素点就需要一个脉冲,所以将整个屏幕刷新一次需要128*32个CLK脉冲,所以实现一帧RGB555需要128*32*5个CLK脉冲,所以数组的长度就等于 128*32*5*2 = 40960;因为数组是uint16_t类型的,所以需要的RAM大小就是40960*2=81920字节=80kb(DMA+GPIO的方法需要占用80kb的RAM,后面讲到的DMA+GPIO+PWM的方法可以节省一半内存)
确定好数组长度后,开始初始化数组的内容;首先确定一下引脚分配,如下表(如果引脚分配不一样,则初始化数组的方法就需要改动)
第一步:将数组中的数据清零
//:数据全部置0
for(uint32_t i=0;i<hub75_data_size;i++)HUB75_SrcBuf[i] = 0;
第二步:初始化CLK引脚的时序
//:初始化CLK-》PD0 时序
n = 0;
for(uint32_t i=0;i<hub75_data_size;i++){
if(i%2)
HUB75_SrcBuf[i] &=~(1<<n);
else HUB75_SrcBuf[i] |= (1<<n);
}
因为CLK引脚连接在PD0上,所以HUB75_SrcBuf数组中的每一个数据的第0位决定CLK引脚的电平
第三步:初始化A、B、C、D、E 时序
//:初始化A,B,C,D,E -》PD1,PD2,,,PD5 时序
n = 1;
for(uint32_t i=0;i<hub75_data_size;i++){
HUB75_SrcBuf[i] |= ((((i/256)+31)% 32)<<n);
}
因为在引脚分配时,A、B、C、D、E引脚分别对应GPIOD端口的bit1、bit2、bit3、bit4、bit5,是连续的,所以初始化时就比较容易实现;需要注意的是A、B、C、D、E这5个地址线是每128个像素点加1,而在数组中,128个像素点需要256个数据,这也是上述公式中"/256"而不是"/128"的原因
第四步:初始化LAT时序
//:初始化LAT -》PD6 时序
n = 6;
for(uint32_t i=0;i<hub75_data_size;i++){
if(i%256) HUB75_SrcBuf[i] &= ~(1<<n);
else{
HUB75_SrcBuf[i] |= (1<<n);
}
}
跟地址线类似,LAT引脚也是128个像素点产生一次高电平,对应到数组中就是256个数据;因为LAT连接在PD6上,所以n=6。
第五步:初始化OE 时序
OE的时序至关重要,因为RGB555就是通过OE来模拟出来的,所以OE的时序就决定了RGB555的显示效果,调的好,显示效果就好。
//:初始化OE -》PD7 时序
n = 7;
uint32_t cont = 0;
uint16_t a = 0;
for(uint8_t i=0;i<5;i++)
for(uint16_t ii=0;ii<32;ii++)
for(uint16_t iii=0;iii<256;iii++){
if(i == 0){
a = 127+123;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}else if(i == 1){
a = 127+100;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}else if(i == 2){
a = 127+80;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}else if(i == 3){
a = 117+50;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}else if(i == 4){
a = 20+15;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}
cont++;
}
调整变量a的值,就能调整RGB555中第1遍、第2遍、、、第5遍的亮度;
if((iii < (256-a/2)) && (iii>a/2) )这个判断是为了使OE的低电平保持在时序的中间,从而达到更好的显示效果。
到这里HUB75的时序就已经出来了,但是我们还不知道精准的控制每一个像素点的颜色。
第六步:打点函数的实现
一个RGB555颜色数据是没法直接放到源地址数组中的,需要对颜色数据逐位处理,如果该位为一,就将源地址数组中对应的数据置1,否则就置0
void drawPoint(uint16_t x,uint16_t y,uint16_t color___){
uint32_t addr = ((x+1)<<1 ) + ((y%32)<<8);
addr %= hub75_data_size;
uint8_t ii=0;
uint32_t var = 0;
for(uint8_t i=0;i<5;i++){
if(y<31){//亮度梯度01234
var = addr + 8192*i;
if((color___ & 0x400) != 0)(HUB75_SrcBuf[var] |= 0x2000);else (HUB75_SrcBuf[var] &= ~0x2000);//R1->PD13
if((color___ & 0x20) != 0)(HUB75_SrcBuf[var] |= 0x1000);else (HUB75_SrcBuf[var] &= ~0x1000);//G1->PD12
if((color___ & 0x1) != 0)(HUB75_SrcBuf[var] |= 0x800);else (HUB75_SrcBuf[var] &= ~0x800);//B1->PD11
}else if(y==31){//亮度梯度12340
ii = i-1;
if(ii>4) ii = 4;
var = addr + 8192*ii;
if((color___ & 0x400) != 0)(HUB75_SrcBuf[var] |= 0x2000);else (HUB75_SrcBuf[var] &= ~0x2000);//R1->PD13
if((color___ & 0x20) != 0)(HUB75_SrcBuf[var] |= 0x1000);else (HUB75_SrcBuf[var] &= ~0x1000);//G1->PD12
if((color___ & 0x1) != 0)(HUB75_SrcBuf[var] |= 0x800);else (HUB75_SrcBuf[var] &= ~0x800);//B1->PD11
}else if(y<63){//亮度梯度01234
var = addr + 8192*i;
if(((color___)&0x400) != 0)(HUB75_SrcBuf[var] |= 0x400);else (HUB75_SrcBuf[var] &= ~0x400);//R2->PD10
if(((color___)&0x20) != 0)(HUB75_SrcBuf[var] |= 0x200);else (HUB75_SrcBuf[var] &= ~0x200);//G2->PD9
if(((color___)&0x1) != 0)(HUB75_SrcBuf[var] |= 0x100);else (HUB75_SrcBuf[var] &= ~0x100);//B2->PD8
}else {//亮度梯度12340
ii = i-1;
if(ii>4) ii = 4;
var = addr + 8192*ii;
if((color___ & 0x400) != 0)(HUB75_SrcBuf[var] |= 0x400);else (HUB75_SrcBuf[var] &= ~0x400);//R2->PD10
if((color___ & 0x20) != 0)(HUB75_SrcBuf[var] |= 0x200);else (HUB75_SrcBuf[var] &= ~0x200);//G2->PD9
if((color___ & 0x1) != 0)(HUB75_SrcBuf[var] |= 0x100);else (HUB75_SrcBuf[var] &= ~0x100);//B2->PD8
}
color___ >>= 1;
}
}
至此就实现了DMA+GPIO并行驱动P2.5全彩单元板,CPU不需要任何刷屏操作,只需要调用打点函数就行了。
附一张HUB75抓波图
驱动效果
3.3.DMA+PWM+GPIO并行驱动
上一种方法是用定时器的更新事件来触发DMA搬运数据,CLK引脚的脉冲信号也由GPIOD端口的引脚产生,所以就导致一个CLK脉冲需要DMA搬运两次数据才能产生;既然用到了定时器,那么肯定可以在触发DMA搬运的同时,在定时器的一个通道上产生PWM波(可能有些垃圾单片机上实现不了),我们将这个PWM波连到CLK引脚上,这样定时器产生一次更新事件的同时,就能产生一个CLK脉冲,同时触发DMA搬运一次数据,所以需要的源地址数组长度就减小了一般,只需要128*32*5 = 20480,所以就只需要所20480*2=40960字节=40kb RAM。
初始化源地址数组
第一步:将数组中的数据清零
//:数据全部置0
for(i=0;i<hub75_data_size;i++)HUB75_SrcBuf[i] = 0;
第二步:初始化CLK引脚的时序
//CLK时序已经不需要初始化了,由定时器的PWM通道产生
第三步:初始化A、B、C、D、E 时序
//:初始化A,B,C,D,E -》PD1,PD2,,,PD5 时序
n = 1;
for(i=0;i<hub75_data_size;i++){
HUB75_SrcBuf[i] |= ((((i/128)+30)% 32)<<n);
}
第四步:初始化LAT时序
//:初始化LAT -》PD6 时序
n = 6;
for(i=0;i<hub75_data_size;i++)
{
if(i%128) //256
HUB75_SrcBuf[i] &= ~(1<<n);
else
{
HUB75_SrcBuf[i] |= (1<<n);
}
}
第五步:初始化OE 时序
//:初始化OE -》PD7 时序
n = 7;
for(i=0;i<5;i++)
{
for(ii=0;ii<16;ii++)
{
for(iii=0;iii<256;iii++) //128
{
if(i == 0)
{
a = (127+123+4*(255-cTotalBrightness)/255)/1;
//a = 124*2; 亮度会降低,电流降低
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}
else if(i == 1)
{
a = (127+100+23*(255-cTotalBrightness)/255)/1;
//a = 120*2;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}
else if(i == 2)
{
a = (127+80+40*(255-cTotalBrightness)/255)/1;
//a = 112*2;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}
else if(i == 3)
{
a = (127+40+75*(255-cTotalBrightness)/255)/1;
//a = 96*2;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}
else if(i == 4)
{
a = (20+15+190*(255-cTotalBrightness)/255)/1;
//a = 32*2;
if((iii < (256-a/2)) && (iii>a/2) )HUB75_SrcBuf[cont] &=~ (1<<n);
else HUB75_SrcBuf[cont] |= (1<<n);
}
cont++; // if(cont == 128) cont = 0;
}
}
}
第六步:打点函数的实现
void drawPoint(uint16_t x1,uint16_t y1,uint16_t color___){
uint16_t i = 0,ii = 0;
uint16_t x =x1;
uint16_t y =y1;
uint32_t addr = ((x+1)<<0 ) + (y%32<<7);
addr %= hub75_data_size;
for(i=0;i<5;i++)
{//亮度梯度01234
if(y<31){ //条件 ? 表达式1 : 表达式2 条件成立跳转表达式1 ,否则跳转到表达式2
(color___ & 0x400) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] |= (1<<13)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] &= ~(1<<13)); //R1->PB13 0x400 红色最低亮度
(color___ & 0x20) != 0?(LedPixelData[addr + ADDR_INC_MUL*i] |= (1<<12)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] &= ~(1<<12)); //G1->PB12 0x20 绿色最低亮度
(color___ & 0x1) != 0?(LedPixelData[addr + ADDR_INC_MUL*i] |= (1<<11)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] &= ~(1<<11)); //B1->PB11 0x01 蓝色最低亮度
}else if(y==31){//亮度梯度12340
ii = i-1;
if(ii>4) ii = 4;
(color___ & 0x400) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] |= (1<<13)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] &= ~(1<<13));//R1->PB13
(color___ & 0x20) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] |= (1<<12)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] &= ~(1<<12)); //G1->PB12
(color___ & 0x1) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] |= (1<<11)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] &= ~(1<<11)); //B1->PB11
}else if(y<63){//亮度梯度01234
((color___)&0x400) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] |= (1<<10)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] &= ~(1<<10)); //R2->PB10
((color___)&0x20) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] |= (1<<9)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] &= ~(1<<9)); //G2->PB9
((color___)&0x1) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] |= (1<<8)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*i] &= ~(1<<8)); //B2->PB8
}else {//亮度梯度12340
ii = i-1;
if(ii>4) ii = 4;
(color___ & 0x400) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] |= (1<<10)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] &= ~(1<<10));//R2->PB10
(color___ & 0x20) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] |= (1<<9)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] &= ~(1<<9)); //G2->PB9
(color___ & 0x1) != 0?(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] |= (1<<8)):(HUB75_SrcBuf[addr + ADDR_INC_MUL*ii] &= ~(1<<8)); //B2->PB8
}
color___ >>= 1;
}
}
3.4.别用HC32F4PATB单片机来模拟HUB75时序
HC32F4PATB这个单片机的DMA功能设计的非常灵活,能实现在搬运的时候进行地址偏移,能灵活的控制一次传所输搬运的数据块的大小等等,但是用来控制DMA地址重复次数的数据位宽只有10bit,如下图
也就是我们想要的,搬运20480次或者40960次后重复为源地址,这个无法在这个单片机上实现。
对应的程序如下
static void DmaInit(void){
stc_dma_repeat_init_t stcDmaRepeatInit;
stc_dma_init_t stcDmaInit;
/* DMA/AOS/TMR2 FCG enable */
FCG_Fcg0PeriphClockCmd(FCG0_PERIPH_DMA1|FCG0_PERIPH_AOS, ENABLE);//开DMA、AOS时钟
AOS_SetTriggerEventSrc(DMA_TRIGGER_CH, EVT_SRC_TMR2_4_CMP_A);//设置DMA触发源
(void)DMA_StructInit(&stcDmaInit);
stcDmaInit.u32IntEn = DMA_INT_DISABLE;//关闭中断
stcDmaInit.u32BlockSize = 1;//块的大小
stcDmaInit.u32TransCount = 0;//重复传输,只要有触发信号就能一直传输
stcDmaInit.u32DataWidth = DMA_DW;//数据大小16bit
stcDmaInit.u32DestAddr = (uint32_t)(&CM_GPIO->PODRD);//目标地址
stcDmaInit.u32SrcAddr = (uint32_t)(HUB75_SrcBuf);//源地址
stcDmaInit.u32SrcAddrInc = DMA_SRC_ADDR_INC;//源地址递增
stcDmaInit.u32DestAddrInc = DMA_DEST_ADDR_FIX;//目标地址不递增
(void)DMA_Init(DMA_UNIT, DMA_CH, &stcDmaInit);
(void)DMA_RepeatStructInit(&stcDmaRepeatInit);
stcDmaRepeatInit.u32Mode = DMA_RPT_SRC;//源地址重复,目标地址不重复
stcDmaRepeatInit.u32SrcCount = 40960;//源地址每40960次就重复一次
(void)DMA_RepeatInit(DMA_UNIT, DMA_CH, &stcDmaRepeatInit);
/* DMA module enable */
DMA_Cmd(DMA_UNIT, ENABLE);
/* DMA channel enable */
(void)DMA_ChCmd(DMA_UNIT, DMA_CH, ENABLE);
}
上述程序中的下面行是实现不了的,因为它最大不能超过1023,为0时,代表最大1024
stcDmaRepeatInit.u32SrcCount = 40960;//源地址每40960次就重复一次
后面又继续深入研究了一下这个DMA的地址偏移功能,发现也是无法实现我们想要的功能。
所以用这个单片机无法实现CPU不干预的情况下实现HUB75时序
要想用这个单片机实现HUB75时序,只能加入DMA传输完成中断,每传输完40960或者20480个数据后,触发中断,在中断函数中重新开启DMA传输,这样CPU就需要处理一个中断才能实现HUB75时序,配置如下
stc_dma_init_t stcDmaInit;
void DMA1_CH3_TransEnd_IrqCallback(void){
DMA_Init(DMA_UNIT, DMA_CH, &stcDmaInit);
DMA_ChCmd(DMA_UNIT, DMA_CH, ENABLE);
}
static void DmaInit(void){
stc_irq_signin_config_t stcIrqSignConfig;
/* DMA/AOS/TMR2 FCG enable */
FCG_Fcg0PeriphClockCmd(FCG0_PERIPH_DMA1|FCG0_PERIPH_AOS, ENABLE);//开DMA、AOS时钟
AOS_SetTriggerEventSrc(DMA_TRIGGER_CH, EVT_SRC_TMR2_4_CMP_A);//设置DMA触发源
(void)DMA_StructInit(&stcDmaInit);
stcDmaInit.u32IntEn = DMA_INT_ENABLE;//中断使能
stcDmaInit.u32BlockSize = 1;//块的大小
stcDmaInit.u32TransCount = 40960;//40960个触发信号后,传输停止,需要重新打开
stcDmaInit.u32DataWidth = DMA_DW;//数据大小16bit
stcDmaInit.u32DestAddr = (uint32_t)(&CM_GPIO->PODRD);//目标地址
stcDmaInit.u32SrcAddr = (uint32_t)(HUB75_SrcBuf);//源地址
stcDmaInit.u32SrcAddrInc = DMA_SRC_ADDR_INC;//源地址递增
stcDmaInit.u32DestAddrInc = DMA_DEST_ADDR_FIX;//目标地址不递增
(void)DMA_Init(DMA_UNIT, DMA_CH, &stcDmaInit);
stcIrqSignConfig.enIntSrc = DMA_INT_SRC;
stcIrqSignConfig.enIRQn = DMA_IRQn;
stcIrqSignConfig.pfnCallback = &DMA1_CH3_TransEnd_IrqCallback;
(void)INTC_IrqSignIn(&stcIrqSignConfig);
DMA_ClearTransCompleteStatus(DMA_UNIT, DMA_FLAG_TC_CH3);
/* NVIC setting */
NVIC_ClearPendingIRQ(DMA_IRQn);
NVIC_SetPriority(DMA_IRQn, DDL_IRQ_PRIO_03);//中断优先级设置
NVIC_EnableIRQ(DMA_IRQn);
/* DMA module enable */
DMA_Cmd(DMA_UNIT, ENABLE);
/* DMA channel enable */
(void)DMA_ChCmd(DMA_UNIT, DMA_CH, ENABLE);
}
最后附一篇先楫半导体使用DMA+GPIO的推文:经验分享 | HPMicro定制化波形–DMA推GPIO指南
作者:qq_51178659