一、项目背景与原理

1.1 项目目标

本项目的核心目标是:

  • 用 STM32 自带的 ADC 读取光敏电阻的模拟电压值

  • 利用 DMA 实现数据的自动传输,解放 CPU 资源

  • 将采集到的数据通过 USART 串口发送到电脑,在串口助手上实时显示

1.2 关键技术原理

  • 光敏电阻:光照越强,电阻值越小。通过分压电路将电阻变化转化为电压变化,再由 ADC 采集电压值。

  • ADC(模数转换器):将光敏电阻输出的连续模拟电压,转换为单片机能处理的数字信号(12 位精度,数值范围 0~4095)。

  • DMA(直接存储器访问):让数据从 ADC 外设直接传输到内存,无需 CPU 干预,实现高效连续采样。

  • USART 串口通信:将采集到的数字数据,通过串口发送到电脑端的串口助手进行显示。

二、硬件连接

我使用的是 STM32F103VET6 开发板,硬件连接非常简单:

  • 光敏电阻电路:由于是集成在STM32F103VET6 开发板上的,不用进行硬件的连接,下面我就放上它的原理图。

我对原理图的认识如下(可能存在错误,若有理解错误的地方请大家谅解):电路分为两路独立的信号采集通道,最终都通过排针J3连接到 STM32 的PC1PC2引脚(ADC1 输入通道)。

通道 12(PC2):光敏电阻分压采集通道。

通道 11(PC1):可调分压采集通道。

此张表格信息来自高博士ppt或哔哩哔哩网课

以上图片的解释来自AI。若是大家仍然有不理解的地方,可以参考以下链接的课程内容。

[13-02]STM32的ADC外设详解_哔哩哔哩_bilibili

  • 串口连接

    • STM32 的 USART1_TX(PA9) 接 USB 转 TTL 模块的 RX

    • STM32 的 USART1_RX(PA10) 接 USB 转 TTL 模块的 TX

    • 开发板和 USB 转 TTL 模块的 GND 共地。

三、CubeMX 配置(硬核操作过程)

3.1 基础配置

  1. 新建工程:ADC_DMA_USART_2,选择对应的 STM32F103VET6 芯片。

  2. 配置 RCC 时钟,使用外部晶振,将系统时钟配置为 72MHz。

  3. 配置 SYS,选择 Serial Wire 调试模式。

3.2 ADC1配置

  1. 模式选择:Scan Conversion Mode 扫描模式 关闭

  2. Continuous Conversion Mode 连续转换 开启

  3. 转换通道数:Number Of Conversion 设置为 2

  4. 通道配置:

    • Rank1:ADC1_IN11 (PC1 电位器)

    • Rank2:ADC1_IN12 (PC2 光敏电阻)

  5. 采样时间都设为:55.5 Cycles(数值更稳定)

这个adc1选择了11和12通道,时钟会跳出来一个红色的警告

我把右下角这里调整了一下,红色的警告就消失了

3.3 DMA 配置

ADC1 设置中,开启 DMA请求

3.4 USART 配置

  1. 打开 USART1,模式选择 异步通信

  2. 波特率设置为 115200,数据位 8,无校验位,停止位 1。

配置完成后,点击菜单栏上面的小齿轮生成初始化代码。

进入到main.c代码,增加用户代码以及计算的公式。

借助AI工具,它提供了一定的代码给我们(软件自己会生成一定代码,会有点代码重复)

#include <stdio.h> //标准输入输出头文件,支持printf、scanf
#include <string.h> // 字符串处理头文件,支持strlen、strcmp、strcpy等

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
UART_HandleTypeDef huart1;

代码在软件中粘贴后的情况

#define ADC_DMA_BATCH_LEN 2 //ADC DMA一次搬运数据个数
uint16_t adc_dma_buffer[ADC_DMA_BATCH_LEN]; //ADC DMA接收缓冲区,存放ADC采样原始值
uint8_t adc_dma_cplt_flg = 0; // ADC DMA传输完成标志位:0未完成,1完成

#define ADC_VREF_PLUS 3300.0f //ADC参考电压 3.3V,单位mV
#define ADC_RESOLUTION 4095.0f //ADC分辨率 12位 最大数值 0~4095

代码在软件中粘贴后的情况

由于写本篇博客文章,中间有断开,写文章的时间不是连续,不是一气呵成的。中间可能就有一点问题,且代码是基于AI辅助做成的,为了大家方便,我这里就把main.c代码直接放在了下方。

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2026 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  ******************************************************************************
  */
/* USER CODE END Header */

#include "main.h"
#include <stdio.h>
#include <string.h>

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
UART_HandleTypeDef huart1;

/* USER CODE BEGIN PV */
#define ADC_DMA_BATCH_LEN 2
uint16_t adc_dma_buffer[ADC_DMA_BATCH_LEN];
uint8_t adc_dma_cplt_flg = 0;

#define ADC_VREF_PLUS 3300.0f
#define ADC_RESOLUTION 4095.0f
/* USER CODE END PV */

