imx6ull裸机-定时器

1 RTC定时器#

1.1 RTC定时器介绍#

RTC定时器被叫做实时时钟(real time clock)。 CPU内部有很多定时器,像看门狗WDT,PWM定时器,高精度定时器Timer等等, 只在“启动”即“通电时”运行,断电时停止。当然,如果时钟不能连续跟踪时间,则必须手动设置。那么当关机后就没办法自动计数统计时间了。
定时器的本质就是计数器,有向上计数,也有向下计数。RTC有一个与主机单独分离的电源,如纽扣电池(备用电池),即使主机电源关闭,它也保持计数定时功能。这也是为什么我们手机关机后时间还能保持准确。再比如以前的老诺基亚手机,拆掉电池就时间不准了,因为rtc电源被切断了,无法在计数,RTC定时器的计数器会被清0,需要手动设置当前时间。
RTC一般都是用纽扣电池给外部晶振和电路供电。
!image

1.2 RTC定时器原理#

以IMX6U芯片的RTC定时器为例,I.MX6U 内部也有 个 RTC 模块,但是不叫作“RTC”,而是叫做“SNVS”。
RTC模块结构图如下:
image
SNVS 分为两个子模块:SNVS_HP 和 SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域(SNVS_LP),这两个域的电源来源如下:

1
2
SNVS_LP:专用的 always-powered-on 电源域,系统主电源和备用电源都可以为其供电。
SNVS_HP:系统(芯片)电源。

系统主电源断电以后 SNVS_HP 也会断电,但是在备用电源支持下,SNVS_LP 是不会断电的,而且 SNVS_LP 是和芯片复位隔离开的,因此 SNVS_LP 相关的寄存器的值会一直保存着, 也就是low Power Domain是不受系统电源影响。
上图各个序号含义如下:

1
2
3
4
1. VDD_HIGH_IN 是系统(芯片)主电源,这个电源会同时供给给 SNVS_HP 和 SNVS_LP。
2. VDD_SNVS_IN 是纽扣电池供电的电源,这个电源只会供给给 SNVS_LP,保证在系统主电源 VDD_HIGH_IN 掉电以后 SNVS_LP 会继续运行。
3. SNVS_HP 部分。
4. SNVS_LP 部分,此部分有个 SRTC,这个就是要使用的 RTC。

SRTC 需要外界提供一个 32.768KHz 的时钟,I.MX6U-ALPHA 核心板上的 32.768KHz 的晶振就是提供这个时钟的。
image

1.3 RTC定时器寄存器#

1
2
3
4
5
SNVS_SRTCMR[14:0]代表SRTC计数器的高15
SNVS_SRTCLR[31:15]代表SRTC计数器的低17
注意:是以 197011000秒为起点,加上经过的总秒数即可得到现在的时间点。
SNVS_HPCOMR[31], NPSWA_EN位,非特权软件访问控制位,如果非特权软件要访问 SNVS 的话此位必须为 1
SNVS_LPCR[0], SRTC_ENV位,使能 STC 计数器。

1.4 RTC裸机源码展示#

