原题展示 

    本试题目的是制作一个商品管理系统,其主要功能为:购买商品、增加商品储量、调节商品价格、查询商品价格,并且能够保存改变后的商品数量与商品价格,总体上看跟第一场的试题差不多,下面就让我们一起去看看题目吧!

    通过阅读上述原题,我们可以知道本试题涉及到的模块有串口、LCD、按键、LED、EEPROM、PWM六大部分,其中串口、按键、LCD、LED、PWM五个部分都是试题的常客,而EEPROM相比出场率就非常惨淡了,因此,本次题解需要关注的就是:如何完成EEPROM连续读取。

题解 

    在正式题解前,大家需要注意以下几点:

由于LCD与LED有部分引脚是共用的,因此初始化完成LCD后最好手动关闭LED;使用CubeMX配置完成串口USART1后需要更改默认引脚为PA9、PA10;EEPROM不能够连续读取,两次相邻的读取一定要有时间间隔;EEPROM存储的数据不能是小数;

LED模块

    通过查询产品手册知,LED的引脚为PC8~PC15,外加锁存器74HC573需要用到的引脚PD2。(由于题目要求除LED1、LED2外的其他LED都处于熄灭状态,此处特意将所有的LED都初始化)

CubeMX配置: 代码样例

    由于G431的所有LED都跟锁存器74HC573连接,因此每次更改LED状态时都需要先打开锁存器,写入数据后再关闭锁存器。

/*****************************************************

* 函数功能:改变所有LED的状态

* 函数参数:

* char LEDSTATE: 0-表示关闭 1-表示打开

* 函数返回值:无

******************************************************/

void changeAllLedByStateNumber(char LEDSTATE)

{

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,(LEDSTATE==1?GPIO_PIN_RESET:GPIO_PIN_SET));

//打开锁存器 准备写入数据

HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);

//关闭锁存器 锁存器的作用为 使得锁存器输出端的电平一直维持在一个固定的状态

HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);

}

/*****************************************************

* 函数功能:根据LED的位置打开或者是关闭LED

* 函数参数:

* uint16_t LEDLOCATION:需要操作LED的位置

* char LEDSTATE: 0-表示关闭 1-表示打开

* 函数返回值:无

******************************************************/

void changeLedStateByLocation(uint16_t LEDLOCATION,char LEDSTATE)

{

HAL_GPIO_WritePin(GPIOC,LEDLOCATION,(LEDSTATE==1?GPIO_PIN_RESET:GPIO_PIN_SET));

HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);

HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);

}

    为了提高系统的实时性并且完成试题要求的LED1亮5秒及LED2以0.1s时间闪烁的要求,此处的LED工作计算时间特意借助了定时器7来完成(定时器7每中断一次代表时间0.1s):

/*************************************

* 函数功能:按键工作函数

* 函数参数:无

* 函数返回值:无

*************************************/

void LedDisplay(void)

{

//关闭所有LED

changeAllLedByStateNumber(0);

//确认购买商品

if(checkFlag)

{

uiTime7CountFlag[0] = 1;

}

//判断LED时长是否符合条件

if(uiTime7CountFlag[0] && uiTime7Count[0]%50<50)

changeLedStateByLocation(LED1,1);

if(uiTime7Count[0]%55>51)

{

changeLedStateByLocation(LED1,0);

uiTime7CountFlag[0] = 0;

uiTime7Count[0] = 0;

}

//商品数量都为0

if(!goods[0]->iGoodsCount && !goods[1]->iGoodsCount)

uiTime7CountFlag[1] = 1;

else

{

uiTime7CountFlag[1] = 0;

uiTime7Count[1] = 0;

}

//判断时间是否间隔0.1秒

if(uiTime7Count[1]%2 == 1)

rollbackLedByLocation(LED2);

}

LCD模块

    LCD模块官方会提供源码,内含初始化,大家会用即可。如下面是一段将LCD初始化成——文字颜色为白色、背景为蓝色的LCD屏:

/******************************************************************************

* 函数功能:LCD初始化

* 函数参数:无

* 函数返回值:无

*******************************************************************************/

void lcdInit(void)

{

//HAL库的初始化

LCD_Init();

//设置LCD的背景色

LCD_Clear(Blue);

//设置LCD字体颜色

LCD_SetTextColor(White);

//设置LCD字体的背景色

LCD_SetBackColor(Blue);

}

    系统的LCD显示界面的设计函数,displayMode为全局变量,每次按下B1其值都会加1:

