1. 项目概述与硬件选型
做嵌入式课设的时候,AD采集和电机调速真的是经典组合了。我自己用STM32F103C8T6这块板子做过好几次类似的项目,性价比真的高,资源够用又不会太浪费。Proteus仿真环境对学生特别友好,不用买实物硬件就能验证方案,特别适合课程设计和毕业设计。
STM32F103C8T6是ST的经典款,72MHz主频的Cortex-M3内核,64KB闪存,20KB RAM,完全够处理AD转换和电机控制任务。最重要的是它有2个12位的ADC,最多16个输入通道,采样速率最高1MHz,做双通道AD采集绰绰有余。电机驱动我用的是L298N,这模块驱动能力够强,能同时带两个直流电机,正反转控制也简单。
OLED显示屏我选了0.96寸的SSD1306,I2C接口的,接线简单不占IO口。显示个电压值、电机状态什么的完全够用。滑动变阻器就是普通的10kΩ可调电阻,用来模拟电压输入。
提示:STM32F103C8T6和R6系列引脚兼容,如果你的老师要求用R6,直接在Keil里改个设备型号就行,代码都不用动。
2. Proteus原理图设计与实战技巧
画原理图这块,新手最容易卡在找元件和接线上了。我刚开始用Proteus的时候,光是找输入输出端子就找了半天。
2.1 核心元件添加与连接
打开Proteus,点击左侧的"P"按钮进入元件库。STM32F103C8T6直接搜"STM32F103C8",别输全称反而找不到。L298N搜"L298",OLED搜"OLED",滑动变阻器搜"POT-HG"。
接线的时候注意,STM32的PA0和PA1用作ADC输入,接两个滑动变阻器的中间引脚。L298N的输入IN1-IN4接STM32的PA4-PA7,这样用4个IO口就能控制两个电机的正反转。OLED的SDA和SCL分别接PB7和PB6,我用的是I2C1。
2.2 常见问题解决方案
AD采集有问题是我踩过最大的坑。一开始电压读数老是乱跳,查了半天才发现是VBAT引脚没接。STM32的VBAT是后备供电引脚,不接的话ADC参考电压会不稳定。简单粗暴的解决方法就是直接把它和VDD连起来。
找输入输出端子有个小技巧:点击左侧工具栏的"Terminals Mode",选"INPUT"或"OUTPUT"就能放置了。放好后双击可以修改标号,我一般直接用引脚名,比如"PA0"、"PA1"这样,不容易乱。
电压探针在"Probe Mode"里,选"VOLTAGE"就能放置。把它放到PA0引脚上,运行时就能实时显示电压值了,调试特别方便。
示波器在"Virtual Instruments"里,图标是个小 oscilloscope。有时候不小心关了示波器窗口,别慌,点菜单的"Debug" -> "Reset Debug Popup Windows",选"Yes"就回来了。
3. 软件编程与代码解析
代码结构我分了几个模块,这样清晰好维护。主程序负责调度,AD采集、PWM生成、电机控制都单独写成模块。
3.1 主程序逻辑设计
主程序里我先初始化各个外设,然后进入循环读取两个ADC的值,根据大小关系决定电机转向和转速。中间值2047对应3.3V/2=1.65V,这样就能根据电位器位置控制正反转了。
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
#include "PWM.h"
#include "MOTOR.h"
uint16_t ADValueA, ADValueB;
int main(void)
{
OLED_Init();
AD_Init();
PWM_Init();
MOTOR_Init();
OLED_ShowString(1, 1, "ADValue:");
OLED_ShowString(2, 1, "State:");
while(1)
{
ADValueA = AD_GetValue(ADC_Channel_0);
ADValueB = AD_GetValue(ADC_Channel_1);
Delay_ms(50);
if(ADValueA > ADValueB)
{
// 电机A正转或反转逻辑
if(ADValueA > 2047){
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
move(1, 1, (ADValueA-2047)*100/2048);
}
// 其他状态处理...
}
else
{
// 电机B控制逻辑
}
}
}
3.2 ADC采集模块实现
ADC配置要注意时钟分频和采样时间。我设的是6分频,因为APB2时钟是72MHz,6分频后ADC时钟是12MHz,在允许范围内。采样时间设为55.5个周期,这样转换时间足够准确。
void AD_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
}
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
return ADC_GetConversionValue(ADC1);
}
3.3 PWM生成与电机控制
PWM用TIM2生成,频率设为2kHz,这个频率对直流电机来说正合适,既听不到噪音控制效果也好。ARR设为100-1,这样调速分辨率是1%,够用了。
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OC3Init(TIM2, &TIM_OCInitStructure);
TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_OC4Init(TIM2, &TIM_OCInitStructure);
TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM2, ENABLE);
TIM_Cmd(TIM2, ENABLE);
}
电机控制函数里,我用select参数选择电机,FX参数控制方向,PWM参数控制速度。L298N的IN1和IN2控制一个电机的方向,ENABLE脚接PWM信号控制速度。
void move(uint8_t select, uint8_t FX, uint16_t PWM)
{
if(select == 1) // 电机A
{
if(FX == 1) // 正转
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare3(PWM);
}
else if(FX == 2) // 反转
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare3(PWM);
}
}
// 电机B控制类似...
}
4. 调试技巧与性能优化
仿真调试的时候,有几个小技巧能省不少时间。首先是ADC读数校准,最好在程序开始时连续读几次丢弃掉,因为前几次读数可能不太准。
PWM频率选择很重要,我用2kHz是因为频率太低电机会有噪音,太高了MOS管开关损耗又大。如果控制的是小型直流电机,1-5kHz都是不错的选择。
注意:Proteus仿真速度比实物慢很多,特别是开了示波器之后。如果仿真卡顿,可以暂时关闭示波器,需要的时候再打开。
电源去耦很重要,虽然在仿真里不太明显,但实际做板子时每个芯片的VCC和GND之间都要加0.1uF的瓷片电容。STM32的VDDA和VSSA也要单独加电容,这对ADC精度影响很大。
如果发现电机干扰ADC读数,可以在软件里加个滤波算法。简单的移动平均滤波就很有效,取10次平均值就能让读数稳定很多。
#define SAMPLE_COUNT 10
uint16_t AD_GetFilteredValue(uint8_t ADC_Channel)
{
uint32_t sum = 0;
for(uint8_t i=0; i<SAMPLE_COUNT; i++)
{
sum += AD_GetValue(ADC_Channel);
Delay_ms(1);
}
return sum / SAMPLE_COUNT;
}
5. 常见问题与解决方案
做这个项目最容易遇到的就是电机干扰问题。表现在仿真里就是ADC读数跳变,电机转动时读数不准。除了加滤波算法外,还可以在硬件上做好隔离。
L298N的供电最好和STM32分开,如果用同一个电源,电机启动时的电流波动会影响ADC参考电压。在Proteus里可以给电机驱动部分单独加个电源,实际做板子时也可以用光耦或者磁耦隔离。
另一个常见问题是PWM输出没反应。先检查时钟配置对不对,TIM2是APB1上的,时钟频率是36MHz。再检查GPIO是否配置为复用推挽输出,普通推挽输出是产生不了PWM的。
OLED显示不正常的话,检查I2C地址对不对。0.96寸OLED的地址通常是0x78或0x7A,用0x78的时候要多。如果显示乱码,可能是初始化序列没写对,江科大的库我用过挺稳的。
ADC读数始终为0的话,先检查VBAT接了吗,然后看看GPIO模式是不是设成了模拟输入(AIN)。如果用库函数开发,还要记得开启ADC和GPIO的时钟,这个很容易忘。
最后提醒一下,Proteus仿真和实物还是有区别的。仿真能跑通不代表实物就一定行,特别是电机这类大电流负载,实际做板子时一定要留足余量,电源线要够粗,地线要完整。
180



被折叠的 条评论
为什么被折叠?



