feat(peripheral): add fill light class with PWM dimming on MaixCAM2

- add maix::peripheral::light::Light with on/off/is_on/set_duty/duty/pin/freq,
  exposed to MaixPy as maix.light.Light
- MaixCAM2: drive the light by PWM and read maix_light_freq/maix_light_duty
  from /boot/configs (default 10000Hz/50%), warn when duty > 70% because
  long time high power running may cause the LED to age quickly
- MaixCAM: keep GPIO on/off control
- app_camera: use the new Light class instead of raw GPIO
This commit is contained in:
lxowalle
2026-11-05 16:00:45 +08:00
parent b7f5766530
commit d7a9938092
6 changed files with 434 additions and 26 deletions

View File

@@ -0,0 +1,102 @@
/**
* @author Sipeed
* @copyright Sipeed Ltd 2026-
* @license Apache 2.0
* @update 2026.11.5: create this file.
*/
#pragma once
#include <string>
#include "maix_basic.hpp"
namespace maix::peripheral::light
{
/**
* @brief Fill light(补光灯) control class.
*
* The light pin is read from the board config file(/boot/board or /boot/boards/board.<id>)
* item "cam_light_io", default is "B25" on MaixCAM2 and "B3" on MaixCAM.
*
* On MaixCAM2 the light is driven by PWM so its brightness can be adjusted,
* the PWM frequency and duty are read from the system config file(/boot/configs):
* maix_light_freq: PWM frequency, unit Hz, default 10000
* maix_light_duty: PWM duty, unit %, range [0, 100], default 50
* On MaixCAM the light is driven by GPIO, only on/off is supported.
*
* @maixpy maix.peripheral.light.Light
*/
class Light
{
public:
/**
* @brief Fill light constructor, will open the light hardware and turn it off by default.
* @throw err::Exception if open the light hardware failed.
* @maixpy maix.peripheral.light.Light.__init__
*/
Light();
~Light();
/**
* @brief Turn on the fill light.
* On MaixCAM2 the PWM duty will be set to the configured duty(maix_light_duty),
* on MaixCAM the GPIO will be set to high level.
* @return error code, err::ERR_NONE means success.
* @maixpy maix.peripheral.light.Light.on
*/
err::Err on();
/**
* @brief Turn off the fill light.
* @return error code, err::ERR_NONE means success.
* @maixpy maix.peripheral.light.Light.off
*/
err::Err off();
/**
* @brief Get the fill light state.
* @return true if the light is on, else false.
* @maixpy maix.peripheral.light.Light.is_on
*/
bool is_on();
/**
* @brief Set the fill light brightness(PWM duty), only take effect at runtime,
* will not modify the config file.
*
* On MaixCAM only 0(off) and non-zero(on) are supported.
*
* @note If duty > 70, a warning will be printed, because long time high power
* running may cause the LED to age quickly.
*
* @param[in] duty PWM duty, unit %, range [0, 100]. 0 means turn off the light.
* @return error code, err::ERR_NONE means success.
* @maixpy maix.peripheral.light.Light.set_duty
*/
err::Err set_duty(double duty);
/**
* @brief Get the current fill light brightness(PWM duty).
* @return PWM duty, unit %, range [0, 100].
* @maixpy maix.peripheral.light.Light.duty
*/
double duty();
/**
* @brief Get the fill light pin name.
* @return pin name, e.g. "B25".
* @maixpy maix.peripheral.light.Light.pin
*/
std::string pin();
/**
* @brief Get the fill light PWM frequency.
* @return PWM frequency, unit Hz. Return 0 if the light is driven by GPIO.
* @maixpy maix.peripheral.light.Light.freq
*/
int freq();
private:
void *_priv;
};
} // namespace maix::peripheral::light

View File

@@ -1,5 +1,6 @@
#include "maix_adc.hpp" #include "maix_adc.hpp"
#include "maix_gpio.hpp" #include "maix_gpio.hpp"
#include "maix_light.hpp"
#include "maix_i2c.hpp" #include "maix_i2c.hpp"
#include "maix_pwm.hpp" #include "maix_pwm.hpp"
#include "maix_spi.hpp" #include "maix_spi.hpp"

View File