/*************************************

* 函数功能:界面显示

* 函数参数:无

* 函数返回值:无

*************************************/

void display(void)

{

char temp[20] = {0};

//购买界面显示

if(displayMode == 0)

{

LCD_DisplayStringLine(Line1,(uint8_t*)" SHOP ");

sprintf(temp," X:%d ",goods[0]->iGoodsBuyCount);

LCD_DisplayStringLine(Line3,(uint8_t*)temp);

sprintf(temp," Y:%d ",goods[1]->iGoodsBuyCount);

LCD_DisplayStringLine(Line4,(uint8_t*)temp);

}

//商品价格界面显示

else if(displayMode == 1)

{

LCD_DisplayStringLine(Line1,(uint8_t*)" PRICE ");

sprintf(temp," X:%.1f ",goods[0]->dGoodsPrice);

LCD_DisplayStringLine(Line3,(uint8_t*)temp);

sprintf(temp," Y:%.1f ",goods[1]->dGoodsPrice);

LCD_DisplayStringLine(Line4,(uint8_t*)temp);

}

//库存信息界面显示

else if(displayMode == 2)

{

LCD_DisplayStringLine(Line1,(uint8_t*)" REP ");

sprintf(temp," X:%d ",goods[0]->iGoodsCount);

LCD_DisplayStringLine(Line3,(uint8_t*)temp);

sprintf(temp," Y:%d ",goods[1]->iGoodsCount);

LCD_DisplayStringLine(Line4,(uint8_t*)temp);

}

}

按键模块

    通过查询产品手册知,开发板上的四个按键引脚为PB0~PB2、PA0。

CubeMX配置 代码样例

    由于G431开发板上按键数量较少以及本次按键不涉及长短按、单击双击等复杂按键的设计,因此,我们直接使用含锁机制的if判断即可。

第一步,判断按键是否按键以及锁是否处于打开状态,如果两者有一个不满足函数直接返回;否则,进入下一步;第二步,上锁,延时消抖;第三步,再次读取各个按键,判断具体是哪个按键按下;第四步,判断按键是否松开,如果松开,则开锁;否则函数直接返回。

/*********************************************

* 函数功能:按键扫描 含按键消抖 无长按短按设计

* 函数参数:无

* 函数返回值:按键的位置

* 返回值说明:B1-1 B2-2 B3-3 B4-4

*********************************************/

unsigned char scanKey(void)

{

//按键锁

static unsigned char keyLock = 1;

//记录按键消抖时间

// static uint16_t keyCount = 0;

//按键按下

if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == RESET || HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == RESET

|| HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == RESET || HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET)

&& keyLock == 1){

//给按键上锁 避免多次触发按键

keyLock = 0;

//按键消抖 这里最好不要使用延时函数进行消抖 会影响系统的实时性

// if(++keyCount % 10 < 5) return 0;

// if(HAL_GetTick()%15 < 10) return 0;

HAL_Delay(10);

//按键B1

if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == RESET){

return 1;

}

//按键B2

if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == RESET){

return 2;

}

//按键B3

if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == RESET){

return 3;

}

//按键B4

if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET){

return 4;

}

}

//按键松开

if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == SET && HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == SET

&& HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == SET && HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == SET)

&& keyLock == 0){

//开锁

keyLock = 1;

}

return 0;

}

    按键按下后的题目要求的逻辑函数:

/*************************************

* 函数功能:按键工作函数

* 函数参数:无

* 函数返回值:无

*************************************/

void keyPro(void)

{

//按键扫描

keyValue = scanKey();

switch(keyValue)

{

//按键B1

case 1:

//切换LCD显示界面

if(++displayMode == 3) displayMode = 0;

break;

//按键B2

case 2:

//购买界面 购买数量加1

if(displayMode == 0)

if(++goods[0]->iGoodsBuyCount == goods[0]->iGoodsCount+1)

goods[0]->iGoodsBuyCount = 0;

//商品价格界面 单价加0.1

if(displayMode == 1)

goods[0]->dGoodsPrice += 0.1;

//库存界面 库存加1

if(displayMode == 2)

goods[0]->iGoodsCount++;

break;

//按键B3

case 3:

//购买界面 购买数量加1

if(displayMode == 0)

if(++goods[1]->iGoodsBuyCount == goods[1]->iGoodsCount+1)

goods[1]->iGoodsBuyCount = 0;

//商品价格界面 单价加0.1

if(displayMode == 1)

goods[1]->dGoodsPrice += 0.1;

//库存界面 库存加1

if(displayMode == 2)

goods[1]->iGoodsCount++;

break;

//按键B4

case 4:

if(displayMode == 0)

checkFlag = 1;

break;

//其他

default : break;

}

keyValue = 0;

}

