ARM-pwm脉冲宽度调制、ADC模数转换

昨天完成了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代码说明
  1. 引脚复用:GPIO1_IO08 设置为PWM1输出功能,配置PAD电气属性。
  2. 外设复位:PWM上电需要软件复位,循环等待复位完成。
  3. 时钟与周期配置:PWMPR寄存器配置PWM周期,这里赋值998。
  4. 中断配置:GIC配置PWM中断优先级、注册中断服务函数,开启IRQ。
  5. 中断服务函数 pwm1_irq_handler:PWM周期溢出触发中断,在中断内部更新PWMSAR比较寄存器,动态修改PWM占空比。
  6. 接口函数
    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代码说明
  1. 引脚配置:GPIO1_IO01配置为模拟输入引脚,配置PAD上下拉、使能。
  2. ADC初始化:配置ADC时钟分频、开启ADC电源。
  3. 自校准 Calibration_self:ADC上电必须执行自校准,消除硬件带来的采样误差,校准失败采样结果会漂移不准。
  4. get_adc_value():启动一次ADC转换,等待转换完成,读取12bit原始采样数据。
  5. 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函数说明
  1. 外设初始化顺序:系统中断 → 系统时钟 → LED、蜂鸣器、按键 → GPT定时器 → UART串口 → PWM1 → ADC1 → LCD,然后清屏。
  2. 主循环逻辑:
    • 读取ADC原始(Voltage = \frac{采样值}{4096} \times 3.3V)
    • 调用get_voltage计算电压浮点数
    • 把浮点数拆分为整数m和小数n,方便LCD显示
    • sprintf格式化字符串存入buffer
    • lcd_show_string 在屏幕(100,100)位置打印数据
    • 延时10ms循环刷新

三、实验现象

  1. 调节电位器,ADC原始(Voltage = \frac{采样值}{4096} \times 3.3V)adc = xxx实时跟着变化;
  2. LCD屏幕实时刷新电压 vol = x.xxx;
  3. PWM通道持续输出设定占空比的方波,可以外接LED或者LCD背光引脚,用来调节亮度。

小结

  1. ADC自校准不能省略:上电不校准,采样结果不准,甚至读取异常。
  2. PWM中断:每次PWM周期完成触发中断,在中断里面修改PWMSAR,动态修改占空比。IM6ULL的PWM中断标志是写1清零,不能读清零。
  3. LCD浮点数打印:LCD没有标准printf,使用sprintf把数值转为字符串,再调用lcd_show_string显示。
  4. 引脚复用:IMX6ULL一个引脚有多个复用功能,PWM输出引脚必须配置为PWM功能;ADC引脚配置为模拟输入,不能当成普通GPIO。
  5. GPIO与PWM对比:GPIO只能高低电平开关;PWM用于调光、电机调速;ADC用来采集外部模拟量,这两个外设是嵌入式模拟交互的基础。
相关推荐
an123212113 小时前
ARM嵌入式学习笔记:SPI通信协议详解
arm开发·笔记·学习
CodeFragment15 小时前
裸机开发速记:启动流程|GIC 中断|时钟系统|UART 通信
arm开发·单片机·嵌入式硬件
小雪崩15 小时前
嵌入式学习 day60:ARM汇编——触摸屏与SPI
汇编·arm开发·学习
M78佐菲20 小时前
ARM学习笔记(12)
linux·arm开发·笔记·嵌入式硬件·学习
sunoo-22921 小时前
【嵌入式 ARM 学习第十二天】SPI 裸机驱动 + I2C 触摸屏驱动 + 三大串行协议深度对比
arm开发·笔记·vscode·学习·imx6ull
ARM&开发(Haidong)1 天前
ARM DMA硬件和驱动原理
arm开发
AOI小白新手上路1 天前
4-3_ARM汇编_学习笔记
汇编·arm开发·学习
kkkkkkkkkk_Z1 天前
新手自学嵌入式 | 学习日记:ARM架构初探与基础概念梳理
arm开发·学习·架构
SKH.1 天前
ARM-lcd屏幕点亮
arm开发