前言
该系统在以ARMCortex-M4为处理器的嵌入式芯片STM32开发板下运行。系统通过TFT-LCD电阻触摸屏作为人机交互的方式和信号的输入方式,输入的消息在屏幕上实时可见。
通过DAC+DMA+TIMER的方式,通过引脚,输出频率从1Hz到200KHz的正弦信号、三角信号、矩形信号。其中,矩形信号能够调节占空比从10%至90%。具体的频率可以根据板子的主频更改。在项目验收时系统的误差很小。在100K以下,示波器矫正好的情况下,调多少输出多少。
文章目前还在更新中。可以提供项目源码。
DAC+DMA+TIM配置
芯片用的stm32f407。平台是正点原子的stm32f407最小系统板。
初始化参考《stm32f4xx中文参考手册》
寄存器编写。
stm32f4xx中文参考手册》链接: https://pan.baidu.com/s/1aeG4RD4r8rJq5vnV16efIA 提取码: tgfb
DAC初始化
(开始先讲讲我的编写思路以及过程。
需要CV的话,代码在最后~~)
初始化DAC时钟
RCC->APB1ENR|=1<<29; //使能DAC时钟
使能porta
RCC->AHB1ENR|=1<<0; //使能PORTA时钟
配置DAC
DAC->CR|=1<<1; //DAC1输出缓存不使能 BOFF1=1
DAC->CR|=1<<2; //使用触发功能 TEN1=0
DAC->CR&=~(7<<3); //DAC TIM6 TRGO,不过要TEN1=1才行
DAC->CR|=0<<6; //不使用波形发生
DAC->CR|=0<<8; //屏蔽、幅值设置
DAC->CR|=1<<12; //DAC1 DMA使能
DAC->DHR12R1=0;//使能通道1
总的代码如下:
/******************DAC初始化*************************/
void SineWave_DAC_Config(u8 NewState1)
{
RCC->APB1ENR|=1<<29; //使能DAC时钟
RCC->AHB1ENR|=1<<0; //使能PORTA时钟
DAC->CR|=1<<0; //使能DAC1
DAC->CR|=1<<1; //DAC1输出缓存不使能 BOFF1=1
DAC->CR|=1<<2; //使用触发功能 TEN1=0
DAC->CR&=~(7<<3); //DAC TIM6 TRGO,不过要TEN1=1才行
DAC->CR|=0<<6; //不使用波形发生
DAC->CR|=0<<8; //屏蔽、幅值设置
DAC->CR|=1<<12; //DAC1 DMA使能
DAC->DHR12R1=0;//使能通道1
}
定时器配置
首先使能定时器
RCC->APB1ENR|=1<<4; //TIM6时钟使能
对于波形输出,使用的是基本定时器tim6控制的。
反应到代码上:
TIM6->CR1 &= 0xFF00;
TIM6->CR1|=0<<4;//向上计数模式
TIM6->CR1|=1<<0;//在控制寄存器中,使能timer6
通过预分频寄存器分频
TIM6->PSC=0x0;//
TIM6频率其实关系这最终输出波形的频率。如果想输出速度低一些的话可以通过更改tim6的预分频实现
TIM6->CR2&=~(7<<4);//设置控制寄存器2,将TIM6更新设为TRGO
TIM6->CR2|=2<<4;
TIM6->DIER|=1<<8; //允许更新DMA请求
最终的代码如下
/*********定时器配置************/
void SineWave_TIM_Config( u32 Wave1_Fre ,u8 NewState1)
{
if(NewState1){
RCC->APB1ENR|=1<<4; //TIM6时钟使能
TIM6->PSC=0x0;//
TIM6->CR1 &= 0xFF00;
TIM6->CR1|=0<<4;//向上计数模式
TIM6->ARR=Wave1_Fre;
TIM6->CR2&=~(7<<4);//设置控制寄存器2,将TIM6更新设为TRGO
TIM6->CR2|=2<<4;
TIM6->DIER|=1<<8; //允许更新DMA请求
TIM6->CR1|=1<<0;//在控制寄存器中,使能timer6
// TIM6->CR2 &= (u16)~((u16)0x0070);//设置TIM2输出触发为更新模式
// TIM6->CR2 |=0x0020;//设置TIM2输出触发为更新模式
}
}
DMA配置
void MYDMA_Config(DMA_Stream_TypeDef *DMA_Streamx,u8 chx,u16 *mar)
{
DMA_TypeDef *DMAx;
u8 streamx;
if((u32)DMA_Streamx>(u32)DMA2)//得到当前stream是属于DMA2还是DMA1
{
DMAx=DMA2;
RCC->AHB1ENR|=1<<22;//DMA2时钟使能
}else
{
DMAx=DMA1;
RCC->AHB1ENR|=1<<21;//DMA1时钟使能
}
while(DMA_Streamx->CR&0X01);//等待DMA可配置
streamx=(((u32)DMA_Streamx-(u32)DMAx)-0X10)/0X18; //得到stream通道号
if(streamx>=6)DMAx->HIFCR|=0X3D<<(6*(streamx-6)+16); //清空之前该stream上的所有中断标志
else if(streamx>=4)DMAx->HIFCR|=0X3D<<6*(streamx-4); //清空之前该stream上的所有中断标志
else if(streamx>=2)DMAx->LIFCR|=0X3D<<(6*(streamx-2)+16);//清空之前该stream上的所有中断标志
else DMAx->LIFCR|=0X3D<<6*streamx; //清空之前该stream上的所有中断标志
DMA_Streamx->PAR=DAC_DHR12R1; //DMA外设地址
DMA_Streamx->M0AR=(u32)mar; //DMA 存储器0地址
DMA_Streamx->NDTR=256; //DMA 存储器0地址
DMA_Streamx->CR=0; //先全部复位CR寄存器值
DMA_Streamx->CR|=1<<6; //存储器到外设模式
DMA_Streamx->CR|=1<<8; //循环模式
DMA_Streamx->CR|=0<<9; //外设非增量模式
DMA_Streamx->CR|=1<<10; //存储器增量模式
DMA_Streamx->CR|=1<<11; //外设数据长度:16位
DMA_Streamx->CR|=1<<13; //存储器数据长度:16位
DMA_Streamx->CR|=1<<16; //top优先级
DMA_Streamx->CR|=1<<17;
DMA_Streamx->CR|=0<<21; //外设突发单次传输
DMA_Streamx->CR|=0<<23; //存储器突发单次传输
DMA_Streamx->CR|=(u32)chx<<25;//通道选择
DMA_Streamx->CR|=1<<0; //开启DMA传输
//DMA_Streamx->FCR=0X21; //FIFO控制寄存器
}
码表设置
正弦码表
首先用一个数组保存码表,里面实际上存储的是电压值
#define Vref 3.2 //0.1~3.3V可调
#define Um (Vref/2)
#define N 256
u16 SineWave_Value[256]; //已用函数代替
SineWave_Data( N ,SineWave_Value); //生成波形表1
/********生成正弦波形输出表***********/
void SineWave_Data( u16 cycle ,u16 *D)
{
u16 i;
for( i=0;i<cycle;i++)
{
D[i]=(u16)((Um*sin((1.0*i/(cycle-1))*2*PI)+Um)*4095/3.3);
}
}
对算式的解释
D[i]=(u16)((Umsin((1.0i/(cycle-1))2PI)+Um)*4095/3.3);
-
(1.0*i/(cycle-1))
: 这部分是用来生成正弦波的周期。i
是循环变量,从 0 到cycle-1
,这个值会决定正弦波形的采样点数。将i
值除以cycle-1
可以确保在一个周期内等间隔地采样。 -
2*PI
: 这是一个常量,表示一个完整的圆周。在这里乘以前面的部分相当于将周期映射到 0 到 2π 的范围内。 -
sin()
: 这是一个数学函数,用来计算正弦值。它接受一个角度(以弧度为单位)作为输入,并返回该角度的正弦值。 -
Um
: 是一个振幅参数,用来控制正弦波的振幅大小。 -
*4095/3.3
: 这部分是将正弦波的振幅映射到 0 到 4095 的范围内,以适应12位的DAC。通常在嵌入式系统中,模拟信号的范围是 0 到 3.3V,将其映射到 0 到 4095 对应着一种标准的DAC(数模拟转换器)输出范围。
三角波码表
#define N 256
u16 TriWave_Value[256]
TreAngle_Data(N,TriWave_Value);
/********生成锯齿波形输出表***********/
void TreAngle_Data( u16 cycle ,u16 *D)
{
u16 i;
for( i=0;i<cycle/2;i++)
{
D[i]= (u16)(1.0*i/(cycle/2-1)*4095);
D[cycle-1-i]= D[i];
}
D[cycle/2-1]=(u16)(1.0*4095);
}
在函数中,通过一个 for
循环来生成三角波形数据。循环从 0
开始,逐步增加到 cycle/2
。在循环体内,使用了如下公式来计算每个点的数值:
D[i] = (u16)(1.0 * i / (cycle/2 - 1) * 4095);
这个公式的含义是,将当前循环的索引 i
除以周期的一半减一 (cycle/2 - 1)
,然后乘以 4095
,即12位的DAC最大值,这样就生成了三角波形的上半部分数据。
接着,通过 D[cycle-1-i] = D[i];
将生成的数据复制到下半部分。
最后,将数组中间位置的值设置为最大值,以确保波形的上半部分和下半部分连接起来形成闭合的波形。
这样就完成了三角波形输出表的生成。
矩形码表
u16 Square_Value[100];
Square_Data( 5*10 ,Square_Value);
/
void Square_Data(u16 Duty,u16 *D)
{
u16 i;
for( i=0;i<100;i++)
{
if(i<Duty)D[i]= (u16)(1.0*4095);
else D[i]=0;
}
}
函数接受两个参数:Duty
是方波的占空比,D
是一个指向 u16
类型的数组的指针,用来存储生成的方波数据。
在函数中,通过一个 for
循环来生成方波形数据。循环从 0
开始,逐步增加到 99
。在循环体内,通过判断当前索引 i
是否小于 Duty
,来确定方波的状态。如果 i
小于 Duty
,则将数组元素设为最大值 4095
(高电平),否则设为 0
(低电平),这样就形成了方波。
这段代码生成的方波数据长度为 100
,你传入的 Duty
参数表示的是方波的占空比,即方波高电平持续的时间在总周期内的比例。
对于幅度,也可以通过与4095(即12位的DAC最大值)的比例构成。
4095/num=3.2/volt
码表与定时器、dma的配合使用
直接上代码!
#define N 256
u16 SineWave_Value[256];
MZYWave_Init(100);
/***********正弦波初始化***************/
void MZYWave_Init(u16 Wave_Fre)
{
u16 f1=(u16)(84000000/sizeof(SineWave_Value)*2/Wave_Fre);
SineWave_Data( N ,SineWave_Value); //生成波形表1
SineWave_Data( N/2 ,SineWave_Value2); //生成波形表
SineWave_Data( N/4 ,SineWave_Value3); //生成波形表
SineWave_Data( N/16 ,SineWave_Value4); //生成波形表
TreAngle_Data(N,TriWave_Value);
TreAngle_Data(N/16,TriWave_Value2);
Square_Data( 5*10 ,Square_Value);
SineWave_GPIO_Config(ENABLE); //初始化引脚
SineWave_TIM_Config(f1,1); //初始化定时器
SineWave_DAC_Config(1); //初始化DAC
MYDMA_Config(DMA1_Stream5,7,TriWave_Value);//DMA2,STEAM7,CH4,外设为串口1,存储器为SendBuff,长度为:SEND_BUF_SIZE //初始化DMA
TIM6->CR1|=0x01; //使能定时器2; //使能TIM6,开始产生波形
DMA1_Stream5->CR |=1<<0; //开启DMA传输
// DMA1_Stream5->CR &= ~0x0001;
// TIM6->CR1&=~0x0001; //关定时器
}
计算方式是基于系统时钟频率(84MHz)除以波形表的大小’(sizeof(SineWave_Value))',然后乘以 2,再除以所需的波形频率(Wave_Fre)。这样计算得到的 f1 就是使得定时器产生所需频率的周期。
触摸屏幕的处理
硬件使用正点原子 2.8 寸TFTLCD屏。像素是320*240.
屏幕触摸识别的原理不赘述。在触摸时,屏幕返还给单片机的其实是横纵像素的位置。横坐标(0到320),纵坐标(0到240)。基于这个原理,我们就可以通过识别坐标的方式,判断屏幕按下的位置。
typedef struct _st_2d_int_point_info_
{
unsigned short m_i16x;//
unsigned short m_i16y;//
} ST_2D_INT_POINT_INFO;
这段代码用于定义x,y坐标。这种方式可以用于各种二维坐标的判断。比如飞卡二维图像的处理。
typedef struct _my_lcd_key_
{
ST_2D_INT_POINT_INFO SPOT_PUTIN;
uint32_t KeyNum;
unsigned short KeySelceted;
unsigned short KeySelcetedTimes;
uint32_t FreIn;
unsigned char Singal_Type;
unsigned char Square_Duty;
unsigned char Duty_Seceled;
} MY_LCD_KEY;
SPOT_PUTIN是有用的,其余变量只是做个例子。
tp_dev.scan(0); //扫描放TIM3里面
if(TIMCnt.KeyDown==3) //触摸屏被按下
{
if(tp_dev.x[0]<lcddev.width&&tp_dev.y[0]<lcddev.height)
{
LCD_ZONE_SECLECT(tp_dev.x[0],tp_dev.y[0]);
}
}else
{
LCD_KEY.KeySelceted=0;
}
-tp_dev.scan(0) 是正点原子官方提供的函数,在touch.c里面。还是有几百行,我就不想写了。
- lcddev.width是屏幕长度,lcddev.height是屏幕宽度。
- KeyDown=3前面跟着一个消抖过程。
void LCD_ZONE_SECLECT(u16 spotx,u16 spoty)
{
uint32_t FreSet=LCD_KEY.FreIn;
u16 f1;
int i;
u16 *add;
u16 IntPart;
u16 TempPart;
ST_2D_INT_POINT_INFO LCD_UI={20,150};
ST_2D_INT_POINT_INFO STEP={60,30};
LCD_KEY.SPOT_PUTIN.m_i16x=spotx;
LCD_KEY.SPOT_PUTIN.m_i16y=spoty;
// LCD_Clear(WHITE);
// for(i=0;i<16;i++)
// LCD_Fast_DrawPoint(SineWave_Value4[i]/50,i,RED);
if (!LCD_KEY.KeySelceted)
{
LCD_KEY.KeySelceted=1;//防长按,按一次不抬起认为是按一次
LCD_KEY.KeySelcetedTimes++;
if (LCD_KEY.SPOT_PUTIN.m_i16x > LCD_UI.m_i16x && LCD_KEY.SPOT_PUTIN.m_i16x < LCD_UI.m_i16x + STEP.m_i16x)//按了按键1
{
if (LCD_KEY.SPOT_PUTIN.m_i16y> LCD_UI.m_i16y && LCD_KEY.SPOT_PUTIN.m_i16y<LCD_UI.m_i16y + STEP.m_i16y )
{
LCD_KEY.KeyNum=1;//后面用来运算
}
else if (LCD_KEY.SPOT_PUTIN.m_i16y < LCD_UI.m_i16y + 2*STEP.m_i16y && LCD_KEY.SPOT_PUTIN.m_i16y>LCD_UI.m_i16y + STEP.m_i16y )
{
LCD_KEY.KeyNum=4;//后面用来运算
}
else if (LCD_KEY.SPOT_PUTIN.m_i16y < LCD_UI.m_i16y + 3*STEP.m_i16y && LCD_KEY.SPOT_PUTIN.m_i16y>LCD_UI.m_i16y + 2* STEP.m_i16y )
{
LCD_KEY.KeyNum=7;//后面用来运算
}
else if (LCD_KEY.SPOT_PUTIN.m_i16y < LCD_UI.m_i16y + 4*STEP.m_i16y&& LCD_KEY.SPOT_PUTIN.m_i16y>LCD_UI.m_i16y + 3* STEP.m_i16y)
{
LCD_KEY.KeyNum=11;
}
else if (LCD_KEY.SPOT_PUTIN.m_i16y < LCD_UI.m_i16y + 5*STEP.m_i16y&& LCD_KEY.SPOT_PUTIN.m_i16y>LCD_UI.m_i16y + 4* STEP.m_i16y)
{
LCD_KEY.KeyNum=13;
LCD_KEY.Singal_Type++;
if (LCD_KEY.Singal_Type==3)
{
LCD_KEY.Singal_Type=0;
}
if (LCD_KEY.Singal_Type==0)
{
LCD_KEY.Duty_Seceled=1;
Show_Str(10,60,200,16,"波形类型:正弦",16,0);
}
else if (LCD_KEY.Singal_Type==1)
{
LCD_KEY.Duty_Seceled=1;
Show_Str(10,60,200,16,"波形类型:三角",16,0);
}
else if (LCD_KEY.Singal_Type==2)
{
Show_Str(10,60,200,16,"波形类型:方波",16,0);
}
}
}
else if (LCD_KEY.SPOT_PUTIN.m_i16x > LCD_UI.m_i16x + STEP.m_i16x && LCD_KEY.SPOT_PUTIN.m_i16x < LCD_UI.m_i16x + 2*STEP.m_i16x)
{
if (LCD_KEY.SPOT_PUTIN.m_i16y> LCD_UI.m_i16y && LCD_KEY.SPOT_PUTIN.m_i16y<LCD_UI.m_i16y + STEP.m_i16y )
{
LCD_KEY.KeyNum=2;//后面用来运算
}
else if (LCD_KEY.SPOT_PUTIN.m_i16y < LCD_UI.m_i16y + 2*STEP.m_i16y && LCD_KEY.SPOT_PUTIN.m_i16y>LCD_UI.m_i16y + STEP.m_i16y)
{
LCD_KEY.KeyNum=5;//后面用来运算
}
else if (LCD_KEY.SPOT_PUTIN.m_i16y < LCD_UI.m_i16y + 3*STEP.m_i16y && LCD_KEY.SPOT_PUTIN.m_i16y>LCD_UI.m_i16y + 2* STEP.m_i16y)
{
LCD_KEY.KeyNum=8;//后面用来运算
}
else if (LCD_KEY.SPOT_PUTIN.m_i16y < LCD_UI.m_i16y + 4*STEP.m_i16y&& LCD_KEY.SPOT_PUTIN.m_i16y>LCD_UI.m_i16y + 3* STEP.m_i16y)
{
LCD_KEY.KeyNum=12;
}
else if (LCD_KEY.SPOT_PUTIN.m_i16y < LCD_UI.m_i16y + 5*STEP.m_i16y&& LCD_KEY.SPOT_PUTIN.m_i16y>LCD_UI.m_i16y + 4* STEP.m_i16y)
{
LCD_KEY.KeyNum=0;
Show_Str(90,120,200,16,"停止",16,0);
TIM6->CR1&=~0x0001; //关定时器
}
}
上图是关于识别屏幕按下范围的部分代码。您可以按照自己的想法优化一下。首先解释下变量:
- 1
LCD_KEY.SPOT_PUTIN.m_i16x=spotx;
LCD_KEY.SPOT_PUTIN.m_i16y=spoty;
是传入屏幕按下的横纵坐标。不使用全局变量,提高函数内聚性。
- 2
ST_2D_INT_POINT_INFO LCD_UI={20,150};是如下图的,一个键盘,坐上脚,的起始点。
ST_2D_INT_POINT_INFO STEP={60,30};是键盘中每条横线的间隔。
- 3
u16 IntPart;
u16 TempPart;
用于保存需要输出的频率的整数和小数部分。(使用前把示波器自校准下,小数部分也能准确输出)
判断的方式其实很粗糙和直接,判断按下的像素点是否处于某个区间。由于误差的存在,每个数字的判断范围拉的稍微大一点。
如果你有其它的方法也说出来吧我也学习下。
下面是关于代码的解释。例如
if (LCD_KEY.SPOT_PUTIN.m_i16x > LCD_UI.m_i16x && LCD_KEY.SPOT_PUTIN.m_i16x < LCD_UI.m_i16x + STEP.m_i16x)//按了按键1
判断是否处于数字一的范围。如果是,就保存1.在最后,参与最终的频率值的计算。如下
if (LCD_KEY.Duty_Seceled)
{
if (LCD_KEY.KeyNum==11)
{
FreSet = FreSet/10;
}
else if (LCD_KEY.KeyNum==12)
{
FreSet = FreSet * 10;
}
else if (LCD_KEY.KeyNum>0&&LCD_KEY.KeyNum<=9)
{
FreSet = FreSet*10+LCD_KEY.KeyNum;
}
LCD_KEY.KeyNum=0;
}
else
{
if (LCD_KEY.KeyNum>0&&LCD_KEY.KeyNum<=9)
LCD_KEY.Square_Duty=LCD_KEY.KeyNum;
}
if (FreSet<200001)
{
LCD_KEY.FreIn = FreSet;
}
LCD_KEY.KeyNum 就是按下的值,不同的数字对应不同的计算方式。
–最新写于2024.7.13-- 更新中