@@ -0,0 +1,317 @@
/**
* @author Sipeed
* @copyright Sipeed Ltd 2026-
* @license Apache 2.0
* @update 2026.11.5: create this file.
*/
#include "maix_light.hpp"
#include "maix_log.hpp"
#include "maix_sys.hpp"
#include "maix_app.hpp"
#include "maix_pinmap.hpp"
#include "maix_gpio.hpp"
#include "maix_pwm.hpp"
#include <cstdlib>
#include <string>
#include <vector>
#if defined(PLATFORM_MAIXCAM2)
#define LIGHT_DEFAULT_PIN "B25"
#else
#define LIGHT_DEFAULT_PIN "B3"
#endif
namespace maix::peripheral::light
{
static std::string _str_trim(const std::string &s)
{
size_t start = s.find_first_not_of(" \t\r\n");
if (start == std::string::npos)
return "";
size_t end = s.find_last_not_of(" \t\r\n");
return s.substr(start, end - start + 1);
}
static std::string _get_light_pin()
{
std::string pin = LIGHT_DEFAULT_PIN;
try
{
auto configs = sys::device_configs();
auto it = configs.find("cam_light_io");
if (it != configs.end())
{
std::string value = _str_trim(it->second);
if (!value.empty())
pin = value;
}
}
catch (const std::exception &e)
{
log::warn("read device configs failed: %s", e.what());
}
return pin;
}
#if defined(PLATFORM_MAIXCAM2)
#define LIGHT_DUTY_WARN_THRESHOLD 70.0
static void _warn_high_duty(double duty)
{
log::warn("light duty %.0f%% is greater than %.0f%%, long time high power running may cause the LED to age quickly",
duty, LIGHT_DUTY_WARN_THRESHOLD);
}
#endif
struct LightPriv
{
bool is_on;
std::string pin;
int freq; // PWM frequency, 0 if driven by GPIO
double duty; // configured duty from /boot/configs, used when on() called
double cur_duty;
#if defined(PLATFORM_MAIXCAM2)
pwm::PWM *pwm;
#endif
gpio::GPIO *gpio;
LightPriv()
: is_on(false)
, freq(0)
, duty(0)
, cur_duty(0)
#if defined(PLATFORM_MAIXCAM2)
, pwm(nullptr)
#endif
, gpio(nullptr)
{
}
};
#if defined(PLATFORM_MAIXCAM2)
static int _get_pin_pwm_id(const std::string &pin, int &pwm_id)
{
try
{
std::vector<std::string> funcs = pinmap::get_pin_functions(pin);
for (auto &func : funcs)
{
if (func.rfind("PWM", 0) == 0)
{
pwm_id = atoi(func.c_str() + 3);
return 0;
}
}
}
catch (const std::exception &e)
{
log::warn("get pin %s functions failed: %s", pin.c_str(), e.what());
}
return -1;
}
#endif
Light::Light()
{
LightPriv *priv = new LightPriv();
_priv = priv;
priv->pin = _get_light_pin();
bool use_pwm = false;
#if defined(PLATFORM_MAIXCAM2)
{
std::string freq_str = app::get_sys_config_kv("light", "freq", "10000");
std::string duty_str = app::get_sys_config_kv("light", "duty", "50");
priv->freq = atoi(freq_str.c_str());
if (priv->freq <= 0)
priv->freq = 10000;
priv->duty = atof(duty_str.c_str());
if (priv->duty < 0)
priv->duty = 0;
if (priv->duty > 100)
priv->duty = 100;
if (priv->duty > LIGHT_DUTY_WARN_THRESHOLD)
_warn_high_duty(priv->duty);
int pwm_id = -1;
if (_get_pin_pwm_id(priv->pin, pwm_id) == 0)
{
err::check_raise(pinmap::set_pin_function(priv->pin, "PWM" + std::to_string(pwm_id)), "set light pin function failed");
try
{
priv->pwm = new pwm::PWM(pwm_id, priv->freq, 0);
use_pwm = true;
}
catch (const std::exception &e)
{
log::warn("open light pwm failed: %s, fallback to gpio", e.what());
priv->pwm = nullptr;
}
}
else
{
log::warn("pin %s has no PWM function, fallback to gpio", priv->pin.c_str());
}
}
#endif
if (!use_pwm)
{
priv->freq = 0;
err::check_raise(pinmap::set_pin_function(priv->pin, "GPIO" + priv->pin), "set light pin function failed");
priv->gpio = new gpio::GPIO(priv->pin, gpio::Mode::OUT);
err::check_null_raise(priv->gpio, "open light gpio failed");
priv->gpio->low();
}
priv->is_on = false;
priv->cur_duty = 0;
log::info("light pin: %s, freq: %d, duty: %.1f", priv->pin.c_str(), priv->freq, priv->duty);
}
Light::~Light()
{
LightPriv *priv = (LightPriv *)_priv;
if (!priv)
return;
#if defined(PLATFORM_MAIXCAM2)
if (priv->pwm)
{
priv->pwm->duty(0);
delete priv->pwm;
priv->pwm = nullptr;
}
#endif
if (priv->gpio)
{
priv->gpio->low();
delete priv->gpio;
priv->gpio = nullptr;
}
delete priv;
_priv = nullptr;
}
err::Err Light::on()
{
LightPriv *priv = (LightPriv *)_priv;
if (!priv)
return err::ERR_RUNTIME;
#if defined(PLATFORM_MAIXCAM2)
if (priv->pwm)
{
if (priv->pwm->duty(priv->duty) < 0)
return err::ERR_IO;
priv->cur_duty = priv->duty;
priv->is_on = true;
return err::ERR_NONE;
}
#endif
if (priv->gpio)
{
priv->gpio->high();
priv->cur_duty = 100;
priv->is_on = true;
return err::ERR_NONE;
}
return err::ERR_RUNTIME;
}
err::Err Light::off()
{
LightPriv *priv = (LightPriv *)_priv;
if (!priv)
return err::ERR_RUNTIME;
#if defined(PLATFORM_MAIXCAM2)
if (priv->pwm)
{
if (priv->pwm->duty(0) < 0)
return err::ERR_IO;
priv->cur_duty = 0;
priv->is_on = false;
return err::ERR_NONE;
}
#endif
if (priv->gpio)
{
priv->gpio->low();
priv->cur_duty = 0;
priv->is_on = false;
return err::ERR_NONE;
}
return err::ERR_RUNTIME;
}
bool Light::is_on()
{
LightPriv *priv = (LightPriv *)_priv;
if (!priv)
return false;
return priv->is_on;
}
err::Err Light::set_duty(double duty)
{
LightPriv *priv = (LightPriv *)_priv;
if (!priv)
return err::ERR_RUNTIME;
if (duty < 0)
duty = 0;
if (duty > 100)
duty = 100;
#if defined(PLATFORM_MAIXCAM2)
if (priv->pwm)
{
if (duty > LIGHT_DUTY_WARN_THRESHOLD && priv->cur_duty <= LIGHT_DUTY_WARN_THRESHOLD)
_warn_high_duty(duty);
if (priv->pwm->duty(duty) < 0)
return err::ERR_IO;
priv->cur_duty = duty;
priv->is_on = duty > 0;
return err::ERR_NONE;
}
#endif
if (priv->gpio)
{
if (duty > 0)
{
priv->gpio->high();
priv->cur_duty = 100;
priv->is_on = true;
}
else
{
priv->gpio->low();
priv->cur_duty = 0;
priv->is_on = false;
}
return err::ERR_NONE;
}
return err::ERR_RUNTIME;
}
double Light::duty()
{
LightPriv *priv = (LightPriv *)_priv;
if (!priv)
return 0;
return priv->cur_duty;
}
std::string Light::pin()
{
LightPriv *priv = (LightPriv *)_priv;
if (!priv)
return "";
return priv->pin;
}
int Light::freq()
{
LightPriv *priv = (LightPriv *)_priv;
if (!priv)
return 0;
return priv->freq;
}
} // namespace maix::peripheral::light

