ARM编程--->ADC编程实例

ARM 下的 ADC(模数转换器)

  • ADC(Analog to Digital Converter,模数转换器)是嵌入式系统中非常重要的一个模块,负责将连续变化的模拟信号(如温度、电压、电流等)转换为数字信号,供 CPU 和程序进行处理。
  1. ADC 基本概念
    • 模拟量(Analog Quantity):模拟量是连续变化的物理量,比如温度、湿度、光照强度、电压等。它们的变化是连续的,无法直接被数字处理器理解。
    • 数字量(Digital Quantity):数字量是离散的、不连续的。数字处理器只能处理数字量,比如 1 和 0 的组合。
    • 转换:ADC 模块负责将模拟信号(比如电压信号)转换为相应的数字量,使处理器能够读取和处理。
  2. 工作原理
  • 在 ARM 系统中的 ADC 模块通常是一个基于电压的转换器,它通过对输入电压信号的采样,将其转换为对应的数字值。
  • 例如,在 12 位 ADC 中,输入电压的范围通常为 0 - Vref,其中 Vref 是参考电压。输入电压 Vx 可以通过以下公式转换为数字值
    B L = V x V r e f × 4095 BL = \frac{V_x}{V_{ref}} \times 4095 BL=VrefVx×4095
    • BL 是 ADC 转换得到的数字值,12 位 ADC 的最大值是 4095(即 2^12 - 1)
    • Vx 是输入的电压值
    • Vref 是参考电压,通常为 3.3V 或 5V
  1. ARM下的ADC特点
    • 分辨率:ADC 的分辨率决定了它能将模拟量细分为多少个数字值。例如,12 位分辨率的 ADC 能将输入电压分成 4096 个离散的数字量(即 0 - 4095)
    • 采样速度:这是 ADC 每秒能够执行的转换次数,通常以 Samples Per Second(SPS,样本/秒)为单位
    • 参考电压(Vref):这是 ADC 转换时的参考电压,通常由外部或者内部的电压源提
  2. ADC编程步骤
    • 配置 GPIO 引脚为模拟输入模式: 在一些 ARM Cortex-M 系列的 MCU 中,ADC 通道与特定的 GPIO 引脚绑定,在使用 ADC 之前需要将 GPIO 配置为模拟模式。
    • 配置 ADC 模块:
      • 配置分辨率和采样速度(如 12 位,1 MSPS)。
      • 设置参考电压(Vref)。
    • 启动转换: 启动 ADC 转换,等待 ADC 模块完成采样并将结果存储在数据寄存器中。
    • 读取 ADC 数据寄存器: 一旦转换完成,ADC 数据寄存器将存储转换后的数字值,可以通过访问寄存器读取这个值。
  3. ARM 典型寄存器配置(伪代码)
c 复制代码
// 伪代码 - ARM ADC 配置与读取
//基于 ARM Cortex-M 系列的 ADC 配置步骤
void ADC_Init(void)
{
    // 1. 配置 GPIO 引脚为模拟输入模式
    GPIO_PinConfig(GPIOA, PIN1, GPIO_MODE_ANALOG);

    // 2. 配置 ADC 模块
    ADC1->CR1 = 0;               // 配置为12位模式
    ADC1->SQR1 = 0;              // 设置规则序列长度为 1
    ADC1->SMPR2 |= ADC_SMPR2_SMP10; // 设置采样时间

    // 3. 启动 ADC
    ADC1->CR2 |= ADC_CR2_ADON;    // 启用 ADC 模块
}

uint16_t ADC_Read(void)
{
    // 4. 启动转换
    ADC1->CR2 |= ADC_CR2_SWSTART; // 启动转换

    // 5. 等待转换完成
    while(!(ADC1->SR & ADC_SR_EOC)); // 等待转换结束

    // 6. 读取 ADC 数据寄存器
    return ADC1->DR; // 返回转换后的值
}
  1. ARM 下的 ADC 实际应用
    • 温度测量:读取热敏电阻输出的模拟电压值,将其转换为数字值,再根据转换结果计算温度。
    • 电压监测:通过电压分压电路,监测电池或电源的电压值,进而判断电源状态。
    • 光强度检测:读取光电二极管或光敏电阻的模拟信号,转换为数字量后判断环境光照强度。

