HumansXドキュメントセンター 公式サイトへ ↗

実験6 ビジョンによる追従 — 4輪メカナムホイールシャーシによる移動物体の追跡

実験の目的:

  1. DMA の基本的な動作原理を理解し、DMA を用いて AI ビジョンモジュールが取得したデータを扱えるようにします。

  2. 4輪駆動メカナムホイールシャーシの運動原理に基づき、AI ビジョンモジュールと組み合わせて追従を実現します。

  3. STM32 の GPIO 周辺機能ピンの設定方法を習得します。

  4. STM32CubeMX で新規プロジェクトを作成し、MDK_ARM プロジェクトファイルを生成できるようにします。

  5. keil IDE でユーザーコードを追加し、プロジェクトのテストを行えるようにします。

DMA の基本原理:

DMA:Direct Memory Access(ダイレクトメモリアクセス)の略です。DMA 転送は、あるアドレス空間から別のアドレス空間へデータをコピーするもので、周辺機器とメモリ間、またはメモリ間で高速なデータ転送を実現します。

DMA の転送モード:DMA の役割は直接的なデータ転送を実現することで、従来のデータ転送で必要だった CPU レジスタの関与を省きます。主に次の4種類のデータ転送があります。

  • 周辺機器からメモリへ
  • メモリから周辺機器へ
  • メモリからメモリへ
  • 周辺機器から周辺機器へ

周辺機器とメモリ間で通信する際のデータの流れは次のとおりです。

​

4輪駆動メカナムホイールシャーシの基本運動原理:

4輪全方向メカナムホイールシャーシは全方向への平行移動が可能で、各ホイールの回転方向を制御する必要があります。シャーシの移動方向と各ホイールの回転方向の関係は次のとおりです。

前進 後退
右平行移動 左平行移動
その場で左旋回 その場で右旋回

応用シーン

自動化された生産ラインでは、産業用ロボットが物体追跡技術を用いてワークの位置と動きをリアルタイムに追跡し、正確な把持・組立・搬送などの作業を実現して、生産効率と製品品質を向上させることができます。

機能要件:

4輪駆動メカナムホイールシャーシで移動物体(例えば青い物体)を追跡します。

ハードウェアの準備:

STM32 メインボード V1.3 * 1、ST-Link ダウンロードケーブル * 1、3S バッテリーボックス * 1、リチウム電池 * 3、AI ビジョンモジュール * 1、Type-C データケーブル * 1

試作構成:

実験7 サスペンション付き矩形フレームと4輪駆動全方向メカナムホイールシャーシの構造設計​

回路接続:

ハードウェアモジュール

ヒント:開発ボード、環境設定、電源モジュールの基本的な使い方に習熟しておくことを強くお勧めします。電子モジュールについては必要に応じて参照してください。

UNI-STM32 開発ボード【V1.3】の紹介

UNI-STM32 開発環境の設定【V2.0】

バッテリーおよび電源モジュール使用上の注意(必読)

電子モジュールの紹介(クリックするとモーターと AI ビジョンモジュールの基本的な紹介を確認できます)

ピンの紹介

まず モーター M1~M4、AI ビジョンモジュール( K210 )のインターフェース の位置を全体的に把握し、その後ピン表に従って配線を完了します。

シャーシモーター STM32 インターフェース AI モジュール(K210) STM32 開発ボード
左前輪モーター M1 5V 5V
右前輪モーター M2 GND GND
左後輪モーター M3 RX USART3_TX
右後輪モーター M4 TX USART3_RX

このうち K210 インターフェース のピン位置は下図のとおりです。ピン表に従って配線を完了してください。

実際の配線

配線のコツ:エンコーダ付きモーターの付属ケーブルは逆挿し防止構造になっています。ソケットの向きをよく確認してから接続してください。

​

ソフトウェア設計:

設計フロー

STM32 は K210 ビジョンモジュールから目標の座標とサイズを受信し、領域分割+領域フィードバック によって車両が自動的に目標を追跡します。

プロジェクトのダウンロード

ソースファイル:17 Car_Vision_Tracking.zip

プログラムコード

プロジェクトの完全なコードは(ソースファイル)をクリックしてダウンロードしてください。

AI ビジョンモジュール(K210)のプログラム