串口模块

    试题中要求串口具备接收与发送两个功能,接收采用中断接收的方式,发送则需要发送时直接发送。

CubeMX配置     配置时一定一定记得改引脚!!! 代码样例

    HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)函数解析:

UART_HandleTypeDef *huart:串口通道;uint8_t *pData:存放数据的buff;uint16_t Size:一次接收数据的长度     不过使用时还需要初始化,否则不能够进入中断接收数据;

/***使用HAL_UART_Receive_IT中断接收数据 每次接收完成数据后就会执行该函数***/

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

{

if(huart->Instance == USART1){

// 重新使能中断

HAL_UART_Receive_IT(huart,(uint8_t *)&cRxBuff,sizeof(cRxBuff));

}

}

    串口工作处理函数,这里主要包含串口数据接受收的处理以及串口数据发送的条件:

/*************************************

* 函数功能:串口数据处理函数

* 函数参数:无

* 函数返回值:无

*************************************/

void usartProcess(void)

{

char temp[20] = {0};

//确认购买后发送商品信息

if(checkFlag)

{

sprintf(temp,"X:%d,Y:%d,Z:%.1f\r\n",goods[0]->iGoodsBuyCount,goods[1]->iGoodsBuyCount,

(goods[0]->dGoodsPrice*goods[0]->iGoodsBuyCount+goods[1]->dGoodsPrice*goods[1]->iGoodsBuyCount));

HAL_UART_Transmit(&huart1,(uint8_t*)temp,sizeof(temp),50);

}

//查询商品 发送目前的商品信息

if(iRxFlag==1 && strcmp((char*)cRxBuff,(char*)"?") == 0)

{

sprintf(temp,"X:%.1f,Y:%.1f\r\n",goods[0]->dGoodsPrice,goods[1]->dGoodsPrice);

HAL_UART_Transmit(&huart1,(uint8_t*)temp,sizeof(temp),50);

}

//清空串口接收到的数据以及打开串口锁

memset(cRxBuff,'\0',sizeof(cRxBuff));

iRxFlag = 0;

}

PWM模块

CubeMX配置 修改PWM的占空比样例代码(可直接调用)

/*************************************

* 函数功能:PWM输出切换函数

* 函数参数:无

* 函数返回值:无

*************************************/

void pwmWorkState(void)

{

//输出占空比为30% 这里的判断条件是借用LED1的判断条件 因为他们的时长需求是一样的

if(uiTime7CountFlag[0] && uiTime7Count[0]%50<50)

__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,150);

//输出占空比为5%

if(uiTime7Count[0]%55>51)

__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,25);

}

EEPROM模块

EEPROM初始化

    有两种方法,一种是使用CubeMx配置引脚,另一种是使用官方给的可直接使用的IIC初始化代码。

方法一:CubeMx配置

    使用CubeMx配置后,就可直接使用EEPROM的读取函数,不用在执行其他操作。

方法一:官方提供代码初始化

    官方提供的代码中含有基于HAL库写的IIC初始化函数,即EEPROM的初始化函数,直接使用其初始化就可,其函数名称为I2CInit()。

EEPROM操作的时序

    由于官方提供的库函数是基于软件实现的,在不使用硬件llC外设的微处理器中,需要根据总线时序设计lIC接口的驱动程序,其包括:起始信号、停止信号、产生应答、等待应答、发送数据和接收数据6个函数。

EEPROM读取函数

时序图 读取时源码

/***************************************

* 函数功能:读取eeprom相应位置的值

* 函数参数:

* unsigned char ucAddr:读取的地址

* 函数返回值:

* ucRes:读取到的值

***************************************/

unsigned char readEepromByBit(unsigned char ucAddr)

{

unsigned char ucRes = 0;

//发送起始信号

I2CStart();

//发送设备地址

I2CSendByte(0xa0);

//等待应答

I2CWaitAck();

//发送读取地址

I2CSendByte(ucAddr);

//等待应答

I2CWaitAck();

//发送停止信号

I2CStop();

//发送起始信号

I2CStart();

//发送读取数据命令

I2CSendByte(0xa1);

//等待应答

I2CWaitAck();

//接收数据

ucRes = I2CReceiveByte();

//发送应答

I2CSendNotAck();

//发送停止信号

I2CStop();

return ucRes;

}

EEPROM写入函数

时序图 写入时源码

