# 2024 嵌入式作業系統分析與實做 Final Project ### 專案名稱: 倉鼠智慧照顧系統 Hamster Smart Care System [Github](https://github.com/ssheep773/FreeRTOS_AutomaticFeeder) ## 動機 倉鼠的飼主遇到以下幾個問題: 1.忘記餵食飼料 2.觀察倉鼠有無按時吃飯 3.觀察倉鼠的活動量 ## 實作內容 #### 1. 餵食模組: 每隔固定時間投放飼料,同時感測倉鼠有無站在飼料區前進食,若無則發出聲音提示 #### 2. 運動計時模組: 透過滾輪的圈數計算倉鼠的運動量,使用紅外線感應器,搭配滾輪上的擋板,檢測滾輪圈數。 使用紅外線檢測,易於按裝在現有飼養盒中 #### 3. 顯示模組: 運用 LCD 面板顯示倉鼠的相關資訊 1. 進食次數與多久未進食 2. 所跑圈數 #### 實作環境: Evaluation Boards: STM32F407G-DISC1 IDE: STM32CubeIDE OS: FreeRTOS Language: C ## 程式碼說明 ### 所有的任務列表 ```c /*Final_project/Final_project/Core/Src/main.c*/ MsgQueue = xQueueCreate(1, sizeof(unsigned int)); // sign for switch display mode xTaskCreate(InfraredTask, "InfraredTask", 128, NULL, 1, NULL); // detect running lap xTaskCreate(ButtonTask, "ButtonTask", 128, NULL, 1, NULL); // switch lcd displaymode xTaskCreate(Display, "Display", 128, NULL, 1, NULL); // show lcd content xTaskCreate(BuzzerTask, "BuzzerTask", 128, NULL, 1, NULL); // sound beep to remind eating xTaskCreate(Motor_Task, "Motor_Task", 128, NULL, 1, NULL); // check if have eaten xTaskCreate(TimerTask, "TimerTask", configMINIMAL_STACK_SIZE, NULL, 1, &xTimerTaskHandle); // Timer ``` ### 紅外線感測滾輪圈數 InfraredTask() 在原有的滾輪上加裝擋板,用於紅外線的感測。每當紅外線感測到檔板時增加圈數,並且是當狀態改變時才增加,避免檔板剛好停在感應處,造成一直增加圈數的問題。 `objectDetected` :感測到檔板 `RunningLap` :實際的圈數 ```c void InfraredTask(void *pvParameters) { for (;;) { if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_4) == GPIO_PIN_RESET) { if (objectDetected == 0) { objectDetected = 1; RunningLap++; } } else objectDetected = 0; vTaskDelay(10); } } ``` ### 切換顯示模式的按鈕 ButtonTask() `task` 用於紀錄切換的模式,`count` 紀錄按鈕按壓次數 這裡切換顯示模式的方法,是透過檢查 count 是奇數或是偶數來切換,再透過 MsgQueue 將切換的資訊傳遞給其他任務,最後為了減少切換的延遲,使用 `taskYIELD()` 將 CPU 釋放給其他需要的任務。 ```c void ButtonTask(void *pvParameters) { unsigned int task = 0; unsigned int count = 0; for(;;) { if(HAL_GPIO_ReadPin(btn_blue_GPIO_Port, GPIO_PIN_0)) { HAL_Delay(100);//debounce while(HAL_GPIO_ReadPin(btn_blue_GPIO_Port, GPIO_PIN_0)) {;} ++count ; if(count & 0x01) task = 1; else task = 0; xQueueSend(MsgQueue,(int * ) &task,1); taskYIELD(); } } } ``` ### LCD 螢幕顯示 Display() 透過藍色按鈕切換兩種顯示模式,顯示餵食狀況與運動狀況 實作方法: 運用 `i2c-lcd.h` 和 `i2c-lcd.c` 控制 LCD 面板。 並接收來 分為兩個模式顯示根據 displaymode 決定, 這裡使用 `for (;;)` 讓畫面一直更新顯示內容,並透過 `MsgQueue` 接收 `ButtonTask()` 的訊號,當數值改變時跳出顯示迴圈,切換顯示模式。 ```c // Final_project/Final_project/Core/Src/main.c #include "i2c-lcd.h" // for LCD module void Display(void *pvParameters) { unsigned int displaymode = 0; for(;;) { char buffer[30]; if (displaymode == 0){ for (;;) { lcd_clear(); lcd_put_cur(0, 0); sprintf(buffer, "eat: %u %lu",eat,timeCounter); lcd_send_string(buffer); lcd_put_cur(1, 0); memset(buffer, 0, sizeof(buffer)); sprintf(buffer, "noneat time: %lu", wait_timeCounter); lcd_send_string(buffer); xQueueReceive(MsgQueue, &displaymode, 0); vTaskDelay(10); if(displaymode == 1) break; } } else if(displaymode == 1){ for (;;) { lcd_clear(); lcd_put_cur(0, 0); sprintf(buffer, "RunningLap: %lu", RunningLap); lcd_send_string(buffer); xQueueReceive(MsgQueue, &displaymode, 0); vTaskDelay(500); if(displaymode == 0) break; } } } } ``` ### 蜂鳴器警示 Beep() 主要是透過 `GPIO_PIN_RESET` 發出聲音,以及 `GPIO_PIN_SET` 停止聲音,來控制蜂鳴器的聲響 > 這裡應該改以使用 `vTaskDelay()` 的方式等待,蜂鳴器不需要太精確的時間控制。 ```c void Beep(void){ uint32_t From_begin_time = HAL_GetTick(); HAL_GPIO_WritePin(GPIOA, Beep_Pin, GPIO_PIN_RESET); while(HAL_GetTick() - From_begin_time < 50 / portTICK_RATE_MS){} From_begin_time = HAL_GetTick(); HAL_GPIO_WritePin(GPIOA, Beep_Pin, GPIO_PIN_SET); while(HAL_GetTick() - From_begin_time < 500 / portTICK_RATE_MS){} } ``` ### 馬達驅動餵食器 Motor_Task() 利用 PWM 控制馬達的運行 `if (timeCounter % FEEDING_INTERVAL_SECONDS ==0)` 控制餵食頻率,此專案設定每 15 秒投放一次飼料。 初始化馬達 `HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4)` , 轉動馬達分為三個步驟,首先是快速轉動閥門,接著短暫的停頓,再將閥門快速關閉。 `__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, 150)` 控制馬達的轉速,目的是將馬達轉動 90 度並稍微停頓後轉回,模擬到飼料的情境。 這裡使用 `while(HAL_GetTick() - From_begin_time < 500 / portTICK_RATE_MS){}` 不是使用 `vTaskDelay(500)` 是因為 `vTaskDelay(500)` 會受到其他任務執行時間的影響,而馬達轉動需要較精確的時間控制,所以使用此 busy waiting 的方式 > 原本的設計是希望透過兩個閥門控制,第一個閥門(下閥)用於控制倒出飼料,第二的閥門(上閥)用於將飼料填滿兩個閥門之間的空間,而這個空間就是每次飼料的投放量。(最後僅實作一個) ```c void Motor_Task(void *pvParameters){ for (;;){ if (timeCounter % FEEDING_INTERVAL_SECONDS ==0){ HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15, GPIO_PIN_RESET); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4); uint32_t From_begin_time = HAL_GetTick(); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, 150); while(HAL_GetTick() - From_begin_time < 500 / portTICK_RATE_MS){} From_begin_time = HAL_GetTick(); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, 50); while(HAL_GetTick() - From_begin_time < 500 / portTICK_RATE_MS){} From_begin_time = HAL_GetTick(); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, 150); while(HAL_GetTick() - From_begin_time < 500 / portTICK_RATE_MS){} } } } ``` ### 時間計算器 TimerTask() 我們選擇使用軟體的方式來計算時間,雖然 STM32 也有內建硬體計時器可以使用,但由於需要將時間計算與馬達等模組的硬體設置進行同步,因此決定使用軟體計算方式來保持一致性。 ``` void TimerTask(void *pvParameters) { TickType_t xLastWakeTime; const TickType_t xFrequency = pdMS_TO_TICKS(1000); // 1 second as cycle // record currnet time xLastWakeTime = xTaskGetTickCount(); for (;;) { // wait until next cycle vTaskDelayUntil(&xLastWakeTime, xFrequency); // update timer timeCounter++; wait_timeCounter++; } } ``` ### 壓力感測模組 HAL_GPIO_EXTI_Callback() 使用硬體中斷 call back 函數,並在函數內部判斷 GPIO_Pin 來處理不同的 GPIO 中斷。 當觸摸壓力感測器,表示有進食因此重設未進食時間,同時用黃燈警示。 `touch = 1` 這裡使用數值而非布林值是因為 FreeRTOS 沒有布林值的數值型態。 當觸發時會中斷前的任務,然後執行此中斷函式,稍微減緩同步問題。 ```c void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) // touch pressure sensor { if (GPIO_Pin == GPIO_PIN_5) { HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12); // yellow light touch = 1; // set torch to True(1) wait_timeCounter = 0; // reset wait_timeCounter } } ``` ## 硬體設定與腳位 SYS -> Timebase Source : **TIM7** ### 馬達的硬體設置 ![螢幕擷取畫面 2024-07-15 220054](https://hackmd.io/_uploads/BJf7hjMdA.jpg) ![螢幕擷取畫面 2024-07-15 220113](https://hackmd.io/_uploads/Bk773jfu0.jpg) ![螢幕擷取畫面 2024-07-15 220135](https://hackmd.io/_uploads/SkG72iz_A.jpg) ![螢幕擷取畫面 2024-07-15 220153](https://hackmd.io/_uploads/HJMmnsG_R.jpg) ### Pin 腳 因為需要連接 5 個設備,有些設備的 VCC 不是接建議的 5V ,但是還是可以運作 | LCD screen | STM board | | -------- | -------- | | GND | GND | | VCC | 5V | | SDA | PB9 | | SCL | PB6 | | 馬達 SG-90 | STM board | | -------- | -------- | | GND (棕色) | GND | | VCC (紅色) | 5V | | PWM 輸入 (橘色) | PA3 | | 紅外線 | STM board | | -------- | -------- | | GND | GND | | VCC | VDD | | OUT | PC4 | | 蜂鳴器 | STM board | | -------- | -------- | | GND | GND | | VCC | 3V | | I/O | PA1 | | 壓力感測器 | STM board | | -------- | -------- | | GND | GND | | VCC | VDD | | SIG | PC5 |