음.. 통신을 CAN으로 했는데 기존 232 프로토콜 하고 맞춰서 사용을 해야 하는 상황이다.
그래서 overlay 느낌으로, can 드라이버 위에다가
can_serialize로 명명해서 하나 만든 후,
write 할 내용은 ring buffer 에 넣고 8바이트 TX 하게 한다.
그리고 TX 완료 인터럽트 뜰때마다 전송할 거 있으면 8바이트 혹은 그 이하로 꺼내서 전송해주도록 했다.
read 내용은 RX 인터럽트 뜨면, ring buffer에 때려 넣고 상위단에 콜백 때려주는 식으로 만들었다.
타깃 MCU는 STM32F103RB이다.
일단 can 드라이버 작성해둔 것인데
can.c -> write 함수 (아래에 중하다 보는 부분을 풀어 적어놨다.)
// can.c
typedef struct {
uint32_t id;
bool id_extended;
uint8_t tx_data[8];
uint8_t length;
} can_write_t;
size_t can_write(void* _self, void* buffer, size_t len) {
can_t* self = (can_t*)_self;
// Write 요청할 때, can id와 버퍼, 그리고 버퍼 길이를 포함한 can_write_t로 받는다.
// can_write 함수는 인터페이스로 추상화가 되어있어서 이렇게 해두었다.
can_write_t* data_to_write = (can_write_t*)buffer;
// 최소한의 검증(구조체가 맞는지..)
if (len != sizeof(can_write_t))
return 0;
// CAN TX Header. RTR, Timestamp 전송, 데이터 길이, CAN ID, ID Type 등을 정의
// HAL로 CAN 전송할때 필요한 객체이다.
CAN_TxHeaderTypeDef tx_header;
tx_header.RTR = CAN_RTR_DATA;
tx_header.TransmitGlobalTime = DISABLE; // Timestamp 전송 안함
tx_header.DLC = data->length; // 데이터 길이(8 Byte 가 max)
tx_header.StdId = data->id; // Standard ID
tx_header.ExtId = data->id; // Extended ID
// ^ 둘다 때려박아놓고, IDE로 ID 지정만 잘 해주면된다.
// ID 타입 Standard ID (0x000 ~ 0x7FF)
tx_header.IDE = CAN_ID_STD;
if (data->id_extended) // 확장 아이디로 되어있으면 CAN_ID_EXT 로 다시 설정해주고.
tx_header.IDE = CAN_ID_EXT;
uint32_t mailbox = 0;
// 후술
if (HAL_CAN_AddTxMessage(self->handle, &tx_header, data->tx_data, &mailbox))
return 0; // 전송 실패
// CAN_TX_MAILBOX0 ~ CAN_TX_MAILBOX2 에 해당되지 않는다면, 전송 요청 실패한 것.
if (mailbox != CAN_TX_MAILBOX0 &&
mailbox != CAN_TX_MAILBOX1 &&
mailbox != CAN_TX_MAILBOX2) {
return 0;
}
// 전송 요청 성공했으면 할당된 메일박스 ID를 Return 해준다.
return mailbox;
}
위 코드 중 가장 중요한 HAL_CAN_AddTxMessage 부분을 조금 더 보겠다.
// can.c
uint32_t mailbox = 0;
if (HAL_CAN_AddTxMessage(self->handle, &tx_header, data->tx_data, &mailbox))
return 0; // 전송 실패
- self->handle
- HAL CAN struct이다.
- tx_header
- TX 관련된 정보 (ID 값, ID 타입(STD/EXT), 전송 데이터 길이(DLC))
- data->tx_data
- 실제로 전송할 데이터 버퍼 (Max 8 Byte)
- mailbox (*핵심)
- mailbox는 전송 요청에 성공하면, 어떤 메일 박스에 할당이 되었는지를 돌려준다.
STM32F103 같은 경우에는
#define CAN_TX_MAILBOX0 (0x00000001U) /*!< Tx Mailbox 0 */
#define CAN_TX_MAILBOX1 (0x00000002U) /*!< Tx Mailbox 1 */
#define CAN_TX_MAILBOX2 (0x00000004U) /*!< Tx Mailbox 2 */
이렇게 총 3개의 TX 메일박스가 있다.
만약에 전송 요청 당시에 메일박스 3개가 다 Busy 상태면 mailbox 변수는 0 이 돼서 나오고,
아니라면 할당된 메일박스 ID를 준다 (CAN_TX_MAILBOX0 ~ CAN_TX_MAILBOX2)
이제 이 할당된 mailbox 값을 return을 해주는 게 내가 만든 can_write 함수가 해주는 일이다.
그러면 윗단에서는 아래와 같이 처리한다.
1. 할당된 mailbox 값을 가지고 있다가 CAN TX 완료 콜백을 받으면(HAL TxMailbox Complete에서 내가 콜 하는 콜백)
2. 기존에 받은 mailbox 값과 대조를 해서 자기 TX가 끝났다는 걸 감지할 수가 있다.
// can.c
// 이거 쓰려면 CAN_IT_TX_MAILBOX_EMPTY <- 이거
// HAL_CAN_ActivateNotification 통해서 활성화 해줘야 한다.
// HAL CAN 완료 콜백 mailbox 0~2
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) {
can_t* self = (can_t*)get_device_by_reference((uint32_t)hcan);
emit_cb(&self->desc, CAN_TX_CH_0_DONE); // my custom callback
}
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan) {
can_t* self = (can_t*)get_device_by_reference((uint32_t)hcan);
emit_cb(&self->desc, CAN_TX_CH_1_DONE); // my custom callback
}
void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan) {
can_t* self = (can_t*)get_device_by_reference((uint32_t)hcan);
emit_cb(&self->desc, CAN_TX_CH_2_DONE); // my custom callback
}
이제 윗단에서 처리하는 것에 대해서 적어보면
-> Mailbox 값 대조를 통해서 자기 Tx가 끝났음을 확인했으니 다음 Tx 요청을 할 수 있다.
윗단에서 콜백 처리하는 것을 보면 이런 식이다.
// can_serilize.c
switch (evt_id) { //
case CAN_TX_CH_0_DONE:
case CAN_TX_CH_1_DONE:
case CAN_TX_CH_2_DONE:
// assigned_mailbox 값이 can_write 에서 할당된 mailbox 값이다.
// 대조해서, 자기가 할당받은 mailbox와, TX 완료된 mailbox를 비교해서
// 같은 mailbox 라고 하면
// write 할 데이터가 더 있으면 다음 can_write를 진행하게된다.
if (self->assigned_mailbox == CAN_TX_MAILBOX0 && evt_id == CAN_TX_CH_0_DONE ||
self->assigned_mailbox == CAN_TX_MAILBOX1 && evt_id == CAN_TX_CH_1_DONE ||
self->assigned_mailbox == CAN_TX_MAILBOX2 && evt_id == CAN_TX_CH_2_DONE) {
// CAN 으로 전송할 데이터가 남아있는가? (Lightweight Ringbuffer 라이브러리 사용)
if (lwrb_get_full(&self->tx_rb)) {
// 얼마나 남아있는가? 8개 이상이면 8, 아니면 그대로 tx.length에 설정한다.
self->tx.length = lwrb_read(&self->tx_rb, self->tx.tx_data, 8);
// can_write call 한다. (interface로 추상화 되어있어서 write로..)
self->assigned_mailbox = write(self->dev_can, &self->tx, sizeof(can_write_t));
} else {
// 쓸 데이터가 없으면 할당받은 mailbox 값 초기화 해버린다.
// 그래야 지금 TX 중인지 여부 확인 할 수 있으니
// (HAL에 정의된 mailbox ID중 0은 없기에 이걸로 TX 중인지 여부 판단한다.)
self->assigned_mailbox = 0;
}
}
break;
}
암튼 이런 식으로 해놓고 RX도 이런 식으로 해뒀는데, 잘 되는 것 같아서 좋다.
그리고 can_serialize 도 인터페이스로 추상화해놓았다.
그래서 최종 사용 단에서는 uart 인터페이스와 사용하는 게 전혀 다르지 않다.
나중에 can에서 232로 바꿀 일이 생긴다면, 그리고 vice versa
초기화 코드에서 장치 ID 하나만 바꾸면, 로직은 안 건들여도 된다.
comm_init(UART_1) 이걸 comm_init(CAN_SERIALIZE)
이런 식으로 만.
계속 더 좋은 코드가 뭔지에 대한 고민을 하고 여러 시행착오를 겪었는데,
인터페이스 통하는 방식이 지금까지 내가 시도해봤던 방식 중엔 제일 깔끔한 것 같다.
C 쓰다가 Python으로 넘어오면서 여러가지 실험을 해볼 수 있었는데, 코드가 개선되 것 같아서 기분이 좋다.
나중에 코드 정리해서 github에 올려야겠다.

'개발 > MCU' 카테고리의 다른 글
| STM32 MCU C언어 통신 인터페이스 설계(1) (0) | 2022.06.01 |
|---|