python環境下實現OrangePi Zero寄存器訪問及GPIO控制
最近入手OrangePi Zero一塊,程序上需要使用板子上自帶的LED燈,在網上一查,不得不說OPi的支持跟樹莓派無法相比。自己摸索了一下,實現簡單的GPIO控制方法,作者的Zero安裝的是Armbian系統,使用python寫了一個讀寫寄存器的簡單模塊,通過這個模塊,即可實現對GPIO的控制。
作者以前使用過STM32的MCU,這類MCU,如果要實現對GPIO的控制,只需要根據datasheet查找相應GPIO寄存器並進行配置,即可實現IO控制,例如,要將內存地址為0x12345678的寄存器全部置為0xFFFFFFFF,只需要一條C語句:
1 (uint32 *)(0x12345678) = 0xFFFFFFFF;
但是,這個方法在Linux中行不通,編譯運行的時候,會提示"segmentation fault",這個段錯誤應該就是訪問了不可訪問的內存,這個內存區要麽是不存在的,要麽是受到系統保護的。所以只能使用其他方法。
首先總結一下實現對OrangePi GPIO控制的兩種方法,第一種是通過Linux內存映射的方式,將實際CPU硬件的內存地址映射到用戶程序的內存空間,再進行操作;第二種是通過sysfs方式控制GPIO,在程序中,操作/sys/class/gpio目錄實現io口的控制。
這兩種方式都有現成已完成的案例,例如在python環境使用的pyH3庫、C環境的WiringPi庫使用的就是第一種方式;從樹莓派移植的OPi.GPIO庫使用的是第二種方式。其中第二種方式個人感覺更加簡單,因為只需要在用戶的程序裏面讀寫系統目錄的文件,即可實現對GPIO的控制,非常方便,但作者發現這種方式有個嚴重的問題,就是它無法使用那些板子上沒引出來的IO口,因為板載的兩個LED(紅燈和綠燈)分別是使用了PA17和PL10引腳,如果使用第二種方式控制這兩個引腳,會提示“Device or resource busy”的錯誤,通過以下命令:
cat /sys/kernel/debug/gpio
可以看出系統已經占用的IO口如下:
第一個GPIO17即為PA17,系統默認已經把該引腳配置為輸出模式,並置為低電平。這裏需要說明一下系統裏對GPIO口的編號方法,系統是按照PA~PL共12組、每組32個引腳的方式對IO口進行統一編號的,如上圖,GPIO-0~GPIO-31為PA口的IO,GPIO-32~GPIO-63為PB口的IO,以此類推。所以上圖最後一行GPIO-362實際就是PL10引腳,即電源的綠燈引腳。
回歸本文,因為第二種方法無法操作PA17,所以只能使用第一種方法。
第一種方法已經有現成的實現,通過深入研究庫源碼,內部實際都是通過C的mmap函數來實現CPU的物理地址映射到用戶程序的內存空間。作者習慣使用Pyhon在Linux環境下進行程序開發,pyH3庫使用感覺比較繁瑣。所以特意研究了一下能否在python環境下實現物理地址的映射。實際果然不出所料,C裏有mmap函數,python裏同樣有內置的mmap模塊。說明文檔在這裏:https://docs.python.org/2/library/mmap.html
其中最重要的就是mmap類的構造函數:
class mmap.mmap(fileno, length[, flags[, prot[, access[, offset]]]])
fileno: 文件描述符,可以是file對象的fileno()方法,或者來自os.open(),在調用mmap()之前打開文件,不再需要文件時要關閉。
length:要映射文件部分的大小(以字節為單位),這個值為0,則映射整個文件,如果大小大於文件當前大小,則擴展這個文件。
flags:MAP_PRIVATE:這段內存映射只有本進程可用;mmap.MAP_SHARED:將內存映射和其他進程共享,所有映射了同一文件的進程,都能夠看到其中一個所做的更改;
prot:mmap.PROT_READ, mmap.PROT_WRITE 和 mmap.PROT_WRITE | mmap.PROT_READ。最後一者的含義是同時可讀可寫。
access:在mmap中有可選參數access的值有
ACCESS_READ:讀訪問。
ACCESS_WRITE:寫訪問,默認。
ACCESS_COPY:拷貝訪問,不會把更改寫入到文件,使用flush把更改寫到
fileno參數需要指定為系統“/dev/mem”的文件描述符,可以通過open()函數和fileno()方法得到,flags、prot、access參數指定為讀寫訪問即可。
length和offset參數比較重要,首先是offset參數,這個參數指示從哪個內存地址開始映射,註意,這個數值必須是頁大小的整數倍,在OrangePi Zero中,頁大小為4096字節。根據datasheet,GPIO的內存地址是從0x01C200800開始,但這個值並不是頁大小的整數倍,所以只能往前截取,最近一個頁大小整數倍的地址是0x01C200000,offset就是要設置為這個值。
那還有0x0800的偏移量怎麽辦呢,這個就通過length參數來設定了,length參數指定了從這個offset開始,映射多少字節的物理內存到用戶程序的內存空間,顯然,這個length必須足夠長把整個gpio模塊的寄存器地址全部映射了,才能在用戶程序裏正常訪問GPIO寄存器,這裏設置為兩個頁大小,即8192字節(0x01C20000 ~ 0x01C21FFF),從datasheet看,這個地址空間包含了CCU、PIO、TIMER、OWA、PWM、KEYADC模塊的所有寄存器。
映射之後,可以得到一個mmap類的對象,使用這個對象,我們可以像操作文件一樣對寄存器進行讀寫操作。在以下模塊的代碼中,實現了兩個方法:讀寄存器和寫寄存器。註意在操作寄存器的過程中,有一點必須註意,每次讀寫寄存器必須四字節對齊,即一次讀取或寫入4個字節(所有寄存器都是32位),讀寫的寄存器地址也必須是4的倍數,否則會操作失敗,板子會死機。
import mmap import struct class GPIO: #-------------------------------------------------------------------------------------# #定義GPIO相對0x01C20000的偏移地址 PIO_ADDR_OFFSET = 0x0800 #定義GPIOA的寄存器相對0x01C20000的偏移地址 #作者只寫了GPIOA的寄存器定義,如果需要使用其他IO,請參考datasheet在下面增加定義 PIO_PA_CFG0_REG = PIO_ADDR_OFFSET + 0x00 PIO_PA_CFG1_REG = PIO_ADDR_OFFSET + 0x04 PIO_PA_CFG2_REG = PIO_ADDR_OFFSET + 0x08 PIO_PA_CFG3_REG = PIO_ADDR_OFFSET + 0x0C PIO_PA_DATA_REG = PIO_ADDR_OFFSET + 0x10 PIO_PA_DRV0_REG = PIO_ADDR_OFFSET + 0x14 PIO_PA_DRV1_REG = PIO_ADDR_OFFSET + 0x18 PIO_PA_PUL0_REG = PIO_ADDR_OFFSET + 0x1C PIO_PA_PUL1_REG = PIO_ADDR_OFFSET + 0x20 #-------------------------------------------------------------------------------------# #以下是構造函數和析構函數 def __init__(self): self.m_mmap = None self.fd = None def __del__(self): if(self.m_mmap != None): self.m_mmap.close() if(self.fd != None): self.fd.close() #-------------------------------------------------------------------------------------# #以下是成員函數 def Init(self): """ GPIO初始化函數 函數會打開/dev/mem文件,並映射從0x01C20000地址開始,共8192字節長度(2頁)的內存空間到用戶的虛擬地址 返回值:無 """ START_ADDR = 0x01C20000 self.fd = open("/dev/mem", "rb+") self.m_mmap = mmap.mmap(self.fd.fileno(), 4096 * 2, mmap.MAP_SHARED, mmap.PROT_WRITE | mmap.PROT_READ, mmap.ACCESS_WRITE, START_ADDR) assert self.m_mmap != None,"Init Fails" def ReadReg(self,reg_addr): """ 讀取一個寄存器的值 reg_addr:要讀取的寄存器地址(必須為4的倍數),且範圍在2個pagesize內,即小於8192 返回值:寄存器的值(4字節) """ assert self.m_mmap != None,"Init Fails" assert reg_addr%4 == 0,"reg_addr must be mutiple of 4" assert 0<=reg_addr<=8192,"reg_addr must be less than 8192,which is 2 pagesize" self.m_mmap.seek(reg_addr) ReadBytes = self.m_mmap.read(4) return struct.unpack(‘L‘,ReadBytes)[0] def WriteReg(self,reg_addr,value): """ 寫一個寄存器的值 reg_addr:要寫入的寄存器地址(必須為4的倍數),且範圍在2個pagesize內,即小於8192 value:要寫入的值,整形,一次寫入四個字節長度的整數,即0xffffffff 返回值:無 """ assert self.m_mmap != None,"Init Fails" assert reg_addr%4 == 0,"reg_addr must be mutiple of 4" assert 0<=reg_addr<=8192,"reg_addr must be less than 8192,which is 2 pagesize" assert 0<=value<=0xFFFFFFFF,"value must be less than 0xFFFFFFFF,which is 4 bytes" self.m_mmap.seek(reg_addr) BytesToWrite = struct.pack(‘L‘,value) self.m_mmap.write(BytesToWrite) return
要使用這個模塊,只需要把這個模塊的py文件放在用戶程序同一個目錄下,直接導入即可,以下是令PA17(紅色LED)閃爍的範例。
說明:
1、OPiZero_GPIO是上面定義的模塊的文件名,直接導入使用即可。
2、PIO_PA_CFG2_REG寄存器的第4~第7位為Pin17的模式配置,配置為001即輸出模式
3、PIO_PA_DATA_REG寄存器的第17位為Pin17的高底電平輸出控制,這裏采用了一個巧妙的方法,讀取PIO_PA_DATA_REG的值與0x00020000按位異或即可實現第17位的取反。
4、切勿直接往寄存器裏寫入數據,因為PA口有很多IO用作板子內部使用,直接寫入的話很容易導致其他IO口邏輯輸出錯誤,導致板子死機,作者已親身體驗n次,務必使用讀-修改-寫的模式修改寄存器的值。
import OPiZero_GPIO import time #以下為主程序 GPIO = OPiZero_GPIO.GPIO() GPIO.Init(); #PA17配置為輸出模式 GPIO.WriteReg(GPIO.PIO_PA_CFG2_REG,GPIO.ReadReg(GPIO.PIO_PA_CFG2_REG) | 0x00000010) while(1): GPIO.WriteReg(GPIO.PIO_PA_DATA_REG,GPIO.ReadReg(GPIO.PIO_PA_DATA_REG) ^ 0x00020000) time.sleep(0.3)
實際效果如下:
最後把源碼附上:
https://files.cnblogs.com/files/qzrzq1/OPiZero_GPIO.zip
https://pan.baidu.com/s/1yiely1q_4LPZ4Bs8gyKDGg
python環境下實現OrangePi Zero寄存器訪問及GPIO控制