標籤:assert key 成員函數 ref timer set port 開始 ini
最近入手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 mmapimport structclass 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_GPIOimport 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控制