橙猫猫的嵌入式复习攻略-Day1

一:准备cubemx:

可以在官方提供的文件中找到cubemx文件以及库函数,比赛电脑大概率无法联网下载库文件,可以自己从官方文件里解压到HELP里自己设定的地址

二:配置cubemx:

选择STMG431RBT6,打开固定几件事

        选择RCC选项,设置高速外部时钟使能(Crystal/Ceramic Resonator)

        选择SYS选项,设置debug选择Serial Wire

        切换到时钟页面,设置24MHZ频率,设置system clock hum为PLL,将输出设置为80HMZ

        设置好工程名称和目录,Basic,MDK-ARM,V5勾选Generate peripheral initialization as a pair of '.c/h' file per peripheral选项,然后根据自己的库文件地址来选择是否勾选Use Default Fimware Location,生成代码

(可能存在的问题,Basic,MDK-ARM,V5非必要,不同人可能操作不一样。以及生成代码后,有的需要再从官方那里拷贝来startup文件复制到新工程根目录并添加才可以正常编译)

三:点亮LED: 

首先看一下CT117E-M4的产品手册关于LED指示灯的部分,最左侧正端是VDD高电平, 也就是说这款LED是低电平点亮,再注意看这块硬件的核心-锁存器(由PD2引脚控制),这个锁存器的功能简单来理解就是高电平允许通过低电平锁住,所以每次操作完LED都需要让锁存器SET再RESET一下(或者先高电平开锁存器,再操作led再锁住也是一样的,程序反应太快了没区别),这块在代码里会体现的很明显

简化步骤:

                      step1:PC8~PC15输出高/低电平

                    step2:PD2输出高电平

                    step3:PD2输出低电平

cubemx:

PC8~PC15+PD2设置为GPIO output,可以全部设置高电平,其他默认即可

代码部分:

此处的模块函数:

void LED_Disp(uint8_t ucled)
{
	HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_8
                          |GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12, GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
	
	HAL_GPIO_WritePin(GPIOC, ucled<<8, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}

 其中,ucled<<8的原理可以参考这一段底层代码:

#define GPIO_PIN_8                 ((uint16_t)0x0100)  /* Pin 8 selected    */
#define GPIO_PIN_9                 ((uint16_t)0x0200)  /* Pin 9 selected    */
#define GPIO_PIN_10                ((uint16_t)0x0400)  /* Pin 10 selected   */
#define GPIO_PIN_11                ((uint16_t)0x0800)  /* Pin 11 selected   */
#define GPIO_PIN_12                ((uint16_t)0x1000)  /* Pin 12 selected   */
#define GPIO_PIN_13                ((uint16_t)0x2000)  /* Pin 13 selected   */
#define GPIO_PIN_14                ((uint16_t)0x4000)  /* Pin 14 selected   */
#define GPIO_PIN_15                ((uint16_t)0x8000)  /* Pin 15 selected   */
#define GPIO_PIN_All               ((uint16_t)0xFFFF)  /* All pins selected */

 所以在主函数里完全可以直接写ucled  = 0xff  ; LED_Disp(ucled); 简单高效

四:按键key

 首先看一下原理图,主要注意一下引脚的名称对应顺序,以及在原理图中,按下按键后会连通接地,所以没按下是高电平,按下时是低电平,在代码里就是按下后时RESET

简化步骤:

        step:按下PB0~PB2/PA0按钮,输入低电平

cubemx:

        PB0~PB2+PA0设置为GPIO input,其他默认即可

代码部分:

首先,我们先写一个经典代码来确定按键状态,由四个变量组成

uint8_t ucKey_Val;uint8_t ucKey_Down;uint8_t ucKey_Up;uint8_t ucKey_Old;

ucKey_Val = Key_Scan();
ucKey_Down = ucKey_Val & (ucKey_Old ^ ucKey_Val); 
ucKey_Up = ~ucKey_Val & (ucKey_Old ^ ucKey_Val);	
ucKey_Old = ucKey_Val;

这个经典代码可以识别按键状态,非常好用,然后我们写一个读取按键的模块,我整理的模块分两种,一种是普通的可以满足大部分需求的简单代码:

uint8_t Key_Scan(void)
{
	uint8_t unKey_Val = 0;
	
	if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)    
		unKey_Val = 1;

	if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
		unKey_Val = 2;

	if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET)
		unKey_Val = 3;
	
	if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
		unKey_Val = 4;	
	
	return unKey_Val;
}

 这个Key_Scan()函数可以判断按下的是哪个键,然后我们在主函数里要求对应的功能即可

