昨天完成了LCD屏幕驱动实验,当时LCD背光直接用GPIO引脚高低电平控制,只有亮/灭两档,无法调节亮度。今天继续基于IM6ULL开发板,学习两大常用外设:PWM脉冲宽度调制、ADC模数转换器。
本次实验目标:ADC采集模拟电压,在LCD屏幕实时打印ADC原始(Voltage = \frac{采样值}{4096} \times 3.3V)与电压;同时使用PWM输出信号,可以用来控制LED/LCD背光亮度,实现平滑调光。
一、基础概念理解
1. PWM 脉冲宽度调制
PWM(Pulse Width Modulation):脉冲宽度调制。
频率单位HZ,周期公式:周期 = 1 / 频率,例:1KHz = 1000Hz,对应周期就是1ms。
占空比:在一个周期内,高电平持续时间占总周期时间的比例。
原理:利用人眼视觉暂留效应,快速不断切换高低电平。占空比越大,等效平均电压越高,LED越亮;占空比越小,亮度越低。
输出极性:波形极性会影响高低电平对应的占空比。
30%占空比:高电平持续300单位时间,低电平700;
70%占空比:高电平持续700单位时间,低电平300。
对比昨天LCD背光GPIO控制:GPIO只有0和1两种状态,只能开关;PWM可以实现连续多级亮度调节。
2. ADC 模数转换器
ADC:Analog-to-Digital Converter,模拟信号转数字信号。
IM6ULL片上ADC为12位ADC,采样电压范围0~3.3V,采样数字值范围 0 ~ 4095。
电压计算公式:
Voltage = \frac{(Voltage = \frac{采样值}{4096} \times 3.3V)}{4096} \times 3.3V
可以用来采集电位器、光敏电阻这类模拟传感器,把连续变化的模拟电压转换成CPU能够识别的数字量。
二、完整代码解析
本次代码分为 pwm.c、adc.c、main.c 三部分,全部基于IM6ULL寄存器直接操作,裸机开发,不依赖高层SDK封装。
1. pwm.c 完整代码
c
#include "pwm.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "interrupt.h"
static float g_dc = 0.5;
void set_dc(float dc)
{
g_dc = dc;
}
float get_dc(void)
{
return g_dc;
}
void set_rotia(float dc)
{
unsigned int temp = PWM1->PWMPR;
int i = 0;
for (i = 0; i < 4; i++)
{
PWM1->PWMSAR = temp * (1 - dc);
}
}
void pwm1_irq_handler(void)
{
if ((PWM1->PWMSR & (1 << 3)) != 0)
{
set_rotia(g_dc);
//清除中断标志,IMX6ULL PWM为写1清零
PWM1->PWMSR |= (1 << 3);
}
}
void pwm1_init(void)
{
//复用功能配置:GPIO1_IO08复用为PWM1_OUT
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_PWM1_OUT, 0);
//电气特性
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_PWM1_OUT, 0xB9);
//PWM模块复位
PWM1->PWMCR |= (1 << 3);
while((PWM1->PWMCR & (1 << 3)) != 0);
//PWM1寄存器配置
PWM1->PWMCR = 0;
PWM1->PWMCR |= (1 << 26) | (1 << 16) | (65 << 4) | (3 << 1);
PWM1->PWMIR |= (1 << 0);
PWM1->PWMPR = 998;
//3.设置中断优先级
GIC_SetPriority(PWM1_IRQn, 0);
//4.注册中断处理函数
system_interrupt_register(PWM1_IRQn, pwm1_irq_handler);
//5.设置中断使能
GIC_EnableIRQ(PWM1_IRQn);
set_rotia(g_dc);
//开启PWM模块
PWM1->PWMCR |= (1 << 0);
}
PWM代码说明
- 引脚复用:
GPIO1_IO08设置为PWM1输出功能,配置PAD电气属性。 - 外设复位:PWM上电需要软件复位,循环等待复位完成。
- 时钟与周期配置:
PWMPR寄存器配置PWM周期,这里赋值998。 - 中断配置:GIC配置PWM中断优先级、注册中断服务函数,开启IRQ。
- 中断服务函数
pwm1_irq_handler:PWM周期溢出触发中断,在中断内部更新PWMSAR比较寄存器,动态修改PWM占空比。 - 接口函数
set_dc():全局变量设置目标占空比
get_dc():读取当前占空比
set_rotia():根据占空比计算PWMSAR的值;1-dc和PWM输出极性匹配
2. adc.c 完整代码
c
#include "adc.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "stdio.h"
int Calibration_self(void)
{
ADC1->GS |= (1 << 1);
ADC1->GC |= (1 << 7);
while ((ADC1->GC & (1 << 7)) != 0);
return ((ADC1->GS & (1 << 1)) == 0);
}
void adc1_init(void)
{
//复用功能配置,GPIO1_IO01作为ADC模拟输入
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO01_GPIO1_IO01, 0);
//电气特性
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO01_GPIO1_IO01, IOMUXC_SW_PAD_CTL_PAD_PKE(1));
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO01_GPIO1_IO01, IOMUXC_SW_PAD_CTL_PAD_PUE(0));
//ADC寄存器配置
ADC1->HC[0] &= ~(1 << 7);
ADC1->CFG = 0;
ADC1->CFG |= (2 << 2) | (3 << 0);
ADC1->GC = 0;
ADC1->GC |= (1 << 0);
//ADC自校准,必须执行,消除硬件偏移误差
printf(Calibration_self() == 0 ? " Calibration completed normally.\n" : "Calibration failed.\n");
}
unsigned short get_adc_value(void)
{
ADC1->HC[0] = 0x1F;
ADC1->HC[0] = 1;
while((ADC1->HS & (1 << 0)) == 0);
return (unsigned short)(ADC1->R[0] & 0xFFF);
}
float get_voltage(void)
{
unsigned short temp = get_adc_value();
return (float)temp / 4096.0f * 3.3f;
}
ADC代码说明
- 引脚配置:GPIO1_IO01配置为模拟输入引脚,配置PAD上下拉、使能。
- ADC初始化:配置ADC时钟分频、开启ADC电源。
- 自校准 Calibration_self:ADC上电必须执行自校准,消除硬件带来的采样误差,校准失败采样结果会漂移不准。
get_adc_value():启动一次ADC转换,等待转换完成,读取12bit原始采样数据。get_voltage():将12位数字量换算成实际电压,电压 = adc原始值 /4096.0 *3.3。
3. main.c 完整代码
c
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "interrupt.h"
#include "clock.h"
#include "epit.h"
#include "gpt.h"
#include "uart.h"
#include "stdio.h"
#include "string.h"
#include "i2c.h"
#include "at24c02.h"
#include "lm75.h"
#include "lcd.h"
#include "framebuffer.h"
#include "pwm.h"
#include "adc.h"
int main(void)
{
system_interrupt_init();
clock_init();
led_init();
beep_init();
key_init();
//epit1_init();
gpt1_init();
uart1_init();
unsigned short temp = 0;
float vol = 0;
char buffer[128] = {0};
pwm1_init();
adc1_init();
lcd_init();
screen_clear(0x00FFFFFF);
//lcd_h_drawline(100, 100, 200, 0x00FF0000);
//extern void lcd_show_string(unsigned short x,unsigned short y, unsigned short width, unsigned short height, unsigned char size,char *p);
//lcd_show_string(200,200, 300, 32, 32, "hello world!");
while(1)
{
temp = get_adc_value();
vol = get_voltage();
int m = 0, n = 0;
m = (int)(vol * 1000) / 1000;
n = (int)(vol * 1000) % 1000;
sprintf(buffer,"adc = %d, vol = %d.%d\n", temp, m, n);
lcd_show_string(100, 100, 500, 32, 32, buffer);
delay_ms(10);
}
return 0;
}
main函数说明
- 外设初始化顺序:系统中断 → 系统时钟 → LED、蜂鸣器、按键 → GPT定时器 → UART串口 → PWM1 → ADC1 → LCD,然后清屏。
- 主循环逻辑:
- 读取ADC原始(Voltage = \frac{采样值}{4096} \times 3.3V)
- 调用
get_voltage计算电压浮点数 - 把浮点数拆分为整数m和小数n,方便LCD显示
- sprintf格式化字符串存入buffer
- lcd_show_string 在屏幕(100,100)位置打印数据
- 延时10ms循环刷新
三、实验现象
- 调节电位器,ADC原始(Voltage = \frac{采样值}{4096} \times 3.3V)
adc = xxx实时跟着变化; - LCD屏幕实时刷新电压
vol = x.xxx; - PWM通道持续输出设定占空比的方波,可以外接LED或者LCD背光引脚,用来调节亮度。
小结
- ADC自校准不能省略:上电不校准,采样结果不准,甚至读取异常。
- PWM中断:每次PWM周期完成触发中断,在中断里面修改PWMSAR,动态修改占空比。IM6ULL的PWM中断标志是写1清零,不能读清零。
- LCD浮点数打印:LCD没有标准printf,使用sprintf把数值转为字符串,再调用
lcd_show_string显示。 - 引脚复用:IMX6ULL一个引脚有多个复用功能,PWM输出引脚必须配置为PWM功能;ADC引脚配置为模拟输入,不能当成普通GPIO。
- GPIO与PWM对比:GPIO只能高低电平开关;PWM用于调光、电机调速;ADC用来采集外部模拟量,这两个外设是嵌入式模拟交互的基础。