Compare commits
10 Commits
fe6598ca60
...
7e3971dab3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e3971dab3 | |||
| 02da823a94 | |||
| 34cbd92808 | |||
| 3dc636e012 | |||
| 873ac438f3 | |||
| 13f2c8fe64 | |||
| 11bf8b6c11 | |||
| 1248a6551a | |||
| 54f0ba7b0e | |||
| c7798c02db |
93
README.md
Normal file
93
README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# templates
|
||||||
|
|
||||||
|
Общий репозиторий переносимых библиотек. Одно место, откуда прошивки и
|
||||||
|
инструменты забирают код, который не привязан к плате и повторяется из
|
||||||
|
проекта в проект.
|
||||||
|
|
||||||
|
Правило отбора одно: **ядро не знает о платформе**. Ни HAL, ни ОС, ни
|
||||||
|
динамической памяти, ни глобального состояния — всё аппаратное живёт в порте,
|
||||||
|
который приносит с собой проект. Библиотеки не зависят друг от друга и
|
||||||
|
подключаются поодиночке.
|
||||||
|
|
||||||
|
```
|
||||||
|
templates/
|
||||||
|
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
||||||
|
python/ модули на чистом Python 3.9+, только stdlib
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что лежит
|
||||||
|
|
||||||
|
### C
|
||||||
|
|
||||||
|
| Библиотека | Что делает | Зависимости | Что нужно от платформы |
|
||||||
|
|---|---|---|---|
|
||||||
|
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
|
||||||
|
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
|
||||||
|
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
|
||||||
|
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
||||||
|
| [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра |
|
||||||
|
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порт STM32F1 в комплекте** |
|
||||||
|
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** |
|
||||||
|
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
| Модуль | Что делает | Зависимости |
|
||||||
|
|---|---|---|
|
||||||
|
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
||||||
|
|
||||||
|
Кодировщики `c/protocan-transport` и `python/protocan` дают побайтово
|
||||||
|
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
||||||
|
|
||||||
|
## Как подключить к проекту
|
||||||
|
|
||||||
|
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
|
||||||
|
|
||||||
|
```
|
||||||
|
git submodule add https://git.rd12.ru/Andrey/templates.git lib/templates
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
Дальше в сборку добавляются только нужные каталоги:
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/templates/c/st7789/st7789.c
|
||||||
|
lib/templates/c/menu/menu.c
|
||||||
|
```
|
||||||
|
|
||||||
|
**Subtree** — когда правки чаще идут из проекта в библиотеку:
|
||||||
|
|
||||||
|
```
|
||||||
|
git subtree add --prefix lib/templates https://git.rd12.ru/Andrey/templates.git master --squash
|
||||||
|
git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git master --squash
|
||||||
|
```
|
||||||
|
|
||||||
|
Копировать файлы руками не нужно: копия расходится с оригиналом за пару
|
||||||
|
недель, и потом непонятно, какая версия правильная.
|
||||||
|
|
||||||
|
## Где эти библиотеки уже работают
|
||||||
|
|
||||||
|
| Проект | Что берёт |
|
||||||
|
|---|---|
|
||||||
|
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20 |
|
||||||
|
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
|
||||||
|
| `CAN_to_RS485` | protocan-transport, python/protocan |
|
||||||
|
| `SETGUI` | python/protocan |
|
||||||
|
| `k1921vk028` | rtc-service |
|
||||||
|
|
||||||
|
## Что сюда не попало и почему
|
||||||
|
|
||||||
|
| Код | Почему не переносится | Что можно вытащить |
|
||||||
|
|---|---|---|
|
||||||
|
| `SETCAN/Src/protocan.c` | 59 вызовов `HAL_*`, привязка к `CAN_HandleTypeDef`, `RTC_HandleTypeDef`, `NVIC_SystemReset()` | разбиение длинных посылок на кадры и проверка даты — чистая логика, нужны два интерфейса: «отправить кадр» и «часы». Раскладка идентификатора уже вынесена в `pcan_id` |
|
||||||
|
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
||||||
|
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
||||||
|
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
||||||
|
|
||||||
|
Строчку в этой таблице стоит завести, когда очередной драйвер решили не
|
||||||
|
переносить: следующий человек не будет разбираться заново.
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
[CONTRIBUTING.md](CONTRIBUTING.md) — что считается переносимой библиотекой,
|
||||||
|
как оформлять порт, как называть коммиты.
|
||||||
53
c/can-sensor/README.md
Normal file
53
c/can-sensor/README.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# can-sensor
|
||||||
|
|
||||||
|
Передача 64-битных идентификаторов датчиков (ROM 1-Wire) по шине CAN.
|
||||||
|
|
||||||
|
Ядро на C99: не включает заголовки периферии, не обращается к регистрам,
|
||||||
|
не пользуется прерываниями. Обмен идёт через таблицу `CanSensor_Io`,
|
||||||
|
которую заполняет порт платы.
|
||||||
|
|
||||||
|
Сообщение состоит из двух классических CAN-кадров — 64-битный идентификатор
|
||||||
|
и преамбула вместе не помещаются в восемь байтов поля данных:
|
||||||
|
|
||||||
|
```
|
||||||
|
кадр 1 — преамбула, DLC = 2 | команда | позиция |
|
||||||
|
кадр 2 — идентификатор, DLC = 8| ROM 8 байт |
|
||||||
|
```
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Файл | Что делает | Зависимости |
|
||||||
|
|---|---|---|
|
||||||
|
| `can_sensor.h`, `can_sensor.c` | сборка и разбор пары кадров, повторы передачи, счётчики обмена | `stdint.h` |
|
||||||
|
|
||||||
|
Приём собирает сообщение сам: кадр ROM без преамбулы и кадр с неверной
|
||||||
|
длиной отбрасываются и учитываются в `dropped_frames`.
|
||||||
|
|
||||||
|
## Что нужно от платформы
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t send(void *ctx, const CanSensor_Frame *frame); /* 1 — кадр принят */
|
||||||
|
uint8_t receive(void *ctx, CanSensor_Frame *frame); /* 1 — кадр получен */
|
||||||
|
```
|
||||||
|
|
||||||
|
`receive` необязателен: с нулевым полем узел работает только на передачу.
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```c
|
||||||
|
CanSensor link;
|
||||||
|
CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board };
|
||||||
|
CanSensor_Config config;
|
||||||
|
CanSensor_ConfigDefault(&config); /* tx_id 0x200, кадр ROM 0x201, 3 попытки */
|
||||||
|
|
||||||
|
CanSensor_Init(&link, &io, &config);
|
||||||
|
CanSensor_SendId(&link, position, rom);
|
||||||
|
|
||||||
|
CanSensor_Message message;
|
||||||
|
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверено в проектах
|
||||||
|
|
||||||
|
`KONOR_ds18b20` — bxCAN на STM32F103C8T6. Естественная пара — [`ds18b20`](../ds18b20):
|
||||||
|
ROM-коды, найденные `DS18B20_Search()`, уходят в шину как есть.
|
||||||
246
c/can-sensor/can_sensor.c
Normal file
246
c/can-sensor/can_sensor.c
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
/**
|
||||||
|
* @file can_sensor.c
|
||||||
|
* @brief Сборка, передача и разбор сообщений «преамбула + идентификатор».
|
||||||
|
*
|
||||||
|
* Реализация не хранит очередей и не пользуется временем: состояние приёма
|
||||||
|
* ограничено признаком принятой преамбулы, поэтому библиотека одинаково
|
||||||
|
* работает и в главном цикле, и в обработчике прерывания порта.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "can_sensor.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает идентификатор кадра данных для заданных настроек.
|
||||||
|
*
|
||||||
|
* @param config Настройки узла.
|
||||||
|
* @return Идентификатор кадра ROM при передаче.
|
||||||
|
*/
|
||||||
|
static uint32_t can_sensor_tx_data_id(const CanSensor_Config *config)
|
||||||
|
{
|
||||||
|
if (config->tx_data_id != 0U) {
|
||||||
|
return config->tx_data_id;
|
||||||
|
}
|
||||||
|
return config->tx_id + CAN_SENSOR_DATA_ID_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает ожидаемый идентификатор преамбулы при приёме.
|
||||||
|
*
|
||||||
|
* @param config Настройки узла.
|
||||||
|
* @return Идентификатор кадра преамбулы.
|
||||||
|
*/
|
||||||
|
static uint32_t can_sensor_rx_id(const CanSensor_Config *config)
|
||||||
|
{
|
||||||
|
if (config->rx_id != 0U) {
|
||||||
|
return config->rx_id;
|
||||||
|
}
|
||||||
|
return config->tx_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает ожидаемый идентификатор кадра ROM при приёме.
|
||||||
|
*
|
||||||
|
* @param config Настройки узла.
|
||||||
|
* @return Идентификатор кадра идентификатора датчика.
|
||||||
|
*/
|
||||||
|
static uint32_t can_sensor_rx_data_id(const CanSensor_Config *config)
|
||||||
|
{
|
||||||
|
if (config->rx_data_id != 0U) {
|
||||||
|
return config->rx_data_id;
|
||||||
|
}
|
||||||
|
return can_sensor_rx_id(config) + CAN_SENSOR_DATA_ID_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Передаёт один кадр с повторами при отказе контроллера.
|
||||||
|
*
|
||||||
|
* @param link Состояние узла.
|
||||||
|
* @param frame Передаваемый кадр.
|
||||||
|
* @return 1, если кадр принят контроллером, иначе 0.
|
||||||
|
*/
|
||||||
|
static uint8_t can_sensor_send_frame(CanSensor *link, const CanSensor_Frame *frame)
|
||||||
|
{
|
||||||
|
uint8_t attempt;
|
||||||
|
|
||||||
|
for (attempt = 0U; attempt < link->config.retries; attempt++) {
|
||||||
|
if (link->io.send(link->io.context, frame) != 0U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CanSensor_ConfigDefault(CanSensor_Config *config)
|
||||||
|
{
|
||||||
|
if (config == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config->tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||||
|
config->tx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
|
||||||
|
config->rx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||||
|
config->rx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
|
||||||
|
config->extended = 0U;
|
||||||
|
config->retries = CAN_SENSOR_DEFAULT_RETRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||||
|
const CanSensor_Config *config)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
if ((link == 0) || (io == 0) || (io->send == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
link->io = *io;
|
||||||
|
if (config != 0) {
|
||||||
|
link->config = *config;
|
||||||
|
} else {
|
||||||
|
CanSensor_ConfigDefault(&link->config);
|
||||||
|
}
|
||||||
|
if (link->config.tx_id == 0U) {
|
||||||
|
link->config.tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||||
|
}
|
||||||
|
if (link->config.retries == 0U) {
|
||||||
|
link->config.retries = CAN_SENSOR_DEFAULT_RETRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
link->rx.command = 0U;
|
||||||
|
link->rx.position = 0U;
|
||||||
|
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||||
|
link->rx.id[index] = 0U;
|
||||||
|
}
|
||||||
|
link->rx_preamble = 0U;
|
||||||
|
link->sent_messages = 0U;
|
||||||
|
link->send_errors = 0U;
|
||||||
|
link->received_messages = 0U;
|
||||||
|
link->dropped_frames = 0U;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||||
|
uint8_t command, uint16_t position)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
if ((config == 0) || (frame == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
frame->id = config->tx_id;
|
||||||
|
frame->extended = config->extended;
|
||||||
|
frame->length = CAN_SENSOR_PREAMBLE_SIZE;
|
||||||
|
frame->data[0] = command;
|
||||||
|
/* Позиция занимает два байта в порядке от старшего к младшему. */
|
||||||
|
frame->data[1] = (uint8_t)((position >> 8U) & 0xFFU);
|
||||||
|
frame->data[2] = (uint8_t)(position & 0xFFU);
|
||||||
|
for (index = CAN_SENSOR_PREAMBLE_SIZE; index < CAN_SENSOR_MAX_DATA; index++) {
|
||||||
|
frame->data[index] = 0U;
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||||
|
const uint8_t *id)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
if ((config == 0) || (frame == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
frame->id = can_sensor_tx_data_id(config);
|
||||||
|
frame->extended = config->extended;
|
||||||
|
frame->length = CAN_SENSOR_ID_SIZE;
|
||||||
|
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||||
|
frame->data[index] = (id != 0) ? id[index] : 0U;
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CanSensor_Send(CanSensor *link, uint8_t command, uint16_t position,
|
||||||
|
const uint8_t *id)
|
||||||
|
{
|
||||||
|
CanSensor_Frame frame;
|
||||||
|
|
||||||
|
if ((link == 0) || (link->io.send == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
(void)CanSensor_BuildPreamble(&link->config, &frame, command, position);
|
||||||
|
if (can_sensor_send_frame(link, &frame) == 0U) {
|
||||||
|
link->send_errors++;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
(void)CanSensor_BuildIdFrame(&link->config, &frame, id);
|
||||||
|
if (can_sensor_send_frame(link, &frame) == 0U) {
|
||||||
|
/* Одиночная преамбула приёмником отбрасывается, позиция не изменится. */
|
||||||
|
link->send_errors++;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
link->sent_messages++;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CanSensor_SendId(CanSensor *link, uint16_t position, const uint8_t *id)
|
||||||
|
{
|
||||||
|
if (id == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return CanSensor_Send(link, CAN_SENSOR_CMD_WRITE_POSITION, position, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
||||||
|
CanSensor_Message *out)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
if ((link == 0) || (frame == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((frame->id == can_sensor_rx_id(&link->config))
|
||||||
|
&& (frame->extended == link->config.extended)) {
|
||||||
|
if (frame->length < CAN_SENSOR_PREAMBLE_SIZE) {
|
||||||
|
link->dropped_frames++;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
link->rx.command = frame->data[0];
|
||||||
|
link->rx.position = (uint16_t)(((uint16_t)frame->data[1] << 8U)
|
||||||
|
| frame->data[2]);
|
||||||
|
link->rx_preamble = 1U;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((frame->id != can_sensor_rx_data_id(&link->config))
|
||||||
|
|| (frame->extended != link->config.extended)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if ((link->rx_preamble == 0U) || (frame->length < CAN_SENSOR_ID_SIZE)) {
|
||||||
|
/* Идентификатор без преамбулы не говорит, в какую позицию его писать. */
|
||||||
|
link->rx_preamble = 0U;
|
||||||
|
link->dropped_frames++;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||||
|
link->rx.id[index] = frame->data[index];
|
||||||
|
}
|
||||||
|
link->rx_preamble = 0U;
|
||||||
|
link->received_messages++;
|
||||||
|
if (out != 0) {
|
||||||
|
*out = link->rx;
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out)
|
||||||
|
{
|
||||||
|
CanSensor_Frame frame;
|
||||||
|
|
||||||
|
if ((link == 0) || (link->io.receive == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
while (link->io.receive(link->io.context, &frame) != 0U) {
|
||||||
|
if (CanSensor_HandleFrame(link, &frame, out) != 0U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
217
c/can-sensor/can_sensor.h
Normal file
217
c/can-sensor/can_sensor.h
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
/**
|
||||||
|
* @file can_sensor.h
|
||||||
|
* @brief Портируемая передача идентификаторов датчиков по шине CAN.
|
||||||
|
*
|
||||||
|
* Библиотека не привязана к микроконтроллеру: она не включает заголовки
|
||||||
|
* периферии, не обращается к регистрам и не пользуется прерываниями. Обмен
|
||||||
|
* идёт через таблицу обратных вызовов CanSensor_Io, которую заполняет порт
|
||||||
|
* платы (для этой сборки — @c src/can.c поверх bxCAN STM32F103C8T6).
|
||||||
|
*
|
||||||
|
* Сообщение состоит из двух кадров классического CAN, потому что 64-битный
|
||||||
|
* идентификатор датчика и преамбула вместе не помещаются в восемь байтов
|
||||||
|
* поля данных:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* кадр 1 — преамбула, DLC = 2 +---------+----------+
|
||||||
|
* | команда | позиция |
|
||||||
|
* +---------+----------+
|
||||||
|
* кадр 2 — идентификатор, DLC=8 +----------------------------+
|
||||||
|
* | ROM датчика, байты 0..7 |
|
||||||
|
* +----------------------------+
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Преамбула несёт команду записи датчика в позицию: первый байт — код
|
||||||
|
* команды (@ref CAN_SENSOR_CMD_WRITE_POSITION), второй — номер позиции в
|
||||||
|
* таблице узла-приёмника. Кадр идентификатора передаётся сразу за преамбулой
|
||||||
|
* и без неё считается недействительным, поэтому приёмник не запишет ROM в
|
||||||
|
* позицию, которая ему не была назначена.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef CAN_SENSOR_H
|
||||||
|
#define CAN_SENSOR_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/** Длина идентификатора датчика (ROM 1-Wire) в байтах. */
|
||||||
|
#define CAN_SENSOR_ID_SIZE 8U
|
||||||
|
|
||||||
|
/** Длина преамбулы: код команды (1 байт) и номер позиции (2 байта, u16 BE). */
|
||||||
|
#define CAN_SENSOR_PREAMBLE_SIZE 3U
|
||||||
|
|
||||||
|
/** Предельная длина поля данных классического кадра CAN. */
|
||||||
|
#define CAN_SENSOR_MAX_DATA 8U
|
||||||
|
|
||||||
|
/** Код команды «записать идентификатор датчика в позицию». */
|
||||||
|
#define CAN_SENSOR_CMD_WRITE_POSITION 0xA1U
|
||||||
|
|
||||||
|
/** Код команды «очистить позицию»; кадр идентификатора передаётся нулевым. */
|
||||||
|
#define CAN_SENSOR_CMD_CLEAR_POSITION 0xA2U
|
||||||
|
|
||||||
|
/** Смещение идентификатора кадра данных относительно кадра преамбулы. */
|
||||||
|
#define CAN_SENSOR_DATA_ID_OFFSET 1U
|
||||||
|
|
||||||
|
/** Число попыток передачи одного кадра по умолчанию. */
|
||||||
|
#define CAN_SENSOR_DEFAULT_RETRIES 3U
|
||||||
|
|
||||||
|
/** Идентификатор кадра преамбулы по умолчанию (стандартный, 11 бит). */
|
||||||
|
#define CAN_SENSOR_DEFAULT_TX_ID 0x200U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Кадр шины CAN в форме, не зависящей от контроллера.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint32_t id; /**< Идентификатор кадра, 11 или 29 бит. */
|
||||||
|
uint8_t extended; /**< 1 — расширенный идентификатор. */
|
||||||
|
uint8_t length; /**< Число значащих байтов поля данных. */
|
||||||
|
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Поле данных кадра. */
|
||||||
|
} CanSensor_Frame;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Принятое или собранное сообщение: преамбула и идентификатор датчика.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t command; /**< Код команды из преамбулы. */
|
||||||
|
uint16_t position; /**< Позиция записи из преамбулы. */
|
||||||
|
uint8_t id[CAN_SENSOR_ID_SIZE]; /**< Идентификатор датчика. */
|
||||||
|
} CanSensor_Message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Доступ библиотеки к контроллеру CAN.
|
||||||
|
*
|
||||||
|
* Обязателен только @c send; при нулевом @c receive функция CanSensor_Poll()
|
||||||
|
* ничего не делает и узел работает только на передачу.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/** Ставит кадр в очередь передачи; 1 — кадр принят контроллером. */
|
||||||
|
uint8_t (*send)(void *context, const CanSensor_Frame *frame);
|
||||||
|
/** Забирает принятый кадр; 1 — кадр получен, 0 — очередь пуста. */
|
||||||
|
uint8_t (*receive)(void *context, CanSensor_Frame *frame);
|
||||||
|
void *context; /**< Контекст порта, передаётся вызовам без изменений. */
|
||||||
|
} CanSensor_Io;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Идентификаторы кадров и режим передачи.
|
||||||
|
*
|
||||||
|
* Нулевые поля заменяются значениями по умолчанию вызовом
|
||||||
|
* CanSensor_ConfigDefault() или самой CanSensor_Init().
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint32_t tx_id; /**< Идентификатор кадра преамбулы при передаче. */
|
||||||
|
uint32_t tx_data_id; /**< Идентификатор кадра ROM; 0 — @c tx_id + 1. */
|
||||||
|
uint32_t rx_id; /**< Ожидаемая преамбула при приёме; 0 — @c tx_id. */
|
||||||
|
uint32_t rx_data_id; /**< Ожидаемый кадр ROM; 0 — @c rx_id + 1. */
|
||||||
|
uint8_t extended; /**< 1 — расширенные идентификаторы 29 бит. */
|
||||||
|
uint8_t retries; /**< Попыток передачи кадра; 0 — значение по умолчанию. */
|
||||||
|
} CanSensor_Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние узла: настройки, счётчики и сборка принимаемого сообщения.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
CanSensor_Io io; /**< Обратные вызовы порта. */
|
||||||
|
CanSensor_Config config; /**< Идентификаторы кадров и режим передачи. */
|
||||||
|
CanSensor_Message rx; /**< Собираемое сообщение приёма. */
|
||||||
|
uint8_t rx_preamble; /**< 1 — преамбула принята, ожидается кадр ROM. */
|
||||||
|
uint32_t sent_messages; /**< Полностью переданные сообщения. */
|
||||||
|
uint32_t send_errors; /**< Сообщения, не ушедшие в шину. */
|
||||||
|
uint32_t received_messages; /**< Полностью принятые сообщения. */
|
||||||
|
uint32_t dropped_frames; /**< Кадры без преамбулы или с неверной длиной. */
|
||||||
|
} CanSensor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Заполняет настройки значениями по умолчанию.
|
||||||
|
*
|
||||||
|
* Идентификатор преамбулы — @ref CAN_SENSOR_DEFAULT_TX_ID, кадр ROM идёт
|
||||||
|
* следующим идентификатором, приём настроен на те же значения.
|
||||||
|
*
|
||||||
|
* @param config Настройки, принадлежащие вызывающему коду.
|
||||||
|
*/
|
||||||
|
void CanSensor_ConfigDefault(CanSensor_Config *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Готовит узел к работе.
|
||||||
|
*
|
||||||
|
* @param link Состояние узла, принадлежащее вызывающему коду.
|
||||||
|
* @param io Обратные вызовы порта; копируются внутрь состояния.
|
||||||
|
* @param config Настройки либо 0 для значений по умолчанию.
|
||||||
|
* @return 1 при успешной настройке, 0 при неполных аргументах.
|
||||||
|
*/
|
||||||
|
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||||
|
const CanSensor_Config *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Собирает кадр преамбулы.
|
||||||
|
*
|
||||||
|
* @param config Настройки узла.
|
||||||
|
* @param frame Кадр приёмника.
|
||||||
|
* @param command Код команды, например @ref CAN_SENSOR_CMD_WRITE_POSITION.
|
||||||
|
* @param position Номер позиции записи датчика.
|
||||||
|
* @return 1 при успешной сборке, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||||
|
uint8_t command, uint16_t position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Собирает кадр идентификатора датчика.
|
||||||
|
*
|
||||||
|
* @param config Настройки узла.
|
||||||
|
* @param frame Кадр приёмника.
|
||||||
|
* @param id Идентификатор датчика длиной @ref CAN_SENSOR_ID_SIZE либо 0 для
|
||||||
|
* нулевого кадра команды очистки.
|
||||||
|
* @return 1 при успешной сборке, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||||
|
const uint8_t *id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Передаёт преамбулу и идентификатор датчика.
|
||||||
|
*
|
||||||
|
* Кадры уходят подряд; при отказе контроллера передача повторяется
|
||||||
|
* @c retries раз. Если преамбула ушла, а кадр ROM — нет, сообщение считается
|
||||||
|
* несостоявшимся: приёмник отбросит одиночную преамбулу.
|
||||||
|
*
|
||||||
|
* @param link Состояние узла.
|
||||||
|
* @param command Код команды преамбулы.
|
||||||
|
* @param position Номер позиции записи датчика.
|
||||||
|
* @param id Идентификатор датчика либо 0 для команды очистки позиции.
|
||||||
|
* @return 1, если оба кадра приняты контроллером, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t CanSensor_Send(CanSensor *link, uint8_t command, uint16_t position,
|
||||||
|
const uint8_t *id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Передаёт команду записи датчика в позицию.
|
||||||
|
*
|
||||||
|
* Краткая форма CanSensor_Send() с кодом @ref CAN_SENSOR_CMD_WRITE_POSITION.
|
||||||
|
*
|
||||||
|
* @param link Состояние узла.
|
||||||
|
* @param position Номер позиции записи датчика.
|
||||||
|
* @param id Идентификатор датчика длиной @ref CAN_SENSOR_ID_SIZE.
|
||||||
|
* @return 1, если оба кадра приняты контроллером, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t CanSensor_SendId(CanSensor *link, uint16_t position, const uint8_t *id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Разбирает принятый кадр и собирает из пары кадров сообщение.
|
||||||
|
*
|
||||||
|
* Кадр идентификатора без предшествующей преамбулы отбрасывается, а новая
|
||||||
|
* преамбула заменяет незавершённую: сборка не требует таймера.
|
||||||
|
*
|
||||||
|
* @param link Состояние узла.
|
||||||
|
* @param frame Принятый кадр.
|
||||||
|
* @param out Приёмник готового сообщения либо 0.
|
||||||
|
* @return 1, если сообщение собрано полностью, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
||||||
|
CanSensor_Message *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Забирает кадры у порта и возвращает первое собранное сообщение.
|
||||||
|
*
|
||||||
|
* @param link Состояние узла.
|
||||||
|
* @param out Приёмник сообщения либо 0.
|
||||||
|
* @return 1, если сообщение собрано, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out);
|
||||||
|
|
||||||
|
#endif /* CAN_SENSOR_H */
|
||||||
35
c/ds18b20/CMakeLists.txt
Normal file
35
c/ds18b20/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(ds18b20 C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# Переносимое ядро: ни регистров, ни задержек, ни stm32f10x.h.
|
||||||
|
add_library(ds18b20 STATIC
|
||||||
|
src/onewire.c
|
||||||
|
src/ds18b20.c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(ds18b20 PUBLIC include)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(ds18b20 PRIVATE /W4)
|
||||||
|
else()
|
||||||
|
target_compile_options(ds18b20 PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Порты (ports/) сюда не входят: им нужен CMSIS-заголовок целевого МК,
|
||||||
|
# они подключаются напрямую в проект прошивки.
|
||||||
|
|
||||||
|
option(DS18B20_BUILD_TESTS "Собирать тесты на симуляторе шины" ON)
|
||||||
|
|
||||||
|
if(DS18B20_BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_executable(test_ds18b20
|
||||||
|
tests/test_ds18b20.c
|
||||||
|
tests/fake_onewire.c
|
||||||
|
)
|
||||||
|
target_link_libraries(test_ds18b20 PRIVATE ds18b20)
|
||||||
|
target_include_directories(test_ds18b20 PRIVATE tests)
|
||||||
|
add_test(NAME ds18b20 COMMAND test_ds18b20)
|
||||||
|
endif()
|
||||||
127
c/ds18b20/README.md
Normal file
127
c/ds18b20/README.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# ds18b20
|
||||||
|
|
||||||
|
Драйвер термометров **DS18B20** поверх программной шины **1-Wire**.
|
||||||
|
Вынесен из проекта `KONOR_ds18b20` и подключается в него сабмодулем.
|
||||||
|
|
||||||
|
Ядро на C99: ни динамической памяти, ни ОС, ни `stm32f10x.h`. Всё
|
||||||
|
аппаратно-зависимое собрано в порте — на новую платформу переносятся
|
||||||
|
пять функций, остальное собирается как есть.
|
||||||
|
|
||||||
|
```
|
||||||
|
приложение
|
||||||
|
│
|
||||||
|
ds18b20.c поиск датчиков, чтение и запись scratchpad
|
||||||
|
│
|
||||||
|
onewire.c побайтовый обмен, CRC8, SEARCH ROM, MATCH/SKIP ROM
|
||||||
|
│
|
||||||
|
ports/<платформа> Init, DelayUs, Reset, WriteBit, ReadBit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Файл | Что делает | Зависимости |
|
||||||
|
|---|---|---|
|
||||||
|
| `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` |
|
||||||
|
| `include/onewire.h`, `src/onewire.c` | байты и блоки, CRC8 Dallas/Maxim, обход SEARCH ROM | `stdint.h` |
|
||||||
|
| `ports/stm32f1/` | тайминги Maxim, вывод с открытым стоком, задержки по DWT | CMSIS `stm32f10x.h` |
|
||||||
|
| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — |
|
||||||
|
|
||||||
|
## Порт: что нужно от платформы
|
||||||
|
|
||||||
|
```c
|
||||||
|
void OneWire_Init(void); /* настроить вывод и источник задержек */
|
||||||
|
void OneWire_DelayUs(uint32_t us);
|
||||||
|
uint8_t OneWire_Reset(void); /* сброс и проверка presence */
|
||||||
|
void OneWire_WriteBit(uint8_t bit);
|
||||||
|
uint8_t OneWire_ReadBit(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
Всё остальное — `OneWire_WriteByte`, `OneWire_ReadByte`, `OneWire_Crc8`,
|
||||||
|
`OneWire_SearchNext`, `OneWire_SelectRom`, `OneWire_SkipRom` и весь
|
||||||
|
`ds18b20.c` — уже написано и от платформы не зависит.
|
||||||
|
|
||||||
|
### Готовый порт STM32F1
|
||||||
|
|
||||||
|
Вывод настроен как выход с открытым стоком; чтение идёт через `IDR`,
|
||||||
|
поэтому режим не переключается. Прерывания запрещаются только на
|
||||||
|
длительность одного тайм-слота, чтобы не терять байты USART. Источник
|
||||||
|
микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`.
|
||||||
|
|
||||||
|
Привязку вывода порт берёт из `onewire_config.h`, который предоставляет
|
||||||
|
приложение — шаблон лежит рядом:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "stm32f10x.h"
|
||||||
|
|
||||||
|
#define ONEWIRE_GPIO GPIOA
|
||||||
|
#define ONEWIRE_PIN 1U
|
||||||
|
#define ONEWIRE_GPIO_CLOCK RCC_APB2ENR_IOPAEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Требуется внешняя подтяжка 4.7 кОм к питанию.
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
```c
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
|
||||||
|
OneWire_Init();
|
||||||
|
DS18B20_Scan(&bus); /* найти датчики на шине */
|
||||||
|
|
||||||
|
DS18B20_StartConversion(); /* SKIP ROM + CONVERT T */
|
||||||
|
/* выдержать DS18B20_ConversionTimeMs(config) */
|
||||||
|
DS18B20_ReadAll(&bus); /* обновить измерения */
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < bus.count; i++) {
|
||||||
|
if (bus.sensors[i].status & DS18B20_STATUS_VALID) {
|
||||||
|
float celsius = bus.sensors[i].raw / 16.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ошибка одного датчика не прерывает опрос остальных — она видна в поле
|
||||||
|
`status` соответствующей записи.
|
||||||
|
|
||||||
|
## Тесты
|
||||||
|
|
||||||
|
Симулятор шины реализует те же пять функций, что и настоящий порт, но
|
||||||
|
работает не с выводом, а с моделью: виртуальные датчики отвечают побитно,
|
||||||
|
включая полный алгоритм `SEARCH ROM`. Так на хосте проверяется именно то,
|
||||||
|
что иначе проверялось бы только осциллографом.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build && cmake --build build && ctest --test-dir build
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clang -std=c99 -Wall -Wextra -Iinclude -Itests \
|
||||||
|
tests/test_ds18b20.c tests/fake_onewire.c src/onewire.c src/ds18b20.c -o test && ./test
|
||||||
|
```
|
||||||
|
|
||||||
|
Покрыто: CRC8, обход шины с несколькими датчиками и пустой шины, отсев
|
||||||
|
чужих семейств по коду ROM, паразитное питание, битый scratchpad,
|
||||||
|
пропажа датчика, подтверждение и отказ записи, сохранение в EEPROM
|
||||||
|
датчика, пределы разрешения 9..12 бит.
|
||||||
|
|
||||||
|
## Особенности, которые стоит знать
|
||||||
|
|
||||||
|
* **Порядок датчиков задаёт шина, а не код.** `SEARCH ROM` обходит их в
|
||||||
|
порядке битов идентификатора, поэтому индекс в `bus.sensors[]` не
|
||||||
|
совпадает с порядком подключения. Искать датчик нужно по ROM через
|
||||||
|
`DS18B20_Find()`.
|
||||||
|
* **Пропажу датчика на живой шине не видно по presence.** Импульс
|
||||||
|
подтверждают соседи, и `MATCH ROM` к снятому датчику проходит без
|
||||||
|
признаков ошибки — она проявится как `DS18B20_STATUS_CRC_ERROR`.
|
||||||
|
`DS18B20_STATUS_NO_PRESENCE` означает, что молчит вся шина.
|
||||||
|
* **`DS18B20_MAX_SENSORS` равен 8** — столько датчиков помещается в один
|
||||||
|
кадр `SENSOR_DATA` протокола SETGUI. Для другого применения значение
|
||||||
|
можно поднять.
|
||||||
|
* **Обратное чтение после записи обязательно.** Датчик может ответить и
|
||||||
|
оставить регистры прежними; такой случай отличается от ошибки шины
|
||||||
|
кодом `DS18B20_WRITE_REJECTED`.
|
||||||
|
|
||||||
|
## Лицензия и происхождение
|
||||||
|
|
||||||
|
Код вынесен из `KONOR_ds18b20` (модуль термометрии на STM32F103C8T6)
|
||||||
|
без изменения поведения: разделение на ядро и порт сделано переносом
|
||||||
|
функций, тела не правились.
|
||||||
125
c/ds18b20/include/ds18b20.h
Normal file
125
c/ds18b20/include/ds18b20.h
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* @file ds18b20.h
|
||||||
|
* @brief Драйвер термометров DS18B20 поверх шины 1-Wire.
|
||||||
|
*
|
||||||
|
* Модуль хранит перечень найденных датчиков и последние измерения, поэтому
|
||||||
|
* ответ на запрос GUI формируется без обращения к шине.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DS18B20_H
|
||||||
|
#define DS18B20_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "onewire.h"
|
||||||
|
|
||||||
|
/** Максимум датчиков, помещающихся в один кадр SENSOR_DATA. */
|
||||||
|
#define DS18B20_MAX_SENSORS 8U
|
||||||
|
|
||||||
|
/** Код семейства DS18B20 в первом байте ROM. */
|
||||||
|
#define DS18B20_FAMILY_CODE 0x28U
|
||||||
|
|
||||||
|
/** Размер scratchpad вместе с байтом CRC8. */
|
||||||
|
#define DS18B20_SCRATCHPAD_SIZE 9U
|
||||||
|
|
||||||
|
#define DS18B20_STATUS_VALID 0x01U /**< Измерение достоверно. */
|
||||||
|
#define DS18B20_STATUS_CRC_ERROR 0x02U /**< Не сошёлся CRC8 scratchpad. */
|
||||||
|
#define DS18B20_STATUS_NO_PRESENCE 0x04U /**< Датчик не ответил на сброс. */
|
||||||
|
#define DS18B20_STATUS_PARASITE 0x08U /**< Датчик питается паразитно. */
|
||||||
|
|
||||||
|
#define DS18B20_WRITE_OK 0U /**< Запись подтверждена обратным чтением. */
|
||||||
|
#define DS18B20_WRITE_BUS_ERROR 1U /**< Нет presence или не сошёлся CRC8. */
|
||||||
|
#define DS18B20_WRITE_REJECTED 2U /**< Датчик ответил, но значение не изменил. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние одного датчика на шине.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t rom[ONEWIRE_ROM_SIZE]; /**< Уникальный 64-битный идентификатор. */
|
||||||
|
int16_t raw; /**< Код температуры из scratchpad. */
|
||||||
|
uint8_t user_byte1; /**< Пользовательский байт 1 (регистр TH). */
|
||||||
|
uint8_t user_byte2; /**< Пользовательский байт 2 (регистр TL). */
|
||||||
|
uint8_t config; /**< Байт конфигурации с разрешением. */
|
||||||
|
uint8_t status; /**< Битовая маска DS18B20_STATUS_*. */
|
||||||
|
} DS18B20_Sensor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Перечень датчиков шины и общее состояние питания.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
DS18B20_Sensor sensors[DS18B20_MAX_SENSORS]; /**< Найденные датчики. */
|
||||||
|
uint8_t count; /**< Число заполненных элементов массива. */
|
||||||
|
uint8_t parasite; /**< 1, если хотя бы один датчик питается паразитно. */
|
||||||
|
} DS18B20_Bus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ищет датчики DS18B20 и запоминает их идентификаторы.
|
||||||
|
*
|
||||||
|
* Устройства других семейств пропускаются; ранее накопленные измерения
|
||||||
|
* сбрасываются.
|
||||||
|
*
|
||||||
|
* @param bus Перечень датчиков, принадлежащий приложению.
|
||||||
|
* @return Количество найденных датчиков.
|
||||||
|
*/
|
||||||
|
uint8_t DS18B20_Scan(DS18B20_Bus *bus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Запускает преобразование во всех датчиках командой SKIP ROM.
|
||||||
|
*
|
||||||
|
* @return 1 при наличии presence-импульса, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t DS18B20_StartConversion(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Читает scratchpad всех известных датчиков и обновляет измерения.
|
||||||
|
*
|
||||||
|
* Ошибка отдельного датчика не прерывает опрос остальных: она отражается в
|
||||||
|
* поле status соответствующего элемента.
|
||||||
|
*
|
||||||
|
* @param bus Перечень датчиков, заполненный DS18B20_Scan.
|
||||||
|
* @return Количество датчиков с достоверным измерением.
|
||||||
|
*/
|
||||||
|
uint8_t DS18B20_ReadAll(DS18B20_Bus *bus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает время преобразования для заданного разрешения.
|
||||||
|
*
|
||||||
|
* @param config Байт конфигурации scratchpad.
|
||||||
|
* @return Время преобразования в миллисекундах.
|
||||||
|
*/
|
||||||
|
uint16_t DS18B20_ConversionTimeMs(uint8_t config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Собирает байт конфигурации из разрешения в битах.
|
||||||
|
*
|
||||||
|
* @param bits Разрешение 9..12 бит.
|
||||||
|
* @return Байт конфигурации scratchpad.
|
||||||
|
*/
|
||||||
|
uint8_t DS18B20_ConfigFromResolution(uint8_t bits);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ищет датчик по идентификатору в накопленном перечне.
|
||||||
|
*
|
||||||
|
* @param bus Перечень датчиков.
|
||||||
|
* @param rom Искомый идентификатор.
|
||||||
|
* @return Указатель на элемент перечня либо 0, если ROM не найден.
|
||||||
|
*/
|
||||||
|
DS18B20_Sensor *DS18B20_Find(DS18B20_Bus *bus, const uint8_t *rom);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Записывает три байта scratchpad выбранного датчика.
|
||||||
|
*
|
||||||
|
* @param sensor Датчик из перечня; кэш обновляется при успешной записи.
|
||||||
|
* @param user_byte1 Новое значение пользовательского байта 1 (TH).
|
||||||
|
* @param user_byte2 Новое значение пользовательского байта 2 (TL).
|
||||||
|
* @param config Байт конфигурации с требуемым разрешением.
|
||||||
|
* @param save_to_eeprom Признак копирования scratchpad в EEPROM датчика.
|
||||||
|
* @return DS18B20_WRITE_OK при подтверждённой записи, DS18B20_WRITE_BUS_ERROR
|
||||||
|
* при молчании датчика или сбое CRC8, DS18B20_WRITE_REJECTED если
|
||||||
|
* датчик ответил, но оставил прежние значения.
|
||||||
|
*/
|
||||||
|
uint8_t DS18B20_WriteScratchpad(DS18B20_Sensor *sensor, uint8_t user_byte1,
|
||||||
|
uint8_t user_byte2, uint8_t config,
|
||||||
|
uint8_t save_to_eeprom);
|
||||||
|
|
||||||
|
#endif /* DS18B20_H */
|
||||||
149
c/ds18b20/include/onewire.h
Normal file
149
c/ds18b20/include/onewire.h
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* @file onewire.h
|
||||||
|
* @brief Программная шина 1-Wire на выводе с открытым стоком.
|
||||||
|
*
|
||||||
|
* Драйвер не зависит от типа датчика: он выдаёт тайминги, ищет ROM и считает
|
||||||
|
* CRC8. Работа с DS18B20 вынесена в ds18b20.c.
|
||||||
|
*
|
||||||
|
* Реализация разделена надвое:
|
||||||
|
* src/onewire.c переносимая часть - побайтовый обмен, CRC8, поиск ROM,
|
||||||
|
* MATCH ROM и SKIP ROM; ни регистров, ни задержек;
|
||||||
|
* ports/<платформа> пять функций, зависящих от железа: OneWire_Init,
|
||||||
|
* OneWire_DelayUs, OneWire_Reset, OneWire_WriteBit
|
||||||
|
* и OneWire_ReadBit.
|
||||||
|
*
|
||||||
|
* Чтобы перенести библиотеку на другой микроконтроллер, достаточно написать
|
||||||
|
* эти пять функций - остальное собирается как есть.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ONEWIRE_H
|
||||||
|
#define ONEWIRE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/** Размер идентификатора устройства 1-Wire в байтах. */
|
||||||
|
#define ONEWIRE_ROM_SIZE 8U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние обхода шины алгоритмом SEARCH ROM.
|
||||||
|
*
|
||||||
|
* Структура принадлежит вызывающему коду; параллельные обходы должны
|
||||||
|
* использовать разные экземпляры.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t rom[ONEWIRE_ROM_SIZE]; /**< Текущий найденный идентификатор. */
|
||||||
|
uint8_t last_discrepancy; /**< Позиция последней развилки, 1..64. */
|
||||||
|
uint8_t last_family_discrepancy; /**< Развилка внутри кода семейства. */
|
||||||
|
uint8_t last_device; /**< Признак завершённого обхода. */
|
||||||
|
} OneWire_Search;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Настраивает вывод шины и счётчик циклов для микросекундных задержек.
|
||||||
|
*
|
||||||
|
* Вызывается один раз после инициализации тактирования системы.
|
||||||
|
*/
|
||||||
|
void OneWire_Init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Формирует импульс сброса и проверяет ответ устройств.
|
||||||
|
*
|
||||||
|
* @note На время выборки presence прерывания запрещаются примерно на 75 мкс;
|
||||||
|
* длительность самого импульса сброса на точность не влияет.
|
||||||
|
*
|
||||||
|
* @return 1, если хотя бы одно устройство выдало presence-импульс, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t OneWire_Reset(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Передаёт один бит в тайм-слоте записи.
|
||||||
|
*
|
||||||
|
* @param bit Передаваемое значение, отличное от нуля означает единицу.
|
||||||
|
*/
|
||||||
|
void OneWire_WriteBit(uint8_t bit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Считывает один бит в тайм-слоте чтения.
|
||||||
|
*
|
||||||
|
* @return Принятое значение бита: 0 или 1.
|
||||||
|
*/
|
||||||
|
uint8_t OneWire_ReadBit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Передаёт байт младшим битом вперёд.
|
||||||
|
*
|
||||||
|
* @param value Передаваемый байт.
|
||||||
|
*/
|
||||||
|
void OneWire_WriteByte(uint8_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Принимает байт младшим битом вперёд.
|
||||||
|
*
|
||||||
|
* @return Принятый байт.
|
||||||
|
*/
|
||||||
|
uint8_t OneWire_ReadByte(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Передаёт блок байтов подряд без промежуточных сбросов.
|
||||||
|
*
|
||||||
|
* @param data Указатель на передаваемые байты.
|
||||||
|
* @param size Количество байтов.
|
||||||
|
*/
|
||||||
|
void OneWire_WriteBytes(const uint8_t *data, uint32_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Принимает блок байтов подряд.
|
||||||
|
*
|
||||||
|
* @param data Буфер приёмника.
|
||||||
|
* @param size Количество байтов.
|
||||||
|
*/
|
||||||
|
void OneWire_ReadBytes(uint8_t *data, uint32_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Считает CRC8 Dallas/Maxim с полиномом 0x8C.
|
||||||
|
*
|
||||||
|
* @param data Проверяемые байты.
|
||||||
|
* @param size Количество байтов.
|
||||||
|
* @return Значение CRC8; для блока вместе с контрольным байтом равно нулю.
|
||||||
|
*/
|
||||||
|
uint8_t OneWire_Crc8(const uint8_t *data, uint32_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Подготавливает состояние к новому полному обходу шины.
|
||||||
|
*
|
||||||
|
* @param search Состояние обхода, принадлежащее вызывающему коду.
|
||||||
|
*/
|
||||||
|
void OneWire_SearchInit(OneWire_Search *search);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Находит следующий идентификатор на шине.
|
||||||
|
*
|
||||||
|
* @param search Состояние обхода, ранее подготовленное OneWire_SearchInit.
|
||||||
|
* @return 1, если поле rom содержит новый проверенный ROM, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t OneWire_SearchNext(OneWire_Search *search);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Адресует конкретное устройство командой MATCH ROM.
|
||||||
|
*
|
||||||
|
* @param rom Восьмибайтовый идентификатор устройства.
|
||||||
|
* @return 1 при наличии presence-импульса, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t OneWire_SelectRom(const uint8_t *rom);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Адресует все устройства шины командой SKIP ROM.
|
||||||
|
*
|
||||||
|
* @return 1 при наличии presence-импульса, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t OneWire_SkipRom(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Задерживает выполнение на заданное число микросекунд.
|
||||||
|
*
|
||||||
|
* Используется драйверами шины и модулем DS18B20 при ожидании EEPROM.
|
||||||
|
*
|
||||||
|
* @param microseconds Длительность задержки.
|
||||||
|
*/
|
||||||
|
void OneWire_DelayUs(uint32_t microseconds);
|
||||||
|
|
||||||
|
#endif /* ONEWIRE_H */
|
||||||
26
c/ds18b20/ports/stm32f1/onewire_config.template.h
Normal file
26
c/ds18b20/ports/stm32f1/onewire_config.template.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* @file onewire_config.template.h
|
||||||
|
* @brief Шаблон привязки шины 1-Wire для порта STM32F1.
|
||||||
|
*
|
||||||
|
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
||||||
|
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
||||||
|
* привязка выводов принадлежит плате, а не драйверу.
|
||||||
|
*
|
||||||
|
* Требуется внешняя подтяжка 4.7 кОм к питанию.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ONEWIRE_CONFIG_H
|
||||||
|
#define ONEWIRE_CONFIG_H
|
||||||
|
|
||||||
|
#include "stm32f10x.h"
|
||||||
|
|
||||||
|
/** Порт вывода шины. */
|
||||||
|
#define ONEWIRE_GPIO GPIOA
|
||||||
|
|
||||||
|
/** Номер вывода в порту, 0..15. */
|
||||||
|
#define ONEWIRE_PIN 1U
|
||||||
|
|
||||||
|
/** Бит включения тактирования порта в RCC->APB2ENR. */
|
||||||
|
#define ONEWIRE_GPIO_CLOCK RCC_APB2ENR_IOPAEN
|
||||||
|
|
||||||
|
#endif /* ONEWIRE_CONFIG_H */
|
||||||
158
c/ds18b20/ports/stm32f1/onewire_stm32f1.c
Normal file
158
c/ds18b20/ports/stm32f1/onewire_stm32f1.c
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* @file onewire_stm32f1.c
|
||||||
|
* @brief Порт шины 1-Wire на STM32F1: тайминги, вывод и задержки.
|
||||||
|
*
|
||||||
|
* Вывод постоянно настроен как выход с открытым стоком: чтение уровня
|
||||||
|
* выполняется через регистр IDR, поэтому переключать режим не требуется.
|
||||||
|
* Прерывания запрещаются только на длительность одного тайм-слота, чтобы не
|
||||||
|
* терять байты USART во время обмена.
|
||||||
|
*
|
||||||
|
* Привязка вывода приходит из onewire_config.h, который предоставляет
|
||||||
|
* приложение. Шаблон - onewire_config.template.h рядом с этим файлом.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "onewire.h"
|
||||||
|
|
||||||
|
#include "onewire_config.h"
|
||||||
|
|
||||||
|
#if !defined(ONEWIRE_GPIO) || !defined(ONEWIRE_PIN) || !defined(ONEWIRE_GPIO_CLOCK)
|
||||||
|
#error "onewire_config.h должен задать ONEWIRE_GPIO, ONEWIRE_PIN и ONEWIRE_GPIO_CLOCK"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Длительности тайм-слотов в микросекундах согласно рекомендациям Maxim. */
|
||||||
|
#define OW_RESET_LOW_US 480U
|
||||||
|
#define OW_PRESENCE_WAIT_US 70U
|
||||||
|
#define OW_RESET_TAIL_US 410U
|
||||||
|
#define OW_WRITE1_LOW_US 6U
|
||||||
|
#define OW_WRITE1_TAIL_US 64U
|
||||||
|
#define OW_WRITE0_LOW_US 60U
|
||||||
|
#define OW_WRITE0_TAIL_US 10U
|
||||||
|
#define OW_READ_LOW_US 6U
|
||||||
|
#define OW_READ_SAMPLE_US 9U
|
||||||
|
#define OW_READ_TAIL_US 55U
|
||||||
|
|
||||||
|
/** Число тактов ядра на микросекунду; заполняется в OneWire_Init. */
|
||||||
|
static uint32_t g_cycles_per_us = 72U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Переводит вывод шины в активный низкий уровень.
|
||||||
|
*/
|
||||||
|
static void ow_drive_low(void)
|
||||||
|
{
|
||||||
|
ONEWIRE_GPIO->BRR = (uint32_t)(1UL << ONEWIRE_PIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Отпускает шину; уровень поднимает внешний резистор подтяжки.
|
||||||
|
*/
|
||||||
|
static void ow_release(void)
|
||||||
|
{
|
||||||
|
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << ONEWIRE_PIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Читает текущий уровень на выводе шины.
|
||||||
|
*
|
||||||
|
* @return 1 для высокого уровня, 0 для низкого.
|
||||||
|
*/
|
||||||
|
static uint8_t ow_read_pin(void)
|
||||||
|
{
|
||||||
|
return (uint8_t)((ONEWIRE_GPIO->IDR >> ONEWIRE_PIN) & 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneWire_DelayUs(uint32_t microseconds)
|
||||||
|
{
|
||||||
|
const uint32_t start = DWT->CYCCNT;
|
||||||
|
const uint32_t ticks = microseconds * g_cycles_per_us;
|
||||||
|
|
||||||
|
while ((DWT->CYCCNT - start) < ticks) {
|
||||||
|
__NOP();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneWire_Init(void)
|
||||||
|
{
|
||||||
|
uint32_t config;
|
||||||
|
|
||||||
|
g_cycles_per_us = SystemCoreClock / 1000000U;
|
||||||
|
if (g_cycles_per_us == 0U) {
|
||||||
|
g_cycles_per_us = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Счётчик тактов ядра — единственный источник микросекундных задержек. */
|
||||||
|
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||||
|
DWT->CYCCNT = 0U;
|
||||||
|
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||||
|
|
||||||
|
RCC->APB2ENR |= ONEWIRE_GPIO_CLOCK;
|
||||||
|
|
||||||
|
/* Выход с открытым стоком, 50 МГц: CNF = 01, MODE = 11. */
|
||||||
|
if (ONEWIRE_PIN < 8U) {
|
||||||
|
config = ONEWIRE_GPIO->CRL;
|
||||||
|
config &= ~(0xFUL << (ONEWIRE_PIN * 4U));
|
||||||
|
config |= (0x7UL << (ONEWIRE_PIN * 4U));
|
||||||
|
ONEWIRE_GPIO->CRL = config;
|
||||||
|
} else {
|
||||||
|
config = ONEWIRE_GPIO->CRH;
|
||||||
|
config &= ~(0xFUL << ((ONEWIRE_PIN - 8U) * 4U));
|
||||||
|
config |= (0x7UL << ((ONEWIRE_PIN - 8U) * 4U));
|
||||||
|
ONEWIRE_GPIO->CRH = config;
|
||||||
|
}
|
||||||
|
ow_release();
|
||||||
|
OneWire_DelayUs(OW_RESET_LOW_US);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_Reset(void)
|
||||||
|
{
|
||||||
|
uint32_t primask;
|
||||||
|
uint8_t presence;
|
||||||
|
|
||||||
|
/* Удлинение импульса сброса безопасно, поэтому прерывания не запрещаем. */
|
||||||
|
ow_drive_low();
|
||||||
|
OneWire_DelayUs(OW_RESET_LOW_US);
|
||||||
|
|
||||||
|
primask = __get_PRIMASK();
|
||||||
|
__disable_irq();
|
||||||
|
ow_release();
|
||||||
|
OneWire_DelayUs(OW_PRESENCE_WAIT_US);
|
||||||
|
presence = (uint8_t)(ow_read_pin() == 0U);
|
||||||
|
__set_PRIMASK(primask);
|
||||||
|
|
||||||
|
OneWire_DelayUs(OW_RESET_TAIL_US);
|
||||||
|
return (uint8_t)(presence && (ow_read_pin() != 0U));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneWire_WriteBit(uint8_t bit)
|
||||||
|
{
|
||||||
|
const uint32_t primask = __get_PRIMASK();
|
||||||
|
|
||||||
|
__disable_irq();
|
||||||
|
ow_drive_low();
|
||||||
|
if (bit != 0U) {
|
||||||
|
OneWire_DelayUs(OW_WRITE1_LOW_US);
|
||||||
|
ow_release();
|
||||||
|
OneWire_DelayUs(OW_WRITE1_TAIL_US);
|
||||||
|
} else {
|
||||||
|
OneWire_DelayUs(OW_WRITE0_LOW_US);
|
||||||
|
ow_release();
|
||||||
|
OneWire_DelayUs(OW_WRITE0_TAIL_US);
|
||||||
|
}
|
||||||
|
__set_PRIMASK(primask);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_ReadBit(void)
|
||||||
|
{
|
||||||
|
const uint32_t primask = __get_PRIMASK();
|
||||||
|
uint8_t bit;
|
||||||
|
|
||||||
|
__disable_irq();
|
||||||
|
ow_drive_low();
|
||||||
|
OneWire_DelayUs(OW_READ_LOW_US);
|
||||||
|
ow_release();
|
||||||
|
OneWire_DelayUs(OW_READ_SAMPLE_US);
|
||||||
|
bit = ow_read_pin();
|
||||||
|
__set_PRIMASK(primask);
|
||||||
|
|
||||||
|
OneWire_DelayUs(OW_READ_TAIL_US);
|
||||||
|
return bit;
|
||||||
|
}
|
||||||
202
c/ds18b20/src/ds18b20.c
Normal file
202
c/ds18b20/src/ds18b20.c
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/** test
|
||||||
|
* @file ds18b20.c
|
||||||
|
* @brief Реализация драйвера DS18B20: поиск, чтение и запись scratchpad.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ds18b20.h"
|
||||||
|
|
||||||
|
#define DS_CMD_CONVERT_T 0x44U
|
||||||
|
#define DS_CMD_WRITE_SCRATCHPAD 0x4EU
|
||||||
|
#define DS_CMD_READ_SCRATCHPAD 0xBEU
|
||||||
|
#define DS_CMD_COPY_SCRATCHPAD 0x48U
|
||||||
|
#define DS_CMD_READ_POWER 0xB4U
|
||||||
|
|
||||||
|
/** Задержка записи EEPROM датчика по datasheet, мкс. */
|
||||||
|
#define DS_EEPROM_WRITE_US 12000U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Считывает scratchpad адресованного датчика с проверкой CRC8.
|
||||||
|
*
|
||||||
|
* @param rom Идентификатор датчика.
|
||||||
|
* @param scratchpad Буфер на DS18B20_SCRATCHPAD_SIZE байт.
|
||||||
|
* @return Битовая маска ошибок DS18B20_STATUS_* без бита VALID.
|
||||||
|
*/
|
||||||
|
static uint8_t ds_read_scratchpad(const uint8_t *rom, uint8_t *scratchpad)
|
||||||
|
{
|
||||||
|
if (OneWire_SelectRom(rom) == 0U) {
|
||||||
|
return DS18B20_STATUS_NO_PRESENCE;
|
||||||
|
}
|
||||||
|
OneWire_WriteByte(DS_CMD_READ_SCRATCHPAD);
|
||||||
|
OneWire_ReadBytes(scratchpad, DS18B20_SCRATCHPAD_SIZE);
|
||||||
|
|
||||||
|
if (OneWire_Crc8(scratchpad, DS18B20_SCRATCHPAD_SIZE) != 0U) {
|
||||||
|
return DS18B20_STATUS_CRC_ERROR;
|
||||||
|
}
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Опрашивает тип питания датчика командой READ POWER SUPPLY.
|
||||||
|
*
|
||||||
|
* @param rom Идентификатор датчика.
|
||||||
|
* @return 1 для паразитного питания, иначе 0.
|
||||||
|
*/
|
||||||
|
static uint8_t ds_read_parasite(const uint8_t *rom)
|
||||||
|
{
|
||||||
|
if (OneWire_SelectRom(rom) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
OneWire_WriteByte(DS_CMD_READ_POWER);
|
||||||
|
/* Датчик с паразитным питанием прижимает шину к нулю. */
|
||||||
|
return (uint8_t)(OneWire_ReadBit() == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t DS18B20_ConfigFromResolution(uint8_t bits)
|
||||||
|
{
|
||||||
|
if (bits < 9U) {
|
||||||
|
bits = 9U;
|
||||||
|
} else if (bits > 12U) {
|
||||||
|
bits = 12U;
|
||||||
|
}
|
||||||
|
return (uint8_t)(0x1FU | ((bits - 9U) << 5U));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t DS18B20_ConversionTimeMs(uint8_t config)
|
||||||
|
{
|
||||||
|
switch ((config >> 5U) & 0x03U) {
|
||||||
|
case 0U:
|
||||||
|
return 94U;
|
||||||
|
case 1U:
|
||||||
|
return 188U;
|
||||||
|
case 2U:
|
||||||
|
return 375U;
|
||||||
|
default:
|
||||||
|
return 750U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t DS18B20_Scan(DS18B20_Bus *bus)
|
||||||
|
{
|
||||||
|
OneWire_Search search;
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
bus->count = 0U;
|
||||||
|
bus->parasite = 0U;
|
||||||
|
OneWire_SearchInit(&search);
|
||||||
|
|
||||||
|
while ((bus->count < DS18B20_MAX_SENSORS) && (OneWire_SearchNext(&search) != 0U)) {
|
||||||
|
if (search.rom[0] != DS18B20_FAMILY_CODE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DS18B20_Sensor *sensor = &bus->sensors[bus->count];
|
||||||
|
for (index = 0U; index < ONEWIRE_ROM_SIZE; index++) {
|
||||||
|
sensor->rom[index] = search.rom[index];
|
||||||
|
}
|
||||||
|
sensor->raw = 0;
|
||||||
|
sensor->user_byte1 = 0U;
|
||||||
|
sensor->user_byte2 = 0U;
|
||||||
|
sensor->config = DS18B20_ConfigFromResolution(12U);
|
||||||
|
sensor->status = 0U;
|
||||||
|
if (ds_read_parasite(sensor->rom) != 0U) {
|
||||||
|
sensor->status |= DS18B20_STATUS_PARASITE;
|
||||||
|
bus->parasite = 1U;
|
||||||
|
}
|
||||||
|
bus->count++;
|
||||||
|
}
|
||||||
|
return bus->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t DS18B20_StartConversion(void)
|
||||||
|
{
|
||||||
|
if (OneWire_SkipRom() == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
OneWire_WriteByte(DS_CMD_CONVERT_T);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t DS18B20_ReadAll(DS18B20_Bus *bus)
|
||||||
|
{
|
||||||
|
uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE];
|
||||||
|
uint8_t valid = 0U;
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < bus->count; index++) {
|
||||||
|
DS18B20_Sensor *sensor = &bus->sensors[index];
|
||||||
|
const uint8_t parasite = (uint8_t)(sensor->status & DS18B20_STATUS_PARASITE);
|
||||||
|
const uint8_t error = ds_read_scratchpad(sensor->rom, scratchpad);
|
||||||
|
|
||||||
|
if (error != 0U) {
|
||||||
|
sensor->status = (uint8_t)(parasite | error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sensor->raw = (int16_t)((uint16_t)scratchpad[0] | ((uint16_t)scratchpad[1] << 8U));
|
||||||
|
sensor->user_byte1 = scratchpad[2];
|
||||||
|
sensor->user_byte2 = scratchpad[3];
|
||||||
|
sensor->config = scratchpad[4];
|
||||||
|
sensor->status = (uint8_t)(parasite | DS18B20_STATUS_VALID);
|
||||||
|
valid++;
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
DS18B20_Sensor *DS18B20_Find(DS18B20_Bus *bus, const uint8_t *rom)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
uint8_t byte;
|
||||||
|
|
||||||
|
for (index = 0U; index < bus->count; index++) {
|
||||||
|
for (byte = 0U; byte < ONEWIRE_ROM_SIZE; byte++) {
|
||||||
|
if (bus->sensors[index].rom[byte] != rom[byte]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (byte == ONEWIRE_ROM_SIZE) {
|
||||||
|
return &bus->sensors[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t DS18B20_WriteScratchpad(DS18B20_Sensor *sensor, uint8_t user_byte1,
|
||||||
|
uint8_t user_byte2, uint8_t config,
|
||||||
|
uint8_t save_to_eeprom)
|
||||||
|
{
|
||||||
|
uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE];
|
||||||
|
|
||||||
|
if (OneWire_SelectRom(sensor->rom) == 0U) {
|
||||||
|
sensor->status = (uint8_t)((sensor->status & DS18B20_STATUS_PARASITE)
|
||||||
|
| DS18B20_STATUS_NO_PRESENCE);
|
||||||
|
return DS18B20_WRITE_BUS_ERROR;
|
||||||
|
}
|
||||||
|
OneWire_WriteByte(DS_CMD_WRITE_SCRATCHPAD);
|
||||||
|
OneWire_WriteByte(user_byte1);
|
||||||
|
OneWire_WriteByte(user_byte2);
|
||||||
|
OneWire_WriteByte(config);
|
||||||
|
|
||||||
|
/* Обратное чтение подтверждает, что датчик принял все три байта. */
|
||||||
|
if (ds_read_scratchpad(sensor->rom, scratchpad) != 0U) {
|
||||||
|
return DS18B20_WRITE_BUS_ERROR;
|
||||||
|
}
|
||||||
|
if ((scratchpad[2] != user_byte1) || (scratchpad[3] != user_byte2)
|
||||||
|
|| (scratchpad[4] != config)) {
|
||||||
|
/* Обмен состоялся, но регистры остались прежними: значение отвергнуто. */
|
||||||
|
sensor->user_byte1 = scratchpad[2];
|
||||||
|
sensor->user_byte2 = scratchpad[3];
|
||||||
|
sensor->config = scratchpad[4];
|
||||||
|
return DS18B20_WRITE_REJECTED;
|
||||||
|
}
|
||||||
|
sensor->user_byte1 = scratchpad[2];
|
||||||
|
sensor->user_byte2 = scratchpad[3];
|
||||||
|
sensor->config = scratchpad[4];
|
||||||
|
|
||||||
|
if (save_to_eeprom != 0U) {
|
||||||
|
if (OneWire_SelectRom(sensor->rom) == 0U) {
|
||||||
|
return DS18B20_WRITE_BUS_ERROR;
|
||||||
|
}
|
||||||
|
OneWire_WriteByte(DS_CMD_COPY_SCRATCHPAD);
|
||||||
|
/* Датчик с внешним питанием тоже допускает выдержку по времени. */
|
||||||
|
OneWire_DelayUs(DS_EEPROM_WRITE_US);
|
||||||
|
}
|
||||||
|
return DS18B20_WRITE_OK;
|
||||||
|
}
|
||||||
177
c/ds18b20/src/onewire.c
Normal file
177
c/ds18b20/src/onewire.c
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* @file onewire.c
|
||||||
|
* @brief Переносимая часть шины 1-Wire: побайтовый обмен, CRC8 и поиск ROM.
|
||||||
|
*
|
||||||
|
* Здесь нет ни регистров, ни задержек, ни запрета прерываний: всё это живёт
|
||||||
|
* в порте (ports/), а этот файл собирается любым C99-компилятором и
|
||||||
|
* проверяется host-тестами на симуляторе шины.
|
||||||
|
*
|
||||||
|
* От порта требуются пять функций: OneWire_Init, OneWire_DelayUs,
|
||||||
|
* OneWire_Reset, OneWire_WriteBit и OneWire_ReadBit.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "onewire.h"
|
||||||
|
|
||||||
|
#define OW_CMD_SEARCH_ROM 0xF0U
|
||||||
|
#define OW_CMD_MATCH_ROM 0x55U
|
||||||
|
#define OW_CMD_SKIP_ROM 0xCCU
|
||||||
|
|
||||||
|
void OneWire_WriteByte(uint8_t value)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < 8U; index++) {
|
||||||
|
OneWire_WriteBit((uint8_t)(value & 1U));
|
||||||
|
value = (uint8_t)(value >> 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_ReadByte(void)
|
||||||
|
{
|
||||||
|
uint8_t value = 0U;
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < 8U; index++) {
|
||||||
|
value = (uint8_t)(value >> 1U);
|
||||||
|
if (OneWire_ReadBit() != 0U) {
|
||||||
|
value |= 0x80U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneWire_WriteBytes(const uint8_t *data, uint32_t size)
|
||||||
|
{
|
||||||
|
uint32_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < size; index++) {
|
||||||
|
OneWire_WriteByte(data[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneWire_ReadBytes(uint8_t *data, uint32_t size)
|
||||||
|
{
|
||||||
|
uint32_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < size; index++) {
|
||||||
|
data[index] = OneWire_ReadByte();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_Crc8(const uint8_t *data, uint32_t size)
|
||||||
|
{
|
||||||
|
uint8_t crc = 0U;
|
||||||
|
uint32_t index;
|
||||||
|
uint8_t bit;
|
||||||
|
|
||||||
|
for (index = 0U; index < size; index++) {
|
||||||
|
crc ^= data[index];
|
||||||
|
for (bit = 0U; bit < 8U; bit++) {
|
||||||
|
crc = (uint8_t)((crc & 1U) != 0U ? ((crc >> 1U) ^ 0x8CU) : (crc >> 1U));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneWire_SearchInit(OneWire_Search *search)
|
||||||
|
{
|
||||||
|
uint32_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < ONEWIRE_ROM_SIZE; index++) {
|
||||||
|
search->rom[index] = 0U;
|
||||||
|
}
|
||||||
|
search->last_discrepancy = 0U;
|
||||||
|
search->last_family_discrepancy = 0U;
|
||||||
|
search->last_device = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_SearchNext(OneWire_Search *search)
|
||||||
|
{
|
||||||
|
uint8_t id_bit_number = 1U;
|
||||||
|
uint8_t last_zero = 0U;
|
||||||
|
uint8_t rom_byte_number = 0U;
|
||||||
|
uint8_t rom_byte_mask = 1U;
|
||||||
|
uint8_t search_direction;
|
||||||
|
uint8_t id_bit;
|
||||||
|
uint8_t cmp_id_bit;
|
||||||
|
|
||||||
|
if (search->last_device != 0U) {
|
||||||
|
OneWire_SearchInit(search);
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (OneWire_Reset() == 0U) {
|
||||||
|
OneWire_SearchInit(search);
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
OneWire_WriteByte(OW_CMD_SEARCH_ROM);
|
||||||
|
|
||||||
|
do {
|
||||||
|
id_bit = OneWire_ReadBit();
|
||||||
|
cmp_id_bit = OneWire_ReadBit();
|
||||||
|
if ((id_bit != 0U) && (cmp_id_bit != 0U)) {
|
||||||
|
/* Ни одно устройство не ответило: обход прерван. */
|
||||||
|
OneWire_SearchInit(search);
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id_bit != cmp_id_bit) {
|
||||||
|
search_direction = id_bit;
|
||||||
|
} else if (id_bit_number < search->last_discrepancy) {
|
||||||
|
search_direction =
|
||||||
|
(uint8_t)((search->rom[rom_byte_number] & rom_byte_mask) != 0U);
|
||||||
|
} else {
|
||||||
|
search_direction = (uint8_t)(id_bit_number == search->last_discrepancy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((id_bit == cmp_id_bit) && (search_direction == 0U)) {
|
||||||
|
last_zero = id_bit_number;
|
||||||
|
if (last_zero < 9U) {
|
||||||
|
search->last_family_discrepancy = last_zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search_direction != 0U) {
|
||||||
|
search->rom[rom_byte_number] |= rom_byte_mask;
|
||||||
|
} else {
|
||||||
|
search->rom[rom_byte_number] &= (uint8_t)~rom_byte_mask;
|
||||||
|
}
|
||||||
|
OneWire_WriteBit(search_direction);
|
||||||
|
|
||||||
|
id_bit_number++;
|
||||||
|
rom_byte_mask = (uint8_t)(rom_byte_mask << 1U);
|
||||||
|
if (rom_byte_mask == 0U) {
|
||||||
|
rom_byte_number++;
|
||||||
|
rom_byte_mask = 1U;
|
||||||
|
}
|
||||||
|
} while (rom_byte_number < ONEWIRE_ROM_SIZE);
|
||||||
|
|
||||||
|
if (OneWire_Crc8(search->rom, ONEWIRE_ROM_SIZE) != 0U) {
|
||||||
|
OneWire_SearchInit(search);
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
search->last_discrepancy = last_zero;
|
||||||
|
if (search->last_discrepancy == 0U) {
|
||||||
|
search->last_device = 1U;
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_SelectRom(const uint8_t *rom)
|
||||||
|
{
|
||||||
|
if (OneWire_Reset() == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
OneWire_WriteByte(OW_CMD_MATCH_ROM);
|
||||||
|
OneWire_WriteBytes(rom, ONEWIRE_ROM_SIZE);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_SkipRom(void)
|
||||||
|
{
|
||||||
|
if (OneWire_Reset() == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
OneWire_WriteByte(OW_CMD_SKIP_ROM);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
305
c/ds18b20/tests/fake_onewire.c
Normal file
305
c/ds18b20/tests/fake_onewire.c
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
/**
|
||||||
|
* @file fake_onewire.c
|
||||||
|
* @brief Симулятор шины 1-Wire с виртуальными датчиками DS18B20.
|
||||||
|
*
|
||||||
|
* Реализует те же пять функций, что и настоящий порт, но работает не с
|
||||||
|
* выводом, а с моделью шины. Этого достаточно, чтобы прогнать на хосте
|
||||||
|
* весь алгоритм SEARCH ROM и обмен с датчиком побитно - именно там
|
||||||
|
* прячутся ошибки, которых не видно в обзоре кода.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "fake_onewire.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "onewire.h"
|
||||||
|
|
||||||
|
/** Состояние разбора команд шины. */
|
||||||
|
typedef enum {
|
||||||
|
FAKE_IDLE = 0, /**< После сброса, ждём код команды ROM. */
|
||||||
|
FAKE_SEARCH, /**< Идёт SEARCH ROM. */
|
||||||
|
FAKE_MATCH, /**< Принимаем 8 байт адреса. */
|
||||||
|
FAKE_FUNCTION, /**< ROM-команда отработала, ждём функцию. */
|
||||||
|
FAKE_READ_SCRATCH, /**< Отдаём scratchpad. */
|
||||||
|
FAKE_WRITE_SCRATCH, /**< Принимаем TH, TL, config. */
|
||||||
|
FAKE_READ_POWER /**< Отдаём один бит типа питания. */
|
||||||
|
} fake_state_t;
|
||||||
|
|
||||||
|
static fake_bus_t *g_bus;
|
||||||
|
static fake_state_t g_state;
|
||||||
|
|
||||||
|
/* Сборка входящих и исходящих байтов по битам. */
|
||||||
|
static uint8_t g_in_byte;
|
||||||
|
static uint8_t g_in_bits;
|
||||||
|
static uint8_t g_out_byte;
|
||||||
|
static uint8_t g_out_bits;
|
||||||
|
static uint8_t g_out_index;
|
||||||
|
|
||||||
|
/* Состояние SEARCH ROM. */
|
||||||
|
static uint8_t g_search_bit; /**< Номер бита ROM, 0..63. */
|
||||||
|
static uint8_t g_search_phase; /**< 0 - прямой, 1 - обратный, 2 - выбор. */
|
||||||
|
static uint8_t g_match_index;
|
||||||
|
static uint8_t g_write_index;
|
||||||
|
|
||||||
|
/** Активен ли датчик в текущей транзакции. */
|
||||||
|
static uint8_t g_selected[FAKE_MAX_DEVICES];
|
||||||
|
|
||||||
|
void fake_bus_attach(fake_bus_t *bus)
|
||||||
|
{
|
||||||
|
g_bus = bus;
|
||||||
|
g_state = FAKE_IDLE;
|
||||||
|
g_in_byte = 0U;
|
||||||
|
g_in_bits = 0U;
|
||||||
|
g_out_bits = 0U;
|
||||||
|
g_out_index = 0U;
|
||||||
|
bus->reset_count = 0U;
|
||||||
|
bus->convert_count = 0U;
|
||||||
|
bus->copy_count = 0U;
|
||||||
|
bus->delay_us_total = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t fake_bus_crc8(const uint8_t *data, uint32_t size)
|
||||||
|
{
|
||||||
|
uint8_t crc = 0U;
|
||||||
|
for (uint32_t i = 0U; i < size; i++) {
|
||||||
|
crc ^= data[i];
|
||||||
|
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||||
|
crc = (uint8_t)((crc & 1U) ? ((crc >> 1) ^ 0x8CU) : (crc >> 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fake_device_init(fake_device_t *device, const uint8_t rom7[7],
|
||||||
|
int16_t raw, uint8_t parasite)
|
||||||
|
{
|
||||||
|
memcpy(device->rom, rom7, 7U);
|
||||||
|
device->rom[7] = fake_bus_crc8(device->rom, 7U);
|
||||||
|
device->present = 1U;
|
||||||
|
device->parasite = parasite;
|
||||||
|
device->accept_writes = 1U;
|
||||||
|
device->corrupt_crc = 0U;
|
||||||
|
|
||||||
|
device->scratchpad[0] = (uint8_t)((uint16_t)raw & 0xFFU);
|
||||||
|
device->scratchpad[1] = (uint8_t)(((uint16_t)raw >> 8) & 0xFFU);
|
||||||
|
device->scratchpad[2] = 0x4BU; /* TH по умолчанию */
|
||||||
|
device->scratchpad[3] = 0x46U; /* TL по умолчанию */
|
||||||
|
device->scratchpad[4] = 0x7FU; /* 12 бит */
|
||||||
|
device->scratchpad[5] = 0xFFU;
|
||||||
|
device->scratchpad[6] = 0x0CU;
|
||||||
|
device->scratchpad[7] = 0x10U;
|
||||||
|
device->scratchpad[8] = fake_bus_crc8(device->scratchpad, 8U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void refresh_crc(fake_device_t *device)
|
||||||
|
{
|
||||||
|
device->scratchpad[8] = fake_bus_crc8(device->scratchpad, 8U);
|
||||||
|
if (device->corrupt_crc != 0U) {
|
||||||
|
device->scratchpad[8] ^= 0xFFU;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Единственный выбранный датчик либо NULL, если их несколько или ноль. */
|
||||||
|
static fake_device_t *sole_selected(void)
|
||||||
|
{
|
||||||
|
fake_device_t *found = NULL;
|
||||||
|
for (uint8_t i = 0U; i < g_bus->count; i++) {
|
||||||
|
if ((g_selected[i] != 0U) && (g_bus->devices[i].present != 0U)) {
|
||||||
|
if (found != NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
found = &g_bus->devices[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Порт: пять функций, которых ждёт переносимая часть ------------------- */
|
||||||
|
|
||||||
|
void OneWire_Init(void)
|
||||||
|
{
|
||||||
|
g_state = FAKE_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneWire_DelayUs(uint32_t microseconds)
|
||||||
|
{
|
||||||
|
if (g_bus != NULL) {
|
||||||
|
g_bus->delay_us_total += microseconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_Reset(void)
|
||||||
|
{
|
||||||
|
uint8_t presence = 0U;
|
||||||
|
|
||||||
|
g_state = FAKE_IDLE;
|
||||||
|
g_in_byte = 0U;
|
||||||
|
g_in_bits = 0U;
|
||||||
|
g_out_bits = 0U;
|
||||||
|
g_out_index = 0U;
|
||||||
|
g_search_bit = 0U;
|
||||||
|
g_search_phase = 0U;
|
||||||
|
|
||||||
|
for (uint8_t i = 0U; i < g_bus->count; i++) {
|
||||||
|
g_selected[i] = g_bus->devices[i].present;
|
||||||
|
if (g_bus->devices[i].present != 0U) {
|
||||||
|
presence = 1U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_bus->reset_count++;
|
||||||
|
return presence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Обрабатывает собранный байт команды. */
|
||||||
|
static void dispatch_byte(uint8_t value)
|
||||||
|
{
|
||||||
|
switch (g_state) {
|
||||||
|
case FAKE_IDLE:
|
||||||
|
if (value == 0xF0U) { /* SEARCH ROM */
|
||||||
|
g_state = FAKE_SEARCH;
|
||||||
|
g_search_bit = 0U;
|
||||||
|
g_search_phase = 0U;
|
||||||
|
} else if (value == 0x55U) { /* MATCH ROM */
|
||||||
|
g_state = FAKE_MATCH;
|
||||||
|
g_match_index = 0U;
|
||||||
|
} else if (value == 0xCCU) { /* SKIP ROM */
|
||||||
|
g_state = FAKE_FUNCTION;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FAKE_MATCH:
|
||||||
|
for (uint8_t i = 0U; i < g_bus->count; i++) {
|
||||||
|
if (g_bus->devices[i].rom[g_match_index] != value) {
|
||||||
|
g_selected[i] = 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_match_index++;
|
||||||
|
if (g_match_index >= 8U) {
|
||||||
|
g_state = FAKE_FUNCTION;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FAKE_FUNCTION: {
|
||||||
|
fake_device_t *device = sole_selected();
|
||||||
|
if (value == 0x44U) { /* CONVERT T */
|
||||||
|
g_bus->convert_count++;
|
||||||
|
} else if (value == 0xBEU) { /* READ SCRATCHPAD */
|
||||||
|
if (device != NULL) {
|
||||||
|
refresh_crc(device);
|
||||||
|
g_state = FAKE_READ_SCRATCH;
|
||||||
|
g_out_index = 0U;
|
||||||
|
g_out_bits = 0U;
|
||||||
|
}
|
||||||
|
} else if (value == 0x4EU) { /* WRITE SCRATCHPAD */
|
||||||
|
g_state = FAKE_WRITE_SCRATCH;
|
||||||
|
g_write_index = 0U;
|
||||||
|
} else if (value == 0x48U) { /* COPY SCRATCHPAD */
|
||||||
|
g_bus->copy_count++;
|
||||||
|
} else if (value == 0xB4U) { /* READ POWER SUPPLY */
|
||||||
|
g_state = FAKE_READ_POWER;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case FAKE_WRITE_SCRATCH: {
|
||||||
|
fake_device_t *device = sole_selected();
|
||||||
|
if ((device != NULL) && (device->accept_writes != 0U)) {
|
||||||
|
device->scratchpad[2U + g_write_index] = value;
|
||||||
|
}
|
||||||
|
g_write_index++;
|
||||||
|
if (g_write_index >= 3U) {
|
||||||
|
if (device != NULL) {
|
||||||
|
refresh_crc(device);
|
||||||
|
}
|
||||||
|
g_state = FAKE_FUNCTION;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneWire_WriteBit(uint8_t bit)
|
||||||
|
{
|
||||||
|
if (g_state == FAKE_SEARCH) {
|
||||||
|
/* Третья фаза SEARCH ROM: мастер сообщает выбранную ветвь. */
|
||||||
|
for (uint8_t i = 0U; i < g_bus->count; i++) {
|
||||||
|
if (g_selected[i] == 0U) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint8_t rom_bit = (uint8_t)((g_bus->devices[i].rom[g_search_bit / 8U]
|
||||||
|
>> (g_search_bit % 8U)) & 1U);
|
||||||
|
if (rom_bit != (bit != 0U)) {
|
||||||
|
g_selected[i] = 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_search_bit++;
|
||||||
|
g_search_phase = 0U;
|
||||||
|
if (g_search_bit >= 64U) {
|
||||||
|
g_state = FAKE_FUNCTION;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_in_byte = (uint8_t)((g_in_byte >> 1) | ((bit != 0U) ? 0x80U : 0U));
|
||||||
|
g_in_bits++;
|
||||||
|
if (g_in_bits >= 8U) {
|
||||||
|
uint8_t value = g_in_byte;
|
||||||
|
g_in_byte = 0U;
|
||||||
|
g_in_bits = 0U;
|
||||||
|
dispatch_byte(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t OneWire_ReadBit(void)
|
||||||
|
{
|
||||||
|
if (g_state == FAKE_SEARCH) {
|
||||||
|
uint8_t ones = 0U;
|
||||||
|
uint8_t zeros = 0U;
|
||||||
|
for (uint8_t i = 0U; i < g_bus->count; i++) {
|
||||||
|
if (g_selected[i] == 0U) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint8_t rom_bit = (uint8_t)((g_bus->devices[i].rom[g_search_bit / 8U]
|
||||||
|
>> (g_search_bit % 8U)) & 1U);
|
||||||
|
if (rom_bit != 0U) {
|
||||||
|
ones = 1U;
|
||||||
|
} else {
|
||||||
|
zeros = 1U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((ones == 0U) && (zeros == 0U)) {
|
||||||
|
return 1U; /* никто не отвечает - обе фазы единичны */
|
||||||
|
}
|
||||||
|
uint8_t result = (g_search_phase == 0U) ? (uint8_t)(zeros == 0U)
|
||||||
|
: (uint8_t)(ones == 0U);
|
||||||
|
g_search_phase = (uint8_t)(g_search_phase + 1U);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_state == FAKE_READ_POWER) {
|
||||||
|
fake_device_t *device = sole_selected();
|
||||||
|
/* Паразитное питание прижимает шину к нулю. */
|
||||||
|
return (uint8_t)((device != NULL) && (device->parasite != 0U) ? 0U : 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_state == FAKE_READ_SCRATCH) {
|
||||||
|
fake_device_t *device = sole_selected();
|
||||||
|
if (device == NULL) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
if (g_out_bits == 0U) {
|
||||||
|
g_out_byte = (g_out_index < 9U) ? device->scratchpad[g_out_index] : 0xFFU;
|
||||||
|
}
|
||||||
|
uint8_t bit = (uint8_t)((g_out_byte >> g_out_bits) & 1U);
|
||||||
|
g_out_bits++;
|
||||||
|
if (g_out_bits >= 8U) {
|
||||||
|
g_out_bits = 0U;
|
||||||
|
g_out_index++;
|
||||||
|
}
|
||||||
|
return bit;
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
41
c/ds18b20/tests/fake_onewire.h
Normal file
41
c/ds18b20/tests/fake_onewire.h
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @file fake_onewire.h
|
||||||
|
* @brief Модель шины 1-Wire с виртуальными датчиками для host-тестов.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FAKE_ONEWIRE_H
|
||||||
|
#define FAKE_ONEWIRE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define FAKE_MAX_DEVICES 8U
|
||||||
|
|
||||||
|
/** Виртуальный датчик: ROM, scratchpad и управляемые отказы. */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t rom[8];
|
||||||
|
uint8_t scratchpad[9];
|
||||||
|
uint8_t present; /**< 0 - датчик снят с шины. */
|
||||||
|
uint8_t parasite; /**< 1 - отвечает как паразитно питаемый. */
|
||||||
|
uint8_t accept_writes; /**< 0 - молча игнорирует запись. */
|
||||||
|
uint8_t corrupt_crc; /**< 1 - портит CRC8 scratchpad. */
|
||||||
|
} fake_device_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
fake_device_t devices[FAKE_MAX_DEVICES];
|
||||||
|
uint8_t count;
|
||||||
|
uint32_t reset_count;
|
||||||
|
uint32_t convert_count;
|
||||||
|
uint32_t copy_count;
|
||||||
|
uint32_t delay_us_total;
|
||||||
|
} fake_bus_t;
|
||||||
|
|
||||||
|
/** Подключает модель к порту; после вызова работают функции OneWire_*. */
|
||||||
|
void fake_bus_attach(fake_bus_t *bus);
|
||||||
|
|
||||||
|
/** Заполняет датчик: семь байт ROM без CRC, температура и тип питания. */
|
||||||
|
void fake_device_init(fake_device_t *device, const uint8_t rom7[7],
|
||||||
|
int16_t raw, uint8_t parasite);
|
||||||
|
|
||||||
|
uint8_t fake_bus_crc8(const uint8_t *data, uint32_t size);
|
||||||
|
|
||||||
|
#endif /* FAKE_ONEWIRE_H */
|
||||||
356
c/ds18b20/tests/test_ds18b20.c
Normal file
356
c/ds18b20/tests/test_ds18b20.c
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
/**
|
||||||
|
* Тесты драйвера DS18B20 и переносимой части 1-Wire на симуляторе шины.
|
||||||
|
*
|
||||||
|
* clang -std=c99 -Wall -Wextra -Iinclude -Itests \
|
||||||
|
* tests/test_ds18b20.c tests/fake_onewire.c src/onewire.c src/ds18b20.c -o test
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "ds18b20.h"
|
||||||
|
#include "fake_onewire.h"
|
||||||
|
#include "onewire.h"
|
||||||
|
|
||||||
|
static int g_fail;
|
||||||
|
|
||||||
|
#define CHECK(cond) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
|
||||||
|
g_fail++; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* Три датчика с ROM, которые расходятся в разных битах: обход должен
|
||||||
|
пройти по всем ветвям дерева SEARCH ROM. */
|
||||||
|
static const uint8_t ROM_A[7] = { 0x28, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 };
|
||||||
|
static const uint8_t ROM_B[7] = { 0x28, 0x11, 0x22, 0x33, 0x44, 0x55, 0x67 };
|
||||||
|
static const uint8_t ROM_C[7] = { 0x28, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01 };
|
||||||
|
/* Датчик чужого семейства: DS18B20_Scan обязан его пропустить. */
|
||||||
|
static const uint8_t ROM_OTHER[7] = { 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 };
|
||||||
|
|
||||||
|
static fake_bus_t g_bus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Находит запись по ROM виртуального датчика.
|
||||||
|
*
|
||||||
|
* Порядок обхода SEARCH ROM задаётся битами идентификаторов, а не порядком
|
||||||
|
* массива, поэтому опираться на индекс в тестах нельзя.
|
||||||
|
*/
|
||||||
|
static DS18B20_Sensor *by_device(DS18B20_Bus *bus, uint8_t device_index)
|
||||||
|
{
|
||||||
|
return DS18B20_Find(bus, g_bus.devices[device_index].rom);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup(uint8_t count)
|
||||||
|
{
|
||||||
|
memset(&g_bus, 0, sizeof g_bus);
|
||||||
|
g_bus.count = count;
|
||||||
|
fake_bus_attach(&g_bus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- CRC8 ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void test_crc8(void)
|
||||||
|
{
|
||||||
|
/* Контрольный ROM из datasheet Maxim: CRC блока вместе с байтом = 0. */
|
||||||
|
static const uint8_t rom[8] = { 0x28, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x00 };
|
||||||
|
uint8_t crc = OneWire_Crc8(rom, 7U);
|
||||||
|
uint8_t full[8];
|
||||||
|
|
||||||
|
memcpy(full, rom, 8U);
|
||||||
|
full[7] = crc;
|
||||||
|
CHECK(OneWire_Crc8(full, 8U) == 0U);
|
||||||
|
/* Симулятор и библиотека считают CRC8 одинаково. */
|
||||||
|
CHECK(fake_bus_crc8(rom, 7U) == crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Поиск ----------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void test_search_finds_all(void)
|
||||||
|
{
|
||||||
|
setup(3U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0, 0U);
|
||||||
|
fake_device_init(&g_bus.devices[1], ROM_B, 0, 0U);
|
||||||
|
fake_device_init(&g_bus.devices[2], ROM_C, 0, 0U);
|
||||||
|
|
||||||
|
OneWire_Search search;
|
||||||
|
uint8_t found[3][8];
|
||||||
|
uint8_t count = 0U;
|
||||||
|
|
||||||
|
OneWire_SearchInit(&search);
|
||||||
|
while ((count < 3U) && (OneWire_SearchNext(&search) != 0U)) {
|
||||||
|
memcpy(found[count], search.rom, 8U);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
CHECK(count == 3U);
|
||||||
|
/* Обход завершён: следующий вызов не должен выдавать четвёртый ROM. */
|
||||||
|
CHECK(OneWire_SearchNext(&search) == 0U);
|
||||||
|
|
||||||
|
/* Каждый ROM найден ровно один раз. */
|
||||||
|
for (uint8_t d = 0U; d < 3U; d++) {
|
||||||
|
uint8_t hits = 0U;
|
||||||
|
for (uint8_t f = 0U; f < count; f++) {
|
||||||
|
if (memcmp(found[f], g_bus.devices[d].rom, 8U) == 0) {
|
||||||
|
hits++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(hits == 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_search_on_empty_bus(void)
|
||||||
|
{
|
||||||
|
setup(0U);
|
||||||
|
OneWire_Search search;
|
||||||
|
OneWire_SearchInit(&search);
|
||||||
|
CHECK(OneWire_SearchNext(&search) == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_search_single_device(void)
|
||||||
|
{
|
||||||
|
setup(1U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_C, 0, 0U);
|
||||||
|
|
||||||
|
OneWire_Search search;
|
||||||
|
OneWire_SearchInit(&search);
|
||||||
|
CHECK(OneWire_SearchNext(&search) == 1U);
|
||||||
|
CHECK(memcmp(search.rom, g_bus.devices[0].rom, 8U) == 0);
|
||||||
|
CHECK(search.last_device == 1U);
|
||||||
|
CHECK(OneWire_SearchNext(&search) == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Сканирование и чтение ------------------------------------------------- */
|
||||||
|
|
||||||
|
static void test_scan_filters_family(void)
|
||||||
|
{
|
||||||
|
setup(3U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0, 0U);
|
||||||
|
fake_device_init(&g_bus.devices[1], ROM_OTHER, 0, 0U);
|
||||||
|
fake_device_init(&g_bus.devices[2], ROM_C, 0, 1U);
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
CHECK(DS18B20_Scan(&bus) == 2U);
|
||||||
|
CHECK(bus.count == 2U);
|
||||||
|
CHECK(bus.sensors[0].rom[0] == DS18B20_FAMILY_CODE);
|
||||||
|
CHECK(bus.sensors[1].rom[0] == DS18B20_FAMILY_CODE);
|
||||||
|
/* Паразитное питание одного датчика поднимает флаг на всю шину. */
|
||||||
|
CHECK(bus.parasite == 1U);
|
||||||
|
CHECK((by_device(&bus, 2U)->status & DS18B20_STATUS_PARASITE) != 0U);
|
||||||
|
CHECK((by_device(&bus, 0U)->status & DS18B20_STATUS_PARASITE) == 0U);
|
||||||
|
/* Чужое семейство в перечень не попало. */
|
||||||
|
CHECK(DS18B20_Find(&bus, g_bus.devices[1].rom) == NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_read_all(void)
|
||||||
|
{
|
||||||
|
setup(2U);
|
||||||
|
/* +25.0625 C и -10.125 C в формате DS18B20. */
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0x0191, 0U);
|
||||||
|
fake_device_init(&g_bus.devices[1], ROM_C, (int16_t)0xFF5E, 0U);
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
CHECK(DS18B20_Scan(&bus) == 2U);
|
||||||
|
CHECK(DS18B20_StartConversion() == 1U);
|
||||||
|
CHECK(g_bus.convert_count == 1U);
|
||||||
|
CHECK(DS18B20_ReadAll(&bus) == 2U);
|
||||||
|
|
||||||
|
DS18B20_Sensor *warm = by_device(&bus, 0U);
|
||||||
|
DS18B20_Sensor *cold = by_device(&bus, 1U);
|
||||||
|
CHECK(warm != NULL && cold != NULL);
|
||||||
|
CHECK(warm->raw == 0x0191);
|
||||||
|
CHECK(cold->raw == (int16_t)0xFF5E);
|
||||||
|
CHECK((warm->status & DS18B20_STATUS_VALID) != 0U);
|
||||||
|
CHECK(warm->user_byte1 == 0x4BU);
|
||||||
|
CHECK(warm->user_byte2 == 0x46U);
|
||||||
|
CHECK(warm->config == 0x7FU);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_crc_error_does_not_stop_the_poll(void)
|
||||||
|
{
|
||||||
|
setup(2U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U);
|
||||||
|
fake_device_init(&g_bus.devices[1], ROM_C, 0x0200, 0U);
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
CHECK(DS18B20_Scan(&bus) == 2U);
|
||||||
|
g_bus.devices[0].corrupt_crc = 1U;
|
||||||
|
|
||||||
|
/* Первый датчик отдаёт битый scratchpad, второй обязан быть опрошен. */
|
||||||
|
CHECK(DS18B20_ReadAll(&bus) == 1U);
|
||||||
|
DS18B20_Sensor *broken = by_device(&bus, 0U);
|
||||||
|
DS18B20_Sensor *good = by_device(&bus, 1U);
|
||||||
|
CHECK((broken->status & DS18B20_STATUS_CRC_ERROR) != 0U);
|
||||||
|
CHECK((broken->status & DS18B20_STATUS_VALID) == 0U);
|
||||||
|
CHECK((good->status & DS18B20_STATUS_VALID) != 0U);
|
||||||
|
CHECK(good->raw == 0x0200);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_missing_device_is_reported(void)
|
||||||
|
{
|
||||||
|
setup(2U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U);
|
||||||
|
fake_device_init(&g_bus.devices[1], ROM_C, 0x0200, 0U);
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
CHECK(DS18B20_Scan(&bus) == 2U);
|
||||||
|
|
||||||
|
/* Датчик сняли с шины между сканированием и опросом. */
|
||||||
|
DS18B20_Sensor *gone = by_device(&bus, 0U);
|
||||||
|
DS18B20_Sensor *alive = by_device(&bus, 1U);
|
||||||
|
g_bus.devices[0].present = 0U;
|
||||||
|
CHECK(DS18B20_ReadAll(&bus) == 1U);
|
||||||
|
|
||||||
|
/* NO_PRESENCE здесь не появится: presence-импульс выдаёт оставшийся
|
||||||
|
датчик, и MATCH ROM к пропавшему проходит без признаков ошибки.
|
||||||
|
Пропажа видна только по CRC8 пустого scratchpad - так устроена шина. */
|
||||||
|
CHECK((gone->status & DS18B20_STATUS_CRC_ERROR) != 0U);
|
||||||
|
CHECK((gone->status & DS18B20_STATUS_VALID) == 0U);
|
||||||
|
/* Пропажа соседа не должна мешать опросу оставшихся. */
|
||||||
|
CHECK((alive->status & DS18B20_STATUS_VALID) != 0U);
|
||||||
|
|
||||||
|
/* А вот когда молчит вся шина, драйвер обязан сказать NO_PRESENCE. */
|
||||||
|
g_bus.devices[1].present = 0U;
|
||||||
|
CHECK(DS18B20_ReadAll(&bus) == 0U);
|
||||||
|
CHECK((gone->status & DS18B20_STATUS_NO_PRESENCE) != 0U);
|
||||||
|
CHECK((alive->status & DS18B20_STATUS_NO_PRESENCE) != 0U);
|
||||||
|
CHECK(DS18B20_StartConversion() == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Поиск по ROM и запись -------------------------------------------------- */
|
||||||
|
|
||||||
|
static void test_find(void)
|
||||||
|
{
|
||||||
|
setup(2U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0, 0U);
|
||||||
|
fake_device_init(&g_bus.devices[1], ROM_C, 0, 0U);
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
CHECK(DS18B20_Scan(&bus) == 2U);
|
||||||
|
|
||||||
|
DS18B20_Sensor *found = DS18B20_Find(&bus, g_bus.devices[1].rom);
|
||||||
|
CHECK(found != NULL);
|
||||||
|
CHECK(memcmp(found->rom, g_bus.devices[1].rom, 8U) == 0);
|
||||||
|
|
||||||
|
static const uint8_t unknown[8] = { 0x28, 0, 0, 0, 0, 0, 0, 0 };
|
||||||
|
CHECK(DS18B20_Find(&bus, unknown) == NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_write_scratchpad(void)
|
||||||
|
{
|
||||||
|
setup(1U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U);
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
CHECK(DS18B20_Scan(&bus) == 1U);
|
||||||
|
|
||||||
|
DS18B20_Sensor *sensor = by_device(&bus, 0U);
|
||||||
|
uint8_t config = DS18B20_ConfigFromResolution(10U);
|
||||||
|
CHECK(DS18B20_WriteScratchpad(sensor, 0x11U, 0x22U, config, 0U)
|
||||||
|
== DS18B20_WRITE_OK);
|
||||||
|
CHECK(sensor->user_byte1 == 0x11U);
|
||||||
|
CHECK(sensor->user_byte2 == 0x22U);
|
||||||
|
CHECK(sensor->config == config);
|
||||||
|
CHECK(g_bus.devices[0].scratchpad[2] == 0x11U);
|
||||||
|
CHECK(g_bus.copy_count == 0U);
|
||||||
|
|
||||||
|
/* Сохранение в EEPROM датчика: COPY SCRATCHPAD плюс выдержка. */
|
||||||
|
uint32_t before = g_bus.delay_us_total;
|
||||||
|
CHECK(DS18B20_WriteScratchpad(sensor, 0x33U, 0x44U, config, 1U)
|
||||||
|
== DS18B20_WRITE_OK);
|
||||||
|
CHECK(g_bus.copy_count == 1U);
|
||||||
|
CHECK((g_bus.delay_us_total - before) >= 12000U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_write_rejected(void)
|
||||||
|
{
|
||||||
|
setup(1U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U);
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
CHECK(DS18B20_Scan(&bus) == 1U);
|
||||||
|
|
||||||
|
/* Датчик отвечает, но регистры не меняет: обмен состоялся, значение нет. */
|
||||||
|
g_bus.devices[0].accept_writes = 0U;
|
||||||
|
DS18B20_Sensor *sensor = by_device(&bus, 0U);
|
||||||
|
CHECK(DS18B20_WriteScratchpad(sensor, 0x77U, 0x88U, 0x3FU, 0U)
|
||||||
|
== DS18B20_WRITE_REJECTED);
|
||||||
|
/* Кэш обновлён тем, что реально лежит в датчике. */
|
||||||
|
CHECK(sensor->user_byte1 == 0x4BU);
|
||||||
|
CHECK(sensor->config == 0x7FU);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_write_bus_error(void)
|
||||||
|
{
|
||||||
|
setup(1U);
|
||||||
|
fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U);
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
CHECK(DS18B20_Scan(&bus) == 1U);
|
||||||
|
|
||||||
|
DS18B20_Sensor *sensor = by_device(&bus, 0U);
|
||||||
|
g_bus.devices[0].present = 0U;
|
||||||
|
CHECK(DS18B20_WriteScratchpad(sensor, 0x11U, 0x22U, 0x7FU, 0U)
|
||||||
|
== DS18B20_WRITE_BUS_ERROR);
|
||||||
|
CHECK((sensor->status & DS18B20_STATUS_NO_PRESENCE) != 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Разрешение ------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void test_resolution(void)
|
||||||
|
{
|
||||||
|
CHECK(DS18B20_ConfigFromResolution(9U) == 0x1FU);
|
||||||
|
CHECK(DS18B20_ConfigFromResolution(10U) == 0x3FU);
|
||||||
|
CHECK(DS18B20_ConfigFromResolution(11U) == 0x5FU);
|
||||||
|
CHECK(DS18B20_ConfigFromResolution(12U) == 0x7FU);
|
||||||
|
/* Выход за диапазон зажимается, а не заворачивается. */
|
||||||
|
CHECK(DS18B20_ConfigFromResolution(0U) == 0x1FU);
|
||||||
|
CHECK(DS18B20_ConfigFromResolution(200U) == 0x7FU);
|
||||||
|
|
||||||
|
CHECK(DS18B20_ConversionTimeMs(0x1FU) == 94U);
|
||||||
|
CHECK(DS18B20_ConversionTimeMs(0x3FU) == 188U);
|
||||||
|
CHECK(DS18B20_ConversionTimeMs(0x5FU) == 375U);
|
||||||
|
CHECK(DS18B20_ConversionTimeMs(0x7FU) == 750U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_sensor_limit(void)
|
||||||
|
{
|
||||||
|
/* На шине больше датчиков, чем помещается в кадр SENSOR_DATA. */
|
||||||
|
setup(FAKE_MAX_DEVICES);
|
||||||
|
for (uint8_t i = 0U; i < FAKE_MAX_DEVICES; i++) {
|
||||||
|
uint8_t rom[7] = { 0x28, 0x10, 0x20, 0x30, 0x40, 0x50, 0x00 };
|
||||||
|
rom[6] = (uint8_t)(0xA0U + i);
|
||||||
|
fake_device_init(&g_bus.devices[i], rom, (int16_t)(0x100 + i), 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
DS18B20_Bus bus;
|
||||||
|
uint8_t count = DS18B20_Scan(&bus);
|
||||||
|
CHECK(count <= DS18B20_MAX_SENSORS);
|
||||||
|
CHECK(count == DS18B20_MAX_SENSORS);
|
||||||
|
CHECK(DS18B20_ReadAll(&bus) == DS18B20_MAX_SENSORS);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_crc8();
|
||||||
|
test_search_finds_all();
|
||||||
|
test_search_on_empty_bus();
|
||||||
|
test_search_single_device();
|
||||||
|
test_scan_filters_family();
|
||||||
|
test_read_all();
|
||||||
|
test_crc_error_does_not_stop_the_poll();
|
||||||
|
test_missing_device_is_reported();
|
||||||
|
test_find();
|
||||||
|
test_write_scratchpad();
|
||||||
|
test_write_rejected();
|
||||||
|
test_write_bus_error();
|
||||||
|
test_resolution();
|
||||||
|
test_sensor_limit();
|
||||||
|
|
||||||
|
if (g_fail == 0) {
|
||||||
|
printf("all ds18b20 tests passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
printf("%d check(s) failed\n", g_fail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
65
c/eeprom-ft24c256/README.md
Normal file
65
c/eeprom-ft24c256/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# eeprom-ft24c256
|
||||||
|
|
||||||
|
Драйвер последовательной EEPROM **FT24C256** и совместимого семейства
|
||||||
|
24Cxx по шине I²C.
|
||||||
|
|
||||||
|
Ядро на C99: не включает заголовки периферии, не обращается к регистрам,
|
||||||
|
не пользуется прерываниями. Обмен идёт через таблицу `FT24C256_Io`,
|
||||||
|
которую заполняет порт платы.
|
||||||
|
|
||||||
|
```
|
||||||
|
приложение
|
||||||
|
│
|
||||||
|
ft24c256.c нарезка записи по страницам, ожидание цикла записи,
|
||||||
|
двухбайтовый адрес слова, счётчики ошибок
|
||||||
|
│
|
||||||
|
FT24C256_Io write, write_read, delay_ms, set_write_protect
|
||||||
|
│
|
||||||
|
порт платы I2C
|
||||||
|
```
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Файл | Что делает | Зависимости |
|
||||||
|
|---|---|---|
|
||||||
|
| `ft24c256.h`, `ft24c256.c` | чтение и запись произвольных блоков, стирание, защита записи, опрос готовности | `stdint.h` |
|
||||||
|
|
||||||
|
FT24C256 — 32768 байт (256 кбит), адрес слова двухбайтовый, страница записи
|
||||||
|
64 байта. Микросхема принимает до 64 байт за транзакцию, но только внутри
|
||||||
|
страницы: при переходе через границу счётчик адреса заворачивается на начало
|
||||||
|
той же страницы и затирает уже принятые байты. Поэтому `FT24C256_Write()`
|
||||||
|
сама режет запрос по границам страниц — вызывающему коду об этом думать не надо.
|
||||||
|
|
||||||
|
## Что нужно от платформы
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t write(void *ctx, uint8_t addr, const uint8_t *data, uint16_t size);
|
||||||
|
uint8_t write_read(void *ctx, uint8_t addr, const uint8_t *tx, uint16_t tx_size,
|
||||||
|
uint8_t *rx, uint16_t rx_size); /* с повторным стартом */
|
||||||
|
void delay_ms(void *ctx, uint32_t ms);
|
||||||
|
```
|
||||||
|
|
||||||
|
Адрес передаётся семибитным, без бита направления — сдвиг делает порт.
|
||||||
|
`set_write_protect` необязателен: если вывод WP не разведён, поле оставляется нулём.
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```c
|
||||||
|
FT24C256 eeprom;
|
||||||
|
FT24C256_Io io = { .write = i2c_write, .write_read = i2c_write_read,
|
||||||
|
.delay_ms = board_delay, .context = &board };
|
||||||
|
FT24C256_Config config;
|
||||||
|
FT24C256_ConfigDefault(&config); /* 32 КБ, страница 64, адрес 0x50 */
|
||||||
|
|
||||||
|
FT24C256_Init(&eeprom, &io, &config);
|
||||||
|
|
||||||
|
uint8_t settings[100];
|
||||||
|
FT24C256_Write(&eeprom, 0x0030, settings, sizeof settings);
|
||||||
|
FT24C256_Read(&eeprom, 0x0030, settings, sizeof settings);
|
||||||
|
```
|
||||||
|
|
||||||
|
До восьми кристаллов на шине: адрес `0x50 + index`, `FT24C256_AddressOf()`.
|
||||||
|
|
||||||
|
## Проверено в проектах
|
||||||
|
|
||||||
|
`KONOR_ds18b20`, `OpticalTester` — I2C1 на STM32F103C8T6.
|
||||||
378
c/eeprom-ft24c256/ft24c256.c
Normal file
378
c/eeprom-ft24c256/ft24c256.c
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
/**
|
||||||
|
* @file ft24c256.c
|
||||||
|
* @brief Постраничная запись, сплошное чтение и ожидание готовности FT24C256.
|
||||||
|
*
|
||||||
|
* Реализация не хранит буфера всего массива: единственная крупная переменная —
|
||||||
|
* кадр одной транзакции записи на стеке, длиной адрес слова плюс страница
|
||||||
|
* (@ref FT24C256_ADDRESS_SIZE + @ref FT24C256_PAGE_MAX байт). Времени
|
||||||
|
* библиотека не измеряет: выдержку цикла записи даёт порт, а готовность
|
||||||
|
* определяется откликом самой микросхемы.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ft24c256.h"
|
||||||
|
|
||||||
|
/** Кадр записи: адрес слова и страница данных. */
|
||||||
|
#define FT24C256_FRAME_MAX (FT24C256_ADDRESS_SIZE + FT24C256_PAGE_MAX)
|
||||||
|
|
||||||
|
/** Длина куска, которым FT24C256_Update() сверяет содержимое. */
|
||||||
|
#define FT24C256_COMPARE_MAX 32U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Проверяет пригодность состояния к обмену.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @return 1, если состояние настроено и обратные вызовы на месте, иначе 0.
|
||||||
|
*/
|
||||||
|
static uint8_t ft24c256_valid(const FT24C256 *device)
|
||||||
|
{
|
||||||
|
if (device == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (device->ready == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return (uint8_t)((device->io.write != 0) && (device->io.write_read != 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Проверяет, что область целиком помещается в массив.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param address Адрес первого байта.
|
||||||
|
* @param size Число байтов.
|
||||||
|
* @return 1, если область допустима, иначе 0.
|
||||||
|
*/
|
||||||
|
static uint8_t ft24c256_in_range(const FT24C256 *device, uint32_t address,
|
||||||
|
uint32_t size)
|
||||||
|
{
|
||||||
|
if (address >= device->config.size) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return (uint8_t)(size <= (device->config.size - address));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Раскладывает адрес слова в два байта, старший первым.
|
||||||
|
*
|
||||||
|
* @param address Адрес байта в массиве.
|
||||||
|
* @param frame Приёмник кадра длиной не менее @ref FT24C256_ADDRESS_SIZE.
|
||||||
|
*/
|
||||||
|
static void ft24c256_put_address(uint32_t address, uint8_t *frame)
|
||||||
|
{
|
||||||
|
frame[0] = (uint8_t)((address >> 8U) & 0xFFU);
|
||||||
|
frame[1] = (uint8_t)(address & 0xFFU);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Выдерживает паузу, если порт умеет её отсчитывать.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param milliseconds Длительность паузы.
|
||||||
|
*/
|
||||||
|
static void ft24c256_delay(FT24C256 *device, uint32_t milliseconds)
|
||||||
|
{
|
||||||
|
if (device->io.delay_ms != 0) {
|
||||||
|
device->io.delay_ms(device->io.context, milliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Записывает часть страницы одной транзакцией и ждёт готовности.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param address Адрес первого байта; не пересекает границу страницы.
|
||||||
|
* @param data Записываемые данные.
|
||||||
|
* @param size Число байтов, не больше размера страницы.
|
||||||
|
* @return 1 при успехе, иначе 0.
|
||||||
|
*/
|
||||||
|
static uint8_t ft24c256_write_page(FT24C256 *device, uint32_t address,
|
||||||
|
const uint8_t *data, uint16_t size)
|
||||||
|
{
|
||||||
|
uint8_t frame[FT24C256_FRAME_MAX];
|
||||||
|
uint16_t index;
|
||||||
|
|
||||||
|
if (size > FT24C256_PAGE_MAX) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
ft24c256_put_address(address, frame);
|
||||||
|
for (index = 0U; index < size; index++) {
|
||||||
|
frame[FT24C256_ADDRESS_SIZE + index] = data[index];
|
||||||
|
}
|
||||||
|
if (device->io.write(device->io.context, device->config.address, frame,
|
||||||
|
(uint16_t)(FT24C256_ADDRESS_SIZE + size)) == 0U) {
|
||||||
|
device->write_errors++;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
device->written_bytes += size;
|
||||||
|
return FT24C256_WaitReady(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FT24C256_ConfigDefault(FT24C256_Config *config)
|
||||||
|
{
|
||||||
|
if (config == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config->size = FT24C256_SIZE;
|
||||||
|
config->page_size = FT24C256_PAGE_SIZE;
|
||||||
|
config->address = FT24C256_BASE_ADDRESS;
|
||||||
|
config->write_ms = FT24C256_WRITE_MS;
|
||||||
|
config->poll_retries = FT24C256_POLL_RETRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_Init(FT24C256 *device, const FT24C256_Io *io,
|
||||||
|
const FT24C256_Config *config)
|
||||||
|
{
|
||||||
|
if ((device == 0) || (io == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if ((io->write == 0) || (io->write_read == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
device->io = *io;
|
||||||
|
if (config != 0) {
|
||||||
|
device->config = *config;
|
||||||
|
} else {
|
||||||
|
FT24C256_ConfigDefault(&device->config);
|
||||||
|
}
|
||||||
|
if (device->config.size == 0U) {
|
||||||
|
device->config.size = FT24C256_SIZE;
|
||||||
|
}
|
||||||
|
if (device->config.page_size == 0U) {
|
||||||
|
device->config.page_size = FT24C256_PAGE_SIZE;
|
||||||
|
}
|
||||||
|
if (device->config.page_size > FT24C256_PAGE_MAX) {
|
||||||
|
device->config.page_size = FT24C256_PAGE_MAX;
|
||||||
|
}
|
||||||
|
if (device->config.address == 0U) {
|
||||||
|
device->config.address = FT24C256_BASE_ADDRESS;
|
||||||
|
}
|
||||||
|
if (device->config.write_ms == 0U) {
|
||||||
|
device->config.write_ms = FT24C256_WRITE_MS;
|
||||||
|
}
|
||||||
|
/* Без опроса готовности единственная мера времени — выдержка порта. */
|
||||||
|
if ((device->config.poll_retries == 0U) && (device->io.delay_ms == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
device->ready = 1U;
|
||||||
|
device->written_bytes = 0U;
|
||||||
|
device->read_bytes = 0U;
|
||||||
|
device->write_errors = 0U;
|
||||||
|
device->read_errors = 0U;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_IsPresent(FT24C256 *device)
|
||||||
|
{
|
||||||
|
if (ft24c256_valid(device) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return device->io.write(device->io.context, device->config.address, 0, 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_WaitReady(FT24C256 *device)
|
||||||
|
{
|
||||||
|
uint8_t attempt;
|
||||||
|
|
||||||
|
if (ft24c256_valid(device) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (device->config.poll_retries == 0U) {
|
||||||
|
ft24c256_delay(device, device->config.write_ms);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
for (attempt = 0U; attempt < device->config.poll_retries; attempt++) {
|
||||||
|
if (device->io.write(device->io.context, device->config.address, 0,
|
||||||
|
0U) != 0U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
ft24c256_delay(device, 1U);
|
||||||
|
}
|
||||||
|
device->write_errors++;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_Read(FT24C256 *device, uint32_t address, void *buffer,
|
||||||
|
uint16_t size)
|
||||||
|
{
|
||||||
|
uint8_t frame[FT24C256_ADDRESS_SIZE];
|
||||||
|
|
||||||
|
if ((ft24c256_valid(device) == 0U) || (buffer == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (size == 0U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
if (ft24c256_in_range(device, address, size) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
ft24c256_put_address(address, frame);
|
||||||
|
if (device->io.write_read(device->io.context, device->config.address, frame,
|
||||||
|
FT24C256_ADDRESS_SIZE, (uint8_t *)buffer,
|
||||||
|
size) == 0U) {
|
||||||
|
device->read_errors++;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
device->read_bytes += size;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_Write(FT24C256 *device, uint32_t address, const void *data,
|
||||||
|
uint16_t size)
|
||||||
|
{
|
||||||
|
const uint8_t *source = (const uint8_t *)data;
|
||||||
|
uint32_t position = address;
|
||||||
|
uint16_t left = size;
|
||||||
|
|
||||||
|
if ((ft24c256_valid(device) == 0U) || (data == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (size == 0U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
if (ft24c256_in_range(device, address, size) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (left != 0U) {
|
||||||
|
const uint32_t page = device->config.page_size;
|
||||||
|
const uint32_t tail = page - (position % page);
|
||||||
|
uint16_t chunk = (tail < left) ? (uint16_t)tail : left;
|
||||||
|
|
||||||
|
if (ft24c256_write_page(device, position, source, chunk) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
position += chunk;
|
||||||
|
source += chunk;
|
||||||
|
left = (uint16_t)(left - chunk);
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_Update(FT24C256 *device, uint32_t address, const void *data,
|
||||||
|
uint16_t size)
|
||||||
|
{
|
||||||
|
const uint8_t *source = (const uint8_t *)data;
|
||||||
|
uint32_t position = address;
|
||||||
|
uint16_t left = size;
|
||||||
|
|
||||||
|
if ((ft24c256_valid(device) == 0U) || (data == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (size == 0U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
if (ft24c256_in_range(device, address, size) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (left != 0U) {
|
||||||
|
uint8_t stored[FT24C256_COMPARE_MAX];
|
||||||
|
const uint16_t chunk = (left < FT24C256_COMPARE_MAX)
|
||||||
|
? left
|
||||||
|
: (uint16_t)FT24C256_COMPARE_MAX;
|
||||||
|
uint16_t index;
|
||||||
|
uint8_t differs = 0U;
|
||||||
|
|
||||||
|
if (FT24C256_Read(device, position, stored, chunk) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
for (index = 0U; index < chunk; index++) {
|
||||||
|
if (stored[index] != source[index]) {
|
||||||
|
differs = 1U;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((differs != 0U)
|
||||||
|
&& (FT24C256_Write(device, position, source, chunk) == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
position += chunk;
|
||||||
|
source += chunk;
|
||||||
|
left = (uint16_t)(left - chunk);
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_ReadByte(FT24C256 *device, uint32_t address, uint8_t *value)
|
||||||
|
{
|
||||||
|
if (value == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return FT24C256_Read(device, address, value, 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_WriteByte(FT24C256 *device, uint32_t address, uint8_t value)
|
||||||
|
{
|
||||||
|
return FT24C256_Write(device, address, &value, 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_Fill(FT24C256 *device, uint32_t address, uint8_t value,
|
||||||
|
uint32_t size)
|
||||||
|
{
|
||||||
|
uint8_t page[FT24C256_PAGE_MAX];
|
||||||
|
uint32_t position = address;
|
||||||
|
uint32_t left = size;
|
||||||
|
uint16_t index;
|
||||||
|
|
||||||
|
if (ft24c256_valid(device) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (size == 0U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
if (ft24c256_in_range(device, address, size) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index = 0U; index < device->config.page_size; index++) {
|
||||||
|
page[index] = value;
|
||||||
|
}
|
||||||
|
while (left != 0U) {
|
||||||
|
const uint32_t tail =
|
||||||
|
device->config.page_size - (position % device->config.page_size);
|
||||||
|
const uint32_t chunk = (tail < left) ? tail : left;
|
||||||
|
|
||||||
|
if (ft24c256_write_page(device, position, page, (uint16_t)chunk) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
position += chunk;
|
||||||
|
left -= chunk;
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_Erase(FT24C256 *device)
|
||||||
|
{
|
||||||
|
if (ft24c256_valid(device) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return FT24C256_Fill(device, 0U, 0xFFU, device->config.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_Protect(FT24C256 *device, uint8_t enabled)
|
||||||
|
{
|
||||||
|
if (device == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (device->io.set_write_protect == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
device->io.set_write_protect(device->io.context, (uint8_t)(enabled != 0U));
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t FT24C256_Size(const FT24C256 *device)
|
||||||
|
{
|
||||||
|
if ((device == 0) || (device->ready == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return device->config.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FT24C256_AddressOf(uint8_t index)
|
||||||
|
{
|
||||||
|
return (uint8_t)(FT24C256_BASE_ADDRESS + (index & 0x7U));
|
||||||
|
}
|
||||||
277
c/eeprom-ft24c256/ft24c256.h
Normal file
277
c/eeprom-ft24c256/ft24c256.h
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/**
|
||||||
|
* @file ft24c256.h
|
||||||
|
* @brief Портируемый драйвер последовательной EEPROM FT24C256 и её семейства.
|
||||||
|
*
|
||||||
|
* Библиотека не привязана к микроконтроллеру: она не включает заголовки
|
||||||
|
* периферии, не обращается к регистрам и не пользуется прерываниями. Обмен
|
||||||
|
* идёт через таблицу обратных вызовов FT24C256_Io, которую заполняет порт
|
||||||
|
* платы (для этой сборки — @c src/eeprom.c поверх I2C1 STM32F103C8T6).
|
||||||
|
*
|
||||||
|
* FT24C256 — 32768 байт (256 кбит) с двухбайтовым адресом слова и страницей
|
||||||
|
* записи 64 байта. Запись выполняется постранично: микросхема принимает до
|
||||||
|
* 64 байт в одной транзакции, но только внутри страницы — при переходе через
|
||||||
|
* её границу счётчик адреса заворачивается на начало той же страницы и
|
||||||
|
* затирает уже принятые байты. Поэтому FT24C256_Write() сама режет запрос по
|
||||||
|
* границам страниц:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* запись 100 байт с адреса 0x0030
|
||||||
|
*
|
||||||
|
* 0x0000 0x0040 0x0080 0x00C0
|
||||||
|
* | страница 0 | страница 1 | страница 2 |
|
||||||
|
* +---------------+---------------+---------------+
|
||||||
|
* [ 16 ][ 64 ][ 20 ]
|
||||||
|
* 1-я 2-я 3-я транзакция
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* После каждой транзакции кристалл уходит во внутренний цикл записи (до 5 мс)
|
||||||
|
* и не отвечает на шину. Готовность определяется опросом подтверждения
|
||||||
|
* (ACK polling): библиотека шлёт адресный байт без данных, пока микросхема не
|
||||||
|
* ответит. Порт, который не умеет передавать транзакцию нулевой длины,
|
||||||
|
* выставляет @c poll_retries в ноль — тогда используется выдержка
|
||||||
|
* @c write_ms.
|
||||||
|
*
|
||||||
|
* Кристалл выбирается тремя адресными выводами A0...A2, поэтому на одной шине
|
||||||
|
* живут до восьми микросхем с адресами 0x50...0x57. Задавая @c size и
|
||||||
|
* @c page_size, тем же кодом обслуживаются младшие члены семейства с
|
||||||
|
* двухбайтовым адресом: FT24C32 (4 КБ, страница 32), FT24C64 (8 КБ, 32),
|
||||||
|
* FT24C128 (16 КБ, 64), FT24C512 (64 КБ, 128). Микросхемы до FT24C16
|
||||||
|
* включительно адресуют слово одним байтом и этой библиотекой не
|
||||||
|
* поддерживаются.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FT24C256_H
|
||||||
|
#define FT24C256_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/** Объём FT24C256 в байтах (256 кбит). */
|
||||||
|
#define FT24C256_SIZE 32768UL
|
||||||
|
|
||||||
|
/** Размер страницы записи FT24C256 в байтах. */
|
||||||
|
#define FT24C256_PAGE_SIZE 64U
|
||||||
|
|
||||||
|
/** Предельная страница семейства (FT24C512); задаёт размер буфера транзакции. */
|
||||||
|
#define FT24C256_PAGE_MAX 128U
|
||||||
|
|
||||||
|
/** Базовый адрес на шине при A0 = A1 = A2 = 0. */
|
||||||
|
#define FT24C256_BASE_ADDRESS 0x50U
|
||||||
|
|
||||||
|
/** Число микросхем семейства на одной шине. */
|
||||||
|
#define FT24C256_MAX_DEVICES 8U
|
||||||
|
|
||||||
|
/** Длительность внутреннего цикла записи по даташиту, мс. */
|
||||||
|
#define FT24C256_WRITE_MS 5U
|
||||||
|
|
||||||
|
/** Число опросов подтверждения после записи по умолчанию. */
|
||||||
|
#define FT24C256_POLL_RETRIES 20U
|
||||||
|
|
||||||
|
/** Длина адреса слова в байтах: FT24C32 и старше адресуют двумя байтами. */
|
||||||
|
#define FT24C256_ADDRESS_SIZE 2U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Доступ библиотеки к шине I2C.
|
||||||
|
*
|
||||||
|
* Обязательны @c write и @c write_read. Адрес в вызовах — семибитный, без
|
||||||
|
* бита направления: сдвиг и добавление бита выполняет порт.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/**
|
||||||
|
* Передаёт транзакцию «старт, адрес, данные, стоп».
|
||||||
|
* Вызов с @c size, равным нулю, передаёт один адресный байт и применяется
|
||||||
|
* для опроса готовности; 1 — микросхема ответила подтверждением.
|
||||||
|
*/
|
||||||
|
uint8_t (*write)(void *context, uint8_t address, const uint8_t *data,
|
||||||
|
uint16_t size);
|
||||||
|
/**
|
||||||
|
* Передаёт @c tx_size байт, затем без освобождения шины (повторный старт)
|
||||||
|
* читает @c rx_size байт; 1 — обмен завершён подтверждениями.
|
||||||
|
*/
|
||||||
|
uint8_t (*write_read)(void *context, uint8_t address, const uint8_t *tx,
|
||||||
|
uint16_t tx_size, uint8_t *rx, uint16_t rx_size);
|
||||||
|
/** Выдержка в миллисекундах; обязателен, если @c poll_retries равен нулю. */
|
||||||
|
void (*delay_ms)(void *context, uint32_t milliseconds);
|
||||||
|
/** Управление выводом WP: 1 — запись запрещена. Допускает значение 0. */
|
||||||
|
void (*set_write_protect)(void *context, uint8_t enabled);
|
||||||
|
void *context; /**< Контекст порта, передаётся вызовам без изменений. */
|
||||||
|
} FT24C256_Io;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Параметры кристалла.
|
||||||
|
*
|
||||||
|
* Нулевые поля заменяются значениями по умолчанию вызовом
|
||||||
|
* FT24C256_ConfigDefault() или самой FT24C256_Init().
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint32_t size; /**< Объём в байтах; 0 — @ref FT24C256_SIZE. */
|
||||||
|
uint16_t page_size; /**< Страница записи; 0 — @ref FT24C256_PAGE_SIZE. */
|
||||||
|
uint8_t address; /**< Семибитный адрес; 0 — @ref FT24C256_BASE_ADDRESS. */
|
||||||
|
uint8_t write_ms; /**< Выдержка цикла записи; 0 — @ref FT24C256_WRITE_MS. */
|
||||||
|
uint8_t poll_retries; /**< Опросов готовности; 0 — только выдержка. */
|
||||||
|
} FT24C256_Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние кристалла: настройки, обратные вызовы и счётчики обмена.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
FT24C256_Io io; /**< Обратные вызовы порта. */
|
||||||
|
FT24C256_Config config; /**< Параметры кристалла. */
|
||||||
|
uint8_t ready; /**< 1, если FT24C256_Init() прошла успешно. */
|
||||||
|
uint32_t written_bytes; /**< Байтов, отправленных на запись. */
|
||||||
|
uint32_t read_bytes; /**< Байтов, полученных чтением. */
|
||||||
|
uint32_t write_errors; /**< Транзакций записи, оставшихся без ответа. */
|
||||||
|
uint32_t read_errors; /**< Транзакций чтения, оставшихся без ответа. */
|
||||||
|
} FT24C256;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Заполняет параметры значениями FT24C256 с адресными выводами на GND.
|
||||||
|
*
|
||||||
|
* @param config Параметры, принадлежащие вызывающему коду.
|
||||||
|
*/
|
||||||
|
void FT24C256_ConfigDefault(FT24C256_Config *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Готовит кристалл к работе.
|
||||||
|
*
|
||||||
|
* Обмена с шиной не выполняет: наличие микросхемы проверяется отдельно
|
||||||
|
* вызовом FT24C256_IsPresent().
|
||||||
|
*
|
||||||
|
* @param device Состояние, принадлежащее вызывающему коду.
|
||||||
|
* @param io Обратные вызовы порта; копируются внутрь состояния.
|
||||||
|
* @param config Параметры либо 0 для значений по умолчанию.
|
||||||
|
* @return 1 при успешной настройке, 0 при неполных аргументах.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_Init(FT24C256 *device, const FT24C256_Io *io,
|
||||||
|
const FT24C256_Config *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Проверяет отклик микросхемы на её адрес.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @return 1, если получено подтверждение, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_IsPresent(FT24C256 *device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ожидает завершения внутреннего цикла записи.
|
||||||
|
*
|
||||||
|
* При ненулевом @c poll_retries шлёт адресные байты, пока микросхема не
|
||||||
|
* ответит; иначе выдерживает паузу @c write_ms.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @return 1, если микросхема готова, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_WaitReady(FT24C256 *device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Читает произвольное число байтов подряд.
|
||||||
|
*
|
||||||
|
* Чтение границами страниц не ограничено: счётчик адреса проходит весь
|
||||||
|
* массив, поэтому запрос выполняется одной транзакцией.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param address Адрес первого байта.
|
||||||
|
* @param buffer Приёмник данных.
|
||||||
|
* @param size Число байтов.
|
||||||
|
* @return 1 при успехе; 0 при выходе за границы массива или отказе шины.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_Read(FT24C256 *device, uint32_t address, void *buffer,
|
||||||
|
uint16_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Записывает произвольное число байтов, разрезая запрос по страницам.
|
||||||
|
*
|
||||||
|
* После каждой страницы выполняется ожидание готовности, поэтому вызов
|
||||||
|
* блокирующий: запись всего массива занимает около 2.6 с.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param address Адрес первого байта.
|
||||||
|
* @param data Записываемые данные.
|
||||||
|
* @param size Число байтов.
|
||||||
|
* @return 1 при успехе; 0 при выходе за границы массива или отказе шины.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_Write(FT24C256 *device, uint32_t address, const void *data,
|
||||||
|
uint16_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Записывает только те страницы, содержимое которых отличается.
|
||||||
|
*
|
||||||
|
* Ресурс кристалла — миллион циклов записи на страницу, поэтому сохранение
|
||||||
|
* настроек, которые меняются редко, выгоднее вести этой функцией: неизменные
|
||||||
|
* страницы не переписываются.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param address Адрес первого байта.
|
||||||
|
* @param data Записываемые данные.
|
||||||
|
* @param size Число байтов.
|
||||||
|
* @return 1 при успехе, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_Update(FT24C256 *device, uint32_t address, const void *data,
|
||||||
|
uint16_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Читает один байт.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param address Адрес байта.
|
||||||
|
* @param value Приёмник значения.
|
||||||
|
* @return 1 при успехе, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_ReadByte(FT24C256 *device, uint32_t address, uint8_t *value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Записывает один байт.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param address Адрес байта.
|
||||||
|
* @param value Записываемое значение.
|
||||||
|
* @return 1 при успехе, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_WriteByte(FT24C256 *device, uint32_t address, uint8_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Заполняет область одинаковым значением.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param address Адрес первого байта.
|
||||||
|
* @param value Записываемое значение.
|
||||||
|
* @param size Число байтов.
|
||||||
|
* @return 1 при успехе, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_Fill(FT24C256 *device, uint32_t address, uint8_t value,
|
||||||
|
uint32_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Заполняет весь массив значением 0xFF.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @return 1 при успехе, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_Erase(FT24C256 *device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Включает или снимает аппаратную защиту записи выводом WP.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @param enabled 1 — запись запрещена, 0 — разрешена.
|
||||||
|
* @return 1, если порт управляет выводом WP, иначе 0.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_Protect(FT24C256 *device, uint8_t enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает объём кристалла.
|
||||||
|
*
|
||||||
|
* @param device Состояние кристалла.
|
||||||
|
* @return Число байтов; 0 для ненастроенного состояния.
|
||||||
|
*/
|
||||||
|
uint32_t FT24C256_Size(const FT24C256 *device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает адрес кристалла на шине для указанных выводов A0...A2.
|
||||||
|
*
|
||||||
|
* @param index Значение адресных выводов, 0...7.
|
||||||
|
* @return Семибитный адрес 0x50...0x57.
|
||||||
|
*/
|
||||||
|
uint8_t FT24C256_AddressOf(uint8_t index);
|
||||||
|
|
||||||
|
#endif /* FT24C256_H */
|
||||||
59
c/keypad/README.md
Normal file
59
c/keypad/README.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# keypad
|
||||||
|
|
||||||
|
Опрос шести кнопок навигации с антидребезгом, автоповтором и удержанием.
|
||||||
|
|
||||||
|
Библиотека не знает ни о портах, ни о таймерах: уровни кнопок приходят через
|
||||||
|
обратный вызов порта, время — параметром `Keypad_Poll()`. События складываются
|
||||||
|
в короткую очередь, поэтому быстрое нажатие не теряется, пока приложение
|
||||||
|
перерисовывает экран.
|
||||||
|
|
||||||
|
```
|
||||||
|
порт платы keypad.c приложение
|
||||||
|
read(key) ──────► антидребезг, автоповтор ──► Keypad_GetEvent()
|
||||||
|
удержание, очередь на 8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Файл | Что делает | Зависимости |
|
||||||
|
|---|---|---|
|
||||||
|
| `keypad.h`, `keypad.c` | шесть кнопок, антидребезг, автоповтор, длинное нажатие, очередь событий | `stdint.h` |
|
||||||
|
|
||||||
|
Кнопок ровно шесть: четыре направления, ввод и возврат — набор покрывает
|
||||||
|
навигацию по меню без матрицы и сдвиговых регистров.
|
||||||
|
|
||||||
|
## Что нужно от платформы
|
||||||
|
|
||||||
|
Один вызов и монотонное время в миллисекундах:
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t read(void *ctx, uint8_t key); /* 1 — кнопка нажата */
|
||||||
|
```
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```c
|
||||||
|
Keypad keypad;
|
||||||
|
Keypad_Config config = { .read = board_key_read, .context = &board };
|
||||||
|
/* нулевые поля заменяются умолчаниями: дребезг 20 мс, автоповтор 400/120 мс,
|
||||||
|
удержание 800 мс, автоповтор только для стрелок */
|
||||||
|
|
||||||
|
Keypad_Init(&keypad, &config);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
Keypad_Poll(&keypad, board_millis());
|
||||||
|
|
||||||
|
Keypad_Event event;
|
||||||
|
while (Keypad_GetEvent(&keypad, &event)) {
|
||||||
|
if (event.type == KEYPAD_EVENT_PRESS) { /* ... */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Опрашивать `Keypad_Poll()` достаточно раз в 1..5 мс — из основного цикла
|
||||||
|
или из системного тика.
|
||||||
|
|
||||||
|
## Проверено в проектах
|
||||||
|
|
||||||
|
`KONOR_ds18b20`, `OpticalTester`. Естественная пара — [`menu`](../menu):
|
||||||
|
`Keypad_Key` отображается в `Menu_Key` один в один.
|
||||||
173
c/keypad/keypad.c
Normal file
173
c/keypad/keypad.c
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* @file keypad.c
|
||||||
|
* @brief Антидребезг, автоповтор и очередь событий шести кнопок.
|
||||||
|
*
|
||||||
|
* Опрос программный: уровни читаются обратным вызовом порта, состояние
|
||||||
|
* подтверждается выдержкой debounce_ms, а автоповтор и удержание отсчитываются
|
||||||
|
* от момента подтверждённого нажатия. Прерывания не используются, поэтому
|
||||||
|
* библиотека одинаково работает с выводами МК, расширителями и матрицами.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "keypad.h"
|
||||||
|
|
||||||
|
/** Значения по умолчанию для незаданных параметров. */
|
||||||
|
#define KEYPAD_DEFAULT_DEBOUNCE_MS 20U
|
||||||
|
#define KEYPAD_DEFAULT_REPEAT_DELAY_MS 400U
|
||||||
|
#define KEYPAD_DEFAULT_REPEAT_PERIOD_MS 120U
|
||||||
|
#define KEYPAD_DEFAULT_LONG_PRESS_MS 800U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Кладёт событие в очередь, отбрасывая его при переполнении.
|
||||||
|
*
|
||||||
|
* @param keypad Состояние клавиатуры.
|
||||||
|
* @param key Кнопка.
|
||||||
|
* @param type Вид события.
|
||||||
|
*/
|
||||||
|
static void keypad_push(Keypad *keypad, uint8_t key, Keypad_EventType type)
|
||||||
|
{
|
||||||
|
uint8_t tail;
|
||||||
|
|
||||||
|
if (keypad->count >= (uint8_t)KEYPAD_QUEUE_SIZE) {
|
||||||
|
keypad->overflow = 1U;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tail = (uint8_t)((keypad->head + keypad->count) % (uint8_t)KEYPAD_QUEUE_SIZE);
|
||||||
|
keypad->queue[tail].key = (Keypad_Key)key;
|
||||||
|
keypad->queue[tail].type = type;
|
||||||
|
keypad->count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Keypad_Init(Keypad *keypad, const Keypad_Config *config)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
if ((keypad == 0) || (config == 0) || (config->read == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
keypad->config = *config;
|
||||||
|
if (keypad->config.debounce_ms == 0U) {
|
||||||
|
keypad->config.debounce_ms = KEYPAD_DEFAULT_DEBOUNCE_MS;
|
||||||
|
}
|
||||||
|
if (keypad->config.repeat_delay_ms == 0U) {
|
||||||
|
keypad->config.repeat_delay_ms = KEYPAD_DEFAULT_REPEAT_DELAY_MS;
|
||||||
|
}
|
||||||
|
if (keypad->config.repeat_period_ms == 0U) {
|
||||||
|
keypad->config.repeat_period_ms = KEYPAD_DEFAULT_REPEAT_PERIOD_MS;
|
||||||
|
}
|
||||||
|
if (keypad->config.long_press_ms == 0U) {
|
||||||
|
keypad->config.long_press_ms = KEYPAD_DEFAULT_LONG_PRESS_MS;
|
||||||
|
}
|
||||||
|
if (keypad->config.repeat_mask == 0U) {
|
||||||
|
keypad->config.repeat_mask = KEYPAD_MASK_ARROWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index = 0U; index < (uint8_t)KEYPAD_KEY_COUNT; index++) {
|
||||||
|
keypad->keys[index].raw = 0U;
|
||||||
|
keypad->keys[index].stable = 0U;
|
||||||
|
keypad->keys[index].long_sent = 0U;
|
||||||
|
keypad->keys[index].changed_ms = 0U;
|
||||||
|
keypad->keys[index].pressed_ms = 0U;
|
||||||
|
keypad->keys[index].repeated_ms = 0U;
|
||||||
|
}
|
||||||
|
keypad->head = 0U;
|
||||||
|
keypad->count = 0U;
|
||||||
|
keypad->overflow = 0U;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Keypad_Poll(Keypad *keypad, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
if ((keypad == 0) || (keypad->config.read == 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index = 0U; index < (uint8_t)KEYPAD_KEY_COUNT; index++) {
|
||||||
|
Keypad_KeyState *state = &keypad->keys[index];
|
||||||
|
const uint8_t level = (uint8_t)((keypad->config.read(keypad->config.context, index) != 0U)
|
||||||
|
? 1U : 0U);
|
||||||
|
|
||||||
|
if (level != state->raw) {
|
||||||
|
/* Уровень изменился: отсчёт выдержки начинается заново. */
|
||||||
|
state->raw = level;
|
||||||
|
state->changed_ms = now_ms;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (level == state->stable) {
|
||||||
|
/* Подтверждённое нажатие: автоповтор и порог удержания. */
|
||||||
|
if (state->stable == 0U) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((state->long_sent == 0U)
|
||||||
|
&& ((now_ms - state->pressed_ms) >= keypad->config.long_press_ms)) {
|
||||||
|
state->long_sent = 1U;
|
||||||
|
keypad_push(keypad, index, KEYPAD_EVENT_LONG);
|
||||||
|
}
|
||||||
|
if ((((keypad->config.repeat_mask >> index) & 1U) != 0U)
|
||||||
|
&& ((now_ms - state->pressed_ms) >= keypad->config.repeat_delay_ms)
|
||||||
|
&& ((now_ms - state->repeated_ms) >= keypad->config.repeat_period_ms)) {
|
||||||
|
state->repeated_ms = now_ms;
|
||||||
|
keypad_push(keypad, index, KEYPAD_EVENT_REPEAT);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((now_ms - state->changed_ms) < keypad->config.debounce_ms) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->stable = level;
|
||||||
|
if (level != 0U) {
|
||||||
|
state->pressed_ms = now_ms;
|
||||||
|
state->repeated_ms = now_ms;
|
||||||
|
state->long_sent = 0U;
|
||||||
|
keypad_push(keypad, index, KEYPAD_EVENT_PRESS);
|
||||||
|
} else {
|
||||||
|
keypad_push(keypad, index, KEYPAD_EVENT_RELEASE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Keypad_GetEvent(Keypad *keypad, Keypad_Event *event)
|
||||||
|
{
|
||||||
|
if ((keypad == 0) || (event == 0) || (keypad->count == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
*event = keypad->queue[keypad->head];
|
||||||
|
keypad->head = (uint8_t)((keypad->head + 1U) % (uint8_t)KEYPAD_QUEUE_SIZE);
|
||||||
|
keypad->count--;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Keypad_IsDown(const Keypad *keypad, Keypad_Key key)
|
||||||
|
{
|
||||||
|
if ((keypad == 0) || (key >= KEYPAD_KEY_COUNT)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return keypad->keys[key].stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Keypad_Overflow(Keypad *keypad)
|
||||||
|
{
|
||||||
|
uint8_t flag;
|
||||||
|
|
||||||
|
if (keypad == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
flag = keypad->overflow;
|
||||||
|
keypad->overflow = 0U;
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *Keypad_KeyName(Keypad_Key key)
|
||||||
|
{
|
||||||
|
static const char *const names[KEYPAD_KEY_COUNT] = {
|
||||||
|
"UP", "DOWN", "LEFT", "RIGHT", "ENTER", "BACK"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (key >= KEYPAD_KEY_COUNT) {
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
return names[key];
|
||||||
|
}
|
||||||
145
c/keypad/keypad.h
Normal file
145
c/keypad/keypad.h
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* @file keypad.h
|
||||||
|
* @brief Портируемый опрос шести кнопок навигации с антидребезгом.
|
||||||
|
*
|
||||||
|
* Библиотека не знает ни о портах, ни о таймерах: уровни кнопок она получает
|
||||||
|
* через обратный вызов порта, а время — параметром Keypad_Poll(). События
|
||||||
|
* складываются в короткую очередь, поэтому быстрое нажатие не теряется, даже
|
||||||
|
* если приложение перерисовывает экран.
|
||||||
|
*
|
||||||
|
* Кнопок ровно шесть: четыре направления, ввод и возврат. Такой набор
|
||||||
|
* покрывает навигацию по меню без матрицы и сдвиговых регистров.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KEYPAD_H
|
||||||
|
#define KEYPAD_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/** @brief Кнопки клавиатуры; порядок задаёт индексы для обратного вызова. */
|
||||||
|
typedef enum {
|
||||||
|
KEYPAD_KEY_UP = 0, /**< Вверх: предыдущий пункт меню. */
|
||||||
|
KEYPAD_KEY_DOWN, /**< Вниз: следующий пункт меню. */
|
||||||
|
KEYPAD_KEY_LEFT, /**< Влево: уменьшение значения. */
|
||||||
|
KEYPAD_KEY_RIGHT, /**< Вправо: увеличение значения. */
|
||||||
|
KEYPAD_KEY_ENTER, /**< Ввод: вход в пункт или подтверждение. */
|
||||||
|
KEYPAD_KEY_BACK, /**< Назад: возврат на предыдущий экран. */
|
||||||
|
KEYPAD_KEY_COUNT /**< Число кнопок; не является кнопкой. */
|
||||||
|
} Keypad_Key;
|
||||||
|
|
||||||
|
/** @brief Вид события клавиатуры. */
|
||||||
|
typedef enum {
|
||||||
|
KEYPAD_EVENT_PRESS = 0, /**< Нажатие после подтверждения антидребезгом. */
|
||||||
|
KEYPAD_EVENT_REPEAT, /**< Автоповтор при удержании. */
|
||||||
|
KEYPAD_EVENT_LONG, /**< Удержание дольше порога, выдаётся один раз. */
|
||||||
|
KEYPAD_EVENT_RELEASE /**< Отпускание кнопки. */
|
||||||
|
} Keypad_EventType;
|
||||||
|
|
||||||
|
/** @brief Событие очереди клавиатуры. */
|
||||||
|
typedef struct {
|
||||||
|
Keypad_Key key; /**< Кнопка, вызвавшая событие. */
|
||||||
|
Keypad_EventType type; /**< Вид события. */
|
||||||
|
} Keypad_Event;
|
||||||
|
|
||||||
|
/** Глубина очереди событий; хватает на любой темп нажатий вручную. */
|
||||||
|
#define KEYPAD_QUEUE_SIZE 8U
|
||||||
|
|
||||||
|
/** Маска кнопок направлений для поля repeat_mask. */
|
||||||
|
#define KEYPAD_MASK_ARROWS ((uint8_t)((1U << KEYPAD_KEY_UP) | (1U << KEYPAD_KEY_DOWN) \
|
||||||
|
| (1U << KEYPAD_KEY_LEFT) | (1U << KEYPAD_KEY_RIGHT)))
|
||||||
|
|
||||||
|
/** Маска всех кнопок клавиатуры. */
|
||||||
|
#define KEYPAD_MASK_ALL ((uint8_t)((1U << KEYPAD_KEY_COUNT) - 1U))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Параметры клавиатуры; нулевые поля заменяются значениями по умолчанию.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/** Читает состояние кнопки: 1 — нажата. Индекс — значение Keypad_Key. */
|
||||||
|
uint8_t (*read)(void *context, uint8_t key);
|
||||||
|
void *context; /**< Указатель платы для обратного вызова. */
|
||||||
|
uint16_t debounce_ms; /**< Время подтверждения уровня, по умолчанию 20. */
|
||||||
|
uint16_t repeat_delay_ms; /**< Пауза до автоповтора, по умолчанию 400. */
|
||||||
|
uint16_t repeat_period_ms;/**< Период автоповтора, по умолчанию 120. */
|
||||||
|
uint16_t long_press_ms; /**< Порог удержания, по умолчанию 800. */
|
||||||
|
uint8_t repeat_mask; /**< Кнопки с автоповтором, по умолчанию стрелки. */
|
||||||
|
} Keypad_Config;
|
||||||
|
|
||||||
|
/** @brief Состояние одной кнопки. */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t raw; /**< Последний прочитанный уровень. */
|
||||||
|
uint8_t stable; /**< Подтверждённое состояние: 1 — нажата. */
|
||||||
|
uint8_t long_sent; /**< Признак выданного события удержания. */
|
||||||
|
uint32_t changed_ms; /**< Момент последнего изменения сырого уровня. */
|
||||||
|
uint32_t pressed_ms; /**< Момент подтверждённого нажатия. */
|
||||||
|
uint32_t repeated_ms; /**< Момент последнего события автоповтора. */
|
||||||
|
} Keypad_KeyState;
|
||||||
|
|
||||||
|
/** @brief Состояние клавиатуры; принадлежит вызывающему коду. */
|
||||||
|
typedef struct {
|
||||||
|
Keypad_Config config; /**< Копия параметров. */
|
||||||
|
Keypad_KeyState keys[KEYPAD_KEY_COUNT]; /**< Состояния кнопок. */
|
||||||
|
Keypad_Event queue[KEYPAD_QUEUE_SIZE]; /**< Кольцевая очередь событий. */
|
||||||
|
uint8_t head; /**< Индекс чтения очереди. */
|
||||||
|
uint8_t count; /**< Число событий в очереди. */
|
||||||
|
uint8_t overflow; /**< Признак потери событий. */
|
||||||
|
} Keypad;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Готовит клавиатуру к работе и подставляет значения по умолчанию.
|
||||||
|
*
|
||||||
|
* Состояние всех кнопок считается отпущенным, поэтому зажатая при включении
|
||||||
|
* кнопка даёт обычное событие нажатия после выдержки антидребезга.
|
||||||
|
*
|
||||||
|
* @param keypad Состояние клавиатуры.
|
||||||
|
* @param config Параметры; обязателен только обратный вызов чтения.
|
||||||
|
* @return 1 при успешной настройке, 0 если обратный вызов не задан.
|
||||||
|
*/
|
||||||
|
uint8_t Keypad_Init(Keypad *keypad, const Keypad_Config *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Опрашивает кнопки и наполняет очередь событий.
|
||||||
|
*
|
||||||
|
* Вызывается из главного цикла с любым темпом: шаг антидребезга задаётся не
|
||||||
|
* периодом вызова, а метками времени.
|
||||||
|
*
|
||||||
|
* @param keypad Состояние клавиатуры.
|
||||||
|
* @param now_ms Текущее монотонное время в миллисекундах.
|
||||||
|
*/
|
||||||
|
void Keypad_Poll(Keypad *keypad, uint32_t now_ms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Забирает следующее событие из очереди.
|
||||||
|
*
|
||||||
|
* @param keypad Состояние клавиатуры.
|
||||||
|
* @param event Приёмник события.
|
||||||
|
* @return 1, если событие получено, 0 если очередь пуста.
|
||||||
|
*/
|
||||||
|
uint8_t Keypad_GetEvent(Keypad *keypad, Keypad_Event *event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сообщает подтверждённое состояние кнопки.
|
||||||
|
*
|
||||||
|
* @param keypad Состояние клавиатуры.
|
||||||
|
* @param key Опрашиваемая кнопка.
|
||||||
|
* @return 1, если кнопка нажата.
|
||||||
|
*/
|
||||||
|
uint8_t Keypad_IsDown(const Keypad *keypad, Keypad_Key key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает и сбрасывает признак переполнения очереди.
|
||||||
|
*
|
||||||
|
* @param keypad Состояние клавиатуры.
|
||||||
|
* @return 1, если события терялись с прошлого вызова.
|
||||||
|
*/
|
||||||
|
uint8_t Keypad_Overflow(Keypad *keypad);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Даёт короткое имя кнопки для отладочных сообщений и экранов.
|
||||||
|
*
|
||||||
|
* @param key Кнопка.
|
||||||
|
* @return Строка вида "UP"; для неизвестного кода — "?".
|
||||||
|
*/
|
||||||
|
const char *Keypad_KeyName(Keypad_Key key);
|
||||||
|
|
||||||
|
#endif /* KEYPAD_H */
|
||||||
71
c/menu/README.md
Normal file
71
c/menu/README.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# menu
|
||||||
|
|
||||||
|
Экранное меню для дисплея и шести кнопок навигации.
|
||||||
|
|
||||||
|
Движок хранит стек открытых экранов, курсор и окно прокрутки, а содержимое
|
||||||
|
запрашивает у приложения обратными вызовами: так один и тот же экран описывает
|
||||||
|
и статический список, и перечень датчиков, число которых меняется на ходу.
|
||||||
|
|
||||||
|
Рисует движок через таблицу `Menu_Painter`, поэтому от драйвера дисплея он не
|
||||||
|
зависит: для ST7789V адаптер занимает несколько строк, для знакосинтезирующего
|
||||||
|
ЖКИ или SSD1306 — столько же.
|
||||||
|
|
||||||
|
```
|
||||||
|
приложение
|
||||||
|
count/text/enter/adjust ← чем наполнен экран
|
||||||
|
│
|
||||||
|
menu.c стек экранов, курсор, прокрутка, перерисовка
|
||||||
|
│
|
||||||
|
Menu_Painter fill_rect, draw_text
|
||||||
|
│
|
||||||
|
драйвер дисплея
|
||||||
|
```
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Файл | Что делает | Зависимости |
|
||||||
|
|---|---|---|
|
||||||
|
| `menu.h`, `menu.c` | стек экранов до 4 уровней, курсор, прокрутка, тема оформления, строка состояния | `stdint.h` |
|
||||||
|
|
||||||
|
Вспомогательное: `Menu_TextCopy()` и `Menu_TextInt()` — форматирование строк
|
||||||
|
пунктов без `snprintf`. `Menu_GetSnapshot()` отдаёт состояние без отрисовки,
|
||||||
|
что удобно для хостовых тестов.
|
||||||
|
|
||||||
|
## Что нужно от платформы
|
||||||
|
|
||||||
|
Только два вызова отрисовки — цвет передаётся как есть, поэтому в него
|
||||||
|
укладывается и RGB565, и палитра монохромного индикатора:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void fill_rect(void *ctx, int16_t x, int16_t y, uint16_t w, uint16_t h, uint32_t color);
|
||||||
|
void draw_text(void *ctx, int16_t x, int16_t y, const char *text,
|
||||||
|
uint32_t color, uint32_t background, uint8_t scale);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```c
|
||||||
|
static const Menu_Screen screen_root = {
|
||||||
|
.title = "Главное меню",
|
||||||
|
.count = root_count, /* сколько пунктов сейчас */
|
||||||
|
.text = root_text, /* текст пункта по индексу */
|
||||||
|
.enter = root_enter, /* вернуть вложенный экран или 0 */
|
||||||
|
.context = &app,
|
||||||
|
};
|
||||||
|
|
||||||
|
Menu menu;
|
||||||
|
Menu_Theme theme;
|
||||||
|
Menu_ThemeDefault(&theme);
|
||||||
|
Menu_Init(&menu, &painter, &theme, &screen_root);
|
||||||
|
|
||||||
|
Menu_HandleKey(&menu, MENU_KEY_DOWN);
|
||||||
|
Menu_Render(&menu, 0); /* перерисует только если что-то изменилось */
|
||||||
|
```
|
||||||
|
|
||||||
|
Данные, меняющиеся сами по себе (температура, состояние связи), обновляются
|
||||||
|
вызовом `Menu_Invalidate()` — движок перерисует экран на ближайшем `Menu_Render()`.
|
||||||
|
|
||||||
|
## Проверено в проектах
|
||||||
|
|
||||||
|
`KONOR_ds18b20`, `OpticalTester` — поверх [`st7789`](../st7789) и [`keypad`](../keypad).
|
||||||
|
Меню от них не зависит: связывает всё адаптер в приложении.
|
||||||
739
c/menu/menu.c
Normal file
739
c/menu/menu.c
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
/**
|
||||||
|
* @file menu.c
|
||||||
|
* @brief Навигация по экранам меню и их отрисовка через Menu_Painter.
|
||||||
|
*
|
||||||
|
* Экран рисуется целиком: верхняя полоса с заголовком, окно пунктов и нижняя
|
||||||
|
* строка состояния. Перерисовка выполняется только по признаку dirty, поэтому
|
||||||
|
* обмен с дисплеем не занимает главный цикл между нажатиями.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "menu.h"
|
||||||
|
|
||||||
|
/** @brief Тёмная тема в формате RGB565. */
|
||||||
|
#define MENU_COLOR_BACKGROUND 0x0000U /**< Чёрный фон. */
|
||||||
|
#define MENU_COLOR_TITLE_FG 0xFFFFU /**< Белый заголовок. */
|
||||||
|
#define MENU_COLOR_TITLE_BG 0x001FU /**< Синяя полоса заголовка. */
|
||||||
|
#define MENU_COLOR_ITEM_FG 0xC618U /**< Светло-серый текст пункта. */
|
||||||
|
#define MENU_COLOR_VALUE_FG 0x07FFU /**< Голубое значение. */
|
||||||
|
#define MENU_COLOR_CURSOR_FG 0x0000U /**< Чёрный текст выбранного пункта. */
|
||||||
|
#define MENU_COLOR_CURSOR_BG 0xFFE0U /**< Жёлтая подсветка выбора. */
|
||||||
|
#define MENU_COLOR_STATUS_FG 0x8410U /**< Серая строка состояния. */
|
||||||
|
#define MENU_COLOR_SCROLL_FG 0xFFE0U /**< Жёлтые указатели прокрутки. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает число пунктов экрана.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Экран.
|
||||||
|
* @return Число пунктов.
|
||||||
|
*/
|
||||||
|
static uint8_t menu_count(const Menu *menu, const Menu_Screen *screen)
|
||||||
|
{
|
||||||
|
if (screen == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (screen->count != 0) {
|
||||||
|
return screen->count((screen->context != 0) ? screen->context : menu->context);
|
||||||
|
}
|
||||||
|
return screen->item_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает контекст, с которым вызываются обработчики экрана.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Экран.
|
||||||
|
* @return Контекст экрана либо контекст меню.
|
||||||
|
*/
|
||||||
|
static void *menu_context(const Menu *menu, const Menu_Screen *screen)
|
||||||
|
{
|
||||||
|
return (screen->context != 0) ? screen->context : menu->context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Высота одной строки списка вместе с просветом.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Высота строки в пикселях.
|
||||||
|
*/
|
||||||
|
static uint16_t menu_row_height(const Menu *menu)
|
||||||
|
{
|
||||||
|
return (uint16_t)((menu->painter.char_height * menu->theme.item_scale)
|
||||||
|
+ menu->theme.row_gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Высота верхней полосы с заголовком.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Высота полосы в пикселях.
|
||||||
|
*/
|
||||||
|
static uint16_t menu_title_height(const Menu *menu)
|
||||||
|
{
|
||||||
|
return (uint16_t)((menu->painter.char_height * menu->theme.title_scale)
|
||||||
|
+ (menu->theme.padding * 2U));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Высота нижней строки состояния.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Высота строки в пикселях.
|
||||||
|
*/
|
||||||
|
static uint16_t menu_status_height(const Menu *menu)
|
||||||
|
{
|
||||||
|
return (uint16_t)(menu->painter.char_height + menu->theme.padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Пересчитывает число видимых пунктов под текущее оформление.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
static void menu_update_rows(Menu *menu)
|
||||||
|
{
|
||||||
|
const uint16_t reserved = (uint16_t)(menu_title_height(menu) + menu_status_height(menu));
|
||||||
|
const uint16_t row = menu_row_height(menu);
|
||||||
|
uint16_t rows = 0U;
|
||||||
|
|
||||||
|
if ((row != 0U) && (menu->painter.height > reserved)) {
|
||||||
|
rows = (uint16_t)((menu->painter.height - reserved) / row);
|
||||||
|
}
|
||||||
|
if (rows == 0U) {
|
||||||
|
rows = 1U;
|
||||||
|
}
|
||||||
|
if (rows > (uint16_t)MENU_MAX_ROWS) {
|
||||||
|
rows = (uint16_t)MENU_MAX_ROWS;
|
||||||
|
}
|
||||||
|
menu->rows = (uint8_t)rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Приводит курсор и окно прокрутки к допустимым значениям.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
static void menu_clamp(Menu *menu)
|
||||||
|
{
|
||||||
|
const uint8_t level = (uint8_t)(menu->depth - 1U);
|
||||||
|
const uint8_t total = menu_count(menu, menu->stack[level]);
|
||||||
|
|
||||||
|
if (total == 0U) {
|
||||||
|
menu->cursor[level] = 0U;
|
||||||
|
menu->first[level] = 0U;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (menu->cursor[level] >= total) {
|
||||||
|
menu->cursor[level] = (uint8_t)(total - 1U);
|
||||||
|
}
|
||||||
|
if (menu->cursor[level] < menu->first[level]) {
|
||||||
|
menu->first[level] = menu->cursor[level];
|
||||||
|
}
|
||||||
|
if (menu->cursor[level] >= (uint8_t)(menu->first[level] + menu->rows)) {
|
||||||
|
menu->first[level] = (uint8_t)(menu->cursor[level] - menu->rows + 1U);
|
||||||
|
}
|
||||||
|
if (total <= menu->rows) {
|
||||||
|
menu->first[level] = 0U;
|
||||||
|
} else if (menu->first[level] > (uint8_t)(total - menu->rows)) {
|
||||||
|
menu->first[level] = (uint8_t)(total - menu->rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_ThemeDefault(Menu_Theme *theme)
|
||||||
|
{
|
||||||
|
if (theme == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
theme->background = MENU_COLOR_BACKGROUND;
|
||||||
|
theme->title_fg = MENU_COLOR_TITLE_FG;
|
||||||
|
theme->title_bg = MENU_COLOR_TITLE_BG;
|
||||||
|
theme->item_fg = MENU_COLOR_ITEM_FG;
|
||||||
|
theme->item_bg = MENU_COLOR_BACKGROUND;
|
||||||
|
theme->value_fg = MENU_COLOR_VALUE_FG;
|
||||||
|
theme->cursor_fg = MENU_COLOR_CURSOR_FG;
|
||||||
|
theme->cursor_bg = MENU_COLOR_CURSOR_BG;
|
||||||
|
theme->status_fg = MENU_COLOR_STATUS_FG;
|
||||||
|
theme->status_bg = MENU_COLOR_BACKGROUND;
|
||||||
|
theme->scroll_fg = MENU_COLOR_SCROLL_FG;
|
||||||
|
theme->title_scale = 2U;
|
||||||
|
theme->item_scale = 2U;
|
||||||
|
theme->padding = 4U;
|
||||||
|
theme->row_gap = 4U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Menu_Init(Menu *menu, const Menu_Painter *painter, const Menu_Theme *theme,
|
||||||
|
const Menu_Screen *root, void *context)
|
||||||
|
{
|
||||||
|
uint8_t level;
|
||||||
|
|
||||||
|
if ((menu == 0) || (painter == 0) || (root == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if ((painter->fill_rect == 0) || (painter->draw_text == 0)
|
||||||
|
|| (painter->char_width == 0U) || (painter->char_height == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (root->label == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu->painter = *painter;
|
||||||
|
if (theme != 0) {
|
||||||
|
menu->theme = *theme;
|
||||||
|
} else {
|
||||||
|
Menu_ThemeDefault(&menu->theme);
|
||||||
|
}
|
||||||
|
if (menu->theme.title_scale == 0U) {
|
||||||
|
menu->theme.title_scale = 1U;
|
||||||
|
}
|
||||||
|
if (menu->theme.item_scale == 0U) {
|
||||||
|
menu->theme.item_scale = 1U;
|
||||||
|
}
|
||||||
|
menu->context = context;
|
||||||
|
for (level = 0U; level < (uint8_t)MENU_MAX_DEPTH; level++) {
|
||||||
|
menu->stack[level] = 0;
|
||||||
|
menu->cursor[level] = 0U;
|
||||||
|
menu->first[level] = 0U;
|
||||||
|
}
|
||||||
|
menu->stack[0] = root;
|
||||||
|
menu->depth = 1U;
|
||||||
|
menu->status[0] = '\0';
|
||||||
|
|
||||||
|
/* Кэш пуст: первая отрисовка обязана пройти по всему экрану. */
|
||||||
|
menu->cache_screen = 0;
|
||||||
|
menu->cache_first = 0U;
|
||||||
|
menu->cache_valid = 0U;
|
||||||
|
menu->cache_title[0] = '\0';
|
||||||
|
menu->cache_status[0] = '\0';
|
||||||
|
for (level = 0U; level < (uint8_t)MENU_MAX_ROWS; level++) {
|
||||||
|
menu->cache_selected[level] = 0U;
|
||||||
|
menu->cache_label[level][0] = '\0';
|
||||||
|
menu->cache_value[level][0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_update_rows(menu);
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Open(Menu *menu, const Menu_Screen *screen)
|
||||||
|
{
|
||||||
|
if ((menu == 0) || (screen == 0) || (screen->label == 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (menu->depth >= (uint8_t)MENU_MAX_DEPTH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu->stack[menu->depth] = screen;
|
||||||
|
menu->cursor[menu->depth] = 0U;
|
||||||
|
menu->first[menu->depth] = 0U;
|
||||||
|
menu->depth++;
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Back(Menu *menu)
|
||||||
|
{
|
||||||
|
if ((menu == 0) || (menu->depth <= 1U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu->depth--;
|
||||||
|
menu->stack[menu->depth] = 0;
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Home(Menu *menu)
|
||||||
|
{
|
||||||
|
if (menu == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (menu->depth > 1U) {
|
||||||
|
menu->depth--;
|
||||||
|
menu->stack[menu->depth] = 0;
|
||||||
|
}
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_HandleKey(Menu *menu, Menu_Key key)
|
||||||
|
{
|
||||||
|
const Menu_Screen *screen;
|
||||||
|
uint8_t level;
|
||||||
|
uint8_t total;
|
||||||
|
|
||||||
|
if ((menu == 0) || (menu->depth == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
level = (uint8_t)(menu->depth - 1U);
|
||||||
|
screen = menu->stack[level];
|
||||||
|
total = menu_count(menu, screen);
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case MENU_KEY_UP:
|
||||||
|
if (total != 0U) {
|
||||||
|
menu->cursor[level] = (uint8_t)((menu->cursor[level] == 0U)
|
||||||
|
? (total - 1U) : (menu->cursor[level] - 1U));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_DOWN:
|
||||||
|
if (total != 0U) {
|
||||||
|
menu->cursor[level] = (uint8_t)((menu->cursor[level] >= (total - 1U))
|
||||||
|
? 0U : (menu->cursor[level] + 1U));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_LEFT:
|
||||||
|
if ((screen->adjust != 0) && (total != 0U)) {
|
||||||
|
screen->adjust(menu_context(menu, screen), menu->cursor[level], -1);
|
||||||
|
} else {
|
||||||
|
Menu_Back(menu);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_RIGHT:
|
||||||
|
if ((screen->adjust != 0) && (total != 0U)) {
|
||||||
|
screen->adjust(menu_context(menu, screen), menu->cursor[level], 1);
|
||||||
|
} else if (screen->enter != 0) {
|
||||||
|
Menu_Open(menu, screen->enter(menu_context(menu, screen), menu->cursor[level]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_ENTER:
|
||||||
|
if ((screen->enter != 0) && (total != 0U)) {
|
||||||
|
Menu_Open(menu, screen->enter(menu_context(menu, screen), menu->cursor[level]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_BACK:
|
||||||
|
default:
|
||||||
|
Menu_Back(menu);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Invalidate(Menu *menu)
|
||||||
|
{
|
||||||
|
if (menu != 0) {
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Menu_IsDirty(const Menu *menu)
|
||||||
|
{
|
||||||
|
return (uint8_t)((menu != 0) ? menu->dirty : 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_SetStatus(Menu *menu, const char *text)
|
||||||
|
{
|
||||||
|
if (menu == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Menu_TextCopy(menu->status, (uint8_t)MENU_TEXT_MAX, text);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Menu_Screen *Menu_Current(const Menu *menu)
|
||||||
|
{
|
||||||
|
if ((menu == 0) || (menu->depth == 0U)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return menu->stack[menu->depth - 1U];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Menu_Cursor(const Menu *menu)
|
||||||
|
{
|
||||||
|
if ((menu == 0) || (menu->depth == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return menu->cursor[menu->depth - 1U];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Menu_GetSnapshot(const Menu *menu, Menu_Snapshot *out)
|
||||||
|
{
|
||||||
|
const Menu_Screen *screen;
|
||||||
|
uint8_t level;
|
||||||
|
uint8_t visible;
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
if ((menu == 0) || (out == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if ((menu->depth == 0U) || (menu->cache_valid == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
level = (uint8_t)(menu->depth - 1U);
|
||||||
|
screen = menu->stack[level];
|
||||||
|
|
||||||
|
out->title = menu->cache_title;
|
||||||
|
out->status = menu->cache_status;
|
||||||
|
out->first = menu->cache_first;
|
||||||
|
out->total = menu_count(menu, screen);
|
||||||
|
out->cursor = menu->cursor[level];
|
||||||
|
out->depth = menu->depth;
|
||||||
|
|
||||||
|
/* Строк в снимке столько же, сколько занято на панели: окно минус хвост. */
|
||||||
|
visible = menu->rows;
|
||||||
|
if (visible > (uint8_t)MENU_MAX_ROWS) {
|
||||||
|
visible = (uint8_t)MENU_MAX_ROWS;
|
||||||
|
}
|
||||||
|
if (out->total > out->first) {
|
||||||
|
const uint8_t rest = (uint8_t)(out->total - out->first);
|
||||||
|
|
||||||
|
out->rows = (rest < visible) ? rest : visible;
|
||||||
|
} else {
|
||||||
|
out->rows = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index = 0U; index < (uint8_t)MENU_MAX_ROWS; index++) {
|
||||||
|
if (index < out->rows) {
|
||||||
|
out->label[index] = menu->cache_label[index];
|
||||||
|
out->value[index] = menu->cache_value[index];
|
||||||
|
out->selected[index] = menu->cache_selected[index];
|
||||||
|
} else {
|
||||||
|
out->label[index] = "";
|
||||||
|
out->value[index] = "";
|
||||||
|
out->selected[index] = 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_TextCopy(char *out, uint8_t size, const char *text)
|
||||||
|
{
|
||||||
|
uint8_t index = 0U;
|
||||||
|
|
||||||
|
if ((out == 0) || (size == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (text != 0) {
|
||||||
|
while ((text[index] != '\0') && (index < (uint8_t)(size - 1U))) {
|
||||||
|
out[index] = text[index];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[index] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_TextInt(char *out, uint8_t size, int32_t value, const char *suffix)
|
||||||
|
{
|
||||||
|
char digits[12];
|
||||||
|
uint8_t count = 0U;
|
||||||
|
uint8_t index = 0U;
|
||||||
|
uint32_t magnitude;
|
||||||
|
|
||||||
|
if ((out == 0) || (size == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
magnitude = (value < 0) ? (uint32_t)(-value) : (uint32_t)value;
|
||||||
|
do {
|
||||||
|
digits[count] = (char)('0' + (magnitude % 10U));
|
||||||
|
magnitude /= 10U;
|
||||||
|
count++;
|
||||||
|
} while ((magnitude != 0U) && (count < sizeof(digits)));
|
||||||
|
|
||||||
|
if ((value < 0) && (index < (uint8_t)(size - 1U))) {
|
||||||
|
out[index] = '-';
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
while ((count != 0U) && (index < (uint8_t)(size - 1U))) {
|
||||||
|
count--;
|
||||||
|
out[index] = digits[count];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (suffix != 0) {
|
||||||
|
uint8_t tail = 0U;
|
||||||
|
|
||||||
|
while ((suffix[tail] != '\0') && (index < (uint8_t)(size - 1U))) {
|
||||||
|
out[index] = suffix[tail];
|
||||||
|
index++;
|
||||||
|
tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[index] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Рисует верхнюю полосу с заголовком открытого экрана.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Открытый экран.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @brief Сравнивает две строки с ограничением по длине буфера кэша.
|
||||||
|
*
|
||||||
|
* @param left Первая строка.
|
||||||
|
* @param right Вторая строка.
|
||||||
|
* @return 1, если строки совпадают, иначе 0.
|
||||||
|
*/
|
||||||
|
static uint8_t menu_text_equal(const char *left, const char *right)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < (uint8_t)MENU_TEXT_MAX; index++) {
|
||||||
|
if (left[index] != right[index]) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (left[index] == '\0') {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Копирует строку в буфер кэша с обрезкой по его размеру.
|
||||||
|
*
|
||||||
|
* @param destination Буфер кэша длиной MENU_TEXT_MAX.
|
||||||
|
* @param source Исходная строка.
|
||||||
|
*/
|
||||||
|
static void menu_text_remember(char *destination, const char *source)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < (uint8_t)(MENU_TEXT_MAX - 1U); index++) {
|
||||||
|
destination[index] = source[index];
|
||||||
|
if (source[index] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destination[MENU_TEXT_MAX - 1U] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void menu_draw_title(Menu *menu, const Menu_Screen *screen, uint8_t force)
|
||||||
|
{
|
||||||
|
const uint16_t height = menu_title_height(menu);
|
||||||
|
const Menu_Painter *painter = &menu->painter;
|
||||||
|
const char *const title = (screen->title != 0) ? screen->title : "";
|
||||||
|
|
||||||
|
/* Заголовок перерисовывается только при смене текста: иначе экран мигает. */
|
||||||
|
if ((force == 0U) && (menu_text_equal(menu->cache_title, title) != 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu_text_remember(menu->cache_title, title);
|
||||||
|
|
||||||
|
painter->fill_rect(painter->context, 0, 0, painter->width, height, menu->theme.title_bg);
|
||||||
|
painter->draw_text(painter->context, (int16_t)menu->theme.padding,
|
||||||
|
(int16_t)menu->theme.padding,
|
||||||
|
(screen->title != 0) ? screen->title : "",
|
||||||
|
menu->theme.title_fg, menu->theme.title_bg, menu->theme.title_scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Рисует нижнюю строку состояния и указатели прокрутки.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param total Число пунктов экрана.
|
||||||
|
* @param first Номер верхнего видимого пункта.
|
||||||
|
*/
|
||||||
|
static void menu_draw_status(Menu *menu, uint8_t total, uint8_t first, uint8_t force)
|
||||||
|
{
|
||||||
|
const Menu_Painter *painter = &menu->painter;
|
||||||
|
const uint16_t height = menu_status_height(menu);
|
||||||
|
const int16_t top = (int16_t)(painter->height - height);
|
||||||
|
char marks[4];
|
||||||
|
char signature[MENU_TEXT_MAX];
|
||||||
|
uint8_t index = 0U;
|
||||||
|
uint8_t length;
|
||||||
|
|
||||||
|
/* Список длиннее окна: показываем, в какую сторону есть скрытые пункты. */
|
||||||
|
if (total > menu->rows) {
|
||||||
|
if (first != 0U) {
|
||||||
|
marks[index] = '^';
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if ((uint8_t)(first + menu->rows) < total) {
|
||||||
|
marks[index] = 'v';
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
marks[index] = '\0';
|
||||||
|
|
||||||
|
/* Подпись объединяет текст и указатели, поэтому сравнение одно на всё. */
|
||||||
|
menu_text_remember(signature, menu->status);
|
||||||
|
length = 0U;
|
||||||
|
while ((length < (uint8_t)(MENU_TEXT_MAX - 1U)) && (signature[length] != '\0')) {
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
if ((uint8_t)(length + index) < (uint8_t)(MENU_TEXT_MAX - 1U)) {
|
||||||
|
uint8_t mark;
|
||||||
|
|
||||||
|
for (mark = 0U; mark < index; mark++) {
|
||||||
|
signature[length] = marks[mark];
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
signature[length] = '\0';
|
||||||
|
}
|
||||||
|
if ((force == 0U) && (menu_text_equal(menu->cache_status, signature) != 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu_text_remember(menu->cache_status, signature);
|
||||||
|
|
||||||
|
painter->fill_rect(painter->context, 0, top, painter->width, height, menu->theme.status_bg);
|
||||||
|
painter->draw_text(painter->context, (int16_t)menu->theme.padding,
|
||||||
|
(int16_t)(top + (int16_t)(menu->theme.padding / 2U)),
|
||||||
|
menu->status, menu->theme.status_fg, menu->theme.status_bg, 1U);
|
||||||
|
|
||||||
|
if (index != 0U) {
|
||||||
|
const int16_t x = (int16_t)(painter->width - menu->theme.padding
|
||||||
|
- (index * painter->char_width));
|
||||||
|
|
||||||
|
painter->draw_text(painter->context, x,
|
||||||
|
(int16_t)(top + (int16_t)(menu->theme.padding / 2U)), marks,
|
||||||
|
menu->theme.scroll_fg, menu->theme.status_bg, 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Рисует одну строку списка: название слева, значение справа.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Открытый экран.
|
||||||
|
* @param item Номер пункта.
|
||||||
|
* @param top Верхняя граница строки.
|
||||||
|
* @param selected 1, если пункт выбран курсором.
|
||||||
|
*/
|
||||||
|
static void menu_draw_row(Menu *menu, const Menu_Screen *screen, uint8_t item,
|
||||||
|
int16_t top, uint8_t selected, uint8_t slot, uint8_t force)
|
||||||
|
{
|
||||||
|
const Menu_Painter *painter = &menu->painter;
|
||||||
|
const uint16_t row = menu_row_height(menu);
|
||||||
|
const uint16_t step = (uint16_t)(painter->char_width * menu->theme.item_scale);
|
||||||
|
const uint32_t foreground = (selected != 0U) ? menu->theme.cursor_fg : menu->theme.item_fg;
|
||||||
|
const uint32_t background = (selected != 0U) ? menu->theme.cursor_bg : menu->theme.item_bg;
|
||||||
|
const uint32_t value_fg = (selected != 0U) ? menu->theme.cursor_fg : menu->theme.value_fg;
|
||||||
|
char label[MENU_TEXT_MAX];
|
||||||
|
char value[MENU_TEXT_MAX];
|
||||||
|
uint8_t label_room;
|
||||||
|
uint8_t value_length = 0U;
|
||||||
|
int16_t x;
|
||||||
|
|
||||||
|
label[0] = '\0';
|
||||||
|
screen->label(menu_context(menu, screen), item, label, (uint8_t)MENU_TEXT_MAX);
|
||||||
|
value[0] = '\0';
|
||||||
|
if (screen->value != 0) {
|
||||||
|
screen->value(menu_context(menu, screen), item, value, (uint8_t)MENU_TEXT_MAX);
|
||||||
|
while (value[value_length] != '\0') {
|
||||||
|
value_length++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Название обрезается так, чтобы значение справа осталось целиком. */
|
||||||
|
if (step != 0U) {
|
||||||
|
const uint16_t columns = (uint16_t)((painter->width - (menu->theme.padding * 2U)) / step);
|
||||||
|
|
||||||
|
label_room = (uint8_t)((columns > (value_length + 1U))
|
||||||
|
? (columns - value_length - 1U) : 1U);
|
||||||
|
if (label_room > (uint8_t)(MENU_TEXT_MAX - 1U)) {
|
||||||
|
label_room = (uint8_t)(MENU_TEXT_MAX - 1U);
|
||||||
|
}
|
||||||
|
label[label_room] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Совпадение с прошлой отрисовкой означает, что строку трогать не нужно. */
|
||||||
|
if ((force == 0U) && (slot < (uint8_t)MENU_MAX_ROWS)
|
||||||
|
&& (menu->cache_selected[slot] == selected)
|
||||||
|
&& (menu_text_equal(menu->cache_label[slot], label) != 0U)
|
||||||
|
&& (menu_text_equal(menu->cache_value[slot], value) != 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (slot < (uint8_t)MENU_MAX_ROWS) {
|
||||||
|
menu->cache_selected[slot] = selected;
|
||||||
|
menu_text_remember(menu->cache_label[slot], label);
|
||||||
|
menu_text_remember(menu->cache_value[slot], value);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter->fill_rect(painter->context, 0, top, painter->width, row, background);
|
||||||
|
painter->draw_text(painter->context, (int16_t)menu->theme.padding,
|
||||||
|
(int16_t)(top + (int16_t)(menu->theme.row_gap / 2U)), label,
|
||||||
|
foreground, background, menu->theme.item_scale);
|
||||||
|
if (value_length != 0U) {
|
||||||
|
x = (int16_t)(painter->width - menu->theme.padding - (value_length * step));
|
||||||
|
painter->draw_text(painter->context, x,
|
||||||
|
(int16_t)(top + (int16_t)(menu->theme.row_gap / 2U)), value,
|
||||||
|
value_fg, background, menu->theme.item_scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Render(Menu *menu, uint8_t force)
|
||||||
|
{
|
||||||
|
const Menu_Screen *screen;
|
||||||
|
const Menu_Painter *painter;
|
||||||
|
uint8_t level;
|
||||||
|
uint8_t total;
|
||||||
|
uint8_t first;
|
||||||
|
uint8_t index;
|
||||||
|
uint8_t full;
|
||||||
|
uint16_t row;
|
||||||
|
int16_t top;
|
||||||
|
int16_t list_bottom;
|
||||||
|
|
||||||
|
if ((menu == 0) || (menu->depth == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((menu->dirty == 0U) && (force == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
painter = &menu->painter;
|
||||||
|
level = (uint8_t)(menu->depth - 1U);
|
||||||
|
screen = menu->stack[level];
|
||||||
|
menu_update_rows(menu);
|
||||||
|
menu_clamp(menu);
|
||||||
|
total = menu_count(menu, screen);
|
||||||
|
first = menu->first[level];
|
||||||
|
row = menu_row_height(menu);
|
||||||
|
top = (int16_t)menu_title_height(menu);
|
||||||
|
list_bottom = (int16_t)(painter->height - menu_status_height(menu));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Полная отрисовка нужна только при смене экрана, прокрутке списка или по
|
||||||
|
* явному запросу. В остальных случаях перерисовываются лишь те строки,
|
||||||
|
* содержимое которых изменилось, поэтому обновление данных не мигает.
|
||||||
|
*/
|
||||||
|
full = (uint8_t)(((force != 0U) || (menu->cache_valid == 0U)
|
||||||
|
|| (menu->cache_screen != screen) || (menu->cache_first != first))
|
||||||
|
? 1U : 0U);
|
||||||
|
|
||||||
|
menu_draw_title(menu, screen, full);
|
||||||
|
for (index = 0U; index < menu->rows; index++) {
|
||||||
|
const uint8_t item = (uint8_t)(first + index);
|
||||||
|
|
||||||
|
if ((top + (int16_t)row) > list_bottom) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (item < total) {
|
||||||
|
menu_draw_row(menu, screen, item, top,
|
||||||
|
(uint8_t)((item == menu->cursor[level]) ? 1U : 0U), index, full);
|
||||||
|
} else if ((full != 0U) || (index >= (uint8_t)MENU_MAX_ROWS)
|
||||||
|
|| (menu->cache_label[index][0] != 0)
|
||||||
|
|| (menu->cache_value[index][0] != 0)) {
|
||||||
|
/* Строка опустела: гасим её и запоминаем пустое содержимое. */
|
||||||
|
painter->fill_rect(painter->context, 0, top, painter->width, row,
|
||||||
|
menu->theme.background);
|
||||||
|
if (index < (uint8_t)MENU_MAX_ROWS) {
|
||||||
|
menu->cache_label[index][0] = 0;
|
||||||
|
menu->cache_value[index][0] = 0;
|
||||||
|
menu->cache_selected[index] = 0U;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Пустая строка уже погашена в прошлый раз. */
|
||||||
|
}
|
||||||
|
top = (int16_t)(top + (int16_t)row);
|
||||||
|
}
|
||||||
|
if ((full != 0U) && (top < list_bottom)) {
|
||||||
|
painter->fill_rect(painter->context, 0, top, painter->width,
|
||||||
|
(uint16_t)(list_bottom - top), menu->theme.background);
|
||||||
|
}
|
||||||
|
menu_draw_status(menu, total, first, full);
|
||||||
|
|
||||||
|
menu->cache_screen = screen;
|
||||||
|
menu->cache_first = first;
|
||||||
|
menu->cache_valid = 1U;
|
||||||
|
menu->dirty = 0U;
|
||||||
|
}
|
||||||
317
c/menu/menu.h
Normal file
317
c/menu/menu.h
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
/**
|
||||||
|
* @file menu.h
|
||||||
|
* @brief Портируемое меню для дисплея и шести кнопок навигации.
|
||||||
|
*
|
||||||
|
* Движок хранит стек открытых экранов, курсор и окно прокрутки, а содержимое
|
||||||
|
* запрашивает у приложения обратными вызовами: так один и тот же экран описывает
|
||||||
|
* и статический список, и перечень датчиков, число которых меняется на ходу.
|
||||||
|
*
|
||||||
|
* Рисует движок через таблицу Menu_Painter, поэтому от драйвера дисплея он не
|
||||||
|
* зависит: для ST7789V адаптер занимает несколько строк, для знакосинтезирующего
|
||||||
|
* ЖКИ или SSD1306 — столько же.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MENU_H
|
||||||
|
#define MENU_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/** Предельная глубина вложенности экранов. */
|
||||||
|
#define MENU_MAX_DEPTH 4U
|
||||||
|
|
||||||
|
/** Размер буфера строки пункта вместе с завершающим нулём. */
|
||||||
|
#define MENU_TEXT_MAX 32U
|
||||||
|
|
||||||
|
/** Предельное число одновременно видимых пунктов; ограничивает кэш строк. */
|
||||||
|
#define MENU_MAX_ROWS 20U
|
||||||
|
|
||||||
|
/** @brief Кнопки, которые понимает движок меню. */
|
||||||
|
typedef enum {
|
||||||
|
MENU_KEY_UP = 0, /**< Предыдущий пункт. */
|
||||||
|
MENU_KEY_DOWN, /**< Следующий пункт. */
|
||||||
|
MENU_KEY_LEFT, /**< Уменьшить значение пункта. */
|
||||||
|
MENU_KEY_RIGHT, /**< Увеличить значение пункта. */
|
||||||
|
MENU_KEY_ENTER, /**< Войти в пункт или подтвердить. */
|
||||||
|
MENU_KEY_BACK /**< Вернуться на предыдущий экран. */
|
||||||
|
} Menu_Key;
|
||||||
|
|
||||||
|
/** @brief Экран меню; описание неизменно и хранится во флеш-памяти. */
|
||||||
|
typedef struct Menu_Screen Menu_Screen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сообщает текущее число пунктов экрана.
|
||||||
|
*
|
||||||
|
* @param context Контекст экрана либо меню.
|
||||||
|
* @return Число пунктов; ноль допустим.
|
||||||
|
*/
|
||||||
|
typedef uint8_t (*Menu_CountFn)(void *context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Формирует текст пункта или его значения.
|
||||||
|
*
|
||||||
|
* @param context Контекст экрана либо меню.
|
||||||
|
* @param index Номер пункта.
|
||||||
|
* @param out Буфер приёмника, завершается нулём.
|
||||||
|
* @param size Размер буфера в байтах.
|
||||||
|
*/
|
||||||
|
typedef void (*Menu_TextFn)(void *context, uint8_t index, char *out, uint8_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает нажатие ввода на пункте.
|
||||||
|
*
|
||||||
|
* @param context Контекст экрана либо меню.
|
||||||
|
* @param index Номер пункта.
|
||||||
|
* @return Экран для открытия либо 0, если пункт лишь выполняет действие.
|
||||||
|
*/
|
||||||
|
typedef const Menu_Screen *(*Menu_EnterFn)(void *context, uint8_t index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Меняет значение пункта кнопками влево и вправо.
|
||||||
|
*
|
||||||
|
* @param context Контекст экрана либо меню.
|
||||||
|
* @param index Номер пункта.
|
||||||
|
* @param delta Шаг: -1 для влево, +1 для вправо.
|
||||||
|
*/
|
||||||
|
typedef void (*Menu_AdjustFn)(void *context, uint8_t index, int8_t delta);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Описание экрана меню.
|
||||||
|
*
|
||||||
|
* Обязательны заголовок и обратный вызов @c label. Число пунктов берётся из
|
||||||
|
* @c count, а при нулевом указателе — из поля @c item_count.
|
||||||
|
*/
|
||||||
|
struct Menu_Screen {
|
||||||
|
const char *title; /**< Заголовок в верхней полосе. */
|
||||||
|
uint8_t item_count; /**< Число пунктов статического экрана. */
|
||||||
|
Menu_CountFn count; /**< Число пунктов динамического экрана либо 0. */
|
||||||
|
Menu_TextFn label; /**< Текст пункта; обязателен. */
|
||||||
|
Menu_TextFn value; /**< Текст значения справа либо 0. */
|
||||||
|
Menu_EnterFn enter; /**< Реакция на ввод либо 0. */
|
||||||
|
Menu_AdjustFn adjust; /**< Реакция на влево и вправо либо 0. */
|
||||||
|
void *context; /**< Контекст экрана; 0 — использовать контекст меню. */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Обратные вызовы отрисовки и метрика шрифта.
|
||||||
|
*
|
||||||
|
* Цвета передаются как есть, поэтому в них укладывается и RGB565, и палитра
|
||||||
|
* монохромного индикатора.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/** Заливает прямоугольник цветом. */
|
||||||
|
void (*fill_rect)(void *context, int16_t x, int16_t y, uint16_t width,
|
||||||
|
uint16_t height, uint32_t color);
|
||||||
|
/** Выводит строку с непрозрачным фоном заданным масштабом шрифта. */
|
||||||
|
void (*draw_text)(void *context, int16_t x, int16_t y, const char *text,
|
||||||
|
uint32_t color, uint32_t background, uint8_t scale);
|
||||||
|
void *context; /**< Указатель адаптера дисплея. */
|
||||||
|
uint16_t width; /**< Ширина области вывода, пикселей. */
|
||||||
|
uint16_t height; /**< Высота области вывода, пикселей. */
|
||||||
|
uint8_t char_width; /**< Шаг знакоместа при масштабе 1. */
|
||||||
|
uint8_t char_height; /**< Высота знакоместа при масштабе 1. */
|
||||||
|
} Menu_Painter;
|
||||||
|
|
||||||
|
/** @brief Цвета и отступы оформления. */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t background; /**< Фон рабочей области. */
|
||||||
|
uint32_t title_fg; /**< Текст заголовка. */
|
||||||
|
uint32_t title_bg; /**< Фон заголовка. */
|
||||||
|
uint32_t item_fg; /**< Текст обычного пункта. */
|
||||||
|
uint32_t item_bg; /**< Фон обычного пункта. */
|
||||||
|
uint32_t value_fg; /**< Текст значения обычного пункта. */
|
||||||
|
uint32_t cursor_fg; /**< Текст выбранного пункта. */
|
||||||
|
uint32_t cursor_bg; /**< Фон выбранного пункта. */
|
||||||
|
uint32_t status_fg; /**< Текст нижней строки состояния. */
|
||||||
|
uint32_t status_bg; /**< Фон нижней строки состояния. */
|
||||||
|
uint32_t scroll_fg; /**< Указатели прокрутки. */
|
||||||
|
uint8_t title_scale; /**< Масштаб шрифта заголовка. */
|
||||||
|
uint8_t item_scale; /**< Масштаб шрифта пунктов. */
|
||||||
|
uint8_t padding; /**< Отступ от краёв, пикселей. */
|
||||||
|
uint8_t row_gap; /**< Просвет между пунктами, пикселей. */
|
||||||
|
} Menu_Theme;
|
||||||
|
|
||||||
|
/** @brief Состояние меню; принадлежит вызывающему коду. */
|
||||||
|
typedef struct {
|
||||||
|
const Menu_Screen *stack[MENU_MAX_DEPTH]; /**< Стек открытых экранов. */
|
||||||
|
uint8_t cursor[MENU_MAX_DEPTH]; /**< Выбранный пункт по уровням. */
|
||||||
|
uint8_t first[MENU_MAX_DEPTH]; /**< Верхний видимый пункт. */
|
||||||
|
uint8_t depth; /**< Число экранов в стеке. */
|
||||||
|
Menu_Painter painter; /**< Обратные вызовы отрисовки. */
|
||||||
|
Menu_Theme theme; /**< Оформление. */
|
||||||
|
void *context; /**< Контекст приложения. */
|
||||||
|
uint8_t rows; /**< Сколько пунктов видно сразу. */
|
||||||
|
uint8_t dirty; /**< 1 — экран требует перерисовки. */
|
||||||
|
char status[MENU_TEXT_MAX]; /**< Нижняя строка состояния. */
|
||||||
|
/*
|
||||||
|
* Кэш последней отрисовки. Перерисовываются только изменившиеся строки,
|
||||||
|
* поэтому обновление данных не вызывает мигания всего экрана.
|
||||||
|
*/
|
||||||
|
const Menu_Screen *cache_screen; /**< Экран, для которого кэш верен. */
|
||||||
|
uint8_t cache_first; /**< Верхний видимый пункт в кэше. */
|
||||||
|
uint8_t cache_valid; /**< 1 — кэш содержит отрисованное. */
|
||||||
|
uint8_t cache_selected[MENU_MAX_ROWS]; /**< Признак курсора по строкам. */
|
||||||
|
char cache_label[MENU_MAX_ROWS][MENU_TEXT_MAX]; /**< Названия строк. */
|
||||||
|
char cache_value[MENU_MAX_ROWS][MENU_TEXT_MAX]; /**< Значения строк. */
|
||||||
|
char cache_title[MENU_TEXT_MAX]; /**< Заголовок экрана. */
|
||||||
|
char cache_status[MENU_TEXT_MAX]; /**< Строка состояния с указателями. */
|
||||||
|
} Menu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Снимок видимой части экрана.
|
||||||
|
*
|
||||||
|
* Повторяет то, что сейчас нарисовано на панели: движок отдаёт содержимое из
|
||||||
|
* кэша последней отрисовки, не обращаясь к обратным вызовам приложения.
|
||||||
|
* Поэтому снимок дёшев и совпадает с изображением байт в байт.
|
||||||
|
*
|
||||||
|
* Строки указывают внутрь состояния меню и действительны до следующего
|
||||||
|
* вызова Menu_Render.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
const char *title; /**< Заголовок верхней полосы. */
|
||||||
|
const char *status; /**< Нижняя строка вместе с указателями прокрутки. */
|
||||||
|
const char *label[MENU_MAX_ROWS]; /**< Названия видимых пунктов. */
|
||||||
|
const char *value[MENU_MAX_ROWS]; /**< Значения видимых пунктов; пустая строка, если нет. */
|
||||||
|
uint8_t selected[MENU_MAX_ROWS]; /**< 1 — на этой строке курсор. */
|
||||||
|
uint8_t rows; /**< Число заполненных строк снимка. */
|
||||||
|
uint8_t first; /**< Номер верхнего видимого пункта. */
|
||||||
|
uint8_t total; /**< Всего пунктов на открытом экране. */
|
||||||
|
uint8_t cursor; /**< Выбранный пункт, номер в пределах экрана. */
|
||||||
|
uint8_t depth; /**< Глубина стека экранов; 1 — корневой. */
|
||||||
|
} Menu_Snapshot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Заполняет оформление тёмной темой для цветного дисплея.
|
||||||
|
*
|
||||||
|
* Значения цветов задаются в формате RGB565, отступы рассчитаны на шрифт 6x8.
|
||||||
|
*
|
||||||
|
* @param theme Оформление, принадлежащее вызывающему коду.
|
||||||
|
*/
|
||||||
|
void Menu_ThemeDefault(Menu_Theme *theme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Готовит меню к работе и открывает корневой экран.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param painter Обратные вызовы отрисовки; копируются внутрь состояния.
|
||||||
|
* @param theme Оформление; при значении 0 берётся Menu_ThemeDefault().
|
||||||
|
* @param root Корневой экран.
|
||||||
|
* @param context Контекст приложения для экранов без своего контекста.
|
||||||
|
* @return 1 при успешной настройке, 0 при неполных аргументах.
|
||||||
|
*/
|
||||||
|
uint8_t Menu_Init(Menu *menu, const Menu_Painter *painter, const Menu_Theme *theme,
|
||||||
|
const Menu_Screen *root, void *context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает нажатие кнопки навигации.
|
||||||
|
*
|
||||||
|
* Перерисовка не выполняется: функция только меняет состояние и поднимает
|
||||||
|
* признак устаревшего изображения.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param key Нажатая кнопка.
|
||||||
|
*/
|
||||||
|
void Menu_HandleKey(Menu *menu, Menu_Key key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Открывает вложенный экран, если стек не заполнен.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Открываемый экран.
|
||||||
|
*/
|
||||||
|
void Menu_Open(Menu *menu, const Menu_Screen *screen);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращается на предыдущий экран; на корневом ничего не делает.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
void Menu_Back(Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Закрывает все вложенные экраны и возвращается к корневому.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
void Menu_Home(Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Помечает изображение устаревшим после обновления данных.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
void Menu_Invalidate(Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сообщает, требуется ли перерисовка.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return 1, если изображение устарело.
|
||||||
|
*/
|
||||||
|
uint8_t Menu_IsDirty(const Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Задаёт текст нижней строки состояния.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param text Строка либо 0, чтобы очистить строку.
|
||||||
|
*/
|
||||||
|
void Menu_SetStatus(Menu *menu, const char *text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Перерисовывает экран, если изображение устарело.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param force 1 — рисовать безусловно.
|
||||||
|
*/
|
||||||
|
void Menu_Render(Menu *menu, uint8_t force);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает открытый экран.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Текущий экран либо 0, если меню не настроено.
|
||||||
|
*/
|
||||||
|
const Menu_Screen *Menu_Current(const Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает номер выбранного пункта открытого экрана.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Номер пункта; 0 для пустого экрана.
|
||||||
|
*/
|
||||||
|
uint8_t Menu_Cursor(const Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Снимает видимое содержимое экрана для передачи наружу.
|
||||||
|
*
|
||||||
|
* Источник данных — кэш последней отрисовки, поэтому вызывать функцию имеет
|
||||||
|
* смысл после Menu_Render: до первой отрисовки кэш пуст и снимок не выдаётся.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param out Снимок, принадлежащий вызывающему коду.
|
||||||
|
* @return 1, если снимок заполнен, 0 при пустом кэше или неверных аргументах.
|
||||||
|
*/
|
||||||
|
uint8_t Menu_GetSnapshot(const Menu *menu, Menu_Snapshot *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Копирует строку в буфер пункта с обрезкой по размеру.
|
||||||
|
*
|
||||||
|
* Вспомогательная функция для обратных вызовов приложения.
|
||||||
|
*
|
||||||
|
* @param out Буфер приёмника.
|
||||||
|
* @param size Размер буфера вместе с завершающим нулём.
|
||||||
|
* @param text Копируемая строка либо 0.
|
||||||
|
*/
|
||||||
|
void Menu_TextCopy(char *out, uint8_t size, const char *text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Печатает целое со знаком без обращения к stdio.
|
||||||
|
*
|
||||||
|
* @param out Буфер приёмника.
|
||||||
|
* @param size Размер буфера вместе с завершающим нулём.
|
||||||
|
* @param value Печатаемое значение.
|
||||||
|
* @param suffix Приписываемая справа строка либо 0.
|
||||||
|
*/
|
||||||
|
void Menu_TextInt(char *out, uint8_t size, int32_t value, const char *suffix);
|
||||||
|
|
||||||
|
#endif /* MENU_H */
|
||||||
35
c/protocan-transport/CMakeLists.txt
Normal file
35
c/protocan-transport/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(protocan_transport C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# Ядро библиотеки: платформенных зависимостей нет, собирается под что угодно.
|
||||||
|
add_library(protocan_transport STATIC
|
||||||
|
src/pcan_crc.c
|
||||||
|
src/pcan_frame.c
|
||||||
|
src/pcan_id.c
|
||||||
|
src/pcan_link.c
|
||||||
|
src/pcan_ring.c
|
||||||
|
src/pcan_gas.c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(protocan_transport PUBLIC include)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(protocan_transport PRIVATE /W4)
|
||||||
|
else()
|
||||||
|
target_compile_options(protocan_transport PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Порт ports/stm32f4 сюда не входит: ему нужен CMSIS-заголовок stm32f4xx.h,
|
||||||
|
# он подключается напрямую в проект прошивки.
|
||||||
|
|
||||||
|
option(PCAN_BUILD_TESTS "Собирать тесты" ON)
|
||||||
|
|
||||||
|
if(PCAN_BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_executable(test_transport tests/test_transport.c)
|
||||||
|
target_link_libraries(test_transport PRIVATE protocan_transport)
|
||||||
|
add_test(NAME transport COMMAND test_transport)
|
||||||
|
endif()
|
||||||
126
c/protocan-transport/README.md
Normal file
126
c/protocan-transport/README.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# protocan-transport
|
||||||
|
|
||||||
|
Переносимая библиотека транспортного уровня для протокола **ProtoCAN**:
|
||||||
|
кадрирование поверх любого байтового потока (RS485, RS232, USB CDC),
|
||||||
|
разбор идентификатора и общее адресное пространство регистров.
|
||||||
|
|
||||||
|
Написана на C99, без динамической памяти, без ОС, без зависимостей от HAL
|
||||||
|
и от конкретного микроконтроллера. Состояние живёт в структурах вызывающего,
|
||||||
|
поэтому в одной прошивке поднимается сколько угодно независимых каналов.
|
||||||
|
|
||||||
|
```
|
||||||
|
ваш код protocan-transport платформа
|
||||||
|
┌────────┐ ┌────────────────────┐ ┌──────────────┐
|
||||||
|
│ кадры │─────►│ pcan_link_send() │─────►│ io.write() │──► UART/DMA
|
||||||
|
│ │◄─────│ on_frame() │◄─────│ pcan_link_feed()
|
||||||
|
└────────┘ └────────────────────┘ └──────────────┘
|
||||||
|
pcan_gas_* pcan_id_*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Модуль | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `pcan_frame` | кадр `AA 55 … CRC16` и потоковый разборщик с ресинхронизацией |
|
||||||
|
| `pcan_crc` | CRC-16/CCITT-FALSE, побитовый или табличный |
|
||||||
|
| `pcan_link` | экземпляр канала: приём, передача, SEQ, счётчики |
|
||||||
|
| `pcan_ring` | кольцевой буфер, отдаёт непрерывный участок для DMA |
|
||||||
|
| `pcan_id` | упаковка и разбор 29-битного идентификатора ProtoCAN |
|
||||||
|
| `pcan_gas` | общее адресное пространство: карта регионов, чтение/запись, мост к кадрам |
|
||||||
|
| `ports/stm32f4` | готовый порт USART + DMA (пакетная передача, кольцевой приём) |
|
||||||
|
|
||||||
|
## Кадр
|
||||||
|
|
||||||
|
```
|
||||||
|
AA 55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3 | DATA[0..8] | CRC_L CRC_H
|
||||||
|
```
|
||||||
|
|
||||||
|
`LEN = 6 + DLC` (6..14), CRC-16/CCITT-FALSE по байтам `LEN..DATA`,
|
||||||
|
little-endian. Подробности — [docs/FRAME.md](docs/FRAME.md).
|
||||||
|
|
||||||
|
## Общее адресное пространство
|
||||||
|
|
||||||
|
Плоское пространство 16-битных регистров `0x0000..0xFFFF`, собранное из
|
||||||
|
регионов. Регион ссылается либо на массив в памяти, либо на пару колбэков —
|
||||||
|
так в карту попадают и переменные, и вычисляемые значения, и регистры
|
||||||
|
периферии. Подробности — [docs/GAS.md](docs/GAS.md).
|
||||||
|
|
||||||
|
```c
|
||||||
|
static uint16_t holding[8];
|
||||||
|
|
||||||
|
static const pcan_gas_region_t regions[] = {
|
||||||
|
{ 0x0000, 8, holding, NULL, NULL, 0, NULL, "holding" },
|
||||||
|
{ 0xFF00, 4, NULL, diag_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
|
||||||
|
};
|
||||||
|
static const pcan_gas_map_t map = { regions, 2 };
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "protocan_transport.h"
|
||||||
|
|
||||||
|
static void on_frame(const pcan_frame_t *f, void *user)
|
||||||
|
{
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
pcan_io_t io;
|
||||||
|
pcan_uart_io(&uart, &io); /* или свой io */
|
||||||
|
|
||||||
|
pcan_link_t link;
|
||||||
|
pcan_link_init(&link, &io, on_frame, NULL);
|
||||||
|
|
||||||
|
pcan_frame_t f = { .flags = PCAN_FLAG_IDE, .id = 0x1234567, .dlc = 2 };
|
||||||
|
f.data[0] = 0xAA; f.data[1] = 0xBB;
|
||||||
|
pcan_link_send(&link, &f); /* SEQ проставится сам */
|
||||||
|
```
|
||||||
|
|
||||||
|
Для своей платформы достаточно реализовать две функции:
|
||||||
|
|
||||||
|
```c
|
||||||
|
size_t my_write(void *ctx, const uint8_t *data, size_t len); /* всё-или-ничего */
|
||||||
|
size_t my_space(void *ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сборка
|
||||||
|
|
||||||
|
Библиотека — шесть файлов в `src/` и заголовки в `include/`. Добавьте их
|
||||||
|
в проект и укажите `include/` в путях поиска. CMake для тестов и хостовых
|
||||||
|
сборок:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build && cmake --build build && ctest --test-dir build
|
||||||
|
```
|
||||||
|
|
||||||
|
Либо напрямую:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test && ./test
|
||||||
|
```
|
||||||
|
|
||||||
|
Порт `ports/stm32f4` в тесты не входит: ему нужен CMSIS-заголовок
|
||||||
|
`stm32f4xx.h`, подключайте его в проект прошивки отдельно.
|
||||||
|
|
||||||
|
## Настройки
|
||||||
|
|
||||||
|
Переопределяются через `-D` либо через свой `pcan_config_user.h`
|
||||||
|
(с `-DPCAN_USE_USER_CONFIG`):
|
||||||
|
|
||||||
|
| Макрос | По умолчанию | Смысл |
|
||||||
|
|---|---|---|
|
||||||
|
| `PCAN_DATA_MAX` | 8 | длина поля данных CAN |
|
||||||
|
| `PCAN_CRC_TABLE` | 0 | 1 — таблица на 512 байт вместо побитового расчёта |
|
||||||
|
| `PCAN_GAS_MAX_REGIONS` | 16 | предел числа регионов в карте |
|
||||||
|
| `PCAN_BARRIER()` | барьер компилятора | для очередей, разделяемых с прерыванием |
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Очереди рассчитаны на схему «один писатель + один читатель». Если
|
||||||
|
писателей несколько, оборачивайте вызовы своей блокировкой.
|
||||||
|
- Размер кольцевого буфера обязан быть степенью двойки; `pcan_ring_init()`
|
||||||
|
вернёт `false`, а не станет молча портить индексы.
|
||||||
|
- На STM32F407 DMA не видит CCM RAM (`0x10000000`) — буферы держите
|
||||||
|
в основном SRAM.
|
||||||
|
- Порядок байт на линии фиксирован (little-endian) и не зависит от порядка
|
||||||
|
байт хоста.
|
||||||
78
c/protocan-transport/docs/FRAME.md
Normal file
78
c/protocan-transport/docs/FRAME.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Транспортный кадр
|
||||||
|
|
||||||
|
## Формат
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+-----+-----+-------+----------------+-------------+-------+-------+
|
||||||
|
| 0xAA | 0x55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3| DATA[0..8] | CRC_L | CRC_H |
|
||||||
|
+------+------+-----+-----+-------+----------------+-------------+-------+-------+
|
||||||
|
```
|
||||||
|
|
||||||
|
| Поле | Байт | Описание |
|
||||||
|
|---|---:|---|
|
||||||
|
| SOF | 2 | сигнатура `0xAA 0x55` |
|
||||||
|
| `LEN` | 1 | длина участка `SEQ..DATA` = `6 + DLC`, диапазон 6..14 |
|
||||||
|
| `SEQ` | 1 | счётчик кадров 0..255, инкремент на каждый успешно отданный кадр |
|
||||||
|
| `FLAGS` | 1 | см. ниже |
|
||||||
|
| `ID` | 4 | 29-битный CAN-идентификатор, little-endian (старшие 3 бита = 0) |
|
||||||
|
| `DATA` | 0..8 | `DLC = LEN - 6` байт данных CAN |
|
||||||
|
| `CRC` | 2 | CRC-16/CCITT-FALSE, little-endian |
|
||||||
|
|
||||||
|
CRC считается по байтам от `LEN` до последнего байта `DATA` включительно;
|
||||||
|
сигнатура SOF в расчёт не входит. Полином `0x1021`, начальное значение
|
||||||
|
`0xFFFF`, без рефлексии и без финального XOR — контрольное значение для
|
||||||
|
строки `123456789` равно `0x29B1`.
|
||||||
|
|
||||||
|
Максимальный размер кадра — 19 байт (`PCAN_FRAME_MAX`).
|
||||||
|
|
||||||
|
## FLAGS
|
||||||
|
|
||||||
|
| Бит | Имя | Значение |
|
||||||
|
|---:|---|---|
|
||||||
|
| 0 | `IDE` | 1 = расширенный ID (29 бит), 0 = стандартный (11 бит) |
|
||||||
|
| 1 | `RTR` | 1 = remote frame |
|
||||||
|
| 2 | `DIR` | 0 = кадр пришёл из CAN, 1 = кадр надо передать в CAN |
|
||||||
|
| 3 | `ERR` | 1 = служебный кадр моста (диагностика), не трафик шины |
|
||||||
|
| 7..4 | — | резерв, передавать нулями |
|
||||||
|
|
||||||
|
## Синхронизация
|
||||||
|
|
||||||
|
Приёмник ищет `0xAA 0x55`, читает `LEN`, проверяет диапазон `6..14`,
|
||||||
|
набирает `LEN + 2` байт и сверяет CRC. При неверном `LEN` или несовпадении
|
||||||
|
CRC разборщик возвращается к поиску сигнатуры, причём байт, оборвавший
|
||||||
|
разбор, сам проверяется на `0xAA` — последовательность `AA AA 55` тоже
|
||||||
|
распознаётся. Потеря синхронизации стоит не больше одного кадра.
|
||||||
|
|
||||||
|
Счётчики разбора (`pcan_parse_stats_t`) отдельно считают кадры, ошибки CRC,
|
||||||
|
неверные `LEN` и байты вне кадров — по ним видно, шумит линия или сбоит
|
||||||
|
источник.
|
||||||
|
|
||||||
|
## SEQ
|
||||||
|
|
||||||
|
`SEQ` инкрементируется только на успешно отданном кадре: если кадр не влез
|
||||||
|
в очередь передачи, счётчик не двигается, и приёмник не засчитает потерю
|
||||||
|
там, где кадра просто не было. Разрыв в `SEQ` на приёме означает реальную
|
||||||
|
потерю в линии.
|
||||||
|
|
||||||
|
## Почему так
|
||||||
|
|
||||||
|
- **Длина плюс CRC, без байт-стаффинга.** Стаффинг раздувает кадр
|
||||||
|
непредсказуемо и усложняет расчёт таймингов на полудуплексной линии.
|
||||||
|
Фиксированный заголовок даёт заранее известный максимум 19 байт.
|
||||||
|
- **Сигнатура из двух байт.** Один байт слишком часто встречается в
|
||||||
|
случайных данных; два дают приемлемую вероятность ложного старта,
|
||||||
|
который всё равно отсеет CRC.
|
||||||
|
- **`LEN` в начале.** Приёмник сразу знает, сколько байт набирать, и не
|
||||||
|
зависит от содержимого данных.
|
||||||
|
- **Little-endian везде.** Совпадает с порядком регистров в
|
||||||
|
`PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()` и с обоими целевыми МК.
|
||||||
|
|
||||||
|
## Пример
|
||||||
|
|
||||||
|
CAN-кадр: ID `0x1234567`, DLC 2, данные `AA BB`, `SEQ = 1`, флаг `IDE`.
|
||||||
|
|
||||||
|
```
|
||||||
|
AA 55 08 01 01 67 45 23 01 AA BB FE 14
|
||||||
|
```
|
||||||
|
|
||||||
|
`LEN = 6 + 2 = 8`, `FLAGS = 0x01`, CRC = `0x14FE`.
|
||||||
87
c/protocan-transport/docs/GAS.md
Normal file
87
c/protocan-transport/docs/GAS.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Общее адресное пространство (GAS)
|
||||||
|
|
||||||
|
Плоское пространство 16-битных регистров с адресом `0x0000..0xFFFF`.
|
||||||
|
Пространство собирается из **регионов**; регионы не перекрываются и
|
||||||
|
хранятся отсортированными по адресу, поиск — двоичный.
|
||||||
|
|
||||||
|
## Регион
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct pcan_gas_region {
|
||||||
|
uint16_t base; /* адрес первого регистра */
|
||||||
|
uint16_t count; /* число регистров */
|
||||||
|
uint16_t *storage; /* массив либо NULL */
|
||||||
|
pcan_gas_read_fn read;
|
||||||
|
pcan_gas_write_fn write;
|
||||||
|
uint8_t flags; /* PCAN_GAS_RDONLY / PCAN_GAS_WRONLY */
|
||||||
|
void *user;
|
||||||
|
const char *name;
|
||||||
|
} pcan_gas_region_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `storage != NULL`, чтение и запись идут прямо в массив — это самый
|
||||||
|
дешёвый вариант для обычных уставок. Если нужен вычисляемый регистр
|
||||||
|
(счётчик, состояние периферии, время работы), задайте `read`/`write`:
|
||||||
|
колбэк получает смещение внутри региона и указатель `user`.
|
||||||
|
|
||||||
|
`pcan_gas_map_validate()` проверяет карту на этапе старта: нулевые регионы,
|
||||||
|
выход за `0xFFFF`, перекрытие, нарушение порядка и регион без источника
|
||||||
|
данных. Вызывайте её один раз при инициализации — ошибка в таблице ловится
|
||||||
|
сразу, а не через месяц в поле.
|
||||||
|
|
||||||
|
## Доступ
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint16_t v;
|
||||||
|
pcan_gas_read(&map, 0x0002, &v);
|
||||||
|
pcan_gas_write(&map, 0x0002, 0x1234);
|
||||||
|
|
||||||
|
uint16_t block[4];
|
||||||
|
uint16_t n = pcan_gas_read_block(&map, 0x0000, block, 4);
|
||||||
|
```
|
||||||
|
|
||||||
|
Блочное чтение обрывается на первом адресе, которого нет в карте, поэтому
|
||||||
|
вызывающий всегда получает непрерывный кусок и знает его длину.
|
||||||
|
|
||||||
|
## Отображение на кадры ProtoCAN
|
||||||
|
|
||||||
|
Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит
|
||||||
|
в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд.
|
||||||
|
Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()`
|
||||||
|
в `SETCAN/Src/protocan.c`, поэтому обмен совместим с существующими
|
||||||
|
устройствами.
|
||||||
|
|
||||||
|
| Кадр | DLC | Смысл |
|
||||||
|
|---|---:|---|
|
||||||
|
| GAS, `MsgBody = addr` | 0 | **запрос на чтение** |
|
||||||
|
| GAS, `MsgBody = addr` | 2..8 | значения регистров начиная с `addr` |
|
||||||
|
|
||||||
|
Запрос на чтение с `DLC = 0` — **расширение**: в исходном коде SETCAN
|
||||||
|
такой кодировки нет, там ответы на GAS формировала заглушка `ProtoCanMsgToGeneralAddressSpace()`,
|
||||||
|
возвращавшая строку `GAS-XXXX`. Кодировка выбрана так, чтобы не занимать
|
||||||
|
новых типов сообщений и не конфликтовать с существующим форматом ответа.
|
||||||
|
|
||||||
|
```c
|
||||||
|
pcan_frame_t rsp;
|
||||||
|
if (pcan_gas_handle(&map, &incoming, &rsp)) {
|
||||||
|
pcan_link_send(&link, &rsp); /* был запрос на чтение */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`pcan_gas_handle()`:
|
||||||
|
|
||||||
|
- на запрос чтения кладёт в `rsp` до 4 регистров и переключает `Route`
|
||||||
|
на `FROM_DEVICE`, возвращает `true`;
|
||||||
|
- на запись пишет регистры в карту и возвращает `false` — ответа нет;
|
||||||
|
- если адреса нет в карте, возвращает `false`: отвечать нечем, а молчание
|
||||||
|
честнее, чем ответ с нулями.
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- В один кадр помещается не больше 4 регистров (`PCAN_GAS_REGS_PER_FRAME`).
|
||||||
|
Длинные блоки разбивайте на несколько кадров.
|
||||||
|
- Частичная запись: если в середине блока попался адрес вне карты или
|
||||||
|
регион только для чтения, запись обрывается на нём. `pcan_gas_write_block()`
|
||||||
|
возвращает число фактически записанных регистров.
|
||||||
|
- Атомарности между регистрами нет. Если два регистра обязаны меняться
|
||||||
|
вместе, заведите колбэк, который применяет их по записи второго.
|
||||||
158
c/protocan-transport/docs/GUI_CATALOG.md
Normal file
158
c/protocan-transport/docs/GUI_CATALOG.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# Каталог общего адресного пространства и поток значений
|
||||||
|
|
||||||
|
Расширение GUI-протокола SETGUI (`A5 5A`, см. `gui_desktop/core/protocol.py`)
|
||||||
|
для работы с общим адресным пространством: прибор сам объявляет, какие
|
||||||
|
регистры у него есть и как они называются, а оператор выбирает, что
|
||||||
|
показывать. Схема повторяет то, как устроен реестр регистров в
|
||||||
|
ST Motor Control Workbench / Motor Pilot.
|
||||||
|
|
||||||
|
Транспорт не меняется: `A5 5A | ver | type | seq(BE) | size(BE) | payload | CRC32(LE)`,
|
||||||
|
payload до 512 байт. Добавлены только три типа сообщений.
|
||||||
|
|
||||||
|
```
|
||||||
|
прибор GUI
|
||||||
|
│ ── GAS_CATALOG (seq = 0) ──────────► │ каталог приходит сам
|
||||||
|
│ ── GAS_CATALOG (seq = 0) ──────────► │ при инициализации
|
||||||
|
│ ── GAS_CATALOG (seq = 0) ──────────► │
|
||||||
|
│ │ оператор отмечает нужное
|
||||||
|
│ ◄──────────────── GAS_WATCH_SET ──── │
|
||||||
|
│ ── GAS_WATCH_SET (эхо) ────────────► │
|
||||||
|
│ ── GAS_WATCH_DATA (seq = 0) ───────► │ поток значений
|
||||||
|
│ ── GAS_WATCH_DATA (seq = 0) ───────► │
|
||||||
|
```
|
||||||
|
|
||||||
|
## Типы сообщений
|
||||||
|
|
||||||
|
Заняты из свободного диапазона `0x11..0x1F` (между `READ_LOGS = 0x10`
|
||||||
|
и `SENSOR_SCAN = 0x20`). Существующие значения не перенумерованы.
|
||||||
|
|
||||||
|
| Код | Имя | Направление |
|
||||||
|
|---:|---|---|
|
||||||
|
| `0x11` | `GAS_CATALOG` | запрос GUI → прибор; записи прибор → GUI |
|
||||||
|
| `0x12` | `GAS_WATCH_SET` | GUI → прибор, прибор отвечает эхом |
|
||||||
|
| `0x13` | `GAS_WATCH_DATA` | прибор → GUI, без запроса, `sequence = 0` |
|
||||||
|
|
||||||
|
Незапрошенные кадры прибор публикует с `sequence = 0` — так же, как уже
|
||||||
|
устроены `SENSOR_DATA` и `UI_STATE`.
|
||||||
|
|
||||||
|
## `GAS_CATALOG` (0x11)
|
||||||
|
|
||||||
|
### Запрос (GUI → прибор), 4 байта
|
||||||
|
|
||||||
|
| Смещение | Тип | Поле |
|
||||||
|
|---:|---|---|
|
||||||
|
| 0 | u16 LE | `start_index` — порядковый номер записи, **не адрес** |
|
||||||
|
| 2 | u16 LE | `max_count` — сколько записей вернуть; 0 = сколько влезет |
|
||||||
|
|
||||||
|
### Ответ и автопубликация (прибор → GUI)
|
||||||
|
|
||||||
|
| Смещение | Тип | Поле |
|
||||||
|
|---:|---|---|
|
||||||
|
| 0 | u16 LE | `total` — всего записей в каталоге |
|
||||||
|
| 2 | u16 LE | `start_index` — индекс первой записи в пакете |
|
||||||
|
| 4 | u16 LE | `count` — записей в пакете |
|
||||||
|
| 6 | | `count` записей по 32 байта |
|
||||||
|
|
||||||
|
Одна запись — 32 байта:
|
||||||
|
|
||||||
|
| Смещение | Тип | Поле |
|
||||||
|
|---:|---|---|
|
||||||
|
| 0 | u16 LE | `address` — адрес в общем адресном пространстве |
|
||||||
|
| 2 | u8 | `type` — формат значения |
|
||||||
|
| 3 | u8 | `flags` — доступ |
|
||||||
|
| 4 | i8 | `scale_pow10` — значение = `raw * 10^scale` |
|
||||||
|
| 5 | u8 | `unit` — код единицы измерения |
|
||||||
|
| 6 | u16 LE | резерв, нули |
|
||||||
|
| 8 | 24 байта | `name` — UTF-8, дополнено нулями |
|
||||||
|
|
||||||
|
При payload 512 байт в один пакет входит `(512 - 6) / 32 = 15` записей.
|
||||||
|
|
||||||
|
Под имя отведено 24 байта — это 12 кириллических символов в UTF-8.
|
||||||
|
На 16 байтах не помещалось даже «Температура», поэтому поле шире,
|
||||||
|
чем кажется нужным для латиницы.
|
||||||
|
|
||||||
|
### `type`
|
||||||
|
|
||||||
|
| Код | Значение | Регистров |
|
||||||
|
|---:|---|---:|
|
||||||
|
| 0 | `U16` — беззнаковое | 1 |
|
||||||
|
| 1 | `I16` — знаковое | 1 |
|
||||||
|
| 2 | `U32` — беззнаковое, младшее слово первым | 2 |
|
||||||
|
| 3 | `I32` — знаковое, младшее слово первым | 2 |
|
||||||
|
| 4 | `BITS` — битовое поле | 1 |
|
||||||
|
|
||||||
|
Многословные значения занимают подряд идущие адреса; в потоке они
|
||||||
|
приходят отдельными словами, GUI собирает их сам.
|
||||||
|
|
||||||
|
### `flags`
|
||||||
|
|
||||||
|
| Бит | Смысл |
|
||||||
|
|---:|---|
|
||||||
|
| 0 | доступно на чтение |
|
||||||
|
| 1 | доступно на запись |
|
||||||
|
| 2 | включить в подписку по умолчанию |
|
||||||
|
|
||||||
|
### `unit`
|
||||||
|
|
||||||
|
| Код | Единица | Код | Единица |
|
||||||
|
|---:|---|---:|---|
|
||||||
|
| 0 | — | 6 | мс |
|
||||||
|
| 1 | В | 7 | с |
|
||||||
|
| 2 | А | 8 | кбит/с |
|
||||||
|
| 3 | °C | 9 | шт. |
|
||||||
|
| 4 | % | 10 | об/мин |
|
||||||
|
| 5 | Гц | 11 | Вт |
|
||||||
|
|
||||||
|
## `GAS_WATCH_SET` (0x12)
|
||||||
|
|
||||||
|
### Запрос (GUI → прибор)
|
||||||
|
|
||||||
|
| Смещение | Тип | Поле |
|
||||||
|
|---:|---|---|
|
||||||
|
| 0 | u16 LE | `period_ms` — период потока; **0 останавливает поток** |
|
||||||
|
| 2 | u16 LE | `count` — число адресов, не больше `GAS_WATCH_MAX` (64) |
|
||||||
|
| 4 | | `count` × u16 LE — адреса в нужном порядке |
|
||||||
|
|
||||||
|
Порядок адресов сохраняется: значения в `GAS_WATCH_DATA` приходят
|
||||||
|
ровно в том же порядке, без повторной передачи адресов.
|
||||||
|
|
||||||
|
### Ответ (прибор → GUI), 4 байта
|
||||||
|
|
||||||
|
| Смещение | Тип | Поле |
|
||||||
|
|---:|---|---|
|
||||||
|
| 0 | u16 LE | `period_ms` — период, который прибор реально установил |
|
||||||
|
| 2 | u16 LE | `count` — сколько адресов принято |
|
||||||
|
|
||||||
|
Прибор может принять меньше, чем попросили: адрес вне карты в подписку
|
||||||
|
не берётся. Расхождение `count` с запросом — сигнал GUI, что часть
|
||||||
|
адресов отвергнута.
|
||||||
|
|
||||||
|
## `GAS_WATCH_DATA` (0x13)
|
||||||
|
|
||||||
|
Прибор → GUI, `sequence = 0`, без запроса.
|
||||||
|
|
||||||
|
| Смещение | Тип | Поле |
|
||||||
|
|---:|---|---|
|
||||||
|
| 0 | u32 LE | `timestamp_ms` — время прибора от старта |
|
||||||
|
| 4 | u16 LE | `count` |
|
||||||
|
| 6 | | `count` × u16 LE — значения в порядке подписки |
|
||||||
|
|
||||||
|
`timestamp_ms` берётся у прибора, а не у хоста: по нему видно реальный
|
||||||
|
период и провалы, которые иначе замаскировала бы буферизация UART.
|
||||||
|
|
||||||
|
## Замечания по реализации
|
||||||
|
|
||||||
|
* **Каталог статичен.** Он описывает прошивку, а не состояние, поэтому
|
||||||
|
публикуется один раз при инициализации периферии. GUI может перечитать
|
||||||
|
его запросом в любой момент.
|
||||||
|
* **Подписка живёт до переподключения.** Прибор не сохраняет её в
|
||||||
|
энергонезависимой памяти: после сброса поток молчит, пока GUI не
|
||||||
|
пришлёт `GAS_WATCH_SET` снова.
|
||||||
|
* **Поток не должен забивать линию.** При периоде 10 мс и 64 адресах
|
||||||
|
выходит 134 байта на пакет и 13.4 кбайт/с — половина пропускной
|
||||||
|
способности 256000 бод. Прибор пропускает такт, если в очереди
|
||||||
|
передачи нет места для целого пакета, и это видно по разрыву
|
||||||
|
`timestamp_ms`.
|
||||||
|
* **Значения передаются сырыми.** Пересчёт в физические величины делает
|
||||||
|
GUI по `scale_pow10` и `unit` — прибор не тратит на это такты и не
|
||||||
|
теряет точность на промежуточном округлении.
|
||||||
155
c/protocan-transport/include/gui_catalog.h
Normal file
155
c/protocan-transport/include/gui_catalog.h
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* @file gui_catalog.h
|
||||||
|
* @brief Каталог общего адресного пространства и поток выбранных значений.
|
||||||
|
*
|
||||||
|
* Прибор объявляет GUI, какие регистры у него есть и как они называются;
|
||||||
|
* оператор отмечает нужное, и прибор шлёт только отмеченное пакетами.
|
||||||
|
* Схема повторяет реестр регистров ST Motor Control Workbench.
|
||||||
|
*
|
||||||
|
* Двоичный контракт: docs/GUI_CATALOG.md.
|
||||||
|
* Зеркало на Python: gui_desktop/core/gas_catalog.py.
|
||||||
|
*/
|
||||||
|
#ifndef GUI_CATALOG_H
|
||||||
|
#define GUI_CATALOG_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "gui_frame.h"
|
||||||
|
#include "pcan_gas.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Длина записи каталога на линии. */
|
||||||
|
#define GUI_ENTRY_SIZE 32U
|
||||||
|
/**
|
||||||
|
* Длина поля имени. 24 байта - это 12 кириллических символов в UTF-8;
|
||||||
|
* на 16 байтах не помещалось даже "Температура".
|
||||||
|
*/
|
||||||
|
#define GUI_NAME_SIZE 24U
|
||||||
|
/** Заголовок ответа GAS_CATALOG: total, start_index, count. */
|
||||||
|
#define GUI_CATALOG_HEADER 6U
|
||||||
|
/** Сколько записей входит в один кадр. */
|
||||||
|
#define GUI_ENTRIES_PER_FRAME ((GUI_MAX_PAYLOAD - GUI_CATALOG_HEADER) / GUI_ENTRY_SIZE)
|
||||||
|
|
||||||
|
/** Максимум адресов в подписке. */
|
||||||
|
#ifndef GUI_WATCH_MAX
|
||||||
|
#define GUI_WATCH_MAX 64U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Формат значения. */
|
||||||
|
#define GUI_OBJ_U16 0U
|
||||||
|
#define GUI_OBJ_I16 1U
|
||||||
|
#define GUI_OBJ_U32 2U /* два регистра, младшее слово первым */
|
||||||
|
#define GUI_OBJ_I32 3U
|
||||||
|
#define GUI_OBJ_BITS 4U
|
||||||
|
|
||||||
|
/** Доступ и признаки. */
|
||||||
|
#define GUI_OBJ_READABLE 0x01U
|
||||||
|
#define GUI_OBJ_WRITABLE 0x02U
|
||||||
|
#define GUI_OBJ_DEFAULT_WATCH 0x04U
|
||||||
|
|
||||||
|
/** Коды единиц измерения; входят в контракт и не перенумеровываются. */
|
||||||
|
#define GUI_UNIT_NONE 0U
|
||||||
|
#define GUI_UNIT_VOLT 1U
|
||||||
|
#define GUI_UNIT_AMPERE 2U
|
||||||
|
#define GUI_UNIT_CELSIUS 3U
|
||||||
|
#define GUI_UNIT_PERCENT 4U
|
||||||
|
#define GUI_UNIT_HERTZ 5U
|
||||||
|
#define GUI_UNIT_MS 6U
|
||||||
|
#define GUI_UNIT_SECOND 7U
|
||||||
|
#define GUI_UNIT_KBPS 8U
|
||||||
|
#define GUI_UNIT_COUNT 9U
|
||||||
|
#define GUI_UNIT_RPM 10U
|
||||||
|
#define GUI_UNIT_WATT 11U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Описание одного значения в общем адресном пространстве.
|
||||||
|
*
|
||||||
|
* Таблица объявляется `static const` и живёт во flash: на МК её незачем
|
||||||
|
* держать в ОЗУ.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint16_t address; /**< адрес первого регистра */
|
||||||
|
uint8_t type; /**< GUI_OBJ_* */
|
||||||
|
uint8_t flags; /**< GUI_OBJ_READABLE и прочие */
|
||||||
|
int8_t scale_pow10; /**< физическое = raw * 10^scale */
|
||||||
|
uint8_t unit; /**< GUI_UNIT_* */
|
||||||
|
const char *name; /**< UTF-8, не длиннее GUI_NAME_SIZE */
|
||||||
|
} gui_object_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const gui_object_t *items;
|
||||||
|
uint16_t count;
|
||||||
|
} gui_catalog_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Проверяет таблицу на этапе старта.
|
||||||
|
*
|
||||||
|
* Ловит имена длиннее поля и адреса, которых нет в карте GAS: ошибку
|
||||||
|
* в таблице лучше увидеть при инициализации, чем гадать над пустой
|
||||||
|
* строкой в GUI.
|
||||||
|
*
|
||||||
|
* @param map карта GAS для сверки адресов; NULL - не сверять.
|
||||||
|
*/
|
||||||
|
bool gui_catalog_validate(const gui_catalog_t *catalog, const pcan_gas_map_t *map);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Собирает payload ответа GAS_CATALOG.
|
||||||
|
*
|
||||||
|
* @param start_index индекс первой записи;
|
||||||
|
* @param max_count сколько записей отдать; 0 - сколько влезет в кадр;
|
||||||
|
* @return длина payload либо 0, если start_index за концом каталога.
|
||||||
|
*/
|
||||||
|
size_t gui_catalog_encode(const gui_catalog_t *catalog, uint16_t start_index,
|
||||||
|
uint16_t max_count, uint8_t *out, size_t out_size);
|
||||||
|
|
||||||
|
/* --- Подписка на поток ----------------------------------------------------- */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t period_ms; /**< 0 - поток остановлен */
|
||||||
|
uint16_t count;
|
||||||
|
uint16_t address[GUI_WATCH_MAX];
|
||||||
|
uint32_t next_ms; /**< когда слать следующий пакет */
|
||||||
|
uint32_t sent;
|
||||||
|
uint32_t skipped; /**< тактов пропущено из-за занятой линии */
|
||||||
|
} gui_watch_t;
|
||||||
|
|
||||||
|
void gui_watch_init(gui_watch_t *watch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Применяет payload GAS_WATCH_SET.
|
||||||
|
*
|
||||||
|
* Адреса, которых нет в карте, в подписку не берутся - GUI увидит это
|
||||||
|
* по расхождению count в эхо-ответе.
|
||||||
|
*
|
||||||
|
* @return число принятых адресов; при неверном payload подписка не меняется
|
||||||
|
* и возвращается 0xFFFF.
|
||||||
|
*/
|
||||||
|
uint16_t gui_watch_apply(gui_watch_t *watch, const pcan_gas_map_t *map,
|
||||||
|
const uint8_t *payload, uint16_t size);
|
||||||
|
|
||||||
|
/** Собирает 4 байта эхо-ответа: период и число принятых адресов. */
|
||||||
|
size_t gui_watch_encode_ack(const gui_watch_t *watch, uint8_t *out, size_t out_size);
|
||||||
|
|
||||||
|
/** Пора ли слать очередной пакет. */
|
||||||
|
bool gui_watch_due(const gui_watch_t *watch, uint32_t now_ms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Собирает payload GAS_WATCH_DATA из текущих значений карты.
|
||||||
|
* @return длина payload либо 0, если подписка пуста.
|
||||||
|
*/
|
||||||
|
size_t gui_watch_encode_data(const gui_watch_t *watch, const pcan_gas_map_t *map,
|
||||||
|
uint32_t timestamp_ms, uint8_t *out, size_t out_size);
|
||||||
|
|
||||||
|
/** Сдвигает момент следующей отправки. */
|
||||||
|
void gui_watch_advance(gui_watch_t *watch, uint32_t now_ms);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* GUI_CATALOG_H */
|
||||||
185
c/protocan-transport/include/gui_frame.h
Normal file
185
c/protocan-transport/include/gui_frame.h
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* @file gui_frame.h
|
||||||
|
* @brief Транспорт GUI-протокола SETGUI на стороне МК.
|
||||||
|
*
|
||||||
|
* A5 5A | ver | type | seq_hi seq_lo | size_hi size_lo | payload | CRC32 LE
|
||||||
|
*
|
||||||
|
* Заголовок big-endian, CRC32 (IEEE, как zlib) little-endian, payload
|
||||||
|
* до 512 байт. Байт в байт совпадает с gui_desktop/core/protocol.py.
|
||||||
|
*
|
||||||
|
* Это второй протокол библиотеки и он намеренно отдельный от pcan_frame:
|
||||||
|
* тот повторяет форму CAN-кадра (8 байт данных) и живёт на полевой шине,
|
||||||
|
* а здесь нужен большой payload для каталога и потока значений. Общего
|
||||||
|
* у них только платформенный слой pcan_io_t.
|
||||||
|
*/
|
||||||
|
#ifndef GUI_FRAME_H
|
||||||
|
#define GUI_FRAME_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcan_config.h"
|
||||||
|
#include "pcan_link.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define GUI_SOF0 0xA5U
|
||||||
|
#define GUI_SOF1 0x5AU
|
||||||
|
#define GUI_PROTOCOL_VERSION 0x01U
|
||||||
|
|
||||||
|
#define GUI_HEADER_SIZE 8U /* SOF(2) + ver + type + seq(2) + size(2) */
|
||||||
|
#define GUI_CRC_SIZE 4U
|
||||||
|
#define GUI_MAX_PAYLOAD 512U
|
||||||
|
#define GUI_FRAME_MAX (GUI_HEADER_SIZE + GUI_MAX_PAYLOAD + GUI_CRC_SIZE)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Наибольший payload, который прибор готов принять.
|
||||||
|
*
|
||||||
|
* Ограничивает буфер разборщика. 512 нужны только для WRITE_REGISTERS
|
||||||
|
* на 254 слова; если такие записи не используются, значение можно
|
||||||
|
* уменьшить и сэкономить ОЗУ.
|
||||||
|
*/
|
||||||
|
#ifndef GUI_RX_PAYLOAD_MAX
|
||||||
|
#define GUI_RX_PAYLOAD_MAX GUI_MAX_PAYLOAD
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Типы сообщений, нужные мосту. Полный список - в protocol.py. */
|
||||||
|
#define GUI_MSG_PING 0x01U
|
||||||
|
#define GUI_MSG_DEVICE_INFO 0x02U
|
||||||
|
#define GUI_MSG_DIAGNOSTICS 0x08U
|
||||||
|
#define GUI_MSG_READ_REGISTERS 0x09U
|
||||||
|
#define GUI_MSG_WRITE_REGISTERS 0x0AU
|
||||||
|
#define GUI_MSG_GAS_CATALOG 0x11U
|
||||||
|
#define GUI_MSG_GAS_WATCH_SET 0x12U
|
||||||
|
#define GUI_MSG_GAS_WATCH_DATA 0x13U
|
||||||
|
#define GUI_MSG_NACK 0x80U
|
||||||
|
#define GUI_MSG_ACK 0x81U
|
||||||
|
#define GUI_MSG_ERROR 0x82U
|
||||||
|
|
||||||
|
/* Коды результата, общие с GUITransport_ObjectResult и ObjectResult. */
|
||||||
|
#define GUI_RESULT_OK 0U
|
||||||
|
#define GUI_RESULT_INVALID_ARGUMENT 1U
|
||||||
|
#define GUI_RESULT_INVALID_LENGTH 2U
|
||||||
|
#define GUI_RESULT_NOT_FOUND 3U
|
||||||
|
#define GUI_RESULT_ACCESS_DENIED 4U
|
||||||
|
#define GUI_RESULT_INTERNAL 7U
|
||||||
|
|
||||||
|
/** CRC32 IEEE: poly 0xEDB88320 с рефлексией, init и финальный XOR 0xFFFFFFFF. */
|
||||||
|
uint32_t gui_crc32(const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Кодирует кадр целиком.
|
||||||
|
* @param out буфер приёмника;
|
||||||
|
* @param out_size его размер, нужно `GUI_HEADER_SIZE + size + GUI_CRC_SIZE`;
|
||||||
|
* @return число записанных байт либо 0 при неверных аргументах.
|
||||||
|
*/
|
||||||
|
size_t gui_frame_encode(uint8_t type, uint16_t sequence,
|
||||||
|
const uint8_t *payload, uint16_t size,
|
||||||
|
uint8_t *out, size_t out_size);
|
||||||
|
|
||||||
|
/** Разобранный кадр; payload указывает внутрь буфера разборщика. */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t type;
|
||||||
|
uint16_t sequence;
|
||||||
|
uint16_t size;
|
||||||
|
const uint8_t *payload;
|
||||||
|
} gui_frame_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GUI_ST_SOF0 = 0,
|
||||||
|
GUI_ST_SOF1,
|
||||||
|
GUI_ST_HEADER,
|
||||||
|
GUI_ST_PAYLOAD,
|
||||||
|
GUI_ST_CRC
|
||||||
|
} gui_parse_state_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t frames;
|
||||||
|
uint32_t crc_errors;
|
||||||
|
uint32_t version_errors;
|
||||||
|
uint32_t length_errors;
|
||||||
|
uint32_t stray_bytes;
|
||||||
|
} gui_parse_stats_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
gui_parse_state_t state;
|
||||||
|
uint8_t header[6]; /**< ver, type, seq(2), size(2) */
|
||||||
|
uint16_t idx;
|
||||||
|
uint16_t size;
|
||||||
|
uint8_t crc_bytes[GUI_CRC_SIZE];
|
||||||
|
uint8_t payload[GUI_RX_PAYLOAD_MAX];
|
||||||
|
gui_parse_stats_t stats;
|
||||||
|
} gui_parser_t;
|
||||||
|
|
||||||
|
void gui_parser_init(gui_parser_t *p);
|
||||||
|
|
||||||
|
/** @return true, если кадр собран и проверен; payload лежит в p->payload. */
|
||||||
|
bool gui_parser_push(gui_parser_t *p, uint8_t byte, gui_frame_t *out);
|
||||||
|
|
||||||
|
typedef void (*gui_frame_cb_t)(const gui_frame_t *frame, void *user);
|
||||||
|
|
||||||
|
size_t gui_parser_feed(gui_parser_t *p, const uint8_t *data, size_t len,
|
||||||
|
gui_frame_cb_t cb, void *user);
|
||||||
|
|
||||||
|
/* --- Канал ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Экземпляр GUI-канала поверх того же pcan_io_t, что и полевой.
|
||||||
|
*
|
||||||
|
* Буфер передачи лежит внутри: кадр собирается целиком и отдаётся в порт
|
||||||
|
* одной записью, иначе половина пакета в линии сбила бы приёмник.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
pcan_io_t io;
|
||||||
|
gui_parser_t parser;
|
||||||
|
gui_frame_cb_t on_frame;
|
||||||
|
void *user;
|
||||||
|
uint32_t tx_frames;
|
||||||
|
uint32_t tx_dropped;
|
||||||
|
uint8_t tx_buf[GUI_FRAME_MAX];
|
||||||
|
} gui_link_t;
|
||||||
|
|
||||||
|
bool gui_link_init(gui_link_t *link, const pcan_io_t *io,
|
||||||
|
gui_frame_cb_t on_frame, void *user);
|
||||||
|
|
||||||
|
/** @return false, если в очереди передачи не хватило места на весь кадр. */
|
||||||
|
bool gui_link_send(gui_link_t *link, uint8_t type, uint16_t sequence,
|
||||||
|
const uint8_t *payload, uint16_t size);
|
||||||
|
|
||||||
|
/** Короткий отказ: NACK с кодом результата в payload. */
|
||||||
|
bool gui_link_nack(gui_link_t *link, uint16_t sequence, uint16_t result);
|
||||||
|
|
||||||
|
size_t gui_link_feed(gui_link_t *link, const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
/** Помещается ли ещё кадр с payload указанной длины. */
|
||||||
|
bool gui_link_can_send(const gui_link_t *link, uint16_t size);
|
||||||
|
|
||||||
|
/* --- Мелкие помощники для payload ----------------------------------------- */
|
||||||
|
|
||||||
|
static inline uint16_t gui_get_u16(const uint8_t *p)
|
||||||
|
{
|
||||||
|
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void gui_put_u16(uint8_t *p, uint16_t value)
|
||||||
|
{
|
||||||
|
p[0] = (uint8_t)(value & 0xFFU);
|
||||||
|
p[1] = (uint8_t)(value >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void gui_put_u32(uint8_t *p, uint32_t value)
|
||||||
|
{
|
||||||
|
p[0] = (uint8_t)(value & 0xFFU);
|
||||||
|
p[1] = (uint8_t)((value >> 8) & 0xFFU);
|
||||||
|
p[2] = (uint8_t)((value >> 16) & 0xFFU);
|
||||||
|
p[3] = (uint8_t)((value >> 24) & 0xFFU);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* GUI_FRAME_H */
|
||||||
45
c/protocan-transport/include/pcan_config.h
Normal file
45
c/protocan-transport/include/pcan_config.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @file pcan_config.h
|
||||||
|
* @brief Настройки времени компиляции.
|
||||||
|
*
|
||||||
|
* Значения по умолчанию рассчитаны на МК без ОС. Переопределяются
|
||||||
|
* либо через -D, либо через собственный pcan_config_user.h.
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_CONFIG_H
|
||||||
|
#define PCAN_CONFIG_H
|
||||||
|
|
||||||
|
#if defined(PCAN_USE_USER_CONFIG)
|
||||||
|
#include "pcan_config_user.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Максимальная длина поля данных CAN. */
|
||||||
|
#ifndef PCAN_DATA_MAX
|
||||||
|
#define PCAN_DATA_MAX 8U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Барьер компилятора для разделяемых с прерыванием переменных.
|
||||||
|
*
|
||||||
|
* Библиотека не отключает прерывания сама: очереди рассчитаны на схему
|
||||||
|
* "один писатель + один читатель", где индексы обновляются атомарно.
|
||||||
|
* Если у вас несколько писателей, оберните вызовы своей блокировкой.
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_BARRIER
|
||||||
|
# if defined(__GNUC__) || defined(__clang__) || defined(__ARMCC_VERSION)
|
||||||
|
# define PCAN_BARRIER() __asm volatile("" ::: "memory")
|
||||||
|
# else
|
||||||
|
# define PCAN_BARRIER() do { } while (0)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Таблица CRC (512 байт flash) вместо побитового расчёта. */
|
||||||
|
#ifndef PCAN_CRC_TABLE
|
||||||
|
#define PCAN_CRC_TABLE 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Количество регионов, которые может содержать одна карта GAS. */
|
||||||
|
#ifndef PCAN_GAS_MAX_REGIONS
|
||||||
|
#define PCAN_GAS_MAX_REGIONS 16U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PCAN_CONFIG_H */
|
||||||
28
c/protocan-transport/include/pcan_crc.h
Normal file
28
c/protocan-transport/include/pcan_crc.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* @file pcan_crc.h
|
||||||
|
* @brief CRC-16/CCITT-FALSE: poly 0x1021, init 0xFFFF, без рефлексии,
|
||||||
|
* без финального XOR. Контрольное значение для "123456789" - 0x29B1.
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_CRC_H
|
||||||
|
#define PCAN_CRC_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcan_config.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Начальное значение для инкрементального расчёта. */
|
||||||
|
#define PCAN_CRC_INIT 0xFFFFU
|
||||||
|
|
||||||
|
uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte);
|
||||||
|
uint16_t pcan_crc16(const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PCAN_CRC_H */
|
||||||
113
c/protocan-transport/include/pcan_frame.h
Normal file
113
c/protocan-transport/include/pcan_frame.h
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* @file pcan_frame.h
|
||||||
|
* @brief Транспортный кадр и потоковый разборщик.
|
||||||
|
*
|
||||||
|
* AA 55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3 | DATA[0..8] | CRC_L CRC_H
|
||||||
|
*
|
||||||
|
* LEN = 6 + DLC (длина участка SEQ..DATA), диапазон 6..14
|
||||||
|
* CRC = CRC-16/CCITT-FALSE по байтам LEN..DATA, little-endian
|
||||||
|
*
|
||||||
|
* Кадр самосинхронизирующийся: при мусоре в линии разборщик сдвигается
|
||||||
|
* на байт и продолжает искать сигнатуру, теряя не больше одного кадра.
|
||||||
|
*
|
||||||
|
* Формат не зависит ни от платформы, ни от физического уровня: он одинаково
|
||||||
|
* ложится на RS485, RS232, USB CDC и любой байтовый поток.
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_FRAME_H
|
||||||
|
#define PCAN_FRAME_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcan_config.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PCAN_SOF0 0xAAU
|
||||||
|
#define PCAN_SOF1 0x55U
|
||||||
|
|
||||||
|
/** Расширенный (29-битный) идентификатор. */
|
||||||
|
#define PCAN_FLAG_IDE 0x01U
|
||||||
|
/** Remote frame. */
|
||||||
|
#define PCAN_FLAG_RTR 0x02U
|
||||||
|
/** 0: кадр пришёл из CAN; 1: кадр надо передать в CAN. */
|
||||||
|
#define PCAN_FLAG_DIR 0x04U
|
||||||
|
/** Служебный кадр моста (диагностика), не относится к трафику CAN. */
|
||||||
|
#define PCAN_FLAG_ERR 0x08U
|
||||||
|
|
||||||
|
#define PCAN_LEN_MIN 6U
|
||||||
|
#define PCAN_LEN_MAX (PCAN_LEN_MIN + PCAN_DATA_MAX)
|
||||||
|
/** SOF(2) + LEN(1) + LEN_MAX + CRC(2). */
|
||||||
|
#define PCAN_FRAME_MAX (PCAN_LEN_MAX + 5U)
|
||||||
|
|
||||||
|
/** Разобранный кадр. */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t seq;
|
||||||
|
uint8_t flags;
|
||||||
|
uint32_t id; /**< 29-битный идентификатор */
|
||||||
|
uint8_t dlc;
|
||||||
|
uint8_t data[PCAN_DATA_MAX];
|
||||||
|
} pcan_frame_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сериализует кадр.
|
||||||
|
* @param out буфер приёмника;
|
||||||
|
* @param out_size его размер, нужно не меньше PCAN_FRAME_MAX;
|
||||||
|
* @return число записанных байт либо 0, если буфер мал.
|
||||||
|
*/
|
||||||
|
size_t pcan_frame_encode(const pcan_frame_t *frame, uint8_t *out, size_t out_size);
|
||||||
|
|
||||||
|
/* --- Потоковый разборщик -------------------------------------------------- */
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PCAN_ST_SOF0 = 0,
|
||||||
|
PCAN_ST_SOF1,
|
||||||
|
PCAN_ST_LEN,
|
||||||
|
PCAN_ST_BODY,
|
||||||
|
PCAN_ST_CRC_L,
|
||||||
|
PCAN_ST_CRC_H
|
||||||
|
} pcan_parse_state_t;
|
||||||
|
|
||||||
|
/** Счётчики разборщика: полезны для диагностики линии. */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t frames; /**< успешно разобрано кадров */
|
||||||
|
uint32_t crc_errors; /**< кадров отброшено по CRC */
|
||||||
|
uint32_t bad_len; /**< LEN вне допустимого диапазона */
|
||||||
|
uint32_t stray_bytes; /**< байт отброшено вне кадров */
|
||||||
|
} pcan_parse_stats_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
pcan_parse_state_t state;
|
||||||
|
uint8_t len;
|
||||||
|
uint8_t idx;
|
||||||
|
uint8_t body[PCAN_LEN_MAX + 1U]; /**< LEN + участок SEQ..DATA */
|
||||||
|
uint16_t crc_rx;
|
||||||
|
pcan_parse_stats_t stats;
|
||||||
|
} pcan_parser_t;
|
||||||
|
|
||||||
|
void pcan_parser_init(pcan_parser_t *p);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Скармливает разборщику один байт.
|
||||||
|
* @return true, если кадр собран целиком и записан в out.
|
||||||
|
*/
|
||||||
|
bool pcan_parser_push(pcan_parser_t *p, uint8_t byte, pcan_frame_t *out);
|
||||||
|
|
||||||
|
/** Колбэк на готовый кадр. */
|
||||||
|
typedef void (*pcan_frame_cb_t)(const pcan_frame_t *frame, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Скармливает разборщику блок байт, вызывая cb на каждый кадр.
|
||||||
|
* @return число разобранных кадров.
|
||||||
|
*/
|
||||||
|
size_t pcan_parser_feed(pcan_parser_t *p, const uint8_t *data, size_t len,
|
||||||
|
pcan_frame_cb_t cb, void *user);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PCAN_FRAME_H */
|
||||||
129
c/protocan-transport/include/pcan_gas.h
Normal file
129
c/protocan-transport/include/pcan_gas.h
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* @file pcan_gas.h
|
||||||
|
* @brief Общее адресное пространство (General Address Space).
|
||||||
|
*
|
||||||
|
* Плоское пространство 16-битных регистров с адресом 0x0000..0xFFFF,
|
||||||
|
* собранное из регионов. Регион либо ссылается на массив в памяти,
|
||||||
|
* либо обслуживается колбэками - так в карту попадают и обычные
|
||||||
|
* переменные, и вычисляемые значения, и регистры периферии.
|
||||||
|
*
|
||||||
|
* Транспорт ProtoCAN (MsgType = 0b0011) кладёт в MsgBody адрес первого
|
||||||
|
* регистра, а в данные - до 4 регистров подряд, младшим байтом вперёд.
|
||||||
|
* Это ровно то, что делает PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()
|
||||||
|
* в SETCAN/Src/protocan.c, поэтому обмен совместим с существующими
|
||||||
|
* устройствами.
|
||||||
|
*
|
||||||
|
* Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде
|
||||||
|
* SETCAN такой кодировки нет, это расширение - см. docs/GAS.md.
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_GAS_H
|
||||||
|
#define PCAN_GAS_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcan_config.h"
|
||||||
|
#include "pcan_frame.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Сколько регистров помещается в один кадр (8 байт / 2). */
|
||||||
|
#define PCAN_GAS_REGS_PER_FRAME 4U
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PCAN_GAS_OK = 0,
|
||||||
|
PCAN_GAS_NO_REG, /**< адрес не покрыт ни одним регионом */
|
||||||
|
PCAN_GAS_READ_ONLY, /**< запись в регион только для чтения */
|
||||||
|
PCAN_GAS_WRITE_ONLY,
|
||||||
|
PCAN_GAS_REJECTED /**< колбэк отверг значение */
|
||||||
|
} pcan_gas_status_t;
|
||||||
|
|
||||||
|
/** Регион только для чтения. */
|
||||||
|
#define PCAN_GAS_RDONLY 0x01U
|
||||||
|
/** Регион только для записи. */
|
||||||
|
#define PCAN_GAS_WRONLY 0x02U
|
||||||
|
|
||||||
|
struct pcan_gas_region;
|
||||||
|
|
||||||
|
typedef pcan_gas_status_t (*pcan_gas_read_fn)(const struct pcan_gas_region *region,
|
||||||
|
uint16_t offset, uint16_t *value);
|
||||||
|
typedef pcan_gas_status_t (*pcan_gas_write_fn)(const struct pcan_gas_region *region,
|
||||||
|
uint16_t offset, uint16_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Непрерывный участок адресного пространства.
|
||||||
|
*
|
||||||
|
* Если storage != NULL, чтение и запись идут прямо в массив.
|
||||||
|
* Иначе вызываются read/write.
|
||||||
|
*/
|
||||||
|
typedef struct pcan_gas_region {
|
||||||
|
uint16_t base; /**< адрес первого регистра */
|
||||||
|
uint16_t count; /**< число регистров */
|
||||||
|
uint16_t *storage; /**< массив либо NULL */
|
||||||
|
pcan_gas_read_fn read;
|
||||||
|
pcan_gas_write_fn write;
|
||||||
|
uint8_t flags;
|
||||||
|
void *user;
|
||||||
|
const char *name; /**< для отладки, может быть NULL */
|
||||||
|
} pcan_gas_region_t;
|
||||||
|
|
||||||
|
/** Карта: набор регионов. Регионы не должны перекрываться. */
|
||||||
|
typedef struct {
|
||||||
|
const pcan_gas_region_t *regions;
|
||||||
|
uint16_t count;
|
||||||
|
} pcan_gas_map_t;
|
||||||
|
|
||||||
|
/** @return true, если регионы отсортированы и не перекрываются. */
|
||||||
|
bool pcan_gas_map_validate(const pcan_gas_map_t *map);
|
||||||
|
|
||||||
|
const pcan_gas_region_t *pcan_gas_find(const pcan_gas_map_t *map, uint16_t addr);
|
||||||
|
|
||||||
|
pcan_gas_status_t pcan_gas_read(const pcan_gas_map_t *map, uint16_t addr,
|
||||||
|
uint16_t *value);
|
||||||
|
pcan_gas_status_t pcan_gas_write(const pcan_gas_map_t *map, uint16_t addr,
|
||||||
|
uint16_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Читает подряд идущие регистры.
|
||||||
|
* @return сколько регистров удалось прочитать, начиная с addr.
|
||||||
|
*
|
||||||
|
* Чтение прекращается на первом адресе, которого нет в карте,
|
||||||
|
* поэтому вызывающий всегда получает непрерывный блок.
|
||||||
|
*/
|
||||||
|
uint16_t pcan_gas_read_block(const pcan_gas_map_t *map, uint16_t addr,
|
||||||
|
uint16_t *out, uint16_t max);
|
||||||
|
|
||||||
|
uint16_t pcan_gas_write_block(const pcan_gas_map_t *map, uint16_t addr,
|
||||||
|
const uint16_t *in, uint16_t count);
|
||||||
|
|
||||||
|
/* --- Мост между картой и кадрами ProtoCAN ---------------------------------- */
|
||||||
|
|
||||||
|
/** Извлекает регистры из данных кадра GAS (младший байт первым). */
|
||||||
|
uint16_t pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out,
|
||||||
|
uint16_t max);
|
||||||
|
|
||||||
|
/** Заполняет кадр GAS: адрес в MsgBody, до 4 регистров в данных. */
|
||||||
|
void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id,
|
||||||
|
uint16_t addr, const uint16_t *regs, uint16_t count);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает входящий кадр GAS.
|
||||||
|
*
|
||||||
|
* DLC = 0 - запрос на чтение: в rsp кладётся до 4 регистров с адреса
|
||||||
|
* MsgBody, Route переключается на FROM_DEVICE.
|
||||||
|
* DLC > 0 - запись: регистры пишутся в карту, ответ не формируется.
|
||||||
|
*
|
||||||
|
* @param[out] rsp кадр ответа; заполняется, только если функция вернула true.
|
||||||
|
* @return true, если ответ нужно передать.
|
||||||
|
*/
|
||||||
|
bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req,
|
||||||
|
pcan_frame_t *rsp);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PCAN_GAS_H */
|
||||||
95
c/protocan-transport/include/pcan_id.h
Normal file
95
c/protocan-transport/include/pcan_id.h
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* @file pcan_id.h
|
||||||
|
* @brief Упаковка и разбор 29-битного идентификатора ProtoCAN.
|
||||||
|
*
|
||||||
|
* Раскладка (соответствует ProtoCanId_t из SETCAN/Inc/protocan.h):
|
||||||
|
*
|
||||||
|
* биты 28 Priority
|
||||||
|
* 27 Route
|
||||||
|
* 26:24 DeviceType
|
||||||
|
* 23:20 DeviceID
|
||||||
|
* 19:16 MsgType
|
||||||
|
* 15:0 MsgBody
|
||||||
|
*
|
||||||
|
* Здесь намеренно не используются битовые поля: их раскладка в C
|
||||||
|
* определяется реализацией, и один и тот же заголовок на другом
|
||||||
|
* компиляторе может дать другой порядок. Сдвиги переносимы всегда.
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_ID_H
|
||||||
|
#define PCAN_ID_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcan_config.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PCAN_ID_MASK 0x1FFFFFFFUL
|
||||||
|
|
||||||
|
/* --- Приоритет и маршрут --------------------------------------------------- */
|
||||||
|
#define PCAN_PRIORITY_CRITICAL 0U
|
||||||
|
#define PCAN_PRIORITY_STANDARD 1U
|
||||||
|
|
||||||
|
#define PCAN_ROUTE_FROM_PM 0U
|
||||||
|
#define PCAN_ROUTE_FROM_DEVICE 1U
|
||||||
|
|
||||||
|
/* --- Типы сообщений -------------------------------------------------------- */
|
||||||
|
typedef enum {
|
||||||
|
PCAN_MSG_BROADCAST = 0x0U,
|
||||||
|
PCAN_MSG_DISCRETE = 0x1U,
|
||||||
|
PCAN_MSG_ANALOG = 0x2U,
|
||||||
|
PCAN_MSG_GAS = 0x3U, /**< общее адресное пространство */
|
||||||
|
PCAN_MSG_MODBUS_COIL = 0x4U,
|
||||||
|
PCAN_MSG_MODBUS_DISCR = 0x5U,
|
||||||
|
PCAN_MSG_MODBUS_HOLDING = 0x6U,
|
||||||
|
PCAN_MSG_MODBUS_INPUT = 0x7U,
|
||||||
|
PCAN_MSG_ERROR = 0x8U,
|
||||||
|
PCAN_MSG_PULSE = 0xFU
|
||||||
|
} pcan_msgtype_t;
|
||||||
|
|
||||||
|
/** Разобранный идентификатор. */
|
||||||
|
typedef struct {
|
||||||
|
uint16_t msg_body;
|
||||||
|
uint8_t msg_type; /**< pcan_msgtype_t */
|
||||||
|
uint8_t device_id; /**< 0..15 */
|
||||||
|
uint8_t device_type; /**< 0..7 */
|
||||||
|
uint8_t route;
|
||||||
|
uint8_t priority;
|
||||||
|
} pcan_id_t;
|
||||||
|
|
||||||
|
uint32_t pcan_id_pack(const pcan_id_t *id);
|
||||||
|
void pcan_id_unpack(uint32_t raw, pcan_id_t *id);
|
||||||
|
|
||||||
|
/* --- Раскладки MsgBody ----------------------------------------------------- */
|
||||||
|
|
||||||
|
/** BROADCAST: Type[15:4], Body[3:0]. */
|
||||||
|
static inline uint16_t pcan_body_broadcast(uint16_t type, uint8_t body)
|
||||||
|
{
|
||||||
|
return (uint16_t)(((type & 0x0FFFU) << 4) | (body & 0x0FU));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DISCRETE / ANALOG: Type[15:12], Body[11:0]. */
|
||||||
|
static inline uint16_t pcan_body_typed(uint8_t type, uint16_t body)
|
||||||
|
{
|
||||||
|
return (uint16_t)(((uint16_t)(type & 0x0FU) << 12) | (body & 0x0FFFU));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MODBUS: StrAdr[15:4], RegCount[3:0]. */
|
||||||
|
static inline uint16_t pcan_body_modbus(uint16_t addr, uint8_t count)
|
||||||
|
{
|
||||||
|
return (uint16_t)(((addr & 0x0FFFU) << 4) | (count & 0x0FU));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ERROR: Info[15:8], Code[7:0]. */
|
||||||
|
static inline uint16_t pcan_body_error(uint8_t info, uint8_t code)
|
||||||
|
{
|
||||||
|
return (uint16_t)(((uint16_t)info << 8) | code);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PCAN_ID_H */
|
||||||
97
c/protocan-transport/include/pcan_link.h
Normal file
97
c/protocan-transport/include/pcan_link.h
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* @file pcan_link.h
|
||||||
|
* @brief Экземпляр канала связи поверх произвольного байтового потока.
|
||||||
|
*
|
||||||
|
* Библиотека не знает ни про UART, ни про DMA, ни про ОС: платформа
|
||||||
|
* отдаёт две функции (записать байты, узнать свободное место) и сама
|
||||||
|
* решает, как их реализовать - опросом, по прерыванию или через DMA.
|
||||||
|
*
|
||||||
|
* Состояния в глобальных переменных нет, поэтому в одной прошивке
|
||||||
|
* поднимается сколько угодно каналов: полевой RS485, отдельный UART
|
||||||
|
* для GUI, USB CDC - каждый со своим pcan_link_t.
|
||||||
|
*
|
||||||
|
* Пример:
|
||||||
|
* @code
|
||||||
|
* static uint8_t txbuf[512];
|
||||||
|
* pcan_link_t link;
|
||||||
|
* pcan_link_init(&link, &io, on_frame, NULL);
|
||||||
|
* pcan_link_send(&link, &frame); // из фонового кода
|
||||||
|
* pcan_link_feed(&link, chunk, n); // из порта, когда пришли байты
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_LINK_H
|
||||||
|
#define PCAN_LINK_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcan_config.h"
|
||||||
|
#include "pcan_frame.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Интерфейс, который реализует платформа.
|
||||||
|
*
|
||||||
|
* write() обязана быть неблокирующей и принимать блок целиком либо
|
||||||
|
* не принимать вовсе - частично отданный кадр сломал бы приёмник.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/** @return число принятых байт: len (принято) либо 0 (не влезло). */
|
||||||
|
size_t (*write)(void *ctx, const uint8_t *data, size_t len);
|
||||||
|
/** Свободное место в очереди передачи, байт. */
|
||||||
|
size_t (*tx_space)(void *ctx);
|
||||||
|
void *ctx;
|
||||||
|
} pcan_io_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t tx_frames;
|
||||||
|
uint32_t tx_dropped; /**< кадров не влезло в очередь передачи */
|
||||||
|
} pcan_link_tx_stats_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
pcan_io_t io;
|
||||||
|
pcan_parser_t parser;
|
||||||
|
pcan_frame_cb_t on_frame;
|
||||||
|
void *user;
|
||||||
|
uint8_t tx_seq;
|
||||||
|
pcan_link_tx_stats_t tx;
|
||||||
|
} pcan_link_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Инициализирует канал.
|
||||||
|
* @param io интерфейс платформы, копируется внутрь;
|
||||||
|
* @param on_frame вызывается на каждый принятый кадр, может быть NULL;
|
||||||
|
* @param user произвольный контекст для on_frame.
|
||||||
|
*/
|
||||||
|
bool pcan_link_init(pcan_link_t *link, const pcan_io_t *io,
|
||||||
|
pcan_frame_cb_t on_frame, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Отправляет кадр, проставив в него очередной SEQ.
|
||||||
|
* @return false, если в очереди передачи не хватило места.
|
||||||
|
*/
|
||||||
|
bool pcan_link_send(pcan_link_t *link, pcan_frame_t *frame);
|
||||||
|
|
||||||
|
/** Как pcan_link_send(), но SEQ берётся из кадра. */
|
||||||
|
bool pcan_link_send_raw(pcan_link_t *link, const pcan_frame_t *frame);
|
||||||
|
|
||||||
|
/** Помещается ли ещё хотя бы один кадр максимального размера. */
|
||||||
|
bool pcan_link_can_send(const pcan_link_t *link);
|
||||||
|
|
||||||
|
/** Скармливает каналу принятые из линии байты. @return число кадров. */
|
||||||
|
size_t pcan_link_feed(pcan_link_t *link, const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
static inline const pcan_parse_stats_t *pcan_link_rx_stats(const pcan_link_t *link)
|
||||||
|
{
|
||||||
|
return &link->parser.stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PCAN_LINK_H */
|
||||||
71
c/protocan-transport/include/pcan_ring.h
Normal file
71
c/protocan-transport/include/pcan_ring.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* @file pcan_ring.h
|
||||||
|
* @brief Кольцевой буфер байтов: один писатель, один читатель.
|
||||||
|
*
|
||||||
|
* Размер обязан быть степенью двойки - индексы сворачиваются маской,
|
||||||
|
* поэтому head/tail обновляются одной записью и блокировки между
|
||||||
|
* фоновым кодом и обработчиком прерывания не нужны.
|
||||||
|
*
|
||||||
|
* Отдельно есть pcan_ring_linear(): отдаёт непрерывный участок данных
|
||||||
|
* без заворота - именно то, что требуется, чтобы отдать кусок в DMA.
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_RING_H
|
||||||
|
#define PCAN_RING_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcan_config.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *buf;
|
||||||
|
uint16_t size; /**< степень двойки */
|
||||||
|
volatile uint16_t head; /**< пишет производитель */
|
||||||
|
volatile uint16_t tail; /**< пишет потребитель */
|
||||||
|
volatile uint32_t dropped; /**< байт потеряно при переполнении */
|
||||||
|
} pcan_ring_t;
|
||||||
|
|
||||||
|
/** @return false, если size не является степенью двойки. */
|
||||||
|
bool pcan_ring_init(pcan_ring_t *rb, uint8_t *storage, uint16_t size);
|
||||||
|
|
||||||
|
uint16_t pcan_ring_count(const pcan_ring_t *rb);
|
||||||
|
uint16_t pcan_ring_free(const pcan_ring_t *rb);
|
||||||
|
|
||||||
|
bool pcan_ring_put(pcan_ring_t *rb, uint8_t byte);
|
||||||
|
bool pcan_ring_get(pcan_ring_t *rb, uint8_t *byte);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Кладёт блок целиком либо не кладёт ничего.
|
||||||
|
*
|
||||||
|
* Всё-или-ничего принципиально: половина кадра в линии заставила бы
|
||||||
|
* приёмник ресинхронизироваться.
|
||||||
|
*/
|
||||||
|
bool pcan_ring_write(pcan_ring_t *rb, const uint8_t *data, uint16_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Непрерывный участок готовых к выдаче данных (без заворота).
|
||||||
|
* @param[out] ptr указатель на начало участка;
|
||||||
|
* @return длина участка; 0 - данных нет.
|
||||||
|
*
|
||||||
|
* После того как участок передан, вызовите pcan_ring_consume().
|
||||||
|
*/
|
||||||
|
uint16_t pcan_ring_linear(const pcan_ring_t *rb, const uint8_t **ptr);
|
||||||
|
|
||||||
|
/** Помечает len байт как прочитанные. */
|
||||||
|
void pcan_ring_consume(pcan_ring_t *rb, uint16_t len);
|
||||||
|
|
||||||
|
static inline bool pcan_ring_empty(const pcan_ring_t *rb)
|
||||||
|
{
|
||||||
|
return rb->head == rb->tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PCAN_RING_H */
|
||||||
27
c/protocan-transport/include/protocan_transport.h
Normal file
27
c/protocan-transport/include/protocan_transport.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* @file protocan_transport.h
|
||||||
|
* @brief Зонтичный заголовок библиотеки. Достаточно подключить его одного.
|
||||||
|
*
|
||||||
|
* Библиотека переносимая: C99, без динамической памяти, без ОС,
|
||||||
|
* без зависимостей от HAL и от конкретного МК. Платформа подключается
|
||||||
|
* через pcan_io_t (см. pcan_link.h) и примеры в ports/.
|
||||||
|
*/
|
||||||
|
#ifndef PROTOCAN_TRANSPORT_H
|
||||||
|
#define PROTOCAN_TRANSPORT_H
|
||||||
|
|
||||||
|
#define PCAN_VERSION_MAJOR 1
|
||||||
|
#define PCAN_VERSION_MINOR 0
|
||||||
|
#define PCAN_VERSION_PATCH 0
|
||||||
|
#define PCAN_VERSION_U16 ((PCAN_VERSION_MAJOR << 8) | \
|
||||||
|
(PCAN_VERSION_MINOR << 4) | \
|
||||||
|
PCAN_VERSION_PATCH)
|
||||||
|
|
||||||
|
#include "pcan_config.h"
|
||||||
|
#include "pcan_crc.h"
|
||||||
|
#include "pcan_frame.h"
|
||||||
|
#include "pcan_gas.h"
|
||||||
|
#include "pcan_id.h"
|
||||||
|
#include "pcan_link.h"
|
||||||
|
#include "pcan_ring.h"
|
||||||
|
|
||||||
|
#endif /* PROTOCAN_TRANSPORT_H */
|
||||||
287
c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.c
Normal file
287
c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.c
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcan_uart_stm32f4.h"
|
||||||
|
|
||||||
|
/* --- Работа с флагами DMA -------------------------------------------------
|
||||||
|
* У DMA на F4 флаги четырёх потоков лежат в LISR/LIFCR, ещё четырёх -
|
||||||
|
* в HISR/HIFCR, причём внутри регистра смещения неравномерны.
|
||||||
|
*/
|
||||||
|
static const uint8_t s_flag_shift[4] = { 0U, 6U, 16U, 22U };
|
||||||
|
|
||||||
|
#define DMA_FLAG_FEIF 0x01U
|
||||||
|
#define DMA_FLAG_DMEIF 0x04U
|
||||||
|
#define DMA_FLAG_TEIF 0x08U
|
||||||
|
#define DMA_FLAG_HTIF 0x10U
|
||||||
|
#define DMA_FLAG_TCIF 0x20U
|
||||||
|
#define DMA_FLAG_ALL (DMA_FLAG_FEIF | DMA_FLAG_DMEIF | DMA_FLAG_TEIF | \
|
||||||
|
DMA_FLAG_HTIF | DMA_FLAG_TCIF)
|
||||||
|
|
||||||
|
static inline uint32_t dma_isr(const DMA_TypeDef *dma, uint8_t stream)
|
||||||
|
{
|
||||||
|
uint32_t reg = (stream < 4U) ? dma->LISR : dma->HISR;
|
||||||
|
return (reg >> s_flag_shift[stream & 3U]) & DMA_FLAG_ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void dma_clear(DMA_TypeDef *dma, uint8_t stream, uint32_t flags)
|
||||||
|
{
|
||||||
|
uint32_t value = (flags & DMA_FLAG_ALL) << s_flag_shift[stream & 3U];
|
||||||
|
if (stream < 4U) {
|
||||||
|
dma->LIFCR = value;
|
||||||
|
} else {
|
||||||
|
dma->HIFCR = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void dma_disable(DMA_Stream_TypeDef *stream)
|
||||||
|
{
|
||||||
|
stream->CR &= ~DMA_SxCR_EN;
|
||||||
|
/* Аппаратура снимает EN не мгновенно; трогать регистры потока
|
||||||
|
до этого момента нельзя. */
|
||||||
|
while ((stream->CR & DMA_SxCR_EN) != 0U) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Управление драйвером RS485 ------------------------------------------- */
|
||||||
|
|
||||||
|
static inline void de_assert(const pcan_uart_t *u)
|
||||||
|
{
|
||||||
|
if (u->cfg.de_port == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t pin = 1UL << u->cfg.de_pin;
|
||||||
|
u->cfg.de_port->BSRR = u->cfg.de_active_low ? (pin << 16) : pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void de_release(const pcan_uart_t *u)
|
||||||
|
{
|
||||||
|
if (u->cfg.de_port == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t pin = 1UL << u->cfg.de_pin;
|
||||||
|
u->cfg.de_port->BSRR = u->cfg.de_active_low ? pin : (pin << 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Передача -------------------------------------------------------------
|
||||||
|
* Кольцо заворачивается, а DMA нужен непрерывный участок, поэтому за раз
|
||||||
|
* отдаём кусок до конца буфера. Остаток уйдёт следующей посылкой.
|
||||||
|
*/
|
||||||
|
static void tx_start(pcan_uart_t *u)
|
||||||
|
{
|
||||||
|
if (u->tx_busy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *ptr = NULL;
|
||||||
|
uint16_t len = pcan_ring_linear(&u->tx, &ptr);
|
||||||
|
if (len == 0U) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
u->tx_busy = 1U;
|
||||||
|
u->tx_chunk = len;
|
||||||
|
|
||||||
|
de_assert(u);
|
||||||
|
|
||||||
|
/* Флаг TC мог остаться от прошлой посылки - иначе прерывание придёт
|
||||||
|
сразу и отпустит DE посреди новой. */
|
||||||
|
(void)u->cfg.uart->SR;
|
||||||
|
u->cfg.uart->SR = ~USART_SR_TC;
|
||||||
|
|
||||||
|
dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL);
|
||||||
|
u->cfg.tx_stream->M0AR = (uint32_t)(uintptr_t)ptr;
|
||||||
|
u->cfg.tx_stream->NDTR = len;
|
||||||
|
u->cfg.tx_stream->CR |= DMA_SxCR_EN;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pcan_uart_dma_tx_irq(pcan_uart_t *u)
|
||||||
|
{
|
||||||
|
uint32_t flags = dma_isr(u->cfg.dma, u->cfg.tx_stream_idx);
|
||||||
|
|
||||||
|
if ((flags & (DMA_FLAG_TEIF | DMA_FLAG_DMEIF | DMA_FLAG_FEIF)) != 0U) {
|
||||||
|
dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL);
|
||||||
|
dma_disable(u->cfg.tx_stream);
|
||||||
|
/* Ошибка передачи: кусок считаем отданным, иначе встанет очередь. */
|
||||||
|
pcan_ring_consume(&u->tx, u->tx_chunk);
|
||||||
|
u->tx_busy = 0U;
|
||||||
|
u->uart_errors++;
|
||||||
|
tx_start(u);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & DMA_FLAG_TCIF) == 0U) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL);
|
||||||
|
dma_disable(u->cfg.tx_stream);
|
||||||
|
pcan_ring_consume(&u->tx, u->tx_chunk);
|
||||||
|
u->tx_chunk = 0U;
|
||||||
|
u->tx_busy = 0U;
|
||||||
|
|
||||||
|
if (!pcan_ring_empty(&u->tx)) {
|
||||||
|
tx_start(u);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Данных больше нет. DMA лишь дописал последний байт в DR - в линии он
|
||||||
|
ещё не весь. Отпускаем DE только по TC. */
|
||||||
|
if (u->cfg.de_port != NULL) {
|
||||||
|
u->cfg.uart->CR1 |= USART_CR1_TCIE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void pcan_uart_irq(pcan_uart_t *u)
|
||||||
|
{
|
||||||
|
uint32_t sr = u->cfg.uart->SR;
|
||||||
|
|
||||||
|
if ((sr & (USART_SR_ORE | USART_SR_FE | USART_SR_NE | USART_SR_PE)) != 0U) {
|
||||||
|
(void)u->cfg.uart->DR; /* чтение SR + DR снимает флаги */
|
||||||
|
u->uart_errors++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((sr & USART_SR_TC) != 0U) &&
|
||||||
|
((u->cfg.uart->CR1 & USART_CR1_TCIE) != 0U)) {
|
||||||
|
u->cfg.uart->SR = ~USART_SR_TC;
|
||||||
|
u->cfg.uart->CR1 &= ~USART_CR1_TCIE;
|
||||||
|
/* Если пока ждали TC успел встать новый кадр - DE не опускаем. */
|
||||||
|
if (!u->tx_busy && pcan_ring_empty(&u->tx)) {
|
||||||
|
de_release(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t io_write(void *ctx, const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
pcan_uart_t *u = (pcan_uart_t *)ctx;
|
||||||
|
|
||||||
|
if (len > 0xFFFFU) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (!pcan_ring_write(&u->tx, data, (uint16_t)len)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
/* Прерывание DMA может добраться до очереди одновременно с нами,
|
||||||
|
но tx_busy защищает от двойного запуска потока. */
|
||||||
|
tx_start(u);
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t io_space(void *ctx)
|
||||||
|
{
|
||||||
|
pcan_uart_t *u = (pcan_uart_t *)ctx;
|
||||||
|
return pcan_ring_free(&u->tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pcan_uart_io(pcan_uart_t *u, pcan_io_t *io)
|
||||||
|
{
|
||||||
|
io->write = io_write;
|
||||||
|
io->tx_space = io_space;
|
||||||
|
io->ctx = u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Приём ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
size_t pcan_uart_read(pcan_uart_t *u, uint8_t *out, size_t max)
|
||||||
|
{
|
||||||
|
uint16_t head = (uint16_t)(u->rx_size - (uint16_t)u->cfg.rx_stream->NDTR);
|
||||||
|
uint16_t tail = u->rx_tail;
|
||||||
|
|
||||||
|
if ((head == tail) || (max == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pending = (uint16_t)((head - tail) & (uint16_t)(u->rx_size - 1U));
|
||||||
|
if (pending > (uint16_t)(u->rx_size - (u->rx_size / 4U))) {
|
||||||
|
/* Кольцо почти догнало нас: часть байт, вероятно, уже затёрта.
|
||||||
|
Точнее аппаратура не скажет - в circular-режиме DMA не
|
||||||
|
сигнализирует переполнение. */
|
||||||
|
u->rx_overruns++;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t n = (pending > max) ? max : pending;
|
||||||
|
for (size_t i = 0U; i < n; i++) {
|
||||||
|
out[i] = u->rx_buf[tail];
|
||||||
|
tail = (uint16_t)((tail + 1U) & (uint16_t)(u->rx_size - 1U));
|
||||||
|
}
|
||||||
|
u->rx_tail = tail;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pcan_uart_poll(pcan_uart_t *u, pcan_link_t *link)
|
||||||
|
{
|
||||||
|
uint8_t chunk[64];
|
||||||
|
size_t frames = 0U;
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
while ((n = pcan_uart_read(u, chunk, sizeof chunk)) != 0U) {
|
||||||
|
frames += pcan_link_feed(link, chunk, n);
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Инициализация -------------------------------------------------------- */
|
||||||
|
|
||||||
|
bool pcan_uart_init(pcan_uart_t *u, const pcan_uart_cfg_t *cfg,
|
||||||
|
uint8_t *tx_buf, uint16_t tx_size,
|
||||||
|
uint8_t *rx_buf, uint16_t rx_size)
|
||||||
|
{
|
||||||
|
if ((u == NULL) || (cfg == NULL) || (cfg->uart == NULL) ||
|
||||||
|
(cfg->dma == NULL) || (cfg->tx_stream == NULL) ||
|
||||||
|
(cfg->rx_stream == NULL) || (cfg->baud == 0U)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((rx_buf == NULL) || (rx_size < 2U) ||
|
||||||
|
((rx_size & (uint16_t)(rx_size - 1U)) != 0U)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
u->cfg = *cfg;
|
||||||
|
if (!pcan_ring_init(&u->tx, tx_buf, tx_size)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
u->rx_buf = rx_buf;
|
||||||
|
u->rx_size = rx_size;
|
||||||
|
u->rx_tail = 0U;
|
||||||
|
u->tx_chunk = 0U;
|
||||||
|
u->tx_busy = 0U;
|
||||||
|
u->rx_overruns = 0U;
|
||||||
|
u->uart_errors = 0U;
|
||||||
|
|
||||||
|
/* -- USART: 8N1, OVER8 = 0 -- */
|
||||||
|
cfg->uart->CR1 = 0U;
|
||||||
|
cfg->uart->CR2 = 0U;
|
||||||
|
cfg->uart->CR3 = 0U;
|
||||||
|
cfg->uart->BRR = (uint16_t)((cfg->pclk_hz + (cfg->baud / 2U)) / cfg->baud);
|
||||||
|
|
||||||
|
/* -- DMA передачи: память -> периферия, обычный режим -- */
|
||||||
|
dma_disable(cfg->tx_stream);
|
||||||
|
dma_clear(cfg->dma, cfg->tx_stream_idx, DMA_FLAG_ALL);
|
||||||
|
cfg->tx_stream->PAR = (uint32_t)(uintptr_t)&cfg->uart->DR;
|
||||||
|
cfg->tx_stream->FCR = 0U; /* прямой режим, без FIFO */
|
||||||
|
cfg->tx_stream->CR =
|
||||||
|
((uint32_t)cfg->tx_channel << DMA_SxCR_CHSEL_Pos)
|
||||||
|
| DMA_SxCR_DIR_0 /* мем -> периферия */
|
||||||
|
| DMA_SxCR_MINC
|
||||||
|
| (2UL << DMA_SxCR_PL_Pos) /* высокий приоритет */
|
||||||
|
| DMA_SxCR_TCIE | DMA_SxCR_TEIE;
|
||||||
|
|
||||||
|
/* -- DMA приёма: периферия -> память, кольцевой, работает всегда -- */
|
||||||
|
dma_disable(cfg->rx_stream);
|
||||||
|
dma_clear(cfg->dma, cfg->rx_stream_idx, DMA_FLAG_ALL);
|
||||||
|
cfg->rx_stream->PAR = (uint32_t)(uintptr_t)&cfg->uart->DR;
|
||||||
|
cfg->rx_stream->M0AR = (uint32_t)(uintptr_t)rx_buf;
|
||||||
|
cfg->rx_stream->NDTR = rx_size;
|
||||||
|
cfg->rx_stream->FCR = 0U;
|
||||||
|
cfg->rx_stream->CR =
|
||||||
|
((uint32_t)cfg->rx_channel << DMA_SxCR_CHSEL_Pos)
|
||||||
|
| DMA_SxCR_MINC
|
||||||
|
| DMA_SxCR_CIRC
|
||||||
|
| (2UL << DMA_SxCR_PL_Pos);
|
||||||
|
cfg->rx_stream->CR |= DMA_SxCR_EN;
|
||||||
|
|
||||||
|
de_release(u);
|
||||||
|
|
||||||
|
cfg->uart->CR3 = USART_CR3_DMAT | USART_CR3_DMAR;
|
||||||
|
cfg->uart->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
110
c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.h
Normal file
110
c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.h
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* @file pcan_uart_stm32f4.h
|
||||||
|
* @brief Порт транспорта на USART + DMA для STM32F4.
|
||||||
|
*
|
||||||
|
* Передача - пакетная: DMA отдаёт непрерывный кусок очереди целиком,
|
||||||
|
* одно прерывание на пакет вместо одного на байт.
|
||||||
|
* Приём - кольцевой DMA, позиция вычисляется по NDTR, поэтому длина
|
||||||
|
* посылки заранее не нужна и байты не теряются между вызовами.
|
||||||
|
*
|
||||||
|
* Чего порт НЕ делает намеренно (это забота платы):
|
||||||
|
* - не включает такты RCC для USART, DMA и GPIO;
|
||||||
|
* - не настраивает выводы и альтернативные функции;
|
||||||
|
* - не разрешает прерывания в NVIC.
|
||||||
|
* Так один и тот же порт живёт на любой плате без правок.
|
||||||
|
*
|
||||||
|
* ВАЖНО про буферы: на STM32F407 DMA не имеет доступа к CCM RAM
|
||||||
|
* (0x10000000). Буферы обязаны лежать в основном SRAM (0x20000000),
|
||||||
|
* иначе передача молча не пойдёт.
|
||||||
|
*
|
||||||
|
* Прерывания, которые нужно прокинуть из вектора:
|
||||||
|
* DMAx_StreamN_IRQHandler -> pcan_uart_dma_tx_irq()
|
||||||
|
* USARTx_IRQHandler -> pcan_uart_irq()
|
||||||
|
*/
|
||||||
|
#ifndef PCAN_UART_STM32F4_H
|
||||||
|
#define PCAN_UART_STM32F4_H
|
||||||
|
|
||||||
|
#include "stm32f4xx.h"
|
||||||
|
|
||||||
|
#include "pcan_link.h"
|
||||||
|
#include "pcan_ring.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
USART_TypeDef *uart;
|
||||||
|
uint32_t pclk_hz; /**< частота шины этого USART */
|
||||||
|
uint32_t baud;
|
||||||
|
|
||||||
|
DMA_TypeDef *dma;
|
||||||
|
DMA_Stream_TypeDef *tx_stream;
|
||||||
|
uint8_t tx_stream_idx; /**< 0..7, нужен для флагов LISR/HISR */
|
||||||
|
uint8_t tx_channel; /**< 0..7 */
|
||||||
|
DMA_Stream_TypeDef *rx_stream;
|
||||||
|
uint8_t rx_stream_idx;
|
||||||
|
uint8_t rx_channel;
|
||||||
|
|
||||||
|
/** Управление драйвером RS485. NULL - обычный полнодуплексный UART. */
|
||||||
|
GPIO_TypeDef *de_port;
|
||||||
|
uint8_t de_pin;
|
||||||
|
uint8_t de_active_low; /**< 1, если DE активен низким уровнем */
|
||||||
|
} pcan_uart_cfg_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
pcan_uart_cfg_t cfg;
|
||||||
|
|
||||||
|
pcan_ring_t tx; /**< очередь передачи */
|
||||||
|
uint8_t *rx_buf; /**< кольцо приёма DMA */
|
||||||
|
uint16_t rx_size;
|
||||||
|
uint16_t rx_tail;
|
||||||
|
|
||||||
|
volatile uint16_t tx_chunk; /**< длина текущей DMA-посылки */
|
||||||
|
volatile uint8_t tx_busy;
|
||||||
|
|
||||||
|
uint32_t rx_overruns; /**< подозрение на потерю в кольце */
|
||||||
|
uint32_t uart_errors; /**< ORE / FE / NE / PE */
|
||||||
|
} pcan_uart_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Настраивает USART и оба потока DMA.
|
||||||
|
* @param tx_buf,tx_size очередь передачи, размер - степень двойки;
|
||||||
|
* @param rx_buf,rx_size кольцо приёма, размер - степень двойки.
|
||||||
|
* @return false при неверных аргументах.
|
||||||
|
*/
|
||||||
|
bool pcan_uart_init(pcan_uart_t *u, const pcan_uart_cfg_t *cfg,
|
||||||
|
uint8_t *tx_buf, uint16_t tx_size,
|
||||||
|
uint8_t *rx_buf, uint16_t rx_size);
|
||||||
|
|
||||||
|
/** Заполняет интерфейс для pcan_link_init(). */
|
||||||
|
void pcan_uart_io(pcan_uart_t *u, pcan_io_t *io);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Забирает принятые DMA байты в буфер вызывающего.
|
||||||
|
*
|
||||||
|
* Порт не знает, какой протокол поверх него живёт, поэтому отдаёт сырые
|
||||||
|
* байты: на одном и том же UART так работают и кадр полевой шины,
|
||||||
|
* и GUI-протокол.
|
||||||
|
*
|
||||||
|
* @return число скопированных байт; 0 - новых данных нет.
|
||||||
|
*/
|
||||||
|
size_t pcan_uart_read(pcan_uart_t *u, uint8_t *out, size_t max);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Отдаёт принятые DMA байты в канал pcan_link. Из главного цикла.
|
||||||
|
* @return число разобранных кадров.
|
||||||
|
*/
|
||||||
|
size_t pcan_uart_poll(pcan_uart_t *u, pcan_link_t *link);
|
||||||
|
|
||||||
|
/** Обработчик прерывания потока DMA передачи. */
|
||||||
|
void pcan_uart_dma_tx_irq(pcan_uart_t *u);
|
||||||
|
|
||||||
|
/** Обработчик прерывания USART: снимает DE по флагу TC, считает ошибки. */
|
||||||
|
void pcan_uart_irq(pcan_uart_t *u);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PCAN_UART_STM32F4_H */
|
||||||
195
c/protocan-transport/src/gui_catalog.c
Normal file
195
c/protocan-transport/src/gui_catalog.c
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
#include "gui_catalog.h"
|
||||||
|
|
||||||
|
static size_t name_len(const char *name)
|
||||||
|
{
|
||||||
|
size_t n = 0U;
|
||||||
|
if (name == NULL) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
while ((name[n] != '\0') && (n <= GUI_NAME_SIZE)) {
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t object_registers(uint8_t type)
|
||||||
|
{
|
||||||
|
return ((type == GUI_OBJ_U32) || (type == GUI_OBJ_I32)) ? 2U : 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gui_catalog_validate(const gui_catalog_t *catalog, const pcan_gas_map_t *map)
|
||||||
|
{
|
||||||
|
if ((catalog == NULL) || ((catalog->count != 0U) && (catalog->items == NULL))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint16_t i = 0U; i < catalog->count; i++) {
|
||||||
|
const gui_object_t *item = &catalog->items[i];
|
||||||
|
if (name_len(item->name) > GUI_NAME_SIZE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (item->type > GUI_OBJ_BITS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (map != NULL) {
|
||||||
|
uint8_t words = object_registers(item->type);
|
||||||
|
for (uint8_t w = 0U; w < words; w++) {
|
||||||
|
uint32_t addr = (uint32_t)item->address + w;
|
||||||
|
if (addr > 0xFFFFUL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pcan_gas_find(map, (uint16_t)addr) == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t gui_catalog_encode(const gui_catalog_t *catalog, uint16_t start_index,
|
||||||
|
uint16_t max_count, uint8_t *out, size_t out_size)
|
||||||
|
{
|
||||||
|
if ((catalog == NULL) || (out == NULL) || (start_index >= catalog->count)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t available = (uint16_t)(catalog->count - start_index);
|
||||||
|
uint16_t count = ((max_count == 0U) || (max_count > available)) ? available : max_count;
|
||||||
|
if (count > GUI_ENTRIES_PER_FRAME) {
|
||||||
|
count = (uint16_t)GUI_ENTRIES_PER_FRAME;
|
||||||
|
}
|
||||||
|
size_t total = (size_t)GUI_CATALOG_HEADER + ((size_t)count * GUI_ENTRY_SIZE);
|
||||||
|
if (out_size < total) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
gui_put_u16(&out[0], catalog->count);
|
||||||
|
gui_put_u16(&out[2], start_index);
|
||||||
|
gui_put_u16(&out[4], count);
|
||||||
|
|
||||||
|
for (uint16_t i = 0U; i < count; i++) {
|
||||||
|
const gui_object_t *item = &catalog->items[start_index + i];
|
||||||
|
uint8_t *dst = &out[GUI_CATALOG_HEADER + ((size_t)i * GUI_ENTRY_SIZE)];
|
||||||
|
|
||||||
|
gui_put_u16(&dst[0], item->address);
|
||||||
|
dst[2] = item->type;
|
||||||
|
dst[3] = item->flags;
|
||||||
|
dst[4] = (uint8_t)item->scale_pow10;
|
||||||
|
dst[5] = item->unit;
|
||||||
|
dst[6] = 0U;
|
||||||
|
dst[7] = 0U;
|
||||||
|
|
||||||
|
size_t n = name_len(item->name);
|
||||||
|
if (n > GUI_NAME_SIZE) {
|
||||||
|
n = GUI_NAME_SIZE;
|
||||||
|
}
|
||||||
|
for (size_t c = 0U; c < n; c++) {
|
||||||
|
dst[8U + c] = (uint8_t)item->name[c];
|
||||||
|
}
|
||||||
|
for (size_t c = n; c < GUI_NAME_SIZE; c++) {
|
||||||
|
dst[8U + c] = 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gui_watch_init(gui_watch_t *watch)
|
||||||
|
{
|
||||||
|
watch->period_ms = 0U;
|
||||||
|
watch->count = 0U;
|
||||||
|
watch->next_ms = 0U;
|
||||||
|
watch->sent = 0U;
|
||||||
|
watch->skipped = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t gui_watch_apply(gui_watch_t *watch, const pcan_gas_map_t *map,
|
||||||
|
const uint8_t *payload, uint16_t size)
|
||||||
|
{
|
||||||
|
if (size < 4U) {
|
||||||
|
return 0xFFFFU;
|
||||||
|
}
|
||||||
|
uint16_t period = gui_get_u16(&payload[0]);
|
||||||
|
uint16_t requested = gui_get_u16(&payload[2]);
|
||||||
|
if (size != (uint16_t)(4U + (requested * 2U))) {
|
||||||
|
return 0xFFFFU;
|
||||||
|
}
|
||||||
|
if (requested > GUI_WATCH_MAX) {
|
||||||
|
return 0xFFFFU;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t accepted = 0U;
|
||||||
|
for (uint16_t i = 0U; i < requested; i++) {
|
||||||
|
uint16_t address = gui_get_u16(&payload[4U + (i * 2U)]);
|
||||||
|
/* Адрес вне карты молча пропускаем: иначе одна опечатка в GUI
|
||||||
|
отменяла бы всю подписку. */
|
||||||
|
if ((map != NULL) && (pcan_gas_find(map, address) == NULL)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
watch->address[accepted++] = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch->count = accepted;
|
||||||
|
watch->period_ms = (accepted == 0U) ? 0U : period;
|
||||||
|
watch->next_ms = 0U;
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t gui_watch_encode_ack(const gui_watch_t *watch, uint8_t *out, size_t out_size)
|
||||||
|
{
|
||||||
|
if (out_size < 4U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
gui_put_u16(&out[0], watch->period_ms);
|
||||||
|
gui_put_u16(&out[2], watch->count);
|
||||||
|
return 4U;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gui_watch_due(const gui_watch_t *watch, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
if ((watch->period_ms == 0U) || (watch->count == 0U)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* Разность беззнаковая: переполнение счётчика миллисекунд обрабатывается
|
||||||
|
само собой, сравнение с абсолютным временем - нет. */
|
||||||
|
return (uint32_t)(now_ms - watch->next_ms) < 0x80000000UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t gui_watch_encode_data(const gui_watch_t *watch, const pcan_gas_map_t *map,
|
||||||
|
uint32_t timestamp_ms, uint8_t *out, size_t out_size)
|
||||||
|
{
|
||||||
|
if ((watch->count == 0U) || (out == NULL)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
size_t total = 6U + ((size_t)watch->count * 2U);
|
||||||
|
if (out_size < total) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
gui_put_u32(&out[0], timestamp_ms);
|
||||||
|
gui_put_u16(&out[4], watch->count);
|
||||||
|
for (uint16_t i = 0U; i < watch->count; i++) {
|
||||||
|
uint16_t value = 0U;
|
||||||
|
/* Регистр мог исчезнуть между подпиской и опросом - тогда 0,
|
||||||
|
а не мусор из стека. */
|
||||||
|
(void)pcan_gas_read(map, watch->address[i], &value);
|
||||||
|
gui_put_u16(&out[6U + ((size_t)i * 2U)], value);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gui_watch_advance(gui_watch_t *watch, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
if (watch->period_ms == 0U) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t next = watch->next_ms + watch->period_ms;
|
||||||
|
/* Если отстали больше чем на такт (линия была занята), не пытаемся
|
||||||
|
нагнать пачкой пакетов - идём от текущего момента. */
|
||||||
|
if ((uint32_t)(now_ms - next) >= 0x80000000UL) {
|
||||||
|
watch->next_ms = next;
|
||||||
|
} else {
|
||||||
|
watch->next_ms = now_ms + watch->period_ms;
|
||||||
|
}
|
||||||
|
watch->sent++;
|
||||||
|
}
|
||||||
230
c/protocan-transport/src/gui_frame.c
Normal file
230
c/protocan-transport/src/gui_frame.c
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#include "gui_frame.h"
|
||||||
|
|
||||||
|
uint32_t gui_crc32(const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
uint32_t crc = 0xFFFFFFFFUL;
|
||||||
|
for (size_t i = 0U; i < len; i++) {
|
||||||
|
crc ^= data[i];
|
||||||
|
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||||
|
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc ^ 0xFFFFFFFFUL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t gui_frame_encode(uint8_t type, uint16_t sequence,
|
||||||
|
const uint8_t *payload, uint16_t size,
|
||||||
|
uint8_t *out, size_t out_size)
|
||||||
|
{
|
||||||
|
if ((out == NULL) || (size > GUI_MAX_PAYLOAD) ||
|
||||||
|
((size > 0U) && (payload == NULL))) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
size_t total = (size_t)GUI_HEADER_SIZE + size + GUI_CRC_SIZE;
|
||||||
|
if (out_size < total) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
out[0] = GUI_SOF0;
|
||||||
|
out[1] = GUI_SOF1;
|
||||||
|
/* CRC считается от version до конца payload; SOF в него не входит. */
|
||||||
|
out[2] = GUI_PROTOCOL_VERSION;
|
||||||
|
out[3] = type;
|
||||||
|
out[4] = (uint8_t)(sequence >> 8); /* заголовок big-endian */
|
||||||
|
out[5] = (uint8_t)(sequence & 0xFFU);
|
||||||
|
out[6] = (uint8_t)(size >> 8);
|
||||||
|
out[7] = (uint8_t)(size & 0xFFU);
|
||||||
|
for (uint16_t i = 0U; i < size; i++) {
|
||||||
|
out[GUI_HEADER_SIZE + i] = payload[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t crc = gui_crc32(&out[2], (size_t)6U + size);
|
||||||
|
gui_put_u32(&out[GUI_HEADER_SIZE + size], crc); /* CRC little-endian */
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gui_parser_init(gui_parser_t *p)
|
||||||
|
{
|
||||||
|
p->state = GUI_ST_SOF0;
|
||||||
|
p->idx = 0U;
|
||||||
|
p->size = 0U;
|
||||||
|
p->stats.frames = 0U;
|
||||||
|
p->stats.crc_errors = 0U;
|
||||||
|
p->stats.version_errors = 0U;
|
||||||
|
p->stats.length_errors = 0U;
|
||||||
|
p->stats.stray_bytes = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gui_parser_push(gui_parser_t *p, uint8_t byte, gui_frame_t *out)
|
||||||
|
{
|
||||||
|
switch (p->state) {
|
||||||
|
case GUI_ST_SOF0:
|
||||||
|
if (byte == GUI_SOF0) {
|
||||||
|
p->state = GUI_ST_SOF1;
|
||||||
|
} else {
|
||||||
|
p->stats.stray_bytes++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GUI_ST_SOF1:
|
||||||
|
if (byte == GUI_SOF1) {
|
||||||
|
p->idx = 0U;
|
||||||
|
p->state = GUI_ST_HEADER;
|
||||||
|
} else if (byte == GUI_SOF0) {
|
||||||
|
/* A5 A5 5A - тоже валидное начало, остаёмся здесь. */
|
||||||
|
p->stats.stray_bytes++;
|
||||||
|
} else {
|
||||||
|
p->stats.stray_bytes += 2U;
|
||||||
|
p->state = GUI_ST_SOF0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GUI_ST_HEADER:
|
||||||
|
/* Версию проверяем сразу на первом байте заголовка. Если ждать все
|
||||||
|
шесть, ложная сигнатура успеет проглотить начало настоящего кадра
|
||||||
|
и он будет потерян целиком. */
|
||||||
|
if ((p->idx == 0U) && (byte != GUI_PROTOCOL_VERSION)) {
|
||||||
|
p->stats.version_errors++;
|
||||||
|
p->state = (byte == GUI_SOF0) ? GUI_ST_SOF1 : GUI_ST_SOF0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p->header[p->idx++] = byte;
|
||||||
|
if (p->idx < 6U) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p->size = (uint16_t)(((uint16_t)p->header[4] << 8) | p->header[5]);
|
||||||
|
if (p->size > GUI_RX_PAYLOAD_MAX) {
|
||||||
|
/* Либо мусор, либо кадр, который прибор обслужить не может.
|
||||||
|
И то и другое - повод вернуться к поиску сигнатуры. */
|
||||||
|
p->stats.length_errors++;
|
||||||
|
p->state = GUI_ST_SOF0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p->idx = 0U;
|
||||||
|
p->state = (p->size > 0U) ? GUI_ST_PAYLOAD : GUI_ST_CRC;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GUI_ST_PAYLOAD:
|
||||||
|
p->payload[p->idx++] = byte;
|
||||||
|
if (p->idx >= p->size) {
|
||||||
|
p->idx = 0U;
|
||||||
|
p->state = GUI_ST_CRC;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GUI_ST_CRC: {
|
||||||
|
p->crc_bytes[p->idx++] = byte;
|
||||||
|
if (p->idx < GUI_CRC_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p->state = GUI_ST_SOF0;
|
||||||
|
|
||||||
|
uint32_t received = ((uint32_t)p->crc_bytes[0])
|
||||||
|
| ((uint32_t)p->crc_bytes[1] << 8)
|
||||||
|
| ((uint32_t)p->crc_bytes[2] << 16)
|
||||||
|
| ((uint32_t)p->crc_bytes[3] << 24);
|
||||||
|
|
||||||
|
/* CRC защищает header[0..5] и payload; считаем по частям,
|
||||||
|
чтобы не собирать кадр целиком во втором буфере. */
|
||||||
|
uint32_t crc = 0xFFFFFFFFUL;
|
||||||
|
for (uint8_t i = 0U; i < 6U; i++) {
|
||||||
|
crc ^= p->header[i];
|
||||||
|
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||||
|
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (uint16_t i = 0U; i < p->size; i++) {
|
||||||
|
crc ^= p->payload[i];
|
||||||
|
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||||
|
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crc ^= 0xFFFFFFFFUL;
|
||||||
|
|
||||||
|
if (crc != received) {
|
||||||
|
p->stats.crc_errors++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->type = p->header[1];
|
||||||
|
out->sequence = (uint16_t)(((uint16_t)p->header[2] << 8) | p->header[3]);
|
||||||
|
out->size = p->size;
|
||||||
|
out->payload = p->payload;
|
||||||
|
p->stats.frames++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
p->state = GUI_ST_SOF0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t gui_parser_feed(gui_parser_t *p, const uint8_t *data, size_t len,
|
||||||
|
gui_frame_cb_t cb, void *user)
|
||||||
|
{
|
||||||
|
gui_frame_t frame;
|
||||||
|
size_t count = 0U;
|
||||||
|
|
||||||
|
for (size_t i = 0U; i < len; i++) {
|
||||||
|
if (gui_parser_push(p, data[i], &frame)) {
|
||||||
|
count++;
|
||||||
|
if (cb != NULL) {
|
||||||
|
cb(&frame, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gui_link_init(gui_link_t *link, const pcan_io_t *io,
|
||||||
|
gui_frame_cb_t on_frame, void *user)
|
||||||
|
{
|
||||||
|
if ((link == NULL) || (io == NULL) || (io->write == NULL)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
link->io = *io;
|
||||||
|
link->on_frame = on_frame;
|
||||||
|
link->user = user;
|
||||||
|
link->tx_frames = 0U;
|
||||||
|
link->tx_dropped = 0U;
|
||||||
|
gui_parser_init(&link->parser);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gui_link_can_send(const gui_link_t *link, uint16_t size)
|
||||||
|
{
|
||||||
|
if (link->io.tx_space == NULL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return link->io.tx_space(link->io.ctx) >= ((size_t)GUI_HEADER_SIZE + size + GUI_CRC_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gui_link_send(gui_link_t *link, uint8_t type, uint16_t sequence,
|
||||||
|
const uint8_t *payload, uint16_t size)
|
||||||
|
{
|
||||||
|
size_t n = gui_frame_encode(type, sequence, payload, size,
|
||||||
|
link->tx_buf, sizeof link->tx_buf);
|
||||||
|
if (n == 0U) {
|
||||||
|
link->tx_dropped++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (link->io.write(link->io.ctx, link->tx_buf, n) != n) {
|
||||||
|
link->tx_dropped++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
link->tx_frames++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gui_link_nack(gui_link_t *link, uint16_t sequence, uint16_t result)
|
||||||
|
{
|
||||||
|
uint8_t payload[2];
|
||||||
|
gui_put_u16(payload, result);
|
||||||
|
return gui_link_send(link, GUI_MSG_NACK, sequence, payload, sizeof payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t gui_link_feed(gui_link_t *link, const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
return gui_parser_feed(&link->parser, data, len, link->on_frame, link->user);
|
||||||
|
}
|
||||||
66
c/protocan-transport/src/pcan_crc.c
Normal file
66
c/protocan-transport/src/pcan_crc.c
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#include "pcan_crc.h"
|
||||||
|
|
||||||
|
#if PCAN_CRC_TABLE
|
||||||
|
|
||||||
|
static const uint16_t s_table[256] = {
|
||||||
|
0x0000U, 0x1021U, 0x2042U, 0x3063U, 0x4084U, 0x50A5U, 0x60C6U, 0x70E7U,
|
||||||
|
0x8108U, 0x9129U, 0xA14AU, 0xB16BU, 0xC18CU, 0xD1ADU, 0xE1CEU, 0xF1EFU,
|
||||||
|
0x1231U, 0x0210U, 0x3273U, 0x2252U, 0x52B5U, 0x4294U, 0x72F7U, 0x62D6U,
|
||||||
|
0x9339U, 0x8318U, 0xB37BU, 0xA35AU, 0xD3BDU, 0xC39CU, 0xF3FFU, 0xE3DEU,
|
||||||
|
0x2462U, 0x3443U, 0x0420U, 0x1401U, 0x64E6U, 0x74C7U, 0x44A4U, 0x5485U,
|
||||||
|
0xA56AU, 0xB54BU, 0x8528U, 0x9509U, 0xE5EEU, 0xF5CFU, 0xC5ACU, 0xD58DU,
|
||||||
|
0x3653U, 0x2672U, 0x1611U, 0x0630U, 0x76D7U, 0x66F6U, 0x5695U, 0x46B4U,
|
||||||
|
0xB75BU, 0xA77AU, 0x9719U, 0x8738U, 0xF7DFU, 0xE7FEU, 0xD79DU, 0xC7BCU,
|
||||||
|
0x48C4U, 0x58E5U, 0x6886U, 0x78A7U, 0x0840U, 0x1861U, 0x2802U, 0x3823U,
|
||||||
|
0xC9CCU, 0xD9EDU, 0xE98EU, 0xF9AFU, 0x8948U, 0x9969U, 0xA90AU, 0xB92BU,
|
||||||
|
0x5AF5U, 0x4AD4U, 0x7AB7U, 0x6A96U, 0x1A71U, 0x0A50U, 0x3A33U, 0x2A12U,
|
||||||
|
0xDBFDU, 0xCBDCU, 0xFBBFU, 0xEB9EU, 0x9B79U, 0x8B58U, 0xBB3BU, 0xAB1AU,
|
||||||
|
0x6CA6U, 0x7C87U, 0x4CE4U, 0x5CC5U, 0x2C22U, 0x3C03U, 0x0C60U, 0x1C41U,
|
||||||
|
0xEDAEU, 0xFD8FU, 0xCDECU, 0xDDCDU, 0xAD2AU, 0xBD0BU, 0x8D68U, 0x9D49U,
|
||||||
|
0x7E97U, 0x6EB6U, 0x5ED5U, 0x4EF4U, 0x3E13U, 0x2E32U, 0x1E51U, 0x0E70U,
|
||||||
|
0xFF9FU, 0xEFBEU, 0xDFDDU, 0xCFFCU, 0xBF1BU, 0xAF3AU, 0x9F59U, 0x8F78U,
|
||||||
|
0x9188U, 0x81A9U, 0xB1CAU, 0xA1EBU, 0xD10CU, 0xC12DU, 0xF14EU, 0xE16FU,
|
||||||
|
0x1080U, 0x00A1U, 0x30C2U, 0x20E3U, 0x5004U, 0x4025U, 0x7046U, 0x6067U,
|
||||||
|
0x83B9U, 0x9398U, 0xA3FBU, 0xB3DAU, 0xC33DU, 0xD31CU, 0xE37FU, 0xF35EU,
|
||||||
|
0x02B1U, 0x1290U, 0x22F3U, 0x32D2U, 0x4235U, 0x5214U, 0x6277U, 0x7256U,
|
||||||
|
0xB5EAU, 0xA5CBU, 0x95A8U, 0x8589U, 0xF56EU, 0xE54FU, 0xD52CU, 0xC50DU,
|
||||||
|
0x34E2U, 0x24C3U, 0x14A0U, 0x0481U, 0x7466U, 0x6447U, 0x5424U, 0x4405U,
|
||||||
|
0xA7DBU, 0xB7FAU, 0x8799U, 0x97B8U, 0xE75FU, 0xF77EU, 0xC71DU, 0xD73CU,
|
||||||
|
0x26D3U, 0x36F2U, 0x0691U, 0x16B0U, 0x6657U, 0x7676U, 0x4615U, 0x5634U,
|
||||||
|
0xD94CU, 0xC96DU, 0xF90EU, 0xE92FU, 0x99C8U, 0x89E9U, 0xB98AU, 0xA9ABU,
|
||||||
|
0x5844U, 0x4865U, 0x7806U, 0x6827U, 0x18C0U, 0x08E1U, 0x3882U, 0x28A3U,
|
||||||
|
0xCB7DU, 0xDB5CU, 0xEB3FU, 0xFB1EU, 0x8BF9U, 0x9BD8U, 0xABBBU, 0xBB9AU,
|
||||||
|
0x4A75U, 0x5A54U, 0x6A37U, 0x7A16U, 0x0AF1U, 0x1AD0U, 0x2AB3U, 0x3A92U,
|
||||||
|
0xFD2EU, 0xED0FU, 0xDD6CU, 0xCD4DU, 0xBDAAU, 0xAD8BU, 0x9DE8U, 0x8DC9U,
|
||||||
|
0x7C26U, 0x6C07U, 0x5C64U, 0x4C45U, 0x3CA2U, 0x2C83U, 0x1CE0U, 0x0CC1U,
|
||||||
|
0xEF1FU, 0xFF3EU, 0xCF5DU, 0xDF7CU, 0xAF9BU, 0xBFBAU, 0x8FD9U, 0x9FF8U,
|
||||||
|
0x6E17U, 0x7E36U, 0x4E55U, 0x5E74U, 0x2E93U, 0x3EB2U, 0x0ED1U, 0x1EF0U
|
||||||
|
};
|
||||||
|
|
||||||
|
uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte)
|
||||||
|
{
|
||||||
|
return (uint16_t)((crc << 8) ^ s_table[(uint8_t)((crc >> 8) ^ byte)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else /* побитовый расчёт: 0 байт таблиц, ~8 итераций на байт */
|
||||||
|
|
||||||
|
uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte)
|
||||||
|
{
|
||||||
|
crc ^= (uint16_t)((uint16_t)byte << 8);
|
||||||
|
for (uint8_t bit = 0U; bit < 8U; bit++) {
|
||||||
|
crc = (crc & 0x8000U) ? (uint16_t)((crc << 1) ^ 0x1021U)
|
||||||
|
: (uint16_t)(crc << 1);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint16_t pcan_crc16(const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
uint16_t crc = PCAN_CRC_INIT;
|
||||||
|
for (size_t i = 0U; i < len; i++) {
|
||||||
|
crc = pcan_crc16_update(crc, data[i]);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
141
c/protocan-transport/src/pcan_frame.c
Normal file
141
c/protocan-transport/src/pcan_frame.c
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#include "pcan_frame.h"
|
||||||
|
#include "pcan_crc.h"
|
||||||
|
|
||||||
|
size_t pcan_frame_encode(const pcan_frame_t *frame, uint8_t *out, size_t out_size)
|
||||||
|
{
|
||||||
|
if ((frame == NULL) || (out == NULL) || (out_size < PCAN_FRAME_MAX)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t dlc = (frame->dlc > PCAN_DATA_MAX) ? (uint8_t)PCAN_DATA_MAX : frame->dlc;
|
||||||
|
size_t n = 0U;
|
||||||
|
|
||||||
|
out[n++] = PCAN_SOF0;
|
||||||
|
out[n++] = PCAN_SOF1;
|
||||||
|
|
||||||
|
size_t body = n;
|
||||||
|
out[n++] = (uint8_t)(PCAN_LEN_MIN + dlc);
|
||||||
|
out[n++] = frame->seq;
|
||||||
|
out[n++] = frame->flags;
|
||||||
|
out[n++] = (uint8_t)(frame->id & 0xFFU);
|
||||||
|
out[n++] = (uint8_t)((frame->id >> 8) & 0xFFU);
|
||||||
|
out[n++] = (uint8_t)((frame->id >> 16) & 0xFFU);
|
||||||
|
out[n++] = (uint8_t)((frame->id >> 24) & 0x1FU);
|
||||||
|
for (uint8_t i = 0U; i < dlc; i++) {
|
||||||
|
out[n++] = frame->data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t crc = pcan_crc16(&out[body], n - body);
|
||||||
|
out[n++] = (uint8_t)(crc & 0xFFU);
|
||||||
|
out[n++] = (uint8_t)(crc >> 8);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pcan_parser_init(pcan_parser_t *p)
|
||||||
|
{
|
||||||
|
p->state = PCAN_ST_SOF0;
|
||||||
|
p->len = 0U;
|
||||||
|
p->idx = 0U;
|
||||||
|
p->crc_rx = 0U;
|
||||||
|
p->stats.frames = 0U;
|
||||||
|
p->stats.crc_errors = 0U;
|
||||||
|
p->stats.bad_len = 0U;
|
||||||
|
p->stats.stray_bytes = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcan_parser_push(pcan_parser_t *p, uint8_t byte, pcan_frame_t *out)
|
||||||
|
{
|
||||||
|
switch (p->state) {
|
||||||
|
case PCAN_ST_SOF0:
|
||||||
|
if (byte == PCAN_SOF0) {
|
||||||
|
p->state = PCAN_ST_SOF1;
|
||||||
|
} else {
|
||||||
|
p->stats.stray_bytes++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PCAN_ST_SOF1:
|
||||||
|
if (byte == PCAN_SOF1) {
|
||||||
|
p->state = PCAN_ST_LEN;
|
||||||
|
} else if (byte == PCAN_SOF0) {
|
||||||
|
/* 0xAA 0xAA 0x55 - тоже валидное начало, остаёмся здесь. */
|
||||||
|
p->stats.stray_bytes++;
|
||||||
|
} else {
|
||||||
|
p->stats.stray_bytes += 2U;
|
||||||
|
p->state = PCAN_ST_SOF0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PCAN_ST_LEN:
|
||||||
|
if ((byte < PCAN_LEN_MIN) || (byte > PCAN_LEN_MAX)) {
|
||||||
|
p->stats.bad_len++;
|
||||||
|
/* Байт мог оказаться началом настоящего кадра. */
|
||||||
|
p->state = (byte == PCAN_SOF0) ? PCAN_ST_SOF1 : PCAN_ST_SOF0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p->len = byte;
|
||||||
|
p->body[0] = byte;
|
||||||
|
p->idx = 1U;
|
||||||
|
p->state = PCAN_ST_BODY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PCAN_ST_BODY:
|
||||||
|
p->body[p->idx++] = byte;
|
||||||
|
if (p->idx >= (uint8_t)(p->len + 1U)) {
|
||||||
|
p->state = PCAN_ST_CRC_L;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PCAN_ST_CRC_L:
|
||||||
|
p->crc_rx = byte;
|
||||||
|
p->state = PCAN_ST_CRC_H;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PCAN_ST_CRC_H: {
|
||||||
|
p->crc_rx |= (uint16_t)((uint16_t)byte << 8);
|
||||||
|
p->state = PCAN_ST_SOF0;
|
||||||
|
if (p->crc_rx != pcan_crc16(p->body, (size_t)p->len + 1U)) {
|
||||||
|
p->stats.crc_errors++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->seq = p->body[1];
|
||||||
|
out->flags = p->body[2];
|
||||||
|
out->id = ((uint32_t)p->body[3])
|
||||||
|
| ((uint32_t)p->body[4] << 8)
|
||||||
|
| ((uint32_t)p->body[5] << 16)
|
||||||
|
| ((uint32_t)p->body[6] << 24);
|
||||||
|
out->id &= 0x1FFFFFFFUL;
|
||||||
|
out->dlc = (uint8_t)(p->len - PCAN_LEN_MIN);
|
||||||
|
for (uint8_t i = 0U; i < out->dlc; i++) {
|
||||||
|
out->data[i] = p->body[7U + i];
|
||||||
|
}
|
||||||
|
for (uint8_t i = out->dlc; i < PCAN_DATA_MAX; i++) {
|
||||||
|
out->data[i] = 0U;
|
||||||
|
}
|
||||||
|
p->stats.frames++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
p->state = PCAN_ST_SOF0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pcan_parser_feed(pcan_parser_t *p, const uint8_t *data, size_t len,
|
||||||
|
pcan_frame_cb_t cb, void *user)
|
||||||
|
{
|
||||||
|
pcan_frame_t frame;
|
||||||
|
size_t count = 0U;
|
||||||
|
|
||||||
|
for (size_t i = 0U; i < len; i++) {
|
||||||
|
if (pcan_parser_push(p, data[i], &frame)) {
|
||||||
|
count++;
|
||||||
|
if (cb != NULL) {
|
||||||
|
cb(&frame, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
200
c/protocan-transport/src/pcan_gas.c
Normal file
200
c/protocan-transport/src/pcan_gas.c
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
#include "pcan_gas.h"
|
||||||
|
#include "pcan_id.h"
|
||||||
|
|
||||||
|
bool pcan_gas_map_validate(const pcan_gas_map_t *map)
|
||||||
|
{
|
||||||
|
if ((map == NULL) || ((map->count != 0U) && (map->regions == NULL))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (map->count > PCAN_GAS_MAX_REGIONS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t prev_end = 0U;
|
||||||
|
for (uint16_t i = 0U; i < map->count; i++) {
|
||||||
|
const pcan_gas_region_t *r = &map->regions[i];
|
||||||
|
if (r->count == 0U) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* Регион не должен уходить за 0xFFFF. */
|
||||||
|
if (((uint32_t)r->base + r->count) > 0x10000UL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((r->storage == NULL) && (r->read == NULL) && (r->write == NULL)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((i != 0U) && ((uint32_t)r->base < prev_end)) {
|
||||||
|
return false; /* не отсортировано либо перекрытие */
|
||||||
|
}
|
||||||
|
prev_end = (uint32_t)r->base + r->count;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pcan_gas_region_t *pcan_gas_find(const pcan_gas_map_t *map, uint16_t addr)
|
||||||
|
{
|
||||||
|
if ((map == NULL) || (map->regions == NULL)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
/* Регионы отсортированы, поэтому двоичный поиск. */
|
||||||
|
uint16_t lo = 0U;
|
||||||
|
uint16_t hi = map->count;
|
||||||
|
while (lo < hi) {
|
||||||
|
uint16_t mid = (uint16_t)(lo + ((hi - lo) / 2U));
|
||||||
|
const pcan_gas_region_t *r = &map->regions[mid];
|
||||||
|
if (addr < r->base) {
|
||||||
|
hi = mid;
|
||||||
|
} else if ((uint32_t)addr >= ((uint32_t)r->base + r->count)) {
|
||||||
|
lo = (uint16_t)(mid + 1U);
|
||||||
|
} else {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pcan_gas_status_t pcan_gas_read(const pcan_gas_map_t *map, uint16_t addr,
|
||||||
|
uint16_t *value)
|
||||||
|
{
|
||||||
|
const pcan_gas_region_t *r = pcan_gas_find(map, addr);
|
||||||
|
if (r == NULL) {
|
||||||
|
return PCAN_GAS_NO_REG;
|
||||||
|
}
|
||||||
|
if ((r->flags & PCAN_GAS_WRONLY) != 0U) {
|
||||||
|
return PCAN_GAS_WRITE_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t offset = (uint16_t)(addr - r->base);
|
||||||
|
if (r->read != NULL) {
|
||||||
|
return r->read(r, offset, value);
|
||||||
|
}
|
||||||
|
if (r->storage != NULL) {
|
||||||
|
*value = r->storage[offset];
|
||||||
|
return PCAN_GAS_OK;
|
||||||
|
}
|
||||||
|
return PCAN_GAS_WRITE_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
pcan_gas_status_t pcan_gas_write(const pcan_gas_map_t *map, uint16_t addr,
|
||||||
|
uint16_t value)
|
||||||
|
{
|
||||||
|
const pcan_gas_region_t *r = pcan_gas_find(map, addr);
|
||||||
|
if (r == NULL) {
|
||||||
|
return PCAN_GAS_NO_REG;
|
||||||
|
}
|
||||||
|
if ((r->flags & PCAN_GAS_RDONLY) != 0U) {
|
||||||
|
return PCAN_GAS_READ_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t offset = (uint16_t)(addr - r->base);
|
||||||
|
if (r->write != NULL) {
|
||||||
|
return r->write(r, offset, value);
|
||||||
|
}
|
||||||
|
if (r->storage != NULL) {
|
||||||
|
r->storage[offset] = value;
|
||||||
|
return PCAN_GAS_OK;
|
||||||
|
}
|
||||||
|
return PCAN_GAS_READ_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pcan_gas_read_block(const pcan_gas_map_t *map, uint16_t addr,
|
||||||
|
uint16_t *out, uint16_t max)
|
||||||
|
{
|
||||||
|
uint16_t n = 0U;
|
||||||
|
while (n < max) {
|
||||||
|
/* Останавливаемся на 0xFFFF, чтобы адрес не завернулся в 0. */
|
||||||
|
if (pcan_gas_read(map, (uint16_t)(addr + n), &out[n]) != PCAN_GAS_OK) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
if (((uint32_t)addr + n) > 0xFFFFUL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pcan_gas_write_block(const pcan_gas_map_t *map, uint16_t addr,
|
||||||
|
const uint16_t *in, uint16_t count)
|
||||||
|
{
|
||||||
|
uint16_t n = 0U;
|
||||||
|
while (n < count) {
|
||||||
|
if (pcan_gas_write(map, (uint16_t)(addr + n), in[n]) != PCAN_GAS_OK) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
if (((uint32_t)addr + n) > 0xFFFFUL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out,
|
||||||
|
uint16_t max)
|
||||||
|
{
|
||||||
|
uint16_t n = 0U;
|
||||||
|
for (uint8_t i = 0U; (uint8_t)(i + 1U) < frame->dlc; i = (uint8_t)(i + 2U)) {
|
||||||
|
if (n >= max) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out[n++] = (uint16_t)((uint16_t)frame->data[i] |
|
||||||
|
((uint16_t)frame->data[i + 1U] << 8));
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id,
|
||||||
|
uint16_t addr, const uint16_t *regs, uint16_t count)
|
||||||
|
{
|
||||||
|
if (count > PCAN_GAS_REGS_PER_FRAME) {
|
||||||
|
count = PCAN_GAS_REGS_PER_FRAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
pcan_id_t id;
|
||||||
|
pcan_id_unpack(base_id, &id);
|
||||||
|
id.msg_type = PCAN_MSG_GAS;
|
||||||
|
id.msg_body = addr;
|
||||||
|
|
||||||
|
frame->id = pcan_id_pack(&id);
|
||||||
|
frame->flags = PCAN_FLAG_IDE;
|
||||||
|
frame->seq = 0U;
|
||||||
|
frame->dlc = (uint8_t)(count * 2U);
|
||||||
|
for (uint16_t i = 0U; i < count; i++) {
|
||||||
|
frame->data[i * 2U] = (uint8_t)(regs[i] & 0xFFU);
|
||||||
|
frame->data[(i * 2U) + 1U] = (uint8_t)(regs[i] >> 8);
|
||||||
|
}
|
||||||
|
for (uint8_t i = frame->dlc; i < PCAN_DATA_MAX; i++) {
|
||||||
|
frame->data[i] = 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req,
|
||||||
|
pcan_frame_t *rsp)
|
||||||
|
{
|
||||||
|
pcan_id_t id;
|
||||||
|
pcan_id_unpack(req->id, &id);
|
||||||
|
if (id.msg_type != (uint8_t)PCAN_MSG_GAS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req->dlc == 0U) {
|
||||||
|
uint16_t regs[PCAN_GAS_REGS_PER_FRAME];
|
||||||
|
uint16_t n = pcan_gas_read_block(map, id.msg_body, regs,
|
||||||
|
PCAN_GAS_REGS_PER_FRAME);
|
||||||
|
if (n == 0U) {
|
||||||
|
return false; /* адреса нет в карте - отвечать нечем */
|
||||||
|
}
|
||||||
|
pcan_gas_regs_to_frame(rsp, req->id, id.msg_body, regs, n);
|
||||||
|
pcan_id_unpack(rsp->id, &id);
|
||||||
|
id.route = PCAN_ROUTE_FROM_DEVICE;
|
||||||
|
rsp->id = pcan_id_pack(&id);
|
||||||
|
rsp->flags = (uint8_t)(req->flags & PCAN_FLAG_IDE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t regs[PCAN_GAS_REGS_PER_FRAME];
|
||||||
|
uint16_t n = pcan_gas_frame_to_regs(req, regs, PCAN_GAS_REGS_PER_FRAME);
|
||||||
|
(void)pcan_gas_write_block(map, id.msg_body, regs, n);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
22
c/protocan-transport/src/pcan_id.c
Normal file
22
c/protocan-transport/src/pcan_id.c
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#include "pcan_id.h"
|
||||||
|
|
||||||
|
uint32_t pcan_id_pack(const pcan_id_t *id)
|
||||||
|
{
|
||||||
|
return ((uint32_t)(id->priority & 0x1U) << 28)
|
||||||
|
| ((uint32_t)(id->route & 0x1U) << 27)
|
||||||
|
| ((uint32_t)(id->device_type & 0x7U) << 24)
|
||||||
|
| ((uint32_t)(id->device_id & 0xFU) << 20)
|
||||||
|
| ((uint32_t)(id->msg_type & 0xFU) << 16)
|
||||||
|
| ((uint32_t)id->msg_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pcan_id_unpack(uint32_t raw, pcan_id_t *id)
|
||||||
|
{
|
||||||
|
raw &= PCAN_ID_MASK;
|
||||||
|
id->msg_body = (uint16_t)(raw & 0xFFFFU);
|
||||||
|
id->msg_type = (uint8_t)((raw >> 16) & 0xFU);
|
||||||
|
id->device_id = (uint8_t)((raw >> 20) & 0xFU);
|
||||||
|
id->device_type = (uint8_t)((raw >> 24) & 0x7U);
|
||||||
|
id->route = (uint8_t)((raw >> 27) & 0x1U);
|
||||||
|
id->priority = (uint8_t)((raw >> 28) & 0x1U);
|
||||||
|
}
|
||||||
59
c/protocan-transport/src/pcan_link.c
Normal file
59
c/protocan-transport/src/pcan_link.c
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#include "pcan_link.h"
|
||||||
|
|
||||||
|
bool pcan_link_init(pcan_link_t *link, const pcan_io_t *io,
|
||||||
|
pcan_frame_cb_t on_frame, void *user)
|
||||||
|
{
|
||||||
|
if ((link == NULL) || (io == NULL) || (io->write == NULL)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
link->io = *io;
|
||||||
|
link->on_frame = on_frame;
|
||||||
|
link->user = user;
|
||||||
|
link->tx_seq = 0U;
|
||||||
|
link->tx.tx_frames = 0U;
|
||||||
|
link->tx.tx_dropped = 0U;
|
||||||
|
pcan_parser_init(&link->parser);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcan_link_can_send(const pcan_link_t *link)
|
||||||
|
{
|
||||||
|
if (link->io.tx_space == NULL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return link->io.tx_space(link->io.ctx) >= PCAN_FRAME_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcan_link_send_raw(pcan_link_t *link, const pcan_frame_t *frame)
|
||||||
|
{
|
||||||
|
uint8_t raw[PCAN_FRAME_MAX];
|
||||||
|
|
||||||
|
size_t n = pcan_frame_encode(frame, raw, sizeof raw);
|
||||||
|
if (n == 0U) {
|
||||||
|
link->tx.tx_dropped++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (link->io.write(link->io.ctx, raw, n) != n) {
|
||||||
|
link->tx.tx_dropped++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
link->tx.tx_frames++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcan_link_send(pcan_link_t *link, pcan_frame_t *frame)
|
||||||
|
{
|
||||||
|
frame->seq = link->tx_seq;
|
||||||
|
if (!pcan_link_send_raw(link, frame)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* SEQ двигаем только на успешно отданном кадре: иначе приёмник
|
||||||
|
засчитает потерю там, где кадра просто не было. */
|
||||||
|
link->tx_seq++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pcan_link_feed(pcan_link_t *link, const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
return pcan_parser_feed(&link->parser, data, len, link->on_frame, link->user);
|
||||||
|
}
|
||||||
88
c/protocan-transport/src/pcan_ring.c
Normal file
88
c/protocan-transport/src/pcan_ring.c
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#include "pcan_ring.h"
|
||||||
|
|
||||||
|
bool pcan_ring_init(pcan_ring_t *rb, uint8_t *storage, uint16_t size)
|
||||||
|
{
|
||||||
|
if ((rb == NULL) || (storage == NULL) || (size < 2U) ||
|
||||||
|
((size & (uint16_t)(size - 1U)) != 0U)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rb->buf = storage;
|
||||||
|
rb->size = size;
|
||||||
|
rb->head = 0U;
|
||||||
|
rb->tail = 0U;
|
||||||
|
rb->dropped = 0U;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pcan_ring_count(const pcan_ring_t *rb)
|
||||||
|
{
|
||||||
|
return (uint16_t)((rb->head - rb->tail) & (uint16_t)(rb->size - 1U));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pcan_ring_free(const pcan_ring_t *rb)
|
||||||
|
{
|
||||||
|
return (uint16_t)(rb->size - 1U - pcan_ring_count(rb));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcan_ring_put(pcan_ring_t *rb, uint8_t byte)
|
||||||
|
{
|
||||||
|
uint16_t next = (uint16_t)((rb->head + 1U) & (uint16_t)(rb->size - 1U));
|
||||||
|
if (next == rb->tail) {
|
||||||
|
rb->dropped++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rb->buf[rb->head] = byte;
|
||||||
|
PCAN_BARRIER();
|
||||||
|
rb->head = next;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcan_ring_get(pcan_ring_t *rb, uint8_t *byte)
|
||||||
|
{
|
||||||
|
if (rb->head == rb->tail) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*byte = rb->buf[rb->tail];
|
||||||
|
PCAN_BARRIER();
|
||||||
|
rb->tail = (uint16_t)((rb->tail + 1U) & (uint16_t)(rb->size - 1U));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcan_ring_write(pcan_ring_t *rb, const uint8_t *data, uint16_t len)
|
||||||
|
{
|
||||||
|
if (pcan_ring_free(rb) < len) {
|
||||||
|
rb->dropped += len;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t mask = (uint16_t)(rb->size - 1U);
|
||||||
|
uint16_t head = rb->head;
|
||||||
|
for (uint16_t i = 0U; i < len; i++) {
|
||||||
|
rb->buf[head] = data[i];
|
||||||
|
head = (uint16_t)((head + 1U) & mask);
|
||||||
|
}
|
||||||
|
/* Публикуем весь блок одной записью: читатель никогда не увидит
|
||||||
|
кадр наполовину. */
|
||||||
|
PCAN_BARRIER();
|
||||||
|
rb->head = head;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pcan_ring_linear(const pcan_ring_t *rb, const uint8_t **ptr)
|
||||||
|
{
|
||||||
|
uint16_t head = rb->head;
|
||||||
|
uint16_t tail = rb->tail;
|
||||||
|
|
||||||
|
if (head == tail) {
|
||||||
|
*ptr = NULL;
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
*ptr = &rb->buf[tail];
|
||||||
|
return (head > tail) ? (uint16_t)(head - tail)
|
||||||
|
: (uint16_t)(rb->size - tail);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pcan_ring_consume(pcan_ring_t *rb, uint16_t len)
|
||||||
|
{
|
||||||
|
rb->tail = (uint16_t)((rb->tail + len) & (uint16_t)(rb->size - 1U));
|
||||||
|
}
|
||||||
374
c/protocan-transport/tests/test_gui.c
Normal file
374
c/protocan-transport/tests/test_gui.c
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
/**
|
||||||
|
* Тесты GUI-протокола и каталога общего адресного пространства.
|
||||||
|
* Эталонные байты совпадают с SETGUI/tests/test_gas_catalog.py.
|
||||||
|
*
|
||||||
|
* clang -std=c99 -Wall -Wextra -Iinclude tests/test_gui.c src/pcan_*.c src/gui_*.c -o test_gui
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "gui_catalog.h"
|
||||||
|
#include "gui_frame.h"
|
||||||
|
#include "pcan_gas.h"
|
||||||
|
|
||||||
|
static int g_fail;
|
||||||
|
|
||||||
|
#define CHECK(cond) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
|
||||||
|
g_fail++; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static void dump(const char *tag, const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
printf("%s:", tag);
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
printf(" %02X", data[i]);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Транспорт ------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void test_crc32(void)
|
||||||
|
{
|
||||||
|
/* Контрольное значение CRC32/ISO-HDLC, оно же zlib.crc32. */
|
||||||
|
CHECK(gui_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL);
|
||||||
|
CHECK(gui_crc32((const uint8_t *)"", 0) == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_ping_reference(void)
|
||||||
|
{
|
||||||
|
/* Эталон из PROJECT_RELATIONS.md:
|
||||||
|
A5 5A 01 01 12 34 00 00 EE 89 8C 9E */
|
||||||
|
static const uint8_t want[] = {
|
||||||
|
0xA5, 0x5A, 0x01, 0x01, 0x12, 0x34, 0x00, 0x00, 0xEE, 0x89, 0x8C, 0x9E
|
||||||
|
};
|
||||||
|
uint8_t out[GUI_FRAME_MAX];
|
||||||
|
size_t n = gui_frame_encode(GUI_MSG_PING, 0x1234, NULL, 0, out, sizeof out);
|
||||||
|
if ((n != sizeof want) || (memcmp(out, want, sizeof want) != 0)) {
|
||||||
|
dump("получено", out, n);
|
||||||
|
dump("ожидалось", want, sizeof want);
|
||||||
|
}
|
||||||
|
CHECK(n == sizeof want);
|
||||||
|
CHECK(memcmp(out, want, sizeof want) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int s_rx_count;
|
||||||
|
static uint8_t s_rx_type;
|
||||||
|
static uint16_t s_rx_seq;
|
||||||
|
static uint16_t s_rx_size;
|
||||||
|
static uint8_t s_rx_payload[64];
|
||||||
|
|
||||||
|
static void on_frame(const gui_frame_t *frame, void *user)
|
||||||
|
{
|
||||||
|
(void)user;
|
||||||
|
s_rx_count++;
|
||||||
|
s_rx_type = frame->type;
|
||||||
|
s_rx_seq = frame->sequence;
|
||||||
|
s_rx_size = frame->size;
|
||||||
|
if (frame->size <= sizeof s_rx_payload) {
|
||||||
|
memcpy(s_rx_payload, frame->payload, frame->size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parser(void)
|
||||||
|
{
|
||||||
|
uint8_t payload[5] = { 1, 2, 3, 4, 5 };
|
||||||
|
uint8_t out[GUI_FRAME_MAX];
|
||||||
|
size_t n = gui_frame_encode(GUI_MSG_GAS_WATCH_DATA, 0, payload, 5, out, sizeof out);
|
||||||
|
|
||||||
|
gui_parser_t p;
|
||||||
|
gui_parser_init(&p);
|
||||||
|
s_rx_count = 0;
|
||||||
|
/* Мусор и оборванная сигнатура перед настоящим кадром. */
|
||||||
|
static const uint8_t junk[] = { 0x00, 0xFF, 0xA5, 0xA5, 0x5A, 0x02 };
|
||||||
|
gui_parser_feed(&p, junk, sizeof junk, on_frame, NULL);
|
||||||
|
gui_parser_feed(&p, out, n, on_frame, NULL);
|
||||||
|
|
||||||
|
CHECK(s_rx_count == 1);
|
||||||
|
CHECK(s_rx_type == GUI_MSG_GAS_WATCH_DATA);
|
||||||
|
CHECK(s_rx_size == 5U);
|
||||||
|
CHECK(memcmp(s_rx_payload, payload, 5) == 0);
|
||||||
|
CHECK(p.stats.version_errors == 1U); /* 0x02 вместо версии 0x01 */
|
||||||
|
CHECK(p.stats.frames == 1U);
|
||||||
|
|
||||||
|
/* Битый CRC отвергается. */
|
||||||
|
out[n - 1U] ^= 0xFFU;
|
||||||
|
gui_parser_init(&p);
|
||||||
|
s_rx_count = 0;
|
||||||
|
gui_parser_feed(&p, out, n, on_frame, NULL);
|
||||||
|
CHECK(s_rx_count == 0);
|
||||||
|
CHECK(p.stats.crc_errors == 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_max_payload(void)
|
||||||
|
{
|
||||||
|
static uint8_t payload[GUI_MAX_PAYLOAD];
|
||||||
|
for (size_t i = 0; i < sizeof payload; i++) {
|
||||||
|
payload[i] = (uint8_t)i;
|
||||||
|
}
|
||||||
|
static uint8_t out[GUI_FRAME_MAX];
|
||||||
|
size_t n = gui_frame_encode(GUI_MSG_WRITE_REGISTERS, 0xFFFF, payload,
|
||||||
|
GUI_MAX_PAYLOAD, out, sizeof out);
|
||||||
|
CHECK(n == GUI_FRAME_MAX);
|
||||||
|
CHECK(out[6] == 0x02U && out[7] == 0x00U); /* size = 512 big-endian */
|
||||||
|
|
||||||
|
/* Больше максимума не кодируем и буфер не переполняем. */
|
||||||
|
CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, GUI_MAX_PAYLOAD + 1U,
|
||||||
|
out, sizeof out) == 0U);
|
||||||
|
CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, 100U, out, 50U) == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Каталог --------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static uint16_t s_settings[4];
|
||||||
|
static uint16_t s_diag[4] = { 0x0100, 0x003C, 0x0000, 0x0002 };
|
||||||
|
|
||||||
|
static const pcan_gas_region_t s_regions[] = {
|
||||||
|
{ 0x0000U, 4U, s_settings, NULL, NULL, 0U, NULL, "settings" },
|
||||||
|
{ 0xFF00U, 4U, s_diag, NULL, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
|
||||||
|
};
|
||||||
|
static const pcan_gas_map_t s_map = { s_regions, 2U };
|
||||||
|
|
||||||
|
static const gui_object_t s_objects[] = {
|
||||||
|
{ 0xFF01U, GUI_OBJ_U16, GUI_OBJ_READABLE | GUI_OBJ_DEFAULT_WATCH,
|
||||||
|
0, GUI_UNIT_SECOND, "Uptime" },
|
||||||
|
{ 0x0000U, GUI_OBJ_I16, GUI_OBJ_READABLE | GUI_OBJ_WRITABLE,
|
||||||
|
-2, GUI_UNIT_VOLT, "Uставка" },
|
||||||
|
{ 0xFF02U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, GUI_UNIT_COUNT, "Кадров" },
|
||||||
|
};
|
||||||
|
static const gui_catalog_t s_catalog = { s_objects, 3U };
|
||||||
|
|
||||||
|
static void test_catalog_validate(void)
|
||||||
|
{
|
||||||
|
CHECK(gui_catalog_validate(&s_catalog, &s_map));
|
||||||
|
|
||||||
|
/* Адрес вне карты GAS. */
|
||||||
|
static const gui_object_t bad_addr[] = {
|
||||||
|
{ 0x8000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "нет" },
|
||||||
|
};
|
||||||
|
const gui_catalog_t c1 = { bad_addr, 1U };
|
||||||
|
CHECK(!gui_catalog_validate(&c1, &s_map));
|
||||||
|
|
||||||
|
/* U32 требует двух подряд идущих адресов; 0xFF03 есть, 0xFF04 - нет. */
|
||||||
|
static const gui_object_t split[] = {
|
||||||
|
{ 0xFF03U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, 0, "хвост" },
|
||||||
|
};
|
||||||
|
const gui_catalog_t c2 = { split, 1U };
|
||||||
|
CHECK(!gui_catalog_validate(&c2, &s_map));
|
||||||
|
|
||||||
|
/* Имя длиннее поля: 13 кириллических символов - это 26 байт. */
|
||||||
|
static const gui_object_t long_name[] = {
|
||||||
|
{ 0x0000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "тринадцать сим" },
|
||||||
|
};
|
||||||
|
const gui_catalog_t c3 = { long_name, 1U };
|
||||||
|
CHECK(!gui_catalog_validate(&c3, &s_map));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_catalog_encode(void)
|
||||||
|
{
|
||||||
|
uint8_t out[GUI_MAX_PAYLOAD];
|
||||||
|
size_t n = gui_catalog_encode(&s_catalog, 0U, 1U, out, sizeof out);
|
||||||
|
|
||||||
|
CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE);
|
||||||
|
CHECK(gui_get_u16(&out[0]) == 3U); /* total */
|
||||||
|
CHECK(gui_get_u16(&out[2]) == 0U); /* start_index */
|
||||||
|
CHECK(gui_get_u16(&out[4]) == 1U); /* count */
|
||||||
|
|
||||||
|
/* Запись Uptime совпадает с эталоном из Python-тестов. */
|
||||||
|
static const uint8_t want[] = {
|
||||||
|
0x01, 0xFF, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00,
|
||||||
|
'U', 'p', 't', 'i', 'm', 'e', 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
};
|
||||||
|
if (memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) != 0) {
|
||||||
|
dump("запись", &out[GUI_CATALOG_HEADER], GUI_ENTRY_SIZE);
|
||||||
|
}
|
||||||
|
CHECK(sizeof want == GUI_ENTRY_SIZE);
|
||||||
|
CHECK(memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) == 0);
|
||||||
|
|
||||||
|
/* Отрицательный масштаб кодируется как int8. */
|
||||||
|
n = gui_catalog_encode(&s_catalog, 1U, 1U, out, sizeof out);
|
||||||
|
CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE);
|
||||||
|
CHECK(out[GUI_CATALOG_HEADER + 4U] == 0xFEU); /* -2 */
|
||||||
|
|
||||||
|
/* max_count = 0 отдаёт всё, что влезает. */
|
||||||
|
n = gui_catalog_encode(&s_catalog, 0U, 0U, out, sizeof out);
|
||||||
|
CHECK(n == GUI_CATALOG_HEADER + (3U * GUI_ENTRY_SIZE));
|
||||||
|
CHECK(gui_get_u16(&out[4]) == 3U);
|
||||||
|
|
||||||
|
/* Индекс за концом каталога - пустой ответ, а не мусор. */
|
||||||
|
CHECK(gui_catalog_encode(&s_catalog, 3U, 0U, out, sizeof out) == 0U);
|
||||||
|
/* Маленький буфер отвергается. */
|
||||||
|
CHECK(gui_catalog_encode(&s_catalog, 0U, 1U, out, 8U) == 0U);
|
||||||
|
|
||||||
|
CHECK(GUI_ENTRIES_PER_FRAME == 15U);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Подписка -------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void test_watch(void)
|
||||||
|
{
|
||||||
|
gui_watch_t watch;
|
||||||
|
gui_watch_init(&watch);
|
||||||
|
|
||||||
|
/* period = 100, два адреса: один есть в карте, второй нет. */
|
||||||
|
static const uint8_t request[] = {
|
||||||
|
0x64, 0x00, 0x02, 0x00, 0x01, 0xFF, 0x00, 0x80
|
||||||
|
};
|
||||||
|
uint16_t accepted = gui_watch_apply(&watch, &s_map, request, sizeof request);
|
||||||
|
CHECK(accepted == 1U);
|
||||||
|
CHECK(watch.count == 1U);
|
||||||
|
CHECK(watch.address[0] == 0xFF01U);
|
||||||
|
CHECK(watch.period_ms == 100U);
|
||||||
|
|
||||||
|
uint8_t ack[4];
|
||||||
|
CHECK(gui_watch_encode_ack(&watch, ack, sizeof ack) == 4U);
|
||||||
|
CHECK(gui_get_u16(&ack[0]) == 100U);
|
||||||
|
CHECK(gui_get_u16(&ack[2]) == 1U);
|
||||||
|
|
||||||
|
/* Значение берётся из карты GAS. */
|
||||||
|
uint8_t data[GUI_MAX_PAYLOAD];
|
||||||
|
size_t n = gui_watch_encode_data(&watch, &s_map, 0x12345678UL, data, sizeof data);
|
||||||
|
CHECK(n == 8U);
|
||||||
|
CHECK(data[0] == 0x78U && data[3] == 0x12U); /* время little-endian */
|
||||||
|
CHECK(gui_get_u16(&data[4]) == 1U);
|
||||||
|
CHECK(gui_get_u16(&data[6]) == 0x003CU); /* s_diag[1] = uptime */
|
||||||
|
|
||||||
|
/* Битый payload не должен ломать действующую подписку. */
|
||||||
|
static const uint8_t broken[] = { 0x64, 0x00, 0x05, 0x00, 0x01, 0xFF };
|
||||||
|
CHECK(gui_watch_apply(&watch, &s_map, broken, sizeof broken) == 0xFFFFU);
|
||||||
|
CHECK(watch.count == 1U);
|
||||||
|
CHECK(watch.period_ms == 100U);
|
||||||
|
|
||||||
|
/* Слишком длинная подписка отвергается целиком. */
|
||||||
|
static uint8_t huge[4U + ((GUI_WATCH_MAX + 1U) * 2U)];
|
||||||
|
gui_put_u16(&huge[0], 10U);
|
||||||
|
gui_put_u16(&huge[2], GUI_WATCH_MAX + 1U);
|
||||||
|
CHECK(gui_watch_apply(&watch, &s_map, huge, sizeof huge) == 0xFFFFU);
|
||||||
|
CHECK(watch.count == 1U);
|
||||||
|
|
||||||
|
/* period = 0 останавливает поток. */
|
||||||
|
static const uint8_t stop[] = { 0x00, 0x00, 0x00, 0x00 };
|
||||||
|
CHECK(gui_watch_apply(&watch, &s_map, stop, sizeof stop) == 0U);
|
||||||
|
CHECK(watch.period_ms == 0U);
|
||||||
|
CHECK(!gui_watch_due(&watch, 1000U));
|
||||||
|
CHECK(gui_watch_encode_data(&watch, &s_map, 0U, data, sizeof data) == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_watch_timing(void)
|
||||||
|
{
|
||||||
|
gui_watch_t watch;
|
||||||
|
gui_watch_init(&watch);
|
||||||
|
static const uint8_t request[] = { 0x0A, 0x00, 0x01, 0x00, 0x01, 0xFF };
|
||||||
|
CHECK(gui_watch_apply(&watch, &s_map, request, sizeof request) == 1U);
|
||||||
|
|
||||||
|
watch.next_ms = 1000U;
|
||||||
|
CHECK(!gui_watch_due(&watch, 999U));
|
||||||
|
CHECK(gui_watch_due(&watch, 1000U));
|
||||||
|
CHECK(gui_watch_due(&watch, 1500U));
|
||||||
|
|
||||||
|
/* Идём в такт, пока успеваем. */
|
||||||
|
gui_watch_advance(&watch, 1000U);
|
||||||
|
CHECK(watch.next_ms == 1010U);
|
||||||
|
|
||||||
|
/* Отстали сильно - не нагоняем пачкой, а идём от текущего момента. */
|
||||||
|
watch.next_ms = 1000U;
|
||||||
|
gui_watch_advance(&watch, 5000U);
|
||||||
|
CHECK(watch.next_ms == 5010U);
|
||||||
|
|
||||||
|
/* Переполнение счётчика миллисекунд не должно вешать поток. */
|
||||||
|
watch.next_ms = 0xFFFFFFF0UL;
|
||||||
|
CHECK(!gui_watch_due(&watch, 0xFFFFFFE0UL));
|
||||||
|
CHECK(gui_watch_due(&watch, 0x00000010UL));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Канал ----------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static uint8_t s_line[1024];
|
||||||
|
static size_t s_line_len;
|
||||||
|
static size_t s_line_space = sizeof s_line;
|
||||||
|
|
||||||
|
static size_t fake_write(void *ctx, const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
if ((s_line_len + len) > s_line_space) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
memcpy(&s_line[s_line_len], data, len);
|
||||||
|
s_line_len += len;
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t fake_space(void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return s_line_space - s_line_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_link(void)
|
||||||
|
{
|
||||||
|
pcan_io_t io = { fake_write, fake_space, NULL };
|
||||||
|
static gui_link_t link;
|
||||||
|
CHECK(gui_link_init(&link, &io, on_frame, NULL));
|
||||||
|
|
||||||
|
s_line_len = 0U;
|
||||||
|
s_rx_count = 0;
|
||||||
|
|
||||||
|
uint8_t payload[3] = { 0xDE, 0xAD, 0xBE };
|
||||||
|
CHECK(gui_link_send(&link, GUI_MSG_GAS_CATALOG, 7U, payload, 3U));
|
||||||
|
CHECK(link.tx_frames == 1U);
|
||||||
|
|
||||||
|
/* Поток обратно кусками произвольной длины. */
|
||||||
|
size_t off = 0U;
|
||||||
|
while (off < s_line_len) {
|
||||||
|
size_t chunk = ((s_line_len - off) > 4U) ? 4U : (s_line_len - off);
|
||||||
|
gui_link_feed(&link, &s_line[off], chunk);
|
||||||
|
off += chunk;
|
||||||
|
}
|
||||||
|
CHECK(s_rx_count == 1);
|
||||||
|
CHECK(s_rx_type == GUI_MSG_GAS_CATALOG);
|
||||||
|
CHECK(s_rx_seq == 7U);
|
||||||
|
CHECK(s_rx_size == 3U);
|
||||||
|
|
||||||
|
/* NACK - два байта кода результата. */
|
||||||
|
s_line_len = 0U;
|
||||||
|
s_rx_count = 0;
|
||||||
|
CHECK(gui_link_nack(&link, 9U, GUI_RESULT_NOT_FOUND));
|
||||||
|
gui_link_feed(&link, s_line, s_line_len);
|
||||||
|
CHECK(s_rx_count == 1);
|
||||||
|
CHECK(s_rx_type == GUI_MSG_NACK);
|
||||||
|
CHECK(gui_get_u16(s_rx_payload) == GUI_RESULT_NOT_FOUND);
|
||||||
|
|
||||||
|
/* Не хватило места - кадр не уходит обрывком. */
|
||||||
|
s_line_space = 8U;
|
||||||
|
s_line_len = 0U;
|
||||||
|
CHECK(!gui_link_can_send(&link, 100U));
|
||||||
|
CHECK(!gui_link_send(&link, GUI_MSG_PING, 1U, payload, 3U));
|
||||||
|
CHECK(s_line_len == 0U);
|
||||||
|
CHECK(link.tx_dropped == 1U);
|
||||||
|
s_line_space = sizeof s_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_crc32();
|
||||||
|
test_ping_reference();
|
||||||
|
test_parser();
|
||||||
|
test_max_payload();
|
||||||
|
test_catalog_validate();
|
||||||
|
test_catalog_encode();
|
||||||
|
test_watch();
|
||||||
|
test_watch_timing();
|
||||||
|
test_link();
|
||||||
|
|
||||||
|
if (g_fail == 0) {
|
||||||
|
printf("all gui tests passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
printf("%d check(s) failed\n", g_fail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user