NXP 官方 SDK 包是针对 I.MX6ULL 编写的,因此文件 MCIMX6Y2.h中的结构体 SNVS_Type 里面的寄存器是不全的,我们需要在其中加入本章实验所需要的寄存器,修改 SNVS_Type 为如下所示:

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
/*!
* @addtogroup SNVS_Peripheral_Access_Layer SNVS Peripheral Access Layer
* @{
*/
/** SNVS - Register Layout Typedef */
typedef struct {
__IO uint32_t HPLR; /**< SNVS_HP Lock register, offset: 0x0 */
__IO uint32_t HPCOMR; /**< SNVS_HP Command register, offset: 0x4 */
__IO uint32_t HPCR; /**< SNVS_HP Control register, offset: 0x8 */
__IO uint32_t HPSICR; /**< SNVS_HP Control register, offset: 0x8 */
__IO uint32_t HPSVCR;
__IO uint32_t HPSR;
__IO uint32_t HPSVSR;
__IO uint32_t HPHACIVR;
__IO uint32_t HPHACR;
__IO uint32_t HPRTCMR;
__IO uint32_t HPRTCLR;
__IO uint32_t HPTAMR;
__IO uint32_t HPTALR;
__IO uint32_t LPLR;
__IO uint32_t LPCR;
__IO uint32_t LPMKCR;
__IO uint32_t LPSVCR;
__IO uint32_t LPTGFCR;
__IO uint32_t LPTDCR;
__IO uint32_t LPSR;
__IO uint32_t LPSRTCMR;
__IO uint32_t LPSRTCLR;
__IO uint32_t LPTAR;
__IO uint32_t LPSMCMR;
__IO uint32_t LPSMCLR;
}SNVS_Type;
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
#ifndef _BSP_RTC_H
#define _BSP_RTC_H
#include "imx6ul.h"

/* 相关宏定义 */
#define SECONDS_IN_A_DAY (86400) /* 一天86400秒 */
#define SECONDS_IN_A_HOUR (3600) /* 一个小时3600秒 */
#define SECONDS_IN_A_MINUTE (60) /* 一分钟60秒 */
#define DAYS_IN_A_YEAR (365) /* 一年365天 */
#define YEAR_RANGE_START (1970) /* 开始年份1970年 */
#define YEAR_RANGE_END (2099) /* 结束年份2099年 */

/* 时间日期结构体 */
struct rtc_datetime {
unsigned short year; /* 范围为:1970 ~ 2099 */
unsigned char month; /* 范围为:1 ~ 12 */
unsigned char day; /* 范围为:1 ~ 31 (不同的月,天数不同).*/
unsigned char hour; /* 范围为:0 ~ 23 */
unsigned char minute; /* 范围为:0 ~ 59 */
unsigned char second; /* 范围为:0 ~ 59 */
};

/* 函数声明 */
void rtc_init(void);
void rtc_enable(void);
void rtc_disable(void);
unsigned int rtc_coverdate_to_seconds(struct rtc_datetime *datetime);
unsigned int rtc_getseconds(void);
void rtc_setdatetime(struct rtc_datetime *datetime);
void rtc_getdatetime(struct rtc_datetime *datetime);
#endif
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#include "bsp_rtc.h"
#include "stdio.h"

void rtc_init(void) {
/*
* 设置HPCOMR寄存器
* bit[31] 1 : 允许访问SNVS寄存器,一定要置1
* bit[8] 1 : 此位置1,需要签署NDA协议才能看到此位的详细说明,
* 这里不置1也没问题
*/
SNVS->HPCOMR |= (1 << 31) | (1 << 8);

#if 0
struct rtc_datetime rtcdate;
rtcdate.year = 2018U;
rtcdate.month = 12U;
rtcdate.day = 13U;
rtcdate.hour = 14U;
rtcdate.minute = 52;
rtcdate.second = 0;
rtc_setDatetime(&rtcdate); //初始化时间和日期
#endif
rtc_enable(); //使能RTC
}

void rtc_enable(void) {
/*
* LPCR寄存器bit0置1,使能RTC
*/
SNVS->LPCR |= 1 << 0;
while(!(SNVS->LPCR & 0X01));//等待使能完成

}

void rtc_disable(void) {
/*
* LPCR寄存器bit0置0,关闭RTC
*/
SNVS->LPCR &= ~(1 << 0);
while(SNVS->LPCR & 0X01);//等待关闭完成
}

/*
* @description : 判断指定年份是否为闰年,闰年条件如下:
* @param - year: 要判断的年份
* @return : 1 是闰年,0 不是闰年
*/
unsigned char rtc_isleapyear(unsigned short year) {
unsigned char value=0;

if(year % 400 == 0)
value = 1;
else {
if((year % 4 == 0) && (year % 100 != 0))
value = 1;
else
value = 0;
}
return value;
}