1. 物体の Lab 色域値を取得する
'''
功能:实现检测特定物体,并将该物体的lab色域值范围打印出来
日期:2026-04-09
'''

import sensor
import image
import time
import lcd

#载入按键控制模块
from hiwonder import hw_key, hw_led , fill_light

#初始化LCD
lcd.init()
#以下是初始化传感器
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time = 100)
sensor.set_auto_gain(False)
sensor.set_auto_whitebal(False)

#创建LED控制对象
led = hw_led()
#创建补光灯控制对象
f_led = fill_light()

#LED,补光灯开
#led.led_onoff(1)    #LED开
#led.led_onoff(0)    #LED关
#f_led.fill_onoff(0) #补光灯开
#time.sleep(0.5)  #延时0.5s

#帧率时钟
clock = time.clock()

#方框参数
r = [(320//2)-(50//2), (240//2)-(50//2), 50, 50]

#创建按键控制对象
key = hw_key()

state = key.key_scan(1)

#显示图像,等待按键按下
while (state == 0):
    state = key.key_scan(1)
    img = sensor.snapshot()
    img.draw_rectangle(r)
    lcd.display(img)

print("Start learning ...")
threshold = [50, 50, 0, 0, 0, 0] # Middle L, A, B values.

#第一次赋值
img = sensor.snapshot()
hist = img.get_histogram(roi=r)
lo = hist.get_percentile(0.01) #获得直方图在1%范围内的CDF(根据需要调整)!
hi = hist.get_percentile(0.99) #获得直方图在99%范围内的CDF(根据需要调整)!
threshold[0] = (threshold[0] + lo.l_value()) // 2
threshold[1] = (threshold[1] + hi.l_value()) // 2
threshold[2] = (threshold[2] + lo.a_value())
threshold[3] = (threshold[3] + hi.a_value())
threshold[4] = (threshold[4] + lo.b_value())
threshold[5] = (threshold[5] + hi.b_value())

#连续捕捉了50张图像学习阶段
for i in range(50):
    img = sensor.snapshot()
    hist = img.get_histogram(roi=r)
    lo = hist.get_percentile(0.01) #获得直方图在1%范围内的CDF(根据需要调整)!
    hi = hist.get_percentile(0.99) #获得直方图在99%范围内的CDF(根据需要调整)!
    #hi = hist.get_percentile(0.70) #获得直方图在99%范围内的CDF(根据需要调整)!
    #以百分位数表示的平均值
    threshold[0] = (threshold[0] + lo.l_value()) // 2
    threshold[1] = (threshold[1] + hi.l_value()) // 2
    threshold[2] = (threshold[2] + lo.a_value()) // 2
    threshold[3] = (threshold[3] + hi.a_value()) // 2
    threshold[4] = (threshold[4] + lo.b_value()) // 2
    threshold[5] = (threshold[5] + hi.b_value()) // 2
    #将识别区域用方框圈出来
    for blob in img.find_blobs([threshold], pixels_threshold=100, area_threshold=100, merge=True, margin=10):
        img.draw_rectangle(blob.rect())
        img.draw_cross(blob.cx(), blob.cy())
        img.draw_rectangle(r, color=(0,255,0))
    lcd.display(img)
print("threshold:", threshold)

#loop
while(True):
    #用于计算帧率的函数,这里表示开始计时
    clock.tick()
    #从传感器捕获一张图像
    img = sensor.snapshot()
    #遍历图像中找到的颜色区块
    for blob in img.find_blobs([threshold], pixels_threshold=100, area_threshold=100, merge=True, margin=10):
        #绘制矩形和十字标记
        img.draw_rectangle(blob.rect())
        img.draw_cross(blob.cx(), blob.cy())
    #显示在LCD上
    lcd.display(img)
    #打印帧率
    #print(clock.fps())
2. Lab 色域値をシリアルポートプログラムに書き込む
'''
功能: 将检测得到的红色/蓝色物体数据通过封装的串口通讯协议发送
日期:2026-04-09
'''

import binascii
import sensor
import image
import time
import lcd
import _thread
from hiwonder import hw_key, hw_led , fill_light, hw_uart

# 创建串口对象
serial = hw_uart()

"""
视觉颜色检测封装类
"""
class VisionModule:
    def __init__(self):
        # 初始化LCD
        lcd.init()
        # 初始化传感器
        sensor.reset()
        sensor.set_pixformat(sensor.RGB565)
        sensor.set_framesize(sensor.QVGA)
        sensor.skip_frames(time = 100)
        sensor.set_auto_gain(False)
        sensor.set_auto_whitebal(False)

        # 帧率时钟
        self.clock = time.clock()

        # 创建LED控制对象
        led = hw_led()
        # 创建补光灯控制对象
        f_led = fill_light()

        # 打开补光灯(根据需要修改)
        #led.led_onoff(1)    # LED开
        #led.led_onoff(0)    # LED关(若需关闭则取消注释)
        #f_led.fill_onoff(1) # 补光灯开
        #time.sleep(0.5)     # 延时0.5s

        #LED,补光灯开
        led.led_onoff(1)    #LED开
        led.led_onoff(0)    #LED关
        f_led.fill_onoff(0) #补光灯开
        time.sleep(0.5)  #延时0.5s

        # 储存颜色的LAB阈值(示例仅红、蓝,可自行添加更多)
        self.color_thresholds = [
            #(42, 80, 21, 54, 22, 42),       #Red1
            #(37, 76, 21, 45, 37, 51),       #Red2
            #(33, 81, 28, 55, -4, 19),       #Red3
            #(44, 76, 24, 52, 41, 53),       #Red4
            #(29, 57, 37, 52, 21, 37),       #Red5
            (29, 81, 21, 55, -4, 53),        #Red 红色色域值数据融合结果

            #(28, 71, -33, -14, -11, 4),     #blue1
            #(24, 71, -4, 12, -41, -21),     #blue2
            #(46, 78, -6, 15, -53, -33),     #blue3
            #(31, 71, -20, -4, -27, -6),     #blue4
            #(21, 53, -18, -2, -25, -4),     #blue5
            #(45, 74, -10, 7, -45, -23),     #blue6
            #(29, 76, -41, -22, -7, 14),     #blue7
            #(38, 79, -27, -12, -21, -3),    #blue8
            #(21, 79, -41, 15, -53, 14),     #Blue 蓝色色域值数据融合结果
            #(43, 57, -6, 12, -46, -24),       #Blue 蓝色色域值数据融合结果
            (10, 98, -2, 27, -46, -16),

            # (23, 52, -29, -9, -9, 7),     # Green
            # (65, 78, -10, -5, 38, 50),    # Yellow
            # (20, 50, 17, 37, -34, -14),   # purple
        ]

        self.color_strings = ['Red', 'Blue', 'Green', 'Yellow', 'purple', 'Other1', "Other2", "Other3", "Other4", "Other5"]

class FuncTag:
    """功能号定义"""
    START = 0
    FIND_MULTI_COLOR = 0x10

def str_2_int(data_str):
    """字符串转int"""
    bb = binascii.hexlify(data_str)
    bb = str(bb)[2:-1]
    int_h = int(bb[0]) * 16
    int_l = int(bb[1], 16)
    return int_h + int_l

def int_to_hex_str(byte_val):
    """将整数转换为两位十六进制字符串"""
    hex_str = hex(byte_val)
    # 去掉 '0x' 前缀,并转换为大写
    hex_str = hex_str[2:].upper()
    # 确保是两位
    if len(hex_str) == 1:
        hex_str = '0' + hex_str
    return hex_str

def send_data(img, x, y, w, h, msg):
    '''
    发送数据(协议头 0xAA 0x55 + 功能号 + 数据长度 + 数据 + 回车符)
    将原来的CRC校验位替换为\n(回车)

    Args:
        x: 检测框中心x坐标
        y: 检测框中心y坐标
        w: 检测框宽度
        h: 检测框高度
        msg: 消息内容
    '''
    START_1 = 0xAA
    START_2 = 0x55
    FUNC_num = FuncTag.FIND_MULTI_COLOR  # 功能编号
    Length = 0  # 数据长度
    data = []  # 数据组

    # 参数都为0
    if x == 0 and y == 0 and w == 0 and h == 0:
        pass
    else:
        # x(小端模式)
        low = x & 0xFF  # 低位
        high = x >> 8 & 0xFF  # 高位
        data.append(low)
        data.append(high)

        # y(小端模式)
        low = y & 0xFF  # 低位
        high = y >> 8 & 0xFF  # 高位
        data.append(low)
        data.append(high)

        # w(小端模式)
        low = w & 0xFF  # 低位
        high = w >> 8 & 0xFF  # 高位
        data.append(low)
        data.append(high)

        # h(小端模式)
        low = h & 0xFF  # 低位
        high = h >> 8 & 0xFF  # 高位
        data.append(low)
        data.append(high)

    # msg
    color_r = 0
    color_g = 0
    color_b = 0
    position_x = 0
    position_y = 0
    if msg is not None:
        if msg == "Red":
            color_r = 255
            color_g = 0
            color_b = 0
            position_x = 46
            position_y = 0
            img.draw_string(0, 0, "SenRed:", color=(color_r, color_g, color_b), scale=1)
        elif msg == "Blue":
            color_r = 0
            color_g = 0
            color_b = 255
            position_x = 52
            position_y = 20
            img.draw_string(0, 20, "SendBlue:", color=(color_r, color_g, color_b), scale=1)
        else:
            color_r = 255
            color_g = 255
            color_b = 255
            position_x = 56
            position_y = 30
            img.draw_string(0, 30, "SendOther:", color=(color_r, color_g, color_b), scale=1)
        for i in range(len(msg)):
            msg_int = str_2_int(msg[i])
            data.append(msg_int)

    Length += len(data)
    send_buf = [FUNC_num, Length]
    for i in range(len(data)):
        send_buf.append(data[i])

    send_buf.insert(0, START_1)  # 插入协议头1
    send_buf.insert(1, START_2)  # 插入协议头2
    send_buf.append(0x0A)  # 加入回车符 \n (0x0A),替换原来的CRC校验位

    serial.send_bytearray(send_buf)  # 发送数据

    # 将字节数组转换为十六进制字符串显示
    hex_str_list = [int_to_hex_str(b) for b in send_buf]
    hex_str = ' '.join(hex_str_list)
    img.draw_string(position_x, position_y, hex_str, color=(color_r, color_g, color_b), scale=1)

def main():
    # 创建视觉检测管理实例
    vision = VisionModule()
    while True:
        #用于计算帧率的函数,这里表示开始计时
        #从传感器捕获一张图像
        img = sensor.snapshot()
        #遍历多种颜色
        for color_idx, threshold in enumerate(vision.color_thresholds):
            #遍历图像中找到的颜色区块
            blobs = img.find_blobs([threshold], pixels_threshold=100, area_threshold=100, merge=True, margin=10)
            #是否找到
            if blobs:
                area_max = 0
                blob_max = blobs[0]
                #寻找最大的颜色块
                for blob in blobs:
                    area = blob.w()*blob.h()
                    if(area_max < area):
                        blob_max = blob
                        area_max = area
                if area_max < 2000:
                    continue
                #画方框
                color_set = (255, 255, 255)
                if color_idx == 0:
                    color_set = (255, 0, 0)      # 红色框
                elif color_idx == 1:
                    color_set = (0, 0, 255)      # 蓝色框
                elif color_idx == 2:
                    color_set = (0, 255, 0)      # 绿色框
                else:
                    color_new = (255, 255, 255)
                img.draw_rectangle(blob_max.rect(), color=color_set, thickness=5)
                img.draw_cross(blob_max.cx(), blob_max.cy(), color=(255, 255, 0), thickness=5)
                img.draw_string(blob_max.cx() + 10, blob_max.cy() - 10, vision.color_strings[color_idx], color=(color_set))
                #将方框数据与消息赋值
                send_x = blob_max.cx()
                send_y = blob_max.cy()
                send_w = blob_max.w()
                send_h = blob_max.h()
                #发送数据
                send_data(img, send_x,send_y,send_w,send_h, vision.color_strings[color_idx])

        #显示在LCD上
        lcd.display(img)
        #打印帧率
        #print(clock.fps())

if __name__ == '__main__':
    main()

下位機 stm32 のメインプログラム

以下に示す stm32 のメインプログラムは main.c です。

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "motor.h"
#include "config.h"
#include "DWT_time.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

    /* USER CODE BEGIN 1 */

    /* USER CODE END 1 */

    /* MCU Configuration--------------------------------------------------------*/

    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();

    /* USER CODE BEGIN Init */

    /* USER CODE END Init */

    /* Configure the system clock */
    SystemClock_Config();

    /* USER CODE BEGIN SysInit */

    /* USER CODE END SysInit */

    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_TIM1_Init();
    MX_USART1_UART_Init();
    MX_TIM3_Init();
    MX_TIM8_Init();
    MX_TIM5_Init();
    MX_TIM6_Init();
    MX_USART3_UART_Init();
    /* USER CODE BEGIN 2 */
    HAL_Delay(20);
    Motor_Init();                           //motor init
    dwt_init();                             //DWT_start
    enable(&m1);                            //M1 motor enable
    enable(&m2);                            //M2 motor enable
    enable(&m3);                            //M3 motor enable
    enable(&m4);                            //M4 motor enable

    //If omitted here, the radius defaults to 0.0325
  set_radius(&m1, 0.0475);                //set m1 wheel radius. unit: m
  set_radius(&m2, 0.0475);                //set m2 wheel radius. unit: m
  set_radius(&m3, 0.0475);                //set m3 wheel radius. unit: m
  set_radius(&m4, 0.0475);                //set m4 wheel radius. unit: m

  //If omitted here, the acceleration defaults to 10.0, the uniform velocity to 6.28, and the deceleration to 10.0
  set_t_speed(&m1, 6.0, 3.14, 6.0);       //set m1 T speed (include: acc,uniform,dec)
  set_t_speed(&m2, 6.0, 3.14, 6.0);       //set m2 T speed (include: acc,uniform,dec)
  set_t_speed(&m3, 6.0, 3.14, 6.0);       //set m3 T speed (include: acc,uniform,dec)
  set_t_speed(&m4, 6.0, 3.14, 6.0);       //set m4 T speed (include: acc,uniform,dec)
  HAL_TIM_Base_Start_IT(&htim6);          //Base TIM6 start

  HAL_UART_Receive_IT(&huart3, &rx_data, 1);

  float vel_y = 0.0f;
  float vel_x = 0.0f;
  uint16_t area = 0;
  uint16_t max_area = 4000;
  uint16_t min_area = 3200;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    //Check whether new visual data has been received from the host computer
    if(recv_flag == true){

        //step 1:Convert pixel coordinates to region indices (0~63)
        int8_t cur_zone = received_data.x / ZONE_WIDTH;

        //Boundary protection: Ensure that cur_zone remains within the valid range [0, TOTAL_ZONES-1]
        if(cur_zone < 0) cur_zone = 0;
        if(cur_zone >= TOTAL_ZONES) cur_zone = TOTAL_ZONES - 1;

        //step 2:Initialize the lateral velocity variable
        vel_y = 0.0f;
        vel_x = 0.0f;
        area = 0;
        area = received_data.w * received_data.h;

        //step 3:Calculate speed based on the target area
        //The target is in the left area (area 0~29)
        //The closer the target is to the left, the faster the car should move left to track it
        if(cur_zone < DEAD_ZONE_START){

            //Calculate the "distance" from the boundary of the dead zone
            //When offset = 0, cur_zone = 29 (nearest to the center, with the slowest speed)
            //When offset = 29, cur_zone = 0 (furthest from the center, fastest speed)
            int16_t offset = DEAD_ZONE_START - 1 - cur_zone;

            // Linear interpolation formula for speed:
            // When offset=0: vel_y = -VEL_MIN (the slowest leftward movement speed)
            // When offset=29: vel_y = -VEL_MAX (the fastest leftward moving speed)
            // Formula: vel = base value + (range) * (progress ratio)
            vel_y = -VEL_MIN - (VEL_MAX - VEL_MIN) * (float)offset / (DEAD_ZONE_START - 1);
        }

        //The target is in the right area (area 35~63)
        //The closer the target is to the right, the faster the car should move to the right to track it
        else if(cur_zone > DEAD_ZONE_END){

            int16_t offset = cur_zone - (DEAD_ZONE_END + 1);
            vel_y = VEL_MIN + (VEL_MAX - VEL_MIN) * (float)offset / (TOTAL_ZONES - DEAD_ZONE_END - 1);
        }

        //30~34
        else{
            vel_y = 0.0f;
            if(area > max_area){
                vel_x = -0.06f;
            }
            else if(area < min_area){
                vel_x = 0.06f;
            }
            else{
                vel_x = 0.0f;
            }
        }

        //step4: Execute motion command
        car_move(vel_x, -vel_y, 0.0f);
        //printf("area:%d\r\n", area);

        //Clear the received flag and reset received_data for the next iteration
        clear();
    }

    //No valid visual data has been received
    else{
        car_move(0.0f, 0.0f, 0.0f);
    }
    HAL_Delay(100);

  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Configure the main internal regulator output voltage
  */
  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 4;
  RCC_OscInitStruct.PLL.PLLN = 84;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = 4;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim->Instance == TIM6){
    update(&m1);
    update(&m2);
    update(&m3);
    update(&m4);
  }
}
/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

