feat(firmware): add GPIO expander and keypad drivers

This commit is contained in:
2026-07-02 23:32:27 +02:00
parent 5ed0412c67
commit 7b31618199
18 changed files with 992 additions and 19 deletions
+161
View File
@@ -0,0 +1,161 @@
# Firmware Debug
## Purpose
This document describes the debugging methods used during the development of the OpenParcelBox firmware.
The objective is to provide simple diagnostic tools that work even when a debugger is unavailable.
---
# Debug Methods
The firmware currently supports the following debugging methods:
- UART console (`printf`)
- On-board RGB LED
- Zephyr Shell (when enabled)
Additional methods may be added later:
- Zigbee diagnostics
- Home Assistant diagnostic entities
- NFC diagnostics
---
# UART Console
The UART console is the primary debugging interface during firmware development.
Example:
```c
printf("Initialization completed.\n");
printf("Key pressed: %c\n", key);
```
UART should be preferred whenever a serial connection is available.
---
# RGB LED Status Codes
The onboard RGB LED is used to quickly identify the firmware state.
## Startup
| Color | Meaning |
| ------- | ----------------------------------- |
| Red | Firmware initialization in progress |
| Green | Firmware initialized successfully |
| Magenta | Initialization failed |
---
## Runtime
| Color | Meaning |
| ------- | ------------------------- |
| Green | Idle / Ready |
| Blue | User interaction detected |
| Yellow | Warning |
| Cyan | Reserved |
| White | Reserved |
| Red | Reserved |
| Magenta | Fatal error |
---
# Current Diagnostic Sequence
Current startup sequence:
1. Initialize RGB LED
2. Turn LED Red
3. Initialize peripherals
4. If initialization fails:
- Turn LED Magenta
- Stop execution
5. Turn LED Green
6. Start main application loop
---
# UART Examples
Keypad:
```text
Key pressed: 1
Key pressed: 5
Key pressed: #
```
Future lock module:
```text
Opening lock...
Lock opened.
```
Future NFC module:
```text
NFC tag detected.
UID: xx xx xx xx
```
---
# Zephyr Shell
The Zephyr Shell is enabled during development.
Typical commands:
```text
uart:~$ i2c scan i2c@40004000
```
This allows verification of:
- I²C devices
- GPIO configuration
- Driver availability
The shell is intended for development only and may be disabled in production builds.
---
# Production Firmware
The production firmware should not rely on the UART console.
Diagnostics should instead use:
- RGB LED
- Zigbee diagnostic messages
- Home Assistant entities
- Error reporting through the application
---
# Future Improvements
Planned debugging features:
- Configurable log levels
- Persistent error codes
- Diagnostic mode
- Self-test during startup
- Hardware validation report
- Watchdog diagnostics
---
# Notes
Debugging features should remain lightweight to minimize firmware size and power consumption.
Development-only diagnostics should be removable without affecting the application logic.
+82
View File
@@ -0,0 +1,82 @@
# GPIO Expander
## Purpose
This module provides a hardware abstraction layer (HAL) for the external GPIO expander.
The current implementation targets the PCF8574 connected over the I²C bus. Other GPIO expanders (PCF8575, MCP23017, etc.) can later be supported by modifying only this module.
All higher-level modules (keypad, lock, etc.) must use this API instead of directly accessing the I²C bus.
---
## Development Hardware
- Seeed Studio XIAO nRF52840
- PCF8574
- I²C address: `0x20`
---
## Public API
```c
int gpio_expander_init(void);
int gpio_expander_read_port(uint8_t *value);
int gpio_expander_write_port(uint8_t value);
bool gpio_expander_read_pin(uint8_t pin);
int gpio_expander_write_pin(uint8_t pin, bool state);
int gpio_expander_update_port(uint8_t mask, uint8_t value);
```
---
## Design Goals
- Hide the hardware implementation.
- Avoid direct I²C access from application modules.
- Allow replacing the GPIO expander without modifying the rest of the firmware.
---
## Current Implementation
- PCF8574
- 8 GPIO
- Quasi-bidirectional outputs
- I²C communication
---
## Future Improvements
Possible future implementations:
- PCF8575
- MCP23008
- MCP23017
Only `gpio_expander.c` should require modifications.
---
## Used By
- Keypad
- Future lock module (if additional GPIO are required)
- Future peripherals connected through an external GPIO expander
---
## Notes
The PCF8574 uses quasi-bidirectional GPIOs.
A pin must be written HIGH before it can be used as an input.
This behavior is fully handled by this module and should remain transparent to higher-level drivers.
+80
View File
@@ -0,0 +1,80 @@
# Keypad
## Purpose
This module manages the matrix keypad used by OpenParcelBox.
The current implementation targets the development hardware based on a Freenove 4x4 keypad connected through a PCF8574 I²C GPIO expander.
The production hardware will use a custom 2x6 keypad. The scanning algorithm will remain identical. Only the hardware mapping and key table will change.
---
## Development Hardware
- Seeed Studio XIAO nRF52840
- Freenove 4x4 Matrix Keypad
- PCF8574 GPIO Expander
- I²C address: `0x20`
---
## Wiring
| PCF8574 | Function |
| ------- | -------- |
| P0 | Row 1 |
| P1 | Row 2 |
| P2 | Row 3 |
| P3 | Row 4 |
| P4 | Column 1 |
| P5 | Column 2 |
| P6 | Column 3 |
| P7 | Column 4 |
---
## Key Mapping
| | Col 1 | Col 2 | Col 3 | Col 4 |
| ----- | ----- | ----- | ----- | ----- |
| Row 1 | 1 | 2 | 3 | A |
| Row 2 | 4 | 5 | 6 | B |
| Row 3 | 7 | 8 | 9 | C |
| Row 4 | * | 0 | # | D |
---
## Public API
```c
int keypad_init(void);
char keypad_get_key(void);
```
`keypad_get_key()` returns:
- `0` if no key is available.
- The ASCII value of the pressed key.
Each key press is reported only once.
---
## Current Features
- Matrix scanning
- Debounce
- Single key detection
- Anti-repeat
- Modular GPIO Expander interface
---
## Planned Improvements
- Support for the production 2x6 keypad.
- Configurable key mapping.
- Optional audible feedback handled by the buzzer module.
- Unit tests.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

