很多灯珠驱动芯片,比如用的很多的WS2812,他们都是使用的归零码来进行灯珠驱动,关于归零码的详细介绍,在这里不过多说明,只需要注意一下几点:
1. 单个归零码是由一个高电平和一个低电平组成的,且总是高电平在前。
2. 归零码通过高电平的持续时间和低电平的持续时间(所占整个周期的比例)来判断数据是 1 还是 0。
3. 逻辑1或者0的归零码的高低电平时间总和一般是相等的(如果不相等就稍微麻烦一点,需要不断的切换定时器频率,最好还是选相等的)
1. PWM参数计算
因为PWM波也是方波,符合一个高电平和一个低电平,且定时器频率可调、占空比可调,非常适合我们的归零码生成。
比如我的芯片的归零码要求如下:
可以看到,码元周期是1200ns,所谓的码元周期,也就是对应我们单个PWM的持续长度,或者叫PWM的频率。
这里可以简单换算一下:1200ns = 1.2us = 0.83333kHz, 这个值是个无限循环小数,不是很方便。
但由于数据手册上说码元周期是一个范围,因此我们可以选附近的值取一个整数,比如1250ns。
1250ns = 1.25us = 800kHz。
我的芯片是120MHz的主频,要生成800kHz的PWM的话,则计数周期可以选择149,分频系数选择0,那么可以得到这个等式:
确定好PWM的频率后,接下来就需要确定PWM的占空比。
还是根据数据手册,我们可以计算出 1 码和 0 码对应码元周期的所占比例,也就是占空比了。
当然,我们的占空比都是表示高电平所占整个周期的比例,因为归零码总是以高电平开始。
由于我们的码元周期选择的是1250ns,所以需要给1 码 和 0 码的计算式稍微修改一下。
0 码:
1码:
因为我这个芯片的1 0码的高低电平时间正好是互补的,所以直接采用了简便计算。
现在需要确定具体的比较值,也就是告诉定时器的比较器,小于某个值的时候,输出高电平,大于这个值的时候,输出低电平。
因为我选择的计数周期是149,因此我们可以计算出,对应的0码和1码的定时器的比较值。
0 码:
1码:
这里乘以150是因为计数周期是从0开始,一共有150次计数。
因此,当我们设置比较值为39后,启动定时器,那么就会输出26%的占空比,800kHz的PWM波,对应0码。
2. PWM开关操作
正常生成0 1码PWM后,我们继续接下来的操作。
一个芯片有RGB三个通道,每个通道需要8个Bit来表示颜色,一个Bit对应一个归零码。因此我们控制一个灯珠的颜色就需要发送24个归零码。控制N个就需要N*24个归零码。
由于芯片有一个Reset码,因此我们每个归零码的间隔不能隔太久,否则就会发送失败。
那么有没有办法可以让PWM连续生成不同的归零码呢?
方案一:定时器溢出中断
当定时器计数溢出时,我们可以设置其产生Update中断(或者叫溢出中断),然后在中断函数中设置下一次定时器的比较值。他的代码可能如下:
#define LIGHT_LEN 5
#define CODE1 111
#define CODE0 39
uint8_t Light_Data[24*LIGHT_LEN]; // 保存0 1码数据
uint16_t cnt_val;
void SetValue()
{
Light_Data[0] = CODE0;
Light_Data[1] = CODE1;
...
...
...
cnt_val = 0;
tmr_count_enable(TMRx, TRUE); // 开始发送数据
}
TMR_Update_IRQHandler()
{
// 检查标志位
// 发送数据
tmr_count_enable(TMRx, FALSE);
tmr_channel_value_set(TMRx, Light_Data[cnt_val]); // 设置比较值
tmr_count_enable(TMRx, TRUE);
// 维护归零码计数值
cnt_val += 1;
if(cnt_val == 24*LIGHT_LEN)
tmr_count_enable(TMRx, FALSE);
// 发送完毕, 关闭计时器
// 清除标志位
}
方案二:DMA+定时器自动设置比较值
这种方案也是最主流的方案,在这里我先简短的介绍一下DMA的工作方式。
DMA可以将你存在芯片中的数据(通常是一个数组)传输到DMA的一个缓冲区,之后再将缓冲区的数据给到某个外设的寄存器(比如我们这里定时器的比较值寄存器)
但是注意,将内存中的数据给DMA是一瞬间全部传输的,但是DMA的数据给外设是一帧一帧的传输的,有一个特殊的寄存器可以控制DMA什么时候传输数据给外设。
因此我们只需要着重关注一下什么时候让我们的数组传输到DMA的缓冲区,以及怎么控制DMA什么时候把缓冲区的数据给定时器的比较值寄存器
当然,这里直接说结论了。
①. 当我们开启DMA传输时,数据会传输到DMA缓冲区。
②. 当我们设置DMA请求时,DMA就会将缓冲区里的数据给到外设。
比如,我们开启定时器3,想要在定时器3计数溢出的时候传输一次DMA数据,那么就要开启通道3的TMR3_OVERFLOW通道,也就是定时器溢出。
注意这里不是看TMR3_CHx这个通道,TMR3_CHx是有关TMR输入捕获一类的请求
而发送部分看起来像这样:
void test_tmr3ch1()
{
tmr_counter_enable(TMR3, FALSE);
/* 配置DMA发送数据大小 */
dma_channel_enable(TMR3_OVERFLOW_DMA_CHANNEL, FALSE);
dma_data_number_set(TMR3_OVERFLOW_DMA_CHANNEL, PIXEL_BUFF_SIZE);
dma_channel_enable(TMR3_OVERFLOW_DMA_CHANNEL, TRUE);
/* 开启TMR3, 发送归零码 */
tmr_counter_enable(TMR3, TRUE);
}
因此这个代码的工作情况就像是,开启DMA后,开启定时器。
然后定时器的比较器开始参考DMA里的数据生成对应的0/1码,并在每次溢出后重新去DMA里面拿新的值,直到DMA中的数据清空。
但是这里有一个新的问题,清空后,定时器并没有关闭,还在继续输出最后一个数据的0/1码PWM波,这种情况肯定不是我们想要的。
这里也有几个解决办法,但这两种方法都依赖DMA的标志位:
DMA_FDTx_Flag,可以用于判断DMA是否发送完毕。
①. 在发送函数后,继续判断标志位
使用while(get_flag_status(DMA_FDTx_Flag) != RESET))持续判断,直到DMA发送完毕后,关闭定时器。
这种方法会将程序一直堵死在发送函数中,不是最优解。这个时候,就需要中断出手了。
②. 在发送函数后,在中断中判断标志位
是的,DMA当然有中断,比如 半传输、全传输、错误三个中断。
我们可以判断全传输中断,当传输完毕后,关闭定时器。
void DMA1_Channel3_2_IRQHandler(void)
{
if(dma_flag_get(TMR3_OVERFLOW_DMA_IRQ) != RESET)
{
tmr_counter_enable(TMR3, FALSE);
dma_channel_enable(TMR3_OVERFLOW_DMA_CHANNEL, FALSE);
dma_flag_clear(TMR3_OVERFLOW_DMA_IRQ);
}
}
至此,应该就能稳定的发送归零码了,接下来就可以正常开始编写灯带的驱动代码。
在这里给出我的DMA配置函数:
#define TMR3_OVERFLOW_DMA_CHANNEL DMA1_CHANNEL3
#define TMR3_OVERFLOW_DMA_IRQ DMA1_FDT3_FLAG
void DMA1_Init()
{
dma_init_type dma_init_struct;
crm_periph_clock_enable(CRM_DMA1_PERIPH_CLOCK, TRUE);
// --- TMR3 - CH1
dma_reset(TMR3_OVERFLOW_DMA_CHANNEL);
dma_default_para_init(&dma_init_struct);
dma_init_struct.buffer_size = PIXEL_BUFF_SIZE;
dma_init_struct.direction = DMA_DIR_MEMORY_TO_PERIPHERAL;
dma_init_struct.memory_base_addr = (uint32_t)PIXEL_BUFFER;
dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_HALFWORD;
dma_init_struct.memory_inc_enable = TRUE;
dma_init_struct.peripheral_base_addr = (uint32_t)&TMR3->c1dt;
dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_HALFWORD;
dma_init_struct.peripheral_inc_enable = FALSE;
dma_init_struct.priority = DMA_PRIORITY_MEDIUM;
dma_init_struct.loop_mode_enable = FALSE;
dma_init(TMR3_OVERFLOW_DMA_CHANNEL, &dma_init_struct);
nvic_irq_enable(DMA1_Channel3_2_IRQn, 0, 0);
dma_flag_clear(TMR3_OVERFLOW_DMA_IRQ);
dma_interrupt_enable(TMR3_OVERFLOW_DMA_CHANNEL, DMA_FDT_INT, TRUE);
dma_channel_enable(TMR3_OVERFLOW_DMA_CHANNEL, TRUE);
}