/*
* @description : 将时间转换为秒数
* @param - datetime: 要转换日期和时间。
* @return : 转换后的秒数
*/
unsigned int rtc_coverdate_to_seconds(struct rtc_datetime *datetime) {
unsigned short i = 0;
unsigned int seconds = 0;
unsigned int days = 0;
unsigned short monthdays[] = {0U, 0U, 31U, 59U, 90U, 120U, 151U, 181U, 212U, 243U, 273U, 304U, 334U};

for(i = 1970; i < datetime->year; i++) {
days += DAYS_IN_A_YEAR; /* 平年,每年365天 */
if(rtc_isleapyear(i)) days += 1;/* 闰年多加一天 */
}

days += monthdays[datetime->month];
if(rtc_isleapyear(i) && (datetime->month >= 3)) days += 1;/* 闰年,并且当前月份大于等于3月的话加一天 */

days += datetime->day - 1;
seconds = days * SECONDS_IN_A_DAY +
datetime->hour * SECONDS_IN_A_HOUR +
datetime->minute * SECONDS_IN_A_MINUTE +
datetime->second;
return seconds;
}

/*
* @description : 设置时间和日期
* @param - datetime: 要设置的日期和时间
* @return : 无
*/
void rtc_setdatetime(struct rtc_datetime *datetime) {
unsigned int seconds = 0;
unsigned int tmp = SNVS->LPCR;
rtc_disable(); /* 设置寄存器HPRTCMR和HPRTCLR的时候一定要先关闭RTC */
/* 先将时间转换为秒 */
seconds = rtc_coverdate_to_seconds(datetime);
SNVS->LPSRTCMR = (unsigned int)(seconds >> 17); /* 设置高16位 */
SNVS->LPSRTCLR = (unsigned int)(seconds << 15); /* 设置地16位 */
/* 如果此前RTC是打开的在设置完RTC时间以后需要重新打开RTC */
if (tmp & 0x1)
rtc_enable();
}

/*
* @description : 将秒数转换为时间
* @param - seconds : 要转换的秒数
* @param - datetime: 转换后的日期和时间
* @return : 无
*/
void rtc_convertseconds_to_datetime(u64 seconds, struct rtc_datetime *datetime) {
u64 x;
u64 secondsRemaining, days;
unsigned short daysInYear;

/* 每个月的天数 */
unsigned char daysPerMonth[] = {0U, 31U, 28U, 31U, 30U, 31U, 30U, 31U, 31U, 30U, 31U, 30U, 31U};
secondsRemaining = seconds; /* 剩余秒数初始化 */
days = secondsRemaining / SECONDS_IN_A_DAY + 1; /* 根据秒数计算天数,加1是当前天数 */
secondsRemaining = secondsRemaining % SECONDS_IN_A_DAY; /*计算天数以后剩余的秒数 */
/* 计算时、分、秒 */
datetime->hour = secondsRemaining / SECONDS_IN_A_HOUR;
secondsRemaining = secondsRemaining % SECONDS_IN_A_HOUR;
datetime->minute = secondsRemaining / 60;
datetime->second = secondsRemaining % SECONDS_IN_A_MINUTE;
/* 计算年 */
daysInYear = DAYS_IN_A_YEAR;
datetime->year = YEAR_RANGE_START;
while(days > daysInYear) {
/* 根据天数计算年 */
days -= daysInYear;
datetime->year++;
/* 处理闰年 */
if (!rtc_isleapyear(datetime->year))
daysInYear = DAYS_IN_A_YEAR;
else /*闰年,天数加一 */
daysInYear = DAYS_IN_A_YEAR + 1;
}
/*根据剩余的天数计算月份 */
if(rtc_isleapyear(datetime->year)) /* 如果是闰年的话2月加一天 */
daysPerMonth[2] = 29;

for(x = 1; x <= 12; x++) {
if (days <= daysPerMonth[x]) {
datetime->month = x;
break;
} else {
days -= daysPerMonth[x];
}
}
datetime->day = days;
}

