https://m.toutiao.com/is/D1EDpvn/ 本例程采用了HAL庫進行項目開發(fā)(主要使用軟件CubexMX和keil5),文章末尾會有代碼開源,歡迎各位對文章進行指正和探討。 基于PID的減速電機控制一、 硬件模塊與原理圖1、硬件組成硬件組成:stm32f103c8t6最小系統(tǒng)板;0.96寸LED12864(I2C通訊模式);智能小車12v移動電源;25GA370直流減速電機(帶霍爾編碼器);JDY-31藍牙模塊;L298N電機驅(qū)動模塊;杜邦線若干;1個面包板; 圖片如下: ![]() 2、模塊分析1、L298N電機驅(qū)動模塊![]() ![]() 1.模塊可驅(qū)動兩路直流電機,輸出A和B各接一直流電機即可; 2.若使用12V供電,將12V供電端口及GND接上電源正負即可,同時5V供電端可以作為最小系統(tǒng)板的輸入電源; 3.若不需要使用PWM調(diào)速,只需要控制電機正反轉(zhuǎn),則邏輯A與B跳線帽插上即可,相當于始終使能; 4.若需要使用PWM調(diào)速,需將跳線帽拔起,將使能端接上單片機IO口。(定時器IO口,PWM輸出模式); 5.邏輯輸入四個端口IN1、IN2、IN3、IN4接單片機四個IO口,每兩個端口控制的一路電機。 溫馨提示: 特別不建議新手或者資金有限的情況下,使用電機驅(qū)動模塊直連成品開發(fā)板,很容易燒壞。 原因:(1) 由于電機的特性,電機在堵轉(zhuǎn)或者高負載下,電流會增大,可能會影響到單片機。(2)新手玩單片機可能出現(xiàn)短路等情況,很容易板子冒煙; L298N的轉(zhuǎn)動邏輯圖: ![]() 2、0.96寸OLED(I2C通訊)![]() ![]() (1)目前市面主要分為OLED與LCD這2種屏幕; (2)OLED自發(fā)光特性,LCD都要背光,而OLED不需要,因為它是自發(fā)光。這樣同樣的顯示,OLED效果要來得好一些; (3)多種接口方式:6800,8080兩種并行接口方式,4線的穿行SPI接口,IIC接口方式(2線); (4)不要接過高電壓,3.3V就可以正常工作了; (5)OLED不足之處是做大之后成本較高。 本實驗采用了0.96寸OLED的屏幕(通訊方式IIC),4個接線柱(SCL,SDA,GND,VCC); IIC通訊實現(xiàn)方式: IIC(Inter-Integrated Circuit)總線是一種由 PHILIPS 公司開發(fā)的兩線式串行總線,用于連接微控制器及其外圍設備。它是由數(shù)據(jù)線 SDA 和時鐘 SCL 構成的串行總線,可發(fā)送和接收數(shù)據(jù)。高速 IIC 總線一般可達 400kbps 以上。 模擬IIC通訊: I2C 是支持多從機的,也就是一個 I2C 控制器下可以掛多個 I2C 從設備,這些不同的 I2C從設備有不同的器件地址,這樣 I2C 主控制器就可以通過 I2C 設備的器件地址訪問指定的 I2C設備了。SDA 和SCL 這兩根線必須要接一個上拉電阻,一般是 4.7K。其余的 I2C 從器件都掛接到 SDA 和 SCL 這兩根線上,這樣就可以通過 SDA 和 SCL 這兩根線來訪問多個 I2C設備。 I2C 協(xié)議:(1)起始位;(2)停止位;(3)數(shù)據(jù)傳輸;(4)應答信號;(5)I2C 寫時序;(6)I2C 讀時序 I2C 寫時序: ![]() 1)、開始信號。 2)、發(fā)送 I2C 設備地址,每個 I2C 器件都有一個設備地址,通過發(fā)送具體的設備地址來決 定訪問哪個 I2C 器件。這是一個 8 位的數(shù)據(jù),其中高 7 位是設備地址,最后 1 位是讀寫位,為 1 的話表示這是一個讀操作,為 0 的話表示這是一個寫操作。 3)、 I2C 器件地址后面跟著一個讀寫位,為 0 表示寫操作,為 1 表示讀操作。 4)、從機發(fā)送的 ACK 應答信號。 5)、重新發(fā)送開始信號。 6)、發(fā)送要寫寫入數(shù)據(jù)的寄存器地址。 7)、從機發(fā)送的 ACK 應答信號。 8)、發(fā)送要寫入寄存器的數(shù)據(jù)。 9)、從機發(fā)送的 ACK 應答信號。 10)、停止信號。 I2C 讀時序: ![]() I2C 單字節(jié)讀時序比寫時序要復雜一點,讀時序分為 4 大步,第一步是發(fā)送設備地址,第二步是發(fā)送要讀取的寄存器地址,第三步重新發(fā)送設備地址,最后一步就是 I2C 從器件輸出要讀取的寄存器值,我們具體來看一下這幾步。 1)、主機發(fā)送起始信號。 2)、主機發(fā)送要讀取的 I2C 從設備地址。 3)、讀寫控制位,因為是向 I2C 從設備發(fā)送數(shù)據(jù),因此是寫信號。 4)、從機發(fā)送的 ACK 應答信號。 5)、重新發(fā)送 START 信號。 6)、主機發(fā)送要讀取的寄存器地址。 7)、從機發(fā)送的 ACK 應答信號。 8)、重新發(fā)送 START 信號。 9)、重新發(fā)送要讀取的 I2C 從設備地址。 10)、讀寫控制位,這里是讀信號,表示接下來是從 I2C 從設備里面讀取數(shù)據(jù)。 11)、從機發(fā)送的 ACK 應答信號。 12)、從 I2C 器件里面讀取到的數(shù)據(jù)。 13)、主機發(fā)出 NO ACK 信號,表示讀取完成,不需要從機再發(fā)送 ACK 信號了。 14)、主機發(fā)出 STOP 信號,停止 I2C 通信。 3、JDY-31藍牙模塊![]() ![]() 市場上藍牙模塊有很多,常見的JDY-xx,HC-xx等系列。其實看似高級的藍牙功能背后就是簡單的串口通訊; USART 的全稱是 Universal Synchronous/Asynchronous Receiver/Transmitter,也就是同步/異步串行收發(fā)器。相比 UART 多了一個同步的功能,在硬件上體現(xiàn)出來的就是多了一條時鐘線。一般 USART 是可以作為 UART 使用的,也就是不使用其同步的功能。 串口通訊協(xié)議: 數(shù)據(jù)包:串口通訊的數(shù)據(jù)包由發(fā)送設備通過自身的TXD接口傳輸?shù)浇邮赵O備得RXD接口,在協(xié)議層中規(guī)定了數(shù)據(jù)包的內(nèi)容,具體包括起始位、主體數(shù)據(jù)(8位或9位)、校驗位以及停止位,通訊的雙方必須將數(shù)據(jù)包的格式約定一致才能正常收發(fā)數(shù)據(jù)。 具體如圖所示: ![]() 波特率:由于異步通信中沒有時鐘信號,所以接收雙方要約定好波特率,即每秒傳輸?shù)拇a元個數(shù),以便對信號進行解碼,常見的波特率有4800、9600、115200等。STM32中波特率的設置通過串口初始化結(jié)構體來實現(xiàn)。 注意:MCU設置的波特率大小要與藍牙APP設置的大小一致! 4、6線減速電機(帶編碼器)模塊: 市面上電機有很多,常用的有步進電機,直流減速電機,伺服電機等等; 編碼器:用來測量電機轉(zhuǎn)速的儀器元件,常見的有:霍爾編碼器,光電編碼器等 電機的驅(qū)動原理很簡單,給電壓差即可使得電機轉(zhuǎn)動,調(diào)速則利用PWM調(diào)節(jié)發(fā)。 ![]() 編碼器原理: 編碼器是一種將角位移或者角速度轉(zhuǎn)換成一串電數(shù)字脈沖的旋轉(zhuǎn)式傳感器。 編碼器工作原理: 霍爾編碼器是有霍爾馬盤和霍爾元件組成?;魻栺R盤是在一定直徑的圓板上等分的布置有不同的磁極?;魻栺R盤與電動機同軸,電動機旋轉(zhuǎn)時,霍爾元件檢測輸出若干脈沖信號,為判斷轉(zhuǎn)向,一般輸出兩組存在一定相位差的方波信號。 ![]() ![]() 注意:通過判斷A與B相哪一位在前,即可判斷出正轉(zhuǎn)還是反轉(zhuǎn) 二、CubexMX設置使用的MCU為stm32f103c8t6: ![]() RCC: ![]() SYS: ![]() 注意:Debug這里一定要設置成Serial Wire否則可能出現(xiàn)芯片自鎖 GPIO設置: ![]() 定時TIM2用來測速與測量正轉(zhuǎn)反轉(zhuǎn)(計數(shù)器模式) ![]() 定時3:PWM調(diào)節(jié) ![]() I2C: ![]() USART1: ![]() 之后按照自己習慣生成初始化文件 三、代碼自動生成的: ![]() 需要自己編寫的: ![]() I2C代碼: #include 'oled.h'#include 'asc.h'#include 'main.h'void WriteCmd(unsigned char I2C_Command)//??? { HAL_I2C_Mem_Write(&hi2c2,OLED0561_ADD,COM,I2C_MEMADD_SIZE_8BIT,&I2C_Command,1,100); } void WriteDat(unsigned char I2C_Data)//??? { HAL_I2C_Mem_Write(&hi2c2,OLED0561_ADD,DAT,I2C_MEMADD_SIZE_8BIT,&I2C_Data,1,100); } void OLED_Init(void){ HAL_Delay(100); //???????? WriteCmd(0xAE); //display off WriteCmd(0x20); //Set Memory Addressing Mode WriteCmd(0x10); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid WriteCmd(0xb0); //Set Page Start Address for Page Addressing Mode,0-7 WriteCmd(0xc8); //Set COM Output Scan Direction WriteCmd(0x00); //---set low column address WriteCmd(0x10); //---set high column address WriteCmd(0x40); //--set start line address WriteCmd(0x81); //--set contrast control register WriteCmd(0xff); //???? 0x00~0xff WriteCmd(0xa1); //--set segment re-map 0 to 127 WriteCmd(0xa6); //--set normal display WriteCmd(0xa8); //--set multiplex ratio(1 to 64) WriteCmd(0x3F); // WriteCmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content WriteCmd(0xd3); //-set display offset WriteCmd(0x00); //-not offset WriteCmd(0xd5); //--set display clock divide ratio/oscillator frequency WriteCmd(0xf0); //--set divide ratio WriteCmd(0xd9); //--set pre-charge period WriteCmd(0x22); // WriteCmd(0xda); //--set com pins hardware configuration WriteCmd(0x12); WriteCmd(0xdb); //--set vcomh WriteCmd(0x20); //0x20,0.77xVcc WriteCmd(0x8d); //--set DC-DC enable WriteCmd(0x14); // WriteCmd(0xaf); //--turn on oled panel} void OLED_SetPos(unsigned char x, unsigned char y) //???????{ WriteCmd(0xb0+y); WriteCmd(((x&0xf0)>>4)|0x10); WriteCmd((x&0x0f)|0x01);} void OLED_Fill(unsigned char fill_Data)//????{ unsigned char m,n; for(m=0;m<8;m++) { WriteCmd(0xb0+m); //page0-page1 WriteCmd(0x00); //low column start address WriteCmd(0x10); //high column start address for(n=0;n<128;n++) { WriteDat(fill_Data); } }} void OLED_CLS(void)//??{ OLED_Fill(0x00);} void OLED_ON(void){ WriteCmd(0X8D); //????? WriteCmd(0X14); //????? WriteCmd(0XAF); //OLED??} void OLED_OFF(void){ WriteCmd(0X8D); //????? WriteCmd(0X10); //????? WriteCmd(0XAE); //OLED??} // Parameters : x,y -- ?????(x:0~127, y:0~7); ch[] -- ???????; TextSize -- ????(1:6*8 ; 2:8*16)// Description : ??codetab.h??ASCII??,?6*8?8*16???void OLED_ShowStr(unsigned char x, unsigned char y, unsigned char ch[], unsigned char TextSize){ unsigned char c = 0,i = 0,j = 0; switch(TextSize) { case 1: { while(ch[j] != '\0') { c = ch[j] - 32; if(x > 126) { x = 0; y++; } OLED_SetPos(x,y); for(i=0;i<6;i++) WriteDat(F6x8[c][i]); x += 6; j++; } }break; case 2: { while(ch[j] != '\0') { c = ch[j] - 32; if(x > 120) { x = 0; y++; } OLED_SetPos(x,y); for(i=0;i<8;i++) WriteDat(F8X16[c*16+i]); OLED_SetPos(x,y+1); for(i=0;i<8;i++) WriteDat(F8X16[c*16+i+8]); x += 8; j++; } }break; }} // Parameters : x,y -- ?????(x:0~127, y:0~7); N:???.h????// Description : ??ASCII_8x16.h????,16*16??void OLED_ShowCN(unsigned char x, unsigned char y, unsigned char N){ unsigned char wm=0; unsigned int adder=32*N; OLED_SetPos(x , y); for(wm = 0;wm < 16;wm++) { WriteDat(F16x16[adder]); adder += 1; } OLED_SetPos(x,y + 1); for(wm = 0;wm < 16;wm++) { WriteDat(F16x16[adder]); adder += 1; }} // ????????????????,????????“??——???——????”??????ascll.h?????(????)//???????:x:????? // y:???(??0-7) // begin:????????????????ascll.c??????? // num:????????// ????“??”,??????????????????0,1,???0,??????,??:x:0,y:2,begin:0,num:2void OLED_ShowCN_STR(u8 x , u8 y , u8 begin , u8 num){ u8 i; for(i=0;i<num;i++){OLED_ShowCN(i*16+x,y,i+begin);} //OLED????} // Parameters : x0,y0 -- ?????(x0:0~127, y0:0~7); x1,y1 -- ?????(???)???(x1:1~128,y1:1~8)// Description : ??BMP??void OLED_DrawBMP(unsigned char x0,unsigned char y0,unsigned char x1,unsigned char y1,unsigned char BMP[]){ unsigned int j=0; unsigned char x,y; if(y1%8==0) y = y1/8; else y = y1/8 + 1; for(y=y0;y<y1;y++) { OLED_SetPos(x0,y); for(x=x0;x<x1;x++) { WriteDat(BMP[j++]); } }} void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 Char_Size){ unsigned char c=0,i=0; c=chr-' ';//??????? if(x>128-1){x=0;y=y+2;} if(Char_Size ==16) { OLED_SetPos(x,y); for(i=0;i<8;i++) WriteDat(F8X16[c*16+i]); OLED_SetPos(x,y+1); for(i=0;i<8;i++) WriteDat(F8X16[c*16+i+8]); } else { OLED_SetPos(x,y); for(i=0;i<6;i++) WriteDat(F6x8[c][i]); }}u32 oled_pow(u8 m,u8 n){ u32 result=1; while(n--)result*=m; return result;} //??2???//x,y :???? //len :?????//size:????//mode:?? 0,????;1,????//num:??(0~4294967295); void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size2){ u8 t,temp; u8 enshow=0; for(t=0;t<len;t++) { temp=(num/oled_pow(10,len-t-1))%10; if(enshow==0&&t<(len-1)) { if(temp==0) { OLED_ShowChar(x+(size2/2)*t,y,' ',size2); continue; }else enshow=1; } OLED_ShowChar(x+(size2/2)*t,y,temp+'0',size2); }} UART代碼:
常規(guī)的編寫如上,但是本人的MCU存在問題,單片機并未接收到預設的數(shù)據(jù)。 所以,本人項目中采用了下方代碼: #include 'uart.h' uint8_t USART1_RX_BUF[USART1_REC_LEN];//????,??USART_REC_LEN???.uint16_t USART1_RX_STA=0;//??????//bit15:??????,bit14~0:??????????uint8_t USART1_NewData;//?????????1???????? extern int flag; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//????????{ if(huart ==&huart1) { USART1_RX_BUF[USART1_RX_STA&0X7FFF]=USART1_NewData; USART1_RX_STA++; //???????1 if(USART1_RX_STA>(USART1_REC_LEN-1))USART1_RX_STA=0;//??????,?????? if(USART1_RX_BUF[USART1_RX_STA-4] == 0xA0) { flag = 1; } if(USART1_RX_BUF[USART1_RX_STA-4] == 0x90) { flag = 2; } if(USART1_RX_BUF[USART1_RX_STA-4] == 0xD0) { flag = 3; } if(USART1_RX_BUF[USART1_RX_STA-4] == 0x88) { flag = 4; } if(USART1_RX_BUF[USART1_RX_STA-4] == 0x48) { flag = 5; } HAL_UART_Receive_IT(&huart1,(uint8_t *)&USART1_NewData,1); }} 如果大家自己使用的花,可以根據(jù)自己的藍牙APP寫這段程序,有問題歡迎留言 Motor代碼:
PID: PID算法: PID分為位置型和增量型 增量型即通過 u(k)-u(k-1) 從而得出式子: 公式的第一部分是比例式 是為了讓值按一定比例達到目標值; 第二部分是積分值,正值,在計算的過程中往往會受到環(huán)境等一些其他因素的影響,導致值不能到達目標值; 第三部分是微分值,通常是負值,后一次偏差值往往小于前一次偏差值,目的是為了防止值增加過大,通常起一個阻礙的作用; ![]() PID代碼: #include 'pid.h'#include 'tim.h'#include 'main.h'#include 'math.h'#include 'i2c.h'#include 'oled.h' unsigned int MotorSpeed; //è???±?á?£?μ??úμ±?°×a?ùint SpeedTarget = 750; //??±ê×a?ùint MotorOutput; //μ??úê?3? //1.à?ó?TIM2????μ??ú×a?ù void GetMotorSpeed(void){// int CaptureNumber = (short)__HAL_TIM_GET_COUNTER(&htim2); //HAL?aoˉêy??????3?′?êy// // //μ??ú×a?ù×a??Speed=1s?úμ???3?êy/44(ò?è|11??D?o?£?4±??μ·¨)/34???ù±è// int MotorSpeed=CaptureNumber*20/44/34*2*3.14*3;// OLED_ShowNum(40,0,MotorSpeed,4,16);// // __HAL_TIM_GET_COUNTER(&htim2) = 0; //??êy?÷??á? int CaptureNumber = (short)__HAL_TIM_GET_COUNTER(&htim2); //??????? __HAL_TIM_GET_COUNTER(&htim2) = 0;// int Speed=CaptureNumber*5/44/34*2*3.14*3; int Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2); if(Direction == 1) { CaptureNumber -= 65535; } MotorSpeed=CaptureNumber; OLED_ShowNum(40,0,MotorSpeed,4,16); HAL_Delay(100); OLED_CLS();// __HAL_TIM_GET_COUNTER(&htim2) = 0;} //2.??á?ê?PID?????÷£¨PID3£??·??a????PIDoí??á?ê?PID£? int Error_Last,Error_Prev; //é?′??ó2?£?é?é?′??ó2?int Pwm_add,Pwm; //PWM??á?,PWM????±è int Kp = 5, Ki = 3, Kd = 1;//PID??·¨?μêy£???μ?ààDí£?D????????üá|ò?°?ê±?¨òé??Dí£??ò??*1024 int SpeedInnerControl(int Speed,int Target) //?ù?è?ú?·????{ int Error = Target - Speed; //?ó2? = ??±ê?ù?è - êμ?ê?ù?è Pwm_add = Kp * (Error - Error_Last) + //±èày Ki * Error + //?y·? Kd * (Error - 2.0f * Error_Last + Error_Prev); //?¢·? Pwm += Pwm_add; //ê?3?á?=?-ê?á?+??á? Error_Prev = Error_Last; //±£′?é?é?′??ó2? Error_Last = Error; //±£′?é?′??ó2? if(Pwm > 4999) Pwm = 3000; //?T??é????T£?·à?1PWM3?3?á?3ì if(Pwm <-4999) Pwm =-3000; return Pwm; //·μ??ê?3??μ} //3.μ??ú×a?ùó?·??òμ?oˉêy£¨PID????£? void SetMotorVoltageAndDirection(int Pwm){ if(Pwm < 0) //è?1?PWMD?óú0 { HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET); Pwm = (-Pwm); //PWM???üè??y?μ£?è?1??a?oêy£??±?óè?·′ __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, Pwm); //PWMμ÷?ù } else { HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, Pwm); //PWMμ÷?ù }} void ModePID(){ GetMotorSpeed(); MotorOutput = SpeedInnerControl(MotorSpeed,SpeedTarget); SetMotorVoltageAndDirection(MotorOutput);} 主函數(shù)代碼:
藍牙APP源代碼以及技術論文:鏈接: |
|