​

操作手順:

全体の流れ:

(1) STM32CubeMX ソフトウェアでピン割り当てと周辺機能の初期化を設定し、設定コードを生成します。

(2) keil5 で MDK_ARM プロジェクトファイルを開き、コードの編集・コンパイル・デバッグを行います。

基本パラメータ:

本ドキュメントの(ハードウェアモジュール)に従って開発環境を設定した後の基本パラメータは次のとおりです。

ソフトウェアバージョン:STM32CubeMX V6.14.0

ハードウェアプラットフォーム:STM32F407IGT6

コンパイル環境:MDK_ARM V5.3.2

デバッグツール:ST-LINK/V2

1. 新規プロジェクトの作成と MCU の設定

1.1 MCU の選択

  1. STM32CubeMX ソフトウェアを開き、File > New Project をクリックします。

2. MCU の絞り込み条件を設定して STM32F407IGT6 を選択し、バージョン欄をクリックしてから、最後に Start Project をクリックしてプロジェクトの設定を開始します。

  1. プロジェクト設定画面は次のとおりです。

1.2 クロック設定とコア設定

  1. Pinout & Configuration の System Core で、RCC(リセットおよびクロック制御) をクリックします。

  1. HSE クロックソースを Crystal/Ceramic Resonator(水晶またはセラミック振動子)に設定します。

  1. 設定が完了すると、対応するピン(OSC_IN と OSC_OUT)が自動的に設定されます(下図の緑色の部分を参照)。