另一种模块是进阶版,可以识别多个按键同时按下的情况,这个模块需要用到位操作,于是我们需要在模块代码的这一页先定义位运算:

#define KEY_PIN_0 (1 << 0) // GPIOB, GPIO_PIN_0  代表0001
#define KEY_PIN_1 (1 << 1) // GPIOB, GPIO_PIN_1     0010
#define KEY_PIN_2 (1 << 2) // GPIOB, GPIO_PIN_2     0100
#define KEY_PIN_3 (1 << 3) // GPIOA, GPIO_PIN_0     1000

这样的话,就可以实现按下第一个按钮,二进制的第一位被赋值,按下第二个按钮,二进制的第二位被赋值,以此类推,就可以实现多个不同含义的信息被一串二进制数字同时表示 

 然后是进阶版模块代码:

uint8_t Key_Scan(void)
{
	uint8_t ucKey_Val = 0;
	if(HAL_GPIO_ReadPin (GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET)
			ucKey_Val |= KEY_PIN_0;  
	if(HAL_GPIO_ReadPin (GPIOB,GPIO_PIN_1) == GPIO_PIN_RESET)
			ucKey_Val |= KEY_PIN_1;  
	if(HAL_GPIO_ReadPin (GPIOB,GPIO_PIN_2) == GPIO_PIN_RESET)
			ucKey_Val |= KEY_PIN_2;  
	if(HAL_GPIO_ReadPin (GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET)
			ucKey_Val |= KEY_PIN_3;  
	
	return ucKey_Val ;
}

|= 是一个复合赋值运算符,它表示按位或赋值。这样的话,return回去的ucKey_Val值就可以是一个携带多个预设信息的二进制数字,识别原理要比原先复杂一点但传递的信息也更多,比如可以设置0001就是第一个灯亮,0011我就可以设置为我想要所有的灯都亮,我们就可以在主函数里通过对应的判断来识别,如

if (ucKey_Val == 0x01 ) ucled = 0x01;
if ((ucKey_Val & 0x03) == 0x03) ucled = 0xff;

 第二行这串if的意思(你可能需要掌握十六进制和二进制的快速换算关系才能看懂)就是如果ucKey_Val的值与0x03相同时(也就是同时按下两个按键),把所有灯都点亮,这样就可以完美实现多个按键同时按下的情况,唯一的缺点就是看上去比较复杂

(为什么这么改呢,因为经典代码不好动,ucKey_Val同时只能代表一个值,也就是说不可能去设置if(ucKey_Val==1&&ucKey_Val==2)这种操作,就算是用ucKey_Old也不行,所以只能在Key_Scan()这里想办法,于是这样设计后,ucKey_Val就直接在Key_Scan()里面就被加工完毕了,所以主函数里的ucKey_Val值就是稳定的一串数字,不会影响到经典代码的运行)

tip:这里稍微啰嗦一下,点一下经典代码的四个变量的动态关系,后续在设计长短按键的时候会方便不少 

从宏观上看,ucKey_Val 和 ucKey_Old 的值 几乎是保持一致的,ucKey_Down ucKey_Up 基本保持为0;

从微观上看,ucKey_Down ucKey_Up反映的是按键的上升和下降沿,ucKey_Val被赋值的一瞬间,ucKey_Down 被赋值,随后立刻归零,ucKey_Old与 ucKey_Val保持一致;松开按键的一瞬间,ucKey_Up被赋值,随后立刻归零,并且此时所有值均归零

        

五:LCD

原理图部分没什么好说的,只需要注意一下PC引脚会和LED冲突,其他部分直接搬官方的LCD代码即可,至于这个冲突的问题,LED_Disp()代码开头每次都关闭一次就可以解决,所以后续可以忽略这个问题,然后直接在lcd.h里面调用对应的函数即可。

简化步骤: 

        step1:去官方的LED例子把lcd.h  fonts.h   lcd.c 直接复制到我们的代码文件对应位置中

        step2:根据自己的需求在lcd.h里使用对应函数

cubemx:

        无

代码部分:

//*LCD显示专用变量
uint8_t Lcd_Disp_String[21];//最多显示20个字符

void LCD_Proc(void)
{		
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line0, Lcd_Disp_String);	
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line1, Lcd_Disp_String);	
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line2, Lcd_Disp_String);
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line3, Lcd_Disp_String);
		sprintf((char *)Lcd_Disp_String, "       橙猫猫       "); LCD_DisplayStringLine(Line4, Lcd_Disp_String);
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line5, Lcd_Disp_String);
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line6, Lcd_Disp_String);
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line7, Lcd_Disp_String);
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line8, Lcd_Disp_String);
		sprintf((char *)Lcd_Disp_String, "                    "); LCD_DisplayStringLine(Line9, Lcd_Disp_String);	
}

