本文是意法半导体(ST)社区两篇系列的第一篇:在 STM32N6570-DK 上把中间件接起来:TouchGFX 做图形界面,Camera 中间件做实时采图。目标是说明这些组件在 STM32N6 上怎么一起跑、不打架,并为后面接 AI 留好基础。
引言
带图形和视觉的嵌入式应用越来越多。STM32N6570-DK 的处理器与外设能力适合这类任务。本工程演示如何在这块板上集成 TouchGFX 与 Camera 中间件,把资源用稳、界面跑顺。下面按搭建、配置与编码走一遍。
1. 前置条件
开始前请准备:
- N6 开发板:STM32N6570-DK
- STM32CubeMX(v6.17.0)
- STM32CubeProgrammer(v2.21.0)
- STM32CubeIDE(v1.19.0)
- TouchGFX(v4.26.1)
- stm32-mw-camera(v1.5.1)
- stm32-mw-isp(v2.0.0)
2. 硬件搭建
演示硬件为 STM32N6570-DK。编程时将器件设为 **DEV boot mode**。
若已熟悉 TouchGFX 操作,可跳过本节,直接进入 STM32CubeMX 配置;请确保包含文中所列要素。
3. 工程创建 - TouchGFX
TouchGFX 工程怎么建看需求。本文做一个简单 GUI,只放说明结果需要的元素;可按目标再改。
点击 [Create New]。按 “N6” 过滤并选择板卡;本例使用带 **ThreadX** 的 STM32N6570-DK。路径可保持不变,工程名示例为 MyProject。请使用相同版本以免冲突。点 [Create]。
要让摄像头正常工作、GUI 仍能响应,请按下列步骤做完,再按自己的喜好改界面。初始屏命名为 **Menu**,再进 **ActivateCamera**,下一篇再接 Activate AI model 屏。
文末附件 `images_part1.zip` 含所需图片。解压后在 TouchGFX Images 中点右上角 + 导入(可一次全选)。
逐屏操作如下。
3.1 Menu 屏
1. 背景:顶部工具栏第 3 项(Image)加入图片;选工程图片里的 `background.png`,本例重命名为 `Menu_Background`(ActivateCamera 屏可复用同一背景)。
2. 导航按钮:顶部工具栏第 2 项(Button)加入按钮,用于从 Menu 切到 ActivateCamera。
3. 按钮文字:顶部工具栏第 7 项(Text Area)叠在按钮上;文案按界面需要填写(例如进入摄像头)。
4. Typographies:打开 Texts → Typographies,新建自定义字体,设置字号、字重、字距等,使 Text Area 可读、与按钮匹配。
5. Interactions:右上角 Interactions 点 +,增加「点击该按钮 → Change screen → ActivateCamera」交互(Trigger 选按钮 Clicked,Action 选切换到 ActivateCamera)。
3.2 ActivateCamera 屏
与 Menu 相同方式加背景后:
1. 启用摄像头:再加一组 Button + Text Area,用于启动摄像头管线(后续在 `ActivateCameraView::EnableCamera` 中发 `MSG_CAMERA_ON`)。
2. 返回:再加 Back 按钮(及文字);点击后禁用管线并回到 Menu(对应 `DisableCamera` + Change screen → Menu)。
3. typography2:在 Typographies 再建一套字体(本例称 typography2),专供本屏文字,属性与 Menu 自定义字体类似,按屏上可读性微调。
4. Interactions(本屏关键):
- 启用按钮 Clicked → 调用 `EnableCamera`(或等价自定义动作),保持在本屏;
- Back 按钮 Clicked → 调用 `DisableCamera`,再 Change screen → Menu。
生成前可用 [Run simulator] 或 **F5** 预览。设计完成后用 [Generate Code] 或 **F4** 生成。
4. STM32CubeMX 配置
生成代码后,打开工程 `.ioc`,配置时钟、摄像头管线与第二 LTDC 层。
4.1 激活 Camera Serial Interface(CSI)
Pinout & Configuration → Multimedia → CSI:在 Application 勾选并点 Activated。
4.2 激活 DCMIPP
同选项卡选择 DCMIPP 为 camera serial interface,按图配置 Pipe 1 并应用。
4.3 激活 LTDC 第二层
LTDC → Features 激活第二层;配置为两层,并按图填写细节。**Note:** 配置 Layer 1 Color Frame Buffer Start Address 前,点右侧小图标选 **[No check]**。
4.4 中断
System Core → NVIC1_S_Application:激活 DCMIPP global interrupt 与 CSI global interrupt;抢占优先级设为 **7**。
4.5 时钟
Clock Configuration:DCMIPP 目标 **300 MHz**,时钟源 IC17(来自 PLL2,分频 2)。按参考手册,CSI 物理连到 IC18,最大约 **20 MHz**;本例 PLL2 分频 30 作为 IC18 源,得到约 20 MHz CSI 时钟。
4.6 RIF
在 RIF 配置中为 DCMIPP 与 LTDC_L2 勾选 Privilege,以获得对 RAM 接口的特权访问。
4.7 生成代码
完成后点 [Generate code];警告弹窗选 [Yes]。
5. STM32CubeIDE 导入与设置
5.1 集成 BSP
从 `STM32Cube\Repository\STM32Cube_FW_N6_Vx.y.z\Drivers` 复制 BSP 到工程 Drivers(社区文作者所用版本 **1.3.0**;其他版本可能需改路径)。在 STM32N6570-DK_Appli 的 Drivers 下新建 BSP 文件夹,导入 `stm32n6570_discovery_bus.c`。
Include paths 增加:`../../../Drivers/BSP/STM32N6570-DK`。
若缺少 `stm32n6570_discovery_conf.h`:将 `stm32n6570_discovery_conf_template.h` 复制到 `Appli\Core\Inc` 并重命名为 `stm32n6570_discovery_conf.h`。
5.2 集成 STM32_ISP
将下载的 stm32-mw-isp 解压并放到 `Middlewares\ST`。在 IDE 中间件下建 STM32_ISP 文件夹并导入必要文件。Include:`../../../Middlewares/ST/stm32-mw-isp-2.0.0/isp/Inc`。
从 `isp_param_conf` 复制 `imx335_JudgeII_isp_param_conf.h` 到 `Appli\Core\Inc`,重命名为 `isp_param_conf.h`。确保 `../../../Appli/Core/Inc` 位于 include 列表顶部,以免用到 BSP 中的同名文件。
5.3 集成 stm32-mw-camera
将包放到 `Middlewares\ST`,按 BSP/ISP 类似方式导入;须导入附件自定义文件 `cmw_camera_customized.c` 到 `Appli\Core\Src` 并加入工程树。
打开 `cmw_camera_conf_template.h`,注释未用传感器,仅保留:
```c
#define USE_IMX335_SENSOR
```
放到 `Appli\Core\Inc` 并重命名为 `cmw_camera_conf.h`。
Include 增加:
```
../../../Middlewares/ST/stm32-mw-camera-main
../../../Middlewares/ST/stm32-mw-camera-main/sensors
../../../Middlewares/ST/stm32-mw-camera-main/sensors/imx335
```
此时构建可能仍报 `L2_BASE_ADDRESS` 未声明,属预期,因 Layer 2 缓冲故意未定义,在 Coding 节修复。
5.4 编码
在 `main.c` USER CODE BEGIN 0:
```c
DCMIPP_HandleTypeDef *hDcmipp;
ALIGN_32BYTES(uint8_t __attribute__((section (".DCMI_RAM"))) u8DCMIPP_ImageArray[400*240*2]);
#define L2_BASE_ADDRESS &u8DCMIPP_ImageArray[0]
```
链接脚本增加:
```
FB_RAM (xrw) : ORIGIN = 0x34146000, LENGTH = 0x001D6000
DCMI_RAM (xrw) : ORIGIN = 0x3431c000, LENGTH = 0x0002EE00
```
以及 `DCMIBufferSection (NOLOAD) { *(.DCMI_RAM) } >DCMI_RAM`。
Includes 加入 `#include "cmw_camera.h"`。因 CubeMX 已配置 I2C2,与 BSP 冲突:将 `MX_I2C2_Init` 重命名为 `MX_I2C2_Init_Cube`。
实现 `Camera_Config`(IMX335、fps 30、镜像等;Pipe1 RGB565;PixelPipePitch 1600;手动曝光等),在 USER CODE BEGIN 2 调用;若 CubeMX 再生后丢失,补上 `MX_TouchGFX_PreOSInit();`。
在 `app_threadx.c` 集成摄像头线程与消息队列。按 USER CODE 分区加入下列内容。
Includes / 命令枚举 / 栈与队列尺寸:
```c
/* USER CODE BEGIN Includes */
#include "main.h"
#include "cmw_camera.h"
/* USER CODE END Includes */
/* USER CODE BEGIN PTD */
typedef enum {
MSG_CAMERA_ON ,
MSG_CAMERA_OFF ,
} CameraCommand_t;
/* USER CODE END PTD */
/* USER CODE BEGIN PD */
#define THREAD_STACK_SIZE 1024
#define QUEUE_SIZE 128
/* USER CODE END PD */
/* USER CODE BEGIN PV */
extern DCMIPP_HandleTypeDef hdmipp;
extern LTDC_HandleTypeDef hltdc;
extern DCMIPP_HandleTypeDef *hDcmipp;
extern uint8_t u8DCMIPP_ImageArray[];
uint8_t Camera_thread_stack[THREAD_STACK_SIZE];
uint8_t Camera_queue[QUEUE_SIZE];
TX_THREAD Camera_thread_ptr;
TX_QUEUE Camera_Queue_ptr;
/* USER CODE END PV */
/* USER CODE BEGIN PFP */
VOID Camera_thread_entry(ULONG initial_input);
/* USER CODE END PFP */
```
在 `App_ThreadX_Init` 中创建队列与线程:
```c
/* USER CODE BEGIN App_ThreadX_Init */
tx_queue_create(&Camera_Queue_ptr,
"Camera_Queue",
1,
Camera_queue,
QUEUE_SIZE);
tx_thread_create(&Camera_thread_ptr,
"Camera_thread",
Camera_thread_entry,
0x1234,
Camera_thread_stack,
THREAD_STACK_SIZE,
15,
15,
1,
TX_AUTO_START);
/* USER CODE END App_ThreadX_Init */
```
线程入口:配置 DCMIPP 抽稀与 pitch、初始禁用 LTDC Layer 2、`CMW_CAMERA_Start` 后进入循环,按队列消息 Resume/Suspend 并开关 LTDC 层,运行中调用 `CMW_CAMERA_Run`:
```c
/* USER CODE BEGIN 1 */
VOID Camera_thread_entry(ULONG initial_input)
{
ISP_StatusTypeDef ret;
DCMIPP_DecimationConfTypeDef pDecConfig = {0};
uint32_t pitch_value;
ULONG received_State;
uint8_t camstatus = 0;
pitch_value = (800 * 2) / 2;
pDecConfig.HRatio = DCMIPP_HDEC_1_OUT_2;
pDecConfig.VRatio = DCMIPP_VDEC_1_OUT_2;
HAL_DCMIPP_PIPE_SetDecimationConfig(hDcmipp, DCMIPP_PIPE1, &pDecConfig);
HAL_DCMIPP_PIPE_EnableDecimation(hDcmipp, DCMIPP_PIPE1);
HAL_DCMIPP_PIPE_SetPitch(hDcmipp, DCMIPP_PIPE1, pitch_value);
HAL_LTDC_SetWindowSize(&hltdc, 800/2, 480/2, LTDC_LAYER_2);
__HAL_LTDC_LAYER_DISABLE(&hltdc, LTDC_LAYER_2);
HAL_LTDC_ReloadLayer(&hltdc, LTDC_RELOAD_VERTICAL_BLANKING, LTDC_LAYER_2);
SCB_CleanDCache();
if (CMW_CAMERA_Start(DCMIPP_PIPE1, (uint8_t *) u8DCMIPP_ImageArray, CMW_MODE_CONTINUOUS) != CMW_ERROR_NONE)
{
Error_Handler();
}
HAL_Delay(1000);
while (1)
{
UINT status = tx_queue_receive(&Camera_Queue_ptr, &received_State, 10);
if (status == TX_SUCCESS)
{
if (received_State == MSG_CAMERA_ON)
{
camstatus = 1;
SCB_CleanDCache();
if (CMW_CAMERA_Resume(DCMIPP_PIPE1) != CMW_ERROR_NONE)
{
Error_Handler();
}
__HAL_LTDC_LAYER_ENABLE(&hltdc, 1);
HAL_LTDC_ReloadLayer(&hltdc, LTDC_RELOAD_VERTICAL_BLANKING, 1);
SCB_CleanInvalidateDCache();
tx_thread_sleep(500);
}
else if (received_State == MSG_CAMERA_OFF)
{
camstatus = 0;
__HAL_LTDC_LAYER_DISABLE(&hltdc, 1);
HAL_LTDC_ReloadLayer(&hltdc, LTDC_RELOAD_VERTICAL_BLANKING, 1);
if (CMW_CAMERA_Suspend(DCMIPP_PIPE1) != CMW_ERROR_NONE)
{
Error_Handler();
}
SCB_CleanDCache();
tx_thread_sleep(500);
}
}
if (camstatus == 1)
{
ret = CMW_CAMERA_Run();
assert(ret == CMW_ERROR_NONE);
}
}
}
/* USER CODE END 1 */
```
在 `ActivateCameraView.cpp` / `.hpp` 中实现 `EnableCamera` / `DisableCamera`,经队列发命令。在 `ActivateCameraView.cpp` 中、`ActivateCameraView::ActivateCameraView()` 之前加入:
```c
#include "main.h"
extern "C"
{
#include "tx_api.h"
extern TX_QUEUE Camera_Queue_ptr;
extern LTDC_HandleTypeDef hltdc;
typedef enum {
MSG_CAMERA_ON ,
MSG_CAMERA_OFF ,
} CameraCommand_t;
}
```
在 `tearDownScreen` 之后实现:
```c
void ActivateCameraView::EnableCamera()
{
CameraCommand_t cmd = MSG_CAMERA_ON;
UINT status = tx_queue_send(&Camera_Queue_ptr, &cmd, TX_NO_WAIT);
if (status != TX_SUCCESS)
{
Error_Handler();
}
HAL_GPIO_WritePin(GPIOO, GPIO_PIN_1, GPIO_PIN_SET);
}
void ActivateCameraView::DisableCamera()
{
__HAL_LTDC_LAYER_DISABLE(&hltdc, 1);
HAL_LTDC_ReloadLayer(&hltdc, LTDC_RELOAD_VERTICAL_BLANKING, 1);
CameraCommand_t cmd = MSG_CAMERA_OFF;
UINT status = tx_queue_send(&Camera_Queue_ptr, &cmd, TX_NO_WAIT);
if (status != TX_SUCCESS)
{
Error_Handler();
}
HAL_GPIO_WritePin(GPIOO, GPIO_PIN_1, GPIO_PIN_RESET);
}
```
在 `ActivateCameraView.hpp` 于 `tearDownScreen` 后声明:
```c
virtual void EnableCamera();
virtual void DisableCamera();
```
在 `MX_DCMIPP_Init` 中配置 downsize 到 **800×480**(HRatio=25656,VRatio=33161,HSize=800,VSize=480,HDivFactor=316,VDivFactor=253),因传感器全分辨率 2592×1944,需缩小以匹配显示。
6. 生成并烧录可信固件
构建应无错误。在 FSBL Debug 目录用 `STM32_SigningTool_CLI.exe` 签名(将 PATH 配好),示例:
```
STM32_SigningTool_CLI.exe -bin Project.bin -nk -of 0x80000000 -t fsbl -o Project-trusted.bin -hv 2.3 -dump Project-trusted.bin -align
```
用 STM32CubeProgrammer(配置好外部存储器 loader)将可信 FSBL 烧到 **0x70000000**;对 Appli 同样签名后烧到 **0x70100000**;再烧录 `STM32N6570-DK_Appli_assets.hex`(hex 无需指定地址)。
7. 测试
烧录后板卡界面应与文中预期界面一致(Menu / ActivateCamera 及摄像头画面)。注:可进一步优化,例如为摄像头任务实现类似 TouchGFX 的双缓冲以改善显示。
结论
第一部分说明如何在 STM32N6570-DK 上集成 TouchGFX、Camera 与 ISP 中间件,配双层 LTDC,并拉起摄像头管线。集成顺了,后面做更丰富的图形、视觉和 AI(第二篇)会省事。
另一途径
自 TouchGFX Designer **4.26.1** 起,也可用板级演示 “Camera output in TextureMapper” 做出等效摄像头画面,并支持移动/缩放/旋转等。本文仍保留分步集成流程,方便衔接下文 AI 用例。
来源:https://community.st.com/stm32-mcus-60/how-to-integrate-touchgfx-and-camera-middleware-on-an-stm32n6570-dk-159856
参数以意法半导体官方最新资料为准。
90