機能:

  • 外部高速水晶発振器(HSE)を接続し、システムに高精度のクロック源を供給するために使用します。
  • **RCC_OSC_IN** 水晶入力ピン。
  • **RCC_OSC_OUT** 水晶出力ピン(帰還信号)
  • デフォルト機能:
  • **PH0** : **OSC_IN**(外部水晶入力)。
  • **PH1** : **OSC_OUT**(外部水晶出力)。
  1. デバッグインターフェースのピンを設定します。System Core で SYS をクリックし、Debug モードを Serial Wire に設定します。

設定が完了すると、対応するピン配置(pin out)が緑色になります。

緑色の部分をクリックすると、具体的な内容を確認できます。

1.3 クロックツリーの設定

クロックパラメータを順に設定します:1. 入力周波数の値を 8 に変更、2. HSE を選択、3. PLLCLK を選択、4. HCLK パラメータを 84 に変更。その他のパラメータはデフォルトのままで構いません。

ヒント:設定時にはエラーが自動的に検出されます。周波数設定が誤っている場合は、エラーが赤色で表示されます。

2. MCU 初期化パラメータの設定

2.1 USART シリアルポートの設定

  1. PA9 と PA10 をそれぞれ USART1_TX と USART1_RX に設定します。GPIO をクリックし、ピンを設定します(この時点では USART1 が未設定のため、ピンに黄色の警告が表示されます)。

  1. Connectivity — USART1 をクリックし、モードを Asynchronous に設定します(USART1 を設定すると関連ピンが緑色になり、ピン設定が正常であることを示します)。

