零基础蓝桥杯嵌入式教程

写在前面

包含全部模块和lcd高亮和翻转、ADC_DMA等特殊操作。

新建工程

选择new project

84191a4fd55e43219cf50d92485c8399.png

选择正确的型号

6f5085456e0d4aa98cd9509db65df119.png

RCC配置fc2340aedc934282b271f6b17763b548.png

SYS中debug配置

a0f8e8862dd140aaac8547118f91b26f.png

时钟配置8b1849d360224e9182af654d74221d15.png

project设置,其中名称、路径不能带有空格或者中文,否则会生成工程失败。软件包选择下载好的比赛提供的版本。

435f857c74564ef5be3ebe20ba33739f.png

勾选b2f26d07b5e54ce6a643424fc3bdfca9.png

之后点击generate code生成工程,打开工程,debug选项更改为板载调试器

5d866f5b5f914736b2d2e2062fe75ea4.png

C/C++选项添加文件头文件目录,这里新建一个文件夹bsp用来放置个人的文件6ce994f510224614abb2ce7d21bfb2f0.png

ed54577460c341e68170d30e8f554f3e.png

c854897d7627490b9cf9396ad87e03ec.png

utilities中点击setting,选择合适的下载算法a93dc816c7a64291bac3448151e2657b.pnga5d537dbe1be4e979b06f6676d558cf8.png

随后先建一个组,更名为bsp,之后个人的文件添加进这个组中

b1a3b1e57ec74c19acbc680da847e2d0.png

c1e30bbd21e64e78affef8503c96ccdc.png

新建两个文件并保存为my_main.c和my_main.h,保存在bsp文件夹中,右键bsp,添加已经存在的文件,将my_main.c添加 。之前学习时将每个模块分别建立.c和.h文件,但是在不同的模块中使用同一个函数时会出现交叉引用和声明,非常容易出现重复定义和缺少声明,而且每次都要在.h文件中写重复的代码,在main.c中包含头文件和书写初始化函数,比较繁琐和出现问题,所以另建一个个人代码文件,代码都在此。

bf988df24e064b83919ed062b128f7f9.pngb66d50774d00421d8da338d6e4f5943a.png

点击扳手,将字体格式更改为UTF-8,否则中文可能会出现乱码99d91632f1f44abc8f06fb3e7ca37d82.png

个人代码要在begin end中间,否则cubemx重新生成代码后会删除,在main.h里定义uint和uchar。代码编程时注意遵循C语言的模块化编程和基本语法和逻辑规则。

ab56654a054b4cde8dce4b210c04e266.png

在my_main.h和my_main.c中分别添加以下代码 

#ifndef _MY_MAIN_H_
#define _MY_MAIN_H_

#include "main.h"

void setup(void);
void loop(void);

#endif
#include "my_main.h"

void setup(void)
{

}

void loop(void)
{

}

 main.c中添加头文件和setup,loop函数f7c620d4390444728ba1a936ddf8bdc9.pngad725fcdedc14b09965f66e59b19a043.pnga90ca08d702b47c09c13c67d5d172f68.png

LED

根据原理图,LED用到PC8到PC15,左侧引脚可以看做芯片直接和右侧引脚连通,引脚全部设置为output,左侧高电平,右侧低电平的话LED导通,此处使用前四个LED。PD2高电平使能,低电平关闭。设置为关闭。37d32125f2934377b51d0784f3b78318.png

0d4baf21b2084871a9b1ac501b3d5631.png1c37bfe961084ef39dccfe0ad2090a14.png

my_main.h代码 


#include "stdbool.h"

void LED_disp(uint lednumb,bool on_off);


 led.c代码



void LED_disp(uint lednumb,bool on_off)
{
 static uint8_t mmry=0x00;
	if(on_off==1) mmry|=lednumb;
	if(on_off==0) mmry&=~lednumb;
	HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);//GPIO_PIN_All,只有A大写
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);//打开锁存器PD2
	HAL_GPIO_WritePin(GPIOC,mmry<<8,GPIO_PIN_RESET);//mmry左移八位到要控制的高八位
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);//关闭锁存器PD2
}

LED的控制是前一个参数控制灯的编号,具体为十六进制参数转化为二进制后为1的位置,例如LED_disp(0x80,1)的0x80转化为二进制为1000,则控制的是第四位。1是亮,0是灭。闪烁可以通过systick的不断改变flag位来实现,如LED_disp(0x80,flag)。

注意:LED会有残留,不用时需要写清零灭灯语句

KEY

