現(xiàn)在很多廠家都支持一線通的通信協(xié)議,控制器呀,儀表呀,BMS呀,通通的提供了一線通的接口,這幾乎成了兩輪車行業(yè)的一個標準了。
第一次看到一線協(xié)議蠻驚訝的,因為這居然是一個國際標準,比如:
似乎每一家的協(xié)議文檔的第一部分,都聲稱自己采用的是國際標準SIF通信協(xié)議。
這么看來,我們不得不了解一下,這個大名鼎鼎的國際標準是個什么標準,哪幾個組織聯(lián)合發(fā)布的。
我?guī)痛蠹宜堰^了,Google搜索記錄一共3頁,除了像上面那樣介紹自家協(xié)議的引用外,關于SIF國際標準的關鍵字都是醫(yī)學,或者視頻行業(yè)的詞條。看來Google也是孤陋寡聞了,GPT也是干瞪眼。
我翻到了自行車電動車協(xié)會發(fā)布的團標,里面引用了一線通,并沒有說明一線通是什么國際標準,只是簡單介紹了其通信架構。
從上面的一線通架構中,可以看出,這個一線通主要是一發(fā)多收的半雙工機制。跟半個Uart一樣。我之前為了方便,也是采用一根線做Uart通信,只不過我的MCU支持Tx和Rx調(diào)換一下,因此可以做到收發(fā)一體。
一線通的具體協(xié)議
相信很多朋友第一次接觸單線通信都是從DS18B20開始的,這是一種單總線結(jié)構,它支持主機發(fā)送查詢命令,然后接收各個子設備的信息。
那么兩輪車的這個一線通呢?其實就是砍掉一半,主機只發(fā)送,不接受,就像串口uart的Tx一樣,自己按照固定的波特率發(fā)送,線上的所有監(jiān)聽者按照協(xié)定的波特率接收。
區(qū)別在于,uart的電平高代表1,電平低代表0,這里指的TTL電平。而一線通該表了這個策略,它使用了固定周期,不同占空比的PWM波形來表示0和1。
這樣抗干擾的能力就強了很多,看上去也很眼熟是不是? 這跟我們常常使用的彩色LED,WS2812的通信非常的相似,下圖是WS2812的時序波形圖。
其實它們都是采用的單極性歸零碼,歸零碼是一種數(shù)字信號編碼方式,在每個時鐘周期內(nèi),信號電平都會返回到零電平。
與WS2812所不同的是,一線通采用的同步機制并不是一長段的低電平,而是一長段的低電平后面跟上一個高電平,這樣有一個好處,就是可以讓通信更健壯,兼容性更佳。
有了同步幀和數(shù)據(jù)幀的定義,然后再把每一個數(shù)據(jù)周期定義下來,我們就可以接收到整個完整的數(shù)據(jù)包了,這里,一線通的數(shù)據(jù)周期定義為12*8bit,也就是在同步幀后,持續(xù)的接收96個bit。
一線通解析方法
網(wǎng)絡上看到很多解析一線通的方法,所示配置一個定時器,不斷地讀取GPIO的電平進行采樣,然后根據(jù)個數(shù)來計算電平的持續(xù)時間,通用性還是可以的,只不過進入中斷的次數(shù)比較多,不過抗干擾會比較好。
我使用的單片機都是有外部中斷的,所以我決定采用中斷配合定時器計時的方法來接收一線通數(shù)據(jù)。只需要定義兩個邊沿都觸發(fā),然后再觸發(fā)中斷里面根據(jù)觸發(fā)源,或者再次讀取一下IO電平就可以判斷出上一次計時器中的計時是高電平還是低電平。
邏輯很簡單,但是寫起來還得費點功夫,我肯定是想一兩分鐘搞定的,畢竟有AI加持了,誰還自己敲代碼呢。我把波形圖和GPIO中斷的函數(shù)喂給了AI。告訴他,將收到的96個數(shù)據(jù)存到一個12字節(jié)的數(shù)組里面。
還記得剛剛說的同步幀嗎? 同步幀后面有一個高電平的。我跟AI說,這個同步幀的高電平其實就是一個單位時間,后面跟來的數(shù)據(jù)幀都是按同步幀中高電平來計算的,這樣做,我們就可以一套代碼適用不同廠家的一線通了,AI絕對的聰明,同時他也肯定了我的理解。
瞅瞅,不單看明白了我的意圖,還給我增加了允許誤差,提高程序的健壯性,代碼也不含糊
voidone_wire_int_process(void)
{
int?curr_level =?HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3);
HAL_TIM_Base_Stop(&g_hTIMx);
uint16_t?pulse_time = __HAL_TIM_GET_COUNTER(&g_hTIMx);
// 重新計時,準備捕獲下一個電平
? ? __HAL_TIM_SET_COUNTER(&g_hTIMx,?0);
HAL_TIM_Base_Start(&g_hTIMx);
if?(curr_level ==?0)
? ? {
? ? ? ? last_high_time = pulse_time;
? ? }
else
? ? {
? ? ? ? last_low_time = pulse_time;
? ? }
// 采集到一對高低電平后,直接解析
if?(last_high_time)
? ? {
if?(state == WAIT_SYNC)
? ? ? ? {
if?((last_low_time > LOW_SYNC_MIN && last_low_time < LOW_SYNC_MAX) &&
? ? ? ? ? ? ? ? (last_high_time > HIGH_SYNC_MIN && last_high_time < HIGH_SYNC_MAX))
? ? ? ? ? ? {
? ? ? ? ? ? ? ? t_unit = last_high_time;
? ? ? ? ? ? ? ? state = RECEIVE_DATA;
? ? ? ? ? ? ? ? bit_idx =?0;
for?(int?i =?0; i <?12; i++)
? ? ? ? ? ? ? ? ? ? data_buf[i] =?0;
? ? ? ? ? ? }
? ? ? ? }
elseif?(state == RECEIVE_DATA)
? ? ? ? {
int?t1 = t_unit;
int?t2 = t_unit *?2;
int?tol1 =?TOLERANCE_INT(t1);
int?tol2 =?TOLERANCE_INT(t2);
// DATA(0): 低2T高1T
if?(abs(last_low_time - t2) < tol2 &&?abs(last_high_time - t1) < tol1)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? data_buf[bit_idx /?8] <<=?1;
? ? ? ? ? ? ? ? bit_idx++;
? ? ? ? ? ? }
// DATA(1): 低1T高2T
elseif?(abs(last_low_time - t1) < tol1 &&?abs(last_high_time - t2) < tol2)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? data_buf[bit_idx /?8] <<=?1;
? ? ? ? ? ? ? ? data_buf[bit_idx /?8] |=?1;
? ? ? ? ? ? ? ? bit_idx++;
? ? ? ? ? ? }
else
? ? ? ? ? ? {
? ? ? ? ? ? ? ? state = WAIT_SYNC;
? ? ? ? ? ? }
if?(bit_idx >= DATA_BITS_TOTAL)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? state = WAIT_SYNC;
// TODO 你可以在這里從數(shù)組獲取數(shù)據(jù)
? ? ? ? ? ? }
? ? ? ? }
// 處理完一對高低電平后,必須立即清零
? ? ? ? last_low_time =?0;
? ? ? ? last_high_time =?0;
? ? }
}
上面是中斷里面的處理函數(shù),很多宏定義和變量初始化的部分,AI會幫我添加到頭文件,我這里就不拷貝了。
Cursor是真的棒,大家一定要多多體驗!