2.2 プロジェクト管理の設定

  1. デフォルトの画面は次のとおりです。

  1. プロジェクト名を入力します(任意に設定可能)。

  2. プロジェクトの保存先を選択します(C ドライブには保存しないでください)。

  3. コンパイル環境として MDK_ARM を選択します。

  4. バージョン番号 V5.3.2 を選択します。

  5. ファームウェアパッケージ名とバージョン。

(ヒント:デフォルトでは Use latest available version にチェックが入っていますが、ローカル PC に最新バージョンがない場合はエラーになります。その場合はパッケージを自分でダウンロードしてローカルにインストールする必要があります。インストール方法は下記リンクを参照してください。)

UNI-STM32 開発環境の設定【V2.0】(リンク先の「7. よくある設定 Q&A」の Q3 に解決方法があります)

上記の設定が完了すると、下図のようになります。

2.Project Manager > Code Generator ウィンドウを次のように設定します。

3.Project Manager > Advanced Settings ウィンドウで HAL ライブラリを選択します。

  1. プロジェクトを保存します:File — Save Project をクリックします。

3. GPIO 設定

M1 モーターのパラメータ設定

モーター ピン パラメータ User Label
M1 PE13 TIM1_CH3 ​
PE14 TIM1_CH4 ​
PD10 GPIO_Input M1B
PG2 GPIO_EXTI2 M1A
  1. GPIO をクリックして PE13、PE14 ピンを探します。