按键有短按长按双击。按键按下,相连引脚低电平,未按下低高电平。用到的按键引脚改为GPIOINPUT,GPIO中把上下拉模式中选择上拉pull up。911520afa5234f34bea15645143595a4.png17a447d52b1842d8b49f236fa74470d8.png
定时器配置,随便找一个TIM4,clock source选择internal clock内部时钟。下面的参数,分频系数psc,自动重装载值ARR,定时器工作频率=外部总线频率/(PSC+1)/(ARR+1),这样就可知每次按键按下的判断一次的时间,可以根据改变按键按下时间的计数times来改变消抖和长按的判别时间。NVIC设置里面enabled中断使能勾选。最后生成工程。458472128b6945dcbc3d1afbabc35cd5.png7e204cc7a08842e1883e8f34c7912bf3.png

在setup函数中开启定时器中断

HAL_TIM_Base_Start_IT(&htim4);

.h代码

#include "tim.h"

实现代码,其中void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)是定时器中断回调函数,每次定时器产生中断都会进入一次,下面if语句判断中断属于定时器4时,会执行其中内容。

typedef struct keys
{
	bool status;
	bool shortdown;
	bool longdown;
	uint8_t times;
}KEY;
 
KEY key[4];
 
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM4)
	{
		key[0].status=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
		key[1].status=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
		key[2].status=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
		key[3].status=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
	}
	for(int i=0;i<4;i++)
	{
		if(key[i].status==0)
		{
			key[i].times++;
		}
		if(key[i].status==1)
		{
			if(key[i].times>40)key[i].longdown=1;
			else if((key[i].times>2)&&(key[i].times<40))key[i].shortdown=1;
			key[i].times=0;
		}
	}
}

key[i].times=0清零要在给出长短按的判断之后,且不能是在for循环之中,否则会一直清零。 

如果不记得中断回调函数如何写,可以在如下文件的靠近最下端处找到。b04719a053454ae5aa4e199c7a4076f5.png

LCD

lcd和led有共用的io口,led和lcd的刷新函数要在同一个顺序执行函数中,不能一个在while1中,另一个在中断中。

不需要cubemx配置,将fotns.h和lcd.h和lcd.c添加到个人工程文件夹bsp,lcd.c加入到工程。036d9d7a1b4a4f2692cfd35836cab621.pngc389e7e123cd497baa64bd4f0e9210c1.png

my_main.h代码


#include "lcd.h"
#include "stdio.h"
void lcd_proce(void);

mylcd.c代码


bool view=0;

void lcd_proce(void)
{
	if(view==0)
	{	
		char text[30];
		sprintf(text,"     0     ");
		LCD_DisplayStringLine(Line3,(uint8_t*)text);
	}
	if(view==1)
	{	
		char text[30];
		sprintf(text,"     1     ");
		LCD_DisplayStringLine(Line3,(uint8_t*)text);
	}
}

在setup中初始化,清屏,设置背景色,设置文本色。显示背景色和前景色就是backcolor和textcolor,用LCD_SetBackColor()LCD_SetTextColor();来实现。选中括号中的颜色后,比如设置的是Black,右键选择Go to Definition Of ‘Black’,能看到定义的其他颜色可填。

LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);

在官方例程里面main.c就是官方的写法例子,在LCD_Init()后就是LCD使用的具体语法。

定义一个数组紧接着写中括号并且加上数字表示数组长度,sprintf(第一个参数text就是定义的数组,”第二个参数写自己想要打印的内容”),打印的内容后面可跟一个数据,则需要加第三个参数并且指名所跟数据的类型和名称,使用sprintf函数显示百分号%时输入%%即可。如

char text[30];
		sprintf(text,"     PA1     ");
		LCD_DisplayStringLine(Line3,(uint8_t*)text);
		sprintf(text,"     F:%dHz     ",pa1_frq);
		LCD_DisplayStringLine(Line4,(uint8_t*)text);
		sprintf(text,"     D:%d%%     ",pa1_duty);
		LCD_DisplayStringLine(Line5,(uint8_t*)text);

566f0cfe77554275bdfef0f78b2f54bb.png能选择的行数Line为0到9一共十行。

高亮显示

如果需要高亮显示,在写sprintf和LCD_DisplayStringLine之前写一下需要高亮的颜色的背景色设置语句,以设置line0为黄色高亮显示为例,其他行不变。

LCD_SetBackColor(Yellow);
Sprintf(text,"numbe",1);
LCD_DisplayStringLine(Line0,(uint8_t *)text,);

LCD_SetBackColor(Black);
Sprintf(text,"numbe",1);
LCD_DisplayStringLine(Line0,(uint8_t *)text,);

 lcd翻转显示

在所给的lcd液晶控制器资料中手册中,搜索diretion能够找到对于显示方向的描述df47efa0d41649e7b6247d7a44ff8d1c.pngSS位控制横向扫描方向,GS位控制纵向扫描方向,搜索GS和SS,能看到分别所在的寄存器位置875a554609da44e7984785523040792a.pngd422e481748a46f98060335822155eb3.png

