自定義的通信協議
- 自定義一主多從串口通訊_1
- 硬體基礎
- 兩個從機的 Tx 是相互連接的,導致一個從機在需要發送資料時發不出去了
- 協議思路
- 資料包封裝和解封裝
- 樹莓派python串口的使用注意
- 更改樹莓派串口設備驅動
- 關閉控制臺功能
- python串口的使用
- 通訊應答測驗
自定義一主多從串口通訊_1
這是一個類似開發日志的小記錄,這篇主要是記錄自己在畢設里自定義的一種基于串口通訊的通訊協議,至于為什么沒有使用modbus只是因為我沒有用過,
硬體基礎
我的畢設大體上需要通訊的主要有三個模塊分別是:樹莓派當做主要的控制板、傳感器采集板、動力驅動板,后面兩個是自己用stm32f103rct6做的板子,以后有機會我也會展示一下
在連接上,我們知道樹莓派只有一個串口是引腳引出的,所以我希望可以通過一個串口同時控制兩個stm32的板子,所以一主多從的自定義串口通訊協議想法就這樣出來了,
首先要解決連線問題:我們知道兩個設備的串口在連線時Rx、Tx是需要反接的,但是在兩個從機的時候就需要解決一個小問題:
兩個從機的 Tx 是相互連接的,導致一個從機在需要發送資料時發不出去了
通過查資料我們知道,想mcu的串口都是以低電平為有效值,所以問題這就轉換成為 當從機1的Tx變為低電平時如何能不受從機2 Tx高電平的影響,
顯而易見,用一個二極管應該就可以解決,我直接上框圖應該就清楚了:
當從機1 發送資料時Tx低電平會拉低主機Rx,同時從機2 Tx保持高電平不影響通訊,

協議思路
我的協議主要包含以下6個內容:
- 起始標志:
我這里是直接用 '>' 的值來表示 - 目標地址:
一共有三個模塊所以地址也有三個
enum ModuleAddrees {
kRaspberryAddr = 0x1,
kChuanGanQiAddr = 0x2,
kQuDongBanAddr = 0x3,
kReseve1 = 0x4,
kReseve2 = 0x5,
kReseve3 = 0x6,
kReseve4 = 0x7,
}; ///< 標記模塊地址
- 訊息型別:
用來標記當前資料包屬于那種資料型別,目前只想到了以下幾個
enum DataTypeId {
kAck_dti = 0x0,
kYaw_dti = 0x1,
kCio_dti = 0x2,
kAdc_dti = 0x3,
kEncoder_dti = 0x4,
kMoto_dti = 0x5,
kServe_dti = 0x6,
kPower_dti = 0x7,
kReseveLast_dti = 0x1f, //0001 1111
}; ///< 標記資料型別
- 內容長度 :
就是簡單記錄訊息內容的長度,主要是想用來校驗,后來好像沒用上,太懶了直接沒寫校驗 - 內容:
就是要攜帶的主要內容 - 結束標志:
慣用換行組合 “\r\n”,對應陣列就是[13 10]
資料包封裝和解封裝
基本上用一個陣列把上面的內容全部接下來就可以了,不過我為了能少一個byte把目標地址和訊息型別的值通過移位放到了一個8位資料里了(高3位的bit表示目標地址, 低5位的bit表示訊息型別),我就直接上代碼了,這樣簡單直接
/**
* @brief 封裝資料包
* @param package 資料包
* @param detAddr 目標模塊地址
* @param dti 資料型別標號
* @param len 資料長度
* @param srcData 原始資料(組)
* @return 資料包長度, 構造失敗回傳-1
*/
int MES_PackageData(char *package, enum ModuleAddrees detAddr, enum DataTypeId dti,
unsigned char len, void *srcData)
{
int i;
int result = -1;
char *p = package;
unsigned char addrTid = (((unsigned char)detAddr) << 5) | ((unsigned char)dti);
// 起始標志 ‘>’
*p = '>';
p++;
// 模塊號(3bit)+資料型別號(5bit)
*p = addrTid;
p++;
// 資料(組)長度,單位(byte)
*p = len;
p++;
// 資料(組)
for (i = 0; i < len; i++, p++) {
*p = ((char *)srcData)[i];
}
// 結束標志 “\r\n”
*p = '\r'; p++; *p = '\n';
// 計算包長度
result = p - package + 1;
return result;
}
/**
* @brief 拆解資料包
* @param package 資料包
* @param selfAddr 本機模塊號
* @param res 獲得資料
* @param dti 資料型別編號
* @return 資料長度
*/
int MES_UnpackageData(char *package, enum ModuleAddrees selfAddr,
char *res, enum DataTypeId *dti)
{
int result = -1;
char *p = package;
unsigned char temp;
int i;
// 判斷起始標志
if (*p != '>') {
return result;
}
p++;
// 判斷模塊號(3bit)+獲取資料型別號(5bit)
temp = *p;
if ((temp >> 5) != (unsigned char)selfAddr) {
return result;
}
*dti = temp & 0x1f;
p++;
// 獲取資料
result = *p;
p++;
for (i = 0; i < result; i++, p++) {
res[i] = *p;
}
return result;
}
以上是stm32從機的代碼,下面是在樹莓派上寫的python代碼
def __packData(self, detAddr = ModuleAddrees.kNone,
dti = DataTypeId.kAdc_dti,
len = 0, srcData = np.zeros(0,dtype = np.uint8)):
'''
:breif 打包資料
:param detAddr: 目標地址
:param dti: 資料型別
:param len: 引數長度
:param srcData: 引數串列
:return: 完整資料包 np.ndarray
'''
pack = np.zeros(len + 5, dtype=np.uint8)
pack[0] = ord('>') # begin flag
pack[1] = np.uint8(detAddr << 5) | np.uint8(dti & 0x1f)
pack[2] = np.uint8(len)
pack[-2] = ord('\r')
pack[-1] = ord('\n')
i = 0
while i < len:
pack[i+3] = np.uint8(srcData[i])
i += 1
return pack
def __Unpackage(self, pack = '', len = 0):
'''
解開資料包
:param pack:
:param len:
:return: 內容 長度 型別
'''
pck = np.array(list(pack), dtype=np.uint8)
if pack.__len__() < 6:
dataContent = []
dataType = -1
dataSize = -1
return (dataContent, dataSize, dataType)
dataContent = pck[3:-2]
dataSize = pck[2]
dataType = DataTypeId(pck[1] & 0x1f)
return (dataContent, dataSize, dataType)
'''
這兩函式是一個類方法,不能直接用,能看出來流程就行,
到這我的協議內容基本就結束了,后面主要是記錄在第一次使用樹莓派串口時候要做的作業和注意事項
'''
樹莓派python串口的使用注意
更改樹莓派串口設備驅動
我用的是樹莓派4b,芯片內部有兩個串口:一個稱之為硬體串口(/dev/ttyAMA0),一個稱之為mini串口(/dev/ttyS0),兩個區別是ttyAMA0是外設串口有單獨的時鐘,而ttyS0是簡易的串口由內核提供時鐘,所以AMA0會相對穩定,ttyS0會受到內核時鐘的影響,
通過指令ls -l /dev ,可以看出目前引腳引出的串口是ttyS0,所以第一步的作業就是要修改這個映射關系

