Warning:寫作不易,請勿轉載,感謝。
PS:我講解的只是編程題,並且只是我個人的一點點看法,不喜勿噴,感謝。
這次比賽的題目:傳送門
這次嵌入式比賽的我寫的程序:傳送門
長天嵌入式組織的模擬賽-賽題分析
1:賽題分析
廢話不多說,直接分析就完事了。
1.1:基礎部分要求
基本功能
- 通過競賽板上電位器R37輸出模擬電壓信號,經微控制器內部AD採集處理後,通過液晶屏實時顯示。
- 通過串口接收上位機指令,執行指令,並返回數據。
- 支持按鍵掃描功能,可識別當前各個按鍵狀態。
- LED亮滅受控。
這就是賽題的基礎要求部分。拿到賽題之後首先就要快速的去初始化號各個模塊,具體代碼模塊,查看第二節。 在這裏我分享一些我自己的習慣。
- 打開工程就把stm32所有的官方庫都加進來,方便後續的使用,省的說在一個一個的找然後再加進來,記住先別編譯。
- 然後再打開stm32的微庫模式(當然如果說是不用串口的話可以不用選擇)。具體什麼是微庫,爲什麼要打開。我在前面的文章已經講過了,不懂得可以點擊傳送門。
- 記住當32所有的庫都添加了並選擇了微庫,再進行編譯因爲這樣可以節省大部分的時間。
初始化這部分是很簡答的,多餘的部分我就不講了。我想講的是比較精彩的部分,也就是出題的扣分點。那就是串口和按鍵的時間判斷!
1.2:按鍵功能要求
- 按鍵B1短按鍵操作:所有LED指示燈熄滅。
- 按鍵B1長按鍵操作(按下時長超過800ms):所有LED指示燈狀態翻轉。
這個功能的要求就是根據按鍵按下時間的長短,然後表現出不同的狀態。具體檢測按鍵的時間,可以看我之前的一篇文章有很詳細的講解,想要進一步瞭解的點擊傳送門。在這裏我不過多去解讀了,這個還是比較簡單的
1.3:串口通信功能要求
-
串口基本配置
使用競賽板USART2完成全部串口通信功能.
通信波特率配置爲9600bps。 -
LED亮滅控制指令
指令格式:“LDn:0”、“LDn:1”或“LDn:2”
指令解析:編號爲n的LED指示燈點亮或熄滅,n的範圍是1-8。0表示熄滅,1表示點亮,2控制指示燈狀態翻轉。
指令舉例:
“LD1:0”,控制指示燈LD1熄滅。
“LD1:1”,控制指示燈LD1點亮。
“LD2:2”,控制指示燈LD2亮滅狀態翻轉。
指令回覆:
本條指令不需要回復任何內容。 -
按鍵狀態查詢
指令格式:“Bn?”
指令解析:查詢編號爲n的按鍵狀態。n的範圍是1-4;
指令舉例:“B1?”
指令回覆:
“B1:P”或“B1:R”,其中P表示B1按鍵處於按下的狀態,R表示B1按鍵處於釋放的狀態。 -
模擬電壓查詢指令
指令格式:“ADC?”
指令解析:查詢當前微控制器採集到的實時電壓值。
指令舉例:“ADC?”
指令回覆:
“ADC:3.02V”,表示當前採集到的電壓值爲3.02V,電壓值保留小數點後兩位有效數字。 -
未知指令
當設備收到收到未知的錯誤指令時,返回“error”。 -
通信指令要求
請嚴格按照上述1-5條中要求設計串口交互過程,注意指令格式、大小寫等設計細節。
1.3.1:串口接受指令的結束判斷
這次的串口模塊的功能要求可以說是比較麻煩的了,因爲他這次的發送指令缺少了一個結束符,所以我們就沒有辦法去判斷這個指令到哪裏纔算是信息的結束。
可能有些人看起來有點不懂我的意思,在這我稍微解釋一下。比如說PC機發送LDn:0LDn:1,這到底是不是錯誤的指令的呢??答案是這是個錯誤的指令,我們需要做的是串口發送"error"並且應該把接受數組重新初始化,然後等待下次的接受。但是問題就出來了,現在指令裏缺少了’\n’,單片機就不知道什麼時候結束,並且去發送。
可能有人說了,全部接受後然後用字符串比對,只要不符合要求的全部強制讓串口結束,並且發送"error",那麼恭喜你就進入他的一個陷阱(當然這是我自己的想的,不知道他們有沒有在這挖坑,哈哈!也許我是與空氣鬥智鬥勇)。假如說它用PC機首先發送一個"B1",然後過了0.5S發送一個"?",那麼問題就來了,這到底算不算對呢,因爲它們加起來是"B1?"。在我看來這是兩個指令,都不符合規定因此都是錯誤的指令。那麼單片機應該在PC機發送"B1"的時候,就應該發送出去"error"了,並且在PC機發送"?“的時候,這個時候也要發送"error”,我認爲這樣纔是對的。
那麼怎麼樣纔算一個完整的指令呢?並且怎麼去操作呢?其實很簡單,這道編程題在最後面有這樣一句話 “各類串口配置、查詢指令響應時間要求:≤300ms。”,這句話給了我一個思路,他要求的串口響應要求是≤300s,那我們用定時器模塊的配合。
具體操作:我們利用定時器模塊進行計時,讓它在串口接收到第一個數據的時候就立刻開始計時,然後10ms後強制關閉串口的接受中斷,然後進行字符串比對,如果說正確就繼續操作,如果說不對,就發送"error"即可,但是無論對還是不對都別忘了再次打開串口接收中斷哦。但是爲什麼定時10ms呢?因爲波特率要求是9600,根據計算9600/8/1000=1.2 symbol/ms,相當於10ms就能接收到12個字符。正確的接收指令最多也不會超過10個字符,所以說這裏的10ms足夠了。
因此我們只需要做的是接收到一個字符後,開啓定時器,過了10ms後關閉串口,並且進行字符串比對和接受數組的初始化,然後再打開串口就可以了。PS:記住每次計時的時間在關閉串口後要進行清0,並且停止計時!
1.3.2:按鍵狀態查詢和模擬電壓查詢指令
這兩個指令收到了做出的相應的反饋可以說是比較簡單的,具體函數放到主函數中的while(1)就可以。
這裏唯一要注意得就是發送出去指令的格式一定要嚴格的符合他的要求,他可是沒有要求讓你發送指令後面加"\n\r"的,千萬別自作聰明加上,如果加上了,恭喜你分又沒了。。。
if(RxBuffer1[0]=='A'&&RxBuffer1[1]=='D'&&RxBuffer1[2]=='C'&&RxBuffer1[3]=='?'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
printf("ADC:%1.2fV",Adc_GetVal());
}
if(RxBuffer1[0]=='B'&&RxBuffer1[1]=='1'&&RxBuffer1[2]=='?'&&RxBuffer1[3]=='`'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
if(Key1==0) printf("B1:P");
else printf("B1:R");
}
if(RxBuffer1[0]=='B'&&RxBuffer1[1]=='2'&&RxBuffer1[2]=='?'&&RxBuffer1[3]=='`'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
if(Key2==0) printf("B2:P");
else printf("B2:R");
}
if(RxBuffer1[0]=='B'&&RxBuffer1[1]=='3'&&RxBuffer1[2]=='?'&&RxBuffer1[3]=='`'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
if(Key3==0) printf("B3:P");
else printf("B3:R");
}
if(RxBuffer1[0]=='B'&&RxBuffer1[1]=='4'&&RxBuffer1[2]=='?'&&RxBuffer1[3]=='`'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
if(Key4==0) printf("B4:P");
else printf("B4:R");
}
沒錯就那麼粗暴。。。當然可以自己寫一個字符串掃描函數如下:
_Bool Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2)
{
while(*pBuffer1!=0)
{
if(*pBuffer1 != *pBuffer2)
{
return 0;
}
pBuffer1++;
pBuffer2++;
}
return 1;
}
這個完全是可以的,自己先初始化好相應的數組,與接收到的數組進行比對都可以的,看個人習慣罷了。
1.3.3:LED亮滅控制指令
這個串口指令可謂是整道題的靈魂,因爲我們要保證某一位改變的同時,不能去影響其他位。其實如果寫過寄存器就應該認爲這是很簡單的,因爲這裏只需要與,或和異或操作就能實現題目的要求了。
比如說如下要求:
“LD1:0”控制指示燈LD1熄滅。
Led &= 0xfe; //最低位變成0,其他位不變
“LD1:1”控制指示燈LD1點亮。
Led |= 0x01; //最低位變成1,其他位不變
“LD1:2”,控制指示燈LD1亮滅狀態翻轉。
異或運算與0異或相當於保持不變,與1異或相當於取反。
Led ^= 0x01; //最低位的值取反,其他位不變
這樣就可以對相應的位進行操作了,然後在進行下個操作就可以了。
Led_Control(Led_All,0);
Led_Control((u16)Led <<8,1);
第一行:先把所有的燈關掉。
第二行:Led左移8位,相當於就是引腳了,然後使引腳對應的LED亮起來。
比如說Led=0x01,左移8位就變成了0x0100這裏對應的引腳就是PIN8,也就會LED1這個引腳。
至於爲什麼我要操作關閉所有的led這條指令呢?我的意思在這裏,因爲我第二行的代碼只能使LED亮起來,並不可以關閉LED。我再打個比方,假如現在的Led=0x03,理論上是LD1和LD2在亮着呢,其他的都滅,然後我PC機發送一個指令“LD1:2”,這裏就會使得Led的值變爲0x02了。但是如果說我沒有第一行的全滅,我直接操作第二行的代碼,雖然說傳的led的值爲0x02,但是並不會使LED1滅,因爲你只對引腳PIN9進行操作,並沒有改變其他的引腳,因此我們需要先把之前的燈全部關掉,然後再去操作,就可以完美解決了。
可能有的人說了,我這是脫褲子放屁。。把與、或、和異或操作這些指令改爲直接對單個led燈進行操作就可以,沒必要改這個變量,可以是可以。。但是你就沒辦法把led的狀態通過LCD顯示出來了。。當然你也可以通過讀取LED相應的引腳的值,也可以知道LED的狀態的。。方法多的是,只要能成功就可以的。
1.3.4:串口功能要求總結
其實這個難度並不大,只要靜下心,一步一步的去思考就可以了。。至於更多關於串口模塊的功能的接受請看我之前的文章-藍橋杯嵌入式基礎板模塊之串口模塊的發送與接收。
1.4:液晶顯示功能
這個模塊需要注意的是一定要把各個字符顯示到固定的行列上,因爲它是機審,這點是很重要的一點。
1.4.1:LCD之ADC數值顯示
void Show_AdcVal(void){
u8 str[20];
float Adc_Number = 0;
Adc_Number = Adc_GetVal();
sprintf((char *)str," ADC:%1.2fV",Adc_Number);
LCD_DisplayStringLine(Line2,str);
}
這裏沒有什麼好說的,你只要把這個float型的數值,按相應的數值格式進行顯示就可以了。
1.4.2:LCD之按鍵狀態顯示
void Show_KeyVal(void){
u8 str[20];
if(Key1_State) sprintf((char *)str," B1:P");
else sprintf((char *)str," B1:R");
LCD_DisplayStringLine(Line3,str);
if(Key2_State) sprintf((char *)str," B2:P");
else sprintf((char *)str," B2:R");
LCD_DisplayStringLine(Line4,str);
if(Key3_State) sprintf((char *)str," B3:P");
else sprintf((char *)str," B3:R");
LCD_DisplayStringLine(Line5,str);
if(Key4_State) sprintf((char *)str," B4:P");
else sprintf((char *)str," B4:R");
LCD_DisplayStringLine(Line6,str);
}
這個也沒什麼可說的。。。
1.4.3:重點:LCD之LED狀態顯示!!
void Show_LedVal(void){
u8 str[20];
sprintf((char *)str," LED:%02X",Led);
LCD_DisplayStringLine(Line7,str);
}
這是我唯一想說的一個點,還記得我之前那個Led這個變量嗎?我這裏就是利用這個變量,直接利用spinrtf()函數的功能,直接可以轉換爲大寫的16進制,可以說是非常的便捷和簡單!!!!
2:各個模塊初始化和部分重點函數
2.1:Led模塊或LED不受控制
沒什麼想說的,自己看。如果說碰到LED不受控制的情況,請參考我之前的博客-藍橋杯嵌入式基礎板模塊之LED模塊不受控制的解決方法。
void Led_Init(void){
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD, ENABLE);
GPIO_InitStructure.GPIO_Pin = Led_All;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOD, &GPIO_InitStructure);
Led_Control(Led_All,0);
}
void Led_Control(u16 LED,u8 state){
if(state==0){
GPIO_SetBits(GPIOC,LED);
GPIO_SetBits(GPIOD,GPIO_Pin_2);
GPIO_ResetBits(GPIOD,GPIO_Pin_2);
}
else {
GPIO_ResetBits(GPIOC,LED);
GPIO_SetBits(GPIOD,GPIO_Pin_2);
GPIO_ResetBits(GPIOD,GPIO_Pin_2);
}
if(LED==0){
GPIO_SetBits(GPIOC,Led_All);
GPIO_SetBits(GPIOD,GPIO_Pin_2);
GPIO_ResetBits(GPIOD,GPIO_Pin_2);
}
}
2.2:ADC模塊
也沒什麼想說的,自己看。如果說想學更多關於ADC模塊的知識,比如說32自帶的溫度芯片和多路DMA採集-請參考我之前的博客-藍橋杯嵌入式基礎板模塊之ADC模塊-溫度傳感器的單通道採集與多通道採集。
void Adc_Init(void){
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1|RCC_APB2Periph_GPIOB, ENABLE);// PB0 C8
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOB, &GPIO_InitStructure);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_239Cycles5);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
}
float Adc_GetVal(void){
float temp=0;
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC));
ADC_ClearFlag(ADC1,ADC_FLAG_EOC);
temp = ADC_GetConversionValue(ADC1)*3.3/4095;
return temp;
}
2.3:按鍵模塊
也沒什麼想說的,自己看。如果說想知道怎麼樣判斷按鍵時間-請參考我之前的博客STM32實現按鍵功能之短按加一次而長按連續加的功能。
void Key_Init(void){
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
u8 KeyScan(u8 mode){
static _Bool KeyScan_Flag=1;
if(mode) KeyScan_Flag=1;
if((Key1==0||Key2==0||Key3==0||Key4==0)&&KeyScan_Flag){
Delay_Ms(10);
KeyScan_Flag=0;
if(Key1==0) return 1;
else if(Key2==0) return 2;
else if(Key3==0) return 3;
else if(Key4==0) return 4;
}
else if(Key1==1&&Key2==1&&Key3==1&&Key4==1) KeyScan_Flag=1;
return 0;
}
2.4:定時器模塊
由於我定時器模塊的中斷函數內容過多,在這裏我就不展示出來了,有興趣的可以直接下載我的程序去看。如果說想學習TIM中的多路捕獲和多路輸出功能,請參考我之前的博客藍橋杯嵌入式基礎板模塊之單個定時器的多路捕獲和多路輸出。
void Timer2_Init(void){
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_TimeBaseStructure.TIM_Period = 4999;
TIM_TimeBaseStructure.TIM_Prescaler = 71;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
}
2.5:串口模塊和字符串比較函數
這個也沒什麼想說的,自己看。如果說學習關於更多串口模塊的功能-請參考我之前的博客藍橋杯嵌入式基礎板模塊之串口模塊的發送與接收。
void Usart2_Init(void){
USART_InitTypeDef USART_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART2, &USART_InitStructure);
USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
USART_Cmd(USART2, ENABLE);
}
void USART2_IRQHandler(void)
{
u8 temp=0;
if(USART_GetITStatus(USART2,USART_IT_RXNE)!=RESET)
{
usart_State = 1;
USART_ClearFlag(USART2, USART_IT_RXNE);
temp= USART_ReceiveData(USART2);
if(RxCounter1==999)
{
RxBuffer1[RxCounter1]=temp;
Rx_Flag=1;
USART_ITConfig(USART2,USART_IT_RXNE,DISABLE);
USART_Cmd(USART2, DISABLE);
}
else{
RxBuffer1[RxCounter1++] = temp;
}
}
}
_Bool Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2)
{
while(*pBuffer1!=0)
{
if(*pBuffer1 != *pBuffer2)
{
return 0;
}
pBuffer1++;
pBuffer2++;
}
return 1;
}
PUTCHAR_PROTOTYPE
{
USART_SendData(USART2, (uint8_t) ch);
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET)
{}
return ch;
}
3:賽後總結
關於這次模擬賽,由於運氣好,拿到了第二名。。關於這次比賽的編程題,我個人認爲難度中等偏下吧,三個小時都可以說非常寬裕了,根本用不了5個小時。我這裏說的三個小時可是包括所有模塊的初始化的編寫的。
其實我感覺這個比賽,就是考的是你發散的思維,千萬不要侷限於別人的思想中。我寫着博客的作用更多的是給那些寫出來題目但是解題思路跟我不同的人看得,從而使大腦更加的活躍。
這個比賽是沒有技巧的,需要大量的經驗。如果說你還沒有做過往年省賽題和模擬題,那你就要趕緊加快速度了。我其實準備這個比賽也就用了一個月,我之前更多玩的是51和K60開發板,當然也玩過32但是就頂多算是入門。。在這一個月學的內容包括32的基礎知識,基礎板和拓展版的各個模塊學習,並且寫了往年所有的賽題。。。。至於爲什麼學那麼快,可能因爲我是個吃貨吧,我想拿公費去北京吃烤鴨!!!哈哈,當然那只是動力罷了,其實是因爲大二參加了智能車實驗室(當然現在還在),我們實驗室當時是每週一道綜合編程題。。。
最後送給大家我喜歡的六個字:
越努力越幸運