在嵌入式系统中,微控制器经常需要与现实世界的模拟信号进行交互。STM32微控制器内置了模拟数字转换器(ADC)和数字模拟转换器(DAC),它们是实现这种交互的关键模块。
ADC的作用是将连续变化的模拟电压信号转换为离散的数字值,以便微控制器进行处理。
工作原理:
STM32的ADC通常采用**逐次逼近型(SAR)**架构。其基本思想是通过一系列比较来逐渐逼近输入模拟电压的数字表示。
原理图示意:
虽然STM32内部的ADC结构复杂,但其简化原理可以表示为:
+-------------------+
模拟输入 (Vin) ----| 采样保持电路 (S/H) |----
+-------------------+
|
V
+-------------------+
| 比较器 (Comparator) |----
+-------------------+
|
V
+-------------------+
| 逐次逼近寄存器 (SAR) |----
+-------------------+
|
V
+-------------------+
| 控制逻辑 (Control Logic) |----
+-------------------+
|
V
数字输出 (Digital Out) <--------------------
主要特点:
应用场景:
DAC的作用是将微控制器产生的数字信号转换为连续变化的模拟电压信号。
工作原理:
STM32的DAC通常采用R-2R梯形网络或电阻串型结构。其基本思想是根据数字输入的不同位,通过精确的电阻网络组合,产生不同大小的电流或电压,然后叠加形成最终的模拟输出。
原理图示意:
以简化的R-2R梯形网络为例:
+-------------------+
数字输入 (Dout) | |
(例如12位) | |
D11 -----+---+ R/2R 网络 +----- 模拟输出 (Vout)
D10 -----| | |
... | | |
D0 -----+---+ |
+-------------------+
主要特点:
应用场景:
假设我们要使用ADC1的通道0(PA0引脚)来读取一个模拟电压值,并将其通过UART打印出来。
所需硬件:
代码流程概述:
main
函数中,执行ADC和UART的初始化(CubeMX生成的)。代码示例:
#include "main.h" // 包含CubeMX生成的头文件
#include "string.h" // 用于字符串操作,例如sprintf
// ADC句柄声明 (通常由CubeMX生成在main.h中,或自行声明)
extern ADC_HandleTypeDef hadc1; // 假设CubeMX已生成此句柄
extern UART_HandleTypeDef huart2; // 假设CubeMX已生成此UART句柄
void MX_ADC1_Init(void); // ADC初始化函数声明 (CubeMX生成)
void MX_USART2_UART_Init(void); // UART初始化函数声明 (CubeMX生成)
int main(void)
{
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the System clock */
SystemClock_Config(); // 系统时钟配置 (CubeMX生成)
/* Initialize all configured peripherals */
MX_GPIO_Init(); // GPIO初始化 (CubeMX生成)
MX_ADC1_Init(); // ADC1初始化 (CubeMX生成)
MX_USART2_UART_Init(); // USART2初始化 (CubeMX生成)
uint32_t adc_raw_value;
float voltage;
char uart_buf[50];
while (1)
{
/* 1. 启动ADC转换 */
HAL_ADC_Start(&hadc1);
/* 2. 等待ADC转换完成 */
// 超时时间设置为100ms,可以根据实际情况调整
if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
{
/* 3. 获取ADC转换结果 */
adc_raw_value = HAL_ADC_GetValue(&hadc1);
/* 4. 将原始ADC值转换为实际电压 (假设ADC参考电压为3.3V,12位分辨率) */
// Vout = (ADC_Value / Max_ADC_Value) * Vref
// 对于12位ADC,Max_ADC_Value = 4095
voltage = (float)adc_raw_value / 4095.0f * 3.3f;
/* 5. 打印结果到UART */
sprintf(uart_buf, "ADC Raw: %lu, Voltage: %.3fV\r\n", adc_raw_value, voltage);
HAL_UART_Transmit(&huart2, (uint8_t*)uart_buf, strlen(uart_buf), HAL_MAX_DELAY);
}
/* 6. 停止ADC (可选,如果是非连续模式) */
HAL_ADC_Stop(&hadc1);
/* 延时,以便观察结果,避免UART输出过快 */
HAL_Delay(500); // 每500ms读取一次
}
}
// 注意:MX_ADC1_Init(), MX_USART2_UART_Init(), SystemClock_Config(), MX_GPIO_Init()
// 这些函数通常由STM32CubeMX自动生成,并放在main.c或其他独立的.c文件中。
// 它们会配置ADC和UART的寄存器,包括时钟、引脚、模式等。
/* 示例:MX_ADC1_Init() 结构概要 (CubeMX生成的一部分) */
/*
void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_DIV4; // 时钟预分频
hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 12位分辨率
hadc1.Init.ScanConvMode = DISABLE; // 单通道,不扫描
hadc1.Init.ContinuousConvMode = DISABLE; // 单次转换模式
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; // 软件触发
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = DISABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
// 配置ADC通道
sConfig.Channel = ADC_CHANNEL_0; // 选择通道0
sConfig.Rank = 1; // 序列中的第一个
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES; // 采样时间
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
}
*/
ADC DMA示例 (更高效):
如果需要连续、高速地采集数据,使用DMA是非常高效的方式,可以减轻CPU的负担。
#include "main.h"
#include "string.h"
extern ADC_HandleTypeDef hadc1;
extern UART_HandleTypeDef huart2;
// DMA缓冲区,用于存储ADC转换结果
#define ADC_BUF_SIZE 10 // 假设我们采集10个样本
uint16_t adc_dma_buffer[ADC_BUF_SIZE];
void MX_ADC1_Init(void);
void MX_USART2_UART_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_ADC1_Init(); // 确保在CubeMX中配置ADC的DMA请求
MX_USART2_UART_Init();
char uart_buf[100];
/* 启动ADC DMA传输 */
// 一旦启动,ADC会持续转换并将结果通过DMA存入adc_dma_buffer
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_BUF_SIZE);
while (1)
{
/* 在这里,你可以执行其他任务,ADC转换在后台通过DMA进行 */
/* 例如,周期性地检查DMA缓冲区数据 */
// 注意:如果DMA模式是Circular,数据会不断更新
// 如果是Normal,需要在HAL_ADC_ConvCpltCallback中重新启动DMA
// 简单地打印第一个值作为示例
sprintf(uart_buf, "ADC DMA Raw (first sample): %u\r\n", adc_dma_buffer[0]);
HAL_UART_Transmit(&huart2, (uint8_t*)uart_buf, strlen(uart_buf), HAL_MAX_DELAY);
HAL_Delay(100); // 延时
}
}
// ADC转换完成回调函数 (在stm32f4xx_it.c中实现)
// 当ADC的DMA传输完成一半或全部完成时,会调用此函数
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc->Instance == ADC1)
{
// 可以在这里处理ADC转换完成的数据
// 例如,对adc_dma_buffer中的所有数据进行处理
// 如果DMA模式是Normal,你需要在这里再次启动DMA传输:
// HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_BUF_SIZE);
}
}
// ADC半转换完成回调函数 (仅在DMA模式下)
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc->Instance == ADC1)
{
// 可以在这里处理ADC转换到一半的数据 (例如,处理前半部分的缓冲区)
}
}
假设我们要使用DAC1的通道1(PA4引脚)来输出一个简单的三角波。
所需硬件:
代码流程概述:
main
函数中,执行DAC和定时器的初始化。代码示例:
#include "main.h"
#include "math.h" // 用于生成正弦波,这里我们生成三角波,也可以手动定义
// DAC句柄和定时器句柄 (CubeMX生成)
extern DAC_HandleTypeDef hdac; // 假设DAC1通道1
extern TIM_HandleTypeDef htim6; // 假设使用TIM6作为DAC触发源
void MX_DAC_Init(void); // DAC初始化函数声明
void MX_TIM6_Init(void); // TIM6初始化函数声明
// 定义三角波数据 (例如,12位分辨率DAC,0-4095)
// 这里为了简化,我们定义一个简单的上升和下降序列
#define TRIANGLE_WAVE_SIZE 20
uint16_t TriangleWave[TRIANGLE_WAVE_SIZE] = {
0, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, // 上升
1800, 1600, 1400, 1200, 1000, 800, 600, 400, 200, 0 // 下降
};
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DAC_Init(); // 确保在CubeMX中配置DAC使用TIM6触发和DMA
MX_TIM6_Init();
/* 启动定时器 */
HAL_TIM_Base_Start(&htim6);
/* 启动DAC,通过DMA传输三角波数据 */
// HAL_DAC_Start_DMA(DAC句柄, DAC通道, 数据缓冲区, 缓冲区大小, 数据对齐方式)
// DAC_CHANNEL_1对应PA4
// HAL_DAC_Start_DMA 会将TIM6的更新事件作为触发源,每次触发传输一个数据到DAC
if (HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)TriangleWave, TRIANGLE_WAVE_SIZE, DAC_ALIGN_12B_R) != HAL_OK)
{
Error_Handler();
}
while (1)
{
// DAC和定时器在后台自动运行,生成三角波
// 你可以在这里执行其他任务
HAL_Delay(10);
}
}
// 注意:MX_DAC_Init() 和 MX_TIM6_Init() 也会由CubeMX自动生成。
/* 示例:MX_DAC_Init() 结构概要 (CubeMX生成的一部分) */
/*
void MX_DAC_Init(void)
{
DAC_ChannelConfTypeDef sConfig = {0};
hdac.Instance = DAC;
if (HAL_DAC_Init(&hdac) != HAL_OK)
{
Error_Handler();
}
sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO; // 定时器6触发
sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; // 使能输出缓冲
if (HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
}
*/
/* 示例:MX_TIM6_Init() 结构概要 (CubeMX生成的一部分) */
/*
void MX_TIM6_Init(void)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim6.Instance = TIM6;
htim6.Init.Prescaler = 0; // 根据系统时钟和期望频率设置
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 100 - 1; // 周期,例如100个计数触发一次
htim6.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; // 更新事件作为触发源
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
}
*/
DAC软件模式示例:
如果不需要复杂的波形,或者只需要输出一个固定的模拟电压,可以直接通过软件写入。
#include "main.h"
extern DAC_HandleTypeDef hdac;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DAC_Init();
/* 启动DAC (非DMA模式) */
if (HAL_DAC_Start(&hdac, DAC_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
uint16_t dac_value = 0; // 0-4095 对应 0V-3.3V (假设Vref=3.3V)
while (1)
{
/* 设置DAC输出电压 */
// 这里我们简单地让DAC输出电压在0V到3.3V之间循环变化
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value);
dac_value += 100; // 每次增加100
if (dac_value > 4095)
{
dac_value = 0;
}
HAL_Delay(50); // 延时50ms
}
}
总结:
HAL_ADC_Start
或 HAL_ADC_Start_DMA
),等待转换完成 (HAL_ADC_PollForConversion
或利用回调函数),获取结果 (HAL_ADC_GetValue
)。HAL_DAC_Start
或 HAL_DAC_Start_DMA
),通过软件或DMA提供数据 (HAL_DAC_SetValue
或 DMA传输)。HAL_ADC_ConvCpltCallback
, HAL_DAC_ConvCpltCallback
等) 是HAL库中处理中断和DMA完成事件的重要机制,可以方便地在数据准备就绪时执行自定义逻辑。