View File

@@ -46,14 +46,14 @@
2. 码率修改:仅在非录制状态下可修改码率,录制过程中修改码率会提示「视频忙」且不生效; 2. 码率修改:仅在非录制状态下可修改码率,录制过程中修改码率会提示「视频忙」且不生效;
3. 延时摄影:开启延时摄影后,音频会自动关闭,关闭延时摄影后需手动重新开启音频; 3. 延时摄影:开启延时摄影后,音频会自动关闭,关闭延时摄影后需手动重新开启音频;
4. RAW格式:开启RAW格式后,照片保存体积会增大,且需要专用软件解析; 4. RAW格式:开启RAW格式后,照片保存体积会增大,且需要专用软件解析;
5. 补光灯:补光灯的硬件引脚默认配置为GPIOB3,可通过设备配置文件修改引脚映射; 5. 补光灯:补光灯由 `maix::peripheral::light::Light` 控制,引脚可通过设备配置文件中的「cam_light_io」修改(MaixCam 默认 B3,MaixCam2 默认 B25);MaixCam2 使用 PWM 调光,频率和占空比由 `/boot/configs` 中的 `maix_light_freq`(默认 10000)和 `maix_light_duty`(默认 50)决定,MaixCam 使用 GPIO 仅支持开关;
6. 存储路径:照片/视频默认按日期分目录存储,需确保存储设备有足够空间,否则会保存失败; 6. 存储路径:照片/视频默认按日期分目录存储,需确保存储设备有足够空间,否则会保存失败;
7. 音视频同步:录制视频时,若出现音频卡顿,可检查音频采样率配置(默认48000Hz)。 7. 音视频同步:录制视频时,若出现音频卡顿,可检查音频采样率配置(默认48000Hz)。
## 5. 更多介绍 ## 5. 更多介绍
### 5.1 硬件适配 ### 5.1 硬件适配
- 支持MaixCam、MaixCam2两款硬件平台,MaixCam2支持更高码率(最高100Mbps)和AI-ISP功能; - 支持MaixCam、MaixCam2两款硬件平台,MaixCam2支持更高码率(最高100Mbps)和AI-ISP功能;
- 补光灯引脚可通过设备配置文件中的「cam_light_io」参数自定义,默认B3引脚; - 补光灯由 `maix::peripheral::light::Light` 控制,引脚可通过设备配置文件中的「cam_light_io」参数自定义(MaixCam 默认 B3,MaixCam2 默认 B25);MaixCam2 通过 PWM 调光,频率/占空比由 `/boot/configs` 的 `maix_light_freq`/`maix_light_duty` 配置,MaixCam 通过 GPIO 控制;
- 摄像头传感器最大分辨率决定可选分辨率档位,适配前需确认硬件传感器规格。 - 摄像头传感器最大分辨率决定可选分辨率档位,适配前需确认硬件传感器规格。
### 5.2 存储说明 ### 5.2 存储说明