我自己喜欢这么用,更直观一点,省赛无需考虑特殊符号的显示

六:串口通讯

产品手册上的原理图很简单,TX发送数据,RX接收数据

简化步骤:

       没想好,直接简没了吧,重点都在代码

cubemx:

        选一个usart,PA9设置为TX,PA10设置为RX,模式设置为异步,设置好需要的波特率,开中断选项

代码部分:

串口通讯这里只有两个函数需要熟练掌握:

HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);


HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

第一个就是传输数据的函数,一般要和sprintf一起用,按照函数要求填空就是了

第二个要复杂一点点,是中断接收回调函数,这个函数我们直接另外声明定义在里面写需求即可,需要注意的点是要先在主函数里使能一下中断:

HAL_UART_Receive_IT(&huart1,&rx_buffer,1);

毕竟中中断接收回调就是要先接收再回调,以及回调的时候结尾再使能一个接收,完成持续进行的闭环,另外回调函数不需要加减速。

tip:一杯茶,一台电脑,一个bug改一天,个人惨痛教训,串口这里的减速函数爆了一个基础错误,减速变量一定得是uint32_t开头,8位直接爆存储bug

uint32_t uwTick_Usart_Set_Point;
char str[100];
uint8_t rx_buffer;

void Usart_Proc(void)
{
	if((uwTick - uwTick_Usart_Set_Point)<5000)  return;
			uwTick_Usart_Set_Point = uwTick;
	
	sprintf(str,"hello world\n");
	HAL_UART_Transmit(&huart1, (unsigned char *)str, strlen(str), 500);

}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	sprintf(str,"oh!!!!!!!!!!\n");
	HAL_UART_Transmit(&huart1, (unsigned char *)str, strlen(str), 50);
	HAL_UART_Receive_IT(&huart1,&rx_buffer,1);
}

很好,你已经学会了串口部分,去拿着新手装备打隔壁的省赛真题BOSS吧,有这么题型,会让你判断接收到的信息是否正确,不对的话要回调一个error。那么,我们的新手村装备明明是说每次只能接收一个字符,这个规矩改不了,那就只能在逻辑上想办法

1:每接收一个字符,按照顺序检查是否有问题

2:接收完字符串,再检查字符串是否有问题

那么问题来了,怎么判断字符串是否接收完毕?用if来检测吗?似乎可以,这里推荐用计数器算波特率来数数,反正一般情况下也不会让我们从一大堆含噪音的字符里面去挑是否有符合的一串,那样的话可能就必须要if严格判断开始和结束了,这里用计数器tim更简单一点,直接算间隔就行,上代码


void Usart_Proc(void)
{
		if(rx_flag == 1 && TIM4->CNT >15)
		{
				if(rx_val[0] == 'l'&&rx_val[1] == 'q'&&rx_val[2] == 'b')
				{
					sprintf(str,"oh!!!!!!!!!!\n");
					HAL_UART_Transmit(&huart1, (uint8_t *)str, strlen(str), 50);
				}
				else 
				{
					sprintf(str,"error!\n");
					HAL_UART_Transmit(&huart1, (uint8_t *)str, strlen(str), 50);
				}
				rx_flag = 0;
				for(int j = 0;j<=count_flag;j++)
				{
					rx_val[j] = 0;
				}
				count_flag=0;	
		}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	
	TIM4->CNT = 0;
	rx_flag=1;
	rx_val[count_flag] = rx_buffer;
	count_flag++;

	sprintf(str,"%c\n",rx_buffer);
	HAL_UART_Transmit(&huart1, (unsigned char *)str, strlen(str), 50);
	
	HAL_UART_Receive_IT(&huart1,&rx_buffer,1);
}

代码的意思很简单,数串口接收数据是否为lqb,逻辑上就是使用tim4去计数,超过15下还没有新数据就说明传输完毕,这里是一个人造空闲中断的思路。不过需要注意的是,这个代码必须要开FIFO模式才行,不然的话回调函数里那两行检查输入数据的代码会导致接收数据丢失,具体原因不明大概率是底层响应不及时的原因。另外就是使用TIM->CNT来计算什么时候数据传输完毕的话会有一些底层细节问题,那就是当你的CNT设置的不严谨小于11时也会大概率success,这是由于连续的中断回调不一定会中间能成功执行while,所以不一定error的出来导致。就这简短一句话的原因我破防了两天一夜才搞明白

注意:回调函数不要啰嗦,不然会数据丢失(除非开FIFO模式)