搜索60h和01h,能找到GS和SS设置。这里的60和01都是16进制,对应1和96号寄存器。0464fc51c4cd47049a57ee5ccac33c7c.png7c9c478c962b44d7aed75df8283d7f0b.pnge30945ff649845fca4d7ae6b6185c6fa.png

lcd.c中将函数void REG_932X_Init(void)复制粘贴到下方,更名为void REG_932X_Init1(void),其中1和96号寄存器对应的SS和GS的值更改即可,此时看到代码注释给了需要更改的值,更改即可。

60f2832e28a64f29bee8d71d5dded3e1.png

1f3eb06e55554f1889cd52943e4c8d00.png

void REG_932X_Init1(void)
{
    LCD_WriteReg(R227, 0x3008);   // Set internal timing
    LCD_WriteReg(R231, 0x0012); // Set internal timing
    LCD_WriteReg(R239, 0x1231);   // Set internal timing
    LCD_WriteReg(R1, 0x0100);   // set SS and SM bit		  //0x0100
    LCD_WriteReg(R2, 0x0700);   // set 1 line inversion
    LCD_WriteReg(R3, 0x1030);     // set GRAM write direction and BGR=1.
    LCD_WriteReg(R4, 0x0000);     // Resize register
    LCD_WriteReg(R8, 0x0207);     // set the back porch and front porch
    LCD_WriteReg(R9, 0x0000);     // set non-display area refresh cycle ISC[3:0]
    LCD_WriteReg(R10, 0x0000);    // FMARK function
    LCD_WriteReg(R12, 0x0000);  // RGB interface setting
    LCD_WriteReg(R13, 0x0000);    // Frame marker Position
    LCD_WriteReg(R15, 0x0000);  // RGB interface polarity
    /**************Power On sequence ****************/
    LCD_WriteReg(R16, 0x0000);    // SAP, BT[3:0], AP, DSTB, SLP, STB
    LCD_WriteReg(R17, 0x0007);    // DC1[2:0], DC0[2:0], VC[2:0]
    LCD_WriteReg(R18, 0x0000);  // VREG1OUT voltage
    LCD_WriteReg(R19, 0x0000);    // VDV[4:0] for VCOM amplitude
    //	Delay_Ms(200);                    // Delay 200 MS , Dis-charge capacitor power voltage
    HAL_Delay(200);
    LCD_WriteReg(R16, 0x1690);    // SAP, BT[3:0], AP, DSTB, SLP, STB
    LCD_WriteReg(R17, 0x0227);  // R11H=0x0221 at VCI=3.3V, DC1[2:0], DC0[2:0], VC[2:0]
    //	Delay_Ms(50);      // Delay 50ms
    HAL_Delay(50);
    LCD_WriteReg(R18, 0x001D);  // External reference voltage= Vci;
    //	Delay_Ms(50);      // Delay 50ms
    HAL_Delay(50);
    LCD_WriteReg(R19, 0x0800);  // R13H=1D00 when R12H=009D;VDV[4:0] for VCOM amplitude
    LCD_WriteReg(R41, 0x0014);  // R29H=0013 when R12H=009D;VCM[5:0] for VCOMH
    LCD_WriteReg(R43, 0x000B);    // Frame Rate = 96Hz
    //	Delay_Ms(50);      // Delay 50ms
    HAL_Delay(50);
    LCD_WriteReg(R32, 0x0000);  // GRAM horizontal Address
    LCD_WriteReg(R33, 0x0000);  // GRAM Vertical Address
    /* ----------- Adjust the Gamma Curve ---------- */
    LCD_WriteReg(R48, 0x0007);
    LCD_WriteReg(R49, 0x0707);
    LCD_WriteReg(R50, 0x0006);
    LCD_WriteReg(R53, 0x0704);
    LCD_WriteReg(R54, 0x1F04);
    LCD_WriteReg(R55, 0x0004);
    LCD_WriteReg(R56, 0x0000);
    LCD_WriteReg(R57, 0x0706);
    LCD_WriteReg(R60, 0x0701);
    LCD_WriteReg(R61, 0x000F);
    /* ------------------ Set GRAM area --------------- */
    LCD_WriteReg(R80, 0x0000);    // Horizontal GRAM Start Address
    LCD_WriteReg(R81, 0x00EF);    // Horizontal GRAM End Address
    LCD_WriteReg(R82, 0x0000);  // Vertical GRAM Start Address
    LCD_WriteReg(R83, 0x013F);  // Vertical GRAM Start Address
    LCD_WriteReg(R96, 0xA700);  // Gate Scan Line		  0xA700
    LCD_WriteReg(R97, 0x0001);  // NDL,VLE, REV
    LCD_WriteReg(R106, 0x0000); // set scrolling line
    /* -------------- Partial Display Control --------- */
    LCD_WriteReg(R128, 0x0000);
    LCD_WriteReg(R129, 0x0000);
    LCD_WriteReg(R130, 0x0000);
    LCD_WriteReg(R131, 0x0000);
    LCD_WriteReg(R132, 0x0000);
    LCD_WriteReg(R133, 0x0000);
    /* -------------- Panel Control ------------------- */
    LCD_WriteReg(R144, 0x0010);
    LCD_WriteReg(R146, 0x0000);
    LCD_WriteReg(R147, 0x0003);
    LCD_WriteReg(R149, 0x0110);
    LCD_WriteReg(R151, 0x0000);
    LCD_WriteReg(R152, 0x0000);
    /* Set GRAM write direction and BGR = 1 */
    /* I/D=01 (Horizontal : increment, Vertical : decrement) */
    /* AM=1 (address is updated in vertical writing direction) */
    LCD_WriteReg(R3, 0x01018);    //0x1018

    LCD_WriteReg(R7, 0x0173);   // 262K color and display ON
}

 随后在lcd.h中将新的函数声明。

