PWM的原理我这里就不再说了,脉冲宽度调制,通过改变周期和产空比满足负载不同的功率需求。
I.MX6UL的PWM功能
I.MX6UL的PWM和处理器内核对接的时候遵循外设总线协议,PWM和其他模块之间只有时钟信号(CCM模块)和重启信号(SRC模块)相关(还有中断处理),还有一个单独的输出信号。功能特性如下:
- 16位的向上计数器、时钟源可选择。
- 4X16位的FIFO,可以降低中断资源
- 可配置的输出高低电平方式
- 可以生产回滚或比较中断
I.MX6UL提供了8组PWM,每组的输出信号可以在几个引脚之间通过复用配置选择。,整体的框架结构如下:
I.MX6UL的PWM产生流程是这样的:
时钟选择器决定了计数器的工作频率,和前面一样,我们使用外设时钟周期(IPG_CLK,66MHz)
12位分频器,可以实现1~4097分频,假设我们选用66分频,每次counter计数的时候就是1us
Period Resgiter,周期寄存器,16位,当计数器的值和周期寄存器的值相等时,产生中断(这个中断可以不生成,类似于溢出中断)。计数器值等于周期,从0开始重新计数。
Sample Register,采样寄存器,16位。这个寄存器的值决定信号的占空比。这个比较重要,我们后面单独讲一下。但是意思就是计数器的值和SampleRegister的值相等时信号反转。
SampleRegister和FIFO
占空比是通过SampleRegister交给FIFO的,注意FIFO深度是4组,实际信号的占空比是通过FIFO的值去确定的,所以FIFO需要不停的写入新的值才能不断的生成新的信号。FIFO在任何时间都可以进行写操作,但要读取值必须在PWM使能的条件下。因为FIFO的深度是4,写入数据的时候要注意防止上溢,否则FWE异常(FIFO Write Error)。同理,我们还要检测FIFO的内部元素数量,防止FIFO空了无法生成新的信号。这个过程可以通过中断产生,当FIFO内元素低于指定值就可以生成中断,中断中给FIFO写入新的数据。还有几个注意
- 我们只要对SampleRegister进行读操作,FIFO数据就会减少;
- PWM在被Disable后,FIFO的元素就不再减少了;
- 如果PWM被进行软复位,FIFO里所有内容会被清除
回滚和比较事件
当计数器到PWM_PR寄存器+2的值时会重置到0然后重新开始计数。这个过程和定时中断的计数器一样。这个事件可以当做一次回滚,回滚时输出可以根据设置去置0、1或无反应;这个过程也可以产生一个中断(前提是使能中断),当计数器值累计到sample值,输出状态会按设置进行更改,这是个比较事件,也可以触发一次中断。总之就是信号在到Period的时候反转一下,到Sample值时候再反转一下, 构成一个信号周期。
寄存器说明
I.MX6UL提供了8组PWM,每组使用6个寄存器
其中CNR、PR和SAR是3个16位的寄存器,保存了计数器、Sample和Period的值,后面不在讲了 ,剩下的我们再看看
PWMCR
控制寄存器,
FWM[27:26]:FIFO剩余多少会触发FIFO空中断,一般设置为2,如果1的话比较危险,可能会FIFO空报错
POUTC[19:18]:输出配置,设置在回滚和比较事件时输出状态
CLKSRC[17:16]:时钟源,我们一般选择外设时钟源,值为01
SWR[3]:软复位,设置为1时软复位,复位完成后自动回0
REPEAT[2:1]:Sample重复,可以设置每个Sample值重复使用的次数
EN[0]:PWM使能
PWMSR
状态寄存器
FWE[6]:FIFO写错误
CMP[5]:比较事件
ROV[4]:回滚事件
FE[3]:FIFO空状态
FIFOAV[2:0]:FIFO内元素数量
中断使能
中断使能一共有3个bit可以用
CIE[2]:比较中断使能
RIE[1]:回滚中断使能
FIE[0]:FIFO空中断使能,FIFO元素小于FWM指定值触发中断。
PWMPR
周期寄存器,计算方法如下
PWMO:PWM输出信号频率
PCLK:经过分频器分频后的时钟周期
period:写入PR寄存器的值
注意实际周期是period+1,当我们写入PR的值为0xFFFF时实际效果和0xFFFE是一样的。
代码编写
代码很简单,主要就是注意定一个全局变量pwm_duty,每次调用设置占空比的时候都会把这个变量修改,这个变量主要作用是给中断服务使用。开始调试时忘了这个变量,一直测不到输出,加了通过不停添加打印节点发现只有duty的值只有4个满足设置要求,应该对应的就是FIFO的深度。肯定是在中断服务有问题,后来发现中断调用函数时传参的值是0,才发现这个问题。
文件结构
c文件
/** * @file bsp_pwm.c * @author your name (you@domain.com) * @brief * @version 0.1 * @date 2022-01-22 * * @copyright Copyright (c) 2022 * */ #include "bsp_pwm.h" #include "stdio.h" unsigned char pwm_duty; /** * @brief PWM1初始化 * * @param period 周期 微秒 * @param duty 占空比 */ void pwm1_init(unsigned int period,unsigned int duty) { /** * IO初始化 电气性能: *bit [16] : 0 HYS关闭 *bit [15:14] : 10 100K上拉 *bit [13] : 1 pull功能 *bit [12] : 1 pull/keeper使能 *bit [11] : 0 关闭开路输出 *bit [7:6] : 10 速度100Mhz *bit [5:3] : 010 驱动能力为R0/2 *bit [0] : 0 低转换率 */ IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_PWM1_OUT,0); IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_PWM1_OUT,0xB090); PWM1->PWMCR = 0; //PWMCR清零 PWM1->PWMCR |= (1<<26)|(1<<16)|(65<<4); //PWCR[27:26](FWM)=01 [17:16](CLKSRC)=01 [15:4](PRESCALER)=65 pwm1_setperiod(period); //设置周期 for(int i=0;i<4;i++){ //通过SAM寄存器写入FIFO pwm1_setduty(duty); //定占空比 } PWM1->PWMIR = (1<<0); //FIE=1,使能FIFO Empty中断 system_register_irqHandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler,NULL); //中断函数注册 GIC_EnableIRQ(PWM1_IRQn); //GIC使能 PWM1->PWMSR = 0; //PWMSR寄存器清零 PWM1->PWMCR |= (1<<0); //PWMCR[0](EN)=1,使能 } /** * @brief 设置周期 * * @param value */ void pwm1_setperiod(unsigned int value) { unsigned int regvalue = 0; if (value<2){ regvalue=2; } else{ regvalue=value-2; } PWM1->PWMPR = (regvalue & 0xFFFF); } /** * @brief 设置占空比 * * @param duty 占空比 */ void pwm1_setduty(unsigned char duty) { unsigned short period; unsigned short sample; pwm_duty = duty; //这行代码别忘了,全局变量pwm_duty,中断服务需要用到这个变量 period = PWM1->PWMPR +2; sample = period *duty / 100; PWM1->PWMSAR = (sample & 0xFFFF); } void pwm1_irqhandler(unsigned int gcciar,void *userParam) { if(PWM1->PWMSR &(1<<3)){ pwm1_setduty(pwm_duty); PWM1->PWMSR |= (1<<3); } }
代码很清楚了,备注可以直接看,就是几个寄存器的设置
头文件
/** * @file bsp_pwm.h * @author your name (you@domain.com) * @brief * @version 0.1 * @date 2022-01-22 * * @copyright Copyright (c) 2022 * */ #ifndef __BSP_PWM_H #define __BSP_PWM_H #include "imx6ul.h" #include "bsp_int.h" void pwm1_init(unsigned int period,unsigned int duty); void pwm1_setperiod(unsigned int value); void pwm1_setduty(unsigned char duty); void pwm1_irqhandler(unsigned int gcciar,void *userParam); #endif
使用
模块导入以后,可以在main函数中使用
int main(void) { int_init(); imx6u_clkinit(); clk_enable(); delay_init(); uart_init(); key_init(); unsigned char duty = 50; unsigned char kv = 0; pwm1_init(1000,duty); while(1){ kv = key_getvalue(); if(kv == KEY0_VALUE){ duty += 5; if(duty>100){ duty=5; } pwm1_setduty(duty); delay_ms(50); printf("duty:%d",duty); } } return 0; }
在main函数中,还使用了按键,每次按键按下时占空比自增5%
初始化的时候定义的周期是1000,因为用的时钟源是66MHz,分频直接定死的值65对应66分频,周期就是1000微秒,输出频率1KHz很稳定。我用了个手持的示波器测了下输出的值,变化的过程是通过按键改变占空比的过程。这个输出频率我试过250K,很稳定,但是在低频(50Hz
左右)的时候波形不是特别好,高电平有个下降的趋势,不知道是不是示波器的原因还是怎么的。