STC8H開發(四): FwLib_STC8 封裝庫的介紹和注意事項

目錄

前面已經介紹了如何在Keil5和PlatformIO環境下使用FwLib_STC8, 展示了ADC數模轉換的例子. 這篇整體介紹一下這個封裝庫, 以及使用這個封裝庫進行開發的注意事項.

為什麼要寫 FwLib_STC8

如果直接用暫存器開發, 在不同的MCU之間切換就會感覺到每次寫都像是第一次寫, 都得去查手冊去計算, 還容易出錯, 費時費力. 把一些先驗知識程式碼化, 就能簡化這個過程, 用一次的時間節省將來無數時間.

寫這個封裝庫的初衷是希望知識和經驗能復用, 避免每次在做STC8G和STC8H的開發時去查手冊, 這個是最主要的動機; 其次是要在復用的情況下還能使程式接近直接操作暫存器的效率, 不能因為引入封裝庫造成明顯的資源開銷.

在 STC89/STC90 這一代, 幾十個SFR還是可以記憶的. 到了STC11, STC12, 開始出現ADC, SPI這些外設, 也還可以接受. 到STC15之後, SFR數量一下子上來, 單單PWM就有十幾個SFR, 單憑記憶就很難記住這些東西了. 並且在STC15之後, 同系列之間差異增加, 每個MCU的運行時鐘都可能不一樣, 從6MHz到40MHz可以自由設定, 就連基礎的定時器和串口設置都帶來了很大的難度.

STC-ISP工具中提供了一些程式碼模板, 但是這些程式碼並非完全可用, 靈活性也不夠, 例如延時方法都是不帶參數的.

早期的嘗試

邏輯程式碼化

在MCS51這個場景是比較尷尬的: 片內資源太少了.

如果你把各種初始化和計算的工作都放到程式碼里, 那麼就會佔用運行資源, 導致韌體體積增大, 運行時耗費的記憶體增加, 一些稍微複雜一點的邏輯就沒法跑了. 就像在 HML_FwLib_STC12 這個項目里的嘗試一樣, 很好用, 但是也很佔資源, 一不小心就超出記憶體限制. 以至於後來將串口1初始化單獨寫了個直接寫暫存器的方法.

HML_FwLib_STC12 這個項目還存在一個問題, 就是SFR變數名與STC官方的命名不一致. 如果僅僅是在Linux下開發, 自成一體, 這個問題不是很重要, 但是如果要使用網路上其他人的程式碼, 這些程式碼大都是在Keil C51下開發的, 就不能直接用 HML_FwLib_STC12 運行了, 因為有很多命名要改, 否則編譯都通不過.

使用python工具生成程式碼

所以對於STC8, 最初從另一個方向做了嘗試, 就是 stcmx 這個項目.

stcmx 這個項目是用python寫的, 在命令行中以交互的形式對各個外設進行選項設置, 然後直接生成C程式碼.

生成的程式碼非常簡潔, 都是對暫存器的直接賦值, 一步到位直接完成初始化. 風格是這樣的

void clock_init()
{
  // [  BAH,0,0x00]: 外設埠切換控制暫存器2,串口2/3/4,I2C,比較器
  P_SW2      = 0x80;
  // [FE01H,1,0x00]: 時鐘分頻暫存器,ISP可能寫入預設值
  CLKDIV     = 0x00;
  // [  9FH,0,0x00]: IRC頻率調整暫存器, ISP可能寫入預設值, 0x75:24MHz
  IRTRIM     = 0x75;
  // [  9EH,0,0x00]: IRC頻率微調暫存器, ISP可能寫入預設值
  LIRTRIM    = 0x00;
  // [  BAH,0,0x00]: 外設埠切換控制暫存器2,串口2/3/4,I2C,比較器
  P_SW2      = 0x00;
}

void timer_init()
{
  // [  D6H,0,0x00]: 定時器2高位元組
  T2H        = 0xFF;
  // [  D7H,0,0x00]: 定時器2低位元組
  T2L        = 0xCB;
  // [  87H,0,0x30]: 電源控制暫存器
  PCON       = 0xB0;
  // [  8EH,0,0x01]: 輔助暫存器
  AUXR       = 0x15;
}

void uart_init()
{
  // [  98H,0,0x00]: 串口1控制暫存器
  SCON       = 0x50;
  // [  87H,0,0x30]: 電源控制暫存器
  PCON       = 0xB0;
  // [  8EH,0,0x01]: 輔助暫存器
  AUXR       = 0x15;
}

這種方式極其節省資源, 也解決了知識復用的問題, 比如我要在36.864MHz下用timer2開啟uart1, 波特率為115200, 只需要設置選項, 輸入這些數字, 直接就能得到暫存器的初始化程式碼.

但是這種形式的缺點是工具本身的開發極其繁瑣, 等於要在python裡面把MCU的每個暫存器每個bit的邏輯都結構化了, 還得配上文字說明, 可以認為和STM32CubeMx做的事情是類似的.

還有一個更大的缺點是不靈活, 在已經生成程式碼之後, 如果需要對某些項做調整, 那麼要麼重新生成一遍, 要麼繼續查手冊.

在寫了一段時間後, 投入太大, 逐漸放棄了這個方向.

使用宏的方式將邏輯程式碼化

這就是 FwLib_STC8 這個項目的嘗試, 兼顧了靈活性和節約資源. 我從來都不喜歡宏語句, 但是在這個場景, 確實宏語句有獨特的好處.