void REG_932X_Init1(void);

通过搜索发现此函数是在lcd初始化函数中调用的,所以在需要反转显示的位置前调用LCD_Clear(Black)清屏后调用新的修改后的函数void REG_932X_Init1(void)。97124ce6086a4836b6f54bece5dffa38.png

PWM

以PA6和PA7为例,引脚配置为CH1通道,CH1N为互补PWM波,不选,选择定时器和对应的CH1的模式,以TIM16为例,TIM17同理。预分频(psc):CPU运行频率先经过它分频再进入计时器,如CPU运行在 x Mhz 下,预分频为 y,那进入计时器的频率也就为 x/(y+1) Mhz(因为从0计数,所以是y+1)。自动重装值(arr):指一次周期的计数长度。脉冲长度(pulse):指输出脉冲的计数长度。

频率=外部总线频率/(PSC+1)/(ARR+1),占空比为pulse/(ARR+1),PSC,ARR,pulse在代码中可以重新设置和调整:


/*

__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,180); //修改CCR寄存器
__HAL_TIM_SET_AUTORELOAD(&htim2,1440); //修改ARR寄存器,没有通道限制,整个定时器一起改
__HAL_TIM_SET_PRESCALER(TIM_TypeDef* TIMx, uint32_t Prescaler);//修改预分频系数
举一个例子:
__HAL_TIM_SET_PRESCALER(&htim2, 1000);
这条语句就是把定时器二的预分频系数设为1000
*/

a0f34979b3664fa0a44908baf20cfd6b.pngfcd1c0242c624be690d7978ab009cb49.png

my_main.h代码

void pwm_proce(void);

my_main.c代码

int pa6_duty=200;
int pa7_duty=100;

void pwm_proce(void)
{
	__HAL_TIM_SetCompare(&htim16,TIM_CHANNEL_1,pa6_duty);
	__HAL_TIM_SetCompare(&htim16,TIM_CHANNEL_1,pa7_duty);
}

setup中开启

HAL_TIM_PWM_Start(&htim16,TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim17,TIM_CHANNEL_1);

输入捕获频率和占空比

可用CH1和CH2通道的引脚(主要)

通过跳线帽连接到PA15和PB4引脚,选择CH1通道,只有ch1和ch2可以从模式清除,这里选择了TIM2和TIM3的CH1。b84598743cd4474bb640bfe6d633a19c.png

31064fee831c4949998ef7da6e232480.pngbadcdc8f1d2549c4aaded72eabbbac50.png

TI1FP1对上升沿敏感,TI1FP2对下降沿敏感,CNT计数器在80MHZ经过分频后的频率下进行计数(比如分频80,则一微秒计时一次),而ccr1和ccr2则会在上升和下降沿来临时记录下cnt的值,cnt通过从模式在上升沿来临时,ccr1存下数据后归位。由此ccr1就是一个周期的计数,ccr2就是高电平时间的计数。 输入到从模式控制器的信号只能是TI1FP1和TI1FP2,所以只能用CH1和CH2通道2ed1cb9473df40be945bdc3f4c7e29cd.png

 tim2配置,tim3同理

从模式触发源:所连引脚是通道一就是TI1FP1,通道二就是TI2FP2。

channel1和2的直接模式和间接模式:所用的引脚直接相连的通道号为直接模式,如果引脚是channel2,则channel2为直接模式,否则间接模式占用直接相连的通道,直接模式会另开一个引脚。

channel1和2的上升沿和下降沿:直接通道是上升沿,间接通道是下降沿。