/***************************************

* 函数功能:向eeprom对应地址写入数据

* 函数参数:

* unsigned char ucAddr:写入的地址

* unsigned char ucData:写入的数据

* 函数返回值:无

***************************************/

void writeEepromByBit(unsigned char ucAddr,unsigned char ucData)

{

//发送起始信号

I2CStart();

//发送设备地址

I2CSendByte(0xa0);

//等待应答

I2CWaitAck();

//发送写入地址

I2CSendByte(ucAddr);

//发送应答

I2CSendAck();

//发送写入数据

I2CSendByte(ucData);

//等待应答

I2CWaitAck();

//发送停止信号

I2CStop();

}

    由于题中要求E2PROM内部地址0-3要存储两个商品的数量及价格信息,这就标志着该试题题解一定存在连续存取。     试题中还要求在设备第一次读取时使用代码中的初始化,后续启动使用EEPROM中存储值作为商品信息的初值,那么如何判断设备是否第一次启动就成了商品信息初始化的关键点。这里采用的方法是:使用EEPROM中的两个地址存储特定的值,每次启动时都读取这两个地址,一旦读取出的数值不是设定的特定值就判断是第一次启动设备,再将特定值写入;否则,就不是第一次启动,那么就使用EEPROM中的值作为初值。

//软件初始化商品信息以及EEPROM的值

for(i=0;i<2;i++)

{

//初始化商品信息

goods[i] = goodsInit(10,1.0,0);

//EEPROM结构体初始化

eepromMsg[i] = eepromInit(0,0);

}

//读取EEPROM特定位置查看该设备是否启动过

ucEdata[0] = readEepromByBit(0xa4);

HAL_Delay(10);

ucEdata[1] = readEepromByBit(0xa5);

//判断固定位置是否存储过信息 其表示的是该设备是否第一次烧入代码

if(ucEdata[0]==55 && ucEdata[1]==66)

{

//加载EEPROM中商品的数量、价格信息

for(i=0;i<2;i++)

{

eepromMsg[i]->count = readEepromByBit(0xa0+ucEepromLocation[i]);

goods[i]->iGoodsCount = eepromMsg[i]->count;

HAL_Delay(10);//由于此处是初始化,使用HAL库提供的延时函数效果最佳,且不会影响系统的任何工作

eepromMsg[i]->price = readEepromByBit(0xa1+ucEepromLocation[i])*1.0/10;

goods[i]->dGoodsPrice = eepromMsg[i]->price;

HAL_Delay(10);

}

}

else

{

//向EEPROM写入数据表明该设备已经启动过

for(i=0;i<2;i++)

{

writeEepromByBit(0xa4+i,55+11*i);

HAL_Delay(10);

}

}

    由于EEPROM连续存取需要一定的时间间隔,那么意味着必须得使用延时/计时函数。HAL库提供了延时函数HAL_Delay(),但是我们一旦使用它,LED跟EEPROM写入条件同时满足时,就会在EEPROM写入后产生阻塞,就会导致LED工作时间不准确,因此,最好使用定时器来完成计时功能。     数组中uiTime7Count[2]的值记录定时器7中断次数,定时器7中断一次就意味着经过了0.1s时间。将uiTime7Count[2]的计数值分成5部分,前面0-3表示可以写入,第4部分时EEPROM就休息,这样可以避免第0、3部分时间都写入导致产生错误数据。

//判断库存数量是否跟EEPROM存储是否相同 不同就写入

for(i=0;i<2;i++)

{

//判断数值是否相同并且时间间隔是否合理

if(goods[i]->iGoodsCount!=eepromMsg[i]->count && uiTime7Count[2]%5==(0+ucEepromLocation[i]))

{

writeEepromByBit(0xa0+ucEepromLocation[i],(unsigned char)goods[i]->iGoodsCount);

eepromMsg[i]->count = goods[i]->iGoodsCount;

}

if(goods[i]->dGoodsPrice!=eepromMsg[i]->price && uiTime7Count[2]%5==(1+ucEepromLocation[i]))

{

writeEepromByBit(0xa1+ucEepromLocation[i],(unsigned char)(goods[i]->dGoodsPrice*10));

eepromMsg[i]->price = goods[i]->dGoodsPrice;

}

}

小结 

    试题中比较难受的部分是如何判定设备是否是第一次启动以及EEPROM连续读取需要一定的时间间隔,再解决完这些问题,总的来说,该试题还是比较简单的,就剩一些常见的解题模式框架,直接!!!

好文推荐

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: