本文是遷移一年半前我在 https://github.com/junhuanchen/esp-idf-software-serial 項目下寫下的記錄。
ESP-IDF SoftWare Serial
基於該項目 Github Arduino Esp32-SoftwareSerial 。
花了點時間寫了一下軟串口,因為娛樂和工程需要,所以我從過去自己在 Arduino 上實現的軟串口移植到 ESP-IDF 下,為此也寫一周了吧,使用硬件為 Bpi:Uno (esp32)。
更新了一次 esp8266 rtos 用的軟串口,大概只做到了 57600 這個范圍內穩定使用,但開頭總有一兩個字節要出錯,應該是硬件電平上的干擾,持續使用是沒有問題的。
soft_urat_esp8285_57600.c
本模塊的意義是?
大多數國產/傳統的傳感器接口,會采用 9600 的通信協議,而 ESP32/ESP8266 的硬串口很少(其中一個無法進行發送數據),舉例來說,如果我們想要集成了 GRPS 模塊、RS232 模塊、PMX.X 模塊、MicroPython REPL、XX 串口傳感器的模塊,此時怎樣都不夠用,所以軟串口可以解決此問題。
注意不能使用的引腳
在 arduino 里有這樣的定義,NULL 的意味着它無法作為接收引腳,其他的一般都可以作為發送引腳,注意別和硬串口沖突(比如 0 2 16 17 ),不然就是浪費了。
static void (*ISRList[MAX_PIN + 1])() = {
sws_isr_0,
NULL,
sws_isr_2,
NULL,
sws_isr_4,
sws_isr_5,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
sws_isr_12,
sws_isr_13,
sws_isr_14,
sws_isr_15,
sws_isr_16,
sws_isr_17,
sws_isr_18,
sws_isr_19,
NULL,
sws_isr_21,
sws_isr_22,
sws_isr_23,
NULL,
sws_isr_25,
sws_isr_26,
sws_isr_27,
NULL,
NULL,
NULL,
NULL,
sws_isr_32,
sws_isr_33,
sws_isr_34,
sws_isr_35};
ESP32 Unit Test
- 9600 在 ESP-IDF 和 MicroPython 環境下測試完美,其中 0x00 - 0xFF 256 個字節的數據發送與接收均正常,多次測試的結果非常好。
- 57600 在 ESP-IDF 環境下測試和 9600 的效果一致,但是 MicroPython 中多次發送數據后,數據會抖動,需要優化一下每個字節的發送部分的間隔才能改善,另外 0x00 - 0xFF 256 個字節的數據接收正常。
- 115200 在 ESP-IDF 環境下測試收發通信正常,但是在 MicroPython 下無法正常,輕微的 4us 誤差數據抖動,就會導致每次采集數據不准確,也沒有對此添加過采樣(多次采樣選其一),所以需要設定波特率到 136000 才能相對准確(更快的發送,從而忽略掉兩次執行發送間隔的影響,這個部分我想在還需要多加優化才能相對完美)。
所以 esp-idf 中,你可以任意使用 9600 57600 115200 的波特率,但如果發現存在問題,需要去修改 sw_serial.h 中的兩個參數 rx_start_time 和 rx_end_time ,或設置其他波特率。
// suggest max datalen <= 256 and baudRate <= 115200
esp_err_t sw_open(SwSerial *self, uint32_t baudRate)
{
// The oscilloscope told me
self->bitTime = (esp_clk_cpu_freq() / baudRate);
// Rx bit Timing Settings
switch (baudRate)
{
case 115200:
self->rx_start_time = (self->bitTime / 256);
self->rx_end_time = (self->bitTime * 127 / 256);
break;
case 9600:
self->rx_start_time = (self->bitTime / 9);
self->rx_end_time = (self->bitTime * 8 / 9);
break;
default: // tested 57600 len 256
self->rx_start_time = (self->bitTime / 9);
self->rx_end_time = (self->bitTime * 8 / 9);
break;
}
// printf("sw_open %u %d\n", self->rx_start_time, self->rx_end_time);
sw_write(self, 0x00); // Initialization uart link
return sw_enableRx(self, true);
}
而 micropython 中,不超過 57600 都是可以正常使用的,但 115200 只能靠改參數來滿足,比如 115200 改成 137000 可以讓局部數據准確傳輸,通常我們認為完整的數據范圍是 0x00 - 0xFF 之間。
DSView Tool
對於其中的數據傳輸情況,你需要一個邏輯分析儀,例如我使用的是這個 dreamsourcelab 。
比如我下圖做的分析。