5d1ca50dbfd34fcaaabaf7cef21e42f7.png

 .h

void in_pro(void);

 setup中开启IC输入捕获(频率和占空比分别是上升和下降的通道,防止开错或者忘开,都开启)

	HAL_TIM_IC_Start(&htim2,TIM_CHANNEL_1);
	HAL_TIM_IC_Start(&htim2,TIM_CHANNEL_2);
	HAL_TIM_IC_Start(&htim3,TIM_CHANNEL_1);
	HAL_TIM_IC_Start(&htim3,TIM_CHANNEL_2);

.c

float frq1=0,frq2=0;
float duty1=0,duty2=0;

void in_pro(void)
{
	frq1=1000000.0f/(HAL_TIM_ReadCapturedValue(&htim2,TIM_CHANNEL_1)+1);
	duty1=(HAL_TIM_ReadCapturedValue(&htim2,TIM_CHANNEL_2)+1)*100.0f/(HAL_TIM_ReadCapturedValue(&htim2,TIM_CHANNEL_1)+1);
	
	frq2=1000000.0f/(HAL_TIM_ReadCapturedValue(&htim3,TIM_CHANNEL_1)+1);
	duty2=(HAL_TIM_ReadCapturedValue(&htim3,TIM_CHANNEL_2)+1)*100.0f/(HAL_TIM_ReadCapturedValue(&htim3,TIM_CHANNEL_1)+1);
}

不可用CH1和CH2的引脚(备用)

和能使用从模式清零的方式相比,此方式通用性更好,有的引脚不能使用CH1和CH2,区别在于

1.需首先开启的输入捕获是在中断方式下,函数多了"IT"。

2.判断中断的计时器号,然后判断htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1

3.此方式在读取两个通道的寄存器的值后需要手动归零计时器并且重新打开输入捕获的中断模式。Clock Source选择Integral Clock

以TIM2_CH1为例

Channel1选择 Input Capture direct modeb6d18dde0a574bd198f0977f7b406fb2.png

对方波的一个周期的上升沿时间计数,frq=(80000000/80)/得到的时间值。测量占空比的原理:高电平的时间比上一个周期的时间。在定时器的另外一个通道,配置成间接模式Input Capture indirect mode,直接模式去测上升沿,间接模式去测下降沿。Polarity Selection一个是Rinsing Edge,另一个是falling Edge。打开中断使能。7ce30c72c50b47249782738f9251e8df.png887b8a01580e43358ebc674fd5e9f189.png

剩下一个定时器TIM3同理。 

setup初始化的地方把定时器中断打开。

HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_2);
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_2);

my_main.h

//

my_main.c

double val1a=0,val2a=0;
uint val1b=0,val2b=0;

uint frq1=0,frq2=0;
float duty1=0,duty2=0;

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM2)
	{
		if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
		{
			val1a=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
			val1b=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
			__HAL_TIM_SetCounter(htim,0);
			frq1=(80000000/80)/val1a;
			duty1=(val1b/val1a)*100;
			HAL_TIM_IC_Start(&htim2,TIM_CHANNEL_1);
			HAL_TIM_IC_Start(&htim2,TIM_CHANNEL_2);
		}
	}
	
	if(htim->Instance==TIM3)
	{
		if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
		{
			val2a=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
			val2b=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
			__HAL_TIM_SetCounter(htim,0);
			frq2=(80000000/80)/val2a;
			duty2=(val2b/val2a)*100;
			HAL_TIM_IC_Start(&htim3,TIM_CHANNEL_1);
			HAL_TIM_IC_Start(&htim3,TIM_CHANNEL_2);
		}
	}
}

 `HAL_TIM_IC_Start`: 这个函数用于启动定时器的输入捕获模式,但不启用中断。当只需要获取输入捕获的值而不需要中断处理时,可以使用这个函数。

 `HAL_TIM_IC_Start_IT`: 这个函数不仅启动了定时器的输入捕获模式,还启用了中断。这样,当输入捕获事件发生时,会触发中断,并调用相应的中断处理函数。这个函数适用于需要在输入捕获事件发生时执行某些特定操作的情况。

如果中断回调函数太长记不住也可以在如下文件的后部分找到3fe9083cb50a48bd9f7c0f6b460a22c4.png

ADC_DMA 

af0c2d1fff1f4af4a312f4ceb4379af8.jpege6dd73a869a742c3a03f506adb4c3f74.jpegb2b06e5520a341d496a5328efcaa0999.jpeg

单通道

用到接口PB15和PB125975740ea58d43f7a4eab83041ed76df.png选择ADC IN,左边Analog里面找到PB15用到的ADC1,ADC1里面找到用到的IN 11,选择single-ended,ADC2中的IN15同理。采样时间增大对于稳定结果的作用有限,这里采用一个过采样。cb033f2648564513b452b0198608f1c8.png