/*
* @description : 获取RTC当前秒数
* @param : 无
* @return : 当前秒数
*/
unsigned int rtc_getseconds(void) {
unsigned int seconds = 0;
seconds = (SNVS->LPSRTCMR << 17) | (SNVS->LPSRTCLR >> 15);
return seconds;
}

/*
* @description : 获取当前时间
* @param - datetime: 获取到的时间,日期等参数
* @return : 无
*/
void rtc_getdatetime(struct rtc_datetime *datetime) {
//unsigned int seconds = 0;
u64 seconds;
seconds = rtc_getseconds();
rtc_convertseconds_to_datetime(seconds, datetime);
}

可以看到RTC定时器是以秒为计时单位的,每过1s SRTC计数器的值加1。
首先调用rtc_init初始化并启动,然后调用rtc_setdatetime设定当前日期时间,调用rtc_getdatetime获取当前日期时间,期间会利用rtc_convertseconds_to_datetime把总秒数转换成当前的日期和时间。

2 PWM定时器#

2.1 pwm定时器介绍#

imx6ull一共有 8 路 PWM 信号,每个 PWM 包含一个 16 位的计数器和一个 4 x 16 的数据 FIFO。一路框图如下:
image

1
2
3
4
5
6
7
①、此部分是一个选择器,用于选择 PWM 信号的时钟源,一共有三种时钟源:ipg_clk,pg_clk_highfreq 和 ipg_clk_32k。
②、这是一个 12 位的分频器,可以对①中选择的时钟源进行分频。
③、这是 PWM 的 16 位计数器寄存器,保存着 PWM 的计数值。
④、这是 PWM 的 16 位周期寄存器,此寄存器用来控制 PWM 的频率。
⑤、这是 PWM 的 16 位采样寄存器,此寄存器用来控制 PWM 的占空比。
⑥、此部分是 PWM 的中断信号,PWM 是提供中断功能的,如果使能了相应的中断的话就会产生中断。
⑦、此部分是 PWM 对应的输出 IO,产生的 PWM 信号就会从对应的 IO 中输出。

2.2 PWM控制器#

2.2.1 PWMx_PWMPR寄存器-周期设置#

PWM 的 16 位计数器是个上计数器,此计数器会从 0X0000 开始计数,直到计数值等于寄存器PWMx_PWMPR(x=1~8)+ 1,然后计数器就会重新从0X0000 开始计数,如此往复。PWMx_PWMPR设置频率。PWM周期公式如下:
PWM_FRE = PWM_CLK / (PERIOD + 2)
也就是PWMO(Hz) = PCLK(Hz) / (PERIOD + 2)
image

比如当前PWM_CLK=1MHz, 要产生1KHz的PWM,那么PERIOD = 1000000/1K - 2 = 998。,如下设置1000,即可得到PERIOD=998,也就是1khz.

1
2
3
4
5
6
7
8
void pwm1_setperiod_value(unsigned int value) {
unsigned int regvalue = 0;
if(value < 2)
regvalue = 2;
else
regvalue = value - 2;
PWM1->PWMPR = (regvalue & 0XFFFF);
}

2.2.2 PWMx_PWMSAR寄存器-占空比#

设置Sample采样寄存器,Sample数据会写入到FIFO中。当计数器的值小于 SAMPLE 的时候输出高电平(或低电平)。当计数器值大于等于 SAMPLE,小于寄存器PWM1_PWMPR 的 PERIO 的时候输出低电平(或高电平)。
假如我们要设置 PWM 信号的占空比为 50%,那么就可以将 SAMPLE 设置为(PERIOD + 2) / 2 = 1000 / 2=500。
image

