UAV021(六):系统架构优化、SBUS协议、遥控器控制电机转动
目錄
- 序
- 一、系統(tǒng)架構(gòu)優(yōu)化
- 1.1 從全局變量到API函數(shù)
- 1.2 函數(shù)傳值
- 二、SBUS協(xié)議讀取及解析
- 2.1 協(xié)議格式
- 2.2 協(xié)議解析
- 2.3 程序設(shè)計(jì)
- 2.3.1 頭文件一覽
- 2.3.2 串口配置與捕獲解析Sbus幀
- 2.2.3 數(shù)據(jù)解析細(xì)節(jié)
- 2.3.4 遙控器校準(zhǔn)與測(cè)試
- 2.4 測(cè)試效果
- 2.4.1 協(xié)議采集與解析
- 2.4.2 遙控器控制電機(jī)
序
系統(tǒng)架構(gòu)優(yōu)化部分主要實(shí)現(xiàn)設(shè)計(jì)API代替直接引用全局變量。
Sbus協(xié)議是遙控器常用協(xié)議,此文將實(shí)現(xiàn)讀取并解析協(xié)議內(nèi)容。
作為測(cè)試,使用遙控器油門(mén)控制PWM,調(diào)節(jié)電機(jī)轉(zhuǎn)速。
一、系統(tǒng)架構(gòu)優(yōu)化
1.1 從全局變量到API函數(shù)
之前實(shí)現(xiàn)的程序里定義了幾個(gè)全局變量,例如全局的時(shí)間計(jì)時(shí) tim,姿態(tài)角結(jié)構(gòu)體 atti 等。全局變量會(huì)增強(qiáng)文件之間的關(guān)聯(lián)性,定義、聲明、賦值、應(yīng)用可能在不同的文件,使得變量難以管理。對(duì)此,改用API接口的形式,一個(gè)簡(jiǎn)單的例子如下:
在 timer.c 里實(shí)現(xiàn)一個(gè)每 0.1ms 加1的變量 tim。之前的做法是把此變量作為全局變量使用,也即在 timer.c 里定義并賦值,在 timer.h 里使用 extern 關(guān)鍵詞說(shuō)明,其他文件只要包含 timer.h 即可使用此變量。下面分別是 timer.h, timer.c 和 attitude.c 里面定義和使用 tim 的情況:
// timer.h extern uint32_t tim; // timer.c uint32_t tim; void TIM6_DAC_IRQHandler(void) {if(__HAL_TIM_GET_IT_SOURCE(&TIM6_Handler, TIM_IT_UPDATE) !=RESET){__HAL_TIM_CLEAR_IT(&TIM6_Handler, TIM_IT_UPDATE); // 清除中斷標(biāo)志位}tim ++; // 又是0.1ms,全局時(shí)間計(jì)數(shù)加1 } // attitude.c #include "timer.h"Ts = (float)(tim - last_t) / 10.0 / 1000.0; // 0.1ms加1 = 1/10/1000s加1last_t = tim; // 更新時(shí)間這種方式有一定缺點(diǎn),一是只讀到 attitude.c 突然給 last_t 賦值 tim 容易讓人迷惑,可能需要跳轉(zhuǎn)到定義后才發(fā)現(xiàn)原來(lái)這是一個(gè)全局變量。對(duì)于其他一些變量,我們還想追蹤在哪里賦值的,這將讓問(wèn)題更加復(fù)雜。二是在 attitude.c 里,我們?nèi)匀豢梢孕薷?tim 的值,這會(huì)讓變量變得不安全。還有要避免命名重復(fù),不然會(huì)讓人腦闊疼的。
使用API函數(shù)后,可以有效解決上面的問(wèn)題。
我們?cè)僭?timer.c 里定義一個(gè)函數(shù) GetTimeApi(),當(dāng)然,在頭文件里聲明:
// timer.h uint32_t GetTimeApi(void); // timer.cuint32 tim_;// 定時(shí)器6中斷服務(wù)函數(shù) void TIM6_DAC_IRQHandler(void) {if(__HAL_TIM_GET_IT_SOURCE(&TIM6_Handler, TIM_IT_UPDATE) !=RESET){__HAL_TIM_CLEAR_IT(&TIM6_Handler, TIM_IT_UPDATE); // 清除中斷標(biāo)志位}tim_ ++; // 又是0.1ms,全局時(shí)間計(jì)數(shù)加1 }/* 獲取全局時(shí)間接口 */ uint32_t GetTimeApi(void) {return tim_; } // attitude.c #include "timer.h"Ts = (float)(GetTimeApi() - last_t) / 10.0 / 1000.0; // 0.1ms加1 = 1/10/1000s加1last_t = GetTimeApi(); // 更新時(shí)間做兩個(gè)小小的約定,提供全局變量的函數(shù)以 Api 結(jié)束,全局變量以 下劃線結(jié)束。
此時(shí),使用函數(shù)的方式代替變量,程序可讀性、安全性和獨(dú)立性都增強(qiáng),是一個(gè)不錯(cuò)的選擇。
1.2 函數(shù)傳值
注意函數(shù)的定義問(wèn)題,以姿態(tài)結(jié)構(gòu)體的傳遞為例,比較以下兩個(gè)函數(shù):
struct ATTI_t atti_; /* 獲取姿態(tài)接口 */ void GetAttiApi(struct ATTI_t *atti) {atti->theta = atti_.theta;atti->phi = atti_.phi;atti->psi = atti_.psi; } struct ATTI_t atti_; /* 獲取姿態(tài)接口 */ void GetAttiApi(struct ATTI_t atti) {atti.theta = atti_.theta;atti.phi = atti_.phi;atti.psi = atti_.psi; }我們希望的是傳入GetAttiApi() 函數(shù)的結(jié)構(gòu)體變量 atti 能夠獲取真實(shí)姿態(tài) atti_ 的數(shù)據(jù)。第一種定義,使用指針的方式是有效的;第二種定義無(wú)效,atti 作為形參,函數(shù)調(diào)用結(jié)束后即被釋放,不能達(dá)到預(yù)期效果。
因此,一般我們都采用指針來(lái)傳值。數(shù)組和指針有一樣的效果,因?yàn)閿?shù)組名就是指向該數(shù)組第一個(gè)數(shù)值得指針,以下兩段程序是等價(jià)的:
/* 獲取三軸加速度接口 */ void GetAccelDataApi(float acc[3]) {acc[0] = acc_[0];acc[1] = acc_[1];acc[2] = acc_[2]; } /* 獲取三軸加速度接口 */ void GetAccelDataApi(float *acc) {acc[0] = acc_[0];acc[1] = acc_[1];acc[2] = acc_[2]; }二、SBUS協(xié)議讀取及解析
2.1 協(xié)議格式
協(xié)議幀很簡(jiǎn)潔,一幀包括25字節(jié)數(shù)據(jù):
首部(1字節(jié))+ 數(shù)據(jù)(22字節(jié))+ 標(biāo)志位(1字節(jié))+ 結(jié)束符(1字節(jié))
這里容易有一個(gè)思維定勢(shì),就是里面的22個(gè)數(shù)據(jù)是從頭到尾每11位作為一個(gè)通道的。認(rèn)真看協(xié)議解析容易發(fā)現(xiàn)剛好是相反的(吐槽ing),是從尾到頭每11位放一塊。請(qǐng)看這張經(jīng)典圖片(全網(wǎng)幾乎只此一張):
并不是第一個(gè)字節(jié)與第二個(gè)字節(jié)的高三位組合在一起,而是與低三位。其實(shí),反過(guò)來(lái)看就對(duì)勁了:
第三個(gè)字節(jié)的12被拿走了,于是有345678,不夠從第二個(gè)字節(jié)拿,又拿了12345;
第二個(gè)字節(jié)被拿走了黃色的,只剩 678了,沒(méi)有了繼續(xù)從第一個(gè)字節(jié)拿,拿到了12345678。
2.2 協(xié)議解析
整個(gè)協(xié)議可用串口進(jìn)行解析:
8位數(shù)據(jù) 2位停止位 1位校驗(yàn)位 波特率100kHz這個(gè)100kHz是非標(biāo)準(zhǔn)的,一般的串口助手不支持,只能解析出來(lái)之后再使用串口打印(吐槽ing)。
Sbus協(xié)議里,使用TTL電平,高電平(3.3V)代表邏輯 ‘0’,低電平代表邏輯 ‘1’,邏輯反了無(wú)所謂,取個(gè)反不就可以嗎?還真不可以。雖然網(wǎng)上都說(shuō)要硬件取反,還是抱著僥幸心理試一試,果然不行。非要硬件取反一下,一般的接收機(jī)也不帶這功能,簡(jiǎn)直是個(gè)設(shè)計(jì)bug(吐槽ing)。
硬件取反電路如下,實(shí)際上就是一個(gè)很簡(jiǎn)單的三極管電路。Sbus的信號(hào)從基極輸入,從集電極輸出。基極輸入 ‘0’,集電極上拉輸出 ‘1’;基極輸入 ‘1’,三極管導(dǎo)通,輸出被拉低為 ‘0’,實(shí)現(xiàn)了反向。
不過(guò)為什么軟件直接取反不行呢?還是沒(méi)有想清楚,目前個(gè)人理解是接收機(jī)輸出的驅(qū)動(dòng)不足,或者和單片機(jī)引腳電阻不匹配?只能通過(guò)通過(guò)三極管放大來(lái)驅(qū)動(dòng)引腳?暫不猜測(cè),繼續(xù)往下。總之吐槽了三次,覺(jué)得這個(gè)協(xié)議沒(méi)有多少人性化的地方,它的成功或許是靠著強(qiáng)大的商業(yè)資本吧。
知道了規(guī)則,便可以使用串口進(jìn)行解析了,請(qǐng)看程序。
2.3 程序設(shè)計(jì)
2.3.1 頭文件一覽
先看頭文件,可見(jiàn)此文件主要功能:
包括兩個(gè)宏定義、兩個(gè)結(jié)構(gòu)體,SBUS_t 用于存儲(chǔ)一幀數(shù)據(jù),MC6C_t 專門(mén)針對(duì) MC6C遙控器,僅六通道數(shù)據(jù)。
后面還有 SBUS硬件初始化,也即配置串口2的函數(shù);Sbus協(xié)議解析任務(wù);遙控器校準(zhǔn)函數(shù),也即將遙控?cái)?shù)據(jù)映射到想要區(qū)間;測(cè)試遙控器控制電機(jī)任務(wù)。
最后是兩個(gè)API函數(shù),向外提供遙控器數(shù)據(jù)。
2.3.2 串口配置與捕獲解析Sbus幀
之前使用了 USART1,用于調(diào)試打印,此處使用 USART2。
串口配置流程如下:
2.2.3 數(shù)據(jù)解析細(xì)節(jié)
注意到 SbusParseTask() 里面延時(shí)的位置。正常的思維是放在while(1) 的最后一行,也即 if else外,此處就不行了(為此冥思苦想了幾個(gè)小時(shí),排除各種可能,偶然解決問(wèn)題后才想通)。
如果把延時(shí)放在最后 if else外,邏輯是這樣的:解析完此幀后,使能中斷,這個(gè)函數(shù)還在延時(shí)100ms的路途中,接收機(jī)的數(shù)據(jù)蜂擁而至,一直發(fā)一直發(fā),不出bug就不正常了。
但是把這100ms放在 if else內(nèi)的開(kāi)啟中斷和接收下一幀前,不過(guò)這 100ms,打死也進(jìn)不來(lái)中斷的,實(shí)現(xiàn)了解析完一幀,休息一下,再解析下一幀的目的,這是我們預(yù)期的效果。
2.3.4 遙控器校準(zhǔn)與測(cè)試
上面的內(nèi)容以及成功獲取遙控器指令,存儲(chǔ)在 sbus_ 結(jié)構(gòu)體之中。比如油門(mén)(第三通道)數(shù)據(jù),取值可能在196 ~1289之間。這個(gè)數(shù)據(jù)不能直接使用,現(xiàn)在我們希望將這個(gè)數(shù)據(jù)轉(zhuǎn)化在 400 ~ 800(程序里有解釋)之間,用于直接調(diào)節(jié)電機(jī)占空比。因此我們需要做一個(gè)線性變化,將油門(mén)的數(shù)據(jù)變化到我們想要的區(qū)間。其他通道亦如此,暫且習(xí)慣性地叫做“校準(zhǔn)”吧。
除此之外,我們?cè)O(shè)計(jì)遙控器控制電機(jī)的任務(wù),也即讀取遙控器油門(mén)數(shù)據(jù),轉(zhuǎn)化為PWM波控制電機(jī)轉(zhuǎn)速。兩個(gè)API接口函數(shù)也在此,不再贅述。
// sbus.c 遙控器校準(zhǔn)與控制電機(jī)測(cè)試部分 /* CH1 -- 俯仰角, 歸中0°, 最大最小 ±30° CH2 -- 滾轉(zhuǎn)角, 歸中0°, 最大最小 ±30° CH3 -- 油門(mén), 歸中0°, 最大設(shè)置占空比 80%, 最小設(shè)置占空比 40%, 電調(diào)驅(qū)動(dòng)頻率為400Hz=2.5ms, 40%=1ms, 80%=2ms CH4 -- 偏航角角速度, 歸中0°/s, 最大最小 ±36°/s CH5 -- 檔位, 上中下分別為 1, 2, 3三檔 CH6 -- 檔位, 上下分別為 1, 2兩檔 */void CaliMc6cData(struct MC6C_t *mc6c) {static const float mc6c_min[6] = {64, 174, 196, 129, 193, 200}; // 轉(zhuǎn)動(dòng)搖桿,各通道最小值,本為 uint16_t,為方便計(jì)算直接為 floatstatic const float mc6c_max[6] = {1812, 1800, 1289, 1833, 1973, 1544}; // 轉(zhuǎn)動(dòng)搖桿,各通道最大值static const float mc6c_mid[6] = {1030, 1001, 489, 948, 996, 200}; // CH6 只有兩通道float k;float b;// CH1 映射, [64, 894] --> [-30, 0]; [894, 1812] --> [0 30]if (mc6c->ail < mc6c_mid[0]){k = (0 - (-30)) / (mc6c_mid[0] - mc6c_min[0]);b = 0 - k * mc6c_mid[0];mc6c->ail = k * mc6c->ail + b;}else{k = (30 - 0) / (mc6c_max[0] - mc6c_mid[0]);b = 0 - k * mc6c_mid[0];mc6c->ail = k * mc6c->ail + b;}// CH2 映射, [174, 1001] --> [-30, 0]; [1001, 1800] --> [0 30]if (mc6c->ele < mc6c_mid[1]){k = (0 - (-30)) / (mc6c_mid[1] - mc6c_min[1]);b = 0 - k * mc6c_mid[1];mc6c->ele = k * mc6c->ele + b; }else{k = (30 - 0) / (mc6c_max[1] - mc6c_mid[1]);b = 0 - k * mc6c_mid[1];mc6c->ele = k * mc6c->ele + b;} // CH3 映射, [196, 1289] --> [400, 800]if (mc6c->thr < mc6c_min[2])mc6c->thr = mc6c_min[2];else if (mc6c->thr > mc6c_max[2])mc6c->thr = mc6c_max[2];else{k = (800 - 400) / (mc6c_max[2] - mc6c_min[2]);b = 400 - k * mc6c_min[2];mc6c->thr = k * mc6c->thr + b;}// CH4 映射,[129, 948] --> [-36, 0]; [948, 1833] --> [0, 36]if (mc6c->rud < mc6c_mid[3]){k = (0 - (-36)) / (mc6c_mid[3] - mc6c_min[3]);b = 0 - k * mc6c_mid[3];mc6c->rud = k * mc6c->rud + b;}else{k = (36 - 0) / (mc6c_max[3] - mc6c_mid[3]);b = 0 - k * mc6c_mid[3];mc6c->rud = k * mc6c->rud + b;}// CH5 映射,得到三檔分別賦值 1,2,3if (mc6c->ch5 < (mc6c_min[4] + mc6c_mid[4]) / 2)mc6c->ch5 = 1;else if (mc6c->ch5 < (mc6c_mid[4] + mc6c_max[4]) / 2)mc6c->ch5 = 2;elsemc6c->ch5 = 3;// CH6 映射,得到兩檔分別賦值 1,2if (mc6c->ch6 < (mc6c_min[5] + mc6c_max[5]) / 2)mc6c->ch6 = 1;elsemc6c->ch6 = 2; }/* MC6C遙控器數(shù)據(jù)接口 */ /* 依賴SbusParseTask()任務(wù) */void GetMc6cDataApi(struct MC6C_t *mc6c) {mc6c->ail = (float)sbus_.ch[0];mc6c->ele = (float)sbus_.ch[1];mc6c->thr = (float)sbus_.ch[2];mc6c->rud = (float)sbus_.ch[3];mc6c->ch5 = sbus_.ch[4];mc6c->ch6 = sbus_.ch[5];CaliMc6cData(mc6c); }/* 獲取遙控器數(shù)據(jù)接口 */ /* 目前未使用此函數(shù) */ void GetSbusDataApi(struct SBUS_t *sbus) {sbus->head = sbus_.head;sbus->flag = sbus_.flag;sbus->end = sbus_.end ;sbus->ch[0] = sbus_.ch[0]; sbus->ch[1] = sbus_.ch[1]; sbus->ch[2] = sbus_.ch[2]; sbus->ch[3] = sbus_.ch[3]; sbus->ch[4] = sbus_.ch[4]; sbus->ch[5] = sbus_.ch[5]; sbus->ch[6] = sbus_.ch[6]; sbus->ch[7] = sbus_.ch[7]; sbus->ch[8] = sbus_.ch[8];sbus->ch[9] = sbus_.ch[9]; sbus->ch[10] = sbus_.ch[10];sbus->ch[11] = sbus_.ch[11];sbus->ch[12] = sbus_.ch[12];sbus->ch[13] = sbus_.ch[13];sbus->ch[14] = sbus_.ch[14];sbus->ch[15] = sbus_.ch[15]; }/* 遙控器控制電機(jī)測(cè)試 */ /* 遙控器油門(mén)將調(diào)節(jié)電機(jī)占空比 */ void TestCtrlMotorTask(void *arg) {struct MC6C_t mc6c;while (1){GetMc6cDataApi(&mc6c);SetMotorDutyApi(MOTOR1, (uint16_t)mc6c.thr); // MOTOR1, 引腳為T(mén)IM3 CH1, PB4SetMotorDutyApi(MOTOR2, (uint16_t)mc6c.thr); // MOTOR2, 引腳為T(mén)IM3 CH2, PB5SetMotorDutyApi(MOTOR3, (uint16_t)mc6c.thr); // MOTOR3, 引腳為T(mén)IM3 CH3, PB0SetMotorDutyApi(MOTOR4, (uint16_t)mc6c.thr); // MOTOR4, 引腳為T(mén)IM3 CH4, PB1delay_ms(200);} }2.4 測(cè)試效果
2.4.1 協(xié)議采集與解析
運(yùn)行結(jié)果如下,正常時(shí),head為0x0F,flag為0x00,end為0x00。
2.4.2 遙控器控制電機(jī)
現(xiàn)在已經(jīng)開(kāi)啟四路電機(jī)輸出PWM波,隨便測(cè)一路即可,輸入捕獲也開(kāi)啟,遙控器也接上,引腳如下:
PWM1 -- PB4 PWM2 -- PB5 PWM3 -- PB0 PWM4 -- PB1 CAP -- PA0 SBUS_TX -- PA3注意:
有一個(gè)問(wèn)題,依舊沒(méi)有解決,遙控器油門(mén)通道總是有跳變,其他通道正常,一直沒(méi)找到bug,如果遇到相同問(wèn)題或解決辦法,討論區(qū)見(jiàn)。
此時(shí)接收Sbus協(xié)議幀沒(méi)有使用DMA,后期將優(yōu)化。
完整工程源程序下載需積分:https://download.csdn.net/download/weixin_41869763/13054663
— 完 —
總結(jié)
以上是生活随笔為你收集整理的UAV021(六):系统架构优化、SBUS协议、遥控器控制电机转动的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: cesium 页面截图_Cesium开发
- 下一篇: nyoj 71 独木舟上的旅行 贪心