參考博客:修改樹莓派串口設備映射
主要內容是找到相關檔案 我這里是 “/boot/overlays/miniuart-bt.dtbo” ,不同版本系統多少會有點不一樣

然后編輯 /boot/config.txt
在該檔案中增加一行代碼 dtoverlay=“對應的檔案名”

然后保存檔案,重啟樹莓派使之生效,
在此查看設備,對應關系就改過來了

關閉控制臺功能
起初為了能通過串口進入系統,打開了控制臺功能,這里需要關閉,兩個指令即可
sudo systemctl stop serial-getty@ttyAMA0.service
sudo systemctl disable serial-getty@ttyAMA0.service
我看的博客都有講到還需要修改 /boot/cmdline.txt
把里面關于串口的內容都刪掉,但是我發現我的里面直接就已經沒有了,所以有需要的轉置上面的超鏈接
再次重啟就可以正常使用了
python串口的使用
我沒有使用WiringPi庫,而是直接用的PySerial,在邊學邊用的程序中發現,python的串口資料在決議的時候不像C語言的那么方便(應該是我太菜了),我總是在糾結python如何對資料進行宣告,如何對資料強制轉換,
目前為止,我發現通過串口發送陣列型別是相對方便的所以在python中我的串口資料包都是以陣列的形式進行傳遞(dtype=np.uint8),
可能是因為python中萬物皆物件的概念導致我沒辦法實作類似C語言中union這樣的相同記憶體不同資料型別的資料結構(應該還是我太菜了)
所以我準備后面用c語言寫個靜態庫,python呼叫庫來實作這樣的功能,希望確保資料的準確
通訊應答測驗
為了方便模擬訊息的應答,從機我是直接插到了串口助手上,這樣我就可以控制從機發送,
測驗思路是這樣的,樹莓派向從機發送應答命令包從機回復,如果樹莓派可以正常發送和接收在控制臺上會有訊息顯示,如果正常即為成功
- 撰寫測驗demo
def __task_enter(self,argm = []):
'''
對接收到的資料進行分析并且做出回應
:return:
'''
lock = threading.Lock()
try:
while self.__TaskIsRun:
# step.1 接收資料包
(self.r_data, self.r_len) = self.ReadLine()
# step.2 決議資料包
lock.acquire()
(cont, size, type) = self.__Unpackage(self.r_data, self.r_len)
lock.release()
# step.3 分析資料
if type == DataTypeId.kAck_dti: #決議應答回復內容
print("get kAck_dti\n")
if cont[0] == ModuleAddrees.kChuanGanQiAddr.value:
self.dataApi.ack_CGQ = 1
self.ipc.cmdAckCGQ.set()
elif cont[0] == ModuleAddrees.kQuDongBanAddr.value:
self.dataApi.ack_QDB = 1
self.ipc.cmdAckQDB.set()
else:
print("ack pack cont error\n")
pass
elif type == DataTypeId.kMoto_dti:
print("get kMoto_dti\n")
pass
continue
except:
print("The thread is end >> " + self.task.getName())
return
if __name__ == "__main__":
prl = SpcProtocol("protocolTest", "/dev/ttyAMA0", 115200)
prl.StartTask()
while True:
key = input("輸入cmd: ")
print(key, end = '\n')
if key == 'ack':
prl.SendCmdAck(ModuleAddrees.kQuDongBanAddr) # 發送應答命令
print("wait for ack\n")
prl.ipc.cmdAckQDB.wait()
print('get it from QuDongBan\n')
continue
-
輸入指令發送并檢查


-
查看控制臺回復內容


另外也測驗了其他錯誤的資料回復包,在設定中的錯誤控制臺也會做出對應的反映,
至此,自定義的通訊協議初步完成了點對點的實驗,明天開始測驗對兩個從機的通訊回應,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/413995.html
標籤:其他
