從0開始使用面對對象C語言搭建一個基于OLED的圖形顯示框架(協議層封裝)

目錄

協議層設計,以IIC為例子

關于軟硬件IIC

設計的一些原則

完成協議層的抽象

刨析我們的原理

如何完成我們的抽象

插入幾個C語言小技巧

完成軟件IIC通信

開始我們的IIC通信

結束我們的IIC通信

發送一個字節

(重要)完成命令傳遞和數據傳遞

最終一擊,完成我們的IIC通信

硬件IIC


關于架構設計概述等內容,筆者放到了:從0開始使用面對對象C語言搭建一個基于OLED的圖形顯示框架-CSDN博客,任何疑問可以到這里看看,這是一個總覽。

協議層設計,以IIC為例子

我們先按照最經典的軟硬件IIC為例子!筆者大部分接觸到的都是4針腳的使用IIC協議通信的OLED片子。所以,筆者打算優先的搭建起來IIC部分的代碼。所有完整的代碼放到了:MCU_Libs/OLED/library/OLED/Driver at main · Charliechen114514/MCU_Libs (github.com),這個文件夾內部都是協議層的代碼。

關于軟硬件IIC

軟硬件IIC都是完成IIC通信協議的東西。但區別在于,我們到底是使用自己手動模擬的IIC還是使用專門硬件特化的IIC。

關于IIC,看到這里的朋友都很熟悉了:IIC(Inter-Integrated Circuit)是一種常用的串行通信總線協議,用于微控制器與傳感器、顯示模塊等外設之間的通信。而我們的軟件IIC就是使用GPIO來模擬IIC時序。

優點:

  1. 靈活性強,可以使用任意引腳進行通信,不受特定硬件限制。

  2. 適用于不具備硬件IIC模塊的微控制器。

  3. 可以方便地調節時序,兼容性較好。

缺點:

  1. 通信效率較低,占用CPU資源較多。

  2. 對實時性要求高的應用不太適合。

  3. 穩定性較差,容易受程序時序影響。

硬件IIC則是將IIC應答處理委托給了專門的硬件。

優點

  1. 通信速度快,效率高,因為由專用硬件處理時序。

  2. 占用CPU資源少,適合需要高實時性的場合。

  3. 通信穩定可靠,不易受到程序時序干擾。

缺點:

  1. 只能使用特定的IIC引腳,不夠靈活。

  2. 不同微控制器之間的硬件IIC兼容性可能存在差異。

  3. 部分微控制器可能沒有硬件IIC模塊,導致無法使用硬IIC。

我們大概清楚了。代碼上的實現就不會復雜。下面我們就可以開始聊一聊設計了。

設計的一些原則

你認為這樣的代碼好看嗎?