View File

@@ -52,7 +52,7 @@ This application is a camera control program developed based on Maix series hard
2. **Bitrate Modification:** Bitrate can only be modified when not recording. Attempting to change it during recording will show a "Video Busy" warning and will not take effect. 2. **Bitrate Modification:** Bitrate can only be modified when not recording. Attempting to change it during recording will show a "Video Busy" warning and will not take effect.
3. **Time-lapse Photography:** Audio is automatically disabled in time-lapse mode. You need to manually re-enable it if you switch back to normal recording. 3. **Time-lapse Photography:** Audio is automatically disabled in time-lapse mode. You need to manually re-enable it if you switch back to normal recording.
4. **RAW Format:** Enabling RAW format increases file size significantly, and dedicated software is required for viewing/processing. 4. **RAW Format:** Enabling RAW format increases file size significantly, and dedicated software is required for viewing/processing.
5. **Fill Light:** The default hardware pin for the light is GPIOB3. This can be modified via the device configuration file (`cam_light_io`). 5. **Fill Light:** The fill light is controlled by `maix::peripheral::light::Light`. The pin can be changed via `cam_light_io` in the device config file (default: B3 on MaixCam, B25 on MaixCam2). On MaixCam2 the light is driven by PWM, the frequency and duty are read from `/boot/configs` (`maix_light_freq`, default 10000; `maix_light_duty`, default 50). On MaixCam it is driven by GPIO and only on/off is supported.
6. **Storage Path:** Files are stored in date-based directories by default. Ensure sufficient storage space is available to prevent save failures. 6. **Storage Path:** Files are stored in date-based directories by default. Ensure sufficient storage space is available to prevent save failures.
7. **AV Sync:** If audio stutters during recording, check the audio sample rate configuration (default is 48000Hz). 7. **AV Sync:** If audio stutters during recording, check the audio sample rate configuration (default is 48000Hz).
@@ -61,7 +61,7 @@ This application is a camera control program developed based on Maix series hard
### 5.1 Hardware Compatibility ### 5.1 Hardware Compatibility
* Supports **MaixCam** and **MaixCam2** platforms. * Supports **MaixCam** and **MaixCam2** platforms.
* **MaixCam2** supports higher bitrates (up to 100Mbps) and features AI-ISP. * **MaixCam2** supports higher bitrates (up to 100Mbps) and features AI-ISP.
* The fill light pin is configurable via the `cam_light_io` parameter in the device config file (default: B3). * The fill light is controlled by `maix::peripheral::light::Light`. Its pin is configurable via `cam_light_io` (default: B3 on MaixCam, B25 on MaixCam2). On MaixCam2 it is driven by PWM with `maix_light_freq`/`maix_light_duty` from `/boot/configs`; on MaixCam it is driven by GPIO.
* The available resolution options are determined by the camera sensor's maximum resolution. * The available resolution options are determined by the camera sensor's maximum resolution.
### 5.2 Storage Details ### 5.2 Storage Details

