雅特力AT32F421 DMA+TMR发送变换PWM波模拟归零码

很多灯珠驱动芯片,比如用的很多的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,那么可以得到这个等式:

0.8MHz = \frac{120MHz}{(0+1)\cdot (149+1)}

确定好PWM的频率后,接下来就需要确定PWM的占空比。

还是根据数据手册,我们可以计算出 1 码和 0 码对应码元周期的所占比例,也就是占空比了。

当然,我们的占空比都是表示高电平所占整个周期的比例,因为归零码总是以高电平开始。

由于我们的码元周期选择的是1250ns,所以需要给1 码 和 0 码的计算式稍微修改一下。

0 码:\frac{300+25}{1200+50}=26\%

1码:100\%-26\%=74\%

因为我这个芯片的1 0码的高低电平时间正好是互补的,所以直接采用了简便计算。

现在需要确定具体的比较值,也就是告诉定时器的比较器,小于某个值的时候,输出高电平,大于这个值的时候,输出低电平。

因为我选择的计数周期是149,因此我们可以计算出,对应的0码和1码的定时器的比较值。

0 码:26\%\times150=39

1码:150-39=111

这里乘以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);
    

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值