void OLED_ShowImage(int16_t X, int16_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image)
{uint8_t i = 0, j = 0;int16_t Page, Shift;/*將圖像所在區域清空*/OLED_ClearArea(X, Y, Width, Height);/*遍歷指定圖像涉及的相關頁*//*(Height - 1) / 8 + 1的目的是Height / 8并向上取整*/for (j = 0; j < (Height - 1) / 8 + 1; j ++){/*遍歷指定圖像涉及的相關列*/for (i = 0; i < Width; i ++){if (X + i >= 0 && X + i <= 127) ? ? ? //超出屏幕的內容不顯示{/*負數坐標在計算頁地址和移位時需要加一個偏移*/Page = Y / 8;Shift = Y % 8;if (Y < 0){Page -= 1;Shift += 8;}if (Page + j >= 0 && Page + j <= 7) ? ? ? //超出屏幕的內容不顯示{/*顯示圖像在當前頁的內容*/OLED_DisplayBuf[Page + j][X + i] |= Image[j * Width + i] << (Shift);}if (Page + j + 1 >= 0 && Page + j + 1 <= 7) ? ? ? //超出屏幕的內容不顯示{ ? ? ? ? ? ? ? ? ? /*顯示圖像在下一頁的內容*/OLED_DisplayBuf[Page + j + 1][X + i] |= Image[j * Width + i] >> (8 - Shift);}}}}
}

好吧,好像大部分人的代碼都是這樣的。

那這樣呢?

void CCGraphicWidget_draw_image(CCDeviceHandler* ?  handler,CCGraphic_Image* ?  image)
{if(!image->sources_register) return;handler->operations.draw_area_device_function(handler, image->point.x, image->point.y,image->image_size.width, image->image_size.height, image->sources_register);
}

你需要在乎image是如何實現的嗎?你需要知道如何完成OLED圖像的顯示是如何做的嗎?

你不需要!

這段代碼無非就是告訴了你一件事情:提供一個設備句柄作為“告知一個設備,在上面繪制”,告知一個“圖像”你需要繪制,直接提供進來,由設備自己約定的方法繪制即可。怎么繪制的?你需要關心嗎?你不需要。

直到你需要考慮設備是如何工作的時候,你會看一眼內部的設備

void oled_helper_draw_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t* sources)
{// 嘿!超出繪制范圍了if(x > POINT_X_MAX)  return;if(y > POINT_Y_MAX) return;
?// clear the area before being set// 先清理一下這個區域,不要干擾賦值oled_helper_clear_area(handle, x, y , width, height); 
?for(uint16_t j = 0; j < (height -1) / 8 + 1; j++){for(uint16_t i = 0; i < width; i++){if(x + i > OLED_WIDTH){break;}if(y / 8 + j > OLED_HEIGHT - 1){return;}
?OLED_GRAM[y / 8 + j][x + i] |= sources[j * width + i] << (y % 8);
?if(y / 8 + j + 1 > OLED_HEIGHT - 1){continue;}
?OLED_GRAM[y / 8 + j + 1][x + i] |= sources[j * width + i] >> (8 - y % 8);}}
}

原來如此,是通過寫OLED緩存賦值就可以把這個事情給搞明白的——但是當你不關心如何實現的時候,你并不需要付出心血代價把代碼看懂然后——哦我的天,這個我壓根不關心!當然,代價就是多支付若干次的函數調用(笑)

這就是架構抽象帶來的對開發的好處,但是只有這個不足以讓我們使用復雜的抽象,我們一定還有別的好處,不是嗎?讓我們慢慢看吧!

完成協議層的抽象

我已經說過,我們的OLED框架是由協議層(使用何種協議進行通信?),設備層(這個設備可以做什么?),圖像層(可以使用設備繪制哪一些圖像?),組件層(可以使用圖像繪制哪一些組件?),層層遞進,保證互相之間互不干擾。我們下面就著重的關心協議層。協議層需要完成的就是將委托的命令(OLED命令)和委托的數據(OLED數據)發送到設備上即可。

刨析我們的原理

協議需要進行初始化,對于硬件,特別是HAL庫,只需要咔咔調API就能把事情做完了。但是對于軟件IIC,事情就需要麻煩一些,我們需要自己完成IIC時序的通信。

讓我們看看IIC的基本原理,基本上看,就是:通知起始通信,通知數據(他是命令還是數據并不關心)和通知停止。

  1. 起始條件(Start Condition) 主設備將SDA從高電平拉低,同時保持SCL為高電平。當SDA從高到低時形成起始條件(START),通知從設備通信即將開始。

  2. 地址傳輸(Address Transmission) 主設備發送一個7位或10位的從設備地址,緊接著是1位的讀寫方向標志位(R/W位)。

    • R/W為0表示寫操作,主設備發送數據

    • R/W為1表示讀操作,主設備接收數據 每發送一位數據時,SCL產生一個時鐘脈沖(SCL上升沿鎖存數據)。

  3. 應答信號(ACK/NACK) 從設備在收到地址和R/W位后,如果能夠正常接收數據,會在下一個時鐘周期內將SDA拉低產生應答信號ACK(Acknowledge)。如果不響應,則保持SDA為高電平,產生非應答信號NACK(Not Acknowledge)。

  4. 數據傳輸(Data Transmission) 主設備根據讀寫操作繼續發送或接收數據,每次傳輸8位數據。

    • 寫操作:主設備發送數據,從設備應答ACK

    • 讀操作:從設備發送數據,主設備應答ACK 每個字節傳輸完成后,從設備需發送ACK信號以確認接收正常。

  5. 停止條件(Stop Condition) 通信結束時,主設備將SDA從低電平拉高,同時保持SCL為高電平。當SDA從低到高時形成停止條件(STOP),表示通信結束。

說了一大堆,其實就是:

  1. 起始條件:SDA高變低,SCL保持高

  2. 數據傳輸:SDA根據數據位變化,SCL上升沿鎖存數據

  3. 應答信號:從設備將SDA拉低產生ACK,高電平為NACK

  4. 停止條件:SDA低變高,SCL保持高

所以這樣看來,無非就是使用兩個引腳,按照上述規則進行高低電平的按照時序的拉高拉低。

話里有話,我的意思就是:軟件IIC需要知道你使用哪兩個引腳進行通信,需要你來告知如何完成上面的協議約定控制設備。最終我們提供的,是像我們跟人聊天一般的:

嘿!我用軟件IIC發送了一個Byte的命令/數據!

這是重點!也是我們協議層抽象的終點:完成委托給我們的數據傳輸的任務,其他的任何事情都與我們無關,也不在乎這個數據到底是啥!

如何完成我們的抽象

軟件IIC需要知道你使用哪兩個引腳進行通信,需要你來告知如何完成上面的協議約定控制設備!我再強調的一次!

所以,我們給一個被抽象為軟件IIC的實體,提供一個配置,這個配置委婉的提醒了我們的IIC使用哪兩個引腳進行通信。最終這個軟件IIC實體將會提供可以完成“委托給我們的數據傳輸的任務”這個任務,需要注意的是,OLED發送數據需要區分他是命令還是數據。這樣來看,我們最終就是提供兩套方法:

/* command send fucntion */
typedef void(*SendCommand)(void*, uint8_t);
/* data send fucntion */
typedef void(*SendData)(void*, uint8_t*, uint16_t);
?
/* driver level oled driver's functionalities */
typedef struct __OLED_Operations{SendCommand command_sender;SendData ?  data_sender;
}OLED_Operations;

好像很是罕見!這是一個包裝了函數指針的結構體。說的拗口,讓我們引入面對對象的設計邏輯來再闡述上面的句子。

這是一個可以保證完成數據傳輸的OLED方法。調用這個方法,就可以保證我們完成了一個字節傳遞的命令,或者是完成一系列字節的數據傳輸

又問我咋做的?先別管,你現在需要知道的是——我一調用!他就能干好這個事情!實現是下面的事情!它隸屬于我們的協議實體的結構體,如下所示

/* this will make the gpio used for iic */
typedef struct __OLED_SOFT_IIC_Private_Config
{/* soft gpio handling */ OLED_IICGPIOPack ? ? ? sda;OLED_IICGPIOPack ? ? ? scl;uint32_t ? ? ? ? ?  accepted_time_delay;uint16_t ? ? ? ? ?  device_address;OLED_Operations ? ? operation;
}OLED_SOFT_IIC_Private_Config;

OLED_IICGPIOPack sda 表示用于IIC的SDA(數據線)引腳配置。

  • OLED_IICGPIOPack 應該是一個結構體或類型,定義了與GPIO相關的參數,比如引腳號、端口等。

  • 該成員用來指定IIC通信中用作SDA的具體引腳。

OLED_IICGPIOPack scl 表示用于IIC的SCL(時鐘線)引腳配置。

  • 同樣是 OLED_IICGPIOPack 類型,用來配置時鐘信號線(SCL)的具體引腳。

  • 這個成員和 sda 一起決定了軟IIC使用的GPIO引腳。

uint32_t accepted_time_delay 用于設置IIC時序中的時間延遲。

  • 因為軟IIC需要軟件控制時序,這個值可能表示每個時鐘周期的延遲時間(以微秒或納秒為單位)。

  • 調節這個值可以改變IIC的通信速度,從而適配不同的外設設備。

uint16_t device_address IIC從設備的地址。

  • IIC通信中,每個從設備都有唯一的地址,用于主設備區分不同的從設備。

  • 這個值通常是7位或10位地址,需要根據設備規格書配置。

OLED_Operations operation 表示IIC通信的操作類型。

  • OLED_Operations定義了常見的IIC操作,比如 READ(讀操作)、WRITE(寫操作)等。

初始化的辦法,這里就只需要按部就班的賦值。

void oled_bind_softiic_handle(OLED_SOFT_IIC_Private_Config* ? config,OLED_IICGPIOPack* ? ? ? ? ? ? ? ?  sda, ?OLED_IICGPIOPack* ? ? ? ? ? ? ? ?  scl,uint16_t ? ? ? ? ? ? ? ? ? ? ?  device_address,uint32_t ? ? ? ? ? ? ? ? ? ? ?  accepted_time_delay
)
{config->accepted_time_delay = accepted_time_delay;config->device_address = device_address;config->sda = *sda;config->scl = *scl;config->operation.command_sender ?  = ?config->operation.data_sender ? ? ? = ?/* we need to init the gpio type for communications */
}

我們的函數寫到下面就頓住了。對啊,咋發送啊?咋操作啊?這才是這個時候我們思考的問題:如何實現軟件IIC呢?

我們首先需要完成的是:初始化我們的引腳,讓他們可以完成傳遞電平的任務。

static void __pvt_on_init_iic_gpio(OLED_SOFT_IIC_Private_Config* config)
{/* Enable the GPIOB clock *//* 這就是把時鐘打開了而已,是__HAL_RCC_GPIOB_CLK_ENABLE的一個等價替換 *//* #define OLED_ENABLE_GPIO_SCL_CLK() __HAL_RCC_GPIOB_CLK_ENABLE()#define OLED_ENABLE_GPIO_SDA_CLK() __HAL_RCC_GPIOB_CLK_ENABLE()*/// 為什么這樣做。。。你換引腳了直接改上面的#define不香嗎?集中起來處理一坨屎而不是讓你的史滿天飛到處改OLED_ENABLE_GPIO_SCL_CLK();OLED_ENABLE_GPIO_SDA_CLK();
?GPIO_InitTypeDef GPIO_InitStructure = {0};/* configuration */GPIO_InitStructure.Pin = config->sda.pin | config->scl.pin;GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;          // 開漏模式GPIO_InitStructure.Pull = GPIO_NOPULL;                  // 不上拉也不下拉GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);// 這個是一個非常方便的宏,是筆者自己封裝的:/*#define SET_SCL(config, pinstate) \do{\HAL_GPIO_WritePin(config->scl.port, config->scl.pin, pinstate);\}while(0)
?#define SET_SDA(config, pinstate) \do{\HAL_GPIO_WritePin(config->sda.port, config->sda.pin, pinstate);\}while(0)*/SET_SCL(config, 1);SET_SDA(config, 1);
}

插入幾個C語言小技巧

  1. 結構體的使用更加像是對一個物理實體的抽象,比如說我們的軟件IIC實體由兩個GPIO引腳,提供一個OLED地址和延遲時間組成,他可以發送命令和數據

    /* this will make the gpio used for iic */
    typedef struct __OLED_SOFT_IIC_Private_Config
    {/* soft gpio handling */ OLED_IICGPIOPack ? ? ? sda;OLED_IICGPIOPack ? ? ? scl;uint32_t ? ? ? ? ?  accepted_time_delay;uint16_t ? ? ? ? ?  device_address;OLED_Operations ? ? operation;
    }OLED_SOFT_IIC_Private_Config;

    這樣的抽象也就呼之欲出了

  2. 為什么使用do while呢?答案是:符合大部分人的使用習慣。

    避免宏定義中的語法問題 在宏中使用 do { } while(0); 可以確保宏內容被當作一個獨立的語句塊執行。 例如:

    #define MY_MACRO(x) do { if (x) func(); } while (0) ?

    這樣,即使在使用時加上分號也不會引發編譯錯誤:

    if (condition) ?MY_MACRO(1);  // 正確處理,避免語法歧義 ?
    else ?other_func(); ?

    如果直接使用 {} 而不加 do-while(0),編譯器可能會報錯或者導致意外的邏輯問題。

    提升代碼的可讀性與可維護性 do { } while(0); 語法塊明確限制了語句作用范圍,避免宏或語句中的變量污染外部作用域,從而增強代碼的封裝性。

    兼容語法規則,減少隱患 do { } while(0); 總能確保語法結構合法,即使宏中包含復雜的控制語句也不會影響邏輯。

    #define SAFE_BLOCK do { statement1; statement2; } while(0) ?

    這樣即便加了分號也能正常執行,符合常規語句格式。

    避免空語句問題 使用 do-while(0) 可以有效避免空語句可能帶來的邏輯漏洞。

    你問我擔心開銷?拜托!編譯器會自動優化!全給你消的一點不剩了,完全就是正常的調用,為啥不用?

  3. 為什么在函數的起頭帶上static?

    保證我們的函數在文件作用域是私有的,不會跟其他函數起沖突的。說白了,就是我說的:你需要在干別的事情還要擔心一下自己的軟件IIC是咋工作的嗎?你不需要!擔心是一個有病的行為。所以,他保證了接口是簡潔的。

完成軟件IIC通信

開始我們的IIC通信

軟件IIC通信開始,需要先拉高SDA和SCL保證處于高電平,然后拽低SDA和SCL的電平

static void __pvt_on_start_iic(OLED_SOFT_IIC_Private_Config* config) 
{SET_SDA(config, 1);SET_SCL(config, 1);SET_SDA(config, 0);SET_SCL(config, 0); ? ?
}
結束我們的IIC通信

設置我們的SDA先低,之后讓SDA和SCL都處于高電平結束戰斗

static void __pvt_on_stop_iic(OLED_SOFT_IIC_Private_Config* handle)
{SET_SDA(handle, 0);     SET_SCL(handle, 1);     SET_SDA(handle, 1);      
}
發送一個字節

發送一個目標字節給我們的設備,你不需要關心這個字節是什么,你不需要現在關心它!

static void __pvt_iic_send_bytes(OLED_SOFT_IIC_Private_Config* handle, uint8_t data)
{   for (uint8_t i = 0; i < 8; i++){   SET_SDA(handle,!!(data & (0x80 >> i)));SET_SCL(handle,1);  SET_SCL(handle,0);  }SET_SCL(handle,1);      SET_SCL(handle,0);
}

!! 的作用是將任意數值轉換為布爾值,保證我們發的就是0和1,(0x80 >> i)萃取了從高向低數的第I位數字發送,也就是往SDA電平上傳遞我們的data上的第I位。之后拉起釋放SCL告知完成傳遞。

(重要)完成命令傳遞和數據傳遞

我們現在開始想起來,我們最終的目的是:完成一個字節命令的傳遞或者是傳遞一系列的數據比特。結合手冊,我們來看看實際上怎么做。

按照順序,依次傳遞

  • 開啟IIC通信

  • 設備的地址

  • 數據類型(是命令還是數據)

  • 數據本身。

  • 結束IIC通信

/*#define DATA_PREFIX ? ? (0x40)#define CMD_PREFIX ? ?  (0x00)
*/
static void __pvt_iic_send_command(void* pvt_handle, uint8_t cmd)
{OLED_SOFT_IIC_Private_Config* config = (OLED_SOFT_IIC_Private_Config*)pvt_handle;
?__pvt_on_start_iic(config);__pvt_iic_send_bytes(config, config->device_address);__pvt_iic_send_bytes(config, CMD_PREFIX);__pvt_iic_send_bytes(config, cmd);__pvt_on_stop_iic(config);
}
?
static void __pvt_iic_send_data(void* pvt_handle, uint8_t* data, uint16_t size)
{OLED_SOFT_IIC_Private_Config* config = (OLED_SOFT_IIC_Private_Config*)pvt_handle;__pvt_on_start_iic(config);__pvt_iic_send_bytes(config, config->device_address);__pvt_iic_send_bytes(config, DATA_PREFIX);for(uint16_t i = 0; i < size; i++)__pvt_iic_send_bytes(config, data[i]);__pvt_on_stop_iic(config); 
}
最終一擊,完成我們的IIC通信
/*config: Pointer to an OLED_SOFT_IIC_Private_Config structure that contains the configuration settings for the software I2C communication,such as timing, pins, and other relevant parameters.config should be blank or uninitialized.sda: Pointer to an OLED_GPIOPack structure that represents the GPIO configuration for the Serial Data (SDA) line of the software I2C interface.
?scl: Pointer to an OLED_GPIOPack structure that represents the GPIO configuration for the Serial Clock (SCL) line of the software I2C interface.
?device_address: The 7-bit I2C address of the device that the software I2C communication is targeting, typically used to identify the device on the I2C bus.
?accepted_time_delay: A timeout value in milliseconds, specifying the maximum allowed delay for the software I2C communication process.
*/
void oled_bind_softiic_handle(OLED_SOFT_IIC_Private_Config* ? config,OLED_IICGPIOPack* ? ? ? ? ? ? ? ?  sda, ?OLED_IICGPIOPack* ? ? ? ? ? ? ? ?  scl,uint16_t ? ? ? ? ? ? ? ? ? ? ?  device_address,uint32_t ? ? ? ? ? ? ? ? ? ? ?  accepted_time_delay
){config->accepted_time_delay = accepted_time_delay;config->device_address = device_address;config->sda = *sda;config->scl = *scl;config->operation.command_sender ?  = __pvt_iic_send_command;config->operation.data_sender ? ? ? = __pvt_iic_send_data;__pvt_on_init_iic_gpio(config); 
}

我們把方法和數據都傳遞給這個軟件iic實體,現在,他就能完成一次軟件IIC通信了。給各位看看如何使用

config->operation.command_sender(config, oled_spi_init_command[i]);

可以看到,我們的結構體函數指針就是這樣使用的。

硬件IIC

硬件IIC事情就會簡單特別多,原因在于,我們有專門的硬件幫助我們完成IIC通信

#ifndef OLED_HARD_IIC_H
#define OLED_HARD_IIC_H
#include "OLED/Driver/oled_config.h"
#include "stm32f1xx_hal.h"
#include "stm32f1xx_hal_i2c.h"
?
typedef struct __OLED_HARD_IIC_Private_Config{I2C_HandleTypeDef*  pvt_handle;uint32_t ? ? ? ? ?  accepted_time_delay;uint16_t ? ? ? ? ?  device_address;OLED_Operations ? ? operation;
}OLED_HARD_IIC_Private_Config;
?
/* handle binder, bind the raw data to the oled driverblank_config: Pointer to an OLED_HARD_IIC_Private_Config structure that holds the configuration settings for the I2C communication, typically initializing the OLED hardware interface.raw_handle: Pointer to an I2C_HandleTypeDef structure, representing the raw I2C peripheral handle used to configure and manage I2C communication for the device.
?device_address: The 7-bit I2C address of the device to which the communication is being established, typically used for identifying the target device on the I2C bus.
?accepted_time_delay: A timeout value in milliseconds that specifies the maximum allowable delay for the I2C communication process.
*/
void bind_hardiic_handle(OLED_HARD_IIC_Private_Config* blank_config,I2C_HandleTypeDef* raw_handle,uint16_t ?  device_address,uint32_t ?  accepted_time_delay
);
?
#endif

現在我們可以不需要兩個引腳了,只需要客戶端提供一個硬件IIC句柄就好。

#include "OLED/Driver/hard_iic/hard_iic.h"
?
static void __pvt_hardiic_send_data(void* pvt_handle, uint8_t* data, uint16_t size)
{OLED_HARD_IIC_Private_Config* config = (OLED_HARD_IIC_Private_Config*)pvt_handle;for (uint8_t i = 0; i < size; i ++){HAL_I2C_Mem_Write(config->pvt_handle,config->device_address,DATA_PREFIX,I2C_MEMADD_SIZE_8BIT,&data[i], 1, config->accepted_time_delay);  //依次發送Data的每一個數據}
}
?
static void __pvt_hardiic_send_command(void* pvt_handle, uint8_t cmd)
{OLED_HARD_IIC_Private_Config* config = (OLED_HARD_IIC_Private_Config*)pvt_handle;HAL_I2C_Mem_Write(config->pvt_handle, config->device_address,CMD_PREFIX,I2C_MEMADD_SIZE_8BIT,&cmd,1,config->accepted_time_delay);
}
?
void bind_hardiic_handle(OLED_HARD_IIC_Private_Config* blank_config,I2C_HandleTypeDef* raw_handle,uint16_t ?  device_address,uint32_t ?  accepted_time_delay
)
{blank_config->accepted_time_delay = accepted_time_delay;blank_config->device_address = device_address;blank_config->pvt_handle = raw_handle;blank_config->operation.command_sender  = __pvt_hardiic_send_command;blank_config->operation.data_sender ? ? = __pvt_hardiic_send_data;
}

HAL_I2C_Mem_Write函數直接完成了我們的委托,注意的是,我們每一次的調用這個函數,內部都是重新開始一次IIC通信的,所以,發送數據的時候,只能一個字節一個字節的發送(因為每一次都要指定這個是數據還是命令)。這一點,SPI協議的OLED就要好很多!(內部的引腳高低就直接決定了整個是命令還是數據,不需要通過解析傳遞的數據本身!)

這樣,一個典型的基于軟硬件IIC的協議層抽象就完成了。如果你著急測試的話,可以自己替換原本OLED的操作。

我們下一篇,就是開始抽象OLED的設備層。

目錄導覽

總覽

協議層封裝

OLED設備封裝

繪圖設備抽象

基礎圖形庫封裝

基礎組件實現

動態菜單組件實現

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/67498.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/67498.shtml
英文地址,請注明出處:http://en.pswp.cn/web/67498.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

舉例說明python單利模式的必要性

單例模式的核心目的是確保一個類只有一個實例&#xff0c;并提供一個全局訪問點來獲取這個實例。這種設計模式在某些場景下非常必要&#xff0c;尤其是在需要嚴格控制資源訪問、共享狀態或配置管理的場景中。下面通過幾個具體的例子來說明Python中單例模式的必要性。 1. 數據庫…

【騰訊云】騰訊云docker搭建單機hadoop

這里寫目錄標題 下載jdk hadoop修改hadoop配置編寫Dockerfile構建鏡像運行鏡像創建客戶端 下載jdk hadoop wget --no-check-certificate https://repo.huaweicloud.com/java/jdk/8u151-b12/jdk-8u151-linux-x64.tar.gz wget --no-check-certificate https://repo.huaweicloud.…

設計模式 - 行為模式_Template Method Pattern模板方法模式在數據處理中的應用

文章目錄 概述1. 核心思想2. 結構3. 示例代碼4. 優點5. 缺點6. 適用場景7. 案例&#xff1a;模板方法模式在數據處理中的應用案例背景UML搭建抽象基類 - 數據處理的 “總指揮”子類定制 - 適配不同供應商供應商 A 的數據處理器供應商 B 的數據處理器 在業務代碼中整合運用 8. 總…

HTML5+SVG+CSS3實現雪中點亮的圣誕樹動畫效果源碼

源碼介紹 這是一款基于HTML5SVGCSS3實現雪中點亮的圣誕樹動畫效果源碼。畫面中的圣誕樹矗立在雪地中&#xff0c;天上飄落著雪花。當鼠標滑過圣誕樹時&#xff0c;可見到圣誕樹上的燈光閃爍&#xff0c;同時左下角探出雪怪模樣的半個腦袋&#xff0c;四處張望著。整體畫面栩栩…

C基礎寒假練習(3)

一、求數組中的第二大值 #include <stdio.h> int main() {int arr[] {12, 35, 1, 10, 34, 1};int size sizeof(arr) / sizeof(arr[0]);if (size < 2) {printf("數組元素不足兩個\n");return 0;}int first -2147483648, second -2147483648; // 使用IN…

【linux三劍客】grep練習題

題目 進入/lianxi目錄&#xff0c;復制/etc/passwd到當前目錄下&#xff0c;然后對passwd進行操作查找出當前passwd文件中以ftp或者mail開頭的行&#xff0c;在屏幕上輸出。查找出當前passwd文件中有沒有以r、m、f開頭的行&#xff0c;在屏幕上輸出。查找出當前passwd文件中以…

C++,STL,【目錄篇】

文章目錄 一、簡介二、內容提綱第一部分&#xff1a;STL 概述第二部分&#xff1a;STL 容器第三部分&#xff1a;STL 迭代器第四部分&#xff1a;STL 算法第五部分&#xff1a;STL 函數對象第六部分&#xff1a;STL 高級主題第七部分&#xff1a;STL 實戰應用 三、寫作風格四、…

【Node.js】Koa2 整合接口文檔

部分學習來源&#xff1a;https://blog.csdn.net/qq_38734862/article/details/107715579 依賴 // koa2-swagger-ui UI視圖組件 swagger-jsdoc 識別寫的 /***/ 轉 json npm install koa2-swagger-ui swagger-jsdoc --save配置 config\swaggerConfig.js const Router requir…

Maven的單元測試

1. 單元測試的基本概念 單元測試&#xff08;Unit Testing&#xff09; 是一種軟件測試方法&#xff0c;專注于測試程序中的最小可測試單元——通常是單個類或方法。通過單元測試&#xff0c;可以確保每個模塊按預期工作&#xff0c;從而提高代碼的質量和可靠性。 2.安裝和配…

論文閱讀(八):結構方程模型用于研究數量遺傳學中的因果表型網絡

1.論文鏈接&#xff1a;Structural Equation Models for Studying Causal Phenotype Networks in Quantitative Genetics 摘要&#xff1a; 表型性狀可能在它們之間發揮因果作用。例如&#xff0c;農業物種的高產可能會增加某些疾病的易感性&#xff0c;相反&#xff0c;疾病的…

LeetCode | 不同路徑

一個機器人位于一個 m x n 網格的左上角 &#xff08;起始點在下圖中標記為 “Start” &#xff09;。 機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角&#xff08;在下圖中標記為 “Finish” &#xff09;。 問總共有多少條不同的路徑&#xff1f; 示例 1…

C++的類Class

文章目錄 一、C的struct和C的類的區別二、關于OOP三、舉例&#xff1a;一個商品類CGoods四、構造函數和析構函數1、定義一個順序棧2、用構造和析構代替s.init(5);和s.release();3、在不同內存區域構造對象4、深拷貝和淺拷貝5、構造函數和深拷貝的簡單應用6、構造函數的初始化列…

Excel 技巧21 - Excel中整理美化數據實例,Ctrl+T 超級表格(★★★)

本文講Excel中如何整理美化數據的實例&#xff0c;以及CtrlT 超級表格的常用功能。 目錄 1&#xff0c;Excel中整理美化數據 1-1&#xff0c;設置間隔行顏色 1-2&#xff0c;給總銷量列設置數據條 1-3&#xff0c;根據總銷量設置排序 1-4&#xff0c;加一個銷售趨勢列 2&…

Leetcode 131 分割回文串(純DFS)

131. 分割回文串https://leetcode.cn/problems/palindrome-partitioning/https://leetcode.cn/problems/palindrome-partitioning/ 給你一個字符串 s&#xff0c;請你將 s 分割成一些子串&#xff0c;使每個子串都是 回文串 。返回 s 所有可能的分割方案。 示例 1&#xff1a…

電梯系統的UML文檔14

對于 HallButtonControl&#xff0c;我們有二個狀態: "門廳燈開 " 和 " 門廳燈關"。 從給出的初始信息&#xff0c;初始的狀態應該是"門廳燈關"。行為定義&#xff1a; " 當 HallCall[f&#xff0c;d]是真&#xff0c;則指令 HallLight[f&…

關于安卓greendao打包時報錯問題修復

背景 項目在使用greendao的時候&#xff0c;debug安裝沒有問題&#xff0c;一到打包簽名就報了。 環境 win10 jdk17 gradle8 項目依賴情況 博主的greendao是一個獨立的module項目&#xff0c;項目目前只適配了java&#xff0c;不支持Kotlin。然后被外部集成。greendao版本…

SQL server 數據庫使用整理

標題&#xff1a;SQL server 數據庫使用整理 1.字符串表名多次查詢 2.讀取SQL中Json字段中的值&#xff1a;JSON_VALUE&#xff08;最新版本支持&#xff0c;屬性名大小寫敏感&#xff09; 1.字符串表名多次查詢 SELECT ROW_NUMBER() OVER (ORDER BY value ASC) rowid,value…

一文講解Java中的BIO、NIO、AIO之間的區別

BIO、NIO、AIO是Java中常見的三種IO模型 BIO&#xff1a;采用阻塞式I/O模型&#xff0c;線程在執行I/O操作時被阻塞&#xff0c;無法處理其他任務&#xff0c;適用于連接數比較少的場景&#xff1b;NIO&#xff1a;采用非阻塞 I/O 模型&#xff0c;線程在等待 I/O 時可執行其…

分布式系統架構怎么搭建?

分布式系統架構 互聯網企業的業務飛速發展&#xff0c;促使系統架構不斷變化。總體來說&#xff0c;系統架構大致經歷了單體應用架構—垂直應用架構—分布式架構—SOA架構—微服務架構的演變&#xff0c;很多互聯網企業的系統架構已經向服務化網格&#xff08;Service Mesh&am…

Effective C++ 規則50:了解 new 和 delete 的合理替換時機

1、背景 在 C 中&#xff0c;new 和 delete 是動態分配內存的核心操作符。然而&#xff0c;直接使用它們有時會增加程序的復雜性&#xff0c;甚至導致內存泄漏和其他問題。因此&#xff0c;了解何時替換 new 和 delete 并選擇更適合的內存管理策略&#xff0c;是編寫高效、健壯…