View File

@@ -6,8 +6,7 @@
#include "maix_util.hpp" #include "maix_util.hpp"
#include "maix_image.hpp" #include "maix_image.hpp"
#include "maix_app.hpp" #include "maix_app.hpp"
#include "maix_pinmap.hpp" #include "maix_light.hpp"
#include "maix_gpio.hpp"
#include "maix_fs.hpp" #include "maix_fs.hpp"
#include "maix_vision.hpp" #include "maix_vision.hpp"
#include "maix_ffmpeg.hpp" #include "maix_ffmpeg.hpp"
@@ -67,7 +66,7 @@ static struct {
display::Display *other_disp; display::Display *other_disp;
touchscreen::TouchScreen *touchscreen; touchscreen::TouchScreen *touchscreen;
video::Encoder *encoder; video::Encoder *encoder;
gpio::GPIO *light; maix::peripheral::light::Light *light;
ffmpeg::FFmpegPacker *ffmpeg_packer; ffmpeg::FFmpegPacker *ffmpeg_packer;
audio::Recorder *audio_recorder; audio::Recorder *audio_recorder;
Region *region; Region *region;
@@ -347,22 +346,11 @@ int app_base_init(void)
priv.touchscreen = new touchscreen::TouchScreen(); priv.touchscreen = new touchscreen::TouchScreen();
err::check_bool_raise(priv.touchscreen->is_opened(), "touchscreen open failed"); err::check_bool_raise(priv.touchscreen->is_opened(), "touchscreen open failed");
// init light // init fill light
auto device_configs = sys::device_configs(); // MaixCAM2: PWM dimming, read frequency and duty from /boot/configs(maix_light_freq, maix_light_duty)
auto light_io = std::string("B3"); // default use GPIOB3 // MaixCAM : GPIO on/off
auto it = device_configs.find("cam_light_io"); priv.light = new light::Light();
if (it != device_configs.end()) { priv.light->off();
auto new_io = it->second;
trim(new_io);
if (new_io.size() > 0) {
light_io = new_io;
}
}
log::info("light_io: %s", light_io.c_str());
pinmap::set_pin_function(light_io, "GPIO" + light_io);
priv.light = new gpio::GPIO(light_io, gpio::Mode::OUT);
err::check_null_raise(priv.light, "light gpio open failed");
priv.light->low();
// init audio // init audio
priv.audio_recorder = new audio::Recorder(); priv.audio_recorder = new audio::Recorder();
@@ -410,7 +398,7 @@ int app_base_deinit(void)
} }
if (priv.light) { if (priv.light) {
priv.light->low(); priv.light->off();
delete priv.light; delete priv.light;
priv.light = NULL; priv.light = NULL;
} }
@@ -884,12 +872,12 @@ static int app_config_param(void)
if (ui_get_light_btn_touched()) { if (ui_get_light_btn_touched()) {
log::info("light on"); log::info("light on");
if (priv.light) { if (priv.light) {
priv.light->high(); priv.light->on();
} }
} else { } else {
log::info("light off"); log::info("light off");
if (priv.light) { if (priv.light) {
priv.light->low(); priv.light->off();
} }
} }
} }