目录
LED
LED:通过向GPIO口输出特定的高低电平信号,来控制LED灯的亮灭。值得注意的是LED和LCD有引脚共用,想控制lLED亮灭,必须在锁存器打开期间进行(PD2输出高电平),操作完成后,立即关闭锁存器。
原理图:
由图看出:LED一端连接电源,一端连接锁存器,当锁存器打开时,输入低电平,LED两端拥有电压差,LED亮,反之,LED,灭。
关闭所有LED:
void ledOff(void)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);//输出高电平
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);//打开锁存器
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);//关闭锁存器
}
打开其中一个LED:
void ledOne(int i)
{
HAL_GPIO_WritePin(GPIOC,i<<8,GPIO_PIN_RESET);//输出低电平
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);//打开锁存器
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);//关闭锁存器
}
注意:输入0x01点亮第一个灯,0x02点亮第二个,0x04点亮第3个,以此类推。
LCD
LCD:将引脚全部配置成为输出即可,使用官方的头文件。
初始化:
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
显示一行:(不能显示汉字)
int i=1;
char chProc[21];
sprintf(chProc,(const char *)"ok %d ",i);
LCD_DisplayStringLine(Line6,(u8 *)chProc);
显示一个字符:屏幕像素320*240,一行最多20字符,显示在第一行第一个位置即写入参数(Line0,320-16*0),第一行第二个位置(Line0,320-16*1)
char test1='A';
char test2='C';
LCD_DisplayChar(Line0,320-16*0,test1);
LCD_DisplayChar(Line0,320-16*1,test2);
高亮显示:想让某一个字符高亮,在写入之前改变背景色或字体颜色,显示文字输入完成后,恢复背景色和字体颜色。
char test1='A';
char test2='C';
LCD_SetBackColor(Red);
LCD_DisplayChar(Line0,320-16*0,test1);
LCD_DisplayChar(Line0,320-16*1,test2);
LCD_SetBackColor(Black);
按键长按、短按、双击(定时器实现)
使用cubemx软件,利用定时器10ms中断实现按键的长按、短按、双击。
定时器通常是通过一个晶体振荡器和一个计数器实现的。晶体振荡器可以产生稳定的时间基准,计数器可以根据振荡器提供的稳定时钟信号进行计数。当计数达到预设的值时,计数器就会触发一个中断信号。通过调整计数器的预设值和时钟信号的频率,我们就可以实现不同时间间隔的定时器。
例如,我们的频率为80MHz,我们可以将分频系数设置为80-1,重装载值设置为10000-1,则中断时间为80000000/80/10000=100HZ,1/100=0.01秒,即10ms。
定时器10ms进入一次中断,回调函数中读取引脚电平状态,由于按键抖动不会超过10ms,通过多次引脚电平保持时长进行判断,可实现短按、长按、双击。
软件配置:
时钟树配置:
定时器配置(蓝桥杯选手建议使用TIM6):
记得开启NVIC:
引脚配置为输入状态即可。
代码如下:
开启定时器中断代码如下:
HAL_TIM_Base_Start_IT(&htim6);
中断回调函数如下:
struct keystruct {
uint8_t keyLevel;
uint8_t keyShort;
uint8_t keyLong;
uint8_t KeyDouble;
uint8_t keyTime1;
uint8_t keyTime2;
uint8_t keyChoose;
}key[4];
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
key[0].keyLevel=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].keyLevel=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].keyLevel=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].keyLevel=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
for(int i=0;i<4;i++)
{
switch(key[i].keyChoose)
{
case 0:
if(key[i].keyLevel==0)
key[i].keyChoose=1; //按下
break;
case 1:
if(key[i].keyLevel==0)
key[i].keyChoose=2; //消抖
else
key[i].keyChoose=0; //去抖
break;
case 2:
if(key[i].keyLevel==0) //单击计时
key[i].keyTime1++;
else
key[i].keyChoose=3; //已松手
break;
case 3:
if(key[i].keyLevel==1)
{
key[i].keyTime2++; //等待双击开始计时
if(key[i].keyTime2>30) // 等待时间过长,长按短按判断
{
if(key[i].keyTime1<70)
key[i].keyShort=1; //是短按
else
key[i].keyLong=1; //长按
key[i].keyChoose=0; //清除标志
key[i].keyTime1=0;
key[i].keyTime2=0;
}
}
else
key[i].keyChoose=4; //双击开始
break;
case 4:
if(key[i].keyLevel==0) //不是消抖
key[i].keyChoose=5;
else //是抖动
{
if(key[i].keyTime1<70)
key[i].keyShort=1; //是短按
else
key[i].keyLong=1; //长按
key[i].keyChoose=0;
key[i].keyTime1=0;
key[i].keyTime2=0;
}
break;
case 5:
if(key[i].keyLevel==1) //双击松手
{
key[i].KeyDouble=1;
key[i].keyChoose=0;
key[i].keyTime1=0;
key[i].keyTime2=0;
}
break;
}
}
}
屏幕显示代码如下:
void keyProc(void)
{
char chPorc[21];
for(int i=0;i<4;i++)
{
if(key[i].keyShort==1)
{
key[i].keyShort=0;
sprintf(chPorc,(const char *)"KEY SHORT = %d ",i+1);
LCD_DisplayStringLine(Line2,(u8 *)chPorc);
}
if(key[i].keyLong==1)
{
key[i].keyLong=0;
sprintf(chPorc,(const char *)"KEY LONG = %d ",i+1);
LCD_DisplayStringLine(Line2,(u8 *)chPorc);
}
if(key[i].KeyDouble==1)
{
key[i].KeyDouble=0;
sprintf(chPorc,(const char *)"KEY DOUBLE = %d ",i+1);
LCD_DisplayStringLine(Line2,(u8 *)chPorc);
}
}
}
PWM(方法1)
使用定时器实现PWM波,我们使用重装载值可实现输出一定频率的波形,通过设置比较寄存器的值,将高电平与低电平信号按照一定频率交替输出,输出信号便称为PWM波形。
例如,对于一个高电平10V和低电平0V的方波,占空比为50%时,就是一个DC 5V的信号输出。而占空比为10%时,输出电压将只有1V。
产生一个PWM波有两张方式
第一种:具体实现方法如下:
(1)Cubemx中选定需要使用的GPIO口,并将其配置为定时器的PWM输出模式。
(2)按需要选择定时器的模式,并设置所需的预分频系数、自动重载寄存器值、计数器模式等参数。
(3)根据需要设置定时器的比较寄存器值,从而实现对PWM波形的输出控制。
(4)Cubemx中生成代码并导出,通过修改CCR,PSC实现不同频率,不同占空比的PWM波
值得注意的是,PWM模式,在同时输出多路PWM波时,频率是相同的,占空比可以不同,想要同时输出多路不同频率占空比的PWM波,可使用输出比较模式,具体实现可参考下一种。
CUBEMX配置:
本次实验使用TIM2的通道1输出频率1KHZ,占空比10%的PWM波,CUBEMX中更改时钟源为内部时钟,通道一选择PWM Genneration CH1,修改预分频系数80-1,重装载值1000-1,比较寄存器100。
计算公式如下: 频率=80M/(80)/(1000) 占空比=100/1000
代码实现如下:
(1)开启PWM波
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);
(2)修改频率(及修改重装载值)
__HAL_TIM_SetAutoreload(&htim2,99);
__HAL_TIM_SET_AUTORELOAD(&htim2,99);
这两个函数作用是一样的,参数有两个,第一个是定时器选择,第二个是写入的ARR值。
频率=80M/80/ARR,这俩函数原型如下:
(3)修改占空比
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_1,10);
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,10);
这两个函数作用是一样的,参数有三个,第一个是定时器选择,第二个是选择通道,第三是CCR值。
占空比=CCR/ARR,这俩函数原型如下:
PWM波形的频率和占空比是关联的。一般情况下,PWM波形的频率越大,其在电路中的平均功率也会越大,因此在实际应用中需要根据具体情况选择合适的频率和占空比值,以达到最佳效果。
一定记得,使用过程中CCR值不能比ARR值高!!!
PWM(方法2)
上一段介绍了使用PWM Genneration 模式生成PWM波,本文将简绍PWM波生成的另外一种模式,输出比较(Output Compare )生成PWM波。
定时器输出比较是一种在定时器中使用的比较功能。它可以将定时器的计数器值与预设的比较器值进行比较,来对输出电平进行置1、置0或翻转的操作。
CUBEMX配置如下:
修改部分:内部时钟源,通道一模式,分频系数,输出比较模式,记得开启NVIC
代码部分:
开启输出比较:
HAL_TIM_OC_Start_IT(&htim2,TIM_CHANNEL_1);
回调函数:
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM2)
{
if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
{
uint32_t value=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
if(flag)
{
__HAL_TIM_SetCompare(htim,TIM_CHANNEL_1,value+Frq-Dut); //低电平脉冲个数
}
else
{
__HAL_TIM_SetCompare(htim,TIM_CHANNEL_1,value+Dut); //高电平脉冲个数
}
flag=!flag; //轮流设置高低电平
}
}
}
解析:
频率=1000000/Frq 占空比=Dut/Frq
定时器的计数器每次递增时与比较器中的预设值进行比较。当计数器值等于比较器值时,输出端口进行电平翻转,同时触发中断,在中断函数中写入下一次函数触发中断的值(value+Frq-Dut为低电平,value+Dut为高电平)。
IC输入捕获(第一种)
IC输入捕获通常用于测量脉冲信号、计数器、测速器等方面。它的基本原理是通过输入计数器来捕获外部脉冲信号,并将捕获的数据存储在输入寄存器中。
本段将介绍双通道输入捕获。
cubemx配置如下:
模式选择复位,触发源选择TI1FP1,时钟选择内部,分频系数设置,通道一选择直连,通道二选择交叉。
其中,通道一的触发极性选择上升沿,通道二选择下降沿。
记得开启NVIC。
代码如下:
初始化代码
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_2);
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
{
icValue1=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1)+1; //上升沿脉冲个数
}
else if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_2)
{
icValue2=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2)+1; //下升沿脉冲个数
}
}
}
IC输入捕获(第二种)
上一篇讲了双通道输入捕获测频率,一个通道测上升沿,一个通道测下降沿,编码方式简单,但是却占用了两个通道。
本文将介绍一种使用单通道测量频率和占空比的方法。
CUBEMX配置如下:
相比之下,单通道测量频率占空比,只需要选择内部时钟源,通道一选择直连输入,设置一下分频系数,同时开启NVIC。
代码部分:
开启IC捕获
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
中断回调函数
struct
{
uint32_t Value1;
uint32_t Value2;
uint32_t choose;
uint32_t High;
uint32_t Low;
uint32_t Frq;
uint32_t Dut;
}ic[1];
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
{
if(ic[0].choose==0)
{
ic[0].Value1=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1); //记录初始值,即T0时刻值
__HAL_TIM_SET_CAPTUREPOLARITY(htim,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_FALLING); //设置下一次为下降沿捕获
ic[0].choose=1;
}
else if(ic[0].choose==1)
{
ic[0].Value2=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1); //记录高电平时间,即T1时刻值
__HAL_TIM_SET_CAPTUREPOLARITY(htim,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_RISING); //设置下一次为上降沿捕获
if(ic[0].Value2>ic[0].Value1) //计算高电平脉冲个数 T1-T0
ic[0].High=ic[0].Value2-ic[0].Value1;
else if(ic[0].Value2<ic[0].Value1)
ic[0].High=ic[0].Value2+1+0xffff-ic[0].Value1;
else
Error_Handler();
ic[0].Value1=ic[0].Value2;
ic[0].choose=2;
}
else if(ic[0].choose==2)
{
ic[0].Value2=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1); //记录低电平时间,即T2时刻值
if(ic[0].Value2>ic[0].Value1) //计算低电平脉冲个数 T2-T1
ic[0].Low=ic[0].Value2-ic[0].Value1;
else if(ic[0].Value2<ic[0].Value1)
ic[0].Low=ic[0].Value2+1+0xffff-ic[0].Value1;
else
Error_Handler();
ic[0].Frq=1000000/(ic[0].High+ic[0].Low); //计算频率和占空比
ic[0].Dut=ic[0].High*100/(ic[0].High+ic[0].Low);
ic[0].choose=0;
}
}
}
}
注意:中断函数中计算占空比不要用浮点型,浮点数运算需要有额外的寄存器参入计算,是不可重入的。如果要保留为小数,其在其它地方计算。
串口(共3种)
简单分析,阻塞方式接受发送数据,中断接收发送数据,以及串口空闲中断+DMA方式接收发送数据。
阻塞方式:
串口数据发送的阻塞方式一般是通过等待发送缓冲区为空的方式实现。当向串口发送数据时,用户程序将数据写入到串口发送的缓冲区,并通过查询方式检测缓冲区的空闲状态。如果缓冲区为空,则代表数据已经全部发送完毕,程序就可以退出发送函数。
串口数据接收的阻塞方式一般是通过等待接收缓冲区非空的方式实现。当串口接收到数据时,数据会被存储到串口接收缓冲区中。用户程序可以通过查询方式检测缓冲区状态,当缓冲区中有数据时,程序会读取数据并进行处理。如果缓冲区中没有数据,则程序会一直等待。
串口阻塞方式的数据接收和发送虽然简单易用,但由于是采用阻塞方式进行数据接收和发送,会导致程序在等待期间无法执行其他任务,从而降低了系统的效率。其次,如果传输的数据很多并且速率很快,可能会导致接收缓冲区溢出或者发送缓冲区堆积,引起数据丢失或者数据冲突等问题,所以不做具体简绍。
中断方式:
CUBEMX配置如下:
先配置引脚PA9和PA10,在按需设置波特率,开启一下NVIC,即可。
代码部分:
主函数一定记得提前开启一次接收中断。(第二个参数是地址,切记)
HAL_UART_Receive_IT(&huart1, &rx_date, 1);//中断方式接收,一次接收一个字符
中断接收回调 函数如下:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
rxBuf[rxPoint++]=rx_date;
HAL_UART_Receive_IT(&huart1, rx_date, 1); //每一次接收完,下一次都要开启
}
}
发送采用阻塞方式发送:
HAL_UART_Transmit(&huart1, txBuf, sizeof(txBuf), 100);
理论上到这里就可以了,但实际使用过程中,我发现当数据量较多时,配合其他模块一起使用时,容易出现接收不完全的情况。改进方法如下。
处理函数
void uartProc(void)
{
int len=rxPoint; //先获取当前字符长度
HAL_Delay(2); //延时两个毫秒,如果没有接收完成,中断回调函数会改变rxPoint的值,导致len!=rxPoint
if(len==rxPoint) //说明接收完成
{
//添加处理代码
}
}
串口空闲中断+DMA
串口空闲中断是指当串口没有传输数据的时候就会触发相应的空闲中断信号,这个信号可以被外设作为数据传输触发源。而DMA(Data Memory Access)技术是指不需要CPU参与的直接从外设的硬件接口直接读写内存,因此在传输大量数据时的速度非常快,而且可以提高CPU的利用率。
CUBEMX配置如下:
与中断方式下没有太大区别,只是增加了DMA。
代码如下:
在stm32g4xx_it.c的void USART1_IRQHandler(void)函数中添加处理代码
初始化代码:
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1,rx_Buf,64);
中断函数添加处理代码:路径如图
代码如下:
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) //判断是不是串口空闲中断
{
uint8_t test;
__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除标志位
test= huart1.Instance->ISR;
test= huart1.Instance->RDR;
HAL_UART_AbortReceive(&huart1); //关闭串口
temp = hdma_usart1_rx.Instance->CNDTR; //CNDTR寄存器存储剩下空间大小
rx_len = 64-temp; //一共64个,减掉剩余的个数,得到接收到的个数
rx_Flag = 1; //说明发生了空闲中断
}
进行数据处理:
if(rx_Flag == 1)
{
HAL_UART_Transmit_DMA(&huart1,rx_Buffer,rx_len);
//这里自己添加其他处理代码
rx_len=0;
rx_Flag=0;
HAL_UART_Receive_DMA(&huart1,rx_Buffer,64);
}
注意:也可以把中断空闲标志改为中断回调函数,在中断回调函数中处理数据。
注意:也下两行代码需要和CUBEMX生成代码紧挨在一起。
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1,rx_Buf,64);
如图:
串口中断的处理方式是通过中断触发进行数据的传输,这种方式没办法快速缓存大量的数据,导致数据的接收和传输速度有很大的限制,但是通过串口空闲中断+DMA可以解决这个问题。
串口空闲中断+DMA的好处有以下几个方面:
传输速度更快:通过DMA技术,数据的传输速度可以更快,可以提高串口通信的传输速率。
可以大缓存量传输:在传输大量数据时,通过DMA技术可以进行批量传输,从而可以提高传输效率。
降低系统资源占用率:通过使用DMA,CPU无需参与数据传输,大大降低了CPU的占用率,更好的利用了CPU的资源。
注意:在中断函数中,不要使用printf,sprintf,浮点数运算。printf,sprintf效率低,不适合放到中断里面,浮点数运算将涉及到额外的寄存器操作,三者均属于不可重入函数,使用不安全
i2c
软件模拟I2C是指利用单片机的GPIO口模拟I2C总线进行通信,这种方式不需要专门的I2C外设硬件支持,仅通过软件实现I2C的各种通信协议,具有较高的灵活性和兼容性。
起始信号,其具体步骤如下:
把SDA置为高电平。
把SCL置为高电平,等待一个半个I2C时钟周期的时间,确保I2C总线空闲。
把SDA置为低电平,表示发出起始信号。
把SCL置为低电平,为发送第一个字节做准备。
结束信号的步骤如下:
把SDA置为低电平,表示发出结束信号。
把SCL置为高电平,待半个I2C时钟周期后,把SDA设置为高电平,表示结束传输。
软件I2C的读操作
写操作的主体部分,具体步骤如下:
发送开始信号
发送设备地址和读写控制位,根据具体设备的地址和I2C协议可知道读写控制位在设备地址的最后一位。通常读操作控制位为1,写操作控制位为0。
响应设备给出的应答信号。
将要写入的数据发送给设备。
响应设备给出的应答信号。
重复步骤,直到写入所有要发送的数据。
发送结束信号。
读操作的主体部分,具体步骤如下:
发送开始信号
发送设备地址和读写控制位,并响应从设备的应答信号。
发送要读取的寄存器地址,并响应从设备的应答信号。
发送开始信号
发送读取命令,并等待从设备的应答信号。
读取从设备发送的数据,并给出应答信号或非应答信号。
重复步骤,直到读取到所有所需的数据。
发送结束信号。
代码如下:
读一个字节:
int8_t i2cRead(uint8_t address)
{
uint8_t val;
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(address);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
val = I2CReceiveByte();
I2CWaitAck();
I2CStop();
return val;
}
写一个字节:
void i2cWrite(uint8_t address, uint8_t val)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(address);
I2CWaitAck();
I2CSendByte(val);
I2CWaitAck();
I2CStop();
}
值得注意的是,开发板上的EEP芯片是8位的只能写0-255,数据超出的话,可以考虑拆分为低八位,高八位。(嫌麻烦的直接把数字转成字符串存)。
RTC
RTC指的是实时时钟,也称为硬件时钟,用于提供准确的日期和时间信息,其内部具有专门的时钟电路和电池供电电路,可以在断电状态下维持准确的时间计数和日期计算。RTC的主要功能是提供年、月、日、时、分、秒等时间信息,同时还可以产生周期性的报警信号和计时器功能,并支持外部的中断和输入信号。RTC可以根据外部信号进行同步校准,以提高时间精度和稳定性。
CUBEMX配置:
RTC的CUBEMX比较多,值得注意的是 数据格式修改位BCD。
750K/6000/125=1HZ,即一秒一个中断。
代码如下:
因为需要在其他地方修改sAlarm变量,所以把它弄为全局变量。
找到初始化代码, MX_RTC_Init();按下F12跳转到定义。
修改如下:
RTC_TimeTypeDef rtcTime;
RTC_DateTypeDef rtcDate ;
extern RTC_TimeTypeDef sTime;
extern RTC_AlarmTypeDef sAlarm ;
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
HAL_RTC_GetDate(hrtc,&rtcDate,RTC_FORMAT_BIN); //时间日期同时获取
HAL_RTC_GetTime(hrtc,&rtcTime,RTC_FORMAT_BIN);
char chProc[21];
sprintf(chProc,(const char *)"%d%d%d,%d%d%d ",
rtcDate.Year,rtcDate.Month,rtcDate.Date , rtcTime.Hours,rtcTime.Minutes,rtcTime.Seconds);
sAlarm.AlarmTime.Seconds=sAlarm.AlarmTime.Seconds+1; //下一次中断时间等于本次时间+1秒
if(sAlarm.AlarmTime.Seconds==60)
sAlarm.AlarmTime.Seconds=0;
HAL_RTC_SetAlarm_IT(hrtc,&sAlarm,RTC_FORMAT_BIN); //写入
}
ADC采集
ADC,即模数转换器,是一种将模拟信号转换成数字信号的电路。ADC采集是指将模拟信号转换成数字信号的过程,其中ADC芯片负责将模拟信号转换成数字信号,同时需要传感器、放大器、滤波器等辅助电路对模拟信号进行前置处理。
ADC采集过程包括取样、量化和编码3个过程:
取样:将连续的模拟信号转换为离散的模拟信号,即在特定的时间间隔内,对模拟信号进行采样得到离散的样本值。
量化:将连续的模拟信号转换为离散的数字信号,量化是将采样得到的离散信号按照一定的规律进行分段,每一段对应一个数字量,从而将连续信号转换为离散的数字信号。
编码:将采样和量化后得到的数字信号进行编码,以便于存储和传输。编码可以采用二进制补码编码、格雷码编码等方式进行。
ADC采集过程中需要注意以下几点:
采样频率应根据被采集信号的最高频率进行选择,过低的采样率会导致信号失真。
量化精度越高,数字信号的质量越好,但是时间和空间成本也会随之增加。
CUBEMX配置
使能ADC通道,修改时钟分频系数,设置采样时间,通道,采用软件触发,多通道扫描不循环。其它部分,不做修改。
代码部分:
HAL_ADC_Start(&hadc2);
adc1=HAL_ADC_GetValue(&hadc2);
HAL_Delay(1);
adc2=HAL_ADC_GetValue(&hadc2);
多通道采集,校准函数用了和没用没啥太大差别。
校准函数如下:
HAL_ADCEx_Calibration_GetValue(&hadc2,ADC_SINGLE_ENDED);
使用如下:
char chProc[21];
sprintf(chProc,(const char *)"V1=%.2f,V2=%.2f",adc1*3.3/4096,adc2*3.3/4096);
LCD_DisplayStringLine(Line2,(u8 *)chProc);
将ADC采样的数字量转换为对应的模拟电压值时,需要将采集值乘上电压基准(一般是开发板中的供电电压),然后将结果除以2的n次方,其中n为ADC采样分辨率。因为ADC采样取值范围为0到2的n次方-1,所以在电压转换时要除以2的n次方来得到对应的电压值。
该采样分辨率为12位,即n=12,开发板供电通常为3.3V。因此,针对ADC采集到的数字量,我们需要将其乘上3.3V(电压基准)并将结果除以2的12次方,即4096(采样分辨率),才能得到对应的模拟电压值。
例如,如果ADC采集到的数字量为2048,那么它对应的模拟电压值即为:2048 * 3.3 / 4096 = 1.65V
拓展数码管
无需CUBEMX配置,将需要引脚配置为输出即可。
在数码管驱动原理图中,数码管是采用共阴数码管,SN74LS595包含了三个重要的引脚:SER、SCK、RLCK。 在这个过程中,SER、SCK和RLCK的配合是非常重要的。只有当这三个引脚都被正确连接并以正确的方式配合使用,才能成功驱动数码管并在数码管上显示所需的数字。
SER是74LS595串行数据输入引脚,通过每一次SCK上升沿将其电平移进移位寄存器的最低位。而当SCK下降沿时,移位寄存器的数据不发生变化。该过程实现了将数字转换为二进制数据后,一位一位的依次输入到移位寄存器中。
SCK是74LS595串行存储时钟输入引脚,在上升沿时移动数据寄存器中的数据,下降沿时,移位寄存器的数据不发生变化。
RLCK将移位寄存器中的数据迁移到数据存储寄存器中。在SCK边沿上升的同时,即导致移位寄存器的数据向左移动一位,并使移位寄存器最右侧的位被移动到数据存储寄存器的最左侧。当SCK的下降沿出现时,存储寄存器的数据则不会发生变化。该过程实现了数据从移位寄存器到存储寄存器的迁移。
简单来说, 显示一个数字,在SCK高电平期间,SER需要将该数字的二进制代码输入到移位寄存器中,然后RLCK将其移动到存储寄存器中,最后由SN74LS595N芯片输出电平,使数码管显示该数字。
数码管段位表,无需记忆,STC-ISP烧录工具例程有。
seg[17]={ 0x3f,0x06,0x5b,0x4f, 0x66,0x6d,0x7d,0x07,0x7f,0x6f
,0x77,0x7c, 0x39,0x5e,0x79,0x71,0x00};
STC-ISP
代码如下:
void segProc(int num1,int num2,int num3)
{
int bmp;
bmp=Seg7[num3];
for(int i=0;i<8;i++)
{
if(bmp&0x80)
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);
else
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_RESET);
bmp=bmp<<1;
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_RESET);
}
bmp=Seg7[num2];
for(int i=0;i<8;i++)
{
if(bmp&0x80)
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);
else
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_RESET);
bmp=bmp<<1;
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_RESET);
}
bmp=Seg7[num1];
for(int i=0;i<8;i++)
{
if(bmp&0x80)
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);
else
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_RESET);
bmp=bmp<<1;
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_RESET);
}
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2,GPIO_PIN_SET);
}
拓展DS18B20和DHT11
ds18b20:直接使用官方提供的.c.h文件,初始化函数ds18b20_init_x();
需要自己编写ds18b20_read();
uint16_t ds18b20_read(void)//返回值为16位数据
{
u8 val1,val2;
uint16_t val = 0;
ow_reset();
ow_byte_wr(OW_SKIP_ROM);
ow_byte_wr(DS18B20_CONVERT);
delay_us(750000);
ow_reset();
ow_byte_wr( OW_SKIP_ROM );
ow_byte_wr ( DS18B20_READ );
val1 = ow_byte_rd(); //低8位
val2 = ow_byte_rd(); //高8位
val = (val2<<8);
val =val |val1; //整合成16位的温度值
return val ;
}
dht11:直接使用官方提供的.c.h文件,初始化函数dht11_init();。(注意,下一届可能不提供驱动文件,2023年国赛提供了一部分驱动文件。)
使用DHT11_Read_Data()读取温湿度。
void dht11Proc(void)
{
char chProc[21];
u8 tem,him;
DHT11_Read_Data(&tem,&him);
if(tem>0&&him<100)
{
sprintf(chProc,(const char *)"T=%d H=%d",tem,him);
LCD_DisplayStringLine(Line3,(u8 *)chProc);
}
}
其它资料
包含省赛国赛真题,相关驱动代码,开发板工程模板,竞赛笔记。
链接:https://pan.baidu.com/s/1EF8ibwiiwpjjj5spjtN1dw?pwd=dafr
提取码:dafr
最后:
希望下一次参加蓝桥杯的朋友们,熟悉软件的安装使用。在本次的国赛中,出现了很多软件安装失败的选手(包括我),比赛之前,建议卸载一遍,断网安装。
注意事项:大家安装软件,很可能是联网下载的芯片包,但比赛现场,不允许CUBEMX联网获取包。另外,下一届的选手,国赛还得去练习一下根据时序写驱动,以后可能官方就不提供DS18B20和DHT11的驱动了。