/* USER CODE BEGIN 0 */
float ADC_Value_To_Volt(uint16_t adc_value)
{
	return (float)adc_value * ADC_VREF_PLUS / ADC_RESOLUTION / 1000.0f;
}

float Volt_To_LightRes(float volt)
{
	return (volt * 10.0f) / (3.3f - volt);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
	if (hadc->Instance == ADC1)
	{
		HAL_ADC_Stop_DMA(&hadc1);
		adc_dma_cplt_flg = 1;
	}
}
/* USER CODE END 0 */

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
static void MX_USART1_UART_Init(void);

int main(void)
{
	HAL_Init();
	SystemClock_Config();

	MX_GPIO_Init();
	MX_DMA_Init();
	MX_ADC1_Init();
	MX_USART1_UART_Init();

	/* USER CODE BEGIN 2 */
	HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buffer, ADC_DMA_BATCH_LEN);
	/* USER CODE END 2 */

	while (1)
	{
		if (adc_dma_cplt_flg)
		{
			char buf[64];
			float light_volt = ADC_Value_To_Volt(adc_dma_buffer[1]);
			float light_res = Volt_To_LightRes(light_volt);

			sprintf(buf, "LIGHT: ADC=%d | %.2fV | %.2fkΩ\r\n",
					adc_dma_buffer[1], light_volt, light_res);

			HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);

			adc_dma_buffer[0] = 0;
			adc_dma_buffer[1] = 0;
			HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buffer, ADC_DMA_BATCH_LEN);
			adc_dma_cplt_flg = 0;
		}
		HAL_Delay(200);
	}
}

void SystemClock_Config(void)
{
	RCC_OscInitTypeDef RCC_OscInitStruct = {0};
	RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
	RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

	RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
	RCC_OscInitStruct.HSEState = RCC_HSE_ON;
	RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
	RCC_OscInitStruct.HSIState = RCC_HSI_ON;
	RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
	RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
	RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
	if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
	{
		Error_Handler();
	}

	RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
	RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
	RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
	RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
	RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

	if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
	{
		Error_Handler();
	}
	PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
	PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
	if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
	{
		Error_Handler();
	}
}

static void MX_ADC1_Init(void)
{
	ADC_ChannelConfTypeDef sConfig = {0};

	hadc1.Instance = ADC1;
	hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
	hadc1.Init.ContinuousConvMode = ENABLE;
	hadc1.Init.DiscontinuousConvMode = DISABLE;
	hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
	hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
	hadc1.Init.NbrOfConversion = 2;
	if (HAL_ADC_Init(&hadc1) != HAL_OK)
	{
		Error_Handler();
	}

	sConfig.Channel = ADC_CHANNEL_11;
	sConfig.Rank = ADC_REGULAR_RANK_1;
	sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
	if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
	{
		Error_Handler();
	}

	sConfig.Channel = ADC_CHANNEL_12;
	sConfig.Rank = ADC_REGULAR_RANK_2;
	sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
	if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
	{
		Error_Handler();
	}
}

static void MX_USART1_UART_Init(void)
{
	huart1.Instance = USART1;
	huart1.Init.BaudRate = 115200;
	huart1.Init.WordLength = UART_WORDLENGTH_8B;
	huart1.Init.StopBits = UART_STOPBITS_1;
	huart1.Init.Parity = UART_PARITY_NONE;
	huart1.Init.Mode = UART_MODE_TX_RX;
	huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	huart1.Init.OverSampling = UART_OVERSAMPLING_16;
	if (HAL_UART_Init(&huart1) != HAL_OK)
	{
		Error_Handler();
	}
}

static void MX_DMA_Init(void)
{
	__HAL_RCC_DMA1_CLK_ENABLE();

	HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
	HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}

static void MX_GPIO_Init(void)
{
	__HAL_RCC_GPIOC_CLK_ENABLE();
	__HAL_RCC_GPIOA_CLK_ENABLE();
}

void Error_Handler(void)
{
	__disable_irq();
	while (1)
	{
	}
}

#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line)
{
}
#endif

大家可以对比一下,借助ai分析解析一下代码内容

3.5  编译无误以后将代码烧录进单片机

3.6 打开串口助手,观察代码运行后的实验情况

串口助手有网页版也有电脑端的,大家可以自行选择。

为了方便,我这里就用了波特律动网页版串口助手,网址如下:

串口设置

光敏电阻无遮挡的情况(LIGHT: ADC=816 | 0.66V | 2.49kΩ  大致在这个范围内波动)

光敏电阻手指遮挡的情况(LIGHT: ADC=2734 | 2.20V | 20.09kΩ 大致在这个范围内波动)

光敏电阻特性:

  • 光线越强 → 电阻越小
  • 光线越暗 → 电阻越大

数据算是符合物理规律

相关的操作视频请各位小伙伴,关注我的哔哩哔哩视频动态,实验有问题的话可以在博客的评论区或者视频的评论区留言,感谢各位小伙伴的支撑,大家一起共同进步!!

不会c的小谢的个人空间-不会c的小谢个人主页-哔哩哔哩视频

3.7 基于AI的实验总结

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