bcmio 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bcmio-0.1.0/PKG-INFO +132 -0
- bcmio-0.1.0/README.md +110 -0
- bcmio-0.1.0/bcmio/__init__.py +16 -0
- bcmio-0.1.0/bcmio/constants.py +93 -0
- bcmio-0.1.0/bcmio/exceptions.py +28 -0
- bcmio-0.1.0/bcmio/gpio.py +231 -0
- bcmio-0.1.0/bcmio/interrupts.py +9 -0
- bcmio-0.1.0/bcmio/memory.py +208 -0
- bcmio-0.1.0/bcmio/pin.py +71 -0
- bcmio-0.1.0/bcmio/pwm.py +9 -0
- bcmio-0.1.0/bcmio/utils.py +35 -0
- bcmio-0.1.0/bcmio/utils_logging.py +14 -0
- bcmio-0.1.0/bcmio.egg-info/PKG-INFO +132 -0
- bcmio-0.1.0/bcmio.egg-info/SOURCES.txt +18 -0
- bcmio-0.1.0/bcmio.egg-info/dependency_links.txt +1 -0
- bcmio-0.1.0/bcmio.egg-info/requires.txt +5 -0
- bcmio-0.1.0/bcmio.egg-info/top_level.txt +1 -0
- bcmio-0.1.0/pyproject.toml +46 -0
- bcmio-0.1.0/setup.cfg +4 -0
- bcmio-0.1.0/setup.py +6 -0
bcmio-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bcmio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Low-level GPIO library for Raspberry Pi 4 (BCM2711) using /dev/mem + mmap
|
|
5
|
+
Author: bcmio contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: raspberrypi,gpio,bcm2711,mmap,embedded,linux
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Topic :: System :: Hardware
|
|
15
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
21
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
22
|
+
|
|
23
|
+
# bcmio
|
|
24
|
+
|
|
25
|
+
Biblioteca Python **low-level** para controle de GPIO do **Raspberry Pi 4 Model B (BCM2711)** usando
|
|
26
|
+
**acesso direto a registradores via `mmap`** e **`/dev/mem`** (MMIO).
|
|
27
|
+
|
|
28
|
+
> Aviso: acessar `/dev/mem` normalmente requer **root** (`sudo`). Use com cuidado: MMIO incorreto pode
|
|
29
|
+
> travar o sistema.
|
|
30
|
+
|
|
31
|
+
## Objetivos
|
|
32
|
+
|
|
33
|
+
- Performance: escrita/leitura direto em registradores (`GPSET/GPCLR/GPLEV`)
|
|
34
|
+
- Arquitetura modular: separar low-level (MMIO/registradores) e high-level (abstrações)
|
|
35
|
+
- API familiar (inspirada em RPi.GPIO/pigpio/gpiozero), mas **sem dependências** dessas bibliotecas
|
|
36
|
+
- Código tipado, com docstrings, pronto para evoluir para PWM/SPI/I2C/UART/interrupts/DMA
|
|
37
|
+
|
|
38
|
+
## Arquitetura (módulos)
|
|
39
|
+
|
|
40
|
+
- `bcmio/memory.py`: abertura de `/dev/mem`, `mmap`, leitura/escrita 32-bit
|
|
41
|
+
- `bcmio/constants.py`: offsets e constantes (GPIO modes, pulls, endereços base)
|
|
42
|
+
- `bcmio/gpio.py`: acesso aos registradores GPIO (FSEL, SET/CLR, LEV, PULL)
|
|
43
|
+
- `bcmio/pin.py`: classe `Pin` (alto nível) para um pino individual
|
|
44
|
+
- `bcmio/exceptions.py`: exceções customizadas
|
|
45
|
+
- `bcmio/utils.py`: helpers (validação, bit operations)
|
|
46
|
+
- `bcmio/pwm.py`, `bcmio/interrupts.py`: placeholders para evolução
|
|
47
|
+
|
|
48
|
+
## Como `mmap` funciona (visão geral)
|
|
49
|
+
|
|
50
|
+
1. Abrimos `/dev/mem` (arquivo especial que expõe memória física do SoC).
|
|
51
|
+
2. Fazemos `mmap` de uma página (ou mais) a partir do **endereço físico** dos periféricos GPIO.
|
|
52
|
+
3. A partir do ponteiro mapeado, fazemos leituras/escritas de 32 bits em offsets específicos.
|
|
53
|
+
|
|
54
|
+
No Linux, isso é MMIO (Memory Mapped I/O): escrever em um registrador mapeado altera o hardware.
|
|
55
|
+
|
|
56
|
+
## Endereços base (BCM2711)
|
|
57
|
+
|
|
58
|
+
O datasheet usa endereços no **barramento** (bus) como `0x7E200000` para o bloco GPIO. No Raspberry Pi 4,
|
|
59
|
+
o endereço **físico** tipicamente é `0xFE200000` (peripheral base `0xFE000000` + GPIO offset `0x200000`).
|
|
60
|
+
|
|
61
|
+
Esta biblioteca:
|
|
62
|
+
|
|
63
|
+
- expõe ambos em `bcmio.constants` (`GPIO_BASE_BUS`, `GPIO_BASE_PHYS_DEFAULT`)
|
|
64
|
+
- por padrão usa o **físico** (`0xFE200000`)
|
|
65
|
+
- permite sobrescrever o endereço base via `GPIO.open(base_phys=...)`
|
|
66
|
+
|
|
67
|
+
## Registradores GPIO usados (BCM2711)
|
|
68
|
+
|
|
69
|
+
Offsets relativos ao base do GPIO (bloco `GPIO`):
|
|
70
|
+
|
|
71
|
+
- `GPFSEL0..5` (Function Select): 3 bits por pino para definir `IN`, `OUT`, ou função alternativa
|
|
72
|
+
- `GPSET0..1` (Set): escrever `1` no bit seta o pino em nível alto (atômico)
|
|
73
|
+
- `GPCLR0..1` (Clear): escrever `1` no bit seta o pino em nível baixo (atômico)
|
|
74
|
+
- `GPLEV0..1` (Level): lê o nível atual do pino
|
|
75
|
+
- `GPIO_PUP_PDN_CNTRL_REG0..3`: 2 bits por pino para pull-up/pull-down/no-pull (BCM2711)
|
|
76
|
+
|
|
77
|
+
## Uso
|
|
78
|
+
|
|
79
|
+
### API estilo “módulo” (`GPIO`)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from bcmio import GPIO
|
|
83
|
+
|
|
84
|
+
GPIO.open() # inicializa /dev/mem + mmap
|
|
85
|
+
GPIO.setup(17, GPIO.OUT, pull=GPIO.PULL_NONE)
|
|
86
|
+
GPIO.write(17, GPIO.HIGH)
|
|
87
|
+
value = GPIO.read(17)
|
|
88
|
+
GPIO.cleanup()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### API orientada a objeto (`Pin`)
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from bcmio import Pin
|
|
95
|
+
|
|
96
|
+
led = Pin(17, mode=Pin.OUT)
|
|
97
|
+
led.high()
|
|
98
|
+
led.low()
|
|
99
|
+
led.toggle()
|
|
100
|
+
led.close()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Exemplos
|
|
104
|
+
|
|
105
|
+
Veja `exemplos/`:
|
|
106
|
+
|
|
107
|
+
- `exemplos/blink.py`
|
|
108
|
+
- `exemplos/button_read.py`
|
|
109
|
+
- `exemplos/toggle.py`
|
|
110
|
+
- `exemplos/read_digital.py`
|
|
111
|
+
|
|
112
|
+
## Testes
|
|
113
|
+
|
|
114
|
+
Os testes em `testes/` não acessam `/dev/mem`. Eles usam um backend fake de memória para validar:
|
|
115
|
+
|
|
116
|
+
- cálculo de offsets e bitfields de `GPFSEL`
|
|
117
|
+
- `GPSET/GPCLR` e leitura em `GPLEV`
|
|
118
|
+
- configuração de pull em `GPIO_PUP_PDN_CNTRL_REGx`
|
|
119
|
+
|
|
120
|
+
Rodar:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python -m pip install -e ".[dev]"
|
|
124
|
+
pytest -q
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Segurança e boas práticas
|
|
128
|
+
|
|
129
|
+
- Use `sudo` apenas quando necessário.
|
|
130
|
+
- Prefira isolar e revisar o endereço base antes de usar em produção.
|
|
131
|
+
- Em produção, considere um modo “safe” ou `/dev/gpiomem` (não implementado aqui por requisito).
|
|
132
|
+
|
bcmio-0.1.0/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# bcmio
|
|
2
|
+
|
|
3
|
+
Biblioteca Python **low-level** para controle de GPIO do **Raspberry Pi 4 Model B (BCM2711)** usando
|
|
4
|
+
**acesso direto a registradores via `mmap`** e **`/dev/mem`** (MMIO).
|
|
5
|
+
|
|
6
|
+
> Aviso: acessar `/dev/mem` normalmente requer **root** (`sudo`). Use com cuidado: MMIO incorreto pode
|
|
7
|
+
> travar o sistema.
|
|
8
|
+
|
|
9
|
+
## Objetivos
|
|
10
|
+
|
|
11
|
+
- Performance: escrita/leitura direto em registradores (`GPSET/GPCLR/GPLEV`)
|
|
12
|
+
- Arquitetura modular: separar low-level (MMIO/registradores) e high-level (abstrações)
|
|
13
|
+
- API familiar (inspirada em RPi.GPIO/pigpio/gpiozero), mas **sem dependências** dessas bibliotecas
|
|
14
|
+
- Código tipado, com docstrings, pronto para evoluir para PWM/SPI/I2C/UART/interrupts/DMA
|
|
15
|
+
|
|
16
|
+
## Arquitetura (módulos)
|
|
17
|
+
|
|
18
|
+
- `bcmio/memory.py`: abertura de `/dev/mem`, `mmap`, leitura/escrita 32-bit
|
|
19
|
+
- `bcmio/constants.py`: offsets e constantes (GPIO modes, pulls, endereços base)
|
|
20
|
+
- `bcmio/gpio.py`: acesso aos registradores GPIO (FSEL, SET/CLR, LEV, PULL)
|
|
21
|
+
- `bcmio/pin.py`: classe `Pin` (alto nível) para um pino individual
|
|
22
|
+
- `bcmio/exceptions.py`: exceções customizadas
|
|
23
|
+
- `bcmio/utils.py`: helpers (validação, bit operations)
|
|
24
|
+
- `bcmio/pwm.py`, `bcmio/interrupts.py`: placeholders para evolução
|
|
25
|
+
|
|
26
|
+
## Como `mmap` funciona (visão geral)
|
|
27
|
+
|
|
28
|
+
1. Abrimos `/dev/mem` (arquivo especial que expõe memória física do SoC).
|
|
29
|
+
2. Fazemos `mmap` de uma página (ou mais) a partir do **endereço físico** dos periféricos GPIO.
|
|
30
|
+
3. A partir do ponteiro mapeado, fazemos leituras/escritas de 32 bits em offsets específicos.
|
|
31
|
+
|
|
32
|
+
No Linux, isso é MMIO (Memory Mapped I/O): escrever em um registrador mapeado altera o hardware.
|
|
33
|
+
|
|
34
|
+
## Endereços base (BCM2711)
|
|
35
|
+
|
|
36
|
+
O datasheet usa endereços no **barramento** (bus) como `0x7E200000` para o bloco GPIO. No Raspberry Pi 4,
|
|
37
|
+
o endereço **físico** tipicamente é `0xFE200000` (peripheral base `0xFE000000` + GPIO offset `0x200000`).
|
|
38
|
+
|
|
39
|
+
Esta biblioteca:
|
|
40
|
+
|
|
41
|
+
- expõe ambos em `bcmio.constants` (`GPIO_BASE_BUS`, `GPIO_BASE_PHYS_DEFAULT`)
|
|
42
|
+
- por padrão usa o **físico** (`0xFE200000`)
|
|
43
|
+
- permite sobrescrever o endereço base via `GPIO.open(base_phys=...)`
|
|
44
|
+
|
|
45
|
+
## Registradores GPIO usados (BCM2711)
|
|
46
|
+
|
|
47
|
+
Offsets relativos ao base do GPIO (bloco `GPIO`):
|
|
48
|
+
|
|
49
|
+
- `GPFSEL0..5` (Function Select): 3 bits por pino para definir `IN`, `OUT`, ou função alternativa
|
|
50
|
+
- `GPSET0..1` (Set): escrever `1` no bit seta o pino em nível alto (atômico)
|
|
51
|
+
- `GPCLR0..1` (Clear): escrever `1` no bit seta o pino em nível baixo (atômico)
|
|
52
|
+
- `GPLEV0..1` (Level): lê o nível atual do pino
|
|
53
|
+
- `GPIO_PUP_PDN_CNTRL_REG0..3`: 2 bits por pino para pull-up/pull-down/no-pull (BCM2711)
|
|
54
|
+
|
|
55
|
+
## Uso
|
|
56
|
+
|
|
57
|
+
### API estilo “módulo” (`GPIO`)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from bcmio import GPIO
|
|
61
|
+
|
|
62
|
+
GPIO.open() # inicializa /dev/mem + mmap
|
|
63
|
+
GPIO.setup(17, GPIO.OUT, pull=GPIO.PULL_NONE)
|
|
64
|
+
GPIO.write(17, GPIO.HIGH)
|
|
65
|
+
value = GPIO.read(17)
|
|
66
|
+
GPIO.cleanup()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### API orientada a objeto (`Pin`)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from bcmio import Pin
|
|
73
|
+
|
|
74
|
+
led = Pin(17, mode=Pin.OUT)
|
|
75
|
+
led.high()
|
|
76
|
+
led.low()
|
|
77
|
+
led.toggle()
|
|
78
|
+
led.close()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Exemplos
|
|
82
|
+
|
|
83
|
+
Veja `exemplos/`:
|
|
84
|
+
|
|
85
|
+
- `exemplos/blink.py`
|
|
86
|
+
- `exemplos/button_read.py`
|
|
87
|
+
- `exemplos/toggle.py`
|
|
88
|
+
- `exemplos/read_digital.py`
|
|
89
|
+
|
|
90
|
+
## Testes
|
|
91
|
+
|
|
92
|
+
Os testes em `testes/` não acessam `/dev/mem`. Eles usam um backend fake de memória para validar:
|
|
93
|
+
|
|
94
|
+
- cálculo de offsets e bitfields de `GPFSEL`
|
|
95
|
+
- `GPSET/GPCLR` e leitura em `GPLEV`
|
|
96
|
+
- configuração de pull em `GPIO_PUP_PDN_CNTRL_REGx`
|
|
97
|
+
|
|
98
|
+
Rodar:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python -m pip install -e ".[dev]"
|
|
102
|
+
pytest -q
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Segurança e boas práticas
|
|
106
|
+
|
|
107
|
+
- Use `sudo` apenas quando necessário.
|
|
108
|
+
- Prefira isolar e revisar o endereço base antes de usar em produção.
|
|
109
|
+
- Em produção, considere um modo “safe” ou `/dev/gpiomem` (não implementado aqui por requisito).
|
|
110
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`bcmio` - GPIO low-level for Raspberry Pi 4 (BCM2711) using /dev/mem + mmap.
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
- GPIO: module-style API (class with classmethods)
|
|
6
|
+
- Pin: high-level per-pin abstraction
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .gpio import GPIO
|
|
12
|
+
from .pin import Pin
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
__all__ = ["GPIO", "Pin", "__version__"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants and register offsets for BCM2711 GPIO.
|
|
3
|
+
|
|
4
|
+
Notes about addresses:
|
|
5
|
+
- Datasheets often describe GPIO bus address as 0x7E200000.
|
|
6
|
+
- On Raspberry Pi 4 (BCM2711), physical peripheral base is typically 0xFE000000.
|
|
7
|
+
So GPIO physical base is typically 0xFE200000.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
# --- Base addresses ---
|
|
15
|
+
|
|
16
|
+
# GPIO base address as shown in the peripherals documentation (bus address)
|
|
17
|
+
GPIO_BASE_BUS: int = 0x7E200000
|
|
18
|
+
|
|
19
|
+
# Default physical base address for Raspberry Pi 4 (BCM2711).
|
|
20
|
+
# This is the typical address used with /dev/mem mappings.
|
|
21
|
+
GPIO_BASE_PHYS_DEFAULT: int = 0xFE200000
|
|
22
|
+
|
|
23
|
+
# --- Register offsets (relative to GPIO base) ---
|
|
24
|
+
|
|
25
|
+
GPFSEL0: int = 0x00
|
|
26
|
+
GPFSEL1: int = 0x04
|
|
27
|
+
GPFSEL2: int = 0x08
|
|
28
|
+
GPFSEL3: int = 0x0C
|
|
29
|
+
GPFSEL4: int = 0x10
|
|
30
|
+
GPFSEL5: int = 0x14
|
|
31
|
+
|
|
32
|
+
GPSET0: int = 0x1C
|
|
33
|
+
GPSET1: int = 0x20
|
|
34
|
+
|
|
35
|
+
GPCLR0: int = 0x28
|
|
36
|
+
GPCLR1: int = 0x2C
|
|
37
|
+
|
|
38
|
+
GPLEV0: int = 0x34
|
|
39
|
+
GPLEV1: int = 0x38
|
|
40
|
+
|
|
41
|
+
# BCM2711 pull-up/down control registers (2 bits per GPIO)
|
|
42
|
+
GPIO_PUP_PDN_CNTRL_REG0: int = 0xE4
|
|
43
|
+
GPIO_PUP_PDN_CNTRL_REG1: int = 0xE8
|
|
44
|
+
GPIO_PUP_PDN_CNTRL_REG2: int = 0xEC
|
|
45
|
+
GPIO_PUP_PDN_CNTRL_REG3: int = 0xF0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class PinMode:
|
|
50
|
+
"""GPIO function select values (3-bit fields in GPFSEL registers)."""
|
|
51
|
+
|
|
52
|
+
IN: int = 0b000
|
|
53
|
+
OUT: int = 0b001
|
|
54
|
+
|
|
55
|
+
# Alternate function values (kept for extensibility)
|
|
56
|
+
ALT0: int = 0b100
|
|
57
|
+
ALT1: int = 0b101
|
|
58
|
+
ALT2: int = 0b110
|
|
59
|
+
ALT3: int = 0b111
|
|
60
|
+
ALT4: int = 0b011
|
|
61
|
+
ALT5: int = 0b010
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class LogicLevel:
|
|
66
|
+
"""Logical levels."""
|
|
67
|
+
|
|
68
|
+
LOW: int = 0
|
|
69
|
+
HIGH: int = 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class Pull:
|
|
74
|
+
"""
|
|
75
|
+
Pull configuration values (2-bit fields in GPIO_PUP_PDN_CNTRL_REGx).
|
|
76
|
+
|
|
77
|
+
BCM2711 encoding (per pin):
|
|
78
|
+
00 = No resistor
|
|
79
|
+
01 = Pull-up
|
|
80
|
+
10 = Pull-down
|
|
81
|
+
11 = Reserved
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
NONE: int = 0b00
|
|
85
|
+
UP: int = 0b01
|
|
86
|
+
DOWN: int = 0b10
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
GPIO_MIN: int = 0
|
|
90
|
+
GPIO_MAX: int = 53 # BCM2711 exposes GPIO0..GPIO53 (some are not on header).
|
|
91
|
+
|
|
92
|
+
REGISTER_SIZE_BYTES: int = 4
|
|
93
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Custom exceptions for bcmio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BCMIOError(RuntimeError):
|
|
7
|
+
"""Base error for bcmio."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PermissionDeniedError(BCMIOError):
|
|
11
|
+
"""Raised when /dev/mem cannot be opened due to insufficient permissions."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeviceNotFoundError(BCMIOError):
|
|
15
|
+
"""Raised when expected device file is missing (e.g. /dev/mem)."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NotInitializedError(BCMIOError):
|
|
19
|
+
"""Raised when GPIO is used before initialization/open()."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InvalidPinError(ValueError, BCMIOError):
|
|
23
|
+
"""Raised when a GPIO pin number is invalid."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MMIOMapError(BCMIOError):
|
|
27
|
+
"""Raised when mmap fails or MMIO access cannot be established."""
|
|
28
|
+
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GPIO register access for BCM2711.
|
|
3
|
+
|
|
4
|
+
Implements:
|
|
5
|
+
- function select (input/output)
|
|
6
|
+
- atomic set/clear
|
|
7
|
+
- level read
|
|
8
|
+
- pull-up/pull-down configuration via GPIO_PUP_PDN_CNTRL_REG0..3 (BCM2711)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from threading import RLock
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from .constants import (
|
|
19
|
+
GPIO_BASE_PHYS_DEFAULT,
|
|
20
|
+
GPIO_MAX,
|
|
21
|
+
GPFSEL0,
|
|
22
|
+
GPCLR0,
|
|
23
|
+
GPLEV0,
|
|
24
|
+
GPSET0,
|
|
25
|
+
GPIO_PUP_PDN_CNTRL_REG0,
|
|
26
|
+
LogicLevel,
|
|
27
|
+
PinMode,
|
|
28
|
+
Pull,
|
|
29
|
+
)
|
|
30
|
+
from .exceptions import NotInitializedError
|
|
31
|
+
from .memory import FakeMMIORegion, MemoryBackend, open_gpio_region
|
|
32
|
+
from .utils import bit_mask, replace_field, validate_gpio_pin
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
GPIO_BLOCK_SIZE: int = 0x1000 # enough to cover up to 0xF0 registers
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fsel_reg_and_shift(pin: int) -> tuple[int, int]:
|
|
41
|
+
# Each GPFSEL register controls 10 pins, 3 bits per pin.
|
|
42
|
+
reg_index = pin // 10 # 0..5
|
|
43
|
+
shift = (pin % 10) * 3
|
|
44
|
+
offset = GPFSEL0 + (reg_index * 4)
|
|
45
|
+
return offset, shift
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _setclr_reg_and_bit(pin: int, base_offset: int) -> tuple[int, int]:
|
|
49
|
+
# GPSET0/GPCLR0 cover pins 0..31, GPSET1/GPCLR1 cover 32..53.
|
|
50
|
+
reg_index = pin // 32 # 0 or 1
|
|
51
|
+
bit = pin % 32
|
|
52
|
+
offset = base_offset + (reg_index * 4)
|
|
53
|
+
return offset, bit
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _lev_reg_and_bit(pin: int) -> tuple[int, int]:
|
|
57
|
+
reg_index = pin // 32
|
|
58
|
+
bit = pin % 32
|
|
59
|
+
offset = GPLEV0 + (reg_index * 4)
|
|
60
|
+
return offset, bit
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _pull_reg_and_shift(pin: int) -> tuple[int, int]:
|
|
64
|
+
# BCM2711: 2 bits per pin, 16 pins per 32-bit register.
|
|
65
|
+
reg_index = pin // 16 # 0..3
|
|
66
|
+
shift = (pin % 16) * 2
|
|
67
|
+
offset = GPIO_PUP_PDN_CNTRL_REG0 + (reg_index * 4)
|
|
68
|
+
return offset, shift
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(slots=True)
|
|
72
|
+
class GPIOConfig:
|
|
73
|
+
base_phys: int = GPIO_BASE_PHYS_DEFAULT
|
|
74
|
+
devmem_path: str = "/dev/mem"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class GPIO:
|
|
78
|
+
"""
|
|
79
|
+
Module-style API for GPIO access.
|
|
80
|
+
|
|
81
|
+
This class manages a single global MMIO mapping (per-process) for simplicity.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
IN: int = PinMode().IN
|
|
85
|
+
OUT: int = PinMode().OUT
|
|
86
|
+
|
|
87
|
+
LOW: int = LogicLevel().LOW
|
|
88
|
+
HIGH: int = LogicLevel().HIGH
|
|
89
|
+
|
|
90
|
+
PULL_NONE: int = Pull().NONE
|
|
91
|
+
PULL_UP: int = Pull().UP
|
|
92
|
+
PULL_DOWN: int = Pull().DOWN
|
|
93
|
+
|
|
94
|
+
_lock = RLock()
|
|
95
|
+
_region: Optional[MemoryBackend] = None
|
|
96
|
+
_config: GPIOConfig = GPIOConfig()
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def open(
|
|
100
|
+
cls,
|
|
101
|
+
*,
|
|
102
|
+
base_phys: int = GPIO_BASE_PHYS_DEFAULT,
|
|
103
|
+
devmem_path: str = "/dev/mem",
|
|
104
|
+
backend: Optional[MemoryBackend] = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Initialize MMIO mapping for GPIO.
|
|
108
|
+
|
|
109
|
+
Pass `backend` only for tests (e.g. FakeMMIORegion).
|
|
110
|
+
"""
|
|
111
|
+
with cls._lock:
|
|
112
|
+
if cls._region is not None:
|
|
113
|
+
return
|
|
114
|
+
cls._config = GPIOConfig(base_phys=base_phys, devmem_path=devmem_path)
|
|
115
|
+
if backend is None:
|
|
116
|
+
cls._region = open_gpio_region(base_phys=base_phys, size=GPIO_BLOCK_SIZE, devmem_path=devmem_path)
|
|
117
|
+
else:
|
|
118
|
+
cls._region = open_gpio_region(base_phys=base_phys, size=GPIO_BLOCK_SIZE, backend=backend)
|
|
119
|
+
logger.info("GPIO MMIO opened at phys=0x%X (%s)", base_phys, devmem_path)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def is_open(cls) -> bool:
|
|
123
|
+
return cls._region is not None
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def _require_open(cls) -> MemoryBackend:
|
|
127
|
+
if cls._region is None:
|
|
128
|
+
raise NotInitializedError("GPIO not initialized. Call GPIO.open() first.")
|
|
129
|
+
return cls._region
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def close(cls) -> None:
|
|
133
|
+
"""Unmap and close the MMIO region."""
|
|
134
|
+
with cls._lock:
|
|
135
|
+
if cls._region is None:
|
|
136
|
+
return
|
|
137
|
+
try:
|
|
138
|
+
cls._region.close()
|
|
139
|
+
finally:
|
|
140
|
+
cls._region = None
|
|
141
|
+
logger.info("GPIO MMIO closed")
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def cleanup(cls) -> None:
|
|
145
|
+
"""Alias for close()."""
|
|
146
|
+
cls.close()
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def setup(cls, pin: int, mode: int, *, pull: int = Pull().NONE) -> None:
|
|
150
|
+
"""Configure pin function (IN/OUT) and pull resistor."""
|
|
151
|
+
validate_gpio_pin(pin)
|
|
152
|
+
if mode not in (cls.IN, cls.OUT):
|
|
153
|
+
raise ValueError(f"Invalid mode: {mode}")
|
|
154
|
+
if pull not in (cls.PULL_NONE, cls.PULL_UP, cls.PULL_DOWN):
|
|
155
|
+
raise ValueError(f"Invalid pull: {pull}")
|
|
156
|
+
|
|
157
|
+
with cls._lock:
|
|
158
|
+
region = cls._require_open()
|
|
159
|
+
fsel_off, shift = _fsel_reg_and_shift(pin)
|
|
160
|
+
cur = region.read32(fsel_off)
|
|
161
|
+
cur = replace_field(cur, field_mask=0b111, field_value=mode, shift=shift)
|
|
162
|
+
region.write32(fsel_off, cur)
|
|
163
|
+
|
|
164
|
+
# Pull is independent of mode on BCM2711
|
|
165
|
+
cls._set_pull_locked(region, pin, pull)
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def _set_pull_locked(cls, region: MemoryBackend, pin: int, pull: int) -> None:
|
|
169
|
+
off, shift = _pull_reg_and_shift(pin)
|
|
170
|
+
cur = region.read32(off)
|
|
171
|
+
cur = replace_field(cur, field_mask=0b11, field_value=pull, shift=shift)
|
|
172
|
+
region.write32(off, cur)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def set_pull(cls, pin: int, pull: int) -> None:
|
|
176
|
+
"""Configure pull resistor for a pin (PULL_NONE, PULL_UP, PULL_DOWN)."""
|
|
177
|
+
validate_gpio_pin(pin)
|
|
178
|
+
if pull not in (cls.PULL_NONE, cls.PULL_UP, cls.PULL_DOWN):
|
|
179
|
+
raise ValueError(f"Invalid pull: {pull}")
|
|
180
|
+
with cls._lock:
|
|
181
|
+
region = cls._require_open()
|
|
182
|
+
cls._set_pull_locked(region, pin, pull)
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def write(cls, pin: int, value: int) -> None:
|
|
186
|
+
"""Write a logical level using atomic GPSET/GPCLR registers."""
|
|
187
|
+
validate_gpio_pin(pin)
|
|
188
|
+
if value not in (cls.LOW, cls.HIGH):
|
|
189
|
+
raise ValueError(f"Invalid value: {value}")
|
|
190
|
+
|
|
191
|
+
with cls._lock:
|
|
192
|
+
region = cls._require_open()
|
|
193
|
+
if value == cls.HIGH:
|
|
194
|
+
off, bit = _setclr_reg_and_bit(pin, GPSET0)
|
|
195
|
+
else:
|
|
196
|
+
off, bit = _setclr_reg_and_bit(pin, GPCLR0)
|
|
197
|
+
region.write32(off, bit_mask(bit))
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def high(cls, pin: int) -> None:
|
|
201
|
+
cls.write(pin, cls.HIGH)
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def low(cls, pin: int) -> None:
|
|
205
|
+
cls.write(pin, cls.LOW)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def read(cls, pin: int) -> int:
|
|
209
|
+
"""Read the pin level from GPLEV registers."""
|
|
210
|
+
validate_gpio_pin(pin)
|
|
211
|
+
with cls._lock:
|
|
212
|
+
region = cls._require_open()
|
|
213
|
+
off, bit = _lev_reg_and_bit(pin)
|
|
214
|
+
level = region.read32(off)
|
|
215
|
+
return cls.HIGH if (level & bit_mask(bit)) else cls.LOW
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def toggle(cls, pin: int) -> int:
|
|
219
|
+
"""Toggle pin output based on current level. Returns new level."""
|
|
220
|
+
current = cls.read(pin)
|
|
221
|
+
new = cls.LOW if current == cls.HIGH else cls.HIGH
|
|
222
|
+
cls.write(pin, new)
|
|
223
|
+
return new
|
|
224
|
+
|
|
225
|
+
# --- Convenience for tests ---
|
|
226
|
+
@classmethod
|
|
227
|
+
def _open_fake(cls) -> FakeMMIORegion:
|
|
228
|
+
fake = FakeMMIORegion(GPIO_BLOCK_SIZE)
|
|
229
|
+
cls.open(backend=fake)
|
|
230
|
+
return fake
|
|
231
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Interrupts placeholder (future work)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Interrupts: # pragma: no cover
|
|
7
|
+
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
8
|
+
raise NotImplementedError("Interrupts not implemented yet. Planned module placeholder.")
|
|
9
|
+
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory-mapped I/O helpers for BCM2711 peripherals via /dev/mem.
|
|
3
|
+
|
|
4
|
+
This module intentionally focuses on:
|
|
5
|
+
- opening /dev/mem
|
|
6
|
+
- mapping a physical address range with mmap
|
|
7
|
+
- reading/writing 32-bit little-endian registers
|
|
8
|
+
|
|
9
|
+
It is kept separate from GPIO logic to enable future modules (PWM/SPI/I2C/UART/DMA).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import mmap
|
|
16
|
+
import os
|
|
17
|
+
import struct
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from .exceptions import DeviceNotFoundError, MMIOMapError, PermissionDeniedError
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _page_align(addr: int, page_size: int) -> tuple[int, int]:
|
|
27
|
+
base = addr - (addr % page_size)
|
|
28
|
+
offset = addr - base
|
|
29
|
+
return base, offset
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class MMIORegion:
|
|
34
|
+
"""
|
|
35
|
+
Represents a mapped MMIO region.
|
|
36
|
+
|
|
37
|
+
`base_phys` is the physical base address requested (not page-aligned).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
base_phys: int
|
|
41
|
+
size: int
|
|
42
|
+
_fd: int
|
|
43
|
+
_mmap: mmap.mmap
|
|
44
|
+
_offset: int
|
|
45
|
+
|
|
46
|
+
def close(self) -> None:
|
|
47
|
+
"""Unmap and close the underlying /dev/mem file descriptor."""
|
|
48
|
+
try:
|
|
49
|
+
self._mmap.close()
|
|
50
|
+
finally:
|
|
51
|
+
os.close(self._fd)
|
|
52
|
+
|
|
53
|
+
def read32(self, offset: int) -> int:
|
|
54
|
+
"""Read a 32-bit little-endian value at `offset` within the region."""
|
|
55
|
+
if offset < 0 or offset + 4 > self.size:
|
|
56
|
+
raise MMIOMapError(f"read32 offset out of range: 0x{offset:X}")
|
|
57
|
+
pos = self._offset + offset
|
|
58
|
+
data = self._mmap[pos : pos + 4]
|
|
59
|
+
return struct.unpack_from("<I", data)[0]
|
|
60
|
+
|
|
61
|
+
def write32(self, offset: int, value: int) -> None:
|
|
62
|
+
"""Write a 32-bit little-endian value at `offset` within the region."""
|
|
63
|
+
if offset < 0 or offset + 4 > self.size:
|
|
64
|
+
raise MMIOMapError(f"write32 offset out of range: 0x{offset:X}")
|
|
65
|
+
pos = self._offset + offset
|
|
66
|
+
self._mmap[pos : pos + 4] = struct.pack("<I", value & 0xFFFFFFFF)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MemoryMapper:
|
|
70
|
+
"""
|
|
71
|
+
Maps physical peripheral registers through /dev/mem.
|
|
72
|
+
|
|
73
|
+
Typical usage:
|
|
74
|
+
mapper = MemoryMapper()
|
|
75
|
+
region = mapper.map_region(0xFE200000, 0x1000) # GPIO block
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, devmem_path: str = "/dev/mem") -> None:
|
|
79
|
+
self._devmem_path = devmem_path
|
|
80
|
+
|
|
81
|
+
def map_region(self, base_phys: int, size: int) -> MMIORegion:
|
|
82
|
+
"""
|
|
83
|
+
Map a physical address range.
|
|
84
|
+
|
|
85
|
+
`size` should include all needed registers; it will be rounded up to page size.
|
|
86
|
+
"""
|
|
87
|
+
if size <= 0:
|
|
88
|
+
raise ValueError("size must be > 0")
|
|
89
|
+
|
|
90
|
+
page_size = mmap.PAGESIZE
|
|
91
|
+
aligned_base, page_offset = _page_align(base_phys, page_size)
|
|
92
|
+
mapped_size = ((page_offset + size + page_size - 1) // page_size) * page_size
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
fd = os.open(self._devmem_path, os.O_RDWR | os.O_SYNC)
|
|
96
|
+
except FileNotFoundError as e:
|
|
97
|
+
raise DeviceNotFoundError(f"Device not found: {self._devmem_path}") from e
|
|
98
|
+
except PermissionError as e:
|
|
99
|
+
raise PermissionDeniedError(
|
|
100
|
+
f"Permission denied opening {self._devmem_path}. Try running as root (sudo)."
|
|
101
|
+
) from e
|
|
102
|
+
except OSError as e:
|
|
103
|
+
raise MMIOMapError(f"Failed opening {self._devmem_path}: {e}") from e
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
mm = mmap.mmap(
|
|
107
|
+
fd,
|
|
108
|
+
mapped_size,
|
|
109
|
+
flags=mmap.MAP_SHARED,
|
|
110
|
+
prot=mmap.PROT_READ | mmap.PROT_WRITE,
|
|
111
|
+
offset=aligned_base,
|
|
112
|
+
)
|
|
113
|
+
except OSError as e:
|
|
114
|
+
os.close(fd)
|
|
115
|
+
raise MMIOMapError(
|
|
116
|
+
f"mmap failed for phys=0x{base_phys:X} aligned=0x{aligned_base:X} size=0x{mapped_size:X}: {e}"
|
|
117
|
+
) from e
|
|
118
|
+
|
|
119
|
+
logger.debug(
|
|
120
|
+
"Mapped %s: phys=0x%X aligned=0x%X size=0x%X offset=0x%X",
|
|
121
|
+
self._devmem_path,
|
|
122
|
+
base_phys,
|
|
123
|
+
aligned_base,
|
|
124
|
+
mapped_size,
|
|
125
|
+
page_offset,
|
|
126
|
+
)
|
|
127
|
+
return MMIORegion(base_phys=base_phys, size=size, _fd=fd, _mmap=mm, _offset=page_offset)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class FakeMMIORegion:
|
|
131
|
+
"""
|
|
132
|
+
Test helper: in-memory fake MMIO region implementing read32/write32.
|
|
133
|
+
|
|
134
|
+
This is intentionally simple and NOT thread-safe.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, size: int) -> None:
|
|
138
|
+
self.size = size
|
|
139
|
+
self._buf = bytearray(size)
|
|
140
|
+
|
|
141
|
+
def close(self) -> None: # pragma: no cover
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
def read32(self, offset: int) -> int:
|
|
145
|
+
if offset < 0 or offset + 4 > self.size:
|
|
146
|
+
raise MMIOMapError(f"read32 offset out of range: 0x{offset:X}")
|
|
147
|
+
return struct.unpack_from("<I", self._buf, offset)[0]
|
|
148
|
+
|
|
149
|
+
def write32(self, offset: int, value: int) -> None:
|
|
150
|
+
if offset < 0 or offset + 4 > self.size:
|
|
151
|
+
raise MMIOMapError(f"write32 offset out of range: 0x{offset:X}")
|
|
152
|
+
struct.pack_into("<I", self._buf, offset, value & 0xFFFFFFFF)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class MemoryBackend:
|
|
156
|
+
"""
|
|
157
|
+
Protocol-like base for memory backends.
|
|
158
|
+
|
|
159
|
+
Any backend must implement:
|
|
160
|
+
- read32(offset) -> int
|
|
161
|
+
- write32(offset, value) -> None
|
|
162
|
+
- close() -> None
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def read32(self, offset: int) -> int: # pragma: no cover
|
|
166
|
+
raise NotImplementedError
|
|
167
|
+
|
|
168
|
+
def write32(self, offset: int, value: int) -> None: # pragma: no cover
|
|
169
|
+
raise NotImplementedError
|
|
170
|
+
|
|
171
|
+
def close(self) -> None: # pragma: no cover
|
|
172
|
+
raise NotImplementedError
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class GPIORegion(MemoryBackend):
|
|
176
|
+
"""
|
|
177
|
+
A small wrapper to hold the actual MMIORegion (or FakeMMIORegion).
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(self, region: MemoryBackend) -> None:
|
|
181
|
+
self._region = region
|
|
182
|
+
|
|
183
|
+
def read32(self, offset: int) -> int:
|
|
184
|
+
return self._region.read32(offset)
|
|
185
|
+
|
|
186
|
+
def write32(self, offset: int, value: int) -> None:
|
|
187
|
+
self._region.write32(offset, value)
|
|
188
|
+
|
|
189
|
+
def close(self) -> None:
|
|
190
|
+
self._region.close()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def open_gpio_region(
|
|
194
|
+
*,
|
|
195
|
+
base_phys: int,
|
|
196
|
+
size: int,
|
|
197
|
+
devmem_path: str = "/dev/mem",
|
|
198
|
+
backend: Optional[MemoryBackend] = None,
|
|
199
|
+
) -> GPIORegion:
|
|
200
|
+
"""
|
|
201
|
+
Factory to open the GPIO MMIO region.
|
|
202
|
+
|
|
203
|
+
If `backend` is provided, it will be used (useful for tests).
|
|
204
|
+
"""
|
|
205
|
+
if backend is not None:
|
|
206
|
+
return GPIORegion(backend)
|
|
207
|
+
mapper = MemoryMapper(devmem_path=devmem_path)
|
|
208
|
+
return GPIORegion(mapper.map_region(base_phys=base_phys, size=size))
|
bcmio-0.1.0/bcmio/pin.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""High-level Pin abstraction built on top of bcmio.gpio.GPIO."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .gpio import GPIO
|
|
9
|
+
from .utils import validate_gpio_pin
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Pin:
|
|
15
|
+
"""
|
|
16
|
+
High-level representation of a single GPIO pin.
|
|
17
|
+
|
|
18
|
+
This class uses the global `GPIO` mapping under the hood for performance.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
IN: int = GPIO.IN
|
|
22
|
+
OUT: int = GPIO.OUT
|
|
23
|
+
LOW: int = GPIO.LOW
|
|
24
|
+
HIGH: int = GPIO.HIGH
|
|
25
|
+
PULL_NONE: int = GPIO.PULL_NONE
|
|
26
|
+
PULL_UP: int = GPIO.PULL_UP
|
|
27
|
+
PULL_DOWN: int = GPIO.PULL_DOWN
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
pin: int,
|
|
32
|
+
*,
|
|
33
|
+
mode: int = OUT,
|
|
34
|
+
pull: int = PULL_NONE,
|
|
35
|
+
initial: Optional[int] = None,
|
|
36
|
+
auto_open: bool = True,
|
|
37
|
+
) -> None:
|
|
38
|
+
validate_gpio_pin(pin)
|
|
39
|
+
self.pin = pin
|
|
40
|
+
self.mode = mode
|
|
41
|
+
self.pull = pull
|
|
42
|
+
self.initial = initial
|
|
43
|
+
self.auto_open = auto_open
|
|
44
|
+
|
|
45
|
+
if self.auto_open and not GPIO.is_open():
|
|
46
|
+
GPIO.open()
|
|
47
|
+
GPIO.setup(self.pin, self.mode, pull=self.pull)
|
|
48
|
+
if self.initial is not None:
|
|
49
|
+
GPIO.write(self.pin, self.initial)
|
|
50
|
+
|
|
51
|
+
def read(self) -> int:
|
|
52
|
+
return GPIO.read(self.pin)
|
|
53
|
+
|
|
54
|
+
def write(self, value: int) -> None:
|
|
55
|
+
GPIO.write(self.pin, value)
|
|
56
|
+
|
|
57
|
+
def high(self) -> None:
|
|
58
|
+
GPIO.high(self.pin)
|
|
59
|
+
|
|
60
|
+
def low(self) -> None:
|
|
61
|
+
GPIO.low(self.pin)
|
|
62
|
+
|
|
63
|
+
def toggle(self) -> int:
|
|
64
|
+
return GPIO.toggle(self.pin)
|
|
65
|
+
|
|
66
|
+
def set_pull(self, pull: int) -> None:
|
|
67
|
+
GPIO.set_pull(self.pin, pull)
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
"""Close is intentionally a no-op for now (shared GPIO mapping)."""
|
|
71
|
+
logger.debug("Pin.close() called for GPIO%d (shared mapping left open)", self.pin)
|
bcmio-0.1.0/bcmio/pwm.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Utility helpers (validation, bit operations)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .constants import GPIO_MAX, GPIO_MIN
|
|
6
|
+
from .exceptions import InvalidPinError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_gpio_pin(pin: int) -> None:
|
|
10
|
+
"""Validate that `pin` is within the BCM2711 GPIO range."""
|
|
11
|
+
if not isinstance(pin, int):
|
|
12
|
+
raise InvalidPinError(f"GPIO pin must be int, got {type(pin).__name__}")
|
|
13
|
+
if pin < GPIO_MIN or pin > GPIO_MAX:
|
|
14
|
+
raise InvalidPinError(f"Invalid GPIO pin: {pin} (valid range {GPIO_MIN}..{GPIO_MAX})")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def bit_mask(bit: int) -> int:
|
|
18
|
+
"""Return a 32-bit mask with a single bit set."""
|
|
19
|
+
return 1 << bit
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def set_bits(value: int, mask: int) -> int:
|
|
23
|
+
return value | mask
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def clear_bits(value: int, mask: int) -> int:
|
|
27
|
+
return value & ~mask
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def replace_field(value: int, field_mask: int, field_value: int, shift: int) -> int:
|
|
31
|
+
"""Replace a bitfield defined by mask+shift with a new value."""
|
|
32
|
+
value &= ~(field_mask << shift)
|
|
33
|
+
value |= (field_value & field_mask) << shift
|
|
34
|
+
return value
|
|
35
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Basic logging setup helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def configure_logging(level: int = logging.INFO) -> None:
|
|
9
|
+
"""Configure a basic root logger formatting."""
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=level,
|
|
12
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
13
|
+
)
|
|
14
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bcmio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Low-level GPIO library for Raspberry Pi 4 (BCM2711) using /dev/mem + mmap
|
|
5
|
+
Author: bcmio contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: raspberrypi,gpio,bcm2711,mmap,embedded,linux
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Topic :: System :: Hardware
|
|
15
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
21
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
22
|
+
|
|
23
|
+
# bcmio
|
|
24
|
+
|
|
25
|
+
Biblioteca Python **low-level** para controle de GPIO do **Raspberry Pi 4 Model B (BCM2711)** usando
|
|
26
|
+
**acesso direto a registradores via `mmap`** e **`/dev/mem`** (MMIO).
|
|
27
|
+
|
|
28
|
+
> Aviso: acessar `/dev/mem` normalmente requer **root** (`sudo`). Use com cuidado: MMIO incorreto pode
|
|
29
|
+
> travar o sistema.
|
|
30
|
+
|
|
31
|
+
## Objetivos
|
|
32
|
+
|
|
33
|
+
- Performance: escrita/leitura direto em registradores (`GPSET/GPCLR/GPLEV`)
|
|
34
|
+
- Arquitetura modular: separar low-level (MMIO/registradores) e high-level (abstrações)
|
|
35
|
+
- API familiar (inspirada em RPi.GPIO/pigpio/gpiozero), mas **sem dependências** dessas bibliotecas
|
|
36
|
+
- Código tipado, com docstrings, pronto para evoluir para PWM/SPI/I2C/UART/interrupts/DMA
|
|
37
|
+
|
|
38
|
+
## Arquitetura (módulos)
|
|
39
|
+
|
|
40
|
+
- `bcmio/memory.py`: abertura de `/dev/mem`, `mmap`, leitura/escrita 32-bit
|
|
41
|
+
- `bcmio/constants.py`: offsets e constantes (GPIO modes, pulls, endereços base)
|
|
42
|
+
- `bcmio/gpio.py`: acesso aos registradores GPIO (FSEL, SET/CLR, LEV, PULL)
|
|
43
|
+
- `bcmio/pin.py`: classe `Pin` (alto nível) para um pino individual
|
|
44
|
+
- `bcmio/exceptions.py`: exceções customizadas
|
|
45
|
+
- `bcmio/utils.py`: helpers (validação, bit operations)
|
|
46
|
+
- `bcmio/pwm.py`, `bcmio/interrupts.py`: placeholders para evolução
|
|
47
|
+
|
|
48
|
+
## Como `mmap` funciona (visão geral)
|
|
49
|
+
|
|
50
|
+
1. Abrimos `/dev/mem` (arquivo especial que expõe memória física do SoC).
|
|
51
|
+
2. Fazemos `mmap` de uma página (ou mais) a partir do **endereço físico** dos periféricos GPIO.
|
|
52
|
+
3. A partir do ponteiro mapeado, fazemos leituras/escritas de 32 bits em offsets específicos.
|
|
53
|
+
|
|
54
|
+
No Linux, isso é MMIO (Memory Mapped I/O): escrever em um registrador mapeado altera o hardware.
|
|
55
|
+
|
|
56
|
+
## Endereços base (BCM2711)
|
|
57
|
+
|
|
58
|
+
O datasheet usa endereços no **barramento** (bus) como `0x7E200000` para o bloco GPIO. No Raspberry Pi 4,
|
|
59
|
+
o endereço **físico** tipicamente é `0xFE200000` (peripheral base `0xFE000000` + GPIO offset `0x200000`).
|
|
60
|
+
|
|
61
|
+
Esta biblioteca:
|
|
62
|
+
|
|
63
|
+
- expõe ambos em `bcmio.constants` (`GPIO_BASE_BUS`, `GPIO_BASE_PHYS_DEFAULT`)
|
|
64
|
+
- por padrão usa o **físico** (`0xFE200000`)
|
|
65
|
+
- permite sobrescrever o endereço base via `GPIO.open(base_phys=...)`
|
|
66
|
+
|
|
67
|
+
## Registradores GPIO usados (BCM2711)
|
|
68
|
+
|
|
69
|
+
Offsets relativos ao base do GPIO (bloco `GPIO`):
|
|
70
|
+
|
|
71
|
+
- `GPFSEL0..5` (Function Select): 3 bits por pino para definir `IN`, `OUT`, ou função alternativa
|
|
72
|
+
- `GPSET0..1` (Set): escrever `1` no bit seta o pino em nível alto (atômico)
|
|
73
|
+
- `GPCLR0..1` (Clear): escrever `1` no bit seta o pino em nível baixo (atômico)
|
|
74
|
+
- `GPLEV0..1` (Level): lê o nível atual do pino
|
|
75
|
+
- `GPIO_PUP_PDN_CNTRL_REG0..3`: 2 bits por pino para pull-up/pull-down/no-pull (BCM2711)
|
|
76
|
+
|
|
77
|
+
## Uso
|
|
78
|
+
|
|
79
|
+
### API estilo “módulo” (`GPIO`)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from bcmio import GPIO
|
|
83
|
+
|
|
84
|
+
GPIO.open() # inicializa /dev/mem + mmap
|
|
85
|
+
GPIO.setup(17, GPIO.OUT, pull=GPIO.PULL_NONE)
|
|
86
|
+
GPIO.write(17, GPIO.HIGH)
|
|
87
|
+
value = GPIO.read(17)
|
|
88
|
+
GPIO.cleanup()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### API orientada a objeto (`Pin`)
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from bcmio import Pin
|
|
95
|
+
|
|
96
|
+
led = Pin(17, mode=Pin.OUT)
|
|
97
|
+
led.high()
|
|
98
|
+
led.low()
|
|
99
|
+
led.toggle()
|
|
100
|
+
led.close()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Exemplos
|
|
104
|
+
|
|
105
|
+
Veja `exemplos/`:
|
|
106
|
+
|
|
107
|
+
- `exemplos/blink.py`
|
|
108
|
+
- `exemplos/button_read.py`
|
|
109
|
+
- `exemplos/toggle.py`
|
|
110
|
+
- `exemplos/read_digital.py`
|
|
111
|
+
|
|
112
|
+
## Testes
|
|
113
|
+
|
|
114
|
+
Os testes em `testes/` não acessam `/dev/mem`. Eles usam um backend fake de memória para validar:
|
|
115
|
+
|
|
116
|
+
- cálculo de offsets e bitfields de `GPFSEL`
|
|
117
|
+
- `GPSET/GPCLR` e leitura em `GPLEV`
|
|
118
|
+
- configuração de pull em `GPIO_PUP_PDN_CNTRL_REGx`
|
|
119
|
+
|
|
120
|
+
Rodar:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python -m pip install -e ".[dev]"
|
|
124
|
+
pytest -q
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Segurança e boas práticas
|
|
128
|
+
|
|
129
|
+
- Use `sudo` apenas quando necessário.
|
|
130
|
+
- Prefira isolar e revisar o endereço base antes de usar em produção.
|
|
131
|
+
- Em produção, considere um modo “safe” ou `/dev/gpiomem` (não implementado aqui por requisito).
|
|
132
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
bcmio/__init__.py
|
|
5
|
+
bcmio/constants.py
|
|
6
|
+
bcmio/exceptions.py
|
|
7
|
+
bcmio/gpio.py
|
|
8
|
+
bcmio/interrupts.py
|
|
9
|
+
bcmio/memory.py
|
|
10
|
+
bcmio/pin.py
|
|
11
|
+
bcmio/pwm.py
|
|
12
|
+
bcmio/utils.py
|
|
13
|
+
bcmio/utils_logging.py
|
|
14
|
+
bcmio.egg-info/PKG-INFO
|
|
15
|
+
bcmio.egg-info/SOURCES.txt
|
|
16
|
+
bcmio.egg-info/dependency_links.txt
|
|
17
|
+
bcmio.egg-info/requires.txt
|
|
18
|
+
bcmio.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bcmio
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bcmio"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Low-level GPIO library for Raspberry Pi 4 (BCM2711) using /dev/mem + mmap"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "bcmio contributors" }]
|
|
13
|
+
keywords = ["raspberrypi", "gpio", "bcm2711", "mmap", "embedded", "linux"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Topic :: System :: Hardware",
|
|
22
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=8", "ruff>=0.5", "mypy>=1.10"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["bcmio"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 100
|
|
33
|
+
target-version = "py310"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["E", "F", "I", "B", "UP", "SIM"]
|
|
37
|
+
|
|
38
|
+
[tool.mypy]
|
|
39
|
+
python_version = "3.10"
|
|
40
|
+
warn_unused_ignores = true
|
|
41
|
+
warn_redundant_casts = true
|
|
42
|
+
warn_unused_configs = true
|
|
43
|
+
disallow_untyped_defs = true
|
|
44
|
+
check_untyped_defs = true
|
|
45
|
+
no_implicit_optional = true
|
|
46
|
+
|
bcmio-0.1.0/setup.cfg
ADDED