ピン割り当て図で PE13 をクリックし、左クリックで PE13 の機能を TIM1_CH3 に、PE14 の機能を TIM1_CH4 に設定します。詳細は下図を参照してください(注:ピン割り当て完了後、ピンはオレンジ色になります。これはピン割り当てのみが完了し、対応する周辺機能がまだ有効化されていないことを示します)。

  1. 左側の Timers パネルをクリックして TIM1 の設定を選択し、パラメータを設定します。
  • Channel3:PWM Generation CH3
  • Channel4(チャネル4):PWM Generation CH4

この時点でピンの色が緑色に変われば、正常に設定が完了しています。

  1. GPIO をクリックし、画面下部で PD10 を検索すると、ピンビューの PD10 が点滅します。

PD10 を左クリックし、GPIO_Input を選択します。

User Label を設定します:設定項目で PD10 の行をダブルクリックし、右側の GPIO mode のドロップダウンで Input mode を選択し、User Label に M1B と入力します。

  1. GPIO をクリックし、画面下部で PG2 を検索すると、ピンビューの PG2 が点滅します。

PG2 を左クリックし、GPIO_EXTI2 を選択します。

User Label を設定します:設定項目で PG2 の行をダブルクリックし、右側の GPIO mode のドロップダウンで External Interrupt Mode with Rising/Falling edge trigger detection を選択し、User Label に M1A と入力します。

