目錄
定時器
一些小細節
輸入捕獲計算信號頻率
輸入捕獲計算占空比與頻率
使用定時器不改變占空比的同時改變頻率的方法
串口
重定向原理
重定向代碼
怎么從串口接收到的字符串數據中解析出float型的數據
strchr
sscanf
memset
第一種實現方法
RTC實時時鐘
LCD顯示
%s占位符與%c占位符的區別
I2C讀寫操作
定時器
一些小細節
其中HAL_TIM_Base_Start和HAL_TIM_Base_Start_IT這兩個函數是不能夠同時調用的,
- ??
HAL_TIM_Base_Start()
??:僅啟動定時器,不涉及中斷- ??
HAL_TIM_Base_Start_IT()
??:啟動定時器并開啟更新中斷- ??不能同時調用??:二者會沖突操作硬件寄存器和 HAL 庫狀態機
- ??正確做法??:根據需求選擇其中一個函數,或通過?
HAL_TIM_ENABLE_IT()
?動態管理中斷
輸入捕獲計算信號頻率
僅僅用于信號頻率的檢測就只需要使用定時器的上升沿觸發就可以了 , 捕獲兩個高電平的之間時間 ,用定時器時鐘比上時間差就能得到頻率 , 需要注意的細節在于+1的操作 , 因為計數都是從零開始的
下面是定時器17的輸入捕獲配置
?
//主函數里面打開定時器17的輸入捕獲中斷
HAL_TIM_IC_Start_IT(&htim17,TIM_CHANNEL_1);//編寫中斷回調函數
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM17){//測量頻率就是定時器的時鐘比上上升沿計數值capture = HAL_TIM_ReadCapturedValue(&htim17 ,TIM_CHANNEL_1);TIM17->CNT = 0;fre_out = main_clock/(capture*((TIM17->PSC)+1));}
}
輸入捕獲計算占空比與頻率
使用定時器3 , combined mode的PWM input on CH1實現
通道2配置為直接模式 , 通道2配置為間接模式(硬件自動配置) , 通道1和通道2分別捕獲上升沿和下降沿 , 設置濾波系數都為3.
配置定時器預分頻系數為80(從0開始計數)?, 主頻80M?, 所以定時器時鐘為1M , 頻率的計算就是定時器時鐘/上升沿到上升沿的時間
//主函數中打開定時器三的輸入捕獲中斷,兩個通道都需要打開
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){//如果是定時器3的中斷period = TIM3->CCR1+1;//硬件自動捕獲周期上升沿到上升沿high_time = TIM3->CCR2+1;//硬件自動捕獲高電平時間 , 上升沿到下降沿fre_cap = main_clock / (period * (TIM3->PSC + 1));//直接計算頻率duty = ((float)high_time / period) * 100;//計算占空比}
}
發現的hal庫小細節
//HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1 | TIM_CHANNEL_2);
//同時打開定時器三的中斷是不行的 , 需要像下面這樣一個通道一個通道的打開才能正常進入中斷HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);
使用定時器不改變占空比的同時改變頻率的方法
思路就是直接在定時器中改變定時器的預分頻系數 , 這樣占空比就能保持不變 , 但是頻率會逐漸改變
好像使用__HAL_TIM_SET_PRESCALER(&htim15 ,prescaler);這個函數在中斷中去進行改變定時器的預分頻系數總是會卡死的 , 改成直接操作寄存器的方式似乎就可以了TIM->PSC = prescaler
定時器中斷中不要改變其他定時器的預分頻系數 ,不然會程序會直接卡死,測試了幾個其他的函數發現只有setprescaler會卡死 , 然后加上先失能全局中斷后使能全局中斷之后就正常了 , 說明可能修改的定時器的中斷函數正在運行 , 但是預分頻系數突然被改變了導致的,所以需要先關閉全局中斷 , 修改完成之后再使能全局中斷 , 也可以直接操作寄存器實現預分頻系數的修改 .
也就是加上這兩句 , 更加具體的內容可以問Ai__disable_irq();//失能全局中斷
.........
__enable_irq();//使能全局中斷
下面是代碼 , 還有一些細節需要注意的就是ARR , CCR , PSC都是從零開始的所以需要仔細的調節一下
//按鍵部分
else if(short_press == 2){//選擇if(jiemian_mode == 0){//如果是數據界面__HAL_TIM_ENABLE_IT(&htim15, TIM_IT_UPDATE);//重新使能更新中斷
}//實現定時器的使能
就是手動開啟定時器的中斷 , 中斷中關閉定時器的中斷//定時器就15實現上升或者下降沿
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//中斷實際上是只能有一個的
{//如果是定時器15的中斷if(htim->Instance == TIM15){if(fre_mode == 0){//如果現在是低頻,需要緩慢變化到高頻if(TIM2->PSC >= 40){//40進來之后-最后一次//__disable_irq();//失能所有中斷TIM2->PSC-=1;if(TIM2->PSC <= 39){//這里也應該是減1的值fre_mode =1;__HAL_TIM_DISABLE_IT(&htim15,TIM_IT_UPDATE);//完成之后關閉更新中斷}//__enable_irq();//使能所有中斷}}else if(fre_mode == 1){//如果現在是高頻需要變化到低頻if(TIM2->PSC <= 78){//78進來再+最后一次//__disable_irq();//失能所有中斷TIM2->PSC+=1;if(TIM2->PSC >= 79){//應該是-1的值也就是80-1的值fre_mode =0;__HAL_TIM_DISABLE_IT(&htim15,TIM_IT_UPDATE);//完成之后關閉更新中斷}//__enable_irq();//使能所有中斷}}}
}
串口
重定向原理
就是更改了printf與scanf的底層函數,從另外的地址進行讀寫操作
重定向代碼
可以直接套用 ,模式配置成最異步最簡單的就可以
這個是普通的阻塞發送的串口重定向
/*--------------------串口重定向------------------*/
int fputc(int ch,FILE *f)
{
//采用輪詢方式發送1字節數據,超時時間設置為無限等待HAL_UART_Transmit(&huart2,(uint8_t *)&ch,1,HAL_MAX_DELAY);return ch;
}int fgetc(FILE *f)
{uint8_t ch;// 采用輪詢方式接收 1字節數據,超時時間設置為無限等待HAL_UART_Receive( &huart2,(uint8_t*)&ch,1, HAL_MAX_DELAY);return ch;
}
使用串口中斷進行數據接受的時候使用會使用到
HAL_UART_Receive_IT(&huart2,(uint8_t *)rec_data,num_byte);
這個函數會使能串口中斷接受然后我們在串口接受回調函數中接受數據 ,需要注意的點在于這個函數只要接收到1個字節就會觸發中斷,所以需要進行逐個字節的解析,這是比較麻煩的下面怎么利用串口的空閑中斷和DMA進行數據傳輸
配置界面如下
?
//主函數中
HAL_UARTEx_ReceiveToIdle_DMA(&huart1 , rx_buffer , sizeof(rx_buffer));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx , DMA_IT_HT);
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if(huart->Instance == USART1){//如果是串口1的中斷HAL_UARTEx_ReceiveToIdle_DMA(&huart1 , rx_buffer ,sizeof(rx_buffer));sscanf(rx_buffer ,"%d-%d",&a,&b);//讀取特定字符,強大好使printf("%d-%d\n",a,b);if(rx_buffer[0]=='1'&&rx_buffer[1]=='2'&&rx_buffer[2]=='3'&&rx_buffer[3]=='\n'){light(1,1);}else if(rx_buffer[0]=='3'&&rx_buffer[1]=='2'&&rx_buffer[2]=='1'&&rx_buffer[3]=='\n'){light(1,0);}}__HAL_DMA_DISABLE_IT(&hdma_usart1_rx , DMA_IT_HT);memset(rx_buffer,0,sizeof(rx_buffer));
}void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){//錯誤中斷HAL_UARTEx_ReceiveToIdle_DMA(&huart1 , rx_buffer ,sizeof(rx_buffer));__HAL_DMA_DISABLE_IT(&hdma_usart1_rx , DMA_IT_HT);memset(rx_buffer,0,sizeof(rx_buffer));}
}
怎么從串口接收到的字符串數據中解析出float型的數據
其實不只是浮點數像整數型等等都是可以的
從串口接收到的字符數據中解析出我們想要的數據總共有三種方法 , 分別是使用ssacnf函數,使用atof函數以及純手動進行數據解析 ,我將講解其中的第一和第二種
首先來講講C語言中的字符串處理函數
strchr
char *strchr(const char *str, int c)
這個函數用于查找字符串中我們想要的字符 , 其中str是給定字符串 , 其中c這個字符是在str中要搜索的字符串 ,返回值是指向該字符串中第一次出現的字符的指針,不包含時返回空指針
sscanf
sscanf()這個函數與我們常見的scanf函數的區別主要是sscanf()函數從給定的字符串中讀取指定字符,而scanf函數從屏幕中讀取指定字符 ,他們的函數原型分別如下
Int sscanf( string str, string fmt, mixed var1, mixed var2 ... );//str就是給定字符串,fmt是我們要的格式 ,var是我們指定的格式字符串保存的變量
int scanf( const char *format [,argument]... );//這個不過多講了
下面的資料來自于百度百科 , 這個函數真的超級強大 ,才注意到
1、一般用法
char?buf[512]?=?;
sscanf("123456?",?"%s",?buf);
printf("%s\n",?buf);
結果為:123456
2. 取指定長度的字符串。如在下例中,取最大長度為4字節的字符串。
sscanf("123456?",?"%4s",?buf);
printf("%s\n",?buf);
結果為:1234
3. 取到指定字符為止的字符串。如在下例中,取遇到空格為止字符串。
sscanf("123456?abcdedf",?"%[^?]",?buf);
printf("%s\n",?buf);
結果為:123456
4. 取僅包含指定字符集的字符串。如在下例中,取僅包含1到9和小寫字母的字符串。
sscanf("123456abcdedfBCDEF",?"%[1-9a-z]",?buf);
printf("%s\n",?buf);
結果為:123456abcdedf
5. 取到指定字符集為止的字符串。如在下例中,取遇到大寫字母為止的字符串。
sscanf("123456abcdedfBCDEF",?"%[^A-Z]",?buf);
printf("%s\n",?buf);
結果為:123456abcdedf
6.給定一個字符串iios/12DDWDFF@122,獲取 / 和 @ 之間的字符串,先將 "iios/"過濾掉,再將非'@'的一串內容送到buf中
sscanf("iios/12DDWDFF@122",?"%*[^/]/%[^@]",?buf);
printf("%s\n",?buf);
結果為:12DDWDFF
7.給定一個字符串"hello, world",僅保留"world"。(注意:“,”之后有一空格)
sscanf("hello,?world",?"%*s%s",?buf);
printf("%s\n",?buf);
結果為:world P.S.?%*s表示第一個匹配到的%s被過濾掉,即hello,被過濾了,如果沒有空格則結果為NULL
memset
memset是C和C++中的內存初始化函數,可以將指定內存區域設置為特定值
void *memset(void *s, int ch,?size_t?n);//函數原型
void *s是要填充的內存區域的起始地址
int ch指定了要填充到內存的值(實際填充時ch 會被轉換為unsigned char 類型)
size_t n 決定了要填充的字節數
第一種實現方法
使用時記得在keil中勾選use micro lib這個選項
strchr+sscanf實現
#include <stdio.h> // 確保標準庫支持char rx_data[] = "k0.2\n";
float value = 0;// 查找'k'的位置并解析
char *key_ptr = strchr(rx_data, 'k');
if (key_ptr != NULL) {if (sscanf(key_ptr + 1, "%f", &value) == 1) { // +1跳過'k'// 成功解析,value = 0.2} else {// 解析失敗處理}
}
uint8_t rx_buffer[128];//接受緩存區
uint8_t rx_index = 0;
_Bool rx_complete = 0;
float new_k = 0;
//在這里我們發送的數據是k0.4\n,總共是5個字符
//中斷進行數據解析
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if (huart->Instance == USART2){HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buffer, 128); //接收完畢后重啟串口DMA模式接收數據//數據處理,從字符串中解析數據變成0.4,使用字符函數實現if(rx_buffer[0] == 'k'&& rx_buffer[4] == '\n')//進行{//其中target是指向字符串中第一次出現我們想要字符的指針char* target = strchr((const char *)rx_buffer , 'k');if(target != NULL){if(sscanf((const char*)target + 1,"%f",&new_k) == 1)//解析成功的處理{light(3,1);} else {//解析失敗的處理light(4,1);}}}//HAL_UART_Transmit(&huart2, rx_buffer, Size, 0xffff); // 將接收到的數據再發出__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); // 手動關閉DMA_IT_HT中斷memset(rx_buffer, 0, 128); // 清除接收緩存}
}
第二種
使用atof函數
RTC實時時鐘
RTC實時時鐘就是32內部的一個獨立的定時器 ,如果使用外部低速晶振那就是32.768khz,進行預分頻,如果是127和255,就是32768/((127+1)*(255+1)) = 1Hz
即便不需要獲取日期,也需要添加獲取日期的函數,不然時間是不會流動的
RTC_TimeTypeDef sTime = {0};RTC_DateTypeDef sDate = {0};RTC_AlarmTypeDef sAlarm = {0};//在上面先定義結構體HAL_RTC_GetTime(&hrtc,&sTime,RTC_FORMAT_BIN);//獲取時間保存到Time結構體HAL_RTC_GetDate(&hrtc,&sDate,RTC_FORMAT_BIN);//獲取日期保存到Date結構體
如果想要手動修改RTC鬧鐘的時間可以自己再寫一個函數
void Set_RTC_Alarm(uint8_t hours ,uint8_t minutes ,uint8_t seconds)
{RTC_AlarmTypeDef sAlarm = {0};HAL_RTC_DeactivateAlarm(&hrtc, RTC_ALARM_A);//棄用舊鬧鐘//實際測試中好像不需要這兩句代碼也能正常修改,不知道為什么__HAL_RCC_PWR_CLK_ENABLE();//解除備份域時鐘保護HAL_PWR_EnableBkUpAccess();//使能備份域時鐘操作sAlarm.AlarmTime.Hours = hours;sAlarm.AlarmTime.Minutes = minutes;sAlarm.AlarmTime.Seconds = seconds;sAlarm.AlarmTime.SubSeconds = 0;sAlarm.AlarmTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;//這個接口是棄用的sAlarm.AlarmTime.StoreOperation = RTC_STOREOPERATION_RESET;sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY;sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_NONE;sAlarm.AlarmDateWeekDaySel = RTC_ALARMSUBSECONDMASK_NONE;sAlarm.AlarmDateWeekDay = 1;sAlarm.Alarm = RTC_ALARM_A;HAL_RTC_SetAlarm_IT(&hrtc , &sAlarm,RTC_FORMAT_BIN);HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0);HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
}
LCD顯示
主要點在于通過定時器定時中斷設置標志為進行閃爍顯示,在中斷里面寫一個標志位 ,不斷的翻轉這個標志位的狀態實現,不同的狀態顯示不同的內容就能實現閃爍的顯示 , 或者下劃線什么的 ,這個比較簡單了
顯示一些不常見字符的方法 例如顯示百分號的方法
char baifen = '%';
sprintf(showstring," P:%.2f%c ",duty,baifen);
LCD_DisplayStringLine(Line4,(uint8_t *)showstring);
//這樣就能顯示百分號了
%s占位符與%c占位符的區別
特性 | %c | %s |
---|---|---|
操作對象 | 單個字符 (char ) | 字符串(字符數組/指針) |
內存操作 | 1字節 | 連續內存直到?\0 |
輸入行為 | 讀取任何字符(包括空格) | 跳過空白符,讀到空格停止 |
是否需要?& | 需要(scanf ?中) | 不需要(數組名即地址) |
輸出終止條件 | 無(固定1字符) | 遇到?\0 ?停止 |
?C語言中 , 不要直接寫3/8這樣你會得到0 , 想要得到浮點數就寫3.0/8.0這樣計算得到浮點數
I2C讀寫操作
實際上就是E2PROM的讀寫操作的實現應當是較為簡單的 , 下面給出代碼
//0xa0是寫地址
//0xa1是讀地址
void eeprom_write(uint8_t addr ,uint8_t dat)
{I2CStart();I2CSendByte(AT24C02_Adress);//寫哪個從機I2CWaitAck();//等待應答I2CSendByte(addr);//發送要寫地址I2CWaitAck();I2CSendByte(dat);//發送要寫的數據I2CWaitAck();I2CStop();HAL_Delay(20);//防止連續寫入時出錯
}uint8_t eeprom_read(uint8_t addr)
{I2CStart();I2CSendByte(AT24C02_Adress);//寫哪個從機I2CWaitAck();//等待應答I2CSendByte(addr);//發送要寫地址I2CWaitAck();//等待應答I2CStop();I2CStart();//重新開始是為了將控制權交給從機I2CSendByte(0xa1);//發送要讀的地址I2CWaitAck();//等待應答uint8_t data = I2CReceiveByte();//接受數據I2CSendNotAck();//發送不回應,也就是不繼續讀了I2CStop();//停止return data;
}