본문 바로가기
로봇 이야기/mcu

PCA9685 디바이스 드라이버 - stm32f103

by 노땅엔진니어 2022. 12. 13.

1. 들어가기 전에

ST사의 STM32F103은 저가의 MCU로써 아주 저가의 Blue Pill 보드가 있어 취미로 로봇을 만드는데 적합 합니다. 최근에는 정품 Blue Pill보드는 구하기 힘들며 짝퉁 보드만 구할 수 있습니다. 또한, 짝퉁보드를 넘어서 STM32F103까지 짝퉁으로 풀리고 있으며, 오히려 ST사의 정품 칩으로 된 보드를 구하기가 힘듭니다.
정품과 짝퉁 칩의 가장 큰 차이점은 정품은 ST-LINK를 통해서 디버깅이 가능하나 짝퉁은 디버깅은 불가능하고 바이너리 다운로드는 가능 합니다.

 

왼쪽 그림은 Blue Pill 짝퉁 보드로써 칩 마크도 ST사의 로고와 STM32F103이라 프린트되어 있지만 ST-LINK를 연결해 보아야 짝퉁인지 정품인지 알 수 있습니다.
다만, ST사의 HAL 드라이버를 통해 작성된 프로그램은 동작 됩니다.

보드는 헤더핀을 통해 STM32F103의 대부분의 핀을 사용할 수 있어 PCA9685보드와 점퍼선으로 연결할 수 있습니다.

PCA9685와는 GND, VCC(3.3V), PB8(SCL), PB9(SDA) 4선을점퍼 선으로 연결을 합니다.

 

 

PCA9685 디바이스 드라이버 : https://duvallee.tistory.com/7 

2. 디바이스 드라이버 작성

ST사는 STM32 시리즈를 쉽게 사용할 수 있도록 HAL 라이브러리(소스로 제공)를 제공합니다. ST사의 HAL 라이브러리를 사용하여 작성을 합니다.

I2C Port 1

MCU(MicroController Unit)는 하나의 핀을 여러가지 기능으로 사용할 수 있도록 합니다. 기본 핀은 GPIO(General Purpose Input/Output)로써 핀을 로우(LOW, 0)과 하이(HIGH, 1)과 같이 출력을 하거나 외부로 부터 핀에 인가된 로우(0), 하이(1) 상태를 읽을 수 있습니다.

PB8(SCL)과 PB9(SDA)를 HAL API(Application Programming Interface)를 사용하여 I2C의 SCL과 SDA로 설정을 합니다.

GPIO_InitTypeDef GPIO_InitStruct = {0};

__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
__HAL_AFIO_REMAP_I2C1_ENABLE();

I2C SCL과 SDA 핀에는 반드시 풀업(pull-up)이 걸려 있어야 합니다.  Blue Pill 보드의 PB8과 PB9에는 외부 풀업이 걸려 있지 않기 때문에 위 코드에서 GPIO_PULLUP 속성을 사용하면 MCU는 PB8과 PB9에 내부 풀업을 연결 합니다.

 

다음은 MCU에 있는 I2C 포트 1번을 초기화를 하고 사용할 수 있도록 준비를 합니다.

I2C_HandleTypeDef i2c_handle;

__HAL_RCC_I2C1_CLK_ENABLE();

i2c_handle.Instance = I2C1;
i2c_handle.Init.ClockSpeed = 400000;
i2c_handle.Init.DutyCycle = I2C_DUTYCYCLE_2;
i2c_handle.Init.OwnAddress1 = 0;
i2c_handle.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
i2c_handle.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
i2c_handle.Init.OwnAddress2 = 0;
i2c_handle.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
i2c_handle.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;

if (HAL_I2C_Init(&i2c_handle) != HAL_OK)
{
   // error
}

위 코드까지 실행하면 MCU에 있는 I2C 포트 1번으로 PCA9685와 400 kHz(Fast-speed mode)로 통신할 준비가 끝났습니다.

PCA9685 레지스터 정의

C 프로그래밍을 하면서 #define문으로 상수를 정의해 놓으면 프로그램의 가독성이 높아 집니다. 다음은 PCA9685의 슬레이브 주소 및 중요 레지스터를 정의 합니다.

#define DEFAULT_PCA9865_I2C_ADDR               0x80

#define PCA9685_SET_BIT_MASK(BYTE, MASK)       ((BYTE) |= (uint8_t)(MASK))
#define PCA9685_CLEAR_BIT_MASK(BYTE, MASK)     ((BYTE) &= (uint8_t)(~(uint8_t)(MASK)))
#define PCA9685_READ_BIT_MASK(BYTE, MASK)      ((BYTE) & (uint8_t)(MASK))

// MODE1
#define PCA9685_MODE1                          0x00
#define PCA9685_MODE1_SLEEP                    0x10
#define PCA9685_MODE1_RESTART                  0x80

// MODE2
#define PCA9685_MODE2                          0x01

#define PCA9685_LEDX_ON_L                      0x06
#define PCA9685_LEDX_ON_H                      0x07
#define PCA9685_LEDX_OFF_L                     0x08
#define PCA9685_LEDX_OFF_H                     0x09

#define PCA9685_ALL_LED_ON_L                   0xFA
#define PCA9685_ALL_LED_ON_H                   0xFB
#define PCA9685_ALL_LED_OFF_L                  0xFC
#define PCA9685_ALL_LED_OFF_H                  0xFD
#define PCA9685_PRESCALE                       0xFE

#define PCA9685_COUNTER_NUM                    4096
#define PCA9685_OSC_CLOCK_MHZ                  25

static uint8_t prescaler = 254;   // 전역변수

PCA9685 I2C 쓰기 함수