MicroPyhton 的效果

harvest
首先我學會了使用邏輯分析儀 :),我可以自己去捕獲數據的情況來分析數據源,分析它的發送和接收都是相對麻煩的事情,但從編程上講,一定是發送要比接收更簡單。
how to do
首先我最早在 Arduino 上使用軟串口,作為軟件出身的,我知道如何進行邏輯分析和拆解,因此我從 Arduino 上分離了邏輯到 ESP-IDF ,但是當我移植完成后完全不能使用,因為這個不同於軟件模塊,遷移之后只需要關系邏輯問題,這個還要結合通信時序來分析問題。
因此,我移植完成后,先審核數據發送接口的邏輯,最先遇到的問題的是 主頻 和 時間周期的關系,經過群里小伙伴的教育后,我才知道 波特率 以及主頻頻率的意義,所謂的 115200 波特率是指 1 * 1000 * 1000 us 下的 bit 數量(可能描述不准確,主要就是量化 bit 的傳輸周期),因此在 1 秒下 115200 波特率的 bit 周期為 8 us , 結合標准的傳輸內容 起始位 數據位 停止位,總共 10 個 bit ,也就是 80 us 一個字節,因此 115200 下傳輸一個字節需要 80 us 左右。
基於此繼續說,所以發送的時候,假設為上升沿觸發后,將會持續 80us 的過程進行字節判定,對方將會捕獲此數據進行協議解析,而沒有邏輯分析儀,你就無法准確判斷,怎么才是正確的數據。
所以我從師弟手上搶掠了一台蘿莉分析儀,先是捕獲 CH340 的發送數據,以確保標准發送的數據源,再結合自己產生的數據做比較,結果才發現,發送的邏輯結構是一樣的,但周期間隔完全不一樣,因此假設邏輯已經正確,消除時序的周期差異,只需要解決差異的倍數就可以了,所以回到 主頻 和 時間的關系,例如 esp32 160 的時候此時 芯片的 1 us 差應該如何獲得,為了能夠創造這個 1 us 的關系,實際上就是假設為單周期的計數器的結果,所以我們可以假設 160 次累加后相當於 1 us,所以 8u 就是放大 8 * 160 的結果,有了這個基准,就可以准確的進行每次數據的發送間隔,在代碼中的 WaitBitTime 和 getCycleCount 就是做這個用途的。
有了周期的基准,也有了邏輯結構,程序的功能已經成型,那么就是核對測試的問題。
本以為有了這一切都已經可以正常發送數據了后。
結果發現硬件存在一點差異化問題,不知為何,第一個字節發送的一定會錯誤,因為分析儀得到的數據中有一個極小的抖動,突然向下跳了一下,導致后續數據混亂,所以先發一個數據,清理掉這個不知道是不是上電帶來的影響(軟解),此后數據一切正常。
接着測試發送 0x00 - 0xFF 的定義域數據,核對邊界,切換 9600 、57600、115200 進行核對,期間沒有什么異常。
進入到接收部分開發,中斷觸發已經確認,但發現,此時邏輯分析儀已經無法派上用場了,因為解析全指望芯片的寄存器和中斷函數不出問題,處理這部分的時候,幸運的結合了 Arduino 的經驗,假設數據源為 CH340 ,選擇起始位的上升沿的 1 / 9 區域作為捕獲上升沿信號的采樣(沒有過采樣),此后依次采樣,然后 停止位的時候 8 / 9 的區域收尾停止位 bit ,此時一幀完成。
這個邏輯在 9600 和 57600 的時候沒有出現太大問題,當 115200 出現后,1 / 9 的比例無法保障 255 字節數據傳輸過程中的執行誤差。(還是因為沒有過采樣XD),所以 115200 的時候,出現了接收錯誤,沒有辦法使用邏輯分析儀,結合代碼邏輯,盡量優化中斷函數的操作,然后確保每次中斷函數的獨占和退出都最小化影響,並調整到 1 / 255 的區間,此時 255 定義域字節數據一切正常,測試完成。
ESP-IDF 開發完成,移植到 MicroPyton 存在的問題。
主要是發送數據的函數間隔和接收數據的其他函數影響,總體來講。
發送函數在 Python 環境中,所以 115200 的時候,數據位的發送過程中與 標准源的誤差去到了 4us ,這意味着可能錯過半個位,因此可以通過設置較高的波特率調快發送的位等待(bit wait time),但接收函數就無法保證了,所以 115200 還存在一些需要深度優化的才能解決的細節問題(比如過采樣XD,也需要測試一下 ESP32 的 IO 翻轉速度)。
problem
目前是半雙工的軟串口,所以你需要一個 CH340 之類的做數據收發測試,注意發送的數據,不能亂發,容易讓電腦藍屏(使用的時候盡量是 ASCII)。
MicroPython 暫時無法使用 115200 的波特率,但你如果是指定的某些數據協議,還是可以通過修改源碼的時序盡可能解決的,但這個做法並不通用。
還需要更長的數據和更大量的數據傳輸來測試,否則也只是消費級的娛樂代碼水准。
result
最后 蘿莉分析儀真是個好東西,來一張全家桶合照。