如下设置50,即可得到sample=500,也就是占空比50%.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct backlight_dev_struc {	
unsigned char pwm_duty; /* 占空比 */
};
struct backlight_dev_struc backlight_dev;
void pwm1_setsample_value(unsigned int value) {
PWM1->PWMSAR = (value & 0XFFFF);
}
void pwm1_setduty(unsigned char duty) {
unsigned short preiod;
unsigned short sample;
backlight_dev.pwm_duty = duty;
preiod = PWM1->PWMPR + 2;
sample = preiod * backlight_dev.pwm_duty / 100;
pwm1_setsample_value(sample);
}

2.2.3 PWMCR 控制寄存器#

image

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
FWM(bit27:26):FIFO 水位线,用来设置 FIFO 空余位置为多少的时候表示 FIFO 为空。
设置为 0 的时候表示 FIFO 空余位置大于等于 1 的时候 FIFO 为空;
设置为 1 的时候表示 FIFO 空余位置大于等于 2 的时候 FIFO 为空;
设置为 2 的时候表示 FIFO 空余位置大于等于 3 的时候FIFO 为空;
设置为 3 的时候表示 FIFO 空余位置大于等于 4 的时候 FIFO 为空。
STOPEN(bit25):此位用来设置停止模式下 PWM 是否工作,为 0 的话表示在停止模式下PWM 不工作,为 1 的话表示停止模式下激活 PWM。
DOZEN(bit24):此位用来设置休眠模式下 PWM 是否工作,为 0 的话表示在休眠模式下PWM 不工作,为 1 的话表示休眠模式下激活 PWM。
WAITEN(bit23):此位用来设置等待模式下 PWM 是否工作,为 0 的话表示在等待模式下PWM 不工作,为 1 的话表示等待模式下激活 PWM。
DEGEN(bit22):此位用来设置调试模式下 PWM 是否工作,为 0 的话表示在调试模式下PWM 不工作,为 1 的话表示调试模式下激活 PWM。
BCTR(bit21):字节交换控制位,用来控制 16 位的数据进入 FIFO 的字节顺序。为 0 的时候不进行字节交换,为 1 的时候进行字节交换。
HCRT(bit20):半字交换控制位,用来决定从 32 位 IP 总线接口传输来的哪个半字数据写入采样寄存器的低 16 位中。
POUTC(bit19:18):PWM 输出控制控制位,用来设置 PWM 输出模式,
0 的时候表示PWM 先输出高电平,当计数器值和采样值相等的话就输出低电平。
1 的时候相反,当为 2 或者 3 的时候 PWM 信号不输出。本章我们设置为 0
也就是一开始输出高电平,当计数器值和采样值相等的话就改为低电平,这样采样值越大高电平时间就越长,占空比就越大。
CLKSRC(bit17:16):PWM 时钟源选择,
0 的话关闭;
1 的话选择 ipg_clk 为时钟源;
2 的话选择 ipg_clk_highfreq 为时钟源;
3 的话选择 ipg_clk_32k 为时钟源。本章我们设置为 1,也就是选择 ipg_clk 为 PWM 的时钟源,因此 PWM 时钟源频率为 66MHz。
PRESCALER(bit15:4):分频值,可设置为 0~4095,对应着 1~4096 分频。
SWR(bit3):软件复位,向此位写 1 就复位 PWM,此位是自清零的,当复位完成以后此位会自动清零。
REPEAT(bit2:1):重复采样设置,此位用来设置 FIFO 中的每个数据能用几次。
可设置 0~3,分别表示 FIFO 中的每个数据能用 1~4 次。本章我们设置为 0,即 FIFO 中的每个数据只能用一次。
EN(bit0):PWM 使能位,为 1 的时候使能 PWM,为 0 的时候关闭 PWM。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void pwm1_enable(void) {
PWM1->PWMCR |= 1 << 0;
}
void pwm1_init(void) {
PWM1->PWMCR = 0; /* 寄存器先清零 */
PWM1->PWMCR |= (1 << 26) | (1 << 16) | (65 << 4);

/* 设置PWM周期为1000,那么PWM频率就是1M/1000 = 1KHz。 */
pwm1_setperiod_value(1000);

/* 设置占空比,默认50%占空比 ,写四次是因为有4个FIFO */
backlight_dev.pwm_duty = 50;
for(i = 0; i < 4; i++) {
pwm1_setduty(backlight_dev.pwm_duty);
}

/* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
PWM1->PWMIR |= 1 << 0;
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 注册中断服务函数 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */

pwm1_enable(); /* 使能PWM1 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
bit[27:26]	: 01  当FIFO中空余位置大于等于2的时候FIFO空标志值位
bit[25] :0 停止模式下PWM不工作
bit[24] : 0 休眠模式下PWM不工作
bit[23] : 0 等待模式下PWM不工作
bit[22] : 0 调试模式下PWM不工作
it[21] : 0 关闭字节交换
bit[20] : 0 关闭半字数据交换
bit[19:18] : 00 PWM输出引脚在计数器重新计数的时候输出高电平,在计数器计数值达到比较值以后输出低电平
bit[17:16] : 01 PWM时钟源选择IPG CLK = 66MHz
bit[15:4] : 65 分频系数为65+1=66,PWM时钟源 = 66MHZ/66=1MHz
bit[3] : 0 PWM不复位
bit[2:1] : 00 FIFO中的sample数据每个只能使用一次。
bit[0] : 0 先关闭PWM,后面再使能

2.2.4 PWM1_PWMIR中断控制寄存器#

image
CIE(bit2):比较中断使能位,为 1 的时候使能比较中断,为 0 的时候关闭比较中断。
RIE(bit1):翻转中断使能位,当计数器值等于采样值并回滚到 0X0000 的时候就会产生此中断,为 1 的时候使能翻转中断,为 0 的时候关闭翻转中断。
FIE(bit0):FIFO 空中断,为 1 的时候使能,为 0 的时候关闭。前面代码写的是使能FIFO空中断.

1
2
/* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
PWM1->PWMIR |= 1 << 0;

2.2.5 PWM1_PWMSR 状态寄存器#

image
FWE(bit6):FIFO 写错误事件,为 1 的时候表示发生了 FIFO 写错误。
CMP(bit5):FIFO 比较事件发标志位,为 1 的时候表示发生 FIFO 比较事件。
ROV(bit4):翻转事件标志位,为 1 的话表示翻转事件发生。
FE(bit3):FIFO 空标志位,为 1 的时候表示 FIFO 位空。
FIFOAV(bit2:0):此位记录 FIFO 中的有效数据个数,有效值为 04,分别表示 FIFO 中有04 个有效数据

初始化先清0,中断服务程序读取状态,并且清中断。FIFO 中的采样值每个周期都会少一个,所以需要不断的向 FIFO 中写入采样值,防止其为空。我们可以使能 FIFO 空中断,这样当 FIFO 为空的时候就会触发相应的中断,然后在中断处理函数中向 FIFO 写入采样值。

1
2
3
4
5
6
7
8
9
10
11
12
13
void pwm1_irqhandler(void) {
if(PWM1->PWMSR & (1 << 3)) /* FIFO为空中断 */
{
/* 将占空比信息写入到FIFO中,其实就是设置占空比 */
pwm1_setduty(backlight_dev.pwm_duty);
PWM1->PWMSR |= (1 << 3); /* 写1清除中断标志位 */
}
}

system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 注册中断服务函数 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */
pwm1_enable(); /* 使能PWM1 */

2.3 测试#

初始化时设置占空比为50%,测试代码读取按键,每次该按键按下就对占空比加10%,如果占空比超过100%,重新从10%开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
while(1) {
keyvalue = key_getvalue();
if(keyvalue == KEY0_VALUE)
{
duty += 10; /* 占空比加10% */
if(duty > 100) /* 如果占空比超过100%,重新从10%开始 */
duty = 10;
lcd_shownum(50 + 72, 90, duty, 3, 16);
pwm1_setduty(duty); /* 设置占空比 */
}

delayms(10);
}

占空比10%时亮度波形如下,亮度很暗。
image
image
占空比90%时亮度如下:
image