.h文件代码

#include "adc.h"
float GETADC_value(ADC_HandleTypeDef *ADCx);

setup校准ADC1

HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);

.c文件代码

float GETADC_value(ADC_HandleTypeDef *ADCx)
{
	float prpt;
	HAL_ADC_Start(ADCx);
	prpt=HAL_ADC_GetValue(ADCx);
	return prpt*3.3f/65536.0f;
}

一般要求浮点保留两位小数,在LCD显示时形式为  V:=%.2f       V:=后面要跟数字,数字是浮点型保留两位小数

定义函数时要写函数返回值类型,后面形参括号里面写形参或者void,主函数调用时就只用写函数名加分号。

获取值代码

ADC_value=GETADC_value(&hadcx);//传参数例如&hadc1给上面的ADCx

DMA(建议)

使用PB12和PB14演示,PB15已经用以上单通道演示过。

使能引脚单端触发,添加DMA设置为扫描模式。参数设置中通道数设置为2,使能扫描模式、连续请求模式、DMA连续请求。使能定时请求,通道采样时间尽可能大,否则占用过多CPU程序可能卡死。

0be66b381dde48b682ac374c7a7f9e48.png7bd493ec927041b090bb4e7161baa316.png

开头设置缓冲区

uint16_t adc_buf[2];

setup中校准和开启DMA 

HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);
HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_buf,2);

数据存在了缓冲区adc_buf[0]和adc_buf[1],可以随时使用,例如显示在屏幕上 ,但是要经过一下处理,例如保留三位小数adc_buf[0]*3.3f/4096.0f,adc_buf[1]*3.3f/4096.0f

char text[30];
sprintf(text,"ADC1:%.3f,2:%.3f",adc_buf[0]*3.3f/4096.0f,adc_buf[1]*3.3f/4096.0f);
LCD_DisplayStringLine(Line0,(uint8_t *)text);

USART 

空闲中断回调函数(简单) 

接受时所使用的空闲中断回调函数是只有在V1.4.0及其之后的版本固件包才有,没有此函数需要用中断回调函数,会麻烦的多,但是可以实现,见后文。

关于格式化输入输出:

例:sscanf(rxdata,"%c",&uart_flag[3]);中的%c是char类型,也可以转化为string类型,需要写为%xs,x为数字,是要转化的长度,例如

sscanf(rxdata,"%4s:%4s:%12s",car_type,car_data,car_time);

在这个sscanf语句中,冒号在格式字符串中的作用是用于匹配输入字符串中的冒号字符。这表示在rxdata中,字符串应该以冒号分隔,并且sscanf将会按照这种格式解析输入字符串。
具体来说:
“%4s” 表示读取最多4个非空格字符的字符串,这里用于匹配 car_type。“:” 表示在输入字符串中匹配冒号字符。
“%4s” 再次表示读取最多4个非空格字符的字符串,用于匹配 car_data。“:” 再次表示匹配冒号。
“%12s” 表示读取最多12个非空格字符的字符串,用于匹配 car_time。
这样的格式说明符确保了输入字符串的特定格式,其中每个字符串之间由冒号分隔。如果rxdata不符合这种格式,sscanf可能无法正确解析字符串,或者解析结果可能不符合预期。
 

C语言中,& 操作符用于获取变量的地址。在 sscanf 函数中,如果你想将读取到的值存储到变量中,需要传递该变量的地址。如果不使用 &,则传递的是变量的值,而不是地址。
对于数组来说,数组名本身就是数组的地址。所以,当你传递数组名时,不需要使用 &。但如果你传递数组的某个元素(如字符或者整型数),那么需要使用 & 来获取该元素的地址。如下:

char usart_flag[5];
 
// 不使用&,传递的是数组的地址
sscanf(rxdata, "%c", usart_flag);
 
// 使用&,传递的是数组的第一个元素的地址
sscanf(rxdata, "%c", &usart_flag[0]);

Cubemx配置,在通信位置connectivity选择uart1,mode改成第二项异步模式Asynchronous。没有配置过LCD的话会默认使用PC4和PC5,需要手动改成PA10和PA9。Baud rate波特率一般使用9600,nvic setting中断打开。ef2f375c44ec49cebb109f2a743351fe.png

空闲中断回调函数位置,建议复制粘贴 

c9de5c0d18d64030b8209efbecac843c.png setup

HAL_UARTEx_ReceiveToIdle_IT(&huart1,(uint8_t *)uart_rx,50);

开启中断只能使用一次,所以接收函数最后还要再开启一次

.h

#include "usart.h"
#include "string.h"

void uart_tx_proce(void);

.c(以接收到四位字符串为例)