2020年10月5日 更新內容
由於上述內容是過去的舊文,所以我一般不會按后來的觀念去重新整理了。
我最近更新了一次 esp8285 在 esp-at rtos 下實現 57600 波特率的串口傳輸,嚴格來講,對以前的一些問題有不一樣的認識了,得益於這一年對硬件特性的理解加深了許多。
例如為什么第一個字節會出錯,例如如何進行自適應數據,例如如何過采樣,總得來說,現在的實現手段比過去要更加精准的獲取想要的數據了,也更加的清楚硬串口怎樣做才可以高速率高精度的傳輸了。
為什么第一個字節會出錯
先來問答第一個問題,為什么第一個字節會錯,這是因為 gpio 的通道打開后,電路中存在電容、電阻等元件,會影響 GPIO 的初次電平瞬間不穩定,其實這樣去思考問題就知道為什么了。
如何改進芯片接收端邏輯
這次補全另外兩個接收邏輯的代碼,可以不用像以往那樣要求嚴格的讀取,但要注意每段 gpio 改變電平的語句執行周期可能會到 1us 所以在制作 esp8285 的 115200 時異常難以克服這種誤差,因為 GPIO 的硬件資源工作需要時間,115200 要求 10us 內完成對一個位的判定,也就是延時精度必須控制在 1us 以下才可以精准的采集相應的信號,到了這一層基本上都看不到了,如果不配合硬件測量,軟件只能憑感覺來寫。
軟串口芯片的接收邏輯我放一張圖做說明。

