DSP 라이브러리를 사용하기 위해 아래 위치에 있는 "Software Packs > Select Components"를 클릭합니다.
여기서 CMSIS DSP가 보입니다. Selection을 Library로 선택하고 OK를 해 줍니다.
만약 ARM.CMSIS가 설치 되어 있지 않다면 Install을 해 줍니다.
그러면 왼쪽 탭에 Software Packs 메뉴가 생기고 ARM.CMSIS.5.6.0을 클릭하여 CMSIS DSP를 체크해 줍니다.
클럭과 프로젝트 옵션 선택 해 주고
Generate Code를 해 줍니다.
STM32CubeIDE로 가서 해당 프로젝트를 Import하여 프로젝트 구조를 보면 아래와 같이 Middlewares에 libarm_cortexM4lf_math.a 와 arm_math.h가 포함된 것을 확인 할 수 있습니다.
이제 main.c 에 #include "arm_math.h" 를 넣어주고 컴파일을 해 봅니다.
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "arm_math.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();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
- Bosch 사에서 차량 내에서 호스트 컴퓨터 없이 마이크로 콘트롤러나 장치들이 서로 통신하기 위해 설계된 통신 규격
- 1993년에 처음 ISO 11898 국제 표준으로 제정
특징[2]
- Multi Master 구조
- 최대 1Mbps 통신 속도
- 최대 8byte 통신 데이터 수
- 통신 프로토콜 / 에러 처리를 하드웨어적으로 처리
- 다수의 장치(Standard: 11bit, Extended 29bit ID구분)간 통신 가능
- Twist pair wire와 차동신호를 사용하여 노이즈 환경에 강함
- 비용이 경제적
프로토콜[3]
- ISO 11898:2003 표준에서 Standard 11bit 식별자를 사용하여 125kbps ~ 1Mbps의 통신 속도를 제공합니다.
이 표준은 나중에 Extended 29bit 식별자로 수정되었습니다.
- Standard 11bit: 2^11, 2048개의 메세지 식별자
- Extended 29bit: 2^29, 536,870,912개의 메세지 식별자
Standard CAN: 11-bit 식별자 Bit Fields
SOF
11-bit Identifier
RTR
IDE
r0
DLC
0~8 bytes Data
CRC
ACK
EOF
IFS
SOF(Start of frame): 프레임의 시작 비트
Identifier: 메세지의 우선순위를 설정하는 11bit 식별자, 값이 낮을수록 우선 순위가 높다
RTR(single remote transmission request): 다른 노드로부터 정보가 필요할 때 정의. (Data Frame: 0, Remote Frame:1)
IDE(dominant single identifier extension): CAN 식별자 타입 정의 (Standard: 0, Extended: 1)
r0: Reservedbit
DLC(data length code): 송신하는 데이터의 byte 수로 4bit
Data: 송신 데이터로 최대 8byte
CRC(Cyclic redundancy check): 16bit checksum (15bit + 1bit 구분기호), 오류 감지를 위한 비트
EOF(End of Frame): 프레임 끝
IFS(interframe space): 7bit
Extended CAN: 29-bit 식별자 Bit Fields
SOF
11-bit Identifier
SRR
IDE
18-bit Identifier
RTR
r1
r0
DLC
0~8 bytes Data
CRC
ACK
EOF
IFS
SRR(substitute remote request): Extension format의 자리를 표시
IDE(identifier extension): 더 많은 식별자 비트가 뒤따른다는 것을 나타냄
r1: Reserved bit
회로
우선 제가 가지고 있는 보드의 회로도는 아래와 같습니다.
- R26, R27은 사용하고자 하는 CAN Transceiver 모듈에 따라 공급전압을 선택하기 위한 저항으로 사용되었습니다.
- R40은 임피던스 매칭을 위한 저항으로 사용되었습니다.
Transceiver
CAN 통신을 하기 위해서는 MCU에 CAN 핀이 있더라도 별도의 Transceiver를 달아주어야 합니다.
그 이유는 CAN 통신은 CANH와 CANL 라인으로 차동 신호로 통신을 하는데
MCU의 신호를 차동신호로, 차동 신호를 MCU가 받아들일 수 있는 디지털 신호로 변환시켜주는 역할을 하기 때문입니다. 또 정전기(ESD)같은 왜란에 의해 Transceiver가 데미지를 입어 MCU가 손상되는 것을 막아주는 역할이 될 수 있습니다.
종단저항
또 CANH와 CANL 양 종단에 120옴 정도의 저항을 달아주어야 합니다(거리가 가까우면 달아주지 않아도 괜찮다고 하기도 하는것 같습니다.) 120옴을 달아주어야 하는 이유는 신호 왜곡을 보호하기 위한 임피던스 매칭을 위해 ISO 11898 표준으로 정의했기 때문입니다.
퀀텀(Quantum, tq)[4][5]: bit time의 기본 시간 단위
* Synchronization Segment(Sync_Seg): 1 Time Quantum 길이, 다양한 버스 노드를 동기화에 사용
* Propagation Time Segment(Prop_Seg): CAN 네트워크 내에서 물리적 지연 시간을 보상에 사용
* Phase Buffer Segment 1(Phase_Seg1): 에지 위상 오류를 보상하는 데 사용
* Phase Buffer Segment 2(Phase_Seg2): 에지 위상 오류를 보상하는 데 사용
CubeMX 설정
우선 STM32F407 데이터시트를 보면, CAN은 APB1을 사용하고 최대 42MHz의 클럭을 가지는 것을 확인할 수 있습니다.
HCLK 클럭 설정을 아래와 같이 최대로 설정하고 APB1의 클럭이 42MHz라는 것을 확인하였습니다.
Can을 활성화 하고 각 파라미터에 대해 알아보겠습니다.
Prescaler: 통신의 속도를 설정, (1/APB1 * Prescaler = Time Quantum, 1/42M * 16 = 380ns)
// 데이터를 송신예제
/* USER CODE BEGIN PV */
CAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8];
uint32_t TxMailbox;
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
// printf 를 사용하기 위한 함수
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, (uint16_t)len, 100);
return (len);
}
/* USER CODE END 0 */
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_CAN1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
printf("Start\r\n");
/* Can Start */
if (HAL_CAN_Start(&hcan1) != HAL_OK)
{
/* Start Error */
Error_Handler();
}
/* Configure Transmission process */
TxHeader.StdId = 0x321; // Standard Identifier, 0 ~ 0x7FF
TxHeader.ExtId = 0x01; // Extended Identifier, 0 ~ 0x1FFFFFFF
TxHeader.RTR = CAN_RTR_DATA; // 전송하는 메세지의 프레임 타입, DATA or REMOTE
TxHeader.IDE = CAN_ID_STD; // 전송하는 메세지의 식별자 타입, STD or EXT
TxHeader.DLC = 8; // 송신 프레임 길이, 0 ~ 8 byte
TxHeader.TransmitGlobalTime = DISABLE; // 프레임 전송 시작될 때 timestamp counter 값을 capture.
/* Set the data to be transmitted */
TxData[0] = 1;
TxData[1] = 2;
TxData[2] = 3;
TxData[3] = 4;
TxData[4] = 5;
TxData[5] = 6;
TxData[6] = 7;
TxData[7] = 8;
/* Start the Transmission process */
if (HAL_CAN_AddTxMessage(&hcan1, &TxHeader, TxData, &TxMailbox) != HAL_OK)
{
printf("Can Send Fail\r\n");
Error_Handler();
}
printf("Can Send Success\r\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
// CAN 수신 예제
/* USER CODE BEGIN PV */
CAN_FilterTypeDef sFilterConfig; // 필터 설정 구조체 변수
CAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
// printf 를 사용하기 위한 함수
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, (uint16_t)len, 100);
return (len);
}
// CAN 수신 인터럽트 콜백
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *CanHandle)
{
printf("%s\r\n", __FUNCTION__);
/* Get RX message */
if (HAL_CAN_GetRxMessage(CanHandle, CAN_RX_FIFO0, &RxHeader, RxData) != HAL_OK)
{
/* Reception Error */
Error_Handler();
}
printf("StdID: %04lx, IDE: %ld, DLC: %ld\r\n", RxHeader.StdId, RxHeader.IDE, RxHeader.DLC);
printf("Data: %d %d %d %d %d %d %d %d\r\n", RxData[0], RxData[1], RxData[2], RxData[3], RxData[4], RxData[5], RxData[6], RxData[7]);
}
// CAN Error 콜백
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan)
{
printf("%s\r\n", __FUNCTION__);
}
/* USER CODE END 0 */
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_CAN1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
printf("Start\r\n");
/* CAN Filter 설정 */
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000; // 0x00000000 = 모든 ID를 받아들이겠다
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14; // CAN2의 FilterBank시작 위치, CAN2를 사용한다면 FilterBank를 SlaveStartFilterBank보다 크게 설정해야 함.
if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
{
/* Filter configuration Error */
Error_Handler();
}
/* Can Start */
if (HAL_CAN_Start(&hcan1) != HAL_OK)
{
/* Start Error */
Error_Handler();
}
/* Activate CAN RX notification */
if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
/* Notification Error */
Error_Handler();
}
printf("Can Ready!!\r\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
그래서 USART와 같은 통신 인터페이스를 사용할때에는 Queue Buffer 구조를 사용합니다.
큐(Queue)는 FIFO(First-In, First-Out)로 가장 먼저 들어온 데이터가 가장 먼저 나가는 자료 구조입니다.
그 중에서도 원형큐(Circular Queue) 또는 링 버퍼(Ring Buffer)는 다음과 같은 구조를 가지고 있습니다.
만약 원이 다 차서 새로운 데이터가 들어오게 된다면 어떻게 될까요?
-> Data1 자리에 덮어쓰게 됩니다. 그 다음 데이터는 Data2 자리에..
원으로 계속 반복하게 된다고 해서 원형 큐라고 합니다.
원형큐를 다음과 같이 구현할 수 있습니다.
/*
* Queue 구조 예시
* 만약 head가 tail을 역전할 경우 처리는..?
*/
#define MAX_QUEUE_SIZE (255)
uint8_t queue_buffer[MAX_QUEUE_SIZE] = {0,};
uint8_t head;
uint8_t tail;
// push: 새로운 데이터를 Queue buffer에 담는다.
void push(uint8_t new_data)
{
// 새로운 데이터를 queue에 넣는다.
queue_buffer[head] = new_data;
head++;
// head가 queue 마지막 위치에 도달했다면, 0으로 초기화
if (head >= MAX_QUEUE_SIZE) {
head = 0;
}
}
// pop: 가장 오래된 데이터를 가져온다.
uint8_t pop(void)
{
// tail의 위치에 데이터를 가져온다
uint8_t pop_data = queue_buffer[tail];
tail++;
// tail이 queue 마지막 위치에 도달했다면, 0으로 초기화
if (tail >= MAX_QUEUE_SIZE) {
tail = 0;
}
return pop_data;
}
uint8_t isEmpty(void)
{
// head와 tail의 위치가 같으면 queue가 비어있음
return head == tail;
}
위의 코드를 이용하여 USART 데이터를 처리하는 방식을 알아봅시다.
/* USER CODE BEGIN PFP */
uint8_t rx_data;
/*Interrupts RX Callback************************************************************/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
push(rx_data);
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
}
int main(void)
{
/* USER CODE BEGIN 1 */
uint8_t data;
/* 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_USART1_UART_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if (isEmpty() == 0) {
data = pop();
// 데이터 처리
// ...
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
상세히 살펴볼게요
HAL_UART_RxCpltCallback 함수는 Uart Rx 인터럽트가 발생하면 호출되는 함수입니다.
STM에는 Flexible static memory controller(이하 FSMC)라고 하는 동기 / 비동기 외부 메모리를 컨트롤 하기 위한 인터페이스가 있습니다. 여기에서 외부 메모리라함은 SRAM, NOR Flash, NAND Flash 그리고 LCD 모듈들이 있습니다.
LCD를 제어하기 위해서는 SPI, I2C로 제어하는 직렬(serial) 방식이나 8bit 또는 16bit로 제어하는 병렬(parallel) 방식이 있습니다.
오늘은 병렬로 제어하는 방식을 알아봅시다.
병렬 인터페이스에는 Intel 사의 8080 타입, 모토로라 사의 6800 타입이 있습니다.
두 타입의 차이점은 아래와 같습니다.
FSMC를 사용해서 LCD를 제어하기 위해서는 아래와 같이 핀을 연결해야 합니다.
FSMC [D0:D15] = FSMC 16bit 데이터 버스
FSMC NEx = FSMC Chip Select
FSMC NOE = FSMC Output Enable
FSMC NWE = FSMC Write Enable
FSMC Ax = LCD Register와 LCD Display RAM을 선택하기 위한 Address line (0~25)
(LCD의 RS핀과 연결합니다)
다음은 8080 타입 인터페이스의 LCD를 STM의 FSMC 인터페이스와 연결하는 방법을 나타냅니다.
다음은 6800 타입 인터페이스의 LCD를 STM의 FSMC 인터페이스와 연결하는 방법을 나타냅니다.
또, LCD를 제어하기 위한 타이밍 에 대한 설명도 나타냅니다. (이 부분은 수식을 봐도 잘 이해가 안됩니다..)
Address setup time
Address hold time
Data setup time
STM32F1 시리즈에서 FSMC관련된 메모리는 아래와 같이 구성됩니다.
STM Application Note에 보면 Nor Flash/SRAM bank는 병렬 LCD 인터페이스를 제어하는데 알맞다고 설명합니다.
따라서 Bank1을 사용하게 되고 Bank1은 sub bank를 4개를 가지고 있습니다. 이 sub bank에 따라 NEx의 숫자가 정해집니다.
그리고 RS핀에 연결된 Ax핀에 따라 LCD Display RAM를 제어하기 위한 주소가 달라집니다.
위 Table 101에 작은 글씨를 보면, 16bit 메모리의 경우에 내부적으로 HADDR의 25:1 의 범위를 사용한다고 되어있습니다. 즉 Ax에왼쪽으로 1칸 쉬프트 시킨 값을 사용합니다. 여기서 x는 0부터 시작합니다.
인터럽트 설정을 하기 위해서 NVIC에서 USART1 global interrupt Enabled에 체크합니다.
UART 관련 함수
// Uart 송신 인터럽트 함수: Uart Tx로 데이터를 Size만큼 전송하면 인터럽트가 발생한다.
// huart: uart 인스턴스
// pData: 송신 데이터 버퍼
// Size: 송신 데이터 개수
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
// Uart 수신 인터럽트 함수: Uart Rx로 데이터가 Size만큼 들어오면 인터럽트가 발생한다.
// huart: uart 인스턴스
// pData: 수신 데이터 버퍼
// Size: 수신 데이터 개수
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
UART Interrupt예제
/*
Uart Interrupt Echo 예제
인터럽트로 수신한 Uart 데이터를 전송한다.
main.c
*/
uint8_t rx_data;
// 인터럽트 콜백 함수: 인터럽트가 발생되면 이 함수가 호출된다.
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 데이터 1개를 수신하면 인터럽트를 발생시킨다.
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
// 받은 데이터를 전송한다.
HAL_UART_Transmit(&huart1, &rx_data, 1, 10);
}
}
void main ()
{
/* 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_USART1_UART_Init();
/* Initialize interrupts */
MX_NVIC_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
예제에서는 간단하게 인터럽트 수신을 받으면 바로 송신하도록 하였지만,
실제 운용할 때에는 Queue Buffer를 구성하고 main 에서 데이터를 빼내어 사용하여야 합니다.