PCA9685의 I2C 쓰기 함수는 2가지 형태로 작성을 합니다. 하나는 한 개의 레지스터에 데이터를 쓰는 함수와 또 하나는 4개의 레지스터에 4개의 데이터를 한 번에 쓰는 함수 입니다.

int i2c_write_pca9685(I2C_HandleTypeDef handle, uint8_t reg_addr, uint8_t reg_data)
{
   uint8_t data[2] = {reg_addr, reg_data);
   if (HAL_I2C_Master_Transmit(&handle, DEFAULT_PCA9865_I2C_ADDR, data, sizeof(data), 0xFFFF) != HAL_OK)
   {
      // error
      return -1;
   }
   return 0;
}

int i2c_write_pca9685_channel(I2C_HandleTypeDef handle, uint8_t reg_addr, uint8_t ch_on_l, uint8_t ch_on_h, uint8_t ch_off_l, uint8_t ch_off_h)
{
   uint8_t data[5] = {reg_addr, ch_on_l, ch_on_h, ch_off_l, ch_off_h);
   if (HAL_I2C_Master_Transmit(&handle, DEFAULT_PCA9865_I2C_ADDR, data, sizeof(data), 0xFFFF) != HAL_OK)
   {
      // error
      return -1;
   }
   return 0;
}

PCA9685 I2C 읽기 함수

PCA9685의 레지스터 값을 읽는 함수 입니다.

int i2c_read_pca9685(I2C_HandleTypeDef handle, uint8_t reg_addr, uint8_t* p_reg_data)
{
   if (HAL_I2C_Master_Transmit(&handle, DEFAULT_PCA9865_I2C_ADDR, reg_addr, sizeof(uint8_t), 0xFFFF) != HAL_OK)
   {
      // error
      return -1;
   }
   if (HAL_I2C_Master_Receive(&handle, DEFAULT_PCA9865_I2C_ADDR, p_reg_data, sizeof(uint8_t), 0xFFFF) != HAL_OK)
   {
      // error
      return -1;
   }
   return 0;
}

여기에서 사용한 I2C HAL 라이브러리 API는 가장 기본적인 API를 사용하였습니다. HAL 라이브러리에는 인터럽트나 DMA를 사용하는 고급 기능의 API도 있습니다.

PCA9685 슬립모드 함수

PCA9685를 슬립모드(sleep mode)로 진입을 하거나 슬립모드인지를 검사하는 API 입니다. sleep_mode_pca9685와 wakeup_pca9685 API에서 모드를 변경하기 위해서는 먼저 MODE1 레지스터를 읽은 후에 관련 비트 값만 수정을 한 후에 저장을 해야 합니다.

int is_sleep_mode_pca9685(I2C_HandleTypeDef handle)
{
   uint8_t mode1_reg = 0;
   if (i2c_read_pca9685(handle, PCA9685_MODE1, &mode1_reg) < 0)
   {
      // error
      return -1;
   }
   if ((mode1_reg & 0x10) == 0x10)
   {
      // sleep mode
      return 1;
   }
   // normal mode
   return 0;
}
int sleep_mode_pca9685(I2C_HandleTypeDef handle)
{
   uint8_t mode1_reg = 0;
   if (i2c_read_pca9685(handle, PCA9685_MODE1, &mode1_reg) < 0)
   {
      // error
      return -1;
   }

   mode1_reg |= 0x10; // sleep mode
   if (i2c_write_pca9685(handle, PCA9685_MODE1, mode1_reg) < 0)
   {
      // error
      return -1;
   }
   return 0;
}

int wakeup_pca9685(I2C_HandleTypeDef handle)
{
   uint8_t mode1_reg = 0;
   if (i2c_read_pca9685(handle, PCA9685_MODE1, &mode1_reg) < 0)
   {
      // error
      return -1;
   }

   PCA9685_CLEAR_BIT_MASK(mode1_reg, 0x10); // normal mode
   PCA9685_CLEAR_BIT_MASK(mode1_reg, 0x80); // disabled restart
   if (i2c_write_pca9685(handle, PCA9685_MODE1, mode1_reg) < 0)
   {
      // error
      return -1;
   }
   return 0;
}

PCA9685 초기화 함수

PCA9685를 초기화를 하고 준비 상태가 됩니다.

PCA9685 PWM 설정 함수

PCA9685의 채널에 PWM 시그널을 출력 합니다.

int set_channel_pca9685(I2C_HandleTypeDef handle, uint8_t ch, pwm_us_time)
{
   uint8_t = reg_addr = 0;
   uint8_t channel_4byte_reg[4] = {0, }
   int off_counter = 0;
   int on_counter = 100;

   if (ch < 0 || ch > 15)
   {
      return -1;   // error
   }
   reg_addr = (ch * 4) + PCA9685_LEDX_ON_L;

   off_counter = ((int) roundf((pwm_us_time * 25.0) / (prescaler + 1.0))) + on_counter;

   channel_4byte_reg[0] = (uint8_t) (on_counter & 0xFF);;  // channe?_on_l
   channel_4byte_reg[1] = (uint8_t) ((on_counter >> 8) & 0xF);  // channe?_on_h
   channel_4byte_reg[2] = (uint8_t) (off_counter & 0xFF);  // channe?_off_l
   channel_4byte_reg[3] = (uint8_t) ((off_counter >> 8) & 0xF);  // channe?_off_h, all off
   if (i2c_write_pca9685_channel(handle, reg_addr, channel_4byte_reg[0], channel_4byte_reg[1], channel_4byte_reg[2], channel_4byte_reg[3]) < 0)
   {
      return -1;  // error
   }

   return 0;
}

댓글