我們看圖可知,實際上只需要芯片在接收的時候能夠在 起始位 到 停止位 之間的 8 個 bit 位被采集出來,但會出現幾個問題。
- 我們一般情況下無法捕捉此時芯片接收期間的所觸發的信號位,間隔太小了,如 115200 在 10us 內准確的確認輸入的電平位。
- 控制 GPIO 的翻轉速度並非能夠一直保持穩定的工作狀態,必然會存在抖動和硬件誤差。
- 控制 GPIO 電平 由高到低 與 由低到高 並不像字面上看到的等效執行周期,嗯,也許字面上上看起來是一樣的,但實際的執行結果並不一樣。
這些誤差就讓通常簡單的軟件邏輯很難在長時間的運行過程中保持絕對不出錯,必須將多次字節可能導致的誤差納入接收邏輯的考慮范疇。
因此我們必須把所有語句和硬件可能的影響帶入考慮,這之后我引入了兩段函數的邏輯來克服真實世界中存在誤差的問題。
在這之前,接收邏輯是以一個字節為單位的,所以起始位的判斷會影響到后續的數據和停止位的判斷,一般我們以觸發沿的三分之一部分為采樣點,這就導致了后續的采樣點均保持同一個間隔進行。
所以我現在改成,等待某個 起始位 或 停止位 輸入的方式進行數據的觸發,如下代碼。
static inline bool IRAM_ATTR wait_bit_state(uint8_t pin, uint8_t state, uint8_t limit)
{
for (uint i = 0; i != limit; i++)
{
// ets_delay_us(1);
WaitBitTime(limit);
if (state == gpio_get_level(pin))
{
return true;
}
}
// ets_delay_us(1);
return false;
}
調用 if (wait_bit_state(self->rxPin, self->invert, self->rx_start_time)) 它的效果是期望 在指定的 limit 時間內 捕獲到 期望的 invert 狀態值 后 返回 真 。
這樣,如果沒有在期望的周期里得到該數據,則說明不存在數據,或者數據溢出,從而靈活的避開了起始位的判斷,與 停止位的固定等待觸發結束。
- 起始位的觸發不再通過固定的跳變周期觸發,從而取消了起始位觸發的區間設置。
- 停止位也可以盡快結束,盡快離開本次中斷,等待下一次的起始位的觸發。
接着是提供一種過采樣的方式,以往我們可以使用多次采樣選擇一次准確的值作為真實值。
static inline uint8_t IRAM_ATTR check_bit_state(uint8_t pin, uint8_t limit)
{
uint8_t flag[2] = { 0 };
for (uint i = 0; i != limit; i++)
{
flag[gpio_get_level(pin)] += 1;
ets_delay_us(1);
}
return flag[0] < flag[1];// flag[0] < flag[1] ? 1 : 0;
}
這個函數的思路就是,准備一個 0 和 1 的緩沖區,進行讀取后直接對索引的緩沖區做加法,最后通過比較大小來返回最大的可能性,表示真正采樣的電平值最多的作為最終理想的結果給回。
不過,這樣的過采樣也存在一些風險,在 esp8285 中低於 10us 的采樣中,要把函數執行周期(如 gpio_get_level)帶來的誤差也要一並考慮,所以在使用的時候要注意過采樣函數的執行時間應小於理論上的時間。
但在這里還會存在新的問題,例如大量連續發送數據后,每個字節的總體數據位的傳輸周期也並非固定且穩定的,如果考慮到真實情況下動態長度的數據接收,目前的解決方法就是通過協議來檢測是否存在緩沖區溢出來消除這種缺陷。
還有很多問題要克服,總之,目前通過這兩個新加的函數,就可以把過去的期望一次周期采樣數據准確的邏輯給強化了,希望在之后的使用中,我會進一步的把問題徹底解決吧,思路就先到這里了。
后記
截至目前,我還未將 115200 的穩定工作在 esp8285 下(可用,但未能通過我的要求&標准),僅是將 57600 20us 的 0x00 ~ 0xFF 在基於 rtos 的 esp-at 下做到穩定通信了,之后有必要我會繼續做的。
除了上面的函數方法,其實還有一些可以微調的參數,就比如 esp8266 的設置 115200 會在硬件傳參的時候相對的提高一些,讓每個位的檢測降低到 8~9us 一個位附近,來克服整體的通信上存在的硬件誤差,當然通過硬件來實現的必然要精准的多,不需要通過語句來交互判斷,自然就不存在執行 gpio 函數過程中帶來的誤差。
現在多結合一些硬件的狀態來考慮問題,就可以寫出比較健壯的代碼了。
2020年10月6日 junhuanchen
