☰
ARM-pwm脉冲宽度调制、ADC模数转换
2026/10/1 18:39:55 网站建设 项目流程

昨天完成了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 完整代码

#include"pwm.h"#include"MCIMX6Y2.h"#include"fsl_iomuxc.h"#include"interrupt.h"staticfloatg_dc=0.5;voidset_dc(floatdc){g_dc=dc;}floatget_dc(void){returng_dc;}voidset_rotia(floatdc){unsignedinttemp=PWM1->PWMPR;inti=0;for(i=0;i<4;i++){PWM1->PWMSAR=temp*(1-dc);}}voidpwm1_irq_handler(void){if((PWM1->PWMSR&(1<<3))!=0){set_rotia(g_dc);//清除中断标志,IMX6ULL PWM为写1清零PWM1->PWMSR|=(1<<3);}}voidpwm1_init(void){//复用功能配置:GPIO1_IO08复用为PWM1_OUTIOMUXC_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 完整代码

#include"adc.h"#include"MCIMX6Y2.h"#include"fsl_iomuxc.h"#include"stdio.h"intCalibration_self(void){ADC1->GS|=(1<<1);ADC1->GC|=(1<<7);while((ADC1->GC&(1<<7))!=0);return((ADC1->GS&(1<<1))==0);}voidadc1_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");}unsignedshortget_adc_value(void){ADC1->HC[0]=0x1F;ADC1->HC[0]=1;while((ADC1->HS&(1<<0))==0);return(unsignedshort)(ADC1->R[0]&0xFFF);}floatget_voltage(void){unsignedshorttemp=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 完整代码

#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"intmain(void){system_interrupt_init();clock_init();led_init();beep_init();key_init();//epit1_init();gpt1_init();uart1_init();unsignedshorttemp=0;floatvol=0;charbuffer[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();intm=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);}return0;}
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用来采集外部模拟量,这两个外设是嵌入式模拟交互的基础。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询