在 FwLib_STC8 中, 90%的暫存器操作都是用宏語句實現的.

宏語句提供了一種類似於暫存器的文字注釋的功能, 在開發時的體驗類似於方法調用, 因為像VSCode這樣的IDE, 會程式碼提示並且自動補全.

在編譯階段, 宏語句就會被翻譯成直接的暫存器操作, 中間節約了方法調用的堆棧. 沒有用到的宏語句不會出現在編譯結果里, 不佔任何資源. 而如果你寫了函數, 函數不管調用沒調用, 只要同一個C文件的函數被調用了, 這個C文件里的所有函數都會一併出現在編譯結果里. 這樣帶來的編譯結果尺寸差異是很明顯的.

唯一比直接使用暫存器賦值更佔用資源的地方, 是對SFR的直接賦值操作可能會根據配置項的不同被拆成好幾步, 但是這點overheat是值得的, 因為這樣才能實現不查手冊直接用封裝庫寫程式碼, 調用的每一步知道自己在做什麼.

現在的程式碼就變成了這樣的風格

SYS_SetClock();
// UART1, baud 115200, baud source Timer2, 1T mode, interrupt on
UART1_Config8bitUart(UART1_BaudSource_Timer2, HAL_State_ON, 115200);
UART1_SetRxState(HAL_State_ON);
// Enable UART1 interrupt
EXTI_Global_SetIntState(HAL_State_ON);
EXTI_UART1_SetIntState(HAL_State_ON);

使用 FwLib_STC8 開發的注意事項

通過前面的兩篇介紹, 能大致了解這個封裝庫在兩個主流開發環境里的使用.

使用Keil C51的用戶應該會相對簡單, 因為直接將封裝庫加入項目就可以, 另外頻率可以直接用STC-ISP設置, 省掉了維護一套頻率參數的煩惱. 而在Linux下的用戶, 就需要維護一套編譯參數, 用於在程式中指定MCU頻率, 如果使用PlatformIO開發, 封裝庫已經通過library.json做了適配, 只要放入項目lib目錄, 就會自動識別並添加到include路徑.

在demo目錄下有豐富的演示示例, 基本上覆蓋了全部片內外設. 另外還有對常見元件, 例如喜聞樂見的MAX7219 8×8點陣, NRF24L01無線模組, SSD1306 OLED螢幕, ST7735 LCD這些設備的驅動.

翻閱一下演示程式碼, 能基本了解這個封裝庫的調用方法.

下面說需要注意的幾點

1. 不能隨便在傳參里使用表達式

類似於i++, var--這樣的都是表達式.

這是宏調用的固有缺陷, 因為宏畢竟不是函數, 它只是字元串模板, 在使用++, --這類操作符時, 會將這個操作放到模板里展開, 如果在模板里對這個變數引用了兩次, 那麼它就會執行兩次, 這會造成意想不到的問題.

2. 如果要同時對Keil C51和SDCC兼容, 就必須使用封裝庫提供的宏

封裝庫中引入了一些宏定義, 用於保證對 Keil C51 和 SDCC 的兼容性. 命名和形式來源於 sdcc compiler.h.
如果你希望程式碼在 Keil C51 和 SDCC 下都能編譯, 在編碼時就應當使用這些宏, 而不是編譯器對應的關鍵詞.

以下是相關的宏定義列表

Macro Keil C51 SDCC
__BIT bit __bit
__IDATA idata __idata
__PDATA pdata __pdata
__XDATA xdata __xdata
__CODE code __code
SBIT(name, addr, bit) sbit name = addr^bit __sbit __at(addr+bit) name
SFR(name, addr) sfr name = addr __sfr __at(addr) name
SFRX(addr) (*(unsigned char volatile xdata *)(addr)) (*(unsigned char volatile __xdata *)(addr))
SFR16X(addr) (*(unsigned int volatile xdata *)(addr)) (*(unsigned int volatile __xdata *)(addr))
INTERRUPT(name, vector) void name (void) interrupt vector void name (void) __interrupt (vector)
INTERRUPT_USING(name, vector, regnum) void name (void) interrupt vector using regnum void name (void) __interrupt (vector) __using (regnum)
NOP() _nop_() __asm NOP __endasm

這些宏定義可以在 include/fw_reg_base.h 中查看

3. 部分宏語句的參數是枚舉, 調用時要留意

使用宏語句的一個缺點就是沒有類型提示, 雖然在變數名上我已經盡量體現出這個參數的類型, 但是寫程式碼時, IDE是沒有提示的. 所以這裡需要注意的是, 有一些輸入參數是枚舉, 在調用時最好切換到聲明這個宏的.h文件中看一眼, 這些枚舉一般都定義在.h文件的開始部分.

4. 不同MCU之間的資源差異

封裝庫本身只區分了STC8G和STC8H兩個大類, 例如STC8G 有 PCA但是沒有PWM, STC8H 中有PWM沒有PCA. 在大類的內部, 例如 STC8H 的各個子系列, 在功能上也是有差異的, 例如 STC8H1K 系列的ADC是 10bit, STC8H3K, STC8H8K 的ADC是12bit, 還有通道的數量以及和IO口的映射關係都有區別.

這些區別基本上都列在了對應外設的.h文件中, 在開發時可以多看一眼, 避免不必要的時間浪費.

結束

以上就是對 FwLib_STC8 封裝庫的開發說明. 希望這個封裝庫能加快STC8G,STC8H的開發速度, 節省更多人的時間.