M2 モーターのパラメータ設定

モーター ピン パラメータ User Label
M2 PB0 TIM3_CH3 ​
PB1 TIM3_CH4 ​
PH14 GPIO_Input M2B
PH15 GPIO_EXTI15 M2A
  1. M1 の設定方法を参考にしてください。M2 モーターのパラメータ設定結果は次のとおりです。

M3 モーターのパラメータ設定

モーター ピン パラメータ User Label
M3 PC8 TIM8_CH3
PC9 TIM8_CH4
PE9 GPIO_EXTI9 M3A
PE11 GPIO_Input M3B
  1. M1 の設定方法を参考にしてください。M3 モーターのパラメータ設定結果は次のとおりです。

M4 モーターのパラメータ設定

モーター ピン パラメータ User Label
M4 PH10 TIM5_CH1
PH11 TIM5_CH2
PD12 GPIO_EXTI12 M4A
PD13 GPIO_Input M4B
  1. M1 の設定方法を参考にしてください。M4 モーターのパラメータ設定結果は次のとおりです。

モーター割り込みの設定

タイマーの役割の比較:

  • モーター用タイマー(TIMxx):モーターの PWM 出力
  • TIM6:制御アルゴリズムの実行周期用タイマー

(1) TIM6 をクリックして Activated を選択します。下の NVIC Settings をクリックし、Enabled を選択します。

(2) NVIC Settings をクリックし、モーターの外部割り込みの Enabled にチェックを入れます。

AI ビジョンモジュールの設定

(2) Connectivity — USART3 をクリックし、モードを Asynchronous に設定します(USART3 を設定すると関連ピンが緑色になり、ピン設定が正常であることを示します)。

(3) 割り込みの設定。NVIC Settings をクリックし、USART3 の割り込みを Enabled に設定します(チェックボックスにチェック)。

​

(4) DMA の設定。DMA Settings をクリックし、Add ボタンをクリックします。

表示された Select 列で Select をクリックし、USART3_TX を選択します。

選択後、表示されるタブはデフォルトのままで構いません。パラメータは下図のように設定します。

続けて Add をクリックし、表示された Select 列で Select をクリックして USART3_RX を選択します。

設定結果は下図のとおりです。これで DMA の設定は完了です。