char uart_tx[50];
char uart_rx[50];
char text1 [4]="0000";
char text[30];


void uart_tx_proce(void)
{
	sprintf(uart_tx,"tx");
	HAL_UART_Transmit(&huart1,(uint8_t *)uart_tx,strlen(uart_tx),50);
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	sscanf(uart_rx,"%3s",text1);
	 sprintf(text,"text1:%s",text1);
	 LCD_DisplayStringLine(Line1,(uint8_t *)text);
	HAL_UARTEx_ReceiveToIdle_IT(&huart1,(uint8_t *)uart_rx,50);
}

另一种中断回调函数方法(应该用不到)

.c文件中利用中断回调函数进行接收,然后进行处理(示例)及发送写法

char rxdata[30];
uint8_t rxdat;
uchar rx_pointer;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//接收中断
{
	rxdata[rx_pointer++]=rxdat;
	HAL_UART_Receive(&huart1,&rxdat,1,10);
}
//char uart_data[3];
void uart_rx_proce()//数据接收处理程序
{
	if(rx_pointer>0)//说明有数据被接收,进行处理
	{	/*
		if(rx_pointer=1)            恰好接收一位数据
		sscanf(rxdata,"%c",&uart_data[3]);     格式化处理接收到的数据
	
	else
	{
		char temp[20];              发送示例
		sprintf(temp,"Error");         发送示例
		HAL_UART_Transmit(&huart1,(uint8_t *)temp,strlen(temp),50);        发送示例
	}*/
		rx_pointer=0;memset(rxdata,0,30);//清零
	}
}

在 HAL_UART_RxCpltCallback 回调函数中,HAL_UART_Receive_IT 函数首先被调用,它开始等待接收一个字节的数据。然后,rxdata[rx_pointer++]=rxdat; 语句被执行,将接收到的数据存储在 rxdata 数组中,并且 rx_pointer 递增。因此,rx_pointer 表示已经接收到的数据数量。虽然 rxdata[rx_pointer++]=rxdat; 在 HAL_UART_Receive_IT 函数之后,但由于这是在回调函数中,它们实际上是异步执行的。即,当有数据可用时,回调函数被触发,首先会启动下一次接收,然后处理已接收的数据。所以,rxdata[rx_pointer++]=rxdat; 的执行确实发生在HAL_UART_Receive_IT(&huart1,&rxdat,1); 之后。

在C语言中,`uint8_t`是一个无符号的8位整数类型,因此可以用来接收字符。字符在C语言中实际上是以ASCII码的形式存储的,而ASCII码的取值范围是0到255,恰好与`uint8_t`类型的取值范围一致。因此,可以使用`uint8_t`类型来接收字符。

main.c中进行初始化

extern uint8_t rxdat;
HAL_UART_Receive_IT(&huart1,&rxdat,1);

SYSTICK

只需要知道SysTick_Handler(void)是一毫秒进入一次的函数,至于上面编写何种功能的函数,然后放在SysTick_Handler(void)里面从而实现何种功能就根据实际而来,这里的是0.1s反转flag的函数。

//stm32Gxx_it.c里面
//------------------------------------------
int flag=0;
int count=0;
 
void sys_tim(int time_set)//time_set放在一毫秒进入一次的中断SysTick_Handler(void)里面,所以单位是毫秒
{
	count++;
	if(count==time_set)
  {
	flag=!flag;
	count=0;	
	}
}
//------------------------------------------------
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */
 
  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */
     sys_tim(100);//可以改变为其他时间
  /* USER CODE END SysTick_IRQn 1 */
}
//控制LED可在其他地方引用flag

还可以用另一种由系统设置好的变量来操作,原理一样,每1ms执行一次HAL_InTick函数36bbd4d0d1ac4ea8b4fc31eef4f9351c.png 

 HAL_InTick的具体作用是自增18f0562616e194fa7935175f6f1631b4c.png

在程序初始化位置定义计时节点 ,500ms=uwTick是定时开始节点

uint32_t 500ms;
500ms=uwTick;

在主程序中书写需要定时的操作,类似地,还可以同时存在其他时长的定时程序,但是此方法精确度不太高。 而且只适合于从上电开始就一直执行的定时程序,如果需要触发的定时,建议设置标志位,然后通过标志位进入SysTick_Handler(void)进行计数,从而特定事件触发,然后执行完毕后清除触发标志位。

if(uwTick-500ms>500)
{
    //此处写需要具体执行的操作
    500ms=uwTick;
}

RTC实时时钟

以秒为单位进行计时,相当于钟表,配置时最下方year会自动补齐20xx,所以20不用写482d2e441bca47b7bd689e806ee3d4d8.png

 .h

#include "rtc.h"