最后在这里放一个大神写的确定数据是否全部接收的代码:

while()
{
    if(RX_counter > 0)
    {
        rx_tmp = RX_counter;
        HAL_Delay(1);
        if(rx_tmp == RX_counter)
            Usart_Proc();
    }
}

简单说一下原理,rx_tmp只是一个临时存放变量的地方,RX_counter是一个在中断回调函数里面计数的int,重点在于中间的HAL_Delay(1); 这里是利用串口中断一定会打断默认的系统中断来确定串口数据是否传输完成,代码很简洁而且非常牛逼,第一次见真的是佩服的五体投地

七:EEPROM

关键点:

1:主机和从机对彼此发消息且对方收到后,要给予回应

2:主机通过地址来找到从机

我们可以查看产品手册和AT24C02的手册11页找到,地址应为 1010 000(read1/write0)

即:写 0xa0     读:0xa1

tip:写入可以加个小延时,防止连续写入时反应不及出错,可能设计到IIC通讯协议的问题

简化步骤:

           step1: start使能

                step2:send1 发送从机地址0xa0

                step3:wait

                step4:send2 发送片内地址

                step5:wait

                step6:send3 发送数据

                step7:wait

                step8:stop

        读    step1: start使能

                step2:send1 发送从机地址0xa0

                step3:wait

                step4:send2 发送片内地址

                step5:wait

                step6:stop

                 step7: start使能

                 step8:send1 发送从机地址0xa1

                 step9:wait

                 step10:receive

                 step11:主机不回应

                 step12:stop

cubemx:

        无,直接复制粘贴

代码部分: 

void EEPROM_Write(uint8_t adr,uint8_t data)
{
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(adr);
	I2CWaitAck();
	I2CSendByte(data);
	I2CWaitAck();
	I2CStop();
	
//	HAL_Delay(20);
}



uint8_t EEPROM_Read(uint8_t adr)
{
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(adr);
	I2CWaitAck();
	I2CStop();

	I2CStart();
	I2CSendByte(0xa1);
	I2CWaitAck();
	uint8_t data = I2CReceiveByte();
	I2CSendNotAck();
	I2CStop();
	
	return data;
}

这块可以根据自己的需求来更改,比如这里的read需要有return,然而当你引入数组地址作为指针来读取数据的话,就不需要return了,然后以数组的形式write和read的话,代码可以更丰富一点,在竞赛题里会遇到

补充一个小tip,I2CInit();初始化一定要写并且要放到SystemClock_Config();之后,原因类似我的上一篇文章。然后的话,关于EEPROM的代码调用一般都是放到主函数里各种初始化声明使能之后,while之前这个位置,可以写一个简单的if来判断是否第一次上电等等来满足题目需求

再补充一个tip:write函数里面结尾最好加上一个HAL_Dealy(10),因为EEPROM在连续写入的情况下会发送bug,原本给两个地址写入的数据会莫名其妙变成255,可以规避一下

八:RTC

tip:即使你不需要获取日期也最好加上,不然时间不会流动

简化步骤:

        step1:定义好三个结构体

        step2:调用函数

        step3:设置好中断回调函数

cubemx:

        选择RTC部分,激活时钟,选择是否需要闹钟,24小时计时,设置好自己想要的时间和闹钟选项,最后重点是设置为二进制格式,这很关键,关系到后面函数的format类型

代码部分:

//RTC专用变量
 RTC_TimeTypeDef sTime = {0};
 RTC_DateTypeDef sDate = {0};
 RTC_AlarmTypeDef sAlarm = {0};

void LCD_Proc(void)
{		
	HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
	HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
	sprintf((char *)Lcd_Disp_String,"     %02d-%02d-%02d    " ,sTime.Hours,sTime.Minutes,sTime.Seconds);
	LCD_DisplayStringLine(Line1,Lcd_Disp_String );
}		

void  HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
	ucled = 0xff;    //点亮LED的一个参数
}

八:ADC

主要就是PB12和PB15

简化步骤:

        1:adc使能

        2:获取值

cubemx:

        找到PB12和PB15设置好对应的 IN,然后都选择SINGLE ENDED就行了,选择异步模式,

代码部分:        

uint32_t getadc1(void)
{
	HAL_ADC_Start(&hadc1);
	uint32_t val = HAL_ADC_GetValue(&hadc1);
	return val;
}

九:PWM

简化步骤:

        1:计算

        2:使能

        3:设置占空比ccr

cubemx:

        1:设置好tim和CH

        2:PSC设置

        3:aar设置

代码部分:

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);

TIM3 -> CCR4 = 50 ;    //二选一
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,50);    //二选一

  • 25
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值