プロジェクトファイルの再保存

ヒント:必ずプロジェクトファイルを再度保存してください(主にパラメータを保存するため)。File — Save Project をクリックします。

4. コードの生成

1. GENERATE CODE をクリックしてコードを生成します。

  1. コードが自動的に生成されます。

コード生成が成功しました。

5. ユーザーコードの追加

上記で生成したフレームワークをもとに、keil エディタでユーザーコードを追加し、コンパイルとデバッグを行います。

ヒント 1:例えば、main.c の /* USER CODE BEGIN xx */ と /* USER CODE END xx */ の間にユーザーコードを追加します。

ヒント 2:.c ファイルを独自に作成することもできます。

ヒント 3:ユーザーコードは自分で記述できます。(コード解説)に掲載されている例を参考に、真似しながら学習することもできます。

6. コードのダウンロードとテスト

  1. プロジェクトをダウンロードします。ここではデモを用意しています(本ドキュメントの「ソフトウェア設計 — プロジェクトのダウンロード」から直接ダウンロードできます)。

  2. 本ドキュメントの 回路接続 に従って実際の配線を完了します。

  3. STM32 プログラムを書き込みます。Keil で拡張子 .uvprojx のプロジェクトファイルを開き、Load します。

書き込み方法はリンクを参照してください。UNI-STM32 開発環境の設定【V2.0】(「5. プログラムのダウンロード」)。

注:STM32 開発ボードへのプログラムのダウンロード後、ST-Link を PC から取り外してください。

  1. AI ビジョンモジュール(K210)にプログラムをダウンロードします。オフラインで動作させる必要があります(ヒント: オフラインモードでダウンロードすると、その後は K210 ビジョンモジュールに電源を入れるだけで、PC に接続していなくても MicroPython ファイルが実行され、オフライン動作が実現されます)。

(1) Type-C データケーブルの一端を AI モジュールに、もう一端を PC の USB ポートに接続します。

(2) CanMV IDE で serial_send_msg.py を開きます。

(3) 接続ボタンをクリックすると接続画面が表示されます。Advanced Settings にチェックを入れ、Mode-3 を選択して OK をクリックし、接続を開始します(注:シリアルポートは K210 モジュールが接続されているポート番号です。不明な場合は、いったん PC の USB ケーブルを抜いてから K210 モジュールを PC に接続し、新たに追加されたポート番号が該当のシリアルポート番号になります)。

接続が完了すると、下図のように接続成功のマークが表示されます。

(4) 接続に成功したら、ツールバーの「Tools」をクリックし、「Save the currently opened script as (main.py) to CanMV Cam」を選択します。

(5) 続いて「Yes」をクリックします。

(6) 書き込みに成功するとメッセージが表示されるので、「OK」をクリックします。これで MicroPython ファイルが K210 ビジョンモジュールに保存されます。

​

  1. バッテリースイッチと STM32 開発ボードのスイッチをオンにすると、プログラムが動作を開始します。物体を動かして動作を確認してください。

実験結果

物体の移動に合わせて、シャーシがビジョンによる追従を行います。この例では、青い物体が移動すると、シャーシが移動して追従します。

コード解説

1. ユーザーコードの解説

下図は、プロジェクトファイルに追加したユーザーコードを示しています。

2. ユーザーコードの取り込み方法

まず、ユーザー定義ファイル(.c や .h など)を自分のプロジェクトファイルに取り込み、keil5 IDE で .c ファイルを追加してコンパイルします。

既存のユーザーファイルの取り込み:keil で既存のユーザーファイルを取り込む方法 を参照してください。

次に、main.c ファイルにユーザーコードを追加します。

ユーザーコード:/* USER CODE BEGIN xx */ で始まり、/* USER CODE END xx */ で終わります。

(1) まず、参照するプロジェクトファイルのコードを開きます。main.c ファイルは主に stm32cubemx によって自動生成されたものです。このフレームワークをもとに実現したい機能を補完・改良します。特に /* USER xx で始まり終わるコードを確認してください。下図の赤い部分が記入したユーザーコードです。

(2) 上記の赤枠のコードを自分のプロジェクトファイル main.c にコピーし、コンパイルしてダウンロードし、動作を確認します。

技術サポート

資料のダウンロード・技術サポートについては、HumansX株式会社までお問い合わせください。

​