+3 -1
View File
@@ -6,5 +6,7 @@ project(OpenParcelBox)
target_sources(app PRIVATE
src/main.c
src/board.c
src/led.c
src/gpio_expander.c
src/keypad.c
)
+11
View File
@@ -0,0 +1,11 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
*
* Development board configuration.
*/
&i2c1 {
status = "okay";
};
+45 -11
View File
@@ -1,18 +1,52 @@
# Contraintes application
# Firmware Constraints
## Technologies
- Développement avec Flutter.
- Compatibilité Android prioritaire.
- Compatibilité iOS souhaitée mais non bloquante pour la V1.
- Development with Zephyr RTOS.
- Programming language: C17.
- Target board: Seeed Studio XIAO nRF52840.
- Build system: CMake + West.
- All source code comments must be written in English.
- The firmware must compile without warnings whenever possible.
## Fonctionnement
## Architecture
- Utilisation locale prioritaire.
- Interface simple pour les utilisateurs non techniques.
- Modular architecture.
- One module per hardware peripheral or functional block.
- Hardware abstraction must be separated from business logic.
- All GPIO assignments must be centralized in `board_config.h`.
- Hardware initialization must be centralized in the board module.
## Fonctionnalités
## Hardware
- Gestion des accès.
- Gestion des codes.
- Consultation de l'état de la boîte.
- Battery-powered operation.
- Minimize power consumption whenever possible.
- GPIO assignments must match the hardware documentation.
- DeviceTree must be used for hardware configuration whenever possible.
- I²C peripherals must use Zephyr drivers.
## Features
- Matrix keypad management.
- Electronic lock control.
- Lock state monitoring.
- PWM buzzer control.
- PCF8574 I/O expander support.
- NFC support.
- Zigbee communication.
- Battery level monitoring.
- LED status management.
## Development Rules
- Develop one hardware module at a time.
- Each module must compile successfully before starting the next one.
- Validate each hardware feature on the development board before integration.
- Avoid unused source files.
- Keep the project structure clean and scalable.
## Documentation
- Every public function must be documented.
- Complex algorithms must include explanatory comments in English.
- Keep the firmware synchronized with the project documentation.
+4 -7
View File
@@ -1,10 +1,7 @@
CONFIG_GPIO=y
CONFIG_I2C=y
CONFIG_PWM=y
CONFIG_SERIAL=y
CONFIG_I2C_SHELL=y
CONFIG_SHELL=y
CONFIG_CONSOLE=y
CONFIG_LOG=y
CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_LOG_DEFAULT_LEVEL=3
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
View File
+26
View File
@@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Board pin definitions.
*/
#ifndef BOARD_PINS_H
#define BOARD_PINS_H
#include <zephyr/devicetree.h>
/* RGB LED */
#define LED_RED_NODE DT_ALIAS(led0)
#define LED_GREEN_NODE DT_ALIAS(led1)
#define LED_BLUE_NODE DT_ALIAS(led2)
/* GPIO Expander */
#define GPIO_EXPANDER_I2C_NODE DT_NODELABEL(i2c1)
#define GPIO_EXPANDER_ADDRESS 0x20
#endif /* BOARD_PINS_H */
+99
View File
@@ -0,0 +1,99 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* GPIO expander abstraction layer.
*
* Current implementation:
* PCF8574 connected on I2C.
*/
#include "gpio_expander.h"
#include "board_pins.h"
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <stdbool.h>
#include <stdint.h>
/* --------------------------------------------------------------------------
* Static variables
* -------------------------------------------------------------------------- */
static const struct device *i2c_dev = DEVICE_DT_GET(GPIO_EXPANDER_I2C_NODE);
/*
* Current GPIO output state.
*
* The PCF8574 uses quasi-bidirectional GPIOs.
* Pins intended to be used as inputs must remain HIGH.
*/
static uint8_t gpio_state = 0xFF;
/* --------------------------------------------------------------------------
* Public API
* -------------------------------------------------------------------------- */
int gpio_expander_init(void) {
if (!device_is_ready(i2c_dev)) {
return -1;
}
gpio_state = 0xFF;
return gpio_expander_write_port(gpio_state);
}
int gpio_expander_read_port(uint8_t *value) {
if (value == NULL) {
return -1;
}
return i2c_read(i2c_dev, value, sizeof(*value), GPIO_EXPANDER_ADDRESS);
}
int gpio_expander_write_port(uint8_t value) {
gpio_state = value;
return i2c_write(i2c_dev, &gpio_state, sizeof(gpio_state),
GPIO_EXPANDER_ADDRESS);
}
bool gpio_expander_read_pin(uint8_t pin) {
uint8_t value;
if (pin > 7U) {
return false;
}
if (gpio_expander_read_port(&value) != 0) {
return false;
}
return (value & (1U << pin)) != 0U;
}
int gpio_expander_write_pin(uint8_t pin, bool state) {
if (pin > 7U) {
return -1;
}
if (state) {
gpio_state |= (1U << pin);
} else {
gpio_state &= ~(1U << pin);
}
return gpio_expander_write_port(gpio_state);
}
int gpio_expander_update_port(uint8_t mask, uint8_t value) {
gpio_state &= ~mask;
gpio_state |= (value & mask);
return gpio_expander_write_port(gpio_state);
}
+88
View File
@@ -0,0 +1,88 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* GPIO expander abstraction layer.
*/
#ifndef GPIO_EXPANDER_H
#define GPIO_EXPANDER_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
/**
* @brief Initialize the GPIO expander.
*
* Initializes the external GPIO expander used by the firmware.
*
* @retval 0 Success.
* @retval <0 Initialization failed.
*/
int gpio_expander_init(void);
/**
* @brief Read the complete GPIO port.
*
* @param[out] value Pointer receiving the GPIO state.
*
* @retval 0 Success.
* @retval <0 Read failed.
*/
int gpio_expander_read_port(uint8_t *value);
/**
* @brief Write the complete GPIO port.
*
* @param value GPIO output state.
*
* @retval 0 Success.
* @retval <0 Write failed.
*/
int gpio_expander_write_port(uint8_t value);
/**
* @brief Read a single GPIO pin.
*
* @param pin GPIO pin number.
*
* @retval true Pin is HIGH.
* @retval false Pin is LOW or an error occurred.
*/
bool gpio_expander_read_pin(uint8_t pin);
/**
* @brief Write a single GPIO pin.
*
* @param pin GPIO pin number.
* @param state Desired pin state.
*
* @retval 0 Success.
* @retval <0 Write failed.
*/
int gpio_expander_write_pin(uint8_t pin, bool state);
/**
* @brief Update selected GPIO pins.
*
* Only the bits specified by the mask are modified.
*
* @param mask GPIO selection mask.
* @param value New value for selected pins.
*
* @retval 0 Success.
* @retval <0 Write failed.
*/
int gpio_expander_update_port(uint8_t mask, uint8_t value);
#ifdef __cplusplus
}
#endif
#endif /* GPIO_EXPANDER_H */
+120
View File
@@ -0,0 +1,120 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Module:
* Keypad driver
*
* Description:
* Driver for the 4x4 matrix keypad used during development.
*
* Development hardware:
* - Freenove 4x4 Matrix Keypad
* - PCF8574 I2C GPIO Expander
* - I2C address: 0x20
*
* Wiring:
*
* Columns
* C1 C2 C3 C4
* PCF8574 P4 P5 P6 P7
*
* Rows
* P0
* P1
* P2
* P3
*
* Future:
* The production OpenParcelBox keypad (2x6) will reuse this driver.
* Only the row/column mapping and key table will change.
*/
#include "keypad.h"
#include "gpio_expander.h"
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/kernel.h>
#define KEYPAD_ROW_COUNT 4
#define KEYPAD_COLUMN_COUNT 4
#define KEYPAD_DEBOUNCE_MS 20
static const uint8_t row_pins[KEYPAD_ROW_COUNT] = {0, 1, 2, 3};
static const uint8_t column_pins[KEYPAD_COLUMN_COUNT] = {4, 5, 6, 7};
static const char keymap[KEYPAD_ROW_COUNT][KEYPAD_COLUMN_COUNT] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}};
static char last_key = 0;
/**
* @brief Scan the keypad matrix.
*
* @return Detected ASCII key or 0.
*/
static char keypad_scan(void) {
uint8_t value;
for (uint8_t row = 0; row < KEYPAD_ROW_COUNT; row++) {
gpio_expander_write_port(0xFF);
gpio_expander_write_pin(row_pins[row], false);
if (gpio_expander_read_port(&value) != 0) {
continue;
}
for (uint8_t column = 0; column < KEYPAD_COLUMN_COUNT; column++) {
if ((value & (1U << column_pins[column])) == 0U) {
return keymap[row][column];
}
}
}
return 0;
}
int keypad_init(void) {
last_key = 0;
return gpio_expander_init();
}
char keypad_get_key(void) {
char key;
key = keypad_scan();
if (key == 0) {
last_key = 0;
return 0;
}
k_msleep(KEYPAD_DEBOUNCE_MS);
if (key != keypad_scan()) {
return 0;
}
if (key == last_key) {
return 0;
}
last_key = key;
return key;
}
+40
View File
@@ -0,0 +1,40 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*/
#ifndef KEYPAD_H
#define KEYPAD_H
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize the keypad driver.
*
* Initializes the GPIO expander and prepares the keypad driver.
*
* @retval 0 Success.
* @retval <0 Initialization failed.
*/
int keypad_init(void);
/**
* @brief Read a key from the keypad.
*
* Performs a keypad scan and returns a single key press.
* The same key is returned only once until it is released.
*
* @return ASCII character corresponding to the pressed key.
* @return 0 if no key is available.
*/
char keypad_get_key(void);
#ifdef __cplusplus
}
#endif
#endif /* KEYPAD_H */
+82
View File
@@ -0,0 +1,82 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* RGB LED driver.
*/
#include "led.h"
#include "board_pins.h"
#include <stdbool.h>
#include <zephyr/drivers/gpio.h>
static const struct gpio_dt_spec led_red_gpio =
GPIO_DT_SPEC_GET(LED_RED_NODE, gpios);
static const struct gpio_dt_spec led_green_gpio =
GPIO_DT_SPEC_GET(LED_GREEN_NODE, gpios);
static const struct gpio_dt_spec led_blue_gpio =
GPIO_DT_SPEC_GET(LED_BLUE_NODE, gpios);
/**
* @brief Set the RGB LED state.
*
* @param red Red LED state.
* @param green Green LED state.
* @param blue Blue LED state.
*/
static void led_set_rgb(bool red, bool green, bool blue) {
gpio_pin_set_dt(&led_red_gpio, red);
gpio_pin_set_dt(&led_green_gpio, green);
gpio_pin_set_dt(&led_blue_gpio, blue);
}
int led_init(void) {
int ret;
if (!gpio_is_ready_dt(&led_red_gpio) || !gpio_is_ready_dt(&led_green_gpio) ||
!gpio_is_ready_dt(&led_blue_gpio)) {
return -1;
}
ret = gpio_pin_configure_dt(&led_red_gpio, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
return ret;
}
ret = gpio_pin_configure_dt(&led_green_gpio, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
return ret;
}
ret = gpio_pin_configure_dt(&led_blue_gpio, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
return ret;
}
led_off();
return 0;
}
void led_off(void) { led_set_rgb(false, false, false); }
void led_set_red(void) { led_set_rgb(true, false, false); }
void led_set_green(void) { led_set_rgb(false, true, false); }
void led_set_blue(void) { led_set_rgb(false, false, true); }
void led_set_yellow(void) { led_set_rgb(true, true, false); }
void led_set_cyan(void) { led_set_rgb(false, true, true); }
void led_set_magenta(void) { led_set_rgb(true, false, true); }
void led_set_white(void) { led_set_rgb(true, true, true); }
+71
View File
@@ -0,0 +1,71 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* RGB LED driver.
*/
#ifndef LED_H
#define LED_H
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize the RGB LED driver.
*
* Configures the three onboard RGB LED GPIOs.
*
* @retval 0 Success.
* @retval <0 Initialization failed.
*/
int led_init(void);
/**
* @brief Turn off the RGB LED.
*/
void led_off(void);
/**
* @brief Turn the RGB LED red.
*/
void led_set_red(void);
/**
* @brief Turn the RGB LED green.
*/
void led_set_green(void);
/**
* @brief Turn the RGB LED blue.
*/
void led_set_blue(void);
/**
* @brief Turn the RGB LED yellow.
*/
void led_set_yellow(void);
/**
* @brief Turn the RGB LED cyan.
*/
void led_set_cyan(void);
/**
* @brief Turn the RGB LED magenta.
*/
void led_set_magenta(void);
/**
* @brief Turn the RGB LED white.
*/
void led_set_white(void);
#ifdef __cplusplus
}
#endif
#endif /* LED_H */
+80
View File
@@ -0,0 +1,80 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Main firmware entry point.
*
* Current test:
* - RGB LED
* - GPIO Expander
* - 4x4 Keypad
*/
#include "keypad.h"
#include "led.h"
#include <zephyr/kernel.h>
#include <stdio.h>
/* --------------------------------------------------------------------------
* Defines
* -------------------------------------------------------------------------- */
#define MAIN_LOOP_DELAY_MS 20
#define KEY_PRESS_LED_MS 100
/* --------------------------------------------------------------------------
* Public API
* -------------------------------------------------------------------------- */
int main(void) {
char key;
if (led_init() < 0) {
return 0;
}
led_set_red();
k_msleep(1000);
if (keypad_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
led_set_green();
printf("\n");
printf("========================================\n");
printf("OpenParcelBox Firmware\n");
printf("Hardware Test: RGB LED + Keypad\n");
printf("========================================\n");
led_set_green();
while (1) {
key = keypad_get_key();
if (key != 0) {
printf("Key pressed: %c\n", key);
led_set_blue();
k_msleep(KEY_PRESS_LED_MS);
led_set_green();
}
k_msleep(MAIN_LOOP_DELAY_MS);
}
return 0;
}