16_定时器的使用之输出PWM

介绍产生PWM控制无源蜂鸣器

Posted by Jerry Chen on August 9, 2019

了解内容

要产生PWM,首先要找到可以配置为定时器输出模式的GPIO。下图来自4412用户手册“2.5 Pin Description”章节。

可以看到GPD0[0]可以配置为定时器0输出模式,也就是TOUT_0模式。本文介绍使用该引脚输出PWM控制蜂鸣器发声。

结合4412用户手册“24 PWM Timer”章节内容,可以看到TCFG0、TCFG1控制分频影响模块时钟,TCMPB0控制比较值,TCNTB0控制自减计数器最大计数值,TCON控制自动重载/单次脉冲和启停等。

开始

一些寄存器

使用定时器0输出的主要寄存器如下图:

基地址0x139D0000,占用连续20个字节的空间。

TCFG0:

TCFG0最低8位决定定时器0的预分频数,范围是1~255;实际生效的是该值加1,后面会有公式。

TCFG1关于定时器0的部分:

TCFG1最低4位决定定时器的分频数{divider value}。

最终频率计算公式如下: \(Timer Input Clock Frequency = PCLK/({prescaler value + 1})/{divider value}\)

TCON关于定时器0的部分:

特别注意的是手动更新位设置1后,需要再次手动清除该位。

推荐步骤

开启定时器PWM输出:

  1. 设置GPD0[0]为TOUT_0定时器0输出模式,可选加上拉;
  2. 设置TCFG0为255,即256预分频;
  3. 设置TCFG1为2,即4分频;
  4. 设置TCMPB0为85,TCNTB0为100,即高电平占空比为85%;
  5. 设置TCON中手动更新、自动重载,然后关闭手动更新;
  6. 设置TCON中定时器启动;

控制关闭PWM输出后引脚保持的高低电平状态:

  1. 不更改GPIO配置,也就是还是TOUT模式

    • 方法一:直接关闭定时器开关默认输出高;

    • 方法二:让定时器保持开启,但让比较值超过范围,这时会默认输出低电平。也就是重新设置TCMPB0>TCNTB0,然后手动更新,再关闭手动更新。如果没有设置输出反转,这时输出就是低电平;如果设置反转就是高电平;

  2. 其他方法:先清空TCON低4位,为了不影响下次设置

    • 方法一:设置GPIO为输出,控制电平;
    • 方法二:设置GPIO为输入,设置无上下拉;

例程

驱动例程

timer_pwm_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <linux/init.h>
#include <linux/module.h>
#include <asm/io.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("jerry");

union TCON_t{
	unsigned long all;
	struct{
		unsigned long start:1;
		unsigned long manual_update:1;
		unsigned long inverter:1;
		unsigned long auto_reload:1;
		unsigned long RSVD:28;
	}bit;
};

struct TIMER0_PWM_t{
	unsigned long TCFG0;	//4字节
	unsigned long TCFG1;	//4字节
	union TCON_t TCON;		//4字节
	unsigned long TCNTB0;	//4字节
	unsigned long TCMPB0;	//4字节
};

volatile static struct TIMER0_PWM_t *PWM;
volatile static unsigned long virt_addr; //虚拟地址
volatile static unsigned long *GPD0CON, *GPD0DAT, *GPD0PUD;

static void gpd0_remap(void){
	//分析知,从0x1140_00a0起映射12个字节即可
	//一个ulong在32位机器上占用4个字节,寄存器都是ulong
	unsigned long phys_addr = 0x114000a0; //物理地址
	virt_addr = (unsigned long)ioremap(phys_addr,12);//将指针强转为整形
	
	//指定需要操作的寄存器地址
	GPD0CON = (unsigned long *)(virt_addr+0x00);
	GPD0DAT = (unsigned long *)(virt_addr+0x04);
	GPD0PUD	= (unsigned long *)(virt_addr+0x08);
}
static void gpd0_config(void){
	//配置为TOUT_0输出
	*GPD0CON &= 0xfffffff0;
	*GPD0CON |= 0x2;
	//配置为上拉
	*GPD0PUD &= 0xfffc;
	*GPD0PUD |= 0x3;
}
static void timer0_remap(void){
	//分析知,从0x139d_0000起映射20个字节即可
	unsigned long phys_addr = 0x139d0000; //物理地址
	PWM = ioremap(phys_addr,20);
}
static void timer0_config(void){
	PWM->TCFG0 &= 0xffffff00;
	PWM->TCFG0 |= 0xff;
	PWM->TCFG1 &= 0xfffffff0;
	PWM->TCFG1 |= 0x2;
	PWM->TCMPB0 = 90;
	PWM->TCNTB0 = 100;
	PWM->TCON.bit.auto_reload = 1;
	PWM->TCON.bit.manual_update = 1;
	PWM->TCON.bit.manual_update = 0;
	
	PWM->TCON.bit.inverter = 0;//不翻转,可选
}
static void pwm_start(void){
	//开启定时器
	PWM->TCON.bit.start = 1;
}
static void pwm_stop(void){
	//保持输出低电平
	PWM->TCMPB0 = PWM->TCNTB0+1;//超过范围会输出低电平
	PWM->TCON.bit.manual_update = 1;
	PWM->TCON.bit.manual_update = 0;
	
	//如何保持输出高电平
	//方法1:直接关定时器开关
	//方法2:在上面低电平基础上设置翻转
}

static int demo_init(void){
	gpd0_remap();	//实现IO内存的映射,得到3个可控制的寄存器
	gpd0_config();	//配置GPD0[0]为TOUT_0输出上拉模式
	timer0_remap();
	timer0_config();
	pwm_start();
	
	printk(KERN_EMERG "buzzer is on!\n");
	return 0;
}

static void demo_exit(void){
	pwm_stop();
	printk(KERN_EMERG "buzzer is off!\n");
}

module_init(demo_init);
module_exit(demo_exit);
测试结果

GPD0[0]引脚在原理图上连接的是蜂鸣器。

编写简单Makefile:参考这里

编译生成模块后,拷贝到开发板中:

  • 首先加载模块后蜂鸣器响,卸载后蜂鸣器停止