【STM32实战】机械臂快递分拣系统(二)——机械臂控制程序(上位机)编写
【STM32實戰】機械臂快遞分揀系統(二)——機械臂控制程序(上位機)編寫
- 前言
- 題目分析
- 藍牙模塊的使用
- 上位機程序的編寫
- 連接阿里云
- 測試
前言
近期回校上最后一門課,剛好是做機械臂有關的題目,所以寫文記錄一下。主要實現的是可以自動識別獲取快遞位置,機械臂可以抓取快遞,以及根據自動識別快遞上的條形碼獲得目的地點,機械臂可以將快遞抓取并移動到目的地點,此外就是還可以通過上位機來控制機械臂,上位機可以是PC,也可以是另一部STM32,需要有圖形化界面。
題目分析
自動識別獲取快遞位置以及識別快遞上的條形碼需要用到攝像頭模塊,需要解決STM32和攝像頭模塊之間的通信問題,以及快遞形狀。通過上位機來控制機械臂則需要使用到藍牙或者WIFI模塊,圖形化界面可以使用物聯網云平臺來開發,這里用的是阿里云。如果使用另一部32進行控制,則還可在另一部32上做界面。
但購買的機械臂是已經組裝完畢的,且上面帶有STM32系統板,型號為STM32F103C8T6,只引出了串口3的接口,也就是說如果要連接攝像頭模塊,就無法與上位機進行通信。所以我打算將該機械臂作為一個下位機來接收命令,上位機采用另一部STM32來做,上位機來處理串口接收到的來自云平臺或者攝像頭的數據,然后算出各個舵機的PWM占空比重裝載值,通過藍牙模塊發送到下位機機械臂,以此進行控制。
之前的博文已經完成了機械臂控制程序(下位機)的編寫,并通過藍牙以及串口調試工具能實現對機械臂的控制,本篇主要是完成上位機程序的編寫,使機械臂可以接收來自上位機的指令并做出相應的動作,方法還是采用串口通信,使用藍牙模塊連接,然后就是可以讓這個上位機能連接阿里云平臺,這樣就可以在阿里云平臺上實時看到各舵機的信息,以及發出控制指令,做出控制。
之前博文的傳送門如下
【STM32實戰】機械臂快遞分揀系統(一)——機械臂控制程序(下位機)編寫
由于需要用到藍牙模塊,先介紹一下怎么將兩個藍牙模塊配對。
藍牙模塊的使用
藍牙模塊就是一種透傳設備,串口給它發送什么,它就接收什么,然后它將接收到的數據通過某種協議發送到跟它配對的藍牙上,這個藍牙將這些數據解析,最后還原成最初的未經處理的數據,這就是透明傳輸意思。這跟使用杜邦線將兩個模塊串口的RT對接是一樣的。
推薦購買的藍牙模塊是這種帶有按鍵的,按下按鍵然后接到電腦(通電)即可進入AT模式,設置藍牙配對。
配對方法也很簡單,接CH340模塊(需要注意RT對接),在按鍵按下的狀態,將CH340接入電腦USB口,即可讓藍牙進入AT模式,一般此模式下藍牙模塊的波特率為38400,在串口工具上發送命令AT,勾選發送新行,即可看到窗口返回OK。
然后可以使用AT命令設置藍牙名稱AT+NAME=Bluetooth-Marster,然后點擊發送,這里設置名稱為Bluetooth-Marster,另一個藍牙的名稱可以設置為Bluetooth-Slave。
然后設置藍牙配對碼AT+PSWD=0425,然后點擊發送,這里設置為0425,另一個藍牙的配對碼也要是這個,才能成功配對。
然后設置工作模式為主機模式,命令是AT+ROLE=1,然后點擊發送,另一個藍牙需要設置為從機模式,命令就是AT+ROLE=0
然后配置藍牙串口參數AT+UART=115200,0,0,然后點擊發送即可,兩個藍牙都要設置一樣。
然后設置一下模式為無需地址綁定模式,這樣只要配對碼一樣就能配對成功了。命令AT+CMODE=1,然后發送即可。
藍牙模塊主機這就配置完畢了,接下來配置從機,也就是上面所說的另一個藍牙。
這里因為我的兩個藍牙不是一樣的,所有前面報錯。這個藍牙設置配對碼需要加上冒號,因此推薦買藍牙模塊的時候最好是買一樣的。
然后設置為從機模式
設置波特率115200
設置任意地址綁定模式
然后就將兩個CH340模塊拔掉再插入電腦,即可在串口工具上測試藍牙能否配對成功了。
需要注意的是現在的波特率就不是38400了。
這里發送111,可以看到另一個藍牙成功接收到了,說明配對成功了。
上位機程序的編寫
這里提供一下工程模板源碼、各類需要用到的庫源碼、以及我配置好的工程源碼,一方面是方便大家將庫用到自己的工程里,一方面是方便不想配置想先上手玩一玩的人的需求。資源鏈接如下,我設置的是0積分免費下載,如果不能下載給我留言吧!
STM32機械臂控制程序(上位機)
然后就是主要介紹一下我是怎么實現的,首先在 main.c 中將這些庫引入(怎么將庫加入工程之前也已經說過很多次了,這里就不再解釋了)
然后就是一些變量定義,這里先是定義一下任務有關的變量,task_num會在定時器中斷觸發的時候自增,直到值大于task_max后會清零,而task_flag會在定時器中斷觸發的時候被置為1,在任務完成的時候被置為0,這樣就可以根據task_num的值執行不同的任務,比如0的時候執行任務1,1的時候執行任務2,task_flag則是控制每一次只執行一次任務。
// 定時器任務隊列參數定義 uint8_t task_num = 0; // 任務序號 uint8_t task_flag = 0; // 任務完成標志 0完成 1未完成 uint8_t task_max = 3; // 任務序號最大值然后就是串口通信相關結構體定義,這部分服務于串口通信的,之前博文(串口通信)中有介紹怎么用,想深刻理解的可以點我主頁去找相應的博文看。
// 串口通信相關結構體定義 DataTransmit data_transmit_uart1; // 聲明全局結構體 data_transmit_uart1 DataReceive data_receive_uart1; // 聲明全局結構體 data_receive_uart1 DataTransmit data_transmit_uart3; // 聲明全局結構體 data_transmit_uart3 DataReceive data_receive_uart3; // 聲明全局結構體 data_receive_uart3 TargetProperty tstm32; // 聲明全局結構體 tstm32 TargetProperty rk210; // 聲明全局結構體 rk210 TargetProperty rstm32; // 聲明全局結構體 rstm32然后是定義一個變量,控制一下是否連接阿里云。主要原因就是連接阿里云太慢,然后我要調LCD屏幕上顯示的參數位置的時候,每一次都需要等連接阿里云,不等就要注釋相對應的代碼,很麻煩,所以我就定義一個變量來控制是否連接,這樣調試LCD屏上顯示的參數位置的時候就很方便了。
// ui 相關參數 uint8_t ui_flag = 0; // ui 調試模式標志 0 開啟 不連接阿里云 1 關閉 連接阿里云然后是定義一下服務于按鍵的相關參數。keycode是按鍵碼,這里我用的按鍵程序是正點原子的,按下板子上不同的按鍵會獲得不同的按鍵碼,我將這個按鍵碼用keycode保存,這樣就可以知道每次我按下的是什么鍵值。sw則是功能選擇標志,sw_max是功能選擇最大值,因為是6個舵機,所以該最大值是5,sw在某個按鍵按下的時候值會自增,直到超過5后清零,sw的值為0的時候,可以通過另外兩個按鍵,根據key_servo的值,將舵機1的占空比重裝在值進行加減key_servo的值的操作。以此類推,sw的值為1就是控制舵機2,為2就是控制舵機3……,每次加減操作舵機占空比重裝載值會變化50。
// 按鍵相關參數定義 uint8_t keycode = 0; // 按鍵碼 按下按鍵此變量將產生改變 uint8_t sw = 0; // 功能選擇標志位 uint8_t sw_max = 5; // 功能選擇最大值 uint16_t key_servo = 50; // 按鍵控制舵機占空比步進然后就是機械臂抓取參數定義,get是抓取計數器,抓取任務每一次進行的時候這個值都會自增,get_time控制的是機械臂抓取物體到移動物體再到放下物體這中間每一個動作的間隔。get_flag是抓取標志,這個值開始是0,到機械臂移動到物體的位置的時候被置為1,然后機械臂就開始抓取物體,get_down是保存抓取物體時候的二號舵機占空比值,因為放下物體的時候要將物體放到桌面再松開爪子。
// 機械臂抓取調試 uint16_t get = 0; // 抓取計數器 uint16_t get_flag = 0; // 0 不抓取 1 開始抓取 uint16_t get_down = 0; // 保存抓取物體時二號舵機占空比 服務于放下物體 uint16_t get_time = 3; // 抓取任務周期然后就是相關函數聲明,c語言中函數寫在后面的,但被前面函數調用到的,需要在最前面聲明,才能正常調用。
// 相關函數聲明 void System_Init(void); // 系統初始化函數 void Run_Task(void); // 運行任務函數 void Send_ALY(void); // 發送參數到阿里云函數 void Key_Dispose(void); // 按鍵處理函數 void RobControl(TargetProperty *rk210); // 機械臂控制函數接下來就是主函數
// 主函數 int main(void) { System_Init(); // 系統初始化while(1) { Run_Task(); // 任務開始運行} }主函數里面初始化函數主要完成的是延時函數初始化、中斷分組設置、各串口初始化、LCD初始化、定時器初始化、MQTT連接阿里云、串口通信相關結構體初始化。
// 系統初始化函數 void System_Init(void) {SysTick_Init(72); // 延時函數初始化 時鐘72MHz NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 設置NVIC中斷分組2 2位搶占優先級 2位響應優先級 即可設置 0-3 搶占優先級 0-3 子優先級 2位就是2的2次方uart2_init(115200); // 串口2 初始化 波特率 115200LCD_Init(); // LCD初始化if(ui_flag != 0) // 如果不為 ui 調試模式{MQTT_Init(); // MQTT初始化 連接阿里云}Init_TIM3(9999,719,0,1); // 720分頻 計數 10000 頻率 10 Hz 搶占優先級0 子優先級1 Init_UART1(115200,1,1); // 串口1 初始化 波特率115200 搶占優先級1 子優先級1 Init_UART3(115200,1,1); // 串口3 初始化 波特率115200 搶占優先級1 子優先級1 KEY_Init(); // 按鍵初始化ui(); // 界面靜態信息顯示Data_Transmit_Init(&data_transmit_uart1,0xFC,0xAA,7); // 設置發送給 K210 的幀頭 FC AA 有效數據長度 7Data_Receive_Init(&data_receive_uart1,0xFC,0xAA); // 設置接收自 K210 的幀頭 FC AA Data_Transmit_Init(&data_transmit_uart3,0xFC,0xAA,12); // 設置發送給 STM32 的幀頭 FC AA 有效數據長度 12Data_Receive_Init(&data_receive_uart3,0xFC,0xAA); // 設置接收自 STM32 的幀頭 FC AA Target_Init(&tstm32); // 初始化結構體 tstm32 Target_Init(&rk210); // 初始化結構體 rk210Target_Init(&rstm32); // 初始化結構體 rstm32 tstm32.servo1 = 1500; // 設置舵機初始狀態 豎直tstm32.servo2 = 1500;tstm32.servo3 = 1500;tstm32.servo4 = 1500;tstm32.servo5 = 1500;tstm32.servo6 = 1400;}運行函數則是按鍵掃描,以及根據不同的任務號完成不同的任務。這里我將任務1(task_num為0)中的串口1數據解析函數注釋掉了,這樣是為了調試方便,如果不注釋掉,在Debug的時候我將不能更改結構體rk210中的值,這樣RobControl就不能根據rk210中的坐標值抓取物體。K210識別物體的代碼后續博文會發,后面只需要接上K210,然后將這里的注釋符號刪除即可。
任務1就是接收K210數據,STM32數據(來自機械臂下位機,作為回顯),然后獲得控制機械臂抓取物體的各舵機占空比值。
任務2就是將任務1中的占空比值發到下位機,這樣機械臂才能做出相應的動作。
任務3就是LCD上的數據更新。
任務4就是將接收的K210的數據發回K210,作為回顯。然后將各數據發送到阿里云。
然后就是定時器中斷函數,這個之前說過了,每次觸發中斷,task_num的值會自增,直到超過task_max被清零,每次觸發中斷都會將task_flag置1。
// 定時器3 中斷函數 void TIM3_IRQHandler(void) // TIM3中斷服務函數 {if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) // 檢查TIM3更新中斷發生與否{if(task_num < task_max){task_num = task_num + 1; // 任務切換task_flag = 1; // 任務完成標志 0完成 1未完成 每次只做一次任務}else{task_num = 0; // 重頭開始執行任務task_flag = 1; // 任務完成標志 0完成 1未完成 每次只做一次任務}TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 清除TIM3更新中斷標志 }}串口中斷函數則是服務于接收串口數據。
// 串口1 中斷函數 void USART1_IRQHandler(void) {if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) // 判斷接收數據寄存器是否有數據{Data_Receive(&data_receive_uart1, USART_ReceiveData(USART1)); // 從 串口1 接收1個數據get_flag = 0; // 開始抓取USART_ClearFlag(USART1,USART_IT_RXNE); // 清空中斷標志 準備下一次接收}}// 串口3 中斷函數 void USART3_IRQHandler(void) {if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) // 判斷接收數據寄存器是否有數據{Data_Receive(&data_receive_uart3, USART_ReceiveData(USART3)); // 從 串口3 接收1個數據USART_ClearFlag(USART3,USART_IT_RXNE); // 清空中斷標志 準備下一次接收}}發送參數到阿里云則是發送各舵機占空比值到阿里云。
// 發送參數到阿里云函數 void Send_ALY(void) {// 各舵機占空比send_data("servo1",(int)tstm32.servo1);send_data("servo2",(int)tstm32.servo2);send_data("servo3",(int)tstm32.servo3);send_data("servo4",(int)tstm32.servo4);send_data("servo5",(int)tstm32.servo5);send_data("servo6",(int)tstm32.servo6);// 發送心跳包_mqtt.SendHeart(); }然后就是按鍵函數,用來控制各舵機占空比,keycode是按鍵碼,這里我用的按鍵程序是正點原子的,按下板子上不同的按鍵會獲得不同的按鍵碼,我將這個按鍵碼用keycode保存,這樣就可以知道每次我按下的是什么鍵值。sw則是功能選擇標志,sw_max是功能選擇最大值,因為是6個舵機,所以該最大值是5,sw在某個按鍵按下的時候值會自增,直到超過5后清零,sw的值為0的時候,可以通過另外兩個按鍵,根據key_servo的值,將舵機1的占空比重裝在值進行加減key_servo的值的操作。以此類推,sw的值為1就是控制舵機2,為2就是控制舵機3……,每次加減操作舵機占空比重裝載值會變化50。
// 按鍵功能選擇函數 void Key_Dispose(void) {// 0 不支持連續按 1 支持連續按keycode=KEY_Scan(0);// KEY 2if(keycode == 3){sw++;if(sw > sw_max) {sw = 0;}}// KEY UPif(keycode == 4){if(sw == 0){tstm32.servo1 += key_servo;}else if(sw == 1){tstm32.servo2 += key_servo;}else if(sw == 2){tstm32.servo3 += key_servo;}else if(sw == 3){tstm32.servo4 += key_servo;}else if(sw == 4){tstm32.servo5 += key_servo;}else if(sw == 5){tstm32.servo6 += key_servo;}}// KEY 1if(keycode == 2){if(sw == 0){tstm32.servo1 -= key_servo;}else if(sw == 1){tstm32.servo2 -= key_servo;}else if(sw == 2){tstm32.servo3 -= key_servo;}else if(sw == 3){tstm32.servo4 -= key_servo;}else if(sw == 4){tstm32.servo5 -= key_servo;}else if(sw == 5){tstm32.servo6 -= key_servo;}}// KEY 0 預留if(keycode == 1){get_flag = 0;}}機械臂控制函數則是根據接收到的物體x坐標的不同,控制爪子先靠近物體,再抓取物體,然后放到固定位置。需要注意的是這里的占空比是寫死的,因為我這個機械臂的精度比較低,誤差感覺上有一厘米左右,所以只能拿一個尺子,然后將物體放到1cm處,調節2、3、4號舵機占空比,使爪子靠近物體中心,記錄下各舵機占空比值,然后再放到2cm處,重復上述動作,我這里只做到了第17cm。
// 機械臂控制函數 void RobControl(TargetProperty *rk210) {if(rk210 -> flag != 0 && rk210 -> x != 0 && get_flag == 0){if(rk210 -> x == 0){tstm32.servo2 = 1460;tstm32.servo3 = 2090;tstm32.servo4 = 2150;get_down = tstm32.servo2;}else if(rk210 -> x == 1){tstm32.servo2 = 1420;tstm32.servo3 = 2050;tstm32.servo4 = 2150;get_down = tstm32.servo2;}else if(rk210 -> x == 2){tstm32.servo2 = 1380;tstm32.servo3 = 1990;tstm32.servo4 = 2150;get_down = tstm32.servo2;}else if(rk210 -> x == 3){tstm32.servo2 = 1340;tstm32.servo3 = 1950;tstm32.servo4 = 2150;get_down = tstm32.servo2;}else if(rk210 -> x == 4){tstm32.servo2 = 1300;tstm32.servo3 = 1910;tstm32.servo4 = 2150;get_down = tstm32.servo2;}else if(rk210 -> x == 5){tstm32.servo2 = 1260;tstm32.servo3 = 1880;tstm32.servo4 = 2150;get_down = tstm32.servo2;}else if(rk210 -> x == 6){tstm32.servo2 = 1300;tstm32.servo3 = 1900;tstm32.servo4 = 2100;get_down = tstm32.servo2;}else if(rk210 -> x == 7){tstm32.servo2 = 1300;tstm32.servo3 = 1880;tstm32.servo4 = 2100;get_down = tstm32.servo2;}else if(rk210 -> x == 8){tstm32.servo2 = 1280;tstm32.servo3 = 1860;tstm32.servo4 = 2100;get_down = tstm32.servo2;}else if(rk210 -> x == 9){tstm32.servo2 = 1280;tstm32.servo3 = 1880;tstm32.servo4 = 2020;get_down = tstm32.servo2;}else if(rk210 -> x == 10){tstm32.servo2 = 1280;tstm32.servo3 = 1880;tstm32.servo4 = 1980;get_down = tstm32.servo2;}else if(rk210 -> x == 11){tstm32.servo2 = 1280;tstm32.servo3 = 1900;tstm32.servo4 = 1940;get_down = tstm32.servo2;}else if(rk210 -> x == 12){tstm32.servo2 = 1280;tstm32.servo3 = 1900;tstm32.servo4 = 1920;get_down = tstm32.servo2;}else if(rk210 -> x == 13){tstm32.servo2 = 1220;tstm32.servo3 = 1820;tstm32.servo4 = 1960;get_down = tstm32.servo2;}else if(rk210 -> x == 14){tstm32.servo2 = 1240;tstm32.servo3 = 1880;tstm32.servo4 = 1860;get_down = tstm32.servo2;}else if(rk210 -> x == 15){tstm32.servo2 = 1240;tstm32.servo3 = 1880;tstm32.servo4 = 1840;get_down = tstm32.servo2;}else if(rk210 -> x == 16){tstm32.servo2 = 1240;tstm32.servo3 = 1880;tstm32.servo4 = 1820;get_down = tstm32.servo2;}else if(rk210 -> x == 17){tstm32.servo2 = 1160;tstm32.servo3 = 1800;tstm32.servo4 = 1820;get_down = tstm32.servo2;}get_flag = 1;}if(get_flag == 1){get++;if(get_time < get && get < 2*get_time){tstm32.servo6 = 1650;}else if(2*get_time < get && get < 3*get_time){tstm32.servo2 = 1500;tstm32.servo1 = 2000;}else if(3*get_time < get && get < 4*get_time){tstm32.servo2 = get_down;}else if(4*get_time < get && get < 5*get_time){tstm32.servo6 = 1400;}else if(5*get_time < get && get < 6*get_time){tstm32.servo2 =1500;}else if(6*get_time < get){tstm32.servo1 =1500;tstm32.servo2 =1500;tstm32.servo3 =1500;tstm32.servo4 =1500;tstm32.servo5 =1500;tstm32.servo6 =1400;get_down = 0;get_flag = 2;get = 0;}}}連接阿里云
這部分參數的配置在iot.h中,需要與你們自己的設備參數對應上。需要注意的是,WIFI名字只能是英文,且只支持2.4G的,5G的不行(ESP8266硬件本身不支持)。
MQTT連接參數查看位置
訂閱相關參數查看位置,${deviceName}需要改成ProductKey
界面我只是簡單的做了個折線圖以及幾個按鈕,可以控制舵機運動,其他的功能還暫時想不到。
按鈕的交互是這樣的,每次點擊,就會給ESP8266發送一個字符串,上位機通過接收到的字符串,控制舵機占空比值加減。這里的屬性值可以自行設置,與上位機那邊接收的代碼中識別的對應上即可。
用于接收這部分命令的代碼在iot.c中。
測試
藍牙模塊上電就會進入配對模式,如果一段時間配對無法成功,則會停止配對,需要重新上電才會重新配對,也就是說,下位機和上位機最好同時上電。
然后按下按鍵即可控制舵機旋轉啦!
如果要控制電機抓取物體的話,在Debug中,將結構體rk210中的x設置為3(我將藍塊放到3cm處),然后將flag置1,機械臂就會開始抓物體了。
同樣的,將結構體rk210中的x設置為9(我將藍塊放到9cm處),然后將flag置1,機械臂就會開始抓9cm處的物體了。
如果要用阿里云平臺的web界面按鈕控制舵機旋轉,記得將這個標志位置1,不然沒辦法連接阿里云。
現在是下位機和上位機程序都寫完了,后面就是差一個K210獲取物體坐標信息的程序需要寫了,然后將K210連接到上位機的UART1就可以了,我們下期再見咯!
總結
以上是生活随笔為你收集整理的【STM32实战】机械臂快递分拣系统(二)——机械臂控制程序(上位机)编写的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电路中相线、中性线和保护线的含义和作用
- 下一篇: xlwings出现的报错