如果只读取时间,也需要把日期读一下,只有日期读完后,数据才会从影子寄存器读取。 

.c,sprintf格式化部分多打几个空格清屏防止残留。

void RTC_proce(void)
{		
	RTC_TimeTypeDef time;
	RTC_DateTypeDef date;
	HAL_RTC_GetTime(&hrtc,&time,RTC_FORMAT_BIN);
	HAL_RTC_GetDate(&hrtc,&date,RTC_FORMAT_BIN);
	char text[30];
	sprintf(text,"time:%d:%d:%d     ",time.Hours,time.Minutes,time.Seconds);
	LCD_DisplayStringLine(Line0,(uint8_t *)text);
}

IIC-EEPROM 

eeprom有寿命,尽量不要频繁写入,以下时序图赛场会芯片手册会提供

c2500e436c184842870a1af7cf9a869e.jpegc88b674c6b544b92869e80c7fc7cded3.jpega5584ae2f5ea4be6b8951b54382a3894.jpeg

复制到bsp文件夹并加入工程,内部是一些时钟线的配置和应答函数等,所以不需要cubemx配置

ee9ae92cac9b4a81a03de4561d475387.png

根据读写时序图编写写入和读取函数 

my_main.h包含i2c_hal.h

#include "i2c_hal.h"
void eeprom_write(uint8_t addr,uint8_t data);
uint8_t eeprom_read(uint8_t addr);
void eeprom_write16(uint8_t addr,uint16_t data);
uint16_t eeprom_read16(uint8_t addr);
void eeprom_write_f(uint8_t addr,float data);
float eeprom_read_f(uint8_t addr);

setup中初始化

I2CInit();

.c

void eeprom_write(uint8_t addr,uint8_t data)
{
	I2CStart();//开启总线
	I2CSendByte(0xA0);//联系芯片 1010 0000
	I2CWaitAck();//等待应答
	I2CSendByte(addr);//写入地址
	I2CWaitAck();//等待应答
	I2CSendByte(data);//传入数据
	I2CWaitAck();//等待应答
	I2CStop();//停止总线
}

uint8_t eeprom_read(uint8_t addr)
{
	uint8_t rec;
	
	I2CStart();//开启总线
	I2CSendByte(0xA0);//联系芯片 1010 0000
	I2CWaitAck();//等待应答
	I2CSendByte(addr);//写入地址
	I2CWaitAck();//等待应答
	I2CStop();//停止总线
	//第一轮通讯先写入
	
	I2CStart();//开启总线
	I2CSendByte(0xA1);//联系芯片 1010 0001 读写控制位改为读(1)
	I2CWaitAck();//等待应答
	rec=I2CReceiveByte();//接到返回值
	
	I2CSendNotAck();//发送非应答信号
	
	return rec;
}

读写地址一致,比如写在第八个位置

eeprom_read(8);

写入值后按下复位键,读取的和写入的一样就是实现。每一个位置只能存储8位数据,存储16位需要拆分。

void eeprom_write16(uint8_t addr,uint16_t data)
{
	uint8_t data_h=data>>8;
	uint8_t data_l=data<<8;
	eeprom_write(addr,data_h);
	HAL_Delay(10);
	eeprom_write(addr+1,data_l);
}

uint16_t eeprom_read16(uint8_t addr)
{
	uint8_t data_h=eeprom_read(addr);
	uint8_t data_l=eeprom_read(addr+1);
	return (data_h<<8)+data_l;
}

如果要存储小数就把小数乘一个10的倍数,读出来时再除以一下

void eeprom_write_f(uint8_t addr,float data)
{
	uint16_t data_f=data*100;
	uint8_t data_h=data_f>>8;
	uint8_t data_l=data_f<<8;
	eeprom_write(addr,data_h);
	HAL_Delay(10);
	eeprom_write(addr+1,data_l);
}

float eeprom_read_f(uint8_t addr)
{
	uint8_t data_h=eeprom_read(addr);
	uint8_t data_l=eeprom_read(addr+1);
	return ((data_h<<8)+data_l)/100.0f;
}

IIC-数字电位器

写入数据0-127对应0-100k的阻值,数据写入时转化为16进制传参

7186eb50c891468f9f531d0fe910e515.jpeg.h

void mcp_write(uint8_t data);

.c代码

void mcp_write(uint8_t data)//0-127对应0-100k的阻值
{
	I2CStart();//开启总线
	I2CSendByte(0x5e);//联系芯片 0101 1110
	I2CWaitAck();//等待应答
	I2CSendByte(data);//刺蛾如数据
	I2CWaitAck();//等待应答
	I2CStop();//停止总线
}

传参写十进制。若想获取PB14的分压值,可以使用上面的DMA多通道采样。

  • 17
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nemophilist12

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值