ARM的ADC编程实例(main.c)

  1. 第一步:UART初始化
    目的:初始化串口通信模块,使 CPU 可以通过 UART 向外部设备发送字符数据(例如,输出 ADC 转换得到的电压值
c 复制代码
#define	GPA1CON  	*(volatile long*)0x11400020	// GPIO A1 控制寄存器地址
#define	ULCON2		*(volatile long*)0x13820000	// UART 控制寄存器 ULCON2,设置 UART 数据格式
#define	UCON2		*(volatile long*)0x13820004	// UART 控制寄存器 UCON2,设置 UART 的控制模式(轮询模式)
#define	UTRSTAT2	*(volatile long*)0x13820010	// UART 状态寄存器,用于查询 UART 是否就绪
#define	UTXH2		*(volatile long*)0x13820020	// UART 发送寄存器,用于向 UART 发送数据

#define	UBRDIV2		*(volatile long*)0x13820028	// UART 波特率寄存器,设置波特率
#define	UFRACVAL2 	*(volatile long*)0x1382002C	// UART 波特率分数寄存器,用于波特率微调

void uart_init(void)
{
	GPA1CON = GPA1CON & ~0xF;                  // 清除 GPA1_0 的配置位,确保后续操作正确
	GPA1CON = GPA1CON | (1<<1);                // 设置 GPA1_0 为 UART 发送功能
	GPA1CON = GPA1CON & ~(0xF<<4);             // 清除 GPA1_1 的配置位
	GPA1CON = GPA1CON | (1<<5);                // 设置 GPA1_1 为 UART 接收功能

	ULCON2 = ULCON2 | 0x3;                     // 设置 UART 数据格式为 8 位数据,无校验位,1 位停止位
	ULCON2 = ULCON2 & ~(1<<2);                 // 确保校验位为无校验

	UCON2 &= ~(3);                             // 清除 UART 控制模式位
	UCON2 |= 1<<0;                             // 设置 UART 为轮询接收模式
	UCON2 &= ~(3<<2);                          // 清除 UART 控制模式位
	UCON2 |= 1<<2;                             // 设置 UART 为轮询发送模式

	UBRDIV2 = 53;                              // 设置 UART 波特率分频值
	UFRACVAL2 = 4;                             // 设置 UART 波特率的分数部分
}
  1. 第二步:ADC初始化
    目的:初始化 ADC,使 CPU 能够从指定的 ADC 通道读取模拟电压值,并转换为数字信号
c 复制代码
#define	ADCCON  *(volatile long*)0x126C0000    // ADC 控制寄存器地址
#define	ADCDAT	*(volatile long*)0x126C000C    // ADC 数据寄存器地址
#define	ADCMUX	*(volatile long*)0x126C001C	// ADC 多路复用器寄存器地址,选择 ADC 输入通道

void adc_init(void)
{
	ADCCON |= 1<<16;                           // 启用 12 位分辨率
	ADCCON |= 1<<14;                           // 使能分频器
	ADCCON &= ~(0xFF<<6);                      // 清除原有的分频器设置
	ADCCON |= 19<<6;                           // 设置分频器值为 19
	ADCCON &= ~(1<<2);                         // 禁用空闲模式
	ADCCON |= 1<<1;                            // 启用自动重新转换
	ADCCON |= 1<<0;                            // 启动 ADC 转换

	ADCMUX = 3;                                // 选择 ADC 输入通道 3

	int reg = ADCDAT;                          // 读取一次 ADC 数据寄存器,准备初始化
}
  1. 第三步:获取 ADC 数据并将其转换为电压值
    目的:通过 ADC 读取输入电压并转换为数字值(mV),用于后续的显示。
c 复制代码
int adc_get_mv(void)
{
	int reg = ADCDAT & 0xFFF;                  // 获取 ADC 数据寄存器中的 12 位有效数据
	int mv = reg * 1800 / 4095;                // 将 ADC 数据转换为毫伏(1800mV 为参考电压)
	return mv;                                 // 返回计算出的电压值
}
  1. 第四步:通过 UART 输出电压值
    目的:将 ADC 读取的电压值以字符形式通过 UART 发送,显示到外部设备。
c 复制代码
void main(void)
{
	int mv;
	int qian, bai, shi, ge;

	uart_init();                               // 初始化 UART,配置波特率和 GPIO
	adc_init();                                // 初始化 ADC,设置分频器和输入通道

	while(1){
		mv = adc_get_mv();                     // 获取 ADC 转换得到的电压值(以 mV 为单位)

		// 分离电压值的每一位数字(千、百、十、个位)
		qian = mv / 1000;                      // 计算千位
		putc(qian + '0');                      // 发送千位数字
		
		bai = mv / 100 % 10;                   // 计算百位
		putc(bai + '0');                       // 发送百位数字
		
		shi = mv / 10 % 10;                    // 计算十位
		putc(shi + '0');                       // 发送十位数字
		
		ge = mv % 10;                          // 计算个位
		putc(ge + '0');                        // 发送个位数字

		// 发送 'mv' 后缀,表示毫伏
		putc('m');
		putc('v');
		putc('\r');                            // 发送回车符
		putc('\n');                            // 发送换行符

		mysleep(200);                          // 延时 200ms
	}
}
  1. 第五步:将电压值拆分成千、百、十、个位并输出
    目的:将电压值(以毫伏为单位)拆分成千位、百位、十位和个位,然后逐位通过 UART 输出字符形式的数值,最后附加上 "mv" 单位。
c 复制代码
void main(void)
{
	int mv;
	int qian, bai, shi, ge;

	uart_init();                               // 初始化 UART,配置波特率和 GPIO
	adc_init();                                // 初始化 ADC,设置分频器和输入通道

	while(1){
		mv = adc_get_mv();                     // 获取 ADC 转换得到的电压值(以 mV 为单位)

		// 分离电压值的每一位数字(千、百、十、个位)
		qian = mv / 1000;                      // 计算千位
		putc(qian + '0');                      // 发送千位数字
		
		bai = mv / 100 % 10;                   // 计算百位
		putc(bai + '0');                       // 发送百位数字
		
		shi = mv / 10 % 10;                    // 计算十位
		putc(shi + '0');                       // 发送十位数字
		
		ge = mv % 10;                          // 计算个位
		putc(ge + '0');                        // 发送个位数字

		// 发送 'mv' 后缀,表示毫伏
		putc('m');                             // 发送字符 'm'
		putc('v');                             // 发送字符 'v'
		putc('\r');                            // 发送回车符 '\r'
		putc('\n');                            // 发送换行符 '\n'

		mysleep(200);                          // 延时 200 毫秒,避免过快刷新输出
	}
}
  1. 第六步:延时函数 mysleep
    目的:通过简单的循环实现延时功能,避免程序过快地进行下一轮 ADC 读取与数据输出
cpp 复制代码
void mysleep(int ms)
{
	while(ms--){                               // 每次减少 1 毫秒
		int num = 0x1FFF/2;                    // 初始化计数器,数值决定延时时间
		while(num--);                          // 空循环,达到延时效果
	}
}
  1. 综合示例
cpp 复制代码
//使用 UART 和 ADC 来获取模拟电压值并通过 UART 输出电压读数。它包括 UART 初始化、ADC 初始化、获取 ADC 数据和通过串口输出电压值的过程。
#define	GPA1CON  	*(volatile long*)0x11400020	  // GPIO A1 控制寄存器
#define	ULCON2		*(volatile long*)0x13820000	  // UART2 控制寄存器,设置数据位、停止位、校验等
#define	UCON2		*(volatile long*)0x13820004	  // UART2 控制寄存器,设置轮询、发送和接收模式
#define	UTRSTAT2	*(volatile long*)0x13820010	  // UART2 状态寄存器,检查发送和接收状态
#define	UTXH2		*(volatile long*)0x13820020	  // UART2 发送缓冲寄存器,发送数据
#define	URXH2		*(volatile long*)0x13820024	  // UART2 接收缓冲寄存器,接收数据
#define	UBRDIV2		*(volatile long*)0x13820028	  // UART2 波特率分频寄存器,设置波特率
#define	UFRACVAL2 	*(volatile long*)0x1382002C	  // UART2 波特率小数部分寄存器

// 初始化 UART
void uart_init(void)
{
	GPA1CON = GPA1CON & ~0xF;        // 清除 GPA1_0 的原有配置
	GPA1CON = GPA1CON | (1<<1);      // 设置 GPA1_0 为 UART TX (发送)
	GPA1CON = GPA1CON & ~(0xF<<4);   // 清除 GPA1_1 的原有配置
	GPA1CON = GPA1CON | (1<<5);      // 设置 GPA1_1 为 UART RX (接收)

	ULCON2 = ULCON2 | 0x3;           // 配置 UART2:8 位数据,无校验,1 位停止位
	ULCON2 = ULCON2 & ~(1<<2);       // 设置 1 位停止位
	ULCON2 = ULCON2 & ~(7<<3);       // 禁用校验位
	ULCON2 = ULCON2 & ~(1<<6);       // 禁用红外模式
	
	UCON2 &= ~(3);                   // 清除发送接收的控制配置
	UCON2 |= 1<<0;                   // 轮询模式接收
	UCON2 &= ~(3<<2);                // 清除发送配置
	UCON2 |= 1<<2;                   // 轮询模式发送
	
	UBRDIV2 = 53;                    // 设置波特率的整数部分
	UFRACVAL2 = 4;                   // 设置波特率的小数部分
}

// 发送单个字符
void putc(char ch)
{
	while(  (UTRSTAT2 & (1<<1)) == 0 ) ;   // 等待直到发送缓冲区为空
	UTXH2 = ch;                            // 向 UART2 发送寄存器写入字符
}

// 发送字符串
void puts(char *s)
{
	int i = 0;
	while (s[i]) {            // 遍历字符串
		putc(s[i]);           // 逐字符发送
		i++;
	}
}

#define	ADCCON  *(volatile long*)0x126C0000	 // ADC 控制寄存器
#define	ADCDAT	*(volatile long*)0x126C000C	 // ADC 数据寄存器,存放采样数据
#define	ADCMUX	*(volatile long*)0x126C001C	 // ADC 多路复用器,选择输入通道

// 简单的毫秒级延时函数
void mysleep(int ms)
{
	while (ms--) {
		int num = 0x1FFF / 2;    // 简单的循环延时
		while (num--);
	}
}

// 初始化 ADC
void adc_init(void)
{
	int reg;
	ADCCON |= 1<<16;             // 启用 12 比特分辨率
	ADCCON |= 1<<14;             // 启用分频值
	ADCCON &= ~(0xFF<<6);        // 清除分频值
	ADCCON |= 19<<6;             // 设置分频值为 19
	ADCCON &= ~(1<<2);           // 禁用空闲模式
	ADCCON |= 1<<1;              // 重新转换
	ADCCON |= 1<<0;              // 启动 ADC 转换
	
	ADCMUX = 3;                  // 设置 ADC 输入通道为 3
	
	reg = ADCDAT;                // 读取 ADC 数据寄存器,清除第一次转换的值
}

// 获取 ADC 值并转换为毫伏值
int adc_get_mv(void)
{
	int reg = ADCDAT & 0xFFF;    // 获取 12 位有效的 ADC 数据
	int mv = reg * 1800 / 4095;  // 将 ADC 值转换为电压(1800 毫伏参考电压)
	return mv;
}

// 主函数
void main(void)
{
	int mv;
	int qian, bai, shi, ge;       // 用于存储电压的千位、百位、十位和个位

	uart_init();                  // 初始化 UART
	adc_init();                   // 初始化 ADC

	while (1) {
		mv = adc_get_mv();         // 获取电压值(例如 1658 mV)
		
		qian = mv / 1000;          // 获取千位
		putc(qian + '0');          // 通过 UART 发送千位数字
		
		bai = mv / 100 % 10;       // 获取百位
		putc(bai + '0');           // 通过 UART 发送百位数字
		
		shi = mv / 10 % 10;        // 获取十位
		putc(shi + '0');           // 通过 UART 发送十位数字
		
		ge = mv % 10;              // 获取个位
		putc(ge + '0');            // 通过 UART 发送个位数字

		putc('m');                 // 发送字符 'm'
		putc('v');                 // 发送字符 'v'
		putc('\r');                // 发送回车符
		putc('\n');                // 发送换行符

		mysleep(200);              // 延时 200 毫秒
	}

	return;
}
其余相关代码
  1. start.S
c 复制代码
.global  delay1s                     @ 声明全局符号 delay1s,用于定义延时函数
.text                                @ 代码段的开始
.global _start                       @ 声明全局符号 _start,程序的入口点

_start:
		b		reset                  @ 程序启动时跳转到 reset,执行复位后的初始化操作
		ldr		pc,_undefined_instruction  @ 设置未定义指令异常的处理地址
		ldr		pc,_software_interrupt     @ 设置软件中断异常的处理地址
		ldr		pc,_prefetch_abort         @ 设置预取指令异常的处理地址
		ldr		pc,_data_abort             @ 设置数据访问异常的处理地址
		ldr		pc,_not_used               @ 没有使用的异常,保留
		ldr		pc,_irq                    @ 设置普通中断 (IRQ) 的处理地址
		ldr		pc,_fiq                    @ 设置快速中断 (FIQ) 的处理地址

_undefined_instruction: .word  _undefined_instruction   @ 定义未定义指令异常处理地址
_software_interrupt:	.word  _software_interrupt      @ 定义软件中断异常处理地址
_prefetch_abort:		.word  _prefetch_abort           @ 定义预取异常处理地址
_data_abort:			.word  _data_abort               @ 定义数据异常处理地址
_not_used:				.word  _not_used                 @ 保留,未使用的异常
_irq:					.word  _irq                      @ 定义 IRQ 处理地址
_fiq:					.word  _fiq                      @ 定义 FIQ 处理地址


reset: 
	ldr	r0,=0x40008000          @ 将异常向量表的起始地址设置为 0x40008000
	mcr	p15,0,r0,c12,c0,0       @ 将向量基地址 (VBAR) 设置为 r0(即 0x40008000)

init_stack:
	ldr		r0,=stacktop           @ 获取栈顶指针的值到 r0 中

	/******** SVC 模式的栈 ********/
	mov		sp,r0                 @ 将栈顶指针设置为当前栈指针 (sp)
	sub		r0,#128*4            @ 为 IRQ 模式预留 512 字节的栈空间

	/**** IRQ 模式的栈 ****/
	msr		cpsr,#0xd2           @ 切换到 IRQ 模式
	mov		sp,r0                 @ 设置 IRQ 模式的栈指针
	sub		r0,#128*4            @ 为 FIQ 模式预留 512 字节的栈空间

	/*** FIQ 模式的栈 ***/
	msr 	cpsr,#0xd1           @ 切换到 FIQ 模式
	mov		sp,r0                 @ 设置 FIQ 模式的栈指针
	sub		r0,#0                @ FIQ 模式不需要更多的栈空间

	/*** Abort 模式的栈 ***/
	msr		cpsr,#0xd7           @ 切换到 Abort 模式
	mov		sp,r0                 @ 设置 Abort 模式的栈指针
	sub		r0,#0                @ Abort 模式不需要更多的栈空间

	/*** Undefined 模式的栈 ***/
	msr		cpsr,#0xdb           @ 切换到 Undefined 模式
	mov		sp,r0                 @ 设置 Undefined 模式的栈指针
	sub		r0,#0                @ Undefined 模式不需要更多的栈空间

	/*** System 和 User 模式的栈 ***/
	msr		cpsr,#0x10           @ 切换到 System/User 模式
	mov		sp,r0                 @ 设置 System/User 模式的栈指针,预留 1024 字节空间

	b		main                 @ 跳转到 main 函数,开始执行主程序

delay1s:
	ldr      r4,=0x1ffffff         @ 将延时循环计数器的值装入 r4
delay1s_loop:
	sub    r4,r4,#1               @ 计数器减 1
	cmp   r4,#0                   @ 比较计数器是否归零
	bne    delay1s_loop           @ 如果没有归零,继续循环
	mov   pc,lr                   @ 如果计数结束,返回调用处

.align	4                         @ 对齐到 4 字节边界,优化存储器访问

/****  SWI 中断处理程序  ****/

.data                              @ 数据段的开始

stack:	
	.space  4*512                 @ 为所有模式分配 512 字节栈空间
stacktop:                           @ 栈顶符号

.end                               @ 汇编文件的结束
  1. Makefile
c 复制代码
all:
	arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o start.o start.S
	arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o main.o main.c
	arm-none-linux-gnueabi-ld start.o main.o -Tmap.lds -o main.elf
	arm-none-linux-gnueabi-objcopy -O binary  main.elf main.bin
	arm-none-linux-gnueabi-objdump -D main.elf > main.dis
clean:
	rm -rf *.bak start.o main.o  main.elf main.bin main.dis
  1. map.lds
相关推荐
韦德斯20 小时前
嵌入式Linux的RTC读写操作应用
linux·运维·c语言·arm开发·实时音视频
byte轻骑兵20 小时前
嵌入式 ARM Linux 系统构成全解:从硬件到应用层层剖析
linux·arm开发·arm·嵌入式开发
思尔芯S2C1 天前
面向未来的智能视觉参考设计与汽车架构,思尔芯提供基于Arm技术的创新方案
arm开发·架构·汽车·iot·fpga原型验证·prototyping·智慧视觉
Eternal-Student2 天前
【docker了解】如何将x86镜像转换为适用于Jetson的ARM镜像
arm开发·docker·容器
不怕犯错,就怕不做2 天前
修复kernel编译栈帧大小异常问题error: the frame size of 1928 bytes is larger than 1024 bytes
linux·arm开发·驱动开发
憧憬一下3 天前
UART硬件介绍
arm开发·嵌入式硬件·串口·嵌入式·linux驱动开发
Petal9909124 天前
UEFI学习笔记(十八):ARM电源管理之PSCI和SCMI概述
arm开发·笔记·学习·uefi
古月居GYH4 天前
一文了解ARM内部架构
arm开发·架构
白书宇4 天前
13.100ASK_T113-PRO RTC实验
linux·arm开发·驱动开发·嵌入式硬件·物联网·硬件工程
简简单单一天吃六顿5 天前
rootfs根文件系统在Linux下制作动态库
linux·服务器·arm开发·iot